eb-evaluation 0.1.1__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.
- eb_evaluation/__init__.py +82 -0
- eb_evaluation/adjustment/__init__.py +25 -0
- eb_evaluation/adjustment/_utils.py +168 -0
- eb_evaluation/adjustment/readiness_adjustment.py +652 -0
- eb_evaluation/dataframe/__init__.py +40 -0
- eb_evaluation/dataframe/cost_ratio.py +224 -0
- eb_evaluation/dataframe/entity.py +250 -0
- eb_evaluation/dataframe/group.py +216 -0
- eb_evaluation/dataframe/hierarchy.py +198 -0
- eb_evaluation/dataframe/panel.py +141 -0
- eb_evaluation/dataframe/sensitivity.py +192 -0
- eb_evaluation/dataframe/single.py +110 -0
- eb_evaluation/dataframe/tolerance.py +674 -0
- eb_evaluation/features/__init__.py +23 -0
- eb_evaluation/features/feature_engineer.py +371 -0
- eb_evaluation/model_selection/__init__.py +43 -0
- eb_evaluation/model_selection/auto_engine.py +299 -0
- eb_evaluation/model_selection/compare.py +419 -0
- eb_evaluation/model_selection/cwsl_regressor.py +428 -0
- eb_evaluation/model_selection/electric_barometer.py +499 -0
- eb_evaluation/utils/__init__.py +29 -0
- eb_evaluation/utils/validation.py +100 -0
- eb_evaluation-0.1.1.dist-info/METADATA +169 -0
- eb_evaluation-0.1.1.dist-info/RECORD +27 -0
- eb_evaluation-0.1.1.dist-info/WHEEL +5 -0
- eb_evaluation-0.1.1.dist-info/licenses/LICENSE +28 -0
- eb_evaluation-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Electric Barometer Evaluation Toolkit (eb-evaluation).
|
|
3
|
+
|
|
4
|
+
This package provides **DataFrame-oriented evaluation, diagnostics, and
|
|
5
|
+
model-selection utilities** built around Cost-Weighted Service Loss (CWSL)
|
|
6
|
+
and related readiness metrics.
|
|
7
|
+
|
|
8
|
+
Scope
|
|
9
|
+
-----
|
|
10
|
+
The eb-evaluation package sits *above* ``eb_metrics`` in the Electric Barometer
|
|
11
|
+
architecture:
|
|
12
|
+
|
|
13
|
+
- ``eb_metrics`` defines **metric math** (CWSL, NSL, UD, HR@τ, FRS, etc.)
|
|
14
|
+
- ``eb-evaluation`` provides **tabular orchestration**, grouping logic,
|
|
15
|
+
sensitivity analysis, tolerance calibration, and model selection
|
|
16
|
+
|
|
17
|
+
Primary capabilities include:
|
|
18
|
+
|
|
19
|
+
DataFrame evaluation
|
|
20
|
+
- Single-slice and grouped CWSL computation
|
|
21
|
+
- Hierarchical evaluation across multiple aggregation levels
|
|
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
|
|
30
|
+
|
|
31
|
+
Model selection
|
|
32
|
+
- Cost-aware comparison of forecast models
|
|
33
|
+
- Holdout and cross-validated selection by minimum CWSL
|
|
34
|
+
- sklearn-style wrappers for downstream pipelines
|
|
35
|
+
|
|
36
|
+
Design principles
|
|
37
|
+
-----------------
|
|
38
|
+
- **Separation of concerns**: metric definitions live in ``eb_metrics``,
|
|
39
|
+
orchestration and evaluation live here.
|
|
40
|
+
- **Operational alignment**: selection and diagnostics are driven by cost
|
|
41
|
+
and readiness, not symmetric error alone.
|
|
42
|
+
- **Deterministic & explicit**: no hidden heuristics; all behavior is
|
|
43
|
+
controlled via parameters and documented outputs.
|
|
44
|
+
|
|
45
|
+
This package is intended to be used alongside ``eb_metrics`` and
|
|
46
|
+
``eb-adapters`` as part of the broader Electric Barometer ecosystem.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from .dataframe import (
|
|
50
|
+
compute_cwsl_df,
|
|
51
|
+
evaluate_groups_df,
|
|
52
|
+
evaluate_hierarchy_df,
|
|
53
|
+
evaluate_panel_df,
|
|
54
|
+
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
|
+
)
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"compute_cwsl_df",
|
|
70
|
+
"evaluate_groups_df",
|
|
71
|
+
"evaluate_hierarchy_df",
|
|
72
|
+
"evaluate_panel_df",
|
|
73
|
+
"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
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adjustment utilities for Electric Barometer evaluation.
|
|
3
|
+
|
|
4
|
+
The `eb_evaluation.adjustment` package contains the **Readiness Adjustment Layer (RAL)**,
|
|
5
|
+
a lightweight post-processing component that converts a baseline statistical forecast into an
|
|
6
|
+
operationally conservative *readiness forecast* via a learned multiplicative uplift.
|
|
7
|
+
|
|
8
|
+
Key ideas
|
|
9
|
+
---------
|
|
10
|
+
- **Metrics live in** `eb_metrics.metrics` (definitions only).
|
|
11
|
+
- **Adjustments live here** (evaluation / selection utilities that *consume* metrics).
|
|
12
|
+
- RAL learns an uplift by grid-searching multipliers and selecting the value that minimizes
|
|
13
|
+
**Cost-Weighted Service Loss (CWSL)** on historical data.
|
|
14
|
+
- Uplifts can be learned globally or per-segment (with a global fallback for unseen segments).
|
|
15
|
+
|
|
16
|
+
Public API
|
|
17
|
+
----------
|
|
18
|
+
- `ReadinessAdjustmentLayer`
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .readiness_adjustment import ReadinessAdjustmentLayer
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ReadinessAdjustmentLayer",
|
|
25
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Internal utilities for the Readiness Adjustment Layer.
|
|
5
|
+
|
|
6
|
+
This module contains small, focused helpers used by the Readiness Adjustment Layer (RAL)
|
|
7
|
+
implementation. These utilities are **not** part of the public API and may change without
|
|
8
|
+
notice.
|
|
9
|
+
|
|
10
|
+
The intent is to keep the main algorithm (and its public surface area) in
|
|
11
|
+
`eb_evaluation.adjustment.readiness_adjustment` clean and readable.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable, Sequence
|
|
15
|
+
from typing import Any, Union, overload
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ArrayLike = Union[np.ndarray, Sequence[float], pd.Series, pd.DataFrame]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Array validation utilities
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
def validate_numeric_array(arr: ArrayLike, name: str = "array") -> np.ndarray:
|
|
28
|
+
"""Validate and coerce an input to a finite float NumPy array.
|
|
29
|
+
|
|
30
|
+
This helper is intentionally strict: it ensures the array is not scalar and that
|
|
31
|
+
all values are finite. This is appropriate for evaluation utilities where silent
|
|
32
|
+
propagation of NaNs can lead to misleading metric comparisons.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
arr
|
|
37
|
+
Input array-like object. Common inputs include lists, NumPy arrays, pandas Series,
|
|
38
|
+
or a single-column DataFrame.
|
|
39
|
+
name
|
|
40
|
+
Name used in error messages to make debugging easier.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
numpy.ndarray
|
|
45
|
+
A NumPy array of dtype ``float64``. The returned array may be 1D or 2D depending on
|
|
46
|
+
the input.
|
|
47
|
+
|
|
48
|
+
Raises
|
|
49
|
+
------
|
|
50
|
+
ValueError
|
|
51
|
+
If ``arr`` is scalar (0-dimensional) or contains NaN/infinite values.
|
|
52
|
+
|
|
53
|
+
Notes
|
|
54
|
+
-----
|
|
55
|
+
- If you pass a DataFrame, its underlying NumPy representation is used (i.e., you are
|
|
56
|
+
responsible for selecting appropriate columns before calling this helper).
|
|
57
|
+
- This helper does *not* drop missing values. If you need filtering behavior, perform
|
|
58
|
+
it upstream and call this only once the array should be clean.
|
|
59
|
+
"""
|
|
60
|
+
if isinstance(arr, (pd.Series, pd.DataFrame)):
|
|
61
|
+
arr = arr.to_numpy()
|
|
62
|
+
|
|
63
|
+
out = np.asarray(arr, dtype=float)
|
|
64
|
+
|
|
65
|
+
if out.ndim == 0:
|
|
66
|
+
raise ValueError(f"{name} must be an array-like (not a scalar).")
|
|
67
|
+
|
|
68
|
+
if not np.isfinite(out).all():
|
|
69
|
+
raise ValueError(f"{name} contains NaN or infinite values.")
|
|
70
|
+
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Safe statistical helpers
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
def safe_mean(values: np.ndarray) -> float:
|
|
78
|
+
"""Compute a mean with a defined result for empty inputs.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
values
|
|
83
|
+
Numeric array. Typically a 1D array of values after filtering.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
float
|
|
88
|
+
The arithmetic mean of ``values``. If ``values`` is empty (``values.size == 0``),
|
|
89
|
+
returns ``0.0``.
|
|
90
|
+
|
|
91
|
+
Notes
|
|
92
|
+
-----
|
|
93
|
+
This is mainly used in group-level computations where a group may end up empty after
|
|
94
|
+
filtering invalid rows. Returning ``0.0`` is a pragmatic default for diagnostics; it
|
|
95
|
+
should not be used as a substitute for input validation in the primary metric pathway.
|
|
96
|
+
"""
|
|
97
|
+
values = np.asarray(values, dtype=float)
|
|
98
|
+
if values.size == 0:
|
|
99
|
+
return 0.0
|
|
100
|
+
return float(np.mean(values))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Groupby helpers
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
def groupby_apply_values(
|
|
107
|
+
df: pd.DataFrame,
|
|
108
|
+
group_cols: Union[str, Sequence[str]],
|
|
109
|
+
value_col: str,
|
|
110
|
+
func: Callable[[np.ndarray], float],
|
|
111
|
+
) -> pd.DataFrame:
|
|
112
|
+
"""Apply a numeric reducer to a column, grouped by one or more keys.
|
|
113
|
+
|
|
114
|
+
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.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
df
|
|
121
|
+
Input DataFrame containing grouping keys and the numeric value column.
|
|
122
|
+
group_cols
|
|
123
|
+
Column name or sequence of column names to group by.
|
|
124
|
+
value_col
|
|
125
|
+
Name of the column whose values are passed to ``func``.
|
|
126
|
+
func
|
|
127
|
+
Reducer function taking a 1D NumPy array and returning a scalar (float).
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
pandas.DataFrame
|
|
132
|
+
A tidy DataFrame with columns:
|
|
133
|
+
|
|
134
|
+
- ``group_cols`` (one column per grouping key)
|
|
135
|
+
- ``f"{value_col}_agg"`` (the aggregated scalar result)
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
KeyError
|
|
140
|
+
If ``value_col`` or any ``group_cols`` are missing from ``df``.
|
|
141
|
+
ValueError
|
|
142
|
+
If group values contain NaN/infinite values (via :func:`validate_numeric_array`).
|
|
143
|
+
|
|
144
|
+
Examples
|
|
145
|
+
--------
|
|
146
|
+
>>> out = groupby_apply_values(df, ["cluster", "daypart"], "uplift", np.mean)
|
|
147
|
+
>>> out.columns
|
|
148
|
+
Index(['cluster', 'daypart', 'uplift_agg'], dtype='object')
|
|
149
|
+
"""
|
|
150
|
+
if isinstance(group_cols, str):
|
|
151
|
+
group_cols_seq: list[str] = [group_cols]
|
|
152
|
+
else:
|
|
153
|
+
group_cols_seq = list(group_cols)
|
|
154
|
+
|
|
155
|
+
missing = [c for c in [*group_cols_seq, value_col] if c not in df.columns]
|
|
156
|
+
if missing:
|
|
157
|
+
raise KeyError(f"Missing required columns: {missing}")
|
|
158
|
+
|
|
159
|
+
agg_col = f"{value_col}_agg"
|
|
160
|
+
|
|
161
|
+
grouped = (
|
|
162
|
+
df.groupby(group_cols_seq, dropna=False)[value_col]
|
|
163
|
+
.apply(lambda s: func(validate_numeric_array(s.to_numpy(), name=value_col)))
|
|
164
|
+
.reset_index()
|
|
165
|
+
.rename(columns={value_col: agg_col})
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return grouped
|