proxyml 0.3.0__tar.gz → 0.4.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -211,7 +211,7 @@ Requires-Dist: pandas>=1.5
211
211
  Requires-Dist: orjson>=3.9
212
212
  Requires-Dist: proxyml-core>=0.1
213
213
  Provides-Extra: local
214
- Requires-Dist: proxyml-core[modeling]>=0.1; extra == "local"
214
+ Requires-Dist: proxyml-core[modeling]>=0.2.1; extra == "local"
215
215
  Provides-Extra: dev
216
216
  Requires-Dist: build; extra == "dev"
217
217
  Requires-Dist: twine; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proxyml"
7
- version = "0.3.0"
7
+ version = "0.4.1"
8
8
  description = "Python SDK for calling the ProxyML API"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [project.optional-dependencies]
37
37
  local = [
38
- "proxyml-core[modeling]>=0.1",
38
+ "proxyml-core[modeling]>=0.2.1",
39
39
  ]
40
40
  dev = [
41
41
  "build",
@@ -1,3 +1,5 @@
1
+ from importlib.metadata import version as _pkg_version
2
+
1
3
  from proxyml.client import (
2
4
  health_check,
3
5
  put_schema,
@@ -27,7 +29,10 @@ from proxyml.client import (
27
29
  )
28
30
  from proxyml.schema_builder import get_schema
29
31
 
32
+ __version__ = _pkg_version("proxyml")
33
+
30
34
  __all__ = [
35
+ "__version__",
31
36
  "health_check",
32
37
  "put_schema",
33
38
  "fetch_schema",
@@ -10,6 +10,7 @@ from proxyml.local.challenger import (
10
10
  Complexity,
11
11
  Rung,
12
12
  TrainedChallenger,
13
+ score_champion,
13
14
  train_auto_challenger,
14
15
  train_challenger,
15
16
  )
@@ -17,6 +18,7 @@ from proxyml.local.challenger import (
17
18
  __all__ = [
18
19
  "train_challenger",
19
20
  "train_auto_challenger",
21
+ "score_champion",
20
22
  "Complexity",
21
23
  "Rung",
22
24
  "TrainedChallenger",
@@ -10,7 +10,7 @@ training target was real ground truth or a black box's predictions.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- from dataclasses import dataclass
13
+ from dataclasses import dataclass, replace
14
14
  from enum import Enum
15
15
  from pathlib import Path
16
16
  from typing import Any, Callable, Literal
@@ -19,7 +19,6 @@ import numpy as np
19
19
  import pandas as pd
20
20
  from sklearn.base import BaseEstimator
21
21
  from sklearn.linear_model import LogisticRegressionCV, RidgeCV
22
- from sklearn.metrics import f1_score, r2_score
23
22
  from sklearn.model_selection import train_test_split
24
23
  from sklearn.pipeline import Pipeline
25
24
 
@@ -34,6 +33,7 @@ from proxyml_core.modeling.estimators import (
34
33
  )
35
34
  from proxyml_core.modeling.extract import extract_export_data
36
35
  from proxyml_core.modeling.preprocess import build_preprocessor
36
+ from proxyml_core.modeling.scoring import score_predictions
37
37
  from proxyml_core.schema import Feature, FeatureSchema
38
38
 
39
39
 
@@ -60,6 +60,8 @@ def _simple_classifier() -> BaseEstimator:
60
60
  max_iter=500,
61
61
  cv=5,
62
62
  n_jobs=-1,
63
+ scoring="accuracy",
64
+ use_legacy_attributes=False,
63
65
  )
64
66
 
65
67
 
@@ -76,6 +78,8 @@ def _flexible_classifier() -> BaseEstimator:
76
78
  max_iter=1000,
77
79
  cv=5,
78
80
  n_jobs=-1,
81
+ scoring="accuracy",
82
+ use_legacy_attributes=False,
79
83
  )
80
84
 
81
85
 
@@ -184,14 +188,10 @@ def train_challenger(
184
188
  hyperparameters = extract_hyperparameters(pipeline.named_steps["estimator"])
185
189
  y_pred = pipeline.predict(X_test)
186
190
 
187
- metrics: dict[str, float] = {}
188
- if classification:
189
- metrics["f1"] = float(f1_score(y_test, y_pred, average="weighted", zero_division=0))
190
- metrics["accuracy"] = float((y_test == y_pred).mean())
191
- else:
192
- metrics["r2"] = float(r2_score(y_test, y_pred))
191
+ metrics = score_predictions(y_test, y_pred, task=resolved_task)
193
192
 
194
193
  export = extract_export_data(pipeline, features, resolved_task)
194
+ export = replace(export, hyperparameters=hyperparameters, metrics=metrics)
195
195
 
196
196
  return TrainedChallenger(
197
197
  pipeline=pipeline,
@@ -202,6 +202,28 @@ def train_challenger(
202
202
  )
203
203
 
204
204
 
205
+ def score_champion(
206
+ labels: np.ndarray | list,
207
+ predictions: np.ndarray | list,
208
+ *,
209
+ task: Literal["classification", "regression"],
210
+ ) -> dict[str, float]:
211
+ """Score a champion model's predictions against real labels, locally.
212
+
213
+ Uses the exact same scoring code as ``train_challenger()``'s internal
214
+ fidelity metrics, so ``champion_metrics`` and a paired
215
+ ``TrainedChallenger.metrics`` are computed identically — required for an
216
+ apples-to-apples champion-vs-challenger comparison. Pass the same task
217
+ the paired challenger resolved to (e.g. ``result.task``); there's no
218
+ ``task="auto"`` here, since letting the two resolve independently risks
219
+ them silently diverging.
220
+
221
+ Returns ``{"f1":..., "accuracy":...}`` for classification or ``{"r2":...}``
222
+ for regression — the same shape as ``TrainedChallenger.metrics``.
223
+ """
224
+ return score_predictions(np.asarray(labels), np.asarray(predictions), task=task)
225
+
226
+
205
227
  def train_auto_challenger(
206
228
  data: str | Path | pd.DataFrame,
207
229
  target_col: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -211,7 +211,7 @@ Requires-Dist: pandas>=1.5
211
211
  Requires-Dist: orjson>=3.9
212
212
  Requires-Dist: proxyml-core>=0.1
213
213
  Provides-Extra: local
214
- Requires-Dist: proxyml-core[modeling]>=0.1; extra == "local"
214
+ Requires-Dist: proxyml-core[modeling]>=0.2.1; extra == "local"
215
215
  Provides-Extra: dev
216
216
  Requires-Dist: build; extra == "dev"
217
217
  Requires-Dist: twine; extra == "dev"
@@ -12,4 +12,4 @@ black
12
12
  ruff
13
13
 
14
14
  [local]
15
- proxyml-core[modeling]>=0.1
15
+ proxyml-core[modeling]>=0.2.1
@@ -1,3 +1,5 @@
1
+ import warnings
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
  import pytest
@@ -6,6 +8,7 @@ from proxyml.local import (
6
8
  Complexity,
7
9
  LADDERS,
8
10
  TrainedChallenger,
11
+ score_champion,
9
12
  train_auto_challenger,
10
13
  train_challenger,
11
14
  )
@@ -62,6 +65,20 @@ def test_train_challenger_classification():
62
65
  assert result.export.classes is not None
63
66
 
64
67
 
68
+ def test_train_challenger_classification_rungs_fit_without_sklearn_deprecation_warnings():
69
+ # scoring="accuracy" and use_legacy_attributes=False on the SIMPLE/FLEXIBLE
70
+ # classifiers pin today's sklearn defaults explicitly, matching what
71
+ # get_default_classifier() (the MODERATE rung) already does.
72
+ schema = _schema()
73
+ df = _df(seed=12, n=200)
74
+ target = np.where(df["age"] > 50, "senior", "junior")
75
+
76
+ with warnings.catch_warnings():
77
+ warnings.simplefilter("error", FutureWarning)
78
+ train_challenger(df, target, schema, complexity=Complexity.SIMPLE, task="classification")
79
+ train_challenger(df, target, schema, complexity=Complexity.FLEXIBLE, task="classification")
80
+
81
+
65
82
  def test_train_challenger_simple_rung_is_more_regularized_than_flexible():
66
83
  schema = _schema()
67
84
  df = _df(seed=3)
@@ -139,6 +156,47 @@ def test_train_auto_challenger_matches_manual_schema_and_train():
139
156
  )
140
157
 
141
158
 
159
+ def test_score_champion_matches_train_challenger_metric_shape_classification():
160
+ schema = _schema()
161
+ df = _df(seed=9, n=300)
162
+ target = np.where(df["age"] > 50, "senior", "junior")
163
+
164
+ result = train_challenger(df, target, schema, complexity=Complexity.MODERATE, task="classification")
165
+ champion_metrics = score_champion(target, target, task="classification")
166
+ assert set(champion_metrics) == set(result.metrics)
167
+
168
+
169
+ def test_score_champion_matches_train_challenger_metric_shape_regression():
170
+ schema = _schema()
171
+ df = _df(seed=10)
172
+ target = df["age"] * 0.5 + df["income"] * 0.0001
173
+
174
+ result = train_challenger(df, target, schema, complexity=Complexity.MODERATE, task="regression")
175
+ champion_metrics = score_champion(target, target, task="regression")
176
+ assert set(champion_metrics) == set(result.metrics)
177
+
178
+
179
+ def test_score_champion_uses_same_scoring_as_train_challenger():
180
+ # Feed score_champion the exact labels/predictions a regression fit produced
181
+ # internally, and assert numeric equality with the challenger's own r2 —
182
+ # proving the two aren't drifting copies of the same formula.
183
+ schema = _schema()
184
+ df = _df(seed=11, n=300)
185
+ target = df["age"] * 0.5 + df["income"] * 0.0001
186
+
187
+ result = train_challenger(df, target, schema, complexity=Complexity.MODERATE, task="regression")
188
+ X = df[["age", "income"]].to_numpy(dtype=object)
189
+ y_pred = result.pipeline.predict(X)
190
+ reproduced_metrics = score_champion(target, y_pred, task="regression")
191
+
192
+ # Not identical to result.metrics (that was scored on a held-out test split,
193
+ # this is scored on the full data) but both must use the same r2 formula —
194
+ # verify by comparing against sklearn directly.
195
+ from sklearn.metrics import r2_score
196
+
197
+ assert reproduced_metrics["r2"] == pytest.approx(r2_score(target, y_pred))
198
+
199
+
142
200
  def test_train_auto_challenger_passes_immutable_cols_to_get_schema():
143
201
  from unittest.mock import patch
144
202
 
File without changes
File without changes
File without changes
File without changes
File without changes