"""C-LipoVAE interactive demo — Hugging Face Space.
A leakage-safe conditional variational autoencoder for binary phenotype
stratification of high-dimensional omics data. Editorial-quality UI
with per-metric interpretive commentary.
"""
from __future__ import annotations
# ---------------------------------------------------------------
# Patch gradio_client to tolerate bool JSON-schema nodes.
# A long-standing bug (present in gradio_client shipped with both
# Gradio 4.x and 5.x): a schema node of `additionalProperties: True`
# — emitted for pandas DataFrame components — reaches `get_type` /
# `_json_schema_to_python_type` as a bare bool and crashes the
# startup api-info generation with
# "TypeError: argument of type 'bool' is not iterable".
# We short-circuit bool schema nodes so startup succeeds.
# Must run before gradio builds the app; import order matters.
# ---------------------------------------------------------------
try:
import gradio_client.utils as _gcu
_orig_j2p = _gcu._json_schema_to_python_type
def _patched_j2p(schema, defs=None):
if isinstance(schema, bool):
return "Any" if schema else "None"
return _orig_j2p(schema, defs)
_gcu._json_schema_to_python_type = _patched_j2p
_orig_get_type = _gcu.get_type
def _patched_get_type(schema):
if isinstance(schema, bool):
return "Any" if schema else "None"
return _orig_get_type(schema)
_gcu.get_type = _patched_get_type
except Exception:
pass
from pathlib import Path
import gradio as gr
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from clipovae import (
active_units,
c2st_auc,
encode_null,
fit_cvae,
integrated_gradients,
mmd2_rbf,
reconstruct,
sym_kl_gauss,
)
APP_ROOT = Path(__file__).parent
EXAMPLE_MAT = APP_ROOT / "examples" / "wbc_matrix.csv"
EXAMPLE_LAB = APP_ROOT / "examples" / "wbc_labels.csv"
MAX_SAMPLES = 2000
MAX_FEATURES = 2000
MAX_CLASSES = 4
# ------------------------------------------------------------------
# Interpretation helpers
# ------------------------------------------------------------------
def auc_verdict(auc: float) -> tuple[str, str, str]:
"""Return (label, color, sentence) for a C2ST AUC value.
Bands follow the interpretation convention used in the C-LipoVAE
manuscript, with numeric anchors from real datasets:
supervised-comparable (AUC >= 0.90) ← WBC benchmark = 0.965
strongly discriminative (0.80-0.90) ← TCM PD/NPD = 0.877
moderately discriminative (0.70-0.80)
weakly discriminative (0.60-0.70) ← PIVUS sex = 0.644
chance-level (< 0.60)
"""
if auc >= 0.90:
return ("Supervised-comparable encoding", "#0f766e",
"The null-condition latent carries near-complete "
"class-discriminative information (5-fold CV C2ST AUC "
f"= {auc:.3f}), of the same order as a direct "
"supervised classifier trained on the raw features. "
"Consistent with a well-separated, low-noise class "
"boundary.")
if auc >= 0.80:
return ("Strongly discriminative latent", "#16a34a",
"The leakage-safe latent geometry retains a "
"substantial fraction of the class signal from the "
"input space (5-fold CV C2ST AUC = "
f"{auc:.3f}); consistent with recoverable, "
"reproducible class-conditional structure.")
if auc >= 0.70:
return ("Moderately discriminative latent", "#ca8a04",
"The null-condition latent captures partial "
"class-discriminative structure (5-fold CV C2ST AUC "
f"= {auc:.3f}). Consistent with a real but incomplete "
"phenotype signature; feature attributions should be "
"corroborated with an external cohort.")
if auc >= 0.60:
return ("Weakly discriminative latent", "#d97706",
"The latent carries limited class-discriminative "
f"signal (5-fold CV C2ST AUC = {auc:.3f}). Consistent "
"with a biologically weak effect, small sample size, "
"or noisy input features. Interpret integrated-"
"gradient attributions with caution.")
return ("Chance-level encoding", "#dc2626",
f"The null-condition latent (5-fold CV C2ST AUC = {auc:.3f}) "
"does not encode the class label beyond chance. Consider a "
"higher-capacity model, more training data, or verifying "
"that the input features actually carry the phenotype.")
def au_verdict(n_au: int, d_latent: int) -> tuple[str, str, str]:
ratio = n_au / max(d_latent, 1)
if ratio >= 0.75:
return ("No posterior collapse", "#0f766e",
f"All {n_au} of {d_latent} latent dimensions have "
"per-dimension KL > 0.01 nats; full latent capacity "
"is being used and the prior is not dominating the "
"posterior.")
if ratio >= 0.5:
return ("Partial posterior collapse", "#ca8a04",
f"Only {n_au} of {d_latent} dimensions are active "
"(per-dim KL > 0.01). The effective latent dimension "
f"is {n_au}; the remaining dimensions have collapsed "
"onto the prior and contribute no information.")
return ("Severe posterior collapse", "#dc2626",
f"Only {n_au} of {d_latent} dimensions carry non-trivial "
"KL. The β regulariser is over-constraining the encoder. "
"Try a lower β (e.g. 0.5) or a larger training set.")
def r2_verdict(r2: float) -> tuple[str, str, str]:
if r2 >= 0.30:
return ("High reconstruction fidelity", "#0f766e",
"The CVAE recovers a substantial fraction of input "
f"variance (R²_rel = {r2:.2f} vs. mean-baseline), so "
"the latent code retains most feature-level "
"information alongside its discriminative signal.")
if r2 >= 0.10:
return ("Moderate reconstruction fidelity", "#ca8a04",
f"R²_rel = {r2:.2f}: the latent preserves the "
"informative signal but sacrifices some raw-feature "
"detail. Typical for β = 1.0 CVAEs tuned for "
"separability over autoencoding fidelity.")
return ("Low reconstruction fidelity", "#d97706",
f"R²_rel = {r2:.2f} indicates the bottleneck is "
"aggressive relative to the input complexity. Consider "
"increasing the latent dimension d or lowering β.")
def mmd_note(mmd: float) -> str:
if mmd >= 0.20:
s = ("large distributional shift between the class-conditional "
"latent clouds")
elif mmd >= 0.05:
s = "clear distributional shift"
else:
s = ("small distributional shift; the two classes overlap "
"substantially in the latent")
return (f"Squared MMD with a Gaussian-RBF kernel at "
f"median-heuristic bandwidth: {s}. Reported alongside "
"the C2ST AUC as an independent, classifier-free "
"separation check.")
def kl_note(kl: float) -> str:
if kl >= 10.0:
s = "well-separated Gaussian fits"
elif kl >= 2.0:
s = "clearly distinguishable Gaussian fits"
else:
s = "overlapping Gaussian fits"
return (f"Symmetrised KL divergence between multivariate-Gaussian "
f"fits to the two class-conditional latent clouds: {s}. "
"Larger values mean the class-conditional posteriors are "
"further apart in Gaussian-approximation terms.")
# ------------------------------------------------------------------
# Biological interpretation: auto-detect the dataset domain from the
# feature names and provide dataset-appropriate framing.
# ------------------------------------------------------------------
def detect_domain(feature_names: list[str]) -> str:
"""Rough classifier based on feature-name lexicon."""
joined = " ".join(feature_names[:100]).lower()
lipid_prefixes = ("pc ", "pe ", "ps ", "pi ", "pg ", "lpc ", "lpe ",
"sm ", "cer ", "cerp ", "tag ", "tag-", "tg ",
"dag ", "dg ", "cholesterol", "ffa ", "coq")
if any(f.strip().lower().startswith(lipid_prefixes)
for f in feature_names[:40]):
return "lipidomics"
wbc_terms = ("radius", "texture", "perimeter", "area",
"concavity", "concave points", "smoothness",
"compactness", "fractal dimension")
if sum(t in joined for t in wbc_terms) >= 4:
return "wbc_morphometry"
if any(f.startswith("ENSG") or f.startswith("ENST")
for f in feature_names[:100]):
return "transcriptomics"
if any(f.upper().startswith(("P0", "P1", "Q9", "Q1"))
and len(f) == 6 for f in feature_names[:100]):
return "proteomics"
if any("HMDB" in f.upper() or "kegg" in f.lower()
or "cid" in f.lower() for f in feature_names[:100]):
return "metabolomics"
return "generic"
def biology_html(domain: str, top_feats: list[str], auc: float) -> str:
"""Domain-appropriate biological framing of the top drivers."""
top3 = ", ".join(f"{t}" for t in top_feats[:3])
if domain == "lipidomics":
body = (
"The top-ranked features are the strongest nonlinear "
"drivers of the class contrast in the leakage-safe latent. "
f"Species carrying names such as {top3} identify the "
"lipid classes whose abundance shift most strongly "
"co-varies with the phenotype. When the leading species "
"are glycerophospholipids (PC, PE, "
"PS, PI), the signature "
"typically points to plasma-membrane remodeling and "
"polyunsaturated-fatty-acid trafficking; when "
"sphingomyelins (SM) dominate, ceramide-"
"signalling and insulin-resistance pathways are the "
"conventional biological reading; when triacylglycerols "
"(TAG) dominate, adipose storage and hepatic "
"lipogenesis are implicated. Corroborate directly against "
"the class-conditional log-fold-change and against an "
"external cohort before making mechanistic claims."
)
header = "Lipidomic interpretation"
elif domain == "wbc_morphometry":
body = (
"This is the Wisconsin Breast Cancer benchmark — "
"quantitative cell-nucleus morphometry from fine-needle "
"aspirates (radius, texture, area, concavity, concave "
f"points, and so on). The top-ranked drivers ({top3}) "
"are the same nuclear-atypia measurements that "
"pathologists use to grade cytological samples: nuclei "
"that are larger, more irregular, and more concave "
"correspond to the malignant class. C-LipoVAE recovers "
"these classical drivers without being told the "
"class label at evaluation time — a direct "
"sanity-check that the leakage-safe protocol still "
"extracts biologically meaningful signal, and a "
"reproduction of what is arguably the best-studied "
"supervised benchmark in medical machine learning."
)
header = "Cytological interpretation (WBC benchmark)"
elif domain == "transcriptomics":
body = (
"The top-ranked features are Ensembl transcripts / genes "
f"({top3}). Their differential representation between "
"classes suggests programme-level transcriptional changes "
"— consider enrichment against MSigDB Hallmark or KEGG "
"gene-sets before drawing pathway conclusions."
)
header = "Transcriptomic interpretation"
elif domain == "proteomics":
body = (
"The top-ranked features are UniProt-style protein "
f"identifiers ({top3}). These are the strongest nonlinear "
"drivers of the class separation; annotate them via "
"UniProt / Reactome and cross-check with an orthogonal "
"proteomic panel."
)
header = "Proteomic interpretation"
elif domain == "metabolomics":
body = (
"The top-ranked features are metabolomic identifiers "
f"({top3}). Map them to HMDB / KEGG to build a "
"pathway-level interpretation; check the direction of "
"shift against the class-conditional means before making "
"biological claims."
)
header = "Metabolomic interpretation"
else:
body = (
"The top-ranked features are the strongest nonlinear "
f"drivers of the class contrast in the latent ({top3}). "
"Because the leakage-safe protocol excludes label "
"information at evaluation time, these are the features "
"that the encoder learned to compress into a "
"class-informative geometry — a data-driven candidate "
"signature rather than a supervised discriminator. "
"Interpret in the context of the actual measurement "
"platform, and validate the direction of each feature "
"against the class-conditional means before making "
"mechanistic claims."
)
header = "Signature interpretation"
strength = ("These interpretations are conditional on the AUC "
"band shown above: the higher the C2ST AUC, the more "
"confidence one can place in the top drivers as a "
"reproducible signature. For AUC < 0.70 the "
"attributions should be treated as exploratory only.")
return f"""
{body}
encode_null(model, X): the class label is replaced by a
zero vector at evaluation time so it cannot leak into the encoder.
This null-condition protocol removes the label side-channel
that inflates naive CVAE evaluations.
A leakage-safe conditional VAE for binary phenotype stratification of high-dimensional omics data.
Upload a feature matrix and binary labels. The model trains a conditional variational autoencoder, evaluates its latent space without feeding the label back in, and returns publication-grade separability metrics with interpretation.
matrix.csv + labels.csv
(or load the WBC example)