model-diagnostics 1.4.1__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.1 → model_diagnostics-1.4.3}/PKG-INFO +9 -8
  2. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/mkdocs.yml +2 -0
  3. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/pyproject.toml +31 -34
  4. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/__about__.py +1 -1
  5. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/binning.py +46 -37
  6. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/partial_dependence.py +1 -1
  7. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/tests/test_binning.py +1 -1
  8. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/tests/test_isotonic.py +1 -1
  9. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/tests/test_partial_dependence.py +6 -12
  10. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/identification.py +26 -13
  11. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/plots.py +22 -10
  12. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/tests/test_identification.py +8 -2
  13. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/tests/test_plots.py +11 -4
  14. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/plots.py +1 -0
  15. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/scoring.py +29 -27
  16. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/.codecov.yml +0 -0
  17. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/.gitignore +0 -0
  18. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/LICENSE +0 -0
  19. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/README.md +0 -0
  20. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/Untitled.ipynb +0 -0
  21. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/dev_tools/print_package_versions.py +0 -0
  22. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/docs/development.md +0 -0
  23. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/docs/examples/classification.ipynb +0 -0
  24. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/docs/examples/quantile_regression.ipynb +0 -0
  25. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/docs/examples/regression_on_workers_compensation.ipynb +0 -0
  26. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/docs/gen_ref_pages.py +0 -0
  27. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/docs/index.md +0 -0
  28. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/docs/javascripts/mathjax.js +0 -0
  29. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/__init__.py +0 -0
  30. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_config.py +0 -0
  31. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/__init__.py +0 -0
  32. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/array.py +0 -0
  33. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/isotonic.py +0 -0
  34. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/plot_helper.py +0 -0
  35. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/test_helper.py +0 -0
  36. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/_utils/tests/test_array.py +0 -0
  37. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/__init__.py +0 -0
  38. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/calibration/tests/__init__.py +0 -0
  39. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/__init__.py +0 -0
  40. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/tests/__init__.py +0 -0
  41. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/tests/test_plots.py +0 -0
  42. {model_diagnostics-1.4.1 → model_diagnostics-1.4.3}/src/model_diagnostics/scoring/tests/test_scoring.py +0 -0
  43. {model_diagnostics-1.4.1 → 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.1
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.1"
17
+ __version__ = "1.4.3"
@@ -30,8 +30,8 @@ def bin_feature(
30
30
  feature_name: Optional[Union[int, str]],
31
31
  n_obs: int,
32
32
  n_bins: int = 10,
33
- bin_method: str = "quantile",
34
- ):
33
+ bin_method: str = "sturges",
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.
@@ -45,11 +45,15 @@ def bin_feature(
45
45
  n_obs : int
46
46
  The expected length of the first dimention of feature.
47
47
  n_bins : int
48
- The number of bins, at least 2, for numerical features and the maximal number
49
- of (most frequent) categories shown for categorical features. Due to ties, the
50
- effective number of bins might be smaller than `n_bins`. Null values are always
51
- included in the output, accounting for one bin. NaN values are treated as null
52
- values.
48
+ The number of bins, at least 2. For numerical features, `n_bins` only applies
49
+ when `bin_method` is set to `"quantile"` or `"uniform"`.
50
+ For string-like and categorical features, the most frequent values are taken.
51
+ Ties are dealt with by taking the first value in natural sorting order.
52
+ The remaining values are merged into `"other n"` with `n` indicating the unique
53
+ count.
54
+
55
+ I present, null values are always included in the output, accounting for one
56
+ bin. NaN values are treated as null values.
53
57
  bin_method : str
54
58
  The method to use for finding bin edges (boundaries). Options are:
55
59
 
@@ -115,7 +119,7 @@ def bin_feature(
115
119
  # The following statement, i.e. possibly the creation of a pl.Categorical,
116
120
  # MUST be under the StringCache context manager!
117
121
  try:
118
- feature = pl.Series(name=feature_name, values=feature)
122
+ feature_out = pl.Series(name=feature_name, values=feature)
119
123
  except ImportError:
120
124
  # FIXME: pyarrow not installed
121
125
  # For non numpy-backed columns, pyarrow is needed. Here we handle the case
@@ -127,38 +131,38 @@ def bin_feature(
127
131
  feature.dtype, # type: ignore
128
132
  pandas.CategoricalDtype,
129
133
  )
130
- feature = pl.from_dataframe(
134
+ feature_out = pl.from_dataframe(
131
135
  feature.to_frame(name=feature_name) # type: ignore
132
136
  )[:, 0]
133
- if is_pandas_categorical and isinstance(feature.dtype, pl.Enum):
137
+ if is_pandas_categorical and isinstance(feature_out.dtype, pl.Enum):
134
138
  # Pandas categoricals usually get mapped to polars categoricals.
135
139
  # But this code path gives pl.Enum.
136
- feature = feature.cast(pl.Categorical)
140
+ feature_out = feature_out.cast(pl.Categorical)
137
141
  else:
138
142
  raise # re-raises the ImportError
139
- if length_of_first_dimension(feature) != n_obs:
143
+ if length_of_first_dimension(feature_out) != n_obs:
140
144
  msg = (
141
145
  f"The feature array {feature_name} does not have length {n_obs} of its"
142
146
  " first dimension."
143
147
  )
144
148
  raise ValueError(msg)
145
- if feature.dtype == pl.Categorical:
149
+ if feature_out.dtype == pl.Categorical:
146
150
  is_categorical = True
147
- elif feature.dtype == pl.Enum:
151
+ elif feature_out.dtype == pl.Enum:
148
152
  is_enum = True
149
- elif feature.dtype in [pl.Utf8, pl.Object]:
153
+ elif feature_out.dtype in [pl.Utf8, pl.Object]:
150
154
  # We could convert strings to categoricals.
151
155
  is_string = True
152
- elif feature.dtype.is_float():
156
+ elif feature_out.dtype.is_float():
153
157
  # We treat NaN as Null values, numpy will see a Null as a NaN.
154
- feature = feature.fill_nan(None)
158
+ feature_out = feature_out.fill_nan(None)
155
159
  else:
156
160
  # integers
157
161
  pass
158
162
 
159
163
  # If we have Null values, we should reserve one bin for it and reduce
160
164
  # the effective number of bins by 1.
161
- n_bins_ef = max(1, n_bins - feature.has_nulls())
165
+ n_bins_ef = max(1, n_bins - feature_out.has_nulls())
162
166
 
163
167
  if is_categorical or is_enum or is_string:
164
168
  # For categorical and string features, knowing the frequency table in
@@ -177,7 +181,7 @@ def bin_feature(
177
181
  # value_counts(sort=True) sorts ties by first occurence, we want
178
182
  # alphanumerical sorting order.
179
183
  value_counts = (
180
- feature.drop_nulls()
184
+ feature_out.drop_nulls()
181
185
  .value_counts()
182
186
  .sort(by=["count", feature_name], descending=[True, False])
183
187
  )
@@ -185,18 +189,21 @@ def bin_feature(
185
189
  if n_bins_ef >= value_counts.shape[0]:
186
190
  # This also covers the case of only null values.
187
191
  n_bins_ef = value_counts.shape[0]
188
- f_binned = pl.DataFrame({"bin": feature})
192
+ f_binned = pl.DataFrame({"bin": feature_out})
189
193
  else:
190
194
  # We keep the n_bins_ef - 1 most frequent values. Ties are resolved by
191
195
  # taking the first one of the sorted values (most often alpha-numerical).
192
- if feature.has_nulls():
196
+ if feature_out.has_nulls():
193
197
  # To ease adding the null value, we take one value more.
194
198
  keep_values = value_counts[feature_name].head(n_bins_ef)
195
- if is_categorical:
196
- # 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
197
204
  keep_values = keep_values.cast(pl.String)
198
205
  keep_values[-1] = None
199
- keep_values = keep_values.cast(pl.Categorical)
206
+ keep_values = keep_values.cast(dtype)
200
207
  else:
201
208
  keep_values[-1] = None
202
209
  else:
@@ -205,14 +212,16 @@ def bin_feature(
205
212
  # n = n_remaining.
206
213
  n_remaining = value_counts.shape[0] - (n_bins_ef - 1)
207
214
  remaining_name = "other " + _format_integer(n_remaining)
208
- while remaining_name in keep_values:
215
+ while remaining_name in keep_values.cast(pl.String):
209
216
  remaining_name = "_" + remaining_name
210
- return_dtype = feature.dtype
217
+ return_dtype = feature_out.dtype
211
218
  if is_enum:
212
219
  return_dtype = pl.Enum(
213
- pl.concat([feature.dtype.categories, pl.Series([remaining_name])])
220
+ pl.concat(
221
+ [feature_out.dtype.categories, pl.Series([remaining_name])]
222
+ )
214
223
  )
215
- f_binned = feature.replace_strict(
224
+ f_binned = feature_out.replace_strict(
216
225
  old=keep_values,
217
226
  new=keep_values,
218
227
  default=remaining_name,
@@ -222,13 +231,13 @@ def bin_feature(
222
231
  else:
223
232
  # Binning a numerical feature
224
233
  # We will need min and max anyway.
225
- feature_min, feature_max = feature.min(), feature.max()
234
+ feature_min, feature_max = feature_out.min(), feature_out.max()
226
235
  if feature_min == -np.inf:
227
- finite_min = feature.filter(feature > -np.inf).min()
236
+ finite_min = feature_out.filter(feature_out > -np.inf).min()
228
237
  else:
229
238
  finite_min = feature_min
230
239
  if feature_max == np.inf:
231
- finite_max = feature.filter(feature < np.inf).max()
240
+ finite_max = feature_out.filter(feature_out < np.inf).max()
232
241
  else:
233
242
  finite_max = feature_max
234
243
  f_range = finite_max - finite_min
@@ -237,7 +246,7 @@ def bin_feature(
237
246
  # We use method="inverted_cdf" instead of the default "linear" because
238
247
  # "linear" produces as many unique values as before.
239
248
  q = np.nanquantile(
240
- feature,
249
+ feature_out,
241
250
  # Improved rounding errors by using integers and dividing at the
242
251
  # end as opposed to np.linspace with 1/n_bins step size.
243
252
  q=np.arange(1, n_bins_ef) / n_bins_ef,
@@ -248,11 +257,11 @@ def bin_feature(
248
257
  bin_edges = finite_min + f_range * np.arange(1, n_bins_ef) / n_bins_ef
249
258
  else:
250
259
  # numpy histogram bin methods
251
- a = feature.filter(feature.is_finite() & feature.is_not_null())
260
+ a = feature_out.filter(feature_out.is_finite() & feature_out.is_not_null())
252
261
  bin_edges = np.histogram_bin_edges(a, bins=bin_method)[1:-1]
253
262
  n_bins_ef = bin_edges.shape[0] + 1
254
263
  # We want: bins[i-1] < x <= bins[i]
255
- f_binned = np.digitize(feature, bins=bin_edges, right=True)
264
+ f_binned = np.digitize(feature_out, bins=bin_edges, right=True)
256
265
  # The full bin edges also include min and max of the feature.
257
266
  if bin_edges.size == 0:
258
267
  bin_edges = np.r_[feature_min, feature_max]
@@ -268,8 +277,8 @@ def bin_feature(
268
277
  f_binned = (
269
278
  pl.LazyFrame(
270
279
  [
271
- feature,
272
- pl.Series("__f_binned", f_binned, dtype=feature.dtype),
280
+ feature_out,
281
+ pl.Series("__f_binned", f_binned, dtype=feature_out.dtype),
273
282
  pl.Series(
274
283
  "__bin_edges",
275
284
  bin_edges[f_binned],
@@ -289,4 +298,4 @@ def bin_feature(
289
298
  )
290
299
  .collect()
291
300
  )
292
- 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
@@ -146,7 +146,7 @@ def test_binning_numerical(bin_method, with_inf, with_null, feature_type):
146
146
 
147
147
 
148
148
  def test_binning_auto():
149
- """ "Test auto bin method"""
149
+ """Test auto bin method"""
150
150
  n_bins = 5
151
151
  feature = pl.Series(name="my_feature", values=[0, 1, 1, 2])
152
152
  n_obs = len(feature)
@@ -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:
@@ -130,7 +131,7 @@ def compute_bias(
130
131
  functional: str = "mean",
131
132
  level: float = 0.5,
132
133
  n_bins: int = 10,
133
- bin_method: str = "auto",
134
+ bin_method: str = "sturges",
134
135
  ):
135
136
  r"""Compute generalised bias conditional on a feature.
136
137
 
@@ -172,10 +173,15 @@ def compute_bias(
172
173
  `level=0.5` and `functional="expectile"` gives the mean.
173
174
  `level=0.5` and `functional="quantile"` gives the median.
174
175
  n_bins : int
175
- The number of bins for numerical features and the maximal number of (most
176
- frequent) categories shown for categorical features. Due to ties, the effective
177
- number of bins might be smaller than `n_bins`. Null values are always included
178
- in the output, accounting for one bin. NaN values are treated as null values.
176
+ The number of bins, at least 2. For numerical features, `n_bins` only applies
177
+ when `bin_method` is set to `"quantile"` or `"uniform"`.
178
+ For string-like and categorical features, the most frequent values are taken.
179
+ Ties are dealt with by taking the first value in natural sorting order.
180
+ The remaining values are merged into `"other n"` with `n` indicating the unique
181
+ count.
182
+
183
+ I present, null values are always included in the output, accounting for one
184
+ bin. NaN values are treated as null values.
179
185
  bin_method : str
180
186
  The method for finding bin edges (boundaries). Options using `n_bins` are:
181
187
 
@@ -230,6 +236,7 @@ def compute_bias(
230
236
  Notes
231
237
  -----
232
238
  [](){#notes}
239
+
233
240
  A model \(m(X)\) is conditionally calibrated iff
234
241
  \(\mathbb{E}(V(m(X), Y)|X)=0\) almost surely with canonical identification
235
242
  function \(V\).
@@ -306,20 +313,21 @@ def compute_bias(
306
313
  with pl.StringCache():
307
314
  feature_name = None
308
315
  if feature is not None:
309
- feature, n_bins, f_binned = bin_feature(
316
+ feature_series, n_bins, f_binned = bin_feature(
310
317
  feature=feature,
311
318
  feature_name=None,
312
319
  n_obs=n_obs,
313
320
  n_bins=n_bins,
314
321
  bin_method=bin_method,
315
322
  )
316
- feature_name = feature.name
317
- is_cat_or_string = feature.dtype in [
323
+ feature_name = feature_series.name
324
+ is_cat_or_string = feature_series.dtype in [
318
325
  pl.Categorical,
319
326
  pl.Enum,
320
327
  pl.Utf8,
321
328
  pl.Object,
322
329
  ]
330
+ feature = feature_series
323
331
 
324
332
  for i in range(len(pred_names)):
325
333
  # Loop over columns of y_pred.
@@ -483,7 +491,7 @@ def compute_marginal(
483
491
  weights: Optional[npt.ArrayLike] = None,
484
492
  *,
485
493
  n_bins: int = 10,
486
- bin_method: str = "auto",
494
+ bin_method: str = "sturges",
487
495
  n_max: int = 1000,
488
496
  rng: Optional[Union[np.random.Generator, int]] = None,
489
497
  ):
@@ -512,10 +520,15 @@ def compute_marginal(
512
520
  Case weights. If given, the bias is calculated as weighted average of the
513
521
  identification function with these weights.
514
522
  n_bins : int
515
- The number of bins for numerical features and the maximal number of (most
516
- frequent) categories shown for categorical features. Due to ties, the effective
517
- number of bins might be smaller than `n_bins`. Null values are always included
518
- in the output, accounting for one bin. NaN values are treated as null values.
523
+ The number of bins, at least 2. For numerical features, `n_bins` only applies
524
+ when `bin_method` is set to `"quantile"` or `"uniform"`.
525
+ For string-like and categorical features, the most frequent values are taken.
526
+ Ties are dealt with by taking the first value in natural sorting order.
527
+ The remaining values are merged into `"other n"` with `n` indicating the unique
528
+ count.
529
+
530
+ I present, null values are always included in the output, accounting for one
531
+ bin. NaN values are treated as null values.
519
532
  bin_method : str
520
533
  The method for finding bin edges (boundaries). Options using `n_bins` are:
521
534
 
@@ -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:
@@ -319,7 +320,7 @@ def plot_bias(
319
320
  functional: str = "mean",
320
321
  level: float = 0.5,
321
322
  n_bins: int = 10,
322
- bin_method: str = "auto",
323
+ bin_method: str = "sturges",
323
324
  confidence_level: float = 0.9,
324
325
  ax: Optional[mpl.axes.Axes] = None,
325
326
  ):
@@ -365,10 +366,15 @@ def plot_bias(
365
366
  `level=0.5` and `functional="expectile"` gives the mean.
366
367
  `level=0.5` and `functional="quantile"` gives the median.
367
368
  n_bins : int
368
- The number of bins for numerical features and the maximal number of (most
369
- frequent) categories shown for categorical features. Due to ties, the effective
370
- number of bins might be smaller than `n_bins`. Null values are always included
371
- in the output, accounting for one bin. NaN values are treated as null values.
369
+ The number of bins, at least 2. For numerical features, `n_bins` only applies
370
+ when `bin_method` is set to `"quantile"` or `"uniform"`.
371
+ For string-like and categorical features, the most frequent values are taken.
372
+ Ties are dealt with by taking the first value in natural sorting order.
373
+ The remaining values are merged into `"other n"` with `n` indicating the unique
374
+ count.
375
+
376
+ I present, null values are always included in the output, accounting for one
377
+ bin. NaN values are treated as null values.
372
378
  bin_method : str
373
379
  The method for finding bin edges (boundaries). Options using `n_bins` are:
374
380
 
@@ -420,6 +426,7 @@ def plot_bias(
420
426
  Notes
421
427
  -----
422
428
  [](){#notes}
429
+
423
430
  A model \(m(X)\) is conditionally calibrated iff \(E(V(m(X), Y))=0\) a.s. The
424
431
  empirical version, given some data, reads \(\frac{1}{n}\sum_i V(m(x_i), y_i)\).
425
432
  See `[FLM2022]`.
@@ -742,7 +749,7 @@ def plot_marginal(
742
749
  weights: Optional[npt.ArrayLike] = None,
743
750
  *,
744
751
  n_bins: int = 10,
745
- bin_method: str = "auto",
752
+ bin_method: str = "sturges",
746
753
  n_max: int = 1000,
747
754
  rng: Optional[Union[np.random.Generator, int]] = None,
748
755
  ax: Optional[mpl.axes.Axes] = None,
@@ -773,10 +780,15 @@ def plot_marginal(
773
780
  Case weights. If given, the bias is calculated as weighted average of the
774
781
  identification function with these weights.
775
782
  n_bins : int
776
- The number of bins for numerical features and the maximal number of (most
777
- frequent) categories shown for categorical features. Due to ties, the effective
778
- number of bins might be smaller than `n_bins`. Null values are always included
779
- in the output, accounting for one bin. NaN values are treated as null values.
783
+ The number of bins, at least 2. For numerical features, `n_bins` only applies
784
+ when `bin_method` is set to `"quantile"` or `"uniform"`.
785
+ For string-like and categorical features, the most frequent values are taken.
786
+ Ties are dealt with by taking the first value in natural sorting order.
787
+ The remaining values are merged into `"other n"` with `n` indicating the unique
788
+ count.
789
+
790
+ I present, null values are always included in the output, accounting for one
791
+ bin. NaN values are treated as null values.
780
792
  bin_method : str
781
793
  The method for finding bin edges (boundaries). Options using `n_bins` are:
782
794
 
@@ -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