Coverage for src / monte_neo / monte_carlo / utils.py: 82%

22 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-28 16:27 +0200

1"""Utility functions for Monte Carlo simulations.""" 

2 

3from __future__ import annotations 

4 

5import numpy as np 

6 

7 

8def summarize_metrics(results: list[dict]) -> dict: 

9 """Summarize metrics across all scenarios. 

10 

11 Args: 

12 results: List of scenario results. 

13 

14 Returns: 

15 Summary statistics. 

16 """ 

17 if not results: 

18 return {} 

19 

20 # Collect all metric values 

21 metric_values: dict[str, list] = {} 

22 for result in results: 

23 for name, value in result.get("metrics", {}).items(): 

24 if name not in metric_values: 

25 metric_values[name] = [] 

26 metric_values[name].append(value) 

27 

28 # Calculate summary stats 

29 summary = {} 

30 for name, values in metric_values.items(): 

31 arr = np.array(values, dtype=float) 

32 

33 # Handle infinite values which cause warnings in std calculation 

34 # We replace inf with nan and use nan-aware functions 

35 is_inf = np.isinf(arr) 

36 if np.any(is_inf): 

37 arr[is_inf] = np.nan 

38 

39 # Check if we have any valid data left 

40 if np.all(np.isnan(arr)): 

41 summary[name] = { 

42 "mean": 0.0, 

43 "std": 0.0, 

44 "min": 0.0, 

45 "max": 0.0, 

46 "median": 0.0, 

47 "p5": 0.0, 

48 "p95": 0.0, 

49 } 

50 continue 

51 

52 summary[name] = { 

53 "mean": float(np.nanmean(arr)), 

54 "std": float(np.nanstd(arr)), 

55 "min": float(np.nanmin(arr)), 

56 "max": float(np.nanmax(arr)), 

57 "median": float(np.nanmedian(arr)), 

58 "p5": float(np.nanpercentile(arr, 5)), 

59 "p95": float(np.nanpercentile(arr, 95)), 

60 } 

61 

62 return summary