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.
@@ -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
+