dicleak 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dicleak-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 manulint contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
dicleak-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: dicleak
3
+ Version: 0.1.0
4
+ Summary: Dataset integrity auditor for medical-imaging AI: catch patient-level data leakage and split flaws before they sink your model or paper
5
+ Author: dicleak contributors
6
+ License: MIT
7
+ Keywords: medical-imaging,dicom,dataset,data-leakage,machine-learning,validation
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Dynamic: license-file
14
+
15
+ # dicleak
16
+
17
+ Dataset integrity auditor for medical-imaging AI. Point it at a dataset manifest
18
+ CSV and it catches the flaws that sink models and papers **before** you train:
19
+ patient-level data leakage across splits, single-class splits, broken
20
+ stratification, tiny test sets, duplicate files, un-deidentified IDs.
21
+
22
+ 医学影像 AI 数据集完整性审计工具。对 manifest CSV 跑一遍,在训练**之前**
23
+ 揪出致命缺陷:病人级数据泄露、单类别数据划分、分布失衡、测试集过小、
24
+ 文件重复、未脱敏的 ID。
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install dicleak
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ dicleak splits.csv
36
+ dicleak splits.csv --report audit.md
37
+ dicleak splits.csv --report audit.md --cta "https://your-service.example/audit"
38
+ ```
39
+
40
+ The manifest CSV needs three columns (headers are flexible):
41
+
42
+ | concept | accepted headers |
43
+ |---|---|
44
+ | patient id | `patient_id`, `patientid`, `patient`, `subject_id`, `subject`, `pid` |
45
+ | split | `split`, `subset`, `fold`, `partition`, `set` (`train`/`val`/`test` and common aliases) |
46
+ | label | `label`, `class`, `target`, `diagnosis`, `annotation` |
47
+ | file path (optional) | `filepath`, `file_path`, `path`, `filename` |
48
+
49
+ Exit code is `0` when the score is 60+, `1` otherwise — handy for CI gates.
50
+
51
+ ## The 7 checks
52
+
53
+ | # | check | severity |
54
+ |---|---|---|
55
+ | 1 | patient-level leakage across splits | CRITICAL |
56
+ | 2 | split containing a single class | CRITICAL |
57
+ | 3 | label distribution drift between splits | HIGH |
58
+ | 4 | test split < 10% (or any split < 5%) | MEDIUM |
59
+ | 5 | duplicate files in the manifest | MEDIUM |
60
+ | 6 | patient IDs that look like real names | HIGH |
61
+ | 7 | too few unique patients | LOW/MEDIUM |
62
+
63
+ ## Why this exists
64
+
65
+ A disturbing share of medical-AI papers report inflated accuracy because the
66
+ same patient ended up in both the training and test sets. Reviewers now check
67
+ for this. `dicleak` makes the check a one-liner.
68
+
69
+ ## Sample
70
+
71
+ `samples/flawed_splits.csv` is a deliberately broken manifest (leaked patients,
72
+ single-class val split, tiny test set, name-like IDs...). It scores 26/100:
73
+
74
+ ```bash
75
+ dicleak samples/flawed_splits.csv
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,66 @@
1
+ # dicleak
2
+
3
+ Dataset integrity auditor for medical-imaging AI. Point it at a dataset manifest
4
+ CSV and it catches the flaws that sink models and papers **before** you train:
5
+ patient-level data leakage across splits, single-class splits, broken
6
+ stratification, tiny test sets, duplicate files, un-deidentified IDs.
7
+
8
+ 医学影像 AI 数据集完整性审计工具。对 manifest CSV 跑一遍,在训练**之前**
9
+ 揪出致命缺陷:病人级数据泄露、单类别数据划分、分布失衡、测试集过小、
10
+ 文件重复、未脱敏的 ID。
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install dicleak
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```bash
21
+ dicleak splits.csv
22
+ dicleak splits.csv --report audit.md
23
+ dicleak splits.csv --report audit.md --cta "https://your-service.example/audit"
24
+ ```
25
+
26
+ The manifest CSV needs three columns (headers are flexible):
27
+
28
+ | concept | accepted headers |
29
+ |---|---|
30
+ | patient id | `patient_id`, `patientid`, `patient`, `subject_id`, `subject`, `pid` |
31
+ | split | `split`, `subset`, `fold`, `partition`, `set` (`train`/`val`/`test` and common aliases) |
32
+ | label | `label`, `class`, `target`, `diagnosis`, `annotation` |
33
+ | file path (optional) | `filepath`, `file_path`, `path`, `filename` |
34
+
35
+ Exit code is `0` when the score is 60+, `1` otherwise — handy for CI gates.
36
+
37
+ ## The 7 checks
38
+
39
+ | # | check | severity |
40
+ |---|---|---|
41
+ | 1 | patient-level leakage across splits | CRITICAL |
42
+ | 2 | split containing a single class | CRITICAL |
43
+ | 3 | label distribution drift between splits | HIGH |
44
+ | 4 | test split < 10% (or any split < 5%) | MEDIUM |
45
+ | 5 | duplicate files in the manifest | MEDIUM |
46
+ | 6 | patient IDs that look like real names | HIGH |
47
+ | 7 | too few unique patients | LOW/MEDIUM |
48
+
49
+ ## Why this exists
50
+
51
+ A disturbing share of medical-AI papers report inflated accuracy because the
52
+ same patient ended up in both the training and test sets. Reviewers now check
53
+ for this. `dicleak` makes the check a one-liner.
54
+
55
+ ## Sample
56
+
57
+ `samples/flawed_splits.csv` is a deliberately broken manifest (leaked patients,
58
+ single-class val split, tiny test set, name-like IDs...). It scores 26/100:
59
+
60
+ ```bash
61
+ dicleak samples/flawed_splits.csv
62
+ ```
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,3 @@
1
+ """dicleak: dataset integrity auditor for medical-imaging AI."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,208 @@
1
+ """The seven integrity checks. Each returns a list of Finding."""
2
+
3
+ import re
4
+ from collections import Counter, defaultdict
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class Finding:
10
+ check_id: str
11
+ severity: str # CRITICAL / HIGH / MEDIUM / LOW
12
+ title: str
13
+ detail: str
14
+ deduction: int
15
+
16
+
17
+ SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
18
+ SEVERITY_ICON = {"CRITICAL": "[!!]", "HIGH": "[!]", "MEDIUM": "[*]", "LOW": "[.]"}
19
+
20
+ _NAME_LIKE = re.compile(r"[A-Za-z]+[ _\-][A-Za-z]+")
21
+
22
+
23
+ def _splits_of(rows):
24
+ return sorted({r["split"] for r in rows})
25
+
26
+
27
+ def check_leakage(rows):
28
+ """CRITICAL: the same patient appears in more than one split."""
29
+ by_patient = defaultdict(set)
30
+ for r in rows:
31
+ by_patient[r["patient_id"]].add(r["split"])
32
+ leaked = {p: sorted(s) for p, s in by_patient.items() if len(s) > 1}
33
+ findings = []
34
+ for i, (patient, splits) in enumerate(sorted(leaked.items())):
35
+ if i >= 10:
36
+ findings.append(Finding(
37
+ "leakage", "CRITICAL",
38
+ "Patient-level data leakage (+%d more patients)" % (len(leaked) - 10),
39
+ "Additional leaked patients not listed individually.",
40
+ 0,
41
+ ))
42
+ break
43
+ findings.append(Finding(
44
+ "leakage", "CRITICAL",
45
+ "Patient '%s' appears in multiple splits: %s" % (patient, ", ".join(splits)),
46
+ "The same patient in train and test lets the model memorize "
47
+ "patient-specific features instead of learning the disease. "
48
+ "This inflates reported accuracy and is a known cause of "
49
+ "non-reproducible results and retractions in medical AI.",
50
+ 10 if i == 0 else 5,
51
+ ))
52
+ return findings
53
+
54
+
55
+ def check_single_class_split(rows):
56
+ """CRITICAL: a split that contains only one label value."""
57
+ findings = []
58
+ by_split = defaultdict(set)
59
+ for r in rows:
60
+ by_split[r["split"]].add(r["label"])
61
+ for split in sorted(by_split):
62
+ labels = by_split[split]
63
+ if len(labels) == 1:
64
+ findings.append(Finding(
65
+ "single_class_split", "CRITICAL",
66
+ "Split '%s' contains only one class: '%s'" % (split, next(iter(labels))),
67
+ "A split with a single class cannot train or fairly evaluate "
68
+ "a classifier. Check your stratification or labeling pipeline.",
69
+ 15,
70
+ ))
71
+ return findings
72
+
73
+
74
+ def check_stratification(rows, threshold=0.20):
75
+ """HIGH: label distribution differs strongly between splits."""
76
+ findings = []
77
+ by_split = defaultdict(Counter)
78
+ for r in rows:
79
+ by_split[r["split"]][r["label"]] += 1
80
+ labels = {lbl for c in by_split.values() for lbl in c}
81
+ for lbl in sorted(labels):
82
+ shares = {}
83
+ for split, counter in by_split.items():
84
+ total = sum(counter.values())
85
+ shares[split] = counter[lbl] / total if total else 0.0
86
+ spread = max(shares.values()) - min(shares.values())
87
+ if spread > threshold:
88
+ detail = ", ".join(
89
+ "%s=%.0f%%" % (s, shares[s] * 100) for s in sorted(shares))
90
+ findings.append(Finding(
91
+ "stratification", "HIGH",
92
+ "Class '%s' is unevenly distributed across splits (%s)" % (lbl, detail),
93
+ "A spread of %.0fpp between splits means the model sees a "
94
+ "different class balance at train vs. test time, which skews "
95
+ "metrics. Stratify by label when splitting." % (spread * 100),
96
+ 8,
97
+ ))
98
+ return findings
99
+
100
+
101
+ def check_split_ratios(rows):
102
+ """MEDIUM: test split too small, or a split nearly empty."""
103
+ findings = []
104
+ counter = Counter(r["split"] for r in rows)
105
+ total = sum(counter.values())
106
+ for split in sorted(counter):
107
+ share = counter[split] / total
108
+ if split == "test" and share < 0.10:
109
+ findings.append(Finding(
110
+ "split_ratios", "MEDIUM",
111
+ "Test split is only %.1f%% of the data (%d rows)" % (share * 100, counter[split]),
112
+ "A tiny test set gives noisy performance estimates with wide "
113
+ "confidence intervals. Aim for at least 10-20%.",
114
+ 6,
115
+ ))
116
+ elif share < 0.05:
117
+ findings.append(Finding(
118
+ "split_ratios", "MEDIUM",
119
+ "Split '%s' is only %.1f%% of the data (%d rows)" % (split, share * 100, counter[split]),
120
+ "A near-empty split contributes little and may indicate a "
121
+ "splitting bug.",
122
+ 6,
123
+ ))
124
+ return findings
125
+
126
+
127
+ def check_patient_count(rows):
128
+ """LOW/MEDIUM: too few unique patients to generalize."""
129
+ n = len({r["patient_id"] for r in rows})
130
+ if n < 20:
131
+ return [Finding(
132
+ "patient_count", "MEDIUM",
133
+ "Only %d unique patients in the whole dataset" % n,
134
+ "With so few patients, results are unlikely to generalize and "
135
+ "reviewers will question external validity.",
136
+ 8,
137
+ )]
138
+ if n < 50:
139
+ return [Finding(
140
+ "patient_count", "LOW",
141
+ "Only %d unique patients in the whole dataset" % n,
142
+ "Small patient counts limit generalizability; consider "
143
+ "multi-center data or external validation.",
144
+ 4,
145
+ )]
146
+ return []
147
+
148
+
149
+ def check_duplicates(rows):
150
+ """MEDIUM: the same file listed twice (only when filepaths are given).
151
+
152
+ Without filepaths, repeated (patient, split, label) rows are expected --
153
+ one patient normally contributes multiple images -- so the check is skipped.
154
+ """
155
+ keyed = [r for r in rows if r.get("filepath")]
156
+ if not keyed:
157
+ return []
158
+ seen = Counter(r["filepath"] for r in keyed)
159
+ dup_files = sum(1 for c in seen.values() if c > 1)
160
+ if dup_files:
161
+ return [Finding(
162
+ "duplicates", "MEDIUM",
163
+ "%d file(s) listed more than once in the manifest" % dup_files,
164
+ "Duplicate files overweight those samples and can hint at a "
165
+ "broken export or merge step upstream.",
166
+ 5,
167
+ )]
168
+ return []
169
+
170
+
171
+ def check_phi_risk(rows):
172
+ """HIGH: patient ids that look like real names (de-identification failure)."""
173
+ flagged = sorted({r["patient_id"] for r in rows if _NAME_LIKE.search(r["patient_id"])})
174
+ if not flagged:
175
+ return []
176
+ shown = ", ".join("'%s'" % p for p in flagged[:5])
177
+ more = " (+%d more)" % (len(flagged) - 5) if len(flagged) > 5 else ""
178
+ return [Finding(
179
+ "phi_risk", "HIGH",
180
+ "%d patient id(s) look like personal names: %s%s" % (len(flagged), shown, more),
181
+ "IDs resembling names suggest the data was not de-identified. "
182
+ "Sharing or publishing this dataset may violate privacy rules "
183
+ "(HIPAA/GDPR) and journal data policies.",
184
+ 8,
185
+ )]
186
+
187
+
188
+ ALL_CHECKS = (
189
+ check_leakage,
190
+ check_single_class_split,
191
+ check_stratification,
192
+ check_split_ratios,
193
+ check_patient_count,
194
+ check_duplicates,
195
+ check_phi_risk,
196
+ )
197
+
198
+
199
+ def run_all(rows):
200
+ findings = []
201
+ for check in ALL_CHECKS:
202
+ findings.extend(check(rows))
203
+ findings.sort(key=lambda f: (SEVERITY_ORDER[f.severity], f.check_id))
204
+ return findings
205
+
206
+
207
+ def score(findings):
208
+ return max(0, 100 - sum(f.deduction for f in findings))
@@ -0,0 +1,44 @@
1
+ """Command-line interface."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from . import __version__
7
+ from .checks import run_all, score
8
+ from .readers import load_manifest
9
+ from .report import render_markdown, render_terminal
10
+
11
+
12
+ def build_parser():
13
+ p = argparse.ArgumentParser(
14
+ prog="dicleak",
15
+ description="Audit a medical-imaging AI dataset manifest for "
16
+ "patient-level leakage and split flaws.",
17
+ )
18
+ p.add_argument("manifest", help="CSV with patient_id, split and label columns")
19
+ p.add_argument("--report", metavar="OUT.md",
20
+ help="also write a Markdown report to OUT.md")
21
+ p.add_argument("--cta", metavar="URL",
22
+ help="append a service link footer to the Markdown report")
23
+ p.add_argument("--version", action="version", version="dicleak " + __version__)
24
+ return p
25
+
26
+
27
+ def main(argv=None):
28
+ args = build_parser().parse_args(argv)
29
+ rows, warnings = load_manifest(args.manifest)
30
+ for w in warnings:
31
+ print("warning: %s" % w, file=sys.stderr)
32
+ findings = run_all(rows)
33
+ value = score(findings)
34
+ print(render_terminal(findings, value, rows))
35
+ if args.report:
36
+ md = render_markdown(findings, value, rows, cta_url=args.cta)
37
+ with open(args.report, "w", encoding="utf-8") as fh:
38
+ fh.write(md)
39
+ print("Markdown report written to %s" % args.report)
40
+ return 0 if value >= 60 else 1
41
+
42
+
43
+ if __name__ == "__main__":
44
+ sys.exit(main())
@@ -0,0 +1,93 @@
1
+ """CSV manifest loading and column normalization."""
2
+
3
+ import csv
4
+ import sys
5
+
6
+ # accepted header spellings -> canonical field name
7
+ COLUMN_ALIASES = {
8
+ "patient_id": "patient_id",
9
+ "patientid": "patient_id",
10
+ "patient": "patient_id",
11
+ "subject_id": "patient_id",
12
+ "subjectid": "patient_id",
13
+ "subject": "patient_id",
14
+ "pid": "patient_id",
15
+ "split": "split",
16
+ "subset": "split",
17
+ "fold": "split",
18
+ "partition": "split",
19
+ "set": "split",
20
+ "phase": "split",
21
+ "label": "label",
22
+ "class": "label",
23
+ "target": "label",
24
+ "diagnosis": "label",
25
+ "annotation": "label",
26
+ "filepath": "filepath",
27
+ "file_path": "filepath",
28
+ "path": "filepath",
29
+ "filename": "filepath",
30
+ "file": "filepath",
31
+ }
32
+
33
+ SPLIT_ALIASES = {
34
+ "train": "train",
35
+ "training": "train",
36
+ "trn": "train",
37
+ "val": "val",
38
+ "valid": "val",
39
+ "validation": "val",
40
+ "dev": "val",
41
+ "test": "test",
42
+ "testing": "test",
43
+ "tst": "test",
44
+ }
45
+
46
+ REQUIRED = ("patient_id", "split", "label")
47
+
48
+
49
+ def _normalize_header(name):
50
+ return name.strip().lower().replace("-", "_").replace(" ", "_")
51
+
52
+
53
+ def load_manifest(path):
54
+ """Load a CSV manifest, return (rows, warnings).
55
+
56
+ rows: list of dicts with keys patient_id / split / label / filepath.
57
+ Raises SystemExit with a message on fatal problems.
58
+ """
59
+ try:
60
+ fh = open(path, newline="", encoding="utf-8-sig")
61
+ except OSError as exc:
62
+ sys.exit("dicleak: cannot open %s: %s" % (path, exc))
63
+ with fh:
64
+ reader = csv.DictReader(fh)
65
+ if not reader.fieldnames:
66
+ sys.exit("dicleak: %s looks empty (no header row)" % path)
67
+ mapping = {}
68
+ for raw in reader.fieldnames:
69
+ key = _normalize_header(raw)
70
+ if key in COLUMN_ALIASES:
71
+ mapping[raw] = COLUMN_ALIASES[key]
72
+ missing = [c for c in REQUIRED if c not in mapping.values()]
73
+ if missing:
74
+ sys.exit(
75
+ "dicleak: %s is missing required column(s): %s "
76
+ "(need patient id, split and label columns)" % (path, ", ".join(missing))
77
+ )
78
+ rows = []
79
+ warnings = []
80
+ for lineno, raw_row in enumerate(reader, start=2):
81
+ row = {}
82
+ for raw_key, value in raw_row.items():
83
+ if raw_key in mapping:
84
+ row[mapping[raw_key]] = (value or "").strip()
85
+ if not row.get("patient_id"):
86
+ warnings.append("line %d: empty patient id, skipped" % lineno)
87
+ continue
88
+ split_raw = row.get("split", "").lower()
89
+ row["split"] = SPLIT_ALIASES.get(split_raw, split_raw)
90
+ rows.append(row)
91
+ if not rows:
92
+ sys.exit("dicleak: no usable data rows in %s" % path)
93
+ return rows, warnings
@@ -0,0 +1,89 @@
1
+ """Terminal and Markdown report rendering."""
2
+
3
+ from collections import Counter
4
+
5
+ from .checks import SEVERITY_ICON
6
+
7
+
8
+ def _stats(rows):
9
+ patients = {r["patient_id"] for r in rows}
10
+ splits = Counter(r["split"] for r in rows)
11
+ labels = Counter(r["label"] for r in rows)
12
+ return {
13
+ "rows": len(rows),
14
+ "patients": len(patients),
15
+ "splits": dict(sorted(splits.items())),
16
+ "labels": dict(sorted(labels.items())),
17
+ }
18
+
19
+
20
+ def _grade(score):
21
+ if score >= 90:
22
+ return "A - publishable shape"
23
+ if score >= 75:
24
+ return "B - minor issues"
25
+ if score >= 60:
26
+ return "C - fix before training"
27
+ return "D - do not train on this"
28
+
29
+
30
+ def render_terminal(findings, score_value, rows):
31
+ st = _stats(rows)
32
+ lines = []
33
+ lines.append("=" * 60)
34
+ lines.append("dicleak dataset audit")
35
+ lines.append("=" * 60)
36
+ lines.append("rows: %d patients: %d" % (st["rows"], st["patients"]))
37
+ lines.append("splits: %s" % ", ".join(
38
+ "%s=%d" % kv for kv in st["splits"].items()))
39
+ lines.append("labels: %s" % ", ".join(
40
+ "%s=%d" % kv for kv in st["labels"].items()))
41
+ lines.append("-" * 60)
42
+ lines.append("score: %d/100 [%s]" % (score_value, _grade(score_value)))
43
+ lines.append("-" * 60)
44
+ if not findings:
45
+ lines.append("No issues found. Clean split.")
46
+ for f in findings:
47
+ icon = SEVERITY_ICON.get(f.severity, "[?]")
48
+ lines.append("%s %s (-%d)" % (icon, f.title, f.deduction))
49
+ lines.append(" %s" % f.detail)
50
+ lines.append("=" * 60)
51
+ return "\n".join(lines)
52
+
53
+
54
+ def render_markdown(findings, score_value, rows, cta_url=None):
55
+ st = _stats(rows)
56
+ lines = []
57
+ lines.append("# dicleak dataset audit report")
58
+ lines.append("")
59
+ lines.append("**Score: %d/100** - %s" % (score_value, _grade(score_value)))
60
+ lines.append("")
61
+ lines.append("## Dataset overview")
62
+ lines.append("")
63
+ lines.append("| metric | value |")
64
+ lines.append("|---|---|")
65
+ lines.append("| rows | %d |" % st["rows"])
66
+ lines.append("| unique patients | %d |" % st["patients"])
67
+ for split, n in st["splits"].items():
68
+ lines.append("| split `%s` | %d |" % (split, n))
69
+ for label, n in st["labels"].items():
70
+ lines.append("| label `%s` | %d |" % (label, n))
71
+ lines.append("")
72
+ lines.append("## Findings (%d)" % len(findings))
73
+ lines.append("")
74
+ if not findings:
75
+ lines.append("No issues found. Clean split.")
76
+ for f in findings:
77
+ lines.append("### %s %s (-%d)" % (
78
+ SEVERITY_ICON.get(f.severity, "[?]"), f.title, f.deduction))
79
+ lines.append("")
80
+ lines.append("Severity: **%s** | Check: `%s`" % (f.severity, f.check_id))
81
+ lines.append("")
82
+ lines.append(f.detail)
83
+ lines.append("")
84
+ if cta_url:
85
+ lines.append("---")
86
+ lines.append(
87
+ "Need expert eyes on your medical-imaging dataset? "
88
+ "[Clinical data audit service](%s)" % cta_url)
89
+ return "\n".join(lines)
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: dicleak
3
+ Version: 0.1.0
4
+ Summary: Dataset integrity auditor for medical-imaging AI: catch patient-level data leakage and split flaws before they sink your model or paper
5
+ Author: dicleak contributors
6
+ License: MIT
7
+ Keywords: medical-imaging,dicom,dataset,data-leakage,machine-learning,validation
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Dynamic: license-file
14
+
15
+ # dicleak
16
+
17
+ Dataset integrity auditor for medical-imaging AI. Point it at a dataset manifest
18
+ CSV and it catches the flaws that sink models and papers **before** you train:
19
+ patient-level data leakage across splits, single-class splits, broken
20
+ stratification, tiny test sets, duplicate files, un-deidentified IDs.
21
+
22
+ 医学影像 AI 数据集完整性审计工具。对 manifest CSV 跑一遍,在训练**之前**
23
+ 揪出致命缺陷:病人级数据泄露、单类别数据划分、分布失衡、测试集过小、
24
+ 文件重复、未脱敏的 ID。
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install dicleak
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ dicleak splits.csv
36
+ dicleak splits.csv --report audit.md
37
+ dicleak splits.csv --report audit.md --cta "https://your-service.example/audit"
38
+ ```
39
+
40
+ The manifest CSV needs three columns (headers are flexible):
41
+
42
+ | concept | accepted headers |
43
+ |---|---|
44
+ | patient id | `patient_id`, `patientid`, `patient`, `subject_id`, `subject`, `pid` |
45
+ | split | `split`, `subset`, `fold`, `partition`, `set` (`train`/`val`/`test` and common aliases) |
46
+ | label | `label`, `class`, `target`, `diagnosis`, `annotation` |
47
+ | file path (optional) | `filepath`, `file_path`, `path`, `filename` |
48
+
49
+ Exit code is `0` when the score is 60+, `1` otherwise — handy for CI gates.
50
+
51
+ ## The 7 checks
52
+
53
+ | # | check | severity |
54
+ |---|---|---|
55
+ | 1 | patient-level leakage across splits | CRITICAL |
56
+ | 2 | split containing a single class | CRITICAL |
57
+ | 3 | label distribution drift between splits | HIGH |
58
+ | 4 | test split < 10% (or any split < 5%) | MEDIUM |
59
+ | 5 | duplicate files in the manifest | MEDIUM |
60
+ | 6 | patient IDs that look like real names | HIGH |
61
+ | 7 | too few unique patients | LOW/MEDIUM |
62
+
63
+ ## Why this exists
64
+
65
+ A disturbing share of medical-AI papers report inflated accuracy because the
66
+ same patient ended up in both the training and test sets. Reviewers now check
67
+ for this. `dicleak` makes the check a one-liner.
68
+
69
+ ## Sample
70
+
71
+ `samples/flawed_splits.csv` is a deliberately broken manifest (leaked patients,
72
+ single-class val split, tiny test set, name-like IDs...). It scores 26/100:
73
+
74
+ ```bash
75
+ dicleak samples/flawed_splits.csv
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ dicleak/__init__.py
5
+ dicleak/checks.py
6
+ dicleak/cli.py
7
+ dicleak/readers.py
8
+ dicleak/report.py
9
+ dicleak.egg-info/PKG-INFO
10
+ dicleak.egg-info/SOURCES.txt
11
+ dicleak.egg-info/dependency_links.txt
12
+ dicleak.egg-info/entry_points.txt
13
+ dicleak.egg-info/requires.txt
14
+ dicleak.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dicleak = dicleak.cli:main
@@ -0,0 +1,3 @@
1
+
2
+ [dev]
3
+ pytest>=8.0
@@ -0,0 +1 @@
1
+ dicleak
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "dicleak"
3
+ version = "0.1.0"
4
+ description = "Dataset integrity auditor for medical-imaging AI: catch patient-level data leakage and split flaws before they sink your model or paper"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "dicleak contributors" }]
9
+ keywords = ["medical-imaging", "dicom", "dataset", "data-leakage", "machine-learning", "validation"]
10
+ dependencies = []
11
+
12
+ [project.optional-dependencies]
13
+ dev = ["pytest>=8.0"]
14
+
15
+ [project.scripts]
16
+ dicleak = "dicleak.cli:main"
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=61"]
20
+ build-backend = "setuptools.build_meta"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["."]
24
+ include = ["dicleak*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+