Coverage for uqmodels / visualization / aux_visualization.py: 72%

258 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-09 08:15 +0000

1import matplotlib.pyplot as plt 

2import numpy as np 

3from uqmodels.utils import compute_born, propagate, _merge_config, _merge_nested 

4 

5import matplotlib.dates as mdates 

6from matplotlib.colors import LinearSegmentedColormap 

7 

8 

9def aux_adjust_axes(ax, x, y_list, ylim=None, x_lim=None, margin=0.05, x_margin=0.5): 

10 """ 

11 Adjust x/y axis limits based on data and optional explicit limits. 

12 

13 Parameters 

14 ---------- 

15 ax : Axes 

16 x : array-like 

17 y_list : array-like or list of array-like 

18 One or several y-series to consider for limits. 

19 ylim : tuple or None 

20 If not None, force (ymin, ymax). 

21 x_lim : tuple or None 

22 If not None, force (xmin, xmax). 

23 margin : float 

24 Relative margin applied to inferred y-limits (ignored if ylim is set). 

25 x_margin : float 

26 Margin added around min/max of x (ignored if x_lim is set). 

27 """ 

28 x = np.asarray(x) 

29 

30 if not isinstance(y_list, (list, tuple)): 

31 y_list = [y_list] 

32 

33 y_min = min(np.asarray(y).min() for y in y_list) 

34 y_max = max(np.asarray(y).max() for y in y_list) 

35 

36 # Y limits 

37 if ylim is None: 

38 y_low = y_min - abs(y_min * margin) 

39 y_high = y_max + abs(y_max * margin) 

40 else: 

41 y_low, y_high = ylim 

42 

43 ax.set_ylim(y_low, y_high) 

44 

45 # X limits 

46 if x_lim is None: 

47 x_low = x.min() - x_margin 

48 x_high = x.max() + x_margin 

49 else: 

50 x_low, x_high = x_lim 

51 

52 ax.set_xlim(x_low, x_high) 

53 

54 

55DEFAULT_PLOT_PRED_CONFIG = { 

56 "truth_line": { # line for y (true / observed curve) 

57 "ls": "dotted", 

58 "color": "black", 

59 "linewidth": 0.9, 

60 "alpha": 1.0, 

61 }, 

62 "pred_line": { # line for pred 

63 "linestyle": "-", # alias to show it works even with different key 

64 "color": "darkgreen", 

65 "alpha": 1.0, 

66 "linewidth": 0.7, 

67 "zorder": -4, 

68 "label": "Prediction", 

69 }, 

70 "obs_scatter": { # scatter for observations as points 

71 "c": "black", 

72 "s": 10, 

73 "marker": "x", 

74 "linewidth": 1, 

75 "label": "Observation", 

76 }, 

77} 

78 

79DEFAULT_LINE_CONFIG = { 

80 "color": "black", 

81 "linestyle": "-", 

82 "linewidth": 1.0, 

83 "marker": None, 

84 "markersize": None, 

85 "label": None, 

86 "zorder": None, 

87} 

88 

89 

90def aux_plot_line(ax, x, y, config=None): 

91 """ 

92 Plot a line (or markers only) on an Axes with configurable style. 

93 

94 Parameters 

95 ---------- 

96 ax : Axes 

97 x, y : array-like 

98 config : dict, optional 

99 Keys: color, linestyle, linewidth, marker, markersize, label, zorder. 

100 """ 

101 x = np.asarray(x) 

102 y = np.asarray(y) 

103 

104 cfg = DEFAULT_LINE_CONFIG.copy() 

105 if config is not None: 

106 cfg.update({k: v for k, v in config.items() if v is not None}) 

107 

108 # Filtrer les None pour ne pas polluer ax.plot 

109 kwargs = {k: v for k, v in cfg.items() if v is not None} 

110 return ax.plot(x, y, **kwargs) 

111 

112 

113DEFAULT_PLOT_PRED_CONFIG = { 

114 "truth_line": { # line for y (true / observed curve) 

115 "color": "black", 

116 "linestyle": "dotted", 

117 "linewidth": 0.9, 

118 "alpha": 1.0, 

119 "zorder": -5, 

120 "label": None, 

121 }, 

122 "pred_line": { # line for pred 

123 "color": "darkgreen", 

124 "linestyle": "-", 

125 "linewidth": 0.7, 

126 "alpha": 1.0, 

127 "zorder": -4, 

128 "label": "Prediction", 

129 }, 

130 "obs_scatter": { # observations as points 

131 "color": "black", 

132 "marker": "x", 

133 "markersize": 4, 

134 "linestyle": "none", 

135 "linewidth": 1.0, 

136 "alpha": 1.0, 

137 "zorder": 10, 

138 "label": "Observation", 

139 }, 

140} 

141 

142 

143def aux_plot_pred(ax, x, y, pred, config=None): 

144 """ 

145 Plot observations and predictions on a given Axes with optional style config. 

146 

147 Parameters 

148 ---------- 

149 ax : Axes 

150 x, y, pred : array-like 

151 Coordinates, observations, and predictions. 

152 config : dict, optional 

153 Style overrides for truth line, prediction line, and observation scatter. 

154 Keys: "truth_line", "pred_line", "obs_scatter". 

155 """ 

156 x = np.asarray(x) 

157 y = np.asarray(y) 

158 pred = np.asarray(pred) 

159 

160 cfg = _merge_config(DEFAULT_PLOT_PRED_CONFIG, config) 

161 

162 # Courbe "vérité terrain" 

163 aux_plot_line(ax, x, y, config=cfg["truth_line"]) 

164 

165 # Courbe de prédiction 

166 aux_plot_line(ax, x, pred, config=cfg["pred_line"]) 

167 

168 # Points d'observation 

169 aux_plot_line(ax, x, y, config=cfg["obs_scatter"]) 

170 

171 

172DEFAULT_PLOT_ANOM_CONFIG = { 

173 "anom_scatter": { 

174 "linewidth": 1, 

175 "marker": "x", 

176 "c": "magenta", 

177 "s": 25, 

178 "label": '"Abnormal" real demand', 

179 } 

180} 

181 

182DEFAULT_PLOT_ANOM_CONFIG = { 

183 "anom_scatter": { 

184 "color": "magenta", 

185 "marker": "x", 

186 "markersize": 5, 

187 "linestyle": "none", 

188 "linewidth": 1.0, 

189 "alpha": 1.0, 

190 "zorder": 15, 

191 "label": '"Abnormal" observation', 

192 } 

193} 

194 

195 

196def aux_plot_anom(ax, x, y, config=None): 

197 """ 

198 Plot anomalous observations on an Axes using aux_plot_line. 

199 

200 Parameters 

201 ---------- 

202 ax : Axes 

203 x, y : array-like 

204 Coordinates and anomalous values. 

205 config : dict, optional 

206 Style overrides for anomalous points. 

207 """ 

208 x = np.asarray(x) 

209 y = np.asarray(y) 

210 

211 cfg = _merge_config(DEFAULT_PLOT_ANOM_CONFIG, config) 

212 aux_plot_line(ax, x, y, config=cfg["anom_scatter"]) 

213 

214 

215DEFAULT_FILL_AREA_CONFIG = { 

216 "color": None, 

217 "alpha": 0.2, 

218 "label": None, 

219} 

220 

221DEFAULT_FILL_BETWEEN_CONFIG = { 

222 "color": None, 

223 "facecolor": None, 

224 "alpha": 0.2, 

225 "label": None, 

226 "interpolate": False, 

227 "zorder": None, 

228} 

229 

230 

231def aux_fill_between(ax, x, y1, y2, where=None, config=None): 

232 """ 

233 Wrapper around ax.fill_between with configurable style. 

234 

235 Parameters 

236 ---------- 

237 ax : Axes 

238 x, y1, y2 : array-like 

239 where : array-like or None 

240 Boolean mask for conditional fill. 

241 config : dict, optional 

242 Style overrides (color, facecolor, alpha, label, interpolate, zorder). 

243 """ 

244 x = np.asarray(x) 

245 y1 = np.asarray(y1) 

246 y2 = np.asarray(y2) 

247 

248 kwargs = DEFAULT_FILL_BETWEEN_CONFIG.copy() 

249 if config is not None: 

250 kwargs.update({k: v for k, v in config.items() if v is not None}) 

251 

252 return ax.fill_between( 

253 x, 

254 y1, 

255 y2, 

256 where=where, 

257 **kwargs, 

258 ) 

259 

260 

261def aux_fill_area(ax, x, env_bot, env_top, config=None): 

262 """ 

263 Fill an envelope between two curves on an Axes. 

264 

265 Parameters 

266 ---------- 

267 ax : Axes 

268 x, env_bot, env_top : array-like 

269 Coordinates and envelope bounds. 

270 config : dict, optional 

271 Style overrides (color, alpha, label). 

272 """ 

273 x = np.asarray(x) 

274 env_bot = np.asarray(env_bot) 

275 env_top = np.asarray(env_top) 

276 

277 final_cfg = DEFAULT_FILL_AREA_CONFIG.copy() 

278 if config is not None: 

279 final_cfg.update({k: v for k, v in config.items() if v is not None}) 

280 

281 return ax.fill_between( 

282 x, 

283 env_bot, 

284 env_top, 

285 color=final_cfg["color"], 

286 alpha=final_cfg["alpha"], 

287 label=final_cfg["label"], 

288 ) 

289 

290 

291DEFAULT_PI_PLOT_CONFIG = { 

292 "line": { # style des lignes de bornes 

293 "ls": "dotted", 

294 "lw": 1.2, 

295 "color": None, 

296 }, 

297 "fill": { # style du remplissage 

298 "color": None, 

299 "alpha": 0.2, 

300 }, 

301} 

302 

303 

304def aux_plot_PIs( 

305 ax, 

306 x, 

307 list_PIs, 

308 list_alpha_PIs, 

309 list_colors_PIs=["lightblue", "lightgreen"], 

310 list_alpha_fig_PIs=[0.3, 0.15], 

311 list_label_PIs=None, 

312 config=None, 

313): 

314 """ 

315 Plot multiple prediction interval envelopes on an Axes. 

316 

317 Parameters 

318 ---------- 

319 ax : Axes 

320 x : array-like 

321 list_PIs : list of array-like 

322 [low_1, ..., low_k, high_k, ..., high_1]. 

323 list_alpha_PIs : list of float 

324 Quantile levels for each bound. 

325 list_colors_PIs : list, optional 

326 Per-interval colors overriding config. 

327 list_alpha_fig_PIs : list, optional 

328 Per-interval alphas overriding config. 

329 list_label_PIs : list, optional 

330 Per-interval labels. 

331 config : dict, optional 

332 Global style config: {"line": {...}, "fill": {...}}. 

333 """ 

334 n_bounds = len(list_PIs) 

335 n_couple = n_bounds // 2 

336 

337 # Merge global config with defaults 

338 line_cfg = DEFAULT_PI_PLOT_CONFIG["line"].copy() 

339 fill_cfg = DEFAULT_PI_PLOT_CONFIG["fill"].copy() 

340 if config is not None: 

341 if "line" in config: 

342 line_cfg.update(config["line"]) 

343 if "fill" in config: 

344 fill_cfg.update(config["fill"]) 

345 

346 # Defaults for per-interval overrides 

347 if list_colors_PIs is None: 

348 list_colors_PIs = [None] * n_couple 

349 if list_alpha_fig_PIs is None: 

350 list_alpha_fig_PIs = [fill_cfg.get("alpha", 0.2)] * n_couple 

351 

352 for i in range(n_couple): 

353 low = list_PIs[i] 

354 high = list_PIs[-(i + 1)] 

355 

356 color = list_colors_PIs[i] if list_colors_PIs[i] is not None else fill_cfg.get("color", None) 

357 alpha = list_alpha_fig_PIs[i] 

358 

359 if list_label_PIs is None: 

360 coverage = (list_alpha_PIs[-(i + 1)] - list_alpha_PIs[i]) * 100 

361 label = f"Predictive interval: {coverage:.0f}%" 

362 else: 

363 label = list_label_PIs[i] 

364 

365 # Lignes de bornes 

366 line_kwargs = line_cfg.copy() 

367 if color is not None: 

368 line_kwargs["color"] = color 

369 ax.plot(x, low, **line_kwargs) 

370 ax.plot(x, high, **line_kwargs) 

371 

372 # Remplissage via le helper 

373 fill_kwargs = fill_cfg.copy() 

374 if color is not None: 

375 fill_kwargs["color"] = color 

376 fill_kwargs["alpha"] = alpha 

377 fill_kwargs["label"] = label 

378 

379 aux_fill_area(ax, x, low, high, config=fill_kwargs) 

380 

381 

382DEFAULT_CONF_SCORE_CONFIG = { 

383 "marker": "D", 

384 "s": 14, 

385 "edgecolors": "black", 

386 "linewidth": 0.2, 

387 "cmap": "RdYlGn_r", 

388 "zorder_base": 10, 

389} 

390 

391 

392def aux_plot_conf_score(ax, x, pred, confidence_lvl, label, mode_res=False, config=None): 

393 """ 

394 Plot confidence scores as colored markers on an Axes. 

395 

396 Parameters 

397 ---------- 

398 ax : Axes 

399 x, pred : array-like 

400 Coordinates and predictions. 

401 confidence_lvl : array-like 

402 Discrete confidence levels (int). 

403 label : list of str 

404 Legend labels per confidence level. 

405 mode_res : bool, default False 

406 If True, plot residual scores around zero. 

407 config : dict, optional 

408 Style overrides (marker, s, edgecolors, linewidth, cmap, zorder_base). 

409 """ 

410 x = np.asarray(x) 

411 pred = np.asarray(pred) 

412 confidence_lvl = np.asarray(confidence_lvl) 

413 

414 if mode_res: 

415 pred = pred - pred 

416 

417 cfg = DEFAULT_CONF_SCORE_CONFIG.copy() 

418 if config is not None: 

419 cfg.update({k: v for k, v in config.items() if v is not None}) 

420 

421 max_conf = int(confidence_lvl.max()) 

422 cmap = plt.get_cmap(cfg["cmap"], len(label) + 1) 

423 

424 for i in range(0, max_conf + 1): 

425 mask = (confidence_lvl == i) 

426 if not mask.any(): 

427 continue 

428 

429 ax.scatter( 

430 x[mask], 

431 pred[mask], 

432 c=confidence_lvl[mask], 

433 marker=cfg["marker"], 

434 s=cfg["s"], 

435 edgecolors=cfg["edgecolors"], 

436 linewidth=cfg["linewidth"], 

437 cmap=cmap, 

438 vmin=0, 

439 vmax=max_conf, 

440 label=label[i], 

441 zorder=cfg["zorder_base"] + i, 

442 ) 

443 

444 

445DEFAULT_CONFIDENCE_PLOT_CONFIG = { 

446 "pred": None, # config -> aux_plot_pred 

447 "anom": None, # config -> aux_plot_anom 

448 "pis_born": None, # config -> aux_plot_PIs (born) 

449 "pis_aleatoric": None, # config -> aux_plot_PIs (aleatoric PI) 

450 "pis_total": None, # config -> aux_plot_PIs (total PI) 

451 "pis_born_bis": None, # config -> aux_plot_PIs (born_bis) 

452 "anom_fill_upper": { # config -> aux_fill_between (born upper) 

453 "facecolor": "red", 

454 "alpha": 0.8, 

455 "label": "Anomaly", 

456 "interpolate": True, 

457 "zorder": -10, 

458 }, 

459 "anom_fill_lower": { # config -> aux_fill_between (born lower) 

460 "facecolor": "red", 

461 "alpha": 0.8, 

462 "interpolate": True, 

463 "zorder": -10, 

464 }, 

465 "axes": { # config -> aux_adjust_axes 

466 "margin": 0.05, 

467 "x_margin": 0.5, 

468 }, 

469} 

470 

471 

472def aux_plot_confiance( 

473 ax, 

474 y, 

475 pred, 

476 var_A, 

477 var_E, 

478 born=None, 

479 born_bis=None, 

480 ylim=None, 

481 split_values=-1, 

482 x=None, 

483 mode_res=False, 

484 min_A=0.08, 

485 min_E=0.02, 

486 env=[0.95, 0.68], 

487 config=None, 

488 **kwarg, 

489): 

490 """ 

491 Plot prediction, uncertainty intervals and anomaly regions on an Axes. 

492 """ 

493 y = np.asarray(y) 

494 pred = np.asarray(pred) 

495 var_A = np.asarray(var_A) 

496 var_E = np.asarray(var_E) 

497 

498 if x is None: 

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

500 else: 

501 x = np.asarray(x) 

502 

503 # Config globale fusionnée 

504 cfg = _merge_nested(DEFAULT_CONFIDENCE_PLOT_CONFIG, config or {}) 

505 

506 # Mode résidus 

507 if mode_res: 

508 y = y - pred 

509 if born is not None: 

510 born = (born[0] - pred, born[1] - pred) 

511 pred = pred * 0.0 

512 

513 # Indicateurs tronqués 

514 ind_A = np.sqrt(var_A) 

515 ind_E = np.sqrt(var_E) 

516 ind_E[ind_E < min_E] = min_E 

517 ind_A[ind_A < min_A] = min_A 

518 

519 # PIs (aleatoric et total) 

520 y_lower, y_upper = compute_born(pred, np.sqrt(var_A + var_E * 0.0), 0.045) 

521 y_lower_N, y_upper_N = compute_born(pred, np.sqrt(var_A + var_E), 0.045) 

522 

523 # Anomalies 

524 if born is not None: 

525 anom_mask = (y < born[0]) | (y > born[1]) 

526 else: 

527 anom_score = np.abs(y - pred) / (2.0 * np.sqrt(ind_E**2 + ind_A**2)) 

528 anom_mask = anom_score > 1.0 

529 

530 # Prédiction + anomalies 

531 aux_plot_pred(ax, x, y, pred, config=cfg["pred"]) 

532 aux_plot_anom(ax, x[anom_mask], y[anom_mask], config=cfg["anom"]) 

533 

534 # Cas avec bornes explicites 

535 if born is not None: 

536 aux_plot_PIs( 

537 ax, 

538 x, 

539 [born[0], born[1]], 

540 list_alpha_PIs=[0.025, 0.975], 

541 list_colors_PIs=["green"], 

542 list_label_PIs=["Normal_limit"], 

543 config=cfg["pis_born"], 

544 ) 

545 

546 aux_fill_between( 

547 ax, 

548 x, 

549 born[1], 

550 y, 

551 where=propagate(y > born[1], 0, sym=True), 

552 config=cfg["anom_fill_upper"], 

553 ) 

554 

555 aux_fill_between( 

556 ax, 

557 x, 

558 born[0], 

559 y, 

560 where=propagate(y < born[0], 0), 

561 config=cfg["anom_fill_lower"], 

562 ) 

563 

564 # Cas sans bornes explicites : PIs théoriques 

565 else: 

566 aux_plot_PIs( 

567 ax, 

568 x, 

569 [y_lower, y_upper], 

570 list_alpha_PIs=[0.025, 0.975], 

571 list_colors_PIs=["green"], 

572 list_label_PIs=["2σAleatoric PIs (95%)"], 

573 list_alpha_fig_PIs=[0.2], 

574 config=cfg["pis_aleatoric"], 

575 ) 

576 

577 aux_plot_PIs( 

578 ax, 

579 x, 

580 [y_lower_N, y_upper_N], 

581 list_alpha_PIs=[0.16, 0.84], 

582 list_colors_PIs=["darkblue"], 

583 list_alpha_fig_PIs=[0.1], 

584 list_label_PIs=["2σTotal PIs(95%)"], 

585 config=cfg["pis_total"], 

586 ) 

587 

588 # Bornes supplémentaires 

589 if born_bis is not None: 

590 aux_plot_PIs( 

591 ax, 

592 x, 

593 [born_bis[0], born_bis[1]], 

594 list_alpha_PIs=[0.025, 0.975], 

595 list_colors_PIs=["teal"], 

596 list_alpha_fig_PIs=[0.1], 

597 list_label_PIs=["Normal_limit"], 

598 config=cfg["pis_born_bis"], 

599 ) 

600 

601 # Ajustement axes 

602 axes_cfg = cfg["axes"] 

603 aux_adjust_axes(ax, x, [y, y_lower], ylim=ylim, margin=axes_cfg["margin"], x_margin=axes_cfg["x_margin"]) 

604 

605 

606# Auxiliar function related to matplot 

607 

608def aux_norm_score_inputs(score, f_obs=None, cmap=None): 

609 """ 

610 Normalise score en liste, infère len_score, dim_score, n_score et f_obs. 

611 """ 

612 if isinstance(score, list): 

613 len_score = len(score[0]) 

614 dim_score = [score_.shape[-1] for score_ in score] 

615 n_score = len(score) 

616 score_list = score 

617 else: 

618 score_list = [score] 

619 len_score = len(score) 

620 dim_score = [score_list[0].shape[-1]] 

621 n_score = 1 

622 

623 if f_obs is None: 

624 f_obs = np.arange(len_score) 

625 else: 

626 f_obs = np.asarray(f_obs) 

627 

628 if cmap is None: 

629 cmap = provide_cmap("bluetored") 

630 

631 return score_list, len_score, dim_score, n_score, f_obs, cmap 

632 

633 

634def aux_prepare_x_extent(x, f_obs, dim_score): 

635 """ 

636 Prépare x (échelle) et la liste d'extent pour imshow. 

637 Retourne x, x_flag (datetime ou non), list_extent. 

638 """ 

639 if x is None: 

640 x_flag = False 

641 x = np.arange(len(f_obs)) 

642 list_extent = [None for _ in dim_score] 

643 else: 

644 x_flag = True 

645 x = np.asarray(x) 

646 d0 = mdates.date2num(x[f_obs][0]) 

647 d1 = mdates.date2num(x[f_obs][-1]) 

648 list_extent = [[d0, d1, 0, dim] for dim in dim_score] 

649 

650 return x, x_flag, list_extent 

651 

652 

653def aux_compute_layout_params(n_score, dim_score, true_label, score2, data, grid_spec=None): 

654 """ 

655 Calcule n_fig, sharey, grid_spec. 

656 """ 

657 n_fig = 3 + n_score 

658 if true_label is None: 

659 n_fig -= 1 

660 if score2 is None: 

661 n_fig -= 1 

662 if data is None: 

663 n_fig -= 1 

664 

665 sharey = False 

666 if ( 

667 (n_score == 1) 

668 and (true_label is not None) 

669 and (dim_score[0] == true_label.shape) 

670 ): 

671 # Même condition que ton code initial (même si peu naturelle) 

672 sharey = True 

673 

674 if grid_spec is None: 

675 grid_spec = np.ones(n_fig) 

676 

677 return n_fig, sharey, grid_spec 

678 

679 

680def aux_create_score_figure(n_fig, sharey, grid_spec, figsize): 

681 """ 

682 Crée la figure et les axes pour la matrice d'anomalies. 

683 """ 

684 fig, ax = plt.subplots( 

685 n_fig, 

686 1, 

687 sharex=True, 

688 sharey=sharey, 

689 gridspec_kw={"height_ratios": grid_spec}, 

690 figsize=figsize, 

691 ) 

692 return fig, ax 

693 

694 

695def aux_plot_score_matrix(ax, score_mat, f_obs, extent, vmin, vmax, cmap, title="score"): 

696 """ 

697 Affiche une matrice de score via imshow. 

698 """ 

699 ax.set_title(title) 

700 ax.imshow( 

701 score_mat[f_obs].T[::-1], 

702 cmap=cmap, 

703 vmin=vmin, 

704 vmax=vmax, 

705 aspect="auto", 

706 extent=extent, 

707 interpolation=None, 

708 ) 

709 

710 

711def aux_overlay_setup_grid(ax, setup, n_points): 

712 """ 

713 Superpose la grille channels/sensors sur une matrice de score. 

714 setup = (n_chan, n_sensor) 

715 """ 

716 if setup is None: 

717 return 

718 n_chan, n_sensor = setup 

719 for i in range(n_chan * n_sensor): 

720 ax.hlines(i, 0, n_points, color="grey", lw=0.5) 

721 for i in range(n_sensor): 

722 ax.hlines(i * n_chan, 0, n_points, color="black", lw=1) 

723 

724 

725def aux_plot_true_label_matrix(ax, true_label, f_obs, extent): 

726 """ 

727 Affiche la matrice de labels vrais. 

728 """ 

729 ax.set_title("True_anom") 

730 ax.imshow( 

731 true_label[f_obs].T[::-1], 

732 cmap="Reds", 

733 aspect="auto", 

734 extent=extent, 

735 interpolation=None, 

736 ) 

737 

738 

739def aux_build_data_colors(data, list_anom_ind=None): 

740 """ 

741 Construit la palette de couleurs par canal, en surlignant éventuellement 

742 certains indices anormaux. 

743 """ 

744 n_chan = data.shape[1] 

745 base_cmap = plt.get_cmap("Greens", n_chan) 

746 colors = [base_cmap(i) for i in range(n_chan)] 

747 

748 if list_anom_ind is not None: 

749 red_cmap = plt.get_cmap("Reds", len(list_anom_ind) + 4) 

750 for n, anom_ind in enumerate(list_anom_ind): 

751 colors[anom_ind] = red_cmap(n + 4) 

752 

753 return colors 

754 

755 

756def aux_plot_data_timeseries(ax, x, data, f_obs, dim, colors, lw=0.9): 

757 """ 

758 Trace les séries temporelles multicanal. 

759 """ 

760 for i in range(dim): 

761 ax.plot(x[f_obs], data[f_obs, i], color=colors[i], lw=lw) 

762 

763 

764def aux_overlay_score_anoms_on_data(ax, x, data, score, f_obs, dim, threshold=1.0): 

765 """ 

766 Superpose les points où |score| > threshold sur les séries de données. 

767 """ 

768 for i in range(dim): 

769 mask = np.abs(score)[f_obs, i] > threshold 

770 ax.scatter( 

771 x[f_obs][mask], 

772 data[f_obs, i][mask], 

773 color="red", 

774 marker="x", 

775 s=1, 

776 zorder=10, 

777 ) 

778 

779 

780def aux_overlay_true_label_on_data(ax, x, data, true_label, f_obs, color="purple"): 

781 """ 

782 Superpose les labels vrais sur les séries temporelles. 

783 """ 

784 for i in range(data.shape[1]): 

785 mask = true_label[f_obs, i] > 0 

786 ax.scatter(x[f_obs][mask], data[f_obs, i][mask], color=color) 

787 

788 

789def aux_format_time_axis(ax, x_flag, x_date): 

790 """ 

791 Configure l'axe des x comme temporel et éventuellement applique un formatter. 

792 """ 

793 if x_flag: 

794 ax.xaxis_date() 

795 if x_date: 

796 ax.xaxis.set_major_formatter(mdates.DateFormatter("%d/%m %H:%M")) 

797 

798 

799def aux_finalize_figure(fig, show_plot=True): 

800 """ 

801 Finalise la figure (tight_layout + show optionnel). 

802 """ 

803 fig.tight_layout() 

804 if show_plot: 

805 plt.show() 

806 

807 

808def provide_cmap(mode="bluetored"): 

809 """Generate a bluetored or a cyantopurple cutsom cmap 

810 

811 Args: 

812 mode (str, optional):Values: bluetored' or 'cyantopurple ' 

813 

814 return: 

815 Colormap matplotlib 

816 """ 

817 if mode == "bluetored": 

818 bluetored = [ 

819 [0.0, (0, 0, 90)], 

820 [0.05, (5, 5, 120)], 

821 [0.1, (20, 20, 150)], 

822 [0.15, (20, 20, 190)], 

823 [0.2, (40, 40, 220)], 

824 [0.25, (70, 70, 255)], 

825 [0.33, (100, 100, 255)], 

826 [0.36, (180, 180, 255)], 

827 [0.4, (218, 218, 255)], 

828 [0.45, (245, 245, 255)], 

829 [0.5, (255, 253, 253)], 

830 [0.55, (255, 245, 245)], 

831 [0.6, (255, 218, 218)], 

832 [0.63, (255, 200, 200)], 

833 [0.66, (255, 160, 160)], 

834 [0.7, (255, 110, 110)], 

835 [0.75, (255, 70, 70)], 

836 [0.8, (230, 40, 40)], 

837 [0.85, (200, 20, 20)], 

838 [0.9, (180, 10, 10)], 

839 [0.95, (150, 5, 5)], 

840 [1.0, (130, 0, 0)], 

841 ] 

842 bluetored_cmap = LinearSegmentedColormap.from_list( 

843 "bluetored", [np.array(i[1]) / 255 for i in bluetored], N=255 

844 ) 

845 return bluetored_cmap 

846 elif mode == "cyantopurple": 

847 cyantopurple = [ 

848 [0.0, (25, 255, 255)], 

849 [0.05, (20, 250, 250)], 

850 [0.1, (20, 230, 230)], 

851 [0.15, (20, 220, 220)], 

852 [0.2, (15, 200, 200)], 

853 [0.25, (10, 170, 170)], 

854 [0.3, (10, 140, 140)], 

855 [0.36, (5, 80, 80)], 

856 [0.4, (5, 50, 50)], 

857 [0.45, (0, 30, 30)], 

858 [0.5, (0, 0, 0)], 

859 [0.55, (30, 0, 30)], 

860 [0.6, (59, 0, 50)], 

861 [0.64, (80, 0, 80)], 

862 [0.7, (140, 0, 140)], 

863 [0.75, (170, 0, 170)], 

864 [0.8, (200, 40, 200)], 

865 [0.85, (220, 20, 220)], 

866 [0.9, (240, 10, 240)], 

867 [0.95, (250, 5, 250)], 

868 [1.0, (255, 0, 255)], 

869 ] 

870 else: 

871 raise NameError 

872 

873 cyantopurple_cmap = LinearSegmentedColormap.from_list( 

874 "cyantopurple", [np.array(i[1]) / 255 for i in cyantopurple], N=255 

875 ) 

876 return cyantopurple_cmap 

877 

878 

879def _get_panel_ax(axs, n_dim, n_ctx, idx_dim, idx_ctx): 

880 """Sélectionne l'Axes correct dans la grille axs (len(dim) x n_ctx).""" 

881 if n_dim == 1 and n_ctx == 1: 

882 return axs 

883 if n_dim == 1: 

884 return axs[idx_ctx] 

885 if n_ctx == 1: 

886 return axs[idx_dim] 

887 return axs[idx_dim, idx_ctx]