1+ #!/usr/bin/env python3
2+ """Load candidate regions and print summary statistics to the terminal."""
3+
4+ import argparse
5+ import logging
6+ from collections import Counter
7+ from pathlib import Path
8+
9+ from ..candidateregions import signalstrength_to_crs
10+
11+ log = logging .getLogger (__name__ )
12+
13+
14+ def compute_percentiles (values : list [int ], steps : int = 10 ) -> dict [int , int ]:
15+ if not values :
16+ return {}
17+
18+ sorted_values = sorted (values )
19+ n = len (sorted_values )
20+ percentiles = {}
21+ for p in range (0 , 101 , steps ):
22+ index = min (max (int (round (p / 100 * (n - 1 ))), 0 ), n - 1 )
23+ percentiles [p ] = sorted_values [index ]
24+ return percentiles
25+
26+
27+ def crs_stats (crs : list [signalstrength_to_crs .datatypes .CandidateRegion ]) -> None :
28+ region_sizes = [cr .referenceEnd - cr .referenceStart for cr in crs ]
29+ chromosomes = [cr .chr for cr in crs ]
30+ read_counts = [len (cr .get_read_names ()) for cr in crs ]
31+
32+ log .info ("Candidate regions loaded: %d" , len (crs ))
33+
34+ print ("=== CRS Summary ===" )
35+ print (f"Total candidate regions: { len (crs )} " )
36+ print ("" )
37+
38+ print ("Region size distribution (percentiles):" )
39+ for p , value in compute_percentiles (region_sizes , steps = 10 ).items ():
40+ print (f" { p :3d} th percentile: { value } " )
41+ print ("" )
42+
43+ print ("Signal read counts per region:" )
44+ print (f" min: { min (read_counts ) if read_counts else 0 } " )
45+ print (f" max: { max (read_counts ) if read_counts else 0 } " )
46+ print (f" mean: { sum (read_counts ) / len (read_counts ) if read_counts else 0 :.2f} " )
47+ print (f" median: { compute_percentiles (read_counts , steps = 50 ).get (50 , 0 )} " )
48+ print ("" )
49+
50+ print ("Regions per chromosome:" )
51+ for chrom , count in Counter (chromosomes ).most_common ():
52+ print (f" { chrom } : { count } " )
53+ print ("" )
54+
55+ print ("20 largest candidate regions:" )
56+ largest = sorted (crs , key = lambda x : x .referenceEnd - x .referenceStart , reverse = True )[:20 ]
57+ for cr in largest :
58+ size = cr .referenceEnd - cr .referenceStart
59+ print (
60+ f" { cr .chr } \t { cr .referenceStart } \t { cr .referenceEnd } \t { size } \t { cr .crID } "
61+ )
62+
63+
64+ def get_parser () -> argparse .ArgumentParser :
65+ parser = argparse .ArgumentParser (
66+ description = "Print statistics for a candidate regions database." ,
67+ formatter_class = argparse .ArgumentDefaultsHelpFormatter ,
68+ )
69+ parser .add_argument (
70+ "-i" ,
71+ "--input" ,
72+ type = Path ,
73+ required = True ,
74+ help = "Path to the candidate regions database file (e.g. crs.db)." ,
75+ metavar = "DB" ,
76+ )
77+ parser .add_argument (
78+ "-v" ,
79+ "--verbose" ,
80+ action = "store_true" ,
81+ help = "Enable verbose logging." ,
82+ )
83+ return parser
84+
85+
86+ def main () -> int :
87+ parser = get_parser ()
88+ args = parser .parse_args ()
89+
90+ log_level = logging .DEBUG if args .verbose else logging .INFO
91+ logging .basicConfig (
92+ level = log_level ,
93+ format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ,
94+ )
95+
96+ if not args .input .exists ():
97+ log .error ("Input database file not found: %s" , args .input )
98+ return 1
99+
100+ try :
101+ crs = signalstrength_to_crs .load_crs_from_db (path_db = args .input )
102+ except Exception as exc :
103+ log .error ("Failed to load candidate regions: %s" , exc )
104+ return 1
105+
106+ crs_stats (crs )
107+ return 0
108+
109+
110+ if __name__ == "__main__" :
111+ raise SystemExit (main ())
0 commit comments