OSCR

Conserved Kir channel mechanisms governing intrinsic excitability in human and rodent parvalbumin neurons.

Code ↔ Paper

6 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 6 matches
  1. [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. [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. [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. [4] § Methods › Statistics and reproducibility ↔ pages/2_Analysis.py, lines 4–38 · score 0.58 · Kruskal Wallis, Mann Whitney, Wilcoxon
  5. [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. [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

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. STREAMLIT_Analysis_Graph.py
  5. Statistical Evaluation and Visualization Module
  6. This standalone Streamlit application is designed to analyze the quantitative data (CSV files)
  7. exported from the main Confocal Analysis Pipeline.
  8. Key Functionalities:
  9. 1. **Data Aggregation:** Merges multiple CSV export files into a single dataset.
  10. 2. **Statistical Inference:** Performs robust non-parametric hypothesis testing to compare
  11. protein distribution across different cellular zones (e.g., Outer, Border, Core).
  12. - Supports both Independent designs (Kruskal-Wallis / Mann-Whitney U).
  13. - Supports Paired/Repeated Measures designs (Friedman / Wilcoxon).
  14. 3. **Visualization:** Generates publication-ready "Raincloud" style plots (Violin + Box + Strip)
  15. to visualize data distribution and statistical significance.
  16. 4. **Reporting:** Exports statistical summary tables and high-resolution figures.
  17. """
  18. from __future__ import annotations
  19. import io
  20. import re
  21. from dataclasses import dataclass
  22. from typing import Dict, List, Optional, Tuple
  23. import numpy as np
  24. import pandas as pd
  25. import streamlit as st
  26. import matplotlib.pyplot as plt
  27. from scipy import stats
  28. from statsmodels.stats.multitest import multipletests
  29. from STREAMLIT_Style import apply_confocal_theme, render_quick_intro
  30. # =========================================================
  31. # Page config
  32. # =========================================================
  33. st.set_page_config(page_title="Confocal pipeline — Analysis", layout="wide")
  34. st.title("📊 Analysis (from exported CSVs)")
  35. apply_confocal_theme()
  36. render_quick_intro()
  37. # =========================================================
  38. # Helpers
  39. # =========================================================
  40. DEFAULT_CELLFILE_SUFFIX = "_full_protein_stats.csv"
  41. REQUIRED_COLS_MIN = {"Zone"} # metric + grouping columns checked later
  42. def _safe_read_csv(uploaded_file) -> pd.DataFrame:
  43. # uploaded_file is a streamlit UploadedFile
  44. # robust read: try utf-8, fallback to latin-1
  45. try:
  46. return pd.read_csv(uploaded_file)
  47. except UnicodeDecodeError:
  48. uploaded_file.seek(0)
  49. return pd.read_csv(uploaded_file, encoding="latin-1")
  50. def _infer_group_from_filename(name: str) -> str:
  51. """
  52. Heuristic: strip the tail like '__seriesX_cellY...' or 'seriesX_cellY...'
  53. so KV_3_1_mouse_1__series4_cell1_z0003_full_protein_stats.csv -> KV_3_1_mouse_1
  54. """
  55. base = name
  56. base = re.sub(r"\.csv$", "", base, flags=re.IGNORECASE)
  57. # common patterns:
  58. base = re.sub(r"__series\d+_cell\d+.*$", "", base, flags=re.IGNORECASE)
  59. base = re.sub(r"_series\d+_cell\d+.*$", "", base, flags=re.IGNORECASE)
  60. base = re.sub(r"series\d+_cell\d+.*$", "", base, flags=re.IGNORECASE)
  61. base = base.strip("_- ")
  62. return base if base else "group"
  63. #
  64. # The following functions extract metadata from filenames, assuming a standard
  65. # naming convention often used in microscopy (Series -> Cell -> Z-slice).
  66. def _infer_series_id(name: str) -> str:
  67. m = re.search(r"(series\d+)", name.lower())
  68. return m.group(1) if m else "unknown"
  69. def _infer_cell_id(name: str) -> str:
  70. # e.g. ...series4_cell1... -> series4_cell1
  71. m = re.search(r"(series\d+_cell\d+)", name.lower())
  72. return m.group(1) if m else "unknown_cell"
  73. def _infer_z_id(name: str) -> str:
  74. # e.g. ...z0003... -> z0003
  75. m = re.search(r"(z\d{1,5})", name.lower())
  76. return m.group(1) if m else "z_unknown"
  77. def _as_pdf_bytes(fig) -> bytes:
  78. buf = io.BytesIO()
  79. fig.savefig(buf, format="pdf", dpi=300, bbox_inches="tight")
  80. plt.close(fig)
  81. return buf.getvalue()
  82. def _as_png_bytes(fig) -> bytes:
  83. buf = io.BytesIO()
  84. fig.savefig(buf, format="png", dpi=300, bbox_inches="tight")
  85. plt.close(fig)
  86. return buf.getvalue()
  87. def _has_pairing(df: pd.DataFrame, pairing_col: str, zones: List[str]) -> bool:
  88. if pairing_col not in df.columns:
  89. return False
  90. # pairing makes sense if at least some IDs appear in multiple zones
  91. tmp = df[df["Zone"].isin(zones)].copy()
  92. if tmp.empty:
  93. return False
  94. counts = tmp.groupby(pairing_col)["Zone"].nunique()
  95. return bool((counts >= 2).any())
  96. def _friedman_ready(df: pd.DataFrame, pairing_col: str, zones: List[str]) -> Tuple[bool, str]:
  97. """
  98. Friedman requires complete blocks: each pairing_id has values for all zones.
  99. We'll enforce that to be safe.
  100. """
  101. if pairing_col not in df.columns:
  102. return False, f"Missing pairing column: {pairing_col}"
  103. wide = df[df["Zone"].isin(zones)].pivot_table(index=pairing_col, columns="Zone", values="Metric", aggfunc="mean")
  104. if wide.empty:
  105. return False, "No data for selected zones."
  106. missing = wide.isna().any(axis=1)
  107. if missing.all():
  108. return False, "No complete paired rows (all rows missing at least one zone)."
  109. if missing.any():
  110. return False, "Friedman requires complete pairs across all selected zones (no missing per pairing ID)."
  111. return True, "OK"
  112. def _wilcoxon_ready(x: np.ndarray, y: np.ndarray) -> bool:
  113. # Wilcoxon needs paired samples; also, if all diffs are 0 it errors
  114. if len(x) != len(y) or len(x) == 0:
  115. return False
  116. dif = x - y
  117. return bool(np.any(dif != 0))
  118. # =========================================================
  119. # UI: upload
  120. # =========================================================
  121. st.subheader("1) Upload CSV files")
  122. uploads = st.file_uploader(
  123. "Upload *_full_protein_stats.csv (multiple)",
  124. type=["csv"],
  125. accept_multiple_files=True,
  126. )
  127. if not uploads:
  128. st.info("Upload one or more CSVs exported from your pipeline (e.g. *_full_protein_stats.csv).")
  129. st.stop()
  130. # Read + merge
  131. dfs: List[pd.DataFrame] = []
  132. errors: List[str] = []
  133. for uf in uploads:
  134. try:
  135. df = _safe_read_csv(uf)
  136. if "Zone" not in df.columns:
  137. raise ValueError("Missing required column: Zone")
  138. df = df.copy()
  139. df["SourceFile"] = uf.name
  140. # Metadata extraction based on filenames
  141. df["Group"] = _infer_group_from_filename(uf.name)
  142. df["Series_ID"] = _infer_series_id(uf.name)
  143. df["Cell_ID"] = _infer_cell_id(uf.name)
  144. df["Z_ID"] = _infer_z_id(uf.name)
  145. dfs.append(df)
  146. except Exception as e:
  147. errors.append(f"{uf.name}: {type(e).__name__}: {e}")
  148. if errors:
  149. st.warning("Some files could not be read:")
  150. st.write("\n".join([f"- {x}" for x in errors]))
  151. if not dfs:
  152. st.error("No valid CSV loaded.")
  153. st.stop()
  154. raw_df = pd.concat(dfs, ignore_index=True)
  155. # Metric candidates: numeric columns except obvious metadata
  156. numeric_cols = [c for c in raw_df.columns if pd.api.types.is_numeric_dtype(raw_df[c])]
  157. for drop in ["Series_ID", "Cell_ID", "Z_ID"]:
  158. if drop in numeric_cols:
  159. numeric_cols.remove(drop)
  160. if not numeric_cols:
  161. st.error("No numeric metric columns found in the uploaded CSVs.")
  162. st.stop()
  163. all_zones = sorted(raw_df["Zone"].dropna().astype(str).unique().tolist())
  164. all_groups = sorted(raw_df["Group"].dropna().astype(str).unique().tolist())
  165. with st.expander("Preview loaded data"):
  166. st.caption(f"Rows: {len(raw_df)} | Files: {len(dfs)} | Groups: {len(all_groups)} | Zones: {len(all_zones)}")
  167. st.dataframe(raw_df.head(30), use_container_width=True)
  168. # =========================================================
  169. # UI: configuration
  170. # =========================================================
  171. st.subheader("2) Configure analysis")
  172. c1, c2, c3 = st.columns([2, 2, 2])
  173. with c1:
  174. metric = st.selectbox("Metric to test", options=numeric_cols, index=numeric_cols.index("Enrichment_Factor") if "Enrichment_Factor" in numeric_cols else 0)
  175. with c2:
  176. zones_to_include = st.multiselect(
  177. "Zones to include",
  178. options=all_zones,
  179. default=all_zones,
  180. )
  181. with c3:
  182. # grouping
  183. group_mode = st.selectbox("Group by", options=["Group", "Original_Folder (not available here)"], index=0)
  184. # (we only have Group right now; later you can add more metadata)
  185. group_col = "Group"
  186. if len(zones_to_include) < 2:
  187. st.warning("Select at least 2 zones.")
  188. st.stop()
  189. # order selector: simple but robust
  190. st.markdown("**Plot order (left → right)**")
  191. zone_order = st.multiselect(
  192. "Choose zones in the order you want them displayed",
  193. options=zones_to_include,
  194. default=zones_to_include,
  195. help="The selection order defines the plot order. Remove/re-add to reorder.",
  196. )
  197. # ensure order contains exactly included zones
  198. zone_order = [z for z in zone_order if z in zones_to_include]
  199. missing_in_order = [z for z in zones_to_include if z not in zone_order]
  200. zone_order += missing_in_order # append any missing
  201. control_zone = st.selectbox("Reference (control) zone", options=zone_order, index=0)
  202. st.divider()
  203. st.subheader("3) Statistics setup")
  204. #
  205. # It is critical to select the correct test type based on experimental design.
  206. # Paired (Friedman/Wilcoxon) is used when the SAME cell/slice is measured across zones.
  207. # Independent (Kruskal/Mann-Whitney) is used when samples are distinct/unrelated.
  208. c4, c5, c6 = st.columns([2, 2, 2])
  209. with c4:
  210. do_global = st.toggle("Run global test first?", value=True)
  211. global_test = st.selectbox("Global test", options=["Kruskal–Wallis (independent)", "Friedman (paired)"], index=0, disabled=not do_global)
  212. with c5:
  213. pairwise_test = st.selectbox("Pairwise test (vs control)", options=["Mann–Whitney U (independent)", "Wilcoxon signed-rank (paired)"], index=0)
  214. with c6:
  215. 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)
  216. alpha = st.slider("Alpha (for significance & FDR)", min_value=0.001, max_value=0.20, value=0.05, step=0.001)
  217. # Create analysis dataframe
  218. df = raw_df.copy()
  219. df = df[df["Zone"].isin(zones_to_include)].copy()
  220. df["Metric"] = pd.to_numeric(df[metric], errors="coerce")
  221. # Basic sanity
  222. df = df.dropna(subset=["Zone", group_col, "Metric"])
  223. df["Zone"] = df["Zone"].astype(str)
  224. df[group_col] = df[group_col].astype(str)
  225. if df.empty:
  226. st.error("No data left after filtering. Check zone selection and metric.")
  227. st.stop()
  228. paired_possible = _has_pairing(df, pairing_col, zones_to_include)
  229. # warn/disable if user chose paired tests but pairing isn't valid
  230. paired_global = (global_test.startswith("Friedman") if do_global else False)
  231. paired_pairwise = pairwise_test.startswith("Wilcoxon")
  232. if paired_global or paired_pairwise:
  233. if not paired_possible:
  234. st.warning(f"Paired tests selected, but pairing doesn't look valid with '{pairing_col}'. Consider independent tests.")
  235. else:
  236. # stricter: Friedman requires complete blocks across all zones
  237. # We'll check later per-group too, but give early warning
  238. pass
  239. # =========================================================
  240. # Core stats
  241. # =========================================================
  242. def _global_test(group_df: pd.DataFrame, zones: List[str]) -> Tuple[str, float]:
  243. """
  244. Returns (test_name, p_value).
  245. group_df columns: Zone, Metric, pairing_col possibly
  246. """
  247. if not do_global:
  248. return ("(skipped)", np.nan)
  249. # Kruskal-Wallis: Non-parametric ANOVA for independent samples
  250. if global_test.startswith("Kruskal"):
  251. arrays = []
  252. for z in zones:
  253. vals = group_df.loc[group_df["Zone"] == z, "Metric"].dropna().values
  254. if len(vals) > 0:
  255. arrays.append(vals)
  256. if len(arrays) < 2:
  257. return ("KruskalWallis", np.nan)
  258. stat, p = stats.kruskal(*arrays)
  259. return ("KruskalWallis", float(p))
  260. # Friedman: Non-parametric test for paired samples (repeated measures)
  261. # needs complete pairing rows per group
  262. wide = group_df.pivot_table(index=pairing_col, columns="Zone", values="Metric", aggfunc="mean")
  263. # require all zones columns present
  264. for z in zones:
  265. if z not in wide.columns:
  266. return ("Friedman", np.nan)
  267. wide = wide[zones]
  268. if wide.empty:
  269. return ("Friedman", np.nan)
  270. if wide.isna().any(axis=1).any():
  271. return ("Friedman", np.nan)
  272. stat, p = stats.friedmanchisquare(*[wide[z].values for z in zones])
  273. return ("Friedman", float(p))
  274. def _pairwise_vs_control(group_df: pd.DataFrame, zones: List[str], control: str) -> pd.DataFrame:
  275. """
  276. Returns DF with columns:
  277. Group, Compared_To, Raw_P, Test, N_Control, N_Target
  278. """
  279. out_rows = []
  280. # control values
  281. ref = group_df[group_df["Zone"] == control].copy()
  282. if ref.empty:
  283. return pd.DataFrame(out_rows)
  284. for target in zones:
  285. if target == control:
  286. continue
  287. tgt = group_df[group_df["Zone"] == target].copy()
  288. if tgt.empty:
  289. continue
  290. if pairwise_test.startswith("Mann"):
  291. # Mann-Whitney U: Non-parametric test for independent samples
  292. x = tgt["Metric"].dropna().values
  293. y = ref["Metric"].dropna().values
  294. if len(x) == 0 or len(y) == 0:
  295. p = 1.0
  296. else:
  297. try:
  298. p = float(stats.mannwhitneyu(x, y, alternative="two-sided").pvalue)
  299. except Exception:
  300. p = 1.0
  301. out_rows.append({
  302. "Compared_To": target,
  303. "Test": "MannWhitneyU_vs_Control",
  304. "Raw_P": p,
  305. "N_Control": int(len(y)),
  306. "N_Target": int(len(x)),
  307. })
  308. else:
  309. # Wilcoxon paired: align by pairing_col
  310. # Wilcoxon Signed-Rank: Non-parametric test for paired samples
  311. wide = group_df[group_df["Zone"].isin([control, target])].pivot_table(
  312. index=pairing_col, columns="Zone", values="Metric", aggfunc="mean"
  313. )
  314. if control not in wide.columns or target not in wide.columns:
  315. p = 1.0
  316. n = 0
  317. else:
  318. sub = wide[[control, target]].dropna()
  319. n = int(len(sub))
  320. if n == 0:
  321. p = 1.0
  322. else:
  323. x = sub[target].values
  324. y = sub[control].values
  325. if _wilcoxon_ready(x, y):
  326. try:
  327. p = float(stats.wilcoxon(x, y, alternative="two-sided").pvalue)
  328. except Exception:
  329. p = 1.0
  330. else:
  331. p = 1.0
  332. out_rows.append({
  333. "Compared_To": target,
  334. "Test": "Wilcoxon_vs_Control",
  335. "Raw_P": p,
  336. "N_Control": int(n),
  337. "N_Target": int(n),
  338. })
  339. return pd.DataFrame(out_rows)
  340. def _bh_fdr(pvals: List[float], alpha_: float) -> Tuple[List[bool], List[float]]:
  341. # Benjamini-Hochberg False Discovery Rate correction
  342. if len(pvals) == 0:
  343. return [], []
  344. reject, qvals, _, _ = multipletests(pvals, alpha=alpha_, method="fdr_bh")
  345. return reject.tolist(), qvals.tolist()
  346. # =========================================================
  347. # Plot (matplotlib-only, no seaborn)
  348. # =========================================================
  349. def _plot_violin_box_strip(subset: pd.DataFrame, metric_label: str, zones_order: List[str], control: str, stats_rows: pd.DataFrame) -> Tuple[bytes, bytes]:
  350. """
  351. Returns (pdf_bytes, png_bytes)
  352. stats_rows: rows with Compared_To, FDR_Corrected_P, Significant
  353. """
  354. #
  355. # This visualization combines three layers:
  356. # 1. Violin Plot: Shows the probability density of the data.
  357. # 2. Box Plot: Shows quartiles (median, 25%, 75%).
  358. # 3. Strip Plot (Jitter): Shows individual data points.
  359. # Prepare data in order
  360. data_by_zone = []
  361. for z in zones_order:
  362. vals = subset.loc[subset["Zone"] == z, "Metric"].dropna().values
  363. data_by_zone.append(vals)
  364. fig = plt.figure(figsize=(8.0, 7.5))
  365. ax = plt.gca()
  366. # Violin
  367. parts = ax.violinplot(
  368. dataset=data_by_zone,
  369. showmeans=False,
  370. showmedians=False,
  371. showextrema=False,
  372. )
  373. # outlines
  374. for pc in parts["bodies"]:
  375. pc.set_alpha(0.4)
  376. pc.set_linewidth(1.2)
  377. pc.set_edgecolor("black")
  378. # Boxplot
  379. bp = ax.boxplot(
  380. data_by_zone,
  381. widths=0.25,
  382. showfliers=False,
  383. patch_artist=True,
  384. )
  385. for box in bp["boxes"]:
  386. box.set_alpha(0.5)
  387. box.set_linewidth(1.5)
  388. box.set_edgecolor("black")
  389. for k in ["whiskers", "caps", "medians"]:
  390. for line in bp[k]:
  391. line.set_linewidth(1.5)
  392. line.set_color("black")
  393. # Strip (jitter)
  394. rng = np.random.default_rng(0)
  395. for i, vals in enumerate(data_by_zone, start=1):
  396. if len(vals) == 0:
  397. continue
  398. # Add jitter to x-axis to separate points
  399. x = rng.normal(loc=i, scale=0.06, size=len(vals))
  400. ax.scatter(x, vals, s=35, edgecolors="black", linewidths=0.6, alpha=0.55)
  401. ax.set_xticks(range(1, len(zones_order) + 1))
  402. ax.set_xticklabels([z.replace("_", " ") for z in zones_order], rotation=45, ha="right")
  403. ax.set_ylabel(metric_label, fontsize=14)
  404. ax.set_xlabel("")
  405. ax.grid(axis="y", linestyle="--", alpha=0.35)
  406. # significance lines: control vs significant targets
  407. try:
  408. z_to_x = {z: i + 1 for i, z in enumerate(zones_order)}
  409. x1 = z_to_x.get(control, None)
  410. if x1 is not None and not stats_rows.empty:
  411. y_max = float(np.nanmax(subset["Metric"].values)) if len(subset) else 1.0
  412. y_min = float(np.nanmin(subset["Metric"].values)) if len(subset) else 0.0
  413. y_range = max(1e-9, (y_max - y_min))
  414. step = y_range * 0.10
  415. y = y_max + y_range * 0.05
  416. sig = stats_rows[stats_rows["Significant"] == True].copy()
  417. # stable order: as zones_order
  418. sig["__order"] = sig["Compared_To"].apply(lambda z: zones_order.index(z) if z in zones_order else 9999)
  419. sig = sig.sort_values("__order")
  420. for _, r in sig.iterrows():
  421. tgt = str(r["Compared_To"])
  422. if tgt not in z_to_x:
  423. continue
  424. x2 = z_to_x[tgt]
  425. q = float(r["FDR_Corrected_P"]) if pd.notna(r["FDR_Corrected_P"]) else float("nan")
  426. h = step * 0.3
  427. ax.plot([x1, x1, x2, x2], [y - h, y, y, y - h], lw=1.5, c="black")
  428. 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"
  429. ax.text((x1 + x2) / 2, y + h * 0.35, label, ha="center", va="bottom", fontsize=11)
  430. y += step
  431. ax.set_ylim(top=y + step * 0.4)
  432. except Exception:
  433. pass
  434. plt.tight_layout()
  435. pdf_bytes = _as_pdf_bytes(fig)
  436. # Need a fresh fig for PNG (since pdf closes fig)
  437. # So rebuild quickly: easiest is to render again
  438. # For simplicity, regenerate figure by calling function logic again is heavy.
  439. # We'll just create a new one the same way:
  440. fig2 = plt.figure(figsize=(8.0, 7.5))
  441. ax2 = plt.gca()
  442. parts2 = ax2.violinplot(data_by_zone, showmeans=False, showmedians=False, showextrema=False)
  443. for pc in parts2["bodies"]:
  444. pc.set_alpha(0.4)
  445. pc.set_linewidth(1.2)
  446. pc.set_edgecolor("black")
  447. bp2 = ax2.boxplot(data_by_zone, widths=0.25, showfliers=False, patch_artist=True)
  448. for box in bp2["boxes"]:
  449. box.set_alpha(0.5); box.set_linewidth(1.5); box.set_edgecolor("black")
  450. for k in ["whiskers", "caps", "medians"]:
  451. for line in bp2[k]:
  452. line.set_linewidth(1.5); line.set_color("black")
  453. rng2 = np.random.default_rng(0)
  454. for i, vals in enumerate(data_by_zone, start=1):
  455. if len(vals) == 0:
  456. continue
  457. x = rng2.normal(loc=i, scale=0.06, size=len(vals))
  458. ax2.scatter(x, vals, s=35, edgecolors="black", linewidths=0.6, alpha=0.55)
  459. ax2.set_xticks(range(1, len(zones_order) + 1))
  460. ax2.set_xticklabels([z.replace("_", " ") for z in zones_order], rotation=45, ha="right")
  461. ax2.set_ylabel(metric_label, fontsize=14)
  462. ax2.set_xlabel("")
  463. ax2.grid(axis="y", linestyle="--", alpha=0.35)
  464. # significance lines on fig2 as well
  465. try:
  466. z_to_x = {z: i + 1 for i, z in enumerate(zones_order)}
  467. x1 = z_to_x.get(control, None)
  468. if x1 is not None and not stats_rows.empty:
  469. y_max = float(np.nanmax(subset["Metric"].values)) if len(subset) else 1.0
  470. y_min = float(np.nanmin(subset["Metric"].values)) if len(subset) else 0.0
  471. y_range = max(1e-9, (y_max - y_min))
  472. step = y_range * 0.10
  473. y = y_max + y_range * 0.05
  474. sig = stats_rows[stats_rows["Significant"] == True].copy()
  475. sig["__order"] = sig["Compared_To"].apply(lambda z: zones_order.index(z) if z in zones_order else 9999)
  476. sig = sig.sort_values("__order")
  477. for _, r in sig.iterrows():
  478. tgt = str(r["Compared_To"])
  479. if tgt not in z_to_x:
  480. continue
  481. x2 = z_to_x[tgt]
  482. q = float(r["FDR_Corrected_P"]) if pd.notna(r["FDR_Corrected_P"]) else float("nan")
  483. h = step * 0.3
  484. ax2.plot([x1, x1, x2, x2], [y - h, y, y, y - h], lw=1.5, c="black")
  485. 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"
  486. ax2.text((x1 + x2) / 2, y + h * 0.35, label, ha="center", va="bottom", fontsize=11)
  487. y += step
  488. ax2.set_ylim(top=y + step * 0.4)
  489. except Exception:
  490. pass
  491. plt.tight_layout()
  492. png_bytes = _as_png_bytes(fig2)
  493. return pdf_bytes, png_bytes
  494. # =========================================================
  495. # Run
  496. # =========================================================
  497. st.subheader("4) Run")
  498. run_btn = st.button("🚀 Run analysis", type="primary", use_container_width=True)
  499. if not run_btn:
  500. st.stop()
  501. # Prepare outputs
  502. summary_rows = []
  503. plot_files: Dict[str, Dict[str, bytes]] = {} # group -> {"pdf":..., "png":...}
  504. groups = sorted(df[group_col].unique().tolist())
  505. for g in groups:
  506. gdf = df[df[group_col] == g].copy()
  507. # Ensure control exists
  508. if control_zone not in gdf["Zone"].unique():
  509. summary_rows.append({
  510. "Group": g,
  511. "Stage": "Global",
  512. "Test": "(skipped)",
  513. "Reference": control_zone,
  514. "Compared_To": "ALL",
  515. "Raw_P": np.nan,
  516. "FDR_Corrected_P": np.nan,
  517. "Significant": False,
  518. "Note": "Missing control zone in this group"
  519. })
  520. continue
  521. # GLOBAL
  522. test_name, p_global = _global_test(gdf, zone_order)
  523. summary_rows.append({
  524. "Group": g,
  525. "Stage": "Global",
  526. "Test": test_name,
  527. "Reference": control_zone,
  528. "Compared_To": "ALL",
  529. "Raw_P": p_global,
  530. "FDR_Corrected_P": np.nan,
  531. "Significant": bool(np.isfinite(p_global) and p_global < alpha) if do_global else False,
  532. "Note": ""
  533. })
  534. # decide posthoc condition
  535. run_posthoc = True
  536. if do_global:
  537. run_posthoc = bool(np.isfinite(p_global) and p_global < alpha)
  538. # PAIRWISE vs control
  539. pw = _pairwise_vs_control(gdf, zone_order, control_zone)
  540. if pw.empty:
  541. continue
  542. if not run_posthoc:
  543. # still record as "not run due to global ns"
  544. for _, r in pw.iterrows():
  545. summary_rows.append({
  546. "Group": g,
  547. "Stage": "PostHoc",
  548. "Test": r["Test"],
  549. "Reference": control_zone,
  550. "Compared_To": r["Compared_To"],
  551. "Raw_P": np.nan,
  552. "FDR_Corrected_P": np.nan,
  553. "Significant": False,
  554. "Note": "Post hoc skipped (global not significant or global skipped off)"
  555. })
  556. continue
  557. pvals = pw["Raw_P"].astype(float).tolist()
  558. reject, qvals = _bh_fdr(pvals, alpha)
  559. pw["FDR_Corrected_P"] = qvals
  560. pw["Significant"] = reject
  561. pw["Group"] = g
  562. for _, r in pw.iterrows():
  563. summary_rows.append({
  564. "Group": g,
  565. "Stage": "PostHoc",
  566. "Test": r["Test"],
  567. "Reference": control_zone,
  568. "Compared_To": r["Compared_To"],
  569. "Raw_P": float(r["Raw_P"]),
  570. "FDR_Corrected_P": float(r["FDR_Corrected_P"]),
  571. "Significant": bool(r["Significant"]),
  572. "Note": ""
  573. })
  574. # Plot for this group
  575. pdf_bytes, png_bytes = _plot_violin_box_strip(
  576. subset=gdf,
  577. metric_label=metric,
  578. zones_order=zone_order,
  579. control=control_zone,
  580. stats_rows=pw[["Compared_To", "FDR_Corrected_P", "Significant"]].copy(),
  581. )
  582. plot_files[g] = {"pdf": pdf_bytes, "png": png_bytes}
  583. summary_df = pd.DataFrame(summary_rows)
  584. st.success("Done.")
  585. st.subheader("Results")
  586. cA, cB = st.columns([2, 1])
  587. with cA:
  588. st.dataframe(summary_df, use_container_width=True)
  589. with cB:
  590. # Download summary CSV
  591. csv_bytes = summary_df.to_csv(index=False).encode("utf-8")
  592. st.download_button(
  593. "⬇️ Download summary CSV",
  594. data=csv_bytes,
  595. file_name="Stats_Summary.csv",
  596. mime="text/csv",
  597. use_container_width=True,
  598. )
  599. st.divider()
  600. st.subheader("Plots (per Group)")
  601. if not plot_files:
  602. st.info("No plots were generated (maybe missing control zone or no significant posthoc).")
  603. else:
  604. for g, files in plot_files.items():
  605. st.markdown(f"### {g}")
  606. c1, c2, c3 = st.columns([2, 1, 1])
  607. with c1:
  608. st.image(files["png"], caption=f"{g} — {metric}", use_container_width=True)
  609. with c2:
  610. st.download_button(
  611. "⬇️ PDF",
  612. data=files["pdf"],
  613. file_name=f"ViolinBox_{g}_RefComparison.pdf".replace(" ", "_"),
  614. mime="application/pdf",
  615. use_container_width=True,
  616. )
  617. with c3:
  618. st.download_button(
  619. "⬇️ PNG",
  620. data=files["png"],
  621. file_name=f"ViolinBox_{g}_RefComparison.png".replace(" ", "_"),
  622. mime="image/png",
  623. use_container_width=True,
  624. )

2_Analysis.py at commit a92a869, under MIT · at the source

Overview

Authors: Szabina Furdan1,2, Abdennour Douida1, Emoke Bakos1,2, Ádám Tiszlavicz1, Kolos Nemes3,4, Lőrinc Sándor Pongor3, Krisztián Péli1,2, Gabor Molnar5, Gabor Tamas5, Daphne Welter6, Jonathan Landry6, Bálint H Kovács7, Miklós Erdélyi7, Balazs Bende8, Gabor Hutoczki9, Attila Papp10, Pal Barzo11, Vladimir Benes6, Attila Szucs12, Viktor Szegedi1,2, Karri Lamsa1,2
  1. Hungarian Center of Excellence for Molecular Medicine Research Group for Human neuron physiology and therapy, Szeged, Hungary
  2. Department of Physiology, Anatomy and Neuroscience, University of Szeged, Szeged, Hungary
  3. Hungarian Center of Excellence for Molecular Medicine Research Group for Cancer Genomics and Epigenetics, Szeged, Hungary
  4. Doctoral School of Experimental and Preventive Medicine, University of Szeged, Szeged, Hungary
  5. ELKH-SZTE Research Group for Cortical Microcircuits, Department of Physiology, Anatomy and Neuroscience, University of Szeged, Szeged, Hungary
  6. European Molecular Biology Laboratory, Heidelberg, Germany
  7. Department of Optics and Quantum Electronics, University of Szeged, Szeged, Hungary
  8. Hungarian Centre of Excellence for Molecular Medicine Research Group for Translational Medicine Development, Szeged, Hungary
  9. Department of Neurosurgery, University of Debrecen Clinical Centre, Debrecen, Hungary
  10. Department of Neurosurgery, Borsod County Hospital, Miskolc, Hungary
  11. Department of Neurosurgery, University of Szeged, Szeged, Hungary
  12. Neuronal Cell Biology Research Group, Eötvös Loránd University, Budapest, Hungary
Journal: Communications biology, volume 9, issue 1, article 806
Dates: received 9 April 2025; accepted 2 April 2026; published online 13 April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s42003-026-10063-9 · PMID 41974795 · PMCID PMC13265741 · OpenAlex W7154068342
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: intracellular / patch clamp (modality), human (organism), mouse (organism), cellular / molecular (subfield)
Methods: Statistics, Connectivity, Machine learning
Keywords: Ion channels in the nervous system, Cellular neuroscience
MeSH: Interneurons*, Neocortex*, Neurons*, Parvalbumins*, Potassium Channels, Inwardly Rectifying*, Animals, Humans, Membrane Potentials, Mice, Patch-Clamp Techniques, Species Specificity (* major topic)
Topic: Ion channel regulation and function (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: Hungarian Science Foundation (2022-2.1.1-NL-2022-00005, ANN-135291); Hungarian Academy of Sciences | Magyar Tudományos Akadémia Számítástechnikai és Automatizálási Kutatóintézet (ADVANCED_150382, OTKA K_134279)
Citations: not cited yet (Europe PMC); 68 references in the paper
Research resources: RRID:SCR_024672

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

License: MIT
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: a92a86948e048c2ef1c0efd33cc2be2067328517, 15 January 2026
Languages: Python (11)
Size: 15 files, 11 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (9 files), Plotly (4 files), SciPy (4 files), Cellpose (2 files), Matplotlib (2 files), pandas (2 files), scikit-image (2 files), napari (1 file), Pillow (1 file), scikit-learn (1 file), statsmodels (1 file), tifffile (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
13 files

APPLICATION

License: none: the authors keep all their rights
State: the link answers, verified on 29 September 2026
Evidence: the link answers
Software Heritage: not checked
Found in: “Data availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 29 September 2026: the link answers (HTTP 200)
  • 29 September 2026: the link answers (HTTP 200)
At the source: github.com/APPLICATION

Code availability

Custom analysis scripts used in this work are available in a publicly accessible repository; (ZoneSig) (https://github.com/HCEMM/ZoneSig). Software versions and full dependency list are in the accompanying code repository (github.com/APPLICATION).

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://github.com/HCEMM/ZoneSig). Software versions and full dependency list are in the accompanying code repository (github.com/APPLICATION).

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://github.com/HCEMM/ZoneSig) and in the accompanying code repository (github.com/APPLICATION). All other data are available from the corresponding author upon reasonable request.Source data supporting the findings of this study are provided with the figures in the manuscript.

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://doi.org/10.1038/s42003-026-10063-9

BibTeX

@article{furdan2026conserved,
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/s42003-026-10063-9},
url = {https://doi.org/10.1038/s42003-026-10063-9},
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/04/13
VL - 9
IS - 1
SP - 806
SN - 2399-3642
PB - Nature Publishing Group
DO - 10.1038/s42003-026-10063-9
UR - https://doi.org/10.1038/s42003-026-10063-9
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s42003-026-10063-9",
"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": "Commun Biol",
"volume": "9",
"issue": "1",
"page": "806",
"DOI": "10.1038/s42003-026-10063-9",
"PMID": "41974795",
"PMCID": "PMC13265741",
"ISSN": "2399-3642",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s42003-026-10063-9",
"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 biology
In 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: Nature
In 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: EBioMedicine
In 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 neuroscience
In 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 neuroscience
In common: intracellular / patch clamp, cellular / molecular, 7 references
[7] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In 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 reports
In 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 advances
In 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 America
In 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.

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.