modelmatrix 0.1.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (52) hide show
  1. modelmatrix-0.3.0/CHANGELOG.md +92 -0
  2. {modelmatrix-0.1.0/modelmatrix.egg-info → modelmatrix-0.3.0}/PKG-INFO +1 -1
  3. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/api/index.rst +1 -0
  4. modelmatrix-0.3.0/docs/api/responses.rst +13 -0
  5. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/api/terms.rst +7 -0
  6. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/index.md +1 -0
  7. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/myst.yml +1 -0
  8. modelmatrix-0.3.0/docs/responses.md +228 -0
  9. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix/__init__.py +7 -2
  10. modelmatrix-0.3.0/modelmatrix/_native.py +95 -0
  11. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix/_version.py +3 -3
  12. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix/model_spec.py +222 -84
  13. modelmatrix-0.3.0/modelmatrix/outcome.py +173 -0
  14. modelmatrix-0.3.0/modelmatrix/response.py +400 -0
  15. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix/transforms.py +27 -8
  16. {modelmatrix-0.1.0 → modelmatrix-0.3.0/modelmatrix.egg-info}/PKG-INFO +1 -1
  17. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix.egg-info/SOURCES.txt +8 -0
  18. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix.egg-info/scm_file_list.json +9 -1
  19. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix.egg-info/scm_version.json +2 -2
  20. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/tests/test_model_matrix.py +17 -0
  21. modelmatrix-0.3.0/tests/test_outcome.py +144 -0
  22. modelmatrix-0.3.0/tests/test_response.py +198 -0
  23. modelmatrix-0.3.0/tests/test_wildcard.py +186 -0
  24. modelmatrix-0.1.0/CHANGELOG.md +0 -29
  25. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/.github/workflows/build_docs.yml +0 -0
  26. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/.github/workflows/build_test.yml +0 -0
  27. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/.gitignore +0 -0
  28. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/LICENSE +0 -0
  29. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/README.md +0 -0
  30. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/.gitignore +0 -0
  31. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/PCA.md +0 -0
  32. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/anova.md +0 -0
  33. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/api/ModelSpec.rst +0 -0
  34. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/api/conf.py +0 -0
  35. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/api/transforms.rst +0 -0
  36. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/comparison.md +0 -0
  37. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/poly.md +0 -0
  38. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/spec.md +0 -0
  39. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/docs/splines.md +0 -0
  40. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix/columns.py +0 -0
  41. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix.egg-info/dependency_links.txt +0 -0
  42. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix.egg-info/requires.txt +0 -0
  43. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/modelmatrix.egg-info/top_level.txt +0 -0
  44. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/pyproject.toml +0 -0
  45. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/setup.cfg +0 -0
  46. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/tests/_datasets.py +0 -0
  47. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/tests/data/Carseats.csv +0 -0
  48. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/tests/data/Wage.csv +0 -0
  49. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/tests/test_boolean_columns.py +0 -0
  50. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/tests/test_columns.py +0 -0
  51. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/tests/test_model_spec.py +0 -0
  52. {modelmatrix-0.1.0 → modelmatrix-0.3.0}/tests/test_model_spec_hard.py +0 -0
@@ -0,0 +1,92 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.3.0 (2026-10-26)
6
+
7
+ ### New features
8
+
9
+ - **`Outcome`**: a response with the other columns a model takes with
10
+ it, each named by its keyword, so an estimator can find them by name:
11
+ `Outcome(Surv('time', 'status'), offset='O', weight='w', strata='inst')`
12
+ gives the columns `stop`, `status`, `offset`, `weight` and `strata`. A
13
+ column is taken as it is (a categorical keeps its labels); a feature of
14
+ a single column is fit as a term, e.g. `offset(log(exposure))`.
15
+ `wildcard(exclude=outcome)` leaves out all of its columns, and
16
+ `transform(X, response=False)` builds the offset of new data. With
17
+ `Binomial(..., output='proportion')`, the trials are the column
18
+ `weight`, times any weights given.
19
+ - Dataframes of other backends keep a categorical column (as a polars
20
+ `Enum`).
21
+
22
+ ### Breaking changes
23
+
24
+ - `Surv` names its columns by their role, `start`, `stop` and `status`
25
+ (as R's and lifelines'), not as in the data: `Surv('time', 'status')`
26
+ gives the columns `stop` and `status`.
27
+
28
+ ## 0.2.0 (2026-10-26)
29
+
30
+ ### New features
31
+
32
+ - **Responses**, built from the same data as the design, as a
33
+ `ModelSpec` builds it: `response.fit(D)`, then `response.transform(D)`
34
+ (or `fit_transform`), in the backend of `D` (e.g. polars).
35
+ - `Surv(time, status)` or `Surv(start, stop, status)`, as R's: a
36
+ dataframe of these columns, as glmstar's `CoxFamily` or lifelines
37
+ take it;
38
+ - `Binomial('y')` for a binary column (0/1, boolean or two levels; the
39
+ second level, or `level=`, is a success), or `Binomial(successes,
40
+ trials=...)` for the successes and failures (`output='counts'`, R's
41
+ `cbind`) or the proportion and number of trials (`output='proportion'`);
42
+ - `Multinomial('class')` for the class of each row, as a categorical or
43
+ one-hot (`output='onehot'`), or `Multinomial([...])` for counts.
44
+
45
+ Responses learn their levels at fit; a level unseen at fit raises.
46
+ - **`wildcard`**, R's `.`: every column of `X`, but those of
47
+ `wildcard(exclude=...)`, so `ModelSpec([wildcard(exclude='y')])` is the
48
+ design of `y ~ .`. `exclude` also takes a response, leaving out its
49
+ columns: `wildcard(exclude=Surv('time', 'status'))`. `wildcard(pow=2)`
50
+ is `.^2`, `(wildcard, 'g')` is `.:g`, and `pca(wildcard(exclude='y'),
51
+ n_components=3)` a feature of the columns. It is expanded at fit into
52
+ `expanded_terms_`; columns that are terms already are not repeated, as
53
+ in R's `y ~ x + .`. `build_submodel` expands it too.
54
+ - `pca()`'s `name` is optional.
55
+ - `fit(X, y)` checks that the indices of `X` and `y` agree when both are
56
+ pandas objects.
57
+
58
+ ### Bug fixes
59
+
60
+ - `poly`, `ns` and `bs` are fit on the observed values of their variable,
61
+ and are missing only in its missing rows: before, a single missing value
62
+ made every value of the feature missing (or, for `ns`, arbitrary).
63
+
64
+ ### Documentation
65
+
66
+ - A page on responses and the wildcard, and on missing values.
67
+
68
+ ## 0.1.0 (2026-10-26)
69
+
70
+ `ModelSpec`, from [ISLP](https://github.com/intro-stat-learning/ISLP) 0.6.0,
71
+ as its own package, with the history of its files. ISLP 0.7 depends on it,
72
+ and `from ISLP.models import ModelSpec` keeps working there.
73
+
74
+ ### Changes from ISLP 0.6.0
75
+
76
+ - **`intercept` defaults to `False`:** pass `intercept=True` for a column
77
+ of ones named `'intercept'`, as statsmodels' models (`OLS`, `GLM`, ...)
78
+ need. Most scikit-learn estimators fit the intercept themselves.
79
+ - No `FutureWarning` of the move.
80
+ - Import from `modelmatrix` (`ModelSpec`, `Column`, `Feature`, `Contrast`,
81
+ `Ordinal`, `poly`, `ns`, `bs`, `pca`, `contrast`, `ordinal`,
82
+ `derived_feature`, `build_columns`); the transforms are in
83
+ `modelmatrix.transforms`.
84
+ - The docs: the guide to `ModelSpec`, the pages on polynomials, splines,
85
+ PCA and ANOVA, and the comparison with `ColumnTransformer`, patsy and
86
+ formulaic, from ISLP's docs, as a Jupyter Book 2 site (as glmstar's),
87
+ with a Sphinx API reference.
88
+
89
+ For the changes up to ISLP 0.6.0, see
90
+ [ISLP's changelog](https://github.com/intro-stat-learning/ISLP/blob/main/CHANGELOG.md).
91
+
92
+ Requires Python 3.11 or later.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelmatrix
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Model matrices from tabular data, as scikit-learn transformers
5
5
  Author-email: Jonathan Taylor <jonathan.taylor@stanford.edu>
6
6
  License-Expression: BSD-3-Clause
@@ -9,4 +9,5 @@ For the guide and examples, see the `modelmatrix documentation <../>`_.
9
9
 
10
10
  ModelSpec
11
11
  terms
12
+ responses
12
13
  transforms
@@ -0,0 +1,13 @@
1
+ Responses
2
+ =========
3
+
4
+ .. automodule:: modelmatrix.response
5
+ :no-members:
6
+
7
+ .. autoclass:: modelmatrix.Surv
8
+ .. autoclass:: modelmatrix.Binomial
9
+ .. autoclass:: modelmatrix.Multinomial
10
+ .. autoclass:: modelmatrix.Outcome
11
+ :members: transform
12
+ .. autoclass:: modelmatrix.Response
13
+ :members: fit, transform, fit_transform
@@ -13,6 +13,13 @@ Helpers that make terms
13
13
  .. autofunction:: modelmatrix.derived_feature
14
14
  .. autofunction:: modelmatrix.build_columns
15
15
 
16
+ The wildcard
17
+ ------------
18
+
19
+ .. autodata:: modelmatrix.wildcard
20
+ :annotation: = Wildcard()
21
+ .. autoclass:: modelmatrix.Wildcard
22
+
16
23
  Terms and columns
17
24
  -----------------
18
25
 
@@ -14,6 +14,7 @@ pip install modelmatrix
14
14
 
15
15
  - [Building design matrices with `ModelSpec`](spec.md)
16
16
  - Features: [polynomials](poly.md), [splines](splines.md) and [PCA](PCA.md)
17
+ - [Responses and the wildcard](responses.md): `y ~ .`, `Surv`, `Binomial`, `Multinomial` and missing values
17
18
  - [ANOVA](anova.md) with `build_sequence`
18
19
  - [`ModelSpec` compared](comparison.md) with `ColumnTransformer`, patsy and formulaic
19
20
  - The [API reference](https://intro-stat-learning.github.io/modelmatrix/api/)
@@ -18,6 +18,7 @@ project:
18
18
  - file: poly.md
19
19
  - file: splines.md
20
20
  - file: PCA.md
21
+ - file: responses.md
21
22
  - file: anova.md
22
23
  - file: comparison.md
23
24
  - title: API Reference
@@ -0,0 +1,228 @@
1
+ ---
2
+ jupytext:
3
+ formats: ipynb,md:myst
4
+ text_representation:
5
+ extension: .md
6
+ format_name: myst
7
+ format_version: 0.13
8
+ kernelspec:
9
+ display_name: python3
10
+ language: python
11
+ name: python3
12
+ ---
13
+
14
+ # Responses and the wildcard
15
+
16
+ As in scikit-learn, the design `X` and the response `y` are built
17
+ separately: a `ModelSpec` builds the design, and a response, such as
18
+ `Surv`, `Binomial` or `Multinomial`, builds `y` from the same data.
19
+ `wildcard` stands for every column, as `.` in R's formulae, and
20
+ `wildcard(exclude=...)` leaves out the response:
21
+ `ModelSpec([wildcard(exclude='Sales')])` is the design of R's
22
+ `Sales ~ .`.
23
+
24
+ ```{code-cell} ipython3
25
+ import numpy as np, pandas as pd
26
+ import statsmodels.api as sm
27
+ from sklearn.linear_model import LinearRegression
28
+ from sklearn.model_selection import GridSearchCV
29
+ from sklearn.pipeline import make_pipeline
30
+
31
+ from ISLP import load_data
32
+ from modelmatrix import (ModelSpec as MS,
33
+ wildcard,
34
+ pca,
35
+ poly,
36
+ Surv,
37
+ Binomial,
38
+ Multinomial)
39
+
40
+ Carseats = load_data('Carseats')
41
+ ```
42
+
43
+ ## `y ~ .`
44
+
45
+ ```{code-cell} ipython3
46
+ spec = MS([wildcard(exclude='Sales')], intercept=True).fit(Carseats)
47
+ X, y = spec.transform(Carseats), Carseats['Sales']
48
+ sm.OLS(y, X).fit().params
49
+ ```
50
+
51
+ The wildcard is expanded when the `ModelSpec` is fit: `terms` is
52
+ unchanged, and `expanded_terms_` holds what it stood for.
53
+
54
+ ```{code-cell} ipython3
55
+ spec.terms, spec.expanded_terms_
56
+ ```
57
+
58
+ Columns that are terms themselves are not repeated, as in R's
59
+ `Sales ~ Price + .`, and more columns can be excluded, as R's `. - x`:
60
+
61
+ ```{code-cell} ipython3
62
+ MS([poly('Price', 2), 'Price',
63
+ wildcard(exclude=['Sales', 'Population', 'Education'])]).fit(Carseats).expanded_terms_
64
+ ```
65
+
66
+ ## `y ~ .^2` and `.:x`
67
+
68
+ `wildcard(pow=2)` adds every interaction of two columns, as R's `.^2`,
69
+ and the wildcard can be a part of an interaction, as R's `.:US`:
70
+
71
+ ```{code-cell} ipython3
72
+ small = Carseats[['Sales', 'Price', 'Income', 'US']]
73
+ MS([wildcard(pow=2, exclude='Sales')]).fit(small).expanded_terms_
74
+ ```
75
+
76
+ ```{code-cell} ipython3
77
+ MS(['US', (wildcard(exclude=['Sales', 'US']), 'US')]).fit(small).expanded_terms_
78
+ ```
79
+
80
+ ## Features of the wildcard
81
+
82
+ A wildcard can be a variable of a feature: here, the first principal
83
+ components of the numeric columns, but the response.
84
+
85
+ ```{code-cell} ipython3
86
+ pc = pca(wildcard(exclude=['Sales', 'ShelveLoc', 'Urban', 'US']), n_components=3, scale=True)
87
+ spec = MS([pc, 'ShelveLoc']).fit(Carseats)
88
+ spec.transform(Carseats).iloc[:5]
89
+ ```
90
+
91
+ Its parameters are tuned as those of any term:
92
+
93
+ ```{code-cell} ipython3
94
+ pipe = make_pipeline(MS([pc, 'ShelveLoc']), LinearRegression())
95
+ grid = GridSearchCV(pipe, {'modelspec__terms__0__n_components': [1, 3, 5, 7]}, cv=5)
96
+ grid.fit(Carseats, Carseats['Sales'])
97
+ grid.best_params_
98
+ ```
99
+
100
+ When `X` and `y` are both pandas objects, `ModelSpec.fit` checks that
101
+ their indices agree.
102
+
103
+ ## Responses
104
+
105
+ A response is fit, e.g. to learn the levels of a categorical, so that
106
+ it is coded the same way on new data, and `transform` builds it:
107
+ `response.fit_transform(D)`. For a dataframe of another backend, such
108
+ as polars, the response is in that backend. `wildcard(exclude=response)`
109
+ leaves out the response's columns, so that it is not a term of its own
110
+ design.
111
+
112
+ ### Survival responses: `Surv`
113
+
114
+ `Surv(time, status)` is R's survival response, and `Surv(start, stop,
115
+ status)` that of (start, stop] intervals. The response is a dataframe
116
+ whose columns are named by their role, whatever their names in the
117
+ data: `stop` (the times), `status` and, if given, `start`, as R's
118
+ `Surv` and lifelines' `CoxTimeVaryingFitter` name them.
119
+
120
+ ```{code-cell} ipython3
121
+ BrainCancer = load_data('BrainCancer').dropna()
122
+ response = Surv('time', 'status')
123
+ y = response.fit_transform(BrainCancer)
124
+ X = MS([wildcard(exclude=response)]).fit_transform(BrainCancer)
125
+ y.iloc[:5]
126
+ ```
127
+
128
+ ```{code-cell} ipython3
129
+ sm.PHReg(y['stop'], X, status=y['status']).fit().params
130
+ ```
131
+
132
+ ### Binomial responses
133
+
134
+ `Binomial('US')` codes a binary column as 0 and 1, the second level, as in
135
+ R, being a success (or the one given by `level=`):
136
+
137
+ ```{code-cell} ipython3
138
+ y = Binomial('US').fit_transform(Carseats)
139
+ pd.crosstab(y, Carseats['US'])
140
+ ```
141
+
142
+ With the number of trials, `Binomial(successes, trials=...)` gives the
143
+ successes and the failures, as R's `cbind(successes, failures)` (which
144
+ statsmodels' `GLM` takes too), or with `output='proportion'` the proportion
145
+ and the number of trials, as a response and weights:
146
+
147
+ ```{code-cell} ipython3
148
+ rng = np.random.default_rng(0)
149
+ D = pd.DataFrame({'x': rng.standard_normal(50), 'size': rng.integers(5, 20, 50)})
150
+ D['cases'] = rng.binomial(D['size'], 1 / (1 + np.exp(-D['x'])))
151
+
152
+ counts = Binomial('cases', trials='size').fit_transform(D)
153
+ prop = Binomial('cases', trials='size', output='proportion').fit_transform(D)
154
+ X = MS(['x'], intercept=True).fit_transform(D)
155
+ family = sm.families.Binomial()
156
+ (sm.GLM(counts, X, family=family).fit().params,
157
+ sm.GLM(prop['cases/size'], X, family=family, var_weights=prop['size']).fit().params)
158
+ ```
159
+
160
+ ### Multinomial responses
161
+
162
+ `Multinomial('ShelveLoc')` gives the class of each row as a categorical
163
+ whose categories are the levels seen at fit, and with `output='onehot'`
164
+ an indicator column for each level; `Multinomial([...])` takes a column
165
+ of counts for each class.
166
+
167
+ ```{code-cell} ipython3
168
+ Multinomial('ShelveLoc', output='onehot').fit_transform(Carseats).iloc[:5]
169
+ ```
170
+
171
+ ### Offsets, weights and strata: `Outcome`
172
+
173
+ `Outcome` puts the other columns a model takes with its response next
174
+ to it, each named by its keyword: an offset, weights, the strata of a
175
+ Cox model, or any other. A column is taken as it is (a categorical keeps
176
+ its labels), and a feature of a single column is fit as a term of a
177
+ `ModelSpec`, e.g. R's `offset(log(exposure))`:
178
+
179
+ ```{code-cell} ipython3
180
+ from sklearn.preprocessing import FunctionTransformer
181
+ from modelmatrix import Outcome, derived_feature
182
+
183
+ rng = np.random.default_rng(1)
184
+ P = pd.DataFrame({'x': rng.standard_normal(100), 'exposure': rng.uniform(1, 5, 100)})
185
+ P['claims'] = rng.poisson(P['exposure'] * np.exp(0.3 * P['x']))
186
+
187
+ log_exposure = derived_feature(['exposure'], encoder=FunctionTransformer(np.log),
188
+ name='log(exposure)')
189
+ outcome = Outcome('claims', offset=log_exposure)
190
+ y = outcome.fit_transform(P)
191
+ X = MS([wildcard(exclude=outcome)], intercept=True).fit_transform(P) # leaves out exposure too
192
+ sm.GLM(y['claims'], X, family=sm.families.Poisson(), offset=y['offset']).fit().params
193
+ ```
194
+
195
+ As the columns are named by their role, an estimator can find them by
196
+ name, e.g. a stratified Cox model with an offset:
197
+
198
+ ```{code-cell} ipython3
199
+ BrainCancer['O'] = 0.1
200
+ outcome = Outcome(Surv('time', 'status'), offset='O', strata='sex')
201
+ y = outcome.fit_transform(BrainCancer)
202
+ y.iloc[:5]
203
+ ```
204
+
205
+ The offset of new data, whose response is not known, is
206
+ `outcome.transform(new, response=False)`. With a `Binomial` of
207
+ `output='proportion'`, the number of trials is the column `weight`
208
+ (times the weights given as `weight=`), as R's weights of a proportion.
209
+
210
+ ## Missing values
211
+
212
+ As scikit-learn's transformers, a `ModelSpec` keeps every row: a row
213
+ of the design is missing where a variable it uses is missing (a
214
+ `poly` or spline is fit on the observed values), and so is the
215
+ response. A transformer cannot drop rows of `X` without those of `y`,
216
+ so the rows to keep, those of R's `na.omit`, are chosen outside the
217
+ `ModelSpec` (or the missing values are imputed, e.g. with
218
+ scikit-learn's `SimpleImputer`, for an estimator that does not take
219
+ them).
220
+
221
+ ```{code-cell} ipython3
222
+ Hitters = load_data('Hitters')
223
+ X = MS([wildcard(exclude='Salary')]).fit_transform(Hitters)
224
+ y = Hitters['Salary']
225
+ keep = X.notna().all(axis=1) & y.notna()
226
+ X, y = X[keep], y[keep]
227
+ X.shape
228
+ ```
@@ -13,8 +13,12 @@ from .model_spec import (ModelSpec,
13
13
  pca,
14
14
  contrast,
15
15
  ordinal,
16
- build_columns)
16
+ build_columns,
17
+ wildcard,
18
+ Wildcard)
17
19
  from .columns import Column
20
+ from .response import Response, Surv, Binomial, Multinomial
21
+ from .outcome import Outcome
18
22
 
19
23
  try:
20
24
  from ._version import version as __version__
@@ -23,4 +27,5 @@ except ImportError: # not installed
23
27
 
24
28
  __all__ = ['ModelSpec', 'Feature', 'Column', 'Contrast', 'Ordinal',
25
29
  'poly', 'ns', 'bs', 'derived_feature', 'pca', 'contrast',
26
- 'ordinal', 'build_columns']
30
+ 'ordinal', 'build_columns', 'wildcard', 'Wildcard',
31
+ 'Response', 'Surv', 'Binomial', 'Multinomial', 'Outcome']
@@ -0,0 +1,95 @@
1
+ """
2
+ Dataframes of other backends
3
+ ============================
4
+
5
+ A dataframe of any eager backend supported by narwhals, e.g. polars or
6
+ pyarrow, is converted to pandas, and the results are converted back.
7
+ """
8
+
9
+ import numpy as np, pandas as pd
10
+ import narwhals as nw
11
+ from scipy import sparse as sp_sparse
12
+
13
+
14
+ def _series_to_native(S, backend):
15
+ return _series_to_nw(S, backend).to_native()
16
+
17
+ def _series_to_nw(S, backend):
18
+ # a categorical is an Enum, if the backend has one, of its
19
+ # categories as strings
20
+ values, dtype = S.to_numpy(), None
21
+ if isinstance(S.dtype, pd.CategoricalDtype):
22
+ values = np.asarray([None if pd.isna(v) else str(v) for v in S], dtype=object)
23
+ dtype = nw.Enum([str(c) for c in S.cat.categories])
24
+ try:
25
+ return nw.new_series(str(S.name), values, dtype=dtype, backend=backend)
26
+ except NotImplementedError: # e.g. no Enum in pyarrow
27
+ return nw.new_series(str(S.name), values, backend=backend)
28
+
29
+ def _from_native(X, columns=None):
30
+ """
31
+ Convert a dataframe (or series) of any eager backend supported by
32
+ narwhals, e.g. polars or pyarrow, to pandas. Returns the converted
33
+ `X` and a function converting a `pd.DataFrame` model matrix back to
34
+ that backend, or None if `X` was not converted.
35
+
36
+ If `columns` is given, only those columns of a dataframe are
37
+ converted (the copy is of the columns a model matrix uses).
38
+
39
+ Conversion through pandas keeps categoricals, including the order
40
+ of a polars Enum or an ordered pyarrow dictionary (as an ordered
41
+ `pd.Categorical`). The levels of an unordered categorical, e.g. a
42
+ polars Categorical, are stored in order of appearance; they are
43
+ sorted, as pandas does when it infers categories, so that the
44
+ model matrix does not depend on the order of the rows.
45
+ """
46
+ if (isinstance(X, (pd.DataFrame, pd.Series, np.ndarray)) or
47
+ sp_sparse.issparse(X)):
48
+ return X, None
49
+ try:
50
+ X_nw = nw.from_native(X, allow_series=True)
51
+ except TypeError: # not a dataframe, e.g. a list: left to _check_X
52
+ return X, None
53
+ if isinstance(X_nw, nw.LazyFrame):
54
+ raise TypeError('Lazy frames are not supported by ModelSpec; '
55
+ 'collect them first, e.g. X.collect()')
56
+ backend = nw.get_native_namespace(X_nw)
57
+ if columns is not None and isinstance(X_nw, nw.DataFrame):
58
+ keep = [c for c in X_nw.columns if c in columns]
59
+ X_nw = X_nw.select(keep or X_nw.columns[:1])
60
+
61
+ def to_native(D):
62
+ if isinstance(D, pd.Series): # e.g. a response
63
+ return _series_to_native(D, backend)
64
+ duplicated = list(D.columns[D.columns.duplicated()])
65
+ if duplicated:
66
+ raise ValueError(f'the model matrix has duplicated column names {duplicated}, '
67
+ 'which this dataframe backend cannot hold')
68
+ if any(isinstance(t, pd.CategoricalDtype) for t in D.dtypes): # e.g. strata
69
+ return nw.from_dict({str(c): _series_to_nw(D[c].rename(str(c)), backend)
70
+ for c in D.columns}).to_native()
71
+ return nw.from_dict({str(c): D[c].to_numpy() for c in D.columns},
72
+ backend=backend).to_native()
73
+
74
+ X = X_nw.to_pandas()
75
+ if isinstance(X_nw, nw.Series):
76
+ dtypes = {X.name: X_nw.dtype}
77
+ else:
78
+ dtypes = dict(X_nw.schema)
79
+ for name, dtype in dtypes.items():
80
+ col = X if isinstance(X, pd.Series) else X[name]
81
+ if not isinstance(col.dtype, pd.CategoricalDtype):
82
+ continue
83
+ # some versions of polars convert an Enum to an unordered
84
+ # pd.Categorical: its categories are in the declared order
85
+ if dtype == nw.Enum:
86
+ col = col.cat.as_ordered()
87
+ elif not col.dtype.ordered:
88
+ col = col.cat.reorder_categories(sorted(col.dtype.categories))
89
+ else:
90
+ continue
91
+ if isinstance(X, pd.Series):
92
+ X = col
93
+ else:
94
+ X[name] = col
95
+ return X, to_native
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.0'
22
- __version_tuple__ = version_tuple = (0, 1, 0)
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g2f08b48f3'
24
+ __commit_id__ = commit_id = 'g8c1f37d25'