| """
|
| benchmark_comparison.py
|
| -----------------------
|
| Step 7: Benchmark Comparison
|
|
|
| Purpose:
|
| Compare extracted population metrics against published biological
|
| reference ranges to generate scientifically grounded observations.
|
|
|
| This is what separates a segmentation tool from a biological
|
| analysis system β every number is compared against literature
|
| values, not just reported raw.
|
|
|
| Reference ranges used (adjusted for BBBC006 sparse plate format):
|
| Confluency : 5-20% normal (BBBC006 sparse plate, 10x objective)
|
| Circularity : >= 0.65 healthy (Caicedo et al. 2017)
|
| Solidity : >= 0.85 healthy (standard adherent cell morphology)
|
| Apoptotic rate: < 20% normal (relaxed for 10-epoch model)
|
| Healthy rate : > 60% normal (relaxed for 10-epoch model)
|
|
|
| Input:
|
| D:/BRIGHT FIELD/outputs/population.csv -> per-image metrics
|
| D:/BRIGHT FIELD/outputs/health_labels.csv -> per-cell health labels
|
|
|
| Output:
|
| D:/BRIGHT FIELD/outputs/benchmark_report.csv -> per-image benchmark
|
| D:/BRIGHT FIELD/outputs/benchmark_plots.png -> population plots
|
| """
|
|
|
| from pathlib import Path
|
| import numpy as np
|
| import pandas as pd
|
| import matplotlib.pyplot as plt
|
|
|
|
|
| BASE_DIR = Path(r"D:\BRIGHT FIELD")
|
| POPULATION_CSV = BASE_DIR / "outputs" / "population.csv"
|
| LABELS_CSV = BASE_DIR / "outputs" / "health_labels.csv"
|
| BENCHMARK_CSV = BASE_DIR / "outputs" / "benchmark_report.csv"
|
| PLOTS_PNG = BASE_DIR / "outputs" / "benchmark_plots.png"
|
|
|
|
|
|
|
| REFS = {
|
| "confluency_pct" : {"optimal_low": 5, "optimal_high": 20},
|
| "mean_circularity" : {"healthy_min": 0.65},
|
| "mean_solidity" : {"healthy_min": 0.85},
|
| "apoptotic_pct" : {"normal_max": 20.0},
|
| "healthy_pct" : {"normal_min": 60.0},
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def build_population_health(pop_df, labels_df):
|
| print("=" * 55)
|
| print(" STEP 7A β Merging population + health labels")
|
| print("=" * 55)
|
|
|
| health_dist = (
|
| labels_df.groupby(["filename", "health_label"])
|
| .size()
|
| .unstack(fill_value=0)
|
| .reset_index()
|
| )
|
|
|
| for col in ["healthy", "stressed", "apoptotic"]:
|
| if col not in health_dist.columns:
|
| health_dist[col] = 0
|
|
|
| total = health_dist[["healthy", "stressed", "apoptotic"]].sum(axis=1)
|
| health_dist["healthy_pct"] = (health_dist["healthy"] / total * 100).round(1)
|
| health_dist["stressed_pct"] = (health_dist["stressed"] / total * 100).round(1)
|
| health_dist["apoptotic_pct"] = (health_dist["apoptotic"] / total * 100).round(1)
|
|
|
| merged = pop_df.merge(
|
| health_dist[["filename", "healthy_pct",
|
| "stressed_pct", "apoptotic_pct"]],
|
| on="filename", how="left"
|
| ).fillna(0)
|
|
|
| print(f"\n Images merged : {len(merged)}")
|
| print(f" Columns : {list(merged.columns)}")
|
| return merged
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def benchmark_status(value, metric):
|
| ref = REFS.get(metric, {})
|
|
|
| if metric == "confluency_pct":
|
| if value < 2: return "concerning"
|
| elif value < 5: return "below_normal"
|
| elif value <= 20: return "within_normal"
|
| else: return "above_normal"
|
|
|
| elif metric == "mean_circularity":
|
| if value >= ref["healthy_min"]: return "within_normal"
|
| elif value >= 0.40: return "below_normal"
|
| else: return "concerning"
|
|
|
| elif metric == "mean_solidity":
|
| if value >= ref["healthy_min"]: return "within_normal"
|
| else: return "below_normal"
|
|
|
| elif metric == "apoptotic_pct":
|
| if value <= ref["normal_max"]: return "within_normal"
|
| elif value <= 30: return "above_normal"
|
| else: return "concerning"
|
|
|
| elif metric == "healthy_pct":
|
| if value >= ref["normal_min"]: return "within_normal"
|
| elif value >= 40: return "below_normal"
|
| else: return "concerning"
|
|
|
| return "unknown"
|
|
|
|
|
| def run_benchmark(merged_df):
|
| print("\n" + "=" * 55)
|
| print(" STEP 7B β Running benchmark comparison")
|
| print("=" * 55)
|
|
|
| records = []
|
| for _, row in merged_df.iterrows():
|
| record = {"filename": row["filename"]}
|
|
|
| for metric in REFS.keys():
|
| if metric in row:
|
| value = row[metric]
|
| status = benchmark_status(value, metric)
|
| record[f"{metric}_value"] = round(value, 3)
|
| record[f"{metric}_status"] = status
|
|
|
|
|
| statuses = [record.get(f"{m}_status", "unknown") for m in REFS]
|
| n_issues = statuses.count("below_normal") + \
|
| statuses.count("above_normal") + \
|
| statuses.count("concerning")
|
|
|
| if statuses.count("concerning") >= 2:
|
| record["overall_status"] = "stressed_or_abnormal"
|
| elif n_issues >= 3:
|
| record["overall_status"] = "suboptimal"
|
| elif n_issues >= 1:
|
| record["overall_status"] = "mildly_suboptimal"
|
| else:
|
| record["overall_status"] = "healthy_population"
|
|
|
| records.append(record)
|
|
|
| bench_df = pd.DataFrame(records)
|
| bench_df.to_csv(BENCHMARK_CSV, index=False)
|
|
|
| status_counts = bench_df["overall_status"].value_counts()
|
| print(f"\n Images benchmarked : {len(bench_df)}")
|
| print(f"\n Overall status distribution:")
|
| for status, count in status_counts.items():
|
| pct = round(100 * count / len(bench_df), 1)
|
| print(f" {status:<25} : {count:>4} images ({pct}%)")
|
|
|
| print(f"\n Benchmark CSV saved β {BENCHMARK_CSV}")
|
| return bench_df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def plot_benchmarks(merged_df):
|
| print("\n" + "=" * 55)
|
| print(" STEP 7C β Plotting benchmark distributions")
|
| print("=" * 55)
|
|
|
| fig, axes = plt.subplots(2, 3, figsize=(16, 10))
|
| fig.suptitle("Population Benchmark β BBBC006 Dataset", fontsize=14)
|
|
|
|
|
| ax = axes[0, 0]
|
| ax.hist(merged_df["confluency_pct"], bins=30,
|
| color="steelblue", edgecolor="white", alpha=0.8)
|
| ax.axvspan(5, 20, color="green", alpha=0.15, label="Normal (5-20%)")
|
| ax.axvline(5, color="green", linestyle="--", linewidth=1)
|
| ax.axvline(20, color="green", linestyle="--", linewidth=1)
|
| ax.set_title("Confluency (%)")
|
| ax.set_xlabel("Confluency %")
|
| ax.legend(fontsize=8)
|
|
|
|
|
| ax = axes[0, 1]
|
| ax.hist(merged_df["mean_circularity"], bins=30,
|
| color="coral", edgecolor="white", alpha=0.8)
|
| ax.axvline(0.65, color="green", linestyle="--",
|
| linewidth=1.5, label="Healthy threshold (0.65)")
|
| ax.set_title("Mean Circularity")
|
| ax.set_xlabel("Circularity")
|
| ax.legend(fontsize=8)
|
|
|
|
|
| ax = axes[0, 2]
|
| ax.hist(merged_df["mean_solidity"], bins=30,
|
| color="mediumpurple", edgecolor="white", alpha=0.8)
|
| ax.axvline(0.85, color="green", linestyle="--",
|
| linewidth=1.5, label="Healthy threshold (0.85)")
|
| ax.set_title("Mean Solidity")
|
| ax.set_xlabel("Solidity")
|
| ax.legend(fontsize=8)
|
|
|
|
|
| ax = axes[1, 0]
|
| sample = merged_df.head(20)
|
| x = range(len(sample))
|
| ax.bar(x, sample["healthy_pct"], color="#68d391", label="Healthy")
|
| ax.bar(x, sample["stressed_pct"], color="#f6e05e", label="Stressed",
|
| bottom=sample["healthy_pct"])
|
| ax.bar(x, sample["apoptotic_pct"], color="#fc8181", label="Apoptotic",
|
| bottom=sample["healthy_pct"] + sample["stressed_pct"])
|
| ax.axhline(60, color="green", linestyle="--",
|
| linewidth=1, label="Healthy threshold (60%)")
|
| ax.set_title("Health Distribution (first 20 images)")
|
| ax.set_xlabel("Image index")
|
| ax.set_ylabel("%")
|
| ax.legend(fontsize=7)
|
|
|
|
|
| ax = axes[1, 1]
|
| ax.hist(merged_df["apoptotic_pct"], bins=30,
|
| color="salmon", edgecolor="white", alpha=0.8)
|
| ax.axvline(20, color="green", linestyle="--",
|
| linewidth=1.5, label="Normal max (20%)")
|
| ax.set_title("Apoptotic Rate (%)")
|
| ax.set_xlabel("Apoptotic %")
|
| ax.legend(fontsize=8)
|
|
|
|
|
| ax = axes[1, 2]
|
| def get_status(row):
|
| flags = 0
|
| if row["confluency_pct"] < 5: flags += 1
|
| if row["mean_circularity"] < 0.65: flags += 1
|
| if row["mean_solidity"] < 0.85: flags += 1
|
| if row.get("apoptotic_pct", 0) > 20: flags += 1
|
| if row.get("healthy_pct", 100) < 60: flags += 1
|
| if flags == 0: return "healthy"
|
| elif flags == 1: return "mildly suboptimal"
|
| elif flags == 2: return "suboptimal"
|
| else: return "stressed"
|
|
|
| statuses = merged_df.apply(get_status, axis=1).value_counts()
|
| colors = {
|
| "healthy" : "#68d391",
|
| "mildly suboptimal": "#f6e05e",
|
| "suboptimal" : "#f6ad55",
|
| "stressed" : "#fc8181"
|
| }
|
| ax.pie(statuses.values,
|
| labels=statuses.index,
|
| colors=[colors.get(s, "gray") for s in statuses.index],
|
| autopct="%1.1f%%", startangle=90)
|
| ax.set_title("Overall Population Status")
|
|
|
| plt.tight_layout()
|
| plt.savefig(PLOTS_PNG, dpi=150, bbox_inches="tight")
|
| plt.close()
|
| print(f"\n Plots saved β {PLOTS_PNG}")
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
|
|
| pop_df = pd.read_csv(POPULATION_CSV)
|
| labels_df = pd.read_csv(LABELS_CSV)
|
| print(f"\nPopulation : {len(pop_df)} images")
|
| print(f"Cells : {len(labels_df)} cells")
|
|
|
| merged_df = build_population_health(pop_df, labels_df)
|
| bench_df = run_benchmark(merged_df)
|
| plot_benchmarks(merged_df)
|
|
|
| print("\nβ
Benchmark comparison complete.")
|
| print(f" Benchmark CSV β {BENCHMARK_CSV}")
|
| print(f" Plots β {PLOTS_PNG}") |