modelmatrix 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelmatrix/__init__.py +26 -0
- modelmatrix/_version.py +24 -0
- modelmatrix/columns.py +324 -0
- modelmatrix/model_spec.py +1710 -0
- modelmatrix/transforms.py +695 -0
- modelmatrix-0.1.0.dist-info/METADATA +72 -0
- modelmatrix-0.1.0.dist-info/RECORD +10 -0
- modelmatrix-0.1.0.dist-info/WHEEL +5 -0
- modelmatrix-0.1.0.dist-info/licenses/LICENSE +27 -0
- modelmatrix-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transformers
|
|
3
|
+
============
|
|
4
|
+
|
|
5
|
+
This module defines some sklearn transformers useful for defining
|
|
6
|
+
flexible regression models for single features, as well as interactions
|
|
7
|
+
between sets of columns.
|
|
8
|
+
|
|
9
|
+
- Poly: for orthogonalized polynomial regression
|
|
10
|
+
|
|
11
|
+
- Interaction: computing pairwise product for interactions
|
|
12
|
+
|
|
13
|
+
- BSpline: arbitrary degree B-splines
|
|
14
|
+
|
|
15
|
+
- NaturalSpline: natural cubic splines
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from itertools import product
|
|
19
|
+
import numpy as np, pandas as pd
|
|
20
|
+
|
|
21
|
+
from sklearn.base import TransformerMixin, BaseEstimator
|
|
22
|
+
from sklearn.utils.validation import check_is_fitted
|
|
23
|
+
from scipy.interpolate import splev
|
|
24
|
+
from scipy import sparse as sp_sparse
|
|
25
|
+
|
|
26
|
+
def _single_column(X):
|
|
27
|
+
"""
|
|
28
|
+
A single feature as a 1-d array of floats (a copy), from a 1-d
|
|
29
|
+
array, a column (e.g. a `pd.Series` or a 1-column `pd.DataFrame`) or
|
|
30
|
+
a single value. A single row stays one value (np.squeeze made it 0-d).
|
|
31
|
+
"""
|
|
32
|
+
X = np.array(X, dtype=float)
|
|
33
|
+
if X.ndim == 2 and X.shape[1] == 1:
|
|
34
|
+
X = X[:, 0]
|
|
35
|
+
elif X.ndim == 0:
|
|
36
|
+
X = X.reshape(1)
|
|
37
|
+
if X.ndim != 1:
|
|
38
|
+
raise ValueError('expecting a single column feature')
|
|
39
|
+
return X
|
|
40
|
+
|
|
41
|
+
class Poly(TransformerMixin, BaseEstimator):
|
|
42
|
+
|
|
43
|
+
'''
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
|
|
48
|
+
degree : int, default=1
|
|
49
|
+
Degree of polynomial.
|
|
50
|
+
|
|
51
|
+
intercept : bool, default=False
|
|
52
|
+
Include a column for intercept?
|
|
53
|
+
|
|
54
|
+
raw : bool, default=False
|
|
55
|
+
If False, perform a QR decomposition on the resulting
|
|
56
|
+
matrix of powers of centered and / or scaled features.
|
|
57
|
+
'''
|
|
58
|
+
|
|
59
|
+
def __init__(self,
|
|
60
|
+
degree=1,
|
|
61
|
+
intercept=False,
|
|
62
|
+
raw=False):
|
|
63
|
+
|
|
64
|
+
self.degree = degree
|
|
65
|
+
self.raw = raw
|
|
66
|
+
self.intercept = intercept
|
|
67
|
+
|
|
68
|
+
def fit(self,
|
|
69
|
+
X,
|
|
70
|
+
y=None):
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
Construct parameters for orthogonal
|
|
74
|
+
polynomials in the feature X.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
X : array-like of shape (n_samples,)
|
|
79
|
+
Features used in fitting `svm`. Assumed to have at least 2 columns.
|
|
80
|
+
|
|
81
|
+
y : default=None
|
|
82
|
+
Ignored. This parameter exists only for compatibility with
|
|
83
|
+
:class:`~sklearn.pipeline.Pipeline`.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
X_orig = X
|
|
88
|
+
X = _single_column(X)
|
|
89
|
+
n = X.shape[0]
|
|
90
|
+
|
|
91
|
+
self.mean_ = X.mean()
|
|
92
|
+
|
|
93
|
+
if not self.raw:
|
|
94
|
+
powX = np.power.outer(X - self.mean_, np.arange(0, self.degree+1))
|
|
95
|
+
# Following R's poly construction
|
|
96
|
+
Q, R = np.linalg.qr(powX)
|
|
97
|
+
Z = Q * np.diag(R)[None,:]
|
|
98
|
+
self.norm2_ = (Z**2).sum(0)
|
|
99
|
+
self.alpha_ = ((X[:,None] * Z**2).sum(0) / self.norm2_)[:self.degree]
|
|
100
|
+
self.norm2_ = np.hstack([1, self.norm2_])
|
|
101
|
+
|
|
102
|
+
# for pandas
|
|
103
|
+
|
|
104
|
+
self.columns_ = range(self.degree+1)
|
|
105
|
+
if not self.intercept:
|
|
106
|
+
self.columns_ = self.columns_[:-1]
|
|
107
|
+
|
|
108
|
+
if isinstance(X_orig, (pd.Series, pd.DataFrame)):
|
|
109
|
+
if isinstance(X_orig, pd.Series):
|
|
110
|
+
name = X_orig.name
|
|
111
|
+
else:
|
|
112
|
+
name = X_orig.columns[0]
|
|
113
|
+
self.columns_ = ['{0}[{1}]'.format(self, d)
|
|
114
|
+
for d in self.columns_]
|
|
115
|
+
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def transform(self, X):
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
Construct parameters for orthogonal
|
|
122
|
+
polynomials in the feature X.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
X : array-like
|
|
127
|
+
X on which features will be evaluated.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
XP : np.ndarray
|
|
132
|
+
Evaluated polynomial features.
|
|
133
|
+
"""
|
|
134
|
+
check_is_fitted(self)
|
|
135
|
+
|
|
136
|
+
X_orig = X
|
|
137
|
+
|
|
138
|
+
X = _single_column(X)
|
|
139
|
+
n = X.shape[0]
|
|
140
|
+
|
|
141
|
+
if not self.raw:
|
|
142
|
+
Z = np.ones((n, self.degree+1))
|
|
143
|
+
Z[:,1] = X - self.alpha_[0]
|
|
144
|
+
|
|
145
|
+
if self.degree > 1:
|
|
146
|
+
for i in range(1, self.degree):
|
|
147
|
+
Z[:,i+1] = ((X - self.alpha_[i]) * Z[:,i] -
|
|
148
|
+
self.norm2_[i+1] / self.norm2_[i] * Z[:,i-1])
|
|
149
|
+
Z /= np.sqrt(self.norm2_[1:])
|
|
150
|
+
powX = Z
|
|
151
|
+
else:
|
|
152
|
+
powX = np.power.outer(X, np.arange(0, self.degree+1))
|
|
153
|
+
|
|
154
|
+
if not self.intercept:
|
|
155
|
+
powX = powX[:,1:]
|
|
156
|
+
|
|
157
|
+
if isinstance(X_orig, (pd.Series, pd.DataFrame)):
|
|
158
|
+
df = pd.DataFrame(powX,
|
|
159
|
+
columns=self.columns_,
|
|
160
|
+
copy=False)
|
|
161
|
+
df.index = X_orig.index
|
|
162
|
+
return df
|
|
163
|
+
else:
|
|
164
|
+
return powX
|
|
165
|
+
|
|
166
|
+
#### Interaction transform
|
|
167
|
+
|
|
168
|
+
class Interaction(TransformerMixin, BaseEstimator):
|
|
169
|
+
|
|
170
|
+
'''
|
|
171
|
+
|
|
172
|
+
Form the tensor product interaction
|
|
173
|
+
from a group of columns.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
|
|
178
|
+
variables : sequence
|
|
179
|
+
Variables in the interactions.
|
|
180
|
+
|
|
181
|
+
columns : dict
|
|
182
|
+
Mapping from variable names to columns.
|
|
183
|
+
|
|
184
|
+
column_names : dict
|
|
185
|
+
Mapping from variable names to lists of column names.
|
|
186
|
+
|
|
187
|
+
'''
|
|
188
|
+
|
|
189
|
+
def __init__(self,
|
|
190
|
+
variables,
|
|
191
|
+
columns,
|
|
192
|
+
column_names):
|
|
193
|
+
|
|
194
|
+
self.variables = variables
|
|
195
|
+
self.columns = columns
|
|
196
|
+
self.column_names = column_names
|
|
197
|
+
self.column_names_ = {}
|
|
198
|
+
self.columns_ = []
|
|
199
|
+
|
|
200
|
+
variable_names = []
|
|
201
|
+
for variable in self.variables:
|
|
202
|
+
cols = self.columns[variable]
|
|
203
|
+
col_names = ['{0}[{1}]'.format(variable, i) for i in range(len(self.columns[variable]))]
|
|
204
|
+
if variable in column_names:
|
|
205
|
+
col_names = [str(c) for c in column_names[variable]]
|
|
206
|
+
if len(cols) > 1:
|
|
207
|
+
variable_names.append(col_names)
|
|
208
|
+
else:
|
|
209
|
+
variable_names.append(['{0}'.format(variable)])
|
|
210
|
+
|
|
211
|
+
for names in product(*variable_names):
|
|
212
|
+
self.columns_.append(':'.join(names))
|
|
213
|
+
|
|
214
|
+
def fit(self,
|
|
215
|
+
X,
|
|
216
|
+
y=None):
|
|
217
|
+
|
|
218
|
+
"""
|
|
219
|
+
Nothing to be computed for the fit.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
X : array-like
|
|
224
|
+
|
|
225
|
+
y : default=None
|
|
226
|
+
Ignored. This parameter exists only for compatibility with
|
|
227
|
+
:class:`~sklearn.pipeline.Pipeline`.
|
|
228
|
+
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
return self
|
|
232
|
+
|
|
233
|
+
def transform(self, X):
|
|
234
|
+
|
|
235
|
+
"""
|
|
236
|
+
Construct columns representing interactions
|
|
237
|
+
of relevant variables.
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
X : array-like
|
|
242
|
+
X on which features will be evaluated.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
XI : np.ndarray
|
|
247
|
+
Evaluated interaction features.
|
|
248
|
+
"""
|
|
249
|
+
check_is_fitted(self)
|
|
250
|
+
|
|
251
|
+
if sp_sparse.issparse(X): # products of sparse columns stay sparse
|
|
252
|
+
X = sp_sparse.csc_matrix(X)
|
|
253
|
+
R = None
|
|
254
|
+
for variable in self.variables:
|
|
255
|
+
V = X[:, list(self.columns[variable])]
|
|
256
|
+
if R is None:
|
|
257
|
+
R = V
|
|
258
|
+
continue
|
|
259
|
+
# column i*q + j of the product is R[:,i] * V[:,j], as for
|
|
260
|
+
# the dense columns below: scale the rows of R by each column
|
|
261
|
+
# of V, then reorder
|
|
262
|
+
r, q = R.shape[1], V.shape[1]
|
|
263
|
+
M = sp_sparse.hstack([sp_sparse.diags(V[:, [j]].toarray().ravel()) @ R
|
|
264
|
+
for j in range(q)], format='csc')
|
|
265
|
+
R = M[:, (np.arange(q)[None, :] * r + np.arange(r)[:, None]).ravel()]
|
|
266
|
+
return sp_sparse.csr_matrix(R)
|
|
267
|
+
|
|
268
|
+
X_orig = X
|
|
269
|
+
X = np.asarray(X)
|
|
270
|
+
|
|
271
|
+
# each column of the product is filled in place
|
|
272
|
+
idx = list(product(*[self.columns[v] for v in self.variables]))
|
|
273
|
+
value = np.empty((X.shape[0], len(idx)))
|
|
274
|
+
for k, cols in enumerate(idx):
|
|
275
|
+
value[:, k] = X[:, cols[0]]
|
|
276
|
+
for j in cols[1:]:
|
|
277
|
+
value[:, k] *= X[:, j]
|
|
278
|
+
|
|
279
|
+
df = pd.DataFrame(value,
|
|
280
|
+
columns=self.columns_,
|
|
281
|
+
copy=False)
|
|
282
|
+
if isinstance(X_orig, (pd.DataFrame, pd.Series)):
|
|
283
|
+
df.index = X_orig.index
|
|
284
|
+
|
|
285
|
+
return df
|
|
286
|
+
|
|
287
|
+
#### Spline specific code
|
|
288
|
+
|
|
289
|
+
def _onehot(p, j):
|
|
290
|
+
v = np.zeros(p)
|
|
291
|
+
v[j] = 1
|
|
292
|
+
return v
|
|
293
|
+
|
|
294
|
+
def _splevf(x,
|
|
295
|
+
tk,
|
|
296
|
+
der=0,
|
|
297
|
+
ext=0):
|
|
298
|
+
"""
|
|
299
|
+
Full B-spline matrix at x
|
|
300
|
+
"""
|
|
301
|
+
x = np.asarray(x)
|
|
302
|
+
knots, degree = tk
|
|
303
|
+
nbasis = len(knots) - (degree + 1)
|
|
304
|
+
tcks = [(knots, _onehot(nbasis, j), degree) for
|
|
305
|
+
j in range(nbasis)]
|
|
306
|
+
return np.column_stack([splev(x, tck, ext=ext, der=der)
|
|
307
|
+
for tck in tcks])
|
|
308
|
+
|
|
309
|
+
def _splev_taylor(x,
|
|
310
|
+
basept,
|
|
311
|
+
tk,
|
|
312
|
+
order):
|
|
313
|
+
"""
|
|
314
|
+
Taylor expansion of B-spline at x around basept
|
|
315
|
+
"""
|
|
316
|
+
x = np.asarray(x)
|
|
317
|
+
derivs = np.array([_splevf([basept],
|
|
318
|
+
tk,
|
|
319
|
+
der=o,
|
|
320
|
+
ext=0).reshape(-1)
|
|
321
|
+
for o in range(order+1)])
|
|
322
|
+
polys = np.power.outer(x-basept,
|
|
323
|
+
np.arange(order+1))
|
|
324
|
+
fact = np.concatenate([[1], np.cumprod(np.arange(1, order+1))])
|
|
325
|
+
polys /= fact[None,:]
|
|
326
|
+
|
|
327
|
+
return polys.dot(derivs)
|
|
328
|
+
|
|
329
|
+
class BSpline(TransformerMixin, BaseEstimator):
|
|
330
|
+
|
|
331
|
+
'''
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
|
|
336
|
+
degree : int, default=3
|
|
337
|
+
Degree of polynomial.
|
|
338
|
+
|
|
339
|
+
intercept : bool, default=False
|
|
340
|
+
If False, a column of basis is dropped so that by
|
|
341
|
+
adding an intercept column design stays full rank.
|
|
342
|
+
|
|
343
|
+
lower_bound : float, default=None
|
|
344
|
+
Lower boundary not. Will be set to minimal value if not supplied.
|
|
345
|
+
|
|
346
|
+
upper_bound : float, default=None
|
|
347
|
+
Upper boundary not. Will be set to maximal value if not supplied.
|
|
348
|
+
|
|
349
|
+
internal_knots : array-like (optional)
|
|
350
|
+
Optional internal knots of B-spline. Will be set to
|
|
351
|
+
appropriate quantiles based on `df`.
|
|
352
|
+
|
|
353
|
+
df : int, default=None
|
|
354
|
+
Degrees of freedom for spline. Defaults to `degree + intercept`.
|
|
355
|
+
|
|
356
|
+
ext : int
|
|
357
|
+
How B-splines are to be extended beyond the boundary using
|
|
358
|
+
`scipy.interpolate.splev`.
|
|
359
|
+
|
|
360
|
+
'''
|
|
361
|
+
|
|
362
|
+
def __init__(self,
|
|
363
|
+
degree=3,
|
|
364
|
+
intercept=False,
|
|
365
|
+
lower_bound=None,
|
|
366
|
+
upper_bound=None,
|
|
367
|
+
internal_knots=None,
|
|
368
|
+
df=None,
|
|
369
|
+
ext=0):
|
|
370
|
+
|
|
371
|
+
self.degree = degree
|
|
372
|
+
self.lower_bound = lower_bound
|
|
373
|
+
self.upper_bound = upper_bound
|
|
374
|
+
self.internal_knots = internal_knots
|
|
375
|
+
self.df = df
|
|
376
|
+
self.ext = ext
|
|
377
|
+
self.intercept = intercept
|
|
378
|
+
|
|
379
|
+
def fit(self,
|
|
380
|
+
X,
|
|
381
|
+
y=None):
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
Compute knots for B-spline representation.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
X : array-like
|
|
389
|
+
Single feature on which B-spline will be evaluated.
|
|
390
|
+
|
|
391
|
+
y : None
|
|
392
|
+
Ignored. This parameter exists only for compatibility with
|
|
393
|
+
:class:`~sklearn.pipeline.Pipeline`.
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
X_orig = X
|
|
398
|
+
X = _single_column(X)
|
|
399
|
+
n = X.shape[0]
|
|
400
|
+
|
|
401
|
+
order = self.degree + 1
|
|
402
|
+
|
|
403
|
+
lower_bound = self.lower_bound
|
|
404
|
+
if lower_bound is None:
|
|
405
|
+
lower_bound = X.min()
|
|
406
|
+
|
|
407
|
+
upper_bound = self.upper_bound
|
|
408
|
+
if upper_bound is None:
|
|
409
|
+
upper_bound = X.max()
|
|
410
|
+
|
|
411
|
+
if self.df is not None:
|
|
412
|
+
if self.df < order - 1 + self.intercept:
|
|
413
|
+
raise ValueError('df must be greater than or equal to %d' % (order - 1 + self.intercept))
|
|
414
|
+
ninternal = self.df - (order - 1 + self.intercept)
|
|
415
|
+
percs = 100*np.linspace(0, 1, ninternal+2)[1:-1]
|
|
416
|
+
internal_knots = np.percentile(X, percs)
|
|
417
|
+
if self.internal_knots is not None:
|
|
418
|
+
raise ValueError('only one of df or internal_knots should be specified')
|
|
419
|
+
else:
|
|
420
|
+
internal_knots = np.asarray(sorted(self.internal_knots))
|
|
421
|
+
if self.internal_knots is None:
|
|
422
|
+
raise ValueError('if df not specified then need internal_knots')
|
|
423
|
+
|
|
424
|
+
if lower_bound >= upper_bound:
|
|
425
|
+
raise ValueError('lower_bound must be smaller than upper_bound')
|
|
426
|
+
|
|
427
|
+
self.internal_knots_ = internal_knots
|
|
428
|
+
|
|
429
|
+
self.knots_ = np.sort(np.concatenate([[lower_bound]*order,
|
|
430
|
+
[upper_bound]*order,
|
|
431
|
+
internal_knots]))
|
|
432
|
+
if self.knots_[0] < lower_bound:
|
|
433
|
+
raise ValueError('internal_knots should be greater than our equal to lower_bound')
|
|
434
|
+
if self.knots_[-1] > upper_bound:
|
|
435
|
+
raise ValueError('internal_knots should be less than our equal to upper_bound')
|
|
436
|
+
|
|
437
|
+
self.boundary_knots_ = [lower_bound, upper_bound]
|
|
438
|
+
|
|
439
|
+
# for pandas
|
|
440
|
+
|
|
441
|
+
self.nbasis_ = len(self.knots_) - (self.degree + 1)
|
|
442
|
+
self.columns_ = range(self.nbasis_)
|
|
443
|
+
|
|
444
|
+
if not self.intercept:
|
|
445
|
+
self.columns_ = self.columns_[:-1]
|
|
446
|
+
|
|
447
|
+
if isinstance(X_orig, (pd.Series, pd.DataFrame)):
|
|
448
|
+
if isinstance(X_orig, pd.Series):
|
|
449
|
+
name = X_orig.name
|
|
450
|
+
else:
|
|
451
|
+
name = X_orig.columns[0] # a pd.DataFrame
|
|
452
|
+
self.columns_ = ['{0}[{1}]'.format(self, d)
|
|
453
|
+
for d in self.columns_]
|
|
454
|
+
return self
|
|
455
|
+
|
|
456
|
+
def transform(self, X):
|
|
457
|
+
|
|
458
|
+
"""
|
|
459
|
+
Construct design for B-splines
|
|
460
|
+
using features X.
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
X : array-like
|
|
465
|
+
X on which splines will be evaluated.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
XS : np.ndarray
|
|
470
|
+
Evaluated splines.
|
|
471
|
+
"""
|
|
472
|
+
check_is_fitted(self)
|
|
473
|
+
|
|
474
|
+
X_orig = X
|
|
475
|
+
X = _single_column(X)
|
|
476
|
+
n = X.shape[0]
|
|
477
|
+
|
|
478
|
+
value = _splevf(X,
|
|
479
|
+
(self.knots_, self.degree),
|
|
480
|
+
der=0,
|
|
481
|
+
ext=self.ext)
|
|
482
|
+
if not self.intercept:
|
|
483
|
+
value = value[:,1:]
|
|
484
|
+
columns_ = self.columns_
|
|
485
|
+
|
|
486
|
+
if isinstance(X_orig, (pd.Series, pd.DataFrame)):
|
|
487
|
+
df = pd.DataFrame(value,
|
|
488
|
+
columns=columns_,
|
|
489
|
+
copy=False)
|
|
490
|
+
df.index = X_orig.index
|
|
491
|
+
return df
|
|
492
|
+
else:
|
|
493
|
+
return value
|
|
494
|
+
|
|
495
|
+
class NaturalSpline(TransformerMixin, BaseEstimator):
|
|
496
|
+
|
|
497
|
+
'''
|
|
498
|
+
|
|
499
|
+
Natural cubic spline.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
|
|
504
|
+
intercept : bool, default=False
|
|
505
|
+
If False, a column of basis is dropped so that by
|
|
506
|
+
adding an intercept column design stays full rank.
|
|
507
|
+
|
|
508
|
+
lower_bound : float, default=None
|
|
509
|
+
Lower boundary not. Will be set to minimal value if not supplied.
|
|
510
|
+
|
|
511
|
+
upper_bound : float, default=None
|
|
512
|
+
Upper boundary not. Will be set to maximal value if not supplied.
|
|
513
|
+
|
|
514
|
+
internal_knots : array-like (optional)
|
|
515
|
+
Optional internal knots of B-spline. Will be set to
|
|
516
|
+
appropriate quantiles based on `df`.
|
|
517
|
+
|
|
518
|
+
df : int, default=None
|
|
519
|
+
Degrees of freedom for spline. Defaults to `3 + intercept`.
|
|
520
|
+
|
|
521
|
+
ext : int, default=0
|
|
522
|
+
How B-splines are to be extended beyond the boundary using
|
|
523
|
+
`scipy.interpolate.splev`.
|
|
524
|
+
|
|
525
|
+
'''
|
|
526
|
+
|
|
527
|
+
def __init__(self,
|
|
528
|
+
intercept=False,
|
|
529
|
+
lower_bound=None,
|
|
530
|
+
upper_bound=None,
|
|
531
|
+
internal_knots=None,
|
|
532
|
+
df=None,
|
|
533
|
+
ext=0):
|
|
534
|
+
|
|
535
|
+
self.lower_bound = lower_bound
|
|
536
|
+
self.upper_bound = upper_bound
|
|
537
|
+
self.internal_knots = internal_knots
|
|
538
|
+
self.df = df
|
|
539
|
+
self.intercept = intercept
|
|
540
|
+
self.ext = ext
|
|
541
|
+
|
|
542
|
+
def fit(self,
|
|
543
|
+
X,
|
|
544
|
+
y=None):
|
|
545
|
+
|
|
546
|
+
"""
|
|
547
|
+
Compute knots for natural spline representation.
|
|
548
|
+
|
|
549
|
+
Parameters
|
|
550
|
+
----------
|
|
551
|
+
X : array-like
|
|
552
|
+
|
|
553
|
+
Single feature on which B-spline will be evaluated.
|
|
554
|
+
|
|
555
|
+
y : None
|
|
556
|
+
Ignored. This parameter exists only for compatibility with
|
|
557
|
+
:class:`~sklearn.pipeline.Pipeline`.
|
|
558
|
+
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
X_orig = X
|
|
562
|
+
X = _single_column(X)
|
|
563
|
+
n = X.shape[0]
|
|
564
|
+
|
|
565
|
+
order = 4
|
|
566
|
+
|
|
567
|
+
lower_bound = self.lower_bound
|
|
568
|
+
if lower_bound is None:
|
|
569
|
+
lower_bound = X.min()
|
|
570
|
+
|
|
571
|
+
upper_bound = self.upper_bound
|
|
572
|
+
if upper_bound is None:
|
|
573
|
+
upper_bound = X.max()
|
|
574
|
+
|
|
575
|
+
if self.df is not None:
|
|
576
|
+
if self.df < order - 1 + self.intercept - 2:
|
|
577
|
+
raise ValueError('df must be greater than or equal to %d' % (order - 1 + self.intercept - 2))
|
|
578
|
+
ninternal = self.df - (order - 1 + self.intercept - 2) # -2 for constraints
|
|
579
|
+
percs = 100*np.linspace(0, 1, ninternal+2)[1:-1]
|
|
580
|
+
internal_knots = np.percentile(X, percs)
|
|
581
|
+
if self.internal_knots is not None:
|
|
582
|
+
raise ValueError('only one of df or internal_knots should be specified')
|
|
583
|
+
else:
|
|
584
|
+
internal_knots = np.asarray(sorted(self.internal_knots))
|
|
585
|
+
if self.internal_knots is None:
|
|
586
|
+
raise ValueError('if df not specified then need internal_knots')
|
|
587
|
+
|
|
588
|
+
if lower_bound >= upper_bound:
|
|
589
|
+
raise ValueError('lower_bound must be smaller than upper_bound')
|
|
590
|
+
|
|
591
|
+
self.internal_knots_ = internal_knots
|
|
592
|
+
self.knots_ = np.sort(np.concatenate([[lower_bound]*order,
|
|
593
|
+
[upper_bound]*order,
|
|
594
|
+
internal_knots]))
|
|
595
|
+
if self.knots_[0] < lower_bound:
|
|
596
|
+
raise ValueError('internal_knots should be greater than our equal to lower_bound')
|
|
597
|
+
if self.knots_[-1] > upper_bound:
|
|
598
|
+
raise ValueError('internal_knots should be less than our equal to upper_bound')
|
|
599
|
+
|
|
600
|
+
self.boundary_knots_ = [lower_bound, upper_bound]
|
|
601
|
+
self.nbasis_ = self.knots_.shape[0] - order
|
|
602
|
+
|
|
603
|
+
# now enforce constraint that 2nd derivative at two boundary knots is 0
|
|
604
|
+
# this depends on the X that is passed to fit!
|
|
605
|
+
|
|
606
|
+
boundary_vals = _splevf(self.boundary_knots_,
|
|
607
|
+
(self.knots_, 3),
|
|
608
|
+
der=2,
|
|
609
|
+
ext=0)
|
|
610
|
+
if not self.intercept:
|
|
611
|
+
boundary_vals = boundary_vals[:,1:]
|
|
612
|
+
Q_, R_ = np.linalg.qr(boundary_vals.T,
|
|
613
|
+
mode='complete')
|
|
614
|
+
self.Qproj_ = Q_[:,2:]
|
|
615
|
+
|
|
616
|
+
# for pandas
|
|
617
|
+
|
|
618
|
+
self.columns_ = range(self.nbasis_-2) # two constraints
|
|
619
|
+
|
|
620
|
+
if not self.intercept:
|
|
621
|
+
self.columns_ = self.columns_[:-1]
|
|
622
|
+
|
|
623
|
+
if isinstance(X_orig, (pd.Series, pd.DataFrame)):
|
|
624
|
+
if isinstance(X_orig, pd.Series):
|
|
625
|
+
name = X_orig.name
|
|
626
|
+
else:
|
|
627
|
+
name = X_orig.columns[0] # a pd.DataFrame
|
|
628
|
+
self.columns_ = ['{0}[{1}]'.format(self, d)
|
|
629
|
+
for d in self.columns_]
|
|
630
|
+
return self
|
|
631
|
+
|
|
632
|
+
def transform(self, X):
|
|
633
|
+
|
|
634
|
+
"""
|
|
635
|
+
Construct design for natural cubic splines
|
|
636
|
+
for features X.
|
|
637
|
+
|
|
638
|
+
Parameters
|
|
639
|
+
----------
|
|
640
|
+
X : array-like
|
|
641
|
+
X on which natural cubic splines will be evaluated.
|
|
642
|
+
|
|
643
|
+
Returns
|
|
644
|
+
-------
|
|
645
|
+
XN : np.ndarray
|
|
646
|
+
Evaluated natural cubic spline features.
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
check_is_fitted(self)
|
|
650
|
+
|
|
651
|
+
lower_bound, upper_bound = self.boundary_knots_
|
|
652
|
+
|
|
653
|
+
X_orig = X
|
|
654
|
+
X = _single_column(X)
|
|
655
|
+
n = X.shape[0]
|
|
656
|
+
|
|
657
|
+
value = np.empty((X.shape[0], self.nbasis_))
|
|
658
|
+
inside = (X >= lower_bound) * (X <= upper_bound)
|
|
659
|
+
if np.any(inside):
|
|
660
|
+
value[inside] = _splevf(X[inside],
|
|
661
|
+
(self.knots_, 3),
|
|
662
|
+
der=0,
|
|
663
|
+
ext=self.ext)
|
|
664
|
+
|
|
665
|
+
to_the_right = (X > upper_bound)
|
|
666
|
+
if np.any(to_the_right):
|
|
667
|
+
value[to_the_right] = _splev_taylor(X[to_the_right],
|
|
668
|
+
upper_bound,
|
|
669
|
+
(self.knots_, 3),
|
|
670
|
+
order=1)
|
|
671
|
+
|
|
672
|
+
to_the_left = (X < lower_bound)
|
|
673
|
+
if np.any(to_the_left):
|
|
674
|
+
value[to_the_left] = _splev_taylor(X[to_the_left],
|
|
675
|
+
lower_bound,
|
|
676
|
+
(self.knots_, 3),
|
|
677
|
+
order=1)
|
|
678
|
+
|
|
679
|
+
# possibly drop column for intercept
|
|
680
|
+
|
|
681
|
+
if not self.intercept:
|
|
682
|
+
value = value[:,1:]
|
|
683
|
+
|
|
684
|
+
# map onto the natural splines
|
|
685
|
+
|
|
686
|
+
value = value.dot(self.Qproj_)
|
|
687
|
+
if isinstance(X_orig, (pd.Series, pd.DataFrame)):
|
|
688
|
+
df = pd.DataFrame(value,
|
|
689
|
+
columns=self.columns_,
|
|
690
|
+
copy=False)
|
|
691
|
+
df.index = X_orig.index
|
|
692
|
+
return df
|
|
693
|
+
else:
|
|
694
|
+
return value
|
|
695
|
+
|