beefcot 1.0.3__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.
- beefcot-1.0.3/.gitignore +69 -0
- beefcot-1.0.3/PKG-INFO +54 -0
- beefcot-1.0.3/README.pypi.md +39 -0
- beefcot-1.0.3/pyproject.toml +45 -0
- beefcot-1.0.3/src/beefcot/__init__.py +101 -0
- beefcot-1.0.3/src/beefcot/anomaly.py +101 -0
- beefcot-1.0.3/src/beefcot/chain.py +295 -0
- beefcot-1.0.3/src/beefcot/checks/__init__.py +8 -0
- beefcot-1.0.3/src/beefcot/checks/anomaly.py +275 -0
- beefcot-1.0.3/src/beefcot/checks/anomaly_figures.py +193 -0
- beefcot-1.0.3/src/beefcot/checks/bins.py +374 -0
- beefcot-1.0.3/src/beefcot/checks/cut_tree.py +92 -0
- beefcot-1.0.3/src/beefcot/checks/figures.py +247 -0
- beefcot-1.0.3/src/beefcot/checks/forest.py +115 -0
- beefcot-1.0.3/src/beefcot/checks/report.py +528 -0
- beefcot-1.0.3/src/beefcot/checks/review.py +67 -0
- beefcot-1.0.3/src/beefcot/checks/run.py +301 -0
- beefcot-1.0.3/src/beefcot/checks/spec.py +301 -0
- beefcot-1.0.3/src/beefcot/checks/table.py +107 -0
- beefcot-1.0.3/src/beefcot/cli.py +263 -0
- beefcot-1.0.3/src/beefcot/cli_presentation.py +270 -0
- beefcot-1.0.3/src/beefcot/cutting_plan/__init__.py +1 -0
- beefcot-1.0.3/src/beefcot/cutting_plan/economics.py +417 -0
- beefcot-1.0.3/src/beefcot/cutting_plan/run.py +307 -0
- beefcot-1.0.3/src/beefcot/cutting_plan/solver.py +104 -0
- beefcot-1.0.3/src/beefcot/decomposition/__init__.py +1 -0
- beefcot-1.0.3/src/beefcot/decomposition/analysis.py +199 -0
- beefcot-1.0.3/src/beefcot/decomposition/fits.py +137 -0
- beefcot-1.0.3/src/beefcot/decomposition/report.py +188 -0
- beefcot-1.0.3/src/beefcot/decomposition/run.py +144 -0
- beefcot-1.0.3/src/beefcot/decomposition/sources.py +72 -0
- beefcot-1.0.3/src/beefcot/decomposition/spec.py +166 -0
- beefcot-1.0.3/src/beefcot/diagnostics/__init__.py +5 -0
- beefcot-1.0.3/src/beefcot/diagnostics/analysis.py +275 -0
- beefcot-1.0.3/src/beefcot/diagnostics/methods.py +241 -0
- beefcot-1.0.3/src/beefcot/diagnostics/plots.py +144 -0
- beefcot-1.0.3/src/beefcot/diagnostics/report.py +252 -0
- beefcot-1.0.3/src/beefcot/diagnostics/run.py +103 -0
- beefcot-1.0.3/src/beefcot/diagnostics/sources.py +416 -0
- beefcot-1.0.3/src/beefcot/diagnostics/spec.py +178 -0
- beefcot-1.0.3/src/beefcot/model/__init__.py +11 -0
- beefcot-1.0.3/src/beefcot/model/expression.py +109 -0
- beefcot-1.0.3/src/beefcot/model/functional.py +182 -0
- beefcot-1.0.3/src/beefcot/model/learner_registry.py +101 -0
- beefcot-1.0.3/src/beefcot/model/learners.py +673 -0
- beefcot-1.0.3/src/beefcot/model/rows.py +48 -0
- beefcot-1.0.3/src/beefcot/model/runtime.py +49 -0
- beefcot-1.0.3/src/beefcot/model/topology.py +129 -0
- beefcot-1.0.3/src/beefcot/model/transforms.py +322 -0
- beefcot-1.0.3/src/beefcot/model/types.py +95 -0
- beefcot-1.0.3/src/beefcot/model_format/__init__.py +25 -0
- beefcot-1.0.3/src/beefcot/model_format/blocks.py +137 -0
- beefcot-1.0.3/src/beefcot/model_format/card.py +339 -0
- beefcot-1.0.3/src/beefcot/model_format/documents.py +152 -0
- beefcot-1.0.3/src/beefcot/model_format/export.py +85 -0
- beefcot-1.0.3/src/beefcot/model_format/fitted.py +139 -0
- beefcot-1.0.3/src/beefcot/model_format/library.py +218 -0
- beefcot-1.0.3/src/beefcot/model_format/schema.py +180 -0
- beefcot-1.0.3/src/beefcot/prediction/__init__.py +1 -0
- beefcot-1.0.3/src/beefcot/prediction/run.py +129 -0
- beefcot-1.0.3/src/beefcot/producers/__init__.py +6 -0
- beefcot-1.0.3/src/beefcot/producers/network.py +162 -0
- beefcot-1.0.3/src/beefcot/producers/rf/__init__.py +2 -0
- beefcot-1.0.3/src/beefcot/producers/rf/loop.py +380 -0
- beefcot-1.0.3/src/beefcot/producers/rf/relevance.py +201 -0
- beefcot-1.0.3/src/beefcot/registry.py +139 -0
- beefcot-1.0.3/src/beefcot/regression/__init__.py +6 -0
- beefcot-1.0.3/src/beefcot/regression/anomaly.py +142 -0
- beefcot-1.0.3/src/beefcot/regression/candidates.py +368 -0
- beefcot-1.0.3/src/beefcot/regression/exhaustive.py +381 -0
- beefcot-1.0.3/src/beefcot/regression/factory.py +60 -0
- beefcot-1.0.3/src/beefcot/regression/mapping_review.py +355 -0
- beefcot-1.0.3/src/beefcot/regression/mapping_space.py +421 -0
- beefcot-1.0.3/src/beefcot/regression/provenance.py +184 -0
- beefcot-1.0.3/src/beefcot/regression/rerf.py +489 -0
- beefcot-1.0.3/src/beefcot/regression/rerf_checkpoint.py +611 -0
- beefcot-1.0.3/src/beefcot/regression/rerf_controller.py +113 -0
- beefcot-1.0.3/src/beefcot/regression/rerf_effort.py +145 -0
- beefcot-1.0.3/src/beefcot/regression/rerf_equivalence.py +131 -0
- beefcot-1.0.3/src/beefcot/regression/rerf_policy.py +262 -0
- beefcot-1.0.3/src/beefcot/regression/rerf_selection.py +295 -0
- beefcot-1.0.3/src/beefcot/regression/rerf_settings.py +14 -0
- beefcot-1.0.3/src/beefcot/regression/rerf_warm_start.py +79 -0
- beefcot-1.0.3/src/beefcot/regression/residual.py +179 -0
- beefcot-1.0.3/src/beefcot/regression/rf_expression.py +335 -0
- beefcot-1.0.3/src/beefcot/regression/run.py +670 -0
- beefcot-1.0.3/src/beefcot/regression/spec.py +996 -0
- beefcot-1.0.3/src/beefcot/regression/table.py +167 -0
- beefcot-1.0.3/src/beefcot/reports/__init__.py +1 -0
- beefcot-1.0.3/src/beefcot/reports/metrics.py +94 -0
- beefcot-1.0.3/src/beefcot/reports/plots.py +145 -0
- beefcot-1.0.3/src/beefcot/reports/regression_space.py +249 -0
- beefcot-1.0.3/src/beefcot/reports/run.py +826 -0
- beefcot-1.0.3/src/beefcot/reports/style.py +101 -0
- beefcot-1.0.3/src/beefcot/resources/cut_tree.default.json +589 -0
- beefcot-1.0.3/src/beefcot/resources/rerf_space.default.yaml +40 -0
- beefcot-1.0.3/src/beefcot/runtime/__init__.py +36 -0
- beefcot-1.0.3/src/beefcot/runtime/expression_ast.py +67 -0
- beefcot-1.0.3/src/beefcot/runtime/loader.py +589 -0
- beefcot-1.0.3/src/beefcot/runtime/model.py +453 -0
- beefcot-1.0.3/src/beefcot/runtime/schemas/checks-3.schema.json +95 -0
- beefcot-1.0.3/src/beefcot/runtime/schemas/model-3.schema.json +687 -0
- beefcot-1.0.3/src/beefcot/runtime/schemas/predictors-3.schema.json +309 -0
- beefcot-1.0.3/src/beefcot/runtime/schemas/request-3.schema.json +18 -0
- beefcot-1.0.3/src/beefcot/runtime/schemas/result-3.schema.json +96 -0
- beefcot-1.0.3/src/beefcot/workspace.py +147 -0
- beefcot-1.0.3/src/beefcot/yamlio.py +108 -0
beefcot-1.0.3/.gitignore
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Local environments and editor junk
|
|
2
|
+
.venv/
|
|
3
|
+
.venv-*/
|
|
4
|
+
venv/
|
|
5
|
+
env/
|
|
6
|
+
.DS_Store
|
|
7
|
+
.idea/
|
|
8
|
+
.vscode/
|
|
9
|
+
*.swp
|
|
10
|
+
*~
|
|
11
|
+
|
|
12
|
+
# Python / test / coverage
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
*$py.class
|
|
16
|
+
*.egg-info/
|
|
17
|
+
.eggs/
|
|
18
|
+
dist/
|
|
19
|
+
build/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
htmlcov/
|
|
26
|
+
coverage.xml
|
|
27
|
+
|
|
28
|
+
# Runtime caches
|
|
29
|
+
.cache/
|
|
30
|
+
|
|
31
|
+
# Run records (written under the workspace's runs folder).
|
|
32
|
+
/runs/
|
|
33
|
+
/workflow-log/
|
|
34
|
+
/docs/reports/
|
|
35
|
+
/docs/deployment/m1-acceptance.md
|
|
36
|
+
/docs/development/relocations-20261020.tsv
|
|
37
|
+
|
|
38
|
+
# Retired code, tests and moved-aside material: local audit, not GitHub.
|
|
39
|
+
/archive/
|
|
40
|
+
|
|
41
|
+
# The model library: delivered models carry rows of the data in their self-checks.
|
|
42
|
+
/models/
|
|
43
|
+
|
|
44
|
+
# Data lives next to this repo under $BEEF_COT_ROOT/DataSets, never in git.
|
|
45
|
+
/DataSets/
|
|
46
|
+
*.xlsx
|
|
47
|
+
*.xls
|
|
48
|
+
*.csv
|
|
49
|
+
*.tsv
|
|
50
|
+
*.parquet
|
|
51
|
+
*.npy
|
|
52
|
+
*.npz
|
|
53
|
+
*.sqlite
|
|
54
|
+
*.sqlite3
|
|
55
|
+
*.db
|
|
56
|
+
*.zip
|
|
57
|
+
*.tar.gz
|
|
58
|
+
|
|
59
|
+
# Fitted blobs if someone dumps them in-tree
|
|
60
|
+
*.pkl
|
|
61
|
+
*.joblib
|
|
62
|
+
*.pt
|
|
63
|
+
*.pth
|
|
64
|
+
*.ckpt
|
|
65
|
+
|
|
66
|
+
# Logs / notebooks
|
|
67
|
+
*.log
|
|
68
|
+
.ipynb_checkpoints/
|
|
69
|
+
.factorized_policy_search*.yaml
|
beefcot-1.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: beefcot
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Carcass yield models and cutting plans: one YAML in, one delivered model out
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: jarvisplot>=2.1
|
|
7
|
+
Requires-Dist: numpy>=2.5
|
|
8
|
+
Requires-Dist: pandas>=3.0
|
|
9
|
+
Requires-Dist: pytest>=8
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Requires-Dist: scikit-learn>=1.9
|
|
12
|
+
Requires-Dist: scipy>=1.18
|
|
13
|
+
Requires-Dist: xgboost>=3.3
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# beefcot
|
|
17
|
+
|
|
18
|
+
Carcass yield regression models and cutting plans, defined by YAML. Python 3.13
|
|
19
|
+
or newer is required.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python3 -m pip install beefcot
|
|
23
|
+
beefcot --help
|
|
24
|
+
beefcot validate /path/to/job.yaml
|
|
25
|
+
beefcot fit /path/to/job.yaml
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Each fit starts from a `kind: regression_tree` YAML declaring its input table,
|
|
29
|
+
feature and target columns, cut-tree mapping, split and regression space.
|
|
30
|
+
Available methods are profile, RF, DNN, ReRF and exhaustive search. Structural
|
|
31
|
+
nodes aggregate measured products; data and fitted models are supplied by users.
|
|
32
|
+
|
|
33
|
+
Delivered format-3 models can be loaded once and called through the NumPy-only
|
|
34
|
+
runtime:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from beefcot.runtime import load
|
|
38
|
+
|
|
39
|
+
model = load("/path/to/delivered/model")
|
|
40
|
+
result = model.predict_one(carcass_inputs)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The source distribution contains package code, required generic resources,
|
|
44
|
+
runtime schemas and build metadata. It contains no animal datasets, fitted
|
|
45
|
+
models, reports, machine setup records or development job configurations.
|
|
46
|
+
|
|
47
|
+
Plotting delegates to the public `jplot plot.yaml` command. Keep `jplot` on the
|
|
48
|
+
active environment's PATH; plotting YAML and input tables are retained beside
|
|
49
|
+
the generated figures for redrawing. Supported usage is available from
|
|
50
|
+
`jplot --help` and `jplot man <topic> --json`.
|
|
51
|
+
|
|
52
|
+
Development configuration templates, tests and tools are maintained separately
|
|
53
|
+
from the installed package. Keep your job YAML, data, outputs and delivered
|
|
54
|
+
models in an independent workspace.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# beefcot
|
|
2
|
+
|
|
3
|
+
Carcass yield regression models and cutting plans, defined by YAML. Python 3.13
|
|
4
|
+
or newer is required.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
python3 -m pip install beefcot
|
|
8
|
+
beefcot --help
|
|
9
|
+
beefcot validate /path/to/job.yaml
|
|
10
|
+
beefcot fit /path/to/job.yaml
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Each fit starts from a `kind: regression_tree` YAML declaring its input table,
|
|
14
|
+
feature and target columns, cut-tree mapping, split and regression space.
|
|
15
|
+
Available methods are profile, RF, DNN, ReRF and exhaustive search. Structural
|
|
16
|
+
nodes aggregate measured products; data and fitted models are supplied by users.
|
|
17
|
+
|
|
18
|
+
Delivered format-3 models can be loaded once and called through the NumPy-only
|
|
19
|
+
runtime:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from beefcot.runtime import load
|
|
23
|
+
|
|
24
|
+
model = load("/path/to/delivered/model")
|
|
25
|
+
result = model.predict_one(carcass_inputs)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The source distribution contains package code, required generic resources,
|
|
29
|
+
runtime schemas and build metadata. It contains no animal datasets, fitted
|
|
30
|
+
models, reports, machine setup records or development job configurations.
|
|
31
|
+
|
|
32
|
+
Plotting delegates to the public `jplot plot.yaml` command. Keep `jplot` on the
|
|
33
|
+
active environment's PATH; plotting YAML and input tables are retained beside
|
|
34
|
+
the generated figures for redrawing. Supported usage is available from
|
|
35
|
+
`jplot --help` and `jplot man <topic> --json`.
|
|
36
|
+
|
|
37
|
+
Development configuration templates, tests and tools are maintained separately
|
|
38
|
+
from the installed package. Keep your job YAML, data, outputs and delivered
|
|
39
|
+
models in an independent workspace.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "beefcot"
|
|
7
|
+
dynamic = ["version"] # from src/beefcot/__init__.py; see CHANGELOG.md
|
|
8
|
+
description = "Carcass yield models and cutting plans: one YAML in, one delivered model out"
|
|
9
|
+
readme = "README.pypi.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
# Lower bounds are the versions this was built and measured on; no upper bounds, because a
|
|
12
|
+
# cap only gets in the way at upgrade time. The delivered numbers are pinned by the run
|
|
13
|
+
# records, not by the dependency ranges.
|
|
14
|
+
dependencies = [
|
|
15
|
+
"numpy>=2.5",
|
|
16
|
+
"pandas>=3.0",
|
|
17
|
+
"scipy>=1.18",
|
|
18
|
+
"scikit-learn>=1.9",
|
|
19
|
+
"xgboost>=3.3",
|
|
20
|
+
"PyYAML>=6.0",
|
|
21
|
+
"JarvisPLOT>=2.1",
|
|
22
|
+
"pytest>=8",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
beefcot = "beefcot.cli:main"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.version]
|
|
29
|
+
path = "src/beefcot/__init__.py"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/beefcot"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
include = [
|
|
36
|
+
"/src/beefcot", "/pyproject.toml", "/README.pypi.md",
|
|
37
|
+
]
|
|
38
|
+
exclude = [
|
|
39
|
+
"**/__pycache__", "**/*.pyc", "**/.DS_Store",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
markers = ["slow: reproduces the recorded RF run (about 100 seconds); run with -m slow"]
|
|
45
|
+
addopts = "-m 'not slow'"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Carcass yield models, found from one YAML.
|
|
2
|
+
|
|
3
|
+
Every job is one YAML and one verb, the same in Python as on the command line (``beefcot fit ...``):
|
|
4
|
+
|
|
5
|
+
import beefcot
|
|
6
|
+
|
|
7
|
+
beefcot.fit("config/regression/murdoch_rf.yaml") # kind: regression_tree -> a run with a model
|
|
8
|
+
beefcot.predict("config/prediction/murdoch_rf_241.yaml") # kind: prediction -> one table
|
|
9
|
+
beefcot.plan("config/plan/murdoch_demo.yaml") # kind: cutting_plan -> a plan
|
|
10
|
+
beefcot.check("config/checks/murdoch_core3_bins.yaml") # kind: bin_variance -> a data check
|
|
11
|
+
beefcot.decompose("config/decomposition/murdoch_rf.yaml") # kind: error_decomposition -> noise / bias² / variance
|
|
12
|
+
beefcot.run("config/pipelines/murdoch_smoke.yaml") # kind: pipeline -> several, in order
|
|
13
|
+
|
|
14
|
+
model = beefcot.load_model("murdoch_rf-rf-20261003-83a160") # a directory or a library id
|
|
15
|
+
model.predict_one({"sidewt": 155.4, "P8": 5, "Bred": "BxH"}) # one carcass -> its products
|
|
16
|
+
|
|
17
|
+
Each verb takes only the kind it names and returns where it wrote (a folder, or the prediction
|
|
18
|
+
table). ``beefcot.runtime`` is what loads and calls a delivered model (numpy only); everything else
|
|
19
|
+
in the package is an implementation detail.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
__version__ = "1.0.3" # the one place it is written; CHANGELOG.md records each version
|
|
28
|
+
|
|
29
|
+
__all__ = ["__version__", "fit", "predict", "plan", "check", "decompose", "run", "load_model"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fit(config: str | Path) -> Path:
|
|
33
|
+
"""Find and deliver a regression tree (``kind: regression_tree``); returns the run folder."""
|
|
34
|
+
return _do("fit", config)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def predict(config: str | Path, *, workspace: str | Path | None = None) -> Path:
|
|
38
|
+
"""Predict every row of a CSV with a delivered model (``kind: prediction``); returns the table.
|
|
39
|
+
``workspace`` names the model library a ``model.id`` is looked up in."""
|
|
40
|
+
return _do("predict", config, workspace)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def plan(config: str | Path, *, workspace: str | Path | None = None) -> Path:
|
|
44
|
+
"""Make a cutting plan (``kind: cutting_plan``); returns the plan folder. ``workspace`` names the
|
|
45
|
+
model library a ``model.id`` is looked up in."""
|
|
46
|
+
return _do("plan", config, workspace)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check(config: str | Path) -> Path:
|
|
50
|
+
"""Measure data, label anomalies or analyse saved regressions; returns the
|
|
51
|
+
analysis folder. An anomaly check fits only its diagnostic detector."""
|
|
52
|
+
return _do("check", config)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def decompose(config: str | Path) -> Path:
|
|
56
|
+
"""Split a regression YAML's error into noise, bias² and estimation variance
|
|
57
|
+
(``kind: error_decomposition``); returns the analysis folder. No model is delivered."""
|
|
58
|
+
return _do("decompose", config)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run(config: str | Path, *, workspace: str | Path | None = None) -> Path:
|
|
62
|
+
"""Run several YAMLs in order (``kind: pipeline``); returns the folder holding the chain's record."""
|
|
63
|
+
return _do("run", config, workspace)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _do(verb: str, config: str | Path, workspace: str | Path | None = None) -> Path:
|
|
67
|
+
"""Every path the YAML names is absolute or relative to the YAML, and nothing is written over an
|
|
68
|
+
earlier output: a taken folder or file gets a ``_02``, ``_03``, ... name."""
|
|
69
|
+
from beefcot import registry
|
|
70
|
+
from beefcot.model.runtime import bootstrap_native_runtime
|
|
71
|
+
from beefcot.workspace import preferring
|
|
72
|
+
|
|
73
|
+
path = Path(config).expanduser().resolve()
|
|
74
|
+
kind = registry.kind_of(path)
|
|
75
|
+
entry = registry.kind(kind)
|
|
76
|
+
if entry.command != verb:
|
|
77
|
+
raise ValueError(f"{path} is kind={kind!r}: use `beefcot {entry.command}` (beefcot.{entry.command}), "
|
|
78
|
+
f"not `beefcot {verb}`")
|
|
79
|
+
bootstrap_native_runtime()
|
|
80
|
+
with preferring(workspace):
|
|
81
|
+
return entry.run(path)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_model(model: str | Path, *, workspace: str | Path | None = None) -> Any:
|
|
85
|
+
"""Load a delivered model (format version 3), by directory or by library id.
|
|
86
|
+
|
|
87
|
+
This is ``beefcot.runtime.load`` with the workspace's model library to find an id in: every check
|
|
88
|
+
runs on the first load, and a model loaded once in a process is the same object every time after.
|
|
89
|
+
"""
|
|
90
|
+
from beefcot import runtime
|
|
91
|
+
|
|
92
|
+
path = Path(model).expanduser()
|
|
93
|
+
if path.exists():
|
|
94
|
+
return runtime.load(path)
|
|
95
|
+
if path.parent != Path("."): # it was written as a path, so say it is missing as one
|
|
96
|
+
raise FileNotFoundError(f"no model directory at {path}")
|
|
97
|
+
from beefcot.model_format.library import ModelLibrary
|
|
98
|
+
from beefcot.workspace import Workspace as _Workspace
|
|
99
|
+
|
|
100
|
+
space = None if workspace is None else _Workspace.load(workspace)
|
|
101
|
+
return ModelLibrary.from_workspace(space).load(str(model))
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Versioned numeric Isolation Forest scoring, used only by YAML jobs.
|
|
2
|
+
|
|
3
|
+
Scores are unusualness, not error probabilities. No source values are changed.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
from math import isfinite
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
VERSION = "isolation_forest_v1"
|
|
16
|
+
DEFAULT_THRESHOLD = 0.5
|
|
17
|
+
LABEL_COLUMNS = ("iforest_score", "iforest_label", "iforest_rank", "iforest_reason")
|
|
18
|
+
PARAMS = {"n_estimators": 256, "max_samples": "auto", "max_features": 1.0,
|
|
19
|
+
"bootstrap": False, "contamination": "auto", "n_jobs": 1}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def threshold(value, *, context: str) -> float:
|
|
23
|
+
if (isinstance(value, bool) or not isinstance(value, (int, float))
|
|
24
|
+
or not isfinite(value) or not 0 <= value <= 1):
|
|
25
|
+
raise ValueError(f"{context}: threshold must be a finite number in [0, 1]")
|
|
26
|
+
return float(value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def seed(value, *, context: str) -> int:
|
|
30
|
+
if type(value) is not int or value < 0:
|
|
31
|
+
raise ValueError(f"{context}: seed must be a nonnegative integer")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def settings(yaml_seed: int) -> dict:
|
|
36
|
+
random_state = int(np.random.SeedSequence(yaml_seed, spawn_key=(0x49464F52,)).generate_state(1)[0])
|
|
37
|
+
return {"version": VERSION, "score": "-score_samples; higher is more unusual",
|
|
38
|
+
"threshold_rule": "score > threshold", "library": "scikit-learn",
|
|
39
|
+
"library_version": version("scikit-learn"),
|
|
40
|
+
"params": {**PARAMS, "random_state": random_state}}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def id_hash(ids) -> str:
|
|
44
|
+
import json
|
|
45
|
+
return hashlib.sha256(json.dumps(list(map(str, ids)), ensure_ascii=False,
|
|
46
|
+
separators=(",", ":")).encode("utf-8")).hexdigest()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def numeric_matrix(raw: pd.DataFrame, columns, *, source) -> np.ndarray:
|
|
50
|
+
arrays = []
|
|
51
|
+
for column in columns:
|
|
52
|
+
values = raw[column]
|
|
53
|
+
numbers = pd.to_numeric(values, errors="coerce")
|
|
54
|
+
wrong = values.notna() & numbers.isna()
|
|
55
|
+
if wrong.any():
|
|
56
|
+
raise ValueError(f"{source}: isolation_forest column {column!r} contains nonnumeric text, "
|
|
57
|
+
f"e.g. {values[wrong].tolist()[:5]}")
|
|
58
|
+
arrays.append(numbers.to_numpy(dtype=float))
|
|
59
|
+
return np.column_stack(arrays)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class Scores:
|
|
64
|
+
values: np.ndarray
|
|
65
|
+
labels: np.ndarray
|
|
66
|
+
reasons: tuple[str, ...]
|
|
67
|
+
reference_rows: np.ndarray
|
|
68
|
+
resolved: dict
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def score(matrix: np.ndarray, columns, reference_rows, *, yaml_seed: int, limit: float,
|
|
72
|
+
context: str) -> Scores:
|
|
73
|
+
"""Fit on finite reference rows, score every finite row; never impute."""
|
|
74
|
+
from sklearn.ensemble import IsolationForest
|
|
75
|
+
|
|
76
|
+
matrix = np.asarray(matrix, dtype=float)
|
|
77
|
+
# sklearn consumes float32 internally; do not silently overflow finite CSV values.
|
|
78
|
+
usable = np.isfinite(matrix) & (np.abs(matrix) <= np.finfo(np.float32).max)
|
|
79
|
+
finite = usable.all(axis=1)
|
|
80
|
+
reference = np.asarray(reference_rows, dtype=np.intp)
|
|
81
|
+
reference = reference[finite[reference]]
|
|
82
|
+
if len(reference) < 2:
|
|
83
|
+
raise ValueError(f"{context}: isolation_forest needs at least 2 scoreable reference rows; "
|
|
84
|
+
f"found {len(reference)} for columns {list(columns)}")
|
|
85
|
+
resolved = settings(yaml_seed)
|
|
86
|
+
detector = IsolationForest(**resolved["params"]).fit(matrix[reference])
|
|
87
|
+
values = np.full(len(matrix), np.nan)
|
|
88
|
+
values[finite] = -detector.score_samples(matrix[finite])
|
|
89
|
+
labels = np.full(len(matrix), "unscored", dtype=object)
|
|
90
|
+
labels[finite] = np.where(values[finite] > limit, "review", "within_threshold")
|
|
91
|
+
reasons = tuple("; ".join(
|
|
92
|
+
f"{columns[j]}:" + ("missing" if np.isnan(matrix[i, j]) else
|
|
93
|
+
"nonfinite" if not np.isfinite(matrix[i, j]) else "float32_overflow")
|
|
94
|
+
for j in np.flatnonzero(~usable[i])) for i in range(len(matrix)))
|
|
95
|
+
return Scores(values, labels, reasons, reference, resolved)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def labelled_columns(result: Scores) -> pd.DataFrame:
|
|
99
|
+
return pd.DataFrame({"iforest_score": result.values, "iforest_label": result.labels,
|
|
100
|
+
"iforest_rank": pd.Series(result.values).rank(method="min", ascending=False).astype("Int64"),
|
|
101
|
+
"iforest_reason": result.reasons})
|