77 MAPE = max(0, 1 - (abs(prediction - true) / true))
88"""
99
10- from src .dataframe_serializer import DataframeSerializer # type: ignore
1110from src .plot_options import PlotOptions # type: ignore
1211import src .constants as cn # type: ignore
12+ from src .statistic_calculator import StatisticCalculator # type: ignore
1313
1414import matplotlib .pyplot as plt # type: ignore
1515import numpy as np # type: ignore
1616import pandas as pd # type: ignore
1717
18- MEAN = "mean"
19- MIN = "min"
20- MAX = "max"
21- COUNT = "count"
2218
23- #########################################
24- class _StatisticAccumulator (object ):
25- STATISTICS = ["mean" , "min" , "max" , "count" , "invalid_count" , "p25" , "p30" ,
26- "p50" , "p80" , "p95" , "p99" ]
27- # count is the number of valid values used in calculating the statistics.
28- # invalid_count is the number of invalid (sentinel -1) values excluded from aggregation.
29-
30- """A container for storing statistics accumulated in a dictionary"""
31- def __init__ (self ) -> None :
32- self .statistic_dct : dict = {n : [] for n in self .STATISTICS }
33- self .statistic_dct [cn .AGGREGATION_TYPE ] = []
34- self .statistic_dct [cn .DESCRIPTION ] = []
35-
36- def add (self ,
37- value_arr : np .ndarray = np .array ([]),
38- label : str = "" ,
39- aggregation_type : str = "" , # "model" or species name
40- ) -> None :
41- """Computes statistics and adds to cumulative values.
42-
43- Parameters
44- ----------
45- value_arr : np.ndarray
46- 1D Array of MAPE metric values. Negative sentinel values (-1)
47- indicate invalid measurements and are excluded from aggregation.
48- label : str
49- Descriptive label for this aggregation.
50- aggregation_type : str
51- Type of aggregation, either "model" or a species name.
52-
53- Returns
54- -------
55- None
56- Modifies internal state (self.statistic_dct) only.
57- """
58- value_arr = value_arr .flatten () # Flatten to 1D for aggregation
59- LARGE_VAL = 1e6
60- # Filter out negative sentinel values (invalid/undefined true values).
61- valid_mask = value_arr >= 0
62- invalid_count = int (np .sum (~ valid_mask ))
63- valid_arr = value_arr [valid_mask ].copy ()
64-
65- count = int (len (valid_arr ))
66-
67- # If no input values at all, still record a row so the CSV reflects that data was collected.
68- if len (value_arr ) == 0 :
69- self .statistic_dct [cn .DESCRIPTION ].append (label )
70- self .statistic_dct [cn .AGGREGATION_TYPE ].append (aggregation_type )
71- self .statistic_dct [MEAN ].append (0.0 )
72- self .statistic_dct [MIN ].append (0.0 )
73- self .statistic_dct [MAX ].append (0.0 )
74- self .statistic_dct [COUNT ].append (0 )
75- self .statistic_dct ["invalid_count" ].append (invalid_count )
76- for p in [p for p in self .STATISTICS if p .startswith ("p" )]:
77- self .statistic_dct [p ].append (0.0 )
78- return
79-
80- # If all values are invalid, still record a row so the CSV reflects that data was collected.
81- if count == 0 :
82- self .statistic_dct [cn .DESCRIPTION ].append (label )
83- self .statistic_dct [cn .AGGREGATION_TYPE ].append (aggregation_type )
84- self .statistic_dct [MEAN ].append (0.0 )
85- self .statistic_dct [MIN ].append (0.0 )
86- self .statistic_dct [MAX ].append (0.0 )
87- self .statistic_dct [COUNT ].append (0 )
88- self .statistic_dct ["invalid_count" ].append (invalid_count )
89- for p in [p for p in self .STATISTICS if p .startswith ("p" )]:
90- self .statistic_dct [p ].append (0.0 )
91- return
92-
93- # Replace remaining NaN/inf/large values with LARGE_VAL for aggregation.
94- sel = np .isnan (valid_arr ) | np .isinf (valid_arr ) | (valid_arr > LARGE_VAL )
95- valid_arr [sel ] = LARGE_VAL
96- # Update dictionary
97- self .statistic_dct [cn .DESCRIPTION ].append (label )
98- self .statistic_dct [cn .AGGREGATION_TYPE ].append (aggregation_type )
99- self .statistic_dct [MEAN ].append (float (np .nanmean (valid_arr )))
100- self .statistic_dct [MIN ].append (float (np .nanmin (valid_arr )))
101- self .statistic_dct [MAX ].append (float (np .nanmax (valid_arr )))
102- self .statistic_dct [COUNT ].append (count )
103- self .statistic_dct ["invalid_count" ].append (invalid_count )
104- percentiles = [p for p in self .STATISTICS if p .startswith ("p" )]
105- for p in percentiles :
106- self .statistic_dct [p ].append (float (np .nanpercentile (valid_arr , int (p [1 :]))))
107-
108-
109- #########################################
11019class Score :
11120 """Scores prediction timecourses against true timecourses using zero-floor MAPE.
11221
@@ -129,20 +38,17 @@ def __init__(self, serialization_path: str = "",
12938 is_persist : bool
13039 Whether to persist the DataFrame to the CSV file.
13140 """
41+ self ._is_persist = is_persist
13242 if len (serialization_path ) == 0 :
13343 serialization_path = self .SERIALIZATION_PATH
134- self ._serializer = DataframeSerializer (serialization_path ,
135- is_initialize = is_initialize , is_persist = is_persist )
44+ self ._serialization_path = serialization_path
13645 #
137- self .statistic_accumulator = _StatisticAccumulator ()
46+ self .score_df = pd .DataFrame ()
47+ self .statistic_calculator = StatisticCalculator ()
13848
13949 @property
14050 def serialization_path (self ) -> str :
141- return self ._serializer .serialization_path
142-
143- @property
144- def score_df (self ) -> pd .DataFrame :
145- return self ._serializer .dataframe
51+ return self ._serialization_path
14652
14753 @staticmethod
14854 def calculateMAPE (true_df : pd .DataFrame ,
@@ -180,14 +86,14 @@ def calculateMAPE(true_df: pd.DataFrame,
18086 def add (self ,
18187 true_timecourse_df : pd .DataFrame ,
18288 prediction_timecourse_df : pd .DataFrame ,
183- label : str = "" ,
89+ system_id : str = "" ,
18490 ) -> pd .DataFrame :
185- """Computes MAPE scores and accumulates statistics for model-level and per-species aggregations.
91+ """Computes scores and accumulates statistics for model-level and per-species aggregations.
18692
18793 Parameters
18894 ----------
189- label : str
190- Descriptive label stored in each aggregation row.
95+ system_id : str
96+ Descriptive ID stored in each aggregation row.
19197 true_timecourse_df : pd.DataFrame
19298 True timecourse with timepoints as index and species as columns.
19399 prediction_timecourse_df : pd.DataFrame
@@ -198,38 +104,18 @@ def add(self,
198104 pd.DataFrame
199105 The full score DataFrame after this addition.
200106 """
201- score_df : pd .DataFrame = self .calculateMAPE (true_timecourse_df , prediction_timecourse_df )
202-
203- # Record how many rows were in the accumulator BEFORE this call.
204- dct = self .statistic_accumulator .statistic_dct
205- start_count = len (dct [cn .DESCRIPTION ])
206-
207- # Model level aggregation (all species and timepoints combined)
208- model_arr = np .asarray (score_df .values , dtype = float )
209- self .statistic_accumulator .add (model_arr ,
210- aggregation_type = cn .AGGREGATION_TYPE_MODEL ,
211- label = label )
212-
107+ mape_df : pd .DataFrame = self .calculateMAPE (true_timecourse_df , prediction_timecourse_df )
108+ self .statistic_calculator .add (cn .AGGREGATION_TYPE_MODEL , mape_df .values .flatten ())
213109 # Species level aggregations (one per species column, across all timepoints)
214- species_names = list (score_df .columns )
110+ species_names = list (mape_df .columns )
215111 for species_name in species_names :
216- species_arr = np .asarray (score_df [species_name ].values , dtype = float )
217- self .statistic_accumulator .add (species_arr ,
218- aggregation_type = species_name ,
219- label = label )
220-
221- # Build list of dicts from only the NEW rows added in this call.
222- end_count = len (dct [cn .DESCRIPTION ])
223- new_rows : list [dict ] = []
224- for i in range (start_count , end_count ):
225- row : dict = {}
226- for key in dct :
227- if isinstance (dct [key ], list ):
228- row [key ] = dct [key ][i ]
229- new_rows .append (row )
230-
231- # Persist the new rows to the DataFrame/CSV.
232- self ._serializer .serializeDct (new_rows )
112+ self .statistic_calculator .add (species_name , mape_df [species_name ].to_numpy ())
113+ # Add the system ID
114+ self .score_df = self .statistic_calculator .dataframe .copy ()
115+ self .score_df [cn .COL_SYSTEM_ID ] = system_id
116+ # Serialize the accumulated statistics
117+ if self ._is_persist :
118+ self .score_df .to_csv (self .serialization_path , index = False )
233119 return self .score_df
234120
235121 def plotCDF (self ,
0 commit comments