margin 0.9.16__tar.gz → 0.9.17__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.
Potentially problematic release.
This version of margin might be problematic. Click here for more details.
- {margin-0.9.16/margin.egg-info → margin-0.9.17}/PKG-INFO +1 -1
- {margin-0.9.16 → margin-0.9.17}/margin/fingerprint.py +18 -4
- {margin-0.9.16 → margin-0.9.17}/margin/streaming.py +4 -1
- {margin-0.9.16 → margin-0.9.17/margin.egg-info}/PKG-INFO +1 -1
- {margin-0.9.16 → margin-0.9.17}/pyproject.toml +1 -1
- {margin-0.9.16 → margin-0.9.17}/tests/test_controller.py +72 -0
- {margin-0.9.16 → margin-0.9.17}/LICENSE +0 -0
- {margin-0.9.16 → margin-0.9.17}/NOTICE +0 -0
- {margin-0.9.16 → margin-0.9.17}/README.md +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/__init__.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/__main__.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/algebra.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/anomaly.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/bridge.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/calibrate.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/causal.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/composite.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/confidence.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/config.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/contract.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/controller.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/correlate.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/diff.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/drift.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/events.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/forecast.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/health.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/intent.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/ledger.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/loop.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/observation.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/persist.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/policy/__init__.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/policy/compose.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/policy/core.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/policy/temporal.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/policy/trace.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/policy/tuning.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/policy/validate.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/predicates.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/provenance.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/transitions.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/uncertain.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin/validity.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin.egg-info/SOURCES.txt +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin.egg-info/dependency_links.txt +0 -0
- {margin-0.9.16 → margin-0.9.17}/margin.egg-info/top_level.txt +0 -0
- {margin-0.9.16 → margin-0.9.17}/setup.cfg +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_algebra.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_anomaly.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_bridge.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_calibrate.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_causal.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_composite.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_confidence.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_config.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_contract.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_correlate.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_diff.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_drift.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_events.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_forecast.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_health.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_healthcare.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_intent.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_ledger.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_loop.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_neuro.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_observation.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_persist.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_policy.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_policy_compose.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_policy_temporal.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_policy_trace.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_policy_tuning.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_policy_validate.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_predicates.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_printer3d.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_provenance.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_reverse_bridge.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_ros2.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_staleness.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_streaming.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_transformer.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_transitions.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_uncertain.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_validity.py +0 -0
- {margin-0.9.16 → margin-0.9.17}/tests/test_windowing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: margin
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.17
|
|
4
4
|
Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
|
|
5
5
|
Author: Cope Labs LLC
|
|
6
6
|
License: MIT
|
|
@@ -87,11 +87,16 @@ class Fingerprint(dict):
|
|
|
87
87
|
``method="trimmed"`` — 10% trimmed mean
|
|
88
88
|
``method="mean"`` — plain mean (same as ``fp[component]["mean"]``)
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
When no raw values are stored, checks for pre-computed ``"median"`` in
|
|
91
|
+
the stats dict (present when fingerprint came from ``Monitor.fingerprint()``),
|
|
92
|
+
then falls back to ``"mean"``.
|
|
91
93
|
"""
|
|
92
94
|
vals = self._values.get(component)
|
|
93
95
|
if not vals:
|
|
94
|
-
|
|
96
|
+
stats = self.get(component, {})
|
|
97
|
+
if method == "median":
|
|
98
|
+
return stats.get("median", stats.get("mean", 0.0))
|
|
99
|
+
return stats.get("mean", 0.0)
|
|
95
100
|
if method == "median":
|
|
96
101
|
return _percentile(vals, 50)
|
|
97
102
|
if method == "trimmed":
|
|
@@ -103,11 +108,20 @@ class Fingerprint(dict):
|
|
|
103
108
|
"""
|
|
104
109
|
Return the p-th percentile (0–100) of observed values for ``component``.
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
When no raw values are stored, checks for pre-computed ``"q25"`` / ``"q75"`` /
|
|
112
|
+
``"median"`` in the stats dict (present when fingerprint came from
|
|
113
|
+
``Monitor.fingerprint()``), then falls back to ``"mean"``.
|
|
107
114
|
"""
|
|
108
115
|
vals = self._values.get(component)
|
|
109
116
|
if not vals:
|
|
110
|
-
|
|
117
|
+
stats = self.get(component, {})
|
|
118
|
+
if p == 25 and "q25" in stats:
|
|
119
|
+
return stats["q25"]
|
|
120
|
+
if p == 75 and "q75" in stats:
|
|
121
|
+
return stats["q75"]
|
|
122
|
+
if p == 50 and "median" in stats:
|
|
123
|
+
return stats["median"]
|
|
124
|
+
return stats.get("mean", 0.0)
|
|
111
125
|
return _percentile(vals, p)
|
|
112
126
|
|
|
113
127
|
def sigma(self, component: str, value: float) -> float:
|
|
@@ -64,7 +64,7 @@ from .confidence import Confidence
|
|
|
64
64
|
from .health import Health, Thresholds, classify
|
|
65
65
|
from .observation import Observation, Expression
|
|
66
66
|
from .provenance import ProvenanceGraph
|
|
67
|
-
from .fingerprint import Fingerprint
|
|
67
|
+
from .fingerprint import Fingerprint, _percentile as _fp_percentile
|
|
68
68
|
from .drift import (
|
|
69
69
|
DriftState, DriftDirection, DriftClassification,
|
|
70
70
|
classify_drift,
|
|
@@ -606,6 +606,9 @@ class Monitor:
|
|
|
606
606
|
"std": std,
|
|
607
607
|
"n": n,
|
|
608
608
|
"trend": dc.state.value if dc is not None else "UNKNOWN",
|
|
609
|
+
"median": _fp_percentile(values, 50),
|
|
610
|
+
"q25": _fp_percentile(values, 25),
|
|
611
|
+
"q75": _fp_percentile(values, 75),
|
|
609
612
|
}
|
|
610
613
|
raw_values[name] = values
|
|
611
614
|
return Fingerprint(stats=stats, values=raw_values)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: margin
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.17
|
|
4
4
|
Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
|
|
5
5
|
Author: Cope Labs LLC
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "margin"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.17"
|
|
8
8
|
description = "Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -561,6 +561,78 @@ class TestFingerprintSigma:
|
|
|
561
561
|
assert fp.sigma("missing", 0.7) == pytest.approx(0.7)
|
|
562
562
|
|
|
563
563
|
|
|
564
|
+
class TestFingerprintPrecomputedPercentiles:
|
|
565
|
+
"""Fingerprints from Monitor.fingerprint() now include median/q25/q75.
|
|
566
|
+
percentile() and robust_target() must use them when no raw values stored."""
|
|
567
|
+
|
|
568
|
+
def test_monitor_fingerprint_includes_median(self):
|
|
569
|
+
monitor = _make_monitor(20)
|
|
570
|
+
fp = monitor.fingerprint()
|
|
571
|
+
assert "median" in fp["alpha"]
|
|
572
|
+
assert "q25" in fp["alpha"]
|
|
573
|
+
assert "q75" in fp["alpha"]
|
|
574
|
+
|
|
575
|
+
def test_robust_sigma_live_after_serialization(self):
|
|
576
|
+
# Simulate the Build #29 scenario:
|
|
577
|
+
# Session 1 fingerprint saved as JSON, loaded in Session 2 — no raw values.
|
|
578
|
+
monitor = _make_monitor(30)
|
|
579
|
+
fp_live = monitor.fingerprint()
|
|
580
|
+
# Round-trip through JSON (loses raw values, keeps precomputed percentiles)
|
|
581
|
+
import json
|
|
582
|
+
fp_reloaded = Fingerprint.from_dict(json.loads(json.dumps(fp_live)))
|
|
583
|
+
# robust_sigma should differ from sigma (IQR != 0, median != mean generally)
|
|
584
|
+
# At minimum, it must not crash and must not return 0.0 for a nonzero input
|
|
585
|
+
result = fp_reloaded.robust_sigma("alpha", 0.55)
|
|
586
|
+
assert isinstance(result, float)
|
|
587
|
+
|
|
588
|
+
def test_robust_sigma_uses_precomputed_iqr(self):
|
|
589
|
+
# Construct fingerprint with known median/q25/q75
|
|
590
|
+
fp = Fingerprint(stats={"rr": {
|
|
591
|
+
"mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
|
|
592
|
+
"median": 0.050, "q25": 0.020, "q75": 0.120,
|
|
593
|
+
}})
|
|
594
|
+
# IQR = 0.120 - 0.020 = 0.100
|
|
595
|
+
# robust_sigma(0.14) = (0.14 - 0.050) / 0.100 = 0.9
|
|
596
|
+
assert fp.robust_sigma("rr", 0.14) == pytest.approx(0.9)
|
|
597
|
+
|
|
598
|
+
def test_robust_sigma_differs_from_mean_based(self):
|
|
599
|
+
# When median != mean (skewed distribution), the two should differ
|
|
600
|
+
fp = Fingerprint(stats={"rr": {
|
|
601
|
+
"mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
|
|
602
|
+
"median": 0.050, "q25": 0.020, "q75": 0.120,
|
|
603
|
+
}})
|
|
604
|
+
assert fp.robust_sigma("rr", 0.14) != pytest.approx(fp.sigma("rr", 0.14))
|
|
605
|
+
|
|
606
|
+
def test_regression_build29_scenario(self):
|
|
607
|
+
# Exact values from the user's Session 1 meta.json
|
|
608
|
+
fp = Fingerprint(stats={"recovery_ratio": {
|
|
609
|
+
"mean": 0.070, "std": 0.498, "n": 98, "trend": "DRIFTING",
|
|
610
|
+
"median": 0.050, "q25": 0.020, "q75": 0.120,
|
|
611
|
+
}})
|
|
612
|
+
r = fp.robust_sigma("recovery_ratio", 0.14)
|
|
613
|
+
# Previously returned 1.0 (mean-based fallback); now uses IQR
|
|
614
|
+
assert r == pytest.approx(0.9)
|
|
615
|
+
mean_based = fp.sigma("recovery_ratio", 0.14)
|
|
616
|
+
assert r != pytest.approx(mean_based)
|
|
617
|
+
|
|
618
|
+
def test_percentile_uses_precomputed_q25(self):
|
|
619
|
+
fp = Fingerprint(stats={"rr": {
|
|
620
|
+
"mean": 1.0, "std": 0.1, "n": 10, "trend": "STABLE",
|
|
621
|
+
"median": 1.0, "q25": 0.8, "q75": 1.2,
|
|
622
|
+
}})
|
|
623
|
+
assert fp.percentile("rr", 25) == pytest.approx(0.8)
|
|
624
|
+
assert fp.percentile("rr", 75) == pytest.approx(1.2)
|
|
625
|
+
assert fp.percentile("rr", 50) == pytest.approx(1.0)
|
|
626
|
+
|
|
627
|
+
def test_percentile_fallback_to_mean_for_other_p(self):
|
|
628
|
+
# Only 25/50/75 are pre-computed; other p falls back to mean
|
|
629
|
+
fp = Fingerprint(stats={"rr": {
|
|
630
|
+
"mean": 1.0, "std": 0.1, "n": 10, "trend": "STABLE",
|
|
631
|
+
"median": 1.0, "q25": 0.8, "q75": 1.2,
|
|
632
|
+
}})
|
|
633
|
+
assert fp.percentile("rr", 10) == pytest.approx(1.0) # falls back to mean
|
|
634
|
+
|
|
635
|
+
|
|
564
636
|
class TestFingerprintRobustSigma:
|
|
565
637
|
def test_median_centered(self):
|
|
566
638
|
# Uniform values: median=3.0, IQR=2.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|