proxyml 0.1.6__tar.gz → 0.1.7__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.1.6
3
+ Version: 0.1.7
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -223,6 +223,8 @@ Python SDK for the [ProxyML API](https://proxyml.ai).
223
223
  > **Status:** Early access — server endpoints coming soon.
224
224
  > [Request early access](mailto:contact@proxyml.ai) or star this repo to follow progress.
225
225
 
226
+ <img width="577" height="115" alt="creditg_car_counterfactual" src="https://github.com/user-attachments/assets/8f913c95-1190-4b3c-ba7d-6690e9ebb3e0" />
227
+
226
228
  ## Why ProxyML?
227
229
 
228
230
  Most explainability tools require sending your data to a third-party server.
@@ -5,6 +5,8 @@ Python SDK for the [ProxyML API](https://proxyml.ai).
5
5
  > **Status:** Early access — server endpoints coming soon.
6
6
  > [Request early access](mailto:contact@proxyml.ai) or star this repo to follow progress.
7
7
 
8
+ <img width="577" height="115" alt="creditg_car_counterfactual" src="https://github.com/user-attachments/assets/8f913c95-1190-4b3c-ba7d-6690e9ebb3e0" />
9
+
8
10
  ## Why ProxyML?
9
11
 
10
12
  Most explainability tools require sending your data to a third-party server.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proxyml"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "Python SDK for calling the ProxyML API"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  logger = logging.getLogger(__name__)
3
+ from typing import Any
3
4
 
4
5
  import requests
5
6
  import os
@@ -14,6 +15,9 @@ _BOOL_STRINGS = {"true", "false"}
14
15
 
15
16
 
16
17
  def _headers() -> dict:
18
+ """
19
+ Constructs the request headers required for making calls to the ProxyML API.
20
+ """
17
21
  api_key = os.getenv("PROXYML_API_KEY", "")
18
22
  if not api_key:
19
23
  raise EnvironmentError(
@@ -27,6 +31,16 @@ def _headers() -> dict:
27
31
 
28
32
 
29
33
  def post(endpoint: str, payload: dict) -> requests.models.Response:
34
+ """
35
+ POSTs a request to a ProxyML API endpoint.
36
+
37
+ Args:
38
+ endpoint (str): ProxyML API endpoint. PROXYML_BASE_URL is prepended e.g. use endpoint='/schemas' not 'https://api.proxyml.ai/api/v1/schemas'.
39
+ payload (dict): JSON payload to POST.
40
+
41
+ Returns:
42
+ requests Response object.
43
+ """
30
44
  r = requests.post(
31
45
  url=f'{PROXYML_BASE_URL}{endpoint}',
32
46
  json=payload,
@@ -36,6 +50,16 @@ def post(endpoint: str, payload: dict) -> requests.models.Response:
36
50
 
37
51
 
38
52
  def put(endpoint: str, payload: dict) -> requests.models.Response:
53
+ """
54
+ PUTs a request to a ProxyML API endpoint.
55
+
56
+ Args:
57
+ endpoint (str): ProxyML API endpoint. PROXYML_BASE_URL is prepended e.g. use endpoint='/schemas' not 'https://api.proxyml.ai/api/v1/schemas'.
58
+ payload (dict): JSON payload to POST.
59
+
60
+ Returns:
61
+ requests Response object.
62
+ """
39
63
  r = requests.put(
40
64
  url=f'{PROXYML_BASE_URL}{endpoint}',
41
65
  json=payload,
@@ -45,6 +69,16 @@ def put(endpoint: str, payload: dict) -> requests.models.Response:
45
69
 
46
70
 
47
71
  def get(endpoint: str, params: dict) -> requests.models.Response:
72
+ """
73
+ GETs a request to a ProxyML API endpoint.
74
+
75
+ Args:
76
+ endpoint (str): ProxyML API endpoint. PROXYML_BASE_URL is prepended e.g. use endpoint='/schemas' not 'https://api.proxyml.ai/api/v1/schemas'.
77
+ params (dict): JSON payload to POST.
78
+
79
+ Returns:
80
+ requests Response object.
81
+ """
48
82
  r = requests.get(
49
83
  url=f'{PROXYML_BASE_URL}{endpoint}',
50
84
  headers=_headers(),
@@ -54,6 +88,15 @@ def get(endpoint: str, params: dict) -> requests.models.Response:
54
88
 
55
89
 
56
90
  def delete(endpoint: str) -> requests.models.Response:
91
+ """
92
+ DELETE request to a ProxyML API endpoint.
93
+
94
+ Args:
95
+ endpoint (str): ProxyML API endpoint. PROXYML_BASE_URL is prepended e.g. use endpoint='/schemas' not 'https://api.proxyml.ai/api/v1/schemas'.
96
+
97
+ Returns:
98
+ requests Response object.
99
+ """
57
100
  r = requests.delete(
58
101
  url=f'{PROXYML_BASE_URL}{endpoint}',
59
102
  headers=_headers()
@@ -61,7 +104,16 @@ def delete(endpoint: str) -> requests.models.Response:
61
104
  return r
62
105
 
63
106
 
64
- def put_schema(schema: dict, name: str = "default"):
107
+ def put_schema(schema: dict, name: str = "default") -> Any:
108
+ """
109
+ Uploads a data schema.
110
+
111
+ Args:
112
+ schema (dict): data schema object
113
+ name (str): optional name for the schema, defaults to "default".
114
+ Returns:
115
+ API response JSON object, or None if the return code was not 200.
116
+ """
65
117
  r = put(endpoint=f'/schema/{name}', payload=schema)
66
118
  if r.status_code == 200:
67
119
  logger.info("Schema '%s' uploaded successfully", name)
@@ -117,6 +169,15 @@ def delete_schema(name: str) -> bool:
117
169
 
118
170
 
119
171
  def _cast_column(series: pd.Series, ftype: str) -> pd.Series:
172
+ """
173
+ Casts a pandas Series to a Python type based on a specified ftype.
174
+
175
+ Args:
176
+ series (pd.Series): data to cast
177
+ ftype (str): type of conversion, one of "continuous", "count", "numeric_ordinal", "categorical", or "categorical_ordinal".
178
+ Returns:
179
+ pd.Series cast to specific ftype.
180
+ """
120
181
  if ftype in ("continuous",):
121
182
  return series.astype(float)
122
183
  if ftype in ("count", "numeric_ordinal"):
@@ -129,7 +190,18 @@ def _cast_column(series: pd.Series, ftype: str) -> pd.Series:
129
190
  return series
130
191
 
131
192
 
132
- def synthesize_data(num_points: int = 100, sample: list | None = None, as_df: bool = True, schema_name: str = "default"):
193
+ def synthesize_data(num_points: int = 100, sample: list | None = None, as_df: bool = True, schema_name: str = "default") -> Any:
194
+ """
195
+ Synthesizes data based on a data schema.
196
+
197
+ Args:
198
+ num_points (int): number of samples to synthesize, defaults to 100.
199
+ sample (list): if specified, synthesized data will be a blend of samples generated from the schema and a number of perturbations around this sample. If not specified (default), all synthesized data is generated by sampling from the data schema.
200
+ as_df (bool): if True (default), data are returned as a pandas DataFrame.
201
+ schema_name (str): name of the data schema to use to generate the data, defaults to "default."
202
+ Returns:
203
+ JSON object representing the synthesized data if as_df=False, a pandas DataFrame if as_df=True, or None if an error occurred.
204
+ """
133
205
  if sample is None:
134
206
  r = post(endpoint='/synthesize/neighbors', payload={'n': num_points, 'schema_name': schema_name})
135
207
  else:
@@ -159,7 +231,22 @@ def train_surrogate(
159
231
  schema_name: str = "default",
160
232
  name: str | None = None,
161
233
  comments: str | None = None,
162
- ):
234
+ ) -> Any:
235
+ """
236
+ Trains a surrogate model to predict a "black box" model's predictions.
237
+
238
+ Args:
239
+ samples (list): list of samples that were provided to the black box for inference, e.g. the output from synthesize_data().
240
+ predictions (list): the inferences on the samples, made by the black box model.
241
+ feature_names (list): names of the features (columns) in the data.
242
+ task (str): specifies the modeling task, one of "classification," "regression," or "auto" in which case ProxyML will attempt to automatically determine the modeling task.
243
+ test_size (float): fraction of the data to set aside for test data, defaults to 0.2.
244
+ schema_name (str): name of the data schema to use, defaults to "default."
245
+ name (str): an optional name for the surrogate.
246
+ comments (str): an optional comment string for the surrogate.
247
+ Returns:
248
+ JSON object denoting surrogate training result, or None if an error occurred.
249
+ """
163
250
  payload = {
164
251
  'samples': samples,
165
252
  'predictions': predictions,
@@ -183,7 +270,16 @@ def train_surrogate(
183
270
  return None
184
271
 
185
272
 
186
- def predict(sample: list, version: str | None = None):
273
+ def predict(sample: list, version: str | None = None) -> Any:
274
+ """
275
+ Calls a surrogate model to make a single prediction.
276
+
277
+ Args:
278
+ sample (list): a single sample
279
+ version (str): name of the surrogate model to use.
280
+ Returns:
281
+ JSON object denoting result of the surrogate prediction, or None if an error occurred.
282
+ """
187
283
  payload = {'inputs': sample}
188
284
  if version is not None:
189
285
  payload['version'] = version
@@ -198,7 +294,21 @@ def predict(sample: list, version: str | None = None):
198
294
  return None
199
295
 
200
296
 
201
- def find_counterfactual(sample, target, n_neighbors: int = 10000, perturbation_scale: float = 0.1, version: str | None = None, as_df: bool = True):
297
+ def find_counterfactual(sample, target, n_neighbors: int = 10000, perturbation_scale: float = 0.1, version: str | None = None, as_df: bool = True) -> Any:
298
+ """
299
+ Attempts to find a counterfactual sample: a sample that is near to a given sample in featurespace, but with a specified prediction. Counterfactuals are
300
+ not guaranteed i.e., it's possible to not be able to find a nearby sample with the desired label / target value.
301
+
302
+ Args:
303
+ sample (list): point around which neighbors will be found.
304
+ target (Any): the desired prediction outcome, can be a string or number for classification or a target predicted value for regression.
305
+ n_neighbors (int): the number of neighbors to search, defaults to 10000.
306
+ pertubation_scale (float): controls the "wiggle" in the features around the provided sample. Defaults to 0.1.
307
+ version (str): name of the surrogate model to run, defaults to "default."
308
+ as_df (bool): if True, return the counterfactual as a dataframe. If False, returns JSON object.
309
+ Returns:
310
+ JSON counterfactual if as_df=False, pandas DataFrame if as_df=True, None if an error occurred.
311
+ """
202
312
  payload = {
203
313
  'instance': sample,
204
314
  'target_label': target.item() if hasattr(target, 'item') else target,
@@ -233,6 +343,17 @@ def interpret_counterfactual(
233
343
  prediction_changed: bool,
234
344
  exclude_from_diff: list[str] | None
235
345
  ) -> str:
346
+ """
347
+ Simple string interpretation of a counterfactual result. No API calls are required.
348
+
349
+ Args:
350
+ sample (dict): the original prediction i.e. the sample from which counterfactuals were drawn.
351
+ counterfactual (dict): the counterfactual returned by ProxyML.
352
+ prediction_changed (bool): True if the prediction changed, False if it did not (or didn't change as much as desired).
353
+ exclude_from_diff (list): list of keys that should not be considered in interpreting the counterfactual.
354
+ Returns:
355
+ String summarizing the differences (if any) observed between sample and counterfactual.
356
+ """
236
357
  if not exclude_from_diff:
237
358
  exclude_from_diff = list()
238
359
  diffs = {
@@ -264,7 +385,16 @@ def interpret_counterfactual(
264
385
  )
265
386
 
266
387
 
267
- def predict_batch(samples: list[list], version: str | None = None):
388
+ def predict_batch(samples: list[list], version: str | None = None) -> Any:
389
+ """
390
+ Calls a surrogate model to make batch predictions.
391
+
392
+ Args:
393
+ sample (list): a list of samples
394
+ version (str): name of the surrogate model to use.
395
+ Returns:
396
+ JSON object denoting result of the surrogate prediction, or None if an error occurred.
397
+ """
268
398
  payload = {'inputs': samples}
269
399
  if version is not None:
270
400
  payload['version'] = version
@@ -286,7 +416,21 @@ def find_counterfactuals(
286
416
  perturbation_scale: float = 0.1,
287
417
  version: str | None = None,
288
418
  as_dfs: bool = True,
289
- ):
419
+ ) -> Any:
420
+ """
421
+ Attempts to find counterfactual samples: samples that are near a given sample in featurespace, but with a specified prediction. Counterfactuals are
422
+ not guaranteed i.e., it's possible to not be able to find a nearby sample with the desired label / target value.
423
+
424
+ Args:
425
+ samples (list): list of points around which neighbors will be found.
426
+ target (Any): the desired prediction outcome, can be a string or number for classification or a target predicted value for regression.
427
+ n_neighbors (int): the number of neighbors to search, defaults to 10000.
428
+ pertubation_scale (float): controls the "wiggle" in the features around the provided sample. Defaults to 0.1.
429
+ version (str): name of the surrogate model to run, defaults to "default."
430
+ as_df (bool): if True, return the counterfactual as a dataframe. If False, returns JSON object.
431
+ Returns:
432
+ JSON counterfactuals if as_df=False, pandas DataFrame if as_df=True, None if an error occurred.
433
+ """
290
434
  payload = {
291
435
  'instances': samples,
292
436
  'target_label': target.item() if hasattr(target, 'item') else target,
@@ -322,7 +466,15 @@ def find_counterfactuals(
322
466
  return None
323
467
 
324
468
 
325
- def get_model_summary(version: str | None = None):
469
+ def get_model_summary(version: str | None = None) -> Any:
470
+ """
471
+ Retrieves a summary of a given surrogate.
472
+
473
+ Args:
474
+ version (str): name of the surrogate model to retrieve.
475
+ Returns:
476
+ JSON object summarizing the surrogate, or None if an error occurred.
477
+ """
326
478
  params = {'version': version} if version is not None else {}
327
479
  r = get(endpoint='/explain/summary', params=params)
328
480
  if r.status_code == 200:
@@ -335,7 +487,37 @@ def get_model_summary(version: str | None = None):
335
487
  return None
336
488
 
337
489
 
338
- def diff_models(version_a: str, version_b: str):
490
+ def get_model_schema(version: str) -> Any:
491
+ """
492
+ Retrieves the schema associated with a particular surrogate.
493
+
494
+ Args:
495
+ version (str): name of the surrogate model.
496
+ Returns:
497
+ JSON object representation of the model's schema, or None if an error occurred.
498
+ """
499
+ r = get(endpoint=f'/surrogate/models/{version}/schema', params=dict())
500
+ if r.status_code == 200:
501
+ return r.json()
502
+ logger.error(
503
+ "Schema retrieval failed with status %s: %s",
504
+ r.status_code,
505
+ r.text,
506
+ )
507
+ return None
508
+
509
+
510
+ def diff_models(version_a: str, version_b: str) -> Any:
511
+ """
512
+ Returns a summary of the differences between two surrogates. The surrogates must have the same model task (i.e. a classification model can't be compared with a regression model), and they must
513
+ have at least one feature in common.
514
+
515
+ Args:
516
+ version_a (str): name of one surrogate
517
+ version_b (str): name of the other surrogate
518
+ Returns:
519
+ JSON object summarizing the difference between version_a and version_b, or None if an error occurred.
520
+ """
339
521
  r = get(endpoint='/explain/diff', params={'version_a': version_a, 'version_b': version_b})
340
522
  if r.status_code == 200:
341
523
  return r.json()
@@ -347,7 +529,15 @@ def diff_models(version_a: str, version_b: str):
347
529
  return None
348
530
 
349
531
 
350
- def get_feature_importances(version: str | None = None):
532
+ def get_feature_importances(version: str | None = None) -> Any:
533
+ """
534
+ Convenience function to directly retrieve feature importances from a surrogate model.
535
+
536
+ Args:
537
+ version (str): ID of the surrogate
538
+ Returns:
539
+ JSON object of feature importances, or None if an error occurred.
540
+ """
351
541
  params = {'version': version} if version is not None else {}
352
542
  r = get(endpoint='/explain/importance', params=params)
353
543
  if r.status_code == 200:
@@ -8,6 +8,15 @@ from pandas.api.types import is_float_dtype, is_integer_dtype
8
8
 
9
9
 
10
10
  def gen_continuous_schema(s: pd.Series, name: str | None = None) -> dict:
11
+ """
12
+ Generates a schema entry for continuous data.
13
+
14
+ Args:
15
+ s (pandas Series): data to characterize
16
+ name (str): optional name for the entry, defaults to s.name.
17
+ Returns:
18
+ schema dict
19
+ """
11
20
  return {
12
21
  'type': 'continuous',
13
22
  'name': name or s.name,
@@ -19,6 +28,15 @@ def gen_continuous_schema(s: pd.Series, name: str | None = None) -> dict:
19
28
 
20
29
 
21
30
  def gen_categorical_schema(s: pd.Series, name: str | None = None) -> dict:
31
+ """
32
+ Generates a schema entry for categorical data.
33
+
34
+ Args:
35
+ s (pandas Series): data to characterize
36
+ name (str): optional name for the entry, defaults to s.name.
37
+ Returns:
38
+ schema dict
39
+ """
22
40
  counts = s.value_counts(normalize=True)
23
41
  return {
24
42
  'type': 'categorical',
@@ -28,6 +46,15 @@ def gen_categorical_schema(s: pd.Series, name: str | None = None) -> dict:
28
46
 
29
47
 
30
48
  def gen_discrete_schema(s: pd.Series, name: str | None = None) -> dict:
49
+ """
50
+ Generates a schema entry for discrete data.
51
+
52
+ Args:
53
+ s (pandas Series): data to characterize
54
+ name (str): optional name for the entry, defaults to s.name.
55
+ Returns:
56
+ schema dict
57
+ """
31
58
  return {
32
59
  'type': 'count',
33
60
  'name': name or s.name,
@@ -37,6 +64,15 @@ def gen_discrete_schema(s: pd.Series, name: str | None = None) -> dict:
37
64
 
38
65
 
39
66
  def get_schema(df: pd.DataFrame, immutable_cols: list[str] | None) -> dict:
67
+ """
68
+ Generates a data schema for a pandas DataFrame, based on the data types of the columns.
69
+
70
+ Args:
71
+ df (pandas DataFrame): data to characterize
72
+ immutable_cols (list): list of columns to consider immutable. These columns will have schema entries, but the surrogate will _not_ use them for inference.
73
+ Returns:
74
+ data schema as dict. Review and adjust as needed.
75
+ """
40
76
  schema = {
41
77
  'features': list(),
42
78
  '_note': (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyml
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Python SDK for calling the ProxyML API
5
5
  Author-email: ProxyML <contact@proxyml.ai>
6
6
  License: Apache License
@@ -223,6 +223,8 @@ Python SDK for the [ProxyML API](https://proxyml.ai).
223
223
  > **Status:** Early access — server endpoints coming soon.
224
224
  > [Request early access](mailto:contact@proxyml.ai) or star this repo to follow progress.
225
225
 
226
+ <img width="577" height="115" alt="creditg_car_counterfactual" src="https://github.com/user-attachments/assets/8f913c95-1190-4b3c-ba7d-6690e9ebb3e0" />
227
+
226
228
  ## Why ProxyML?
227
229
 
228
230
  Most explainability tools require sending your data to a third-party server.
File without changes
File without changes
File without changes
File without changes
File without changes