1010import json
1111import os
1212import time
13+ import tracemalloc
1314from pathlib import Path
1415
1516import pandas as pd
17+ from sklearn .metrics import roc_auc_score
1618
1719from mir .biomarkers .associations import AssociationParams , associate_clonotype_metadata , build_public_clonotype_panel
1820from mir .common .filter import filter_functional
@@ -32,6 +34,41 @@ def _env_int(name: str, default: int) -> int:
3234 return max (1 , value )
3335
3436
37+ def _reference_file (dataset_root : Path ) -> Path | None :
38+ candidates = [
39+ dataset_root / "covid19_biomarker_clonotypes.csv" ,
40+ dataset_root / "covid_associated_clonotypes.csv" ,
41+ ]
42+ for candidate in candidates :
43+ if candidate .exists ():
44+ return candidate
45+ return None
46+
47+
48+ def _reference_cdr3_set (path : Path ) -> set [str ]:
49+ df = pd .read_csv (path )
50+ for col in ("cdr3" , "junction_aa" , "sequence" ):
51+ if col in df .columns :
52+ return {str (x ) for x in df [col ].dropna ().astype (str )}
53+ return set ()
54+
55+
56+ def _sample_biomarker_scores (samples : list [SampleRepertoire ], biomarker_cdr3 : set [str ]) -> pd .DataFrame :
57+ rows = []
58+ for sample in samples :
59+ rep = sample .get_locus ("TRB" )
60+ seqs = {str (c .junction_aa ) for c in rep .clonotypes if c .junction_aa }
61+ score = float (len (seqs & biomarker_cdr3 ))
62+ rows .append (
63+ {
64+ "sample_id" : sample .sample_id ,
65+ "covid" : 1 if str (sample .sample_metadata .get ("COVID_status" , "" )) == "COVID" else 0 ,
66+ "score" : score ,
67+ }
68+ )
69+ return pd .DataFrame (rows )
70+
71+
3572def main () -> int :
3673 dataset_root = ensure_airr_covid19 ()
3774 metadata = pd .read_csv (dataset_root / "metadata_trb_min100000.tsv" , sep = "\t " , dtype = {"donor_id" : "string" }, low_memory = False )
@@ -45,11 +82,14 @@ def main() -> int:
4582 samples : list [SampleRepertoire ] = []
4683
4784 t0 = time .perf_counter ()
85+ tracemalloc .start ()
4886 for _ , row in metadata .sort_values (["COVID_status" , "sample_id" ]).head (max_samples ).iterrows ():
4987 path = Path (dataset_root ) / str (row ["file_name" ])
5088 if not path .exists ():
5189 continue
52- clones = parser .parse (str (path ))
90+ clones = [c for c in parser .parse (str (path )) if str (c .locus ).upper () == "TRB" ]
91+ if not clones :
92+ continue
5393 rep = filter_functional (LocusRepertoire (clonotypes = clones , locus = "TRB" , repertoire_id = str (row ["sample_id" ])))
5494 if rep .clonotype_count == 0 :
5595 continue
@@ -62,8 +102,11 @@ def main() -> int:
62102 )
63103
64104 load_s = time .perf_counter () - t0
105+ _ , load_peak = tracemalloc .get_traced_memory ()
106+ tracemalloc .stop ()
65107 targets = build_public_clonotype_panel (samples , locus = "TRB" , min_sample_fraction = min_fraction )[:max_targets ]
66108
109+ tracemalloc .start ()
67110 t1 = time .perf_counter ()
68111 fisher_res = associate_clonotype_metadata (
69112 samples ,
@@ -73,7 +116,10 @@ def main() -> int:
73116 params = AssociationParams (test = "fisher" , count_mode = "sample" , match_mode = "none" ),
74117 )
75118 fisher_s = time .perf_counter () - t1
119+ _ , fisher_peak = tracemalloc .get_traced_memory ()
120+ tracemalloc .stop ()
76121
122+ tracemalloc .start ()
77123 t2 = time .perf_counter ()
78124 depth_res = associate_clonotype_metadata (
79125 samples ,
@@ -83,17 +129,45 @@ def main() -> int:
83129 params = AssociationParams (test = "depth_glm" , count_mode = "rearrangement" , match_mode = "none" ),
84130 )
85131 depth_s = time .perf_counter () - t2
132+ _ , depth_peak = tracemalloc .get_traced_memory ()
133+ tracemalloc .stop ()
134+
135+ fisher_df = fisher_res .table .to_pandas ().sort_values (["q_value" , "p_value" ]).reset_index (drop = True )
136+ positive_hits = fisher_df [(fisher_df ["odds_ratio" ].fillna (0.0 ) > 1.0 ) & (fisher_df ["q_value" ] < 0.2 )]
137+ if positive_hits .empty :
138+ positive_hits = fisher_df .head (30 )
139+ biomarker_set = set (positive_hits ["junction_aa" ].astype (str ))
140+ score_df = _sample_biomarker_scores (samples , biomarker_set )
141+ auc = float ("nan" )
142+ if score_df ["covid" ].nunique () == 2 and score_df ["score" ].nunique () > 1 :
143+ auc = float (roc_auc_score (score_df ["covid" ], score_df ["score" ]))
144+
145+ ref = _reference_file (Path (dataset_root ))
146+ ref_overlap_top100 = None
147+ ref_overlap_biomarkers = None
148+ if ref is not None :
149+ ref_set = _reference_cdr3_set (ref )
150+ top100 = set (fisher_df .head (100 )["junction_aa" ].astype (str ))
151+ ref_overlap_top100 = len (top100 & ref_set )
152+ ref_overlap_biomarkers = len (biomarker_set & ref_set )
86153
87154 out = {
88155 "dataset_root" : str (dataset_root ),
89156 "samples" : len (samples ),
90157 "targets" : len (targets ),
91158 "load_seconds" : load_s ,
159+ "load_peak_mib" : float (load_peak / (1024 ** 2 )),
92160 "fisher_seconds" : fisher_s ,
161+ "fisher_peak_mib" : float (fisher_peak / (1024 ** 2 )),
93162 "depth_glm_seconds" : depth_s ,
163+ "depth_glm_peak_mib" : float (depth_peak / (1024 ** 2 )),
94164 "fisher_rows" : int (fisher_res .table .height ),
95165 "depth_rows" : int (depth_res .table .height ),
96- "reference_csv_exists" : bool ((Path (dataset_root ) / "covid_associated_clonotypes.csv" ).exists ()),
166+ "biomarker_count" : int (len (biomarker_set )),
167+ "separation_auc" : auc ,
168+ "reference_csv" : str (ref ) if ref is not None else None ,
169+ "reference_overlap_top100" : ref_overlap_top100 ,
170+ "reference_overlap_biomarkers" : ref_overlap_biomarkers ,
97171 }
98172
99173 print (json .dumps (out , indent = 2 , sort_keys = True ))
0 commit comments