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.
Files changed (107) hide show
  1. beefcot-1.0.3/.gitignore +69 -0
  2. beefcot-1.0.3/PKG-INFO +54 -0
  3. beefcot-1.0.3/README.pypi.md +39 -0
  4. beefcot-1.0.3/pyproject.toml +45 -0
  5. beefcot-1.0.3/src/beefcot/__init__.py +101 -0
  6. beefcot-1.0.3/src/beefcot/anomaly.py +101 -0
  7. beefcot-1.0.3/src/beefcot/chain.py +295 -0
  8. beefcot-1.0.3/src/beefcot/checks/__init__.py +8 -0
  9. beefcot-1.0.3/src/beefcot/checks/anomaly.py +275 -0
  10. beefcot-1.0.3/src/beefcot/checks/anomaly_figures.py +193 -0
  11. beefcot-1.0.3/src/beefcot/checks/bins.py +374 -0
  12. beefcot-1.0.3/src/beefcot/checks/cut_tree.py +92 -0
  13. beefcot-1.0.3/src/beefcot/checks/figures.py +247 -0
  14. beefcot-1.0.3/src/beefcot/checks/forest.py +115 -0
  15. beefcot-1.0.3/src/beefcot/checks/report.py +528 -0
  16. beefcot-1.0.3/src/beefcot/checks/review.py +67 -0
  17. beefcot-1.0.3/src/beefcot/checks/run.py +301 -0
  18. beefcot-1.0.3/src/beefcot/checks/spec.py +301 -0
  19. beefcot-1.0.3/src/beefcot/checks/table.py +107 -0
  20. beefcot-1.0.3/src/beefcot/cli.py +263 -0
  21. beefcot-1.0.3/src/beefcot/cli_presentation.py +270 -0
  22. beefcot-1.0.3/src/beefcot/cutting_plan/__init__.py +1 -0
  23. beefcot-1.0.3/src/beefcot/cutting_plan/economics.py +417 -0
  24. beefcot-1.0.3/src/beefcot/cutting_plan/run.py +307 -0
  25. beefcot-1.0.3/src/beefcot/cutting_plan/solver.py +104 -0
  26. beefcot-1.0.3/src/beefcot/decomposition/__init__.py +1 -0
  27. beefcot-1.0.3/src/beefcot/decomposition/analysis.py +199 -0
  28. beefcot-1.0.3/src/beefcot/decomposition/fits.py +137 -0
  29. beefcot-1.0.3/src/beefcot/decomposition/report.py +188 -0
  30. beefcot-1.0.3/src/beefcot/decomposition/run.py +144 -0
  31. beefcot-1.0.3/src/beefcot/decomposition/sources.py +72 -0
  32. beefcot-1.0.3/src/beefcot/decomposition/spec.py +166 -0
  33. beefcot-1.0.3/src/beefcot/diagnostics/__init__.py +5 -0
  34. beefcot-1.0.3/src/beefcot/diagnostics/analysis.py +275 -0
  35. beefcot-1.0.3/src/beefcot/diagnostics/methods.py +241 -0
  36. beefcot-1.0.3/src/beefcot/diagnostics/plots.py +144 -0
  37. beefcot-1.0.3/src/beefcot/diagnostics/report.py +252 -0
  38. beefcot-1.0.3/src/beefcot/diagnostics/run.py +103 -0
  39. beefcot-1.0.3/src/beefcot/diagnostics/sources.py +416 -0
  40. beefcot-1.0.3/src/beefcot/diagnostics/spec.py +178 -0
  41. beefcot-1.0.3/src/beefcot/model/__init__.py +11 -0
  42. beefcot-1.0.3/src/beefcot/model/expression.py +109 -0
  43. beefcot-1.0.3/src/beefcot/model/functional.py +182 -0
  44. beefcot-1.0.3/src/beefcot/model/learner_registry.py +101 -0
  45. beefcot-1.0.3/src/beefcot/model/learners.py +673 -0
  46. beefcot-1.0.3/src/beefcot/model/rows.py +48 -0
  47. beefcot-1.0.3/src/beefcot/model/runtime.py +49 -0
  48. beefcot-1.0.3/src/beefcot/model/topology.py +129 -0
  49. beefcot-1.0.3/src/beefcot/model/transforms.py +322 -0
  50. beefcot-1.0.3/src/beefcot/model/types.py +95 -0
  51. beefcot-1.0.3/src/beefcot/model_format/__init__.py +25 -0
  52. beefcot-1.0.3/src/beefcot/model_format/blocks.py +137 -0
  53. beefcot-1.0.3/src/beefcot/model_format/card.py +339 -0
  54. beefcot-1.0.3/src/beefcot/model_format/documents.py +152 -0
  55. beefcot-1.0.3/src/beefcot/model_format/export.py +85 -0
  56. beefcot-1.0.3/src/beefcot/model_format/fitted.py +139 -0
  57. beefcot-1.0.3/src/beefcot/model_format/library.py +218 -0
  58. beefcot-1.0.3/src/beefcot/model_format/schema.py +180 -0
  59. beefcot-1.0.3/src/beefcot/prediction/__init__.py +1 -0
  60. beefcot-1.0.3/src/beefcot/prediction/run.py +129 -0
  61. beefcot-1.0.3/src/beefcot/producers/__init__.py +6 -0
  62. beefcot-1.0.3/src/beefcot/producers/network.py +162 -0
  63. beefcot-1.0.3/src/beefcot/producers/rf/__init__.py +2 -0
  64. beefcot-1.0.3/src/beefcot/producers/rf/loop.py +380 -0
  65. beefcot-1.0.3/src/beefcot/producers/rf/relevance.py +201 -0
  66. beefcot-1.0.3/src/beefcot/registry.py +139 -0
  67. beefcot-1.0.3/src/beefcot/regression/__init__.py +6 -0
  68. beefcot-1.0.3/src/beefcot/regression/anomaly.py +142 -0
  69. beefcot-1.0.3/src/beefcot/regression/candidates.py +368 -0
  70. beefcot-1.0.3/src/beefcot/regression/exhaustive.py +381 -0
  71. beefcot-1.0.3/src/beefcot/regression/factory.py +60 -0
  72. beefcot-1.0.3/src/beefcot/regression/mapping_review.py +355 -0
  73. beefcot-1.0.3/src/beefcot/regression/mapping_space.py +421 -0
  74. beefcot-1.0.3/src/beefcot/regression/provenance.py +184 -0
  75. beefcot-1.0.3/src/beefcot/regression/rerf.py +489 -0
  76. beefcot-1.0.3/src/beefcot/regression/rerf_checkpoint.py +611 -0
  77. beefcot-1.0.3/src/beefcot/regression/rerf_controller.py +113 -0
  78. beefcot-1.0.3/src/beefcot/regression/rerf_effort.py +145 -0
  79. beefcot-1.0.3/src/beefcot/regression/rerf_equivalence.py +131 -0
  80. beefcot-1.0.3/src/beefcot/regression/rerf_policy.py +262 -0
  81. beefcot-1.0.3/src/beefcot/regression/rerf_selection.py +295 -0
  82. beefcot-1.0.3/src/beefcot/regression/rerf_settings.py +14 -0
  83. beefcot-1.0.3/src/beefcot/regression/rerf_warm_start.py +79 -0
  84. beefcot-1.0.3/src/beefcot/regression/residual.py +179 -0
  85. beefcot-1.0.3/src/beefcot/regression/rf_expression.py +335 -0
  86. beefcot-1.0.3/src/beefcot/regression/run.py +670 -0
  87. beefcot-1.0.3/src/beefcot/regression/spec.py +996 -0
  88. beefcot-1.0.3/src/beefcot/regression/table.py +167 -0
  89. beefcot-1.0.3/src/beefcot/reports/__init__.py +1 -0
  90. beefcot-1.0.3/src/beefcot/reports/metrics.py +94 -0
  91. beefcot-1.0.3/src/beefcot/reports/plots.py +145 -0
  92. beefcot-1.0.3/src/beefcot/reports/regression_space.py +249 -0
  93. beefcot-1.0.3/src/beefcot/reports/run.py +826 -0
  94. beefcot-1.0.3/src/beefcot/reports/style.py +101 -0
  95. beefcot-1.0.3/src/beefcot/resources/cut_tree.default.json +589 -0
  96. beefcot-1.0.3/src/beefcot/resources/rerf_space.default.yaml +40 -0
  97. beefcot-1.0.3/src/beefcot/runtime/__init__.py +36 -0
  98. beefcot-1.0.3/src/beefcot/runtime/expression_ast.py +67 -0
  99. beefcot-1.0.3/src/beefcot/runtime/loader.py +589 -0
  100. beefcot-1.0.3/src/beefcot/runtime/model.py +453 -0
  101. beefcot-1.0.3/src/beefcot/runtime/schemas/checks-3.schema.json +95 -0
  102. beefcot-1.0.3/src/beefcot/runtime/schemas/model-3.schema.json +687 -0
  103. beefcot-1.0.3/src/beefcot/runtime/schemas/predictors-3.schema.json +309 -0
  104. beefcot-1.0.3/src/beefcot/runtime/schemas/request-3.schema.json +18 -0
  105. beefcot-1.0.3/src/beefcot/runtime/schemas/result-3.schema.json +96 -0
  106. beefcot-1.0.3/src/beefcot/workspace.py +147 -0
  107. beefcot-1.0.3/src/beefcot/yamlio.py +108 -0
@@ -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})