morphtree 2.1.0__py3-none-win_amd64.whl

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 (79) hide show
  1. morphtree/__init__.py +28 -0
  2. morphtree/__main__.py +3 -0
  3. morphtree/_experimental/__init__.py +1 -0
  4. morphtree/_experimental/admission.py +364 -0
  5. morphtree/_experimental/base.py +215 -0
  6. morphtree/_experimental/component_cache_v2.py +263 -0
  7. morphtree/_experimental/evolution_report.py +307 -0
  8. morphtree/_experimental/execution_v2.py +697 -0
  9. morphtree/_experimental/facade.py +289 -0
  10. morphtree/_experimental/integrated.py +748 -0
  11. morphtree/_experimental/learning.py +418 -0
  12. morphtree/_experimental/manifest.json +204 -0
  13. morphtree/_experimental/numeric_fixture.py +21 -0
  14. morphtree/_experimental/parameter_contract.py +38 -0
  15. morphtree/_experimental/postfit.py +51 -0
  16. morphtree/_experimental/preprocessing.py +133 -0
  17. morphtree/_experimental/processes.py +191 -0
  18. morphtree/_experimental/producer.py +542 -0
  19. morphtree/_experimental/product.py +156 -0
  20. morphtree/_experimental/product_cli.py +43 -0
  21. morphtree/_experimental/roundtrip_v3.py +311 -0
  22. morphtree/_experimental/runtime_io.py +65 -0
  23. morphtree/_experimental/sampling.py +26 -0
  24. morphtree/_experimental/support.py +281 -0
  25. morphtree/_experimental/target.py +59 -0
  26. morphtree/_experimental/training.py +53 -0
  27. morphtree/_experimental/worker.py +682 -0
  28. morphtree/_installed.py +17 -0
  29. morphtree/_native/__init__.py +1 -0
  30. morphtree/_native/bin/win_amd64/morphtree_kernels.dll +0 -0
  31. morphtree/_native/buffers.py +82 -0
  32. morphtree/_native/loader.py +39 -0
  33. morphtree/_native/manifest.json +29 -0
  34. morphtree/_native/runtime.py +48 -0
  35. morphtree/_native/src/histogram.c +61 -0
  36. morphtree/_native/src/traversal.c +51 -0
  37. morphtree/api.py +388 -0
  38. morphtree/api_compat.py +65 -0
  39. morphtree/backend.py +162 -0
  40. morphtree/booster.py +252 -0
  41. morphtree/cli.py +52 -0
  42. morphtree/config.py +262 -0
  43. morphtree/control.py +20 -0
  44. morphtree/data.py +300 -0
  45. morphtree/diagnostics.py +158 -0
  46. morphtree/estimators.py +156 -0
  47. morphtree/examples.py +25 -0
  48. morphtree/exporting.py +224 -0
  49. morphtree/legacy_artifacts.py +32 -0
  50. morphtree/metrics.py +156 -0
  51. morphtree/model.py +1010 -0
  52. morphtree/mutations.py +205 -0
  53. morphtree/native_lab_backend.py +79 -0
  54. morphtree/numpy_backend.py +180 -0
  55. morphtree/optuna_tuner.py +154 -0
  56. morphtree/persistence.py +414 -0
  57. morphtree/prediction_output.py +35 -0
  58. morphtree/prepared_bins.py +82 -0
  59. morphtree/public.py +106 -0
  60. morphtree/resource_plan.py +170 -0
  61. morphtree/scalable.py +781 -0
  62. morphtree/scalable_backend.py +74 -0
  63. morphtree/scalable_checkpoint.py +129 -0
  64. morphtree/scalable_data.py +255 -0
  65. morphtree/scalable_receipts.py +92 -0
  66. morphtree/scalable_target.py +68 -0
  67. morphtree/scalable_variants.py +133 -0
  68. morphtree/sklearn_view.py +79 -0
  69. morphtree/source_markers.py +25 -0
  70. morphtree/species.py +47 -0
  71. morphtree/temporary.py +65 -0
  72. morphtree/training_fragment.py +130 -0
  73. morphtree/tree.py +231 -0
  74. morphtree/tuning.py +150 -0
  75. morphtree-2.1.0.dist-info/METADATA +212 -0
  76. morphtree-2.1.0.dist-info/RECORD +79 -0
  77. morphtree-2.1.0.dist-info/WHEEL +4 -0
  78. morphtree-2.1.0.dist-info/entry_points.txt +2 -0
  79. morphtree-2.1.0.dist-info/licenses/LICENSE +21 -0
morphtree/exporting.py ADDED
@@ -0,0 +1,224 @@
1
+ """Readable standalone Python export with a standard-library runtime."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from .config import SPEC_VERSION, VERSION
9
+ from .persistence import atomic_text
10
+
11
+ _RUNTIME_MODULES = ("config", "public", "data", "metrics", "backend", "tree", "diagnostics", "control", "booster", "mutations", "tuning",
12
+ "persistence", "species", "model", "exporting")
13
+
14
+
15
+ _STANDALONE_HEADER = '"""MorphTree standalone standard-library runtime.\n\nMIT License\n\nCopyright (c) 2026 Luca Lullo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"""'
16
+
17
+
18
+ class _RemoveLocalImports(ast.NodeTransformer):
19
+ def visit_ImportFrom(self, node):
20
+ if node.level or node.module == "__future__":
21
+ return None
22
+ return node
23
+
24
+
25
+ def _bundle_runtime(inference_only=False, task='binary'):
26
+ source_file = Path(__file__).resolve()
27
+ current = source_file.read_text(encoding="utf-8")
28
+ marker = "\n# MORPHTREE_" + "BUNDLE_END\n"
29
+ if marker in current:
30
+ return current.split(marker, 1)[0]
31
+ directory = source_file.parent
32
+ if not all((directory / f"{name}.py").is_file() for name in _RUNTIME_MODULES):
33
+ raise RuntimeError("Runtime source files are not available for export")
34
+ blocks = [_STANDALONE_HEADER, "from __future__ import annotations\n"]
35
+ for name in _RUNTIME_MODULES:
36
+ if inference_only and name in ('diagnostics', 'control', 'mutations', 'tuning', 'exporting'):
37
+ continue
38
+ source = (directory / f"{name}.py").read_text(encoding="utf-8")
39
+ tree = _RemoveLocalImports().visit(ast.parse(source))
40
+ if inference_only:
41
+ retained = {
42
+ 'MorphTreeClassifier': {'__init__','_validate_backend_config','_make_backend','_require_fitted',
43
+ 'predict_proba','predict','score','load_model','_from_payload'},
44
+ 'MorphTreeRegressor': {'__init__','_make_backend','_require_fitted','predict','score','load_model','_from_payload'},
45
+ 'MorphTreeMulticlassClassifier': {'__init__','_make_backend','_require_fitted','predict_proba','predict','score','load_model','_from_payload'},
46
+ 'BinaryHistogramBooster': {'__init__','predict_raw','predict_proba'},
47
+ 'HistogramTree': {'__init__','predict_binned'},
48
+ 'PythonBackend': {'bin','probabilities','predict_tree','add_update','multiclass_raw','multiclass_probabilities',
49
+ 'multiclass_update','center_multiclass'},
50
+ 'Binner': {'__init__','transform'},
51
+ 'AlgorithmSpecies': {'__post_init__','structural_fingerprint'},
52
+ }
53
+ excluded = {'TrainingCache', 'AutoMorphTree'} | ({'MorphTreeRegressor'} if task == 'binary' else set())
54
+ tree.body = [node for node in tree.body if not isinstance(node, ast.ClassDef) or node.name not in excluded]
55
+ for node in tree.body:
56
+ if isinstance(node, ast.ClassDef) and node.name in retained:
57
+ node.body = [part for part in node.body if not isinstance(part, (ast.FunctionDef, ast.Assign))
58
+ or isinstance(part, ast.FunctionDef) and part.name in retained[node.name]]
59
+ # AlgorithmSpecies dataclass annotations are retained by the above filter.
60
+ ast.fix_missing_locations(tree)
61
+ blocks.append(f"\n# Module: {name}\n" + ast.unparse(tree))
62
+ return "\n\n".join(blocks)
63
+
64
+
65
+ def export_algorithm(species, path):
66
+ payload = json.dumps(species.to_dict(), sort_keys=True, allow_nan=False)
67
+ suffix = '''
68
+ _SPECIES_DATA = json.loads(PAYLOAD_LITERAL)
69
+ _EXPORTED_GENOME, _EXPORTED_PARAMS, _EXPORTED_METADATA = decode_species(_SPECIES_DATA)
70
+
71
+ class DiscoveredAlgorithm(MorphTreeClassifier):
72
+ """Train this frozen algorithm on new data. Optional native numeric retuning."""
73
+ def __init__(self, *, params=None, config=None, retune=False, genome=None, backend='pure'):
74
+ if backend != 'pure':
75
+ raise ValueError('Standalone algorithm supports only pure backend')
76
+ if type(retune) is not bool:
77
+ raise ValueError("retune must be a bool")
78
+ if config is not None and not isinstance(config, MorphTreeConfig):
79
+ raise ValueError("config must be a MorphTreeConfig")
80
+ if genome is not None:
81
+ if not isinstance(genome, AlgorithmGenome) or genome.fingerprint() != _EXPORTED_GENOME.fingerprint():
82
+ raise ValueError("This exported algorithm has a frozen structural fingerprint; genome mismatch")
83
+ recipe = _EXPORTED_METADATA.get('learning_recipe')
84
+ settings = replace(recipe_settings(recipe, config),
85
+ enable_morphing=False, enable_tuning=retune, tuner="native")
86
+ if recipe is None and config is None:
87
+ settings = replace(settings, memory_enabled=True)
88
+ super().__init__(config=settings, genome=_EXPORTED_GENOME, params=_EXPORTED_PARAMS if params is None else params)
89
+ self._algorithm_locked = True
90
+ self._training_partition = recipe['partition'] if recipe else None
91
+
92
+ @classmethod
93
+ def from_algorithm(cls, algorithm, *, retune=False, config=None):
94
+ species = algorithm if isinstance(algorithm, AlgorithmSpecies) else AlgorithmSpecies.load(algorithm)
95
+ return cls(params=species.parameters, genome=species.genome, config=config, retune=retune)
96
+
97
+ @classmethod
98
+ def discover(cls, *args, **kwargs):
99
+ raise ValueError("DiscoveredAlgorithm is frozen; use MorphTreeClassifier.discover() for a new structural search")
100
+
101
+ SPECIES = AlgorithmSpecies(_EXPORTED_GENOME, _EXPORTED_PARAMS, _EXPORTED_METADATA)
102
+ '''.replace("PAYLOAD_LITERAL", repr(payload))
103
+ atomic_text(path, _bundle_runtime() + "\n# MORPHTREE_BUNDLE_END\n" + suffix)
104
+
105
+
106
+ def export_predictor(model, path):
107
+ if getattr(model, 'task', None) == 'multiclass':
108
+ text = json.dumps(model._model_payload(include_report=False), sort_keys=True, allow_nan=False)
109
+ suffix = '''
110
+ _MODEL = MorphTreeMulticlassClassifier._from_payload(json.loads(PAYLOAD_LITERAL))
111
+ classes_ = _MODEL.classes_
112
+
113
+ def predict(X):
114
+ return _MODEL.predict(X)
115
+
116
+ def predict_proba(X):
117
+ return _MODEL.predict_proba(X)
118
+ '''.replace('PAYLOAD_LITERAL', repr(text))
119
+ atomic_text(path, _bundle_runtime(inference_only=True, task='multiclass') + '\n# MORPHTREE_BUNDLE_END\n' + suffix)
120
+ return
121
+ if getattr(model, 'task', None) == 'regression':
122
+ text = json.dumps(model._model_payload(include_report=False), sort_keys=True, allow_nan=False)
123
+ suffix = '''
124
+ _MODEL = MorphTreeRegressor._from_payload(json.loads(PAYLOAD_LITERAL))
125
+
126
+ def predict(X):
127
+ return _MODEL.predict(X)
128
+ '''.replace('PAYLOAD_LITERAL', repr(text))
129
+ atomic_text(path, _bundle_runtime(inference_only=True, task='regression') + '\n# MORPHTREE_BUNDLE_END\n' + suffix)
130
+ return
131
+ from .persistence import species_payload
132
+ payload = {"format": "MorphTreeModel", "spec_version": SPEC_VERSION, "producer_version": VERSION,
133
+ "algorithm": species_payload(model.genome_, model.params_), "n_features": model.n_features_in_,
134
+ "feature_names": model.feature_names_in_, "booster": model.model_.to_dict(), "report": {}}
135
+ payload['algorithm']['metadata'] = {}
136
+ text = json.dumps(payload, sort_keys=True, allow_nan=False)
137
+ suffix = '''
138
+ _MODEL = MorphTreeClassifier._from_payload(json.loads(PAYLOAD_LITERAL))
139
+
140
+ def predict_proba(X):
141
+ return _MODEL.predict_proba(X)
142
+
143
+ def predict(X):
144
+ return _MODEL.predict(X)
145
+ '''.replace("PAYLOAD_LITERAL", repr(text))
146
+ atomic_text(path, _bundle_runtime(inference_only=True) + "\n# MORPHTREE_BUNDLE_END\n" + suffix)
147
+
148
+
149
+ def export_bundle(model, path):
150
+ """Publish a new directory atomically; never overwrite an existing bundle."""
151
+ import tempfile
152
+ import shutil
153
+ import os
154
+ from .persistence import write_json
155
+ destination = Path(path).resolve()
156
+ if destination.exists():
157
+ raise FileExistsError(destination)
158
+ from .temporary import directory as bounded_directory
159
+ temporary = Path(bounded_directory(prefix='.morphtree-', dir=destination.parent))
160
+ try:
161
+ model.save_model(temporary / 'model.mtree')
162
+ regression = getattr(model, 'task', None) == 'regression'
163
+ multiclass = getattr(model, 'task', None) == 'multiclass'
164
+ if not regression and not multiclass:
165
+ model.save_algorithm(temporary / 'species.morph')
166
+ model.save_report(temporary / 'report.json')
167
+ model.export_python(temporary / 'predictor.py')
168
+ write_json(temporary / 'schema.json', {'names': model.feature_names_in_, 'count': model.n_features_in_})
169
+ if multiclass:
170
+ write_json(temporary / 'classes.json', list(model.classes_))
171
+ atomic_text(temporary / 'README.md', '# MorphTree multiclass inference bundle\n\n'
172
+ 'Run trusted predictor.py with Python >=3.10; no packages required.\n'
173
+ 'Import predict or predict_proba. Probability columns follow classes_ and classes.json.\n'
174
+ 'Numeric rows use schema order; missing values are None or NaN.\n'
175
+ 'model.mtree is bounded declarative JSON. No rows or species recipe are bundled.\n'
176
+ 'Report metrics are selection diagnostics. Scripts are executable; integrity is not authenticity.\n')
177
+ elif regression:
178
+ atomic_text(temporary / 'README.md', '# MorphTree regression inference bundle\n\n'
179
+ 'Run trusted predictor.py with Python >=3.10. No packages required.\n'
180
+ 'Import predict; returns continuous values, not class probabilities.\n'
181
+ 'Numeric rows use schema order; missing values are None or NaN.\n'
182
+ 'Named dataframe columns are aligned; unexpected columns are rejected.\n'
183
+ 'model.mtree is bounded declarative JSON. No training rows or species recipe are bundled.\n'
184
+ 'report.json contains training/validation selection diagnostics before optional refit.\n'
185
+ 'Scripts are executable code; integrity is not authenticity.\n')
186
+ else:
187
+ atomic_text(temporary / 'README.md', '# MorphTree inference bundle\n\nRun trusted predictor.py with Python >=3.10. No packages required.\n'
188
+ 'Import predict or predict_proba; pass numeric rows in schema order. Missing: None or NaN.\n'
189
+ 'Named dataframe columns are aligned; unexpected columns are rejected.\n'
190
+ 'model.mtree is validated declarative JSON. species.morph is reusable structure, not weights.\n'
191
+ 'report.json metrics precede refit and are selection metrics. No training rows are bundled.\n'
192
+ 'Scripts are executable code; integrity is not authenticity.\n')
193
+ os.rename(temporary, destination)
194
+ finally:
195
+ if temporary.exists():
196
+ shutil.rmtree(temporary)
197
+
198
+
199
+ def report_markdown(report):
200
+ tuning = report.get("tuning", {})
201
+ lines = ["# Rapporto MorphTree", "", f"Versione: {report.get('version')}. Backend: {report.get('backend')}.", "",
202
+ f"Arresto: `{report.get('stop_reason')}`.", f"Fingerprint strutturale: `{report.get('final_fingerprint')}`.", "",
203
+ "## Metriche di selezione", "", "Le metriche seguenti precedono il refit. SEARCH e GUARD non sono test indipendenti.", "",
204
+ "| Fase | SEARCH logloss | GUARD logloss |", "|---|---:|---:|"]
205
+ for phase, prefix in (("Base", "base"), ("Struttura", "structure"), ("Finale numerico", "final")):
206
+ lines.append(f"| {phase} | {report.get(prefix + '_search_loss', 0):.8f} | {report.get(prefix + '_guard_loss', 0):.8f} |")
207
+ lines += ["", f"Guadagno strutturale SEARCH: {report.get('structural_gain', 0):.8f}.",
208
+ f"Guadagno numerico SEARCH: {report.get('tuning_gain', 0):.8f}.", "", "## Protocollo", "",
209
+ f"Plateau locale verificato: {report.get('plateau_reached')}. Ambito: {report.get('plateau_scope')}.",
210
+ f"Tuning eseguito: {tuning.get('attempted', False)}. Esito: {tuning.get('reason')}.", "",
211
+ "## Parametri finali", "", "```json", json.dumps(report.get("final_parameters", {}), indent=2), "```", "",
212
+ "## Mutazioni", "", "| Round | Gene | Stadio | Decisione |", "|---:|---|---|---|"]
213
+ for item in report.get("morph_history", []):
214
+ lines.append(f"| {item['round']} | {item['gene']} | {item['stage']} | {item['decision']} |")
215
+ sampling = report.get("structural_sampling")
216
+ if sampling:
217
+ lines += ["", "## Campionamento strutturale", "",
218
+ f"Frazione fissa: {sampling['row_fraction']}. Righe per albero: {sampling['sampled_rows']}/{sampling['train_rows']}.",
219
+ f"Politica di sampling operativa: {sampling['policy_active']}."]
220
+ excluded = report.get("coverage", {}).get("excluded_inactive", [])
221
+ if excluded:
222
+ lines.append(f"Mutazioni senza effetto escluse nell'ultimo vicinato: {len(excluded)}. Dettagli nel report JSON.")
223
+ lines += ["", "Il plateau riguarda soltanto il vicinato attivo ai parametri fissati. Non certifica un limite algoritmico globale.", ""]
224
+ return "\n".join(lines)
@@ -0,0 +1,32 @@
1
+ """Known2.0artifact compatibility,never fallback from a malformed new model.
2
+
3
+ Original2.0inference/serialization implementation and original input limits are
4
+ preserved for these historical artifacts. Newly trained scalable models retain
5
+ the current automatic batch/resource paths. No old weights/program are rewritten
6
+ or reinterpreted as a newly trained model.
7
+ """
8
+ PUBLIC_2_0_SCHEMA='product006-native-control-v1'
9
+
10
+
11
+ def restore_public_2_0(path,sha256,intent,*,workdir=None):
12
+ # Use the exact retained decoder,including variant/provenance/graph checks.
13
+ # A checksum of the already validated immutable snapshot prevents re-opening
14
+ # another file under a replaced path without detecting the changed bytes.
15
+ if intent=='classification':
16
+ from .estimators import MorphTreeClassifier
17
+ result=MorphTreeClassifier.load(path,sha256=sha256,workdir=workdir)
18
+ elif intent=='regression':
19
+ from .estimators import MorphTreeRegressor
20
+ result=MorphTreeRegressor.load(path,sha256=sha256,workdir=workdir)
21
+ else:
22
+ from .api import MorphTree
23
+ result=MorphTree.load(path,sha256=sha256,workdir=workdir)
24
+ # The legacy engine must retain its cache for its complete lifetime. Close
25
+ # it explicitly at the same owner-collection boundary instead of relying
26
+ # on TemporaryDirectory's warning-producing implicit cleanup. The finalizer
27
+ # captures only the cache,never the engine,so it cannot keep the owner alive.
28
+ import weakref
29
+ owner=getattr(result,'_engine',result)
30
+ workspace=getattr(owner,'_loaded_workspace',None)
31
+ if workspace is not None:weakref.finalize(owner,workspace.cleanup)
32
+ return result
morphtree/metrics.py ADDED
@@ -0,0 +1,156 @@
1
+ """Binary metrics with explicit shape and finiteness checks."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from numbers import Real
6
+ from .data import validate_y, validate_regression_y
7
+
8
+
9
+ def sigmoid(value):
10
+ if value >= 0:
11
+ return 1.0 / (1.0 + math.exp(-min(value, 745.0)))
12
+ z = math.exp(max(value, -745.0))
13
+ return z / (1.0 + z)
14
+
15
+
16
+ def checked_probabilities(y, p):
17
+ if hasattr(p, "ndim") and p.ndim != 1:
18
+ raise ValueError("Use one probability per sample, not a two-column matrix")
19
+ values = list(p)
20
+ labels = validate_y(y, len(values))
21
+ if any(not isinstance(v, Real) or not math.isfinite(v) or not 0 <= v <= 1 for v in values):
22
+ raise ValueError("Probabilities must be finite and in [0, 1]")
23
+ return labels, values
24
+
25
+
26
+ def point_losses(y, p):
27
+ labels, values = checked_probabilities(y, p)
28
+ return [-math.log(max(1e-15, min(1 - 1e-15, v if label else 1 - v))) for label, v in zip(labels, values)]
29
+
30
+
31
+ def logloss(y, p):
32
+ losses = point_losses(y, p)
33
+ return math.fsum(losses) / len(losses)
34
+
35
+
36
+ def accuracy(y, p):
37
+ labels, values = checked_probabilities(y, p)
38
+ return sum((v >= 0.5) == label for label, v in zip(labels, values)) / len(labels)
39
+
40
+
41
+ def binary_metrics(y, p):
42
+ labels, values = checked_probabilities(y, p)
43
+ counts = [labels.count(c) for c in (0, 1)]
44
+ recalls = [sum((v >= 0.5) == c and label == c for label, v in zip(labels, values)) / counts[c]
45
+ for c in (0, 1) if counts[c]]
46
+ auc = None
47
+ if all(counts):
48
+ order = sorted(zip(values, labels))
49
+ rank_sum, i = 0.0, 0
50
+ while i < len(order):
51
+ j = i + 1
52
+ while j < len(order) and order[j][0] == order[i][0]:
53
+ j += 1
54
+ rank_sum += (i + 1 + j) / 2 * sum(label for _, label in order[i:j])
55
+ i = j
56
+ auc = (rank_sum - counts[1] * (counts[1] + 1) / 2) / (counts[0] * counts[1])
57
+ return {"logloss": logloss(labels, values), "accuracy": accuracy(labels, values),
58
+ "balanced_accuracy": sum(recalls) / len(recalls), "roc_auc": auc,
59
+ "brier_score": math.fsum((v - label) ** 2 for label, v in zip(labels, values)) / len(labels)}
60
+
61
+
62
+ def paired_improvement(y, old_p, new_p, z=0.0):
63
+ old, new = point_losses(y, old_p), point_losses(y, new_p)
64
+ differences = [a - b for a, b in zip(old, new)]
65
+ mean = math.fsum(differences) / len(differences)
66
+ variance = math.fsum((d - mean) ** 2 for d in differences) / max(1, len(differences) - 1)
67
+ se = math.sqrt(variance / len(differences))
68
+ return mean, mean - z * se, se
69
+
70
+
71
+ def _regression_pairs(y, prediction):
72
+ if isinstance(prediction, (str, bytes)) or (hasattr(prediction, 'ndim') and prediction.ndim != 1):
73
+ raise ValueError('Use one continuous prediction per row')
74
+ try:
75
+ values = list(prediction)
76
+ except TypeError as exc:
77
+ raise ValueError('Predictions must be a one-dimensional sequence') from exc
78
+ labels = validate_regression_y(y, len(values))
79
+ normalized = []
80
+ for value in values:
81
+ if isinstance(value, bool) or not isinstance(value, Real):
82
+ raise ValueError('Predictions must be finite real numbers')
83
+ try:
84
+ number = float(value)
85
+ except (ValueError, OverflowError) as exc:
86
+ raise ValueError('Prediction outside floating-point range') from exc
87
+ if not math.isfinite(number):
88
+ raise ValueError('Predictions must be finite')
89
+ normalized.append(number)
90
+ residuals = [a - b for a, b in zip(labels, normalized)]
91
+ if any(not math.isfinite(v) for v in residuals):
92
+ raise ArithmeticError('Regression residual outside floating-point range')
93
+ return labels, residuals
94
+
95
+
96
+ def _root_mean_square(values):
97
+ scale = max(map(abs, values), default=0.0)
98
+ return scale * math.sqrt(math.fsum((v / scale) ** 2 for v in values) / len(values)) if scale else 0.0
99
+
100
+
101
+ def rmse(y, prediction):
102
+ """Scaled sum of squares avoids squaring very large/tiny residuals directly."""
103
+ return _root_mean_square(_regression_pairs(y, prediction)[1])
104
+
105
+
106
+ def mae(y, prediction):
107
+ residuals = _regression_pairs(y, prediction)[1]
108
+ return math.fsum(abs(v) / len(residuals) for v in residuals)
109
+
110
+
111
+ def r2_score(y, prediction):
112
+ labels, residuals = _regression_pairs(y, prediction)
113
+ error = _root_mean_square(residuals)
114
+ if all(v == labels[0] for v in labels):
115
+ return 1.0 if error == 0 else 0.0
116
+ mean = math.fsum(v / len(labels) for v in labels)
117
+ spread = _root_mean_square([v - mean for v in labels])
118
+ # Constant targets, including a single row: explicitly finite convention.
119
+ if spread == 0:
120
+ return 1.0 if error == 0 else 0.0
121
+ ratio = error / spread
122
+ result = 1 - ratio * ratio
123
+ if not math.isfinite(result):
124
+ raise ArithmeticError('R2 outside floating-point range')
125
+ return result
126
+
127
+
128
+ def regression_metrics(y, prediction):
129
+ return {'rmse': rmse(y, prediction), 'mae': mae(y, prediction), 'r2': r2_score(y, prediction)}
130
+ def softmax(scores):
131
+ """Stable categorical probabilities; callers supply finite nonempty logits."""
132
+ maximum = max(scores)
133
+ weights = [math.exp(float(value) - maximum) for value in scores]
134
+ total = math.fsum(weights)
135
+ return [value / total for value in weights]
136
+
137
+
138
+ def multiclass_metrics(y, probabilities):
139
+ """Encoded-label logloss, accuracy and macro F1 (absent classes contribute 0)."""
140
+ if not y or len(y) != len(probabilities) or not probabilities or len(probabilities[0]) < 2:
141
+ raise ValueError('Multiclass metrics require nonempty aligned labels and probabilities')
142
+ k = len(probabilities[0])
143
+ tp, actual, predicted, losses = [0]*k, [0]*k, [0]*k, []
144
+ for label, row in zip(y, probabilities):
145
+ if type(label) is not int or not 0 <= label < k or len(row) != k:
146
+ raise ValueError('Invalid multiclass label/probability dimensions')
147
+ if any(not math.isfinite(v) or not 0 <= v <= 1 for v in row) or abs(math.fsum(row) - 1) > 1e-10:
148
+ raise ValueError('Probabilities must be finite and sum to one')
149
+ winner = max(range(k), key=row.__getitem__)
150
+ actual[label] += 1
151
+ predicted[winner] += 1
152
+ tp[label] += winner == label
153
+ losses.append(-math.log(max(float(row[label]), 1e-15)))
154
+ return {'logloss': math.fsum(losses)/len(y), 'accuracy': sum(tp)/len(y),
155
+ 'macro_f1': math.fsum(2*tp[i]/(actual[i]+predicted[i]) if actual[i]+predicted[i] else 0. for i in range(k))/k}
156
+