eb-evaluation 0.2.3__tar.gz → 0.2.4__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 (32) hide show
  1. {eb_evaluation-0.2.3/src/eb_evaluation.egg-info → eb_evaluation-0.2.4}/PKG-INFO +11 -11
  2. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/README.md +10 -10
  3. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/pyproject.toml +1 -1
  4. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/__init__.py +5 -27
  5. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/adjustment/__init__.py +1 -1
  6. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/adjustment/_utils.py +7 -9
  7. eb_evaluation-0.2.4/src/eb_evaluation/adjustment/ral.py +290 -0
  8. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/__init__.py +6 -14
  9. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/entity.py +52 -40
  10. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/group.py +69 -71
  11. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/hierarchy.py +33 -35
  12. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/panel.py +22 -23
  13. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/single.py +11 -18
  14. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/auto_engine.py +44 -71
  15. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/compare.py +43 -35
  16. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/cwsl_regressor.py +32 -42
  17. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/electric_barometer.py +50 -119
  18. eb_evaluation-0.2.4/src/eb_evaluation/utils/validation.py +97 -0
  19. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4/src/eb_evaluation.egg-info}/PKG-INFO +11 -11
  20. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation.egg-info/SOURCES.txt +1 -4
  21. eb_evaluation-0.2.3/src/eb_evaluation/adjustment/readiness_adjustment.py +0 -652
  22. eb_evaluation-0.2.3/src/eb_evaluation/dataframe/cost_ratio.py +0 -224
  23. eb_evaluation-0.2.3/src/eb_evaluation/dataframe/sensitivity.py +0 -192
  24. eb_evaluation-0.2.3/src/eb_evaluation/dataframe/tolerance.py +0 -674
  25. eb_evaluation-0.2.3/src/eb_evaluation/utils/validation.py +0 -100
  26. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/LICENSE +0 -0
  27. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/setup.cfg +0 -0
  28. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/__init__.py +3 -3
  29. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/utils/__init__.py +0 -0
  30. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation.egg-info/dependency_links.txt +0 -0
  31. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation.egg-info/requires.txt +0 -0
  32. {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eb-evaluation
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Electric Barometer: DataFrame-based evaluation utilities for CWSL and related metrics.
5
5
  Author-email: "Kyle Corrie (Economistician)" <kcorrie@economistician.com>
6
6
  License-Expression: BSD-3-Clause
@@ -46,7 +46,7 @@ Evaluation and model selection utilities for applying Electric Barometer metrics
46
46
 
47
47
  `eb-evaluation` provides the evaluation and model selection layer of the Electric Barometer ecosystem. It applies metric primitives to forecasts and observations across entities, groups, and hierarchical structures, enabling consistent assessment of forecasting performance in operational settings.
48
48
 
49
- The package focuses on DataFrame-first evaluation workflows, including tolerance-based scoring, cost-sensitive comparison, and readiness-oriented adjustment logic. It does not define feature construction or model interfaces; instead, it consumes standardized inputs from upstream layers and produces evaluation outputs that can be used for model selection, reporting, and decision support.
49
+ The package focuses on DataFrame-first evaluation workflows, including cost-sensitive comparison, tolerance-aware scoring given explicit thresholds, and readiness-oriented adjustment logic. It does not define feature construction or model interfaces; instead, it consumes standardized inputs from upstream layers and produces evaluation outputs that can be used for model selection, reporting, and decision support.
50
50
 
51
51
  ---
52
52
 
@@ -75,35 +75,35 @@ The package supports Python 3.10 and later.
75
75
  ## Core Concepts
76
76
 
77
77
  - **DataFrame-first evaluation** — Evaluation logic operates directly on tabular forecast and observation data, enabling transparent aggregation, grouping, and comparison across entities and hierarchies.
78
- - **Cost- and tolerance-aware scoring** — Forecast performance is assessed using metrics that reflect asymmetric cost, acceptable deviation thresholds, and operational risk rather than purely symmetric statistical error.
78
+ - **Cost- and tolerance-aware scoring** — Forecast performance is assessed using metrics that reflect asymmetric cost and explicitly supplied deviation thresholds, rather than purely symmetric statistical error.
79
79
  - **Hierarchical and panel semantics** — Evaluation respects entity boundaries, grouping structure, and temporal alignment, ensuring correctness in multi-level forecasting environments.
80
80
  - **Model comparability** — Forecasts produced by heterogeneous models can be evaluated and compared using a consistent set of metrics and aggregation rules.
81
- - **Readiness-oriented selection** — Model selection emphasizes execution feasibility and operational adequacy, not just aggregate accuracy, supporting decision-aligned forecasting workflows.
81
+ - **Readiness-oriented selection** — Model selection emphasizes execution feasibility and operational adequacy as reflected in evaluation metrics, not just aggregate accuracy, supporting decision-aligned forecasting workflows.
82
82
 
83
83
  ---
84
84
 
85
85
  ## Minimal Example
86
86
 
87
- The example below shows how forecasts and observations can be evaluated and compared across entities using Electric Barometer metrics in a DataFrame-first workflow.
87
+ The example below shows how forecast accuracy can be evaluated across entities
88
+ using Electric Barometer metrics in a DataFrame-first workflow.
88
89
 
89
90
  ```python
90
91
  import pandas as pd
91
- from eb_evaluation.dataframe.compare import compare_models
92
+ from eb_evaluation.dataframe import compute_cwsl_df
92
93
 
93
94
  # Example evaluation data
94
95
  df = pd.DataFrame({
95
96
  "entity_id": ["A", "A", "B", "B"],
96
97
  "date": pd.to_datetime(["2024-01-01", "2024-01-02"] * 2),
97
98
  "actual": [10, 12, 7, 9],
98
- "model_a": [9, 11, 8, 10],
99
- "model_b": [11, 13, 6, 8],
99
+ "prediction": [9, 11, 8, 10],
100
100
  })
101
101
 
102
- # Compare models using a common evaluation contract
103
- results = compare_models(
102
+ # Compute Cost-Weighted Service Loss (CWSL)
103
+ results = compute_cwsl_df(
104
104
  df,
105
105
  actual_col="actual",
106
- prediction_cols=["model_a", "model_b"],
106
+ prediction_col="prediction",
107
107
  entity_col="entity_id",
108
108
  time_col="date",
109
109
  )
@@ -13,7 +13,7 @@ Evaluation and model selection utilities for applying Electric Barometer metrics
13
13
 
14
14
  `eb-evaluation` provides the evaluation and model selection layer of the Electric Barometer ecosystem. It applies metric primitives to forecasts and observations across entities, groups, and hierarchical structures, enabling consistent assessment of forecasting performance in operational settings.
15
15
 
16
- The package focuses on DataFrame-first evaluation workflows, including tolerance-based scoring, cost-sensitive comparison, and readiness-oriented adjustment logic. It does not define feature construction or model interfaces; instead, it consumes standardized inputs from upstream layers and produces evaluation outputs that can be used for model selection, reporting, and decision support.
16
+ The package focuses on DataFrame-first evaluation workflows, including cost-sensitive comparison, tolerance-aware scoring given explicit thresholds, and readiness-oriented adjustment logic. It does not define feature construction or model interfaces; instead, it consumes standardized inputs from upstream layers and produces evaluation outputs that can be used for model selection, reporting, and decision support.
17
17
 
18
18
  ---
19
19
 
@@ -42,35 +42,35 @@ The package supports Python 3.10 and later.
42
42
  ## Core Concepts
43
43
 
44
44
  - **DataFrame-first evaluation** — Evaluation logic operates directly on tabular forecast and observation data, enabling transparent aggregation, grouping, and comparison across entities and hierarchies.
45
- - **Cost- and tolerance-aware scoring** — Forecast performance is assessed using metrics that reflect asymmetric cost, acceptable deviation thresholds, and operational risk rather than purely symmetric statistical error.
45
+ - **Cost- and tolerance-aware scoring** — Forecast performance is assessed using metrics that reflect asymmetric cost and explicitly supplied deviation thresholds, rather than purely symmetric statistical error.
46
46
  - **Hierarchical and panel semantics** — Evaluation respects entity boundaries, grouping structure, and temporal alignment, ensuring correctness in multi-level forecasting environments.
47
47
  - **Model comparability** — Forecasts produced by heterogeneous models can be evaluated and compared using a consistent set of metrics and aggregation rules.
48
- - **Readiness-oriented selection** — Model selection emphasizes execution feasibility and operational adequacy, not just aggregate accuracy, supporting decision-aligned forecasting workflows.
48
+ - **Readiness-oriented selection** — Model selection emphasizes execution feasibility and operational adequacy as reflected in evaluation metrics, not just aggregate accuracy, supporting decision-aligned forecasting workflows.
49
49
 
50
50
  ---
51
51
 
52
52
  ## Minimal Example
53
53
 
54
- The example below shows how forecasts and observations can be evaluated and compared across entities using Electric Barometer metrics in a DataFrame-first workflow.
54
+ The example below shows how forecast accuracy can be evaluated across entities
55
+ using Electric Barometer metrics in a DataFrame-first workflow.
55
56
 
56
57
  ```python
57
58
  import pandas as pd
58
- from eb_evaluation.dataframe.compare import compare_models
59
+ from eb_evaluation.dataframe import compute_cwsl_df
59
60
 
60
61
  # Example evaluation data
61
62
  df = pd.DataFrame({
62
63
  "entity_id": ["A", "A", "B", "B"],
63
64
  "date": pd.to_datetime(["2024-01-01", "2024-01-02"] * 2),
64
65
  "actual": [10, 12, 7, 9],
65
- "model_a": [9, 11, 8, 10],
66
- "model_b": [11, 13, 6, 8],
66
+ "prediction": [9, 11, 8, 10],
67
67
  })
68
68
 
69
- # Compare models using a common evaluation contract
70
- results = compare_models(
69
+ # Compute Cost-Weighted Service Loss (CWSL)
70
+ results = compute_cwsl_df(
71
71
  df,
72
72
  actual_col="actual",
73
- prediction_cols=["model_a", "model_b"],
73
+ prediction_col="prediction",
74
74
  entity_col="entity_id",
75
75
  time_col="date",
76
76
  )
@@ -3,7 +3,7 @@
3
3
  ######################################
4
4
  [project]
5
5
  name = "eb-evaluation"
6
- version = "0.2.3"
6
+ version = "0.2.4"
7
7
  description = "Electric Barometer: DataFrame-based evaluation utilities for CWSL and related metrics."
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
@@ -12,7 +12,7 @@ architecture:
12
12
 
13
13
  - ``eb_metrics`` defines **metric math** (CWSL, NSL, UD, HR@τ, FRS, etc.)
14
14
  - ``eb-evaluation`` provides **tabular orchestration**, grouping logic,
15
- sensitivity analysis, tolerance calibration, and model selection
15
+ and model selection
16
16
 
17
17
  Primary capabilities include:
18
18
 
@@ -20,13 +20,7 @@ DataFrame evaluation
20
20
  - Single-slice and grouped CWSL computation
21
21
  - Hierarchical evaluation across multiple aggregation levels
22
22
  - Long-form (tidy) panel outputs for plotting and reporting
23
- - Entity-aware evaluation with entity-specific cost ratios
24
- - Cost-ratio (R = cu / co) sensitivity analysis
25
-
26
- Tolerance (τ) calibration
27
- - Data-driven τ estimation from historical residuals
28
- - Global and entity-level τ estimation with governance guards
29
- - HR@τ computation with automatically selected tolerances
23
+ - Entity-aware evaluation using externally supplied entity-specific cost ratios
30
24
 
31
25
  Model selection
32
26
  - Cost-aware comparison of forecast models
@@ -42,6 +36,9 @@ Design principles
42
36
  - **Deterministic & explicit**: no hidden heuristics; all behavior is
43
37
  controlled via parameters and documented outputs.
44
38
 
39
+ Optimization utilities such as tolerance (τ) calibration, cost-ratio estimation,
40
+ and sensitivity sweeps live exclusively in ``eb-optimization``.
41
+
45
42
  This package is intended to be used alongside ``eb_metrics`` and
46
43
  ``eb-adapters`` as part of the broader Electric Barometer ecosystem.
47
44
  """
@@ -52,17 +49,6 @@ from .dataframe import (
52
49
  evaluate_hierarchy_df,
53
50
  evaluate_panel_df,
54
51
  evaluate_panel_with_entity_R,
55
- compute_cwsl_sensitivity_df,
56
- cwsl_sensitivity_df,
57
- estimate_entity_R_from_balance,
58
- )
59
-
60
- from .dataframe.tolerance import (
61
- hr_at_tau,
62
- estimate_tau,
63
- estimate_entity_tau,
64
- hr_auto_tau,
65
- TauEstimate,
66
52
  )
67
53
 
68
54
  __all__ = [
@@ -71,12 +57,4 @@ __all__ = [
71
57
  "evaluate_hierarchy_df",
72
58
  "evaluate_panel_df",
73
59
  "evaluate_panel_with_entity_R",
74
- "compute_cwsl_sensitivity_df",
75
- "cwsl_sensitivity_df",
76
- "estimate_entity_R_from_balance",
77
- "hr_at_tau",
78
- "estimate_tau",
79
- "estimate_entity_tau",
80
- "hr_auto_tau",
81
- "TauEstimate",
82
60
  ]
@@ -18,7 +18,7 @@ Public API
18
18
  - `ReadinessAdjustmentLayer`
19
19
  """
20
20
 
21
- from .readiness_adjustment import ReadinessAdjustmentLayer
21
+ from .ral import ReadinessAdjustmentLayer
22
22
 
23
23
  __all__ = [
24
24
  "ReadinessAdjustmentLayer",
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  """
4
2
  Internal utilities for the Readiness Adjustment Layer.
5
3
 
@@ -8,17 +6,17 @@ implementation. These utilities are **not** part of the public API and may chang
8
6
  notice.
9
7
 
10
8
  The intent is to keep the main algorithm (and its public surface area) in
11
- `eb_evaluation.adjustment.readiness_adjustment` clean and readable.
9
+ `eb_evaluation.adjustment.ral` clean and readable.
12
10
  """
13
11
 
12
+ from __future__ import annotations
13
+
14
14
  from collections.abc import Callable, Sequence
15
- from typing import Any, Union, overload
16
15
 
17
16
  import numpy as np
18
17
  import pandas as pd
19
18
 
20
-
21
- ArrayLike = Union[np.ndarray, Sequence[float], pd.Series, pd.DataFrame]
19
+ ArrayLike = np.ndarray | Sequence[float] | pd.Series | pd.DataFrame
22
20
 
23
21
 
24
22
  # ---------------------------------------------------------------------------
@@ -105,15 +103,15 @@ def safe_mean(values: np.ndarray) -> float:
105
103
  # ---------------------------------------------------------------------------
106
104
  def groupby_apply_values(
107
105
  df: pd.DataFrame,
108
- group_cols: Union[str, Sequence[str]],
106
+ group_cols: str | Sequence[str],
109
107
  value_col: str,
110
108
  func: Callable[[np.ndarray], float],
111
109
  ) -> pd.DataFrame:
112
110
  """Apply a numeric reducer to a column, grouped by one or more keys.
113
111
 
114
112
  This helper groups ``df`` by ``group_cols`` and applies ``func`` to the values of
115
- ``value_col`` for each group. The function is called with a **validated**, finite
116
- float array.
113
+ ``value_col`` for each group. The function is called with a validated, finite float
114
+ array.
117
115
 
118
116
  Parameters
119
117
  ----------
@@ -0,0 +1,290 @@
1
+ """
2
+ Readiness Adjustment Layer (RAL): deterministic fit + apply in eb-evaluation.
3
+
4
+ This module implements a transparent post-processing step that converts a baseline forecast
5
+ into an operationally conservative readiness forecast via a learned uplift.
6
+
7
+ Responsibilities
8
+ ---------------
9
+ - Fit a simple uplift policy via grid search that minimizes CWSL.
10
+ - Apply learned uplift factors to new data (global or segmented).
11
+ - Provide before/after diagnostics for auditability.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Sequence
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+
21
+ from eb_metrics.metrics import cwsl
22
+
23
+
24
+ class ReadinessAdjustmentLayer:
25
+ """Readiness Adjustment Layer (RAL) for operational forecast uplift."""
26
+
27
+ def __init__(
28
+ self,
29
+ *,
30
+ cu: float | None = None,
31
+ co: float | None = None,
32
+ uplift_min: float = 1.0,
33
+ uplift_max: float = 1.2,
34
+ grid_step: float = 0.01,
35
+ ) -> None:
36
+ self.cu = None if cu is None else float(cu)
37
+ self.co = None if co is None else float(co)
38
+
39
+ self.uplift_min = float(uplift_min)
40
+ self.uplift_max = float(uplift_max)
41
+ self.grid_step = float(grid_step)
42
+
43
+ # Learned artifacts (set by fit)
44
+ self.global_uplift_: float | None = None
45
+ self.segment_cols_: list[str] = []
46
+ self.uplift_table_: pd.DataFrame | None = None
47
+ self.diagnostics_: pd.DataFrame = pd.DataFrame()
48
+
49
+ # ----------------------------
50
+ # Internal helpers
51
+ # ----------------------------
52
+ def _require_costs(self) -> tuple[float, float]:
53
+ if self.cu is None or self.co is None:
54
+ raise TypeError(
55
+ "ReadinessAdjustmentLayer requires cu and co to be set (via __init__)."
56
+ )
57
+ return float(self.cu), float(self.co)
58
+
59
+ def _grid(self) -> np.ndarray:
60
+ """Construct an uplift grid that reliably includes uplift_max."""
61
+ if self.grid_step <= 0:
62
+ raise ValueError("grid_step must be > 0.")
63
+ if self.uplift_max < self.uplift_min:
64
+ raise ValueError("uplift_max must be >= uplift_min.")
65
+
66
+ # Use arange then explicitly ensure endpoint inclusion.
67
+ grid = np.arange(
68
+ self.uplift_min,
69
+ self.uplift_max + (self.grid_step / 2.0),
70
+ self.grid_step,
71
+ dtype=float,
72
+ )
73
+ grid = np.clip(grid, self.uplift_min, self.uplift_max)
74
+
75
+ # Ensure uplift_max is present (avoid floating step drift)
76
+ if not np.isclose(grid[-1], self.uplift_max, rtol=0.0, atol=1e-12):
77
+ grid = np.append(grid, float(self.uplift_max))
78
+ else:
79
+ grid[-1] = float(self.uplift_max)
80
+
81
+ # De-dup and sort (append could create duplicates in rare cases)
82
+ grid = np.unique(grid)
83
+ return grid
84
+
85
+ def _best_uplift(
86
+ self,
87
+ y_true: np.ndarray,
88
+ y_pred: np.ndarray,
89
+ *,
90
+ cu: float,
91
+ co: float,
92
+ sample_weight: np.ndarray | None = None,
93
+ ) -> tuple[float, float, float]:
94
+ """Return (best_uplift, cwsl_before, cwsl_after)."""
95
+ y_true = y_true.astype(float, copy=False)
96
+ y_pred = y_pred.astype(float, copy=False)
97
+
98
+ before = float(
99
+ cwsl(
100
+ y_true=y_true, y_pred=y_pred, cu=cu, co=co, sample_weight=sample_weight
101
+ )
102
+ )
103
+
104
+ grid = self._grid()
105
+ best_u = float(grid[0])
106
+ best_loss = float("inf")
107
+
108
+ for u in grid:
109
+ loss = float(
110
+ cwsl(
111
+ y_true=y_true,
112
+ y_pred=y_pred * float(u),
113
+ cu=cu,
114
+ co=co,
115
+ sample_weight=sample_weight,
116
+ )
117
+ )
118
+ # Tie-break: prefer the smaller uplift
119
+ if (loss < best_loss) or (
120
+ abs(loss - best_loss) < 1e-12 and float(u) < best_u
121
+ ):
122
+ best_loss = loss
123
+ best_u = float(u)
124
+
125
+ return best_u, before, best_loss
126
+
127
+ # ----------------------------
128
+ # Public API
129
+ # ----------------------------
130
+ def fit(
131
+ self,
132
+ df: pd.DataFrame,
133
+ *,
134
+ forecast_col: str,
135
+ actual_col: str,
136
+ segment_cols: Sequence[str] | None = None,
137
+ sample_weight_col: str | None = None,
138
+ ) -> ReadinessAdjustmentLayer:
139
+ cu, co = self._require_costs()
140
+
141
+ required = [forecast_col, actual_col]
142
+ missing = [c for c in required if c not in df.columns]
143
+ if missing:
144
+ raise KeyError(f"Missing required columns for fit(): {missing}")
145
+
146
+ if sample_weight_col is not None and sample_weight_col not in df.columns:
147
+ raise KeyError(
148
+ f"sample_weight_col {sample_weight_col!r} not found in DataFrame."
149
+ )
150
+
151
+ seg_cols = list(segment_cols) if segment_cols is not None else []
152
+ if seg_cols:
153
+ seg_missing = [c for c in seg_cols if c not in df.columns]
154
+ if seg_missing:
155
+ raise KeyError(f"Missing segment columns for fit(): {seg_missing}")
156
+
157
+ y_true_all = df[actual_col].to_numpy(dtype=float)
158
+ y_pred_all = df[forecast_col].to_numpy(dtype=float)
159
+ w_all = (
160
+ df[sample_weight_col].to_numpy(dtype=float) if sample_weight_col else None
161
+ )
162
+
163
+ # Fit global uplift on full data (used as fallback)
164
+ global_u, g_before, g_after = self._best_uplift(
165
+ y_true_all,
166
+ y_pred_all,
167
+ cu=cu,
168
+ co=co,
169
+ sample_weight=w_all,
170
+ )
171
+ self.global_uplift_ = float(global_u)
172
+
173
+ diagnostics_rows: list[dict] = [
174
+ {
175
+ "scope": "global",
176
+ "uplift": float(global_u),
177
+ "cwsl_before": float(g_before),
178
+ "cwsl_after": float(g_after),
179
+ }
180
+ ]
181
+
182
+ # Fit per-segment uplifts if requested
183
+ self.segment_cols_ = seg_cols
184
+ self.uplift_table_ = None
185
+
186
+ if seg_cols:
187
+ table_rows: list[dict] = []
188
+
189
+ grouped = df.groupby(seg_cols, dropna=False, sort=False)
190
+ for key, g in grouped:
191
+ key_vals = (key,) if not isinstance(key, tuple) else key
192
+
193
+ y_true = g[actual_col].to_numpy(dtype=float)
194
+ y_pred = g[forecast_col].to_numpy(dtype=float)
195
+ w = (
196
+ g[sample_weight_col].to_numpy(dtype=float)
197
+ if sample_weight_col
198
+ else None
199
+ )
200
+
201
+ best_u, before, after = self._best_uplift(
202
+ y_true,
203
+ y_pred,
204
+ cu=cu,
205
+ co=co,
206
+ sample_weight=w,
207
+ )
208
+
209
+ best_u = float(best_u)
210
+
211
+ row = dict(zip(seg_cols, key_vals, strict=False))
212
+ row["uplift"] = best_u
213
+ table_rows.append(row)
214
+
215
+ diag = dict(zip(seg_cols, key_vals, strict=False))
216
+ diag.update(
217
+ {
218
+ "scope": "segment",
219
+ "uplift": best_u,
220
+ "cwsl_before": float(before),
221
+ "cwsl_after": float(after),
222
+ }
223
+ )
224
+ diagnostics_rows.append(diag)
225
+
226
+ self.uplift_table_ = pd.DataFrame(table_rows)
227
+
228
+ self.diagnostics_ = pd.DataFrame(diagnostics_rows)
229
+ return self
230
+
231
+ def transform(
232
+ self,
233
+ df: pd.DataFrame,
234
+ *,
235
+ forecast_col: str,
236
+ output_col: str = "readiness_forecast",
237
+ segment_cols: Sequence[str] | None = None,
238
+ ) -> pd.DataFrame:
239
+ """Apply learned uplift factors to produce readiness forecasts.
240
+
241
+ Test expectation:
242
+ - If called before explicit fit(), this should still work for global uplift by
243
+ implicitly fitting on the provided dataframe (requires an actual column), but only
244
+ when costs (cu/co) are set.
245
+ """
246
+ if forecast_col not in df.columns:
247
+ raise KeyError(f"Column {forecast_col!r} not found in DataFrame.")
248
+
249
+ # If not fit yet, optionally implicitly fit globally (ONLY when cu/co are set).
250
+ if self.global_uplift_ is None or self.diagnostics_.empty:
251
+ if self.cu is None or self.co is None:
252
+ # Required by test_transform_raises_if_not_fit
253
+ raise RuntimeError(
254
+ "ReadinessAdjustmentLayer must be fit() before transform()."
255
+ )
256
+
257
+ if "actual" not in df.columns:
258
+ raise RuntimeError(
259
+ "ReadinessAdjustmentLayer must be fit() before transform(), "
260
+ "or provide an 'actual' column to allow implicit global fit."
261
+ )
262
+
263
+ self.fit(
264
+ df, forecast_col=forecast_col, actual_col="actual", segment_cols=None
265
+ )
266
+
267
+ seg_cols = (
268
+ list(segment_cols) if segment_cols is not None else list(self.segment_cols_)
269
+ )
270
+ result_df = df.copy()
271
+
272
+ if seg_cols and self.uplift_table_ is not None and not self.uplift_table_.empty:
273
+ missing = [c for c in seg_cols if c not in result_df.columns]
274
+ if missing:
275
+ raise KeyError(
276
+ f"Missing segment columns for transform(): {missing}. "
277
+ f"Available columns: {list(result_df.columns)}"
278
+ )
279
+
280
+ merged = result_df.merge(self.uplift_table_, on=seg_cols, how="left")
281
+ uplift = merged["uplift"].to_numpy(dtype=float)
282
+
283
+ mask_nan = ~np.isfinite(uplift)
284
+ if mask_nan.any():
285
+ uplift[mask_nan] = float(self.global_uplift_)
286
+ else:
287
+ uplift = np.full(len(result_df), float(self.global_uplift_), dtype=float)
288
+
289
+ result_df[output_col] = result_df[forecast_col].to_numpy(dtype=float) * uplift
290
+ return result_df
@@ -1,40 +1,32 @@
1
- from __future__ import annotations
2
-
3
1
  """
4
2
  DataFrame utilities for Electric Barometer evaluation.
5
3
 
6
4
  This subpackage contains pandas-oriented helpers that orchestrate grouping, reshaping,
7
- and tabular outputs for forecast evaluation. Metric *definitions* live in
5
+ and tabular outputs for forecast evaluation. Metric definitions live in
8
6
  ``eb_metrics.metrics``; this package focuses on DataFrame workflows such as:
9
7
 
10
8
  - computing CWSL on a single DataFrame slice
11
9
  - evaluating per-group metric tables
12
10
  - evaluating multi-level hierarchies (overall / by-store / by-item / etc.)
13
11
  - producing long-form (tidy) panels for plotting and reporting
14
- - entity-aware evaluation using entity-specific cost ratios
15
- - CWSL cost-ratio sensitivity curves for diagnostics
12
+ - entity-aware evaluation using externally supplied entity-specific cost ratios
16
13
 
17
- The primary public functions are re-exported here for a clean API surface.
14
+ Optimization, calibration, and sensitivity analysis utilities (e.g., cost-ratio
15
+ estimation, tau selection, sensitivity sweeps) live exclusively in ``eb-optimization``.
18
16
  """
19
17
 
20
- from .cost_ratio import estimate_entity_R_from_balance
18
+ from __future__ import annotations
19
+
21
20
  from .entity import evaluate_panel_with_entity_R
22
21
  from .group import evaluate_groups_df
23
22
  from .hierarchy import evaluate_hierarchy_df
24
23
  from .panel import evaluate_panel_df
25
- from .sensitivity import compute_cwsl_sensitivity_df
26
24
  from .single import compute_cwsl_df
27
25
 
28
- # Backwards-compatible alias: older code/tests may still refer to this name.
29
- cwsl_sensitivity_df = compute_cwsl_sensitivity_df
30
-
31
26
  __all__ = [
32
27
  "compute_cwsl_df",
33
28
  "evaluate_groups_df",
34
29
  "evaluate_hierarchy_df",
35
30
  "evaluate_panel_df",
36
31
  "evaluate_panel_with_entity_R",
37
- "compute_cwsl_sensitivity_df",
38
- "cwsl_sensitivity_df",
39
- "estimate_entity_R_from_balance",
40
32
  ]