ahmedtaha100 commited on
Commit
124c010
·
verified ·
1 Parent(s): ce56286

Upload aggregate.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. aggregate.py +163 -16
aggregate.py CHANGED
@@ -4,11 +4,66 @@ import argparse
4
  import contextlib
5
  import csv
6
  import json
7
- from collections import defaultdict
 
8
  from pathlib import Path
9
 
10
  import numpy as np
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  def load_all_reviews(results_dir: Path) -> dict[str, list[dict]]:
14
  reviews_by_reviewer: dict[str, list[dict]] = {}
@@ -83,6 +138,74 @@ def fleiss_kappa(matrix: np.ndarray, categories: list[int | float]) -> float:
83
  return float((p_bar - p_e) / (1.0 - p_e))
84
 
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  def compute_exclusions(
87
  reviews_by_reviewer: dict[str, list[dict]],
88
  num_pairs: int,
@@ -119,7 +242,10 @@ def compute_exclusions(
119
 
120
  scores = quality_scores.get(idx, [])
121
  if len(scores) >= 3 and (max(scores) - min(scores)) >= 3 and idx not in excluded_implausible:
122
- excluded_disputed.append(idx)
 
 
 
123
 
124
  all_excluded = sorted(set(excluded_implausible + excluded_disputed))
125
 
@@ -197,10 +323,12 @@ def compute_stratum_stats(
197
 
198
 
199
  def main() -> None:
 
200
  parser = argparse.ArgumentParser(description="Aggregate radiologist reviews")
201
  parser.add_argument("--pairs", type=str, required=True)
202
  parser.add_argument("--results-dir", type=str, default="results")
203
  parser.add_argument("--output", type=str, default="validation_report.json")
 
204
  args = parser.parse_args()
205
 
206
  with open(args.pairs) as f:
@@ -211,7 +339,7 @@ def main() -> None:
211
  reviewers = sorted(reviews_by_reviewer.keys())
212
 
213
  if not reviewers:
214
- print("No review files found.")
215
  return
216
 
217
  quality_matrix = build_rating_matrix(
@@ -232,6 +360,7 @@ def main() -> None:
232
 
233
  exclusions = compute_exclusions(reviews_by_reviewer, num_pairs)
234
  stratum_stats = compute_stratum_stats(pairs, reviews_by_reviewer, exclusions)
 
235
 
236
  total_reviewed = sum(len(rows) for rows in reviews_by_reviewer.values())
237
  per_reviewer: dict[str, dict] = {}
@@ -259,9 +388,12 @@ def main() -> None:
259
  "reviewers": reviewers,
260
  "total_reviews": total_reviewed,
261
  "inter_rater_reliability": {
262
- "fleiss_kappa_quality": round(kappa_quality, 4) if not np.isnan(kappa_quality) else None,
263
- "fleiss_kappa_plausibility": round(kappa_plausible, 4) if not np.isnan(kappa_plausible) else None,
264
- "fleiss_kappa_pathology_preservation": round(kappa_preserved, 4) if not np.isnan(kappa_preserved) else None,
 
 
 
265
  },
266
  "overall": {
267
  "mean_quality_score": round(mean_quality, 2) if mean_quality else None,
@@ -296,18 +428,33 @@ def main() -> None:
296
  with open(exclusion_path, "w") as f:
297
  json.dump({"excluded_pair_indices": exclusions["all_excluded"]}, f, indent=2)
298
 
299
- print(f"Reviewers: {len(reviewers)}")
300
- print(f"Total reviews: {total_reviewed}")
301
- print(f"Fleiss kappa (quality): {report['inter_rater_reliability']['fleiss_kappa_quality']}")
302
- print(f"Fleiss kappa (plausibility): {report['inter_rater_reliability']['fleiss_kappa_plausibility']}")
303
- print(f"Fleiss kappa (pathology): {report['inter_rater_reliability']['fleiss_kappa_pathology_preservation']}")
304
- print(f"Overall pass rate: {report['overall']['pass_rate']}")
305
- print(f"Total excluded: {report['exclusions']['total_excluded']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  for stratum, stats in stratum_stats.items():
307
  if stats.get("total", 0) > 0:
308
- print(f" {stratum}: {stats['passed']}/{stats['total']} passed ({stats['pass_rate']})")
309
- print(f"Report: {output_path}")
310
- print(f"Exclusion list: {exclusion_path}")
 
311
 
312
 
313
  if __name__ == "__main__":
 
4
  import contextlib
5
  import csv
6
  import json
7
+ import logging
8
+ from collections import Counter, defaultdict
9
  from pathlib import Path
10
 
11
  import numpy as np
12
 
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def write_validation_results_csv(
17
+ output_path: Path,
18
+ pairs: list[dict],
19
+ reviews_by_reviewer: dict[str, list[dict]],
20
+ exclusions: dict,
21
+ ) -> None:
22
+ pair_strata = {idx: classify_transformation(pair) for idx, pair in enumerate(pairs)}
23
+ implausible_any = set(exclusions["implausible_any_rater"])
24
+ implausible_majority = set(exclusions["implausible_majority"])
25
+ disputed_quality = set(exclusions["disputed_quality"])
26
+ final_excluded = set(exclusions["all_excluded"])
27
+
28
+ fieldnames = [
29
+ "pair_index",
30
+ "reviewer_id",
31
+ "clinically_plausible",
32
+ "pathology_preserved",
33
+ "quality_score",
34
+ "comments",
35
+ "timestamp",
36
+ "transform_type",
37
+ "excluded_implausible_any",
38
+ "excluded_implausible_majority",
39
+ "excluded_disputed_quality",
40
+ "excluded_final",
41
+ ]
42
+
43
+ output_path.parent.mkdir(parents=True, exist_ok=True)
44
+ with open(output_path, "w", newline="") as f:
45
+ writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
46
+ writer.writeheader()
47
+ for reviewer, rows in sorted(reviews_by_reviewer.items()):
48
+ for row in sorted(rows, key=lambda r: int(r["pair_index"])):
49
+ pair_index = int(row["pair_index"])
50
+ writer.writerow(
51
+ {
52
+ "pair_index": pair_index,
53
+ "reviewer_id": row.get("reviewer_id", reviewer),
54
+ "clinically_plausible": row.get("clinically_plausible", ""),
55
+ "pathology_preserved": row.get("pathology_preserved", ""),
56
+ "quality_score": row.get("quality_score", ""),
57
+ "comments": row.get("comments", ""),
58
+ "timestamp": row.get("timestamp", ""),
59
+ "transform_type": pair_strata.get(pair_index, "unknown"),
60
+ "excluded_implausible_any": int(pair_index in implausible_any),
61
+ "excluded_implausible_majority": int(pair_index in implausible_majority),
62
+ "excluded_disputed_quality": int(pair_index in disputed_quality),
63
+ "excluded_final": int(pair_index in final_excluded),
64
+ }
65
+ )
66
+
67
 
68
  def load_all_reviews(results_dir: Path) -> dict[str, list[dict]]:
69
  reviews_by_reviewer: dict[str, list[dict]] = {}
 
138
  return float((p_bar - p_e) / (1.0 - p_e))
139
 
140
 
141
+ def build_stratum_rating_matrix(
142
+ reviews_by_reviewer: dict[str, list[dict]],
143
+ field: str,
144
+ indices: list[int],
145
+ value_map: dict[str, float] | None = None,
146
+ ) -> np.ndarray:
147
+ index_set = set(indices)
148
+ reviewers = sorted(reviews_by_reviewer.keys())
149
+ idx_to_row = {idx: row_num for row_num, idx in enumerate(sorted(indices))}
150
+ matrix = np.full((len(indices), len(reviewers)), np.nan)
151
+ for j, reviewer in enumerate(reviewers):
152
+ for row in reviews_by_reviewer[reviewer]:
153
+ pair_idx = int(row["pair_index"])
154
+ if pair_idx in index_set and row.get(field):
155
+ val = row[field]
156
+ matrix_row = idx_to_row[pair_idx]
157
+ if value_map and val in value_map:
158
+ matrix[matrix_row, j] = value_map[val]
159
+ else:
160
+ with contextlib.suppress(ValueError, TypeError):
161
+ matrix[matrix_row, j] = float(val)
162
+ return matrix
163
+
164
+
165
+ def compute_stratum_kappa(
166
+ pairs: list[dict],
167
+ reviews_by_reviewer: dict[str, list[dict]],
168
+ ) -> dict[str, dict[str, float | None]]:
169
+ pair_strata: dict[int, str] = {}
170
+ for i, pair in enumerate(pairs):
171
+ pair_strata[i] = classify_transformation(pair)
172
+
173
+ strata = ["age_only", "sex_only", "intersectional"]
174
+ result: dict[str, dict[str, float | None]] = {}
175
+
176
+ for stratum in strata:
177
+ indices = [i for i, s in pair_strata.items() if s == stratum]
178
+ if not indices:
179
+ result[stratum] = {
180
+ "fleiss_kappa_quality": None,
181
+ "fleiss_kappa_plausibility": None,
182
+ "fleiss_kappa_pathology_preservation": None,
183
+ }
184
+ continue
185
+
186
+ q_matrix = build_stratum_rating_matrix(reviews_by_reviewer, "quality_score", indices)
187
+ p_matrix = build_stratum_rating_matrix(
188
+ reviews_by_reviewer, "clinically_plausible", indices,
189
+ value_map={"Yes": 1.0, "No": 0.0},
190
+ )
191
+ pp_matrix = build_stratum_rating_matrix(
192
+ reviews_by_reviewer, "pathology_preserved", indices,
193
+ value_map={"Yes": 1.0, "No": 0.0, "Uncertain": 0.5},
194
+ )
195
+
196
+ kq = fleiss_kappa(q_matrix, categories=[1, 2, 3, 4, 5])
197
+ kp = fleiss_kappa(p_matrix, categories=[0, 1])
198
+ kpp = fleiss_kappa(pp_matrix, categories=[0, 0.5, 1])
199
+
200
+ result[stratum] = {
201
+ "fleiss_kappa_quality": round(kq, 4) if not np.isnan(kq) else None,
202
+ "fleiss_kappa_plausibility": round(kp, 4) if not np.isnan(kp) else None,
203
+ "fleiss_kappa_pathology_preservation": round(kpp, 4) if not np.isnan(kpp) else None,
204
+ }
205
+
206
+ return result
207
+
208
+
209
  def compute_exclusions(
210
  reviews_by_reviewer: dict[str, list[dict]],
211
  num_pairs: int,
 
242
 
243
  scores = quality_scores.get(idx, [])
244
  if len(scores) >= 3 and (max(scores) - min(scores)) >= 3 and idx not in excluded_implausible:
245
+ score_counts = Counter(scores)
246
+ has_majority = any(c > len(scores) / 2 for c in score_counts.values())
247
+ if not has_majority:
248
+ excluded_disputed.append(idx)
249
 
250
  all_excluded = sorted(set(excluded_implausible + excluded_disputed))
251
 
 
323
 
324
 
325
  def main() -> None:
326
+ logging.basicConfig(level=logging.INFO)
327
  parser = argparse.ArgumentParser(description="Aggregate radiologist reviews")
328
  parser.add_argument("--pairs", type=str, required=True)
329
  parser.add_argument("--results-dir", type=str, default="results")
330
  parser.add_argument("--output", type=str, default="validation_report.json")
331
+ parser.add_argument("--csv-output", type=str, default=None)
332
  args = parser.parse_args()
333
 
334
  with open(args.pairs) as f:
 
339
  reviewers = sorted(reviews_by_reviewer.keys())
340
 
341
  if not reviewers:
342
+ logger.warning("No review files found.")
343
  return
344
 
345
  quality_matrix = build_rating_matrix(
 
360
 
361
  exclusions = compute_exclusions(reviews_by_reviewer, num_pairs)
362
  stratum_stats = compute_stratum_stats(pairs, reviews_by_reviewer, exclusions)
363
+ stratum_kappa = compute_stratum_kappa(pairs, reviews_by_reviewer)
364
 
365
  total_reviewed = sum(len(rows) for rows in reviews_by_reviewer.values())
366
  per_reviewer: dict[str, dict] = {}
 
388
  "reviewers": reviewers,
389
  "total_reviews": total_reviewed,
390
  "inter_rater_reliability": {
391
+ "overall": {
392
+ "fleiss_kappa_quality": round(kappa_quality, 4) if not np.isnan(kappa_quality) else None,
393
+ "fleiss_kappa_plausibility": round(kappa_plausible, 4) if not np.isnan(kappa_plausible) else None,
394
+ "fleiss_kappa_pathology_preservation": round(kappa_preserved, 4) if not np.isnan(kappa_preserved) else None,
395
+ },
396
+ "by_transformation_type": stratum_kappa,
397
  },
398
  "overall": {
399
  "mean_quality_score": round(mean_quality, 2) if mean_quality else None,
 
428
  with open(exclusion_path, "w") as f:
429
  json.dump({"excluded_pair_indices": exclusions["all_excluded"]}, f, indent=2)
430
 
431
+ csv_output_path = (
432
+ Path(args.csv_output)
433
+ if args.csv_output
434
+ else output_path.parent / "validation_results.csv"
435
+ )
436
+ write_validation_results_csv(csv_output_path, pairs, reviews_by_reviewer, exclusions)
437
+
438
+ logger.info("Reviewers: %d", len(reviewers))
439
+ logger.info("Total reviews: %d", total_reviewed)
440
+ irr = report["inter_rater_reliability"]["overall"]
441
+ logger.info("Fleiss kappa (quality): %s", irr["fleiss_kappa_quality"])
442
+ logger.info("Fleiss kappa (plausibility): %s", irr["fleiss_kappa_plausibility"])
443
+ logger.info("Fleiss kappa (pathology): %s", irr["fleiss_kappa_pathology_preservation"])
444
+ for stratum, sk in stratum_kappa.items():
445
+ logger.info(
446
+ " %s kappa: quality=%s plausibility=%s pathology=%s",
447
+ stratum, sk["fleiss_kappa_quality"], sk["fleiss_kappa_plausibility"],
448
+ sk["fleiss_kappa_pathology_preservation"],
449
+ )
450
+ logger.info("Overall pass rate: %s", report["overall"]["pass_rate"])
451
+ logger.info("Total excluded: %d", report["exclusions"]["total_excluded"])
452
  for stratum, stats in stratum_stats.items():
453
  if stats.get("total", 0) > 0:
454
+ logger.info(" %s: %d/%d passed (%s)", stratum, stats["passed"], stats["total"], stats["pass_rate"])
455
+ logger.info("Report: %s", output_path)
456
+ logger.info("Exclusion list: %s", exclusion_path)
457
+ logger.info("Validation CSV: %s", csv_output_path)
458
 
459
 
460
  if __name__ == "__main__":