-
Notifications
You must be signed in to change notification settings - Fork 0
Add ocular health visualization suite #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zanax1990
wants to merge
1
commit into
main
Choose a base branch
from
codex/create-correlation-heatmaps-and-plots
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Ocular Health Visualization Suite | ||
|
|
||
| This project extends the research narrative around the **ODIR-5K** eye disease dataset and the Schwann cell regeneration studies in the cornea. It demonstrates how curated biomedical tables can be transformed into actionable visuals and interactive dashboards. | ||
|
|
||
| ## Project structure | ||
|
|
||
| ``` | ||
| ocular-health-dashboard/ | ||
| ├── analysis.py # Generates static plots (heatmaps, box/violin plots, clustering) | ||
| ├── dashboard_app.py # Plotly Dash interface for real-time exploration | ||
| ├── data/ | ||
| │ └── ocular_health_sample.csv # Lightweight demo subset inspired by ODIR-5K | ||
| ├── data_utils.py # Shared helpers for loading data and clustering | ||
| ├── outputs/ # Saved figures after running analysis.py | ||
| └── requirements.txt # Optional dependency pinning | ||
| ``` | ||
|
|
||
| ## How to use the analysis workflow | ||
|
|
||
| 1. (Optional) create and activate a virtual environment. | ||
| 2. Install dependencies: | ||
|
|
||
| ```bash | ||
| pip install -r requirements.txt | ||
| ``` | ||
|
|
||
| 3. Generate the static figures discussed in the manuscript: | ||
|
|
||
| ```bash | ||
| python analysis.py | ||
| ``` | ||
|
|
||
| The following plots are saved into the `outputs/` directory: | ||
|
|
||
| * `correlation_heatmap.png` – correlation heatmap between demographics, Schwann metrics, and diagnoses. | ||
| * `age_by_disease_boxplot.png` – box/strip plot of patient ages per diagnosis. | ||
| * `schwann_density_violin.png` – violin plot of Schwann cell density vs. diagnosis. | ||
| * `age_vs_regeneration.png` – scatter plot of age vs. corneal nerve regeneration rate. | ||
| * `cluster_scatter.png` – PCA projection of k-means clusters for patient subgroup discovery. | ||
|
|
||
| ## Interactive dashboard (Plotly Dash) | ||
|
|
||
| Launch the dashboard to explore the dataset live and reproduce the figures from the paper: | ||
|
|
||
| ```bash | ||
| python dashboard_app.py | ||
| ``` | ||
|
|
||
| The app exposes: | ||
|
|
||
| * **Correlation heatmap** for demographics vs. disease burden. | ||
| * **Age filter & disease selector** to drive the age boxplot, Schwann cell violin plot, and age vs. regeneration scatter. | ||
| * **Cluster explorer** that highlights PCA components and the patient subgroups obtained from k-means. | ||
|
|
||
| Because the demo dataset is small, the dashboard starts instantly but the layout is ready to scale to the full ODIR-5K cohort. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| """Generate exploratory plots for the ocular health project.""" | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import matplotlib.pyplot as plt | ||
| import pandas as pd | ||
| import seaborn as sns | ||
|
|
||
| from data_utils import ( | ||
| DISEASE_COLUMNS, | ||
| correlation_columns, | ||
| load_ocular_data, | ||
| melt_disease_long, | ||
| run_clustering, | ||
| ) | ||
|
|
||
| OUTPUT_DIR = Path(__file__).parent / "outputs" | ||
| OUTPUT_DIR.mkdir(exist_ok=True) | ||
|
|
||
| sns.set_theme(style="whitegrid") | ||
|
|
||
|
|
||
| def _save_current(fig: plt.Figure, filename: str) -> None: | ||
| path = OUTPUT_DIR / filename | ||
| fig.tight_layout() | ||
| fig.savefig(path, dpi=300) | ||
| plt.close(fig) | ||
| print(f"Saved {path}") | ||
|
|
||
|
|
||
| def create_correlation_heatmap(df: pd.DataFrame) -> None: | ||
| fig, ax = plt.subplots(figsize=(8, 6)) | ||
| corr = df[correlation_columns()].corr() | ||
| sns.heatmap(corr, annot=True, cmap="coolwarm", center=0, ax=ax) | ||
| ax.set_title("Demographic & Disease Correlations") | ||
| _save_current(fig, "correlation_heatmap.png") | ||
|
|
||
|
|
||
| def create_age_boxplot(long_df: pd.DataFrame) -> None: | ||
| fig, ax = plt.subplots(figsize=(9, 5)) | ||
| sns.boxplot(data=long_df, x="Disease", y="Age", ax=ax) | ||
| sns.stripplot(data=long_df, x="Disease", y="Age", color="black", size=3, alpha=0.6, ax=ax) | ||
| ax.set_title("Age distribution for each diagnosis") | ||
| _save_current(fig, "age_by_disease_boxplot.png") | ||
|
|
||
|
|
||
| def create_schwann_violin(long_df: pd.DataFrame) -> None: | ||
| fig, ax = plt.subplots(figsize=(9, 5)) | ||
| sns.violinplot( | ||
| data=long_df, | ||
| x="Disease", | ||
| y="SchwannCellDensity", | ||
| ax=ax, | ||
| inner="quartile", | ||
| cut=0, | ||
| ) | ||
| ax.set_ylabel("Schwann Cell Density (cells/mm)") | ||
| ax.set_title("Schwann cell distribution by diagnosis") | ||
| _save_current(fig, "schwann_density_violin.png") | ||
|
|
||
|
|
||
| def create_age_regeneration_scatter(df: pd.DataFrame) -> None: | ||
| fig, ax = plt.subplots(figsize=(7, 5)) | ||
| sns.scatterplot( | ||
| data=df, | ||
| x="Age", | ||
| y="RegenerationRate", | ||
| hue="PrimaryDiagnosis", | ||
| style="Sex", | ||
| ax=ax, | ||
| ) | ||
| ax.set_ylabel("Corneal nerve regeneration rate") | ||
| ax.set_title("Age vs. regeneration speed") | ||
| _save_current(fig, "age_vs_regeneration.png") | ||
|
|
||
|
|
||
| def create_cluster_plot(cluster_df: pd.DataFrame) -> None: | ||
| fig, ax = plt.subplots(figsize=(7, 5)) | ||
| sns.scatterplot( | ||
| data=cluster_df, | ||
| x="PC1", | ||
| y="PC2", | ||
| hue="Cluster", | ||
| style="PrimaryDiagnosis", | ||
| palette="Set2", | ||
| s=80, | ||
| ax=ax, | ||
| ) | ||
| ax.set_title("Patient subgroups (PCA + k-means)") | ||
| _save_current(fig, "cluster_scatter.png") | ||
|
|
||
|
|
||
| def main() -> None: | ||
| df = load_ocular_data() | ||
| long_df = melt_disease_long(df) | ||
| cluster_df, _pca, _model = run_clustering(df) | ||
|
|
||
| create_correlation_heatmap(df) | ||
| create_age_boxplot(long_df) | ||
| create_schwann_violin(long_df) | ||
| create_age_regeneration_scatter(df) | ||
| create_cluster_plot(cluster_df) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| """Plotly Dash application for real-time ocular health exploration.""" | ||
| from __future__ import annotations | ||
|
|
||
| import dash | ||
| from dash import Dash, Input, Output, dcc, html | ||
| import pandas as pd | ||
| import plotly.express as px | ||
|
|
||
| from data_utils import DISEASE_COLUMNS, correlation_columns, load_ocular_data, melt_disease_long, run_clustering | ||
|
|
||
|
|
||
| def build_figures(df: pd.DataFrame) -> tuple: | ||
| corr = df[correlation_columns()].corr() | ||
| corr_fig = px.imshow( | ||
| corr, | ||
| text_auto=".2f", | ||
| color_continuous_scale="RdBu", | ||
| zmin=-1, | ||
| zmax=1, | ||
| aspect="auto", | ||
| title="Correlation matrix", | ||
| ) | ||
|
|
||
| cluster_df, _pca, _model = run_clustering(df) | ||
| cluster_fig = px.scatter( | ||
| cluster_df, | ||
| x="PC1", | ||
| y="PC2", | ||
| color="Cluster", | ||
| symbol="Sex", | ||
| hover_data=["PatientID", "Age", "PrimaryDiagnosis"], | ||
| title="Patient subgroup structure", | ||
| ) | ||
| return corr_fig, cluster_fig, cluster_df | ||
|
|
||
|
|
||
| def build_app() -> Dash: | ||
| df = load_ocular_data() | ||
| long_df = melt_disease_long(df) | ||
| corr_fig, cluster_fig, cluster_df = build_figures(df) | ||
|
|
||
| app = dash.Dash(__name__) | ||
| app.title = "Ocular Health Dashboard" | ||
|
|
||
| min_age = df["Age"].min() | ||
| max_age = df["Age"].max() | ||
|
|
||
| app.layout = html.Div( | ||
| [ | ||
| html.H1("Schwann Cell & Ocular Disease Explorer"), | ||
| html.P( | ||
| "Interactively explore demographics, diagnoses, and Schwann cell metrics derived from the ODIR-5K study." | ||
| ), | ||
| html.Div( | ||
| [ | ||
| html.Label("Filter patients by age"), | ||
| dcc.RangeSlider( | ||
| id="age-range", | ||
| min=min_age, | ||
| max=max_age, | ||
| step=1, | ||
| allowCross=False, | ||
| value=[min_age, max_age], | ||
| marks={int(min_age): str(int(min_age)), int(max_age): str(int(max_age))}, | ||
| ), | ||
| ], | ||
| className="control-panel", | ||
| ), | ||
| html.Div( | ||
| [ | ||
| html.Label("Highlight a diagnosis"), | ||
| dcc.Dropdown( | ||
| id="disease-select", | ||
| options=[{"label": d, "value": d} for d in DISEASE_COLUMNS], | ||
| value=DISEASE_COLUMNS[0], | ||
| clearable=False, | ||
| ), | ||
| ] | ||
| ), | ||
| dcc.Graph(id="correlation-heatmap", figure=corr_fig), | ||
| html.Div( | ||
| [ | ||
| dcc.Graph(id="age-boxplot"), | ||
| dcc.Graph(id="schwann-violin"), | ||
| ], | ||
| className="split-row", | ||
| ), | ||
| dcc.Graph(id="age-regeneration-scatter"), | ||
| dcc.Graph(id="cluster-graph", figure=cluster_fig), | ||
| ], | ||
| className="container", | ||
| ) | ||
|
|
||
| @app.callback( | ||
| Output("age-boxplot", "figure"), | ||
| Output("schwann-violin", "figure"), | ||
| Output("age-regeneration-scatter", "figure"), | ||
| Input("age-range", "value"), | ||
| Input("disease-select", "value"), | ||
| ) | ||
| def update_patient_plots(age_range, disease): # type: ignore[override] | ||
| min_age, max_age = age_range | ||
| age_mask = (df["Age"] >= min_age) & (df["Age"] <= max_age) | ||
| filtered_df = df[age_mask] | ||
| filtered_long = long_df[(long_df["Age"] >= min_age) & (long_df["Age"] <= max_age)] | ||
|
|
||
| box_fig = px.box( | ||
| filtered_long, | ||
| x="Disease", | ||
| y="Age", | ||
| color="Disease", | ||
| title="Age distribution by diagnosis", | ||
| ) | ||
|
|
||
| violin_fig = px.violin( | ||
| filtered_long[filtered_long["Disease"] == disease], | ||
| x="Disease", | ||
| y="SchwannCellDensity", | ||
| box=True, | ||
| points="all", | ||
| title=f"Schwann cell density • {disease}", | ||
| ) | ||
|
|
||
| scatter_fig = px.scatter( | ||
| filtered_df, | ||
| x="Age", | ||
| y="RegenerationRate", | ||
| color="PrimaryDiagnosis", | ||
| symbol="Sex", | ||
| hover_data=["PatientID", "SchwannCellDensity"], | ||
| title="Age vs. corneal nerve regeneration", | ||
| ) | ||
| return box_fig, violin_fig, scatter_fig | ||
|
|
||
| return app | ||
|
|
||
|
|
||
| def main() -> None: | ||
| app = build_app() | ||
| app.run_server(debug=True) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| PatientID,Age,Sex,Diabetic Retinopathy,Glaucoma,Macular Degeneration,Cataract,SchwannCellDensity,RegenerationRate | ||
| P001,58,F,1,0,0,0,38.4,0.62 | ||
| P002,67,M,0,1,0,1,34.1,0.55 | ||
| P003,45,F,1,0,0,0,42.7,0.71 | ||
| P004,72,M,0,1,1,1,30.9,0.48 | ||
| P005,51,F,0,0,0,0,44.2,0.76 | ||
| P006,63,M,0,0,1,1,33.5,0.59 | ||
| P007,39,F,1,0,0,0,46.8,0.83 | ||
| P008,55,M,0,0,1,0,37.2,0.66 | ||
| P009,48,F,1,0,0,0,41.6,0.74 | ||
| P010,70,M,0,1,0,1,31.8,0.52 | ||
| P011,60,F,0,0,1,0,36.7,0.61 | ||
| P012,42,M,1,0,0,0,45.5,0.79 | ||
| P013,50,F,0,0,0,1,39.4,0.68 | ||
| P014,76,M,0,1,1,1,29.8,0.44 | ||
| P015,57,F,1,0,0,0,40.9,0.69 | ||
| P016,64,M,0,1,0,1,32.5,0.57 | ||
| P017,53,F,0,0,1,0,38.9,0.65 | ||
| P018,47,M,1,0,0,0,43.7,0.77 | ||
| P019,69,F,0,1,1,1,31.4,0.49 | ||
| P020,44,M,0,0,0,0,45.1,0.81 | ||
| P021,59,F,1,0,0,0,39.8,0.67 | ||
| P022,61,M,0,1,0,1,33.2,0.56 | ||
| P023,46,F,0,0,1,0,42.1,0.72 | ||
| P024,52,M,1,0,0,0,41.2,0.70 | ||
| P025,68,F,0,1,1,1,30.4,0.47 | ||
| P026,43,M,0,0,0,0,46.1,0.82 | ||
| P027,56,F,1,0,0,0,40.5,0.68 | ||
| P028,62,M,0,1,0,1,32.8,0.54 | ||
| P029,49,F,0,0,1,0,37.9,0.63 | ||
| P030,65,M,0,1,1,1,31.1,0.51 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Dash callback builds the violin plot from
filtered_long[filtered_long['Disease'] == disease]without checking whether that slice contains data. If the age slider is narrowed to a range that has no patients with the selected disease (which is easy to hit with this 30‑row sample set),plotly.express.violinraisesValueError: DataFrame is empty, causing the callback to fail and the dashboard graphs to stop updating. Guard for empty frames or return a placeholder figure before calling Plotly.Useful? React with 👍 / 👎.