model-diagnostics 1.4.2__tar.gz → 1.4.3__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 (43) hide show
  1. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/PKG-INFO +9 -8
  2. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/mkdocs.yml +2 -0
  3. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/pyproject.toml +31 -34
  4. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/__about__.py +1 -1
  5. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/binning.py +36 -31
  6. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/partial_dependence.py +1 -1
  7. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/tests/test_isotonic.py +1 -1
  8. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/tests/test_partial_dependence.py +6 -12
  9. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/identification.py +6 -3
  10. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/plots.py +2 -0
  11. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/tests/test_identification.py +8 -2
  12. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/tests/test_plots.py +11 -4
  13. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/plots.py +1 -0
  14. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/scoring.py +29 -27
  15. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/.codecov.yml +0 -0
  16. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/.gitignore +0 -0
  17. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/LICENSE +0 -0
  18. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/README.md +0 -0
  19. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/Untitled.ipynb +0 -0
  20. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/dev_tools/print_package_versions.py +0 -0
  21. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/docs/development.md +0 -0
  22. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/docs/examples/classification.ipynb +0 -0
  23. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/docs/examples/quantile_regression.ipynb +0 -0
  24. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/docs/examples/regression_on_workers_compensation.ipynb +0 -0
  25. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/docs/gen_ref_pages.py +0 -0
  26. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/docs/index.md +0 -0
  27. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/docs/javascripts/mathjax.js +0 -0
  28. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/__init__.py +0 -0
  29. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_config.py +0 -0
  30. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/__init__.py +0 -0
  31. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/array.py +0 -0
  32. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/isotonic.py +0 -0
  33. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/plot_helper.py +0 -0
  34. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/test_helper.py +0 -0
  35. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/tests/test_array.py +0 -0
  36. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/tests/test_binning.py +0 -0
  37. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/__init__.py +0 -0
  38. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/tests/__init__.py +0 -0
  39. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/__init__.py +0 -0
  40. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/tests/__init__.py +0 -0
  41. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/tests/test_plots.py +0 -0
  42. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/tests/test_scoring.py +0 -0
  43. {model_diagnostics-1.4.2 → model_diagnostics-1.4.3}/src/model_diagnostics/tests/test_config.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: model-diagnostics
3
- Version: 1.4.2
3
+ Version: 1.4.3
4
4
  Summary: Tools for diagnostics and assessment of (machine learning) models
5
5
  Project-URL: Homepage, https://lorentzenchr.github.io/model-diagnostics/
6
6
  Project-URL: Source, https://github.com/lorentzenchr/model-diagnostics
@@ -27,6 +27,7 @@ License: MIT License
27
27
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
28
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
29
  SOFTWARE.
30
+ License-File: LICENSE
30
31
  Keywords: calibration,machine learning,model diagnostics
31
32
  Classifier: Development Status :: 5 - Production/Stable
32
33
  Classifier: Framework :: Hatch
@@ -34,13 +35,13 @@ Classifier: License :: OSI Approved :: MIT License
34
35
  Classifier: Operating System :: OS Independent
35
36
  Classifier: Programming Language :: Python :: 3
36
37
  Classifier: Topic :: Scientific/Engineering
37
- Requires-Python: >=3.9
38
- Requires-Dist: matplotlib>=3.6.1
39
- Requires-Dist: numpy>=1.22
38
+ Requires-Python: >=3.11
39
+ Requires-Dist: matplotlib>=3.8.4
40
+ Requires-Dist: numpy>=2.0.0
40
41
  Requires-Dist: packaging>=23.1
41
- Requires-Dist: polars>=1.0.0
42
- Requires-Dist: scikit-learn>=1.2.0
43
- Requires-Dist: scipy>=1.10
42
+ Requires-Dist: polars>=1.1.0
43
+ Requires-Dist: scikit-learn>=1.4.2
44
+ Requires-Dist: scipy>=1.13.1
44
45
  Description-Content-Type: text/markdown
45
46
 
46
47
  # model-diagnostics
@@ -53,6 +53,8 @@ nav:
53
53
 
54
54
  watch:
55
55
  - src/model_diagnostics/
56
+ - docs/development.md
57
+ - docs/index.md
56
58
 
57
59
  plugins:
58
60
  # Built-in
@@ -10,7 +10,7 @@ authors = [
10
10
  description = "Tools for diagnostics and assessment of (machine learning) models"
11
11
  readme = "README.md"
12
12
  license = { file="LICENSE" }
13
- requires-python = ">=3.9"
13
+ requires-python = ">=3.11"
14
14
  classifiers = [
15
15
  "Development Status :: 5 - Production/Stable",
16
16
  "Framework :: Hatch",
@@ -21,12 +21,12 @@ classifiers = [
21
21
  ]
22
22
  keywords = ["machine learning", "model diagnostics", "calibration"]
23
23
  dependencies = [
24
- "matplotlib>=3.6.1",
25
- "numpy>=1.22", # type annotations and method arg in quantile
24
+ "matplotlib>=3.8.4", # numpy 2 support
25
+ "numpy>=2.0.0",
26
26
  "packaging>=23.1",
27
- "polars>=1.0.0",
28
- "scikit-learn>=1.2.0", # We rely on the set_output API.
29
- "scipy>=1.10", # scipy.stats.expectile
27
+ "polars>=1.1.0", # numpy 2 support
28
+ "scikit-learn>=1.4.2", # numpy 2 support
29
+ "scipy>=1.13.1", # numpy 2 support
30
30
  ]
31
31
  dynamic = ["version"]
32
32
 
@@ -122,7 +122,6 @@ lint.isort.known-first-party = ["model_diagnostics"]
122
122
  # disallow_untyped_defs = false
123
123
  # follow_imports = "normal"
124
124
  ignore_missing_imports = true
125
- plugins = "numpy.typing.mypy_plugin"
126
125
  pretty = true
127
126
  # show_column_numbers = true
128
127
  # warn_no_return = false
@@ -150,9 +149,10 @@ exclude = [
150
149
  ]
151
150
 
152
151
  [tool.hatch.envs.default]
152
+ installer = "uv"
153
153
  dependencies = [
154
- "pandas>=1.5",
155
- "pyarrow>=11.0.0",
154
+ "pandas>=2.2.2", # numpy 2 support
155
+ "pyarrow>=16.0.0", # numpy 2 support
156
156
  "pytest",
157
157
  "pytest-cov",
158
158
  "pytest-xdist",
@@ -172,9 +172,7 @@ dependencies = [
172
172
  "mkdocs-literate-nav>=0.6.1",
173
173
  "mkdocs-section-index>=0.3.9",
174
174
  "mkdocs-jupyter==0.24.7", # https://github.com/danielfrg/mkdocs-jupyter/issues/212
175
- ]
176
- extra-dependencies = [
177
- "scikit-learn>=1.3.0", # for Gamma deviance in HistGradientBoostingRegressor
175
+ "click<=8.2.1", # https://github.com/mkdocs/mkdocs/issues/4032
178
176
  ]
179
177
 
180
178
  [tool.hatch.envs.docs.scripts]
@@ -185,7 +183,6 @@ ci-build = "mkdocs gh-deploy --force {args}"
185
183
  [tool.hatch.envs.jupyter]
186
184
  extra-dependencies = [
187
185
  "jupyterlab>=4.0.4",
188
- "scikit-learn>=1.3.0",
189
186
  "plotly>=5.11.0",
190
187
  ]
191
188
 
@@ -195,7 +192,7 @@ lab = "jupyter lab"
195
192
  [tool.hatch.envs.format]
196
193
  detached = true
197
194
  dependencies = [
198
- "numpy>=1.22",
195
+ "numpy>=2.3",
199
196
  "mypy>=1.14",
200
197
  "ruff==0.4.4",
201
198
  ]
@@ -241,36 +238,36 @@ dependencies = [
241
238
  ]
242
239
 
243
240
  [[tool.hatch.envs.test.matrix]]
244
- python = ["3.9", "3.10", "3.11", "3.12"]
241
+ python = ["3.11", "3.12", "3.13", "3.14"]
245
242
 
246
243
  [tool.hatch.envs.test.overrides]
247
- # Minimal versions (partially inherited from scikit-learn 1.2.0)
244
+ # Minimal versions
248
245
  # but including pandas and pyarrow
249
- name."py3.9".set-extra-dependencies = [
250
- "numpy==1.22",
251
- "polars==1.0.0",
252
- "scipy==1.10",
253
- "pandas==1.5.*",
254
- "pyarrow==11.0.*"
246
+ name."py3.11".set-extra-dependencies = [
247
+ "numpy==2.0.0",
248
+ "polars==1.1.0",
249
+ "scipy==1.13.1",
250
+ "pandas==2.2.2", # numpy 2 support
251
+ "pyarrow==16.0.*" # numpy 2 support
255
252
  ]
256
253
  # Minimal versions without pandas and pyarrow
257
- name."py3.10".set-extra-dependencies = [
258
- "numpy==1.22",
259
- "polars==1.0.0",
260
- "scipy==1.10",
254
+ name."py3.12".set-extra-dependencies = [
255
+ "numpy==2.0.0",
256
+ "polars==1.1.0",
257
+ "scipy==1.13.1",
261
258
  ]
262
259
  # Some intermediate versions, with pandads but without pyarrow
263
- name."py3.11".set-extra-dependencies = [
264
- "numpy==1.26.*",
265
- "polars==1.0.*",
266
- "scipy==1.12.*",
267
- "pandas==2.0.*",
260
+ name."py3.13".set-extra-dependencies = [
261
+ "numpy==2.1.*",
262
+ "polars==1.10.*",
263
+ "scipy==1.14.*",
264
+ "pandas==2.3.*",
268
265
  "plotly",
269
266
  ]
270
267
  # Latest versions, with plotly
271
- name."py3.12".set-extra-dependencies = [
272
- "numpy>=2.2.0",
273
- "polars>=1.21.0",
268
+ name."py3.14".set-extra-dependencies = [
269
+ "numpy>=2.3.0",
270
+ "polars>=1.31.0",
274
271
  "scipy",
275
272
  "pandas",
276
273
  "plotly",
@@ -14,4 +14,4 @@
14
14
  # Dev branch marker is: 'X.Y.dev' or 'X.Y.devN' where N is an integer.
15
15
  # 'X.Y.dev0' is the canonical version of 'X.Y.dev'
16
16
  #
17
- __version__ = "1.4.2"
17
+ __version__ = "1.4.3"
@@ -31,7 +31,7 @@ def bin_feature(
31
31
  n_obs: int,
32
32
  n_bins: int = 10,
33
33
  bin_method: str = "sturges",
34
- ):
34
+ ) -> tuple[pl.Series, int, pl.DataFrame]:
35
35
  """Helper function to bin features of different dtypes.
36
36
 
37
37
  Best call this function inside a `with pl.StringCache()` context manager.
@@ -119,7 +119,7 @@ def bin_feature(
119
119
  # The following statement, i.e. possibly the creation of a pl.Categorical,
120
120
  # MUST be under the StringCache context manager!
121
121
  try:
122
- feature = pl.Series(name=feature_name, values=feature)
122
+ feature_out = pl.Series(name=feature_name, values=feature)
123
123
  except ImportError:
124
124
  # FIXME: pyarrow not installed
125
125
  # For non numpy-backed columns, pyarrow is needed. Here we handle the case
@@ -131,38 +131,38 @@ def bin_feature(
131
131
  feature.dtype, # type: ignore
132
132
  pandas.CategoricalDtype,
133
133
  )
134
- feature = pl.from_dataframe(
134
+ feature_out = pl.from_dataframe(
135
135
  feature.to_frame(name=feature_name) # type: ignore
136
136
  )[:, 0]
137
- if is_pandas_categorical and isinstance(feature.dtype, pl.Enum):
137
+ if is_pandas_categorical and isinstance(feature_out.dtype, pl.Enum):
138
138
  # Pandas categoricals usually get mapped to polars categoricals.
139
139
  # But this code path gives pl.Enum.
140
- feature = feature.cast(pl.Categorical)
140
+ feature_out = feature_out.cast(pl.Categorical)
141
141
  else:
142
142
  raise # re-raises the ImportError
143
- if length_of_first_dimension(feature) != n_obs:
143
+ if length_of_first_dimension(feature_out) != n_obs:
144
144
  msg = (
145
145
  f"The feature array {feature_name} does not have length {n_obs} of its"
146
146
  " first dimension."
147
147
  )
148
148
  raise ValueError(msg)
149
- if feature.dtype == pl.Categorical:
149
+ if feature_out.dtype == pl.Categorical:
150
150
  is_categorical = True
151
- elif feature.dtype == pl.Enum:
151
+ elif feature_out.dtype == pl.Enum:
152
152
  is_enum = True
153
- elif feature.dtype in [pl.Utf8, pl.Object]:
153
+ elif feature_out.dtype in [pl.Utf8, pl.Object]:
154
154
  # We could convert strings to categoricals.
155
155
  is_string = True
156
- elif feature.dtype.is_float():
156
+ elif feature_out.dtype.is_float():
157
157
  # We treat NaN as Null values, numpy will see a Null as a NaN.
158
- feature = feature.fill_nan(None)
158
+ feature_out = feature_out.fill_nan(None)
159
159
  else:
160
160
  # integers
161
161
  pass
162
162
 
163
163
  # If we have Null values, we should reserve one bin for it and reduce
164
164
  # the effective number of bins by 1.
165
- n_bins_ef = max(1, n_bins - feature.has_nulls())
165
+ n_bins_ef = max(1, n_bins - feature_out.has_nulls())
166
166
 
167
167
  if is_categorical or is_enum or is_string:
168
168
  # For categorical and string features, knowing the frequency table in
@@ -181,7 +181,7 @@ def bin_feature(
181
181
  # value_counts(sort=True) sorts ties by first occurence, we want
182
182
  # alphanumerical sorting order.
183
183
  value_counts = (
184
- feature.drop_nulls()
184
+ feature_out.drop_nulls()
185
185
  .value_counts()
186
186
  .sort(by=["count", feature_name], descending=[True, False])
187
187
  )
@@ -189,18 +189,21 @@ def bin_feature(
189
189
  if n_bins_ef >= value_counts.shape[0]:
190
190
  # This also covers the case of only null values.
191
191
  n_bins_ef = value_counts.shape[0]
192
- f_binned = pl.DataFrame({"bin": feature})
192
+ f_binned = pl.DataFrame({"bin": feature_out})
193
193
  else:
194
194
  # We keep the n_bins_ef - 1 most frequent values. Ties are resolved by
195
195
  # taking the first one of the sorted values (most often alpha-numerical).
196
- if feature.has_nulls():
196
+ if feature_out.has_nulls():
197
197
  # To ease adding the null value, we take one value more.
198
198
  keep_values = value_counts[feature_name].head(n_bins_ef)
199
- if is_categorical:
200
- # FIXME: Workaround for https://github.com/pola-rs/polars/issues/21175
199
+ if is_categorical or is_enum:
200
+ # FIXME: categorical: Workaround for https://github.com/pola-rs/polars/issues/21175
201
+ # FIXME: enum: Workaround for https://github.com/pola-rs/polars/pull/25245
202
+ # to be released in polars 1.35.3 or 1.36.
203
+ dtype = keep_values.dtype
201
204
  keep_values = keep_values.cast(pl.String)
202
205
  keep_values[-1] = None
203
- keep_values = keep_values.cast(pl.Categorical)
206
+ keep_values = keep_values.cast(dtype)
204
207
  else:
205
208
  keep_values[-1] = None
206
209
  else:
@@ -209,14 +212,16 @@ def bin_feature(
209
212
  # n = n_remaining.
210
213
  n_remaining = value_counts.shape[0] - (n_bins_ef - 1)
211
214
  remaining_name = "other " + _format_integer(n_remaining)
212
- while remaining_name in keep_values:
215
+ while remaining_name in keep_values.cast(pl.String):
213
216
  remaining_name = "_" + remaining_name
214
- return_dtype = feature.dtype
217
+ return_dtype = feature_out.dtype
215
218
  if is_enum:
216
219
  return_dtype = pl.Enum(
217
- pl.concat([feature.dtype.categories, pl.Series([remaining_name])])
220
+ pl.concat(
221
+ [feature_out.dtype.categories, pl.Series([remaining_name])]
222
+ )
218
223
  )
219
- f_binned = feature.replace_strict(
224
+ f_binned = feature_out.replace_strict(
220
225
  old=keep_values,
221
226
  new=keep_values,
222
227
  default=remaining_name,
@@ -226,13 +231,13 @@ def bin_feature(
226
231
  else:
227
232
  # Binning a numerical feature
228
233
  # We will need min and max anyway.
229
- feature_min, feature_max = feature.min(), feature.max()
234
+ feature_min, feature_max = feature_out.min(), feature_out.max()
230
235
  if feature_min == -np.inf:
231
- finite_min = feature.filter(feature > -np.inf).min()
236
+ finite_min = feature_out.filter(feature_out > -np.inf).min()
232
237
  else:
233
238
  finite_min = feature_min
234
239
  if feature_max == np.inf:
235
- finite_max = feature.filter(feature < np.inf).max()
240
+ finite_max = feature_out.filter(feature_out < np.inf).max()
236
241
  else:
237
242
  finite_max = feature_max
238
243
  f_range = finite_max - finite_min
@@ -241,7 +246,7 @@ def bin_feature(
241
246
  # We use method="inverted_cdf" instead of the default "linear" because
242
247
  # "linear" produces as many unique values as before.
243
248
  q = np.nanquantile(
244
- feature,
249
+ feature_out,
245
250
  # Improved rounding errors by using integers and dividing at the
246
251
  # end as opposed to np.linspace with 1/n_bins step size.
247
252
  q=np.arange(1, n_bins_ef) / n_bins_ef,
@@ -252,11 +257,11 @@ def bin_feature(
252
257
  bin_edges = finite_min + f_range * np.arange(1, n_bins_ef) / n_bins_ef
253
258
  else:
254
259
  # numpy histogram bin methods
255
- a = feature.filter(feature.is_finite() & feature.is_not_null())
260
+ a = feature_out.filter(feature_out.is_finite() & feature_out.is_not_null())
256
261
  bin_edges = np.histogram_bin_edges(a, bins=bin_method)[1:-1]
257
262
  n_bins_ef = bin_edges.shape[0] + 1
258
263
  # We want: bins[i-1] < x <= bins[i]
259
- f_binned = np.digitize(feature, bins=bin_edges, right=True)
264
+ f_binned = np.digitize(feature_out, bins=bin_edges, right=True)
260
265
  # The full bin edges also include min and max of the feature.
261
266
  if bin_edges.size == 0:
262
267
  bin_edges = np.r_[feature_min, feature_max]
@@ -272,8 +277,8 @@ def bin_feature(
272
277
  f_binned = (
273
278
  pl.LazyFrame(
274
279
  [
275
- feature,
276
- pl.Series("__f_binned", f_binned, dtype=feature.dtype),
280
+ feature_out,
281
+ pl.Series("__f_binned", f_binned, dtype=feature_out.dtype),
277
282
  pl.Series(
278
283
  "__bin_edges",
279
284
  bin_edges[f_binned],
@@ -293,4 +298,4 @@ def bin_feature(
293
298
  )
294
299
  .collect()
295
300
  )
296
- return feature, n_bins_ef + feature.has_nulls(), f_binned
301
+ return feature_out, n_bins_ef + feature_out.has_nulls(), f_binned
@@ -93,7 +93,7 @@ def compute_partial_dependence(
93
93
  pd_values = np.average(
94
94
  y_pred.reshape(n_grid, y_pred.shape[0] // n_grid),
95
95
  axis=1,
96
- weights=weights,
96
+ weights=weights, # type: ignore
97
97
  )
98
98
 
99
99
  return pd_values
@@ -236,7 +236,7 @@ def test_gpava_simple(y, fun_lower, x_lower, fun_upper, x_upper):
236
236
  tol=1e-12,
237
237
  )
238
238
 
239
- assert result.fun == pytest.approx(loss_lower, rel=1e-8)
239
+ assert result.fun == pytest.approx(loss_lower, rel=1e-7)
240
240
 
241
241
  # Check that it is the boundary of solutions.
242
242
  eps = 1e-8 * np.mean(np.abs(xl))
@@ -6,7 +6,7 @@ import polars as pl
6
6
  import pytest
7
7
  from numpy.testing import assert_allclose, assert_equal
8
8
  from polars.testing import assert_frame_equal
9
- from sklearn.base import RegressorMixin
9
+ from sklearn.base import BaseEstimator, RegressorMixin
10
10
  from sklearn.inspection import partial_dependence
11
11
 
12
12
  from model_diagnostics._utils.array import (
@@ -82,21 +82,15 @@ def test_compute_partial_dependence(n_max, weights, feature_type, data_container
82
82
  "b": X["b"].to_numpy(),
83
83
  }
84
84
  )
85
- elif feature_type == "string":
86
- # TODO: With pandas 2.0.3, this does not work for strings. Get
87
- # AssertionError from polars in string_column_to_ndarray.
88
- # X_skl = pandas.api.interchange.from_dataframe(X). So we do it manually.
89
- X_skl = pandas.DataFrame(
90
- {
91
- "a": X["a"].to_numpy(),
92
- "b": X["b"].to_numpy(),
93
- }
94
- )
95
85
  else:
96
86
  X_skl = pandas.api.interchange.from_dataframe(X)
97
87
  else:
98
88
  X_skl = X.to_pandas()
99
89
 
90
+ if feature_type == "numeric" and pandas is not None:
91
+ # scikit-learn does not like integers in partial dependence
92
+ X_skl["a"] = 1.0 * X_skl["a"]
93
+
100
94
  grid = X.get_column("a").unique().sort()
101
95
  # Make sure grid is the same data container as X.
102
96
  if data_container == "list":
@@ -143,7 +137,7 @@ def test_compute_partial_dependence(n_max, weights, feature_type, data_container
143
137
  rng=rng,
144
138
  )
145
139
 
146
- class ModelWrapPredict(RegressorMixin):
140
+ class ModelWrapPredict(RegressorMixin, BaseEstimator):
147
141
  def fit(self, X):
148
142
  self.is_fitted_ = True
149
143
  return self
@@ -60,6 +60,7 @@ def identification_function(
60
60
  Notes
61
61
  -----
62
62
  [](){#notes}
63
+
63
64
  The function \(V(y, z)\) for observation \(y=y_{pred}\) and prediction
64
65
  \(z=y_{pred}\) is a strict identification function for the functional \(T\), or
65
66
  induces the functional \(T\) as:
@@ -235,6 +236,7 @@ def compute_bias(
235
236
  Notes
236
237
  -----
237
238
  [](){#notes}
239
+
238
240
  A model \(m(X)\) is conditionally calibrated iff
239
241
  \(\mathbb{E}(V(m(X), Y)|X)=0\) almost surely with canonical identification
240
242
  function \(V\).
@@ -311,20 +313,21 @@ def compute_bias(
311
313
  with pl.StringCache():
312
314
  feature_name = None
313
315
  if feature is not None:
314
- feature, n_bins, f_binned = bin_feature(
316
+ feature_series, n_bins, f_binned = bin_feature(
315
317
  feature=feature,
316
318
  feature_name=None,
317
319
  n_obs=n_obs,
318
320
  n_bins=n_bins,
319
321
  bin_method=bin_method,
320
322
  )
321
- feature_name = feature.name
322
- is_cat_or_string = feature.dtype in [
323
+ feature_name = feature_series.name
324
+ is_cat_or_string = feature_series.dtype in [
323
325
  pl.Categorical,
324
326
  pl.Enum,
325
327
  pl.Utf8,
326
328
  pl.Object,
327
329
  ]
330
+ feature = feature_series
328
331
 
329
332
  for i in range(len(pred_names)):
330
333
  # Loop over columns of y_pred.
@@ -93,6 +93,7 @@ def plot_reliability_diagram(
93
93
  Notes
94
94
  -----
95
95
  [](){#notes}
96
+
96
97
  The expectation conditional on the predictions is \(E(Y|y_{pred})\). This object is
97
98
  estimated by the pool-adjacent violator (PAV) algorithm, which has very desirable
98
99
  properties:
@@ -425,6 +426,7 @@ def plot_bias(
425
426
  Notes
426
427
  -----
427
428
  [](){#notes}
429
+
428
430
  A model \(m(X)\) is conditionally calibrated iff \(E(V(m(X), Y))=0\) a.s. The
429
431
  empirical version, given some data, reads \(\frac{1}{n}\sum_i V(m(x_i), y_i)\).
430
432
  See `[FLM2022]`.
@@ -296,7 +296,10 @@ def test_compute_bias_n_bins_string_like_feature(feature_type):
296
296
  if feature_type == "cat":
297
297
  dtype = pl.Categorical
298
298
  elif feature_type == "cat_physical":
299
- dtype = pl.Categorical(ordering="physical")
299
+ if Version(pl.__version__) < Version("1.32.0"):
300
+ dtype = pl.Categorical(ordering="lexical")
301
+ else:
302
+ dtype = pl.Categorical()
300
303
  elif feature_type == "enum":
301
304
  dtype = pl.Enum(categories=["b", "a", "c"])
302
305
  else:
@@ -824,7 +827,10 @@ def test_compute_marginal_n_bins_string_like_feature(feature_type):
824
827
  if feature_type == "cat":
825
828
  dtype = pl.Categorical
826
829
  elif feature_type == "cat_physical":
827
- dtype = pl.Categorical(ordering="physical")
830
+ if Version(pl.__version__) < Version("1.32.0"):
831
+ dtype = pl.Categorical(ordering="lexical")
832
+ else:
833
+ dtype = pl.Categorical()
828
834
  elif feature_type == "enum":
829
835
  dtype = pl.Enum(categories=["b", "a", "c"])
830
836
  else:
@@ -5,6 +5,7 @@ import matplotlib.pyplot as plt
5
5
  import numpy as np
6
6
  import polars as pl
7
7
  import pytest
8
+ from packaging.version import Version
8
9
  from sklearn.base import BaseEstimator, TransformerMixin
9
10
  from sklearn.compose import ColumnTransformer
10
11
  from sklearn.datasets import make_classification
@@ -305,7 +306,7 @@ def test_plot_bias(
305
306
  random_state=42,
306
307
  )
307
308
  X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
308
- clf = LogisticRegression(solver="lbfgs")
309
+ clf = LogisticRegression(solver="newton-cg")
309
310
  clf.fit(X_train, y_train)
310
311
  feature = X_test[:, 0]
311
312
  if feature_type != "num":
@@ -319,7 +320,10 @@ def test_plot_bias(
319
320
  elif feature_type == "cat":
320
321
  dtype = pl.Categorical
321
322
  elif feature_type == "cat_physical":
322
- dtype = pl.Categorical(ordering="physical")
323
+ if Version(pl.__version__) < Version("1.32.0"):
324
+ dtype = pl.Categorical(ordering="lexical")
325
+ else:
326
+ dtype = pl.Categorical()
323
327
  elif feature_type == "enum":
324
328
  dtype = pl.Enum(categories=np.unique(feature))
325
329
  else:
@@ -527,7 +531,10 @@ def test_plot_marginal(with_null_values, feature_type, bin_method, ax, plot_back
527
531
  elif feature_type == "cat":
528
532
  dtype = pl.Categorical
529
533
  elif feature_type == "cat_physical":
530
- dtype = pl.Categorical(ordering="physical")
534
+ if Version(pl.__version__) < Version("1.32.0"):
535
+ dtype = pl.Categorical(ordering="lexical")
536
+ else:
537
+ dtype = pl.Categorical()
531
538
  elif feature_type == "enum":
532
539
  dtype = pl.Enum(categories=np.unique(feature))
533
540
  else:
@@ -576,7 +583,7 @@ def test_plot_marginal(with_null_values, feature_type, bin_method, ax, plot_back
576
583
 
577
584
  clf = make_pipeline(
578
585
  ColumnTransformer([("ohe", safe_ohe, [0])], remainder="passthrough"),
579
- LogisticRegression(solver="lbfgs"),
586
+ LogisticRegression(solver="newton-cg"),
580
587
  )
581
588
  clf.fit(X, y)
582
589
 
@@ -75,6 +75,7 @@ def plot_murphy_diagram(
75
75
  Notes
76
76
  -----
77
77
  [](){#notes}
78
+
78
79
  For details, refer to `[Ehm2015]`.
79
80
 
80
81
  References
@@ -60,7 +60,10 @@ class _BaseScoringFunction(ABC):
60
60
  score : float
61
61
  The average score.
62
62
  """
63
- return np.average(self.score_per_obs(y_obs, y_pred), weights=weights)
63
+ return np.average(
64
+ self.score_per_obs(y_obs, y_pred),
65
+ weights=weights, # type: ignore
66
+ )
64
67
 
65
68
  @abstractmethod
66
69
  def score_per_obs(
@@ -142,9 +145,9 @@ class HomogeneousExpectileScore(_BaseScoringFunction):
142
145
  Examples
143
146
  --------
144
147
  >>> hes = HomogeneousExpectileScore(degree=2, level=0.1)
145
- >>> hes(y_obs=[0, 0, 1, 1], y_pred=[-1, 1, 1 , 2]) # doctest: +SKIP
146
- 0.95
147
- """ # FIXME: numpy 2.0.0, doctest skip should not be necessary.
148
+ >>> hes(y_obs=[0, 0, 1, 1], y_pred=[-1, 1, 1 , 2])
149
+ np.float64(0.95)
150
+ """
148
151
 
149
152
  def __init__(self, degree: float = 2, level: float = 0.5) -> None:
150
153
  self.degree = degree
@@ -268,9 +271,9 @@ class SquaredError(HomogeneousExpectileScore):
268
271
  Examples
269
272
  --------
270
273
  >>> se = SquaredError()
271
- >>> se(y_obs=[0, 0, 1, 1], y_pred=[-1, 1, 1 , 2]) # doctest: +SKIP
272
- 0.75
273
- """ # FIXME: numpy 2.0.0, doctest skip should not be necessary.
274
+ >>> se(y_obs=[0, 0, 1, 1], y_pred=[-1, 1, 1 , 2])
275
+ np.float64(0.75)
276
+ """
274
277
 
275
278
  def __init__(self) -> None:
276
279
  super().__init__(degree=2, level=0.5)
@@ -296,9 +299,9 @@ class PoissonDeviance(HomogeneousExpectileScore):
296
299
  Examples
297
300
  --------
298
301
  >>> pd = PoissonDeviance()
299
- >>> pd(y_obs=[0, 0, 1, 1], y_pred=[2, 1, 1 , 2]) # doctest: +SKIP
300
- 1.6534264097200273
301
- """ # FIXME: numpy 2.0.0, doctest skip should not be necessary.
302
+ >>> pd(y_obs=[0, 0, 1, 1], y_pred=[2, 1, 1 , 2])
303
+ np.float64(1.6534264097200273)
304
+ """
302
305
 
303
306
  def __init__(self) -> None:
304
307
  super().__init__(degree=1, level=0.5)
@@ -325,9 +328,9 @@ class GammaDeviance(HomogeneousExpectileScore):
325
328
  Examples
326
329
  --------
327
330
  >>> gd = GammaDeviance()
328
- >>> gd(y_obs=[3, 2, 1, 1], y_pred=[2, 1, 1 , 2]) # doctest: +SKIP
329
- 0.2972674459459178
330
- """ # FIXME: numpy 2.0.0, doctest skip should not be necessary.
331
+ >>> gd(y_obs=[3, 2, 1, 1], y_pred=[2, 1, 1 , 2])
332
+ np.float64(0.2972674459459178)
333
+ """
331
334
 
332
335
  def __init__(self) -> None:
333
336
  super().__init__(degree=0, level=0.5)
@@ -366,11 +369,9 @@ class LogLoss(_BaseScoringFunction):
366
369
  Examples
367
370
  --------
368
371
  >>> ll = LogLoss()
369
- >>> ll(y_obs=[0, 0.5, 1, 1], y_pred=[0.1, 0.2, 0.8 , 0.9], weights=[1, 2, 1, 1]) # doctest: +SKIP
370
- 0.17603033705165635
371
- """ # noqa: E501
372
-
373
- # FIXME: numpy 2.0.0, doctest skip should not be necessary.
372
+ >>> ll(y_obs=[0, 0.5, 1, 1], y_pred=[0.1, 0.2, 0.8 , 0.9], weights=[1, 2, 1, 1])
373
+ np.float64(0.17603033705165635)
374
+ """
374
375
 
375
376
  @property
376
377
  def functional(self):
@@ -466,9 +467,9 @@ class HomogeneousQuantileScore(_BaseScoringFunction):
466
467
  Examples
467
468
  --------
468
469
  >>> hqs = HomogeneousQuantileScore(degree=3, level=0.1)
469
- >>> hqs(y_obs=[0, 0, 1, 1], y_pred=[-1, 1, 1 , 2]) # doctest: +SKIP
470
- 0.6083333333333334
471
- """ # FIXME: numpy 2.0.0, doctest skip should not be necessary.
470
+ >>> hqs(y_obs=[0, 0, 1, 1], y_pred=[-1, 1, 1 , 2])
471
+ np.float64(0.6083333333333334)
472
+ """
472
473
 
473
474
  def __init__(self, degree: float = 2, level: float = 0.5) -> None:
474
475
  self.degree = degree
@@ -569,9 +570,9 @@ class PinballLoss(HomogeneousQuantileScore):
569
570
  Examples
570
571
  --------
571
572
  >>> pl = PinballLoss(level=0.9)
572
- >>> pl(y_obs=[0, 0, 1, 1], y_pred=[-1, 1, 1 , 2]) # doctest: +SKIP
573
- 0.275
574
- """ # FIXME: numpy 2.0.0, doctest skip should not be necessary.
573
+ >>> pl(y_obs=[0, 0, 1, 1], y_pred=[-1, 1, 1 , 2])
574
+ np.float64(0.275)
575
+ """
575
576
 
576
577
  def __init__(self, level: float = 0.5) -> None:
577
578
  super().__init__(degree=1, level=level)
@@ -606,6 +607,7 @@ class ElementaryScore(_BaseScoringFunction):
606
607
  Notes
607
608
  -----
608
609
  [](){#notes}
610
+
609
611
  The elementary scoring or loss function is given by
610
612
 
611
613
  \[
@@ -644,9 +646,9 @@ class ElementaryScore(_BaseScoringFunction):
644
646
  Examples
645
647
  --------
646
648
  >>> el_score = ElementaryScore(eta=2, functional="mean")
647
- >>> el_score(y_obs=[1, 2, 2, 1], y_pred=[4, 1, 2, 3]) # doctest: +SKIP
648
- 0.5
649
- """ # FIXME: numpy 2.0.0, doctest skip should not be necessary.
649
+ >>> el_score(y_obs=[1, 2, 2, 1], y_pred=[4, 1, 2, 3])
650
+ np.float64(0.5)
651
+ """
650
652
 
651
653
  def __init__(
652
654
  self, eta: float, functional: str = "mean", level: float = 0.5