Conserved Kir channel mechanisms governing intrinsic excitability in human and rodent parvalbumin neurons.
The 6 matches
- [1] § Methods › Confocal imaging and immunofluorescence analysis ↔ STREAMLIT_Clustering.py, lines 124–180 · score 0.83 · radial weight, spectral clustering, intensity weight, pixel intensities, distance, neighboring
- [2] § Methods › Confocal imaging and immunofluorescence analysis ↔ STREAMLIT_Analysis.py, lines 68–174 · score 0.78 · enrichment factor, pixel intensities, Ion channel, numpy, concentric, sum
- [3] § Methods › Confocal imaging and immunofluorescence analysis ↔ STREAMLIT_Analysis.py, lines 68–174 · score 0.67 · summed intensities, ion channel, fraction, density, fluorescence, enrichment
- [4] § Methods › Statistics and reproducibility ↔ pages/2_Analysis.py, lines 4–38 · score 0.58 · Kruskal Wallis, Mann Whitney, Wilcoxon
- [5] § Results › Immunofluorescence reveals somatic membrane expression of Kir channels in Pvalb neurons across species ↔ pages/2_Analysis.py, lines 4–38 · score 0.57 · Kruskal Wallis, Mann Whitney, pipeline, confocal, zoning
- [6] § Results › Voltage-dependent regulation of somatic input resistance is mediated by Kir-type potassium channels in human and mouse Pvalb neurons ↔ pages/2_Analysis.py, lines 273–285 · score 0.52 · Wilcoxon signed rank, Mann Whitney
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 756 lines · 26 KB · MIT · 3 matches
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- STREAMLIT_Analysis_Graph.py
- Statistical Evaluation and Visualization Module
- This standalone Streamlit application is designed to analyze the quantitative data (CSV files)
- exported from the main Confocal Analysis Pipeline.
- Key Functionalities:
- 1. **Data Aggregation:** Merges multiple CSV export files into a single dataset.
- 2. **Statistical Inference:** Performs robust non-parametric hypothesis testing to compare
- protein distribution across different cellular zones (e.g., Outer, Border, Core).
- - Supports both Independent designs (Kruskal-Wallis / Mann-Whitney U).
- - Supports Paired/Repeated Measures designs (Friedman / Wilcoxon).
- 3. **Visualization:** Generates publication-ready "Raincloud" style plots (Violin + Box + Strip)
- to visualize data distribution and statistical significance.
- 4. **Reporting:** Exports statistical summary tables and high-resolution figures.
- """
- from __future__ import annotations
- import io
- import re
- from dataclasses import dataclass
- from typing import Dict, List, Optional, Tuple
- import numpy as np
- import pandas as pd
- import streamlit as st
- import matplotlib.pyplot as plt
- from scipy import stats
- from statsmodels.stats.multitest import multipletests
- from STREAMLIT_Style import apply_confocal_theme, render_quick_intro
- # =========================================================
- # Page config
- # =========================================================
- st.set_page_config(page_title="Confocal pipeline — Analysis", layout="wide")
- st.title("📊 Analysis (from exported CSVs)")
- apply_confocal_theme()
- render_quick_intro()
- # =========================================================
- # Helpers
- # =========================================================
- DEFAULT_CELLFILE_SUFFIX = "_full_protein_stats.csv"
- REQUIRED_COLS_MIN = {"Zone"} # metric + grouping columns checked later
- def _safe_read_csv(uploaded_file) -> pd.DataFrame:
- # uploaded_file is a streamlit UploadedFile
- # robust read: try utf-8, fallback to latin-1
- try:
- return pd.read_csv(uploaded_file)
- except UnicodeDecodeError:
- uploaded_file.seek(0)
- return pd.read_csv(uploaded_file, encoding="latin-1")
- def _infer_group_from_filename(name: str) -> str:
- """
- Heuristic: strip the tail like '__seriesX_cellY...' or 'seriesX_cellY...'
- so KV_3_1_mouse_1__series4_cell1_z0003_full_protein_stats.csv -> KV_3_1_mouse_1
- """
- base = name
- base = re.sub(r"\.csv$", "", base, flags=re.IGNORECASE)
- # common patterns:
- base = re.sub(r"__series\d+_cell\d+.*$", "", base, flags=re.IGNORECASE)
- base = re.sub(r"_series\d+_cell\d+.*$", "", base, flags=re.IGNORECASE)
- base = re.sub(r"series\d+_cell\d+.*$", "", base, flags=re.IGNORECASE)
- base = base.strip("_- ")
- return base if base else "group"
- #
- # The following functions extract metadata from filenames, assuming a standard
- # naming convention often used in microscopy (Series -> Cell -> Z-slice).
- def _infer_series_id(name: str) -> str:
- m = re.search(r"(series\d+)", name.lower())
- return m.group(1) if m else "unknown"
- def _infer_cell_id(name: str) -> str:
- # e.g. ...series4_cell1... -> series4_cell1
- m = re.search(r"(series\d+_cell\d+)", name.lower())
- return m.group(1) if m else "unknown_cell"
- def _infer_z_id(name: str) -> str:
- # e.g. ...z0003... -> z0003
- m = re.search(r"(z\d{1,5})", name.lower())
- return m.group(1) if m else "z_unknown"
- def _as_pdf_bytes(fig) -> bytes:
- buf = io.BytesIO()
- fig.savefig(buf, format="pdf", dpi=300, bbox_inches="tight")
- plt.close(fig)
- return buf.getvalue()
- def _as_png_bytes(fig) -> bytes:
- buf = io.BytesIO()
- fig.savefig(buf, format="png", dpi=300, bbox_inches="tight")
- plt.close(fig)
- return buf.getvalue()
- def _has_pairing(df: pd.DataFrame, pairing_col: str, zones: List[str]) -> bool:
- if pairing_col not in df.columns:
- return False
- # pairing makes sense if at least some IDs appear in multiple zones
- tmp = df[df["Zone"].isin(zones)].copy()
- if tmp.empty:
- return False
- counts = tmp.groupby(pairing_col)["Zone"].nunique()
- return bool((counts >= 2).any())
- def _friedman_ready(df: pd.DataFrame, pairing_col: str, zones: List[str]) -> Tuple[bool, str]:
- """
- Friedman requires complete blocks: each pairing_id has values for all zones.
- We'll enforce that to be safe.
- """
- if pairing_col not in df.columns:
- return False, f"Missing pairing column: {pairing_col}"
- wide = df[df["Zone"].isin(zones)].pivot_table(index=pairing_col, columns="Zone", values="Metric", aggfunc="mean")
- if wide.empty:
- return False, "No data for selected zones."
- missing = wide.isna().any(axis=1)
- if missing.all():
- return False, "No complete paired rows (all rows missing at least one zone)."
- if missing.any():
- return False, "Friedman requires complete pairs across all selected zones (no missing per pairing ID)."
- return True, "OK"
- def _wilcoxon_ready(x: np.ndarray, y: np.ndarray) -> bool:
- # Wilcoxon needs paired samples; also, if all diffs are 0 it errors
- if len(x) != len(y) or len(x) == 0:
- return False
- dif = x - y
- return bool(np.any(dif != 0))
- # =========================================================
- # UI: upload
- # =========================================================
- st.subheader("1) Upload CSV files")
- uploads = st.file_uploader(
- "Upload *_full_protein_stats.csv (multiple)",
- type=["csv"],
- accept_multiple_files=True,
- )
- if not uploads:
- st.info("Upload one or more CSVs exported from your pipeline (e.g. *_full_protein_stats.csv).")
- st.stop()
- # Read + merge
- dfs: List[pd.DataFrame] = []
- errors: List[str] = []
- for uf in uploads:
- try:
- df = _safe_read_csv(uf)
- if "Zone" not in df.columns:
- raise ValueError("Missing required column: Zone")
- df = df.copy()
- df["SourceFile"] = uf.name
- # Metadata extraction based on filenames
- df["Group"] = _infer_group_from_filename(uf.name)
- df["Series_ID"] = _infer_series_id(uf.name)
- df["Cell_ID"] = _infer_cell_id(uf.name)
- df["Z_ID"] = _infer_z_id(uf.name)
- dfs.append(df)
- except Exception as e:
- errors.append(f"{uf.name}: {type(e).__name__}: {e}")
- if errors:
- st.warning("Some files could not be read:")
- st.write("\n".join([f"- {x}" for x in errors]))
- if not dfs:
- st.error("No valid CSV loaded.")
- st.stop()
- raw_df = pd.concat(dfs, ignore_index=True)
- # Metric candidates: numeric columns except obvious metadata
- numeric_cols = [c for c in raw_df.columns if pd.api.types.is_numeric_dtype(raw_df[c])]
- for drop in ["Series_ID", "Cell_ID", "Z_ID"]:
- if drop in numeric_cols:
- numeric_cols.remove(drop)
- if not numeric_cols:
- st.error("No numeric metric columns found in the uploaded CSVs.")
- st.stop()
- all_zones = sorted(raw_df["Zone"].dropna().astype(str).unique().tolist())
- all_groups = sorted(raw_df["Group"].dropna().astype(str).unique().tolist())
- with st.expander("Preview loaded data"):
- st.caption(f"Rows: {len(raw_df)} | Files: {len(dfs)} | Groups: {len(all_groups)} | Zones: {len(all_zones)}")
- st.dataframe(raw_df.head(30), use_container_width=True)
- # =========================================================
- # UI: configuration
- # =========================================================
- st.subheader("2) Configure analysis")
- c1, c2, c3 = st.columns([2, 2, 2])
- with c1:
- metric = st.selectbox("Metric to test", options=numeric_cols, index=numeric_cols.index("Enrichment_Factor") if "Enrichment_Factor" in numeric_cols else 0)
- with c2:
- zones_to_include = st.multiselect(
- "Zones to include",
- options=all_zones,
- default=all_zones,
- )
- with c3:
- # grouping
- group_mode = st.selectbox("Group by", options=["Group", "Original_Folder (not available here)"], index=0)
- # (we only have Group right now; later you can add more metadata)
- group_col = "Group"
- if len(zones_to_include) < 2:
- st.warning("Select at least 2 zones.")
- st.stop()
- # order selector: simple but robust
- st.markdown("**Plot order (left → right)**")
- zone_order = st.multiselect(
- "Choose zones in the order you want them displayed",
- options=zones_to_include,
- default=zones_to_include,
- help="The selection order defines the plot order. Remove/re-add to reorder.",
- )
- # ensure order contains exactly included zones
- zone_order = [z for z in zone_order if z in zones_to_include]
- missing_in_order = [z for z in zones_to_include if z not in zone_order]
- zone_order += missing_in_order # append any missing
- control_zone = st.selectbox("Reference (control) zone", options=zone_order, index=0)
- st.divider()
- st.subheader("3) Statistics setup")
- #
- # It is critical to select the correct test type based on experimental design.
- # Paired (Friedman/Wilcoxon) is used when the SAME cell/slice is measured across zones.
- # Independent (Kruskal/Mann-Whitney) is used when samples are distinct/unrelated.
- c4, c5, c6 = st.columns([2, 2, 2])
- with c4:
- do_global = st.toggle("Run global test first?", value=True)
- global_test = st.selectbox("Global test", options=["Kruskal–Wallis (independent)", "Friedman (paired)"], index=0, disabled=not do_global)
- with c5:
- pairwise_test = st.selectbox("Pairwise test (vs control)", options=["Mann–Whitney U (independent)", "Wilcoxon signed-rank (paired)"], index=0)
- with c6:
- pairing_col = st.selectbox("Pairing ID column (for paired tests)", options=[c for c in ["Cell_ID", "Z_ID", "Series_ID"] if c in raw_df.columns], index=0)
- alpha = st.slider("Alpha (for significance & FDR)", min_value=0.001, max_value=0.20, value=0.05, step=0.001)
- # Create analysis dataframe
- df = raw_df.copy()
- df = df[df["Zone"].isin(zones_to_include)].copy()
- df["Metric"] = pd.to_numeric(df[metric], errors="coerce")
- # Basic sanity
- df = df.dropna(subset=["Zone", group_col, "Metric"])
- df["Zone"] = df["Zone"].astype(str)
- df[group_col] = df[group_col].astype(str)
- if df.empty:
- st.error("No data left after filtering. Check zone selection and metric.")
- st.stop()
- paired_possible = _has_pairing(df, pairing_col, zones_to_include)
- # warn/disable if user chose paired tests but pairing isn't valid
- paired_global = (global_test.startswith("Friedman") if do_global else False)
- paired_pairwise = pairwise_test.startswith("Wilcoxon")
- if paired_global or paired_pairwise:
- if not paired_possible:
- st.warning(f"Paired tests selected, but pairing doesn't look valid with '{pairing_col}'. Consider independent tests.")
- else:
- # stricter: Friedman requires complete blocks across all zones
- # We'll check later per-group too, but give early warning
- pass
- # =========================================================
- # Core stats
- # =========================================================
- def _global_test(group_df: pd.DataFrame, zones: List[str]) -> Tuple[str, float]:
- """
- Returns (test_name, p_value).
- group_df columns: Zone, Metric, pairing_col possibly
- """
- if not do_global:
- return ("(skipped)", np.nan)
- # Kruskal-Wallis: Non-parametric ANOVA for independent samples
- if global_test.startswith("Kruskal"):
- arrays = []
- for z in zones:
- vals = group_df.loc[group_df["Zone"] == z, "Metric"].dropna().values
- if len(vals) > 0:
- arrays.append(vals)
- if len(arrays) < 2:
- return ("KruskalWallis", np.nan)
- stat, p = stats.kruskal(*arrays)
- return ("KruskalWallis", float(p))
- # Friedman: Non-parametric test for paired samples (repeated measures)
- # needs complete pairing rows per group
- wide = group_df.pivot_table(index=pairing_col, columns="Zone", values="Metric", aggfunc="mean")
- # require all zones columns present
- for z in zones:
- if z not in wide.columns:
- return ("Friedman", np.nan)
- wide = wide[zones]
- if wide.empty:
- return ("Friedman", np.nan)
- if wide.isna().any(axis=1).any():
- return ("Friedman", np.nan)
- stat, p = stats.friedmanchisquare(*[wide[z].values for z in zones])
- return ("Friedman", float(p))
- def _pairwise_vs_control(group_df: pd.DataFrame, zones: List[str], control: str) -> pd.DataFrame:
- """
- Returns DF with columns:
- Group, Compared_To, Raw_P, Test, N_Control, N_Target
- """
- out_rows = []
- # control values
- ref = group_df[group_df["Zone"] == control].copy()
- if ref.empty:
- return pd.DataFrame(out_rows)
- for target in zones:
- if target == control:
- continue
- tgt = group_df[group_df["Zone"] == target].copy()
- if tgt.empty:
- continue
- if pairwise_test.startswith("Mann"):
- # Mann-Whitney U: Non-parametric test for independent samples
- x = tgt["Metric"].dropna().values
- y = ref["Metric"].dropna().values
- if len(x) == 0 or len(y) == 0:
- p = 1.0
- else:
- try:
- p = float(stats.mannwhitneyu(x, y, alternative="two-sided").pvalue)
- except Exception:
- p = 1.0
- out_rows.append({
- "Compared_To": target,
- "Test": "MannWhitneyU_vs_Control",
- "Raw_P": p,
- "N_Control": int(len(y)),
- "N_Target": int(len(x)),
- })
- else:
- # Wilcoxon paired: align by pairing_col
- # Wilcoxon Signed-Rank: Non-parametric test for paired samples
- wide = group_df[group_df["Zone"].isin([control, target])].pivot_table(
- index=pairing_col, columns="Zone", values="Metric", aggfunc="mean"
- )
- if control not in wide.columns or target not in wide.columns:
- p = 1.0
- n = 0
- else:
- sub = wide[[control, target]].dropna()
- n = int(len(sub))
- if n == 0:
- p = 1.0
- else:
- x = sub[target].values
- y = sub[control].values
- if _wilcoxon_ready(x, y):
- try:
- p = float(stats.wilcoxon(x, y, alternative="two-sided").pvalue)
- except Exception:
- p = 1.0
- else:
- p = 1.0
- out_rows.append({
- "Compared_To": target,
- "Test": "Wilcoxon_vs_Control",
- "Raw_P": p,
- "N_Control": int(n),
- "N_Target": int(n),
- })
- return pd.DataFrame(out_rows)
- def _bh_fdr(pvals: List[float], alpha_: float) -> Tuple[List[bool], List[float]]:
- # Benjamini-Hochberg False Discovery Rate correction
- if len(pvals) == 0:
- return [], []
- reject, qvals, _, _ = multipletests(pvals, alpha=alpha_, method="fdr_bh")
- return reject.tolist(), qvals.tolist()
- # =========================================================
- # Plot (matplotlib-only, no seaborn)
- # =========================================================
- def _plot_violin_box_strip(subset: pd.DataFrame, metric_label: str, zones_order: List[str], control: str, stats_rows: pd.DataFrame) -> Tuple[bytes, bytes]:
- """
- Returns (pdf_bytes, png_bytes)
- stats_rows: rows with Compared_To, FDR_Corrected_P, Significant
- """
- #
- # This visualization combines three layers:
- # 1. Violin Plot: Shows the probability density of the data.
- # 2. Box Plot: Shows quartiles (median, 25%, 75%).
- # 3. Strip Plot (Jitter): Shows individual data points.
- # Prepare data in order
- data_by_zone = []
- for z in zones_order:
- vals = subset.loc[subset["Zone"] == z, "Metric"].dropna().values
- data_by_zone.append(vals)
- fig = plt.figure(figsize=(8.0, 7.5))
- ax = plt.gca()
- # Violin
- parts = ax.violinplot(
- dataset=data_by_zone,
- showmeans=False,
- showmedians=False,
- showextrema=False,
- )
- # outlines
- for pc in parts["bodies"]:
- pc.set_alpha(0.4)
- pc.set_linewidth(1.2)
- pc.set_edgecolor("black")
- # Boxplot
- bp = ax.boxplot(
- data_by_zone,
- widths=0.25,
- showfliers=False,
- patch_artist=True,
- )
- for box in bp["boxes"]:
- box.set_alpha(0.5)
- box.set_linewidth(1.5)
- box.set_edgecolor("black")
- for k in ["whiskers", "caps", "medians"]:
- for line in bp[k]:
- line.set_linewidth(1.5)
- line.set_color("black")
- # Strip (jitter)
- rng = np.random.default_rng(0)
- for i, vals in enumerate(data_by_zone, start=1):
- if len(vals) == 0:
- continue
- # Add jitter to x-axis to separate points
- x = rng.normal(loc=i, scale=0.06, size=len(vals))
- ax.scatter(x, vals, s=35, edgecolors="black", linewidths=0.6, alpha=0.55)
- ax.set_xticks(range(1, len(zones_order) + 1))
- ax.set_xticklabels([z.replace("_", " ") for z in zones_order], rotation=45, ha="right")
- ax.set_ylabel(metric_label, fontsize=14)
- ax.set_xlabel("")
- ax.grid(axis="y", linestyle="--", alpha=0.35)
- # significance lines: control vs significant targets
- try:
- z_to_x = {z: i + 1 for i, z in enumerate(zones_order)}
- x1 = z_to_x.get(control, None)
- if x1 is not None and not stats_rows.empty:
- y_max = float(np.nanmax(subset["Metric"].values)) if len(subset) else 1.0
- y_min = float(np.nanmin(subset["Metric"].values)) if len(subset) else 0.0
- y_range = max(1e-9, (y_max - y_min))
- step = y_range * 0.10
- y = y_max + y_range * 0.05
- sig = stats_rows[stats_rows["Significant"] == True].copy()
- # stable order: as zones_order
- sig["__order"] = sig["Compared_To"].apply(lambda z: zones_order.index(z) if z in zones_order else 9999)
- sig = sig.sort_values("__order")
- for _, r in sig.iterrows():
- tgt = str(r["Compared_To"])
- if tgt not in z_to_x:
- continue
- x2 = z_to_x[tgt]
- q = float(r["FDR_Corrected_P"]) if pd.notna(r["FDR_Corrected_P"]) else float("nan")
- h = step * 0.3
- ax.plot([x1, x1, x2, x2], [y - h, y, y, y - h], lw=1.5, c="black")
- label = (f"q={q:.1e}" if np.isfinite(q) and q < 1e-4 else f"q={q:.4f}") if np.isfinite(q) else "q=NA"
- ax.text((x1 + x2) / 2, y + h * 0.35, label, ha="center", va="bottom", fontsize=11)
- y += step
- ax.set_ylim(top=y + step * 0.4)
- except Exception:
- pass
- plt.tight_layout()
- pdf_bytes = _as_pdf_bytes(fig)
- # Need a fresh fig for PNG (since pdf closes fig)
- # So rebuild quickly: easiest is to render again
- # For simplicity, regenerate figure by calling function logic again is heavy.
- # We'll just create a new one the same way:
- fig2 = plt.figure(figsize=(8.0, 7.5))
- ax2 = plt.gca()
- parts2 = ax2.violinplot(data_by_zone, showmeans=False, showmedians=False, showextrema=False)
- for pc in parts2["bodies"]:
- pc.set_alpha(0.4)
- pc.set_linewidth(1.2)
- pc.set_edgecolor("black")
- bp2 = ax2.boxplot(data_by_zone, widths=0.25, showfliers=False, patch_artist=True)
- for box in bp2["boxes"]:
- box.set_alpha(0.5); box.set_linewidth(1.5); box.set_edgecolor("black")
- for k in ["whiskers", "caps", "medians"]:
- for line in bp2[k]:
- line.set_linewidth(1.5); line.set_color("black")
- rng2 = np.random.default_rng(0)
- for i, vals in enumerate(data_by_zone, start=1):
- if len(vals) == 0:
- continue
- x = rng2.normal(loc=i, scale=0.06, size=len(vals))
- ax2.scatter(x, vals, s=35, edgecolors="black", linewidths=0.6, alpha=0.55)
- ax2.set_xticks(range(1, len(zones_order) + 1))
- ax2.set_xticklabels([z.replace("_", " ") for z in zones_order], rotation=45, ha="right")
- ax2.set_ylabel(metric_label, fontsize=14)
- ax2.set_xlabel("")
- ax2.grid(axis="y", linestyle="--", alpha=0.35)
- # significance lines on fig2 as well
- try:
- z_to_x = {z: i + 1 for i, z in enumerate(zones_order)}
- x1 = z_to_x.get(control, None)
- if x1 is not None and not stats_rows.empty:
- y_max = float(np.nanmax(subset["Metric"].values)) if len(subset) else 1.0
- y_min = float(np.nanmin(subset["Metric"].values)) if len(subset) else 0.0
- y_range = max(1e-9, (y_max - y_min))
- step = y_range * 0.10
- y = y_max + y_range * 0.05
- sig = stats_rows[stats_rows["Significant"] == True].copy()
- sig["__order"] = sig["Compared_To"].apply(lambda z: zones_order.index(z) if z in zones_order else 9999)
- sig = sig.sort_values("__order")
- for _, r in sig.iterrows():
- tgt = str(r["Compared_To"])
- if tgt not in z_to_x:
- continue
- x2 = z_to_x[tgt]
- q = float(r["FDR_Corrected_P"]) if pd.notna(r["FDR_Corrected_P"]) else float("nan")
- h = step * 0.3
- ax2.plot([x1, x1, x2, x2], [y - h, y, y, y - h], lw=1.5, c="black")
- label = (f"q={q:.1e}" if np.isfinite(q) and q < 1e-4 else f"q={q:.4f}") if np.isfinite(q) else "q=NA"
- ax2.text((x1 + x2) / 2, y + h * 0.35, label, ha="center", va="bottom", fontsize=11)
- y += step
- ax2.set_ylim(top=y + step * 0.4)
- except Exception:
- pass
- plt.tight_layout()
- png_bytes = _as_png_bytes(fig2)
- return pdf_bytes, png_bytes
- # =========================================================
- # Run
- # =========================================================
- st.subheader("4) Run")
- run_btn = st.button("🚀 Run analysis", type="primary", use_container_width=True)
- if not run_btn:
- st.stop()
- # Prepare outputs
- summary_rows = []
- plot_files: Dict[str, Dict[str, bytes]] = {} # group -> {"pdf":..., "png":...}
- groups = sorted(df[group_col].unique().tolist())
- for g in groups:
- gdf = df[df[group_col] == g].copy()
- # Ensure control exists
- if control_zone not in gdf["Zone"].unique():
- summary_rows.append({
- "Group": g,
- "Stage": "Global",
- "Test": "(skipped)",
- "Reference": control_zone,
- "Compared_To": "ALL",
- "Raw_P": np.nan,
- "FDR_Corrected_P": np.nan,
- "Significant": False,
- "Note": "Missing control zone in this group"
- })
- continue
- # GLOBAL
- test_name, p_global = _global_test(gdf, zone_order)
- summary_rows.append({
- "Group": g,
- "Stage": "Global",
- "Test": test_name,
- "Reference": control_zone,
- "Compared_To": "ALL",
- "Raw_P": p_global,
- "FDR_Corrected_P": np.nan,
- "Significant": bool(np.isfinite(p_global) and p_global < alpha) if do_global else False,
- "Note": ""
- })
- # decide posthoc condition
- run_posthoc = True
- if do_global:
- run_posthoc = bool(np.isfinite(p_global) and p_global < alpha)
- # PAIRWISE vs control
- pw = _pairwise_vs_control(gdf, zone_order, control_zone)
- if pw.empty:
- continue
- if not run_posthoc:
- # still record as "not run due to global ns"
- for _, r in pw.iterrows():
- summary_rows.append({
- "Group": g,
- "Stage": "PostHoc",
- "Test": r["Test"],
- "Reference": control_zone,
- "Compared_To": r["Compared_To"],
- "Raw_P": np.nan,
- "FDR_Corrected_P": np.nan,
- "Significant": False,
- "Note": "Post hoc skipped (global not significant or global skipped off)"
- })
- continue
- pvals = pw["Raw_P"].astype(float).tolist()
- reject, qvals = _bh_fdr(pvals, alpha)
- pw["FDR_Corrected_P"] = qvals
- pw["Significant"] = reject
- pw["Group"] = g
- for _, r in pw.iterrows():
- summary_rows.append({
- "Group": g,
- "Stage": "PostHoc",
- "Test": r["Test"],
- "Reference": control_zone,
- "Compared_To": r["Compared_To"],
- "Raw_P": float(r["Raw_P"]),
- "FDR_Corrected_P": float(r["FDR_Corrected_P"]),
- "Significant": bool(r["Significant"]),
- "Note": ""
- })
- # Plot for this group
- pdf_bytes, png_bytes = _plot_violin_box_strip(
- subset=gdf,
- metric_label=metric,
- zones_order=zone_order,
- control=control_zone,
- stats_rows=pw[["Compared_To", "FDR_Corrected_P", "Significant"]].copy(),
- )
- plot_files[g] = {"pdf": pdf_bytes, "png": png_bytes}
- summary_df = pd.DataFrame(summary_rows)
- st.success("Done.")
- st.subheader("Results")
- cA, cB = st.columns([2, 1])
- with cA:
- st.dataframe(summary_df, use_container_width=True)
- with cB:
- # Download summary CSV
- csv_bytes = summary_df.to_csv(index=False).encode("utf-8")
- st.download_button(
- "⬇️ Download summary CSV",
- data=csv_bytes,
- file_name="Stats_Summary.csv",
- mime="text/csv",
- use_container_width=True,
- )
- st.divider()
- st.subheader("Plots (per Group)")
- if not plot_files:
- st.info("No plots were generated (maybe missing control zone or no significant posthoc).")
- else:
- for g, files in plot_files.items():
- st.markdown(f"### {g}")
- c1, c2, c3 = st.columns([2, 1, 1])
- with c1:
- st.image(files["png"], caption=f"{g} — {metric}", use_container_width=True)
- with c2:
- st.download_button(
- "⬇️ PDF",
- data=files["pdf"],
- file_name=f"ViolinBox_{g}_RefComparison.pdf".replace(" ", "_"),
- mime="application/pdf",
- use_container_width=True,
- )
- with c3:
- st.download_button(
- "⬇️ PNG",
- data=files["png"],
- file_name=f"ViolinBox_{g}_RefComparison.png".replace(" ", "_"),
- mime="image/png",
- use_container_width=True,
- )
2_Analysis.py at commit a92a869, under MIT · at the source
Overview
- Hungarian Center of Excellence for Molecular Medicine Research Group for Human neuron physiology and therapy, Szeged, Hungary
- Department of Physiology, Anatomy and Neuroscience, University of Szeged, Szeged, Hungary
- Hungarian Center of Excellence for Molecular Medicine Research Group for Cancer Genomics and Epigenetics, Szeged, Hungary
- Doctoral School of Experimental and Preventive Medicine, University of Szeged, Szeged, Hungary
- ELKH-SZTE Research Group for Cortical Microcircuits, Department of Physiology, Anatomy and Neuroscience, University of Szeged, Szeged, Hungary
- European Molecular Biology Laboratory, Heidelberg, Germany
- Department of Optics and Quantum Electronics, University of Szeged, Szeged, Hungary
- Hungarian Centre of Excellence for Molecular Medicine Research Group for Translational Medicine Development, Szeged, Hungary
- Department of Neurosurgery, University of Debrecen Clinical Centre, Debrecen, Hungary
- Department of Neurosurgery, Borsod County Hospital, Miskolc, Hungary
- Department of Neurosurgery, University of Szeged, Szeged, Hungary
- Neuronal Cell Biology Research Group, Eötvös Loránd University, Budapest, Hungary
Abstract
Human cortical interneurons differ from their rodent counterparts in intrinsic membrane properties, yet the mechanisms regulating excitability across physiologically relevant membrane potentials remain poorly defined. Here, we investigated inwardly rectifying potassium (Kir) channel control of subthreshold excitability in parvalbumin-expressing (Pvalb) interneurons from human and mouse neocortex. Using whole-cell recordings, dynamic clamp, patch sequencing, immunofluorescence, and computational modeling, we show that membrane hyperpolarization induces a proportional decrease in input resistance mediated by Kir channels in both species, despite higher baseline input resistance in human neurons. Transcriptomic and anatomical analyses revealed somatic membrane expression of four major Kir channel subtypes with moderate interspecies differences. Kir activation suppresses intrinsic excitability through combined voltage-dependent and shunting inhibition, an effect occurring during inhibitory postsynaptic potentials evoked by neurogliaform cells. Together, these findings show that homologous Pvalb neurons in humans have evolved toward a conserved, archetypal excitability phenotype, despite substantial differences in baseline excitability between species.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repositories
Its files are read in the Code ↔ Paper reader above, with 6 matches between paragraphs and lines of code.
HCEMM/ZoneSig
a92a86948e048c2ef1c0efd33cc2be2067328517, 15 January 2026Availability: 1 check, the latest on 29 September 2026: the link answers
- 29 September 2026: the link answers
13 files
- Cell_segmentation.py, Python, 320 lines
- STREAMLIT_Analysis.py, Python, 413 lines, 2 matches
- STREAMLIT_Clustering.py, Python, 470 lines, 1 match
- STREAMLIT_FolderPicker.p
y , Python, 177 lines - STREAMLIT_Napari_Viewer.
py , Python, 256 lines - STREAMLIT_Preprocessing.
py , Python, 485 lines - STREAMLIT_SaveManager.py
, Python, 716 lines - STREAMLIT_Segmentation.p
y , Python, 904 lines - STREAMLIT_Style.py, Python, 160 lines
- STREAMLIT_Zones.py, Python, 464 lines
- pages/
2_Analysis.py , Python, 756 lines, 3 matches - LICENSE, License, 21 lines
- README.md, Text, 154 lines
APPLICATION
Availability: 1 check, the latest on 29 September 2026: the link answers (HTTP 200)
- 29 September 2026: the link answers (HTTP 200)
Code availability
Custom analysis scripts used in this work are available in a publicly accessible repository; (ZoneSig) (https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 11 scripts, each with its path and the digest of its content;
- 6 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data Availability Statement
Custom analysis scripts used in this work are available in a publicly accessible repository; (ZoneSig) (https://
All data needed to evaluate the conclusions of this study are included in the paper and the Supplementary Materials, or have been uploaded as source files with the manuscript. Custom code used in this study is publicly available in a public repository (ZoneSig; https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 29 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 21 authors, 2 keywords, 11 MeSH terms, 2 funders, 67 references, 1 RRID.
Cite
This paper
Furdan, S., Douida, A., Bakos, E., Tiszlavicz, Á., Nemes, K., Pongor, L. S., Péli, K., Molnar, G., Tamas, G., Welter, D., Landry, J., H Kovács, B., Erdélyi, M., Bende, B., Hutoczki, G., Papp, A., Barzo, P., Benes, V., Szucs, A., . . . Lamsa, K. (2026). Conserved Kir channel mechanisms governing intrinsic excitability in human and rodent parvalbumin neurons. Communications biology, 9(1), 806. https://
BibTeX
@article{furdan2026conse
author = {Furdan, Szabina and Douida, Abdennour and Bakos, Emoke and Tiszlavicz, Ádám and Nemes, Kolos and Pongor, Lőrinc Sándor and Péli, Krisztián and Molnar, Gabor and Tamas, Gabor and Welter, Daphne and Landry, Jonathan and H Kovács, Bálint and Erdélyi, Miklós and Bende, Balazs and Hutoczki, Gabor and Papp, Attila and Barzo, Pal and Benes, Vladimir and Szucs, Attila and Szegedi, Viktor and Lamsa, Karri},
title = {{Conserved Kir channel mechanisms governing intrinsic excitability in human and rodent parvalbumin neurons}},
journal = {Communications biology},
year = {2026},
month = apr,
volume = {9},
number = {1},
pages = {806},
publisher = {Nature Publishing Group},
issn = {2399-3642},
doi = {10.1038/
url = {https://
pmid = {41974795},
pmcid = {PMC13265741}
}
RIS
TY - JOUR
AU - Furdan, Szabina
AU - Douida, Abdennour
AU - Bakos, Emoke
AU - Tiszlavicz, Ádám
AU - Nemes, Kolos
AU - Pongor, Lőrinc Sándor
AU - Péli, Krisztián
AU - Molnar, Gabor
AU - Tamas, Gabor
AU - Welter, Daphne
AU - Landry, Jonathan
AU - H Kovács, Bálint
AU - Erdélyi, Miklós
AU - Bende, Balazs
AU - Hutoczki, Gabor
AU - Papp, Attila
AU - Barzo, Pal
AU - Benes, Vladimir
AU - Szucs, Attila
AU - Szegedi, Viktor
AU - Lamsa, Karri
TI - Conserved Kir channel mechanisms governing intrinsic excitability in human and rodent parvalbumin neurons
T2 - Communications biology
J2 - Commun Biol
PY - 2026
DA - 2026/
VL - 9
IS - 1
SP - 806
SN - 2399-3642
PB - Nature Publishing Group
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"type": "article-journal",
"title": "Conserved Kir channel mechanisms governing intrinsic excitability in human and rodent parvalbumin neurons",
"container-title": "Communications biology",
"author": [
{
"family": "Furdan",
"given": "Szabina"
},
{
"family": "Douida",
"given": "Abdennour"
},
{
"family": "Bakos",
"given": "Emoke"
},
{
"family": "Tiszlavicz",
"given": "Ádám"
},
{
"family": "Nemes",
"given": "Kolos"
},
{
"family": "Pongor",
"given": "Lőrinc Sándor"
},
{
"family": "Péli",
"given": "Krisztián"
},
{
"family": "Molnar",
"given": "Gabor"
},
{
"family": "Tamas",
"given": "Gabor"
},
{
"family": "Welter",
"given": "Daphne"
},
{
"family": "Landry",
"given": "Jonathan"
},
{
"family": "H Kovács",
"given": "Bálint"
},
{
"family": "Erdélyi",
"given": "Miklós"
},
{
"family": "Bende",
"given": "Balazs"
},
{
"family": "Hutoczki",
"given": "Gabor"
},
{
"family": "Papp",
"given": "Attila"
},
{
"family": "Barzo",
"given": "Pal"
},
{
"family": "Benes",
"given": "Vladimir"
},
{
"family": "Szucs",
"given": "Attila"
},
{
"family": "Szegedi",
"given": "Viktor"
},
{
"family": "Lamsa",
"given": "Karri"
}
],
"container-title-short":
"volume": "9",
"issue": "1",
"page": "806",
"DOI": "10.1038/
"PMID": "41974795",
"PMCID": "PMC13265741",
"ISSN": "2399-3642",
"publisher": "Nature Publishing Group",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
13
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1371/journal.pcbi.1014571 [code]
- SynAPSeg: A novel dataset and image analysis framework for deep learning-based synapse detection and quantification.Journal: PLoS computational biologyIn common: Cellpose, napari, tifffile, 8 other tools, 4 references
- [2] doi:10.1038/s41586-026-10323-y [code]
- Genetically encoded assembly recorder temporally resolves cellular history.Journal: NatureIn common: Cellpose, napari, tifffile, 7 other tools, mouse, cellular / molecular
- [3] doi:10.1002/advs.202522762 [code]
- Enhancing Maturation of Human Neuromuscular Organoids via Electrical Stimulation.Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)In common: Cellpose, tifffile, Plotly, 7 other tools, 1 reference
- [4] doi:10.1016/j.ebiom.2026.106362 [code]
- Neuron-derived neurotrophic factor-positive interneurons: a cellular target for anti-seizure therapies.Journal: EBioMedicineIn common: scikit-learn, pandas, SciPy, 2 other tools, mouse, 6 references
- [5] doi:10.1038/s41593-026-02376-z [code]
- A framework for comparative analysis of human and mouse cortical neuron dendrites in corresponding brain regions.Journal: Nature neuroscienceIn common: statsmodels, scikit-learn, pandas, 3 other tools, mouse, 5 references
- [6] doi:10.3389/fnsyn.2026.1843277 [code]
- High-frequency, high-intensity electrical stimulation selectively activates human fast-spiking interneurons.Journal: Frontiers in synaptic neuroscienceIn common: intracellular / patch clamp, cellular / molecular, 7 references
- [7] doi: [code]
- Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement controlJournal: eLifeIn common: napari, tifffile, scikit-image, 7 other tools, mouse
- [8] doi:10.1016/j.celrep.2026.117420 [code]
- Neural population dynamics of direct electrical stimulation of neocortex.Journal: Cell reportsIn common: napari, scikit-image, Pillow, 6 other tools, mouse, 1 reference
- [9] doi:10.1126/sciadv.aeb4205 [code]
- Learning stochastic dynamics and cell-fate landscapes from single-cell snapshots via optimal transport.Journal: Science advancesIn common: Cellpose, tifffile, scikit-image, 6 other tools, cellular / molecular
- [10] doi:10.1073/pnas.2609132123 [code]
- A human lysosomal storage disorder toolkit for decoding proteome landscapes in cortical-like and dopaminergic-like induced neurons.Journal: Proceedings of the National Academy of Sciences of the United States of AmericaIn common: Cellpose, tifffile, Plotly, 5 other tools, cellular / molecular
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 2 repositories of the authors' code, each at its verified commit and with its license, 11 scripts, and 6 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:18b73c30fff5a268…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
