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.
- {eb_evaluation-0.2.3/src/eb_evaluation.egg-info → eb_evaluation-0.2.4}/PKG-INFO +11 -11
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/README.md +10 -10
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/pyproject.toml +1 -1
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/__init__.py +5 -27
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/adjustment/__init__.py +1 -1
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/adjustment/_utils.py +7 -9
- eb_evaluation-0.2.4/src/eb_evaluation/adjustment/ral.py +290 -0
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/__init__.py +6 -14
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/entity.py +52 -40
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/group.py +69 -71
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/hierarchy.py +33 -35
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/panel.py +22 -23
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/dataframe/single.py +11 -18
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/auto_engine.py +44 -71
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/compare.py +43 -35
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/cwsl_regressor.py +32 -42
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/electric_barometer.py +50 -119
- eb_evaluation-0.2.4/src/eb_evaluation/utils/validation.py +97 -0
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4/src/eb_evaluation.egg-info}/PKG-INFO +11 -11
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation.egg-info/SOURCES.txt +1 -4
- eb_evaluation-0.2.3/src/eb_evaluation/adjustment/readiness_adjustment.py +0 -652
- eb_evaluation-0.2.3/src/eb_evaluation/dataframe/cost_ratio.py +0 -224
- eb_evaluation-0.2.3/src/eb_evaluation/dataframe/sensitivity.py +0 -192
- eb_evaluation-0.2.3/src/eb_evaluation/dataframe/tolerance.py +0 -674
- eb_evaluation-0.2.3/src/eb_evaluation/utils/validation.py +0 -100
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/LICENSE +0 -0
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/setup.cfg +0 -0
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/model_selection/__init__.py +3 -3
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation/utils/__init__.py +0 -0
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation.egg-info/dependency_links.txt +0 -0
- {eb_evaluation-0.2.3 → eb_evaluation-0.2.4}/src/eb_evaluation.egg-info/requires.txt +0 -0
- {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
|
+
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
99
|
-
"model_b": [11, 13, 6, 8],
|
|
99
|
+
"prediction": [9, 11, 8, 10],
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
#
|
|
103
|
-
results =
|
|
102
|
+
# Compute Cost-Weighted Service Loss (CWSL)
|
|
103
|
+
results = compute_cwsl_df(
|
|
104
104
|
df,
|
|
105
105
|
actual_col="actual",
|
|
106
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
66
|
-
"model_b": [11, 13, 6, 8],
|
|
66
|
+
"prediction": [9, 11, 8, 10],
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
#
|
|
70
|
-
results =
|
|
69
|
+
# Compute Cost-Weighted Service Loss (CWSL)
|
|
70
|
+
results = compute_cwsl_df(
|
|
71
71
|
df,
|
|
72
72
|
actual_col="actual",
|
|
73
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
]
|
|
@@ -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.
|
|
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:
|
|
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
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
]
|