From e5049309c455cbc0d35b53eab0cd97c9a8694358 Mon Sep 17 00:00:00 2001 From: he weilin Date: Fri, 27 Sep 2024 11:18:09 +0800 Subject: [PATCH] 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):