Coverage for uqmodels / visualization / visualization.py: 61%
262 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-09 08:15 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-09 08:15 +0000
1"""
2Visualization module.
3"""
5import matplotlib as mpl
6import matplotlib.pyplot as plt
7import numpy as np
9import uqmodels.postprocessing.UQ_processing as UQ_proc
10from uqmodels.check import dim_1d_check
11import uqmodels.visualization.aux_visualization as auxvisu
12from uqmodels.visualization.aux_visualization import provide_cmap # noqa: F401
13from uqmodels.visualization.old_visualisation import plot_prediction_interval, plot_sorted_pi # noqa: F401
14from uqmodels.visualization.old_visualisation import visu_latent_space, show_dUQ_refinement # noqa: F401
16plt.rcParams["figure.figsize"] = [8, 8]
17plt.rcParams["font.family"] = "sans-serif"
18plt.rcParams["ytick.labelsize"] = 15
19plt.rcParams["xtick.labelsize"] = 15
20plt.rcParams["axes.labelsize"] = 15
21plt.rcParams["legend.fontsize"] = 15
24def plot_pi(
25 y,
26 y_pred,
27 y_pred_lower,
28 y_pred_upper,
29 mode_res=False,
30 f_obs=None,
31 X=None,
32 size=(12, 2),
33 name=None,
34 show_plot=True,
35 config=None,
36 ylim=None,
37 xlim=None,
38 **kwargs,
39):
40 """
41 Plot prediction intervals (PI) together with observations and predictions.
43 Displays observed values, predicted values, and their prediction interval
44 (upper/lower bounds). Optionally plots residuals instead of absolute values
45 (mode_res=True). Observations falling outside the PI are highlighted.
46 Parameter f_obs selects which observation indices to display.
47 """
49 # Sous-configs avec defaults
50 cfg_pred = (config.get("prediction") if config else {}) or {}
51 cfg_inside = (config.get("inside") if config else {}) or {}
52 cfg_outside = (config.get("outside") if config else {}) or {}
53 cfg_pi = (config.get("pi") if config else {}) or {}
54 cfg_axes = (config.get("axes") if config else {}) or {}
56 # Coercions
57 y = dim_1d_check(y)
58 if y_pred is not None:
59 y_pred = dim_1d_check(y_pred)
60 if y_pred_upper is not None:
61 y_pred_upper = dim_1d_check(y_pred_upper)
62 if y_pred_lower is not None:
63 y_pred_lower = dim_1d_check(y_pred_lower)
65 # x-scale
66 if X is None:
67 x = np.arange(len(y))
68 else:
69 x = dim_1d_check(X)
71 # Indices observés
72 if f_obs is None:
73 f_obs = np.arange(len(y))
74 else:
75 f_obs = np.asarray(f_obs)
77 x_plot = x[f_obs]
79 fig, ax = plt.subplots(figsize=size)
81 # Prediction (+ mode résidus)
82 if y_pred is not None:
83 if mode_res:
84 y = y - y_pred
85 if y_pred_upper is not None:
86 y_pred_upper = y_pred_upper - y_pred
87 if y_pred_lower is not None:
88 y_pred_lower = y_pred_lower - y_pred
89 y_pred = y_pred * 0.0
91 pred_cfg = {
92 "color": cfg_pred.get("color", "black"),
93 "linestyle": cfg_pred.get("linestyle", cfg_pred.get("ls", "-")),
94 "linewidth": cfg_pred.get("linewidth", cfg_pred.get("lw", 1.0)),
95 "label": cfg_pred.get("label", "Prediction"),
96 "zorder": cfg_pred.get("zorder", None),
97 }
98 auxvisu.aux_plot_line(ax, x_plot, y_pred[f_obs], config=pred_cfg)
100 if name is not None:
101 ax.set_title(name)
103 # Observations (inside PI)
104 inside_cfg = {
105 "color": cfg_inside.get("color", "darkgreen"),
106 "marker": cfg_inside.get("marker", "X"),
107 "markersize": cfg_inside.get("markersize", 2),
108 "linestyle": "none",
109 "label": cfg_inside.get("label", "Observation (inside PI)"),
110 "zorder": cfg_inside.get("zorder", 20),
111 }
112 auxvisu.aux_plot_line(ax, x_plot, y[f_obs], config=inside_cfg)
114 ax.set_xlabel("X")
115 ax.set_ylabel("Y")
117 # PI + anomalies
118 if y_pred_upper is not None and y_pred_lower is not None:
119 anom = (y[f_obs] > y_pred_upper[f_obs]) | (y[f_obs] < y_pred_lower[f_obs])
121 # Observations outside PI
122 if np.any(anom):
123 outside_cfg = {
124 "color": cfg_outside.get("color", "red"),
125 "marker": cfg_outside.get("marker", "o"),
126 "markersize": cfg_outside.get("markersize", 2),
127 "linestyle": "none",
128 "label": cfg_outside.get("label", "Observation (outside PI)"),
129 "zorder": cfg_outside.get("zorder", 21),
130 }
131 auxvisu.aux_plot_line(ax, x_plot[anom], y[f_obs][anom], config=outside_cfg)
133 # Bornes du PI
134 pi_line_cfg = {
135 "color": cfg_pi.get("color", "blue"),
136 "linestyle": cfg_pi.get("linestyle", cfg_pi.get("ls", "--")),
137 "linewidth": cfg_pi.get("linewidth", cfg_pi.get("lw", 1)),
138 "zorder": cfg_pi.get("zorder_line", None),
139 }
140 auxvisu.aux_plot_line(ax, x_plot, y_pred_upper[f_obs], config=pi_line_cfg)
141 auxvisu.aux_plot_line(ax, x_plot, y_pred_lower[f_obs], config=pi_line_cfg)
143 # Zone PI
144 auxvisu.aux_fill_area(
145 ax,
146 x_plot,
147 y_pred_lower[f_obs],
148 y_pred_upper[f_obs],
149 config={
150 "color": cfg_pi.get("fill_color", cfg_pi.get("color", "blue")),
151 "alpha": cfg_pi.get("alpha_fill", 0.2),
152 "label": cfg_pi.get("label", "Prediction Interval"),
153 },
154 )
156 # Données à considérer pour les limites
157 y_for_axes = [y[f_obs]]
158 if y_pred_lower is not None and y_pred_upper is not None:
159 y_for_axes.extend([y_pred_lower[f_obs], y_pred_upper[f_obs]])
160 if y_pred is not None:
161 y_for_axes.append(y_pred[f_obs])
163 # Gestion des axes via helper (inclut x_lim)
164 auxvisu.aux_adjust_axes(
165 ax,
166 x_plot,
167 y_for_axes,
168 ylim=ylim,
169 x_lim=xlim,
170 margin=cfg_axes.get("margin", 0.05),
171 x_margin=cfg_axes.get("x_margin", 0.5),
172 )
174 ax.legend(loc="best")
175 if show_plot:
176 plt.show()
178# ---------- Orchestrateur principal ----------
181def plot_anom_matrice(
182 score,
183 score2=None,
184 f_obs=None,
185 true_label=None,
186 data=None,
187 x=None,
188 vmin=-3,
189 vmax=3,
190 cmap=None,
191 list_anom_ind=None,
192 figsize=(15, 6),
193 grid_spec=None,
194 x_date=False,
195 show_plot=True,
196 setup=None,
197):
198 """
199 Visualize anomaly score matrices and optional ground-truth labels or data.
201 This function plots one or several anomaly score matrices (e.g., per model or
202 per transformation), an optional secondary anomaly score matrix, optional
203 ground-truth anomaly labels, and optional multichannel time series data.
204 It supports contextual segmentation, date-based x-axes, sensor/channel
205 structural overlays, and anomaly highlighting. The function preserves its
206 original API while delegating rendering to modular helpers.
208 Parameters
209 ----------
210 score : array-like or list of array-like
211 Primary anomaly score matrix or list of matrices.
212 Each matrix must be of shape (n_samples, n_features).
213 score2 : array-like, optional
214 Secondary anomaly score matrix of shape (n_samples, n_features).
215 f_obs : array-like, optional
216 Indices of samples to visualize; defaults to all.
217 true_label : array-like, optional
218 Ground-truth anomaly labels of shape (n_samples, n_features).
219 data : array-like, optional
220 Multichannel time series of shape (n_samples, n_features), used for
221 overlaying raw data and score-based anomaly markers.
222 x : array-like, optional
223 X-axis values. If None, integer indices are used. If datetime-like,
224 the function automatically switches to a date axis.
225 vmin, vmax : float, default=(-3, 3)
226 Color limits for the anomaly score colormap.
227 cmap : Colormap, optional
228 Colormap for score matrices. If None, a default diverging map is used.
229 list_anom_ind : list of int, optional
230 Indices of features/sensors to highlight in the time-series panel.
231 figsize : tuple, default=(15, 6)
232 Figure size in inches.
233 grid_spec : array-like, optional
234 Height ratios for subplot layout. If None, all subplots have equal height.
235 x_date : bool, default=False
236 If True, the x-axis is formatted as a date axis (dd/mm HH:MM).
237 show_plot : bool, default=True
238 Whether to display the resulting figure.
239 setup : tuple, optional
240 Tuple (n_channel_per_sensor, n_sensor) enabling structural overlays
241 (horizontal grid lines) on score matrices for multi-sensor setups.
243 Notes
244 -----
245 - The function supports:
246 * multiple score matrices displayed in stacked subplots,
247 * contextual slicing when `x` contains datetime values,
248 * ground-truth anomaly maps,
249 * multichannel data with anomaly highlighting,
250 * optional highlighting of anomalous sensor indices.
251 - Rendering is internally modularized via helper functions to improve
252 clarity and maintainability, while keeping the public API identical.
254 Returns
255 -------
256 None
257 The function creates the figure and optionally displays it.
258 """
260 # 1) Normalisation des entrées score / f_obs / cmap
261 score_list, len_score, dim_score, n_score, f_obs, cmap = (
262 auxvisu.aux_norm_score_inputs(score, f_obs=f_obs, cmap=cmap)
263 )
265 # 2) Préparation de x et des extents pour imshow
266 x, x_flag, list_extent = auxvisu.aux_prepare_x_extent(x, f_obs, dim_score)
268 # 3) Paramètres de layout
269 n_fig, sharey, grid_spec = auxvisu.aux_compute_layout_params(
270 n_score, dim_score, true_label, score2, data, grid_spec=grid_spec
271 )
273 # 4) Création figure / axes
274 fig, ax = auxvisu.aux_create_score_figure(n_fig, sharey, grid_spec, figsize)
276 # 5) Cas simple : une seule figure (n_fig == 1)
277 if n_fig == 1:
278 # un seul score, pas de true_label / score2 / data
279 auxvisu.aux_plot_score_matrix(
280 ax,
281 score_list[0],
282 f_obs,
283 list_extent[0],
284 vmin,
285 vmax,
286 cmap,
287 title="score",
288 )
289 auxvisu.aux_overlay_setup_grid(ax, setup, len(f_obs))
291 auxvisu.aux_finalize_figure(fig, show_plot=show_plot)
292 return
294 # 6) Cas général : plusieurs panneaux
295 ind_ax = -1
297 # 6.1: matrices de score (un axe par score principal)
298 for n, score_ in enumerate(score_list):
299 ind_ax += 1
300 auxvisu.aux_plot_score_matrix(
301 ax[ind_ax],
302 score_,
303 f_obs,
304 list_extent[n],
305 vmin,
306 vmax,
307 cmap,
308 title="score",
309 )
311 # 6.2: seconde matrice de score
312 if score2 is not None:
313 ind_ax += 1
314 auxvisu.aux_plot_score_matrix(
315 ax[ind_ax],
316 score2,
317 f_obs,
318 list_extent[0],
319 vmin,
320 vmax,
321 cmap,
322 title="score",
323 )
325 # 6.3: matrice de labels
326 if true_label is not None:
327 ind_ax += 1
328 auxvisu.aux_plot_true_label_matrix(
329 ax[ind_ax],
330 true_label,
331 f_obs,
332 list_extent[0],
333 )
335 # 6.4: données temporelles + anomalies
336 last_ax = None
337 if data is not None:
338 ind_ax += 1
339 last_ax = ax[ind_ax]
340 last_ax.set_title("data")
342 colors = auxvisu.aux_build_data_colors(data, list_anom_ind=list_anom_ind)
343 auxvisu.aux_plot_data_timeseries(
344 last_ax,
345 x,
346 data,
347 f_obs,
348 dim_score[0],
349 colors,
350 lw=0.9,
351 )
352 # anomalies basées sur score[0]
353 auxvisu.aux_overlay_score_anoms_on_data(
354 last_ax,
355 x,
356 data,
357 np.abs(score_list[0]),
358 f_obs,
359 dim_score[0],
360 threshold=1.0,
361 )
363 # 6.5: overlay des labels vrais sur les données
364 if (true_label is not None) and (data is not None) and (last_ax is not None):
365 auxvisu.aux_overlay_true_label_on_data(last_ax, x, data, true_label, f_obs)
367 # 6.6: axe temps (sur le dernier axe utilisé)
368 if last_ax is None:
369 # si pas de data, on applique au dernier axe utilisé pour les matrices
370 last_ax = ax[ind_ax]
371 auxvisu.aux_format_time_axis(last_ax, x_flag=x_flag, x_date=x_date)
373 # 7) Grille éventuelle sur le premier axe (comme avant, uniquement dans le cas n_fig==1)
374 # -> si tu veux aussi la grille quand n_fig > 1, tu peux l’activer ici
375 # if setup is not None:
376 # aux_overlay_setup_grid(ax[0], setup, len(f_obs))
378 # 8) Finalisation figure
379 auxvisu.aux_finalize_figure(fig, show_plot=show_plot)
382def uncertainty_plot(
383 y,
384 output,
385 context=None,
386 size=(15, 5),
387 f_obs=None,
388 name="UQplot",
389 mode_res=False,
390 born=None,
391 born_bis=None,
392 dim=0,
393 confidence_lvl=None,
394 list_percent=[0.8, 0.9, 0.99, 0.999, 1],
395 env=[0.95, 0.65],
396 type_UQ="old",
397 show_plot=True,
398 with_colorbar=False,
399 **kwarg,
400):
401 """
402 Visualize uncertainty diagnostics for multivariate predictive models.
404 This function plots observations, predictions, prediction intervals,
405 aleatoric/epistemic uncertainty contributions, confidence-level scores,
406 optional anomaly bounds, and context-based segmentations. It supports
407 multi-output signals, multiple contextual partitions, residual mode, and
408 both full UQ views and data-only views. The function preserves the original
409 API and integrates with modular visualization helpers (aux_*).
411 Parameters
412 ----------
413 y : array-like
414 Ground-truth observations of shape (n_samples, n_dim).
415 output : tuple or None
416 UQ model outputs. Either (pred, var_A, var_E) or (pred, (var_A, var_E)),
417 depending on `type_UQ`. Set to None in data-only mode.
418 context : array-like, optional
419 Context matrix used for splitting the plot by contextual dimension or
420 highlighting contextual regions.
421 size : tuple, default=(15, 5)
422 Figure size in inches.
423 f_obs : array-like, optional
424 Indices of samples to display; defaults to all.
425 name : str, default="UQplot"
426 Figure suptitle.
427 mode_res : bool, default=False
428 If True, plot residuals instead of raw values.
429 born : tuple of array-like, optional
430 Lower and upper anomaly bounds for each dimension.
431 born_bis : tuple of array-like, optional
432 Secondary set of anomaly bounds.
433 dim : int or list of int, default=0
434 Target output dimensions to visualize.
435 confidence_lvl : array-like, optional
436 Precomputed confidence-level matrix. If None, it is computed internally.
437 list_percent : list of float, default=[0.8, 0.9, 0.99, 0.999, 1]
438 Confidence thresholds used to compute epistemic confidence levels.
439 env : list of float, default=[0.95, 0.65]
440 Default uncertainty envelopes for plotting.
441 type_UQ : {"old", "var_A&E"}, default="old"
442 Format specification of `output`.
443 show_plot : bool, default=True
444 Whether to display the final figure.
445 with_colorbar : bool, default=False
446 Whether to add a confidence-level colorbar.
447 **kwarg :
448 Additional parameters, including:
449 - "ind_ctx": context values to include,
450 - "split_ctx": context dimension used for splitting subplots,
451 - "ylim": vertical limits,
452 - "var_min": minimum (var_A, var_E) values,
453 - "only_data": disable UQ & plot observations only,
454 - "x": explicit x-axis values,
455 - "ctx_attack": tuple defining contextual highlight rules,
456 - "list_name_subset": labels for contextual annotations.
458 Notes
459 -----
460 - This function acts as a high-level orchestrator and delegates rendering
461 to modular visualization helpers (aux_plot_confiance, aux_plot_conf_score,
462 aux_plot_line, aux_fill_between, etc.).
463 - The input API is preserved for backward compatibility.
465 Returns
466 -------
467 None
468 The function creates a figure and optionally displays it.
469 """
471 if f_obs is None:
472 f_obs = np.arange(len(y))
473 f_obs = np.asarray(f_obs)
475 # --- Extraction des options depuis kwarg (API inchangée) ---
476 ind_ctx = kwarg.get("ind_ctx", None)
477 split_ctx = kwarg.get("split_ctx", -1)
478 ylim = kwarg.get("ylim", None)
479 # compare_deg était lu mais jamais utilisé -> no-op conservé implicitement
481 # Bornes mini sur var_A, var_E
482 min_A, min_E = kwarg.get("var_min", (1e-6, 1e-6))
484 # Only data / subset de contexte (pour ctx_attack)
485 only_data = kwarg.get("only_data", False)
486 list_name_subset = kwarg.get("list_name_subset", None)
487 ctx_attack = kwarg.get("ctx_attack", None)
489 if only_data:
490 name = "Data"
492 # Support x
493 if "x" in kwarg:
494 x = kwarg["x"]
495 else:
496 x = np.arange(len(y))
497 x = np.asarray(x)
499 # --- Préparation de pred, var_A, var_E selon type_UQ ---
500 pred = var_A = var_E = None
501 if output is not None:
502 if type_UQ == "old":
503 pred, var_A, var_E = output
504 elif type_UQ == "var_A&E":
505 pred, (var_A, var_E) = output
506 else:
507 raise ValueError(f"Unknown type_UQ '{type_UQ}'.")
509 var_A = np.asarray(var_A)
510 var_E = np.asarray(var_E)
511 var_E[var_E < min_E] = min_E
512 var_A[var_A < min_A] = min_A
514 # --- Dimensions & contextes ---
515 f_obs_full = np.copy(f_obs)
517 if isinstance(dim, int):
518 dim_list = [dim]
519 else:
520 dim_list = list(dim)
521 n_dim = len(dim_list)
523 n_ctx = 1
524 list_ctx_ = [None]
525 if split_ctx > -1 and context is not None:
526 if ind_ctx is None:
527 list_ctx_ = list(set(context[f_obs, split_ctx]))
528 else:
529 list_ctx_ = ind_ctx
530 n_ctx = len(list_ctx_)
532 # --- Figure et axes ---
533 fig, axs = plt.subplots(n_dim, n_ctx, sharex=True, figsize=size)
535 # --- Confidence level : si non fourni, calcul global une fois ---
536 if (confidence_lvl is None) and (output is not None):
537 confidence_lvl, _ = UQ_proc.compute_Epistemic_score(
538 (var_A, var_E),
539 type_UQ="var_A&E",
540 pred=pred,
541 list_percent=list_percent,
542 params_=None,
543 )
544 # sinon : on utilise la matrice fournie en argument
546 label = None # servira pour la colorbar éventuelle
548 # --- Boucle principale : dimensions x contextes ---
549 for idx_dim, d in enumerate(dim_list):
550 for idx_ctx in range(n_ctx):
551 ax = auxvisu._get_panel_ax(axs, n_dim, n_ctx, idx_dim, idx_ctx)
553 # Filtrage par contexte si demandé
554 if split_ctx > -1 and context is not None:
555 mask_ctx = context[f_obs_full, split_ctx] == list_ctx_[idx_ctx]
556 f_obs_ctx = f_obs_full[mask_ctx]
557 else:
558 f_obs_ctx = f_obs_full
560 if f_obs_ctx.size == 0:
561 continue
563 # Mode "only_data" : pas d'UQ, juste la série + scatter
564 if only_data:
565 # scatter
566 auxvisu.aux_plot_line(
567 ax,
568 f_obs_ctx,
569 y[f_obs_ctx, d],
570 config={
571 "color": "black",
572 "marker": "x",
573 "markersize": 10,
574 "linestyle": "none",
575 "linewidth": 1.0,
576 "label": "observation",
577 },
578 )
579 # ligne pointillée
580 auxvisu.aux_plot_line(
581 ax,
582 f_obs_ctx,
583 y[f_obs_ctx, d],
584 config={
585 "color": "darkgreen",
586 "linestyle": ":",
587 "linewidth": 0.7,
588 "alpha": 1.0,
589 "zorder": -4,
590 },
591 )
592 if ylim is not None:
593 ax.set_ylim(ylim[0], ylim[1])
595 else:
596 # Préparation bornes par dimension
597 born_ = None
598 if born is not None:
599 born_ = (born[0][f_obs_ctx, d], born[1][f_obs_ctx, d])
601 born_bis_ = None
602 if born_bis is not None:
603 born_bis_ = (born_bis[0][f_obs_ctx, d], born_bis[1][f_obs_ctx, d])
605 # Appel au helper UQ principal
606 auxvisu.aux_plot_confiance(
607 ax=ax,
608 y=y[f_obs_ctx, d],
609 pred=pred[f_obs_ctx, d],
610 var_A=var_A[f_obs_ctx, d],
611 var_E=var_E[f_obs_ctx, d],
612 born=born_,
613 born_bis=born_bis_,
614 env=env,
615 x=x[f_obs_ctx],
616 mode_res=mode_res,
617 **kwarg,
618 )
620 # Scores de confiance & legends
621 if confidence_lvl is not None:
622 label = [str(i) for i in list_percent]
623 label.append(">1")
624 auxvisu.aux_plot_conf_score(
625 ax,
626 x[f_obs_ctx],
627 pred[f_obs_ctx, d],
628 confidence_lvl[f_obs_ctx, d],
629 label=label,
630 mode_res=mode_res,
631 )
633 # Overlay de contexte "attaque" si demandé
634 if ctx_attack is not None:
635 # ctx_attack = (dim_ctx, ctx_val)
636 dim_ctx, ctx_val = ctx_attack
637 ylim_local = ylim
638 if ylim_local is None:
639 ylim_local = (y.min(), y.max())
641 if ctx_val == -1:
642 # coloration par catégories de contexte
643 list_ctx = list(set(context[f_obs_ctx, dim_ctx]))
644 if list_name_subset is None:
645 list_name_subset = list_ctx
646 cmap_ctx = plt.get_cmap("jet", len(list_name_subset))
647 for i in list_ctx:
648 mask_ctx_val = context[f_obs_ctx, dim_ctx] == i
649 auxvisu.aux_fill_between(
650 ax,
651 f_obs_ctx,
652 np.full_like(f_obs_ctx, ylim_local[0], dtype=float),
653 np.full_like(f_obs_ctx, ylim_local[1], dtype=float),
654 where=mask_ctx_val,
655 config={
656 "color": cmap_ctx(int(i)),
657 "alpha": 0.2,
658 },
659 )
660 else:
661 # un seul contexte mis en évidence
662 mask_ctx_val = context[f_obs_ctx, dim_ctx] == 1
663 auxvisu.aux_fill_between(
664 ax,
665 f_obs_ctx,
666 np.full_like(f_obs_ctx, ylim_local[0], dtype=float),
667 np.full_like(f_obs_ctx, ylim_local[1], dtype=float),
668 where=mask_ctx_val,
669 config={
670 "color": "yellow",
671 "alpha": 0.2,
672 },
673 )
675 # Masquer les ticks y pour les contextes > 0 (comme avant)
676 if idx_ctx != 0:
677 ax.set_yticklabels([])
679 # Légende "fantôme" pour les ctx_attack (list_name_subset)
680 if ctx_attack is not None and list_name_subset is not None:
681 ylim_local = ylim
682 if ylim_local is None:
683 ylim_local = (y.min(), y.max())
684 cmap_ctx = plt.get_cmap("jet", len(list_name_subset))
685 for i in range(len(list_name_subset)):
686 auxvisu.aux_fill_between(
687 ax,
688 np.array([]),
689 np.array([]),
690 np.array([]),
691 where=None,
692 config={
693 "color": cmap_ctx(i),
694 "alpha": 0.08,
695 "label": list_name_subset[int(i)],
696 },
697 )
699 # --- Mise en forme globale : titre / layout / légende / colorbar ---
700 plt.suptitle(name)
701 plt.subplots_adjust(
702 wspace=0.03, hspace=0.03, left=0.1, bottom=0.22, right=0.90, top=0.8
703 )
704 plt.legend(frameon=True, ncol=6, fontsize=10, bbox_to_anchor=(0.5, 0, 0.38, -0.11))
706 # Colorbar pour les niveaux de confiance
707 if label is not None:
708 cmap_vals = [plt.get_cmap("RdYlGn_r", 7)(i) for i in np.arange(len(label))]
709 bounds = np.concatenate(
710 [[0], np.cumsum(np.abs(np.array(list_percent) - 1) + 0.1)]
711 )
712 bounds = 10 * bounds / bounds.max()
714 if with_colorbar:
715 cmap_cb = mpl.colors.ListedColormap(cmap_vals)
716 norm = mpl.colors.BoundaryNorm(bounds, cmap_cb.N)
717 color_ls = mpl.cm.ScalarMappable(norm=norm, cmap=cmap_cb)
718 cbar1 = plt.colorbar(
719 color_ls,
720 pad=0.20,
721 fraction=0.10,
722 shrink=0.5,
723 anchor=(0.2, 0.0),
724 orientation="horizontal",
725 spacing="proportional",
726 )
727 cbar1.set_label("Confidence_lvl", fontsize=14)
729 ticks = (bounds + np.roll(bounds, -1)) / 2
730 ticks[-1] = 10
731 cbar1.set_ticks(ticks)
732 cbar1.set_ticklabels(label, fontsize=12)
734 plt.tight_layout()
735 if show_plot:
736 plt.show()
737 return
740# Display of data curve with mean and variance.
743def aux_get_var_color_sets():
744 """
745 Return the color sets used for percentile envelope visualization
746 in `plot_var`.
748 Returns
749 -------
750 color_full : list of tuple
751 Colors for filled regions between percentile curves.
752 color_full2 : list of tuple
753 Colors for percentile boundary lines.
754 """
755 color_full = [
756 (0.5, 0.0, 0.5),
757 (0.8, 0, 0),
758 (0.8, 0.6, 0),
759 (0, 0.8, 0),
760 (0, 0.4, 0),
761 (0, 0.8, 0),
762 (0.8, 0.6, 0),
763 (0.8, 0, 0),
764 (0.5, 0.0, 0.5),
765 ]
767 color_full2 = [
768 (0.5, 0.0, 0.5),
769 (0.8, 0, 0),
770 (0.8, 0.6, 0),
771 (0, 0.8, 0),
772 (0, 0.4, 0),
773 (0, 0.4, 0),
774 (0, 0.8, 0),
775 (0.8, 0.6, 0),
776 (0.8, 0, 0),
777 (0.5, 0.0, 0.5),
778 ]
780 return color_full, color_full2
783def plot_var(
784 Y,
785 data_full,
786 variance,
787 impact_anom=None,
788 anom=None,
789 f_obs=None,
790 dim=(400, 20, 3),
791 g=0,
792 res_flag=False,
793 fig_s=(20, 3),
794 title=None,
795 ylim=None,
796):
797 """
798 Plot empirical variance envelopes around a univariate time series.
800 This function builds a set of percentile-based envelopes from the provided
801 variance and overlays them on the original (or residual) series together
802 with anomaly markers. It visualizes how the variance translates into
803 coverage levels for a given component of a multivariate signal.
805 Parameters
806 ----------
807 Y : array-like
808 Ground-truth series of shape (n_samples, n_dim).
809 data_full : array-like
810 Reference series used to construct the envelopes, same shape as Y.
811 variance : array-like
812 Point-wise variance of shape (n_samples, n_dim) for the selected
813 component.
814 impact_anom : array-like, optional
815 Anomaly impact indicator of shape (n_samples, n_dim). Non-zero entries
816 are flagged as anomalies.
817 anom : array-like, optional
818 Unused placeholder kept for backward compatibility.
819 f_obs : array-like, optional
820 Indices of samples to visualize. If None, all samples are used.
821 dim : tuple, default=(400, 20, 3)
822 Unused placeholder describing (n_samples, n_time, n_groups). Kept for
823 backward compatibility.
824 g : int, default=0
825 Index of the dimension (component) to plot.
826 res_flag : bool, default=False
827 If True, envelopes are computed around `data_full - data_full`
828 (i.e. residuals), otherwise around `data_full`.
829 fig_s : tuple, default=(20, 3)
830 Figure size in inches.
831 title : str, optional
832 Figure title.
833 ylim : tuple, optional
834 Manual y-axis limits (ymin, ymax). If None, limits are inferred from
835 the outer envelopes.
837 Returns
838 -------
839 per : list of np.ndarray
840 List of envelope curves (one array per percentile in `per_list`).
841 per_list : list of float
842 Percentile levels used to build the envelopes.
843 """
844 import scipy.stats as st
846 def add_noise(data, noise_mult, noise_add):
847 return (data * (1 + noise_mult)) + noise_add
849 # Indices observés
850 if f_obs is None:
851 f_obs = np.arange(Y.shape[0])
852 f_obs = np.asarray(f_obs)
854 step = g # dimension à tracer
856 # Figure / axe
857 fig, ax = plt.subplots(figsize=fig_s)
858 if title is not None:
859 ax.set_title(title)
861 # Série de base : résidus ou données brutes
862 res = data_full * 0
863 if res_flag:
864 res = data_full
866 # Définitions des niveaux et des couleurs
867 ni = [100, 98, 95, 80, 50]
868 color_full, color_full2 = aux_get_var_color_sets()
870 per_list = [0.01, 1, 2.5, 10, 25, 75, 90, 97.5, 99, 99.99]
871 per = []
873 # Construction des enveloppes (percentiles)
874 for p in per_list:
875 noise = st.norm.ppf(p / 100.0, loc=0.0, scale=np.sqrt(variance))
876 per.append(add_noise(data_full - res, 0.0, noise))
878 x_idx = np.arange(len(f_obs))
880 # Zones entre percentiles
881 for i in range(len(per) - 1):
882 auxvisu.aux_fill_area(
883 ax,
884 x_idx,
885 per[i][f_obs, step],
886 per[i + 1][f_obs, step],
887 config={
888 "color": color_full[i],
889 "alpha": 0.20,
890 "label": None,
891 },
892 )
894 # Courbes des percentiles + prototypes de légende
895 for i in range(len(per)):
896 auxvisu.aux_plot_line(
897 ax,
898 x_idx,
899 per[i][f_obs, step],
900 config={
901 "color": color_full2[i],
902 "linewidth": 0.5,
903 "alpha": 0.40,
904 },
905 )
906 if i > 4:
907 # Handles de légende "fantômes" pour les niveaux de couverture
908 auxvisu.aux_fill_area(
909 ax,
910 np.array([]),
911 np.array([]),
912 np.array([]),
913 config={
914 "color": color_full2[i],
915 "alpha": 0.20,
916 "label": f"{ni[9 - i]}% Coverage",
917 },
918 )
920 # Série observée
921 series = Y[f_obs, step] - res[f_obs, step]
922 auxvisu.aux_plot_line(
923 ax,
924 x_idx,
925 series,
926 config={
927 "color": "black",
928 "linewidth": 1.5,
929 "marker": "o",
930 "markersize": 3,
931 "label": "Series",
932 },
933 )
935 # Anomalies (impact_anom)
936 if impact_anom is not None:
937 flag = impact_anom[f_obs, step] != 0
938 if flag.any():
939 auxvisu.aux_plot_line(
940 ax,
941 x_idx[flag],
942 series[flag],
943 config={
944 "color": "red",
945 "marker": "X",
946 "markersize": 10,
947 "linestyle": "none",
948 "label": "Anom",
949 "alpha": 0.8,
950 },
951 )
953 # Gestion des axes (en s'appuyant sur aux_adjust_axes)
954 y_for_axes = [per[0][f_obs, step], per[-1][f_obs, step], series]
955 auxvisu.aux_adjust_axes(
956 ax,
957 x_idx,
958 y_for_axes,
959 ylim=ylim,
960 x_lim=(0, len(f_obs)),
961 margin=0.05,
962 x_margin=0.0,
963 )
965 ax.legend(ncol=7, fontsize=14)
966 fig.tight_layout()
967 plt.show()
969 return per, per_list