views-baseline 1.0.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.
- views_baseline/__init__.py +0 -0
- views_baseline/evaluation/__init__.py +0 -0
- views_baseline/evaluation/closeness.py +152 -0
- views_baseline/infrastructure/__init__.py +0 -0
- views_baseline/infrastructure/exceptions.py +10 -0
- views_baseline/infrastructure/reproducibility_gate.py +112 -0
- views_baseline/manager/__init__.py +0 -0
- views_baseline/manager/baseline_manager.py +152 -0
- views_baseline/model/__init__.py +1 -0
- views_baseline/model/catalog.py +120 -0
- views_baseline/model/defaults.py +9 -0
- views_baseline/model/distributions/__init__.py +64 -0
- views_baseline/model/distributions/families.py +182 -0
- views_baseline/model/distributions/registry.py +39 -0
- views_baseline/model/distributions/transforms.py +61 -0
- views_baseline/model/frames/__init__.py +0 -0
- views_baseline/model/frames/input.py +246 -0
- views_baseline/model/frames/output.py +144 -0
- views_baseline/model/frames/pooling.py +148 -0
- views_baseline/model/grid.py +90 -0
- views_baseline/model/models/__init__.py +0 -0
- views_baseline/model/models/distributional/__init__.py +13 -0
- views_baseline/model/models/distributional/conflictology.py +74 -0
- views_baseline/model/models/distributional/mixture.py +104 -0
- views_baseline/model/models/distributional/parametric.py +95 -0
- views_baseline/model/models/distributional/parametric_hurdle.py +115 -0
- views_baseline/model/models/point/__init__.py +5 -0
- views_baseline/model/models/point/average.py +78 -0
- views_baseline/model/models/point/locf.py +75 -0
- views_baseline/model/models/point/zero.py +63 -0
- views_baseline/model/protocol.py +44 -0
- views_baseline/model/spatial.py +72 -0
- views_baseline-1.0.0.dist-info/METADATA +207 -0
- views_baseline-1.0.0.dist-info/RECORD +36 -0
- views_baseline-1.0.0.dist-info/WHEEL +4 -0
- views_baseline-1.0.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Closeness-to-conflictology measurement harness (ADR-022 / epic #33 S3).
|
|
2
|
+
|
|
3
|
+
Operationalises "I couldn't tell which model the 1000 draws came from" as a
|
|
4
|
+
distributional two-sample comparison between two ``dict[str, PredictionFrame]``:
|
|
5
|
+
|
|
6
|
+
- per (cell, target): 1-Wasserstein, energy distance, and a 1-NN **C2ST** accuracy
|
|
7
|
+
(0.5 = indistinguishable);
|
|
8
|
+
- a **same-model null** (conflictology two seeds) as the finite-N noise floor;
|
|
9
|
+
- **stratification** by activity regime (zero / low / high), target, and level;
|
|
10
|
+
- an **equivalence** verdict against a pre-registered margin ``delta`` (not a p-value).
|
|
11
|
+
|
|
12
|
+
This module is *not* in the model path — it only consumes PredictionFrames.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from scipy.stats import energy_distance, wasserstein_distance
|
|
19
|
+
|
|
20
|
+
# Activity strata by the reference (conflictology) per-cell mean, in raw counts.
|
|
21
|
+
_LOW_HIGH_CUT = 1.0 # mean < this (and not all-zero) = "low"; >= this = "high"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def c2st_1nn(a: np.ndarray, b: np.ndarray, rng: np.random.Generator | None = None) -> float:
|
|
25
|
+
"""Leave-one-out 1-NN classifier two-sample accuracy in 1-D.
|
|
26
|
+
|
|
27
|
+
Pools the two balanced samples, labels them, and for each point asks whether its
|
|
28
|
+
nearest neighbour shares its label. ``0.5`` ⇒ indistinguishable, ``1.0`` ⇒
|
|
29
|
+
perfectly separable. Pure numpy, O(n log n). Ties (e.g. constant/degenerate
|
|
30
|
+
cells) are broken by a random permutation before the stable sort, so identical
|
|
31
|
+
samples score ~0.5 rather than spuriously ~1.0.
|
|
32
|
+
"""
|
|
33
|
+
rng = np.random.default_rng(0) if rng is None else rng
|
|
34
|
+
a = np.asarray(a, dtype=np.float64).ravel()
|
|
35
|
+
b = np.asarray(b, dtype=np.float64).ravel()
|
|
36
|
+
x = np.concatenate([a, b])
|
|
37
|
+
y = np.concatenate([np.zeros(len(a)), np.ones(len(b))])
|
|
38
|
+
n = len(x)
|
|
39
|
+
if n < 2:
|
|
40
|
+
return 0.5
|
|
41
|
+
perm = rng.permutation(n) # randomise tie order
|
|
42
|
+
x, y = x[perm], y[perm]
|
|
43
|
+
order = np.argsort(x, kind="mergesort") # stable → ties keep the random order
|
|
44
|
+
xs, ys = x[order], y[order]
|
|
45
|
+
dl = np.empty(n)
|
|
46
|
+
dl[0] = np.inf
|
|
47
|
+
dl[1:] = xs[1:] - xs[:-1]
|
|
48
|
+
dr = np.empty(n)
|
|
49
|
+
dr[-1] = np.inf
|
|
50
|
+
dr[:-1] = xs[1:] - xs[:-1]
|
|
51
|
+
idx = np.arange(n)
|
|
52
|
+
nn = np.where(dl <= dr, idx - 1, idx + 1)
|
|
53
|
+
return float((ys[nn] == ys).mean())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cell_map(pf) -> dict:
|
|
57
|
+
"""Map (unit, time) -> row index for a PredictionFrame."""
|
|
58
|
+
time = np.asarray(pf.index.time)
|
|
59
|
+
unit = np.asarray(pf.index.unit)
|
|
60
|
+
return {(int(u), int(t)): i for i, (u, t) in enumerate(zip(unit, time))}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _stratum(ref_samples: np.ndarray) -> str:
|
|
64
|
+
m = float(np.mean(ref_samples))
|
|
65
|
+
if m == 0.0:
|
|
66
|
+
return "zero"
|
|
67
|
+
return "low" if m < _LOW_HIGH_CUT else "high"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def compare_models(model: dict, reference: dict, seed: int = 0) -> dict:
|
|
71
|
+
"""Per-cell distances between ``model`` and ``reference`` (e.g. conflictology).
|
|
72
|
+
|
|
73
|
+
Both are ``dict[str, PredictionFrame]`` with ``.values`` shape ``(N, S)``. Cells
|
|
74
|
+
are aligned by ``(unit, time)``; the stratum is assigned from the *reference*
|
|
75
|
+
per-cell mean. ``seed`` makes the C2ST tie-breaking reproducible. Returns
|
|
76
|
+
``{target: {"wasserstein","energy","c2st","stratum","level"}}`` with numpy arrays
|
|
77
|
+
over the common cells.
|
|
78
|
+
"""
|
|
79
|
+
rng = np.random.default_rng(seed)
|
|
80
|
+
out = {}
|
|
81
|
+
for target, ref_pf in reference.items():
|
|
82
|
+
mod_pf = model[target]
|
|
83
|
+
ref_map, mod_map = _cell_map(ref_pf), _cell_map(mod_pf)
|
|
84
|
+
common = [k for k in ref_map if k in mod_map]
|
|
85
|
+
w = np.empty(len(common))
|
|
86
|
+
e = np.empty(len(common))
|
|
87
|
+
c = np.empty(len(common))
|
|
88
|
+
strat = np.empty(len(common), dtype=object)
|
|
89
|
+
for j, key in enumerate(common):
|
|
90
|
+
ra = np.asarray(ref_pf.values[ref_map[key]], dtype=np.float64).ravel()
|
|
91
|
+
ma = np.asarray(mod_pf.values[mod_map[key]], dtype=np.float64).ravel()
|
|
92
|
+
w[j] = wasserstein_distance(ma, ra)
|
|
93
|
+
e[j] = energy_distance(ma, ra)
|
|
94
|
+
c[j] = c2st_1nn(ma, ra, rng)
|
|
95
|
+
strat[j] = _stratum(ra)
|
|
96
|
+
out[target] = {
|
|
97
|
+
"wasserstein": w,
|
|
98
|
+
"energy": e,
|
|
99
|
+
"c2st": c,
|
|
100
|
+
"stratum": strat,
|
|
101
|
+
"level": ref_pf.index.level.name,
|
|
102
|
+
}
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def summarize(metrics: dict) -> dict:
|
|
107
|
+
"""Stratified summary: per (target, stratum) → median / p90 / p99 of each metric."""
|
|
108
|
+
rows = {}
|
|
109
|
+
for target, m in metrics.items():
|
|
110
|
+
for stratum in ("zero", "low", "high", "all"):
|
|
111
|
+
mask = np.ones(len(m["c2st"]), bool) if stratum == "all" else (m["stratum"] == stratum)
|
|
112
|
+
if not mask.any():
|
|
113
|
+
continue
|
|
114
|
+
rows[(target, stratum)] = {
|
|
115
|
+
"n": int(mask.sum()),
|
|
116
|
+
"wasserstein": _pcts(m["wasserstein"][mask]),
|
|
117
|
+
"energy": _pcts(m["energy"][mask]),
|
|
118
|
+
"c2st": _pcts(m["c2st"][mask]),
|
|
119
|
+
}
|
|
120
|
+
return rows
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _pcts(a: np.ndarray) -> dict:
|
|
124
|
+
return {
|
|
125
|
+
"median": float(np.median(a)),
|
|
126
|
+
"p90": float(np.percentile(a, 90)),
|
|
127
|
+
"p99": float(np.percentile(a, 99)),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def equivalence(candidate: dict, null: dict, delta: dict) -> dict:
|
|
132
|
+
"""Per (target, stratum) equivalence verdict.
|
|
133
|
+
|
|
134
|
+
A candidate is *equivalent to conflictology* in a stratum when its median C2ST is
|
|
135
|
+
within ``0.5 + delta["c2st"]`` AND its median Wasserstein is within the same-model
|
|
136
|
+
null median plus ``delta["wasserstein"]``.
|
|
137
|
+
"""
|
|
138
|
+
verdict = {}
|
|
139
|
+
for key, cand in candidate.items():
|
|
140
|
+
null_row = null.get(key)
|
|
141
|
+
c2st_ok = cand["c2st"]["median"] <= 0.5 + delta["c2st"]
|
|
142
|
+
w_ref = null_row["wasserstein"]["median"] if null_row else 0.0
|
|
143
|
+
w_ok = cand["wasserstein"]["median"] <= w_ref + delta["wasserstein"]
|
|
144
|
+
verdict[key] = {
|
|
145
|
+
"equivalent": bool(c2st_ok and w_ok),
|
|
146
|
+
"c2st_median": cand["c2st"]["median"],
|
|
147
|
+
"c2st_ok": bool(c2st_ok),
|
|
148
|
+
"wasserstein_median": cand["wasserstein"]["median"],
|
|
149
|
+
"wasserstein_null_median": w_ref,
|
|
150
|
+
"wasserstein_ok": bool(w_ok),
|
|
151
|
+
}
|
|
152
|
+
return verdict
|
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .exceptions import MissingHyperparameterError
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReproducibilityGate:
|
|
9
|
+
"""
|
|
10
|
+
Canonical hyperparameter contract for views-baseline models.
|
|
11
|
+
|
|
12
|
+
Defines which configuration keys are required by all baseline models
|
|
13
|
+
(CORE_GENOME) and which are required per algorithm (ALGORITHM_GENOMES).
|
|
14
|
+
The audit_manifest() method enforces these contracts at runtime.
|
|
15
|
+
|
|
16
|
+
This class is importable by downstream packages (e.g. views-models)
|
|
17
|
+
so they can validate their config_hyperparameters.py files statically.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
class Config:
|
|
21
|
+
"""Gates related to configuration and hyperparameter integrity."""
|
|
22
|
+
|
|
23
|
+
# Core keys required by ALL baseline models regardless of algorithm.
|
|
24
|
+
CORE_GENOME = ["steps", "time_steps", "prediction_format"]
|
|
25
|
+
|
|
26
|
+
# Algorithm-specific keys (audited only when the algorithm matches).
|
|
27
|
+
ALGORITHM_GENOMES = {
|
|
28
|
+
"ZeroModel": [],
|
|
29
|
+
"LocfModel": [],
|
|
30
|
+
"AverageModel": ["window_months"],
|
|
31
|
+
"ConflictologyModel": ["window_months", "n_samples", "seed"],
|
|
32
|
+
"MixtureBaseline": ["window_months", "lambda_mix", "n_samples", "seed"],
|
|
33
|
+
# Parametric climatology (ADR-022): `family` and `transform` join `seed` as
|
|
34
|
+
# required, audited reproducibility keys — no magic defaults (ADR-021).
|
|
35
|
+
"ParametricConflictology": [
|
|
36
|
+
"window_months", "n_samples", "seed", "family", "transform",
|
|
37
|
+
],
|
|
38
|
+
"ParametricHurdleConflictology": [
|
|
39
|
+
"window_months", "n_samples", "seed", "family", "transform",
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def audit_manifest(config: dict) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Verify that all mandatory hyperparameters are present and non-None.
|
|
47
|
+
|
|
48
|
+
Checks in order:
|
|
49
|
+
1. All CORE_GENOME keys are present.
|
|
50
|
+
2. The algorithm is registered in ALGORITHM_GENOMES.
|
|
51
|
+
3. All algorithm-specific keys are present.
|
|
52
|
+
4. No required key has a value of None.
|
|
53
|
+
|
|
54
|
+
Raises MissingHyperparameterError on any violation.
|
|
55
|
+
"""
|
|
56
|
+
# 1. Audit Core Genome
|
|
57
|
+
missing_core = [
|
|
58
|
+
k
|
|
59
|
+
for k in ReproducibilityGate.Config.CORE_GENOME
|
|
60
|
+
if k not in config
|
|
61
|
+
]
|
|
62
|
+
if missing_core:
|
|
63
|
+
msg = (
|
|
64
|
+
"REPRODUCIBILITY CONTRACT VIOLATED: "
|
|
65
|
+
f"Missing core parameters: {missing_core}"
|
|
66
|
+
)
|
|
67
|
+
logger.error(msg)
|
|
68
|
+
raise MissingHyperparameterError(msg)
|
|
69
|
+
|
|
70
|
+
# 2. Identify algorithm and check it is registered
|
|
71
|
+
if "algorithm" not in config:
|
|
72
|
+
msg = (
|
|
73
|
+
"REPRODUCIBILITY CONTRACT VIOLATED: "
|
|
74
|
+
"Missing required key: 'algorithm'"
|
|
75
|
+
)
|
|
76
|
+
logger.error(msg)
|
|
77
|
+
raise MissingHyperparameterError(msg)
|
|
78
|
+
|
|
79
|
+
algo = config["algorithm"]
|
|
80
|
+
algo_genomes = ReproducibilityGate.Config.ALGORITHM_GENOMES
|
|
81
|
+
if algo not in algo_genomes:
|
|
82
|
+
available = list(algo_genomes.keys())
|
|
83
|
+
msg = (
|
|
84
|
+
"REPRODUCIBILITY CONTRACT VIOLATED: "
|
|
85
|
+
f"Unknown algorithm '{algo}'. "
|
|
86
|
+
f"Available: {available}"
|
|
87
|
+
)
|
|
88
|
+
logger.error(msg)
|
|
89
|
+
raise MissingHyperparameterError(msg)
|
|
90
|
+
|
|
91
|
+
# 3. Audit algorithm-specific keys
|
|
92
|
+
algo_keys = algo_genomes[algo]
|
|
93
|
+
missing_algo = [k for k in algo_keys if k not in config]
|
|
94
|
+
if missing_algo:
|
|
95
|
+
msg = (
|
|
96
|
+
"REPRODUCIBILITY CONTRACT VIOLATED: "
|
|
97
|
+
f"Algorithm '{algo}' requires missing parameters: {missing_algo}"
|
|
98
|
+
)
|
|
99
|
+
logger.error(msg)
|
|
100
|
+
raise MissingHyperparameterError(msg)
|
|
101
|
+
|
|
102
|
+
# 4. Reject None values for all required keys
|
|
103
|
+
all_required = list(ReproducibilityGate.Config.CORE_GENOME) + algo_keys
|
|
104
|
+
explicit_nones = [k for k in all_required if config.get(k) is None]
|
|
105
|
+
if explicit_nones:
|
|
106
|
+
msg = (
|
|
107
|
+
"REPRODUCIBILITY CONTRACT VIOLATED: "
|
|
108
|
+
f"Mandatory parameters set to None: {explicit_nones}. "
|
|
109
|
+
"Implicit defaults are forbidden."
|
|
110
|
+
)
|
|
111
|
+
logger.error(msg)
|
|
112
|
+
raise MissingHyperparameterError(msg)
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pickle
|
|
3
|
+
|
|
4
|
+
from views_pipeline_core.files.utils import generate_model_file_name, read_dataframe
|
|
5
|
+
from views_pipeline_core.managers.model import ForecastingModelManager, ModelPathManager
|
|
6
|
+
from views_pipeline_core.modules.dataloaders.datafactory_contract import (
|
|
7
|
+
DATA_FORMAT_DATAFRAME,
|
|
8
|
+
DATA_FORMAT_FEATURE_FRAME,
|
|
9
|
+
)
|
|
10
|
+
from views_pipeline_core.modules.dataloaders.frame_cache import load_frame_cache
|
|
11
|
+
|
|
12
|
+
from views_baseline.infrastructure.reproducibility_gate import ReproducibilityGate
|
|
13
|
+
from views_baseline.model.catalog import BaselineModelCatalog
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaselineForecastingModelManager(ForecastingModelManager):
|
|
19
|
+
"""
|
|
20
|
+
Baseline Forecasting Model Manager
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
model_path: ModelPathManager,
|
|
26
|
+
wandb_notifications: bool = False,
|
|
27
|
+
use_prediction_store: bool = False,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initialize Baseline Forecasting Model Manager.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(model_path, wandb_notifications, use_prediction_store)
|
|
33
|
+
|
|
34
|
+
logger.info("Initializing BaselineModelManager")
|
|
35
|
+
|
|
36
|
+
def _train_model_artifact(self):
|
|
37
|
+
"""
|
|
38
|
+
Fit the baseline model and save it as a pickle artifact.
|
|
39
|
+
|
|
40
|
+
Although baselines are stateless and deterministic, the downstream
|
|
41
|
+
ensemble manager requires an artifact file to exist so it can
|
|
42
|
+
resolve timestamps via get_latest_model_artifact_path().
|
|
43
|
+
"""
|
|
44
|
+
self.model, _ = self._setup_model_and_data()
|
|
45
|
+
path_artifacts = self._model_path.artifacts
|
|
46
|
+
run_type = self.config["run_type"]
|
|
47
|
+
model_filename = generate_model_file_name(run_type, file_extension=".pkl")
|
|
48
|
+
with open(path_artifacts / model_filename, "wb") as f:
|
|
49
|
+
pickle.dump(self.model, f)
|
|
50
|
+
logger.info(f"Saved baseline artifact: {model_filename}")
|
|
51
|
+
return self.model
|
|
52
|
+
|
|
53
|
+
def _load_source(self):
|
|
54
|
+
"""Load the fit/predict input for the current partition, dispatching on declared format.
|
|
55
|
+
|
|
56
|
+
Frame-native seam (issue #64). A model that declares ``data_format: feature_frame``
|
|
57
|
+
is served the pipeline-core FeatureFrame directory cache directly, so the
|
|
58
|
+
``FeatureFrame`` flows straight into ``fit``/``predict`` with no pandas in the
|
|
59
|
+
manager — the models have consumed either shape since ADR-019/epic #47. Every other
|
|
60
|
+
model keeps the byte-identical pandas-parquet path (``read_dataframe``).
|
|
61
|
+
|
|
62
|
+
Dispatch is on the declared ``_data_format`` (set by the base manager during data
|
|
63
|
+
fetching; absent → ``dataframe``, i.e. legacy behavior). The frame branch resolves
|
|
64
|
+
the cache through the LOUD ``_get_cached_frame_path()`` getter, which raises if the
|
|
65
|
+
frame cache was never populated. There is deliberately NO silent
|
|
66
|
+
``getattr(..., None)`` fallback from the frame path back to pandas: a frames-declared
|
|
67
|
+
config that has lost its cache must fail loud, never silently degrade to a pandas run
|
|
68
|
+
(pipeline-core register C-214).
|
|
69
|
+
"""
|
|
70
|
+
if getattr(self, "_data_format", DATA_FORMAT_DATAFRAME) == DATA_FORMAT_FEATURE_FRAME:
|
|
71
|
+
return load_frame_cache(self._get_cached_frame_path())
|
|
72
|
+
return read_dataframe(self._get_cached_data_path())
|
|
73
|
+
|
|
74
|
+
def _setup_model_and_data(self):
|
|
75
|
+
"""
|
|
76
|
+
Instantiate the baseline model via the catalog, load data, fit, and return both.
|
|
77
|
+
"""
|
|
78
|
+
ReproducibilityGate.Config.audit_manifest(self.config)
|
|
79
|
+
loa = self.config["level"]
|
|
80
|
+
partition_dict = self._data_loader.partition_dict
|
|
81
|
+
catalog = BaselineModelCatalog(
|
|
82
|
+
config=self.config, partition_dict=partition_dict, loa=loa
|
|
83
|
+
)
|
|
84
|
+
model = catalog.get_model(self.config["algorithm"])
|
|
85
|
+
logger.info(f"Model type is {self.config['algorithm']}")
|
|
86
|
+
df_source = self._load_source()
|
|
87
|
+
model.fit(df_source)
|
|
88
|
+
return model, df_source
|
|
89
|
+
|
|
90
|
+
def _generate_predictions(self, model, df, eval_type):
|
|
91
|
+
"""
|
|
92
|
+
Generate predictions for all sequence numbers in an evaluation.
|
|
93
|
+
|
|
94
|
+
All models return Dict[str, PredictionFrame], so this accumulates
|
|
95
|
+
into Dict[str, list[PredictionFrame]] across sequence numbers.
|
|
96
|
+
"""
|
|
97
|
+
sequence_numbers = self._resolve_evaluation_sequence_number(eval_type)
|
|
98
|
+
output_length = self.config["time_steps"]
|
|
99
|
+
|
|
100
|
+
predictions = {}
|
|
101
|
+
for seq_num in range(sequence_numbers):
|
|
102
|
+
pf_dict = model.predict(
|
|
103
|
+
df=df, sequence_number=seq_num, output_length=output_length
|
|
104
|
+
)
|
|
105
|
+
for target, pf in pf_dict.items():
|
|
106
|
+
predictions.setdefault(target, []).append(pf)
|
|
107
|
+
return predictions
|
|
108
|
+
|
|
109
|
+
def _evaluate_model_artifact(self, eval_type: str, artifact_name: str = None):
|
|
110
|
+
"""
|
|
111
|
+
Evaluate trained model artifact.
|
|
112
|
+
"""
|
|
113
|
+
logger.info("Evaluating baseline model artifact")
|
|
114
|
+
if artifact_name:
|
|
115
|
+
path_artifact = self._model_path.artifacts / artifact_name
|
|
116
|
+
else:
|
|
117
|
+
path_artifact = self._model_path.get_latest_model_artifact_path(
|
|
118
|
+
run_type=self.config["run_type"]
|
|
119
|
+
)
|
|
120
|
+
self._config_manager.add_config({"timestamp": path_artifact.stem[-15:]})
|
|
121
|
+
self.model, df_source = self._setup_model_and_data()
|
|
122
|
+
logger.info(f"Generating predictions for {eval_type} evaluation")
|
|
123
|
+
return self._generate_predictions(self.model, df_source, eval_type)
|
|
124
|
+
|
|
125
|
+
def _forecast_model_artifact(self, artifact_name: str = None):
|
|
126
|
+
"""
|
|
127
|
+
Generate forecasts using trained model artifact.
|
|
128
|
+
"""
|
|
129
|
+
logger.info("Generating forecasts")
|
|
130
|
+
if artifact_name:
|
|
131
|
+
path_artifact = self._model_path.artifacts / artifact_name
|
|
132
|
+
else:
|
|
133
|
+
path_artifact = self._model_path.get_latest_model_artifact_path(
|
|
134
|
+
run_type=self.config["run_type"]
|
|
135
|
+
)
|
|
136
|
+
self._config_manager.add_config({"timestamp": path_artifact.stem[-15:]})
|
|
137
|
+
self.model, df_source = self._setup_model_and_data()
|
|
138
|
+
output_length = self.config["time_steps"]
|
|
139
|
+
|
|
140
|
+
return self.model.predict(
|
|
141
|
+
df=df_source, sequence_number=0, output_length=output_length
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _evaluate_sweep(self, eval_type: str, model):
|
|
145
|
+
"""
|
|
146
|
+
Evaluate a baseline model during a WandB sweep iteration.
|
|
147
|
+
|
|
148
|
+
The model has already been fitted by _train_model_artifact().
|
|
149
|
+
We load the data and generate predictions using it.
|
|
150
|
+
"""
|
|
151
|
+
df_source = self._load_source()
|
|
152
|
+
return self._generate_predictions(model, df_source, eval_type)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from views_baseline.infrastructure.reproducibility_gate import ReproducibilityGate
|
|
2
|
+
from views_baseline.model.models.distributional import (
|
|
3
|
+
ConflictologyModel,
|
|
4
|
+
MixtureBaseline,
|
|
5
|
+
ParametricConflictology,
|
|
6
|
+
ParametricHurdleConflictology,
|
|
7
|
+
)
|
|
8
|
+
from views_baseline.model.models.point import AverageModel, LocfModel, ZeroModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaselineModelCatalog:
|
|
12
|
+
# Single source of truth lives in ReproducibilityGate.Config.ALGORITHM_GENOMES.
|
|
13
|
+
# This alias keeps the catalog's get_model() validation unchanged.
|
|
14
|
+
MODEL_GENOMES = ReproducibilityGate.Config.ALGORITHM_GENOMES
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: dict, partition_dict: dict, loa: str):
|
|
17
|
+
"""
|
|
18
|
+
Catalog of available baseline models.
|
|
19
|
+
"""
|
|
20
|
+
self.config = config
|
|
21
|
+
self.partition_dict = partition_dict
|
|
22
|
+
self.loa = loa
|
|
23
|
+
|
|
24
|
+
self.models = {
|
|
25
|
+
"ZeroModel": self._get_zero_model,
|
|
26
|
+
"LocfModel": self._get_locf_model,
|
|
27
|
+
"AverageModel": self._get_average_model,
|
|
28
|
+
"ConflictologyModel": self._get_conflictology_model,
|
|
29
|
+
"MixtureBaseline": self._get_mixture_model,
|
|
30
|
+
"ParametricConflictology": self._get_parametric_conflictology,
|
|
31
|
+
"ParametricHurdleConflictology": self._get_parametric_hurdle,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def get_model(self, model_name: str):
|
|
35
|
+
"""
|
|
36
|
+
Returns an initialized model instance.
|
|
37
|
+
Validates that required config keys for the model are present.
|
|
38
|
+
"""
|
|
39
|
+
if model_name not in self.models:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Model '{model_name}' is not in the catalog. "
|
|
42
|
+
f"Available: {self.list_models()}"
|
|
43
|
+
)
|
|
44
|
+
missing = [k for k in self.MODEL_GENOMES[model_name] if k not in self.config]
|
|
45
|
+
if missing:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Model '{model_name}' requires config keys {missing} "
|
|
48
|
+
f"but they are missing"
|
|
49
|
+
)
|
|
50
|
+
return self.models[model_name]()
|
|
51
|
+
|
|
52
|
+
def list_models(self):
|
|
53
|
+
return list(self.models.keys())
|
|
54
|
+
|
|
55
|
+
def _get_zero_model(self):
|
|
56
|
+
return ZeroModel(
|
|
57
|
+
targets=self.config["targets"], partition_dict=self.partition_dict, loa=self.loa
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _get_locf_model(self):
|
|
61
|
+
return LocfModel(
|
|
62
|
+
targets=self.config["targets"], partition_dict=self.partition_dict, loa=self.loa
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _get_average_model(self):
|
|
66
|
+
return AverageModel(
|
|
67
|
+
targets=self.config["targets"],
|
|
68
|
+
window_months=self.config["window_months"],
|
|
69
|
+
partition_dict=self.partition_dict,
|
|
70
|
+
loa=self.loa,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _get_conflictology_model(self):
|
|
74
|
+
return ConflictologyModel(
|
|
75
|
+
targets=self.config["targets"],
|
|
76
|
+
window_months=self.config["window_months"],
|
|
77
|
+
partition_dict=self.partition_dict,
|
|
78
|
+
loa=self.loa,
|
|
79
|
+
n_samples=self.config["n_samples"],
|
|
80
|
+
seed=self.config["seed"], # required + audited genome key (ADR-021); no silent default
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _get_mixture_model(self):
|
|
84
|
+
return MixtureBaseline(
|
|
85
|
+
targets=self.config["targets"],
|
|
86
|
+
window_months=self.config["window_months"],
|
|
87
|
+
lambda_mix=self.config["lambda_mix"],
|
|
88
|
+
n_samples=self.config["n_samples"],
|
|
89
|
+
partition_dict=self.partition_dict,
|
|
90
|
+
loa=self.loa,
|
|
91
|
+
seed=self.config["seed"], # required + audited genome key (ADR-021); no silent default
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _get_parametric_conflictology(self):
|
|
95
|
+
# family/transform/seed are required, audited genome keys (ADR-021/ADR-022); the
|
|
96
|
+
# constructor fails loud on an unsupported family or an illegal family×transform.
|
|
97
|
+
return ParametricConflictology(
|
|
98
|
+
targets=self.config["targets"],
|
|
99
|
+
window_months=self.config["window_months"],
|
|
100
|
+
partition_dict=self.partition_dict,
|
|
101
|
+
loa=self.loa,
|
|
102
|
+
n_samples=self.config["n_samples"],
|
|
103
|
+
family=self.config["family"],
|
|
104
|
+
transform=self.config["transform"],
|
|
105
|
+
seed=self.config["seed"],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _get_parametric_hurdle(self):
|
|
109
|
+
# family/transform/seed are required, audited genome keys (ADR-021/ADR-022); the
|
|
110
|
+
# constructor fails loud on a non-continuous family or an illegal family×transform.
|
|
111
|
+
return ParametricHurdleConflictology(
|
|
112
|
+
targets=self.config["targets"],
|
|
113
|
+
window_months=self.config["window_months"],
|
|
114
|
+
partition_dict=self.partition_dict,
|
|
115
|
+
loa=self.loa,
|
|
116
|
+
n_samples=self.config["n_samples"],
|
|
117
|
+
family=self.config["family"],
|
|
118
|
+
transform=self.config["transform"],
|
|
119
|
+
seed=self.config["seed"],
|
|
120
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Package-level default sentinels for baseline models.
|
|
2
|
+
|
|
3
|
+
Zero-Magic / Explicit-Defaults (ADR-021): the single source of truth for the
|
|
4
|
+
default RNG seed. It is a **sentinel for direct/test construction only** —
|
|
5
|
+
production configs MUST declare ``seed`` (a required, audited genome key). The
|
|
6
|
+
catalog reads ``config["seed"]`` strictly and never falls back to this constant.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
DEFAULT_SEED = 42
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Distribution family + transform registries for the parametric climatology models.
|
|
2
|
+
|
|
3
|
+
A **strategy registry** (ADR-022) split across three cohesive leaves:
|
|
4
|
+
|
|
5
|
+
* :mod:`transforms` — `TRANSFORMS`, `clamp_log`/`clamp_floor`, `EMIT_*` (pure log math).
|
|
6
|
+
* :mod:`families` — per-family `fit_`/`sample_` (method-of-moments; point-mass fallback).
|
|
7
|
+
* :mod:`registry` — `fit_family`/`sample_family` dispatch + `validate_family_transform`.
|
|
8
|
+
|
|
9
|
+
This package re-exports the stable public surface so importers depend on
|
|
10
|
+
``views_baseline.model.distributions`` and are insulated from the internal split.
|
|
11
|
+
Pure package: numpy + scipy only; no pandas, no views-frames, no model imports.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from views_baseline.model.distributions.families import (
|
|
15
|
+
CONTINUOUS_FAMILIES,
|
|
16
|
+
FAMILIES,
|
|
17
|
+
NATIVE_ZERO_FAMILIES,
|
|
18
|
+
fit_gamma,
|
|
19
|
+
fit_gumbel,
|
|
20
|
+
fit_lognormal,
|
|
21
|
+
fit_nb,
|
|
22
|
+
fit_zinb,
|
|
23
|
+
sample_gamma,
|
|
24
|
+
sample_gumbel,
|
|
25
|
+
sample_lognormal,
|
|
26
|
+
sample_nb,
|
|
27
|
+
sample_zinb,
|
|
28
|
+
)
|
|
29
|
+
from views_baseline.model.distributions.registry import (
|
|
30
|
+
fit_family,
|
|
31
|
+
sample_family,
|
|
32
|
+
validate_family_transform,
|
|
33
|
+
)
|
|
34
|
+
from views_baseline.model.distributions.transforms import (
|
|
35
|
+
EMIT_FLOOR,
|
|
36
|
+
EMIT_LOG_CEIL,
|
|
37
|
+
TRANSFORMS,
|
|
38
|
+
clamp_floor,
|
|
39
|
+
clamp_log,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"CONTINUOUS_FAMILIES",
|
|
44
|
+
"EMIT_FLOOR",
|
|
45
|
+
"EMIT_LOG_CEIL",
|
|
46
|
+
"FAMILIES",
|
|
47
|
+
"NATIVE_ZERO_FAMILIES",
|
|
48
|
+
"TRANSFORMS",
|
|
49
|
+
"clamp_floor",
|
|
50
|
+
"clamp_log",
|
|
51
|
+
"fit_family",
|
|
52
|
+
"fit_gamma",
|
|
53
|
+
"fit_gumbel",
|
|
54
|
+
"fit_lognormal",
|
|
55
|
+
"fit_nb",
|
|
56
|
+
"fit_zinb",
|
|
57
|
+
"sample_family",
|
|
58
|
+
"sample_gamma",
|
|
59
|
+
"sample_gumbel",
|
|
60
|
+
"sample_lognormal",
|
|
61
|
+
"sample_nb",
|
|
62
|
+
"sample_zinb",
|
|
63
|
+
"validate_family_transform",
|
|
64
|
+
]
|