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 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.
@@ -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
+ [![CI](https://github.com/Oak04K/PotatOpt-ML/actions/workflows/ci.yml/badge.svg)](https://github.com/Oak04K/PotatOpt-ML/actions/workflows/ci.yml)
86
+ [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue.svg)](https://www.python.org/)
87
+ [![Version](https://img.shields.io/badge/version-1.6.0-blue.svg)](CHANGELOG.md)
88
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
89
+ [![Core install](https://img.shields.io/badge/core%20install-4%20packages-orange.svg)](#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).