phasmanet 0.1.0__py3-none-any.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.
phasmanet/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """PhasmaNet: a staged spectral token-refinement architecture for tabular data.
2
+
3
+ Quick start::
4
+
5
+ from phasmanet import PhasmaNetRegressor
6
+ reg = PhasmaNetRegressor(random_state=0).fit(X_train, y_train)
7
+ y_pred = reg.predict(X_test)
8
+ """
9
+ from ._version import __version__
10
+ from .calibration import MonotoneCalibrator
11
+ from .estimators import PhasmaNetClassifier, PhasmaNetRegressor
12
+ from .model import PhasmaNet, PhiRefractor, PhiStage, convert_legacy_state_dict
13
+
14
+ __all__ = [
15
+ "__version__",
16
+ "PhasmaNet",
17
+ "PhiStage",
18
+ "PhiRefractor",
19
+ "PhasmaNetRegressor",
20
+ "PhasmaNetClassifier",
21
+ "MonotoneCalibrator",
22
+ "convert_legacy_state_dict",
23
+ ]
phasmanet/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,96 @@
1
+ """Rank-preserving monotonic (PCHIP) recalibration of point predictions.
2
+
3
+ Knots are the medians of (prediction, target) in quantile bins of the predictions,
4
+ made non-decreasing and extended to the observed range, then joined by a monotone
5
+ piecewise cubic Hermite interpolant. The calibrator is fitted on a validation split and
6
+ applied unchanged to new predictions.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Dict, Optional
11
+
12
+ import numpy as np
13
+
14
+ __all__ = ["MonotoneCalibrator"]
15
+
16
+
17
+ class MonotoneCalibrator:
18
+ """Smooth monotonic calibrator mapping raw predictions to calibrated predictions."""
19
+
20
+ def __init__(self, xk: Optional[np.ndarray] = None, yk: Optional[np.ndarray] = None):
21
+ self.xk = None if xk is None else np.asarray(xk, dtype=np.float64)
22
+ self.yk = None if yk is None else np.asarray(yk, dtype=np.float64)
23
+ self._pchip = None
24
+ if self.xk is not None:
25
+ self._build()
26
+
27
+ @property
28
+ def is_identity(self) -> bool:
29
+ return self.xk is None
30
+
31
+ def _build(self) -> None:
32
+ from scipy.interpolate import PchipInterpolator
33
+
34
+ if self.xk.size < 3 or self.yk.size != self.xk.size:
35
+ raise ValueError("need at least 3 knots with matching xk and yk")
36
+ if np.any(np.diff(self.xk) <= 0):
37
+ raise ValueError("xk must be strictly increasing")
38
+ self._pchip = PchipInterpolator(self.xk, self.yk, extrapolate=True)
39
+
40
+ def fit(self, pred, true, bins: int = 60, min_bin: int = 20) -> "MonotoneCalibrator":
41
+ """Fit knots from validation predictions ``pred`` and targets ``true``.
42
+
43
+ If there are too few distinct bins the calibrator stays the identity map.
44
+ """
45
+ pred = np.asarray(pred, dtype=np.float64).ravel()
46
+ true = np.asarray(true, dtype=np.float64).ravel()
47
+ if pred.shape != true.shape:
48
+ raise ValueError("pred and true must have the same length")
49
+ self.xk = self.yk = self._pchip = None
50
+
51
+ edges = np.unique(np.quantile(pred, np.linspace(0.0, 1.0, bins + 1)))
52
+ if len(edges) < 4:
53
+ return self
54
+
55
+ xk, yk = [], []
56
+ for lo, hi in zip(edges[:-1], edges[1:]):
57
+ m = (pred >= lo) & (pred < hi)
58
+ if m.sum() >= min_bin:
59
+ xk.append(np.median(pred[m]))
60
+ yk.append(np.median(true[m]))
61
+ xk = np.asarray(xk, dtype=np.float64)
62
+ yk = np.asarray(yk, dtype=np.float64)
63
+ if xk.size < 3:
64
+ return self
65
+
66
+ yk = np.maximum.accumulate(yk)
67
+ xk, keep = np.unique(xk, return_index=True)
68
+ yk = yk[keep]
69
+
70
+ x_min, x_max = pred.min(), pred.max()
71
+ y_min, y_max = true.min(), true.max()
72
+ if x_min < xk[0]:
73
+ xk = np.r_[x_min, xk]
74
+ yk = np.r_[min(y_min, yk[0]), yk]
75
+ if x_max > xk[-1]:
76
+ xk = np.r_[xk, x_max]
77
+ yk = np.r_[yk, max(y_max, yk[-1])]
78
+ self.xk, self.yk = xk, np.maximum.accumulate(yk)
79
+ self._build()
80
+ return self
81
+
82
+ def __call__(self, x) -> np.ndarray:
83
+ x = np.asarray(x, dtype=np.float64)
84
+ return x.copy() if self._pchip is None else self._pchip(x)
85
+
86
+ transform = __call__
87
+
88
+ def to_dict(self) -> Dict[str, list]:
89
+ if self.is_identity:
90
+ return {"xk": [], "yk": []}
91
+ return {"xk": self.xk.tolist(), "yk": self.yk.tolist()}
92
+
93
+ @classmethod
94
+ def from_dict(cls, d: Dict[str, list]) -> "MonotoneCalibrator":
95
+ xk, yk = d.get("xk") or [], d.get("yk") or []
96
+ return cls() if len(xk) == 0 else cls(xk, yk)