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

1""" 

2Visualization module. 

3""" 

4 

5import matplotlib as mpl 

6import matplotlib.pyplot as plt 

7import numpy as np 

8 

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 

15 

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 

22 

23 

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. 

42 

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 """ 

48 

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 {} 

55 

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) 

64 

65 # x-scale 

66 if X is None: 

67 x = np.arange(len(y)) 

68 else: 

69 x = dim_1d_check(X) 

70 

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) 

76 

77 x_plot = x[f_obs] 

78 

79 fig, ax = plt.subplots(figsize=size) 

80 

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 

90 

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) 

99 

100 if name is not None: 

101 ax.set_title(name) 

102 

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) 

113 

114 ax.set_xlabel("X") 

115 ax.set_ylabel("Y") 

116 

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]) 

120 

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) 

132 

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) 

142 

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 ) 

155 

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]) 

162 

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 ) 

173 

174 ax.legend(loc="best") 

175 if show_plot: 

176 plt.show() 

177 

178# ---------- Orchestrateur principal ---------- 

179 

180 

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. 

200 

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. 

207 

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. 

242 

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. 

253 

254 Returns 

255 ------- 

256 None 

257 The function creates the figure and optionally displays it. 

258 """ 

259 

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 ) 

264 

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) 

267 

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 ) 

272 

273 # 4) Création figure / axes 

274 fig, ax = auxvisu.aux_create_score_figure(n_fig, sharey, grid_spec, figsize) 

275 

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)) 

290 

291 auxvisu.aux_finalize_figure(fig, show_plot=show_plot) 

292 return 

293 

294 # 6) Cas général : plusieurs panneaux 

295 ind_ax = -1 

296 

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 ) 

310 

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 ) 

324 

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 ) 

334 

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") 

341 

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 ) 

362 

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) 

366 

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) 

372 

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)) 

377 

378 # 8) Finalisation figure 

379 auxvisu.aux_finalize_figure(fig, show_plot=show_plot) 

380 

381 

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. 

403 

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_*). 

410 

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. 

457 

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. 

464 

465 Returns 

466 ------- 

467 None 

468 The function creates a figure and optionally displays it. 

469 """ 

470 

471 if f_obs is None: 

472 f_obs = np.arange(len(y)) 

473 f_obs = np.asarray(f_obs) 

474 

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 

480 

481 # Bornes mini sur var_A, var_E 

482 min_A, min_E = kwarg.get("var_min", (1e-6, 1e-6)) 

483 

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) 

488 

489 if only_data: 

490 name = "Data" 

491 

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) 

498 

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}'.") 

508 

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 

513 

514 # --- Dimensions & contextes --- 

515 f_obs_full = np.copy(f_obs) 

516 

517 if isinstance(dim, int): 

518 dim_list = [dim] 

519 else: 

520 dim_list = list(dim) 

521 n_dim = len(dim_list) 

522 

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_) 

531 

532 # --- Figure et axes --- 

533 fig, axs = plt.subplots(n_dim, n_ctx, sharex=True, figsize=size) 

534 

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 

545 

546 label = None # servira pour la colorbar éventuelle 

547 

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) 

552 

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 

559 

560 if f_obs_ctx.size == 0: 

561 continue 

562 

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]) 

594 

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]) 

600 

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]) 

604 

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 ) 

619 

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 ) 

632 

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()) 

640 

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 ) 

674 

675 # Masquer les ticks y pour les contextes > 0 (comme avant) 

676 if idx_ctx != 0: 

677 ax.set_yticklabels([]) 

678 

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 ) 

698 

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)) 

705 

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() 

713 

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) 

728 

729 ticks = (bounds + np.roll(bounds, -1)) / 2 

730 ticks[-1] = 10 

731 cbar1.set_ticks(ticks) 

732 cbar1.set_ticklabels(label, fontsize=12) 

733 

734 plt.tight_layout() 

735 if show_plot: 

736 plt.show() 

737 return 

738 

739 

740# Display of data curve with mean and variance. 

741 

742 

743def aux_get_var_color_sets(): 

744 """ 

745 Return the color sets used for percentile envelope visualization 

746 in `plot_var`. 

747 

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 ] 

766 

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 ] 

779 

780 return color_full, color_full2 

781 

782 

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. 

799 

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. 

804 

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. 

836 

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 

845 

846 def add_noise(data, noise_mult, noise_add): 

847 return (data * (1 + noise_mult)) + noise_add 

848 

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) 

853 

854 step = g # dimension à tracer 

855 

856 # Figure / axe 

857 fig, ax = plt.subplots(figsize=fig_s) 

858 if title is not None: 

859 ax.set_title(title) 

860 

861 # Série de base : résidus ou données brutes 

862 res = data_full * 0 

863 if res_flag: 

864 res = data_full 

865 

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() 

869 

870 per_list = [0.01, 1, 2.5, 10, 25, 75, 90, 97.5, 99, 99.99] 

871 per = [] 

872 

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)) 

877 

878 x_idx = np.arange(len(f_obs)) 

879 

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 ) 

893 

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 ) 

919 

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 ) 

934 

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 ) 

952 

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 ) 

964 

965 ax.legend(ncol=7, fontsize=14) 

966 fig.tight_layout() 

967 plt.show() 

968 

969 return per, per_list