Anticipatory slow potentials before auditory feedback show posterior predominance but limited condition effects in speech-in-noise.
The 5 matches
- [1] § Materials and methods › Statistical analysis ↔ python/run_spn_cnv_analysis.py, lines 741–799 · score 0.93 · Bayesian mixed, ROI interaction models, approximate Bayes factors, hemisphere model, central ROI, ROI terms
- [2] § Results › Primary SPN ROI × hemisphere analysis ↔ python/run_spn_cnv_analysis.py, lines 741–799 · score 0.81 · ROI interaction model, Bayes factor, central ROI, ROI terms, primary model, primary ROI
- [3] § Materials and methods › Statistical analysis ↔ python/run_spn_cnv_analysis.py, lines 842–895 · score 0.62 · noise minus silence, exploratory spatial, response locked, cluster, permutation, ROIs
- [4] § Results › Descriptive sensor-space localization of the SPN condition effect ↔ python/run_spn_cnv_analysis.py, lines 842–895 · score 0.61 · noise minus silence, descriptive ROI, cluster, permutation, localization, exploratory
- [5] § Materials and methods › EEG recording and preprocessing ↔ matlab/stage1_prepare_ica.m, lines 266–292 · score 0.57 · ICA weights, transferred, EEGLAB, channels
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 · 1,065 lines · 41 KB · MIT · 4 matches
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- run_spn_cnv_analysis.py
- Desktop-oriented analysis script for SPN/CNV EEG data.
- This script:
- 1. Loads non-interpolated trial-wise long-format EEG data
- 2. Optionally loads electrode metadata and run-level QC tables
- 3. Applies primary-analysis filtering
- 4. Runs ROI × Hemisphere mixed-effects models for SPN and CNV
- 5. Optionally fits supplementary models with Sex
- 6. Runs electrode-wise mixed-effects models with FDR correction
- 7. Adds reviewer-requested ROI-interaction models
- 8. Adds BIC-approximated Bayes factors for condition-related terms
- 9. Runs cluster-based permutation tests on participant-level difference maps
- 10. Generates descriptive topographic and ROI-panel figures
- 11. Optionally summarizes speech-in-noise and time-estimation performance and
- computes exploratory correlations with EEG measures
- Inputs
- ------
- Required:
- anticipatory_electrode_long_noninterp.csv
- Optional:
- electrode_metadata.csv
- run_qc.csv
- speech_accuracy.csv
- time_estimation.csv or timing.csv
- Outputs
- -------
- Tables and figures are written to the output directory.
- """
- import warnings
- import argparse
- from pathlib import Path
- import numpy as np
- import pandas as pd
- import matplotlib
- matplotlib.use("Agg")
- import matplotlib.pyplot as plt
- from scipy.stats import norm, t as student_t, pearsonr, spearmanr, ttest_rel
- from statsmodels.formula.api import mixedlm
- from statsmodels.stats.multitest import multipletests
- import mne
- warnings.filterwarnings("ignore")
- def find_input_file(input_dir: Path, target_name: str) -> Path:
- matches = sorted(input_dir.glob(f"*{target_name}*"))
- files = [p for p in matches if p.is_file()]
- if not files:
- raise FileNotFoundError(f"No file containing '{target_name}' was found in {input_dir}")
- return files[0]
- def ensure_dir(path: Path) -> None:
- path.mkdir(parents=True, exist_ok=True)
- def save_table(df: pd.DataFrame, path: Path) -> None:
- df.to_csv(path, index=False)
- print(f"[Saved] {path}")
- def fe_table(res):
- fe = res.fe_params
- se = res.bse_fe
- z = fe / se
- p = 2 * norm.sf(np.abs(z))
- ci_low = fe - 1.96 * se
- ci_high = fe + 1.96 * se
- out = (
- pd.DataFrame({
- "b": fe,
- "SE": se,
- "z": z,
- "p": p,
- "CI_low": ci_low,
- "CI_high": ci_high
- })
- .reset_index()
- .rename(columns={"index": "Term"})
- )
- return out
- def try_fit_mixed(formula, data, re="~Condition_c", group="Participant"):
- kind = "random-slope"
- try:
- res = mixedlm(
- formula,
- data=data,
- groups=data[group],
- re_formula=re
- ).fit(method="lbfgs", reml=True)
- return res, kind
- except Exception:
- kind = "random-intercept"
- res = mixedlm(
- formula,
- data=data,
- groups=data[group]
- ).fit(method="lbfgs", reml=True)
- return res, kind
- def try_fit_mixed_ml(formula, data, re="~Condition_c", group="Participant"):
- """Fit a mixed model with ML, used for BIC-based model comparisons."""
- kind = "random-slope"
- try:
- res = mixedlm(
- formula,
- data=data,
- groups=data[group],
- re_formula=re
- ).fit(method="lbfgs", reml=False)
- return res, kind
- except Exception:
- kind = "random-intercept"
- res = mixedlm(
- formula,
- data=data,
- groups=data[group]
- ).fit(method="lbfgs", reml=False)
- return res, kind
- def bic_bayes_factor(null_res, full_res):
- """Return BIC-approximated BF10 and BF01 for full vs null model."""
- bf10 = float(np.exp((null_res.bic - full_res.bic) / 2.0))
- bf01 = float(1.0 / bf10) if bf10 != 0 else np.inf
- return bf10, bf01
- def run_bic_bayes_comparison(data, component_label, comparison_label,
- null_formula, full_formula, out_rows,
- re="~Condition_c", group="Participant"):
- """Fit null/full ML mixed models and append a BIC Bayes-factor summary row."""
- null_res, null_kind = try_fit_mixed_ml(null_formula, data, re=re, group=group)
- full_res, full_kind = try_fit_mixed_ml(full_formula, data, re=re, group=group)
- bf10, bf01 = bic_bayes_factor(null_res, full_res)
- out_rows.append({
- "Component": component_label,
- "Comparison": comparison_label,
- "Null_formula": null_formula,
- "Full_formula": full_formula,
- "Null_random_effects": null_kind,
- "Full_random_effects": full_kind,
- "N_observations": int(full_res.nobs),
- "Null_BIC": float(null_res.bic),
- "Full_BIC": float(full_res.bic),
- "Delta_BIC_full_minus_null": float(full_res.bic - null_res.bic),
- "BF10_full_over_null": bf10,
- "BF01_null_over_full": bf01
- })
- def ids_with_two_levels(df, id_col, factor_col):
- counts = df.groupby(id_col)[factor_col].nunique()
- return counts[counts >= 2].index.tolist()
- def encode_condition(series, pos="in silence", neg="in noise"):
- s = series.astype(str).str.strip().str.lower()
- s = s.replace({"noise": "in noise", "silence": "in silence"})
- mapping = {str(pos).lower(): 0.5, str(neg).lower(): -0.5}
- return s.map(mapping).astype(float)
- def guess_roi(ch):
- chU = str(ch).upper()
- if chU.startswith("F"):
- return "Frontal"
- if chU.startswith("C"):
- return "Central"
- if chU.startswith("P"):
- return "Parietal"
- if chU.startswith("O"):
- return "Occipital"
- if chU.startswith("T"):
- return "Temporal"
- return "Other"
- def guess_hemi_from_name(ch):
- ch = str(ch).strip()
- if ch.endswith(("z", "Z")):
- return "Midline"
- mp = {"T3": "Left", "T5": "Left", "T7": "Left",
- "T4": "Right", "T6": "Right", "T8": "Right"}
- if ch in mp:
- return mp[ch]
- if len(ch) > 0 and ch[-1].isdigit():
- return "Left" if int(ch[-1]) % 2 == 1 else "Right"
- return "Midline"
- def build_meta_from_spn(spn):
- elec = sorted(spn["Electrode"].astype(str).str.strip().unique())
- meta = pd.DataFrame({"Electrode": elec})
- meta["ROI"] = meta["Electrode"].map(guess_roi)
- meta["Hemisphere"] = meta["Electrode"].map(guess_hemi_from_name)
- return meta
- def prepare_meta_safe(meta, df):
- meta = meta.copy()
- meta.columns = [c.strip() for c in meta.columns]
- if "Electrode" not in meta.columns:
- raise ValueError("electrode_metadata.csv must contain an 'Electrode' column.")
- meta["Electrode"] = meta["Electrode"].astype(str).str.strip()
- all_elec = pd.DataFrame({
- "Electrode": sorted(df["Electrode"].astype(str).str.strip().unique())
- })
- meta = all_elec.merge(meta, on="Electrode", how="left")
- if "ROI" not in meta.columns or meta["ROI"].isna().any():
- aux = build_meta_from_spn(df)[["Electrode", "ROI"]]
- meta = meta.drop(columns=[c for c in ["ROI"] if c in meta.columns], errors="ignore")
- meta = meta.merge(aux, on="Electrode", how="left")
- if "Hemisphere" not in meta.columns or meta["Hemisphere"].isna().any():
- aux = build_meta_from_spn(df)[["Electrode", "Hemisphere"]]
- meta = meta.drop(columns=[c for c in ["Hemisphere"] if c in meta.columns], errors="ignore")
- meta = meta.merge(aux, on="Electrode", how="left")
- meta["Hemisphere"] = meta["Hemisphere"].fillna(meta["Electrode"].map(guess_hemi_from_name))
- return meta[["Electrode", "ROI", "Hemisphere"]]
- def save_fig(fig, path: Path, dpi=300, tight=True):
- fig.savefig(
- path,
- dpi=dpi,
- bbox_inches="tight" if tight else None,
- facecolor="white"
- )
- plt.close(fig)
- print(f"[Saved] {path}")
- def plot_roi_condition_panels(desc_roi, out_path: Path,
- roi_order=None,
- title="Mean anticipatory activity by ROI and condition"):
- if roi_order is None:
- roi_order = ["Frontal", "Central", "Parietal", "Occipital"]
- roi_order = [r for r in roi_order if r in desc_roi["ROI"].unique()]
- cond_order = ["in silence", "in noise"]
- n_panels = len(roi_order)
- fig, axes = plt.subplots(1, n_panels, figsize=(3.2 * n_panels, 4.2), sharey=True)
- if n_panels == 1:
- axes = [axes]
- rng = np.random.default_rng(42)
- for ax, roi in zip(axes, roi_order):
- wide = (
- desc_roi[desc_roi["ROI"] == roi]
- .pivot_table(index="Participant", columns="Condition", values="Mean_uV", aggfunc="mean")
- .reindex(columns=cond_order)
- )
- x = [0, 1]
- for _, row in wide.iterrows():
- if row.notna().all():
- ax.plot(x, row.values, color="0.75", linewidth=1.0, alpha=0.8, zorder=1)
- for i, cond in enumerate(cond_order):
- vals = wide[cond].dropna().values
- jitter = rng.normal(loc=0.0, scale=0.035, size=len(vals))
- ax.scatter(np.full(len(vals), i) + jitter, vals, s=26, color="black", alpha=0.85, zorder=2)
- if len(vals) >= 2:
- mu = np.mean(vals)
- se = np.std(vals, ddof=1) / np.sqrt(len(vals))
- tcrit = student_t.ppf(1 - 0.05 / 2, len(vals) - 1)
- ci_lo = mu - tcrit * se
- ci_hi = mu + tcrit * se
- ax.hlines(mu, i - 0.18, i + 0.18, color="black", linewidth=2.0, zorder=3)
- ax.vlines(i, ci_lo, ci_hi, color="black", linewidth=1.5, zorder=3)
- ax.set_xticks(x)
- ax.set_xticklabels(["silence", "noise"])
- ax.set_title(roi, fontsize=12)
- ax.spines["top"].set_visible(False)
- ax.spines["right"].set_visible(False)
- ax.tick_params(axis="both", labelsize=10)
- ax.set_xlim(-0.35, 1.35)
- axes[0].set_ylabel("Mean amplitude (µV)", fontsize=12)
- fig.suptitle(title, fontsize=14, y=1.02)
- save_fig(fig, out_path, dpi=300, tight=True)
- def standardize_channel_names(ch_names):
- """Map legacy 10-20 temporal labels to MNE standard_1020 names for plotting/adjacency."""
- rename_map = {"T3": "T7", "T4": "T8", "T5": "P7", "T6": "P8"}
- return [rename_map.get(str(ch), str(ch)) for ch in ch_names]
- def make_topomap_from_condition(mean_cond_df, condition_label, title, out_path: Path, vlim=None):
- """Draw a topomap. Pass vlim=(low, high) to force a common color scale."""
- tmp = mean_cond_df[mean_cond_df["Condition"] == condition_label].copy()
- tmp = tmp.sort_values("Electrode")
- ch_names = tmp["Electrode"].astype(str).tolist()
- ch_names_plot = standardize_channel_names(ch_names)
- info = mne.create_info(ch_names=ch_names_plot, sfreq=500, ch_types="eeg")
- montage = mne.channels.make_standard_montage("standard_1020")
- info.set_montage(montage)
- data_v = (tmp["Mean_uV"].values * 1e-6).reshape(-1, 1)
- evk = mne.EvokedArray(data_v, info, tmin=0)
- plot_kwargs = dict(
- times=[0],
- time_format="",
- scalings=dict(eeg=1e6),
- units=dict(eeg="µV"),
- show=False
- )
- if vlim is not None:
- plot_kwargs["vlim"] = vlim
- try:
- fig = evk.plot_topomap(**plot_kwargs)
- except TypeError:
- if vlim is not None:
- plot_kwargs.pop("vlim", None)
- plot_kwargs["vmin"], plot_kwargs["vmax"] = vlim
- fig = evk.plot_topomap(**plot_kwargs)
- fig.suptitle(title)
- save_fig(fig, out_path, dpi=300, tight=True)
- def build_roi_trial(df, meta):
- x = df.merge(meta, on="Electrode", how="left").copy()
- x["Hemisphere"] = x["Hemisphere"].fillna(x["Electrode"].map(guess_hemi_from_name))
- x["Condition_c"] = encode_condition(x["Condition"], pos="in silence", neg="in noise")
- x["Hem_c"] = x["Hemisphere"].map({"Right": -0.5, "Left": 0.5, "Midline": 0.0}).fillna(0.0).astype(float)
- roi_trial = (
- x.groupby(["Participant", "RunID", "Trial", "Condition", "ROI", "Hemisphere"], as_index=False)
- .agg(
- Mean_uV=("Value_uV", "mean"),
- n_chan_used=("Electrode", "nunique")
- )
- )
- roi_total = (
- meta.groupby(["ROI", "Hemisphere"], as_index=False)
- .agg(n_chan_total=("Electrode", "nunique"))
- )
- roi_trial = roi_trial.merge(roi_total, on=["ROI", "Hemisphere"], how="left")
- roi_trial["prop_chan_used"] = roi_trial["n_chan_used"] / roi_trial["n_chan_total"]
- roi_trial = roi_trial[roi_trial["prop_chan_used"] >= 0.5].copy()
- roi_trial["Condition_c"] = encode_condition(roi_trial["Condition"], pos="in silence", neg="in noise")
- roi_trial["Hem_c"] = roi_trial["Hemisphere"].map({"Right": -0.5, "Left": 0.5, "Midline": 0.0}).fillna(0.0).astype(float)
- return x, roi_trial
- def fit_with_sex(roi_df, long_df, out_csv: Path):
- if "Sex" not in long_df.columns or long_df["Sex"].isna().all():
- return None
- aux = long_df[["Participant", "RunID", "Trial", "Sex"]].drop_duplicates()
- x = roi_df.merge(aux, on=["Participant", "RunID", "Trial"], how="left")
- x["Sex"] = x["Sex"].astype(str)
- x["Sex_c"] = x["Sex"].map({"F": -0.5, "M": 0.5})
- x = x[x["Sex_c"].notna()].copy()
- if len(x) == 0:
- return None
- formula = "Mean_uV ~ Condition_c * Hem_c + C(ROI) + Sex_c + Condition_c:Sex_c"
- res, kind = try_fit_mixed(formula, x, re="~Condition_c", group="Participant")
- tbl = fe_table(res)
- save_table(tbl, out_csv)
- return tbl, kind
- def electrodewise_lmm(df, out_csv: Path):
- rows = []
- for elec, df_e in df.groupby("Electrode"):
- try:
- model_e = mixedlm(
- "Value_uV ~ Condition_c",
- data=df_e.assign(Condition_c=encode_condition(df_e["Condition"])),
- groups=df_e["Participant"],
- re_formula="~Condition_c"
- ).fit(method="lbfgs", reml=True)
- b = float(model_e.fe_params.get("Condition_c", np.nan))
- se = float(model_e.bse_fe.get("Condition_c", np.nan))
- z = b / se if np.isfinite(se) and se > 0 else np.nan
- p = 2 * norm.sf(abs(z)) if np.isfinite(z) else np.nan
- rows.append((elec, b, se, z, p))
- except Exception:
- rows.append((elec, np.nan, np.nan, np.nan, np.nan))
- tbl = pd.DataFrame(rows, columns=["Electrode", "b_Condition", "SE", "z", "p"])
- mask = tbl["p"].notna()
- tbl.loc[mask, "p_FDR"] = multipletests(tbl.loc[mask, "p"], method="fdr_bh")[1]
- tbl = tbl.sort_values("p")
- save_table(tbl, out_csv)
- return tbl
- def roi_desc(roi_df):
- return (
- roi_df.groupby(["Participant", "Condition", "ROI"], as_index=False)
- .agg(Mean_uV=("Mean_uV", "mean"))
- )
- def cluster_permutation_difference(long_df, out_csv: Path, component_label="SPN",
- n_permutations=5000, seed=42):
- """Cluster-based permutation test on participant-level noise-minus-silence maps.
- The test uses only electrodes with complete paired data across participants.
- This avoids interpolation/imputation and keeps the result conservative for a
- low-density, non-interpolated montage.
- """
- mean_cond = (
- long_df.groupby(["Participant", "Electrode", "Condition"], as_index=False)
- .agg(Mean_uV=("Value_uV", "mean"))
- )
- wide = mean_cond.pivot_table(
- index=["Participant", "Electrode"],
- columns="Condition",
- values="Mean_uV"
- ).reset_index()
- required = {"in noise", "in silence"}
- if not required.issubset(wide.columns):
- tbl = pd.DataFrame([{
- "Component": component_label,
- "Note": "Skipped: both in noise and in silence conditions were not available."
- }])
- save_table(tbl, out_csv)
- return tbl
- wide["Diff_uV"] = wide["in noise"] - wide["in silence"]
- mat = wide.pivot(index="Participant", columns="Electrode", values="Diff_uV")
- mat = mat.dropna(axis=1, how="any").dropna(axis=0, how="any")
- if mat.shape[0] < 3 or mat.shape[1] < 2:
- tbl = pd.DataFrame([{
- "Component": component_label,
- "Note": f"Skipped: insufficient complete paired data for cluster test (n={mat.shape[0]}, electrodes={mat.shape[1]})."
- }])
- save_table(tbl, out_csv)
- return tbl
- elec = list(mat.columns)
- ch_plot = standardize_channel_names(elec)
- info = mne.create_info(ch_names=ch_plot, sfreq=500, ch_types="eeg")
- info.set_montage(mne.channels.make_standard_montage("standard_1020"))
- adjacency, _ = mne.channels.find_ch_adjacency(info, ch_type="eeg")
- X = mat.to_numpy(dtype=float)
- t_obs, clusters, cluster_p, _ = mne.stats.permutation_cluster_1samp_test(
- X,
- n_permutations=n_permutations,
- threshold=None,
- tail=0,
- adjacency=adjacency,
- out_type="indices",
- seed=seed,
- verbose=False
- )
- rows = []
- for i, (clu, pval) in enumerate(zip(clusters, cluster_p), start=1):
- if isinstance(clu, tuple):
- inds = np.asarray(clu[0], dtype=int)
- else:
- inds = np.where(np.asarray(clu))[0]
- if inds.size == 0:
- continue
- stat = float(np.sum(t_obs[inds]))
- polarity = "positive" if stat > 0 else "negative"
- rows.append({
- "Component": component_label,
- "Cluster": i,
- "Polarity": polarity,
- "Electrodes": ";".join([elec[j] for j in inds]),
- "N_participants": int(mat.shape[0]),
- "N_electrodes_in_test": int(mat.shape[1]),
- "Cluster_stat_sum_t": stat,
- "Cluster_p": float(pval)
- })
- if not rows:
- rows.append({
- "Component": component_label,
- "Cluster": np.nan,
- "Polarity": "none",
- "Electrodes": "",
- "N_participants": int(mat.shape[0]),
- "N_electrodes_in_test": int(mat.shape[1]),
- "Cluster_stat_sum_t": np.nan,
- "Cluster_p": np.nan
- })
- tbl = pd.DataFrame(rows).sort_values("Cluster_p", na_position="last")
- save_table(tbl, out_csv)
- return tbl
- def find_timing_file(input_dir: Path):
- for target in ["time_estimation", "timing", "time_est", "response_time"]:
- try:
- return find_input_file(input_dir, target)
- except Exception:
- pass
- return None
- def summarize_time_estimation(timing_csv: Path, tables_dir: Path, run_qc=None):
- """Summarize absolute timing error by condition if a timing CSV is available."""
- timing = pd.read_csv(timing_csv)
- timing.columns = [c.strip() for c in timing.columns]
- if "Participant" not in timing.columns or "Condition" not in timing.columns:
- print("[WARN] Timing file found but skipped: Participant and Condition columns are required.")
- return None
- timing["Participant"] = (
- timing["Participant"].astype(str).str.strip().str.replace("^P", "", regex=True).str.zfill(2)
- )
- timing["Condition"] = (
- timing["Condition"].astype(str).str.strip().str.lower()
- .replace({"noise": "in noise", "silence": "in silence"})
- )
- if run_qc is not None and "keep_primary_participant" in run_qc.columns:
- keep_part = run_qc[["Participant", "keep_primary_participant"]].drop_duplicates()
- timing = timing.merge(keep_part, on="Participant", how="left")
- timing = timing[timing["keep_primary_participant"] == True].copy()
- abs_candidates = [
- "AbsoluteError_s", "absolute_error_s", "AbsError_s", "abs_error_s",
- "AbsoluteError", "absolute_error", "abs_error", "TimingAbsError_s"
- ]
- estimate_candidates = [
- "Estimate_s", "estimate_s", "ResponseTime_s", "response_time_s",
- "ResponseTime", "response_time", "Elapsed_s", "elapsed_s", "RT_s", "rt_s"
- ]
- abs_col = next((c for c in abs_candidates if c in timing.columns), None)
- if abs_col is not None:
- timing["AbsoluteError_s"] = pd.to_numeric(timing[abs_col], errors="coerce").abs()
- else:
- est_col = next((c for c in estimate_candidates if c in timing.columns), None)
- if est_col is None:
- print("[WARN] Timing file found but skipped: no recognizable absolute-error or estimate column.")
- return None
- timing["AbsoluteError_s"] = (pd.to_numeric(timing[est_col], errors="coerce") - 4.0).abs()
- part = (
- timing.groupby(["Participant", "Condition"], as_index=False)
- .agg(
- mean_absolute_error_s=("AbsoluteError_s", "mean"),
- n_trials=("AbsoluteError_s", "count")
- )
- )
- save_table(part, tables_dir / "Supp_Table_TimeEstimationAbsoluteError_ParticipantByCondition.csv")
- summary = (
- part.groupby("Condition", as_index=False)
- .agg(
- n_participants=("Participant", "nunique"),
- mean_absolute_error_s=("mean_absolute_error_s", "mean"),
- sd_absolute_error_s=("mean_absolute_error_s", "std"),
- mean_n_trials=("n_trials", "mean")
- )
- )
- save_table(summary, tables_dir / "Supp_Table_TimeEstimationAbsoluteError_ByCondition.csv")
- wide = part.pivot(index="Participant", columns="Condition", values="mean_absolute_error_s")
- if {"in noise", "in silence"}.issubset(wide.columns):
- paired = wide[["in silence", "in noise"]].dropna()
- if len(paired) >= 2:
- t_stat, p_val = ttest_rel(paired["in noise"], paired["in silence"], nan_policy="omit")
- diff = paired["in noise"] - paired["in silence"]
- test_tbl = pd.DataFrame([{
- "Contrast": "in noise - in silence",
- "N": int(len(paired)),
- "mean_difference_s": float(diff.mean()),
- "sd_difference_s": float(diff.std(ddof=1)),
- "t": float(t_stat),
- "p": float(p_val)
- }])
- else:
- test_tbl = pd.DataFrame([{"Contrast": "in noise - in silence", "N": int(len(paired))}])
- save_table(test_tbl, tables_dir / "Supp_Table_TimeEstimationAbsoluteError_PairedTest.csv")
- return summary
- def corr_row(participant_summary, x, y, xname, yname):
- df = participant_summary[[x, y]].dropna()
- if len(df) < 3:
- return {
- "X": xname, "Y": yname, "N": len(df),
- "pearson_r": np.nan, "pearson_p": np.nan,
- "spearman_rho": np.nan, "spearman_p": np.nan
- }
- pr, pp = pearsonr(df[x], df[y])
- sr, sp = spearmanr(df[x], df[y])
- return {
- "X": xname, "Y": yname, "N": len(df),
- "pearson_r": pr, "pearson_p": pp,
- "spearman_rho": sr, "spearman_p": sp
- }
- def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("--input_dir", type=str, required=True,
- help="Directory containing anticipatory_electrode_long_noninterp.csv")
- parser.add_argument("--output_dir", type=str, required=True,
- help="Directory for output tables and figures")
- parser.add_argument("--data", type=str, default=None,
- help="Path to anticipatory_electrode_long_noninterp.csv")
- parser.add_argument("--metadata", type=str, default=None,
- help="Optional path to electrode_metadata.csv")
- parser.add_argument("--run_qc", type=str, default=None,
- help="Optional path to run_qc.csv")
- parser.add_argument("--speech", type=str, default=None,
- help="Optional path to speech_accuracy.csv")
- parser.add_argument("--timing", type=str, default=None,
- help="Optional path to time-estimation/timing CSV for absolute-error summaries")
- args = parser.parse_args()
- input_dir = Path(args.input_dir)
- output_dir = Path(args.output_dir)
- ensure_dir(output_dir)
- figures_dir = output_dir / "figures"
- tables_dir = output_dir / "tables"
- ensure_dir(figures_dir)
- ensure_dir(tables_dir)
- data_csv = Path(args.data) if args.data else find_input_file(input_dir, "anticipatory_electrode_long_noninterp")
- print(f"[INFO] Using data CSV: {data_csv}")
- dat = pd.read_csv(data_csv)
- if args.metadata:
- meta_csv = Path(args.metadata)
- meta = pd.read_csv(meta_csv)
- print(f"[INFO] Using electrode metadata: {meta_csv}")
- else:
- try:
- meta_csv = find_input_file(input_dir, "electrode_metadata")
- meta = pd.read_csv(meta_csv)
- print(f"[INFO] Using electrode metadata: {meta_csv}")
- except Exception:
- print("[WARN] electrode_metadata.csv not found. Metadata will be inferred from channel names.")
- meta = build_meta_from_spn(dat)
- if args.run_qc:
- qc_csv = Path(args.run_qc)
- run_qc = pd.read_csv(qc_csv)
- print(f"[INFO] Using QC CSV: {qc_csv}")
- else:
- try:
- qc_csv = find_input_file(input_dir, "run_qc")
- run_qc = pd.read_csv(qc_csv)
- print(f"[INFO] Using QC CSV: {qc_csv}")
- except Exception:
- run_qc = None
- print("[WARN] run_qc.csv not found. Proceeding without QC filtering.")
- dat.columns = [c.strip() for c in dat.columns]
- meta.columns = [c.strip() for c in meta.columns]
- need_cols = {"Participant", "RunID", "Trial", "Condition", "Component", "Electrode", "Value_uV"}
- miss = need_cols - set(dat.columns)
- if miss:
- raise ValueError(f"Missing required columns in anticipatory_electrode_long_noninterp.csv: {miss}")
- meta = prepare_meta_safe(meta, dat)
- dat["Participant"] = dat["Participant"].astype(str).str.zfill(2)
- dat["RunID"] = dat["RunID"].astype(str).str.strip()
- dat["Trial"] = pd.to_numeric(dat["Trial"], errors="coerce")
- dat["Condition"] = (
- dat["Condition"]
- .astype(str)
- .str.strip()
- .str.lower()
- .replace({"noise": "in noise", "silence": "in silence"})
- )
- dat["Component"] = dat["Component"].astype(str).str.strip().str.upper()
- if run_qc is not None:
- run_qc["participant"] = run_qc["participant"].astype(str).str.zfill(2)
- run_qc["runID"] = run_qc["runID"].astype(str).str.strip()
- keep_cols = ["runID", "participant", "keep_primary", "keep_primary_participant"]
- keep_cols = [c for c in keep_cols if c in run_qc.columns]
- run_qc = run_qc[keep_cols].drop_duplicates()
- run_qc = run_qc.rename(columns={"runID": "RunID", "participant": "Participant"})
- dat = dat.merge(run_qc, on=["RunID", "Participant"], how="left")
- if "keep_primary" in dat.columns:
- dat = dat[dat["keep_primary"] == True].copy()
- ids = ids_with_two_levels(dat, "Participant", "Condition")
- dat = dat[dat["Participant"].isin(ids)].copy()
- print(f"[INFO] Participants in primary analysis: {dat['Participant'].nunique()}")
- print(f"[INFO] Runs in primary analysis : {dat['RunID'].nunique()}")
- print(f"[INFO] Rows : {dat.shape[0]}")
- spn = dat[dat["Component"] == "SPN"].copy()
- cnv = dat[dat["Component"] == "CNV"].copy()
- spn_long, spn_roi = build_roi_trial(spn, meta)
- cnv_long, cnv_roi = build_roi_trial(cnv, meta)
- formula_primary = "Mean_uV ~ Condition_c * Hem_c + C(ROI)"
- res_spn, kind_spn = try_fit_mixed(formula_primary, spn_roi, re="~Condition_c", group="Participant")
- tbl_spn = fe_table(res_spn)
- save_table(tbl_spn, tables_dir / "Table_Primary_SPN_ROI_Hemisphere_LMM.csv")
- res_cnv, kind_cnv = try_fit_mixed(formula_primary, cnv_roi, re="~Condition_c", group="Participant")
- tbl_cnv = fe_table(res_cnv)
- save_table(tbl_cnv, tables_dir / "Table_Control_CNV_ROI_Hemisphere_LMM.csv")
- # Reviewer-requested ROI interaction models. The central ROI remains the
- # reference level in the default alphabetical coding of C(ROI).
- formula_roi_interaction = "Mean_uV ~ Condition_c * C(ROI) + Hem_c + Condition_c:Hem_c"
- res_spn_roi_int, kind_spn_roi_int = try_fit_mixed(formula_roi_interaction, spn_roi, re="~Condition_c", group="Participant")
- tbl_spn_roi_int = fe_table(res_spn_roi_int)
- save_table(tbl_spn_roi_int, tables_dir / "Supp_Table_SPN_ConditionByROI_LMM.csv")
- res_cnv_roi_int, kind_cnv_roi_int = try_fit_mixed(formula_roi_interaction, cnv_roi, re="~Condition_c", group="Participant")
- tbl_cnv_roi_int = fe_table(res_cnv_roi_int)
- save_table(tbl_cnv_roi_int, tables_dir / "Supp_Table_CNV_ConditionByROI_LMM.csv")
- # BIC-approximated Bayes factors. These are supplementary sensitivity
- # analyses and should be described as BIC approximations, not as fully
- # Bayesian mixed models with explicit priors.
- bf_rows = []
- formula_no_condition = "Mean_uV ~ Hem_c + C(ROI)"
- formula_condition_main = "Mean_uV ~ Condition_c + Hem_c + C(ROI)"
- formula_primary_no_condition_terms = "Mean_uV ~ Hem_c + C(ROI)"
- for label, df_comp in [("SPN", spn_roi), ("CNV", cnv_roi)]:
- run_bic_bayes_comparison(
- df_comp, label,
- "Condition main effect only",
- formula_no_condition,
- formula_condition_main,
- bf_rows
- )
- run_bic_bayes_comparison(
- df_comp, label,
- "All condition-related terms in primary ROI x Hemisphere model",
- formula_primary_no_condition_terms,
- formula_primary,
- bf_rows
- )
- run_bic_bayes_comparison(
- df_comp, label,
- "Added Condition x ROI terms beyond primary model",
- formula_primary,
- formula_roi_interaction,
- bf_rows
- )
- bf_tbl = pd.DataFrame(bf_rows)
- save_table(bf_tbl, tables_dir / "Supp_Table_BIC_BayesFactors_ConditionTerms.csv")
- fit_with_sex(spn_roi, spn_long, tables_dir / "Supp_Table_SPN_LMM_withSex.csv")
- fit_with_sex(cnv_roi, cnv_long, tables_dir / "Supp_Table_CNV_LMM_withSex.csv")
- electrodewise_lmm(spn_long, tables_dir / "Supp_Table_SPN_ElectrodeWise_LMM_FDR.csv")
- electrodewise_lmm(cnv_long, tables_dir / "Supp_Table_CNV_ElectrodeWise_LMM_FDR.csv")
- mean_cond_spn = (
- spn_long.groupby(["Participant", "Electrode", "Condition"], as_index=False)
- .agg(Mean_uV=("Value_uV", "mean"))
- )
- cond_means_spn = (
- mean_cond_spn.groupby(["Condition", "Electrode"], as_index=False)["Mean_uV"]
- .mean()
- )
- # Use the same color scale for the silence and noise maps, as requested by
- # the reviewer. The difference map below uses its own symmetric scale.
- cond_vals = cond_means_spn[
- cond_means_spn["Condition"].isin(["in silence", "in noise"])
- ]["Mean_uV"].to_numpy(dtype=float)
- cond_lim = float(np.nanmax(np.abs(cond_vals))) if np.isfinite(cond_vals).any() else None
- shared_condition_vlim = (-cond_lim, cond_lim) if cond_lim and cond_lim > 0 else None
- make_topomap_from_condition(
- cond_means_spn,
- condition_label="in silence",
- title="SPN in silence (-200 to 0 ms)",
- out_path=figures_dir / "Fig2a_SPN_topomap_in_silence.png",
- vlim=shared_condition_vlim
- )
- make_topomap_from_condition(
- cond_means_spn,
- condition_label="in noise",
- title="SPN in noise (-200 to 0 ms)",
- out_path=figures_dir / "Fig2b_SPN_topomap_in_noise.png",
- vlim=shared_condition_vlim
- )
- pivot_diff = (
- mean_cond_spn.pivot_table(index=["Participant", "Electrode"], columns="Condition", values="Mean_uV")
- .reset_index()
- )
- if {"in silence", "in noise"}.issubset(pivot_diff.columns):
- pivot_diff["Mean_uV"] = pivot_diff["in noise"] - pivot_diff["in silence"]
- diff_mean = pivot_diff.groupby("Electrode", as_index=False)["Mean_uV"].mean()
- diff_mean["Condition"] = "noise_minus_silence"
- diff_lim = float(np.nanmax(np.abs(diff_mean["Mean_uV"].to_numpy(dtype=float))))
- make_topomap_from_condition(
- diff_mean,
- condition_label="noise_minus_silence",
- title="SPN Noise - Silence (µV)",
- out_path=figures_dir / "Fig2c_SPN_topomap_difference.png",
- vlim=(-diff_lim, diff_lim) if diff_lim > 0 else None
- )
- # Report the cluster-based permutation test because it is described in the
- # manuscript. Results remain exploratory/descriptive for spatial localization.
- cluster_permutation_difference(
- spn_long,
- tables_dir / "Supp_Table_SPN_ClusterPermutation_NoiseMinusSilence.csv",
- component_label="SPN",
- n_permutations=5000,
- seed=42
- )
- cluster_permutation_difference(
- cnv_long,
- tables_dir / "Supp_Table_CNV_ClusterPermutation_NoiseMinusSilence.csv",
- component_label="CNV",
- n_permutations=5000,
- seed=42
- )
- spn_desc = roi_desc(spn_roi)
- cnv_desc = roi_desc(cnv_roi)
- save_table(spn_desc, tables_dir / "Table_Descriptive_ROI_ByCondition_SPN.csv")
- save_table(cnv_desc, tables_dir / "Table_Descriptive_ROI_ByCondition_CNV.csv")
- plot_roi_condition_panels(
- spn_desc,
- out_path=figures_dir / "Fig3_SPN_roi_panels_main.png",
- roi_order=["Frontal", "Central", "Parietal", "Occipital"],
- title="Mean SPN by ROI and condition"
- )
- plot_roi_condition_panels(
- cnv_desc,
- out_path=figures_dir / "Supp_Fig_CNV_roi_panels.png",
- roi_order=["Frontal", "Central", "Parietal", "Occipital"],
- title="Mean CNV by ROI and condition"
- )
- notes = [
- "Primary SPN analysis uses NON-INTERPOLATED data.",
- "CNV is included as a response-locked control analysis.",
- "Primary inference is ROI x Hemisphere LMM without Sex covariate.",
- "Sex-adjusted models are supplementary only.",
- "Central ROI is included in ROI models and is the reference level for C(ROI).",
- "Reviewer-requested Condition x ROI models are saved as supplementary tables.",
- "BIC-approximated Bayes factors are supplementary sensitivity analyses fitted with ML.",
- "Cluster-based permutation tables are exploratory spatial analyses and are not used to define follow-up ROIs.",
- "SPN condition topomaps use a common color scale for silence and noise; the difference map uses its own symmetric scale.",
- "SPN topomaps are descriptive and are shown in µV."
- ]
- notes_path = output_dir / "ANALYSIS_NOTES_SPN_CNV_REVISED.txt"
- with open(notes_path, "w", encoding="utf-8") as f:
- for line in notes:
- f.write(line + "\n")
- print(f"[Saved] {notes_path}")
- if args.timing:
- timing_csv = Path(args.timing)
- print(f"[INFO] Using timing CSV: {timing_csv}")
- summarize_time_estimation(timing_csv, tables_dir, run_qc=run_qc)
- else:
- timing_csv = find_timing_file(input_dir)
- if timing_csv is not None:
- print(f"[INFO] Using timing CSV: {timing_csv}")
- summarize_time_estimation(timing_csv, tables_dir, run_qc=run_qc)
- else:
- print("[WARN] time-estimation/timing CSV not found. Skipping absolute-error summaries.")
- if args.speech:
- speech_csv = Path(args.speech)
- speech = pd.read_csv(speech_csv)
- print(f"[INFO] Using speech CSV: {speech_csv}")
- else:
- try:
- speech_csv = find_input_file(input_dir, "speech_accuracy")
- speech = pd.read_csv(speech_csv)
- print(f"[INFO] Using speech CSV: {speech_csv}")
- except Exception:
- speech = None
- print("[WARN] speech_accuracy.csv not found. Skipping speech-in-noise analyses.")
- if speech is not None:
- speech.columns = [c.strip() for c in speech.columns]
- need_speech_cols = {"Participant", "SNR_dB", "Accuracy", "n_trials", "n_correct"}
- miss = need_speech_cols - set(speech.columns)
- if miss:
- raise ValueError(f"Missing required columns in speech_accuracy.csv: {miss}")
- speech["Participant"] = (
- speech["Participant"]
- .astype(str)
- .str.strip()
- .str.replace("^P", "", regex=True)
- .str.zfill(2)
- )
- if run_qc is not None and "keep_primary_participant" in run_qc.columns:
- keep_part = run_qc[["Participant", "keep_primary_participant"]].drop_duplicates()
- speech = speech.merge(keep_part, on="Participant", how="left")
- speech = speech[speech["keep_primary_participant"] == True].copy()
- speech_desc = (
- speech.groupby("SNR_dB", as_index=False)
- .agg(
- n_participants=("Participant", "nunique"),
- mean_accuracy=("Accuracy", "mean"),
- sd_accuracy=("Accuracy", "std"),
- mean_n_correct=("n_correct", "mean"),
- sd_n_correct=("n_correct", "std"),
- n_trials=("n_trials", "mean")
- )
- .sort_values("SNR_dB", ascending=False)
- )
- save_table(speech_desc, tables_dir / "Table1_SpeechAccuracy_BySNR.csv")
- speech_0 = speech[speech["SNR_dB"] == 0].copy()
- speech_0 = (
- speech_0.groupby("Participant", as_index=False)
- .agg(
- accuracy_0dB=("Accuracy", "mean"),
- n_correct_0dB=("n_correct", "mean")
- )
- )
- speech_all = (
- speech.groupby("Participant", as_index=False)
- .agg(
- accuracy_allSNR=("Accuracy", "mean"),
- n_correct_allSNR=("n_correct", "mean")
- )
- )
- speech_diff = speech[speech["SNR_dB"].isin([-5, -10, -15])].copy()
- speech_diff = (
- speech_diff.groupby("Participant", as_index=False)
- .agg(
- accuracy_diffSNR=("Accuracy", "mean"),
- n_correct_diffSNR=("n_correct", "mean")
- )
- )
- speech_part = speech_all.merge(speech_0, on="Participant", how="outer")
- speech_part = speech_part.merge(speech_diff, on="Participant", how="outer")
- save_table(speech_part, tables_dir / "Supp_Table_SpeechAccuracy_ParticipantSummary.csv")
- spn_part = (
- spn_roi.groupby(["Participant", "Condition"], as_index=False)
- .agg(SPN_mean_uV=("Mean_uV", "mean"))
- )
- spn_wide = spn_part.pivot(index="Participant", columns="Condition", values="SPN_mean_uV").reset_index()
- if {"in silence", "in noise"}.issubset(spn_wide.columns):
- spn_wide["SPN_noise_minus_silence"] = spn_wide["in noise"] - spn_wide["in silence"]
- cnv_part = (
- cnv_roi.groupby(["Participant", "Condition"], as_index=False)
- .agg(CNV_mean_uV=("Mean_uV", "mean"))
- )
- cnv_wide = cnv_part.pivot(index="Participant", columns="Condition", values="CNV_mean_uV").reset_index()
- if {"in silence", "in noise"}.issubset(cnv_wide.columns):
- cnv_wide["CNV_noise_minus_silence"] = cnv_wide["in noise"] - cnv_wide["in silence"]
- spn_wide = spn_wide.rename(columns={"in silence": "SPN_silence", "in noise": "SPN_noise"})
- cnv_wide = cnv_wide.rename(columns={"in silence": "CNV_silence", "in noise": "CNV_noise"})
- participant_summary = speech_part.merge(spn_wide, on="Participant", how="inner")
- participant_summary = participant_summary.merge(cnv_wide, on="Participant", how="inner")
- save_table(participant_summary, tables_dir / "Supp_Table_ParticipantSummary_SPN_CNV_Speech.csv")
- corr_rows = []
- predictors = [
- ("accuracy_0dB", "Accuracy at 0 dB"),
- ("accuracy_allSNR", "Accuracy across all SNRs"),
- ("accuracy_diffSNR", "Accuracy at difficult SNRs")
- ]
- outcomes = [
- ("SPN_noise_minus_silence", "SPN noise-silence"),
- ("SPN_noise", "SPN in noise"),
- ("CNV_noise_minus_silence", "CNV noise-silence"),
- ("CNV_noise", "CNV in noise")
- ]
- for px, pxname in predictors:
- for oy, oyname in outcomes:
- if px in participant_summary.columns and oy in participant_summary.columns:
- corr_rows.append(corr_row(participant_summary, px, oy, pxname, oyname))
- corr_tbl = pd.DataFrame(corr_rows)
- save_table(corr_tbl, tables_dir / "Supp_Table_Exploratory_SpeechBrain_Relations.csv")
- fig, ax = plt.subplots(figsize=(4.5, 4.0))
- tmp = speech[speech["SNR_dB"] == 0].copy()
- if len(tmp) > 0:
- vals = tmp.groupby("Participant")["Accuracy"].mean().values
- rng = np.random.default_rng(42)
- jitter = rng.normal(0, 0.04, size=len(vals))
- ax.scatter(np.ones(len(vals)) + jitter, vals, color="black", alpha=0.85, s=30)
- mu = np.mean(vals)
- if len(vals) >= 2:
- se = np.std(vals, ddof=1) / np.sqrt(len(vals))
- tcrit = student_t.ppf(1 - 0.05 / 2, len(vals) - 1)
- ci_lo = mu - tcrit * se
- ci_hi = mu + tcrit * se
- ax.hlines(mu, 0.82, 1.18, color="black", linewidth=2.0)
- ax.vlines(1.0, ci_lo, ci_hi, color="black", linewidth=1.5)
- ax.set_xlim(0.7, 1.3)
- ax.set_xticks([1.0])
- ax.set_xticklabels(["0 dB"])
- ax.set_ylabel("Accuracy")
- ax.set_title("Speech accuracy at 0 dB SNR")
- ax.spines["top"].set_visible(False)
- ax.spines["right"].set_visible(False)
- save_fig(fig, figures_dir / "Supp_Fig_SpeechAccuracy_0dB.png", dpi=300, tight=True)
- print("\n=== Analysis complete ===")
- if __name__ == "__main__":
- main()
run_spn_cnv_analysis.py at commit 7ad7b19, under MIT · at the source
Overview
- Department of Rehabilitation, Faculty of Health Science, Fukui Health Science University, Fukui, Japan
- Cognitive Motor Neuroscience, Department of Human Health Sciences, Graduate School of Medicine, Kyoto University, Kyoto, Japan
- Department of Speech and Language Therapy, Faculty of Health Rehabilitation, Kawasaki University of Medical Welfare, Kurashiki, Japan
Abstract
It remains unclear whether noisy listening reliably modulates anticipatory EEG activity before feedback and whether such activity explains interindividual differences in speech-in-noise performance. We examined these questions in an independent cohort of young adults with normal hearing using a time-estimation task with auditory feedback presented in silence and in continuous multi-talker noise. Stimulus-preceding negativity (SPN) was quantified as the mean EEG amplitude in the −200–0 ms interval before feedback onset. The primary analysis used ROI × hemisphere linear mixed-effects models. A response-locked contingent negative variation (CNV) analysis was included as a control. Speech-in-noise performance was summarized descriptively across 0, −5, −10, and −15 dB signal-to-noise ratios, and brain–behavior associations were treated as exploratory. The primary SPN analysis showed a clear posterior predominance, with more negative amplitudes at parietal and occipital than at central and frontal regions. However, the main effect of listening condition was not significant, and neither hemisphere nor the condition × hemisphere interaction reached significance. The CNV control analysis likewise showed no reliable condition effect. Speech-in-noise performance declined monotonically as the signal-to-noise ratio decreased, indicating that the independent behavioral test captured the expected effect of increasing acoustic difficulty across SNR levels. Exploratory analyses did not reveal robust associations between speech-in-noise performance and SPN or CNV measures. These findings indicate that the task elicited anticipatory slow potentials with posterior predominance, but provided limited evidence for reliable modulation by background noise under the present task parameters and sample characteristics.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 5 matches between paragraphs and lines of code.
kokamoto46/analyze_spn
7ad7b19eb9a0b9edaddfc561731f1e5ca0f8c9e9, 1 May 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
9 files
- matlab/
export_anticipatory_long , MATLAB, 176 lines_interp.m - matlab/
export_anticipatory_long , MATLAB, 179 lines_noninterp.m - matlab/
make_run_qc_tables.m , MATLAB, 122 lines - matlab/
stage1_prepare_ica.m , MATLAB, 292 lines, 1 match - matlab/
stage2a_epoch_and_mark.m , MATLAB, 121 lines - matlab/
stage2b_finalize_epochs. , MATLAB, 242 linesm - python/
run_spn_cnv_analysis.py , Python, 1,065 lines, 4 matches - LICENSE, License, 21 lines
- README.md, Text, 270 lines
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:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 7 scripts, each with its path and the digest of its content;
- 5 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
Datasets cited
- zenodo:19344047, at Zenodo; found in the text, “Data Statement”
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 2, 28 September 2026
- Authors: added Kazuhiro Okamoto (0000-0003-4553-4850); removed Kazuhiro Okamoto
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, volume, pages, dates, 7 authors, 6 keywords, 1 funder, 50 references.
Cite
This paper
Okamoto, K., Hoyano, K., Nomura, T., Irie, K., Obama, N., Kodama, N., & Kobayashi, Y. (2026). Anticipatory slow potentials before auditory feedback show posterior predominance but limited condition effects in speech-in-noise. IBRO neuroscience reports, 21, 32-41. https://
BibTeX
@article{okamoto2026anti
author = {Okamoto, Kazuhiro and Hoyano, Kengo and Nomura, Tomomi and Irie, Keisuke and Obama, Naoya and Kodama, Narihiro and Kobayashi, Yasutaka},
title = {{Anticipatory slow potentials before auditory feedback show posterior predominance but limited condition effects in speech-in-noise}},
journal = {IBRO neuroscience reports},
year = {2026},
month = jun,
volume = {21},
pages = {32--41},
publisher = {Elsevier},
issn = {2667-2421},
doi = {10.1016/
url = {https://
pmid = {42290943},
pmcid = {PMC13254385}
}
RIS
TY - JOUR
AU - Okamoto, Kazuhiro
AU - Hoyano, Kengo
AU - Nomura, Tomomi
AU - Irie, Keisuke
AU - Obama, Naoya
AU - Kodama, Narihiro
AU - Kobayashi, Yasutaka
TI - Anticipatory slow potentials before auditory feedback show posterior predominance but limited condition effects in speech-in-noise
T2 - IBRO neuroscience reports
J2 - IBRO Neurosci Rep
PY - 2026
DA - 2026/
VL - 21
SP - 32
EP - 41
SN - 2667-2421
PB - Elsevier
DO - 10.1016/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1016/
"type": "article-journal",
"title": "Anticipatory slow potentials before auditory feedback show posterior predominance but limited condition effects in speech-in-noise",
"container-title": "IBRO neuroscience reports",
"author": [
{
"family": "Okamoto",
"given": "Kazuhiro"
},
{
"family": "Hoyano",
"given": "Kengo"
},
{
"family": "Nomura",
"given": "Tomomi"
},
{
"family": "Irie",
"given": "Keisuke"
},
{
"family": "Obama",
"given": "Naoya"
},
{
"family": "Kodama",
"given": "Narihiro"
},
{
"family": "Kobayashi",
"given": "Yasutaka"
}
],
"container-title-short":
"volume": "21",
"page": "32-41",
"DOI": "10.1016/
"PMID": "42290943",
"PMCID": "PMC13254385",
"ISSN": "2667-2421",
"publisher": "Elsevier",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
1
]
]
}
}
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.1038/s42003-026-10394-7 [code]
- Cognitive load weakens neural speech tracking without altering response timing.Journal: Communications biologyIn common: MNE-Python, statsmodels, pandas, 3 other tools, EEG, cognitive, 4 references
- [2] doi:10.1097/j.pain.0000000000004044 [code]
- No effect of rhythmic visual stimulation on experimental pain perception.Journal: PainIn common: MNE-Python, statsmodels, pandas, 3 other tools, EEG, cognitive, 4 references
- [3] doi:10.1162/imag.a.105 [code]
- Right posterior theta reflects human parahippocampal phase resetting by salient cues during goal-directed navigationJournal: n/aIn common: EEGLAB, MNE-Python, statsmodels, 4 other tools, EEG, cognitive, 2 references
- [4] doi:10.1038/s42003-026-10108-z [code]
- Time-resolved EEG decoding reveals altered neural dynamics of affective semantic evaluation in depression and suicidality.Journal: Communications biologyIn common: EEGLAB, MNE-Python, statsmodels, 4 other tools, EEG, cognitive, 1 reference
- [5] doi:10.1093/cercor/bhag077 [code]
- The longitudinal development of intrinsic timescales in infancy and their relation to alpha brain rhythm.Journal: Cerebral cortex (New York, N.Y. : 1991)In common: EEGLAB, MNE-Python, statsmodels, 4 other tools, EEG, 1 reference
- [6] doi:10.1002/mds.70348 [code]
- Electroencephalography-B
ased Clustering Reveals Robust Neurophysiological Subtypes in Parkinson's Disease. Journal: Movement disorders : official journal of the Movement Disorder SocietyIn common: EEGLAB, MNE-Python, statsmodels, 4 other tools, EEG, 1 reference - [7] doi:10.3390/s26134019 [code]
- NeuroStat: An Open-Source EEG Connectivity Platform for Randomised Controlled Trials.Journal: Sensors (Basel, Switzerland)In common: MNE-Python, pandas, SciPy, 2 other tools, EEG, 3 references
- [8] doi:10.7554/elife.107088 [code]
- Development of auditory and spontaneous movement responses to music over the first postnatal year.Journal: eLifeIn common: EEGLAB, MNE-Python, pandas, 3 other tools, EEG, 2 references
- [9] doi:10.1038/s41467-026-74824-0 [code]
- Learning regularities in noise engages both neural predictive activity and representational changes.Journal: Nature communicationsIn common: MNE-Python, statsmodels, pandas, 3 other tools, cognitive, 2 references
- [10] doi:10.1111/ejn.70503
- Modulation of Predictive Coding in Auditory Paradigms of Varying Complexity in Children With Developmental Language Disorder.Journal: The European journal of neuroscienceIn common: EEG, cognitive, 5 references
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: 1 repository of the authors' code, each at its verified commit and with its license, 7 scripts, and 5 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:80bf05d80fc284a1…
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.
