prefit 0.1.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.
Files changed (71) hide show
  1. prefit-0.1.0/.gitignore +31 -0
  2. prefit-0.1.0/ARCHITECTURE.md +98 -0
  3. prefit-0.1.0/CHANGELOG.md +111 -0
  4. prefit-0.1.0/CONTRIBUTING.md +53 -0
  5. prefit-0.1.0/LICENSE +21 -0
  6. prefit-0.1.0/PKG-INFO +351 -0
  7. prefit-0.1.0/README.md +297 -0
  8. prefit-0.1.0/benchmarks/fixtures.py +203 -0
  9. prefit-0.1.0/benchmarks/run.py +127 -0
  10. prefit-0.1.0/prefit/__init__.py +23 -0
  11. prefit-0.1.0/prefit/_version.py +1 -0
  12. prefit-0.1.0/prefit/api.py +448 -0
  13. prefit-0.1.0/prefit/assemble.py +91 -0
  14. prefit-0.1.0/prefit/cli.py +61 -0
  15. prefit-0.1.0/prefit/config.py +39 -0
  16. prefit-0.1.0/prefit/data/track_record.json +57 -0
  17. prefit-0.1.0/prefit/enums.py +99 -0
  18. prefit-0.1.0/prefit/export.py +328 -0
  19. prefit-0.1.0/prefit/figures.py +305 -0
  20. prefit-0.1.0/prefit/fingerprint.py +92 -0
  21. prefit-0.1.0/prefit/frozen.py +70 -0
  22. prefit-0.1.0/prefit/guard.py +188 -0
  23. prefit-0.1.0/prefit/llm/__init__.py +4 -0
  24. prefit-0.1.0/prefit/llm/anthropic.py +39 -0
  25. prefit-0.1.0/prefit/llm/config.py +36 -0
  26. prefit-0.1.0/prefit/llm/gemini.py +37 -0
  27. prefit-0.1.0/prefit/llm/interpreter.py +162 -0
  28. prefit-0.1.0/prefit/llm/local.py +38 -0
  29. prefit-0.1.0/prefit/llm/payload.py +80 -0
  30. prefit-0.1.0/prefit/llm/provider.py +21 -0
  31. prefit-0.1.0/prefit/posttrain.py +394 -0
  32. prefit-0.1.0/prefit/profiling/__init__.py +0 -0
  33. prefit-0.1.0/prefit/profiling/kinds.py +82 -0
  34. prefit-0.1.0/prefit/profiling/profile.py +94 -0
  35. prefit-0.1.0/prefit/profiling/stats.py +236 -0
  36. prefit-0.1.0/prefit/profiling/tabular.py +227 -0
  37. prefit-0.1.0/prefit/profiling/task_inference.py +45 -0
  38. prefit-0.1.0/prefit/py.typed +0 -0
  39. prefit-0.1.0/prefit/render/__init__.py +0 -0
  40. prefit-0.1.0/prefit/render/common.py +172 -0
  41. prefit-0.1.0/prefit/render/html.py +484 -0
  42. prefit-0.1.0/prefit/render/tables.py +23 -0
  43. prefit-0.1.0/prefit/render/text.py +94 -0
  44. prefit-0.1.0/prefit/report/__init__.py +20 -0
  45. prefit-0.1.0/prefit/report/model.py +422 -0
  46. prefit-0.1.0/prefit/rules/__init__.py +0 -0
  47. prefit-0.1.0/prefit/rules/catalog.py +659 -0
  48. prefit-0.1.0/prefit/rules/engine.py +50 -0
  49. prefit-0.1.0/prefit/rules/metrics.py +140 -0
  50. prefit-0.1.0/prefit/rules/registry.py +165 -0
  51. prefit-0.1.0/prefit/verify.py +242 -0
  52. prefit-0.1.0/pyproject.toml +104 -0
  53. prefit-0.1.0/tests/conftest.py +43 -0
  54. prefit-0.1.0/tests/golden/datetime_column.json +102 -0
  55. prefit-0.1.0/tests/golden/high_missing.json +97 -0
  56. prefit-0.1.0/tests/golden/imbalanced.json +90 -0
  57. prefit-0.1.0/tests/golden/leaky_categorical_classification.json +90 -0
  58. prefit-0.1.0/tests/golden/skewed_target.json +95 -0
  59. prefit-0.1.0/tests/test_answers.py +61 -0
  60. prefit-0.1.0/tests/test_cli.py +12 -0
  61. prefit-0.1.0/tests/test_export.py +87 -0
  62. prefit-0.1.0/tests/test_guard.py +67 -0
  63. prefit-0.1.0/tests/test_llm.py +149 -0
  64. prefit-0.1.0/tests/test_partition.py +127 -0
  65. prefit-0.1.0/tests/test_profiling.py +63 -0
  66. prefit-0.1.0/tests/test_render.py +146 -0
  67. prefit-0.1.0/tests/test_review.py +64 -0
  68. prefit-0.1.0/tests/test_rules.py +167 -0
  69. prefit-0.1.0/tests/test_snapshot.py +36 -0
  70. prefit-0.1.0/tests/test_surface.py +112 -0
  71. prefit-0.1.0/tests/test_verify.py +111 -0
@@ -0,0 +1,31 @@
1
+ # secrets / local env
2
+ .env
3
+ .venv/
4
+ venv/
5
+
6
+ # notebooks (scratch work; examples live in examples/*.py)
7
+ *.ipynb
8
+ .ipynb_checkpoints/
9
+
10
+ # python / tooling caches
11
+ __pycache__/
12
+ *.py[cod]
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # build artefacts
20
+ dist/
21
+ build/
22
+ *.egg-info/
23
+
24
+ # OS
25
+ .DS_Store
26
+
27
+ # reports generated in the repo root (examples/output/ is tracked on purpose)
28
+ /*.html
29
+
30
+ # private v0 design notes (kept locally, not published)
31
+ ARCHITECTURE.v0.md
@@ -0,0 +1,98 @@
1
+ # Architecture
2
+
3
+ prefit shortens the pre-training analysis of a tabular supervised-learning
4
+ project. It measures the data, derives classified decisions from the
5
+ measurements, tests the decisions that can be tested, asks the user about the
6
+ ones that cannot, and exports only what is safe, confirmed or verified.
7
+
8
+ ## Pipeline
9
+
10
+ ```
11
+ pf.split(df, ...) -> dev_df, test_df (explicit; same splitter as holdout=)
12
+ dev_df, target, [estimator, groups, time_col, holdout, context, answers]
13
+ │
14
+ ▼ guard.py ─ validate inputs (misspelled target, single-class target, groups+time…);
15
+ │ optional internal holdout (only with holdout=…), set aside before profiling
16
+ ▼ fingerprint.py ─ SHA-256 digests of the input frame -> manifest (no data)
17
+ ▼ profiling/ ─ dev rows only, seeded sample above ProfileOptions.max_rows
18
+ │ kinds.py column kind + reason (numeric, categorical, boolean,
19
+ │ datetime, text, id_like, constant)
20
+ │ stats.py per-kind stats, bootstrap / Wilson intervals,
21
+ │ single-feature held-out association score
22
+ │ tabular.py → DataProfile (aggregates only, JSON-serializable)
23
+ ▼ rules/ ─ deterministic: DataProfile + RuleContext → RuleOutput
24
+ │ catalog.py versioned rules with thresholds; emit Evidence → Finding → Decision (+ Question)
25
+ │ metrics.py metric policy: one contextual decision driven by the cost of errors
26
+ ▼ figures.py ─ decision-bound plotly specs (aggregates + color tokens)
27
+ ▼ report/model.py ─ Report (decisions, questions, metric plan, roadmap,
28
+ │ experiments, baseline) + details (findings, evidence,
29
+ │ profile, figures, manifest, context, limitations, llm);
30
+ │ deeply immutable (frozen.py)
31
+ │
32
+ ├─ Report.answer(...) re-runs the rules with new answers (no data needed)
33
+ ├─ Report.verify(df) fingerprint check, then verify.py on dev rows
34
+ ├─ Report.holdout(df) fingerprint check, re-derives the internal holdout
35
+ ├─ Report.register_external_holdout() metadata + exact-overlap count, never profiled
36
+ ├─ Report.export() export.py: model recipe + preprocessor + CV + scoring
37
+ ├─ Report.interpret() llm/: optional, schema-constrained interpretation
38
+ ├─ Report.to_html() render/: single-file report
39
+ └─ pf.review(...) posttrain.py: assumptions vs. actual errors
40
+ ```
41
+
42
+ ## Contracts
43
+
44
+ - **Evidence** — `id`, `label`, `value`, optional 95% `ci`, `method`, `columns`,
45
+ linked `figure_ids`.
46
+ - **Finding** — produced by one rule; `rule_id`, `rule_version`, `severity`,
47
+ `trigger` (`fired` | `uncertain`), `thresholds`, and at least one evidence id
48
+ (enforced by the model).
49
+ - **Decision** — `cls` (procedural / conditional / contextual / review),
50
+ `status`, `stage`, `action`, `rationale`, `risk`, optional `step` (the concrete
51
+ pipeline change), links to findings, evidence, figures, experiments, question.
52
+ - **Decision.eligible()** — the single rule for what enters the pipeline:
53
+ procedural+applied, contextual+confirmed, conditional+verified (or
54
+ supported_by_proxy when explicitly requested).
55
+
56
+ ## Why these boundaries
57
+
58
+ 1. **Thresholds are triggers.** A rule compares a statistic *and its interval*
59
+ to a threshold. Interval straddles the threshold → `uncertain`.
60
+ 2. **Data-modifying actions are never procedural.** Dropping duplicates, rows
61
+ or columns can be a collection artefact or legitimate data; only the user or
62
+ an experiment can decide. The one exception is constant columns, which carry
63
+ no information. Raw datetime/text/string-id columns are left out of the
64
+ export because v1 cannot encode them — a capability limit reported as a
65
+ review item, not a judgement that they are uninformative.
66
+ 3. **The data cannot choose the metric.** It can say which metrics mislead
67
+ (evidence-backed `avoid`); what to optimize depends on what errors cost.
68
+ 4. **Experiments are bounded, and their inference matches the split.** One
69
+ change at a time against a fixed base, identical folds, hard budget. The
70
+ test follows the protocol: corrected repeated k-fold t-test (repeated or
71
+ grouped k-fold), no p-value for time-ordered folds. "Improved" needs
72
+ direction, Holm-adjusted significance and a practical effect threshold.
73
+ Proxy models give sensitivity analysis; only the user's estimator verifies.
74
+ 5. **Split precedence is explicit.** time_col > groups > stratification;
75
+ time_col disables stratification (stated in the decision); groups and
76
+ time_col together raise instead of silently dropping one.
77
+ 6. **The EDA must not leak into the final estimate.** The holdout is reserved
78
+ before profiling and is never used by rules or experiments.
79
+ 7. **Reports are immutable, data is fingerprinted.** answer / verify /
80
+ interpret / register_external_holdout return a new report with an
81
+ incremented manifest revision and a history entry; every model is frozen
82
+ with tuples and read-only dicts. Steps that need data again check the
83
+ frame's fingerprint and refuse a different one instead of re-splitting.
84
+ 8. **The LLM is above the trust boundary.** It sees an allowlisted payload
85
+ (inspectable with `llm_payload()`), must cite existing ids, and can only
86
+ suggest; `answer()` is the only way an answer is applied.
87
+
88
+ ## Adding a rule
89
+
90
+ See CONTRIBUTING.md. In short: write `fn(profile, ctx, out)` in
91
+ `rules/catalog.py` with `@rule(id, version, thresholds)`, use `trigger()` with
92
+ the statistic's interval, pick the decision class honestly, add benchmark cases
93
+ (must fire / must not fire / near threshold), and bump the manifest version.
94
+
95
+ ## Not in v1
96
+
97
+ Other modalities, time-series forecasting, clustering, model selection,
98
+ multi-feature leakage detection, interaction effects between decisions.
@@ -0,0 +1,111 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The project follows
4
+ [Semantic Versioning](https://semver.org/); before 1.0, minor versions may
5
+ change the public API.
6
+
7
+ ## 0.1.0 — 2026-11-01
8
+
9
+ First release of **prefit** (`pip install prefit`, `import prefit as pf`), the
10
+ decision-driven design. Replaces the v0 `Roadmap` API.
11
+
12
+ ### Added
13
+ - `pf.analyze()` returns a `Report`: Evidence → Finding → Decision chain with
14
+ decision classes (procedural / conditional / contextual / review) and statuses.
15
+ - Input guard: misspelled targets and unknown keywords raise; holdout reserved
16
+ before profiling (stratified, group-aware with `groups=`, time-ordered with
17
+ `time_col=`).
18
+ - Column-kind inference (numeric, categorical, boolean, datetime, text,
19
+ id_like, constant) with reasons; bootstrap / Wilson intervals on
20
+ threshold-relevant statistics; near-threshold findings marked `uncertain`.
21
+ - Task-agnostic leakage hint via a single-feature held-out score; leakage is
22
+ reported as "manual review required" with a question, never as a verdict.
23
+ - Rules: task confirmation, validation split, holdout policy, small data,
24
+ temporal order, target missing, duplicates, conflicting duplicates, high
25
+ missingness, near-constant, high correlation, constant columns, unsupported
26
+ columns, id-like columns, leakage hint, fit-in-fold, skewed features, high
27
+ cardinality, class imbalance, target skew, heavy tails, zero inflation;
28
+ metric policy driven by the cost of errors.
29
+ - `Report.answer()`, `Report.verify()` (bounded experiments, corrected paired
30
+ t-test, Holm correction, budget; user estimator verifies, proxies give
31
+ sensitivity analysis), `Report.export()` (sklearn Pipeline + CV + scoring from
32
+ eligible decisions only, with an explicit excluded list).
33
+ - Single-file HTML report with decision cards, linked evidence, rule versions
34
+ and thresholds, experiments, decision-bound plotly figures with data tables,
35
+ light/dark themes; text, markdown and JSON renders; reproducibility manifest.
36
+ - `pf.review()`: reference-based hypothesis checks, baselines, residual /
37
+ confusion / PR / segment analysis, list of never-verified assumptions;
38
+ standalone or embedded HTML.
39
+ - Optional LLM layer: allowlisted payload with `llm_payload()` preview, column
40
+ aliasing, schema-constrained output, unknown-id claims dropped, suggestions
41
+ never applied; Anthropic, Gemini and local OpenAI-compatible providers.
42
+ - Versioned benchmark (`benchmarks/`), track record cited in reports.
43
+ - CLI: `prefit report data.csv --target y`.
44
+ - CI (ruff, mypy, pytest with coverage, HTML smoke test).
45
+
46
+ ### Changed (pre-release API round)
47
+ - Data partitions are explicit: `pf.split(df, target, test_size, strategy)` is the
48
+ primary path; `analyze` defaults to `holdout=None` and only sets a holdout aside
49
+ when asked (`holdout=0.2`). `report.split()` became `report.holdout(df)`, which
50
+ re-derives that partition and never splits anew.
51
+ - Fingerprints: `verify`, `holdout` and `register_external_holdout(dev_df=...)`
52
+ check the frame against SHA-256 digests (column schema, dtypes, index, target,
53
+ row-order-sensitive content) of the frame passed to `analyze`, and explain
54
+ any mismatch (reordered, changed, shorter, different columns).
55
+ - `report.register_external_holdout(test_df, dev_df=...)` records an evaluation
56
+ partition (metadata only, never profiled) and counts exact row overlap with
57
+ the development data, reported as a review item without a leakage verdict.
58
+ - Deep immutability: all report models are frozen, lists are tuples and dicts
59
+ read-only; every change is a new revision. `pf.load_report()` reloads JSON.
60
+ - API surface: reading / flow / automation on `Report`, internals under
61
+ `report.details` (findings, evidence, profile, figures, manifest, context,
62
+ limitations, rule_track_record, llm). Short reprs for all report objects,
63
+ a notebook summary card, and `report.decisions_table()`. v0 aliases removed.
64
+ - Split columns (`groups`, `time_col`) stay out of the features unless
65
+ `split_columns_as_features=True`, which raises a warning.
66
+ - HTML report rebuilt: Summary / Actions / Roadmap / Details navigation, short
67
+ decision cards (why with the decisive number, do, experiment summary, at most
68
+ one figure) with evidence, experiment, figure and rule/benchmark details in
69
+ collapsed sections, consecutive roadmap numbering, collapsed Details section.
70
+ - Verification and baselines use the export splitter (repeated for power).
71
+ - Feature scaling is an export option (`scale=True`), not part of the
72
+ procedural fit-in-fold decision.
73
+ - Bootstrap intervals computed on subsampled resamples are rescaled to the full
74
+ sample size (they were ~sqrt(n/m) times too wide on large data).
75
+ - Single-feature association is a 2-fold cross-validated score.
76
+ - Single-class targets are rejected up front; +/-inf values produce an
77
+ `infinite_values` review item instead of numpy warnings and are treated as
78
+ missing by the exported preprocessing; duplicate detection hashes rows
79
+ directly (much faster on wide data).
80
+ - `benchmarks/run.py` keeps the committed experiment section when run without
81
+ `--verify`.
82
+
83
+ ### Changed (pre-release review)
84
+ - Raw datetime / text / string-id columns are a review item ("unsupported in
85
+ v1, may carry signal"), no longer a procedural drop.
86
+ - `groups=` together with `time_col=` raises instead of silently ignoring
87
+ `groups`; the time split states that class stratification is off; grouped
88
+ classification notes that class balance is approximate.
89
+ - Verification: the test now follows the split protocol (corrected repeated
90
+ k-fold t-test; single-run grouped k-fold; no p-value for time-series folds),
91
+ and "improved" requires a practical effect threshold
92
+ (`Budget.min_relative_effect` / `min_effect`). Per-fold differences and the
93
+ protocol are shown in the report.
94
+ - `export()` returns `model` (full estimator recipe, possibly a
95
+ `TransformedTargetRegressor`) and `preprocessor` instead of `pipeline`;
96
+ estimator-level steps without an estimator go to `needs_estimator`, and an
97
+ estimator without `class_weight` moves that step to `excluded` — nothing
98
+ listed as included is dropped.
99
+ - Reports record `manifest.revision` and `manifest.history` for every
100
+ analyze / answer / verify / interpret step.
101
+ - Wording: "single-feature predictive leakage hint"; benchmark records are a
102
+ "rule detection pass rate"; the HTML report opens with a data-partition note.
103
+
104
+ ### Changed
105
+ - `GroupKFold` is no longer suggested from duplicates or id-like columns.
106
+ - Integer targets with more than 10 distinct values are inferred as regression.
107
+ - `use_llm=`, `provider=`, `api_key=` replaced by `llm=LLMConfig(...)`.
108
+
109
+ ### Removed
110
+ - `Roadmap.make_sklearn()` / `validation_recipe()` (use `Report.export()`).
111
+ - Reserved non-tabular modalities from the public enums.
@@ -0,0 +1,53 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ python -m venv .venv && source .venv/bin/activate
7
+ pip install -e ".[dev]"
8
+ pytest -q # tests (includes the benchmark cases)
9
+ ruff check . && mypy prefit
10
+ python benchmarks/run.py # refreshes benchmarks/results and the track record
11
+ ```
12
+
13
+ ## Adding or changing a rule
14
+
15
+ 1. Write the rule in `prefit/rules/catalog.py` with
16
+ `@rule("id", "1", thresholds={...})`. Compare statistics with `trigger()` and
17
+ pass the statistic's interval so near-threshold cases become `uncertain`.
18
+ 2. Choose the decision class honestly:
19
+ - procedural only if it is about *how to evaluate* and is right for any data;
20
+ - anything that changes the data's content is review, conditional or contextual;
21
+ - anything that needs knowledge outside the data is contextual (ask a question).
22
+ 3. Never phrase a hint as a verdict ("leakage hint — manual review required",
23
+ not "leakage detected").
24
+ 4. If the decision has a pipeline step, make sure `export.build_model` supports
25
+ its `Step.kind`.
26
+ 5. Add benchmark cases in `benchmarks/fixtures.py`: one where it must fire, one
27
+ where it must not, and a near-threshold one where that is meaningful.
28
+ 6. Bump the rule's version when its thresholds or logic change, and bump
29
+ `MANIFEST_VERSION` when cases or expectations change. Re-run
30
+ `python benchmarks/run.py` and commit the results, including failures.
31
+ 7. If decision structure changes, refresh the golden files:
32
+ `UPDATE_GOLDEN=1 pytest tests/test_snapshot.py`, and review the diff.
33
+
34
+ ## Versioning
35
+
36
+ Semantic versioning; before 1.0, minor versions may change the API. Every
37
+ release gets a CHANGELOG entry. Rule versions are independent and appear in
38
+ every report's manifest.
39
+
40
+ ## Pull requests
41
+
42
+ Keep the deterministic core free of new hard dependencies. Tests, ruff and mypy
43
+ must pass; coverage must stay above the configured floor.
44
+
45
+ ## Releasing
46
+
47
+ Publishing runs in GitHub Actions with PyPI Trusted Publishing (no tokens).
48
+
49
+ 1. Bump `version` in `pyproject.toml` and `prefit/_version.py`, add a CHANGELOG entry, merge to `main`.
50
+ 2. Optional dry run: Actions → Release → Run workflow → `target=testpypi`, then
51
+ `pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ prefit`.
52
+ 3. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z`. The workflow checks that the tag matches the
53
+ package version, builds, runs `twine check` and a wheel smoke test, then publishes to PyPI.
prefit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hkln4
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.
prefit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,351 @@
1
+ Metadata-Version: 2.5
2
+ Name: prefit
3
+ Version: 0.1.0
4
+ Summary: Decision-driven EDA for tabular supervised learning: evidence-backed, verifiable pre-training decisions.
5
+ Project-URL: Homepage, https://hkln4.github.io/prefit/
6
+ Project-URL: Example report, https://hkln4.github.io/prefit/report.html
7
+ Project-URL: Repository, https://github.com/hkln4/prefit
8
+ Project-URL: Issues, https://github.com/hkln4/prefit/issues
9
+ Project-URL: Changelog, https://github.com/hkln4/prefit/blob/main/CHANGELOG.md
10
+ Author: hkln4
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: cross-validation,data-profiling,eda,evaluation,leakage,machine-learning,model-evaluation,scikit-learn
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: numpy>=1.23
28
+ Requires-Dist: pandas>=1.5
29
+ Requires-Dist: pydantic>=2
30
+ Provides-Extra: all
31
+ Requires-Dist: anthropic>=0.40; extra == 'all'
32
+ Requires-Dist: plotly>=5; extra == 'all'
33
+ Requires-Dist: scikit-learn>=1.4; extra == 'all'
34
+ Requires-Dist: scipy>=1.9; extra == 'all'
35
+ Provides-Extra: dev
36
+ Requires-Dist: mypy>=1.8; extra == 'dev'
37
+ Requires-Dist: pandas-stubs; extra == 'dev'
38
+ Requires-Dist: plotly>=5; extra == 'dev'
39
+ Requires-Dist: pytest-cov>=4; extra == 'dev'
40
+ Requires-Dist: pytest>=7; extra == 'dev'
41
+ Requires-Dist: ruff>=0.5; extra == 'dev'
42
+ Requires-Dist: scikit-learn>=1.4; extra == 'dev'
43
+ Requires-Dist: scipy>=1.9; extra == 'dev'
44
+ Provides-Extra: gemini
45
+ Requires-Dist: google-genai>=1.0; extra == 'gemini'
46
+ Provides-Extra: llm
47
+ Requires-Dist: anthropic>=0.40; extra == 'llm'
48
+ Provides-Extra: report
49
+ Requires-Dist: plotly>=5; extra == 'report'
50
+ Provides-Extra: verify
51
+ Requires-Dist: scikit-learn>=1.4; extra == 'verify'
52
+ Requires-Dist: scipy>=1.9; extra == 'verify'
53
+ Description-Content-Type: text/markdown
54
+
55
+ # prefit
56
+
57
+ **Decision-driven EDA for tabular supervised learning.**
58
+
59
+ Point `prefit` at your development data before you train. It profiles the
60
+ data, turns what it measures into a short list of *decisions* — how to split,
61
+ which columns to question, what to preprocess, which metric to trust — and shows
62
+ the evidence, threshold and uncertainty behind every one. Decisions that depend
63
+ on your data and model are tested with small, bounded experiments instead of
64
+ being assumed. At the end you get a scikit-learn model recipe that contains only
65
+ steps that are safe, that you confirmed, or that an experiment supported.
66
+
67
+ It is not AutoML: it does not choose a model or tune hyperparameters, and it
68
+ never applies an unconfirmed assumption silently.
69
+
70
+ **[Example report](https://hkln4.github.io/prefit/report.html)** ·
71
+ [Project page](https://hkln4.github.io/prefit/) · [PyPI](https://pypi.org/project/prefit/)
72
+
73
+ ```bash
74
+ pip install "prefit[verify]" # core + experiments/export (scikit-learn, scipy)
75
+ pip install "prefit[verify,report]" # + plotly, for fully offline HTML reports
76
+ pip install "prefit[llm]" # optional LLM interpretation (Anthropic)
77
+ ```
78
+
79
+ The core (`pandas`, `numpy`, `pydantic`) profiles, decides and renders reports
80
+ with no scikit-learn, no network and no API key.
81
+
82
+ ---
83
+
84
+ ## Quickstart
85
+
86
+ ```python
87
+ import prefit as pf
88
+ from sklearn.ensemble import HistGradientBoostingRegressor
89
+
90
+ model = HistGradientBoostingRegressor(random_state=0)
91
+
92
+ # 1. set the test data aside before anything is computed
93
+ dev_df, test_df = pf.split(df, target="price", test_size=0.2, strategy="random", random_state=42)
94
+
95
+ # 2. analyze the development data only
96
+ report = pf.analyze(dev_df, target="price", estimator=model)
97
+ report # notebook: a short summary card with the next step
98
+ print(report) # roadmap: one line of why / do per decision
99
+ report.to_html("report.html") # the full report
100
+
101
+ # 3. every step returns a new report revision
102
+ report = report.answer({"metric_priority": "typical_error"})
103
+ report = report.verify(dev_df) # bounded experiments with your model
104
+ report = report.register_external_holdout(test_df, dev_df=dev_df, name="test set")
105
+
106
+ # 4. export only what is applied, confirmed or verified
107
+ built = report.export()
108
+ print(built.describe()) # what is in, what is out, and why
109
+ fitted = built.model.fit(dev_df[built.feature_columns], dev_df["price"])
110
+
111
+ # 5. once, at the end, on the untouched test data
112
+ review = pf.review(test_df["price"], fitted.predict(test_df[built.feature_columns]), report)
113
+ report.to_html("report.html", review=review)
114
+ ```
115
+
116
+ Shortcut: `pf.analyze(df, target, holdout=0.2)` sets the holdout aside itself,
117
+ before profiling; `dev_df, test_df = report.holdout(df)` gives it back. Without
118
+ `holdout=`, `analyze` never splits — the default is `holdout=None`.
119
+
120
+ From the shell:
121
+
122
+ ```bash
123
+ prefit report dev.csv --target price --verify --answer metric_priority=typical_error --out report.html
124
+ ```
125
+
126
+ An example report produced by `examples/quickstart.py` is live at
127
+ [hkln4.github.io/prefit/report.html](https://hkln4.github.io/prefit/report.html) (source:
128
+ [`examples/output/california_report.html`](https://github.com/hkln4/prefit/blob/main/examples/output/california_report.html)).
129
+
130
+ ---
131
+
132
+ ## The report object
133
+
134
+ ```python
135
+ # reading
136
+ print(report); report.to_html(path); report.to_markdown()
137
+ report.roadmap; report.pending_questions(); report.decisions_table()
138
+ report.target; report.task
139
+
140
+ # flow (each returns a new revision; the old report is unchanged)
141
+ report.answer({...}); report.verify(df); report.interpret(llm)
142
+ report.register_external_holdout(test_df, dev_df=dev_df)
143
+ report.export(estimator); report.holdout(df); pf.review(...)
144
+
145
+ # automation
146
+ report.decisions; report.decision(id); report.questions; report.experiments
147
+ report.metric_plan; report.baseline; report.to_json(); report.llm_payload()
148
+
149
+ # internals: the traceability chain and reproducibility record
150
+ report.details.findings / .finding(id) / .evidence / .profile / .figures
151
+ report.details.manifest / .context / .limitations / .rule_track_record / .llm
152
+ ```
153
+
154
+ `report.decisions_table()` is the best place to start in a notebook: one row per
155
+ decision with its status, a one-line why (with the decisive number), what to do,
156
+ and any experiment result.
157
+
158
+ **Reports are immutable.** Every model is frozen, lists are tuples and dicts are
159
+ read-only, all the way down. `answer`, `verify`, `interpret` and
160
+ `register_external_holdout` return a new report whose
161
+ `details.manifest.revision` is incremented and whose `history` records the step.
162
+ `pf.load_report(path)` reloads a report saved with `to_json`.
163
+
164
+ **The data is checked, never trusted.** A report stores no rows, so `verify`
165
+ and `holdout` take the DataFrame again. Its fingerprint — SHA-256 digests of the
166
+ column schema, dtypes, index, target and row-order-sensitive content — must
167
+ match the frame passed to `analyze`. A shuffled, edited, shortened or different
168
+ frame (the test set, say) raises an error that says what differs; nothing is
169
+ ever re-split.
170
+
171
+ ---
172
+
173
+ ## How decisions are made
174
+
175
+ Every rule turns *evidence* (a measured value, its 95% interval, the method)
176
+ into a *finding*, and the finding into one or more *decisions*. Each decision has
177
+ a **class**, which says how much the data alone can settle it:
178
+
179
+ | class | meaning | enters the model recipe when |
180
+ |---|---|---|
181
+ | **procedural** | about *how to evaluate*; correct regardless of the data (fit preprocessing inside folds, stratify, keep split columns and the holdout out). The only column it removes are constant ones. | always |
182
+ | **conditional** | may help or hurt depending on data and model (log target, power transform, target encoding, class weights, dropping mostly-missing columns) | an experiment with **your** estimator verifies it |
183
+ | **contextual** | cannot be decided from data (metric = cost of errors, is a column known at prediction time, is it an identifier, does time order matter) | you answer the question |
184
+ | **review** | needs a human look (duplicates, label conflicts, informative missingness, infinite values, collinearity, raw datetime/text columns v1 cannot encode) | never; it is a to-do |
185
+
186
+ Ground rules the catalog follows:
187
+
188
+ - **Thresholds are triggers, not verdicts.** Threshold-relevant statistics carry
189
+ a bootstrap or Wilson interval (bootstrap intervals are computed for the full
190
+ sample size even when resamples are subsampled); when the interval straddles
191
+ the threshold the finding is *near threshold* and its decision *uncertain*.
192
+ - **Leakage is only ever a hint.** A feature that alone predicts the target in a
193
+ 2-fold cross-validated single-feature score raises a "single-feature
194
+ predictive leakage hint — manual review required" and a question ("is this
195
+ known at prediction time?"). Genuinely strong features look the same, so
196
+ nothing is dropped until you answer.
197
+ - **Unsupported is not uninformative.** Raw datetimes, free text and string
198
+ identifiers are left out of the exported features because v1 has no encoder
199
+ for them; they are reported as a review item, never as "carries no signal".
200
+ - **No group splitting without real groups.** ID-like columns and duplicates
201
+ produce review items. Group-aware splitters are used only with `groups=`;
202
+ `TimeSeriesSplit` only with `time_col=`. Both columns stay out of the features
203
+ unless `split_columns_as_features=True` (which adds a warning).
204
+ - **Split precedence is explicit.** `time_col` turns off class stratification
205
+ (the report says so); `groups` with classification uses
206
+ `StratifiedGroupKFold`, which balances classes only approximately; `groups`
207
+ and `time_col` together raise an error in v1.
208
+ - **The data you analyze is the data the report describes.** The report opens
209
+ with a data-partition note: which rows were analyzed, whether an internal or
210
+ registered external holdout exists, and that it was never read.
211
+
212
+ ## Experiments, and what they can claim
213
+
214
+ `verify` tests each conditional decision once per model family as "current
215
+ setup" vs. "current setup + this one change", on identical folds. The folds come
216
+ from the same splitter as the export CV decision (repeated when it is a plain
217
+ k-fold, for power); baselines use the same folds. A `Budget` caps the number of
218
+ experiments, wall-clock time and rows.
219
+
220
+ | protocol | when | test |
221
+ |---|---|---|
222
+ | `repeated_kfold` | default | corrected repeated k-fold t-test (Nadeau & Bengio 2003; Bouckaert & Frank 2004), Holm across the run |
223
+ | `grouped_kfold` | `groups=` | same correction on a single grouped k-fold run (low power, said in the report) |
224
+ | `time_series` | `time_col=` | none — expanding windows are not exchangeable; per-fold differences only, outcome left to you |
225
+
226
+ An experiment counts as **improved** only with the right direction, a
227
+ significant Holm-adjusted p-value, *and* a mean difference of at least the
228
+ practical threshold (`Budget(min_relative_effect=0.01)` = 1% of the baseline
229
+ score, or an absolute `min_effect`). Significant but smaller differences are
230
+ **negligible**. Per-fold differences are in the report.
231
+
232
+ - With `estimator=`, an improvement makes the decision **verified** (and
233
+ exportable); a practical-size loss makes it **refuted**.
234
+ - Without it, a linear and a tree proxy are used. Their results are a
235
+ **sensitivity analysis** (`supported_by_proxy`), not a verdict on your model,
236
+ and are excluded from export unless you pass
237
+ `export(include_proxy_supported=True)`. When one family gains and the other
238
+ loses, the decision is marked model-family dependent.
239
+ - All of it is **development evidence**. The generalization claim comes from the
240
+ untouched test data (or nested CV when you have none).
241
+
242
+ ## Export
243
+
244
+ `report.export(estimator, scale=True)` returns:
245
+
246
+ - `model` — the full recipe for `fit` / `cross_validate`: `Pipeline(preprocess,
247
+ estimator)`, wrapped in a `TransformedTargetRegressor` when a target transform
248
+ was verified (`built.model_kind` says which). `None` without an estimator.
249
+ - `preprocessor` — the feature preprocessing alone. Feature scaling is an
250
+ estimator-dependent default (`scale=False` turns it off), not a decision.
251
+ - `cv`, `scoring`, `feature_columns`, `groups`, `sort_by`.
252
+ - `included`, `excluded` (with status and reason), `needs_estimator` (eligible
253
+ estimator-level steps that need an estimator), `unsupported` (columns left
254
+ out), `assumptions` still open.
255
+
256
+ ## The HTML report
257
+
258
+ `report.to_html(path)` writes one file with four sections:
259
+
260
+ - **Summary** — target, task, development rows; how many questions await you and
261
+ how many decisions are critical or need review; one next step; the data
262
+ partition note.
263
+ - **Actions** — what needs your attention (open questions, review items, and
264
+ conditional decisions an experiment verified or refuted), the questions with
265
+ their options (and an LLM suggestion awaiting your confirmation, if you ran
266
+ one), and the metric plan.
267
+ - **Roadmap** — one short card per decision: status, title, one-line why with
268
+ the decisive number, one-line do, experiment summary, and at most one figure.
269
+ Evidence, experiment details, related figures and rule/benchmark details are
270
+ one click away inside the card.
271
+ - **Details** — limitations, then collapsed: all diagnostics, findings,
272
+ evidence, dataset profile, manifest and history.
273
+
274
+ No raw rows are embedded; category labels are shown only for low-cardinality
275
+ columns (`ProfileOptions(redact_values=True)` hides them all). Charts use
276
+ plotly.js: with `plotlyjs="cdn"` (default) the report is a single file but needs
277
+ internet for the charts; `plotlyjs="inline"` embeds plotly.js (~5 MB, needs the
278
+ `report` extra) for fully offline use; every chart also has a data table, which
279
+ works offline either way.
280
+
281
+ ## Optional LLM interpretation
282
+
283
+ ```python
284
+ report.llm_payload(pf.LLMConfig(share_column_names=False)) # inspect first
285
+ report = report.interpret(pf.LLMConfig(provider="anthropic"))
286
+ report = report.interpret(pf.LLMConfig(provider="local", model="llama3.1")) # Ollama etc.
287
+ ```
288
+
289
+ The LLM receives only the computed report — never rows, never category labels —
290
+ and must answer in a fixed JSON schema. It can summarize, prioritize existing
291
+ decisions, suggest answers to open questions (shown as awaiting your
292
+ confirmation, never applied) and raise *semantic* flags on column names, which
293
+ are marked manual-review. Claims that cite unknown evidence or decision ids are
294
+ dropped and counted. With `share_column_names=False` columns are aliased and
295
+ free-text messages are withheld; the report lists exactly what was sent. The
296
+ free-text `context=` never changes a decision by itself. Without an LLM the
297
+ report is complete.
298
+
299
+ Provider status: the interpretation logic and the local OpenAI-compatible
300
+ provider are tested (with a fake provider and a loopback server). The Anthropic
301
+ and Gemini providers are thin wrappers that have **not** been exercised against
302
+ the live APIs in this repository's tests.
303
+
304
+ ## After training
305
+
306
+ `pf.review(y_true, y_pred, report, y_score=..., segments=...)` checks the
307
+ roadmap's hypotheses against real errors, each against an explicit reference
308
+ (e.g. "tails dominate" vs. what Gaussian errors would give), adds residual /
309
+ confusion / precision–recall / segment analysis, and lists every assumption
310
+ that was never verified.
311
+
312
+ ---
313
+
314
+ ## Benchmark
315
+
316
+ `python benchmarks/run.py` runs versioned fixture cases (synthetic data with one
317
+ injected hazard each, plus datasets bundled with scikit-learn) and checks that
318
+ each rule fires, stays silent, or reports *near threshold* as expected. Results,
319
+ including failures, are written to `benchmarks/results/<manifest>.json|md`;
320
+ `--verify` adds experiment outcomes on real datasets (a plain run keeps the
321
+ committed experiment section). Reports cite a rule's "detection pass rate on
322
+ benchmark <manifest>: k/n checks".
323
+
324
+ Current manifest `2026.11-2`: 20/20 cases pass. This is a detection pass rate on
325
+ known-hazard fixtures — not a claim about real-world accuracy, nor about how
326
+ often a decision improves a model.
327
+
328
+ ## Case studies
329
+
330
+ `python examples/case_studies.py` (numbers below are from that script; fixed seeds):
331
+
332
+ | study | rule | before | after |
333
+ |---|---|---|---|
334
+ | California Housing, skewed target, log-transform (HGB) | `target_skew` | MAE 0.3125 | MAE 0.3074 (−1.6%) |
335
+ | Breast Cancer + 20% duplicated rows, de-duplicate (RF) | `duplicate_rows` | CV accuracy 0.9736 | 0.9579 (1.57 pts of inflation) |
336
+ | Diabetes + injected leaked feature, remove it (linear) | `leakage_hint` (critical) | CV R² 0.9999 | 0.4892 |
337
+
338
+ ## Scope and limitations
339
+
340
+ - v1 covers tabular classification and regression. No images, text, time-series
341
+ forecasting, clustering or model selection.
342
+ - Leakage screening sees single-feature association and numeric correlation
343
+ only; leakage through feature combinations, time, preprocessing or repeated
344
+ entities is not detected.
345
+ - Column kinds and the task are inferred heuristically; check the schema table.
346
+ - Experiments test one change at a time; interactions are not measured.
347
+ - `groups` and `time_col` cannot be combined in v1.
348
+ - Raw datetime, free-text and string-identifier columns are not encoded.
349
+
350
+ Design notes: [ARCHITECTURE.md](https://github.com/hkln4/prefit/blob/main/ARCHITECTURE.md) · changes: [CHANGELOG.md](https://github.com/hkln4/prefit/blob/main/CHANGELOG.md) ·
351
+ contributing: [CONTRIBUTING.md](https://github.com/hkln4/prefit/blob/main/CONTRIBUTING.md)