Files
Glaugene_2/Pilot/code/mirna_threshold_analysis.py

257 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
miRNA threshold analysis
------------------------
For each tissue (A = Schirmer strips, B = Lens tissues):
- Derives concentration thresholds from the pilot group's actual measured values
- At each threshold shows: # samples passing from full cohort (healthy / diseased)
and the mean ± SD total miRNA counts from pilot samples that pass that cut-off
Outputs: summary Excel table + multi-panel figure
"""
import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.lines import Line2D
# ── file paths ────────────────────────────────────────────────────────────────
QC_FILE = "BEA25P077_RP.xlsx"
PATIENTS_FILE = "patients.xlsx"
COUNTS_FILE = "miRNA_counts.xlsx"
OUT_EXCEL = "mirna_threshold_summary.xlsx"
OUT_FIG = "mirna_threshold_analysis.png"
# ── colours ───────────────────────────────────────────────────────────────────
COL_HEALTHY = "#4393c3" # blue
COL_DISEASED = "#d6604d" # red
COL_PILOT_A = "#2ca02c" # green
COL_PILOT_B = "#9467bd" # purple
# ── helpers ───────────────────────────────────────────────────────────────────
def parse_conc(val):
if isinstance(val, str):
return np.nan
try:
return float(val)
except (TypeError, ValueError):
return np.nan
def extract_num(series, letter):
"""Pull the patient number from IDs like '4A_G2'."""
extracted = series.str.extract(rf"^(\d+){letter}")[0]
return pd.to_numeric(extracted, errors="coerce")
# ── load data ─────────────────────────────────────────────────────────────────
patients = pd.read_excel(PATIENTS_FILE, usecols=["Number", "Exf"])
patients = patients.dropna(subset=["Number"])
patients["Number"] = patients["Number"].astype(int)
patients["Exf"] = patients["Exf"].astype(int)
counts = pd.read_excel(COUNTS_FILE, sheet_name="Mature", index_col=0)
totals = counts.sum(axis=0).rename("total_miRNA") # total per sample
qa = pd.read_excel(QC_FILE, sheet_name="A-samples")
qb = pd.read_excel(QC_FILE, sheet_name="B-samples")
qa["conc_val"] = qa["conc (ng/ul)"].apply(parse_conc)
qb["conc_val"] = qb["Conc [ng/ul]"].apply(parse_conc)
qa["Number"] = extract_num(qa["ID"], "A").dropna().astype(int)
qb["Number"] = extract_num(qb["ID"], "B").dropna().astype(int)
qa = qa.dropna(subset=["Number"]).merge(patients, on="Number", how="left")
qb = qb.dropna(subset=["Number"]).merge(patients, on="Number", how="left")
# ── build pilot lookup: number → (conc, total_miRNA) ─────────────────────────
def build_pilot(qc_df, tissue_letter):
"""Join QC concentrations with miRNA totals for pilot samples."""
pilot_rows = []
for col in totals.index:
m = re.match(rf"s(\d+){tissue_letter}_G2", col)
if not m:
continue
num = int(m.group(1))
conc_row = qc_df[qc_df["Number"] == num]
if conc_row.empty:
continue
pilot_rows.append({
"Number": num,
"sample_id": col,
"conc_val": conc_row["conc_val"].values[0],
"total_miRNA": totals[col],
"Exf": conc_row["Exf"].values[0],
})
return pd.DataFrame(pilot_rows).sort_values("conc_val")
pilot_A = build_pilot(qa, "A")
pilot_B = build_pilot(qb, "B")
# ── derive thresholds from pilot concentrations ───────────────────────────────
def pilot_thresholds(pilot_df):
"""
Use the sorted unique measured concentrations as tier cut-offs,
prepend 0 (all samples) and append a step just above the max.
"""
measured = sorted(pilot_df["conc_val"].dropna().unique())
# round to 2 dp to keep labels clean
thresholds = [0.0] + [round(v, 2) for v in measured]
return thresholds
thresholds_A = pilot_thresholds(pilot_A)
thresholds_B = pilot_thresholds(pilot_B)
# ── per-threshold summary ─────────────────────────────────────────────────────
def threshold_summary(qc_df, pilot_df, thresholds):
rows = []
for t in thresholds:
# full cohort
if t == 0:
cohort = qc_df.copy()
else:
cohort = qc_df[qc_df["conc_val"] >= t]
n_total = len(cohort)
n_healthy = (cohort["Exf"] == 0).sum()
n_diseased = (cohort["Exf"] == 1).sum()
# pilot subset
if t == 0:
pilot_pass = pilot_df.copy()
else:
pilot_pass = pilot_df[pilot_df["conc_val"] >= t]
n_pilot = len(pilot_pass)
mean_mirna = pilot_pass["total_miRNA"].mean() if n_pilot else np.nan
sd_mirna = pilot_pass["total_miRNA"].std() if n_pilot > 1 else np.nan
rows.append({
"threshold (ng/µl)": t,
"cohort_total": n_total,
"cohort_healthy": n_healthy,
"cohort_diseased": n_diseased,
"pilot_n": n_pilot,
"pilot_mean_miRNA": mean_mirna,
"pilot_sd_miRNA": sd_mirna,
})
return pd.DataFrame(rows)
summary_A = threshold_summary(qa, pilot_A, thresholds_A)
summary_B = threshold_summary(qb, pilot_B, thresholds_B)
# ── save Excel ────────────────────────────────────────────────────────────────
with pd.ExcelWriter(OUT_EXCEL, engine="openpyxl") as writer:
for df, sheet in [(summary_A, "A - Schirmer strips"),
(summary_B, "B - Lens tissues")]:
df.to_excel(writer, sheet_name=sheet, index=False)
ws = writer.sheets[sheet]
# widen columns
for col_cells in ws.columns:
max_len = max(len(str(c.value)) if c.value else 0 for c in col_cells)
ws.column_dimensions[col_cells[0].column_letter].width = max_len + 4
print(f"Excel saved → {OUT_EXCEL}")
# ── figure ────────────────────────────────────────────────────────────────────
fig = plt.figure(figsize=(16, 10))
fig.suptitle("miRNA QC Threshold Analysis", fontsize=14, fontweight="bold", y=0.98)
# 2 tissues × 3 columns: scatter | cohort counts | expected miRNA
gs = gridspec.GridSpec(2, 3, figure=fig, hspace=0.45, wspace=0.38,
left=0.07, right=0.97, top=0.92, bottom=0.08)
def plot_tissue(row, tissue_label, pilot_df, summary_df, pilot_col):
thresholds = summary_df["threshold (ng/µl)"].tolist()
x_labels = [str(t) for t in thresholds]
x = np.arange(len(thresholds))
bar_w = 0.38
# ── panel 1: scatter concentration vs total miRNA ─────────────────────────
ax1 = fig.add_subplot(gs[row, 0])
healthy_mask = pilot_df["Exf"] == 0
diseased_mask = pilot_df["Exf"] == 1
ax1.scatter(pilot_df.loc[healthy_mask, "conc_val"],
pilot_df.loc[healthy_mask, "total_miRNA"] / 1e6,
color=COL_HEALTHY, edgecolors="k", linewidths=0.5,
s=70, zorder=3, label="Healthy")
ax1.scatter(pilot_df.loc[diseased_mask, "conc_val"],
pilot_df.loc[diseased_mask, "total_miRNA"] / 1e6,
color=COL_DISEASED, edgecolors="k", linewidths=0.5,
s=70, zorder=3, label="Diseased")
# annotate patient numbers
for _, r in pilot_df.iterrows():
if pd.notna(r["conc_val"]):
ax1.annotate(str(int(r["Number"])),
(r["conc_val"], r["total_miRNA"] / 1e6),
textcoords="offset points", xytext=(4, 3),
fontsize=7, color="dimgray")
# vertical lines at each threshold (skip 0)
for t in thresholds[1:]:
ax1.axvline(t, color="gray", lw=0.7, ls="--", alpha=0.5)
ax1.set_xlabel("Concentration (ng/µl)", fontsize=9)
ax1.set_ylabel("Total miRNA counts (×10⁶)", fontsize=9)
ax1.set_title(f"{tissue_label}\nPilot: conc vs miRNA yield", fontsize=9)
ax1.legend(fontsize=8, framealpha=0.7)
ax1.tick_params(labelsize=8)
# ── panel 2: cohort counts per threshold ──────────────────────────────────
ax2 = fig.add_subplot(gs[row, 1])
ax2.bar(x - bar_w / 2, summary_df["cohort_healthy"],
width=bar_w, color=COL_HEALTHY, label="Healthy", alpha=0.85)
ax2.bar(x + bar_w / 2, summary_df["cohort_diseased"],
width=bar_w, color=COL_DISEASED, label="Diseased", alpha=0.85)
ax2.set_xticks(x)
ax2.set_xticklabels(x_labels, rotation=45, ha="right", fontsize=8)
ax2.set_xlabel("Min. concentration threshold (ng/µl)", fontsize=9)
ax2.set_ylabel("N samples passing", fontsize=9)
ax2.set_title(f"{tissue_label}\nCohort samples at each threshold", fontsize=9)
ax2.legend(fontsize=8, framealpha=0.7)
ax2.tick_params(labelsize=8)
# add count labels
for bar in ax2.patches:
h = bar.get_height()
if h > 0:
ax2.text(bar.get_x() + bar.get_width() / 2, h + 0.3,
str(int(h)), ha="center", va="bottom", fontsize=7)
# ── panel 3: expected miRNA from pilot ────────────────────────────────────
ax3 = fig.add_subplot(gs[row, 2])
means = summary_df["pilot_mean_miRNA"] / 1e6
sds = summary_df["pilot_sd_miRNA"].fillna(0) / 1e6
ns = summary_df["pilot_n"]
ax3.bar(x, means, width=0.55, color=pilot_col, alpha=0.8, label="Mean ± SD")
ax3.errorbar(x, means, yerr=sds, fmt="none", color="k",
capsize=4, linewidth=1.2, zorder=4)
# label n= above each bar
for i, (m, n) in enumerate(zip(means, ns)):
ax3.text(i, (m + sds.iloc[i]) + 0.05, f"n={int(n)}",
ha="center", va="bottom", fontsize=7, color="dimgray")
ax3.set_xticks(x)
ax3.set_xticklabels(x_labels, rotation=45, ha="right", fontsize=8)
ax3.set_xlabel("Min. concentration threshold (ng/µl)", fontsize=9)
ax3.set_ylabel("Total miRNA counts (×10⁶)", fontsize=9)
ax3.set_title(f"{tissue_label}\nExpected miRNA yield (pilot mean ± SD)", fontsize=9)
ax3.tick_params(labelsize=8)
plot_tissue(0, "A — Schirmer strips", pilot_A, summary_A, COL_PILOT_A)
plot_tissue(1, "B — Lens tissues", pilot_B, summary_B, COL_PILOT_B)
fig.savefig(OUT_FIG, dpi=150, bbox_inches="tight")
print(f"Figure saved → {OUT_FIG}")
plt.show()