Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] Uncertainty streamline naming #1262

Merged
merged 23 commits into from
Apr 29, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 80 additions & 10 deletions docs/source/new-tutorials/tutorial08.ipynb

Large diffs are not rendered by default.

52 changes: 50 additions & 2 deletions neuralprophet/forecaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from neuralprophet.data.transform import _normalize
from neuralprophet.logger import MetricsLogger
from neuralprophet.plot_forecast_matplotlib import plot, plot_components
from neuralprophet.plot_forecast_plotly import conformal_plot_plotly
from neuralprophet.plot_forecast_plotly import plot as plot_plotly
from neuralprophet.plot_forecast_plotly import plot_components as plot_components_plotly
from neuralprophet.plot_model_parameters_matplotlib import plot_parameters
Expand Down Expand Up @@ -2819,6 +2820,7 @@ def conformal_predict(
alpha: Union[float, Tuple[float, float]],
method: str = "naive",
plotting_backend: Optional[str] = None,
show_all_PI: bool = False,
**kwargs,
) -> pd.DataFrame:
"""Apply a given conformal prediction technique to get the uncertainty prediction intervals (or q-hats). Then predict.
Expand Down Expand Up @@ -2851,6 +2853,8 @@ def conformal_predict(
* (default) None: Plotting backend ist set automatically. Use plotly with resampling for jupyterlab
notebooks and vscode notebooks. Automatically switch to plotly without resampling for all other
environments.
show_all_PI : bool
whether to return all prediction intervals (including quantile regression and conformal prediction)
kwargs : dict
additional predict parameters for test df

Expand All @@ -2863,17 +2867,61 @@ def conformal_predict(
df_cal = self.predict(calibration_df)
# get predictions for test dataframe
df_test = self.predict(df, **kwargs)

# initiate Conformal instance
c = Conformal(
alpha=alpha,
method=method,
n_forecasts=self.n_forecasts,
quantiles=self.config_train.quantiles,
leoniewgnr marked this conversation as resolved.
Show resolved Hide resolved
)
# call Conformal's predict to output test df with conformal prediction intervals
df_forecast = c.predict(df=df_test, df_cal=df_cal)

df_forecast = c.predict(df=df_test, df_cal=df_cal, show_all_PI=show_all_PI)

# plot one-sided prediction interval width with q
if plotting_backend:
c.plot(plotting_backend)

return df_forecast

def conformal_plot(
self,
df: pd.DataFrame,
n_highlight: Optional[int] = 1,
plotting_backend: Optional[str] = None,
):
"""Plot conformal prediction intervals and quantile regression intervals.

Parameters
----------
df : pd.DataFrame
conformal forecast dataframe when ``show_all_PI`` is set to True
n_highlight : Optional
i-th step ahead forecast to use for statistics and plotting.
"""
if not any("qr_" in col for col in df.columns):
raise ValueError(
"Conformal prediction intervals not found. Please set `show_all_PI` to True when calling `conformal_predict`."
)

# quantile regression dataframe
cols = list(df.columns[: 2 + self.n_forecasts])

qr_cols = [col for col in df.columns if "qr_" in col]
df_qr = df[cols + qr_cols]
df_qr.columns = [col.replace("qr_", "") for col in df_qr.columns]

plotting_backend = select_plotting_backend(model=self, plotting_backend=plotting_backend)
fig = self.highlight_nth_step_ahead_of_each_forecast(n_highlight).plot(df_qr, plotting_backend=plotting_backend)

# get conformal prediction intervals
cp_cols = [col for col in df.columns if "%" in col and f"yhat{n_highlight}" in col and "qr" not in col]

if plotting_backend.startswith("plotly"):
return conformal_plot_plotly(fig, df.loc[:, ["ds", cp_cols[0]]], df.loc[:, ["ds", cp_cols[1]]])
else:
log.warning(
DeprecationWarning(
"Matplotlib plotting backend is deprecated and will be removed in a future release. Please use the plotly backend instead."
)
)
44 changes: 13 additions & 31 deletions neuralprophet/plot_forecast_matplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,11 @@ def plot(
if line_per_origin:
colname = "origin-"
step = 0
# all yhat column names, including quantiles
yhat_col_names = [col_name for col_name in fcst.columns if f"{colname}" in col_name]
# without quants
yhat_col_names_no_qts = [
col_name for col_name in yhat_col_names if f"{colname}" in col_name and "%" not in col_name
]
# all yhat column names without quantiles
yhat_col_names = [col_name for col_name in fcst.columns if col_name.startswith(colname) and "%" not in col_name]

if highlight_forecast is None or line_per_origin:
for i, name in enumerate(yhat_col_names_no_qts):
for i, name in enumerate(yhat_col_names):
ax.plot(
ds,
fcst[f"{colname}{i if line_per_origin else i + 1}"],
Expand All @@ -106,21 +102,21 @@ def plot(
label=name,
)

if len(quantiles) > 1:
for i in range(1, len(quantiles)):
ax.fill_between(
ds,
fcst[f"{colname}{step}"],
fcst[f"{colname}{step} {round(quantiles[i] * 100, 1)}%"],
color="#0072B2",
alpha=0.2,
)
if len(quantiles) > 1:
for i in range(1, len(quantiles)):
ax.fill_between(
ds,
fcst[f"{colname}{step}"],
fcst[f"{colname}{step} {round(quantiles[i] * 100, 1)}%"],
color="#0072B2",
alpha=0.2,
)

if highlight_forecast is not None:
if line_per_origin:
num_forecast_steps = sum(fcst["origin-0"].notna())
steps_from_last = num_forecast_steps - highlight_forecast
for i in range(len(yhat_col_names_no_qts)):
for i in range(len(yhat_col_names)):
x = ds[-(1 + i + steps_from_last)]
y = fcst[f"origin-{i}"].values[-(1 + i + steps_from_last)]
ax.plot(x, y, "bx")
Expand All @@ -138,20 +134,6 @@ def plot(
alpha=0.2,
)

# Plot any conformal prediction intervals
if any("+ qhat" in col for col in yhat_col_names) and any("- qhat" in col for col in yhat_col_names):
quantile_hi = str(max(quantiles) * 100)
quantile_lo = str(min(quantiles) * 100)
if f"yhat1 {quantile_hi}% + qhat_hi1" in fcst.columns and f"yhat1 {quantile_lo}% - qhat_lo1" in fcst.columns:
ax.plot(ds, fcst[f"yhat1 {quantile_hi}% + qhat_hi1"], c="r", label=f"yhat1 {quantile_hi}% + qhat_hi1")
ax.plot(ds, fcst[f"yhat1 {quantile_lo}% - qhat_lo1"], c="r", label=f"yhat1 {quantile_lo}% - qhat_lo1")
elif f"yhat1 {quantile_hi}% + qhat1" in fcst.columns and f"yhat1 {quantile_hi}% - qhat1" in fcst.columns:
ax.plot(ds, fcst[f"yhat1 {quantile_hi}% + qhat1"], c="r", label=f"yhat1 {quantile_hi}% + qhat1")
ax.plot(ds, fcst[f"yhat1 {quantile_lo}% - qhat1"], c="r", label=f"yhat1 {quantile_lo}% - qhat1")
else:
ax.plot(ds, fcst["yhat1 + qhat1"], c="r", label="yhat1 + qhat1")
ax.plot(ds, fcst["yhat1 - qhat1"], c="r", label="yhat1 - qhat1")

leoniewgnr marked this conversation as resolved.
Show resolved Hide resolved
ax.plot(ds, fcst["y"], "k.", label="actual y")

# Specify formatting to workaround matplotlib issue #12925
Expand Down
39 changes: 31 additions & 8 deletions neuralprophet/plot_forecast_plotly.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,12 @@ def plot(
if line_per_origin:
colname = "origin-"
step = 0
# all yhat column names, including quantiles
yhat_col_names = [col_name for col_name in fcst.columns if f"{colname}" in col_name]
# without quants
yhat_col_names_no_qts = [
col_name for col_name in yhat_col_names if f"{colname}" in col_name and "%" not in col_name
]
# all yhat column names
yhat_col_names = [col_name for col_name in fcst.columns if col_name.startswith(colname) and "%" not in col_name]
data = []

if highlight_forecast is None or line_per_origin:
for i, yhat_col_name in enumerate(yhat_col_names_no_qts):
for i, yhat_col_name in enumerate(yhat_col_names):
data.append(
go.Scatter(
name=yhat_col_name,
Expand Down Expand Up @@ -150,7 +146,7 @@ def plot(
if line_per_origin:
num_forecast_steps = sum(fcst["origin-0"].notna())
steps_from_last = num_forecast_steps - highlight_forecast
for i, yhat_col_name in enumerate(yhat_col_names_no_qts):
for i, yhat_col_name in enumerate(yhat_col_names):
x = [ds[-(1 + i + steps_from_last)]]
y = [fcst[f"origin-{i}"].values[-(1 + i + steps_from_last)]]
data.append(
Expand Down Expand Up @@ -878,3 +874,30 @@ def plot_interval_width_per_timestep(q_hats, method, resampler_active=False):
fig.update_layout(margin=dict(l=70, r=70, t=60, b=50))
unregister_plotly_resampler()
return fig


def conformal_plot_plotly(fig, df_cp_lo, df_cp_hi):
"""Plot conformal prediction intervals and quantile regression intervals in one plot

Parameters
----------
fig : plotly.graph_objects.Figure
Figure showing the quantile regression intervals
df_cp_lo : dataframe
dataframe containing the lower bound of the conformal prediction intervals
df_cp_hi : dataframe
dataframe containing the upper bound of the conformal prediction intervals
"""
col_lo = df_cp_lo.columns
trace_cp_lo = go.Scatter(
name=f"cp_{col_lo[1]}", x=df_cp_lo["ds"], y=df_cp_lo[col_lo[1]], mode="lines", line=dict(color="red")
)

col_hi = df_cp_hi.columns
trace_cp_hi = go.Scatter(
name=f"cp_{col_hi[1]}", x=df_cp_hi["ds"], y=df_cp_hi[col_hi[1]], mode="lines", line=dict(color="red")
)

fig.add_trace(trace_cp_lo)
fig.add_trace(trace_cp_hi)
return fig
Loading