potatopt 1.6.0__py3-none-any.whl
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.
- potatopt/__init__.py +227 -0
- potatopt/_lazy.py +90 -0
- potatopt/_utils.py +162 -0
- potatopt/analysis.py +320 -0
- potatopt/calibration.py +163 -0
- potatopt/constants.py +118 -0
- potatopt/data.py +785 -0
- potatopt/drift.py +342 -0
- potatopt/engine.py +1949 -0
- potatopt/mcp_server.py +349 -0
- potatopt/py.typed +1 -0
- potatopt/quality.py +623 -0
- potatopt/reliability.py +416 -0
- potatopt/spc.py +610 -0
- potatopt-1.6.0.dist-info/METADATA +485 -0
- potatopt-1.6.0.dist-info/RECORD +20 -0
- potatopt-1.6.0.dist-info/WHEEL +5 -0
- potatopt-1.6.0.dist-info/entry_points.txt +2 -0
- potatopt-1.6.0.dist-info/licenses/LICENSE +21 -0
- potatopt-1.6.0.dist-info/top_level.txt +1 -0
potatopt/__init__.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# PotatOpt: Universal Potato-Optimized Industrial AI Engine (OOP Architecture)
|
|
3
|
+
# Designed for Low-Resource, CPU-Friendly Machine Learning & Smart Manufacturing
|
|
4
|
+
# ==============================================================================
|
|
5
|
+
"""
|
|
6
|
+
PotatOpt is a zero-code, production-grade Machine Learning engine designed
|
|
7
|
+
specifically for Industrial Engineering (IE) and Smart Factory environments.
|
|
8
|
+
|
|
9
|
+
Key Architecture Principles:
|
|
10
|
+
----------------------------
|
|
11
|
+
1. LowSpecML (Potato Hardware First):
|
|
12
|
+
Minimizes RAM and CPU overhead using lossless numerical downcasting and
|
|
13
|
+
ordinal encoding instead of memory-heavy one-hot matrices.
|
|
14
|
+
|
|
15
|
+
2. Production Guardrails:
|
|
16
|
+
Automatically detects extreme class imbalance and seamlessly transitions
|
|
17
|
+
to unsupervised Anomaly Detection (Isolation Forest) when defects are too rare (< 5 samples).
|
|
18
|
+
|
|
19
|
+
3. Zero Data Leakage:
|
|
20
|
+
Strictly learns statistical boundaries, imputations, and encodings during `fit()`,
|
|
21
|
+
and applies them idempotently during `transform()` inference.
|
|
22
|
+
|
|
23
|
+
4. Industrial Financial Layer:
|
|
24
|
+
Integrates Cost of Quality (CoQ) equations directly into threshold tuning,
|
|
25
|
+
maximizing ROI by balancing scrap costs against false-alarm inspection labor.
|
|
26
|
+
|
|
27
|
+
5. ISO 9001 Traceability:
|
|
28
|
+
Serializes each model alongside a SHA-256 integrity hash and a metadata
|
|
29
|
+
sidecar, so a deployed file can be shown to be the file that was released.
|
|
30
|
+
That is an integrity record, not a signature - it detects a corrupted or
|
|
31
|
+
swapped file but cannot prove who produced one. See `PotatOptEngine.save()`.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
__version__ = "1.6.0"
|
|
37
|
+
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
# The redundant `x as x` form marks a deliberate re-export rather than an unused
|
|
41
|
+
# import. These four are private, so they cannot be declared in __all__, but the
|
|
42
|
+
# test suite reaches for them through the package object and they have to resolve.
|
|
43
|
+
from ._lazy import _load_automl as _load_automl
|
|
44
|
+
from ._lazy import _load_shap as _load_shap
|
|
45
|
+
from ._lazy import _quiet_dependency_warnings as _quiet_dependency_warnings
|
|
46
|
+
from ._lazy import logger
|
|
47
|
+
from ._utils import (
|
|
48
|
+
enable_audit_log,
|
|
49
|
+
get_library_versions,
|
|
50
|
+
to_jsonable,
|
|
51
|
+
)
|
|
52
|
+
from .analysis import (
|
|
53
|
+
auto_analyze,
|
|
54
|
+
run_seed_sweep,
|
|
55
|
+
)
|
|
56
|
+
from .calibration import (
|
|
57
|
+
check_calibration,
|
|
58
|
+
)
|
|
59
|
+
from .constants import (
|
|
60
|
+
AUTOCORRELATION_WARN,
|
|
61
|
+
CALIBRATION_DEFAULT_BINS,
|
|
62
|
+
CALIBRATION_ECE_LIMIT,
|
|
63
|
+
CAPABILITY_OUTLIER_RATE_LIMIT,
|
|
64
|
+
CAPABILITY_SIGMA_RATIO_LIMIT,
|
|
65
|
+
CONTROL_RULE_DESCRIPTIONS,
|
|
66
|
+
CONTROL_RULES_NELSON,
|
|
67
|
+
CONTROL_RULES_WESTERN_ELECTRIC,
|
|
68
|
+
CUSUM_DEFAULT_DECISION,
|
|
69
|
+
CUSUM_DEFAULT_SLACK,
|
|
70
|
+
DEFAULT_RANDOM_STATE,
|
|
71
|
+
DQS_PRODUCTION_READY,
|
|
72
|
+
DQS_USABLE,
|
|
73
|
+
DQS_WEIGHTS,
|
|
74
|
+
DRIFT_MIN_ROWS,
|
|
75
|
+
DRIFT_NOISE_SIGMAS,
|
|
76
|
+
EWMA_DEFAULT_LAMBDA,
|
|
77
|
+
EWMA_DEFAULT_SIGMAS,
|
|
78
|
+
MIN_TRAIN_ROWS,
|
|
79
|
+
MISSING_SCHEMA_WARN_RATIO,
|
|
80
|
+
MODIFIED_ZSCORE_THRESHOLD,
|
|
81
|
+
MOVING_RANGE_D2,
|
|
82
|
+
NUMERIC_SENTINELS,
|
|
83
|
+
OEE_WORLD_CLASS,
|
|
84
|
+
OUT_OF_BOUNDS_WARN_RATIO,
|
|
85
|
+
PARETO_CUTOFF,
|
|
86
|
+
PSI_DEFAULT_BINS,
|
|
87
|
+
PSI_MAJOR_SHIFT,
|
|
88
|
+
PSI_MAX_CATEGORIES,
|
|
89
|
+
PSI_MODERATE_SHIFT,
|
|
90
|
+
SEED_SWEEP_DEFAULT,
|
|
91
|
+
SILENT_NULL_TOKENS,
|
|
92
|
+
)
|
|
93
|
+
from .data import (
|
|
94
|
+
audit_data_quality,
|
|
95
|
+
calculate_correlations,
|
|
96
|
+
detect_outliers,
|
|
97
|
+
detect_silent_nulls,
|
|
98
|
+
inspect_data,
|
|
99
|
+
split_data,
|
|
100
|
+
split_data_three_way,
|
|
101
|
+
)
|
|
102
|
+
from .drift import (
|
|
103
|
+
calculate_categorical_psi,
|
|
104
|
+
calculate_psi,
|
|
105
|
+
check_asset_drift,
|
|
106
|
+
check_data_drift,
|
|
107
|
+
)
|
|
108
|
+
from .engine import (
|
|
109
|
+
PotatOpt,
|
|
110
|
+
PotatOptEngine,
|
|
111
|
+
)
|
|
112
|
+
from .quality import (
|
|
113
|
+
calculate_capability,
|
|
114
|
+
calculate_gauge_rr,
|
|
115
|
+
)
|
|
116
|
+
from .reliability import (
|
|
117
|
+
calculate_availability,
|
|
118
|
+
calculate_maintenance_savings,
|
|
119
|
+
calculate_mtbf,
|
|
120
|
+
calculate_mttr,
|
|
121
|
+
calculate_oee,
|
|
122
|
+
calculate_pareto,
|
|
123
|
+
wilson_confidence_interval,
|
|
124
|
+
)
|
|
125
|
+
from .spc import _lag1_autocorrelation as _lag1_autocorrelation
|
|
126
|
+
from .spc import (
|
|
127
|
+
calculate_control_rules,
|
|
128
|
+
calculate_cusum_chart,
|
|
129
|
+
calculate_ewma_chart,
|
|
130
|
+
calculate_spc_limits,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def __getattr__(name: str) -> Any:
|
|
135
|
+
"""
|
|
136
|
+
PEP 562 module-level attribute access.
|
|
137
|
+
|
|
138
|
+
Keeps `potatopt.AutoML` and `potatopt.shap` resolvable for callers and tests
|
|
139
|
+
that reach for them directly; they simply trigger the import on first touch
|
|
140
|
+
instead of at module load.
|
|
141
|
+
"""
|
|
142
|
+
if name == "AutoML":
|
|
143
|
+
return _load_automl()
|
|
144
|
+
if name == "shap":
|
|
145
|
+
return _load_shap()
|
|
146
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# The supported public surface. Everything not listed here is an implementation
|
|
150
|
+
# detail and may change without notice - including the third-party modules this
|
|
151
|
+
# file imports, which would otherwise leak out as `potatopt.np`, `potatopt.shap`
|
|
152
|
+
# and so on.
|
|
153
|
+
__all__ = [ # noqa: RUF022
|
|
154
|
+
# Engine
|
|
155
|
+
"PotatOptEngine",
|
|
156
|
+
"PotatOpt",
|
|
157
|
+
# Data inspection, calibration and splitting
|
|
158
|
+
"inspect_data",
|
|
159
|
+
"calculate_correlations",
|
|
160
|
+
"split_data",
|
|
161
|
+
"split_data_three_way",
|
|
162
|
+
"auto_analyze",
|
|
163
|
+
"check_calibration",
|
|
164
|
+
"run_seed_sweep",
|
|
165
|
+
# Data quality
|
|
166
|
+
"audit_data_quality",
|
|
167
|
+
"detect_silent_nulls",
|
|
168
|
+
"detect_outliers",
|
|
169
|
+
# Statistical process control and drift
|
|
170
|
+
"calculate_spc_limits",
|
|
171
|
+
"calculate_ewma_chart",
|
|
172
|
+
"calculate_cusum_chart",
|
|
173
|
+
"calculate_control_rules",
|
|
174
|
+
"calculate_capability",
|
|
175
|
+
"calculate_gauge_rr",
|
|
176
|
+
"check_data_drift",
|
|
177
|
+
"check_asset_drift",
|
|
178
|
+
"calculate_psi",
|
|
179
|
+
"calculate_categorical_psi",
|
|
180
|
+
"wilson_confidence_interval",
|
|
181
|
+
"calculate_maintenance_savings",
|
|
182
|
+
# Reliability, availability and OEE
|
|
183
|
+
"calculate_mtbf",
|
|
184
|
+
"calculate_mttr",
|
|
185
|
+
"calculate_availability",
|
|
186
|
+
"calculate_oee",
|
|
187
|
+
"calculate_pareto",
|
|
188
|
+
# Interop and traceability
|
|
189
|
+
"to_jsonable",
|
|
190
|
+
"enable_audit_log",
|
|
191
|
+
"get_library_versions",
|
|
192
|
+
"logger",
|
|
193
|
+
"__version__",
|
|
194
|
+
# Tuning constants
|
|
195
|
+
"DEFAULT_RANDOM_STATE",
|
|
196
|
+
"SEED_SWEEP_DEFAULT",
|
|
197
|
+
"CALIBRATION_DEFAULT_BINS",
|
|
198
|
+
"CALIBRATION_ECE_LIMIT",
|
|
199
|
+
"CAPABILITY_SIGMA_RATIO_LIMIT",
|
|
200
|
+
"CAPABILITY_OUTLIER_RATE_LIMIT",
|
|
201
|
+
"MIN_TRAIN_ROWS",
|
|
202
|
+
"MISSING_SCHEMA_WARN_RATIO",
|
|
203
|
+
"OUT_OF_BOUNDS_WARN_RATIO",
|
|
204
|
+
"PSI_MODERATE_SHIFT",
|
|
205
|
+
"PSI_MAJOR_SHIFT",
|
|
206
|
+
"PSI_DEFAULT_BINS",
|
|
207
|
+
"PSI_MAX_CATEGORIES",
|
|
208
|
+
"DRIFT_MIN_ROWS",
|
|
209
|
+
"DRIFT_NOISE_SIGMAS",
|
|
210
|
+
"EWMA_DEFAULT_LAMBDA",
|
|
211
|
+
"EWMA_DEFAULT_SIGMAS",
|
|
212
|
+
"CUSUM_DEFAULT_SLACK",
|
|
213
|
+
"CUSUM_DEFAULT_DECISION",
|
|
214
|
+
"MOVING_RANGE_D2",
|
|
215
|
+
"AUTOCORRELATION_WARN",
|
|
216
|
+
"CONTROL_RULES_WESTERN_ELECTRIC",
|
|
217
|
+
"CONTROL_RULES_NELSON",
|
|
218
|
+
"CONTROL_RULE_DESCRIPTIONS",
|
|
219
|
+
"DQS_WEIGHTS",
|
|
220
|
+
"DQS_PRODUCTION_READY",
|
|
221
|
+
"DQS_USABLE",
|
|
222
|
+
"SILENT_NULL_TOKENS",
|
|
223
|
+
"NUMERIC_SENTINELS",
|
|
224
|
+
"MODIFIED_ZSCORE_THRESHOLD",
|
|
225
|
+
"PARETO_CUTOFF",
|
|
226
|
+
"OEE_WORLD_CLASS",
|
|
227
|
+
]
|
potatopt/_lazy.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib # contextmanager for _quiet_dependency_warnings() - scoped, not global, suppression
|
|
4
|
+
import logging # the "potatopt" audit logger every guardrail/warning flows through
|
|
5
|
+
import warnings # scoped suppression of third-party noise; see _quiet_dependency_warnings()
|
|
6
|
+
from collections.abc import Iterator # return annotation for the context manager below
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@contextlib.contextmanager
|
|
10
|
+
def _quiet_dependency_warnings() -> Iterator[None]:
|
|
11
|
+
"""
|
|
12
|
+
Silence third-party warning noise for the duration of ONE call into a dependency.
|
|
13
|
+
|
|
14
|
+
This module used to call `warnings.filterwarnings("ignore")` at import time. That
|
|
15
|
+
is a process-global mutation: merely writing `import potatopt` switched off every
|
|
16
|
+
warning in the importing application, including warnings raised by the caller's
|
|
17
|
+
own code, for the rest of the run. A library has no business doing that, so the
|
|
18
|
+
suppression now wraps only the specific calls that are noisy - FLAML's search and
|
|
19
|
+
SHAP's explainers - and the caller's filters are restored on the way out.
|
|
20
|
+
|
|
21
|
+
Known caveat: Python's warning filters are global to the process, not to the
|
|
22
|
+
thread, so while this block is open another thread's warnings are suppressed too.
|
|
23
|
+
That is a narrow, bounded window and is accepted deliberately; the alternative it
|
|
24
|
+
replaced suppressed them permanently.
|
|
25
|
+
"""
|
|
26
|
+
with warnings.catch_warnings():
|
|
27
|
+
warnings.simplefilter("ignore")
|
|
28
|
+
yield
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------------------
|
|
32
|
+
# Optional heavy backends, imported the first time they are actually needed.
|
|
33
|
+
#
|
|
34
|
+
# FLAML and SHAP together dominate both install size and import time, and neither
|
|
35
|
+
# is touched unless you call fit() or ask for explanations. Keeping them out of
|
|
36
|
+
# the module's import path is what lets the core install be numpy, pandas, scipy
|
|
37
|
+
# and scikit-learn - the difference between a library that runs on low-spec
|
|
38
|
+
# hardware and one that merely claims to.
|
|
39
|
+
# ------------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def _load_automl():
|
|
42
|
+
"""
|
|
43
|
+
Return FLAML's AutoML class, importing it on first use.
|
|
44
|
+
|
|
45
|
+
Cached into module globals so `potatopt.AutoML` and the class used inside
|
|
46
|
+
fit() are the same object - patching one patches the other.
|
|
47
|
+
"""
|
|
48
|
+
automl = globals().get("AutoML")
|
|
49
|
+
if automl is None:
|
|
50
|
+
try:
|
|
51
|
+
from flaml import AutoML as _AutoML
|
|
52
|
+
except ImportError as exc:
|
|
53
|
+
raise ImportError(
|
|
54
|
+
"PotatOptEngine.fit() needs the FLAML AutoML backend, which is not installed. "
|
|
55
|
+
"Install it with: pip install potatopt[automl]"
|
|
56
|
+
) from exc
|
|
57
|
+
globals()["AutoML"] = _AutoML
|
|
58
|
+
automl = _AutoML
|
|
59
|
+
return automl
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_shap():
|
|
63
|
+
"""
|
|
64
|
+
Return the shap module, importing it on first use.
|
|
65
|
+
|
|
66
|
+
Cached into module globals for the same reason as _load_automl.
|
|
67
|
+
"""
|
|
68
|
+
module = globals().get("shap")
|
|
69
|
+
if module is None:
|
|
70
|
+
try:
|
|
71
|
+
import shap as _shap
|
|
72
|
+
except ImportError as exc:
|
|
73
|
+
raise ImportError(
|
|
74
|
+
"get_shap_values() needs the SHAP explainability backend, which is not installed. "
|
|
75
|
+
"Install it with: pip install potatopt[xai]"
|
|
76
|
+
) from exc
|
|
77
|
+
globals()["shap"] = _shap
|
|
78
|
+
module = _shap
|
|
79
|
+
return module
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ISO 9001 audit trail: all guardrail events flow through one named logger so
|
|
83
|
+
# they can be silenced, redirected, or persisted to a file by the operator.
|
|
84
|
+
logger = logging.getLogger("potatopt")
|
|
85
|
+
|
|
86
|
+
logger.setLevel(logging.INFO)
|
|
87
|
+
if not logger.handlers:
|
|
88
|
+
_console_handler = logging.StreamHandler()
|
|
89
|
+
_console_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
|
90
|
+
logger.addHandler(_console_handler)
|
potatopt/_utils.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging # the "potatopt" audit logger every guardrail/warning flows through
|
|
4
|
+
import os # path handling in enable_audit_log
|
|
5
|
+
from datetime import datetime # UTC timestamps in to_jsonable
|
|
6
|
+
from importlib.metadata import PackageNotFoundError
|
|
7
|
+
from importlib.metadata import (
|
|
8
|
+
version as pkg_version, # get_library_versions() ISO 9001 traceability record
|
|
9
|
+
)
|
|
10
|
+
from typing import Any # loose return-type annotations for JSON-shaped dicts
|
|
11
|
+
|
|
12
|
+
import numpy as np # arrays + math for downcasting, NaN/finite checks in to_jsonable and _finite_float
|
|
13
|
+
import pandas as pd # DataFrame/Series is the data contract for every public function
|
|
14
|
+
|
|
15
|
+
from ._lazy import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_numeric_series(series: pd.Series) -> bool:
|
|
19
|
+
"""
|
|
20
|
+
np.issubdtype only understands numpy dtypes and raises TypeError on a pandas
|
|
21
|
+
extension dtype such as CategoricalDtype, so every dtype test in this module
|
|
22
|
+
goes through pandas' own predicate instead. A category column is categorical
|
|
23
|
+
whatever its categories hold, which is the answer this code wants anyway.
|
|
24
|
+
"""
|
|
25
|
+
return bool(pd.api.types.is_numeric_dtype(series))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _finite_float(value: Any, name: str) -> tuple[float | None, str | None]:
|
|
29
|
+
"""
|
|
30
|
+
Coerce a control-chart parameter to a finite float.
|
|
31
|
+
|
|
32
|
+
Returns `(value, None)` on success or `(None, message)` on failure, so the
|
|
33
|
+
caller can hand the message back in an error dict instead of raising - the
|
|
34
|
+
contract both chart functions document.
|
|
35
|
+
|
|
36
|
+
NaN and infinity are rejected explicitly because they slip past ordinary
|
|
37
|
+
comparisons: every comparison against NaN is False, so a plain `<= 0` guard
|
|
38
|
+
lets it through, and it then propagates silently into the limits.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
number = float(value)
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
return None, f"{name} must be a number, got {value!r}."
|
|
44
|
+
if not np.isfinite(number):
|
|
45
|
+
return None, f"{name} must be a finite number, got {value!r}."
|
|
46
|
+
return number, None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_frame(df: Any, name: str, columns: tuple[str, ...]) -> str | None:
|
|
50
|
+
"""
|
|
51
|
+
Return an error sentence if `df` is unusable, otherwise None.
|
|
52
|
+
|
|
53
|
+
Shared by the reliability metrics so a missing column reports which columns
|
|
54
|
+
the frame actually has - the same courtesy `auto_analyze` extends when the
|
|
55
|
+
target column is wrong, and the difference between a caller who can fix the
|
|
56
|
+
call and one who has to guess.
|
|
57
|
+
"""
|
|
58
|
+
if df is None or not isinstance(df, pd.DataFrame):
|
|
59
|
+
return f"{name} must be a pandas DataFrame."
|
|
60
|
+
if df.empty:
|
|
61
|
+
return f"{name} is empty."
|
|
62
|
+
missing = [c for c in columns if c not in df.columns]
|
|
63
|
+
if missing:
|
|
64
|
+
return f"{name} is missing column(s) {missing}. Columns present: {list(df.columns)}."
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def enable_audit_log(filepath: str = "potatopt_audit.log", level: int = logging.INFO) -> str | None:
|
|
69
|
+
"""
|
|
70
|
+
Persist every guardrail and drift event to a timestamped log file.
|
|
71
|
+
Required for ISO 9001 traceability: the operator must be able to prove
|
|
72
|
+
afterwards which production batches triggered a warning and when.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
abs_path = os.path.abspath(filepath)
|
|
76
|
+
for handler in logger.handlers:
|
|
77
|
+
if isinstance(handler, logging.FileHandler) and getattr(handler, "baseFilename", None) == abs_path:
|
|
78
|
+
return abs_path
|
|
79
|
+
|
|
80
|
+
parent_dir = os.path.dirname(abs_path)
|
|
81
|
+
if parent_dir:
|
|
82
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
file_handler = logging.FileHandler(abs_path, encoding="utf-8")
|
|
85
|
+
file_handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s"))
|
|
86
|
+
file_handler.setLevel(level)
|
|
87
|
+
logger.addHandler(file_handler)
|
|
88
|
+
return abs_path
|
|
89
|
+
except (OSError, ValueError, TypeError):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_library_versions() -> dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Collect installed versions of the runtime stack for ISO 9001 reproducibility.
|
|
96
|
+
"""
|
|
97
|
+
names = ["numpy", "pandas", "scipy", "scikit-learn", "flaml", "lightgbm", "xgboost", "shap", "joblib"]
|
|
98
|
+
versions = {}
|
|
99
|
+
for name in names:
|
|
100
|
+
try:
|
|
101
|
+
versions[name] = pkg_version(name)
|
|
102
|
+
except (PackageNotFoundError, ValueError, TypeError):
|
|
103
|
+
versions[name] = None
|
|
104
|
+
return versions
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def to_jsonable(value: Any) -> Any:
|
|
108
|
+
"""
|
|
109
|
+
Convert NumPy / pandas output into plain Python that `json.dumps` accepts.
|
|
110
|
+
|
|
111
|
+
Written for the tool-call boundary: an MCP server, a REST endpoint, or a
|
|
112
|
+
dashboard all need the engine's output as ordinary dicts, lists, numbers and
|
|
113
|
+
strings. Every stateless utility in this module already returns JSON-clean
|
|
114
|
+
dicts; this covers the remaining cases - `predict()` returns an ndarray,
|
|
115
|
+
`get_feature_importance()` returns a DataFrame - without changing either
|
|
116
|
+
method's contract.
|
|
117
|
+
|
|
118
|
+
NaN and Infinity become None rather than passing through, because
|
|
119
|
+
`json.dumps` would otherwise emit the bare tokens `NaN` / `Infinity`, which
|
|
120
|
+
are not valid JSON and are rejected by strict parsers such as
|
|
121
|
+
JavaScript's `JSON.parse`.
|
|
122
|
+
|
|
123
|
+
Anything it does not recognise is rendered with `str()` rather than raising,
|
|
124
|
+
so a serialisation boundary never brings down a request.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
--------
|
|
128
|
+
A structure built only from dict, list, str, int, float, bool and None.
|
|
129
|
+
"""
|
|
130
|
+
if value is None or isinstance(value, (str, bool)):
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
if isinstance(value, (np.bool_,)):
|
|
134
|
+
return bool(value)
|
|
135
|
+
|
|
136
|
+
if isinstance(value, (int, np.integer)):
|
|
137
|
+
return int(value)
|
|
138
|
+
|
|
139
|
+
if isinstance(value, (float, np.floating)):
|
|
140
|
+
as_float = float(value)
|
|
141
|
+
# NaN/Inf are not representable in JSON.
|
|
142
|
+
return as_float if np.isfinite(as_float) else None
|
|
143
|
+
|
|
144
|
+
if isinstance(value, (datetime,)):
|
|
145
|
+
return value.isoformat()
|
|
146
|
+
|
|
147
|
+
if isinstance(value, pd.DataFrame):
|
|
148
|
+
return [to_jsonable(record) for record in value.to_dict(orient="records")]
|
|
149
|
+
|
|
150
|
+
if isinstance(value, (pd.Series, pd.Index)):
|
|
151
|
+
return [to_jsonable(item) for item in value.tolist()]
|
|
152
|
+
|
|
153
|
+
if isinstance(value, np.ndarray):
|
|
154
|
+
return [to_jsonable(item) for item in value.tolist()]
|
|
155
|
+
|
|
156
|
+
if isinstance(value, dict):
|
|
157
|
+
return {str(key): to_jsonable(item) for key, item in value.items()}
|
|
158
|
+
|
|
159
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
160
|
+
return [to_jsonable(item) for item in value]
|
|
161
|
+
|
|
162
|
+
return str(value)
|