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 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)