Download app.py from lewMiao/PD-Web: direct link, hf CLI and curl.
- Browser
- Download file 21.1 kB
-
https://huggingface.co/spaces/lewMiao/PD-Web/resolve/main/app.py
- Command line
-
hf download hf://spaces/lewMiao/PD-Web/app.py
-
curl -L -o app.py https://huggingface.co/spaces/lewMiao/PD-Web/resolve/main/app.py
21.1 kB
| from __future__ import annotations | |
| from html import escape | |
| from math import erf, sqrt | |
| from pathlib import Path | |
| from tempfile import NamedTemporaryFile | |
| import gradio as gr | |
| import numpy as np | |
| import pandas as pd | |
| ROOT = Path(__file__).resolve().parent | |
| ARTIFACTS = ROOT / "artifacts" | |
| CATALOG = pd.read_csv(ARTIFACTS / "gene_insight_catalog.csv").set_index("gene") | |
| ALL_PRIOR = pd.read_csv(ARTIFACTS / "all_common_gene_prior.csv").set_index("gene") | |
| EXPRESSION = pd.read_csv(ARTIFACTS / "candidate_expression_133.csv") | |
| GENES = CATALOG.index.tolist() | |
| DATASETS = ["GSE57475", "GSE6613", "GSE72267", "GSE99039"] | |
| CATEGORY_LABELS = { | |
| "direct_pd_gene": "direct PD evidence", | |
| "synaptic_dopaminergic_neuronal": "synaptic and dopaminergic signaling", | |
| "mitochondrial_oxidative_stress": "mitochondrial and oxidative-stress biology", | |
| "neuroinflammation_immune": "neuroinflammation and immune signaling", | |
| "apoptosis_stress_response": "apoptosis and cellular stress response", | |
| "lysosome_autophagy_proteostasis": "lysosome, autophagy, and proteostasis", | |
| } | |
| PRIOR_MAX = float(CATALOG["evidence_score_norm"].max()) | |
| def normal_cdf(value: float) -> float: | |
| return 0.5 * (1.0 + erf(value / sqrt(2.0))) | |
| def mann_whitney_approx(control: np.ndarray, pd_values: np.ndarray) -> float: | |
| combined = pd.Series(np.concatenate([control, pd_values])) | |
| ranks = combined.rank(method="average").to_numpy() | |
| n_control = len(control) | |
| n_pd = len(pd_values) | |
| u_value = ranks[n_control:].sum() - n_pd * (n_pd + 1) / 2 | |
| mean_u = n_control * n_pd / 2 | |
| counts = combined.value_counts().to_numpy() | |
| total = n_control + n_pd | |
| tie_term = np.sum(counts**3 - counts) | |
| variance = n_control * n_pd / 12 * ( | |
| total + 1 - tie_term / (total * max(total - 1, 1)) | |
| ) | |
| if variance <= 0: | |
| return 1.0 | |
| z_score = (u_value - mean_u) / sqrt(variance) | |
| return float(2 * (1 - normal_cdf(abs(z_score)))) | |
| def dataset_evidence(gene: str) -> pd.DataFrame: | |
| rows = [] | |
| for dataset in DATASETS: | |
| cohort = EXPRESSION.loc[EXPRESSION["batch"].eq(dataset), [gene, "label"]].dropna() | |
| control = cohort.loc[cohort["label"].eq(0), gene].astype(float).to_numpy() | |
| pd_values = cohort.loc[cohort["label"].eq(1), gene].astype(float).to_numpy() | |
| difference = float(pd_values.mean() - control.mean()) | |
| rows.append( | |
| { | |
| "dataset": dataset, | |
| "mean_difference": difference, | |
| "direction": "Higher in PD" if difference > 0 else "Lower in PD", | |
| "p_value": mann_whitney_approx(control, pd_values), | |
| } | |
| ) | |
| return pd.DataFrame(rows) | |
| def biological_themes(categories: str) -> list[str]: | |
| if pd.isna(categories) or str(categories).strip().lower() in {"", "none", "nan"}: | |
| return [] | |
| return [ | |
| CATEGORY_LABELS.get(item, item.replace("_", " ")) | |
| for item in str(categories).split(";") | |
| ] | |
| def fallback_interpretation(gene: str, categories: str) -> str: | |
| return ( | |
| f"{gene} is an evidence-supported PD candidate linked to " | |
| f"{', '.join(biological_themes(categories))}. Gene-specific mechanisms " | |
| "should be verified against primary literature." | |
| ) | |
| def evidence_score(row: pd.Series, data: pd.DataFrame) -> int: | |
| prior_component = 30 * float(row["evidence_score_norm"]) / PRIOR_MAX | |
| stability_component = 30 * float(row["selection_frequency"]) / 4 | |
| gate_value = row.get("mean_gate_probability") | |
| gate_component = 0.0 | |
| if not pd.isna(gate_value): | |
| gate_component = 20 * float(gate_value) | |
| directions = data["direction"].tolist() | |
| direction_consistency = max(directions.count("Higher in PD"), directions.count("Lower in PD")) | |
| data_component = 20 * direction_consistency / 4 | |
| return int(round(prior_component + stability_component + gate_component + data_component)) | |
| def support_label(score: int) -> str: | |
| if score >= 75: | |
| return "Strong candidate biomarker" | |
| if score >= 50: | |
| return "Moderate candidate biomarker" | |
| return "Exploratory candidate biomarker" | |
| def prior_support_label(level: int) -> str: | |
| return { | |
| 0: "Not included in the final 133-gene candidate set", | |
| 1: "Not included in the final 133-gene candidate set", | |
| 2: "Evidence-supported candidate gene", | |
| 3: "High-evidence candidate gene", | |
| }.get(level, "Not evaluated in the common-gene prior") | |
| def format_number(value, digits: int = 3) -> str: | |
| if pd.isna(value): | |
| return "Not available" | |
| return f"{float(value):.{digits}f}" | |
| def metric_rows(items: list[tuple[str, str]]) -> str: | |
| return "".join( | |
| f'<div class="metric-row"><span>{escape(label)}</span><b>{escape(value)}</b></div>' | |
| for label, value in items | |
| ) | |
| def create_download( | |
| gene: str, | |
| row: pd.Series, | |
| data: pd.DataFrame, | |
| score: int, | |
| label: str, | |
| interpretation: str, | |
| ) -> str: | |
| directions = "; ".join( | |
| f"{item.dataset}: {item.direction} (p={item.p_value:.3g})" | |
| for item in data.itertuples() | |
| ) | |
| report = pd.DataFrame( | |
| [ | |
| { | |
| "gene": gene, | |
| "candidate_status": label, | |
| "overall_evidence_score": score, | |
| "selected_by_stablegate": bool(row["selected_by_stablegate"]), | |
| "selection_frequency": f"{int(row['selection_frequency'])}/4", | |
| "stablegate_rank": row.get("stability_rank", ""), | |
| "mean_gate_probability": row.get("mean_gate_probability", ""), | |
| "shap_importance_rank": row.get("shap_importance_rank", ""), | |
| "evidence_level": int(row["evidence_level"]), | |
| "evidence_score_norm": row["evidence_score_norm"], | |
| "biological_themes": "; ".join(biological_themes(row["evidence_categories"])), | |
| "mechanism_summary": interpretation, | |
| "dataset_expression_evidence": directions, | |
| } | |
| ] | |
| ) | |
| handle = NamedTemporaryFile( | |
| delete=False, | |
| suffix=f"_{gene}_evidence_summary.csv", | |
| mode="w", | |
| encoding="utf-8", | |
| newline="", | |
| ) | |
| report.to_csv(handle.name, index=False) | |
| handle.close() | |
| return handle.name | |
| def query_gene(gene: str): | |
| gene = str(gene or "").strip().upper() | |
| if not gene: | |
| gene = "UNSPECIFIED" | |
| if gene not in CATALOG.index: | |
| return query_non_candidate_gene(gene) | |
| row = CATALOG.loc[gene].copy() | |
| data = dataset_evidence(gene) | |
| score = evidence_score(row, data) | |
| label = support_label(score) | |
| interpretation = row.get("interpretation") | |
| if pd.isna(interpretation) or not str(interpretation).strip(): | |
| interpretation = fallback_interpretation(gene, row["evidence_categories"]) | |
| directions = data["direction"].tolist() | |
| dominant_direction = max(set(directions), key=directions.count) | |
| consistent_count = directions.count(dominant_direction) | |
| significant_count = int(data["p_value"].lt(0.05).sum()) | |
| themes = ", ".join(biological_themes(row["evidence_categories"])) | |
| result_card = f""" | |
| <div class="result-content"> | |
| <div class="result-top"> | |
| <div> | |
| <div class="gene-line"><span class="gene-name">{escape(gene)}</span><span class="gene-subtitle">Evidence-supported PD candidate gene</span></div> | |
| <div class="candidate-label">{escape(label)}</div> | |
| </div> | |
| <div class="score-block"> | |
| <span>Overall score</span> | |
| <div><b>{score}</b><small>/ 100</small></div> | |
| </div> | |
| </div> | |
| <div class="score-track"><div style="width:{score}%"></div></div> | |
| </div> | |
| """ | |
| model_evidence = metric_rows( | |
| [ | |
| ("Gate score", format_number(row.get("mean_gate_probability"))), | |
| ("Selection frequency", f"{int(row['selection_frequency'])}/4 datasets"), | |
| ("StableGate importance rank", format_number(row.get("stability_rank"), 0)), | |
| ("SHAP importance rank", format_number(row.get("shap_importance_rank"), 0)), | |
| ] | |
| ) | |
| data_evidence = metric_rows( | |
| [ | |
| ("Dominant expression direction", dominant_direction), | |
| ("Direction consistency", f"{consistent_count}/4 datasets"), | |
| ("Datasets with nominal p<0.05", f"{significant_count}/4 datasets"), | |
| ("Four-cohort analysis status", "Available"), | |
| ] | |
| ) | |
| biological_evidence = ( | |
| metric_rows( | |
| [ | |
| ("Knowledge-prior evidence level", f"{int(row['evidence_level'])}/3"), | |
| ("Knowledge-prior rank", f"{int(row['knowledge_rank'])}/10,939 genes"), | |
| ("PD-related themes", themes), | |
| ] | |
| ) | |
| + f'<div class="explanation-text">{escape(str(interpretation))}</div>' | |
| ) | |
| conclusion = ( | |
| f"{gene} is classified as a {label.lower()} based on StableGate selection " | |
| f"stability, SHAP/gate evidence, expression consistency across four GEO datasets, " | |
| f"and evidence-level {int(row['evidence_level'])} biological prior." | |
| ) | |
| conclusion_card = f""" | |
| <div class="conclusion-content"> | |
| <div class="conclusion-title">Overall summary</div> | |
| <div>{escape(conclusion)}</div> | |
| <div class="research-note">For biomarker research prioritization only. Not intended for clinical diagnosis.</div> | |
| </div> | |
| """ | |
| download = create_download(gene, row, data, score, label, str(interpretation)) | |
| return result_card, model_evidence, data_evidence, biological_evidence, conclusion_card, download | |
| def query_non_candidate_gene(gene: str): | |
| in_common_genes = gene in ALL_PRIOR.index | |
| if in_common_genes: | |
| row = ALL_PRIOR.loc[gene] | |
| level = int(row["evidence_level"]) | |
| normalized_score = float(row["evidence_score_norm"]) | |
| score = int(round(normalized_score * 100)) | |
| categories = str(row["evidence_categories"]) | |
| rank_text = str(int(row["knowledge_rank"])) | |
| level_text = f"{level} / 3" | |
| status = prior_support_label(level) | |
| themes = biological_themes(categories) | |
| theme_text = ", ".join(themes) if themes else "No predefined PD-related theme" | |
| interpretation = ( | |
| f"{gene} was evaluated among the 10,939 common genes but did not meet the " | |
| "Level 2/3 threshold used to construct the final 133-gene candidate set." | |
| ) | |
| coverage_text = "Evaluated among 10,939 common genes" | |
| else: | |
| level = 0 | |
| score = 0 | |
| rank_text = "Not available" | |
| level_text = "Not evaluated" | |
| status = "Not present in the 10,939 common-gene analysis space" | |
| theme_text = "No evidence annotation available" | |
| interpretation = ( | |
| f"{gene} was not present in the 10,939 genes shared across the four GEO " | |
| "datasets; therefore, it was not evaluated by the knowledge-prior or model pipeline." | |
| ) | |
| coverage_text = "Outside the common-gene analysis space" | |
| result_card = f""" | |
| <div class="result-content"> | |
| <div class="result-top"> | |
| <div> | |
| <div class="gene-line"><span class="gene-name">{escape(gene)}</span><span class="gene-subtitle">{escape(coverage_text)}</span></div> | |
| <div class="candidate-label non-candidate">{escape(status)}</div> | |
| </div> | |
| <div class="score-block"> | |
| <span>Prior score</span> | |
| <div><b>{score}</b><small>/ 100</small></div> | |
| </div> | |
| </div> | |
| <div class="score-track prior"><div style="width:{score}%"></div></div> | |
| </div> | |
| """ | |
| model_evidence = metric_rows( | |
| [ | |
| ("133-gene candidate set", "No"), | |
| ("StableGate evaluation", "Not evaluated"), | |
| ("Gate score", "Not available"), | |
| ("SHAP importance", "Not available"), | |
| ] | |
| ) | |
| data_evidence = metric_rows( | |
| [ | |
| ("Four-cohort candidate expression analysis", "Not evaluated"), | |
| ("Expression direction consistency", "Not available"), | |
| ("Differential-expression evidence", "Not available"), | |
| ("Analysis-space status", coverage_text), | |
| ] | |
| ) | |
| biological_evidence = ( | |
| metric_rows( | |
| [ | |
| ("Knowledge-prior evidence level", level_text), | |
| ("Knowledge-prior rank", rank_text), | |
| ("PD-related themes", theme_text), | |
| ] | |
| ) | |
| + f'<div class="explanation-text">{escape(interpretation)}</div>' | |
| ) | |
| conclusion_card = f""" | |
| <div class="conclusion-content"> | |
| <div class="conclusion-title">Overall summary</div> | |
| <div>{escape(interpretation)} This result should not be interpreted as evidence that the gene is unrelated to Parkinson's disease; it only describes its status under the predefined study pipeline.</div> | |
| <div class="research-note">For biomarker research prioritization only. Not intended for clinical diagnosis.</div> | |
| </div> | |
| """ | |
| report = pd.DataFrame( | |
| [ | |
| { | |
| "gene": gene, | |
| "in_133_gene_candidate_set": False, | |
| "in_10939_common_gene_space": in_common_genes, | |
| "candidate_status": status, | |
| "knowledge_prior_evidence_level": level_text, | |
| "knowledge_prior_evidence_score": score, | |
| "knowledge_prior_rank": rank_text, | |
| "biological_themes": theme_text, | |
| "model_evidence": "Not evaluated", | |
| "interpretation": interpretation, | |
| } | |
| ] | |
| ) | |
| handle = NamedTemporaryFile( | |
| delete=False, | |
| suffix=f"_{gene}_evidence_summary.csv", | |
| mode="w", | |
| encoding="utf-8", | |
| newline="", | |
| ) | |
| report.to_csv(handle.name, index=False) | |
| handle.close() | |
| return result_card, model_evidence, data_evidence, biological_evidence, conclusion_card, handle.name | |
| def show_results(): | |
| return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True) | |
| def clear_query(): | |
| return "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) | |
| css = """ | |
| body { background: #FAFAF8 !important; } | |
| .gradio-container { | |
| width: calc(100% - 32px) !important; max-width: 920px !important; min-width: 0 !important; | |
| margin: auto !important; padding-top: 16px !important; | |
| } | |
| footer { display: none !important; } | |
| .panel-card, .result-panel, .evidence-panel, .summary-panel { | |
| border: 1px solid #BCC1C4 !important; border-radius: 14px !important; | |
| background: #FFFFFF !important; box-shadow: none !important; | |
| box-sizing: border-box !important; width: 100% !important; | |
| margin: 0 0 14px 0 !important; | |
| } | |
| .panel-card { padding: 18px 24px !important; } | |
| .panel-card > *, .result-panel > *, .summary-panel > *, | |
| .panel-card .html-container, .panel-card .form, | |
| .result-panel .html-container, | |
| .summary-panel .html-container, .summary-panel .form { | |
| background: transparent !important; border: 0 !important; | |
| box-shadow: none !important; padding-left: 0 !important; padding-right: 0 !important; | |
| } | |
| .platform-title { font-size: 19px; font-weight: 800; color: #191D21; margin-bottom: 12px; | |
| text-align: center; width: 100%; } | |
| .platform-note { height: 8px; width: 100%; max-width: none; border-radius: 4px; margin: 10px 0 4px; | |
| background: linear-gradient(90deg,#ECEAE6 8%,#D9E6F0 28%,#AAC5DE 58%,#EDF0F2 100%); } | |
| .gene-input-label { color: #59616B; font-size: 14px; margin: 14px 0 6px; } | |
| .input-row { align-items: stretch !important; gap: 10px; } | |
| .input-row .form { border: 0 !important; background: transparent !important; } | |
| #gene-input, #gene-input input, #gene-input textarea, #search-button { | |
| height: 44px !important; min-height: 44px !important; margin: 0 !important; | |
| box-sizing: border-box !important; | |
| } | |
| #gene-input input, #gene-input textarea { padding-top: 10px !important; padding-bottom: 10px !important; } | |
| #search-button, #clear-button { | |
| height: 44px !important; min-height: 44px !important; border-radius: 12px !important; | |
| align-self: stretch !important; padding-left: 14px !important; padding-right: 14px !important; | |
| } | |
| #search-button { width: 88px !important; min-width: 88px !important; } | |
| #clear-button { width: 74px !important; min-width: 74px !important; | |
| background: var(--button-secondary-background-fill) !important; | |
| border: var(--button-secondary-border-width) solid var(--button-secondary-border-color) !important; } | |
| .result-panel { padding: 20px 24px !important; } | |
| .result-content { width: 100%; } | |
| .result-top { display: flex; justify-content: space-between; align-items: center; gap: 20px; } | |
| .gene-line { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; } | |
| .gene-name { font-family: Georgia, serif; font-size: 23px; color: #25292E; } | |
| .gene-subtitle { font-size: 13px; color: #59616B; } | |
| .candidate-label { display: inline-block; margin-top: 9px; padding: 3px 12px; | |
| border-radius: 999px; background: #E8F3DC; color: #2F6B2D; font-size: 12px; font-weight: 700; } | |
| .candidate-label.non-candidate { background: #FFF0D5; color: #946000; } | |
| .score-block { text-align: right; min-width: 112px; color: #434A52; } | |
| .score-block > span { font-size: 12px; } | |
| .score-block b { font-size: 45px; color: #111; line-height: 1; } | |
| .score-block small { font-size: 13px; margin-left: 4px; } | |
| .score-track { height: 5px; background: #F1F0EB; border-radius: 4px; margin-top: 17px; overflow: hidden; } | |
| .score-track div { height: 100%; background: #239E72; border-radius: 4px; } | |
| .score-track.prior div { background: #D49A36; } | |
| .evidence-panel { padding: 16px 24px 18px !important; } | |
| .evidence-panel .tab-nav { border-bottom: 1px solid #D5D8DA !important; } | |
| .evidence-panel button[role="tab"] { padding: 10px 18px !important; font-size: 14px !important; } | |
| .evidence-panel button[aria-selected="true"] { color: #202529 !important; border-bottom: 2px solid #202529 !important; } | |
| .metric-row { display: flex; justify-content: space-between; gap: 20px; padding: 10px 0; | |
| border-bottom: 1px solid #E0E2E3; color: #454B52; font-size: 14px; } | |
| .metric-row:last-child { border-bottom: 0; } | |
| .metric-row b { color: #15191D; text-align: right; max-width: 58%; } | |
| .explanation-text { color: #555E68; line-height: 1.65; padding-top: 14px; } | |
| .conclusion-title { font-weight: 800; color: #202529; margin-bottom: 8px; } | |
| .summary-panel { padding: 18px 24px !important; } | |
| .conclusion-content { color: #454B52; line-height: 1.6; } | |
| .research-note { color: #7A828B; font-size: 12px; margin-top: 7px; } | |
| #download-button { width: fit-content !important; min-width: 130px !important; border-radius: 10px !important; | |
| background: #FFFFFF !important; } | |
| @media (max-width: 680px) { | |
| .result-top { align-items: flex-start; } | |
| .score-block b { font-size: 36px; } | |
| .metric-row b { max-width: 52%; } | |
| } | |
| """ | |
| with gr.Blocks(title="PD-BioMarker Evidence Query", css=css) as demo: | |
| with gr.Column(elem_classes="panel-card"): | |
| gr.HTML( | |
| '<div class="platform-title">Parkinson's Disease Blood Biomarker Evidence Portal</div>' | |
| '<div class="platform-note"></div>' | |
| ) | |
| gr.HTML('<div class="gene-input-label">Gene Symbol</div>') | |
| with gr.Row(elem_classes="input-row"): | |
| gene_input = gr.Textbox( | |
| value="", | |
| show_label=False, | |
| container=False, | |
| placeholder="Enter a standard gene symbol, e.g., SNCA", | |
| scale=5, | |
| min_width=0, | |
| elem_id="gene-input", | |
| ) | |
| query_button = gr.Button("Search", scale=0, min_width=88, elem_id="search-button") | |
| clear_button = gr.Button("Clear", scale=0, min_width=74, elem_id="clear-button") | |
| with gr.Column(elem_classes="result-panel", visible=False) as result_panel: | |
| result_card = gr.HTML() | |
| with gr.Tabs(elem_classes="evidence-panel", visible=False) as evidence_panel: | |
| with gr.Tab("Model evidence"): | |
| model_evidence = gr.HTML() | |
| with gr.Tab("Data evidence"): | |
| data_evidence = gr.HTML() | |
| with gr.Tab("Biological interpretation"): | |
| biological_evidence = gr.HTML() | |
| with gr.Column(elem_classes="summary-panel", visible=False) as summary_panel: | |
| conclusion_card = gr.HTML() | |
| download_file = gr.DownloadButton("Download report", elem_id="download-button") | |
| query_event = query_button.click( | |
| fn=query_gene, | |
| inputs=gene_input, | |
| outputs=[ | |
| result_card, | |
| model_evidence, | |
| data_evidence, | |
| biological_evidence, | |
| conclusion_card, | |
| download_file, | |
| ], | |
| api_name="query_gene", | |
| ) | |
| query_event.then( | |
| fn=show_results, | |
| inputs=None, | |
| outputs=[result_panel, evidence_panel, summary_panel], | |
| ) | |
| clear_button.click( | |
| fn=clear_query, | |
| inputs=None, | |
| outputs=[ | |
| gene_input, | |
| result_panel, | |
| evidence_panel, | |
| summary_panel, | |
| ], | |
| api_name="clear_query", | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |