modelmatrix 0.1.0__tar.gz → 0.2.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.
- modelmatrix-0.2.0/CHANGELOG.md +69 -0
- {modelmatrix-0.1.0/modelmatrix.egg-info → modelmatrix-0.2.0}/PKG-INFO +1 -1
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/index.rst +1 -0
- modelmatrix-0.2.0/docs/api/responses.rst +11 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/terms.rst +7 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/index.md +1 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/myst.yml +1 -0
- modelmatrix-0.2.0/docs/responses.md +188 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/__init__.py +6 -2
- modelmatrix-0.2.0/modelmatrix/_native.py +89 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/_version.py +3 -3
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/model_spec.py +222 -84
- modelmatrix-0.2.0/modelmatrix/response.py +397 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/transforms.py +27 -8
- {modelmatrix-0.1.0 → modelmatrix-0.2.0/modelmatrix.egg-info}/PKG-INFO +1 -1
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/SOURCES.txt +6 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/scm_file_list.json +7 -1
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/scm_version.json +2 -2
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_model_matrix.py +17 -0
- modelmatrix-0.2.0/tests/test_response.py +196 -0
- modelmatrix-0.2.0/tests/test_wildcard.py +186 -0
- modelmatrix-0.1.0/CHANGELOG.md +0 -29
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/.github/workflows/build_docs.yml +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/.github/workflows/build_test.yml +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/.gitignore +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/LICENSE +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/README.md +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/.gitignore +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/PCA.md +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/anova.md +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/ModelSpec.rst +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/conf.py +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/transforms.rst +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/comparison.md +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/poly.md +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/spec.md +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/splines.md +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/columns.py +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/dependency_links.txt +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/requires.txt +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/top_level.txt +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/pyproject.toml +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/setup.cfg +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/_datasets.py +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/data/Carseats.csv +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/data/Wage.csv +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_boolean_columns.py +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_columns.py +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_model_spec.py +0 -0
- {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_model_spec_hard.py +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.0 (2026-10-26)
|
|
6
|
+
|
|
7
|
+
### New features
|
|
8
|
+
|
|
9
|
+
- **Responses**, built from the same data as the design, as a
|
|
10
|
+
`ModelSpec` builds it: `response.fit(D)`, then `response.transform(D)`
|
|
11
|
+
(or `fit_transform`), in the backend of `D` (e.g. polars).
|
|
12
|
+
- `Surv(time, status)` or `Surv(start, stop, status)`, as R's: a
|
|
13
|
+
dataframe of these columns, as glmstar's `CoxFamily` or lifelines
|
|
14
|
+
take it;
|
|
15
|
+
- `Binomial('y')` for a binary column (0/1, boolean or two levels; the
|
|
16
|
+
second level, or `level=`, is a success), or `Binomial(successes,
|
|
17
|
+
trials=...)` for the successes and failures (`output='counts'`, R's
|
|
18
|
+
`cbind`) or the proportion and number of trials (`output='proportion'`);
|
|
19
|
+
- `Multinomial('class')` for the class of each row, as a categorical or
|
|
20
|
+
one-hot (`output='onehot'`), or `Multinomial([...])` for counts.
|
|
21
|
+
|
|
22
|
+
Responses learn their levels at fit; a level unseen at fit raises.
|
|
23
|
+
- **`wildcard`**, R's `.`: every column of `X`, but those of
|
|
24
|
+
`wildcard(exclude=...)`, so `ModelSpec([wildcard(exclude='y')])` is the
|
|
25
|
+
design of `y ~ .`. `exclude` also takes a response, leaving out its
|
|
26
|
+
columns: `wildcard(exclude=Surv('time', 'status'))`. `wildcard(pow=2)`
|
|
27
|
+
is `.^2`, `(wildcard, 'g')` is `.:g`, and `pca(wildcard(exclude='y'),
|
|
28
|
+
n_components=3)` a feature of the columns. It is expanded at fit into
|
|
29
|
+
`expanded_terms_`; columns that are terms already are not repeated, as
|
|
30
|
+
in R's `y ~ x + .`. `build_submodel` expands it too.
|
|
31
|
+
- `pca()`'s `name` is optional.
|
|
32
|
+
- `fit(X, y)` checks that the indices of `X` and `y` agree when both are
|
|
33
|
+
pandas objects.
|
|
34
|
+
|
|
35
|
+
### Bug fixes
|
|
36
|
+
|
|
37
|
+
- `poly`, `ns` and `bs` are fit on the observed values of their variable,
|
|
38
|
+
and are missing only in its missing rows: before, a single missing value
|
|
39
|
+
made every value of the feature missing (or, for `ns`, arbitrary).
|
|
40
|
+
|
|
41
|
+
### Documentation
|
|
42
|
+
|
|
43
|
+
- A page on responses and the wildcard, and on missing values.
|
|
44
|
+
|
|
45
|
+
## 0.1.0 (2026-10-26)
|
|
46
|
+
|
|
47
|
+
`ModelSpec`, from [ISLP](https://github.com/intro-stat-learning/ISLP) 0.6.0,
|
|
48
|
+
as its own package, with the history of its files. ISLP 0.7 depends on it,
|
|
49
|
+
and `from ISLP.models import ModelSpec` keeps working there.
|
|
50
|
+
|
|
51
|
+
### Changes from ISLP 0.6.0
|
|
52
|
+
|
|
53
|
+
- **`intercept` defaults to `False`:** pass `intercept=True` for a column
|
|
54
|
+
of ones named `'intercept'`, as statsmodels' models (`OLS`, `GLM`, ...)
|
|
55
|
+
need. Most scikit-learn estimators fit the intercept themselves.
|
|
56
|
+
- No `FutureWarning` of the move.
|
|
57
|
+
- Import from `modelmatrix` (`ModelSpec`, `Column`, `Feature`, `Contrast`,
|
|
58
|
+
`Ordinal`, `poly`, `ns`, `bs`, `pca`, `contrast`, `ordinal`,
|
|
59
|
+
`derived_feature`, `build_columns`); the transforms are in
|
|
60
|
+
`modelmatrix.transforms`.
|
|
61
|
+
- The docs: the guide to `ModelSpec`, the pages on polynomials, splines,
|
|
62
|
+
PCA and ANOVA, and the comparison with `ColumnTransformer`, patsy and
|
|
63
|
+
formulaic, from ISLP's docs, as a Jupyter Book 2 site (as glmstar's),
|
|
64
|
+
with a Sphinx API reference.
|
|
65
|
+
|
|
66
|
+
For the changes up to ISLP 0.6.0, see
|
|
67
|
+
[ISLP's changelog](https://github.com/intro-stat-learning/ISLP/blob/main/CHANGELOG.md).
|
|
68
|
+
|
|
69
|
+
Requires Python 3.11 or later.
|
|
@@ -0,0 +1,11 @@
|
|
|
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.Response
|
|
11
|
+
: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/)
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
|
116
|
+
`pd.DataFrame` with these columns, named as in the data, as glmstar's
|
|
117
|
+
`CoxFamily(event_id=..., status_id=..., start_id=...)` or lifelines take it.
|
|
118
|
+
|
|
119
|
+
```{code-cell} ipython3
|
|
120
|
+
BrainCancer = load_data('BrainCancer').dropna()
|
|
121
|
+
response = Surv('time', 'status')
|
|
122
|
+
y = response.fit_transform(BrainCancer)
|
|
123
|
+
X = MS([wildcard(exclude=response)]).fit_transform(BrainCancer)
|
|
124
|
+
y.iloc[:5]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```{code-cell} ipython3
|
|
128
|
+
sm.PHReg(y['time'], X, status=y['status']).fit().params
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Binomial responses
|
|
132
|
+
|
|
133
|
+
`Binomial('US')` codes a binary column as 0 and 1, the second level, as in
|
|
134
|
+
R, being a success (or the one given by `level=`):
|
|
135
|
+
|
|
136
|
+
```{code-cell} ipython3
|
|
137
|
+
y = Binomial('US').fit_transform(Carseats)
|
|
138
|
+
pd.crosstab(y, Carseats['US'])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
With the number of trials, `Binomial(successes, trials=...)` gives the
|
|
142
|
+
successes and the failures, as R's `cbind(successes, failures)` (which
|
|
143
|
+
statsmodels' `GLM` takes too), or with `output='proportion'` the proportion
|
|
144
|
+
and the number of trials, as a response and weights:
|
|
145
|
+
|
|
146
|
+
```{code-cell} ipython3
|
|
147
|
+
rng = np.random.default_rng(0)
|
|
148
|
+
D = pd.DataFrame({'x': rng.standard_normal(50), 'size': rng.integers(5, 20, 50)})
|
|
149
|
+
D['cases'] = rng.binomial(D['size'], 1 / (1 + np.exp(-D['x'])))
|
|
150
|
+
|
|
151
|
+
counts = Binomial('cases', trials='size').fit_transform(D)
|
|
152
|
+
prop = Binomial('cases', trials='size', output='proportion').fit_transform(D)
|
|
153
|
+
X = MS(['x'], intercept=True).fit_transform(D)
|
|
154
|
+
family = sm.families.Binomial()
|
|
155
|
+
(sm.GLM(counts, X, family=family).fit().params,
|
|
156
|
+
sm.GLM(prop['cases/size'], X, family=family, var_weights=prop['size']).fit().params)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Multinomial responses
|
|
160
|
+
|
|
161
|
+
`Multinomial('ShelveLoc')` gives the class of each row as a categorical
|
|
162
|
+
whose categories are the levels seen at fit, and with `output='onehot'`
|
|
163
|
+
an indicator column for each level; `Multinomial([...])` takes a column
|
|
164
|
+
of counts for each class.
|
|
165
|
+
|
|
166
|
+
```{code-cell} ipython3
|
|
167
|
+
Multinomial('ShelveLoc', output='onehot').fit_transform(Carseats).iloc[:5]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Missing values
|
|
171
|
+
|
|
172
|
+
As scikit-learn's transformers, a `ModelSpec` keeps every row: a row
|
|
173
|
+
of the design is missing where a variable it uses is missing (a
|
|
174
|
+
`poly` or spline is fit on the observed values), and so is the
|
|
175
|
+
response. A transformer cannot drop rows of `X` without those of `y`,
|
|
176
|
+
so the rows to keep, those of R's `na.omit`, are chosen outside the
|
|
177
|
+
`ModelSpec` (or the missing values are imputed, e.g. with
|
|
178
|
+
scikit-learn's `SimpleImputer`, for an estimator that does not take
|
|
179
|
+
them).
|
|
180
|
+
|
|
181
|
+
```{code-cell} ipython3
|
|
182
|
+
Hitters = load_data('Hitters')
|
|
183
|
+
X = MS([wildcard(exclude='Salary')]).fit_transform(Hitters)
|
|
184
|
+
y = Hitters['Salary']
|
|
185
|
+
keep = X.notna().all(axis=1) & y.notna()
|
|
186
|
+
X, y = X[keep], y[keep]
|
|
187
|
+
X.shape
|
|
188
|
+
```
|
|
@@ -13,8 +13,11 @@ 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
|
|
18
21
|
|
|
19
22
|
try:
|
|
20
23
|
from ._version import version as __version__
|
|
@@ -23,4 +26,5 @@ except ImportError: # not installed
|
|
|
23
26
|
|
|
24
27
|
__all__ = ['ModelSpec', 'Feature', 'Column', 'Contrast', 'Ordinal',
|
|
25
28
|
'poly', 'ns', 'bs', 'derived_feature', 'pca', 'contrast',
|
|
26
|
-
'ordinal', 'build_columns'
|
|
29
|
+
'ordinal', 'build_columns', 'wildcard', 'Wildcard',
|
|
30
|
+
'Response', 'Surv', 'Binomial', 'Multinomial']
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
# a categorical is an Enum, if the backend has one, of its
|
|
16
|
+
# categories as strings
|
|
17
|
+
values, dtype = S.to_numpy(), None
|
|
18
|
+
if isinstance(S.dtype, pd.CategoricalDtype):
|
|
19
|
+
values = np.asarray([None if pd.isna(v) else str(v) for v in S], dtype=object)
|
|
20
|
+
dtype = nw.Enum([str(c) for c in S.cat.categories])
|
|
21
|
+
try:
|
|
22
|
+
return nw.new_series(str(S.name), values, dtype=dtype, backend=backend).to_native()
|
|
23
|
+
except NotImplementedError: # e.g. no Enum in pyarrow
|
|
24
|
+
return nw.new_series(str(S.name), values, backend=backend).to_native()
|
|
25
|
+
|
|
26
|
+
def _from_native(X, columns=None):
|
|
27
|
+
"""
|
|
28
|
+
Convert a dataframe (or series) of any eager backend supported by
|
|
29
|
+
narwhals, e.g. polars or pyarrow, to pandas. Returns the converted
|
|
30
|
+
`X` and a function converting a `pd.DataFrame` model matrix back to
|
|
31
|
+
that backend, or None if `X` was not converted.
|
|
32
|
+
|
|
33
|
+
If `columns` is given, only those columns of a dataframe are
|
|
34
|
+
converted (the copy is of the columns a model matrix uses).
|
|
35
|
+
|
|
36
|
+
Conversion through pandas keeps categoricals, including the order
|
|
37
|
+
of a polars Enum or an ordered pyarrow dictionary (as an ordered
|
|
38
|
+
`pd.Categorical`). The levels of an unordered categorical, e.g. a
|
|
39
|
+
polars Categorical, are stored in order of appearance; they are
|
|
40
|
+
sorted, as pandas does when it infers categories, so that the
|
|
41
|
+
model matrix does not depend on the order of the rows.
|
|
42
|
+
"""
|
|
43
|
+
if (isinstance(X, (pd.DataFrame, pd.Series, np.ndarray)) or
|
|
44
|
+
sp_sparse.issparse(X)):
|
|
45
|
+
return X, None
|
|
46
|
+
try:
|
|
47
|
+
X_nw = nw.from_native(X, allow_series=True)
|
|
48
|
+
except TypeError: # not a dataframe, e.g. a list: left to _check_X
|
|
49
|
+
return X, None
|
|
50
|
+
if isinstance(X_nw, nw.LazyFrame):
|
|
51
|
+
raise TypeError('Lazy frames are not supported by ModelSpec; '
|
|
52
|
+
'collect them first, e.g. X.collect()')
|
|
53
|
+
backend = nw.get_native_namespace(X_nw)
|
|
54
|
+
if columns is not None and isinstance(X_nw, nw.DataFrame):
|
|
55
|
+
keep = [c for c in X_nw.columns if c in columns]
|
|
56
|
+
X_nw = X_nw.select(keep or X_nw.columns[:1])
|
|
57
|
+
|
|
58
|
+
def to_native(D):
|
|
59
|
+
if isinstance(D, pd.Series): # e.g. a response
|
|
60
|
+
return _series_to_native(D, backend)
|
|
61
|
+
duplicated = list(D.columns[D.columns.duplicated()])
|
|
62
|
+
if duplicated:
|
|
63
|
+
raise ValueError(f'the model matrix has duplicated column names {duplicated}, '
|
|
64
|
+
'which this dataframe backend cannot hold')
|
|
65
|
+
return nw.from_dict({str(c): D[c].to_numpy() for c in D.columns},
|
|
66
|
+
backend=backend).to_native()
|
|
67
|
+
|
|
68
|
+
X = X_nw.to_pandas()
|
|
69
|
+
if isinstance(X_nw, nw.Series):
|
|
70
|
+
dtypes = {X.name: X_nw.dtype}
|
|
71
|
+
else:
|
|
72
|
+
dtypes = dict(X_nw.schema)
|
|
73
|
+
for name, dtype in dtypes.items():
|
|
74
|
+
col = X if isinstance(X, pd.Series) else X[name]
|
|
75
|
+
if not isinstance(col.dtype, pd.CategoricalDtype):
|
|
76
|
+
continue
|
|
77
|
+
# some versions of polars convert an Enum to an unordered
|
|
78
|
+
# pd.Categorical: its categories are in the declared order
|
|
79
|
+
if dtype == nw.Enum:
|
|
80
|
+
col = col.cat.as_ordered()
|
|
81
|
+
elif not col.dtype.ordered:
|
|
82
|
+
col = col.cat.reorder_categories(sorted(col.dtype.categories))
|
|
83
|
+
else:
|
|
84
|
+
continue
|
|
85
|
+
if isinstance(X, pd.Series):
|
|
86
|
+
X = col
|
|
87
|
+
else:
|
|
88
|
+
X[name] = col
|
|
89
|
+
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g9dbe6c957'
|