From f2e0acb11cbc99138777d449cd8154f063bbc1ca Mon Sep 17 00:00:00 2001 From: Yoo HoJun Date: Mon, 22 Jun 2026 12:47:19 +0900 Subject: [PATCH 1/3] feat: integrate openms-insight into quantms Implement 5 new Streamlit analysis pipeline pages powered by the openms-insight engine to handle downstream proteomics workflows: - Data Filtering (filtering.py): * Support Low Abundance, Low Repeatability, and Low Variance filters using Polars LazyFrame. - Missing Value Imputation (imputation.py): * Connect upstream filtered datasets using session state fallback. * Provide group-aware MAR (mean/median) and technical dropout MNAR (row/global minimum) methods. - Data Normalization & Scaling (normalization.py): * Build a 3-step preprocessing chain: Mathematical transformation, column-wise sample normalization (PQn, Quantile, Reference Feature), and row-wise scaling. - Statistical Inference (statistical.py): * Dynamically route test methods (limma-like, Welch, ANOVA) based on detected group counts. * Integrate multiple testing corrections (BH, Bonferroni). - GO Enrichment Analysis (enrichment.py): * Fetch foreground lists dynamically via P-value and Log2FC constraints. * Query MyGene.info API and render interactive Plotly charts across BP, CC, and MF tabs. --- app.py | 6 +- content/enrichment.py | 140 ++++++++++++++++++++++++++ content/filtering.py | 163 ++++++++++++++++++++++++++++++ content/imputation.py | 134 +++++++++++++++++++++++++ content/normalization.py | 182 ++++++++++++++++++++++++++++++++++ content/results_abundance.py | 32 +++--- content/statistical.py | 163 ++++++++++++++++++++++++++++++ src/common/results_helpers.py | 100 +++++++------------ 8 files changed, 845 insertions(+), 75 deletions(-) create mode 100644 content/enrichment.py create mode 100644 content/filtering.py create mode 100644 content/imputation.py create mode 100644 content/normalization.py create mode 100644 content/statistical.py diff --git a/app.py b/app.py index 194d857..b38e005 100644 --- a/app.py +++ b/app.py @@ -23,11 +23,15 @@ st.Page(Path("content", "results_rescoring.py"), title="Rescoring", icon="πŸ“ˆ"), st.Page(Path("content", "results_filtered.py"), title="Filtered PSMs", icon="🎯"), st.Page(Path("content", "results_abundance.py"), title="Abundance", icon="πŸ“‹"), + st.Page(Path("content", "filtering.py"), title="Filtering", icon="🧹"), + st.Page(Path("content", "imputation.py"), title="Imputation", icon="🩹"), + st.Page(Path("content", "normalization.py"), title="Normalization", icon="βš–οΈ"), + st.Page(Path("content", "statistical.py"), title="Statistical", icon="πŸ”’"), st.Page(Path("content", "results_volcano.py"), title="Volcano", icon="πŸŒ‹"), st.Page(Path("content", "results_pca.py"), title="PCA", icon="πŸ“Š"), st.Page(Path("content", "results_heatmap.py"), title="Heatmap", icon="πŸ”₯"), st.Page(Path("content", "results_library.py"), title="Spectral Library", icon="πŸ“š"), - st.Page(Path("content", "results_proteomicslfq.py"), title="Proteomics LFQ", icon="πŸ§ͺ"), + st.Page(Path("content", "enrichment.py"), title="Pathway Analysis", icon="πŸ“‰"), ], } diff --git a/content/enrichment.py b/content/enrichment.py new file mode 100644 index 0000000..d8c5f51 --- /dev/null +++ b/content/enrichment.py @@ -0,0 +1,140 @@ +"""Pathway Analysis Page.""" + +from pathlib import Path +import pandas as pd +import polars as pl +import streamlit as st +from src.common.common import page_setup +from src.common.results_helpers import get_abundance_data +# Import GO Enrichment modules from openms_insight engine +from openms_insight.analysis.enrichment import calculate_go_enrichment + +params = page_setup() +st.title("GO Enrichment Analysis") + +st.markdown( + """ +Identify overrepresented biological themes (BP, CC, MF) within your differentially expressed protein features using MyGene.info and Fisher's Exact Test. +""" +) + +if "workspace" not in st.session_state: + st.warning("Please initialize your workspace first.") + st.stop() + +# --- STEP 1: Upstream Statistics Checkpoint --- +if ( + "statistics_df" in st.session_state + and st.session_state["statistics_df"] is not None +): + final_statistics_report = st.session_state["statistics_df"] + st.info( + "πŸ”„ **Upstream Pipeline Detected**: Using analyzed matrices from the **Statistical Inference** step." + ) +else: + st.warning( + "⚠️ **Missing Prerequisites**: Statistical inference data not detected. Please run hypothesis testing first." + ) + st.page_link( + "content/results_statistics.py", label="Go to Statistical Inference", icon="πŸ”¬" + ) + st.stop() + +# --- STEP 2: Preprocessing Mapping Key Configuration --- +# Identify target identifier columns dynamically +id_col = "ProteinName" +if id_col not in final_statistics_report.columns: + st.error(f"❌ Structural Error: Column '{id_col}' is missing from the active matrix context.") + st.stop() + +# --- SECTION 1: Parameter Setup & Dynamic Cutoff Labels --- +st.subheader("Configure Enrichment Thresholds") + +# Check if target p-value should be adjusted or raw based on previous selections (Fallback safely to 'p-adj') +target_p_col = "p-adj" if "p-adj" in final_statistics_report.columns else "p-value" +p_label = ( + "Adjusted P-value (p-adj) Cutoff" + if target_p_col == "p-adj" + else "Raw P-value (p-value) Cutoff" +) + +ui_go_col1, ui_go_col2 = st.columns(2) + +with ui_go_col1: + p_cutoff = st.number_input( + f"πŸ”¬ {p_label}", + min_value=0.0001, + max_value=1.0, + value=0.05, + step=0.01, + format="%.4f", + help="Proteins with significance metrics below this value are mapped to the foreground cohort.", + ) + +with ui_go_col2: + fc_cutoff = st.number_input( + "πŸ“ˆ Absolute Difference Cutoff (|log2FC|)", + min_value=0.0, + max_value=10.0, + value=1.0, + step=0.1, + format="%.2f", + help="Proteins with absolute log2 fold change greater than or equal to this threshold will be selected.", + ) + +# --- SECTION 2: Execution and Interactive View Charts --- +st.markdown("
", unsafe_allow_html=True) +if st.button("πŸš€ Run GO Enrichment Analysis", type="primary", key="run_go_analysis"): + + with st.spinner("Querying MyGene.info API & executing hyper-geometric calculation loops..."): + # Convert internal pandas DataFrame to openms_insight Polars DataFrame expectation + stats_pl = pl.from_pandas(final_statistics_report) + + status, output = calculate_go_enrichment( + final_report=stats_pl, + id_col=id_col, + target_p_col=target_p_col, + p_cutoff=p_cutoff, + fc_cutoff=fc_cutoff, + ) + + # Route response structures based on analysis output status code + if status == "empty_data": + st.error("❌ No valid statistical rows found containing standard columns to run GO alignment.") + + elif status == "insufficient_proteins": + st.warning( + f"⚠️ Not enough significant proteins found to construct target datasets. " + f"(Criteria: {target_p_col} < {p_cutoff:.4f}, |log2FC| β‰₯ {fc_cutoff:.2f})." + ) + st.info(f"πŸ’‘ Found significant proteins count: **{output}**. Try relaxing your p-value or log2FC filters.") + + elif status == "success": + st.success("β­• GO Enrichment Analysis completed successfully!") + + # Display operational matrix scale + st.markdown( + f"πŸ“Š **Analysis Profile Scope**: Mapped **{output['fg_count']}** significant foreground profiles out of **{output['bg_count']}** reference background items." + ) + + # Build multi-tab interface layer for ontology subcategories + tabs = st.tabs([ + "🧬 Biological Process (BP)", + "πŸ”¬ Cellular Component (CC)", + "πŸ§ͺ Molecular Function (MF)" + ]) + categories_data = output["categories"] + + for idx, go_type in enumerate(["BP", "CC", "MF"]): + with tabs[idx]: + fig = categories_data[go_type]["fig"] + df_go = categories_data[go_type]["df"] + + if fig is not None and df_go is not None: + # Render plotly bar figures generated straight from backend engine + st.plotly_chart(fig, use_container_width=True) + + st.subheader(f"πŸ“Š {go_type} Results Dataframe") + st.dataframe(df_go, use_container_width=True) + else: + st.info(f"No statistically overrepresented terms identified for Category: **{go_type}**") \ No newline at end of file diff --git a/content/filtering.py b/content/filtering.py new file mode 100644 index 0000000..402c29b --- /dev/null +++ b/content/filtering.py @@ -0,0 +1,163 @@ +"""Filtering Page.""" + +from pathlib import Path +import pandas as pd +import polars as pl +import streamlit as st +from src.common.common import page_setup +from src.common.results_helpers import get_abundance_data + +# Import filtering functions from openms_insight package +from openms_insight.analysis.filter import ( + filter_low_abundance, + filter_low_repeatability, + filter_low_variance, +) + +params = page_setup() +st.title("Data Filtering") + +st.markdown( + """ +Filter out low-quality proteins from your dataset based on abundance, repeatability, or variance thresholds. +""" +) + +if "workspace" not in st.session_state: + st.warning("Please initialize your workspace first.") + st.stop() + +result = get_abundance_data(st.session_state["workspace"]) +if result is None: + st.info( + "Abundance data not available. Please run the workflow and configure sample groups first." + ) + st.page_link( + "content/results_abundance.py", label="Go to Abundance", icon="πŸ“‹" + ) + st.stop() + +pivot_df, group_map = result + +# 1. Identify actual sample columns dynamically +sample_cols = [ + c + for c in pivot_df.columns + if c not in ["ProteinName", "PeptideSequence"] +] + +# --- SECTION 1: Original Data View --- +st.subheader("Original Abundance Table") +st.markdown( + f"Currently displaying **{pivot_df.shape[0]}** proteins and **{len(sample_cols)}** samples before filtering." +) +st.dataframe(pivot_df, use_container_width=True) + +st.markdown("---") + +# --- SECTION 2: Filter Configuration --- +st.subheader("Configure Filter Engine") + +# Prepare Polars Metadata DataFrame required by openms_insight functions +metadata_rows = [{"sample_id": s, "group": group_map[s]} for s in sample_cols] +metadata_pl = pl.DataFrame( + metadata_rows, schema={"sample_id": pl.String, "group": pl.String} +) + +# User selection for filtering strategy +filter_method = st.selectbox( + "Select Filtering Method", + options=["Low Abundance", "Low Repeatability", "Low Variance"], + index=0, + help="Choose the statistical criteria to prune unreliable protein entries.", +) + +# Render threshold sliders dynamically based on the selected filter method +if filter_method == "Low Abundance": + st.markdown( + "**Low Abundance Filter**: Keeps rows where at least one group's median is above the selected percentile threshold." + ) + threshold = st.slider( + "Threshold Percentile (%)", + min_value=0.0, + max_value=100.0, + value=10.0, + step=5.0, + ) + +elif filter_method == "Low Repeatability": + st.markdown( + "**Low Repeatability Filter**: Keeps rows where at least one group has a missing value ratio within the allowed maximum." + ) + threshold = st.slider( + "Max Missing Ratio", + min_value=0.0, + max_value=100.0, + value=50.0, + step=5.0, + help="Allowed missing value (zero or null) ratio per group.", + ) + +elif filter_method == "Low Variance": + st.markdown( + "**Low Variance Filter**: Keeps rows where at least one group's variance is above the selected percentile threshold." + ) + threshold = st.slider( + "Threshold Percentile (%)", + min_value=0.0, + max_value=100.0, + value=10.0, + step=5.0, + ) + +# --- SECTION 3: Filter Execution and Collected Results View --- +if st.button("Apply Filter", type="primary"): + # Convert the original Pandas DataFrame into a Polars LazyFrame graph + quant_lazy = pl.from_pandas(pivot_df).lazy() + + # Route execution to the chosen openms_insight engine function + if filter_method == "Low Abundance": + filtered_lazy = filter_low_abundance( + quantification_data=quant_lazy, + metadata=metadata_pl, + group_column="group", + threshold_percentile=threshold, + ) + elif filter_method == "Low Repeatability": + # Convert percent slider input to ratio expected by the function (e.g., 50.0% -> 0.5) + filtered_lazy = filter_low_repeatability( + quantification_data=quant_lazy, + metadata=metadata_pl, + group_column="group", + max_missing_ratio=threshold / 100.0, + ) + elif filter_method == "Low Variance": + filtered_lazy = filter_low_variance( + quantification_data=quant_lazy, + metadata=metadata_pl, + group_column="group", + threshold_percentile=threshold, + ) + + # Collect the evaluated lazy graph and convert back to Pandas for visualization + filtered_df = filtered_lazy.collect().to_pandas() + st.session_state["filtered_df"] = filtered_df + + # Layout response metrics and the filtered matrix + st.success(f"Successfully applied **{filter_method}** filter!") + + # Display dataset scale compression stats + col1, col2, col3 = st.columns(3) + col1.metric("Original Proteins", pivot_df.shape[0]) + col2.metric("Filtered Proteins", filtered_df.shape[0]) + col3.metric( + "Removed Proteins", pivot_df.shape[0] - filtered_df.shape[0], delta=None + ) + + st.subheader("Filtered Abundance Table") + if filtered_df.empty: + st.warning( + "The filtered table is empty. Try relaxing the threshold constraints." + ) + else: + st.dataframe(filtered_df, use_container_width=True) \ No newline at end of file diff --git a/content/imputation.py b/content/imputation.py new file mode 100644 index 0000000..1cbbb5e --- /dev/null +++ b/content/imputation.py @@ -0,0 +1,134 @@ +"""Imputation Page.""" + +from pathlib import Path +import pandas as pd +import polars as pl +import streamlit as st +from src.common.common import page_setup +from src.common.results_helpers import get_abundance_data + +# Import imputation algorithms from openms_insight engine +from openms_insight.analysis.imputation import impute_mar, impute_smallest_value + +params = page_setup() +st.title("Missing Value Imputation") + +st.markdown( + """ +Handle missing values (zeros or nulls) in your quantification matrix using biological group-aware (MAR) or absolute lowest limit (MNAR) techniques. +""" +) + +if "workspace" not in st.session_state: + st.warning("Please initialize your workspace first.") + st.stop() + +# Load base dataset and clean dictionary keys +result = get_abundance_data(st.session_state["workspace"]) +if result is None: + st.info( + "Abundance data not available. Please run the workflow and configure sample groups first." + ) + st.page_link( + "content/results_abundance.py", label="Go to Abundance", icon="πŸ“‹" + ) + st.stop() + +pivot_df, group_map = result + +# 1. Pipeline Checkpoint: Fetch upstream filtered data if available, fallback to raw pivot matrix +if "filtered_df" in st.session_state and st.session_state["filtered_df"] is not None: + base_df = st.session_state["filtered_df"] + st.info( + "πŸ”„ **Upstream Pipeline Detected**: Using data processed from the **Filtering** step." + ) +else: + base_df = pivot_df + st.warning( + "⚠️ **Raw Input Active**: No filtering history found. Operating on the original unfiltered table." + ) + +# 2. Identify actual sample columns dynamically based on the current active matrix +sample_cols = [ + c for c in base_df.columns if c not in ["ProteinName", "PeptideSequence"] +] + +# --- SECTION 1: Input Matrix Summary --- +st.subheader("Input Matrix Overview") +st.markdown( + f"Currently analyzing **{base_df.shape[0]}** rows across **{len(sample_cols)}** samples before imputation." +) +st.dataframe(base_df, use_container_width=True) + +st.markdown("---") + +# --- SECTION 2: Imputation Configuration --- +st.subheader("Configure Imputation Engine") + +# Build Polars structural metadata DataFrame +metadata_rows = [{"sample_id": s, "group": group_map[s]} for s in sample_cols] +metadata_pl = pl.DataFrame( + metadata_rows, schema={"sample_id": pl.String, "group": pl.String} +) + +# User selection for core missingness assumption strategy +impute_category = st.selectbox( + "Select Imputation Class", + options=["MAR (Missing At Random)", "MNAR (Missing Not At Random)"], + index=0, + help="MAR uses group metrics (Mean/Median). MNAR shifts values below the limit of detection.", +) + +# Render algorithmic options sub-menus based on the parent selection +if impute_category == "MAR (Missing At Random)": + st.markdown( + "**Group Character Imputation**: Fills missing metrics leveraging sample properties belonging to the same group." + ) + strategy_opt = st.radio( + "Mathematical Strategy", + options=["median", "mean"], + index=0, + horizontal=True, + ) + +elif impute_category == "MNAR (Missing Not At Random)": + st.markdown( + "**Smallest Value Imputation**: Replaces missing items with the minimum values detected to reflect technical dropout limits." + ) + scope_opt = st.radio( + "Detection Minimum Scope", + options=["row", "global"], + index=0, + horizontal=True, + help="'row' targets current protein minimum; 'global' searches the entire mass spectrometry matrix profile.", + ) + +# --- SECTION 3: Imputation Execution --- +if st.button("Apply Imputation", type="primary"): + # Initialize optimization pipeline graph via lazy loading conversion + quant_lazy = pl.from_pandas(base_df).lazy() + + # Route configuration matrix parameters to designated engine function channels + if impute_category == "MAR (Missing At Random)": + imputed_lazy = impute_mar( + quantification_data=quant_lazy, + metadata=metadata_pl, + group_column="group", + strategy=strategy_opt, + ) + elif impute_category == "MNAR (Missing Not At Random)": + imputed_lazy = impute_smallest_value( + quantification_data=quant_lazy, metadata=metadata_pl, scope=scope_opt + ) + + # Resolve lazy graph optimization tree and push to display data frame structure + imputed_df = imputed_lazy.collect().to_pandas() + + # πŸ’Ύ Save current output into Session State for down-stream processing (Normalization, Statistics) + st.session_state["imputed_df"] = imputed_df + + st.success(f"Successfully finalized **{impute_category}** imputation step!") + + # Calculate and display a quick performance matrix check + st.subheader("Imputed Result Table") + st.dataframe(imputed_df, use_container_width=True) \ No newline at end of file diff --git a/content/normalization.py b/content/normalization.py new file mode 100644 index 0000000..6df4383 --- /dev/null +++ b/content/normalization.py @@ -0,0 +1,182 @@ +"""Normalization Page.""" + +from pathlib import Path +import pandas as pd +import polars as pl +import streamlit as st +from src.common.common import page_setup +from src.common.results_helpers import get_abundance_data +# Import normalization engine functions from openms_insight +from openms_insight.analysis.normalization import ( + normalize_samples, + scale_data, + transform_data, +) + +params = page_setup() +st.title("Data Normalization & Scaling") + +st.markdown( + """ +Standardize and transform your protein abundance profiles to correct for technical variations and optimize statistical distributions. +""" +) + +if "workspace" not in st.session_state: + st.warning("Please initialize your workspace first.") + st.stop() + +# Load primary database assets +result = get_abundance_data(st.session_state["workspace"]) +if result is None: + st.info( + "Abundance data not available. Please run the workflow and configure sample groups first." + ) + st.page_link( + "content/results_abundance.py", label="Go to Abundance", icon="πŸ“‹" + ) + st.stop() + +pivot_df, group_map = result + +# --- STEP 1: Upstream Pipeline Tracker (Fallback Architecture) --- +if ( + "imputed_df" in st.session_state + and st.session_state["imputed_df"] is not None +): + base_df = st.session_state["imputed_df"] + st.info( + "πŸ”„ **Upstream Pipeline Detected**: Using data processed from the **Imputation** step." + ) +elif ( + "filtered_df" in st.session_state + and st.session_state["filtered_df"] is not None +): + base_df = st.session_state["filtered_df"] + st.warning( + "⚠️ **Imputation Skipped**: Using data processed from the **Filtering** step." + ) +else: + base_df = pivot_df + st.warning( + "⚠️ **Raw Input Active**: No preprocessing history found. Operating on the original unfiltered table." + ) + +# 2. Extract actual active sample columns dynamically +sample_cols = [ + c for c in base_df.columns if c not in ["ProteinName", "PeptideSequence"] +] + +# --- SECTION 1: Active Input Table Preview --- +st.subheader("Input Table Overview") +st.markdown( + f"Currently displaying **{base_df.shape[0]}** rows and **{len(sample_cols)}** samples entering the normalization block." +) +st.dataframe(base_df, use_container_width=True) + +st.markdown("---") + +# --- SECTION 2: Normalization Parameter Configuration --- +st.subheader("Configure Preprocessing & Scaling Chains") + +# Prepare structural Polars metadata DataFrame required by backend functions +metadata_rows = [{"sample_id": s, "group": group_map[s]} for s in sample_cols] +metadata_pl = pl.DataFrame( + metadata_rows, schema={"sample_id": pl.String, "group": pl.String} +) + +col1, col2, col3 = st.columns(3) + +with col1: + st.markdown("### 🧬 1. Mathematical Transformation") + transform_strategy = st.selectbox( + "Select Transformation", + options=["None", "log2", "log10", "square_root", "cube_root"], + index=0, + help="Compress data dynamic range and stabilize heteroscedastic variance profiles.", + ) + +with col2: + st.markdown("### πŸ§ͺ 2. Sample Normalization") + norm_strategy = st.selectbox( + "Select Normalization", + options=["None", "sum", "median", "pqn", "reference_feature", "quantile"], + index=0, + help="Perform column-wise corrections to account for variable sample loading concentrations.", + ) + + # Conditionally display target input field for reference feature matching + ref_feature_input = None + if norm_strategy == "reference_feature": + ref_feature_input = st.text_input( + "Reference Protein Name (ID)", + value="", + placeholder="e.g., P01234 or GAPDH", + help="Enter the exact unique identifier string matching a key inside the 'ProteinName' column.", + ) + +with col3: + st.markdown("### πŸ“Š 3. Row Scaling") + scaling_strategy = st.selectbox( + "Select Scaling Mode", + options=["None", "mean_centering", "pareto_scaling", "range_scaling"], + index=0, + help="Adjust individual feature weights to make low and high abundance proteins comparable.", + ) + + +# --- SECTION 3: Normalization Pipe Sequential Execution --- +st.markdown("
", unsafe_allow_html=True) +if st.button("Apply Normalization Pipelines", type="primary"): + + # Validate reference feature selection if active before hitting polars execution layers + if norm_strategy == "reference_feature" and not ref_feature_input: + st.error( + "❌ Validation Error: Please provide a valid Reference Protein Name to use the 'reference_feature' strategy." + ) + st.stop() + + # Convert pandas memory buffer into optimization lazy dataframe tree graph + processing_lazy = pl.from_pandas(base_df).lazy() + + # Execute Chain 1: Transform Matrix Data + try: + processing_lazy = transform_data( + quantification_data=processing_lazy, + metadata=metadata_pl, + strategy=transform_strategy, + ) + + # Execute Chain 2: Normalize Sample Intensities (Columns) + processing_lazy = normalize_samples( + quantification_data=processing_lazy, + metadata=metadata_pl, + strategy=norm_strategy, + id_col="ProteinName", + reference_feature=ref_feature_input if norm_strategy == "reference_feature" else None, + ) + + # Execute Chain 3: Scale Individual Features (Rows) + processing_lazy = scale_data( + quantification_data=processing_lazy, + metadata=metadata_pl, + strategy=scaling_strategy, + ) + + # Finalize and collect pipeline query graph optimizations + normalized_df = processing_lazy.collect().to_pandas() + + # πŸ’Ύ Save processing checkpoint inside Session State for Downstream (Statistics Block) + st.session_state["normalized_df"] = normalized_df + + st.success("Successfully executed all selected normalization pipelines!") + + # Display the finalized transformation matrix view + st.subheader("Normalized Abundance Table") + st.dataframe(normalized_df, use_container_width=True) + + except ValueError as val_err: + # Gracefully handle validation failures raised from the engine layers (e.g., missing reference protein) + st.error(f"Engine Configuration Error: {str(val_err)}") + except Exception as e: + st.error(f"An unexpected pipeline error occurred: {str(e)}") \ No newline at end of file diff --git a/content/results_abundance.py b/content/results_abundance.py index a7ff453..b391fc2 100644 --- a/content/results_abundance.py +++ b/content/results_abundance.py @@ -59,25 +59,33 @@ st.page_link("content/workflow_configure.py", label="Go to Configure", icon="βš™οΈ") st.stop() - pivot_df, expr_df, group_map = result + pivot_df, group_map = result - # Display group comparison info - groups = sorted(set(group_map.values())) - if len(groups) >= 2: - group1, group2 = sorted(groups)[:2] - st.info(f"Statistical comparison: **{group2} vs {group1}**") + # st.write("------------ pivot_df -------------") + # st.write(pivot_df) - # Get sample columns (between stats and PeptideSequence) - sample_cols = [c for c in pivot_df.columns if c not in ["ProteinName", "log2FC", "p-value", "PeptideSequence"]] + # st.write("----------group_map-------------") + # st.write(group_map) + # 1. Dynamically extract actual sample columns, excluding ProteinName and PeptideSequence + sample_cols = [ + c + for c in pivot_df.columns + if c not in ["ProteinName", "PeptideSequence"] + ] + + # 2. Combine values from sample columns to create an 'Intensity' list column for the bar chart pivot_df["Intensity"] = pivot_df[sample_cols].apply(list, axis=1) - # Reorder columns: place Intensity after p-value - display_cols = ["ProteinName", "log2FC", "p-value", "Intensity"] + sample_cols + ["PeptideSequence"] + # 3. Reorder columns: [ProteinName, Intensity(chart), sample_cols..., PeptideSequence] + display_cols = ( + ["ProteinName", "Intensity"] + sample_cols + ["PeptideSequence"] + ) display_df = pivot_df[display_cols] + # 4. Display the dataframe as is without sorting, since statistical columns are removed st.dataframe( - display_df.sort_values("p-value"), + display_df, column_config={ "Intensity": st.column_config.BarChartColumn( "Intensity", @@ -108,4 +116,4 @@ with col2: st.page_link("content/results_pca.py", label="PCA", icon="πŸ“Š") with col3: - st.page_link("content/results_heatmap.py", label="Heatmap", icon="πŸ”₯") + st.page_link("content/results_heatmap.py", label="Heatmap", icon="πŸ”₯") \ No newline at end of file diff --git a/content/statistical.py b/content/statistical.py new file mode 100644 index 0000000..97ef7ff --- /dev/null +++ b/content/statistical.py @@ -0,0 +1,163 @@ +"""Statistical Inference Page.""" + +from pathlib import Path +import pandas as pd +import polars as pl +import streamlit as st +from src.common.common import page_setup +from src.common.results_helpers import get_abundance_data +# Import statistics engine functions from openms_insight +from openms_insight.analysis.statistics import calculate_statistical_tests, adjust_fdr_lazy + +params = page_setup() +st.title("Statistical Inference") + +st.markdown( + """ +Run differential expression analysis to identify statistically significant proteins across your biological groups. +""" +) + +if "workspace" not in st.session_state: + st.warning("Please initialize your workspace first.") + st.stop() + +# Load primary database assets +result = get_abundance_data(st.session_state["workspace"]) +if result is None: + st.info( + "Abundance data not available. Please run the workflow and configure sample groups first." + ) + st.page_link( + "content/results_abundance.py", label="Go to Abundance", icon="πŸ“‹" + ) + st.stop() + +pivot_df, group_map = result + +# --- STEP 1: Upstream Pipeline Tracker (Fallback Architecture) --- +if ( + "normalized_df" in st.session_state + and st.session_state["normalized_df"] is not None +): + base_df = st.session_state["normalized_df"] + st.info( + "πŸ”„ **Upstream Pipeline Detected**: Using data processed from the **Normalization** step." + ) +elif ( + "imputed_df" in st.session_state + and st.session_state["imputed_df"] is not None +): + base_df = st.session_state["imputed_df"] + st.warning( + "⚠️ **Normalization Skipped**: Using data processed from the **Imputation** step." + ) +elif ( + "filtered_df" in st.session_state + and st.session_state["filtered_df"] is not None +): + base_df = st.session_state["filtered_df"] + st.warning( + "⚠️ **Preprocessing Skipped**: Using data processed from the **Filtering** step." + ) +else: + base_df = pivot_df + st.warning( + "⚠️ **Raw Input Active**: No preprocessing history found. Operating on the original table." + ) + +# 2. Extract actual active sample columns and detect unique biological groups +sample_cols = [ + c for c in base_df.columns if c not in ["ProteinName", "PeptideSequence"] +] +unique_groups = sorted(list(set([group_map[s] for s in sample_cols]))) +group_count = len(unique_groups) + +# --- SECTION 1: Active Input Table Preview --- +st.subheader("Input Table Overview") +st.markdown( + f"Currently analyzing **{base_df.shape[0]}** rows across **{len(sample_cols)}** samples belonging to **{group_count} groups** ({', '.join(unique_groups)})." +) +st.dataframe(base_df, use_container_width=True) + +st.markdown("---") + +# --- SECTION 2: Dynamic Statistical Parameter Configuration --- +st.subheader("Configure Statistical Engine") + +# Prepare structural Polars metadata DataFrame required by backend functions +metadata_rows = [{"sample_id": s, "group": group_map[s]} for s in sample_cols] +metadata_pl = pl.DataFrame( + metadata_rows, schema={"sample_id": pl.String, "group": pl.String} +) + +col1, col2 = st.columns(2) + +with col1: + st.markdown("### πŸ”¬ 1. Hypothesis Testing Method") + + # Route available method options dynamically based on the group count + if group_count == 2: + method_options = ["limma_like", "welch", "paired"] + help_text = "'limma_like' uses Empirical Bayes variance shrinking. 'welch' is for unequal variances. 'paired' is for dependent samples." + elif group_count >= 3: + method_options = ["limma_like", "anova"] + help_text = "'limma_like' supports multi-group design matrices. 'anova' computes standard row-wise One-way ANOVA." + else: + st.error("❌ Statistical testing requires at least 2 unique sample groups.") + st.stop() + + selected_method = st.selectbox( + "Select Statistical Test", + options=method_options, + index=0, + help=help_text + ) + +with col2: + st.markdown("### πŸ›‘οΈ 2. Multiple Testing Correction (FDR)") + selected_fdr = st.selectbox( + "Select FDR Adjustment Strategy", + options=["BH", "Bonferroni", "None"], + index=0, + help="'BH' (Benjamini-Hochberg) controls False Discovery Rate. 'Bonferroni' is strict Family-Wise Error Rate control." + ) + +# --- SECTION 3: Statistical Query Execution --- +st.markdown("
", unsafe_allow_html=True) +if st.button("Run Statistical Analysis", type="primary"): + + # Convert active pandas dataframe into polars lazyframe graph + stats_lazy = pl.from_pandas(base_df).lazy() + + try: + # Execute Chain 1: Calculate core statistics (Adds log2FC, stat, p-value) + stats_lazy = calculate_statistical_tests( + quantification_data=stats_lazy, + metadata=metadata_pl, + method=selected_method + ) + + # Execute Chain 2: Adjust Multiple Testing (Adds p-adj) + stats_lazy = adjust_fdr_lazy( + quantification_data=stats_lazy, + strategy=selected_fdr + ) + + # Resolve lazy graph optimization tree and bring back to pandas memory + statistics_df = stats_lazy.collect().to_pandas() + + # πŸ’Ύ Save processing checkpoint inside Session State for Downstream (e.g., Volcano plot, Volcano/Heatmap UI) + st.session_state["statistics_df"] = statistics_df + + st.success(f"Successfully calculated **{selected_method}** test with **{selected_fdr}** FDR correction!") + + # Display the finalized statistics table view + st.subheader("Statistical Analysis Results") + st.markdown(f"Generated framework containing columns: `ProteinName`, `log2FC`, `stat`, `p-value`, `p-adj`, `PeptideSequence`") + st.dataframe(statistics_df, use_container_width=True) + + except ValueError as val_err: + st.error(f"Engine Validation Fallure: {str(val_err)}") + except Exception as e: + st.error(f"An unexpected pipeline error occurred: {str(e)}") \ No newline at end of file diff --git a/src/common/results_helpers.py b/src/common/results_helpers.py index db3e103..82d5a25 100644 --- a/src/common/results_helpers.py +++ b/src/common/results_helpers.py @@ -9,6 +9,7 @@ from pyopenms import IdXMLFile, MSExperiment, MzMLFile from src.workflow.ParameterManager import ParameterManager from statsmodels.stats.multitest import multipletests +from openms_insight.analysis.statistics import calculate_statistical_tests, adjust_fdr_lazy def get_workflow_dir(workspace): """Get the workflow directory path.""" @@ -184,15 +185,14 @@ def build_spectra_cache(mzml_dir: Path, filename_to_index: dict) -> tuple[pl.Dat @st.cache_data -def load_abundance_data(workspace_path: str, csv_mtime: float) -> tuple | None: - """Load CSV, compute stats (log2FC, p-value), build pivot_df and expr_df. - - Args: - workspace_path: Path to the workspace directory - csv_mtime: Modification time of CSV file (used as cache key) - - Returns: - Tuple of (pivot_df, expr_df, group_map) or None if data unavailable +def load_abundance_data( + workspace_path: str, + csv_mtime: float, +) -> tuple | None: + """Load a long-format CSV, pivot it into a standard wide-format table + + with sample intensity columns, and return the table along with the group + mapping. """ workflow_dir = get_workflow_dir(Path(workspace_path)) quant_dir = workflow_dir / "results" / "quant_results" @@ -214,7 +214,7 @@ def load_abundance_data(workspace_path: str, csv_mtime: float) -> tuple | None: if df.empty: return None - # Get group mapping from parameters + # 1. Extract group mapping information from the parameter JSON param_manager = ParameterManager(workflow_dir) params = param_manager.get_parameters_from_json() group_map = { @@ -226,64 +226,36 @@ def load_abundance_data(workspace_path: str, csv_mtime: float) -> tuple | None: if not group_map: return None + # 2. Extract sample names and map to groups df["Sample"] = df["Reference"].str.replace(".mzML", "", regex=False) df["Group"] = df["Reference"].map(group_map) df = df.dropna(subset=["Group"]) groups = sorted(df["Group"].unique()) - if len(groups) < 2: return None - group1, group2 = groups[:2] - - # Compute statistics per protein - stats_rows = [] - for protein, protein_df in df.groupby("ProteinName"): - g1_vals = protein_df[protein_df["Group"] == group1]["Intensity"].values - g2_vals = protein_df[protein_df["Group"] == group2]["Intensity"].values - - if len(g1_vals) < 2 or len(g2_vals) < 2: - pval = np.nan - else: - _, pval = ttest_ind(g1_vals, g2_vals, equal_var=False) - - mean_g1 = np.mean(g1_vals) if len(g1_vals) > 0 else np.nan - mean_g2 = np.mean(g2_vals) if len(g2_vals) > 0 else np.nan - - log2fc = np.log2(mean_g2 / mean_g1) if mean_g1 > 0 else np.nan - - stats_rows.append({ - "ProteinName": protein, - "log2FC": log2fc, - "p-value": pval, - }) - - stats_df = pd.DataFrame(stats_rows) - - if not stats_df.empty: - mask = stats_df["p-value"].notna() - if mask.any(): - _, p_adj, _, _ = multipletests(stats_df.loc[mask, "p-value"], method="fdr_bh") - stats_df.loc[mask, "p-adj"] = p_adj - else: - stats_df["p-adj"] = np.nan - - # Order samples by group (group2 first, then group1) + # 3. Define ordering and build sample arrays sample_group_df = df[["Sample", "Group"]].drop_duplicates() - group2_samples = sample_group_df[sample_group_df["Group"] == group2]["Sample"].tolist() - group1_samples = sample_group_df[sample_group_df["Group"] == group1]["Sample"].tolist() - all_samples = group2_samples + group1_samples - - # Build pivot table + group1_samples = sample_group_df[sample_group_df["Group"] == groups[0]][ + "Sample" + ].tolist() + group2_samples = sample_group_df[sample_group_df["Group"] == groups[1]][ + "Sample" + ].tolist() + all_samples = group1_samples + group2_samples + + # 4. Convert from long to wide format (Pivot) and fill missing values with 0.0 pivot_list = [] for protein, group_df in df.groupby("ProteinName"): peptides = ";".join(group_df["PeptideSequence"].unique()) intensity_dict = group_df.groupby("Sample")["Intensity"].sum().to_dict() + + # Fill sample columns (use 0.0 if missing) intensity_dict_complete = { - sample: intensity_dict.get(sample, 0) - for sample in all_samples + sample: intensity_dict.get(sample, 0.0) for sample in all_samples } + row = { "ProteinName": protein, **intensity_dict_complete, @@ -292,16 +264,20 @@ def load_abundance_data(workspace_path: str, csv_mtime: float) -> tuple | None: pivot_list.append(row) pivot_df = pd.DataFrame(pivot_list) - pivot_df = pivot_df.merge(stats_df, on="ProteinName", how="left") - pivot_df = pivot_df[["ProteinName", "log2FC", "p-value", "p-adj"] + all_samples + ["PeptideSequence"]] - # Build expression matrix (log2-transformed) - expr_df = pivot_df.set_index("ProteinName")[all_samples] - expr_df = expr_df.replace(0, np.nan) - expr_df = np.log2(expr_df + 1) - expr_df = expr_df.dropna() + # 5. Reorder columns to match the required standard format + # Structure: [ProteinName, Sample_1, Sample_2, ..., PeptideSequence] + columns_order = ["ProteinName"] + all_samples + ["PeptideSequence"] + pivot_df = pivot_df[columns_order] + + # 6. Clean up the group_map keys right before returning to the caller + clean_group_map = {} + for k, v in group_map.items(): + clean_key = k[:-5] if k.endswith(".mzML") else k + clean_group_map[clean_key] = v - return pivot_df, expr_df, group_map + # Return final results with the clean group map + return pivot_df, clean_group_map def get_abundance_data(workspace: Path) -> tuple | None: @@ -324,4 +300,4 @@ def get_abundance_data(workspace: Path) -> tuple | None: return None csv_mtime = csv_files[0].stat().st_mtime - return load_abundance_data(str(workspace), csv_mtime) + return load_abundance_data(str(workspace), csv_mtime) \ No newline at end of file From ec4abbde6bd2861e5d0f82666e12e07a7bf62421 Mon Sep 17 00:00:00 2001 From: Yoo HoJun Date: Fri, 26 Jun 2026 09:54:03 +0900 Subject: [PATCH 2/3] temp --- app.py | 8 +- content/results_heatmap.py | 84 +++++++++++------ content/results_pca.py | 84 +++++++++-------- content/results_volcano.py | 87 ++++++++---------- quantms_protein_heatmap/manifest.json | 49 ++++++++++ .../preprocessed/level_0.parquet | Bin 0 -> 4574 bytes .../preprocessed/level_1.parquet | Bin 0 -> 4574 bytes quantms_volcano_plot/manifest.json | 26 ++++++ .../preprocessed/volcanoData.parquet | Bin 0 -> 28968 bytes requirements.txt | 3 +- src/common/results_helpers.py | 2 + 11 files changed, 226 insertions(+), 117 deletions(-) create mode 100644 quantms_protein_heatmap/manifest.json create mode 100644 quantms_protein_heatmap/preprocessed/level_0.parquet create mode 100644 quantms_protein_heatmap/preprocessed/level_1.parquet create mode 100644 quantms_volcano_plot/manifest.json create mode 100644 quantms_volcano_plot/preprocessed/volcanoData.parquet diff --git a/app.py b/app.py index b38e005..e5e4d0e 100644 --- a/app.py +++ b/app.py @@ -23,6 +23,10 @@ st.Page(Path("content", "results_rescoring.py"), title="Rescoring", icon="πŸ“ˆ"), st.Page(Path("content", "results_filtered.py"), title="Filtered PSMs", icon="🎯"), st.Page(Path("content", "results_abundance.py"), title="Abundance", icon="πŸ“‹"), + st.Page(Path("content", "results_library.py"), title="Spectral Library", icon="πŸ“š"), + st.Page(Path("content", "enrichment.py"), title="Pathway Analysis", icon="πŸ“‰"), + ], + "Differential Protein Analysis": [ st.Page(Path("content", "filtering.py"), title="Filtering", icon="🧹"), st.Page(Path("content", "imputation.py"), title="Imputation", icon="🩹"), st.Page(Path("content", "normalization.py"), title="Normalization", icon="βš–οΈ"), @@ -30,9 +34,7 @@ st.Page(Path("content", "results_volcano.py"), title="Volcano", icon="πŸŒ‹"), st.Page(Path("content", "results_pca.py"), title="PCA", icon="πŸ“Š"), st.Page(Path("content", "results_heatmap.py"), title="Heatmap", icon="πŸ”₯"), - st.Page(Path("content", "results_library.py"), title="Spectral Library", icon="πŸ“š"), - st.Page(Path("content", "enrichment.py"), title="Pathway Analysis", icon="πŸ“‰"), - ], + ] } pg = st.navigation(pages) diff --git a/content/results_heatmap.py b/content/results_heatmap.py index 4ece3f4..1106377 100644 --- a/content/results_heatmap.py +++ b/content/results_heatmap.py @@ -1,19 +1,21 @@ """Heatmap Results Page.""" import streamlit as st import numpy as np +import polars as pl import plotly.express as px from scipy.cluster.hierarchy import linkage, leaves_list from scipy.spatial.distance import pdist from src.common.common import page_setup from src.common.results_helpers import get_abundance_data +from openms_insight import Heatmap params = page_setup() st.title("Heatmap") st.markdown( """ -Hierarchically clustered heatmap of protein-level abundance (Z-score normalized). -Proteins and samples are ordered by similarity. +Interactive hierarchically clustered heatmap of protein-level abundance (Z-score normalized). +Powered by OpenMS-Insight multi-resolution engine. """ ) @@ -21,49 +23,79 @@ st.warning("Please initialize your workspace first.") st.stop() +# 1. Use the refactored get_abundance_data function (returns only pivot_df and group_map) result = get_abundance_data(st.session_state["workspace"]) if result is None: st.info("Abundance data not available. Please run the workflow and configure sample groups first.") st.page_link("content/results_abundance.py", label="Go to Abundance", icon="πŸ“‹") st.stop() -pivot_df, expr_df, group_map = result +pivot_df, group_map = result -top_n = st.slider("Number of proteins", 20, 200, 50, key="heatmap_top_n") +if pivot_df.empty: + st.info("No data available for heatmap.") + st.stop() + +# 2. Compute expr_df directly and derive sample columns internally +# Select only the actual sample columns, excluding metadata fields like ProteinName. +sample_cols = [c for c in pivot_df.columns if c not in ["ProteinName", "PeptideSequence", "log2FC", "p-adj", "stat", "p-value"]] +expr_df = pivot_df.set_index("ProteinName")[sample_cols] +# 3. UI settings (number of top variance proteins) +top_n = st.slider("Number of proteins (Highest Variance)", 20, 200, 50, key="heatmap_top_n") + +# 4. Process data (variance selection -> Z-score normalization) var_series = expr_df.var(axis=1) top_proteins = var_series.sort_values(ascending=False).head(top_n).index heatmap_df = expr_df.loc[top_proteins] + +# Compute Z-scores and clean missing/invalid values heatmap_z = heatmap_df.sub(heatmap_df.mean(axis=1), axis=0).div(heatmap_df.std(axis=1), axis=0) heatmap_z = heatmap_z.replace([np.inf, -np.inf], np.nan).dropna() if not heatmap_z.empty: - row_linkage = linkage(pdist(heatmap_z.values), method="average") - row_order = leaves_list(row_linkage) - - col_linkage = linkage(pdist(heatmap_z.T.values), method="average") - col_order = leaves_list(col_linkage) - - heatmap_clustered = heatmap_z.iloc[row_order, col_order] - - fig_heatmap = px.imshow( - heatmap_clustered, - labels=dict(x="Sample", y="Protein", color="Z-score"), - aspect="auto", - color_continuous_scale=[[0.0, "#3b6fb6"], [0.5, "white"], [1.0, "#b40426"]], - zmin=-3, zmax=3 + # 5. Melt and convert data to Polars to satisfy OpenMS-Insight component requirements + # Restore the ProteinName row index as a column + heatmap_z_reset = heatmap_z.reset_index() + + # Unpivot the wide-format matrix into long-format (X, Y, Intensity) + melted_df = heatmap_z_reset.melt( + id_vars=["ProteinName"], + value_vars=sample_cols, + var_name="Sample", + value_name="Z_score" ) + + # Add sample group mapping if available for heatmap categories + if group_map: + melted_df["Group"] = melted_df["Sample"].map(group_map) + + # Pack the Pandas DataFrame into a Polars LazyFrame + heatmap_pl_lazy = pl.from_pandas(melted_df).lazy() - fig_heatmap.update_layout( - height=700, - xaxis={'side': 'bottom'}, - yaxis={'side': 'left'} + # 6. Initialize the OpenMS-Insight Heatmap component and map attributes + # Component spec: X axis (Sample), Y axis (ProteinName), color intensity (Z_score) + heatmap_component = Heatmap( + cache_id="quantms_protein_heatmap", + x_column="Sample", + y_column="ProteinName", + data=heatmap_pl_lazy, + intensity_column="Z_score", # πŸ”΄ 이 컬럼 수치둜 색상이 μΉ ν•΄μ Έμ•Ό ν•©λ‹ˆλ‹€. + title="Protein Abundance Heatmap (Z-score)", + x_label="Samples", + y_label="Proteins", + colorscale="RdBu", # Red-Blue μŠ€μΌ€μΌ + reversescale=True, + log_scale=False, # Z-scoreλŠ” μŒμˆ˜κ°€ μžˆμœΌλ―€λ‘œ False μœ μ§€ + intensity_label="Z-score", # λ²”λ‘€ 제λͺ©μ„ Z-score둜 μ§€μ • + category_column=None, + min_points=10000, # κ²©μžκ°€ 잘 ν‘œν˜„λ˜λ„λ‘ 점 개수 μƒν•œμ„ λ„‰λ„‰νžˆ μ§€μ • ) - fig_heatmap.update_xaxes(tickfont=dict(size=10)) - fig_heatmap.update_yaxes(tickfont=dict(size=8)) + # 7. Render the component + state_manager = st.session_state.get("state") + heatmap_component(state_manager=state_manager) - st.plotly_chart(fig_heatmap, use_container_width=True) else: st.warning("Insufficient data to generate the heatmap.") @@ -73,4 +105,4 @@ with col1: st.page_link("content/results_volcano.py", label="Volcano Plot", icon="πŸŒ‹") with col2: - st.page_link("content/results_pca.py", label="PCA", icon="πŸ“Š") + st.page_link("content/results_pca.py", label="PCA", icon="πŸ“Š") \ No newline at end of file diff --git a/content/results_pca.py b/content/results_pca.py index 45ea8eb..6f8ebce 100644 --- a/content/results_pca.py +++ b/content/results_pca.py @@ -6,6 +6,7 @@ from sklearn.preprocessing import StandardScaler from src.common.common import page_setup from src.common.results_helpers import get_abundance_data +from openms_insight.components.pca import run_and_plot_pca params = page_setup() st.title("PCA Analysis") @@ -21,67 +22,72 @@ st.warning("Please initialize your workspace first.") st.stop() +# 1. λ³€κ²½λœ get_abundance_data 적용 (λ°˜ν™˜κ°’ 2개: pivot_df, group_map) result = get_abundance_data(st.session_state["workspace"]) if result is None: st.info("Abundance data not available. Please run the workflow and configure sample groups first.") st.page_link("content/results_abundance.py", label="Go to Abundance", icon="πŸ“‹") st.stop() -pivot_df, expr_df, group_map = result +_, group_map = result + +if "statistics_df" not in st.session_state or st.session_state["statistics_df"] is None: + st.info("Statistical analysis data not found. Please run the statistical inference first to obtain p-adj values.") + # st.page_link("content/results_statistical.py", label="Go to Statistical Inference", icon="πŸ“Š") + st.stop() + +target_df = st.session_state["statistics_df"] + +# 2. 이 νŽ˜μ΄μ§€μ—μ„œ 직접 expr_df(λ°œν˜„λŸ‰ 맀트릭슀) κ΅¬μΆ•ν•˜κΈ° +# group_map의 ν‚€(μƒ˜ν”Œλͺ…λ“€)λ₯Ό 컬럼으둜 μ‚¬μš©ν•˜μ—¬ λ°œν˜„λŸ‰ λ°μ΄ν„°λ§Œ μΆ”μΆœν•©λ‹ˆλ‹€. +sample_columns = list(group_map.keys()) + +# pivot_df에 λ‹¨λ°±μ§ˆ μ‹λ³„μž(예: ProteinName)와 μƒ˜ν”Œ μ»¬λŸΌλ“€μ΄ ν¬ν•¨λ˜μ–΄ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€. +if "ProteinName" in target_df.columns: + expr_df = target_df.set_index("ProteinName")[sample_columns] +elif target_df.index.name == "ProteinName": + expr_df = target_df[sample_columns] +else: + # μ˜ˆμ™Έ λ°©μ§€: ProteinName이 μ»¬λŸΌμ— μ—†κ³  인덱슀 이름도 μ§€μ •λ˜μ§€ μ•Šμ€ 경우 첫 번째 μ»¬λŸΌμ„ 인덱슀둜 κ°€μ • + expr_df = target_df.set_index(target_df.columns[0])[sample_columns] top_n = 500 +# 3. p-value κΈ°μ€€ μƒμœ„ n개 λ‹¨λ°±μ§ˆ 필터링 +# pivot_dfμ—μ„œ μœ μ˜λ―Έν•œ λ‹¨λ°±μ§ˆ 탐색 top_proteins = ( - pivot_df + target_df .dropna(subset=["p-adj"]) .sort_values("p-adj", ascending=True) - .head(top_n)["ProteinName"] + .head(top_n) ) +# λ§Œμ•½ μœ„μ—μ„œ 인덱슀λ₯Ό λ°”κΏ¨λ‹€λ©΄ pivot_df ꡬ쑰에 맞게 λ‹¨λ°±μ§ˆ 이름을 κ°€μ Έμ˜΅λ‹ˆλ‹€. +if "ProteinName" in top_proteins.columns: + top_protein_names = top_proteins["ProteinName"] +else: + top_protein_names = top_proteins.index + expr_df_pca = expr_df.loc[ - expr_df.index.intersection(top_proteins) + expr_df.index.intersection(top_protein_names) ] if expr_df_pca.shape[0] < 2: st.info("Not enough proteins after p-value filtering for PCA.") st.stop() -X = expr_df_pca.T -X_scaled = StandardScaler().fit_transform(X) - -pca = PCA(n_components=2) -pcs = pca.fit_transform(X_scaled) - -pca_df = pd.DataFrame( - pcs, - columns=["PC1", "PC2"], - index=X.index -) - -norm_map = { - k.replace(".mzML", ""): v - for k, v in group_map.items() -} -pca_df["Group"] = pca_df.index.map(norm_map) - -fig_pca = px.scatter( - pca_df, - x="PC1", - y="PC2", - color="Group", - text=pca_df.index, -) - -fig_pca.update_traces(textposition="top center") -fig_pca.update_layout( - xaxis_title=f"PC1 ({pca.explained_variance_ratio_[0]*100:.1f}%)", - yaxis_title=f"PC2 ({pca.explained_variance_ratio_[1]*100:.1f}%)", - height=600, -) +# 4. OpenMS-Insight λͺ¨λ“ˆ 호좜 파트 +# 이 μ•„λž˜μ˜ μ§€μ €λΆ„ν•œ 계산 및 Plotly μ‹œκ°ν™” μ½”λ“œλ₯Ό μ™ΈλΆ€ λͺ¨λ“ˆλ‘œ μΊ‘μŠν™”ν•˜μ—¬ ν˜ΈμΆœν•©λ‹ˆλ‹€. +try: + # μ •μ˜λœ 뢄석 및 μ‹œκ°ν™” ν•¨μˆ˜ 호좜 + fig_pca, num_proteins = run_and_plot_pca(expr_df_pca, group_map) + + st.plotly_chart(fig_pca, use_container_width=True) + st.markdown(f"**Proteins used:** {num_proteins} (top {top_n} by p-adj)") -st.plotly_chart(fig_pca, use_container_width=True) +except Exception as e: + st.error(f"PCA μ‹œκ°ν™” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}") -st.markdown(f"**Proteins used:** {expr_df_pca.shape[0]} (top {top_n} by p-adj)") st.markdown("---") st.markdown("**Other visualizations:**") @@ -89,4 +95,4 @@ with col1: st.page_link("content/results_volcano.py", label="Volcano Plot", icon="πŸŒ‹") with col2: - st.page_link("content/results_heatmap.py", label="Heatmap", icon="πŸ”₯") + st.page_link("content/results_heatmap.py", label="Heatmap", icon="πŸ”₯") \ No newline at end of file diff --git a/content/results_volcano.py b/content/results_volcano.py index 8502489..1a4a1e9 100644 --- a/content/results_volcano.py +++ b/content/results_volcano.py @@ -1,9 +1,11 @@ """Volcano Plot Results Page.""" import streamlit as st +import polars as pl import plotly.express as px import numpy as np from src.common.common import page_setup from src.common.results_helpers import get_abundance_data +from openms_insight import VolcanoPlot params = page_setup() st.title("Volcano Plot") @@ -19,23 +21,27 @@ st.warning("Please initialize your workspace first.") st.stop() -result = get_abundance_data(st.session_state["workspace"]) -if result is None: - st.info("Abundance data not available. Please run the workflow and configure sample groups first.") - st.page_link("content/results_abundance.py", label="Go to Abundance", icon="πŸ“‹") +# πŸ” 1. Check if statistical analysis results are available in the session state +if "statistics_df" not in st.session_state or st.session_state["statistics_df"] is None: + st.info("Statistical analysis data not found. Please run the statistical engine first.") + # Set the icon link to the actual statistics page file path (e.g. statistical.py). + st.page_link("content/statistical.py", label="Go to Statistical Inference", icon="πŸ”¬") st.stop() -pivot_df, expr_df, group_map = result +# Retrieve the completed statistical analysis DataFrame +statistics_df = st.session_state["statistics_df"] -if pivot_df.empty: +if statistics_df.empty: st.info("No data available for volcano plot.") st.stop() -volcano_df = pivot_df.copy() -volcano_df = volcano_df.dropna(subset=["log2FC", "p-adj"]) - -volcano_df["neg_log10_padj"] = -np.log10(volcano_df["p-adj"]) +# 2. Clean data and convert to Polars for component input +# Drop missing values from the required 'log2FC' and 'p-adj' columns in `statistics_df`. +volcano_df = statistics_df.dropna(subset=["log2FC", "p-adj"]).copy() +# Convert the Pandas DataFrame to a Polars LazyFrame for component injection. +volcano_pl_lazy = pl.from_pandas(volcano_df).lazy() +# 3. Configure UI sliders (changing thresholds does not invalidate cache) fc_thresh = st.slider( "log2 Fold Change threshold", min_value=0.5, @@ -52,49 +58,34 @@ step=0.001, ) -volcano_df["Significance"] = "Not significant" -volcano_df.loc[ - (volcano_df["p-adj"] <= p_thresh) & (volcano_df["log2FC"] >= fc_thresh), - "Significance", -] = "Up-regulated" - -volcano_df.loc[ - (volcano_df["p-adj"] <= p_thresh) & (volcano_df["log2FC"] <= -fc_thresh), - "Significance", -] = "Down-regulated" - -fig_volcano = px.scatter( - volcano_df, - x="log2FC", - y="neg_log10_padj", - color="Significance", - hover_data=["ProteinName", "log2FC", "p-value", "p-adj"], - color_discrete_map={ - "Up-regulated": "red", - "Down-regulated": "blue", - "Not significant": "lightgrey", - } +# 4. Initialize the OpenMS-Insight VolcanoPlot component +volcano_plot_component = VolcanoPlot( + cache_id="quantms_volcano_plot", + data=volcano_pl_lazy, + log2fc_column="log2FC", + pvalue_column="p-adj", + label_column="ProteinName", + up_color="#E74C3C", + down_color="#3498DB", + ns_color="#95A5A6", + show_threshold_lines=True, + threshold_line_style="dash", ) -fig_volcano.add_vline(x=fc_thresh, line_dash="dash") -fig_volcano.add_vline(x=-fc_thresh, line_dash="dash") -fig_volcano.add_hline(y=-np.log10(p_thresh), line_dash="dash") +# 5. Render the component +state_manager = st.session_state.get("state") # Inject the project state management object -# Make x-axis symmetric around zero -max_abs_fc = volcano_df["log2FC"].abs().max() -x_range = [-max_abs_fc * 1.1, max_abs_fc * 1.1] # 10% padding - -fig_volcano.update_layout( - xaxis_title="log2 Fold Change", - yaxis_title="-log10(p-adj)", - xaxis_range=x_range, +volcano_plot_component( + state_manager=state_manager, + fc_threshold=fc_thresh, + p_threshold=p_thresh, + max_labels=10, # Display labels for the top N significant proteins height=600, ) -st.plotly_chart(fig_volcano, use_container_width=True) - -up_count = (volcano_df["Significance"] == "Up-regulated").sum() -down_count = (volcano_df["Significance"] == "Down-regulated").sum() +# 6. Keep the existing statistical summary and bottom links +up_count = ((volcano_df["p-adj"] <= p_thresh) & (volcano_df["log2FC"] >= fc_thresh)).sum() +down_count = ((volcano_df["p-adj"] <= p_thresh) & (volcano_df["log2FC"] <= -fc_thresh)).sum() st.markdown(f"**Up-regulated:** {up_count} | **Down-regulated:** {down_count}") st.markdown("---") @@ -103,4 +94,4 @@ with col1: st.page_link("content/results_pca.py", label="PCA", icon="πŸ“Š") with col2: - st.page_link("content/results_heatmap.py", label="Heatmap", icon="πŸ”₯") + st.page_link("content/results_heatmap.py", label="Heatmap", icon="πŸ”₯") \ No newline at end of file diff --git a/quantms_protein_heatmap/manifest.json b/quantms_protein_heatmap/manifest.json new file mode 100644 index 0000000..c896f9f --- /dev/null +++ b/quantms_protein_heatmap/manifest.json @@ -0,0 +1,49 @@ +{ + "version": 3, + "component_type": "heatmap", + "created_at": "2026-06-25T14:51:08.657547", + "config_hash": "17538bbd7c8bb0b83ef303c424c0f51e7dccd7b704b8b4d6f2f9b91555db0343", + "config": { + "x_column": "Sample", + "y_column": "ProteinName", + "intensity_column": "Z_score", + "min_points": 10000, + "display_aspect_ratio": 1.7777777777777777, + "x_bins": 188, + "y_bins": 106, + "use_simple_downsample": false, + "use_streaming": true, + "categorical_filters": [], + "zoom_identifier": "heatmap_zoom", + "title": "Protein Abundance Heatmap (Z-score)", + "x_label": "Samples", + "y_label": "Proteins", + "colorscale": "RdBu", + "category_column": null, + "log_scale": false, + "low_values_on_top": false, + "intensity_label": "Z-score" + }, + "filters": {}, + "filter_defaults": {}, + "interactivity": {}, + "data_files": { + "level_0": "level_0.parquet", + "level_1": "level_1.parquet" + }, + "data_values": { + "x_range": [ + "01_1", + "10_3" + ], + "y_range": [ + "sp|Biognosys|iRT-Kit_WR_fusion", + "sp|Q12496|YO098_YEAST" + ], + "total": 300, + "level_sizes": [ + 300 + ], + "num_levels": 2 + } +} \ No newline at end of file diff --git a/quantms_protein_heatmap/preprocessed/level_0.parquet b/quantms_protein_heatmap/preprocessed/level_0.parquet new file mode 100644 index 0000000000000000000000000000000000000000..845428ef23c85b33abcaa2796ec1e2963146c487 GIT binary patch literal 4574 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 literal 0 HcmV?d00001 diff --git a/quantms_protein_heatmap/preprocessed/level_1.parquet b/quantms_protein_heatmap/preprocessed/level_1.parquet new file mode 100644 index 0000000000000000000000000000000000000000..845428ef23c85b33abcaa2796ec1e2963146c487 GIT binary patch literal 4574 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 literal 0 HcmV?d00001 diff --git a/quantms_volcano_plot/manifest.json b/quantms_volcano_plot/manifest.json new file mode 100644 index 0000000..d572f61 --- /dev/null +++ b/quantms_volcano_plot/manifest.json @@ -0,0 +1,26 @@ +{ + "version": 3, + "component_type": "volcanoplot", + "created_at": "2026-06-25T14:41:42.791178", + "config_hash": "b06bb8d4c9dc289910868a8f1d6fdb18e1deb7feaa21962787f7d334e30f5608", + "config": { + "log2fc_column": "log2FC", + "pvalue_column": "p-adj", + "label_column": "ProteinName", + "title": null, + "x_label": "log2 Fold Change", + "y_label": "-log10(p-value)", + "up_color": "#E74C3C", + "down_color": "#3498DB", + "ns_color": "#95A5A6", + "show_threshold_lines": true, + "threshold_line_style": "dash" + }, + "filters": {}, + "filter_defaults": {}, + "interactivity": {}, + "data_files": { + "volcanoData": "volcanoData.parquet" + }, + "data_values": {} +} \ No newline at end of file diff --git a/quantms_volcano_plot/preprocessed/volcanoData.parquet b/quantms_volcano_plot/preprocessed/volcanoData.parquet new file mode 100644 index 0000000000000000000000000000000000000000..6e2d7a2e66ea78a79d6c84e8d86705bc97a050b5 GIT binary patch literal 28968 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index aac2879..a6b90e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -149,4 +149,5 @@ mygene # Redis Queue dependencies (for online mode) redis>=5.0.0 rq>=1.16.0 -statsmodels \ No newline at end of file +statsmodels +polars \ No newline at end of file diff --git a/src/common/results_helpers.py b/src/common/results_helpers.py index 82d5a25..d28fe57 100644 --- a/src/common/results_helpers.py +++ b/src/common/results_helpers.py @@ -223,6 +223,8 @@ def load_abundance_data( if key.startswith("mzML-group-") and value } + st.write(f"group_map: {group_map}") + if not group_map: return None From fc1ffd8343084527434d332867a28cd3d7005429 Mon Sep 17 00:00:00 2001 From: Yoo HoJun Date: Tue, 30 Jun 2026 15:57:55 +0900 Subject: [PATCH 3/3] feat: add auto scaling strategy to normalization --- content/normalization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/normalization.py b/content/normalization.py index 6df4383..a064598 100644 --- a/content/normalization.py +++ b/content/normalization.py @@ -119,7 +119,7 @@ st.markdown("### πŸ“Š 3. Row Scaling") scaling_strategy = st.selectbox( "Select Scaling Mode", - options=["None", "mean_centering", "pareto_scaling", "range_scaling"], + options=["None", "mean_centering", "auto_scaling", "pareto_scaling", "range_scaling"], index=0, help="Adjust individual feature weights to make low and high abundance proteins comparable.", )