potatopt 1.6.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.
- potatopt-1.6.0/LICENSE +21 -0
- potatopt-1.6.0/PKG-INFO +485 -0
- potatopt-1.6.0/README.md +405 -0
- potatopt-1.6.0/potatopt/__init__.py +227 -0
- potatopt-1.6.0/potatopt/_lazy.py +90 -0
- potatopt-1.6.0/potatopt/_utils.py +162 -0
- potatopt-1.6.0/potatopt/analysis.py +320 -0
- potatopt-1.6.0/potatopt/calibration.py +163 -0
- potatopt-1.6.0/potatopt/constants.py +118 -0
- potatopt-1.6.0/potatopt/data.py +785 -0
- potatopt-1.6.0/potatopt/drift.py +342 -0
- potatopt-1.6.0/potatopt/engine.py +1949 -0
- potatopt-1.6.0/potatopt/mcp_server.py +349 -0
- potatopt-1.6.0/potatopt/py.typed +1 -0
- potatopt-1.6.0/potatopt/quality.py +623 -0
- potatopt-1.6.0/potatopt/reliability.py +416 -0
- potatopt-1.6.0/potatopt/spc.py +610 -0
- potatopt-1.6.0/potatopt.egg-info/PKG-INFO +485 -0
- potatopt-1.6.0/potatopt.egg-info/SOURCES.txt +27 -0
- potatopt-1.6.0/potatopt.egg-info/dependency_links.txt +1 -0
- potatopt-1.6.0/potatopt.egg-info/entry_points.txt +2 -0
- potatopt-1.6.0/potatopt.egg-info/requires.txt +39 -0
- potatopt-1.6.0/potatopt.egg-info/top_level.txt +1 -0
- potatopt-1.6.0/pyproject.toml +105 -0
- potatopt-1.6.0/setup.cfg +4 -0
- potatopt-1.6.0/tests/test_chart_engine.py +346 -0
- potatopt-1.6.0/tests/test_examples.py +160 -0
- potatopt-1.6.0/tests/test_mcp_server.py +329 -0
- potatopt-1.6.0/tests/test_potatopt.py +3303 -0
potatopt-1.6.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oak04K
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
potatopt-1.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: potatopt
|
|
3
|
+
Version: 1.6.0
|
|
4
|
+
Summary: Potato-optimized industrial ML for condition-based maintenance: AutoML, drift detection and cost-of-quality on CPU-only low-spec hardware.
|
|
5
|
+
Author: Oak04K
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Oak04K
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/Oak04K/PotatOpt-ML
|
|
29
|
+
Project-URL: Repository, https://github.com/Oak04K/PotatOpt-ML
|
|
30
|
+
Keywords: machine-learning,automl,predictive-maintenance,condition-monitoring,industrial-engineering,statistical-process-control,low-resource,mcp
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Intended Audience :: Manufacturing
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
43
|
+
Requires-Python: >=3.10
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
License-File: LICENSE
|
|
46
|
+
Requires-Dist: numpy>=1.24.0
|
|
47
|
+
Requires-Dist: pandas>=2.0.0
|
|
48
|
+
Requires-Dist: scipy>=1.10.0
|
|
49
|
+
Requires-Dist: scikit-learn>=1.3.0
|
|
50
|
+
Provides-Extra: automl
|
|
51
|
+
Requires-Dist: flaml>=2.1.0; extra == "automl"
|
|
52
|
+
Requires-Dist: lightgbm>=4.0.0; extra == "automl"
|
|
53
|
+
Requires-Dist: xgboost>=2.0.0; extra == "automl"
|
|
54
|
+
Provides-Extra: xai
|
|
55
|
+
Requires-Dist: shap>=0.42.0; extra == "xai"
|
|
56
|
+
Provides-Extra: viz
|
|
57
|
+
Requires-Dist: matplotlib>=3.7.0; extra == "viz"
|
|
58
|
+
Requires-Dist: seaborn>=0.12.0; extra == "viz"
|
|
59
|
+
Provides-Extra: mcp
|
|
60
|
+
Requires-Dist: mcp<3,>=2.1; extra == "mcp"
|
|
61
|
+
Provides-Extra: dev
|
|
62
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
63
|
+
Requires-Dist: ruff<0.17,>=0.16.0; extra == "dev"
|
|
64
|
+
Requires-Dist: psutil>=5.9.0; extra == "dev"
|
|
65
|
+
Requires-Dist: tiktoken>=0.7.0; extra == "dev"
|
|
66
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
67
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
68
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
69
|
+
Requires-Dist: mcp<3,>=2.1; extra == "dev"
|
|
70
|
+
Provides-Extra: all
|
|
71
|
+
Requires-Dist: flaml>=2.1.0; extra == "all"
|
|
72
|
+
Requires-Dist: lightgbm>=4.0.0; extra == "all"
|
|
73
|
+
Requires-Dist: xgboost>=2.0.0; extra == "all"
|
|
74
|
+
Requires-Dist: shap>=0.42.0; extra == "all"
|
|
75
|
+
Requires-Dist: matplotlib>=3.7.0; extra == "all"
|
|
76
|
+
Requires-Dist: seaborn>=0.12.0; extra == "all"
|
|
77
|
+
Requires-Dist: mcp<3,>=2.1; extra == "all"
|
|
78
|
+
Requires-Dist: httpx>=0.27; extra == "all"
|
|
79
|
+
Dynamic: license-file
|
|
80
|
+
|
|
81
|
+
# PotatOpt-ML 🥔
|
|
82
|
+
|
|
83
|
+
**Predictive maintenance that runs on a potato.**
|
|
84
|
+
|
|
85
|
+
[](https://github.com/Oak04K/PotatOpt-ML/actions/workflows/ci.yml)
|
|
86
|
+
[](https://www.python.org/)
|
|
87
|
+
[](CHANGELOG.md)
|
|
88
|
+
[](LICENSE)
|
|
89
|
+
[](#why-it-is-built-this-way)
|
|
90
|
+
|
|
91
|
+
A machine-learning library for **condition-based and predictive maintenance**.
|
|
92
|
+
Sensor readings from factory equipment go in; a maintenance decision, and what
|
|
93
|
+
that decision is worth in money, come out. CPU only, no GPU. It is meant to be
|
|
94
|
+
driven either by an engineer on the shop floor or by an AI agent, in as few
|
|
95
|
+
lines as possible.
|
|
96
|
+
|
|
97
|
+
[อ่านเอกสารฉบับภาษาไทย (ละเอียดกว่า)](README.th.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md)
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Install
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install potatopt # the four-package core
|
|
105
|
+
pip install "potatopt[automl]" # + FLAML, needed for .fit()
|
|
106
|
+
pip install "potatopt[automl,xai]" # + SHAP explanations
|
|
107
|
+
pip install "potatopt[mcp]" # + the MCP server, to drive it from an AI agent
|
|
108
|
+
pip install "potatopt[all]" # everything
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## The shortest path
|
|
112
|
+
|
|
113
|
+
If you only ever read one section, read this one. A single call runs the whole
|
|
114
|
+
job and returns one dictionary:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import potatopt as po
|
|
118
|
+
|
|
119
|
+
report = po.auto_analyze("machine_sensors.csv", target="failure")
|
|
120
|
+
|
|
121
|
+
print(report["metrics"]["f1"]) # how good the model is
|
|
122
|
+
print(report["cost"]["cost_savings"]) # the same answer, in money
|
|
123
|
+
print(report["top_features"]) # what drove it
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`auto_analyze()` audits data quality, splits the data three ways, searches for
|
|
127
|
+
a model, tunes the decision threshold on the **validation** set, scores the
|
|
128
|
+
untouched test set, converts the result to money, and ranks the features.
|
|
129
|
+
|
|
130
|
+
It returns one JSON-ready dictionary and **never raises**. A failure comes back
|
|
131
|
+
as `{"ok": False, "error": "..."}` with a sentence you can actually read.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## The working path
|
|
136
|
+
|
|
137
|
+
The rest of the library is the same job taken one question at a time, in the
|
|
138
|
+
order those questions have to be asked. Each stage assumes the stage above it
|
|
139
|
+
was answered; skipping ahead produces numbers that look fine and mean nothing.
|
|
140
|
+
|
|
141
|
+
`examples/tour.py` walks all of it end to end on simulated data in about a
|
|
142
|
+
minute, and a test fails if the tour ever falls behind the public API.
|
|
143
|
+
|
|
144
|
+
| # | Question | Module | Key functions |
|
|
145
|
+
|---|---|---|---|
|
|
146
|
+
| 1 | Can I trust this data? | `data.py` | `inspect_data`, `audit_data_quality`, `detect_silent_nulls`, `detect_outliers`, `calculate_correlations` |
|
|
147
|
+
| 2 | Can I even measure it? | `quality.py` | `calculate_gauge_rr` |
|
|
148
|
+
| 3 | Is the process stable? | `spc.py` | `calculate_ewma_chart`, `calculate_cusum_chart`, `calculate_control_rules` |
|
|
149
|
+
| 4 | Is it capable? | `quality.py` | `calculate_capability` |
|
|
150
|
+
| 5 | Has it drifted since? | `drift.py` | `check_data_drift`, `check_asset_drift`, `calculate_psi` |
|
|
151
|
+
| 6 | What is it costing me? | `reliability.py` | `calculate_mtbf`, `calculate_mttr`, `calculate_availability`, `calculate_oee`, `calculate_pareto` |
|
|
152
|
+
| 7 | Can a model help? | `engine.py`, `analysis.py`, `calibration.py` | `PotatOptEngine`, `auto_analyze`, `check_calibration`, `run_seed_sweep` |
|
|
153
|
+
|
|
154
|
+
### Stage 1 — Can I trust this data?
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
po.inspect_data(df, target_col="failure") # shape, types, imbalance, suggested task
|
|
158
|
+
po.audit_data_quality(df) # a 0-100 score across five dimensions
|
|
159
|
+
po.detect_silent_nulls(df) # "N/A", "-", "null", and sensor codes like -999
|
|
160
|
+
po.detect_outliers(df, "temperature") # modified z-score, robust to the outliers it hunts
|
|
161
|
+
po.calculate_correlations(df) # which sensors carry the same information
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The data-quality score weighs completeness, consistency, validity, uniqueness
|
|
165
|
+
and timeliness. `detect_silent_nulls` matters more than it sounds: a sensor
|
|
166
|
+
that writes `-999` when it fails is not missing data as far as pandas is
|
|
167
|
+
concerned, so the mean quietly moves and nothing warns you.
|
|
168
|
+
|
|
169
|
+
`calculate_correlations` also reports which columns it **skipped and why**,
|
|
170
|
+
rather than silently omitting them from the matrix.
|
|
171
|
+
|
|
172
|
+
### Stage 2 — Can I even measure it?
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
po.calculate_gauge_rr(readings, "part", "operator", "measurement")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Before judging a process, establish that the measurement system can tell two
|
|
179
|
+
parts apart. A capability figure computed through a gauge that contributes 40%
|
|
180
|
+
of the observed variation is describing the gauge, not the machine.
|
|
181
|
+
|
|
182
|
+
### Stage 3 — Is the process stable?
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
po.calculate_ewma_chart(readings, baseline_n=25) # exact time-varying limits
|
|
186
|
+
po.calculate_cusum_chart(readings, baseline_n=25) # two-sided tabular CUSUM
|
|
187
|
+
po.calculate_control_rules(series, rules=po.CONTROL_RULES_WESTERN_ELECTRIC)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Sigma is estimated from the **moving range divided by d2 (1.128)**, never from
|
|
191
|
+
the sample standard deviation. This is the single most important decision in
|
|
192
|
+
the module. A degrading series inflates its own standard deviation, which
|
|
193
|
+
widens the control limits and hides the very degradation the chart exists to
|
|
194
|
+
catch. Measured on a wear ramp: SD 2.201 against a moving-range sigma of 0.222,
|
|
195
|
+
and EWMA signalled at **sample 3 instead of sample 13**.
|
|
196
|
+
|
|
197
|
+
The moving range assumes consecutive readings are independent, and a sensor
|
|
198
|
+
that behaves like a random walk breaks that assumption the other way: adjacent
|
|
199
|
+
readings echo each other, sigma collapses, and the chart alarms nearly
|
|
200
|
+
everywhere. Both charts therefore return `lag1_autocorrelation`, and raise
|
|
201
|
+
`autocorrelation_warning` above 0.5. On the AI4I 2020 log, 600 points with
|
|
202
|
+
`baseline_n=100`: process temperature had lag-1 **+0.919** and was flagged at
|
|
203
|
+
**573 of 600 points**; torque had lag-1 -0.051 and was flagged at **0 of 600**.
|
|
204
|
+
|
|
205
|
+
The check reads the baseline window only, never the whole series. A wear ramp
|
|
206
|
+
reads +0.998 across the whole series but +0.042 across its in-control window,
|
|
207
|
+
so measuring everything would fire the warning on exactly the case the chart is
|
|
208
|
+
for.
|
|
209
|
+
|
|
210
|
+
### Stage 4 — Is it capable?
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
po.calculate_capability(series, usl=110, lsl=90)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Capability is only meaningful on a stable process, so this function reports on
|
|
217
|
+
that condition rather than assuming it. On a series drifting from 97 to 103,
|
|
218
|
+
`cpk` comes out at **11.4** — world-class by any table — while
|
|
219
|
+
`capability_is_meaningful` is `False`, with the reason attached: sigma_overall
|
|
220
|
+
is 6.11 times sigma_within, and 70% of points sit beyond 3 sigma of the
|
|
221
|
+
in-control spread.
|
|
222
|
+
|
|
223
|
+
The stability test is deliberately **not** "did any control rule fire". On
|
|
224
|
+
healthy in-control data, the four Western Electric rules signal at least once
|
|
225
|
+
on 30.5% of 50-point series and **99.5% of 1,000-point series**. A gate built
|
|
226
|
+
on that rejects nearly every real dataset, and the flag stops meaning anything.
|
|
227
|
+
Two length-independent limits are used instead — a sigma ratio above 1.20, or
|
|
228
|
+
more than 1% of points beyond 3 sigma — which fire on about 2% of healthy
|
|
229
|
+
series while still catching a 3-sigma drift 96% of the time and a 2-sigma step
|
|
230
|
+
every time. False alarms now fall as data accumulates instead of climbing
|
|
231
|
+
toward certainty.
|
|
232
|
+
|
|
233
|
+
Two blind spots are written down rather than left to be discovered: a
|
|
234
|
+
mid-series variance change is caught 44.5% of the time, and a slow 1-sigma
|
|
235
|
+
drift only 4%.
|
|
236
|
+
|
|
237
|
+
### Stage 5 — Has it drifted since?
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
report = po.check_asset_drift(train_df, batch_df, asset_col="machine_id")
|
|
241
|
+
report["assets_drifted"] # ['M-02'] - names the machine to go and look at
|
|
242
|
+
report["assets_skipped"] # {'M-01': '10 batch / 200 train rows, below min_rows=30'}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Machines of the same model still differ; one runs hotter, one sits by a door.
|
|
246
|
+
Pooled into a single profile, that between-machine spread *becomes the ruler*,
|
|
247
|
+
and it fails in both directions at once:
|
|
248
|
+
|
|
249
|
+
| Situation | Pooled | Per asset |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| One machine offline for maintenance, **nothing else changed** | `True`, PSI 1.26 — a false alarm | `False`, M-01 marked `insufficient_data` |
|
|
252
|
+
| One machine genuinely +3 °C | magnitude 0.244, names no machine | magnitude **3.084**, names M-02 |
|
|
253
|
+
|
|
254
|
+
That is a **12.5x dilution**, because the pooled sigma of 4.18 contains the
|
|
255
|
+
spread between machines while only one machine of three actually moved.
|
|
256
|
+
|
|
257
|
+
Splitting by asset creates its own trap — smaller batches make statistics
|
|
258
|
+
unreliable — so it ships with two guards. PSI bins scale to the batch size, and
|
|
259
|
+
the mean-shift threshold is raised to
|
|
260
|
+
`max(threshold_pct, k * sqrt(1/n_batch + 1/n_train))`: practical significance
|
|
261
|
+
and statistical significance, both required. At 30 rows that cuts false alarms
|
|
262
|
+
from 39.3% to 0.7% while still catching every 1.0-sigma shift.
|
|
263
|
+
|
|
264
|
+
### Stage 6 — What is it costing me?
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
po.calculate_mtbf(work_orders) # reliability
|
|
268
|
+
po.calculate_mttr(work_orders) # with waiting split from repairing
|
|
269
|
+
po.calculate_availability(...) # inherent vs operational, and the gap
|
|
270
|
+
po.calculate_oee(...) # availability x performance x quality
|
|
271
|
+
po.calculate_pareto(work_orders, "failure_mode", value_col="downtime_hours")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`calculate_pareto` ranks the vital few by **count or by cost**, which are
|
|
275
|
+
rarely the same list. The most frequent failure mode is often cheap; the
|
|
276
|
+
expensive one happens twice a year.
|
|
277
|
+
|
|
278
|
+
### Stage 7 — Can a model help?
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
X_train, X_val, X_test, y_train, y_val, y_test = po.split_data_three_way(df, "failure")
|
|
282
|
+
|
|
283
|
+
engine = po.PotatOptEngine(task="classification", time_budget=60).fit(X_train, y_train)
|
|
284
|
+
engine.optimize_maintenance_threshold(X_val, y_val) # tuned on validation, not test
|
|
285
|
+
print(engine.evaluate(X_test, y_test))
|
|
286
|
+
print(engine.calculate_maintenance_cost(X_test, y_test))
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`PotatOptEngine` handles encoding, scaling, imputation, collinear-feature
|
|
290
|
+
pruning, memory downcasting and the AutoML search. It subclasses scikit-learn's
|
|
291
|
+
`BaseEstimator`, so `cross_val_score`, `GridSearchCV` and `Pipeline` work on it
|
|
292
|
+
directly. Every stochastic step takes a `random_state` (default `42`), so a
|
|
293
|
+
reported score can be reproduced or swept.
|
|
294
|
+
|
|
295
|
+
Full method list: `fit`, `predict`, `predict_proba`, `evaluate`,
|
|
296
|
+
`optimize_threshold`, `optimize_maintenance_threshold`, `check_calibration`,
|
|
297
|
+
`calculate_cost_of_quality`, `calculate_maintenance_cost`,
|
|
298
|
+
`get_feature_importance`, `explain_predictions`, `detect_drift`,
|
|
299
|
+
`get_training_report`, `get_inference_health`, `save`, `load`.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Why it is built this way
|
|
304
|
+
|
|
305
|
+
Three constraints shaped every design decision. All three are measured in the
|
|
306
|
+
test suite rather than claimed in prose.
|
|
307
|
+
|
|
308
|
+
| Goal | How it is met | Measured |
|
|
309
|
+
|---|---|---|
|
|
310
|
+
| **Few tokens to drive** | `auto_analyze()` does the whole pipeline in one call | **91.7% fewer tokens** than the equivalent scikit-learn pipeline (674 to 56, `tiktoken cl100k_base`) |
|
|
311
|
+
| **Small machines** | Ordinal encoding, dtype downcasting, tree models, no GPU | preprocessing cuts memory **84.5%** (2.71 MB to 0.42 MB); `fit()` held under a 400 MB ceiling |
|
|
312
|
+
| **Reads like a first Python lesson** | Flat functions, plain dictionaries, no framework to learn | see the shortest path, above |
|
|
313
|
+
|
|
314
|
+
The install really is small: **four packages** — `numpy`, `pandas`, `scipy`,
|
|
315
|
+
`scikit-learn`. FLAML and SHAP are optional extras loaded lazily, which keeps
|
|
316
|
+
**1.46 seconds (44%)** of import cost off the table until you actually train
|
|
317
|
+
something. CI verifies this in an environment where the extras are genuinely
|
|
318
|
+
absent.
|
|
319
|
+
|
|
320
|
+
### Costs are stated as maintenance, not accuracy
|
|
321
|
+
|
|
322
|
+
The baseline is **run to failure**: with no model, every failure becomes an
|
|
323
|
+
unplanned breakdown. A caught failure costs an inspection plus a planned
|
|
324
|
+
repair. A false alarm costs the inspection only, because an engineer looks
|
|
325
|
+
before replacing a part.
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
po.calculate_maintenance_savings(true_positives=18, false_positives=25, false_negatives=2)
|
|
329
|
+
# {'cost_savings': 691500.0, 'savings_percentage': 69.15,
|
|
330
|
+
# 'breakdown_avoidance_rate': 0.9, 'unplanned_breakdowns': 2, ...}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`breakdown_avoidance_rate` is reported next to `cost_savings` because the two
|
|
334
|
+
disagree exactly where it matters:
|
|
335
|
+
|
|
336
|
+
| Model | Recall | Saving |
|
|
337
|
+
|---|---|---|
|
|
338
|
+
| Good model (TP 18, FP 25, FN 2) | 0.90 | **+691,500** |
|
|
339
|
+
| Flags every machine (TP 20, FP 980, FN 0) | **1.00** | **-660,000** |
|
|
340
|
+
| Flags nothing (FN 20) | 0.00 | 0 — exactly the baseline |
|
|
341
|
+
|
|
342
|
+
The second row is the whole point. Perfect recall, and it destroys money: 980
|
|
343
|
+
pointless call-outs cost more than the breakdowns they prevented. Break-even is
|
|
344
|
+
540 false alarms. **Recall cannot tell you this. Cost can.**
|
|
345
|
+
|
|
346
|
+
### Leakage is caught, not merely documented
|
|
347
|
+
|
|
348
|
+
- `fit()` sends `split_type="time"` for forecasting. FLAML's `"auto"` resolves
|
|
349
|
+
to a random shuffle for regression tasks, which lets future rows train the
|
|
350
|
+
model.
|
|
351
|
+
- `optimize_threshold()` fingerprints the rows it tuned on, and `evaluate()`
|
|
352
|
+
returns `threshold_leakage_warning` if you then report results on those same
|
|
353
|
+
rows.
|
|
354
|
+
|
|
355
|
+
### Silence is never an answer
|
|
356
|
+
|
|
357
|
+
Every bug this project found in itself returned a confident all-clear. They are
|
|
358
|
+
now impossible:
|
|
359
|
+
|
|
360
|
+
- A machine that stops reporting gets status `missing_from_batch`. A dead
|
|
361
|
+
gateway and a healthy machine no longer look identical.
|
|
362
|
+
- A sensor column missing from the batch appears in `skipped_features`, rather
|
|
363
|
+
than the old `drift_detected: False, max_psi: 0.0`.
|
|
364
|
+
- `min_rows` counts **non-null readings**, not rows. A column that is 98% NaN
|
|
365
|
+
still has 200 rows; with 4 usable readings the false-alarm rate was 100%.
|
|
366
|
+
- `auto_analyze()` returns `top_features_note` when SHAP declines, instead of
|
|
367
|
+
an empty list and no reason.
|
|
368
|
+
- `explain_predictions()` returns `additivity_check_relaxed`, so an approximate
|
|
369
|
+
ranking can never pass for an exact one.
|
|
370
|
+
|
|
371
|
+
### Design guarantees
|
|
372
|
+
|
|
373
|
+
1. **Public functions return errors; they do not raise.** A bad argument comes
|
|
374
|
+
back as `{"error": "..."}`. These functions are meant to sit behind a
|
|
375
|
+
tool-calling layer where the caller may be a language model.
|
|
376
|
+
2. **Everything survives `json.dumps()`.** NaN and infinity become `None`,
|
|
377
|
+
because `json.dumps` otherwise emits a bare `NaN`, which is not valid JSON.
|
|
378
|
+
3. **Nothing heavy is imported until it is used.** Enforced by a test that runs
|
|
379
|
+
`import potatopt` in a clean subprocess.
|
|
380
|
+
4. **New arguments default to the previous behaviour.** Adding a feature never
|
|
381
|
+
changes an existing call's result.
|
|
382
|
+
5. **Zero ruff errors, with no warning tier.**
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## What is built on top
|
|
387
|
+
|
|
388
|
+
| | | |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| **`potatopt/`** | the library | Everything above. Every function returns a JSON-ready dictionary and never raises. |
|
|
391
|
+
| **`chart_engine.py`** | the figures | Draws the dictionaries the library returned. It calculates nothing, so a chart can never disagree with the number printed beside it. |
|
|
392
|
+
| **`potatopt/mcp_server.py`** | the AI adapter | Seven MCP tools over stdio, so an agent can drive all of it without writing Python and without factory data leaving the machine. |
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
python examples/tour.py # every capability, on simulated data, in about a minute
|
|
396
|
+
python examples/quickstart.py # the predictive-maintenance job on the real AI4I dataset
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Charts that cannot disagree with the report
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
import chart_engine as ce
|
|
403
|
+
|
|
404
|
+
ce.plot_ewma(po.calculate_ewma_chart(readings, baseline_n=25))
|
|
405
|
+
ce.plot_confusion_matrix(engine.evaluate(X_test, y_test))
|
|
406
|
+
ce.plot_feature_importance(engine.explain_predictions(X_test))
|
|
407
|
+
ce.plot_pareto(po.calculate_pareto(work_orders, "failure_mode", value_col="downtime_hours"))
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Every function takes a dictionary some `potatopt` function already returned, so
|
|
411
|
+
a figure can be drawn months later from a saved JSON report with no raw data
|
|
412
|
+
present. matplotlib is imported lazily, so requiring the module costs nothing
|
|
413
|
+
to a caller who only wanted the numbers.
|
|
414
|
+
|
|
415
|
+
### Driving it from an AI agent
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
pip install "potatopt[automl,mcp]"
|
|
419
|
+
potatopt-mcp # or: python -m potatopt.mcp_server
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
An MCP server over stdio, so it runs as a child process of the client and
|
|
423
|
+
factory data never touches a network:
|
|
424
|
+
|
|
425
|
+
```json
|
|
426
|
+
{ "mcpServers": { "potatopt": { "command": "potatopt-mcp" } } }
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Seven tools, not fifty-seven.** A tool surface is spent from the agent's
|
|
430
|
+
context window before it answers anything, so the count is a budget. Names,
|
|
431
|
+
descriptions and schemas together measure **1,585 tokens**; at that average the
|
|
432
|
+
full public API would cost around 13,000.
|
|
433
|
+
|
|
434
|
+
The guarantee that public functions **return** errors rather than raise is what
|
|
435
|
+
makes them fit this transport. When a tool raises, the MCP SDK replaces the
|
|
436
|
+
exception with `UnexpectedToolError` and the reason is lost, leaving the agent
|
|
437
|
+
at a dead end. Here a missing file comes back as `{"ok": false, "error": "..."}`,
|
|
438
|
+
which it can read and act on.
|
|
439
|
+
|
|
440
|
+
Set `POTATOPT_MCP_ROOT` to confine file reads to one directory. Left unset, the
|
|
441
|
+
server reads whatever the user running it can read; that is a process
|
|
442
|
+
permission model, not a sandbox.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Try it on real data
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
python examples/quickstart.py # about a minute on a laptop CPU
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
This fetches the **AI4I 2020 Predictive Maintenance Dataset** (UCI, CC BY 4.0,
|
|
453
|
+
SHA-256 pinned, cached and gitignored) and walks the whole job in ten printed
|
|
454
|
+
steps. It starts by deleting five failure-mode columns, because those labels
|
|
455
|
+
only exist once the machine has already failed. On the held-out 2,000 rows it
|
|
456
|
+
avoids 61 of 68 breakdowns and saves **2,269,500** against run-to-failure, at
|
|
457
|
+
the cost of 134 wasted inspections.
|
|
458
|
+
|
|
459
|
+
`python benchmarks/runtime_cost.py` measures what that costs to run — memory,
|
|
460
|
+
time and money — against the same pipeline written by hand in scikit-learn.
|
|
461
|
+
|
|
462
|
+
## Development
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
pip install -e ".[automl,xai,viz,mcp,dev]" # the same set CI installs
|
|
466
|
+
python -m ruff check potatopt chart_engine.py tests benchmarks scripts examples
|
|
467
|
+
python -m pytest tests -q
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
CI runs the suite on Python 3.10 through 3.14 and on Windows, verifies the
|
|
471
|
+
core-only install, and builds the distribution. Every Python version claimed in
|
|
472
|
+
`pyproject.toml` is exercised by CI, and a test fails if those two lists ever
|
|
473
|
+
disagree.
|
|
474
|
+
|
|
475
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the design constraints and the
|
|
476
|
+
measure-before-you-specify habit this project runs on.
|
|
477
|
+
|
|
478
|
+
## About
|
|
479
|
+
|
|
480
|
+
Built as an Industrial Engineering undergraduate thesis: DMAIC framing, SPC
|
|
481
|
+
comparison, SHAP mapped to Fishbone analysis, and every result converted to
|
|
482
|
+
money rather than left as a metric. The name is the design philosophy — a model
|
|
483
|
+
that runs on a potato.
|
|
484
|
+
|
|
485
|
+
MIT licensed. See [LICENSE](LICENSE).
|