From e5049309c455cbc0d35b53eab0cd97c9a8694358 Mon Sep 17 00:00:00 2001 From: he weilin Date: Fri, 27 Sep 2024 11:18:09 +0800 Subject: [PATCH 01/14] Add new feature to plot each series's component separately --- darts/ad/anomaly_model/anomaly_model.py | 2 + darts/ad/anomaly_model/forecasting_am.py | 2 + darts/ad/scorers/scorers.py | 4 + darts/ad/utils.py | 315 +++++++++++++++++------ 4 files changed, 244 insertions(+), 79 deletions(-) diff --git a/darts/ad/anomaly_model/anomaly_model.py b/darts/ad/anomaly_model/anomaly_model.py index be66758a0f..f93ef3301e 100644 --- a/darts/ad/anomaly_model/anomaly_model.py +++ b/darts/ad/anomaly_model/anomaly_model.py @@ -250,6 +250,7 @@ def show_anomalies( names_of_scorers: Union[str, Sequence[str]] = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, + multivariate_plot: bool = False, **score_kwargs, ): """Plot the results of the anomaly model. @@ -313,6 +314,7 @@ def show_anomalies( names_of_scorers=names_of_scorers, title=title, metric=metric, + multivariate_plot=multivariate_plot, ) @property diff --git a/darts/ad/anomaly_model/forecasting_am.py b/darts/ad/anomaly_model/forecasting_am.py index fd3eb9a33a..42dee0960c 100644 --- a/darts/ad/anomaly_model/forecasting_am.py +++ b/darts/ad/anomaly_model/forecasting_am.py @@ -447,6 +447,7 @@ def show_anomalies( names_of_scorers: Union[str, Sequence[str]] = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, + multivariate_plot: bool = False, **score_kwargs, ): """Plot the results of the anomaly model. @@ -535,6 +536,7 @@ def show_anomalies( names_of_scorers=names_of_scorers, title=title, metric=metric, + multivariate_plot=multivariate_plot, **score_kwargs, ) diff --git a/darts/ad/scorers/scorers.py b/darts/ad/scorers/scorers.py index 1afae77d21..ba573229bb 100644 --- a/darts/ad/scorers/scorers.py +++ b/darts/ad/scorers/scorers.py @@ -175,6 +175,7 @@ def show_anomalies_from_prediction( anomalies: TimeSeries = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, + multivariate_plot: bool = False, ): """Plot the results of the scorer. @@ -229,6 +230,7 @@ def show_anomalies_from_prediction( names_of_scorers=scorer_name, title=title, metric=metric, + multivariate_plot=multivariate_plot, ) @property @@ -579,6 +581,7 @@ def show_anomalies( scorer_name: str = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, + multivariate_plot: bool = False, ): """Plot the results of the scorer. @@ -632,6 +635,7 @@ def show_anomalies( names_of_scorers=scorer_name, title=title, metric=metric, + multivariate_plot=multivariate_plot, ) @property diff --git a/darts/ad/utils.py b/darts/ad/utils.py index 0d5260b1f4..0ee00ca6f7 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -310,6 +310,7 @@ def show_anomalies_from_scores( names_of_scorers: Union[str, Sequence[str]] = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, + multivariate_plot: bool = False, ): """Plot the results generated by an anomaly model. @@ -352,6 +353,7 @@ def show_anomalies_from_scores( Only effective when `pred_scores` is not `None`. Default: "AUC_ROC". """ + series = _check_input( series, name="series", @@ -362,6 +364,7 @@ def show_anomalies_from_scores( title = "Anomaly results" nbr_plots = 1 + if anomalies is not None: nbr_plots = nbr_plots + 1 elif metric is not None: @@ -421,105 +424,259 @@ def show_anomalies_from_scores( nbr_plots = nbr_plots + len(set(window)) - fig, axs = plt.subplots( - nbr_plots, - figsize=(8, 4 + 2 * (nbr_plots - 1)), - sharex=True, - gridspec_kw={"height_ratios": [2] + [1] * (nbr_plots - 1)}, - squeeze=False, - ) + if multivariate_plot: + series = _check_input( + series, + name="series", + check_multivariate=True, + )[0] + series_width = series.n_components + + if pred_series is not None: + pred_series = _check_input( + pred_series, + name="pred_series", + width_expected=series.width, + check_multivariate=True, + )[0] + + if anomalies is not None: + anomalies = _check_input( + anomalies, + name="anomalies", + width_expected=series.width, + check_multivariate=True, + )[0] + + if pred_scores is not None: + for pred_score in pred_scores: + pred_score = _check_input( + pred_score, + name="pred_score", + width_expected=series.width, + check_multivariate=True, + )[0] + + series_width = series.n_components + fig, axs = plt.subplots( + nbr_plots * series_width, + figsize=(8, 4 + 2 * (nbr_plots * series_width - 1)), + sharex=True, + gridspec_kw={"height_ratios": [2] + [1] * (nbr_plots * series_width - 1)}, + squeeze=False, + ) - index_ax = 0 + for i in range(series_width): + index_ax = i * nbr_plots - _plot_series(series=series, ax_id=axs[index_ax][0], linewidth=0.5, label_name="") + _plot_series( + series=series[series.components[i]], + ax_id=axs[index_ax][0], + linewidth=0.5, + label_name="", + ) - if pred_series is not None: - _plot_series( - series=pred_series, - ax_id=axs[index_ax][0], - linewidth=0.5, - label_name="model output", - ) + if pred_series[pred_series.components[i]] is not None: + _plot_series( + series=pred_series[pred_series.components[i]], + ax_id=axs[index_ax][0], + linewidth=0.5, + label_name=pred_series.components[i] + " model_output", + ) - axs[index_ax][0].set_title("") + axs[index_ax][0].set_title("") - if anomalies is not None or pred_scores is not None: - axs[index_ax][0].set_xlabel("") + if anomalies is not None or pred_scores is not None: + axs[index_ax][0].set_xlabel("") - axs[index_ax][0].legend(loc="upper center", bbox_to_anchor=(0.5, 1.1), ncol=2) + axs[index_ax][0].legend( + loc="upper center", bbox_to_anchor=(0.5, 1.1), ncol=2 + ) - if pred_scores is not None: - dict_input = {} - - for idx, (score, w) in enumerate(zip(pred_scores, window)): - dict_input[idx] = {"series_score": score, "window": w, "name_id": idx} - - for index, elem in enumerate( - sorted(dict_input.items(), key=lambda x: x[1]["window"]) - ): - if index == 0: - current_window = elem[1]["window"] - index_ax = index_ax + 1 - - idx = elem[1]["name_id"] - w = elem[1]["window"] - - if w != current_window: - current_window = w - index_ax = index_ax + 1 - - if metric is not None: - value = round( - eval_metric_from_scores( - anomalies=anomalies, - pred_scores=pred_scores[idx], - window=w, - metric=metric, - ), - 3, + if pred_scores is not None: + dict_input = {} + + for idx, (score, w) in enumerate(zip(pred_scores, window)): + dict_input[idx] = { + "series_score": score, + "window": w, + "name_id": idx, + } + + for index, elem in enumerate( + sorted(dict_input.items(), key=lambda x: x[1]["window"]) + ): + if index == 0: + current_window = elem[1]["window"] + index_ax = index_ax + 1 + + idx = elem[1]["name_id"] + w = elem[1]["window"] + + if w != current_window: + current_window = w + index_ax = index_ax + 1 + + if metric is not None: + value = round( + eval_metric_from_scores( + anomalies=anomalies[anomalies.components[i]], + pred_scores=pred_scores[idx][ + pred_scores[idx].components[i] + ], + window=w, + metric=metric, + ), + 3, + ) + else: + value = None + + if names_of_scorers is not None: + label = ( + names_of_scorers[idx] + [f" ({value})", ""][value is None] + ) + else: + label = f"score_{str(idx)}" + [f" ({value})", ""][value is None] + + _plot_series( + series=elem[1]["series_score"][ + elem[1]["series_score"].components[i] + ], + ax_id=axs[index_ax][0], + linewidth=0.5, + label_name=label, + ) + + axs[index_ax][0].legend( + loc="upper center", bbox_to_anchor=(0.5, 1.19), ncol=2 + ) + axs[index_ax][0].set_title(f"Window: {str(w)}", loc="left") + axs[index_ax][0].set_title("") + axs[index_ax][0].set_xlabel("") + + if anomalies is not None: + _plot_series( + series=anomalies[anomalies.components[i]], + ax_id=axs[index_ax + 1][0], + linewidth=1, + label_name=anomalies.components[i], + color="red", ) - else: - value = None - if names_of_scorers is not None: - label = names_of_scorers[idx] + [f" ({value})", ""][value is None] + axs[index_ax + 1][0].set_title("") + axs[index_ax + 1][0].set_ylim([-0.1, 1.1]) + axs[index_ax + 1][0].set_yticks([0, 1]) + axs[index_ax + 1][0].set_yticklabels(["no", "yes"]) + axs[index_ax + 1][0].legend( + loc="upper center", bbox_to_anchor=(0.5, 1.2), ncol=2 + ) else: - label = f"score_{str(idx)}" + [f" ({value})", ""][value is None] + axs[index_ax][0].set_xlabel("timestamp") + + fig.suptitle(title) + else: + fig, axs = plt.subplots( + nbr_plots, + figsize=(8, 4 + 2 * (nbr_plots - 1)), + sharex=True, + gridspec_kw={"height_ratios": [2] + [1] * (nbr_plots - 1)}, + squeeze=False, + ) + index_ax = 0 + + _plot_series( + series=series, ax_id=axs[index_ax][0], linewidth=0.5, label_name="" + ) + + if pred_series is not None: _plot_series( - series=elem[1]["series_score"], + series=pred_series, ax_id=axs[index_ax][0], linewidth=0.5, - label_name=label, + label_name="model output", ) - axs[index_ax][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.19), ncol=2 - ) - axs[index_ax][0].set_title(f"Window: {str(w)}", loc="left") - axs[index_ax][0].set_title("") + axs[index_ax][0].set_title("") + + if anomalies is not None or pred_scores is not None: axs[index_ax][0].set_xlabel("") - if anomalies is not None: - _plot_series( - series=anomalies, - ax_id=axs[index_ax + 1][0], - linewidth=1, - label_name="anomalies", - color="red", - ) + axs[index_ax][0].legend(loc="upper center", bbox_to_anchor=(0.5, 1.1), ncol=2) + + if pred_scores is not None: + dict_input = {} + + for idx, (score, w) in enumerate(zip(pred_scores, window)): + dict_input[idx] = {"series_score": score, "window": w, "name_id": idx} + + for index, elem in enumerate( + sorted(dict_input.items(), key=lambda x: x[1]["window"]) + ): + if index == 0: + current_window = elem[1]["window"] + index_ax = index_ax + 1 + + idx = elem[1]["name_id"] + w = elem[1]["window"] + + if w != current_window: + current_window = w + index_ax = index_ax + 1 + + if metric is not None: + value = round( + eval_metric_from_scores( + anomalies=anomalies, + pred_scores=pred_scores[idx], + window=w, + metric=metric, + ), + 3, + ) + else: + value = None + + if names_of_scorers is not None: + label = names_of_scorers[idx] + [f" ({value})", ""][value is None] + else: + label = f"score_{str(idx)}" + [f" ({value})", ""][value is None] + + _plot_series( + series=elem[1]["series_score"], + ax_id=axs[index_ax][0], + linewidth=0.5, + label_name=label, + ) - axs[index_ax + 1][0].set_title("") - axs[index_ax + 1][0].set_ylim([-0.1, 1.1]) - axs[index_ax + 1][0].set_yticks([0, 1]) - axs[index_ax + 1][0].set_yticklabels(["no", "yes"]) - axs[index_ax + 1][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.2), ncol=2 - ) - else: - axs[index_ax][0].set_xlabel("timestamp") + axs[index_ax][0].legend( + loc="upper center", bbox_to_anchor=(0.5, 1.19), ncol=2 + ) + axs[index_ax][0].set_title(f"Window: {str(w)}", loc="left") + axs[index_ax][0].set_title("") + axs[index_ax][0].set_xlabel("") + + if anomalies is not None: + _plot_series( + series=anomalies, + ax_id=axs[index_ax + 1][0], + linewidth=1, + label_name="anomalies", + color="red", + ) + + axs[index_ax + 1][0].set_title("") + axs[index_ax + 1][0].set_ylim([-0.1, 1.1]) + axs[index_ax + 1][0].set_yticks([0, 1]) + axs[index_ax + 1][0].set_yticklabels(["no", "yes"]) + axs[index_ax + 1][0].legend( + loc="upper center", bbox_to_anchor=(0.5, 1.2), ncol=2 + ) + else: + axs[index_ax][0].set_xlabel("timestamp") - fig.suptitle(title) + fig.suptitle(title) def _assert_binary(series: TimeSeries, name: str): From b05eb8a2bc241cbc8e6bbbb439cd046527ee1bd2 Mon Sep 17 00:00:00 2001 From: he weilin Date: Tue, 31 Dec 2024 09:06:50 +0800 Subject: [PATCH 02/14] Update the docstring --- darts/ad/anomaly_model/anomaly_model.py | 2 ++ darts/ad/anomaly_model/forecasting_am.py | 2 ++ darts/ad/scorers/scorers.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/darts/ad/anomaly_model/anomaly_model.py b/darts/ad/anomaly_model/anomaly_model.py index 49ebe63ba9..d75ad18ab0 100644 --- a/darts/ad/anomaly_model/anomaly_model.py +++ b/darts/ad/anomaly_model/anomaly_model.py @@ -284,6 +284,8 @@ def show_anomalies( Default: "AUC_ROC". score_kwargs parameters for the `score()` method. + multivariate_plot + If True, it will separately plot each component in multivariate series. """ series = _check_input(series, name="series", num_series_expected=1)[0] predict_kwargs = predict_kwargs if predict_kwargs is not None else {} diff --git a/darts/ad/anomaly_model/forecasting_am.py b/darts/ad/anomaly_model/forecasting_am.py index 348e025e9f..7fee356cb6 100644 --- a/darts/ad/anomaly_model/forecasting_am.py +++ b/darts/ad/anomaly_model/forecasting_am.py @@ -507,6 +507,8 @@ def show_anomalies( Optionally, the name of the metric function to use. Must be one of "AUC_ROC" (Area Under the Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". + multivariate_plot + If True, it will separately plot each component in multivariate series. score_kwargs parameters for the `score()` method. """ diff --git a/darts/ad/scorers/scorers.py b/darts/ad/scorers/scorers.py index d797c93212..66f01b6692 100644 --- a/darts/ad/scorers/scorers.py +++ b/darts/ad/scorers/scorers.py @@ -209,6 +209,8 @@ def show_anomalies_from_prediction( Optionally, the name of the metric function to use. Must be one of "AUC_ROC" (Area Under the Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". + multivariate_plot + If True, it will separately plot each component in multivariate series. """ series = _check_input(series, name="series", num_series_expected=1)[0] pred_series = _check_input( From 33006d44e3967f9d2e59e61d7a8367af45dde9ff Mon Sep 17 00:00:00 2001 From: he weilin Date: Tue, 31 Dec 2024 10:19:18 +0800 Subject: [PATCH 03/14] refactor the show_anomalies_from_scores() --- darts/ad/utils.py | 347 +++++++++++++++++++++------------------------- 1 file changed, 155 insertions(+), 192 deletions(-) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index 510ef3f924..6f2821a656 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -469,112 +469,23 @@ def show_anomalies_from_scores( for i in range(series_width): index_ax = i * nbr_plots - - _plot_series( + _plot_series_and_anomalies( series=series[series.components[i]], - ax_id=axs[index_ax][0], - linewidth=0.5, - label_name="", - ) - - if pred_series[pred_series.components[i]] is not None: - _plot_series( - series=pred_series[pred_series.components[i]], - ax_id=axs[index_ax][0], - linewidth=0.5, - label_name=pred_series.components[i] + " model_output", - ) - - axs[index_ax][0].set_title("") - - if anomalies is not None or pred_scores is not None: - axs[index_ax][0].set_xlabel("") - - axs[index_ax][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.1), ncol=2 + anomalies=anomalies[anomalies.components[i]] + if anomalies is not None + else None, + pred_series=pred_series[pred_series.components[i]] + if pred_series is not None + else None, + pred_scores=pred_scores, + window=window, + names_of_scorers=names_of_scorers, + metric=metric, + axs=axs, + index_ax=index_ax, + nbr_plots=nbr_plots, ) - if pred_scores is not None: - dict_input = {} - - for idx, (score, w) in enumerate(zip(pred_scores, window)): - dict_input[idx] = { - "series_score": score, - "window": w, - "name_id": idx, - } - - for index, elem in enumerate( - sorted(dict_input.items(), key=lambda x: x[1]["window"]) - ): - if index == 0: - current_window = elem[1]["window"] - index_ax = index_ax + 1 - - idx = elem[1]["name_id"] - w = elem[1]["window"] - - if w != current_window: - current_window = w - index_ax = index_ax + 1 - - if metric is not None: - value = round( - eval_metric_from_scores( - anomalies=anomalies[anomalies.components[i]], - pred_scores=pred_scores[idx][ - pred_scores[idx].components[i] - ], - window=w, - metric=metric, - ), - 3, - ) - else: - value = None - - if names_of_scorers is not None: - label = ( - names_of_scorers[idx] + [f" ({value})", ""][value is None] - ) - else: - label = f"score_{str(idx)}" + [f" ({value})", ""][value is None] - - _plot_series( - series=elem[1]["series_score"][ - elem[1]["series_score"].components[i] - ], - ax_id=axs[index_ax][0], - linewidth=0.5, - label_name=label, - ) - - axs[index_ax][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.19), ncol=2 - ) - axs[index_ax][0].set_title(f"Window: {str(w)}", loc="left") - axs[index_ax][0].set_title("") - axs[index_ax][0].set_xlabel("") - - if anomalies is not None: - _plot_series( - series=anomalies[anomalies.components[i]], - ax_id=axs[index_ax + 1][0], - linewidth=1, - label_name=anomalies.components[i], - color="red", - ) - - axs[index_ax + 1][0].set_title("") - axs[index_ax + 1][0].set_ylim([-0.1, 1.1]) - axs[index_ax + 1][0].set_yticks([0, 1]) - axs[index_ax + 1][0].set_yticklabels(["no", "yes"]) - axs[index_ax + 1][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.2), ncol=2 - ) - else: - axs[index_ax][0].set_xlabel("timestamp") - fig.suptitle(title) else: fig, axs = plt.subplots( @@ -586,97 +497,19 @@ def show_anomalies_from_scores( ) index_ax = 0 - - _plot_series( - series=series, ax_id=axs[index_ax][0], linewidth=0.5, label_name="" + _plot_series_and_anomalies( + series=series, + anomalies=anomalies, + pred_series=pred_series, + pred_scores=pred_scores, + window=window, + names_of_scorers=names_of_scorers, + metric=metric, + axs=axs, + index_ax=index_ax, + nbr_plots=nbr_plots, ) - if pred_series is not None: - _plot_series( - series=pred_series, - ax_id=axs[index_ax][0], - linewidth=0.5, - label_name="model output", - ) - - axs[index_ax][0].set_title("") - - if anomalies is not None or pred_scores is not None: - axs[index_ax][0].set_xlabel("") - - axs[index_ax][0].legend(loc="upper center", bbox_to_anchor=(0.5, 1.1), ncol=2) - - if pred_scores is not None: - dict_input = {} - - for idx, (score, w) in enumerate(zip(pred_scores, window)): - dict_input[idx] = {"series_score": score, "window": w, "name_id": idx} - - for index, elem in enumerate( - sorted(dict_input.items(), key=lambda x: x[1]["window"]) - ): - if index == 0: - current_window = elem[1]["window"] - index_ax = index_ax + 1 - - idx = elem[1]["name_id"] - w = elem[1]["window"] - - if w != current_window: - current_window = w - index_ax = index_ax + 1 - - if metric is not None: - value = round( - eval_metric_from_scores( - anomalies=anomalies, - pred_scores=pred_scores[idx], - window=w, - metric=metric, - ), - 3, - ) - else: - value = None - - if names_of_scorers is not None: - label = names_of_scorers[idx] + [f" ({value})", ""][value is None] - else: - label = f"score_{str(idx)}" + [f" ({value})", ""][value is None] - - _plot_series( - series=elem[1]["series_score"], - ax_id=axs[index_ax][0], - linewidth=0.5, - label_name=label, - ) - - axs[index_ax][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.19), ncol=2 - ) - axs[index_ax][0].set_title(f"Window: {str(w)}", loc="left") - axs[index_ax][0].set_title("") - axs[index_ax][0].set_xlabel("") - - if anomalies is not None: - _plot_series( - series=anomalies, - ax_id=axs[index_ax + 1][0], - linewidth=1, - label_name="anomalies", - color="red", - ) - - axs[index_ax + 1][0].set_title("") - axs[index_ax + 1][0].set_ylim([-0.1, 1.1]) - axs[index_ax + 1][0].set_yticks([0, 1]) - axs[index_ax + 1][0].set_yticklabels(["no", "yes"]) - axs[index_ax + 1][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.2), ncol=2 - ) - else: - axs[index_ax][0].set_xlabel("timestamp") - fig.suptitle(title) @@ -937,3 +770,133 @@ def _assert_fit_called(fit_called: bool, name: str): ), logger=logger, ) + + +def _plot_series_and_anomalies( + series: TimeSeries, + anomalies: TimeSeries, + pred_series: TimeSeries, + pred_scores: Sequence[TimeSeries], + window: Sequence[int], + names_of_scorers: Sequence[str], + metric: str, + axs: plt.Axes, + index_ax: int, + nbr_plots: int, +): + """Helper function to plot series and anomalies. + + Parameters + ---------- + series + The actual series to visualize anomalies from. + anomalies + The ground truth of the anomalies (1 if it is an anomaly and 0 if not). + pred_series + Output of the model given as input the `series` (can be stochastic). + pred_scores + Output of the scorers given the output of the model and `series`. + window + Window parameter for each anomaly scores. + names_of_scorers + Name of the scores. + metric + The name of the metric function to use. + axs + The axes to plot on. + index_ax + The index of the current axis. + nbr_plots + The number of plots. + """ + _plot_series(series=series, ax_id=axs[index_ax][0], linewidth=0.5, label_name="") + + if pred_series is not None: + _plot_series( + series=pred_series, + ax_id=axs[index_ax][0], + linewidth=0.5, + label_name="model output", + ) + + axs[index_ax][0].set_title("") + + if anomalies is not None or pred_scores is not None: + axs[index_ax][0].set_xlabel("") + + axs[index_ax][0].legend(loc="upper center", bbox_to_anchor=(0.5, 1.1), ncol=2) + + if pred_scores is not None: + dict_input = {} + + for idx, (score, w) in enumerate(zip(pred_scores, window)): + dict_input[idx] = {"series_score": score, "window": w, "name_id": idx} + + for index, elem in enumerate( + sorted(dict_input.items(), key=lambda x: x[1]["window"]) + ): + if index == 0: + current_window = elem[1]["window"] + index_ax = index_ax + 1 + + idx = elem[1]["name_id"] + w = elem[1]["window"] + + if w != current_window: + current_window = w + index_ax = index_ax + 1 + + if metric is not None: + value = round( + eval_metric_from_scores( + anomalies=anomalies, + pred_scores=pred_scores[idx][ + pred_scores[idx].components[index_ax // nbr_plots] + ], + window=w, + metric=metric, + ), + 3, + ) + else: + value = None + + if names_of_scorers is not None: + label = names_of_scorers[idx] + [f" ({value})", ""][value is None] + else: + label = f"score_{str(idx)}" + [f" ({value})", ""][value is None] + + _plot_series( + series=elem[1]["series_score"][ + elem[1]["series_score"].components[index_ax // nbr_plots] + ], + ax_id=axs[index_ax][0], + linewidth=0.5, + label_name=label, + ) + + axs[index_ax][0].legend( + loc="upper center", bbox_to_anchor=(0.5, 1.19), ncol=2 + ) + axs[index_ax][0].set_title(f"Window: {str(w)}", loc="left") + axs[index_ax][0].set_title("") + axs[index_ax][0].set_xlabel("") + + if anomalies is not None: + _plot_series( + series=anomalies, + ax_id=axs[index_ax + 1][0], + linewidth=1, + label_name="anomalies", + color="red", + ) + + axs[index_ax + 1][0].set_title("") + axs[index_ax + 1][0].set_ylim([-0.1, 1.1]) + axs[index_ax + 1][0].set_yticks([0, 1]) + axs[index_ax + 1][0].set_yticklabels(["no", "yes"]) + axs[index_ax + 1][0].legend( + loc="upper center", bbox_to_anchor=(0.5, 1.2), ncol=2 + ) + else: + axs[index_ax][0].set_xlabel("timestamp") From e6b68545bd10a2d904242da97ea520850ce878b4 Mon Sep 17 00:00:00 2001 From: he weilin Date: Tue, 31 Dec 2024 10:55:24 +0800 Subject: [PATCH 04/14] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf805cad84..8439668f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ but cannot always guarantee backwards compatibility. Changes that may **break co **Improved** - New model: `StatsForecastAutoTBATS`. This model offers the [AutoTBATS](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#autotbats) model from Nixtla's `statsforecasts` library. [#2611](https://github.com/unit8co/darts/pull/2611) by [He Weilin](https://github.com/cnhwl). +- Added `multivariate_plot` parameter in `show_anomalies()` to separately plot each component in multivariate series. [#2544](https://github.com/unit8co/darts/pull/2544) by [He Weilin](https://github.com/cnhwl). **Fixed** From 8468f6d851d9ce4c1d94473414c045bf9f169b09 Mon Sep 17 00:00:00 2001 From: he weilin Date: Tue, 31 Dec 2024 17:21:59 +0800 Subject: [PATCH 05/14] Improve code in utils.py --- darts/ad/utils.py | 60 +++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index 6f2821a656..672a3a818a 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -353,13 +353,23 @@ def show_anomalies_from_scores( Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Only effective when `pred_scores` is not `None`. Default: "AUC_ROC". + multivariate_plot + If True, it will separately plot each component in multivariate series. """ - series = _check_input( - series, - name="series", - num_series_expected=1, - )[0] + series = ( + _check_input( + series, + name="series", + check_multivariate=True, + )[0] + if multivariate_plot + else _check_input( + series, + name="series", + num_series_expected=1, + )[0] + ) if title is None and pred_scores is not None: title = "Anomaly results" @@ -424,15 +434,17 @@ def show_anomalies_from_scores( ) nbr_plots = nbr_plots + len(set(window)) - - if multivariate_plot: - series = _check_input( - series, - name="series", - check_multivariate=True, - )[0] series_width = series.n_components + plots_per_ts = nbr_plots * series_width if multivariate_plot else nbr_plots + fig, axs = plt.subplots( + plots_per_ts, + figsize=(8, 4 + 2 * (plots_per_ts - 1)), + sharex=True, + gridspec_kw={"height_ratios": [2] + [1] * (plots_per_ts - 1)}, + squeeze=False, + ) + if multivariate_plot: if pred_series is not None: pred_series = _check_input( pred_series, @@ -446,6 +458,7 @@ def show_anomalies_from_scores( anomalies, name="anomalies", width_expected=series.width, + check_binary=True, check_multivariate=True, )[0] @@ -458,17 +471,7 @@ def show_anomalies_from_scores( check_multivariate=True, )[0] - series_width = series.n_components - fig, axs = plt.subplots( - nbr_plots * series_width, - figsize=(8, 4 + 2 * (nbr_plots * series_width - 1)), - sharex=True, - gridspec_kw={"height_ratios": [2] + [1] * (nbr_plots * series_width - 1)}, - squeeze=False, - ) - for i in range(series_width): - index_ax = i * nbr_plots _plot_series_and_anomalies( series=series[series.components[i]], anomalies=anomalies[anomalies.components[i]] @@ -482,21 +485,12 @@ def show_anomalies_from_scores( names_of_scorers=names_of_scorers, metric=metric, axs=axs, - index_ax=index_ax, + index_ax=i * nbr_plots, nbr_plots=nbr_plots, ) fig.suptitle(title) else: - fig, axs = plt.subplots( - nbr_plots, - figsize=(8, 4 + 2 * (nbr_plots - 1)), - sharex=True, - gridspec_kw={"height_ratios": [2] + [1] * (nbr_plots - 1)}, - squeeze=False, - ) - - index_ax = 0 _plot_series_and_anomalies( series=series, anomalies=anomalies, @@ -506,7 +500,7 @@ def show_anomalies_from_scores( names_of_scorers=names_of_scorers, metric=metric, axs=axs, - index_ax=index_ax, + index_ax=0, nbr_plots=nbr_plots, ) From da1e644d9ac3c3937417819ab2b2c2ed3d16d6f9 Mon Sep 17 00:00:00 2001 From: he weilin Date: Thu, 2 Jan 2025 09:21:43 +0800 Subject: [PATCH 06/14] Improve code in utils.py --- darts/ad/utils.py | 72 ++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index 672a3a818a..8bba465551 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -357,25 +357,17 @@ def show_anomalies_from_scores( If True, it will separately plot each component in multivariate series. """ - series = ( - _check_input( - series, - name="series", - check_multivariate=True, - )[0] - if multivariate_plot - else _check_input( - series, - name="series", - num_series_expected=1, - )[0] - ) + series = _check_input( + series, + name="series", + num_series_expected=1, + check_multivariate=multivariate_plot, + )[0] if title is None and pred_scores is not None: title = "Anomaly results" nbr_plots = 1 - if anomalies is not None: nbr_plots = nbr_plots + 1 elif metric is not None: @@ -433,7 +425,7 @@ def show_anomalies_from_scores( logger=logger, ) - nbr_plots = nbr_plots + len(set(window)) + nbr_plots += len(set(window)) series_width = series.n_components plots_per_ts = nbr_plots * series_width if multivariate_plot else nbr_plots fig, axs = plt.subplots( @@ -444,33 +436,36 @@ def show_anomalies_from_scores( squeeze=False, ) - if multivariate_plot: - if pred_series is not None: - pred_series = _check_input( - pred_series, - name="pred_series", - width_expected=series.width, - check_multivariate=True, - )[0] + if pred_series is not None: + pred_series = _check_input( + pred_series, + name="pred_series", + width_expected=series.width, + num_series_expected=1, + check_multivariate=True, + )[0] - if anomalies is not None: - anomalies = _check_input( - anomalies, - name="anomalies", + if anomalies is not None: + anomalies = _check_input( + anomalies, + name="anomalies", + width_expected=series.width, + num_series_expected=1, + check_binary=True, + check_multivariate=True, + )[0] + + if pred_scores is not None: + for pred_score in pred_scores: + pred_score = _check_input( + pred_score, + name="pred_score", width_expected=series.width, - check_binary=True, + num_series_expected=1, check_multivariate=True, )[0] - if pred_scores is not None: - for pred_score in pred_scores: - pred_score = _check_input( - pred_score, - name="pred_score", - width_expected=series.width, - check_multivariate=True, - )[0] - + if multivariate_plot: for i in range(series_width): _plot_series_and_anomalies( series=series[series.components[i]], @@ -489,7 +484,6 @@ def show_anomalies_from_scores( nbr_plots=nbr_plots, ) - fig.suptitle(title) else: _plot_series_and_anomalies( series=series, @@ -504,7 +498,7 @@ def show_anomalies_from_scores( nbr_plots=nbr_plots, ) - fig.suptitle(title) + fig.suptitle(title) def _assert_binary(series: TimeSeries, name: str): From 8e51e2c57d80be72da4126d653dc716a8286c3ae Mon Sep 17 00:00:00 2001 From: he weilin Date: Thu, 2 Jan 2025 09:59:13 +0800 Subject: [PATCH 07/14] make check_multivariate depend on multivariate_plot --- darts/ad/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index 8bba465551..a0a4dfc40a 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -442,7 +442,7 @@ def show_anomalies_from_scores( name="pred_series", width_expected=series.width, num_series_expected=1, - check_multivariate=True, + check_multivariate=multivariate_plot, )[0] if anomalies is not None: @@ -452,7 +452,7 @@ def show_anomalies_from_scores( width_expected=series.width, num_series_expected=1, check_binary=True, - check_multivariate=True, + check_multivariate=multivariate_plot, )[0] if pred_scores is not None: @@ -462,7 +462,7 @@ def show_anomalies_from_scores( name="pred_score", width_expected=series.width, num_series_expected=1, - check_multivariate=True, + check_multivariate=multivariate_plot, )[0] if multivariate_plot: From f3e0db21206547bb42447f8163a283ad3c00342d Mon Sep 17 00:00:00 2001 From: dennisbader Date: Fri, 3 Jan 2025 13:55:01 +0100 Subject: [PATCH 08/14] update utils --- darts/ad/scorers/scorers.py | 2 + darts/ad/utils.py | 87 ++++++++++++++++++------------------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/darts/ad/scorers/scorers.py b/darts/ad/scorers/scorers.py index 66f01b6692..7e935c56dc 100644 --- a/darts/ad/scorers/scorers.py +++ b/darts/ad/scorers/scorers.py @@ -615,6 +615,8 @@ def show_anomalies( Optionally, the name of the metric function to use. Must be one of "AUC_ROC" (Area Under the Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". + multivariate_plot + If True, it will separately plot each component in multivariate series. """ series = _check_input(series, name="series", num_series_expected=1)[0] pred_scores = self.score(series) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index a0a4dfc40a..26a2a6f42f 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -356,7 +356,6 @@ def show_anomalies_from_scores( multivariate_plot If True, it will separately plot each component in multivariate series. """ - series = _check_input( series, name="series", @@ -426,78 +425,80 @@ def show_anomalies_from_scores( ) nbr_plots += len(set(window)) - series_width = series.n_components - plots_per_ts = nbr_plots * series_width if multivariate_plot else nbr_plots - fig, axs = plt.subplots( - plots_per_ts, - figsize=(8, 4 + 2 * (plots_per_ts - 1)), - sharex=True, - gridspec_kw={"height_ratios": [2] + [1] * (plots_per_ts - 1)}, - squeeze=False, - ) + series_width = series.n_components if pred_series is not None: pred_series = _check_input( pred_series, name="pred_series", - width_expected=series.width, + width_expected=series_width, num_series_expected=1, check_multivariate=multivariate_plot, )[0] - if anomalies is not None: + if anomalies is not None and multivariate_plot: anomalies = _check_input( anomalies, name="anomalies", - width_expected=series.width, + width_expected=series_width, num_series_expected=1, check_binary=True, check_multivariate=multivariate_plot, )[0] - if pred_scores is not None: + if pred_scores is not None and multivariate_plot: for pred_score in pred_scores: - pred_score = _check_input( + _ = _check_input( pred_score, name="pred_score", - width_expected=series.width, + width_expected=series_width, num_series_expected=1, check_multivariate=multivariate_plot, )[0] - if multivariate_plot: - for i in range(series_width): - _plot_series_and_anomalies( - series=series[series.components[i]], - anomalies=anomalies[anomalies.components[i]] - if anomalies is not None - else None, - pred_series=pred_series[pred_series.components[i]] + plots_per_ts = nbr_plots * series_width if multivariate_plot else nbr_plots + fig, axs = plt.subplots( + plots_per_ts, + figsize=(8, 4 + 2 * (plots_per_ts - 1)), + sharex=True, + gridspec_kw={"height_ratios": [2] + [1] * (plots_per_ts - 1)}, + squeeze=False, + ) + + for i in range(series_width if multivariate_plot else 1): + if multivariate_plot: + series_ = series[series.components[i]] + anomalies_ = ( + anomalies[anomalies.components[i]] if anomalies is not None else None + ) + pred_series_ = ( + pred_series[pred_series.components[i]] if pred_series is not None - else None, - pred_scores=pred_scores, - window=window, - names_of_scorers=names_of_scorers, - metric=metric, - axs=axs, - index_ax=i * nbr_plots, - nbr_plots=nbr_plots, + else None + ) + pred_scores_ = ( + [pc[pc.components[i]] for pc in pred_scores] + if pred_scores is not None + else None ) + else: + series_ = series + anomalies_ = anomalies + pred_series_ = pred_series + pred_scores_ = pred_scores - else: _plot_series_and_anomalies( - series=series, - anomalies=anomalies, - pred_series=pred_series, - pred_scores=pred_scores, + series=series_, + anomalies=anomalies_, + pred_series=pred_series_, + pred_scores=pred_scores_, window=window, names_of_scorers=names_of_scorers, metric=metric, axs=axs, - index_ax=0, + index_ax=i * nbr_plots, nbr_plots=nbr_plots, ) - fig.suptitle(title) @@ -838,9 +839,7 @@ def _plot_series_and_anomalies( value = round( eval_metric_from_scores( anomalies=anomalies, - pred_scores=pred_scores[idx][ - pred_scores[idx].components[index_ax // nbr_plots] - ], + pred_scores=pred_scores[idx], window=w, metric=metric, ), @@ -855,9 +854,7 @@ def _plot_series_and_anomalies( label = f"score_{str(idx)}" + [f" ({value})", ""][value is None] _plot_series( - series=elem[1]["series_score"][ - elem[1]["series_score"].components[index_ax // nbr_plots] - ], + series=elem[1]["series_score"], ax_id=axs[index_ax][0], linewidth=0.5, label_name=label, From 27156f467cdaf04b0428ec850846a0a05fb9c48b Mon Sep 17 00:00:00 2001 From: he weilin Date: Mon, 6 Jan 2025 11:02:21 +0800 Subject: [PATCH 09/14] improve the spacing between suptitle and axes --- darts/ad/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index 26a2a6f42f..d0e3fb4a3b 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -463,6 +463,7 @@ def show_anomalies_from_scores( sharex=True, gridspec_kw={"height_ratios": [2] + [1] * (plots_per_ts - 1)}, squeeze=False, + constrained_layout=True, ) for i in range(series_width if multivariate_plot else 1): From 1fa81e2b66b02c4b4d3dee99d75cd04be076f555 Mon Sep 17 00:00:00 2001 From: he weilin Date: Mon, 6 Jan 2025 14:55:39 +0800 Subject: [PATCH 10/14] update utils --- darts/ad/scorers/scorers.py | 2 + darts/ad/utils.py | 87 ++++++++++++++++++------------------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/darts/ad/scorers/scorers.py b/darts/ad/scorers/scorers.py index 66f01b6692..7e935c56dc 100644 --- a/darts/ad/scorers/scorers.py +++ b/darts/ad/scorers/scorers.py @@ -615,6 +615,8 @@ def show_anomalies( Optionally, the name of the metric function to use. Must be one of "AUC_ROC" (Area Under the Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". + multivariate_plot + If True, it will separately plot each component in multivariate series. """ series = _check_input(series, name="series", num_series_expected=1)[0] pred_scores = self.score(series) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index a0a4dfc40a..26a2a6f42f 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -356,7 +356,6 @@ def show_anomalies_from_scores( multivariate_plot If True, it will separately plot each component in multivariate series. """ - series = _check_input( series, name="series", @@ -426,78 +425,80 @@ def show_anomalies_from_scores( ) nbr_plots += len(set(window)) - series_width = series.n_components - plots_per_ts = nbr_plots * series_width if multivariate_plot else nbr_plots - fig, axs = plt.subplots( - plots_per_ts, - figsize=(8, 4 + 2 * (plots_per_ts - 1)), - sharex=True, - gridspec_kw={"height_ratios": [2] + [1] * (plots_per_ts - 1)}, - squeeze=False, - ) + series_width = series.n_components if pred_series is not None: pred_series = _check_input( pred_series, name="pred_series", - width_expected=series.width, + width_expected=series_width, num_series_expected=1, check_multivariate=multivariate_plot, )[0] - if anomalies is not None: + if anomalies is not None and multivariate_plot: anomalies = _check_input( anomalies, name="anomalies", - width_expected=series.width, + width_expected=series_width, num_series_expected=1, check_binary=True, check_multivariate=multivariate_plot, )[0] - if pred_scores is not None: + if pred_scores is not None and multivariate_plot: for pred_score in pred_scores: - pred_score = _check_input( + _ = _check_input( pred_score, name="pred_score", - width_expected=series.width, + width_expected=series_width, num_series_expected=1, check_multivariate=multivariate_plot, )[0] - if multivariate_plot: - for i in range(series_width): - _plot_series_and_anomalies( - series=series[series.components[i]], - anomalies=anomalies[anomalies.components[i]] - if anomalies is not None - else None, - pred_series=pred_series[pred_series.components[i]] + plots_per_ts = nbr_plots * series_width if multivariate_plot else nbr_plots + fig, axs = plt.subplots( + plots_per_ts, + figsize=(8, 4 + 2 * (plots_per_ts - 1)), + sharex=True, + gridspec_kw={"height_ratios": [2] + [1] * (plots_per_ts - 1)}, + squeeze=False, + ) + + for i in range(series_width if multivariate_plot else 1): + if multivariate_plot: + series_ = series[series.components[i]] + anomalies_ = ( + anomalies[anomalies.components[i]] if anomalies is not None else None + ) + pred_series_ = ( + pred_series[pred_series.components[i]] if pred_series is not None - else None, - pred_scores=pred_scores, - window=window, - names_of_scorers=names_of_scorers, - metric=metric, - axs=axs, - index_ax=i * nbr_plots, - nbr_plots=nbr_plots, + else None + ) + pred_scores_ = ( + [pc[pc.components[i]] for pc in pred_scores] + if pred_scores is not None + else None ) + else: + series_ = series + anomalies_ = anomalies + pred_series_ = pred_series + pred_scores_ = pred_scores - else: _plot_series_and_anomalies( - series=series, - anomalies=anomalies, - pred_series=pred_series, - pred_scores=pred_scores, + series=series_, + anomalies=anomalies_, + pred_series=pred_series_, + pred_scores=pred_scores_, window=window, names_of_scorers=names_of_scorers, metric=metric, axs=axs, - index_ax=0, + index_ax=i * nbr_plots, nbr_plots=nbr_plots, ) - fig.suptitle(title) @@ -838,9 +839,7 @@ def _plot_series_and_anomalies( value = round( eval_metric_from_scores( anomalies=anomalies, - pred_scores=pred_scores[idx][ - pred_scores[idx].components[index_ax // nbr_plots] - ], + pred_scores=pred_scores[idx], window=w, metric=metric, ), @@ -855,9 +854,7 @@ def _plot_series_and_anomalies( label = f"score_{str(idx)}" + [f" ({value})", ""][value is None] _plot_series( - series=elem[1]["series_score"][ - elem[1]["series_score"].components[index_ax // nbr_plots] - ], + series=elem[1]["series_score"], ax_id=axs[index_ax][0], linewidth=0.5, label_name=label, From 8aea4c07dccc6aed2ac1bb4138b79d1f4d81d930 Mon Sep 17 00:00:00 2001 From: he weilin Date: Mon, 6 Jan 2025 15:49:23 +0800 Subject: [PATCH 11/14] improve the spacing between suptitle and axes --- darts/ad/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index 26a2a6f42f..d0e3fb4a3b 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -463,6 +463,7 @@ def show_anomalies_from_scores( sharex=True, gridspec_kw={"height_ratios": [2] + [1] * (plots_per_ts - 1)}, squeeze=False, + constrained_layout=True, ) for i in range(series_width if multivariate_plot else 1): From c6b799e03bfb103f789c833c3baf42af35013a06 Mon Sep 17 00:00:00 2001 From: he weilin Date: Mon, 6 Jan 2025 17:10:21 +0800 Subject: [PATCH 12/14] Fix the height_ratios of each subplot --- darts/ad/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index d0e3fb4a3b..bc3588cb77 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -459,11 +459,13 @@ def show_anomalies_from_scores( plots_per_ts = nbr_plots * series_width if multivariate_plot else nbr_plots fig, axs = plt.subplots( plots_per_ts, - figsize=(8, 4 + 2 * (plots_per_ts - 1)), + figsize=(8, 4 * (plots_per_ts // nbr_plots) + 2 * (nbr_plots - 1)), sharex=True, - gridspec_kw={"height_ratios": [2] + [1] * (plots_per_ts - 1)}, + gridspec_kw={ + "height_ratios": ([2] + [1] * (nbr_plots - 1)) * (plots_per_ts // nbr_plots) + }, squeeze=False, - constrained_layout=True, + layout="constrained", ) for i in range(series_width if multivariate_plot else 1): From 78baeec4c4e89eeae88d6541e75934449568dd5a Mon Sep 17 00:00:00 2001 From: he weilin Date: Mon, 6 Jan 2025 17:18:38 +0800 Subject: [PATCH 13/14] Change "multivariate_plot" parameter name to "component_wise" --- darts/ad/anomaly_model/anomaly_model.py | 6 +++--- darts/ad/anomaly_model/forecasting_am.py | 6 +++--- darts/ad/scorers/scorers.py | 12 ++++++------ darts/ad/utils.py | 22 +++++++++++----------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/darts/ad/anomaly_model/anomaly_model.py b/darts/ad/anomaly_model/anomaly_model.py index d75ad18ab0..1b13d0553a 100644 --- a/darts/ad/anomaly_model/anomaly_model.py +++ b/darts/ad/anomaly_model/anomaly_model.py @@ -247,7 +247,7 @@ def show_anomalies( names_of_scorers: Union[str, Sequence[str]] = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, - multivariate_plot: bool = False, + component_wise: bool = False, **score_kwargs, ): """Plot the results of the anomaly model. @@ -284,7 +284,7 @@ def show_anomalies( Default: "AUC_ROC". score_kwargs parameters for the `score()` method. - multivariate_plot + component_wise If True, it will separately plot each component in multivariate series. """ series = _check_input(series, name="series", num_series_expected=1)[0] @@ -313,7 +313,7 @@ def show_anomalies( names_of_scorers=names_of_scorers, title=title, metric=metric, - multivariate_plot=multivariate_plot, + component_wise=component_wise, ) @property diff --git a/darts/ad/anomaly_model/forecasting_am.py b/darts/ad/anomaly_model/forecasting_am.py index 7fee356cb6..318fe3361a 100644 --- a/darts/ad/anomaly_model/forecasting_am.py +++ b/darts/ad/anomaly_model/forecasting_am.py @@ -440,7 +440,7 @@ def show_anomalies( names_of_scorers: Union[str, Sequence[str]] = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, - multivariate_plot: bool = False, + component_wise: bool = False, **score_kwargs, ): """Plot the results of the anomaly model. @@ -507,7 +507,7 @@ def show_anomalies( Optionally, the name of the metric function to use. Must be one of "AUC_ROC" (Area Under the Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". - multivariate_plot + component_wise If True, it will separately plot each component in multivariate series. score_kwargs parameters for the `score()` method. @@ -530,7 +530,7 @@ def show_anomalies( names_of_scorers=names_of_scorers, title=title, metric=metric, - multivariate_plot=multivariate_plot, + component_wise=component_wise, **score_kwargs, ) diff --git a/darts/ad/scorers/scorers.py b/darts/ad/scorers/scorers.py index 7e935c56dc..f5887c8314 100644 --- a/darts/ad/scorers/scorers.py +++ b/darts/ad/scorers/scorers.py @@ -176,7 +176,7 @@ def show_anomalies_from_prediction( anomalies: TimeSeries = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, - multivariate_plot: bool = False, + component_wise: bool = False, ): """Plot the results of the scorer. @@ -209,7 +209,7 @@ def show_anomalies_from_prediction( Optionally, the name of the metric function to use. Must be one of "AUC_ROC" (Area Under the Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". - multivariate_plot + component_wise If True, it will separately plot each component in multivariate series. """ series = _check_input(series, name="series", num_series_expected=1)[0] @@ -233,7 +233,7 @@ def show_anomalies_from_prediction( names_of_scorers=scorer_name, title=title, metric=metric, - multivariate_plot=multivariate_plot, + component_wise=component_wise, ) @property @@ -584,7 +584,7 @@ def show_anomalies( scorer_name: str = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, - multivariate_plot: bool = False, + component_wise: bool = False, ): """Plot the results of the scorer. @@ -615,7 +615,7 @@ def show_anomalies( Optionally, the name of the metric function to use. Must be one of "AUC_ROC" (Area Under the Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". - multivariate_plot + component_wise If True, it will separately plot each component in multivariate series. """ series = _check_input(series, name="series", num_series_expected=1)[0] @@ -640,7 +640,7 @@ def show_anomalies( names_of_scorers=scorer_name, title=title, metric=metric, - multivariate_plot=multivariate_plot, + component_wise=component_wise, ) @property diff --git a/darts/ad/utils.py b/darts/ad/utils.py index bc3588cb77..f338d54b79 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -311,7 +311,7 @@ def show_anomalies_from_scores( names_of_scorers: Union[str, Sequence[str]] = None, title: str = None, metric: Optional[Literal["AUC_ROC", "AUC_PR"]] = None, - multivariate_plot: bool = False, + component_wise: bool = False, ): """Plot the results generated by an anomaly model. @@ -353,14 +353,14 @@ def show_anomalies_from_scores( Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Only effective when `pred_scores` is not `None`. Default: "AUC_ROC". - multivariate_plot + component_wise If True, it will separately plot each component in multivariate series. """ series = _check_input( series, name="series", num_series_expected=1, - check_multivariate=multivariate_plot, + check_multivariate=component_wise, )[0] if title is None and pred_scores is not None: @@ -433,30 +433,30 @@ def show_anomalies_from_scores( name="pred_series", width_expected=series_width, num_series_expected=1, - check_multivariate=multivariate_plot, + check_multivariate=component_wise, )[0] - if anomalies is not None and multivariate_plot: + if anomalies is not None and component_wise: anomalies = _check_input( anomalies, name="anomalies", width_expected=series_width, num_series_expected=1, check_binary=True, - check_multivariate=multivariate_plot, + check_multivariate=component_wise, )[0] - if pred_scores is not None and multivariate_plot: + if pred_scores is not None and component_wise: for pred_score in pred_scores: _ = _check_input( pred_score, name="pred_score", width_expected=series_width, num_series_expected=1, - check_multivariate=multivariate_plot, + check_multivariate=component_wise, )[0] - plots_per_ts = nbr_plots * series_width if multivariate_plot else nbr_plots + plots_per_ts = nbr_plots * series_width if component_wise else nbr_plots fig, axs = plt.subplots( plots_per_ts, figsize=(8, 4 * (plots_per_ts // nbr_plots) + 2 * (nbr_plots - 1)), @@ -468,8 +468,8 @@ def show_anomalies_from_scores( layout="constrained", ) - for i in range(series_width if multivariate_plot else 1): - if multivariate_plot: + for i in range(series_width if component_wise else 1): + if component_wise: series_ = series[series.components[i]] anomalies_ = ( anomalies[anomalies.components[i]] if anomalies is not None else None From 95dd225faa91bef62595ec0066ec65e21ded9d68 Mon Sep 17 00:00:00 2001 From: dennisbader Date: Wed, 8 Jan 2025 14:40:38 +0100 Subject: [PATCH 14/14] make title fit better --- CHANGELOG.md | 2 +- darts/ad/anomaly_model/anomaly_model.py | 2 +- darts/ad/anomaly_model/forecasting_am.py | 2 +- darts/ad/scorers/scorers.py | 4 +- darts/ad/utils.py | 63 +++++++++++------------- 5 files changed, 34 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 500c10a09f..1e2ac21663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ but cannot always guarantee backwards compatibility. Changes that may **break co **Improved** - New model: `StatsForecastAutoTBATS`. This model offers the [AutoTBATS](https://nixtlaverse.nixtla.io/statsforecast/src/core/models.html#autotbats) model from Nixtla's `statsforecasts` library. [#2611](https://github.com/unit8co/darts/pull/2611) by [He Weilin](https://github.com/cnhwl). -- Added `multivariate_plot` parameter in `show_anomalies()` to separately plot each component in multivariate series. [#2544](https://github.com/unit8co/darts/pull/2544) by [He Weilin](https://github.com/cnhwl). +- Added parameter `component_wise` to `show_anomalies()` to separately plot each component in multivariate series. [#2544](https://github.com/unit8co/darts/pull/2544) by [He Weilin](https://github.com/cnhwl). **Fixed** - Fixed a bug when performing optimized historical forecasts with `stride=1` using a `RegressionModel` with `output_chunk_shift>=1` and `output_chunk_length=1`, where the forecast time index was not properly shifted. [#2634](https://github.com/unit8co/darts/pull/2634) by [Mattias De Charleroy](https://github.com/MattiasDC). diff --git a/darts/ad/anomaly_model/anomaly_model.py b/darts/ad/anomaly_model/anomaly_model.py index 1b13d0553a..63655db40c 100644 --- a/darts/ad/anomaly_model/anomaly_model.py +++ b/darts/ad/anomaly_model/anomaly_model.py @@ -285,7 +285,7 @@ def show_anomalies( score_kwargs parameters for the `score()` method. component_wise - If True, it will separately plot each component in multivariate series. + If True, will separately plot each component in case of multivariate anomaly detection. """ series = _check_input(series, name="series", num_series_expected=1)[0] predict_kwargs = predict_kwargs if predict_kwargs is not None else {} diff --git a/darts/ad/anomaly_model/forecasting_am.py b/darts/ad/anomaly_model/forecasting_am.py index 318fe3361a..8b4339cd9c 100644 --- a/darts/ad/anomaly_model/forecasting_am.py +++ b/darts/ad/anomaly_model/forecasting_am.py @@ -508,7 +508,7 @@ def show_anomalies( Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". component_wise - If True, it will separately plot each component in multivariate series. + If True, will separately plot each component in case of multivariate anomaly detection. score_kwargs parameters for the `score()` method. """ diff --git a/darts/ad/scorers/scorers.py b/darts/ad/scorers/scorers.py index f5887c8314..3fadee463a 100644 --- a/darts/ad/scorers/scorers.py +++ b/darts/ad/scorers/scorers.py @@ -210,7 +210,7 @@ def show_anomalies_from_prediction( Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". component_wise - If True, it will separately plot each component in multivariate series. + If True, will separately plot each component in case of multivariate anomaly detection. """ series = _check_input(series, name="series", num_series_expected=1)[0] pred_series = _check_input( @@ -616,7 +616,7 @@ def show_anomalies( Receiver Operating Characteristic Curve) and "AUC_PR" (Average Precision from scores). Default: "AUC_ROC". component_wise - If True, it will separately plot each component in multivariate series. + If True, will separately plot each component in case of multivariate anomaly detection. """ series = _check_input(series, name="series", num_series_expected=1)[0] pred_scores = self.score(series) diff --git a/darts/ad/utils.py b/darts/ad/utils.py index f338d54b79..4395afdfeb 100644 --- a/darts/ad/utils.py +++ b/darts/ad/utils.py @@ -354,7 +354,7 @@ def show_anomalies_from_scores( Only effective when `pred_scores` is not `None`. Default: "AUC_ROC". component_wise - If True, it will separately plot each component in multivariate series. + If True, will separately plot each component in case of multivariate anomaly detection. """ series = _check_input( series, @@ -457,15 +457,13 @@ def show_anomalies_from_scores( )[0] plots_per_ts = nbr_plots * series_width if component_wise else nbr_plots + height_ratios = ([2] + [1] * (nbr_plots - 1)) * (plots_per_ts // nbr_plots) + height_total = 2 * sum(height_ratios) fig, axs = plt.subplots( - plots_per_ts, - figsize=(8, 4 * (plots_per_ts // nbr_plots) + 2 * (nbr_plots - 1)), + nrows=plots_per_ts, + figsize=(8, height_total), sharex=True, - gridspec_kw={ - "height_ratios": ([2] + [1] * (nbr_plots - 1)) * (plots_per_ts // nbr_plots) - }, - squeeze=False, - layout="constrained", + gridspec_kw={"height_ratios": height_ratios}, ) for i in range(series_width if component_wise else 1): @@ -500,9 +498,13 @@ def show_anomalies_from_scores( metric=metric, axs=axs, index_ax=i * nbr_plots, - nbr_plots=nbr_plots, ) - fig.suptitle(title) + # make title fit nicely on plot + title_height = 0.1 + title_y = 1 - title_height / height_total + + fig.suptitle(title, y=title_y) + fig.tight_layout() def _assert_binary(series: TimeSeries, name: str): @@ -774,7 +776,6 @@ def _plot_series_and_anomalies( metric: str, axs: plt.Axes, index_ax: int, - nbr_plots: int, ): """Helper function to plot series and anomalies. @@ -798,25 +799,23 @@ def _plot_series_and_anomalies( The axes to plot on. index_ax The index of the current axis. - nbr_plots - The number of plots. """ - _plot_series(series=series, ax_id=axs[index_ax][0], linewidth=0.5, label_name="") + _plot_series(series=series, ax_id=axs[index_ax], linewidth=0.5, label_name="") if pred_series is not None: _plot_series( series=pred_series, - ax_id=axs[index_ax][0], + ax_id=axs[index_ax], linewidth=0.5, label_name="model output", ) - axs[index_ax][0].set_title("") + axs[index_ax].set_title("") if anomalies is not None or pred_scores is not None: - axs[index_ax][0].set_xlabel("") + axs[index_ax].set_xlabel("") - axs[index_ax][0].legend(loc="upper center", bbox_to_anchor=(0.5, 1.1), ncol=2) + axs[index_ax].legend(loc="upper center", bbox_to_anchor=(0.5, 1.1), ncol=2) if pred_scores is not None: dict_input = {} @@ -858,33 +857,29 @@ def _plot_series_and_anomalies( _plot_series( series=elem[1]["series_score"], - ax_id=axs[index_ax][0], + ax_id=axs[index_ax], linewidth=0.5, label_name=label, ) - axs[index_ax][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.19), ncol=2 - ) - axs[index_ax][0].set_title(f"Window: {str(w)}", loc="left") - axs[index_ax][0].set_title("") - axs[index_ax][0].set_xlabel("") + axs[index_ax].legend(loc="upper center", bbox_to_anchor=(0.5, 1.19), ncol=2) + axs[index_ax].set_title(f"Window: {str(w)}", loc="left") + axs[index_ax].set_title("") + axs[index_ax].set_xlabel("") if anomalies is not None: _plot_series( series=anomalies, - ax_id=axs[index_ax + 1][0], + ax_id=axs[index_ax + 1], linewidth=1, label_name="anomalies", color="red", ) - axs[index_ax + 1][0].set_title("") - axs[index_ax + 1][0].set_ylim([-0.1, 1.1]) - axs[index_ax + 1][0].set_yticks([0, 1]) - axs[index_ax + 1][0].set_yticklabels(["no", "yes"]) - axs[index_ax + 1][0].legend( - loc="upper center", bbox_to_anchor=(0.5, 1.2), ncol=2 - ) + axs[index_ax + 1].set_title("") + axs[index_ax + 1].set_ylim([-0.1, 1.1]) + axs[index_ax + 1].set_yticks([0, 1]) + axs[index_ax + 1].set_yticklabels(["no", "yes"]) + axs[index_ax + 1].legend(loc="upper center", bbox_to_anchor=(0.5, 1.2), ncol=2) else: - axs[index_ax][0].set_xlabel("timestamp") + axs[index_ax].set_xlabel("timestamp")