eb-optimization 0.1.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.
- eb_optimization/__init__.py +38 -0
- eb_optimization/_utils.py +122 -0
- eb_optimization/policies/__init__.py +74 -0
- eb_optimization/policies/cost_ratio_policy.py +294 -0
- eb_optimization/policies/ral_policy.py +129 -0
- eb_optimization/policies/tau_policy.py +156 -0
- eb_optimization/search/__init__.py +27 -0
- eb_optimization/search/grid.py +91 -0
- eb_optimization/search/kernels.py +115 -0
- eb_optimization/search/results.py +0 -0
- eb_optimization/tuning/__init__.py +27 -0
- eb_optimization/tuning/cost_ratio.py +270 -0
- eb_optimization/tuning/ral.py +144 -0
- eb_optimization/tuning/sensitivity.py +319 -0
- eb_optimization/tuning/tau.py +510 -0
- eb_optimization-0.1.0.dist-info/METADATA +117 -0
- eb_optimization-0.1.0.dist-info/RECORD +20 -0
- eb_optimization-0.1.0.dist-info/WHEEL +5 -0
- eb_optimization-0.1.0.dist-info/licenses/LICENSE +28 -0
- eb_optimization-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Offline tuning for the Readiness Adjustment Layer (RAL).
|
|
5
|
+
|
|
6
|
+
This module contains optimization logic for selecting RAL policy parameters
|
|
7
|
+
by minimizing Electric Barometer objectives (primarily Cost-Weighted Service
|
|
8
|
+
Loss) over historical data.
|
|
9
|
+
|
|
10
|
+
Responsibilities:
|
|
11
|
+
- Search bounded uplift grids to select optimal RAL parameters
|
|
12
|
+
- Produce portable RALPolicy artifacts
|
|
13
|
+
- Emit audit-ready diagnostics for governance and analysis
|
|
14
|
+
|
|
15
|
+
Non-responsibilities:
|
|
16
|
+
- Applying policies to forecasts
|
|
17
|
+
- Defining metric math (delegated to `eb-metrics`)
|
|
18
|
+
- Production-time inference or real-time decisioning
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import List, Dict, Optional, Sequence, Tuple
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
from eb_metrics.metrics import cwsl, frs, nsl
|
|
25
|
+
from eb_optimization.policies.ral_policy import RALPolicy
|
|
26
|
+
from eb_optimization.search.grid import make_float_grid
|
|
27
|
+
|
|
28
|
+
def tune_ral_policy(
|
|
29
|
+
df: pd.DataFrame,
|
|
30
|
+
*,
|
|
31
|
+
forecast_col: str,
|
|
32
|
+
actual_col: str,
|
|
33
|
+
cu: float = 2.0,
|
|
34
|
+
co: float = 1.0,
|
|
35
|
+
uplift_min: float = 1.0,
|
|
36
|
+
uplift_max: float = 1.15,
|
|
37
|
+
grid_step: float = 0.01,
|
|
38
|
+
segment_cols: Optional[Sequence[str]] = None,
|
|
39
|
+
sample_weight_col: Optional[str] = None,
|
|
40
|
+
) -> Tuple[RALPolicy, pd.DataFrame]:
|
|
41
|
+
"""Tune a Readiness Adjustment Layer (RAL) policy via discrete grid search.
|
|
42
|
+
|
|
43
|
+
This function performs *offline* tuning to select multiplicative uplift factors
|
|
44
|
+
that convert a baseline forecast into an operationally conservative readiness forecast.
|
|
45
|
+
|
|
46
|
+
The optimization objective is **Cost-Weighted Service Loss (CWSL)**.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Ensure necessary columns are present
|
|
50
|
+
if forecast_col not in df.columns or actual_col not in df.columns:
|
|
51
|
+
raise ValueError("Required columns missing in the DataFrame.")
|
|
52
|
+
|
|
53
|
+
# Initialize grid for candidate ratios
|
|
54
|
+
uplift_grid = make_float_grid(uplift_min, uplift_max, grid_step)
|
|
55
|
+
|
|
56
|
+
# Prepare the DataFrame for tuning
|
|
57
|
+
y_true_all = df[actual_col].to_numpy(dtype=float)
|
|
58
|
+
y_pred_all = df[forecast_col].to_numpy(dtype=float)
|
|
59
|
+
|
|
60
|
+
if sample_weight_col is not None:
|
|
61
|
+
sample_weights = df[sample_weight_col].to_numpy(dtype=float)
|
|
62
|
+
else:
|
|
63
|
+
sample_weights = np.ones_like(y_true_all)
|
|
64
|
+
|
|
65
|
+
# Initialize variables for best uplift and diagnostics
|
|
66
|
+
best_uplift = None
|
|
67
|
+
best_cu = None
|
|
68
|
+
best_over_cost = None
|
|
69
|
+
best_under_cost = None
|
|
70
|
+
best_diff = None
|
|
71
|
+
|
|
72
|
+
diagnostics = []
|
|
73
|
+
|
|
74
|
+
# Tune the policy globally (and by segment if needed)
|
|
75
|
+
if segment_cols:
|
|
76
|
+
grouped = df.groupby(segment_cols)
|
|
77
|
+
else:
|
|
78
|
+
grouped = [('', df)] # No segmentation, use the entire DataFrame
|
|
79
|
+
|
|
80
|
+
for segment_key, group in grouped:
|
|
81
|
+
y_true = group[actual_col].to_numpy(dtype=float)
|
|
82
|
+
y_pred = group[forecast_col].to_numpy(dtype=float)
|
|
83
|
+
weights = sample_weights[:len(y_true)]
|
|
84
|
+
|
|
85
|
+
# Calculate shortfall and overbuild
|
|
86
|
+
shortfall = np.maximum(0, y_true - y_pred)
|
|
87
|
+
overbuild = np.maximum(0, y_pred - y_true)
|
|
88
|
+
|
|
89
|
+
# Perform grid search to find the best uplift for the segment
|
|
90
|
+
best_segment_uplift, best_segment_costs = _find_best_uplift(
|
|
91
|
+
uplift_grid, shortfall, overbuild, weights, cu, co
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Store diagnostics
|
|
95
|
+
diagnostics.append({
|
|
96
|
+
"segment": segment_key,
|
|
97
|
+
"uplift": best_segment_uplift,
|
|
98
|
+
"under_cost": best_segment_costs['under_cost'],
|
|
99
|
+
"over_cost": best_segment_costs['over_cost'],
|
|
100
|
+
"diff": best_segment_costs['diff'],
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
# Track the overall best uplift
|
|
104
|
+
if best_diff is None or best_segment_costs['diff'] < best_diff:
|
|
105
|
+
best_diff = best_segment_costs['diff']
|
|
106
|
+
best_uplift = best_segment_uplift
|
|
107
|
+
best_cu = best_segment_costs['under_cost']
|
|
108
|
+
best_over_cost = best_segment_costs['over_cost']
|
|
109
|
+
|
|
110
|
+
# Create the RALPolicy object
|
|
111
|
+
policy = RALPolicy(
|
|
112
|
+
global_uplift=best_uplift,
|
|
113
|
+
uplift_table=pd.DataFrame(diagnostics),
|
|
114
|
+
segment_cols=segment_cols or [],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Return the policy and diagnostics
|
|
118
|
+
return policy, pd.DataFrame(diagnostics)
|
|
119
|
+
|
|
120
|
+
def _find_best_uplift(uplift_grid, shortfall, overbuild, weights, cu, co):
|
|
121
|
+
"""Helper function to find the best uplift for a single segment."""
|
|
122
|
+
best_uplift = None
|
|
123
|
+
best_cost = None
|
|
124
|
+
best_under_cost = None
|
|
125
|
+
best_over_cost = None
|
|
126
|
+
|
|
127
|
+
for uplift in uplift_grid:
|
|
128
|
+
# Calculate underbuild and overbuild costs for this uplift
|
|
129
|
+
cu_val = uplift * cu
|
|
130
|
+
under_cost = np.sum(weights * cu_val * shortfall)
|
|
131
|
+
over_cost = np.sum(weights * co * overbuild)
|
|
132
|
+
diff = abs(under_cost - over_cost)
|
|
133
|
+
|
|
134
|
+
if best_cost is None or diff < best_cost:
|
|
135
|
+
best_cost = diff
|
|
136
|
+
best_uplift = uplift
|
|
137
|
+
best_under_cost = under_cost
|
|
138
|
+
best_over_cost = over_cost
|
|
139
|
+
|
|
140
|
+
return best_uplift, {
|
|
141
|
+
"under_cost": best_under_cost,
|
|
142
|
+
"over_cost": best_over_cost,
|
|
143
|
+
"diff": best_cost,
|
|
144
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
r"""
|
|
4
|
+
CWSL cost-ratio sensitivity utilities.
|
|
5
|
+
|
|
6
|
+
This module provides helpers for computing a *sensitivity curve* of
|
|
7
|
+
Cost-Weighted Service Loss (CWSL) across a grid of cost ratios:
|
|
8
|
+
|
|
9
|
+
$$
|
|
10
|
+
R = \frac{c_u}{c_o}
|
|
11
|
+
$$
|
|
12
|
+
|
|
13
|
+
Given an overbuild cost coefficient $c_o$ and ratio $R$, the implied underbuild cost is:
|
|
14
|
+
|
|
15
|
+
$$
|
|
16
|
+
c_u = R \cdot c_o
|
|
17
|
+
$$
|
|
18
|
+
|
|
19
|
+
Why this lives in eb-optimization
|
|
20
|
+
--------------------------------
|
|
21
|
+
Computing a metric across a candidate grid of hyperparameters (like a cost ratio R)
|
|
22
|
+
is an *analysis / calibration workflow* rather than a metric primitive.
|
|
23
|
+
|
|
24
|
+
- **eb-metrics** remains the source of truth for *metric math* (e.g., ``cwsl``).
|
|
25
|
+
- **eb-optimization** owns grid-based evaluation, diagnostics, and tuning utilities.
|
|
26
|
+
|
|
27
|
+
This module therefore contains:
|
|
28
|
+
- ``cwsl_sensitivity``: array-level sweep (grid evaluation)
|
|
29
|
+
- ``compute_cwsl_sensitivity_df``: DataFrame-oriented wrapper (tidy long-form output)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
import pandas as pd
|
|
36
|
+
|
|
37
|
+
from eb_metrics.metrics.loss import cwsl
|
|
38
|
+
|
|
39
|
+
__all__ = ["cwsl_sensitivity", "compute_cwsl_sensitivity_df"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _as_1d_float_array(x: Sequence[float] | np.ndarray | Iterable[float]) -> np.ndarray:
|
|
43
|
+
"""Convert input to a 1D float NumPy array."""
|
|
44
|
+
return np.asarray(list(x), dtype=float).reshape(-1)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalize_R_list(R_list: Sequence[float] | np.ndarray | Iterable[float]) -> np.ndarray:
|
|
48
|
+
"""
|
|
49
|
+
Normalize and validate a candidate R grid.
|
|
50
|
+
|
|
51
|
+
Backward-compatible behavior:
|
|
52
|
+
- Non-finite values are dropped.
|
|
53
|
+
- Non-positive values (R <= 0) are dropped.
|
|
54
|
+
- If no valid values remain, raises ValueError.
|
|
55
|
+
- De-duplicates and sorts for stable outputs.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
R_list
|
|
60
|
+
Candidate ratios to evaluate.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
numpy.ndarray
|
|
65
|
+
1D array of finite, strictly positive ratios.
|
|
66
|
+
|
|
67
|
+
Raises
|
|
68
|
+
------
|
|
69
|
+
ValueError
|
|
70
|
+
If the candidate list is empty or contains no valid ratios after filtering.
|
|
71
|
+
"""
|
|
72
|
+
R_arr = _as_1d_float_array(R_list)
|
|
73
|
+
if R_arr.ndim != 1 or R_arr.size == 0:
|
|
74
|
+
raise ValueError("R_list must be a non-empty 1D sequence of floats.")
|
|
75
|
+
|
|
76
|
+
R_arr = R_arr[np.isfinite(R_arr)]
|
|
77
|
+
R_arr = R_arr[R_arr > 0]
|
|
78
|
+
|
|
79
|
+
if R_arr.size == 0:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"R_list contains no valid ratios after filtering. Provide at least one R > 0."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return np.unique(R_arr)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cwsl_sensitivity(
|
|
88
|
+
y_true: np.ndarray | Sequence[float],
|
|
89
|
+
y_pred: np.ndarray | Sequence[float],
|
|
90
|
+
*,
|
|
91
|
+
R_list: Sequence[float] | np.ndarray | Iterable[float] = (0.5, 1.0, 2.0, 3.0),
|
|
92
|
+
co: Union[float, np.ndarray] = 1.0,
|
|
93
|
+
sample_weight: Optional[np.ndarray | Sequence[float]] = None,
|
|
94
|
+
) -> Dict[float, float]:
|
|
95
|
+
r"""
|
|
96
|
+
Evaluate CWSL across a grid of cost ratios (cost sensitivity analysis).
|
|
97
|
+
|
|
98
|
+
For each candidate ratio:
|
|
99
|
+
|
|
100
|
+
$$ R = \frac{c_u}{c_o} $$
|
|
101
|
+
|
|
102
|
+
holding ``co`` fixed and setting:
|
|
103
|
+
|
|
104
|
+
$$ c_u = R \cdot c_o $$
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
y_true
|
|
109
|
+
Realized demand values (non-negative).
|
|
110
|
+
y_pred
|
|
111
|
+
Forecast values (non-negative).
|
|
112
|
+
R_list
|
|
113
|
+
Candidate cost ratios to evaluate. Non-finite and non-positive values are ignored.
|
|
114
|
+
co
|
|
115
|
+
Overbuild cost coefficient. Can be scalar or per-interval array.
|
|
116
|
+
sample_weight
|
|
117
|
+
Optional non-negative weights per interval.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
dict[float, float]
|
|
122
|
+
Mapping ``{R: cwsl_value}`` for each valid ``R``.
|
|
123
|
+
|
|
124
|
+
Raises
|
|
125
|
+
------
|
|
126
|
+
ValueError
|
|
127
|
+
If no valid ratios remain after filtering, if inputs are invalid, or if
|
|
128
|
+
sample_weight contains negatives.
|
|
129
|
+
"""
|
|
130
|
+
y_true_arr = np.asarray(y_true, dtype=float).reshape(-1)
|
|
131
|
+
y_pred_arr = np.asarray(y_pred, dtype=float).reshape(-1)
|
|
132
|
+
|
|
133
|
+
if y_true_arr.ndim != 1 or y_pred_arr.ndim != 1:
|
|
134
|
+
raise ValueError("y_true and y_pred must be 1D arrays.")
|
|
135
|
+
if y_true_arr.shape != y_pred_arr.shape:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
"y_true and y_pred must have the same shape; "
|
|
138
|
+
f"got {y_true_arr.shape} and {y_pred_arr.shape}"
|
|
139
|
+
)
|
|
140
|
+
if np.any(y_true_arr < 0) or np.any(y_pred_arr < 0):
|
|
141
|
+
raise ValueError("y_true and y_pred must be non-negative.")
|
|
142
|
+
|
|
143
|
+
if sample_weight is not None:
|
|
144
|
+
w = np.asarray(sample_weight, dtype=float).reshape(-1)
|
|
145
|
+
if w.shape != y_true_arr.shape:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"sample_weight must have shape {y_true_arr.shape}; got {w.shape}"
|
|
148
|
+
)
|
|
149
|
+
if np.any(w < 0):
|
|
150
|
+
raise ValueError("sample_weight must be non-negative.")
|
|
151
|
+
else:
|
|
152
|
+
w = None
|
|
153
|
+
|
|
154
|
+
# normalize candidate grid
|
|
155
|
+
R_arr = _normalize_R_list(R_list)
|
|
156
|
+
|
|
157
|
+
# validate co
|
|
158
|
+
if isinstance(co, np.ndarray):
|
|
159
|
+
co_arr = np.asarray(co, dtype=float).reshape(-1)
|
|
160
|
+
if co_arr.shape != y_true_arr.shape:
|
|
161
|
+
raise ValueError(f"co must have shape {y_true_arr.shape}; got {co_arr.shape}")
|
|
162
|
+
if np.any(co_arr <= 0):
|
|
163
|
+
raise ValueError("co must be strictly positive.")
|
|
164
|
+
co_val: Union[float, np.ndarray] = co_arr
|
|
165
|
+
else:
|
|
166
|
+
co_float = float(co)
|
|
167
|
+
if co_float <= 0:
|
|
168
|
+
raise ValueError("co must be strictly positive.")
|
|
169
|
+
co_val = co_float
|
|
170
|
+
|
|
171
|
+
results: Dict[float, float] = {}
|
|
172
|
+
|
|
173
|
+
for R in R_arr:
|
|
174
|
+
# cu = R * co (supports scalar or per-interval array)
|
|
175
|
+
cu_val = float(R) * co_val # type: ignore[operator]
|
|
176
|
+
|
|
177
|
+
value = cwsl(
|
|
178
|
+
y_true=y_true_arr,
|
|
179
|
+
y_pred=y_pred_arr,
|
|
180
|
+
cu=cu_val,
|
|
181
|
+
co=co_val,
|
|
182
|
+
sample_weight=w,
|
|
183
|
+
)
|
|
184
|
+
results[float(R)] = float(value)
|
|
185
|
+
|
|
186
|
+
# R_arr is guaranteed non-empty; results should be non-empty
|
|
187
|
+
return results
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def compute_cwsl_sensitivity_df(
|
|
191
|
+
df: pd.DataFrame,
|
|
192
|
+
*,
|
|
193
|
+
actual_col: str = "actual_qty",
|
|
194
|
+
forecast_col: str = "forecast_qty",
|
|
195
|
+
R_list: Sequence[float] = (0.5, 1.0, 2.0, 3.0),
|
|
196
|
+
co: Union[float, str] = 1.0,
|
|
197
|
+
group_cols: Optional[Sequence[str]] = None,
|
|
198
|
+
sample_weight_col: Optional[str] = None,
|
|
199
|
+
) -> pd.DataFrame:
|
|
200
|
+
r"""
|
|
201
|
+
Compute CWSL sensitivity curves from a DataFrame.
|
|
202
|
+
|
|
203
|
+
Evaluates CWSL over a grid of cost ratios:
|
|
204
|
+
|
|
205
|
+
$$ R = \frac{c_u}{c_o} $$
|
|
206
|
+
|
|
207
|
+
For each ratio value $R$ in ``R_list``, the implied underbuild cost is:
|
|
208
|
+
|
|
209
|
+
$$ c_u = R \cdot c_o $$
|
|
210
|
+
|
|
211
|
+
where ``co`` may be a scalar (global) or a per-row column name.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
df
|
|
216
|
+
Input data containing actuals, forecasts, optional groups, and optional weights.
|
|
217
|
+
actual_col
|
|
218
|
+
Column containing realized demand values.
|
|
219
|
+
forecast_col
|
|
220
|
+
Column containing forecast values.
|
|
221
|
+
R_list
|
|
222
|
+
Candidate ratios to evaluate. Non-finite and non-positive values are ignored.
|
|
223
|
+
co
|
|
224
|
+
Overbuild cost specification:
|
|
225
|
+
- If ``float``: constant $c_o$ applied to all rows and groups.
|
|
226
|
+
- If ``str``: name of a column in ``df`` containing per-row $c_o(i)$ values.
|
|
227
|
+
group_cols
|
|
228
|
+
Optional grouping columns. If ``None`` or empty, the entire DataFrame is treated
|
|
229
|
+
as a single group.
|
|
230
|
+
sample_weight_col
|
|
231
|
+
Optional column name containing non-negative sample weights per row.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
pandas.DataFrame
|
|
236
|
+
Long-form table of sensitivity results with columns:
|
|
237
|
+
- if not grouped: ``["R", "CWSL"]``
|
|
238
|
+
- if grouped: ``group_cols + ["R", "CWSL"]``
|
|
239
|
+
|
|
240
|
+
Raises
|
|
241
|
+
------
|
|
242
|
+
KeyError
|
|
243
|
+
If required columns are missing from ``df``.
|
|
244
|
+
ValueError
|
|
245
|
+
If no valid ratios remain after filtering, or if sample weights are negative.
|
|
246
|
+
"""
|
|
247
|
+
gcols = [] if group_cols is None else list(group_cols)
|
|
248
|
+
|
|
249
|
+
# ---- validation: columns ----
|
|
250
|
+
required_cols: list[str] = [actual_col, forecast_col]
|
|
251
|
+
if isinstance(co, str):
|
|
252
|
+
required_cols.append(co)
|
|
253
|
+
if sample_weight_col is not None:
|
|
254
|
+
required_cols.append(sample_weight_col)
|
|
255
|
+
if gcols:
|
|
256
|
+
required_cols.extend(gcols)
|
|
257
|
+
|
|
258
|
+
missing = [c for c in required_cols if c not in df.columns]
|
|
259
|
+
if missing:
|
|
260
|
+
raise KeyError(f"Missing required columns in df: {missing}")
|
|
261
|
+
|
|
262
|
+
# ---- validation: R grid (for early failure and stable ordering) ----
|
|
263
|
+
R_arr = _normalize_R_list(R_list)
|
|
264
|
+
|
|
265
|
+
# ---- validation: weights ----
|
|
266
|
+
if sample_weight_col is not None:
|
|
267
|
+
w_all = df[sample_weight_col].to_numpy(dtype=float)
|
|
268
|
+
if np.any(w_all < 0):
|
|
269
|
+
raise ValueError("sample weights must be non-negative.")
|
|
270
|
+
|
|
271
|
+
# ---- compute ----
|
|
272
|
+
results: List[Dict[str, Any]] = []
|
|
273
|
+
|
|
274
|
+
if len(gcols) == 0:
|
|
275
|
+
iter_groups = [((None,), df)]
|
|
276
|
+
else:
|
|
277
|
+
iter_groups = df.groupby(gcols, dropna=False, sort=False)
|
|
278
|
+
|
|
279
|
+
for keys, g in iter_groups:
|
|
280
|
+
if not isinstance(keys, tuple):
|
|
281
|
+
keys = (keys,)
|
|
282
|
+
|
|
283
|
+
y_true = g[actual_col].to_numpy(dtype=float)
|
|
284
|
+
y_pred = g[forecast_col].to_numpy(dtype=float)
|
|
285
|
+
|
|
286
|
+
co_value: Union[float, np.ndarray]
|
|
287
|
+
if isinstance(co, str):
|
|
288
|
+
co_value = g[co].to_numpy(dtype=float)
|
|
289
|
+
else:
|
|
290
|
+
co_value = float(co)
|
|
291
|
+
|
|
292
|
+
sample_weight = (
|
|
293
|
+
g[sample_weight_col].to_numpy(dtype=float)
|
|
294
|
+
if sample_weight_col is not None
|
|
295
|
+
else None
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
sensitivity_map = cwsl_sensitivity(
|
|
299
|
+
y_true=y_true,
|
|
300
|
+
y_pred=y_pred,
|
|
301
|
+
R_list=R_arr,
|
|
302
|
+
co=co_value,
|
|
303
|
+
sample_weight=sample_weight,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
for R_val, cwsl_val in sensitivity_map.items():
|
|
307
|
+
row: Dict[str, Any] = {"R": float(R_val), "CWSL": float(cwsl_val)}
|
|
308
|
+
for col, value in zip(gcols, keys):
|
|
309
|
+
row[col] = value
|
|
310
|
+
results.append(row)
|
|
311
|
+
|
|
312
|
+
result_df = pd.DataFrame(results)
|
|
313
|
+
|
|
314
|
+
if len(gcols) > 0:
|
|
315
|
+
result_df = result_df[gcols + ["R", "CWSL"]]
|
|
316
|
+
else:
|
|
317
|
+
result_df = result_df[["R", "CWSL"]]
|
|
318
|
+
|
|
319
|
+
return result_df
|