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.
Files changed (50) hide show
  1. modelmatrix-0.2.0/CHANGELOG.md +69 -0
  2. {modelmatrix-0.1.0/modelmatrix.egg-info → modelmatrix-0.2.0}/PKG-INFO +1 -1
  3. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/index.rst +1 -0
  4. modelmatrix-0.2.0/docs/api/responses.rst +11 -0
  5. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/terms.rst +7 -0
  6. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/index.md +1 -0
  7. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/myst.yml +1 -0
  8. modelmatrix-0.2.0/docs/responses.md +188 -0
  9. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/__init__.py +6 -2
  10. modelmatrix-0.2.0/modelmatrix/_native.py +89 -0
  11. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/_version.py +3 -3
  12. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/model_spec.py +222 -84
  13. modelmatrix-0.2.0/modelmatrix/response.py +397 -0
  14. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/transforms.py +27 -8
  15. {modelmatrix-0.1.0 → modelmatrix-0.2.0/modelmatrix.egg-info}/PKG-INFO +1 -1
  16. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/SOURCES.txt +6 -0
  17. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/scm_file_list.json +7 -1
  18. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/scm_version.json +2 -2
  19. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_model_matrix.py +17 -0
  20. modelmatrix-0.2.0/tests/test_response.py +196 -0
  21. modelmatrix-0.2.0/tests/test_wildcard.py +186 -0
  22. modelmatrix-0.1.0/CHANGELOG.md +0 -29
  23. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/.github/workflows/build_docs.yml +0 -0
  24. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/.github/workflows/build_test.yml +0 -0
  25. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/.gitignore +0 -0
  26. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/LICENSE +0 -0
  27. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/README.md +0 -0
  28. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/.gitignore +0 -0
  29. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/PCA.md +0 -0
  30. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/anova.md +0 -0
  31. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/ModelSpec.rst +0 -0
  32. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/conf.py +0 -0
  33. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/api/transforms.rst +0 -0
  34. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/comparison.md +0 -0
  35. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/poly.md +0 -0
  36. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/spec.md +0 -0
  37. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/docs/splines.md +0 -0
  38. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix/columns.py +0 -0
  39. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/dependency_links.txt +0 -0
  40. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/requires.txt +0 -0
  41. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/modelmatrix.egg-info/top_level.txt +0 -0
  42. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/pyproject.toml +0 -0
  43. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/setup.cfg +0 -0
  44. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/_datasets.py +0 -0
  45. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/data/Carseats.csv +0 -0
  46. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/data/Wage.csv +0 -0
  47. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_boolean_columns.py +0 -0
  48. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_columns.py +0 -0
  49. {modelmatrix-0.1.0 → modelmatrix-0.2.0}/tests/test_model_spec.py +0 -0
  50. {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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelmatrix
3
- Version: 0.1.0
3
+ Version: 0.2.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,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/)
@@ -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,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.1.0'
22
- __version_tuple__ = version_tuple = (0, 1, 0)
21
+ __version__ = version = '0.2.0'
22
+ __version_tuple__ = version_tuple = (0, 2, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g2f08b48f3'
24
+ __commit_id__ = commit_id = 'g9dbe6c957'