Neural selectivity for social interactions in the infant brain.
The 2 matches
- [1] § STAR★Methods › Quantification and statistical analysis › Preprocessing of fNIRS data ↔ Data and code/fNIRS_analyses_shared.py, lines 109–185 · score 0.96 · Beer Lambert law, band passed filtered, signal enhancement, negative correlation, MNE NIRS, preprocessing
- [2] § STAR★Methods › Quantification and statistical analysis › Analyses ↔ Data and code/fNIRS_analyses_shared.py, lines 596–610 · score 0.60 · 0–14 sec, HbO, channel, interacting
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 · 875 lines · 38 KB · no license · 2 matches
- # -*- coding: utf-8 -*-
- """
- fNIRS analyses for "Neural selectivity for social interactions in the infant brain"
- Analysis script for the infant study (Experiment 2). Analyses for adult data is identical,
- except for small preprocessing dissimilarities, indicated in the manuscript.
- @author: mmello
- """
- #%% Import libraries and define functions
- import os
- import os.path as op
- os.environ['R_HOME'] = '...' # your path to R
- # General-purpose packages
- import matplotlib.pyplot as plt
- import pandas as pd
- import scipy
- import numpy as np
- import matplotlib as mpl
- from matplotlib.colors import LinearSegmentedColormap
- from itertools import compress
- import collections
- from copy import deepcopy
- from scipy.stats import sem, norm
- from scipy.signal import find_peaks
- from scipy.spatial.distance import pdist, squareform
- from scipy.sparse import csr_matrix
- import pywt
- from pymer4.models import Lmer
- # MNE packages
- import mne
- import mne_nirs
- from mne.stats import permutation_cluster_1samp_test
- # The following two imports are used to download necessary files to plot data on adult and infant brain templates
- # from mne.datasets import fetch_infant_template
- # from mne.datasets import fetch_fsaverage
- # Define the wavelet-based filtering function
- def wavelet_based_filtering(signal, wavelet='db5', level=None, alpha=5):
- coeffs = pywt.wavedec(signal, wavelet, level=level)
- for i in range(1, len(coeffs)):
- detail_coeffs = coeffs[i]
- q1 = np.percentile(detail_coeffs, 25)
- q3 = np.percentile(detail_coeffs, 75)
- iqr = q3 - q1
- lower_bound = q1 - alpha * iqr
- upper_bound = q3 + alpha * iqr
- coeffs[i] = np.where((detail_coeffs < lower_bound) | (detail_coeffs > upper_bound), 0, detail_coeffs)
- filtered_signal = pywt.waverec(coeffs, wavelet)
- # Ensure the filtered signal has the same length as the original signal
- if len(filtered_signal) != len(signal):
- filtered_signal = filtered_signal[:len(signal)] # Trim to the original length
- return filtered_signal
- #%% Define individual-level analyses
- data_dir = "C:/Users/mmello/Desktop/fNIRS babies/DATA/together/"
- def facingness_waveform(subj_path, ID):
- raw = mne.io.read_raw_nirx(subj_path, verbose=True, preload=True)
- raw.info["subject_info"]
- raw.get_data().shape
- ## Plot raw data
- # mne.viz.plot_sensors(raw.info, kind='topomap', show_names=True)
- # raw.plot(duration=20000, show_scrollbars=False, n_channels=len(raw.ch_names))
- ## Define triggers
- raw.annotations.set_durations({'1.0': 10, '2.0': 10, '3.0': 10,
- })
- raw.annotations.rename({'1.0': 'Interacting', '2.0': 'NonInteracting', '3.0': 'Single',
- })
- ## Convert to optical density (OD)
- raw_od = mne.preprocessing.nirs.optical_density(raw)
- # Plot OD data
- # raw_od.plot(n_channels=len(raw_od.ch_names), duration=15000, show_scrollbars=False)
- ## Find bad channels and interpolate (plotting is commented out)
- sci = mne.preprocessing.nirs.scalp_coupling_index(raw_od) #h_freq=1.35, h"_trans_bandwidth=0.1
- # fig, ax = plt.subplots()
- # ax.hist(sci)
- # ax.set(xlabel='Scalp Coupling Index', ylabel='Count', xlim=[0, 1])
- raw_od.info["bads"] = list(compress(raw_od.ch_names, sci < 0.7))
- print(raw_od.info["bads"])
- raw_od.interpolate_bads()
- ## Clean data with TDDR
- raw_od = mne.preprocessing.nirs.temporal_derivative_distribution_repair(raw_od)
- ## Wavelet-based filtering
- # Extract data and times from optical density
- data_h, times = raw_od.get_data(return_times=True)
- # Apply wavelet-based filtering to each channel
- filtered_data_wavelet = np.array([wavelet_based_filtering(channel) for channel in data_h])
- raw_od._data = filtered_data_wavelet
- ## Convert to haemoglobin concentration data and band-pass filter
- raw_haemo_b = mne.preprocessing.nirs.beer_lambert_law(raw_od, ppf=0.5)
- raw_haemo_b = raw_haemo_b.filter(0.01, 0.2, h_trans_bandwidth=0.2, l_trans_bandwidth=0.01, verbose=False)
- ## Negative correlation enhancement for systemic noise removal
- raw_haemo_b = mne_nirs.signal_enhancement.enhance_negative_correlation(raw_haemo_b)
- ## Plot cleaned haemodynamic data
- # raw_haemo_b.plot(n_channels=len(raw_haemo_b.ch_names), duration=20000, show_scrollbars=False)
- ## Extracting epochs (and plot events)
- events, event_dict = mne.events_from_annotations(raw_haemo_b, regexp='(Interacting|NonInteracting|Single)')
- # mne.viz.plot_events(events, event_id=event_dict,
- # sfreq=raw_haemo_b.info['sfreq'])
- epochs = mne.Epochs(raw_haemo_b, events, event_id=event_dict, tmin=-2, tmax=16,
- verbose=False, preload=True,
- baseline=(-2, 0),
- )
- ## Removing trials based on video recordings (6-mo)
- if ID == 'SUB02 - 6mo':
- indices_to_remove = [19, 22, 24, 27, 28, 30]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB03 - 6mo':
- indices_to_remove = [33, 36]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB04 - 6mo':
- indices_to_remove = [13, 15, 19, 22, 23, 24]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB05 - 6mo':
- indices_to_remove = [18, 19]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB06 - 6mo':
- indices_to_remove = [34, 37, 38]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB07 - 6mo':
- indices_to_remove = [27, 41, 43, 44, 46, 47]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB08 - 6mo':
- indices_to_remove = [21]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB09 - 6mo':
- indices_to_remove = [12, 18, 19, 22, 23, 24, 26, 27]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB11 - 6mo':
- indices_to_remove = [35, 42]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB12 - 6mo':
- indices_to_remove = [19, 20, 22, 23, 25]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB13 - 6mo':
- indices_to_remove = [16, 17, 19, 21, 22, 27]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB14 - 6mo':
- indices_to_remove = [10, 16, 17, 18, 19]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB15 - 6mo':
- indices_to_remove = [12, 13, 17, 21, 28]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB16 - 6mo':
- indices_to_remove = [15, 17, 20, 25, 27, 28]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB17 - 6mo':
- indices_to_remove = [29]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB18 - 6mo':
- indices_to_remove = [3, 10, 13, 17, 18, 21, 24, 30, 32, 33, 34]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB19 - 6mo':
- indices_to_remove = [17, 18, 19, 26]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB21 - 6mo':
- indices_to_remove = [9, 10, 11, 12, 13, 14, 15, 18, 21, 22]
- epochs.drop(indices_to_remove)
- ## Removing trials based on video recordings (10-mo)
- if ID == 'SUB02 - 10mo':
- indices_to_remove = [14, 17, 21]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB03 - 10mo':
- indices_to_remove = [15, 16, 17]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB04 - 10mo':
- indices_to_remove = [5, 11, 12, 13, 18, 21, 23, 24, 25]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB05 - 10mo':
- indices_to_remove = [25, 32, 34, 46]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB06 - 10mo':
- indices_to_remove = [16, 29, 31, 34, 41, 42]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB07 - 10mo':
- indices_to_remove = [24]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB08 - 10mo':
- indices_to_remove = []
- epochs.drop(indices_to_remove)
- elif ID == 'SUB09 - 10mo':
- indices_to_remove = [23, 24, 29, 31, 33, 34, 38]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB10 - 10mo':
- indices_to_remove = [9, 11, 22, 23, 38]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB11 - 10mo':
- indices_to_remove = [5, 6, 7, 8, 9, 11, 12, 13, 14, 18, 21, 27, 28, 30, 31, 32, 34]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB12 - 10mo':
- indices_to_remove = [20, 21]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB13 - 10mo':
- indices_to_remove = [6, 7, 8, 12, 15, 18, 19, 20, 21, 23]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB14 - 10mo':
- indices_to_remove = [8, 10, 17, 31, 33, 34]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB15 - 10mo':
- indices_to_remove = [8, 9, 17]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB16 - 10mo':
- indices_to_remove = [14, 18, 24, 25]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB17 - 10mo':
- indices_to_remove = [29, 33, 35]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB18 - 10mo':
- indices_to_remove = [13, 14, 20, 21, 23, 28, 30]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB19 - 10mo':
- indices_to_remove = [25, 27, 31, 32]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB20 - 10mo':
- indices_to_remove = [8, 11, 12, 14, 15, 19, 21, 24, 25]
- epochs.drop(indices_to_remove)
- elif ID == 'SUB21 - 10mo':
- indices_to_remove = [12, 17, 20]
- epochs.drop(indices_to_remove)
- print(len(epochs)) #print number of remaining trials
- # Create dictionaries of HbO/HbR responses
- evoked_dict = {'Facing/HbO': epochs['Interacting'].average(picks='hbo'),
- 'Facing/HbR': epochs['Interacting'].average(picks='hbr'),
- 'Away/HbO': epochs['NonInteracting'].average(picks='hbo'),
- 'Away/HbR': epochs['NonInteracting'].average(picks='hbr'),
- 'Single/HbO': epochs['Single'].average(picks='hbo'),
- 'Single/HbR': epochs['Single'].average(picks='hbr')}
- ## Rename channels until the encoding of frequency in ch_name is fixed
- for condition in evoked_dict:
- evoked_dict[condition].rename_channels(lambda x: x[:-4])
- ## Store individual data for group-level analysis
- return raw, raw_haemo_b, epochs
- #%% Individual-level analysis
- ## Load data
- # Separate dictionaries for each group
- all_evokeds_6mo = collections.defaultdict(list)
- all_evokeds_10mo = collections.defaultdict(list)
- for root, dirs, files in os.walk(os.path.normpath(data_dir), topdown=False):
- for subj in dirs:
- if 'sync' not in subj and 'bad' not in subj:
- print("Now processing subject: " + subj)
- subj_path = os.path.join(data_dir, subj)
- # Run your individual analysis
- raw, raw_haemo_b, epochs = facingness_waveform(subj_path, subj)
- # Save data into the appropriate dictionary
- if "6mo" in subj:
- for condition in epochs.event_id:
- all_evokeds_6mo[condition].append(epochs[condition].average())
- elif "10mo" in subj:
- for condition in epochs.event_id:
- all_evokeds_10mo[condition].append(epochs[condition].average())
- #%% Visualize channel montage in infants
- subjects_dir = op.join(mne.datasets.sample.data_path(), "subjects")
- ##6-months
- trans = mne.read_trans("C:/Users/mmello/mne_data/MNE-sample-data/subjects/ANTS6-0Months3T/bem/fsaverage-trans.fif")
- brain = mne.viz.Brain(
- "ANTS6-0Months3T", subjects_dir=subjects_dir, background="w", cortex="0.5"
- )
- brain.add_sensors(
- raw_haemo_b.info, trans=trans, fnirs=["channels", "pairs", "sources", "detectors"]
- )
- brain.show_view(azimuth=180, elevation=80, distance=250)
- ## 10-months
- trans = mne.read_trans("C:/Users/mmello/mne_data/MNE-sample-data/subjects/ANTS10-5Months3T/bem/fsaverage-trans.fif")
- brain = mne.viz.Brain(
- "ANTS10-5Months3T", subjects_dir=subjects_dir, background="w", cortex="0.5"
- )
- brain.add_sensors(
- raw_haemo_b.info, trans=trans, fnirs=["channels", "pairs", "sources", "detectors"]
- )
- brain.show_view(azimuth=180, elevation=80, distance=250)
- #%% Peak detection
- # Merge into one dict: all babies (6mo + 10mo)
- all_evokeds = collections.defaultdict(list)
- # Add 6mo data
- for condition, evokeds in all_evokeds_6mo.items():
- all_evokeds[condition].extend(evokeds)
- # Add 10mo data
- for condition, evokeds in all_evokeds_10mo.items():
- all_evokeds[condition].extend(evokeds)
- # Stack data from all evoked responses for Facing and Away
- evokeds_facing = all_evokeds['Interacting']
- evokeds_away = all_evokeds['NonInteracting']
- # Concatenate both conditions
- evokeds_combined = evokeds_facing + evokeds_away
- # Build array: (n_subjects_total, n_channels, n_times)
- data_array = np.array([evoked._data for evoked in evokeds_combined])
- # Average across channels for each subject
- data_avg_channels = data_array.mean(axis=1)
- # Compute grand average and standard error
- grand_avg = data_avg_channels.mean(axis=0)
- conf_interval = sem(data_avg_channels, axis=0)
- # Get time axis from one of the evoked objects
- times = evokeds_combined[0].times
- # Find peaks in the grand average
- peaks_avg, _ = find_peaks(grand_avg, height=0)
- # Keep only the first peak (if any)
- if len(peaks_avg) > 0:
- first_peak = peaks_avg[0]
- else:
- first_peak = None
- # Plot
- plt.figure(figsize=(10, 5))
- plt.plot(times, grand_avg, color='red', label='Mean across all channels')
- plt.fill_between(times, grand_avg - conf_interval, grand_avg + conf_interval,
- color='red', alpha=0.3, label='95% CI')
- # Plot only the first peak marker
- if first_peak is not None:
- plt.plot(times[first_peak], grand_avg[first_peak], "x", color='black', label='Peak')
- plt.xlabel('Time (s)')
- plt.ylabel('Amplitude')
- plt.title('Grand Average Across All Channels - Interacting + Non-interacting')
- plt.xlim(-2.5,15)
- plt.ylim(-3.5*1e-7,3.5*1e-7)
- plt.legend(loc='lower center')
- plt.grid(True)
- plt.tight_layout()
- plt.show()
- # Print first peak time
- if first_peak is not None:
- print('First peak time (s):', times[first_peak])
- else:
- print('No peaks found.')
- #%% Group-level analyses - Visualization
- # Plot average waveform
- fig, axes = plt.subplots(nrows=1, ncols=len(all_evokeds), figsize=(17, 5))
- lims = dict(hbo=[-3, 3], hbr=[-3, 3])
- #
- for (pick, color) in zip(['hbo', 'hbr'], ['r', 'b']):
- for idx, (evoked_name, evoked_data) in enumerate(all_evokeds.items()):
- mne.viz.plot_compare_evokeds(
- {evoked_name: evoked_data}, combine='mean',
- picks=pick, axes=axes[idx], show=False, ylim=lims,
- colors=[color], legend=False, ci=0.95,
- show_sensors=idx == 1
- )
- axes[idx].set_title('{}'.format(evoked_name))
- if idx == 0: # Set y-axis label for the first subplot
- axes[idx].set_ylabel('Haemoglobin concentration (μM)')
- # Remove grid for the current subplot
- axes[idx].grid(False)
- # Plot topomap for each condition
- times = np.arange(2, 16, 5.0)
- avg_evoked_facing = mne.grand_average(all_evokeds['Interacting'])
- avg_evoked_away = mne.grand_average(all_evokeds['NonInteracting'])
- avg_evoked_facing.plot_topomap(times=times, cmap=mpl.cm.coolwarm, extrapolate='local',
- sphere='auto', vlim=(-3, 3), image_interp='linear')
- avg_evoked_away.plot_topomap(times=times, cmap=mpl.cm.coolwarm, extrapolate='local',
- sphere='auto', vlim=(-3, 3), image_interp='linear')
- plt.show()
- ## Plot topomap for each Interacting - NonInteracting
- avg_evoked_con = avg_evoked_facing._data - avg_evoked_away._data
- info = avg_evoked_facing.info
- # Create a new Evoked object
- evoked_con = mne.EvokedArray(avg_evoked_con, info, tmin=avg_evoked_facing.times[0])
- evoked_con.plot_topomap(times=times, cmap=mpl.cm.coolwarm, extrapolate='local',
- sphere='auto', vlim=(-3, 3), image_interp='linear')
- plt.show()
- #%% Group-level analyses - Channel-wise analysis
- ## Create dataframes with by participant, by channel, by condition average response
- df_babies_6 = pd.DataFrame(columns=['ID', 'Channel', 'Chroma', 'Condition', 'MeanValue'])
- # Loop through the conditions (evoked)
- for idx, evoked in enumerate(all_evokeds_6mo):
- subj_id = 0 # Initialize subject ID counter
- # Loop through each subject's data within the current evoked condition
- for subj_data in all_evokeds_6mo[evoked]:
- subj_id += 1 # Increment subject ID
- # Iterate over the chroma types (e.g., hbo and hbr)
- for chroma in ["hbo"]:
- # Pick the data corresponding to the current chroma type
- data = deepcopy(subj_data).pick(chroma)
- # Crop the data to the specified time range and scale
- cropped_data = data.crop(tmin=0, tmax=14).data * 1.0e6
- # Compute the mean value for each channel
- mean_values = cropped_data.mean(axis=1) # Mean across time points for each channel
- # Iterate over each channel and append the mean value to the all_ROIs DataFrame
- for channel_idx, mean_value in enumerate(mean_values):
- this_df = pd.DataFrame(
- {'ID': subj_id, 'Channel': channel_idx, 'Chroma': chroma,
- 'Condition': evoked, 'MeanValue': mean_value}, index=[0])
- df_babies_6 = pd.concat([df_babies_6, this_df], ignore_index=True)
- # Print a message when done processing data for the current subject
- print(f"Finished processing data for Subject ID: {subj_id}")
- # Print a message when done processing data for the last subject of the current condition
- print(f"Finished processing data for all subjects in Condition: {evoked}")
- df_babies_10 = pd.DataFrame(columns=['ID', 'Channel', 'Chroma', 'Condition', 'MeanValue'])
- # Loop through the conditions (evoked)
- for idx, evoked in enumerate(all_evokeds_10mo):
- subj_id = 0 # Initialize subject ID counter
- # Loop through each subject's data within the current evoked condition
- for subj_data in all_evokeds_10mo[evoked]:
- subj_id += 1 # Increment subject ID
- # Iterate over the chroma types (e.g., hbo and hbr)
- for chroma in ["hbo"]:
- # Pick the data corresponding to the current chroma type
- data = deepcopy(subj_data).pick(chroma)
- # Crop the data to the specified time range and scale
- cropped_data = data.crop(tmin=0, tmax=14).data * 1.0e6
- # Compute the mean value for each channel
- mean_values = cropped_data.mean(axis=1) # Mean across time points for each channel
- # Iterate over each channel and append the mean value to the all_ROIs DataFrame
- for channel_idx, mean_value in enumerate(mean_values):
- this_df = pd.DataFrame(
- {'ID': subj_id, 'Channel': channel_idx, 'Chroma': chroma,
- 'Condition': evoked, 'MeanValue': mean_value}, index=[0])
- df_babies_10 = pd.concat([df_babies_10, this_df], ignore_index=True)
- # Print a message when done processing data for the current subject
- print(f"Finished processing data for Subject ID: {subj_id}")
- # Print a message when done processing data for the last subject of the current condition
- print(f"Finished processing data for all subjects in Condition: {evoked}")
- # Replace channel indices with their original names
- hbo_channels = [ch for ch in raw_haemo_b.info['ch_names'] if 'hbo' in ch.lower()]
- channel_mapping = {i: name for i, name in enumerate(hbo_channels)}
- df_babies_6['ch_name'] = df_babies_6['Channel'].replace(channel_mapping)
- df_babies_10['ch_name'] = df_babies_10['Channel'].replace(channel_mapping)
- ## Compute contrast (Interacting - NonInteracting) for 10-month-olds
- difference_df_6 = df_babies_6.pivot_table(
- index=['ID', 'ch_name', 'Chroma'],
- columns='Condition',
- values='MeanValue')
- difference_df_6['Difference'] = difference_df_6['Interacting'] - difference_df_6['NonInteracting']
- difference_df_6 = difference_df_6.reset_index()
- print(difference_df_6)
- ## Compute contrast (Interacting - NonInteracting) for 10-month-olds
- difference_df_10 = df_babies_10.pivot_table(
- index=['ID', 'ch_name', 'Chroma'],
- columns='Condition',
- values='MeanValue')
- difference_df_10['Difference'] = difference_df_10['Interacting'] - difference_df_10['NonInteracting']
- difference_df_10 = difference_df_10.reset_index()
- print(difference_df_6)
- difference_df_6['age'] = '6mo'
- difference_df_10['age'] = '10mo'
- df_concat = pd.concat([difference_df_10, difference_df_6], ignore_index=True)
- df_concat['age_contrast'] = df_concat['age'].map({'10mo': 0.5, '6mo': -0.5})
- ## Run channel-wise analysis on both infant groups
- model = Lmer('Difference ~ -1 + ch_name*age_contrast + (1|ID)', data=df_concat)
- model_results = model.fit()
- model.anova() # Effect of channel, bu no effect of age, nor interaction
- print(model_results) # Print model estimates (by-channel coefficients)
- ## Visualization of results on brain template (10month-old brain)
- # Necassary steps to adapt output to MNE visualization function (_plot_GLM_surface_projection)
- # Note that we are using this function flexibily to plot HbO concentration
- model_results = model_results.rename(columns={'Estimate': 'Coef.'})
- model_results['ch_name'] = model_results.index.str.replace('ch_name', '', regex=False)
- # Define the desired order for 'ch_name'
- desired_order = [
- 'S1_D1 hbo', 'S1_D2 hbo', 'S1_D9 hbo', 'S2_D1 hbo', 'S2_D2 hbo', 'S2_D3 hbo',
- 'S3_D3 hbo', 'S4_D2 hbo', 'S4_D4 hbo', 'S4_D5 hbo', 'S5_D3 hbo', 'S5_D4 hbo',
- 'S5_D6 hbo', 'S6_D4 hbo', 'S6_D5 hbo', 'S6_D6 hbo', 'S6_D7 hbo', 'S7_D6 hbo',
- 'S8_D6 hbo', 'S8_D7 hbo', 'S8_D8 hbo', 'S9_D1 hbo', 'S9_D9 hbo', 'S9_D10 hbo',
- 'S10_D10 hbo', 'S11_D10 hbo', 'S11_D11 hbo', 'S11_D12 hbo', 'S12_D9 hbo',
- 'S12_D11 hbo', 'S12_D13 hbo', 'S13_D11 hbo', 'S13_D12 hbo', 'S13_D13 hbo',
- 'S13_D14 hbo', 'S14_D12 hbo', 'S15_D12 hbo', 'S15_D14 hbo', 'S15_D15 hbo'
- ]
- # Reorder the DataFrame based on the 'ch_name' column according to the desired order
- model_results['ch_name'] = pd.Categorical(model_results['ch_name'], categories=desired_order, ordered=True)
- # Sort the DataFrame based on the new 'ch_name' order
- model_results = model_results.sort_values('ch_name')
- # # To view the model summary
- model_results = model_results.head(39) # Restrict the output to the channel coefficients (main effect)
- print(model_results) # Print again
- ## Plot the results!
- # Note that, in order to successfully plot with this function, internal options of the function itself need to be updated...
- # ... to show an infant brain (and not the adult one, which is default)
- # To get help about this, please contact me at [email hidden]
- clim = dict(kind='value', pos_lims=(0, 1, 4), neg_lims=(-4, -1, 0))
- brain = mne_nirs.visualisation.plot_glm_surface_projection(raw_haemo_b.copy().pick("hbo"),
- model_results, clim=clim, view='lateral', mode='weighted', colormap='RdBu_r',
- colorbar=False, size=(800, 700), value='T-stat', distance=0.015)
- # Plot only significant channels: t-value >= |1.96|
- significant_results_inter = model_results.copy()
- # # Set values to 0 where p-value is >= 0.05
- significant_results_inter.loc[significant_results_inter['P-val'] > 0.051, 'T-stat'] = 0 # Ensure correct column name
- # clim = dict(kind='value', pos_lims=(0, 1, 4), neg_lims=(-4, -1, 0))
- brain = mne_nirs.visualisation.plot_glm_surface_projection(raw_haemo_b.copy().pick("hbo"),
- significant_results_inter, clim=clim, view='lateral', colormap='RdBu_r',
- colorbar=False, size=(800, 700), value='T-stat', distance=0.015)
- ## Separate analyses for each group. Plot the same way
- ## 6-month-olds
- model = Lmer('Difference ~ -1 + ch_name + (1|ID)', data=difference_df_6)
- model_results = model.fit()
- print(model_results)
- ## 10-month-olds
- model = Lmer('Difference ~ -1 + ch_name + (1|ID)', data=difference_df_10)
- model_results = model.fit()
- print(model_results)
- #%% Group-level analyses - Cluster-based analysis
- ## The following is on both infant groups together.
- ## You can compare the two groups with the function 'permutation_cluster_test'
- # Function to ensure only HbO channels are picked
- def get_hbo_channels(evoked):
- hbo_picks = mne.pick_types(evoked.info, fnirs='hbo')
- return hbo_picks
- # Initialize lists for HbO evoked responses
- evoked_hbo_facing = []
- evoked_hbo_away = []
- # Loop through each evoked response in 'Facing' and 'Away' conditions
- for i in range(len(all_evokeds['Interacting'])):
- hbo_picks_facing = get_hbo_channels(all_evokeds['Interacting'][i])
- hbo_picks_away = get_hbo_channels(all_evokeds['NonInteracting'][i])
- for i in range(len(all_evokeds['Interacting'])):
- evoked_f = all_evokeds['Interacting'][i].copy().pick(mne.pick_types(all_evokeds['Interacting'][i].info, fnirs='hbo'))
- evoked_a = all_evokeds['NonInteracting'][i].copy().pick(mne.pick_types(all_evokeds['NonInteracting'][i].info, fnirs='hbo'))
- # Crop by time (0 to 14 seconds)
- evoked_f.crop(tmin=0, tmax=14)
- evoked_a.crop(tmin=0, tmax=14)
- evoked_hbo_facing.append(evoked_f)
- evoked_hbo_away.append(evoked_a)
- ## Extract average response for both infant groups together ('all_evokeds' dictionary)
- df_babies = pd.DataFrame(columns=['ID', 'Channel', 'Chroma', 'Condition', 'MeanValue'])
- # Loop through the conditions (evoked)
- for idx, evoked in enumerate(all_evokeds):
- subj_id = 0 # Initialize subject ID counter
- # Loop through each subject's data within the current evoked condition
- for subj_data in all_evokeds[evoked]:
- subj_id += 1 # Increment subject ID
- # Iterate over the chroma types (e.g., hbo and hbr)
- for chroma in ["hbo"]:
- # Pick the data corresponding to the current chroma type
- data = deepcopy(subj_data).pick(chroma)
- # Crop the data to the specified time range and scale
- cropped_data = data.crop(tmin=0, tmax=14).data * 1.0e6
- # Compute the mean value for each channel
- mean_values = cropped_data.mean(axis=1) # Mean across time points for each channel
- # Iterate over each channel and append the mean value to the all_ROIs DataFrame
- for channel_idx, mean_value in enumerate(mean_values):
- this_df = pd.DataFrame(
- {'ID': subj_id, 'Channel': channel_idx, 'Chroma': chroma,
- 'Condition': evoked, 'MeanValue': mean_value}, index=[0])
- df_babies = pd.concat([df_babies, this_df], ignore_index=True)
- # Print a message when done processing data for the current subject
- print(f"Finished processing data for Subject ID: {subj_id}")
- # Print a message when done processing data for the last subject of the current condition
- print(f"Finished processing data for all subjects in Condition: {evoked}")
- # Cut down the dataframe just to the conditions we are interested in
- df_babies.query("Condition in ['Interacting', 'NonInteracting']")
- # Compute contrast of interest to use with the 'permutation_cluster_1samp_test' function
- difference_df = df_babies.pivot_table(
- index=['ID', 'Channel', 'Chroma'],
- columns='Condition',
- values='MeanValue'
- )
- difference_df['Difference'] = difference_df['Interacting'] - difference_df['NonInteracting']
- difference_df = difference_df.reset_index()
- print(difference_df)
- # Pivot the DataFrame
- df_pivoted = difference_df.pivot(index='ID', columns='Channel', values='Difference')
- ## Calculate cluster-forming thershold
- n_observations = 20
- pval = 0.1 # arbitrary
- df = n_observations - 1 # degrees of freedom for the test
- thresh = scipy.stats.t.ppf(1 - pval / 2, df) # two-tailed, t distribution
- # Get the sensor locations for HbO channels
- ch_locs = np.array([epochs['Interacting'].info['chs'][pick]['loc'][:3] for pick in hbo_picks_facing])
- # Compute pairwise distances between sensors
- dists = squareform(pdist(ch_locs))
- # Define a distance threshold to determine adjacency (e.g., 0.03 meters)
- distance_threshold = 0.025
- adjacency = dists < distance_threshold
- adjacency_2, chs = mne.channels.find_ch_adjacency(evoked_hbo_facing[0].info, ch_type=None)
- mne.viz.plot_ch_adjacency(evoked_hbo_facing[0].info, adjacency, chs, kind='2d', edit=False)
- # Ensure the adjacency matrix is symmetric and binary
- adjacency = adjacency.astype(int)
- # Convert adjacency matrix to SciPy sparse matrix
- adjacency_sparse = csr_matrix(adjacency)
- # Check if the adjacency matrix has the correct size
- print(f'Adjacency matrix shape: {adjacency_sparse.shape}')
- print(f'Number of channels: {len(hbo_picks_facing)}')
- ## Run permutation test
- T_obs, clusters, cluster_p_values, H0 = permutation_cluster_1samp_test(
- df_pivoted, n_permutations=5000, out_type='indices', tail = 0,
- n_jobs=1, threshold=thresh, adjacency=adjacency_sparse)
- # Find significant clusters
- significant_clusters = np.where(cluster_p_values < 0.05)[0]
- # Filter out single-channel clusters
- filtered_clusters = []
- filtered_p_values = []
- for cluster_idx in significant_clusters:
- cluster = clusters[cluster_idx]
- # Assuming cluster[0] contains channel indices
- unique_channels = np.unique(cluster[0])
- if len(unique_channels) > 0: # Exclude single-channel clusters
- filtered_clusters.append(cluster)
- filtered_p_values.append(cluster_p_values[cluster_idx])
- # Convert to numpy arrays for easier handling
- filtered_clusters = np.array(filtered_clusters, dtype=object)
- filtered_p_values = np.array(filtered_p_values)
- print(f"\nNumber of significant clusters (excluding single-channel clusters): {len(filtered_clusters)}")
- for i, cluster in enumerate(filtered_clusters):
- cluster_idx = significant_clusters[i]
- print(f"Cluster {cluster_idx}:")
- print(f" p-value: {filtered_p_values[i]}")
- print(f" Channels involved: {np.unique(cluster[0])}")
- # Compute the mean T-values across time for visualization
- T_obs_avg = T_obs # Average T-values over the time dimension
- for i, cluster in enumerate(filtered_clusters):
- significant_chs = np.unique(cluster[0]) # Get unique channel indices from the cluster
- # Ensure significant_chs contains valid integer indices
- significant_chs = significant_chs.astype(int)
- significant_chs = significant_chs[significant_chs < len(T_obs_avg)] # Ensure indices are within bounds
- # Create a mask to highlight significant channels
- mask = np.zeros_like(T_obs_avg, dtype=bool)
- mask[significant_chs] = True # Mask significant channels
- # Plot topomap with the mask
- info = evoked_hbo_facing[0].info
- fig, ax = plt.subplots()
- vmin, vmax = -4, 4
- im, _ = mne.viz.plot_topomap(T_obs_avg, info, axes=ax, show=False, cmap='RdBu_r',
- mask=mask, extrapolate='local', sphere='auto',
- image_interp='linear', vlim=(vmin, vmax),
- mask_params=dict(marker='o', markerfacecolor='black',
- markersize=10))
- # Add colorbar
- cbar = fig.colorbar(im, ax=ax)
- cbar.set_label("T-value") # Label for clarity
- ax.set_title(f"Cluster {significant_clusters[i]} (p={filtered_p_values[i]:.3f})")
- plt.show()
- # Plot all clusters on the same topomap (note that this has the effect of creating a 'fake' red blob)
- combined_mask = np.zeros_like(T_obs_avg, dtype=bool)
- for cluster in filtered_clusters:
- significant_chs = np.unique(cluster[0]) # Get unique channel indices
- significant_chs = significant_chs.astype(int)
- significant_chs = significant_chs[significant_chs < len(T_obs_avg)]
- combined_mask[significant_chs] = True # Mark channels as significant
- info = evoked_hbo_facing[0].info
- fig, ax = plt.subplots()
- # Plot topomap with combined mask
- im, _ = mne.viz.plot_topomap(T_obs_avg, info, axes=ax, show=False, cmap='RdBu_r',
- mask=combined_mask, extrapolate='local', sphere='auto',
- vlim=(vmin, vmax),
- mask_params=dict(marker='o', markerfacecolor='black', markersize=10))
- # Add colorbar
- cbar = fig.colorbar(im, ax=ax)
- cbar.set_label("T-value")
- plt.show()
- #%% Group-level analysis - Conjuction [Interacting > NonInteracting] vs. [Single > baseline]
- # == Interacting > NonInteracting
- model_results_interacting = model_results.copy()
- significant_results_interacting = model_results_interacting.copy()
- # # Set values to 0 where p-value is >= 0.05
- significant_results_interacting.loc[significant_results_interacting['P-val'] > 0.051, 'T-stat'] = 0 # Ensure correct column name
- significant_results_interacting.loc[significant_results_interacting['T-stat'] < 0, 'T-stat'] = 0
- # == Single > baseline
- df_single = df_babies.query("Condition in ['Single']")
- df_single['ch_name'] = df_single['Channel'].replace(channel_mapping) #channel mapping defined earlier
- # Run group level model and convert to dataframe
- model_single = Lmer('MeanValue ~ -1 + ch_name + (1|ID)', data=df_single)
- model_results_single = model_single.fit()
- # Rename and sort channel names (defind in previous analyses)
- model_results_single['ch_name'] = model_results_single.index.str.replace('ch_name', '', regex=False)
- # Reorder the DataFrame based on the 'ch_name' column according to the desired order
- model_results_single['ch_name'] = pd.Categorical(model_results_single['ch_name'], categories=desired_order, ordered=True)
- # Sort the DataFrame based on the new 'ch_name' order
- model_results_single = model_results_single.sort_values('ch_name')
- model_results_single['ch_name'] = model_results_single.index.str.replace('ch_name', '', regex=False).str.replace(':ConditionSingle', '', regex=False)
- model_results_single['ch_name'] = pd.Categorical(model_results_single['ch_name'], categories=desired_order, ordered=True)
- # Sort the DataFrame based on the new 'ch_name' order
- model_results_single = model_results_single.sort_values('ch_name')
- # To view the model summary
- print(model_results_single)
- significant_results_single = model_results_single.copy()
- # # Set values to 0 where p-value is >= 0.05
- significant_results_single.loc[significant_results_single['P-val'] > 0.051, 'T-stat'] = 0 # Ensure correct column name
- significant_results_single.loc[significant_results_single['T-stat'] < 0, 'T-stat'] = 0
- # == Combine results
- combined_result = significant_results_interacting.copy()
- significant_results_single.index = significant_results_single.index.str[:16]
- significant_results_interacting.index = significant_results_interacting.index.str[:16]
- # Now check if they match
- print(significant_results_interacting.index.equals(significant_results_single.index)) # Should be True
- # A trick to be able to plot the two contrasts on the same brain. Only t-value is relevant in this dataframe
- combined_result['T-stat'] = significant_results_interacting['T-stat'] - significant_results_single['T-stat']
- ## Plot results with a custom color bar to be able to show significant channels from both contrasts
- n_colors_half = 128 # number of colors per half
- viridis = plt.get_cmap('PiYG')
- rdbu_r = plt.get_cmap('RdBu_r')
- # Extract upper half of viridis and reverse it
- viridis_upper = viridis(np.linspace(0.5, 1.0, n_colors_half))[::-1]
- # Extract upper half of RdBu_r as is
- rdbu_r_upper = rdbu_r(np.linspace(0.5, 1.0, n_colors_half))
- # Concatenate both halves
- combined_colors = np.vstack((viridis_upper, rdbu_r_upper))
- # Define vmin/vmax
- vmin, vmax = -4, 4
- values = np.linspace(vmin, vmax, combined_colors.shape[0])
- # Define fading range
- fade_halfwidth = 0.6 # adjust for smoother fade
- fade_start_neg, fade_end_neg = -1.96 - fade_halfwidth, -1.96 + fade_halfwidth
- fade_start_pos, fade_end_pos = 1.96 - fade_halfwidth, 1.96 + fade_halfwidth
- # White color
- white = np.array([1, 1, 1, 1])
- # Apply fading into white
- for i, val in enumerate(values):
- if -1.96 <= val <= 1.96:
- combined_colors[i] = white
- elif fade_start_neg < val < fade_end_neg:
- t = (val - fade_start_neg) / (fade_end_neg - fade_start_neg)
- combined_colors[i] = (1-t) * combined_colors[i] + t * white
- elif fade_start_pos < val < fade_end_pos:
- t = (val - fade_start_pos) / (fade_end_pos - fade_start_pos)
- combined_colors[i] = (1-t) * white + t * combined_colors[i]
- # Create custom colormap
- custom_cmap = LinearSegmentedColormap.from_list(
- "custom_with_white_fading_center", combined_colors
- )
- ## Plot results!
- brain = mne_nirs.visualisation.plot_glm_surface_projection(
- raw_haemo_b.copy().pick("hbo"),
- combined_result,
- clim=clim,
- view='lateral',
- colormap=custom_cmap,
- colorbar=True,
- size=(800, 700),
- value='T-stat',
- distance=0.015
- )
- ## == Conjunction analysis
- # Ensure both have consistent ch_name as index
- interacting_df = model_results_interacting.set_index('ch_name')
- single_df = model_results_single.set_index('ch_name')
- # Select relevant columns (t-statistics)
- interacting_t = interacting_df[['T-stat']].rename(columns={'T-stat': 't_interacting'})
- single_t = single_df[['T-stat']].rename(columns={'T-stat': 't_single'})
- # Merge on ch_name
- conj_df = interacting_t.join(single_t, how='inner')
- # Compute minimum t-statistics per channel (conjunction)
- conj_df['t_min'] = conj_df[['t_interacting', 't_single']].min(axis=1)
- # Convert min t-stat to one-sided p-value using standard normal survival function
- conj_df['P_conjunction'] = norm.sf(conj_df['t_min']) # sf = 1 - cdf
- # Mark significant conjunctions
- alpha = 0.05
- conj_df['Significant'] = conj_df['P_conjunction'] < alpha
- # List channels passing conjunction test
- significant_conjunction_channels = conj_df[conj_df['Significant']].index.tolist()
- print("Conjunction-significant channels:", significant_conjunction_channels)
- # Optional: display summary table
- print(conj_df.sort_values('P_conjunction'))
fNIRS_analyses_shared.py, no license · at the source
Overview
Abstract
Social cognition develops in the second year of life. Yet even in the first months, infants show sophisticated representation of social interactions. The neural structures supporting these early capacities remain unknown. Here, using functional near-infrared spectroscopy, we show that, like adults, preverbal infants exhibit selective increase in the activity of posterior superior-temporal and temporo-parietal regions when viewing individuals facing and moving toward (vs. away from) each other, as if interacting. No reliable difference emerged between 6- and 10-month-olds, although neural effects were more widespread in the older group, who also showed consistently longer looking times toward interacting dyads. Neural effects of social interactions were spatially distinct from effects of body motion perception, indicating separate mechanisms for perceiving biological agents and interacting agents. The early tuning to visuo-spatial cues of social engagement documented here may orient infants’ attention toward social interactions, thereby facilitating the discovery and learning of human social relationships.
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 2 matches between paragraphs and lines of code.
OSF 874rw
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
- 27 September 2026: the link answers (HTTP 200)
1 file
- Data and code/
fNIRS_analyses_shared.py , Python, 875 lines, 2 matches
The paper's code and data availability statement is in the Data section.
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;
- 1 script, each with its path and the digest of its content;
- 2 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data and code availability
• Pre-processed fNIRS data and infant looking time data that support the findings of this study are publicly available on the Open Science Framework. 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, 27 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 10 keywords, 3 funders, 87 references, 3 RRIDs.
Cite
This paper
Mello, M., Serraille, E., Hochmann, J.-R., & Papeo, L. (2026). Neural selectivity for social interactions in the infant brain. iScience, 29(6), 116287. https://
BibTeX
@article{mello2026neural
author = {Mello, Manuel and Serraille, Emilie and Hochmann, Jean-Rémy and Papeo, Liuba},
title = {{Neural selectivity for social interactions in the infant brain}},
journal = {iScience},
year = {2026},
month = jun,
volume = {29},
number = {6},
pages = {116287},
publisher = {Elsevier},
issn = {2589-0042},
doi = {10.1016/
url = {https://
pmid = {42291211},
pmcid = {PMC13264107}
}
RIS
TY - JOUR
AU - Mello, Manuel
AU - Serraille, Emilie
AU - Hochmann, Jean-Rémy
AU - Papeo, Liuba
TI - Neural selectivity for social interactions in the infant brain
T2 - iScience
J2 - iScience
PY - 2026
DA - 2026/
VL - 29
IS - 6
SP - 116287
SN - 2589-0042
PB - Elsevier
DO - 10.1016/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1016/
"type": "article-journal",
"title": "Neural selectivity for social interactions in the infant brain",
"container-title": "iScience",
"author": [
{
"family": "Mello",
"given": "Manuel"
},
{
"family": "Serraille",
"given": "Emilie"
},
{
"family": "Hochmann",
"given": "Jean-Rémy"
},
{
"family": "Papeo",
"given": "Liuba"
}
],
"container-title-short":
"volume": "29",
"issue": "6",
"page": "116287",
"DOI": "10.1016/
"PMID": "42291211",
"PMCID": "PMC13264107",
"ISSN": "2589-0042",
"publisher": "Elsevier",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
8
]
]
}
}
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.dcn.2026.101765 [code]
- Fusiform face area development correlates with development in higher-order social brain regions.Journal: Developmental cognitive neuroscienceIn common: pandas, SciPy, Matplotlib, 1 other tool, 8 references
- [2] doi:10.1038/s41467-026-71151-2 [code]
- Common and distinct neural correlates of social interaction processing and theory of mind in narratives.Journal: Nature communicationsIn common: pandas, SciPy, Matplotlib, 1 other tool, 7 references
- [3] doi:10.1016/j.isci.2026.116695 [code]
- The naive brain detects face-to-face biological motion.Journal: iScienceIn common: 7 references
- [4] doi:10.1162/imag.a.1309 [code]
- Probing the content of semantic representations in body-selective regions.Journal: Imaging neuroscience (Cambridge, Mass.)In common: pandas, SciPy, Matplotlib, 1 other tool, 5 references
- [5] doi:10.1162/imag.a.1289 [code]
- Measurement prediction and power analysis for fNIRS and DOT.Journal: Imaging neuroscience (Cambridge, Mass.)In common: MNE-Python, SciPy, Matplotlib, 1 other tool, 5 references
- [6] doi:10.1117/1.nph.13.2.025001 [code]
- Surface-based image reconstruction optimization for high-density functional near-infrared spectroscopy.Journal: NeurophotonicsIn common: PyWavelets, MNE-Python, pandas, 3 other tools, 2 references
- [7] doi:10.1038/s42003-026-09834-1 [code]
- Distinct perceptual and conceptual representations of natural actions along the lateral and dorsal visual streams.Journal: Communications biologyIn common: 6 references
- [8] doi:10.1038/s41467-026-76098-y [code]
- A single computational objective can produce specialization of streams in visual cortex.Journal: Nature communicationsIn common: pandas, SciPy, Matplotlib, 1 other tool, 4 references
- [9] doi:10.1117/1.nph.13.3.035006 [code]
- Characterizing developmental changes in infant habituation using functional change point detection.Journal: NeurophotonicsIn common: pandas, SciPy, Matplotlib, 1 other tool, developmental, 3 references
- [10] doi:10.1371/journal.pone.0358044 [code]
- An exploratory fNIRS study on directional asymmetry in segmental discrimination: Lateral-rhotic (/
la-ra/ ) and labial-dorsal (/ ba-ga/ ) contrasts tested in 5- and 9-month-old Japanese infants. Journal: PloS oneIn common: pandas, SciPy, Matplotlib, 1 other tool, developmental, 3 references
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 1 script, and 2 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:9f615c81e4a4a423…
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
[.
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.
