Skip to content

Commit 1e3ae5b

Browse files
authored
feat: add forecast bias metric (#122)
1 parent 5dbd994 commit 1e3ae5b

File tree

7 files changed

+143
-15
lines changed

7 files changed

+143
-15
lines changed

nbs/evaluation.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@
228228
" models : list of str, optional (default=None)\n",
229229
" Names of the models to evaluate.\n",
230230
" If `None` will use every column in the dataframe after removing id, time and target.\n",
231-
" train_df :pandas, polars, dask or spark DataFrame, optional (default=None)\n",
231+
" train_df : pandas, polars, dask or spark DataFrame, optional (default=None)\n",
232232
" Training set. Used to evaluate metrics such as `mase`.\n",
233233
" level : list of int, optional (default=None)\n",
234234
" Prediction interval levels. Used to compute losses that rely on quantiles.\n",

nbs/losses.ipynb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,106 @@
574574
")"
575575
]
576576
},
577+
{
578+
"cell_type": "code",
579+
"execution_count": null,
580+
"metadata": {},
581+
"outputs": [],
582+
"source": [
583+
"#| export\n",
584+
"@_base_docstring\n",
585+
"def bias(\n",
586+
" df: DFType,\n",
587+
" models: List[str],\n",
588+
" id_col: str = 'unique_id',\n",
589+
" target_col: str = 'y',\n",
590+
") -> DFType:\n",
591+
" \"\"\"Forecast estimator bias.\n",
592+
" \n",
593+
" Defined as prediction - actual\"\"\"\n",
594+
" if isinstance(df, pd.DataFrame):\n",
595+
" res = (df[models].sub(df[target_col], axis=0)).groupby(df[id_col], observed=True).mean()\n",
596+
" res.index.name = id_col\n",
597+
" res = res.reset_index()\n",
598+
" else:\n",
599+
" def gen_expr(model):\n",
600+
" return pl.col(model).sub(pl.col(target_col)).alias(model)\n",
601+
"\n",
602+
" res = _pl_agg_expr(df, models, id_col, gen_expr)\n",
603+
" return res"
604+
]
605+
},
606+
{
607+
"cell_type": "code",
608+
"execution_count": null,
609+
"metadata": {},
610+
"outputs": [
611+
{
612+
"data": {
613+
"text/markdown": [
614+
"---\n",
615+
"\n",
616+
"#### bias\n",
617+
"\n",
618+
"> bias (df:~DFType, models:List[str], id_col:str='unique_id',\n",
619+
"> target_col:str='y')\n",
620+
"\n",
621+
"*Forecast estimator bias.\n",
622+
"\n",
623+
"Defined as prediction - actual*\n",
624+
"\n",
625+
"| | **Type** | **Default** | **Details** |\n",
626+
"| -- | -------- | ----------- | ----------- |\n",
627+
"| df | DFType | | Input dataframe with id, actual values and predictions. |\n",
628+
"| models | List | | Columns that identify the models predictions. |\n",
629+
"| id_col | str | unique_id | Column that identifies each serie. |\n",
630+
"| target_col | str | y | Column that contains the target. |\n",
631+
"| **Returns** | **DFType** | | **dataframe with one row per id and one column per model.** |"
632+
],
633+
"text/plain": [
634+
"---\n",
635+
"\n",
636+
"#### bias\n",
637+
"\n",
638+
"> bias (df:~DFType, models:List[str], id_col:str='unique_id',\n",
639+
"> target_col:str='y')\n",
640+
"\n",
641+
"*Forecast estimator bias.\n",
642+
"\n",
643+
"Defined as prediction - actual*\n",
644+
"\n",
645+
"| | **Type** | **Default** | **Details** |\n",
646+
"| -- | -------- | ----------- | ----------- |\n",
647+
"| df | DFType | | Input dataframe with id, actual values and predictions. |\n",
648+
"| models | List | | Columns that identify the models predictions. |\n",
649+
"| id_col | str | unique_id | Column that identifies each serie. |\n",
650+
"| target_col | str | y | Column that contains the target. |\n",
651+
"| **Returns** | **DFType** | | **dataframe with one row per id and one column per model.** |"
652+
]
653+
},
654+
"execution_count": null,
655+
"metadata": {},
656+
"output_type": "execute_result"
657+
}
658+
],
659+
"source": [
660+
"show_doc(bias, title_level=4)"
661+
]
662+
},
663+
{
664+
"cell_type": "code",
665+
"execution_count": null,
666+
"metadata": {},
667+
"outputs": [],
668+
"source": [
669+
"#| polars\n",
670+
"pd_vs_pl(\n",
671+
" bias(series, models),\n",
672+
" bias(series_pl, models),\n",
673+
" models,\n",
674+
")"
675+
]
676+
},
577677
{
578678
"cell_type": "markdown",
579679
"metadata": {},

settings.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[DEFAULT]
22
repo = utilsforecast
33
lib_name = utilsforecast
4-
version = 0.2.4
4+
version = 0.2.5
55
min_python = 3.8
66
license = apache2
77
black_formatting = True

utilsforecast/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.4"
1+
__version__ = "0.2.5"

utilsforecast/_modidx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
'utilsforecast.losses': { 'utilsforecast.losses._base_docstring': ('losses.html#_base_docstring', 'utilsforecast/losses.py'),
7272
'utilsforecast.losses._pl_agg_expr': ('losses.html#_pl_agg_expr', 'utilsforecast/losses.py'),
7373
'utilsforecast.losses._zero_to_nan': ('losses.html#_zero_to_nan', 'utilsforecast/losses.py'),
74+
'utilsforecast.losses.bias': ('losses.html#bias', 'utilsforecast/losses.py'),
7475
'utilsforecast.losses.calibration': ('losses.html#calibration', 'utilsforecast/losses.py'),
7576
'utilsforecast.losses.coverage': ('losses.html#coverage', 'utilsforecast/losses.py'),
7677
'utilsforecast.losses.mae': ('losses.html#mae', 'utilsforecast/losses.py'),

utilsforecast/evaluation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def evaluate(
174174
models : list of str, optional (default=None)
175175
Names of the models to evaluate.
176176
If `None` will use every column in the dataframe after removing id, time and target.
177-
train_df :pandas, polars, dask or spark DataFrame, optional (default=None)
177+
train_df : pandas, polars, dask or spark DataFrame, optional (default=None)
178178
Training set. Used to evaluate metrics such as `mase`.
179179
level : list of int, optional (default=None)
180180
Prediction interval levels. Used to compute losses that rely on quantiles.

utilsforecast/losses.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/losses.ipynb.
22

33
# %% auto 0
4-
__all__ = ['mae', 'mse', 'rmse', 'mape', 'smape', 'mase', 'rmae', 'quantile_loss', 'mqloss', 'coverage', 'calibration',
4+
__all__ = ['mae', 'mse', 'rmse', 'bias', 'mape', 'smape', 'mase', 'rmae', 'quantile_loss', 'mqloss', 'coverage', 'calibration',
55
'scaled_crps']
66

77
# %% ../nbs/losses.ipynb 3
@@ -141,15 +141,42 @@ def rmse(
141141
res = res.with_columns(*[pl.col(c).pow(0.5) for c in models])
142142
return res
143143

144-
# %% ../nbs/losses.ipynb 30
144+
# %% ../nbs/losses.ipynb 27
145+
@_base_docstring
146+
def bias(
147+
df: DFType,
148+
models: List[str],
149+
id_col: str = "unique_id",
150+
target_col: str = "y",
151+
) -> DFType:
152+
"""Forecast estimator bias.
153+
154+
Defined as prediction - actual"""
155+
if isinstance(df, pd.DataFrame):
156+
res = (
157+
(df[models].sub(df[target_col], axis=0))
158+
.groupby(df[id_col], observed=True)
159+
.mean()
160+
)
161+
res.index.name = id_col
162+
res = res.reset_index()
163+
else:
164+
165+
def gen_expr(model):
166+
return pl.col(model).sub(pl.col(target_col)).alias(model)
167+
168+
res = _pl_agg_expr(df, models, id_col, gen_expr)
169+
return res
170+
171+
# %% ../nbs/losses.ipynb 33
145172
def _zero_to_nan(series: Union[pd.Series, "pl.Expr"]) -> Union[pd.Series, "pl.Expr"]:
146173
if isinstance(series, pd.Series):
147174
res = series.replace(0, np.nan)
148175
else:
149176
res = pl.when(series == 0).then(float("nan")).otherwise(series.abs())
150177
return res
151178

152-
# %% ../nbs/losses.ipynb 31
179+
# %% ../nbs/losses.ipynb 34
153180
@_base_docstring
154181
def mape(
155182
df: DFType,
@@ -187,7 +214,7 @@ def gen_expr(model):
187214
res = _pl_agg_expr(df, models, id_col, gen_expr)
188215
return res
189216

190-
# %% ../nbs/losses.ipynb 35
217+
# %% ../nbs/losses.ipynb 38
191218
@_base_docstring
192219
def smape(
193220
df: DFType,
@@ -225,7 +252,7 @@ def gen_expr(model):
225252
res = _pl_agg_expr(df, models, id_col, gen_expr)
226253
return res
227254

228-
# %% ../nbs/losses.ipynb 41
255+
# %% ../nbs/losses.ipynb 44
229256
def mase(
230257
df: DFType,
231258
models: List[str],
@@ -293,7 +320,7 @@ def gen_expr(model):
293320
res = _pl_agg_expr(full_df, models, id_col, gen_expr)
294321
return res
295322

296-
# %% ../nbs/losses.ipynb 46
323+
# %% ../nbs/losses.ipynb 49
297324
def rmae(
298325
df: DFType,
299326
models: List[str],
@@ -347,7 +374,7 @@ def gen_expr(model, baseline) -> pl_Expr:
347374
res = res.select([id_col, *exprs])
348375
return res
349376

350-
# %% ../nbs/losses.ipynb 52
377+
# %% ../nbs/losses.ipynb 55
351378
def quantile_loss(
352379
df: DFType,
353380
models: Dict[str, str],
@@ -409,7 +436,7 @@ def gen_expr(model):
409436
res = _pl_agg_expr(df, list(models.items()), id_col, gen_expr)
410437
return res
411438

412-
# %% ../nbs/losses.ipynb 58
439+
# %% ../nbs/losses.ipynb 61
413440
def mqloss(
414441
df: DFType,
415442
models: Dict[str, List[str]],
@@ -468,7 +495,7 @@ def mqloss(
468495
res = ufp.assign_columns(res, model, model_res[model])
469496
return res
470497

471-
# %% ../nbs/losses.ipynb 64
498+
# %% ../nbs/losses.ipynb 67
472499
def coverage(
473500
df: DFType,
474501
models: List[str],
@@ -527,7 +554,7 @@ def gen_expr(model):
527554
res = _pl_agg_expr(df, models, id_col, gen_expr)
528555
return res
529556

530-
# %% ../nbs/losses.ipynb 68
557+
# %% ../nbs/losses.ipynb 71
531558
def calibration(
532559
df: DFType,
533560
models: Dict[str, str],
@@ -577,7 +604,7 @@ def gen_expr(model):
577604
res = _pl_agg_expr(df, list(models.items()), id_col, gen_expr)
578605
return res
579606

580-
# %% ../nbs/losses.ipynb 72
607+
# %% ../nbs/losses.ipynb 75
581608
def scaled_crps(
582609
df: DFType,
583610
models: Dict[str, List[str]],

0 commit comments

Comments
 (0)