@@ -183,7 +183,7 @@ def save_all(self, output_dir: str = "."):
183183 confmat = BinaryConfusionMatrix ()(self .pred_labels , self .gt_labels )
184184 plt .figure (figsize = (8 , 6 ))
185185 sns .heatmap (confmat .numpy (), annot = True , fmt = 'd' , cmap = 'Blues' ,
186- xticklabels = ['Normal' , 'Anomaly' ], yticklabels = ['Normal' , 'Anomaly' ])
186+ xticklabels = ['Normal' , 'Anomaly' ], yticklabels = ['Normal' , 'Anomaly' ])
187187 plt .title ('Confusion Matrix' )
188188 plt .xlabel ('Predicted Label' )
189189 plt .ylabel ('True Label' )
@@ -273,10 +273,74 @@ def _is_better(metric_name: str, val1, val2) -> bool:
273273 else :
274274 return val1 > val2
275275
276+ def _plot_roc_curve (self , run_data : dict [str , "Metrics | list" ], output_dir : str = "." ):
277+ """
278+ Generates and saves a single ROC curve plot, showing the average curve and variability
279+ if multiple runs are provided for a single model.
280+ """
281+ output_path = Path (output_dir )
282+ output_path .mkdir (exist_ok = True )
283+
284+ plt .figure (figsize = (8 , 6 ))
285+
286+ for name , data in run_data .items ():
287+ if isinstance (data , list ):
288+ # Calculate mean ROC and standard deviation for a list of runs
289+ all_fprs = []
290+ all_tprs = []
291+ all_aucs = []
292+
293+ # We need to use a common set of false positive rates to average the true positive rates
294+ mean_fpr = np .linspace (0 , 1 , 100 )
295+
296+ for run in data :
297+ fpr , tpr , _ = roc_curve (run .gt_labels .cpu ().numpy (), run .pred_scores .cpu ().numpy ())
298+ all_fprs .append (fpr )
299+ all_tprs .append (tpr )
300+ all_aucs .append (auc (fpr , tpr ))
301+
302+ interp_tprs = []
303+ for idx in range (len (data )):
304+ interp_tpr = np .interp (mean_fpr , all_fprs [idx ], all_tprs [idx ])
305+ interp_tpr [0 ] = 0.0
306+ interp_tprs .append (interp_tpr )
307+
308+ mean_tpr = np .mean (interp_tprs , axis = 0 )
309+ mean_tpr [- 1 ] = 1.0
310+ mean_auc = auc (mean_fpr , mean_tpr )
311+ std_auc = np .std (all_aucs )
312+ std_tpr = np .std (interp_tprs , axis = 0 )
313+
314+ tprs_upper = np .minimum (mean_tpr + std_tpr , 1 )
315+ tprs_lower = np .maximum (mean_tpr - std_tpr , 0 )
316+
317+ # Plot mean ROC curve
318+ plt .plot (mean_fpr , mean_tpr , lw = 2 , label = f'{ name } (Mean AUC = { mean_auc :.2f} $\pm$ { std_auc :.2f} )' )
319+
320+ # Plot variability area
321+ plt .fill_between (mean_fpr , tprs_lower , tprs_upper , alpha = 0.2 , label = r"$\pm$ 1 std. dev." )
322+
323+ else :
324+ # Plot single ROC curve
325+ fpr , tpr , _ = roc_curve (data .gt_labels .cpu ().numpy (), data .pred_scores .cpu ().numpy ())
326+ roc_auc = auc (fpr , tpr )
327+ plt .plot (fpr , tpr , lw = 2 , label = f'{ name } (AUC = { roc_auc :.2f} )' )
328+
329+ plt .plot ([0 , 1 ], [0 , 1 ], color = 'navy' , lw = 2 , linestyle = '--' , label = 'Chance Level' )
330+ plt .xlim ([0.0 , 1.0 ])
331+ plt .ylim ([0.0 , 1.05 ])
332+ plt .xlabel ('False Positive Rate' )
333+ plt .ylabel ('True Positive Rate' )
334+ plt .title ('Receiver Operating Characteristic (ROC) Curves' )
335+ plt .legend (loc = "lower right" )
336+ plt .savefig (output_path / 'roc_curve_comparison.png' )
337+ plt .close ()
338+
276339 @classmethod
277340 def compare_multiple_runs (cls , runs_data : dict [str , "Metrics | list" ]) -> str :
278341 """
279- Generates a single LaTeX table comparing multiple runs, with the best value bolded.
342+ Generates a single LaTeX table comparing multiple runs, with the best value bolded,
343+ and plots a combined ROC curve.
280344
281345 Args:
282346 runs_data (dict[str, "Metrics" | list]): A dictionary where keys are run names and values are either
@@ -288,6 +352,10 @@ def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
288352 if not runs_data :
289353 return ""
290354
355+ # Plot the combined ROC curve
356+ metrics_instance = cls ()
357+ metrics_instance ._plot_roc_curve (runs_data , output_dir = "comparison_output" )
358+
291359 run_names = list (runs_data .keys ())
292360 processed_metrics_data = {}
293361 for name , data in runs_data .items ():
@@ -296,7 +364,9 @@ def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
296364 all_run_metrics = [run .get_all_metrics () for run in data ]
297365 avg_metrics = {}
298366 std_dev_metrics = {}
299- for metric_key in next (iter (all_run_metrics )).keys ():
367+ # Use a stable key order from the first run's metrics
368+ first_metrics_keys = next (iter (all_run_metrics )).keys () if all_run_metrics else []
369+ for metric_key in first_metrics_keys :
300370 values = [m .get (metric_key ) for m in all_run_metrics if isinstance (m .get (metric_key ), (int , float ))]
301371 if values :
302372 avg_metrics [metric_key ] = np .mean (values )
@@ -318,7 +388,7 @@ def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
318388
319389 best_val = None
320390 for val in metric_values :
321- if val is not None :
391+ if isinstance ( val , ( int , float )) :
322392 if best_val is None or cls ._is_better (metric_key , val , best_val ):
323393 best_val = val
324394 best_values [metric_key ] = best_val
@@ -348,10 +418,10 @@ def compare_multiple_runs(cls, runs_data: dict[str, "Metrics | list"]) -> str:
348418 # Format value, include std dev if available, and bold if it's the best
349419 formatted_val = f"${ val :.4f} $"
350420 if std_dev_metrics and std_dev_metrics .get (metric_key ) is not None :
351- formatted_val = f"${ val :.4f} \pm { std_dev_metrics .get (metric_key ):.4f} $"
421+ formatted_val = f"${ val :.4f} \pm { std_dev_metrics .get (metric_key ):.4f} $"
352422
353423 if val == best_values .get (metric_key ):
354- formatted_val = f"\\ boldmath {{ formatted_val}}"
424+ formatted_val = f"\\ textbf{{ { formatted_val } }}"
355425 row_values .append (formatted_val )
356426
357427 latex_str += f"{ display_name } & " + " & " .join (row_values ) + " \\ \\ \n "
0 commit comments