PET / scripts /match_adni_metadata.py
DesonDai's picture
Add files using upload-large-folder tool
212e9d7 verified
Raw
History Blame Contribute Delete
7.98 kB
from __future__ import annotations
import argparse
import re
from pathlib import Path
import pandas as pd
FULL_COLUMNS = [
"RID",
"PTID",
"label",
"VISCODE",
"EXAMDATE",
"DX_bl",
"DX",
"AGE",
"PTGENDER",
"PTEDUCAT",
"PTETHCAT",
"PTRACCAT",
"PTMARRY",
"APOE4",
"FDG",
"PIB",
"AV45",
"ABETA",
"TAU",
"PTAU",
"CDRSB",
"ADAS11",
"ADAS13",
"ADASQ4",
"MMSE",
"RAVLT_immediate",
"RAVLT_learning",
"RAVLT_forgetting",
"RAVLT_perc_forgetting",
"LDELTOTAL",
"DIGITSCOR",
"TRABSCOR",
"FAQ",
"MOCA",
"Years_bl",
]
CONVERSION_COLUMNS = [
"PTID",
"label",
"DX.1",
"mPACCdigit",
"mPACCtrailsB",
"Ventricles(心室)",
"Hippocampus(海马)",
"WholeBrain(全脑)",
"Entorhinal(内嗅觉)",
"Fusiform(梭形)",
"MidTemp(中点温度)",
"ICV",
]
def normalize_subject_id(value: object) -> str:
if pd.isna(value):
return ""
text = str(value).strip().upper()
text = text.replace("_S_", "S").replace("_", "")
return re.sub(r"[^A-Z0-9]", "", text)
def clean_numeric(value: object) -> float | pd.NA:
if pd.isna(value):
return pd.NA
text = str(value).strip()
if not text:
return pd.NA
if text.startswith(">"):
text = text[1:]
try:
return float(text)
except ValueError:
return pd.NA
def read_xlsx(path: Path, columns: list[str]) -> pd.DataFrame:
df = pd.read_excel(path)
available = [col for col in columns if col in df.columns]
if "PTID" not in available:
raise ValueError(f"{path} does not contain PTID.")
df = df[available].copy()
df["subject_id_norm"] = df["PTID"].map(normalize_subject_id)
df = df[df["subject_id_norm"] != ""].copy()
df = df.drop_duplicates("subject_id_norm", keep="first")
return df
def add_numeric_clean_columns(df: pd.DataFrame) -> pd.DataFrame:
for col in ["FDG", "PIB", "AV45", "ABETA", "TAU", "PTAU"]:
if col in df.columns:
df[f"{col}_num"] = df[col].map(clean_numeric)
return df
def write_split_manifests(enriched: pd.DataFrame, out_dir: Path) -> None:
out_dir.mkdir(parents=True, exist_ok=True)
for split_name, split_df in enriched.groupby("split", sort=False):
split_df.to_csv(out_dir / f"{split_name}_clinical.csv", index=False)
def summarize(enriched: pd.DataFrame, out_md: Path, out_csv: Path) -> None:
rows = []
total = len(enriched)
for col in [
"clinical_label",
"dx",
"dx_bl",
"conversion_label",
"age",
"sex",
"apoe4",
"mmse",
"cdrsb",
"adas11",
"adas13",
"faq",
"moca",
"fdg_adni",
"av45",
"abeta_num",
"tau_num",
"ptau_num",
"hippocampus",
"wholebrain",
]:
if col not in enriched.columns:
continue
non_missing = int(enriched[col].notna().sum())
rows.append({"field": col, "non_missing": non_missing, "coverage": non_missing / total})
report = pd.DataFrame(rows)
report.to_csv(out_csv, index=False)
label_counts = {}
for col in ["clinical_label", "dx", "dx_bl", "conversion_label"]:
if col in enriched.columns:
label_counts[col] = enriched[col].dropna().astype(str).value_counts().to_dict()
lines = [
"# ADNI Metadata Match Report",
"",
f"- PET/SUVR samples: {total}",
f"- Matched clinical rows: {int(enriched['clinical_label'].notna().sum())}",
"",
"## Field Coverage",
"",
"| field | non_missing | coverage |",
"|---|---:|---:|",
]
for row in rows:
lines.append(f"| {row['field']} | {row['non_missing']} | {row['coverage']:.3f} |")
lines.extend([
"",
"## Label Counts",
"",
])
for col, counts in label_counts.items():
lines.append(f"### {col}")
lines.append("")
for key, value in counts.items():
lines.append(f"- {key}: {value}")
lines.append("")
out_md.write_text("\n".join(lines), encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser(description="Match ADNI Excel metadata to the PET/SUVR manifest.")
parser.add_argument("--manifest", type=Path, default=Path("data/metadata/splits/pet_fdg_manifest_with_split.csv"))
parser.add_argument("--full-xlsx", type=Path, default=Path("data/ADNIbase1416_info.xlsx"))
parser.add_argument("--conversion-xlsx", type=Path, default=Path("data/adni_1203s_info_fix.xlsx"))
parser.add_argument("--out", type=Path, default=Path("data/metadata/adni_matched_clinical.csv"))
parser.add_argument("--split-out-dir", type=Path, default=Path("data/metadata/splits"))
parser.add_argument("--report-md", type=Path, default=Path("data/metadata/adni_match_report.md"))
parser.add_argument("--report-csv", type=Path, default=Path("data/metadata/adni_match_report.csv"))
args = parser.parse_args()
manifest = pd.read_csv(args.manifest)
manifest["subject_id_norm"] = manifest["subject_id"].map(normalize_subject_id)
full = read_xlsx(args.full_xlsx, FULL_COLUMNS)
full = add_numeric_clean_columns(full)
full = full.rename(
columns={
"label": "clinical_label",
"DX": "dx",
"DX_bl": "dx_bl",
"AGE": "age",
"PTGENDER": "sex",
"PTEDUCAT": "education",
"PTETHCAT": "ethnicity",
"PTRACCAT": "race",
"PTMARRY": "marital_status",
"APOE4": "apoe4",
"FDG": "fdg_adni",
"PIB": "pib",
"AV45": "av45",
"ABETA": "abeta",
"TAU": "tau",
"PTAU": "ptau",
"ABETA_num": "abeta_num",
"TAU_num": "tau_num",
"PTAU_num": "ptau_num",
"CDRSB": "cdrsb",
"ADAS11": "adas11",
"ADAS13": "adas13",
"ADASQ4": "adasq4",
"MMSE": "mmse",
"RAVLT_immediate": "ravlt_immediate",
"RAVLT_learning": "ravlt_learning",
"RAVLT_forgetting": "ravlt_forgetting",
"RAVLT_perc_forgetting": "ravlt_perc_forgetting",
"LDELTOTAL": "ldeltotal",
"DIGITSCOR": "digitscor",
"TRABSCOR": "trabscor",
"FAQ": "faq",
"MOCA": "moca",
"Years_bl": "years_bl",
}
)
conversion = read_xlsx(args.conversion_xlsx, CONVERSION_COLUMNS)
conversion = conversion.rename(
columns={
"label": "conversion_label",
"DX.1": "dx_followup",
"mPACCdigit": "mpacc_digit",
"mPACCtrailsB": "mpacc_trailsb",
"Ventricles(心室)": "ventricles",
"Hippocampus(海马)": "hippocampus",
"WholeBrain(全脑)": "wholebrain",
"Entorhinal(内嗅觉)": "entorhinal",
"Fusiform(梭形)": "fusiform",
"MidTemp(中点温度)": "midtemp",
}
)
conversion = conversion.drop(columns=["PTID"], errors="ignore")
enriched = manifest.merge(full.drop(columns=["PTID"], errors="ignore"), on="subject_id_norm", how="left")
enriched = enriched.merge(conversion, on="subject_id_norm", how="left")
enriched = enriched.drop(columns=["subject_id_norm"])
args.out.parent.mkdir(parents=True, exist_ok=True)
enriched.to_csv(args.out, index=False)
write_split_manifests(enriched, args.split_out_dir)
summarize(enriched, args.report_md, args.report_csv)
print(f"wrote={args.out}")
print(f"wrote_splits={args.split_out_dir}/*_clinical.csv")
print(f"wrote_report={args.report_md}")
print(f"samples={len(enriched)} matched={int(enriched['clinical_label'].notna().sum())}")
if __name__ == "__main__":
main()