Diminished Signal-to-Noise Ratio Disrupts Somatosensory Population Encoding and Drives Tactile Hyposensitivity in the Fmr1<sup>-/y</sup> Autism Model.
The 14 matches
- [1] § Materials and Methods › Decoding of Behavioral Responses ↔ Figures/response_decoding.py, lines 517–638 · score 0.85 · double cross validation, Classification accuracy, logistic regression, undersampling, penalty, SEM
- [2] § Materials and Methods › Statistics ↔ percephone/plts/utils.py, lines 41–102 · score 0.76 · Wilcoxon Signed Rank, Shapiro Wilk, Mann Whitney, Variance
- [3] § Results › Neuronal Activity in the S1‐FP Fails to Predict Stimulus Detection in Fmr1−/y‐Hyposensitive Mice ↔ Figures/response_decoding.py, lines 799–919 · score 0.75 · Wilcoxon signed rank, Miss accuracy, Hit accuracy, Response decoding, horizontal, Vertical
- [4] § Results › Forepaw‐Based Perceptual Decision‐Making Task Assesses Tactile Detection in Mice ↔ Figures/noise_assessment.py, lines 2771–2895 · score 0.73 · Correct Rejection, stimulus onset, Pre stimulus, behavioral outcomes, CR, Go trials
- [5] § Results › S1‐FP Pyramidal Neurons Dominate the Decoding of Stimulus Detection ↔ Figures/response_decoding.py, lines 1045–1134 · score 0.73 · trained logistic regression, Decoding accuracy, scored activity, stimulus detection, classifiers, model
- [6] § Results › Reducing Neuronal Excitability Improves Tactile Sensitivity in Fmr1−/y Mice ↔ Figures/response_decoding.py, lines 799–919 · score 0.67 · Miss classification, Miss accuracy, Hit accuracy, Response decoding, shuffled, post
- [7] § Results › Reduced Population Signal‐to‐Noise Ratio is Linked to Impaired Stimulus and Detection Encoding in Fmr1−/y‐Hyposensitive Mice ↔ Figures/noise_assessment.py, lines 2990–3119 · score 0.67 · stimulus evoked activity, population SNR, noise ratio, Go trials, quantified, signal
- [8] § Materials and Methods › Statistics ↔ percephone/plts/stats.py, lines 707–760 · score 0.64 · Shapiro Wilk, Mann Whitney, Friedman, ANOVA
- [9] § Materials and Methods › Decoding of Behavioral Responses ↔ Figures/response_decoding.py, lines 1045–1134 · score 0.63 · compare decoding accuracy, cross validation, fold, excitatory, trained, INH
- [10] § Materials and Methods › Go/No‐Go Vibrotactile Task Analysis ↔ percephone/plts/behavior.py, lines 608–660 · score 0.59 · Psychometric curves, Hit rate, Detection thresholds, sigmoid, fitted, amplitude
- [11] § Results › Neuronal Activity in the S1‐FP Fails to Predict Stimulus Detection in Fmr1−/y‐Hyposensitive Mice ↔ Figures/stimulus_encoding.py, lines 1044–1145 · score 0.57 · Post hoc, stimuli encoded, Neuronal activity, excitatory, stimulation, inhibitory
- [12] § Results › Forepaw‐Based Perceptual Decision‐Making Task Assesses Tactile Detection in Mice ↔ Figures/stimulus_encoding.py, lines 800–884 · score 0.56 · Correct Rejection, stimulus onset, CR, Go trials, FA, Alarm
- [13] § Materials and Methods › Analysis ↔ Figures/noise_assessment.py, lines 2584–2656 · score 0.56 · GABAergic, pyramidal neurons, Inhibitory neurons, interneurons, scores, excitatory
- [14] § Results › Reduced Single‐Neuron Signal‐to‐Noise Ratio Underlies Diminished Neural Recruitment in Fmr1−/y‐Hyposensitive Mice ↔ Figures/noise_assessment.py, lines 2584–2656 · score 0.53 · score response, GABAergic, pyramidal neurons, noise, Linear, interneurons
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,542 lines · 88 KB · GPL-2.0 · 5 matches
- # region ======================================== Imports ==============================================================
- import os
- import random
- import mplcursors
- import numpy as np
- import pandas as pd
- import statsmodels.formula.api as smf
- import statsmodels.api as sm
- import pingouin as pg
- from imblearn.under_sampling import RandomUnderSampler
- from matplotlib import pyplot as plt
- from multiprocessing import cpu_count, pool
- from scipy.stats import pointbiserialr, linregress, binomtest, wilcoxon
- from sklearn.linear_model import LogisticRegression
- from sklearn.metrics import confusion_matrix, classification_report
- from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold
- from imblearn.pipeline import Pipeline
- from itertools import product
- from tqdm import tqdm
- import percephone.core.recording as pc
- import percephone.plts.stats as ppt
- import percephone.plts.style as sty
- # from Figures.noise_assessment import get_mean_trial_activity_df, ntn_cosine_similarity
- from Figures.stimulus_encoding import get_features
- # endregion
- def get_activity_by_frame_df(recs, zscore=True, BMS=False):
- """
- Builds a trial-by-trial DataFrame containing neuronal activity traces aligned to stimulus onset.
- For each neuron and trial, the function extracts activity values from 30 frames before to 30 frames
- after the stimulus (with frame 30 corresponding to the stimulus onset). Metadata such as genotype,
- recording ID, neuron type, trial characteristics, and neuron responsivity are also stored.
- If `zscore=True`, activity traces are taken from z-scored signals (`rec.zscore_exc` and `rec.zscore_inh`);
- otherwise, ΔF/F traces are used (`rec.df_f_exc` and `rec.df_f_inh`). When `BMS=True`, the recording ID
- includes the second part of the genotype string.
- Parameters
- ----------
- recs : list
- List of recording objects, each containing neuronal activity arrays and metadata
- (e.g., filename, genotype, stim_time, stim_ampl, stim_durations, detected_stim, session_threshold).
- zscore : bool, optional
- Whether to use z-scored activity (`True`) or raw ΔF/F traces (`False`). Default is True.
- BMS : bool, optional
- Whether to append the second part of the genotype string to the recording ID. Default is False.
- Returns
- -------
- pandas.DataFrame
- Long-format DataFrame where each row corresponds to the activity of a single neuron
- during a single trial. Columns include:
- - "Genotype": genotype of the recording,
- - "ID": recording identifier,
- - "Threshold": session threshold,
- - "Trial": trial index,
- - "Amplitude": stimulus amplitude,
- - "Duration": stimulus duration,
- - "Behavior": behavioral response label,
- - "n_type": neuron type ("EXC" or "INH"),
- - "resp": responsivity label of the neuron in that trial,
- - "n_ID": neuron index,
- - integer frame columns from -30 to +29 relative to stimulus onset.
- """
- rows = []
- for rec in recs:
- if BMS:
- rec_id = f"{rec.filename}-{rec.genotype.split("-")[1]}"
- else:
- rec_id = rec.filename
- activity_vector = [rec.zscore_exc, rec.zscore_inh] if zscore else [rec.df_f_exc, rec.df_f_inh]
- for n_type, activity in zip(["EXC", "INH"], activity_vector):
- resp = np.array(rec.matrices[n_type]["Responsivity"])
- for neuron_id, neuron in enumerate(np.array(activity)):
- for (trial_id, trial_time), trial_duration, trial_amp, trial_label in zip(enumerate(rec.stim_time),
- rec.stim_durations,
- rec.stim_ampl, rec.detected_stim):
- row = {"Genotype": rec.genotype, "ID": rec_id, "Threshold": rec.session_threshold,
- "Trial": trial_id, "Amplitude": trial_amp, "Duration": trial_duration,
- "Behavior": trial_label, "n_type": n_type, "resp": resp[neuron_id, trial_id], "n_ID": neuron_id}
- for frame_id, frame in enumerate(range(trial_time - 30, trial_time + 30)):
- row[frame_id] = neuron[frame]
- rows.append(row)
- return pd.DataFrame(rows)
- # Not used in the final paper
- def sliding_window_average(df, header_cols, window_size, sum=False):
- """
- Applies a sliding window operation across the numeric columns of a DataFrame, either averaging
- or summing values depending on the `sum` flag. The numeric columns are assumed to be labeled
- consecutively (e.g., 0, 1, 2, …, N-1), while header columns remain unchanged.
- For each numeric column i, the output value is computed over a window of size `window_size`
- centered at i. If the window would extend beyond the available columns (near the edges),
- it is truncated and the operation is performed on the available subset (with at least 2 values).
- Parameters
- ----------
- df : pandas.DataFrame
- Input DataFrame containing header columns and numeric columns labeled 0..N-1.
- header_cols : list of str
- Names of the columns to be preserved as-is in the output.
- window_size : int
- Size of the sliding window (must be >= 1).
- sum : bool, optional
- If False (default), computes the mean within each sliding window.
- If True, computes the sum instead.
- Returns
- -------
- pandas.DataFrame
- DataFrame with the same columns and shape as the input.
- Header columns are preserved unchanged, while numeric columns are replaced
- by their windowed average or sum (depending on `sum`).
- """
- numeric_cols = [col for col in df.columns if col not in header_cols]
- if sum:
- rolled = df[numeric_cols].rolling(window=window_size, axis=1, center=True, min_periods=2).sum()
- else:
- rolled = df[numeric_cols].rolling(window=window_size, axis=1, center=True, min_periods=2).mean()
- # Reconstruct the DataFrame: keep headers, replace numeric columns with rolled values.
- result_df = pd.concat([df[header_cols].reset_index(drop=True), rolled.reset_index(drop=True)], axis=1)
- # Ensure numeric column names stay as ints (or as they were originally).
- result_df.columns = header_cols + [int(c) for c in numeric_cols]
- return result_df
- # Not used in the final paper
- def aggregate_every_3cols(df, header_cols, window_size=3):
- """
- Aggregates consecutive groups of numeric columns into their rowwise mean, while
- preserving the specified header columns. By default, the numeric columns are
- processed in blocks of 3, but the block size can be adjusted with `window_size`.
- The number of numeric columns must be a multiple of `window_size`. Each new
- aggregated column is named according to the range of original columns it summarizes
- (e.g., "0_to_2_mean" for columns [0, 1, 2]).
- Parameters
- ----------
- df : pandas.DataFrame
- Input DataFrame containing header columns and numeric columns.
- header_cols : list of str
- Names of the columns to be preserved as-is in the output.
- window_size : int, optional
- Number of consecutive numeric columns to aggregate (default is 3).
- Returns
- -------
- pandas.DataFrame
- A DataFrame containing the header columns and one new column per block of
- `window_size` numeric columns, each holding the rowwise mean of that block.
- """
- # 1) Split off non-numeric headers
- numeric_cols = [col for col in df.columns if col not in header_cols]
- header = df[header_cols]
- nums = df[numeric_cols]
- # 2) Check we have a multiple of 3 (optional)
- if len(nums.columns) % window_size != 0:
- raise ValueError(f"Expected a multiple of {window_size} numeric columns, got {len(nums.columns)}")
- # 3) For each block of 3, compute the mean
- new_cols = {}
- cols = list(nums.columns)
- for block_idx in range(0, len(cols), window_size):
- trio = cols[block_idx:block_idx + window_size]
- # name the new column after the first of the trio (or anything you like)
- new_name = f"{trio[0]}_to_{trio[-1]}_mean"
- new_cols[new_name] = nums[trio].mean(axis=1)
- # 4) Concatenate header + new means
- result = pd.concat([header, pd.DataFrame(new_cols, index=df.index)], axis=1)
- return result
- # region ======================================== Correlation ==========================================================
- def perceptual_magnitude_behavior_corr(recs):
- """
- Computes the correlation between perceptual magnitude (here: % responsive excitatory neurons)
- and behavioral outcome (Hit vs Miss) for threshold-level trials using a point-biserial correlation.
- This function restricts the analysis to stimulation trials at the session threshold amplitude
- in order to avoid bias. It cannot be applied to all trials simultaneously, because amplitude
- would need to be included as a covariate.
- Parameters
- ----------
- recs : list
- List of recording objects, each expected to provide:
- - rec.filename (str) : Unique identifier for the recording.
- - rec.genotype (str) : Genotype label.
- - rec.session_threshold (float or int) : Threshold amplitude used in the session.
- - rec.stim_ampl_filter(stim_ampl="all") : Boolean mask for threshold trials.
- - rec.get_perc_resp(pattern=1, n_type="EXC") : Returns excitatory responsivity vector.
- - rec.detected_stim : Boolean array of behavioral detection outcomes.
- Returns
- -------
- pandas.DataFrame
- A DataFrame where each row corresponds to a recording, with the following columns:
- - "ID" : Recording filename.
- - "Genotype" : Genotype of the animal.
- - "session_threshold" : Threshold amplitude of the session.
- - "nb_threshold_trials" : Number of threshold trials included in the correlation.
- - "R2" : Coefficient of determination (r²) from the point-biserial correlation.
- - "p_val" : Associated p-value.
- """
- rows = []
- for rec in recs:
- # Keeping only the stimulation at threshold amplitude to limit bias
- threshold_trials_mask = rec.stim_ampl_filter(stim_ampl="all")
- # Getting the vector of the parameter to correlate with the behavior
- act_exc_vector = rec.get_perc_resp(pattern=1, n_type="EXC")[threshold_trials_mask]
- # Getting the vector of behavioral outcome
- behavior_vector = rec.detected_stim[threshold_trials_mask]
- if len(behavior_vector) > 2:
- # Point biserial correlation of the vectors
- r, p_val = pointbiserialr(act_exc_vector, behavior_vector)
- rows.append({"ID": rec.filename, "Genotype": rec.genotype, "session_threshold": rec.session_threshold,
- "nb_threshold_trials": len(behavior_vector), "R2": r**2, "p_val": p_val})
- else:
- print(f"{rec.filename} {rec.genotype} excluded → only {len(behavior_vector)} threshold trial(s)")
- return pd.DataFrame(rows)
- def correlate_mean_zscore_behavior_frame(frame_data):
- """
- Computes, for each recording ID × neuron type × responsivity group, the correlation between
- per-frame mean z-scores and behavioral outcome (Hit vs Miss) using point-biserial correlation.
- Each row in the output DataFrame corresponds either to the correlation coefficient ("r") or
- its associated p-value ("pval") at each frame, for one (ID, Genotype, neuron type, resp) group.
- Parameters
- ----------
- frame_data : pandas.DataFrame
- Trial-wise neuronal activity with both header metadata and per-frame z-scores.
- Expected columns include:
- - "Genotype" : str, genotype label.
- - "ID" : str, recording/animal identifier.
- - "Threshold" : float or int, session threshold amplitude.
- - "Trial" : int, trial index.
- - "Amplitude" : float or int, stimulation amplitude.
- - "Duration" : float or int, stimulation duration.
- - "Behavior" : bool, behavioral outcome (e.g., Hit=True, Miss=False).
- - "n_type" : str, neuron type ("EXC" or "INH").
- - "resp" : int or bool, responsivity of the neuron in the trial.
- - "n_ID" : int, neuron identifier.
- - Frame-wise z-scores as numeric columns (0, 1, ..., N).
- Returns
- -------
- pandas.DataFrame
- Each row corresponds to one correlation metric for a given (Genotype, ID, n_type, resp) group:
- - "Genotype" : genotype label.
- - "ID" : recording/animal identifier.
- - "n_type" : neuron type.
- - "resp" : responsivity group.
- - "metric" : "r" (correlation coefficient) or "pval" (associated p-value).
- - Frame columns : correlation values (r or pval) at each frame.
- Notes
- -----
- - The function first averages z-scores across neurons (per (ID, Trial, n_type, resp)),
- then correlates those per-trial means with the binary behavior vector.
- - Only groups with more than one trial are included in the correlation.
- - Output rows come in pairs: one with correlation coefficients ("r"), one with p-values ("pval").
- """
- # Computing the mean zscore per trial
- header_columns = ["Genotype", "ID", "Threshold", "Trial", "Amplitude", "Duration", "Behavior", "n_type", "resp", "n_ID"]
- mean_accross = ["n_ID"]
- grouping_columns = [col for col in header_columns if col not in mean_accross]
- data = frame_data.groupby(grouping_columns, as_index=False).mean().drop(columns=mean_accross)
- rows = []
- for rec_id in data["ID"].unique():
- rec_data = data[data["ID"] == rec_id]
- genotype = rec_data["Genotype"].values[0]
- for neuron_type in rec_data["n_type"].unique():
- n_type_data = rec_data[rec_data["n_type"] == neuron_type]
- for response in n_type_data["resp"].unique():
- resp_data = n_type_data[rec_data["resp"] == response]
- row_r = {"Genotype": genotype, "ID": rec_id, "n_type": neuron_type, "resp": response, "metric": "r"}
- row_pval = {"Genotype": genotype, "ID": rec_id, "n_type": neuron_type, "resp": response, "metric": "pval"}
- y = resp_data["Behavior"].values
- if len(resp_data) > 1:
- for col in [c for c in data.columns if c not in header_columns]:
- x = resp_data[col].values
- row_r[col], row_pval[col] = pointbiserialr(x, y)
- rows.append(row_r)
- rows.append(row_pval)
- return pd.DataFrame(rows)
- def plot_frame_correlation(corr_data):
- """
- Plots frame-by-frame correlation results (r and p-values) between neuronal activity
- and behavioral outcome, separated by genotype, neuron type, and responsivity.
- Parameters
- ----------
- corr_data : pandas.DataFrame
- Correlation results produced by `correlate_mean_zscore_behavior_frame`.
- Expected columns include:
- - "Genotype" : str, genotype label.
- - "ID" : str, recording/animal identifier.
- - "n_type" : str, neuron type ("EXC" or "INH").
- - "resp" : int or bool, responsivity group.
- - "metric" : str, correlation metric ("r" or "pval").
- - Frame columns : correlation values (float) for each frame index.
- Returns
- -------
- None
- Displays a matplotlib figure with subplots:
- - Rows correspond to correlation metrics ("r", "pval").
- - Columns correspond to genotypes.
- - Each line corresponds to one (ID, n_type, resp) group, with color
- indicating neuron type and responsivity.
- The plot includes the following visual guides:
- - Vertical dashed lines: stimulus onset (frame 30, red) and offset (frame 45, black).
- - For p-values, a horizontal green dashed line marks the 0.05 significance threshold.
- - Color scheme:
- * EXC: light to dark blue for increasing responsivity levels.
- * INH: light to dark magenta for increasing responsivity levels.
- - Figure title: "Frame by frame correlation of zscore with behavior".
- """
- color_dict = {"EXC": ["skyblue", "blue", "navy"], "INH": ["pink", "magenta", "darkviolet"]}
- fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(18, 12), constrained_layout=True)
- for col, genotype in enumerate(corr_data["Genotype"].unique()):
- for row, metric in enumerate(corr_data["metric"].unique()):
- data = corr_data[(corr_data["Genotype"] == genotype) & (corr_data["metric"] == metric)].drop(columns=["Genotype", "metric"])
- header_columns = ["ID", "n_type", "resp"]
- mean_accross = ["ID"]
- grouping_columns = [col for col in header_columns if col not in mean_accross]
- data = data.groupby(grouping_columns, as_index=False).mean().drop(columns=mean_accross)
- # Plotting
- ax[row, col].set_title(f"{metric} for {genotype}")
- for i, curve_row in data.iterrows():
- y = curve_row.drop(labels=["n_type", "resp"]).values.tolist()
- x = np.arange(len(y))
- ax[row, col].plot(x, y, color=color_dict[curve_row["n_type"]][int(curve_row["resp"])], lw=1)
- ax[row, col].axvline(x=30, ls="--", lw=1, color="red")
- ax[row, col].axvline(x=45, ls="--", lw=1, color="black")
- if metric == "pval":
- ax[row, col].axhline(y=0.05, ls="--", lw=1, color="green")
- fig.suptitle("Frame by frame correlation of zscore with behavior")
- fig.canvas.manager.set_window_title("Frame_corr_zscore_behavior")
- plt.show()
- # endregion ============================================================================================================
- # region ======================================== Modelling ============================================================
- # Not used in the final paper
- def glmm_behavior(data):
- """
- Fits a generalized linear mixed model (GLMM) to explain the behavioral outcome
- from neuronal predictors and stimulation parameters.
- Parameters
- ----------
- data : pandas.DataFrame
- Long-format dataset containing:
- - "behavior" : binary outcome (trial detected = 1, not detected = 0).
- - "Genotype" : categorical factor with levels ["WT", "KO", "KO-Hypo"].
- - "ID" : subject/recording identifier (random effect).
- - "amplitude" : stimulus amplitude.
- - "act_EXC_perc", "inh_EXC_perc", "act_INH_perc", "inh_INH_perc" :
- trial-level neuronal activity measures used as predictors.
- Returns
- -------
- result : statsmodels.genmod.generalized_linear_model.BinomialBayesMixedGLMResults
- Fitted GLMM object. A summary of the model is printed to console.
- The model is binomial with a logit link, and uses the following specification:
- ``behavior ~ amplitude*Genotype
- + amplitude:act_EXC_perc*Genotype
- + amplitude:inh_EXC_perc*Genotype
- + amplitude:act_INH_perc*Genotype
- + amplitude:inh_INH_perc*Genotype``
- Random effects are modeled by subject ID, with one variance component per subject.
- Estimation is done using variational Bayes (`fit_vb`). The function contains
- commented-out alternatives for fitting a GEE restricted to WT animals or
- including additional predictors (e.g. amplitude, delay).
- """
- data["Genotype"] = pd.Categorical(data["Genotype"], categories=["WT", "KO", "KO-Hypo"], ordered=True)
- # data["behavior"] = pd.Categorical(data["behavior"], categories=["False", "True"], ordered=True)
- # data["behavior"] = data["behavior"].astype(int)
- # === === Fitting the model === ===
- # --- GEE ---
- formula = ("behavior ~ amplitude*Genotype + amplitude:act_EXC_perc*Genotype + amplitude:inh_EXC_perc*Genotype + "
- "amplitude:act_INH_perc*Genotype + amplitude:inh_INH_perc*Genotype")# + "
- # # "act_EXC_amp + inh_EXC_amp + act_INH_amp + inh_INH_amp + "
- # # "act_EXC_delay + inh_EXC_delay + act_INH_delay + inh_INH_delay + "
- # # "Genotype + amplitude:Genotype")
- # gee_model = smf.gee(formula, groups="ID", data=data[data["Genotype"] == "WT"], family=sm.families.Binomial())
- # gee_result = gee_model.fit()
- # print(gee_result.summary())
- # --- GLMM ---
- vc_formulas = {"ID": "0 + C(ID)"}
- model = sm.genmod.BinomialBayesMixedGLM.from_formula(formula, vc_formulas, data=data)#, family=sm.families.Binomial())
- result = model.fit_vb()
- print(result.summary())
- # Not used in the final paper
- def frame_model_n_type_avg(frame_data):
- """
- Trains a logistic regression model for each frame and each animal to classify
- behavioral outcome (hit vs. miss) based on averaged neuronal responses.
- Cross-validation is used to assess classification accuracy, with undersampling
- performed to correct class imbalance.
- For each frame, a pivoted dataset is constructed with columns corresponding to
- (n_type, resp) neuron categories. Trials with amplitude = 0 and inhibited INH
- neurons are excluded. Missing values are imputed by the per-animal/per-amplitude
- mean. Logistic regression with L2 regularization is then trained using
- undersampled training data, and evaluated on a held-out test set.
- Parameters
- ----------
- frame_data : pandas.DataFrame
- Long-format trial-level dataset containing:
- - "Genotype" : animal genotype.
- - "ID" : animal/session identifier.
- - "Threshold" : stimulation threshold for that session.
- - "Trial" : trial index.
- - "Amplitude" : stimulation amplitude.
- - "Duration" : stimulation duration.
- - "Behavior" : binary outcome (hit/miss).
- - "n_type" : neuron type (EXC or INH).
- - "resp" : response sign (-1, 0, 1).
- - "n_ID" : neuron identifier.
- - plus z-score values across frames.
- Returns
- -------
- results_df : pandas.DataFrame
- A dataframe where each row corresponds to one animal and one frame.
- Includes:
- - "Genotype" : animal genotype.
- - "ID" : animal/session identifier.
- - "Threshold" : stimulation threshold.
- - "Frame" : frame index.
- - "TPR" : true positive rate (sensitivity/recall).
- - "FPR" : false positive rate.
- - "Accuracy" : classification accuracy.
- Logistic regression uses stratified train/test splits, undersampling
- within the training set, and evaluation on the held-out test set.
- The confusion matrix is used to compute the reported metrics.
- """
- # Grouping the different neurons
- header_columns = ["Genotype", "ID", "Threshold", "Trial", "Amplitude", "Duration", "Behavior", "n_type", "resp",
- "n_ID"]
- mean_accross = ["n_ID"]
- grouping_columns = [col for col in header_columns if col not in mean_accross]
- data = frame_data.groupby(grouping_columns, as_index=False).mean().drop(columns=mean_accross)
- # Creating a pivot DataFrame to obtain a dataframe per frame, each column being a neuron type/resp combinaison
- index_columns = [col for col in grouping_columns if col not in ["n_type", "resp"]]
- numeric_columns = [c for c in data.columns if c not in header_columns]
- rows = []
- for frame in numeric_columns:
- print(f"Frame n°{frame}")
- frame_data = data.pivot(index=index_columns, columns=["n_type", "resp"], values=frame).reset_index()
- # Dropping no go trials and the inhibited INH neurons because they are too few and induce NaN values
- frame_data = frame_data.drop(columns=("INH", -1))
- frame_data = frame_data[frame_data["Amplitude"] != 0]
- # Imputing the NaN by the mean value per animal per amplitude
- for col in [("EXC", 0), ("EXC", 1), ("EXC", -1), ("INH", 0), ("INH", 1)]:
- frame_data[col] = frame_data.groupby(["Genotype", "ID", "Threshold", "Amplitude"], as_index=False)[[col]].transform(lambda x: x.fillna(x.mean()))
- # Training a model for each recording and storing the evaluation metrics in a new DataFrame
- for rec_id in frame_data["ID"].unique():
- filtered_data = frame_data[frame_data["ID"] == rec_id]
- X = filtered_data.drop(columns=index_columns)
- y = filtered_data["Behavior"]
- # Splitting the data into training and test sets (using stratification to preserve class distribution)
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=42, stratify=y)
- # 1) Use CV to find best C value for L2 regularization of LR, undersample each fold
- # pipeline = Pipeline(steps=[('under', RandomUnderSampler(random_state=42)),
- # ('clf', LogisticRegression(solver='lbfgs', max_iter=1000))])
- # 2) Train the LR with the best parameters on the global undersampled X_train
- # 3) Assess model performance on test set
- # Create a pipeline that first undersamples then fits a logistic regression model
- undersampler = RandomUnderSampler(random_state=42)
- X_train_res, y_train_res = undersampler.fit_resample(X_train, y_train)
- lr = LogisticRegression(solver='lbfgs', C=1, max_iter=5000)
- # Set up a grid of hyperparameters to tune
- # param_grid = {"clf__C": [0.00001, 0.0001, 0.001, 0.01, 0.1, 1, 10]}
- # Use cross-validation (here, 5-fold) to search for the best hyperparameters
- # grid_search = GridSearchCV(pipeline, param_grid, cv=4, scoring='accuracy')
- # grid_search.fit(X_train, y_train)
- # Evaluate on the test set
- # y_pred = grid_search.predict(X_test)
- lr.fit(X_train_res, y_train_res)
- y_pred = lr.predict(X_test)
- cm = confusion_matrix(y_test, y_pred)
- # Calculate true positive and true negative rates
- tn, fp, fn, tp = cm.ravel()
- tpr = tp / (tp + fn) # Sensitivity / Recall
- fpr = fp / (tn + fp)
- accuracy = (tp + tn) / (tp + tn + fp + fn)
- # Optionally, print a full classification report
- # print(classification_report(y_test, y_pred))
- row = {"Genotype": filtered_data["Genotype"].values[0], "ID": filtered_data["ID"].values[0],
- "Threshold": filtered_data["Threshold"].values[0], "Frame": frame, "TPR": tpr, "FPR": fpr, "Accuracy": accuracy}
- rows.append(row)
- return pd.DataFrame(rows)
- def frame_model(frame_data, neuron_type=["EXC", "INH"], resp_type=[0, 1, -1], db_cv=True, balancing_method="resampling",
- sliding_window=None, window_sum=False, window=None):
- """
- Trains logistic regression models to classify behavioral outcome (hit vs. miss)
- based on neuronal responses across frames. For each frame and each animal,
- a dataset of neuronal activity is pivoted so that each neuron forms one predictor.
- Logistic regression is then trained and evaluated either with nested double
- cross-validation or a train/test split, depending on the `db_cv` flag.
- To handle class imbalance, the user can choose resampling, sample weighting,
- or no balancing method. Optionally, neuronal activity can be aggregated
- over time using a sliding window or a fixed-size window. For each animal and frame,
- the model reports classification metrics including accuracy, sensitivity (TPR),
- and false positive rate (FPR), along with shuffled controls.
- Parameters
- ----------
- frame_data : pandas.DataFrame
- Trial-level dataset containing:
- - "Genotype" : animal genotype.
- - "ID" : animal/session identifier.
- - "Trial" : trial index.
- - "Amplitude" : stimulation amplitude.
- - "Duration" : stimulation duration.
- - "Behavior" : binary outcome (hit/miss).
- - "n_type" : neuron type (EXC or INH).
- - "resp" : response sign (-1, 0, 1).
- - "n_ID" : neuron identifier.
- - plus z-score values across frames.
- neuron_type : list of str, default=["EXC", "INH"]
- Neuron types to include in the analysis.
- resp_type : list of int, default=[0, 1, -1]
- Response categories to include (inactive, activated, inhibited).
- db_cv : bool, default=True
- If True, performs nested double cross-validation to tune hyperparameters
- and evaluate generalization. If False, a single train/test split is used.
- balancing_method : {"resampling", "weights", None}, default="resampling"
- Strategy to address class imbalance:
- - "resampling" : undersampling with RandomUnderSampler.
- - "weights" : class_weight="balanced" in logistic regression.
- - None : no explicit balancing.
- sliding_window : int or None, default=None
- Size of the sliding window (in frames) for temporal averaging of activity.
- Mutually exclusive with `window`.
- window_sum : bool, default=False
- If True and `sliding_window` is provided, sums activity within the window
- instead of averaging.
- window : int or None, default=None
- Size of non-overlapping windows (in frames) for aggregation.
- Mutually exclusive with `sliding_window`.
- Returns
- -------
- frame_model_df : pandas.DataFrame
- Dataframe where each row corresponds to one animal and one frame,
- containing:
- - "Genotype", "ID", "Frame" : identifiers.
- - "TPR" : true positive rate (sensitivity/recall).
- - "FPR" : false positive rate.
- - "Accuracy" : classification accuracy.
- - "TPR_shuffle", "FPR_shuffle", "Accuracy_shuffle" :
- performance metrics after label shuffling (baseline control).
- frame_mean_sem : matplotlib.Figure or tuple
- Summary plot of classification accuracy over frames, averaged across animals
- with SEM. Title encodes selected neuron/response types and whether double CV
- was used.
- """
- data = frame_data.copy()
- assert (sliding_window is None or window is None), "Please choose between each frame, sliding window, and window"
- header_columns = ["Genotype", "ID", "Threshold", "Trial", "Amplitude", "Duration", "Behavior", "n_type", "resp", "n_ID"]
- if sliding_window is not None:
- data = sliding_window_average(frame_data, header_columns, sliding_window, sum=window_sum)
- if window is not None:
- data = aggregate_every_3cols(frame_data, header_columns, window_size=window)
- # Filtering the neuron types and activity
- data = data[data["n_type"].isin(neuron_type)]
- data = data[data["resp"].isin(resp_type)]
- data = data.drop(columns=["Threshold", "Amplitude", "Duration", "resp"])
- index_columns = ["Genotype", "ID", "Trial", "Behavior", "n_type", "n_ID"]
- numeric_columns = [c for c in data.columns if c not in header_columns]
- rows = []
- # Creating a pivot DataFrame to obtain a dataframe per frame, each column being a neuron
- for frame in tqdm(numeric_columns):
- frame_data = frame_data[frame_data["Amplitude"] != 0]
- # Training a model for each recording and storing the evaluation metrics in a new DataFrame
- for rec_id in data["ID"].unique():
- rec_data = data[data["ID"] == rec_id].copy()
- rec_data["Neuron"] = rec_data["n_ID"].astype(str) + "_" + rec_data["n_type"].astype(str)
- final_data = rec_data.pivot(index=["Trial", "Behavior"], columns="Neuron", values=frame).reset_index()
- y = final_data["Behavior"]
- X = final_data.drop(columns=["Trial", "Behavior"])
- if db_cv:
- metrics = double_cv(np.array(X), np.array(y), cv_out_fold=4, cv_in_fold=4,
- param_grid={"C": [0.0001, 0.001, 0.01, 0.1, 1], "penalty": ["l2"]},
- scoring_metric="Accuracy", resampler=RandomUnderSampler(random_state=42),
- random_state=42, get_df=False)
- else:
- # Splitting the data into training and test sets (using stratification to preserve class distribution)
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
- if balancing_method == "resampling":
- undersampler = RandomUnderSampler(random_state=42)
- X_train_res, y_train_res = undersampler.fit_resample(X_train, y_train)
- lr = LogisticRegression(solver='lbfgs', C=1, max_iter=5000)
- lr.fit(X_train_res, y_train_res)
- elif balancing_method == "weights":
- # Uses the weight parameter of the LR to balance the weight of samples rather than resampling
- lr = LogisticRegression(solver='lbfgs', class_weight="balanced", C=1, max_iter=5000)
- lr.fit(X_train, y_train)
- elif balancing_method == None:
- lr = LogisticRegression(solver='lbfgs', C=1, max_iter=5000)
- lr.fit(X_train, y_train)
- y_pred = lr.predict(X_test)
- metrics = get_metrics(y_test, y_pred)
- row = {"Genotype": rec_data["Genotype"].values[0], "ID": rec_data["ID"].values[0],
- "Frame": frame, "TPR": metrics["TPR"], "FPR": metrics["FPR"], "Accuracy": metrics["Accuracy"],
- "TPR_shuffle": metrics["TPR_shuffle"], "FPR_shuffle": metrics["FPR_shuffle"], "Accuracy_shuffle": metrics["Accuracy_shuffle"]}
- # "p_hit": metrics["p_hit"], "p_miss": metrics["p_miss"]}
- rows.append(row)
- frame_model_df = pd.DataFrame(rows)
- frame_mean_sem = plot_hit_miss_classif(frame_model_df, title_precision=f"{neuron_type}{resp_type} - Double CV=={db_cv}", timescale_division_factor=(1 if window is None else window))
- # frame_comp = plot_hit_miss_classif_comp(frame_model_df, gp1="WT", gp2="KO-Hypo", title_precision=f"{neuron_type}{resp_type} - db_cv={db_cv}")
- return frame_model_df, frame_mean_sem #, frame_comp
- def get_metrics(y_test, y_pred):
- """
- Compute basic binary classification metrics from test labels and predictions.
- The confusion matrix is constructed with labels ordered as [False, True],
- so that rows correspond to the true class and columns to the predicted class.
- From this, true positives (TP), true negatives (TN), false positives (FP),
- and false negatives (FN) are extracted, and the following metrics are calculated:
- - TPR (True Positive Rate, also called Sensitivity or Recall): TP / (TP + FN).
- - FPR (False Positive Rate): FP / (TN + FP).
- - Accuracy: (TP + TN) / (TP + TN + FP + FN).
- Parameters
- ----------
- y_test : array-like of shape (n_samples,)
- Ground truth binary labels (must contain values interpretable as True/False).
- y_pred : array-like of shape (n_samples,)
- Predicted binary labels.
- Returns
- -------
- metrics : dict
- Dictionary containing:
- - "TPR" : True Positive Rate (recall).
- - "FPR" : False Positive Rate.
- - "Accuracy" : Overall classification accuracy.
- """
- metrics = {}
- cm = confusion_matrix(y_test, y_pred, labels=[False, True])
- TP = cm[1, 1]
- TN = cm[0, 0]
- FP = cm[0, 1]
- FN = cm[1, 0]
- metrics["TPR"] = TP / (TP + FN) # Sensitivity / Recall
- metrics["FPR"] = FP / (TN + FP)
- metrics["Accuracy"] = (TP + TN) / (TP + TN + FP + FN)
- return metrics
- def double_cv(X, y, cv_out_fold=5, cv_in_fold=5, param_grid={"C": [0.0001, 0.001, 0.01, 0.1, 1], "penalty": ["l2"]},
- scoring_metric="Accuracy", resampler=RandomUnderSampler(random_state=42), random_state=42, get_df=False):
- """
- Perform nested (double) cross-validation with logistic regression for hyperparameter tuning,
- class rebalancing, and performance benchmarking against shuffled labels.
- The outer cross-validation loop (cv_out_fold splits) estimates generalization performance,
- while the inner loop (cv_in_fold splits) selects the best hyperparameters from `param_grid`.
- A resampling strategy (default: random undersampling) can be applied to address class imbalance.
- After model fitting, both performance on true labels and a shuffled-label baseline are reported
- to assess whether classification accuracy is above chance.
- Parameters
- ----------
- X : ndarray of shape (n_samples, n_features)
- Input feature matrix.
- y : ndarray of shape (n_samples,)
- Binary labels corresponding to X.
- cv_out_fold : int, default=5
- Number of outer folds for performance evaluation.
- cv_in_fold : int, default=5
- Number of inner folds for hyperparameter tuning.
- param_grid : dict, default={"C": [0.0001, 0.001, 0.01, 0.1, 1], "penalty": ["l2"]}
- Grid of logistic regression hyperparameters to explore.
- scoring_metric : {"Accuracy", "TPR", "FPR"}, default="Accuracy"
- Metric used to select the best hyperparameters during the inner CV.
- resampler : imbalanced-learn resampler or None, default=RandomUnderSampler(random_state=42)
- Strategy to balance classes. If None, no resampling is applied.
- random_state : int, default=42
- Random seed for reproducibility in CV splitting and resampling.
- get_df : bool, default=False
- If True, return the full per-fold DataFrame. If False, return the mean metrics.
- Returns
- -------
- results : dict or pandas.DataFrame
- - If get_df=False: dictionary with mean performance metrics across outer folds, e.g.:
- {"TPR": ..., "FPR": ..., "Accuracy": ..., "TPR_shuffle": ..., "FPR_shuffle": ..., "Accuracy_shuffle": ...}.
- - If get_df=True: DataFrame containing results for each outer fold, including best parameters and both
- true-label and shuffled-label performance.
- Notes
- -----
- - True performance is compared against a shuffled-label baseline, highlighting whether the classifier
- performs better than chance.
- - Metrics are computed using `get_metrics`, which returns TPR (recall), FPR, and Accuracy.
- - Logistic regression models are trained with `max_iter=5000` to ensure convergence.
- """
- y_true = []
- y_pred = []
- cv_out = StratifiedKFold(n_splits=cv_out_fold, random_state=random_state, shuffle=True)
- rows = []
- # Splitting the data into training and validation sets
- for fold_out, (train_index, val_index) in enumerate(cv_out.split(X, y)):
- row = {"Fold": fold_out}
- X_train, X_val, y_train, y_val = X[train_index], X[val_index], y[train_index], y[val_index]
- # Splitting the train data into tuning and tuning assessment group
- cv_in = StratifiedKFold(n_splits=cv_in_fold, random_state=random_state, shuffle=True)
- inner_scores = {}
- param_names = list(param_grid.keys())
- param_combinations = list(product(*[param_grid[p] for p in param_names]))
- for params in param_combinations:
- params_dict = dict(zip(param_names, params))
- # Performing CV to find the best hyperparameters
- fold_scores = []
- for fold_in, (tuning_index, test_index) in enumerate(cv_in.split(X_train, y_train)):
- X_tuning, X_test, y_tuning, y_test = X_train[tuning_index], X_train[test_index], y_train[tuning_index], y_train[test_index]
- # Tuning hyper-parameters on the tuning data set
- model = LogisticRegression(**params_dict, max_iter=5000, random_state=random_state)
- model.fit(X_tuning, y_tuning)
- # Predicting the test set and storing metrics
- y_pred_in = model.predict(X_test)
- metrics = get_metrics(y_test, y_pred_in)
- fold_scores.append(metrics[scoring_metric])
- inner_scores[tuple(params)] = np.mean(fold_scores)
- # Choosing the best parameters from the inner loop
- best_params_tuple = max(inner_scores, key=inner_scores.get)
- best_params = dict(zip(param_names, best_params_tuple))
- row["Best_param"] = best_params
- # Resampling the training set and training the final model with this set
- if resampler is not None:
- X_train_res, y_train_res = resampler.fit_resample(X_train, y_train)
- else:
- X_train_res, y_train_res = X_train, y_train
- final_model = LogisticRegression(**best_params, max_iter=5000, random_state=random_state)
- final_model.fit(X_train_res, y_train_res)
- # Assessing the final model's performance
- y_val_pred = final_model.predict(X_val)
- outer_metrics = get_metrics(y_val, y_val_pred)
- row.update(outer_metrics)
- rows.append(row)
- # 1) Training the model on shuffled labels to compare its performance
- y_train_res_shuffled = y_train_res.copy()
- random.shuffle(y_train_res_shuffled)
- final_model.fit(X_train_res, y_train_res_shuffled)
- # Assessing the final model's performance
- y_val_pred_shuffle = final_model.predict(X_val)
- outer_metrics_shuffle = get_metrics(y_val, y_val_pred_shuffle)
- outer_metrics_shuffle = {f"{k}_shuffle": v for k, v in outer_metrics_shuffle.items()}
- row.update(outer_metrics_shuffle)
- rows.append(row)
- # 2) Rebuilding true and predicted label vectors to compare the obtained results to chance with binomial test
- # y_true.extend(y_val)
- # y_pred.extend(y_val_pred)
- # Computing the chance that the obtained classification is better than chance levels
- # nb_TP = int(np.sum((y_true == 1) & (y_pred == 1)))
- # nb_hit = int(np.sum(y_true))
- # p_hit = np.nan if nb_hit==0 else binomtest(nb_TP, nb_hit, p=0.5, alternative="greater").pvalue
- # print(f"Nb TP: {nb_TP}, Nb hit: {nb_hit}, P: {p_hit}")
- # nb_FP = int(np.sum((y_true == 0) & (y_pred == 1)))
- # nb_miss = int(len(y_true) - nb_hit)
- # p_miss = np.nan if nb_miss==0 else binomtest(nb_FP, nb_miss, p=0.5, alternative="less").pvalue
- # print(f"Nb FP: {nb_FP}, Nb miss: {nb_miss}, P: {p_miss}")
- results_df = pd.DataFrame(rows)
- results_metrics = results_df.mean(numeric_only=True).to_dict()
- # results_metrics.update({"p_hit": p_hit, "p_miss": p_miss})
- return results_df if get_df else results_metrics
- def plot_hit_miss_classif(frame_model_df, title_precision="", shuffle=False, shuffle_frame_significance=False,
- timescale_division_factor=1):
- """
- Plot hit versus miss classification curves from a DataFrame, where each row contains information about TPR and FPR
- for one frame and one animal. The function averages these values across animals of the same genotype and plots
- mean ± SEM curves, optionally comparing to shuffled-label baselines.
- The plot includes:
- - True Positive Rate (TPR, "hit accuracy") and False Positive Rate (FPR, "miss accuracy") across time.
- - Shuffled-label curves (dotted, semi-transparent) if `shuffle=True`.
- - Statistical significance markers (horizontal bars) on frames where real vs. shuffled curves differ, if
- `shuffle_frame_significance=True`.
- - Vertical dashed lines marking stimulus onset (red, at 30/timescale_division_factor) and offset (black, at
- 45/timescale_division_factor).
- - A horizontal dashed line at 0.5, representing chance level.
- - x-axis ticks aligned to -1, -0.5, 0, 0.5, 1 seconds relative to stimulus timing.
- Parameters
- ----------
- frame_model_df : pd.DataFrame
- Input DataFrame containing columns:
- - "Genotype": categorical group label for each animal (e.g., "WT", "KO").
- - "ID": animal identifier.
- - "Frame": time frame index.
- - "TPR", "FPR": classification metrics for each animal and frame.
- Optionally includes "TPR_shuffle" and "FPR_shuffle" if `shuffle=True`.
- title_precision : str, optional
- Extra string appended to the figure title and window title (default = "").
- shuffle : bool, optional
- If True, also plot performance curves from shuffled labels for comparison (default = False).
- shuffle_frame_significance : bool, optional
- If True and `shuffle=True`, performs Wilcoxon signed-rank tests across animals for each frame to
- mark frames where shuffled vs. real data differ significantly (default = False).
- timescale_division_factor : int, optional
- Factor dividing the frame index to adjust x-axis scale (useful for plotting at different time resolutions,
- default = 1).
- Returns
- -------
- pd.DataFrame
- Subset of the input DataFrame corresponding to the last processed genotype, with "Genotype" and "ID" columns
- dropped.
- """
- draw_style = "default" if timescale_division_factor == 1 else "steps-post"
- fill_style = None if timescale_division_factor == 1 else "post"
- if "WT-BMS" in frame_model_df["Genotype"].unique():
- fig, ax = plt.subplots(nrows=1, ncols=4, figsize=(28, 8), constrained_layout=True)
- else:
- fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(21, 8), constrained_layout=True)
- for i, genotype in enumerate(frame_model_df["Genotype"].unique()):
- data = frame_model_df[frame_model_df["Genotype"] == genotype].drop(columns=["Genotype", "ID"])
- # Curves definition
- tpr_mean = data.groupby("Frame")["TPR"].mean().values
- tpr_sem = data.groupby("Frame")["TPR"].sem().values
- fpr_mean = data.groupby("Frame")["FPR"].mean().values
- fpr_sem = data.groupby("Frame")["FPR"].sem().values
- # Curves plotting
- x = np.arange(len(tpr_mean))
- ax[i].plot(x, tpr_mean, label="Hit accuracy", color=sty.color_dict[genotype][0], lw=2, drawstyle=draw_style)
- ax[i].fill_between(x, tpr_mean - tpr_sem, tpr_mean + tpr_sem, color=sty.color_dict[genotype][0], alpha=0.3, step=fill_style)
- ax[i].plot(x, fpr_mean, label="Miss accuracy", color=sty.color_dict[genotype][1], lw=2, drawstyle=draw_style)
- ax[i].fill_between(x, fpr_mean - fpr_sem, fpr_mean + fpr_sem, color=sty.color_dict[genotype][1], alpha=0.3, step=fill_style)
- if shuffle:
- # Shuffle curves definition
- tpr_mean_shuffle = data.groupby("Frame")["TPR_shuffle"].mean().values
- tpr_sem_shuffle = data.groupby("Frame")["TPR_shuffle"].sem().values
- fpr_mean_shuffle = data.groupby("Frame")["FPR_shuffle"].mean().values
- fpr_sem_shuffle = data.groupby("Frame")["FPR_shuffle"].sem().values
- # Shuffle curves plotting
- x = np.arange(len(tpr_mean))
- ax[i].plot(x, tpr_mean_shuffle, label="Hit accuracy", color=sty.color_dict[genotype][0], lw=1, ls="dotted", alpha=0.25, drawstyle=draw_style)
- ax[i].fill_between(x, tpr_mean_shuffle - tpr_sem_shuffle, tpr_mean_shuffle + tpr_sem_shuffle, color=sty.color_dict[genotype][0], alpha=0.1, step=fill_style)
- ax[i].plot(x, fpr_mean_shuffle, label="Miss accuracy", color=sty.color_dict[genotype][1], lw=1, ls="dotted", alpha=0.25, drawstyle=draw_style)
- ax[i].fill_between(x, fpr_mean_shuffle - fpr_sem_shuffle, fpr_mean_shuffle + fpr_sem_shuffle, color=sty.color_dict[genotype][1], alpha=0.1, step=fill_style)
- if shuffle_frame_significance:
- # Computing the statistical difference with shuffle
- pvals = {}
- for frame in data.Frame.unique():
- real_tpr = data[data.Frame == frame]["TPR"]
- shuffle_tpr = data[data.Frame == frame]["TPR_shuffle"]
- real_fpr = data[data.Frame == frame]["FPR"]
- shuffle_fpr = data[data.Frame == frame]["FPR_shuffle"]
- # paired test across animals
- # drop any animals missing one of the two
- idx = real_tpr.index.intersection(shuffle_tpr.index)
- if len(idx) >= 3:
- stat, p_tpr = wilcoxon(real_tpr.loc[idx], shuffle_tpr.loc[idx], alternative="greater")
- stat, p_fpr = wilcoxon(real_fpr.loc[idx], shuffle_fpr.loc[idx], alternative="less")
- else:
- p_tpr = np.nan
- p_fpr = np.nan
- pvals[frame] = [p_tpr, p_fpr]
- # Drawing a bar above significantly different frames
- for f, p_list in pvals.items():
- p_tpr = p_list[0]
- p_fpr = p_list[1]
- if p_tpr < 0.05:
- # draw a tiny horizontal line spanning the width of one frame
- ax[i].hlines(0.95, f - 0.4, f + 0.4, color=sty.color_dict[genotype][0], linewidth=2)
- if p_fpr < 0.05:
- # draw a tiny horizontal line spanning the width of one frame
- ax[i].hlines(0.05, f - 0.4, f + 0.4, color=sty.color_dict[genotype][1], linewidth=2)
- # Delimitation of the stimulus period and chance level
- ax[i].axvline(x=30/timescale_division_factor, ls="--", lw=1, color="red")
- ax[i].axvline(x=45/timescale_division_factor, ls="--", lw=1, color="black")
- ax[i].axhline(y=0.5, ls="--", lw=1, color="gray")
- # Title and axes formatting
- ax[i].set_title(genotype, color=sty.color_dict[genotype][0], fontsize=20)
- ax[i].set_ylim(0, 1)
- ax[i].set_xlabel("Time (s)")
- ax[i].set_xticks(np.divide([0, 15, 30, 45, 60], timescale_division_factor))
- ax[i].set_xticklabels([-1, -0.5, 0, 0.5, 1])
- fig.suptitle(f"Hit versus Miss classification graph using the mean ΔF/F\n{title_precision}", fontsize=20)
- fig.canvas.manager.set_window_title(f"Hit_Miss_classif_{title_precision}")
- plt.show()
- return data
- def plot_hit_miss_classif_comp(frame_model_df, gp1="WT", gp2="KO-Hypo", title_precision=""):
- """
- Compare hit accuracy (TPR) and miss error (FPR) between two genotypes across stimulus-related periods,
- and evaluate real vs. shuffled performance within each genotype. This function generates two figures:
- 1. **Genotype comparison figure (2x4 subplots)**
- - Top row: boxplots comparing hit accuracy (TPR) between `gp1` and `gp2` across four time windows.
- - Bottom row: boxplots comparing miss error (FPR) between `gp1` and `gp2`.
- - Colors follow `sty.color_dict`, different per genotype.
- - Significance markers can be displayed depending on the boxplot utility function (`ppt.boxplot`).
- 2. **Shuffle comparison figure (4x4 subplots)**
- - For each of the four periods, plots real vs. shuffled TPR and FPR separately for `gp1` (rows 0–1)
- and `gp2` (rows 2–3).
- - Uses paired comparisons (real vs. shuffled for the same animals).
- - Real data colored by genotype, shuffled data in gray.
- - Significance markers are drawn if differences are detected.
- The time windows analyzed are:
- - `"stim"`: full stimulus window (frames 30–45).
- - `"start_stim (250ms)"`: early stimulus subwindow (frames 30–37).
- - `"end_stim (250ms)"`: late stimulus subwindow (frames 37–45).
- - `"pre_stim (200ms)"`: baseline window before stimulus onset (frames 24–30).
- Parameters
- ----------
- frame_model_df : pd.DataFrame
- Input DataFrame containing columns:
- - "Genotype": categorical labels for each animal (e.g., "WT", "KO-Hypo").
- - "ID": animal identifier.
- - "Frame": time frame index.
- - "TPR", "FPR": classification metrics per frame.
- - "TPR_shuffle", "FPR_shuffle": shuffled baseline metrics for comparison.
- gp1 : str, optional
- Name of the first genotype group (default = "WT").
- gp2 : str, optional
- Name of the second genotype group (default = "KO-Hypo").
- title_precision : str, optional
- Extra string appended to figure and window titles (default = "").
- Returns
- -------
- dict of pd.DataFrame
- Dictionary mapping each analyzed period ("stim", "start_stim (250ms)", "end_stim (250ms)",
- "pre_stim (200ms)") to the aggregated DataFrame containing mean values per animal and genotype.
- """
- period_dict = {"stim": [30, 45], "start_stim (250ms)": [30, 37], "end_stim (250ms)": [37, 45], "pre_stim (200ms)": [24, 30]}
- fig, ax = plt.subplots(nrows=2, ncols=4, figsize=(24, 16), constrained_layout=True)
- fig_shuf, ax_shuf = plt.subplots(nrows=4, ncols=4, figsize=(24, 32), constrained_layout=True)
- data_dict = {}
- for col, period in enumerate(period_dict.keys()):
- start, end = period_dict[period]
- data = frame_model_df[frame_model_df["Frame"].isin(range(start, end))].groupby(["Genotype", "ID"], as_index=False).mean().drop(columns="Frame")
- # Plotting the comparison of accuracy between genotypes
- ppt.boxplot(ax[0, col], data[data["Genotype"] == gp1]["TPR"], data[data["Genotype"] == gp2]["TPR"], ylabel="Hit accuracy",
- paired=False, title=period, ylim=[0, 1], colors=[sty.color_dict[gp1][0], sty.color_dict[gp2][0]], det_marker=True, force_markers_identity=True)
- ppt.boxplot(ax[1, col], data[data["Genotype"] == gp1]["FPR"], data[data["Genotype"] == gp2]["FPR"], ylabel="Miss error",
- paired=False, title=period, ylim=[0, 1], colors=[sty.color_dict[gp1][1], sty.color_dict[gp2][1]], det_marker=False, force_markers_identity=False)
- # Plotting the comparisons with shuffled data
- ppt.boxplot(ax_shuf[0, col], data[data["Genotype"] == gp1]["TPR"].values, data[data["Genotype"] == gp1]["TPR_shuffle"].values, ylabel="Hit accuracy",
- paired=True, title=f"{period}\n{gp1}: Real vs. Shuffled", ylim=[0, 1], colors=[sty.color_dict[gp1][0], "darkgray"], det_marker=True, force_markers_identity=True)
- ppt.boxplot(ax_shuf[1, col], data[data["Genotype"] == gp1]["FPR"].values, data[data["Genotype"] == gp1]["FPR_shuffle"].values, ylabel="Miss error",
- paired=True, title=f"{period}\n{gp1}: Real vs. Shuffled", ylim=[0, 1], colors=[sty.color_dict[gp1][1], "gray"], det_marker=False, force_markers_identity=True)
- ppt.boxplot(ax_shuf[2, col], data[data["Genotype"] == gp2]["TPR"].values, data[data["Genotype"] == gp2]["TPR_shuffle"].values, ylabel="Hit accuracy",
- paired=True, title=f"{period}\n{gp2}: Real vs. Shuffled", ylim=[0, 1], colors=[sty.color_dict[gp2][0], "darkgray"], det_marker=True, force_markers_identity=True)
- ppt.boxplot(ax_shuf[3, col], data[data["Genotype"] == gp2]["FPR"].values, data[data["Genotype"] == gp2]["FPR_shuffle"].values, ylabel="Miss error",
- paired=True, title=f"{period}\n{gp2}: Real vs. Shuffled", ylim=[0, 1], colors=[sty.color_dict[gp2][1], "gray"], det_marker=False, force_markers_identity=True)
- data_dict[period] = data
- fig.suptitle(f"Hit accuracy miss error comparison ({gp1}/{gp2})\n{title_precision}", fontsize=20)
- fig.canvas.manager.set_window_title(f"Hit_Miss_classif_comp_{gp1}_{gp2}_{title_precision}")
- fig_shuf.suptitle(f"Shuffle comparison\n{title_precision}", fontsize=20)
- fig_shuf.canvas.manager.set_window_title(f"Hit_Miss_classif_comp_shuffle{title_precision}")
- # plt.savefig(f"Z:/Current_members/Ourania_Semelidou/2p/Figures_paper & submissions/202507/14/shuffle_{gp1}_{gp2}.pdf", format="pdf")
- # plt.savefig(f"Z:/Current_members/Ourania_Semelidou/2p/Figures_paper & submissions/202507/5_Figure2/shuffle_{gp1}_{gp2}.pdf", format="pdf")
- plt.show()
- return data_dict
- def anova_accuracy(frame_model_df):
- """
- Perform one-way ANOVA on hit accuracy (TPR) and miss error (FPR)
- between genotypes during the stimulus window.
- The function selects the `"stim"` period (frames 30–45), averages metrics
- across frames for each animal, and tests for genotype effects using ANOVA.
- Results are printed to console for both hit accuracy and miss error.
- Time windows available in the dataset (but only `"stim"` is used here):
- - `"stim"`: full stimulus window (frames 30–45).
- - `"start_stim (250ms)"`: early stimulus subwindow (frames 30–37).
- - `"end_stim (250ms)"`: late stimulus subwindow (frames 37–45).
- - `"pre_stim (200ms)"`: baseline window before stimulus onset (frames 24–30).
- Parameters
- ----------
- frame_model_df : pd.DataFrame
- Input DataFrame containing columns:
- - "Genotype": categorical labels for each animal (e.g., "WT", "KO-Hypo").
- - "ID": animal identifier.
- - "Frame": time frame index.
- - "TPR", "FPR": classification metrics per frame.
- Returns
- -------
- pd.DataFrame
- Aggregated DataFrame with mean values per animal and genotype across the `"stim"` window.
- """
- period_dict = {"stim": [30, 45], "start_stim (250ms)": [30, 37], "end_stim (250ms)": [37, 45],
- "pre_stim (200ms)": [24, 30]}
- start, end = period_dict["stim"]
- data = frame_model_df[frame_model_df["Frame"].isin(range(start, end))].groupby(["Genotype", "ID"], as_index=False).mean().drop(columns="Frame")
- aov_hit = pg.anova(data=data, dv="TPR", between="Genotype")
- aov_miss = pg.anova(data=data, dv="FPR", between="Genotype")
- print("=== Hit accuracy ===")
- print(aov_hit)
- print("=== Miss error ===")
- print(aov_miss)
- return data
- def compare_accuracy(recs, random=42):
- """
- Train logistic regression classifiers on excitatory (EXC), inhibitory (INH),
- and combined (EXC+INH) neuronal populations to compare decoding accuracy of stimulus detection.
- For each recording, the function extracts mean z-scored activity of EXC and INH neurons
- during the stimulus period. Logistic regression is trained on balanced subsets of the
- training data using random undersampling to avoid class imbalance bias.
- Models are evaluated using 4-fold stratified cross-validation, with accuracies computed for:
- - All neurons (EXC + INH).
- - EXC neurons only.
- - INH neurons only.
- Accuracies are aggregated across folds, compared within and across genotypes,
- and visualized as boxplots:
- - Within-genotype comparisons (all vs. EXC, all vs. INH, EXC vs. INH).
- - Between-genotype comparisons (WT vs. KO-Hypo for each neuron subset).
- Parameters
- ----------
- recs : list
- List of recording objects, each providing:
- - `.get_estimated_activity(zscore=True, n_type="EXC"/"INH", estimator="mean", real_duration=True)`
- → matrix of neuronal activity (neurons × time).
- - `.detected_stim` → binary stimulus detection labels per trial.
- - `.filename` → identifier for the recording.
- - `.genotype` → genotype label (e.g., "WT", "KO-Hypo").
- random : int, default=42
- Random seed for reproducibility in cross-validation splits
- and random undersampling.
- Returns
- -------
- pd.DataFrame
- Aggregated DataFrame containing mean accuracy values per recording and genotype with columns:
- - "Genotype": genotype label.
- - "ID": recording identifier.
- - "acc_all": accuracy with EXC+INH neurons.
- - "acc_exc": accuracy with EXC neurons only.
- - "acc_inh": accuracy with INH neurons only.
- """
- rows = []
- skf = StratifiedKFold(n_splits=4, shuffle=True, random_state=random)
- for rec in recs:
- # Retrieving the neuronal activity
- exc_mat = rec.get_estimated_activity(zscore=True, n_type="EXC", estimator="mean", real_duration=True)
- inh_mat = rec.get_estimated_activity(zscore=True, n_type="INH", estimator="mean", real_duration=True)
- full_mat = np.concatenate([exc_mat, inh_mat], axis=0).T
- nb_exc = exc_mat.shape[0]
- y = rec.detected_stim
- for fold, (train_idx, test_idx) in enumerate(skf.split(full_mat, y), start=1):
- # X_train, X_test, y_train, y_test = train_test_split(full_mat, y, test_size=0.2, stratify=y, random_state=random)
- X_train, X_test = full_mat[train_idx], full_mat[test_idx]
- y_train, y_test = y[train_idx], y[test_idx]
- X_res, y_res = RandomUnderSampler(random_state=random).fit_resample(X_train, y_train)
- model = LogisticRegression(max_iter=5000, random_state=random)
- # Fitting the model with all neurons
- model.fit(X_res, y_res)
- y_pred_all = model.predict(X_test)
- acc_all = get_metrics(y_test, y_pred_all)["Accuracy"]
- # Fitting the model with EXC neurons
- model.fit(X_res[:, :nb_exc], y_res)
- y_pred_exc = model.predict(X_test[:, :nb_exc])
- acc_exc = get_metrics(y_test, y_pred_exc)["Accuracy"]
- # Fitting the model with INH neurons
- model.fit(X_res[:, nb_exc:], y_res)
- y_pred_inh = model.predict(X_test[:, nb_exc:])
- acc_inh = get_metrics(y_test, y_pred_inh)["Accuracy"]
- rows.append({"ID": rec.filename, "Genotype": rec.genotype, "Fold": fold, "acc_all": acc_all, "acc_exc": acc_exc, "acc_inh": acc_inh})
- full_data = pd.DataFrame(rows)
- data = full_data.groupby(["Genotype", "ID"], as_index=False).mean()
- fig, ax = plt.subplots(nrows=3, ncols=4, figsize=(24, 24), constrained_layout=True)
- for row, (group, gp_label) in enumerate(zip([data, data[data["Genotype"] == "WT"], data[data["Genotype"] == "KO-Hypo"]],
- ["all", "WT", "KO-Hypo"])):
- ppt.boxplot(ax[row, 0], group["acc_all"].values, group["acc_exc"].values, ylabel="Accuracy", paired=True, title=f"All neurons/EXC ({gp_label})", ylim=[0, 1],
- colors=[sty.exc_inh_color, sty.exc_color], det_marker=False, force_markers_identity=False)
- ppt.boxplot(ax[row, 1], group["acc_all"].values, group["acc_inh"].values, ylabel="Accuracy", paired=True, title=f"All neurons/INH ({gp_label})", ylim=[0, 1],
- colors=[sty.exc_inh_color, sty.inh_color], det_marker=False, force_markers_identity=False)
- ppt.boxplot(ax[row, 2], group["acc_exc"].values, group["acc_inh"].values, ylabel="Accuracy", paired=True, title=f"EXC/INH ({gp_label})", ylim=[0, 1],
- colors=[sty.exc_color, sty.inh_color], det_marker=False, force_markers_identity=False)
- wt = data[data["Genotype"] == "WT"]
- hypo = data[data["Genotype"] == "KO-Hypo"]
- for row, acc_type in enumerate(["acc_all", "acc_exc", "acc_inh"]):
- ppt.boxplot(ax[row, 3], wt[acc_type].values, hypo[acc_type].values, ylabel="Accuracy", paired=False, title=f"WT/KO-Hypo ({acc_type})", ylim=[0, 1],
- colors=[sty.wt_color, sty.hypo_color], det_marker=False, force_markers_identity=False)
- fig.suptitle("Comparison of decoding accuracy between neuron types")
- fig.canvas.manager.set_window_title(f"Accuracy n_types")
- # plt.show()
- return data
- def correlate_nb_accuracy(recs, accuracy_df, threshold="median"):
- """
- Correlate decoding accuracy with the number of neurons (EXC, INH, and total)
- and compare neuron counts between high- and low-performing animals.
- For each recording, the function retrieves the number of excitatory (EXC) and
- inhibitory (INH) neurons and adds them to the accuracy DataFrame. Linear regression
- analyses are performed between the number of neurons and decoding accuracy
- for EXC, INH, and combined populations. The plots include scatter points grouped
- by genotype, regression lines, and annotations of R² and p-values.
- Additionally, the function splits animals into “low” and “high” performers
- based on an accuracy threshold (median, mean, middle of the range, or a custom float).
- Boxplots compare the number of neurons between low- and high-accuracy groups
- for each metric (acc_exc, acc_inh, acc_all).
- Parameters
- ----------
- recs : dict
- Dictionary mapping recording IDs to recording objects. Each recording object must provide:
- - `.zscore_exc.shape[0]`: number of excitatory neurons.
- - `.zscore_inh.shape[0]`: number of inhibitory neurons.
- accuracy_df : pd.DataFrame
- DataFrame with decoding accuracy results per recording.
- Must include columns:
- - "ID": recording identifier (matching keys in `recs`).
- - "Genotype": genotype label.
- - "acc_exc": decoding accuracy using EXC neurons.
- - "acc_inh": decoding accuracy using INH neurons.
- - "acc_all": decoding accuracy using all neurons.
- threshold : {"median", "mean", "middle"} or float, default="median"
- Method for splitting recordings into low- and high-accuracy groups:
- - "median": median accuracy across animals.
- - "mean": mean accuracy across animals.
- - "middle": midpoint between min and max accuracy.
- - float: user-defined threshold value.
- Returns
- -------
- pd.DataFrame
- Updated accuracy DataFrame including new columns:
- - "n_EXC": number of excitatory neurons per recording.
- - "n_INH": number of inhibitory neurons per recording.
- - "n_all": total number of neurons per recording.
- """
- accuracy_df["n_EXC"] = accuracy_df["ID"].map(lambda id_: recs[id_].zscore_exc.shape[0])
- accuracy_df["n_INH"] = accuracy_df["ID"].map(lambda id_: recs[id_].zscore_inh.shape[0])
- accuracy_df["n_all"] = accuracy_df["n_EXC"] + accuracy_df["n_INH"]
- def plot_lin_reg(ax, data, x_col=None, y_col=None, id_col="ID", group_col=None,
- title=None, xlab=None, ylab=None, colors=None, line_color="red", id_display=True):
- if colors is None:
- colors = {"WT": sty.wt_color, "KO": sty.ko_color, "KO-Hypo": sty.hypo_color}
- xlab = xlab or x_col
- ylab = ylab or y_col
- # Correlation
- results = dict(linregress(data[x_col], data[y_col])._asdict())
- r2 = results["rvalue"] ** 2
- line = results["slope"] * data[x_col] + results["intercept"]
- # Plot the data points and regression line
- ax.plot(data[x_col], line, color=line_color, lw=2)
- if group_col is not None:
- for g in sorted(data[group_col].unique()):
- group = data[data[group_col] == g]
- sc = ax.scatter(group[x_col], group[y_col], color=colors[g], alpha=0.7, label=g, s=10, marker="+")
- if id_display:
- # Save the IDs for this group so that they can be accessed in the callback.
- ids = group[id_col].values
- mplcursors.cursor(sc, hover=True).connect("add", lambda sel, ids=ids: (sel.annotation.set_text(f"ID: {ids[sel.index]}"), sel.annotation.set_fontsize(8)))
- else:
- sc = ax.scatter(data[x_col], data[y_col], color=colors[g], alpha=0.7, label=g, s=10, marker="+")
- if id_display:
- ids = data["ID"].values
- mplcursors.cursor(sc, hover=True).connect("add", lambda sel, ids=ids: (sel.annotation.set_text(f"ID: {ids[sel.index]}"), sel.annotation.set_fontsize(8)))
- # Annotate the plot with R² and p-value
- ax.text(0.05, 0.95, f"$r^2 = {r2:.3f}$\np-value = {results["pvalue"]:.3f}", transform=ax.transAxes, fontsize=8, verticalalignment="top", color="black")
- ax.set_title(title, fontsize=12)
- ax.set_xlabel(xlab, fontsize=10)
- ax.set_ylabel(ylab, fontsize=10)
- for lbl in ax.get_xticklabels() + ax.get_yticklabels():
- lbl.set_fontsize(8)
- return results
- fig, ax = plt.subplots(nrows=4, ncols=3, figsize=(18, 24), constrained_layout=True)
- plot_lin_reg(ax[0 ,0], accuracy_df, x_col="n_EXC", y_col="acc_exc", group_col="Genotype", title="EXC")
- plot_lin_reg(ax[0 ,1], accuracy_df, x_col="n_INH", y_col="acc_inh", group_col="Genotype", title="INH")
- plot_lin_reg(ax[0 ,2], accuracy_df, x_col="n_all", y_col="acc_all", group_col="Genotype", title="All")
- # Comparing the number of neurons between individuals with high and low accuracy
- for col, accuracy_metric in enumerate(["acc_exc", "acc_inh", "acc_all"]):
- if threshold == "median":
- t_val = np.percentile(accuracy_df[accuracy_metric].values, 50)
- elif threshold == "mean":
- t_val = np.mean(accuracy_df[accuracy_metric].values)
- elif threshold == "middle":
- t_val = (max(accuracy_df[accuracy_metric].values) + min(accuracy_df[accuracy_metric].values))/2
- elif isinstance(threshold, float):
- t_val = threshold
- ax[0, col].axhline(y=t_val, linestyle="--", color="gray", lw=0.5)
- low_perf = accuracy_df[accuracy_df[accuracy_metric] < t_val]
- high_perf = accuracy_df[accuracy_df[accuracy_metric] >= t_val]
- ppt.boxplot(ax[1, col], low_perf["n_EXC"].values, high_perf["n_EXC"].values, ylabel="n_EXC", paired=False,
- title=f"{accuracy_metric} Low/High acc", ylim=[],
- colors=[sty.exc_color, sty.exc_color], det_marker=False, force_markers_identity=False)
- ppt.boxplot(ax[2, col], low_perf["n_INH"].values, high_perf["n_INH"].values, ylabel="n_INH", paired=False,
- title=f"{accuracy_metric} Low/High acc", ylim=[],
- colors=[sty.inh_color, sty.inh_color], det_marker=False, force_markers_identity=False)
- ppt.boxplot(ax[3, col], low_perf["n_all"].values, high_perf["n_all"].values, ylabel="n_all", paired=False,
- title=f"{accuracy_metric} Low/High acc", ylim=[],
- colors=[sty.exc_inh_color, sty.exc_inh_color], det_marker=False, force_markers_identity=False)
- fig.suptitle(f"Correlation of the model accuracy with the number of neurons.\nComparison of the number of neurons "
- f"between high and low accuracy animal (threshold={threshold})", fontsize=12)
- fig.canvas.manager.set_window_title("Corr acc nb neurons")
- plt.show()
- return accuracy_df
- def correlate_frame_accuracy_metrics(frame_model_df, features_df, period="stim"):
- """
- Correlates model accuracy during a specific time period with neuronal features to assess
- which features carry the most information about decoding performance.
- The function computes the mean model accuracy (TPR, FPR, and overall Accuracy) over
- a selected period (`stim`, `start`, `end`, or `pre_stim`), then matches these accuracies
- with averaged neuronal features. Only responsive neurons (amplitude != 0) are considered
- when extracting features. Two types of feature DataFrames are created:
- - behavior-specific (features computed separately for hit and miss trials),
- - global (features aggregated across all trials for each animal).
- These features are then correlated with model accuracy metrics, both globally and within
- genotypes (WT and KO-Hypo). Scatter plots with regression lines and R² / p-value annotations
- are generated for each feature-accuracy pair across trial types (All, Hit, Miss).
- Parameters
- ----------
- frame_model_df : pandas.DataFrame
- DataFrame containing frame-wise model results with columns such as
- "Frame", "Genotype", "ID", "TPR", "FPR", "Accuracy".
- features_df : pandas.DataFrame
- DataFrame containing neuronal features with columns like
- "ID", "Genotype", "behavior", "bounded_x0", "amplitude", "threshold", etc.
- Only neurons with nonzero amplitude are considered.
- period : str, optional
- The time window over which model accuracy is averaged. Options are:
- - "stim" (default) : frames 30–45
- - "start" : frames 30–37
- - "end" : frames 37–45
- - "pre_stim" : frames 24–30
- Returns
- -------
- data : pandas.DataFrame
- Merged DataFrame of averaged features and model accuracy for each animal,
- with behavior-specific separation where applicable.
- results : pandas.DataFrame
- Summary of correlation statistics (R² and p-values) for each feature-accuracy pair,
- computed globally, for WT only, and for KO-Hypo only. This can be used to identify
- which neuronal features explain variance in decoding accuracy.
- """
- # Getting a Dataframe of the model mean accuracy during stim, one row per animal
- period_dict = {"stim": [30, 45], "start": [30, 37], "end": [37, 45], "pre_stim": [24, 30]}
- start, end = period_dict[period]
- data_acc = frame_model_df[frame_model_df["Frame"].isin(range(start, end))].groupby(["Genotype", "ID"], as_index=False).mean().drop(columns="Frame")
- # Getting the features DataFrame, defining a single metric per animal, only responsive neurons are considered
- data_features = features_df[features_df.amplitude != 0].groupby(["ID", "Genotype", "behavior"], as_index=False).mean().drop(columns=["bounded_x0", "amplitude", "threshold"])
- data_features_all = features_df[features_df.amplitude != 0].drop(columns="behavior").groupby(["ID", "Genotype"], as_index=False).mean().drop(columns=["bounded_x0", "amplitude", "threshold"])
- data = data_features.merge(data_acc[["ID", "TPR", "FPR", "Accuracy"]], on="ID", how="left")
- data_hit = data[data.behavior == True]
- data_miss = data[data.behavior == False]
- data_all = data_features_all.merge(data_acc[["ID", "TPR", "FPR", "Accuracy"]], on="ID", how="left")
- cols_id = ["ID", "Genotype", "behavior"]
- cols_acc = ["Accuracy", "TPR", "FPR"]
- cols_features = [col for col in data.columns if ((col not in cols_id) and (col not in cols_acc))]
- # Plotting the correlation of the different features with the model accuracy for all genotypes
- fig, ax = plt.subplots(nrows=9, ncols=20, figsize=(100, 45), constrained_layout=True)
- rows = []
- for row, (data, trial_type) in enumerate(zip([data_all, data_hit, data_miss], ["All", "Hit", "Miss"])):
- for sub_row, acc in enumerate(cols_acc):
- for col, feature in enumerate(cols_features):
- # Correlation global
- y_col = data[acc]
- x_col = data[feature]
- results = dict(linregress(x_col, y_col)._asdict())
- r2 = results["rvalue"] ** 2
- line = results["slope"] * x_col + results["intercept"]
- # Correlation WT
- y_col_wt = data[data.Genotype == "WT"][acc]
- x_col_wt = data[data.Genotype == "WT"][feature]
- results_wt = dict(linregress(x_col_wt, y_col_wt)._asdict())
- r2_wt = results_wt["rvalue"] ** 2
- line_wt = results_wt["slope"] * x_col_wt + results_wt["intercept"]
- # Correlation KO-Hypo
- y_col_hypo = data[data.Genotype == "KO-Hypo"][acc]
- x_col_hypo = data[data.Genotype == "KO-Hypo"][feature]
- results_hypo = dict(linregress(x_col_hypo, y_col_hypo)._asdict())
- r2_hypo = results_hypo["rvalue"] ** 2
- line_hypo = results_hypo["slope"] * x_col_hypo + results_hypo["intercept"]
- # Plot the data points and regression lines
- ax[3 * row + sub_row, col].plot(x_col, line, color="black", lw=2)
- ax[3 * row + sub_row, col].plot(x_col_wt, line_wt, color=sty.wt_color, lw=2)
- ax[3 * row + sub_row, col].plot(x_col_hypo, line_hypo, color=sty.hypo_color, lw=2)
- for g in sorted(data["Genotype"].unique()):
- group = data[data["Genotype"] == g]
- sc = ax[3 * row + sub_row, col].scatter(group[feature], group[acc], color=sty.color_dict[g][0], alpha=0.7, s=10, marker="+")
- # Annotate the plot with R² and p-value
- ax[3 * row + sub_row, col].text(0.05, 0.95, f"$r^2={r2:.3f}$ p-val={results["pvalue"]:.3f}", transform=ax[3 * row + sub_row, col].transAxes, fontsize=8, verticalalignment="top", color="black")
- ax[3 * row + sub_row, col].text(0.05, 0.90, f"$r^2={r2_wt:.3f}$ p-val={results_wt["pvalue"]:.3f}", transform=ax[3 * row + sub_row, col].transAxes, fontsize=8, verticalalignment="top", color=sty.wt_color)
- ax[3 * row + sub_row, col].text(0.05, 0.85, f"$r^2={r2_hypo:.3f}$ p-val={results_hypo["pvalue"]:.3f}", transform=ax[3 * row + sub_row, col].transAxes, fontsize=8, verticalalignment="top", color=sty.hypo_color)
- ax[3 * row + sub_row, col].set_title(f"{trial_type} trials", fontsize=10)
- ax[3 * row + sub_row, col].set_xlabel(feature, fontsize=10)
- ax[3 * row + sub_row, col].set_ylabel(acc, fontsize=10)
- ax[3 * row + sub_row, col].set_ylim(ymin=0, ymax=1)
- ax[3 * row + sub_row, col].tick_params(axis='both', which='major', labelsize=5)
- rows.append({"Trials": trial_type, "Acc_metric": acc, "Feature": feature,
- "r2": r2, "pval": results["pvalue"],
- "r2_wt": r2_wt, "pval_wt": results_wt["pvalue"],
- "r2_hypo": r2_hypo, "pval_hypo": results_hypo["pvalue"]})
- results = pd.DataFrame(rows)
- fig.suptitle(f"Correlation of the frame model accuracy during the whole {period} period with the different metrics computed on responsive neurons")
- # plt.savefig(f"{server_address}/response_decoding/corr_acc_model_features_{period}.pdf", format="pdf")
- return data, results
- def correlate_frame_accuracy_ntn_df(frame_model_df, ntn_df, period="stim"):
- """
- Correlates frame-wise model accuracy with neuron-to-neuron cosine similarity (ntn_cosim) metrics.
- For each animal, the mean model accuracy (TPR, FPR, and overall Accuracy) is computed over a
- specified period (`stim`, `start`, `end`, or `pre_stim`). These accuracies are then merged
- with the ntn_cosim metrics from `ntn_df`. Data is separated into trial types: "All", "Hit",
- and "Miss".
- Correlation analyses are performed for each feature in `ntn_df`:
- - Globally across all animals.
- - Within WT genotype only.
- - Within KO-Hypo genotype only.
- Linear regression lines are plotted, and R² and p-values are annotated for each feature-accuracy pair.
- Scatter points are colored by genotype.
- Parameters
- ----------
- frame_model_df : pandas.DataFrame
- DataFrame containing frame-wise model results with columns such as
- "Frame", "Genotype", "ID", "TPR", "FPR", "Accuracy".
- ntn_df : pandas.DataFrame
- DataFrame containing neuron-to-neuron cosine similarity metrics, with columns
- including "ID", "Genotype", "Behavior", and one or more feature columns.
- period : str, optional
- Time window over which model accuracy is averaged. Options:
- - "stim" (default) : frames 30–45
- - "start" : frames 30–37
- - "end" : frames 37–45
- - "pre_stim" : frames 24–30
- Returns
- -------
- data : pandas.DataFrame
- Merged DataFrame of ntn_cosim features and model accuracy for each animal, separated by
- trial type.
- results : pandas.DataFrame
- Summary of correlation statistics (R² and p-values) for each feature-accuracy pair,
- computed globally, for WT only, and for KO-Hypo only.
- """
- # Getting a Dataframe of the model mean accuracy during stim, one row per animal
- period_dict = {"stim": [30, 45], "start": [30, 37], "end": [37, 45], "pre_stim": [24, 30]}
- start, end = period_dict[period]
- data_acc = frame_model_df[frame_model_df["Frame"].isin(range(start, end))].groupby(["Genotype", "ID"], as_index=False).mean().drop(columns="Frame")
- # Getting the features DataFrame, defining a single metric per animal, only responsive neurons are considered
- data = ntn_df.merge(data_acc[["ID", "TPR", "FPR", "Accuracy"]], on="ID", how="left").drop(columns=["Threshold"])
- data_hit = data[data.Behavior == "Hit"]
- data_miss = data[data.Behavior == "Miss"]
- data_all = data[data.Behavior == "All"]
- cols_id = ["ID", "Genotype", "Behavior"]
- cols_acc = ["Accuracy", "TPR", "FPR"]
- cols_features = [col for col in data.columns if ((col not in cols_id) and (col not in cols_acc))]
- n_features = len(cols_features)
- # Plotting the correlation of the different features with the model accuracy for all genotypes
- fig, ax = plt.subplots(nrows=9, ncols=n_features, figsize=(n_features * 5, 45), constrained_layout=True)
- rows = []
- for row, (data, trial_type) in enumerate(zip([data_all, data_hit, data_miss], ["All", "Hit", "Miss"])):
- for sub_row, acc in enumerate(cols_acc):
- for col, feature in enumerate(cols_features):
- # Correlation global
- y_col = data[acc]
- x_col = data[feature]
- results = dict(linregress(x_col, y_col)._asdict())
- r2 = results["rvalue"] ** 2
- line = results["slope"] * x_col + results["intercept"]
- # Correlation WT
- y_col_wt = data[data.Genotype == "WT"][acc]
- x_col_wt = data[data.Genotype == "WT"][feature]
- results_wt = dict(linregress(x_col_wt, y_col_wt)._asdict())
- r2_wt = results_wt["rvalue"] ** 2
- line_wt = results_wt["slope"] * x_col_wt + results_wt["intercept"]
- # Correlation KO-Hypo
- y_col_hypo = data[data.Genotype == "KO-Hypo"][acc]
- x_col_hypo = data[data.Genotype == "KO-Hypo"][feature]
- results_hypo = dict(linregress(x_col_hypo, y_col_hypo)._asdict())
- r2_hypo = results_hypo["rvalue"] ** 2
- line_hypo = results_hypo["slope"] * x_col_hypo + results_hypo["intercept"]
- # Defining the axis
- if n_features == 1:
- axis = ax[3 * row + sub_row]
- else:
- axis = ax[3 * row + sub_row, col]
- # Plot the data points and regression lines
- axis.plot(x_col, line, color="black", lw=2)
- axis.plot(x_col_wt, line_wt, color=sty.wt_color, lw=2)
- axis.plot(x_col_hypo, line_hypo, color=sty.hypo_color, lw=2)
- for g in sorted(data["Genotype"].unique()):
- group = data[data["Genotype"] == g]
- sc = axis.scatter(group[feature], group[acc], color=sty.color_dict[g][0], alpha=0.7, s=10, marker="+")
- # Annotate the plot with R² and p-value
- axis.text(0.05, 0.95, f"$r^2={r2:.3f}$ p-val={results["pvalue"]:.3f}", transform=axis.transAxes, fontsize=8, verticalalignment="top", color="black")
- axis.text(0.05, 0.90, f"$r^2={r2_wt:.3f}$ p-val={results_wt["pvalue"]:.3f}", transform=axis.transAxes, fontsize=8, verticalalignment="top", color=sty.wt_color)
- axis.text(0.05, 0.85, f"$r^2={r2_hypo:.3f}$ p-val={results_hypo["pvalue"]:.3f}", transform=axis.transAxes, fontsize=8, verticalalignment="top", color=sty.hypo_color)
- axis.set_title(f"{trial_type} trials", fontsize=10)
- axis.set_xlabel(feature, fontsize=10)
- axis.set_ylabel(acc, fontsize=10)
- axis.set_ylim(ymin=0, ymax=1)
- axis.tick_params(axis='both', which='major', labelsize=5)
- rows.append({"Trials": trial_type, "Acc_metric": acc, "Feature": feature,
- "r2": r2, "pval": results["pvalue"],
- "r2_wt": r2_wt, "pval_wt": results_wt["pvalue"],
- "r2_hypo": r2_hypo, "pval_hypo": results_hypo["pvalue"]})
- results = pd.DataFrame(rows)
- fig.suptitle(f"Correlation of the frame model accuracy during the whole {period} period with neuron to neuron global cosine similarity")
- # plt.savefig(f"{server_address}response_decoding/corr_acc_model_ntn_cosim_{period}.pdf", format="pdf")
- return data, results
- # endregion ============================================================================================================
- if __name__ == '__main__':
- BMS_analysis = True
- ### Initialisation of recs instances ###
- if BMS_analysis:
- directory = "C:/Users/cvandromme/Desktop/Tactile_detection/Data_DMSO_BMS/"
- roi_path = "C:/Users/cvandromme/Desktop/Tactile_detection/Fmko_bms&dmso_info.xlsx"
- else:
- directory = "C:/Users/cvandromme/Desktop/Tactile_detection/Data/"
- roi_path = "C:/Users/cvandromme/Desktop/Tactile_detection/FmKO_ROIs&inhibitory.xlsx"
- server_address = "Z:/Current_members/Ourania_Semelidou/2p/Figures_paper & submissions/Figures_april_2025/"
- roi_info = pd.read_excel(roi_path)
- files = os.listdir(directory)
- files_ = [file for file in files if file.endswith("synchro")]
- def opening_rec(fil, i):
- rec = pc.RecordingAmplDet(directory + fil + "/", 0, roi_path)
- return rec
- workers = cpu_count()
- pool = pool.ThreadPool(processes=workers)
- async_results = [pool.apply_async(opening_rec, args=(file, i)) for i, file in enumerate(files_)]
- if BMS_analysis:
- recs = {f"{ar.get().filename}-{ar.get().genotype.split("-")[1]}": ar.get() for ar in async_results}
- else:
- recs = {ar.get().filename: ar.get() for ar in async_results}
- for rec in recs.values():
- # rec.auc_neg()
- rec.peak_delay_amp()
- # endregion ============
- # test = perceptual_magnitude_behavior_corr(recs.values())
- # mean_corr = test.groupby("Genotype").mean()
- # glmm_behavior(data)
- # frame_data = get_activity_by_frame_df(recs.values(), zscore=True)
- # corr_data = correlate_mean_zscore_behavior_frame(frame_data[frame_data["Amplitude"] == 12])
- # plot_frame_correlation(corr_data)
- frame_dff = get_activity_by_frame_df(recs.values(), zscore=False, BMS=BMS_analysis)
- # features_df = get_features(recs.values(), amp_delay=True, auc=True)
- # activity_long_df = get_mean_trial_activity_df(recs.values(), zscore=True)
- # ntn_df = ntn_cosine_similarity(activity_long_df, amplitude="all", nogo=False, metric="Resp", filter_out_nr=False)
- # frame_dff_3 = aggregate_every_3cols(frame_dff, ["Genotype", "ID", "Threshold", "Trial", "Amplitude", "Duration", "Behavior", "n_type", "resp", "n_ID"], window_size=3)
- frame_model_df_res = pd.read_csv("Z:/Current_members/Ourania_Semelidou/2p/Figures_paper & submissions/202507/5_Figure2/frame_model_df_res.csv")
- # frame_model_df_avg = pd.read_csv("C:/Users/cvandromme/Desktop/Tactile_detection/Analysis_data/frame_model_df_avg3.csv")
- # frame_model_df_bms, frame_mean_sem_bms = frame_model(frame_dff, neuron_type=["EXC", "INH"], resp_type=[0, 1, -1], db_cv=True, balancing_method="resampling")
- # frame_model_df_res, frame_mean_sem_res = frame_model(frame_dff, neuron_type=["EXC", "INH"], resp_type=[0, 1, -1], db_cv=True, balancing_method="resampling")
- # frame_model_df_avg, frame_mean_sem_avg = frame_model(frame_dff, neuron_type=["EXC", "INH"], resp_type=[0, 1, -1], db_cv=True, balancing_method="resampling", sliding_window=3, window_sum=False)
- # frame_model_df_3, frame_mean_sem_3 = frame_model(frame_dff, neuron_type=["EXC", "INH"], resp_type=[0, 1, -1], db_cv=True, balancing_method="resampling", window=3)
- # frame_model_df_5, frame_mean_sem_5 = frame_model(frame_dff, neuron_type=["EXC", "INH"], resp_type=[0, 1, -1], db_cv=True, balancing_method="resampling", window=5)
- # frame_model_df_2, frame_mean_sem_2 = frame_model(frame_dff, neuron_type=["EXC", "INH"], resp_type=[0, 1, -1], db_cv=True, balancing_method="resampling", window=2)
- # frame_model_df_4, frame_mean_sem_4 = frame_model(frame_dff, neuron_type=["EXC", "INH"], resp_type=[0, 1, -1], db_cv=True, balancing_method="resampling")
- plot_df = plot_hit_miss_classif(frame_model_df_res, title_precision="All neurons, all patterns, doubleCV", shuffle=True, timescale_division_factor=1)
- data_comp = plot_hit_miss_classif_comp(frame_model_df_res, gp1="WT", gp2="KO", title_precision="All neurons, all patterns, doubleCV")
- # plot_df = plot_hit_miss_classif(frame_model_df_4, title_precision="All neurons, all patterns, doubleCV, non-sliding avg4", shuffle=True, timescale_division_factor=4)
- plot_hit_miss_classif_comp(frame_model_df_bms, gp1="WT-DMSO", gp2="WT-BMS", title_precision="All neurons, all patterns, doubleCV")
- plot_hit_miss_classif_comp(frame_model_df_bms, gp1="KO-DMSO", gp2="KO-BMS", title_precision="All neurons, all patterns, doubleCV")
- data_comp = plot_hit_miss_classif_comp(frame_model_df_bms, gp1="WT-DMSO", gp2="KO-DMSO", title_precision="All neurons, all patterns, doubleCV")
- plot_hit_miss_classif_comp(frame_model_df_bms, gp1="WT-BMS", gp2="KO-BMS", title_precision="All neurons, all patterns, doubleCV")
- # corr_acc_features_df, corr_results_df = correlate_frame_accuracy_metrics(frame_model_df_res, features_df, period="stim")
- # corr_acc_ntn_df, corr_ntn_results_df = correlate_frame_accuracy_ntn_df(frame_model_df_res, ntn_df, period="stim")
- # saved_framed_model_df = pd.read_csv("C:/Users/cvandromme/Desktop/frame_model_df.csv")
- # frame_model_df_amp_gp = frame_model_df.groupby(["Genotype", "ID", "Threshold", "Amplitude"], as_index=False).mean().drop(columns=["Trial", "Duration", "Behavior"])
- # plot_hit_miss_classif(saved_framed_model_df)
- # data = plot_hit_miss_classif_comp(saved_framed_model_df, gp1="WT", gp2="KO", title_precision="['EXC',_'INH'][0,_1,_-1]_-_db_cv=True")
- # recs_wt = {k: v for k, v in recs.items() if v.genotype == "WT"}
- # recs_hypo = {k: v for k, v in recs.items() if v.genotype == "KO-Hypo"}
- accuracy_comp_df = compare_accuracy(recs.values(), random=42)
- # acc_nb_df = correlate_nb_accuracy(recs, accuracy_comp_df[accuracy_comp_df["Genotype"] == "KO"], threshold="median")
response_decoding.py at commit 7db6cf1, under GPL-2.0 · at the source
Overview
Abstract
Touch is essential for interacting with the world, and atypical tactile experience is a core feature of autism that profoundly affects daily life. However, we do not know the neural mechanisms of low‐level tactile perception and their alterations in autism. Using a translational forepaw‐based perceptual task, we recapitulate the multifaceted tactile features of autistic individuals in the Fmr1 −/
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 14 matches between paragraphs and lines of code.
FrickLab/Imaging_2PCalciumAnalysis
7db6cf1c509f5f157105c9d610148413b88d5eb7, 14 October 2025Availability: 1 check, the latest on 29 September 2026: the link answers
- 29 September 2026: the link answers
57 files
- Figures/
DMSO_BMS.ipynb , Jupyter, 1,219 lines - Figures/
Dec_imbal_resp.ipynb , Jupyter, 628 lines - Figures/
Decoding_responsivity.ip , Jupyter, 415 linesynb - Figures/
Response_characterizatio , Jupyter, 385 linesn.ipynb - Figures/
WT_paper.py , Python, 226 lines - Figures/
baseline& , Jupyter, 493 linescor.ipynb - Figures/
cross_corr.ipynb , Jupyter, 80 lines - Figures/
mlr_output.ipynb , Jupyter, 219 lines - Figures/
noise_assessment.py , Python, 3,277 lines, 4 matches - Figures/
response_decoding.py , Python, 1,542 lines, 5 matches - Figures/
responsivity_figures.ipy , Jupyter, 1,226 linesnb - Figures/
stimulus_encoding.py , Python, 1,827 lines, 2 matches - Figures/
tbt_pattern_var.ipynb , Jupyter, 522 lines - Figures/
traces.ipynb , Jupyter, 229 lines - Figures/
traces.py , Python, 122 lines - Figures/
tuning_firing.ipynb , Jupyter, 59 lines - docs/
conf.py , Python, 199 lines - percephone/
__init__.py , Python, 1 line - percephone/
analysis/ , Python, 1 line__init__.py - percephone/
analysis/ , Python, 83 linesbaseline_again.py - percephone/
analysis/ , Python, 113 linesbslcor.py - percephone/
analysis/ , Python, 68 linescosinesim.py - percephone/
analysis/ , Python, 155 linescrosscor.py - percephone/
analysis/ , Python, 278 linesmlr.py - percephone/
analysis/ , Python, 126 linesmlr_models.py - percephone/
analysis/ , Python, 361 linesresponse.py - percephone/
analysis/ , Python, 293 linesutils.py - percephone/
cebra/ , Python, 149 linesCEBRA_multisession.py - percephone/
cebra/ , Python, 117 linesCEBRA_trials_viz.py - percephone/
cebra/ , Python, 1 line__init__.py - percephone/
cebra/ , Python, 84 linescebra_decoding.py - percephone/
cebra/ , Python, 114 linescebra_meantrials.py - percephone/
cebra/ , Python, 93 linesmain_cebra.py - percephone/
cebra/ , Python, 79 linesmain_cebra2.py - percephone/
cebra/ , Python, 165 linesmain_multitrials.py - percephone/
core/ , Python, 1 line__init__.py - percephone/
core/ , Python, 984 linesrecording.py - percephone/
deconvo/ , Python, 1 line__init__.py - percephone/
deconvo/ , Python, 120 linesspike_rates.py - percephone/
modelisation/ , Python, 1 line__init__.py - percephone/
modelisation/ , Python, 86 linesdecoding.py - percephone/
modelisation/ , Python, 511 linesframe_decoding.py - percephone/
modelisation/ , Python, 193 linessuprasub_frame_decoding. py - percephone/
plts/ , Python, 1 line__init__.py - percephone/
plts/ , Python, 1,056 lines, 1 matchbehavior.py - percephone/
plts/ , Python, 662 linesheatmap.py - percephone/
plts/ , Python, 251 linesold_heatmap.py - percephone/
plts/ , Python, 51 linesresponse.py - percephone/
plts/ , Python, 76 linesscalebars.py - percephone/
plts/ , Python, 1,017 lines, 1 matchstats.py - percephone/
plts/ , Python, 81 linesstyle.py - percephone/
plts/ , Python, 217 lines, 1 matchutils.py - percephone/
utils/ , Python, 5 lines__init__.py - percephone/
utils/ , Python, 198 linesio.py - percephone/
utils/ , Python, 88 linesmath_formulas.py - LICENSE, License, 339 lines
- README.md, Text, 14 lines
Code Availability
Custom‐made Python Codes Used in this Study can be found on the following GitHub Repository: https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 55 scripts, each with its path and the digest of its content;
- 14 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
- figshare:29900456, at figshare; found in “Data Availability Statement”
Data Availability Statement
The data that support the findings of this study are openly available in figshare at https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 29 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 8 authors, 7 keywords, 12 MeSH terms, 9 funders, 101 references.
Cite
This paper
Semelidou, O., Gauvrit, T., Vandromme, C., Cornier, A., Saint‐Jean, A., Feuvre, Y. L., Ginger, M., & Frick, A. (2026). Diminished Signal-to-Noise Ratio Disrupts Somatosensory Population Encoding and Drives Tactile Hyposensitivity in the Fmr1&
BibTeX
@article{semelidou2026di
author = {Semelidou, Ourania and Gauvrit, Théo and Vandromme, Célien and Cornier, Alexandre and Saint‐Jean, Anna and Feuvre, Yves Le and Ginger, Melanie and Frick, Andreas},
title = {{Diminished Signal-to-Noise Ratio Disrupts Somatosensory Population Encoding and Drives Tactile Hyposensitivity in the Fmr1\&
journal = {Advanced science (Weinheim, Baden-Wurttemberg, Germany)},
year = {2026},
month = apr,
volume = {13},
number = {28},
pages = {e19479},
publisher = {Wiley},
issn = {2198-3844},
doi = {10.1002/
url = {https://
pmid = {41983266},
pmcid = {PMC13185836}
}
RIS
TY - JOUR
AU - Semelidou, Ourania
AU - Gauvrit, Théo
AU - Vandromme, Célien
AU - Cornier, Alexandre
AU - Saint‐Jean, Anna
AU - Feuvre, Yves Le
AU - Ginger, Melanie
AU - Frick, Andreas
TI - Diminished Signal-to-Noise Ratio Disrupts Somatosensory Population Encoding and Drives Tactile Hyposensitivity in the Fmr1&
T2 - Advanced science (Weinheim, Baden-Wurttemberg, Germany)
J2 - Adv Sci (Weinh)
PY - 2026
DA - 2026/
VL - 13
IS - 28
SP - e19479
SN - 2198-3844
PB - Wiley
DO - 10.1002/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1002/
"type": "article-journal",
"title": "Diminished Signal-to-Noise Ratio Disrupts Somatosensory Population Encoding and Drives Tactile Hyposensitivity in the Fmr1&
"container-title": "Advanced science (Weinheim, Baden-Wurttemberg, Germany)",
"author": [
{
"family": "Semelidou",
"given": "Ourania"
},
{
"family": "Gauvrit",
"given": "Théo"
},
{
"family": "Vandromme",
"given": "Célien"
},
{
"family": "Cornier",
"given": "Alexandre"
},
{
"family": "Saint‐Jean",
"given": "Anna"
},
{
"family": "Feuvre",
"given": "Yves Le"
},
{
"family": "Ginger",
"given": "Melanie"
},
{
"family": "Frick",
"given": "Andreas"
}
],
"container-title-short":
"volume": "13",
"issue": "28",
"page": "e19479",
"DOI": "10.1002/
"PMID": "41983266",
"PMCID": "PMC13185836",
"ISSN": "2198-3844",
"publisher": "Wiley",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
15
]
]
}
}
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.1016/j.isci.2026.116825 [code]
- Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.Journal: iScienceIn common: imbalanced-learn, Pingouin, h5py, 7 other tools, mouse
- [2] doi:10.1016/j.celrep.2026.117590 [code]
- Impaired behavioral inhibition in Fmr1 KO mice is linked to disrupted visual cortex theta oscillations.Journal: Cell reportsIn common: Pingouin, statsmodels, seaborn, 5 other tools, autism, other condition, mouse, 1 reference
- [3] doi:10.64898/2026.07.14.26358047 [code]
- From Genes to Neurochemistry: Excitation and Inhibition Mechanisms of Sensory Differences in AutismJournal: medRxiv (preprint)In common: autism, 6 references
- [4] doi:10.7554/elife.109717 [code]
- Retrosplenial cortex enables context-dependent goal-directed sensorimotor transformation.Journal: eLifeIn common: h5py, statsmodels, seaborn, 5 other tools, mouse, 2 references
- [5] doi:10.1016/j.nicl.2026.104012 [code]
- Structural-functional multilayer brain network properties and outcome of combined repetitive transcranial magnetic stimulation and psychotherapy for obsessive-compulsive disorder.Journal: NeuroImage. ClinicalIn common: imbalanced-learn, Pingouin, statsmodels, 6 other tools, other condition
- [6] doi: [code]
- Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement controlJournal: eLifeIn common: Pingouin, h5py, statsmodels, 6 other tools, optical imaging (calcium, voltage, 2-photon), mouse
- [7] doi:10.1038/s41398-026-04081-8 [code]
- Functional system-specific brain aging across the Alzheimer's disease continuum.Journal: Translational psychiatryIn common: imbalanced-learn, Pingouin, statsmodels, 6 other tools
- [8] doi:10.1038/s41598-026-64405-y [code]
- Evaluating clinical and neuroimaging predictors for cognitive-behavioral therapy outcome in obsessive-compulsive disorder.Journal: Scientific reportsIn common: imbalanced-learn, h5py, statsmodels, 6 other tools, other condition
- [9] doi:10.1038/s41467-026-76939-w [code]
- HIPPIE: a generative model for electrophysiological analysis across species, technologies, and modalities.Journal: Nature communicationsIn common: imbalanced-learn, h5py, statsmodels, 6 other tools, mouse
- [10] doi:10.1038/s41593-026-02362-5 [code]
- Replay of procedural memory is independent of the hippocampus.Journal: Nature neuroscienceIn common: Pingouin, h5py, statsmodels, 6 other tools, cognitive, mouse
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, 55 scripts, and 14 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:1201f6ebe0a02d93…
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.
