proxyml 0.2.2__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -209,6 +209,9 @@ Requires-Dist: requests>=2.28
209
209
  Requires-Dist: numpy>=1.23
210
210
  Requires-Dist: pandas>=1.5
211
211
  Requires-Dist: orjson>=3.9
212
+ Requires-Dist: proxyml-core>=0.1
213
+ Provides-Extra: local
214
+ Requires-Dist: proxyml-core[modeling]>=0.2; extra == "local"
212
215
  Provides-Extra: dev
213
216
  Requires-Dist: build; extra == "dev"
214
217
  Requires-Dist: twine; extra == "dev"
@@ -236,6 +239,12 @@ statistics are uploaded — your data stays yours.
236
239
  pip install proxyml
237
240
  ```
238
241
 
242
+ Want to train a challenger model locally, with no round-trip to the API? Install the `local` extra (adds scikit-learn and scipy):
243
+
244
+ ```bash
245
+ pip install 'proxyml[local]'
246
+ ```
247
+
239
248
  ## Setup
240
249
 
241
250
  ProxyML requires an API key. Set it as an environment variable before importing the package:
@@ -261,11 +270,12 @@ from proxyml import get_schema
261
270
  df = pd.read_csv("data.csv")
262
271
  schema = get_schema(df, immutable_cols=["age", "gender"])
263
272
 
264
- # 2. Upload the schema
265
- proxyml.put_schema(schema)
273
+ # 2. Upload the schema under a name — synthesis and training reference it by name
274
+ SCHEMA_NAME = "my_schema"
275
+ proxyml.put_schema(schema, name=SCHEMA_NAME)
266
276
 
267
277
  # 3. Generate synthetic training data
268
- synth_df = proxyml.synthesize_data(num_points=500)
278
+ synth_df = proxyml.synthesize_data(num_points=500, schema_name=SCHEMA_NAME)
269
279
 
270
280
  # 4. Score synthetic data with your black-box model
271
281
  predictions = my_model.predict(synth_df.values.tolist())
@@ -275,6 +285,7 @@ proxyml.train_surrogate(
275
285
  samples=synth_df.values.tolist(),
276
286
  predictions=predictions,
277
287
  feature_names=list(synth_df.columns),
288
+ schema_name=SCHEMA_NAME,
278
289
  )
279
290
 
280
291
  # 6. Find a counterfactual explanation
@@ -301,6 +312,8 @@ See [`docs/quickstart.md`](docs/quickstart.md) for a full walkthrough and [`docs
301
312
 
302
313
  **Surrogate model** — A fast, interpretable model trained to approximate your black-box model's behavior on synthetic data. Once trained, it can be queried directly via the ProxyML API.
303
314
 
315
+ **Challenger model** — Trained the same way as a surrogate, but locally (`proxyml.local`, no round-trip to the API) and against either real ground-truth labels (a genuine challenger to compare against a champion model) or a black box's predictions (a surrogate/explainer). Its export is structurally identical to a server-trained surrogate's, so the two can be compared and scored with the exact same arithmetic.
316
+
304
317
  **Counterfactual explanation** — Given a prediction, a counterfactual is the minimal change to input features that would produce a different prediction. It answers: *"What would have to be different for the outcome to change?"*
305
318
 
306
319
  **Schema** — Describes the statistical properties of each feature (type, range, distribution). Used to generate realistic synthetic data and constrain counterfactual search.
@@ -309,13 +322,17 @@ See [`docs/quickstart.md`](docs/quickstart.md) for a full walkthrough and [`docs
309
322
 
310
323
  | Function | Description |
311
324
  |---|---|
312
- | `get_schema(df, immutable_cols)` | Generate a schema dict from a DataFrame |
313
- | `put_schema(schema)` | Upload a schema to the API |
314
- | `synthesize_data(num_points, sample, as_df)` | Generate synthetic data points |
315
- | `train_surrogate(samples, predictions, feature_names, task, test_size)` | Train a surrogate model |
325
+ | `get_schema(df, immutable_cols)` | Infer a `FeatureSchema` from a DataFrame |
326
+ | `put_schema(schema, name)` | Upload a schema to the API under a name |
327
+ | `synthesize_data(num_points, sample, as_df, schema_name)` | Generate synthetic data points |
328
+ | `train_surrogate(samples, predictions, feature_names, task, test_size, schema_name)` | Train a surrogate model |
329
+ | `train_auto_surrogate(data, target_col, ...)` | Load data + train a surrogate in one call, skipping synthesis |
330
+ | `export_surrogate(version)` | Export a surrogate for offline scoring via `predict_from_export` |
316
331
  | `predict(sample, version)` | Score a single sample with the surrogate model |
317
332
  | `find_counterfactual(sample, target, ...)` | Find a counterfactual for a given sample |
318
333
  | `interpret_counterfactual(sample, counterfactual, ...)` | Generate a human-readable explanation |
334
+ | `proxyml.local.train_challenger(df, target, schema, ...)` | Train a challenger model locally — no API round-trip (`pip install 'proxyml[local]'`) |
335
+ | `proxyml.local.train_auto_challenger(data, target_col, ...)` | Load data + train a local challenger in one call |
319
336
 
320
337
  Full documentation: [`docs/api.md`](docs/api.md)
321
338
 
@@ -323,6 +340,10 @@ Full documentation: [`docs/api.md`](docs/api.md)
323
340
 
324
341
  - [`examples/basic_usage.py`](examples/basic_usage.py) — Schema upload, data synthesis, surrogate training
325
342
  - [`examples/counterfactual_example.py`](examples/counterfactual_example.py) — Counterfactual search and interpretation
343
+ - [`examples/regression_example.py`](examples/regression_example.py) — Regression with immutable features
344
+ - [`examples/multiclass_example.py`](examples/multiclass_example.py) — Multi-class classification with per-class feature importances
345
+ - [`examples/testing_example.py`](examples/testing_example.py) — Using a surrogate as a reference model in CI
346
+ - [`examples/surrogate_export_example.py`](examples/surrogate_export_example.py) — Exporting a surrogate and reproducing its predictions locally
326
347
 
327
348
  ## License
328
349
 
@@ -17,6 +17,12 @@ statistics are uploaded — your data stays yours.
17
17
  pip install proxyml
18
18
  ```
19
19
 
20
+ Want to train a challenger model locally, with no round-trip to the API? Install the `local` extra (adds scikit-learn and scipy):
21
+
22
+ ```bash
23
+ pip install 'proxyml[local]'
24
+ ```
25
+
20
26
  ## Setup
21
27
 
22
28
  ProxyML requires an API key. Set it as an environment variable before importing the package:
@@ -42,11 +48,12 @@ from proxyml import get_schema
42
48
  df = pd.read_csv("data.csv")
43
49
  schema = get_schema(df, immutable_cols=["age", "gender"])
44
50
 
45
- # 2. Upload the schema
46
- proxyml.put_schema(schema)
51
+ # 2. Upload the schema under a name — synthesis and training reference it by name
52
+ SCHEMA_NAME = "my_schema"
53
+ proxyml.put_schema(schema, name=SCHEMA_NAME)
47
54
 
48
55
  # 3. Generate synthetic training data
49
- synth_df = proxyml.synthesize_data(num_points=500)
56
+ synth_df = proxyml.synthesize_data(num_points=500, schema_name=SCHEMA_NAME)
50
57
 
51
58
  # 4. Score synthetic data with your black-box model
52
59
  predictions = my_model.predict(synth_df.values.tolist())
@@ -56,6 +63,7 @@ proxyml.train_surrogate(
56
63
  samples=synth_df.values.tolist(),
57
64
  predictions=predictions,
58
65
  feature_names=list(synth_df.columns),
66
+ schema_name=SCHEMA_NAME,
59
67
  )
60
68
 
61
69
  # 6. Find a counterfactual explanation
@@ -82,6 +90,8 @@ See [`docs/quickstart.md`](docs/quickstart.md) for a full walkthrough and [`docs
82
90
 
83
91
  **Surrogate model** — A fast, interpretable model trained to approximate your black-box model's behavior on synthetic data. Once trained, it can be queried directly via the ProxyML API.
84
92
 
93
+ **Challenger model** — Trained the same way as a surrogate, but locally (`proxyml.local`, no round-trip to the API) and against either real ground-truth labels (a genuine challenger to compare against a champion model) or a black box's predictions (a surrogate/explainer). Its export is structurally identical to a server-trained surrogate's, so the two can be compared and scored with the exact same arithmetic.
94
+
85
95
  **Counterfactual explanation** — Given a prediction, a counterfactual is the minimal change to input features that would produce a different prediction. It answers: *"What would have to be different for the outcome to change?"*
86
96
 
87
97
  **Schema** — Describes the statistical properties of each feature (type, range, distribution). Used to generate realistic synthetic data and constrain counterfactual search.
@@ -90,13 +100,17 @@ See [`docs/quickstart.md`](docs/quickstart.md) for a full walkthrough and [`docs
90
100
 
91
101
  | Function | Description |
92
102
  |---|---|
93
- | `get_schema(df, immutable_cols)` | Generate a schema dict from a DataFrame |
94
- | `put_schema(schema)` | Upload a schema to the API |
95
- | `synthesize_data(num_points, sample, as_df)` | Generate synthetic data points |
96
- | `train_surrogate(samples, predictions, feature_names, task, test_size)` | Train a surrogate model |
103
+ | `get_schema(df, immutable_cols)` | Infer a `FeatureSchema` from a DataFrame |
104
+ | `put_schema(schema, name)` | Upload a schema to the API under a name |
105
+ | `synthesize_data(num_points, sample, as_df, schema_name)` | Generate synthetic data points |
106
+ | `train_surrogate(samples, predictions, feature_names, task, test_size, schema_name)` | Train a surrogate model |
107
+ | `train_auto_surrogate(data, target_col, ...)` | Load data + train a surrogate in one call, skipping synthesis |
108
+ | `export_surrogate(version)` | Export a surrogate for offline scoring via `predict_from_export` |
97
109
  | `predict(sample, version)` | Score a single sample with the surrogate model |
98
110
  | `find_counterfactual(sample, target, ...)` | Find a counterfactual for a given sample |
99
111
  | `interpret_counterfactual(sample, counterfactual, ...)` | Generate a human-readable explanation |
112
+ | `proxyml.local.train_challenger(df, target, schema, ...)` | Train a challenger model locally — no API round-trip (`pip install 'proxyml[local]'`) |
113
+ | `proxyml.local.train_auto_challenger(data, target_col, ...)` | Load data + train a local challenger in one call |
100
114
 
101
115
  Full documentation: [`docs/api.md`](docs/api.md)
102
116
 
@@ -104,6 +118,10 @@ Full documentation: [`docs/api.md`](docs/api.md)
104
118
 
105
119
  - [`examples/basic_usage.py`](examples/basic_usage.py) — Schema upload, data synthesis, surrogate training
106
120
  - [`examples/counterfactual_example.py`](examples/counterfactual_example.py) — Counterfactual search and interpretation
121
+ - [`examples/regression_example.py`](examples/regression_example.py) — Regression with immutable features
122
+ - [`examples/multiclass_example.py`](examples/multiclass_example.py) — Multi-class classification with per-class feature importances
123
+ - [`examples/testing_example.py`](examples/testing_example.py) — Using a surrogate as a reference model in CI
124
+ - [`examples/surrogate_export_example.py`](examples/surrogate_export_example.py) — Exporting a surrogate and reproducing its predictions locally
107
125
 
108
126
  ## License
109
127
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proxyml"
7
- version = "0.2.2"
7
+ version = "0.4.0"
8
8
  description = "Python SDK for calling the ProxyML API"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -30,15 +30,19 @@ dependencies = [
30
30
  "numpy>=1.23",
31
31
  "pandas>=1.5",
32
32
  "orjson>=3.9",
33
+ "proxyml-core>=0.1",
33
34
  ]
34
35
 
35
36
  [project.optional-dependencies]
37
+ local = [
38
+ "proxyml-core[modeling]>=0.2",
39
+ ]
36
40
  dev = [
37
41
  "build",
38
42
  "twine",
39
43
  "pytest",
40
- "black",
41
- "ruff",
44
+ "black",
45
+ "ruff",
42
46
  ]
43
47
 
44
48
  [project.urls]
@@ -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,
@@ -6,6 +8,7 @@ from proxyml.client import (
6
8
  delete_schema,
7
9
  synthesize_data,
8
10
  train_surrogate,
11
+ train_auto_surrogate,
9
12
  predict,
10
13
  predict_batch,
11
14
  find_counterfactual,
@@ -24,14 +27,12 @@ from proxyml.client import (
24
27
  explain_local_batch,
25
28
  update_model,
26
29
  )
27
- from proxyml.schema import (
28
- get_schema,
29
- gen_continuous_schema,
30
- gen_categorical_schema,
31
- gen_discrete_schema,
32
- )
30
+ from proxyml.schema_builder import get_schema
31
+
32
+ __version__ = _pkg_version("proxyml")
33
33
 
34
34
  __all__ = [
35
+ "__version__",
35
36
  "health_check",
36
37
  "put_schema",
37
38
  "fetch_schema",
@@ -39,6 +40,7 @@ __all__ = [
39
40
  "delete_schema",
40
41
  "synthesize_data",
41
42
  "train_surrogate",
43
+ "train_auto_surrogate",
42
44
  "predict",
43
45
  "predict_batch",
44
46
  "find_counterfactual",
@@ -57,7 +59,4 @@ __all__ = [
57
59
  "explain_local_batch",
58
60
  "update_model",
59
61
  "get_schema",
60
- "gen_continuous_schema",
61
- "gen_categorical_schema",
62
- "gen_discrete_schema",
63
62
  ]
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ from pathlib import Path
3
4
  from typing import Any
4
5
 
5
6
  import orjson
@@ -8,6 +9,10 @@ import pandas as pd
8
9
  import requests
9
10
  from pandas.api.types import is_float_dtype, is_integer_dtype
10
11
 
12
+ from proxyml.schema_builder import get_schema
13
+ from proxyml_core.export import SurrogateExport
14
+ from proxyml_core.schema import FeatureSchema
15
+
11
16
  logger = logging.getLogger(__name__)
12
17
 
13
18
  _BOOL_STRINGS = {"true", "false"}
@@ -156,20 +161,20 @@ def delete(endpoint: str) -> requests.models.Response | _ErrorResponse:
156
161
  return _ErrorResponse()
157
162
 
158
163
 
159
- def put_schema(schema: dict, name: str) -> Any:
164
+ def put_schema(schema: FeatureSchema, name: str) -> FeatureSchema | None:
160
165
  """
161
166
  Uploads a data schema.
162
167
 
163
168
  Args:
164
- schema (dict): data schema object
169
+ schema (FeatureSchema): data schema object, e.g. from get_schema()
165
170
  name (str): name for the schema.
166
171
  Returns:
167
- API response JSON object, or None if the return code was not 200.
172
+ The stored FeatureSchema, or None if the return code was not 200.
168
173
  """
169
- r = put(endpoint=f'/schema/{name}', payload=schema)
174
+ r = put(endpoint=f'/schema/{name}', payload=schema.to_dict())
170
175
  if r.status_code == 200:
171
176
  logger.info("Schema '%s' uploaded successfully", name)
172
- return r.json()
177
+ return FeatureSchema.from_dict(r.json())
173
178
  logger.error(
174
179
  "Schema upload failed with status %s: %s",
175
180
  r.status_code,
@@ -178,11 +183,14 @@ def put_schema(schema: dict, name: str) -> Any:
178
183
  return None
179
184
 
180
185
 
181
- def fetch_schema(name: str) -> dict | None:
186
+ def fetch_schema(name: str) -> FeatureSchema | None:
182
187
  """Retrieve a stored feature schema by name."""
183
188
  r = get(endpoint=f'/schema/{name}', params={})
184
189
  if r.status_code == 200:
185
- return r.json()
190
+ payload = r.json()
191
+ if payload.get('schema_warning'):
192
+ logger.warning(payload['schema_warning'])
193
+ return FeatureSchema.from_dict(payload)
186
194
  logger.error(
187
195
  "Fetch schema failed with status %s: %s",
188
196
  r.status_code,
@@ -323,22 +331,84 @@ def train_surrogate(
323
331
  return None
324
332
 
325
333
 
326
- def export_surrogate(version: str) -> Any:
334
+ def train_auto_surrogate(
335
+ data: str | Path | pd.DataFrame,
336
+ target_col: str,
337
+ feature_names: list[str] | None = None,
338
+ task: str = 'auto',
339
+ test_size: float = 0.2,
340
+ *,
341
+ schema_name: str,
342
+ immutable_cols: list[str] | None = None,
343
+ name: str | None = None,
344
+ comments: str | None = None,
345
+ ) -> Any:
327
346
  """
328
- Exports a surrogate model to JSON e.g., classes, intercept, per_class_intercepts, features, scalers, etc.; everything required
329
- to reconstruct the surrogate.
330
-
347
+ Loads data that already contains samples and a target/prediction column,
348
+ infers and uploads a feature schema, and trains a server-side surrogate
349
+ against target_col — all in one call.
350
+
351
+ Assumes `data` already contains the samples you want to train on and the
352
+ value to predict for each one (real ground truth or a black box's
353
+ predictions), so it skips synthesize_data() entirely. If you need the
354
+ server to generate synthetic samples for you to score first, use
355
+ synthesize_data() and train_surrogate() directly instead (see
356
+ examples/basic_usage.py).
357
+
358
+ Args:
359
+ data (str | Path | DataFrame): a CSV path, or an already-loaded DataFrame,
360
+ containing both the feature columns and target_col.
361
+ target_col (str): name of the column to train against.
362
+ feature_names (list): subset of feature columns to train on; omit for all.
363
+ (The full sample width is always uploaded — feature_names only tells
364
+ the surrogate which columns to actually fit on.)
365
+ task (str): specifies the modeling task, one of "classification," "regression," or "auto".
366
+ test_size (float): fraction of the data to set aside for test data, defaults to 0.2.
367
+ schema_name (str): name to store the inferred schema under.
368
+ immutable_cols (list): passed through to get_schema().
369
+ name (str): an optional name for the surrogate.
370
+ comments (str): an optional comment string for the surrogate.
371
+ Returns:
372
+ JSON object denoting surrogate training result, or None if schema upload
373
+ or training failed.
374
+ """
375
+ df = data if isinstance(data, pd.DataFrame) else pd.read_csv(data)
376
+ target = df[target_col]
377
+ features_df = df.drop(columns=[target_col])
378
+
379
+ schema = get_schema(features_df, immutable_cols=immutable_cols)
380
+ if put_schema(schema, name=schema_name) is None:
381
+ return None
382
+
383
+ return train_surrogate(
384
+ samples=features_df.values.tolist(),
385
+ predictions=target.tolist(),
386
+ feature_names=feature_names,
387
+ task=task,
388
+ test_size=test_size,
389
+ schema_name=schema_name,
390
+ name=name,
391
+ comments=comments,
392
+ )
393
+
394
+
395
+ def export_surrogate(version: str) -> SurrogateExport | None:
396
+ """
397
+ Exports a surrogate model — classes, intercept, per_class_intercepts, features,
398
+ scalers, etc.; everything required to reconstruct the surrogate. Score it locally,
399
+ with zero sklearn, via proxyml_core.export.predict_from_export(export, sample).
400
+
331
401
  Args:
332
402
  version (str): name of the surrogate to export
333
403
  Returns:
334
- JSON object representing the surrogate, or None if an error occurred.
404
+ SurrogateExport, or None if an error occurred.
335
405
  """
336
406
  r = get(endpoint=f'/surrogate/models/{version}/export', params=dict())
337
407
  if r.status_code == 200:
338
- return r.json()
339
- logger.error(
340
- "Surrogate export failed (version=%s, status=%s): %s",
341
- version, r.status_code, r.text,
408
+ return SurrogateExport.from_dict(r.json())
409
+ logger.error(
410
+ "Surrogate export failed (version=%s, status=%s): %s",
411
+ version, r.status_code, r.text,
342
412
  )
343
413
  return None
344
414
 
@@ -560,18 +630,21 @@ def get_model_summary(version: str | None = None) -> Any:
560
630
  return None
561
631
 
562
632
 
563
- def get_model_schema(version: str) -> Any:
633
+ def get_model_schema(version: str) -> FeatureSchema | None:
564
634
  """
565
635
  Retrieves the schema associated with a particular surrogate.
566
-
636
+
567
637
  Args:
568
638
  version (str): name of the surrogate model.
569
639
  Returns:
570
- JSON object representation of the model's schema, or None if an error occurred.
640
+ The model's FeatureSchema, or None if an error occurred.
571
641
  """
572
642
  r = get(endpoint=f'/surrogate/models/{version}/schema', params=dict())
573
643
  if r.status_code == 200:
574
- return r.json()
644
+ payload = r.json()
645
+ if payload.get('schema_warning'):
646
+ logger.warning(payload['schema_warning'])
647
+ return FeatureSchema.from_dict(payload)
575
648
  logger.error(
576
649
  "Schema retrieval failed with status %s: %s",
577
650
  r.status_code,
@@ -0,0 +1,26 @@
1
+ try:
2
+ import proxyml_core.modeling # noqa: F401
3
+ except ImportError as exc:
4
+ raise ImportError(
5
+ "Local challenger training requires the 'local' extra: pip install 'proxyml[local]'"
6
+ ) from exc
7
+
8
+ from proxyml.local.challenger import (
9
+ LADDERS,
10
+ Complexity,
11
+ Rung,
12
+ TrainedChallenger,
13
+ score_champion,
14
+ train_auto_challenger,
15
+ train_challenger,
16
+ )
17
+
18
+ __all__ = [
19
+ "train_challenger",
20
+ "train_auto_challenger",
21
+ "score_champion",
22
+ "Complexity",
23
+ "Rung",
24
+ "TrainedChallenger",
25
+ "LADDERS",
26
+ ]