OSCR

Neural selectivity for social interactions in the infant brain.

Code ↔ Paper

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

The 2 matches
  1. [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. [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

  1. # -*- coding: utf-8 -*-
  2. """
  3. fNIRS analyses for "Neural selectivity for social interactions in the infant brain"
  4. Analysis script for the infant study (Experiment 2). Analyses for adult data is identical,
  5. except for small preprocessing dissimilarities, indicated in the manuscript.
  6. @author: mmello
  7. """
  8. #%% Import libraries and define functions
  9. import os
  10. import os.path as op
  11. os.environ['R_HOME'] = '...' # your path to R
  12. # General-purpose packages
  13. import matplotlib.pyplot as plt
  14. import pandas as pd
  15. import scipy
  16. import numpy as np
  17. import matplotlib as mpl
  18. from matplotlib.colors import LinearSegmentedColormap
  19. from itertools import compress
  20. import collections
  21. from copy import deepcopy
  22. from scipy.stats import sem, norm
  23. from scipy.signal import find_peaks
  24. from scipy.spatial.distance import pdist, squareform
  25. from scipy.sparse import csr_matrix
  26. import pywt
  27. from pymer4.models import Lmer
  28. # MNE packages
  29. import mne
  30. import mne_nirs
  31. from mne.stats import permutation_cluster_1samp_test
  32. # The following two imports are used to download necessary files to plot data on adult and infant brain templates
  33. # from mne.datasets import fetch_infant_template
  34. # from mne.datasets import fetch_fsaverage
  35. # Define the wavelet-based filtering function
  36. def wavelet_based_filtering(signal, wavelet='db5', level=None, alpha=5):
  37. coeffs = pywt.wavedec(signal, wavelet, level=level)
  38. for i in range(1, len(coeffs)):
  39. detail_coeffs = coeffs[i]
  40. q1 = np.percentile(detail_coeffs, 25)
  41. q3 = np.percentile(detail_coeffs, 75)
  42. iqr = q3 - q1
  43. lower_bound = q1 - alpha * iqr
  44. upper_bound = q3 + alpha * iqr
  45. coeffs[i] = np.where((detail_coeffs < lower_bound) | (detail_coeffs > upper_bound), 0, detail_coeffs)
  46. filtered_signal = pywt.waverec(coeffs, wavelet)
  47. # Ensure the filtered signal has the same length as the original signal
  48. if len(filtered_signal) != len(signal):
  49. filtered_signal = filtered_signal[:len(signal)] # Trim to the original length
  50. return filtered_signal
  51. #%% Define individual-level analyses
  52. data_dir = "C:/Users/mmello/Desktop/fNIRS babies/DATA/together/"
  53. def facingness_waveform(subj_path, ID):
  54. raw = mne.io.read_raw_nirx(subj_path, verbose=True, preload=True)
  55. raw.info["subject_info"]
  56. raw.get_data().shape
  57. ## Plot raw data
  58. # mne.viz.plot_sensors(raw.info, kind='topomap', show_names=True)
  59. # raw.plot(duration=20000, show_scrollbars=False, n_channels=len(raw.ch_names))
  60. ## Define triggers
  61. raw.annotations.set_durations({'1.0': 10, '2.0': 10, '3.0': 10,
  62. })
  63. raw.annotations.rename({'1.0': 'Interacting', '2.0': 'NonInteracting', '3.0': 'Single',
  64. })
  65. ## Convert to optical density (OD)
  66. raw_od = mne.preprocessing.nirs.optical_density(raw)
  67. # Plot OD data
  68. # raw_od.plot(n_channels=len(raw_od.ch_names), duration=15000, show_scrollbars=False)
  69. ## Find bad channels and interpolate (plotting is commented out)
  70. sci = mne.preprocessing.nirs.scalp_coupling_index(raw_od) #h_freq=1.35, h"_trans_bandwidth=0.1
  71. # fig, ax = plt.subplots()
  72. # ax.hist(sci)
  73. # ax.set(xlabel='Scalp Coupling Index', ylabel='Count', xlim=[0, 1])
  74. raw_od.info["bads"] = list(compress(raw_od.ch_names, sci < 0.7))
  75. print(raw_od.info["bads"])
  76. raw_od.interpolate_bads()
  77. ## Clean data with TDDR
  78. raw_od = mne.preprocessing.nirs.temporal_derivative_distribution_repair(raw_od)
  79. ## Wavelet-based filtering
  80. # Extract data and times from optical density
  81. data_h, times = raw_od.get_data(return_times=True)
  82. # Apply wavelet-based filtering to each channel
  83. filtered_data_wavelet = np.array([wavelet_based_filtering(channel) for channel in data_h])
  84. raw_od._data = filtered_data_wavelet
  85. ## Convert to haemoglobin concentration data and band-pass filter
  86. raw_haemo_b = mne.preprocessing.nirs.beer_lambert_law(raw_od, ppf=0.5)
  87. raw_haemo_b = raw_haemo_b.filter(0.01, 0.2, h_trans_bandwidth=0.2, l_trans_bandwidth=0.01, verbose=False)
  88. ## Negative correlation enhancement for systemic noise removal
  89. raw_haemo_b = mne_nirs.signal_enhancement.enhance_negative_correlation(raw_haemo_b)
  90. ## Plot cleaned haemodynamic data
  91. # raw_haemo_b.plot(n_channels=len(raw_haemo_b.ch_names), duration=20000, show_scrollbars=False)
  92. ## Extracting epochs (and plot events)
  93. events, event_dict = mne.events_from_annotations(raw_haemo_b, regexp='(Interacting|NonInteracting|Single)')
  94. # mne.viz.plot_events(events, event_id=event_dict,
  95. # sfreq=raw_haemo_b.info['sfreq'])
  96. epochs = mne.Epochs(raw_haemo_b, events, event_id=event_dict, tmin=-2, tmax=16,
  97. verbose=False, preload=True,
  98. baseline=(-2, 0),
  99. )
  100. ## Removing trials based on video recordings (6-mo)
  101. if ID == 'SUB02 - 6mo':
  102. indices_to_remove = [19, 22, 24, 27, 28, 30]
  103. epochs.drop(indices_to_remove)
  104. elif ID == 'SUB03 - 6mo':
  105. indices_to_remove = [33, 36]
  106. epochs.drop(indices_to_remove)
  107. elif ID == 'SUB04 - 6mo':
  108. indices_to_remove = [13, 15, 19, 22, 23, 24]
  109. epochs.drop(indices_to_remove)
  110. elif ID == 'SUB05 - 6mo':
  111. indices_to_remove = [18, 19]
  112. epochs.drop(indices_to_remove)
  113. elif ID == 'SUB06 - 6mo':
  114. indices_to_remove = [34, 37, 38]
  115. epochs.drop(indices_to_remove)
  116. elif ID == 'SUB07 - 6mo':
  117. indices_to_remove = [27, 41, 43, 44, 46, 47]
  118. epochs.drop(indices_to_remove)
  119. elif ID == 'SUB08 - 6mo':
  120. indices_to_remove = [21]
  121. epochs.drop(indices_to_remove)
  122. elif ID == 'SUB09 - 6mo':
  123. indices_to_remove = [12, 18, 19, 22, 23, 24, 26, 27]
  124. epochs.drop(indices_to_remove)
  125. elif ID == 'SUB11 - 6mo':
  126. indices_to_remove = [35, 42]
  127. epochs.drop(indices_to_remove)
  128. elif ID == 'SUB12 - 6mo':
  129. indices_to_remove = [19, 20, 22, 23, 25]
  130. epochs.drop(indices_to_remove)
  131. elif ID == 'SUB13 - 6mo':
  132. indices_to_remove = [16, 17, 19, 21, 22, 27]
  133. epochs.drop(indices_to_remove)
  134. elif ID == 'SUB14 - 6mo':
  135. indices_to_remove = [10, 16, 17, 18, 19]
  136. epochs.drop(indices_to_remove)
  137. elif ID == 'SUB15 - 6mo':
  138. indices_to_remove = [12, 13, 17, 21, 28]
  139. epochs.drop(indices_to_remove)
  140. elif ID == 'SUB16 - 6mo':
  141. indices_to_remove = [15, 17, 20, 25, 27, 28]
  142. epochs.drop(indices_to_remove)
  143. elif ID == 'SUB17 - 6mo':
  144. indices_to_remove = [29]
  145. epochs.drop(indices_to_remove)
  146. elif ID == 'SUB18 - 6mo':
  147. indices_to_remove = [3, 10, 13, 17, 18, 21, 24, 30, 32, 33, 34]
  148. epochs.drop(indices_to_remove)
  149. elif ID == 'SUB19 - 6mo':
  150. indices_to_remove = [17, 18, 19, 26]
  151. epochs.drop(indices_to_remove)
  152. elif ID == 'SUB21 - 6mo':
  153. indices_to_remove = [9, 10, 11, 12, 13, 14, 15, 18, 21, 22]
  154. epochs.drop(indices_to_remove)
  155. ## Removing trials based on video recordings (10-mo)
  156. if ID == 'SUB02 - 10mo':
  157. indices_to_remove = [14, 17, 21]
  158. epochs.drop(indices_to_remove)
  159. elif ID == 'SUB03 - 10mo':
  160. indices_to_remove = [15, 16, 17]
  161. epochs.drop(indices_to_remove)
  162. elif ID == 'SUB04 - 10mo':
  163. indices_to_remove = [5, 11, 12, 13, 18, 21, 23, 24, 25]
  164. epochs.drop(indices_to_remove)
  165. elif ID == 'SUB05 - 10mo':
  166. indices_to_remove = [25, 32, 34, 46]
  167. epochs.drop(indices_to_remove)
  168. elif ID == 'SUB06 - 10mo':
  169. indices_to_remove = [16, 29, 31, 34, 41, 42]
  170. epochs.drop(indices_to_remove)
  171. elif ID == 'SUB07 - 10mo':
  172. indices_to_remove = [24]
  173. epochs.drop(indices_to_remove)
  174. elif ID == 'SUB08 - 10mo':
  175. indices_to_remove = []
  176. epochs.drop(indices_to_remove)
  177. elif ID == 'SUB09 - 10mo':
  178. indices_to_remove = [23, 24, 29, 31, 33, 34, 38]
  179. epochs.drop(indices_to_remove)
  180. elif ID == 'SUB10 - 10mo':
  181. indices_to_remove = [9, 11, 22, 23, 38]
  182. epochs.drop(indices_to_remove)
  183. elif ID == 'SUB11 - 10mo':
  184. indices_to_remove = [5, 6, 7, 8, 9, 11, 12, 13, 14, 18, 21, 27, 28, 30, 31, 32, 34]
  185. epochs.drop(indices_to_remove)
  186. elif ID == 'SUB12 - 10mo':
  187. indices_to_remove = [20, 21]
  188. epochs.drop(indices_to_remove)
  189. elif ID == 'SUB13 - 10mo':
  190. indices_to_remove = [6, 7, 8, 12, 15, 18, 19, 20, 21, 23]
  191. epochs.drop(indices_to_remove)
  192. elif ID == 'SUB14 - 10mo':
  193. indices_to_remove = [8, 10, 17, 31, 33, 34]
  194. epochs.drop(indices_to_remove)
  195. elif ID == 'SUB15 - 10mo':
  196. indices_to_remove = [8, 9, 17]
  197. epochs.drop(indices_to_remove)
  198. elif ID == 'SUB16 - 10mo':
  199. indices_to_remove = [14, 18, 24, 25]
  200. epochs.drop(indices_to_remove)
  201. elif ID == 'SUB17 - 10mo':
  202. indices_to_remove = [29, 33, 35]
  203. epochs.drop(indices_to_remove)
  204. elif ID == 'SUB18 - 10mo':
  205. indices_to_remove = [13, 14, 20, 21, 23, 28, 30]
  206. epochs.drop(indices_to_remove)
  207. elif ID == 'SUB19 - 10mo':
  208. indices_to_remove = [25, 27, 31, 32]
  209. epochs.drop(indices_to_remove)
  210. elif ID == 'SUB20 - 10mo':
  211. indices_to_remove = [8, 11, 12, 14, 15, 19, 21, 24, 25]
  212. epochs.drop(indices_to_remove)
  213. elif ID == 'SUB21 - 10mo':
  214. indices_to_remove = [12, 17, 20]
  215. epochs.drop(indices_to_remove)
  216. print(len(epochs)) #print number of remaining trials
  217. # Create dictionaries of HbO/HbR responses
  218. evoked_dict = {'Facing/HbO': epochs['Interacting'].average(picks='hbo'),
  219. 'Facing/HbR': epochs['Interacting'].average(picks='hbr'),
  220. 'Away/HbO': epochs['NonInteracting'].average(picks='hbo'),
  221. 'Away/HbR': epochs['NonInteracting'].average(picks='hbr'),
  222. 'Single/HbO': epochs['Single'].average(picks='hbo'),
  223. 'Single/HbR': epochs['Single'].average(picks='hbr')}
  224. ## Rename channels until the encoding of frequency in ch_name is fixed
  225. for condition in evoked_dict:
  226. evoked_dict[condition].rename_channels(lambda x: x[:-4])
  227. ## Store individual data for group-level analysis
  228. return raw, raw_haemo_b, epochs
  229. #%% Individual-level analysis
  230. ## Load data
  231. # Separate dictionaries for each group
  232. all_evokeds_6mo = collections.defaultdict(list)
  233. all_evokeds_10mo = collections.defaultdict(list)
  234. for root, dirs, files in os.walk(os.path.normpath(data_dir), topdown=False):
  235. for subj in dirs:
  236. if 'sync' not in subj and 'bad' not in subj:
  237. print("Now processing subject: " + subj)
  238. subj_path = os.path.join(data_dir, subj)
  239. # Run your individual analysis
  240. raw, raw_haemo_b, epochs = facingness_waveform(subj_path, subj)
  241. # Save data into the appropriate dictionary
  242. if "6mo" in subj:
  243. for condition in epochs.event_id:
  244. all_evokeds_6mo[condition].append(epochs[condition].average())
  245. elif "10mo" in subj:
  246. for condition in epochs.event_id:
  247. all_evokeds_10mo[condition].append(epochs[condition].average())
  248. #%% Visualize channel montage in infants
  249. subjects_dir = op.join(mne.datasets.sample.data_path(), "subjects")
  250. ##6-months
  251. trans = mne.read_trans("C:/Users/mmello/mne_data/MNE-sample-data/subjects/ANTS6-0Months3T/bem/fsaverage-trans.fif")
  252. brain = mne.viz.Brain(
  253. "ANTS6-0Months3T", subjects_dir=subjects_dir, background="w", cortex="0.5"
  254. )
  255. brain.add_sensors(
  256. raw_haemo_b.info, trans=trans, fnirs=["channels", "pairs", "sources", "detectors"]
  257. )
  258. brain.show_view(azimuth=180, elevation=80, distance=250)
  259. ## 10-months
  260. trans = mne.read_trans("C:/Users/mmello/mne_data/MNE-sample-data/subjects/ANTS10-5Months3T/bem/fsaverage-trans.fif")
  261. brain = mne.viz.Brain(
  262. "ANTS10-5Months3T", subjects_dir=subjects_dir, background="w", cortex="0.5"
  263. )
  264. brain.add_sensors(
  265. raw_haemo_b.info, trans=trans, fnirs=["channels", "pairs", "sources", "detectors"]
  266. )
  267. brain.show_view(azimuth=180, elevation=80, distance=250)
  268. #%% Peak detection
  269. # Merge into one dict: all babies (6mo + 10mo)
  270. all_evokeds = collections.defaultdict(list)
  271. # Add 6mo data
  272. for condition, evokeds in all_evokeds_6mo.items():
  273. all_evokeds[condition].extend(evokeds)
  274. # Add 10mo data
  275. for condition, evokeds in all_evokeds_10mo.items():
  276. all_evokeds[condition].extend(evokeds)
  277. # Stack data from all evoked responses for Facing and Away
  278. evokeds_facing = all_evokeds['Interacting']
  279. evokeds_away = all_evokeds['NonInteracting']
  280. # Concatenate both conditions
  281. evokeds_combined = evokeds_facing + evokeds_away
  282. # Build array: (n_subjects_total, n_channels, n_times)
  283. data_array = np.array([evoked._data for evoked in evokeds_combined])
  284. # Average across channels for each subject
  285. data_avg_channels = data_array.mean(axis=1)
  286. # Compute grand average and standard error
  287. grand_avg = data_avg_channels.mean(axis=0)
  288. conf_interval = sem(data_avg_channels, axis=0)
  289. # Get time axis from one of the evoked objects
  290. times = evokeds_combined[0].times
  291. # Find peaks in the grand average
  292. peaks_avg, _ = find_peaks(grand_avg, height=0)
  293. # Keep only the first peak (if any)
  294. if len(peaks_avg) > 0:
  295. first_peak = peaks_avg[0]
  296. else:
  297. first_peak = None
  298. # Plot
  299. plt.figure(figsize=(10, 5))
  300. plt.plot(times, grand_avg, color='red', label='Mean across all channels')
  301. plt.fill_between(times, grand_avg - conf_interval, grand_avg + conf_interval,
  302. color='red', alpha=0.3, label='95% CI')
  303. # Plot only the first peak marker
  304. if first_peak is not None:
  305. plt.plot(times[first_peak], grand_avg[first_peak], "x", color='black', label='Peak')
  306. plt.xlabel('Time (s)')
  307. plt.ylabel('Amplitude')
  308. plt.title('Grand Average Across All Channels - Interacting + Non-interacting')
  309. plt.xlim(-2.5,15)
  310. plt.ylim(-3.5*1e-7,3.5*1e-7)
  311. plt.legend(loc='lower center')
  312. plt.grid(True)
  313. plt.tight_layout()
  314. plt.show()
  315. # Print first peak time
  316. if first_peak is not None:
  317. print('First peak time (s):', times[first_peak])
  318. else:
  319. print('No peaks found.')
  320. #%% Group-level analyses - Visualization
  321. # Plot average waveform
  322. fig, axes = plt.subplots(nrows=1, ncols=len(all_evokeds), figsize=(17, 5))
  323. lims = dict(hbo=[-3, 3], hbr=[-3, 3])
  324. #
  325. for (pick, color) in zip(['hbo', 'hbr'], ['r', 'b']):
  326. for idx, (evoked_name, evoked_data) in enumerate(all_evokeds.items()):
  327. mne.viz.plot_compare_evokeds(
  328. {evoked_name: evoked_data}, combine='mean',
  329. picks=pick, axes=axes[idx], show=False, ylim=lims,
  330. colors=[color], legend=False, ci=0.95,
  331. show_sensors=idx == 1
  332. )
  333. axes[idx].set_title('{}'.format(evoked_name))
  334. if idx == 0: # Set y-axis label for the first subplot
  335. axes[idx].set_ylabel('Haemoglobin concentration (μM)')
  336. # Remove grid for the current subplot
  337. axes[idx].grid(False)
  338. # Plot topomap for each condition
  339. times = np.arange(2, 16, 5.0)
  340. avg_evoked_facing = mne.grand_average(all_evokeds['Interacting'])
  341. avg_evoked_away = mne.grand_average(all_evokeds['NonInteracting'])
  342. avg_evoked_facing.plot_topomap(times=times, cmap=mpl.cm.coolwarm, extrapolate='local',
  343. sphere='auto', vlim=(-3, 3), image_interp='linear')
  344. avg_evoked_away.plot_topomap(times=times, cmap=mpl.cm.coolwarm, extrapolate='local',
  345. sphere='auto', vlim=(-3, 3), image_interp='linear')
  346. plt.show()
  347. ## Plot topomap for each Interacting - NonInteracting
  348. avg_evoked_con = avg_evoked_facing._data - avg_evoked_away._data
  349. info = avg_evoked_facing.info
  350. # Create a new Evoked object
  351. evoked_con = mne.EvokedArray(avg_evoked_con, info, tmin=avg_evoked_facing.times[0])
  352. evoked_con.plot_topomap(times=times, cmap=mpl.cm.coolwarm, extrapolate='local',
  353. sphere='auto', vlim=(-3, 3), image_interp='linear')
  354. plt.show()
  355. #%% Group-level analyses - Channel-wise analysis
  356. ## Create dataframes with by participant, by channel, by condition average response
  357. df_babies_6 = pd.DataFrame(columns=['ID', 'Channel', 'Chroma', 'Condition', 'MeanValue'])
  358. # Loop through the conditions (evoked)
  359. for idx, evoked in enumerate(all_evokeds_6mo):
  360. subj_id = 0 # Initialize subject ID counter
  361. # Loop through each subject's data within the current evoked condition
  362. for subj_data in all_evokeds_6mo[evoked]:
  363. subj_id += 1 # Increment subject ID
  364. # Iterate over the chroma types (e.g., hbo and hbr)
  365. for chroma in ["hbo"]:
  366. # Pick the data corresponding to the current chroma type
  367. data = deepcopy(subj_data).pick(chroma)
  368. # Crop the data to the specified time range and scale
  369. cropped_data = data.crop(tmin=0, tmax=14).data * 1.0e6
  370. # Compute the mean value for each channel
  371. mean_values = cropped_data.mean(axis=1) # Mean across time points for each channel
  372. # Iterate over each channel and append the mean value to the all_ROIs DataFrame
  373. for channel_idx, mean_value in enumerate(mean_values):
  374. this_df = pd.DataFrame(
  375. {'ID': subj_id, 'Channel': channel_idx, 'Chroma': chroma,
  376. 'Condition': evoked, 'MeanValue': mean_value}, index=[0])
  377. df_babies_6 = pd.concat([df_babies_6, this_df], ignore_index=True)
  378. # Print a message when done processing data for the current subject
  379. print(f"Finished processing data for Subject ID: {subj_id}")
  380. # Print a message when done processing data for the last subject of the current condition
  381. print(f"Finished processing data for all subjects in Condition: {evoked}")
  382. df_babies_10 = pd.DataFrame(columns=['ID', 'Channel', 'Chroma', 'Condition', 'MeanValue'])
  383. # Loop through the conditions (evoked)
  384. for idx, evoked in enumerate(all_evokeds_10mo):
  385. subj_id = 0 # Initialize subject ID counter
  386. # Loop through each subject's data within the current evoked condition
  387. for subj_data in all_evokeds_10mo[evoked]:
  388. subj_id += 1 # Increment subject ID
  389. # Iterate over the chroma types (e.g., hbo and hbr)
  390. for chroma in ["hbo"]:
  391. # Pick the data corresponding to the current chroma type
  392. data = deepcopy(subj_data).pick(chroma)
  393. # Crop the data to the specified time range and scale
  394. cropped_data = data.crop(tmin=0, tmax=14).data * 1.0e6
  395. # Compute the mean value for each channel
  396. mean_values = cropped_data.mean(axis=1) # Mean across time points for each channel
  397. # Iterate over each channel and append the mean value to the all_ROIs DataFrame
  398. for channel_idx, mean_value in enumerate(mean_values):
  399. this_df = pd.DataFrame(
  400. {'ID': subj_id, 'Channel': channel_idx, 'Chroma': chroma,
  401. 'Condition': evoked, 'MeanValue': mean_value}, index=[0])
  402. df_babies_10 = pd.concat([df_babies_10, this_df], ignore_index=True)
  403. # Print a message when done processing data for the current subject
  404. print(f"Finished processing data for Subject ID: {subj_id}")
  405. # Print a message when done processing data for the last subject of the current condition
  406. print(f"Finished processing data for all subjects in Condition: {evoked}")
  407. # Replace channel indices with their original names
  408. hbo_channels = [ch for ch in raw_haemo_b.info['ch_names'] if 'hbo' in ch.lower()]
  409. channel_mapping = {i: name for i, name in enumerate(hbo_channels)}
  410. df_babies_6['ch_name'] = df_babies_6['Channel'].replace(channel_mapping)
  411. df_babies_10['ch_name'] = df_babies_10['Channel'].replace(channel_mapping)
  412. ## Compute contrast (Interacting - NonInteracting) for 10-month-olds
  413. difference_df_6 = df_babies_6.pivot_table(
  414. index=['ID', 'ch_name', 'Chroma'],
  415. columns='Condition',
  416. values='MeanValue')
  417. difference_df_6['Difference'] = difference_df_6['Interacting'] - difference_df_6['NonInteracting']
  418. difference_df_6 = difference_df_6.reset_index()
  419. print(difference_df_6)
  420. ## Compute contrast (Interacting - NonInteracting) for 10-month-olds
  421. difference_df_10 = df_babies_10.pivot_table(
  422. index=['ID', 'ch_name', 'Chroma'],
  423. columns='Condition',
  424. values='MeanValue')
  425. difference_df_10['Difference'] = difference_df_10['Interacting'] - difference_df_10['NonInteracting']
  426. difference_df_10 = difference_df_10.reset_index()
  427. print(difference_df_6)
  428. difference_df_6['age'] = '6mo'
  429. difference_df_10['age'] = '10mo'
  430. df_concat = pd.concat([difference_df_10, difference_df_6], ignore_index=True)
  431. df_concat['age_contrast'] = df_concat['age'].map({'10mo': 0.5, '6mo': -0.5})
  432. ## Run channel-wise analysis on both infant groups
  433. model = Lmer('Difference ~ -1 + ch_name*age_contrast + (1|ID)', data=df_concat)
  434. model_results = model.fit()
  435. model.anova() # Effect of channel, bu no effect of age, nor interaction
  436. print(model_results) # Print model estimates (by-channel coefficients)
  437. ## Visualization of results on brain template (10month-old brain)
  438. # Necassary steps to adapt output to MNE visualization function (_plot_GLM_surface_projection)
  439. # Note that we are using this function flexibily to plot HbO concentration
  440. model_results = model_results.rename(columns={'Estimate': 'Coef.'})
  441. model_results['ch_name'] = model_results.index.str.replace('ch_name', '', regex=False)
  442. # Define the desired order for 'ch_name'
  443. desired_order = [
  444. 'S1_D1 hbo', 'S1_D2 hbo', 'S1_D9 hbo', 'S2_D1 hbo', 'S2_D2 hbo', 'S2_D3 hbo',
  445. 'S3_D3 hbo', 'S4_D2 hbo', 'S4_D4 hbo', 'S4_D5 hbo', 'S5_D3 hbo', 'S5_D4 hbo',
  446. 'S5_D6 hbo', 'S6_D4 hbo', 'S6_D5 hbo', 'S6_D6 hbo', 'S6_D7 hbo', 'S7_D6 hbo',
  447. 'S8_D6 hbo', 'S8_D7 hbo', 'S8_D8 hbo', 'S9_D1 hbo', 'S9_D9 hbo', 'S9_D10 hbo',
  448. 'S10_D10 hbo', 'S11_D10 hbo', 'S11_D11 hbo', 'S11_D12 hbo', 'S12_D9 hbo',
  449. 'S12_D11 hbo', 'S12_D13 hbo', 'S13_D11 hbo', 'S13_D12 hbo', 'S13_D13 hbo',
  450. 'S13_D14 hbo', 'S14_D12 hbo', 'S15_D12 hbo', 'S15_D14 hbo', 'S15_D15 hbo'
  451. ]
  452. # Reorder the DataFrame based on the 'ch_name' column according to the desired order
  453. model_results['ch_name'] = pd.Categorical(model_results['ch_name'], categories=desired_order, ordered=True)
  454. # Sort the DataFrame based on the new 'ch_name' order
  455. model_results = model_results.sort_values('ch_name')
  456. # # To view the model summary
  457. model_results = model_results.head(39) # Restrict the output to the channel coefficients (main effect)
  458. print(model_results) # Print again
  459. ## Plot the results!
  460. # Note that, in order to successfully plot with this function, internal options of the function itself need to be updated...
  461. # ... to show an infant brain (and not the adult one, which is default)
  462. # To get help about this, please contact me at [email hidden]
  463. clim = dict(kind='value', pos_lims=(0, 1, 4), neg_lims=(-4, -1, 0))
  464. brain = mne_nirs.visualisation.plot_glm_surface_projection(raw_haemo_b.copy().pick("hbo"),
  465. model_results, clim=clim, view='lateral', mode='weighted', colormap='RdBu_r',
  466. colorbar=False, size=(800, 700), value='T-stat', distance=0.015)
  467. # Plot only significant channels: t-value >= |1.96|
  468. significant_results_inter = model_results.copy()
  469. # # Set values to 0 where p-value is >= 0.05
  470. significant_results_inter.loc[significant_results_inter['P-val'] > 0.051, 'T-stat'] = 0 # Ensure correct column name
  471. # clim = dict(kind='value', pos_lims=(0, 1, 4), neg_lims=(-4, -1, 0))
  472. brain = mne_nirs.visualisation.plot_glm_surface_projection(raw_haemo_b.copy().pick("hbo"),
  473. significant_results_inter, clim=clim, view='lateral', colormap='RdBu_r',
  474. colorbar=False, size=(800, 700), value='T-stat', distance=0.015)
  475. ## Separate analyses for each group. Plot the same way
  476. ## 6-month-olds
  477. model = Lmer('Difference ~ -1 + ch_name + (1|ID)', data=difference_df_6)
  478. model_results = model.fit()
  479. print(model_results)
  480. ## 10-month-olds
  481. model = Lmer('Difference ~ -1 + ch_name + (1|ID)', data=difference_df_10)
  482. model_results = model.fit()
  483. print(model_results)
  484. #%% Group-level analyses - Cluster-based analysis
  485. ## The following is on both infant groups together.
  486. ## You can compare the two groups with the function 'permutation_cluster_test'
  487. # Function to ensure only HbO channels are picked
  488. def get_hbo_channels(evoked):
  489. hbo_picks = mne.pick_types(evoked.info, fnirs='hbo')
  490. return hbo_picks
  491. # Initialize lists for HbO evoked responses
  492. evoked_hbo_facing = []
  493. evoked_hbo_away = []
  494. # Loop through each evoked response in 'Facing' and 'Away' conditions
  495. for i in range(len(all_evokeds['Interacting'])):
  496. hbo_picks_facing = get_hbo_channels(all_evokeds['Interacting'][i])
  497. hbo_picks_away = get_hbo_channels(all_evokeds['NonInteracting'][i])
  498. for i in range(len(all_evokeds['Interacting'])):
  499. evoked_f = all_evokeds['Interacting'][i].copy().pick(mne.pick_types(all_evokeds['Interacting'][i].info, fnirs='hbo'))
  500. evoked_a = all_evokeds['NonInteracting'][i].copy().pick(mne.pick_types(all_evokeds['NonInteracting'][i].info, fnirs='hbo'))
  501. # Crop by time (0 to 14 seconds)
  502. evoked_f.crop(tmin=0, tmax=14)
  503. evoked_a.crop(tmin=0, tmax=14)
  504. evoked_hbo_facing.append(evoked_f)
  505. evoked_hbo_away.append(evoked_a)
  506. ## Extract average response for both infant groups together ('all_evokeds' dictionary)
  507. df_babies = pd.DataFrame(columns=['ID', 'Channel', 'Chroma', 'Condition', 'MeanValue'])
  508. # Loop through the conditions (evoked)
  509. for idx, evoked in enumerate(all_evokeds):
  510. subj_id = 0 # Initialize subject ID counter
  511. # Loop through each subject's data within the current evoked condition
  512. for subj_data in all_evokeds[evoked]:
  513. subj_id += 1 # Increment subject ID
  514. # Iterate over the chroma types (e.g., hbo and hbr)
  515. for chroma in ["hbo"]:
  516. # Pick the data corresponding to the current chroma type
  517. data = deepcopy(subj_data).pick(chroma)
  518. # Crop the data to the specified time range and scale
  519. cropped_data = data.crop(tmin=0, tmax=14).data * 1.0e6
  520. # Compute the mean value for each channel
  521. mean_values = cropped_data.mean(axis=1) # Mean across time points for each channel
  522. # Iterate over each channel and append the mean value to the all_ROIs DataFrame
  523. for channel_idx, mean_value in enumerate(mean_values):
  524. this_df = pd.DataFrame(
  525. {'ID': subj_id, 'Channel': channel_idx, 'Chroma': chroma,
  526. 'Condition': evoked, 'MeanValue': mean_value}, index=[0])
  527. df_babies = pd.concat([df_babies, this_df], ignore_index=True)
  528. # Print a message when done processing data for the current subject
  529. print(f"Finished processing data for Subject ID: {subj_id}")
  530. # Print a message when done processing data for the last subject of the current condition
  531. print(f"Finished processing data for all subjects in Condition: {evoked}")
  532. # Cut down the dataframe just to the conditions we are interested in
  533. df_babies.query("Condition in ['Interacting', 'NonInteracting']")
  534. # Compute contrast of interest to use with the 'permutation_cluster_1samp_test' function
  535. difference_df = df_babies.pivot_table(
  536. index=['ID', 'Channel', 'Chroma'],
  537. columns='Condition',
  538. values='MeanValue'
  539. )
  540. difference_df['Difference'] = difference_df['Interacting'] - difference_df['NonInteracting']
  541. difference_df = difference_df.reset_index()
  542. print(difference_df)
  543. # Pivot the DataFrame
  544. df_pivoted = difference_df.pivot(index='ID', columns='Channel', values='Difference')
  545. ## Calculate cluster-forming thershold
  546. n_observations = 20
  547. pval = 0.1 # arbitrary
  548. df = n_observations - 1 # degrees of freedom for the test
  549. thresh = scipy.stats.t.ppf(1 - pval / 2, df) # two-tailed, t distribution
  550. # Get the sensor locations for HbO channels
  551. ch_locs = np.array([epochs['Interacting'].info['chs'][pick]['loc'][:3] for pick in hbo_picks_facing])
  552. # Compute pairwise distances between sensors
  553. dists = squareform(pdist(ch_locs))
  554. # Define a distance threshold to determine adjacency (e.g., 0.03 meters)
  555. distance_threshold = 0.025
  556. adjacency = dists < distance_threshold
  557. adjacency_2, chs = mne.channels.find_ch_adjacency(evoked_hbo_facing[0].info, ch_type=None)
  558. mne.viz.plot_ch_adjacency(evoked_hbo_facing[0].info, adjacency, chs, kind='2d', edit=False)
  559. # Ensure the adjacency matrix is symmetric and binary
  560. adjacency = adjacency.astype(int)
  561. # Convert adjacency matrix to SciPy sparse matrix
  562. adjacency_sparse = csr_matrix(adjacency)
  563. # Check if the adjacency matrix has the correct size
  564. print(f'Adjacency matrix shape: {adjacency_sparse.shape}')
  565. print(f'Number of channels: {len(hbo_picks_facing)}')
  566. ## Run permutation test
  567. T_obs, clusters, cluster_p_values, H0 = permutation_cluster_1samp_test(
  568. df_pivoted, n_permutations=5000, out_type='indices', tail = 0,
  569. n_jobs=1, threshold=thresh, adjacency=adjacency_sparse)
  570. # Find significant clusters
  571. significant_clusters = np.where(cluster_p_values < 0.05)[0]
  572. # Filter out single-channel clusters
  573. filtered_clusters = []
  574. filtered_p_values = []
  575. for cluster_idx in significant_clusters:
  576. cluster = clusters[cluster_idx]
  577. # Assuming cluster[0] contains channel indices
  578. unique_channels = np.unique(cluster[0])
  579. if len(unique_channels) > 0: # Exclude single-channel clusters
  580. filtered_clusters.append(cluster)
  581. filtered_p_values.append(cluster_p_values[cluster_idx])
  582. # Convert to numpy arrays for easier handling
  583. filtered_clusters = np.array(filtered_clusters, dtype=object)
  584. filtered_p_values = np.array(filtered_p_values)
  585. print(f"\nNumber of significant clusters (excluding single-channel clusters): {len(filtered_clusters)}")
  586. for i, cluster in enumerate(filtered_clusters):
  587. cluster_idx = significant_clusters[i]
  588. print(f"Cluster {cluster_idx}:")
  589. print(f" p-value: {filtered_p_values[i]}")
  590. print(f" Channels involved: {np.unique(cluster[0])}")
  591. # Compute the mean T-values across time for visualization
  592. T_obs_avg = T_obs # Average T-values over the time dimension
  593. for i, cluster in enumerate(filtered_clusters):
  594. significant_chs = np.unique(cluster[0]) # Get unique channel indices from the cluster
  595. # Ensure significant_chs contains valid integer indices
  596. significant_chs = significant_chs.astype(int)
  597. significant_chs = significant_chs[significant_chs < len(T_obs_avg)] # Ensure indices are within bounds
  598. # Create a mask to highlight significant channels
  599. mask = np.zeros_like(T_obs_avg, dtype=bool)
  600. mask[significant_chs] = True # Mask significant channels
  601. # Plot topomap with the mask
  602. info = evoked_hbo_facing[0].info
  603. fig, ax = plt.subplots()
  604. vmin, vmax = -4, 4
  605. im, _ = mne.viz.plot_topomap(T_obs_avg, info, axes=ax, show=False, cmap='RdBu_r',
  606. mask=mask, extrapolate='local', sphere='auto',
  607. image_interp='linear', vlim=(vmin, vmax),
  608. mask_params=dict(marker='o', markerfacecolor='black',
  609. markersize=10))
  610. # Add colorbar
  611. cbar = fig.colorbar(im, ax=ax)
  612. cbar.set_label("T-value") # Label for clarity
  613. ax.set_title(f"Cluster {significant_clusters[i]} (p={filtered_p_values[i]:.3f})")
  614. plt.show()
  615. # Plot all clusters on the same topomap (note that this has the effect of creating a 'fake' red blob)
  616. combined_mask = np.zeros_like(T_obs_avg, dtype=bool)
  617. for cluster in filtered_clusters:
  618. significant_chs = np.unique(cluster[0]) # Get unique channel indices
  619. significant_chs = significant_chs.astype(int)
  620. significant_chs = significant_chs[significant_chs < len(T_obs_avg)]
  621. combined_mask[significant_chs] = True # Mark channels as significant
  622. info = evoked_hbo_facing[0].info
  623. fig, ax = plt.subplots()
  624. # Plot topomap with combined mask
  625. im, _ = mne.viz.plot_topomap(T_obs_avg, info, axes=ax, show=False, cmap='RdBu_r',
  626. mask=combined_mask, extrapolate='local', sphere='auto',
  627. vlim=(vmin, vmax),
  628. mask_params=dict(marker='o', markerfacecolor='black', markersize=10))
  629. # Add colorbar
  630. cbar = fig.colorbar(im, ax=ax)
  631. cbar.set_label("T-value")
  632. plt.show()
  633. #%% Group-level analysis - Conjuction [Interacting > NonInteracting] vs. [Single > baseline]
  634. # == Interacting > NonInteracting
  635. model_results_interacting = model_results.copy()
  636. significant_results_interacting = model_results_interacting.copy()
  637. # # Set values to 0 where p-value is >= 0.05
  638. significant_results_interacting.loc[significant_results_interacting['P-val'] > 0.051, 'T-stat'] = 0 # Ensure correct column name
  639. significant_results_interacting.loc[significant_results_interacting['T-stat'] < 0, 'T-stat'] = 0
  640. # == Single > baseline
  641. df_single = df_babies.query("Condition in ['Single']")
  642. df_single['ch_name'] = df_single['Channel'].replace(channel_mapping) #channel mapping defined earlier
  643. # Run group level model and convert to dataframe
  644. model_single = Lmer('MeanValue ~ -1 + ch_name + (1|ID)', data=df_single)
  645. model_results_single = model_single.fit()
  646. # Rename and sort channel names (defind in previous analyses)
  647. model_results_single['ch_name'] = model_results_single.index.str.replace('ch_name', '', regex=False)
  648. # Reorder the DataFrame based on the 'ch_name' column according to the desired order
  649. model_results_single['ch_name'] = pd.Categorical(model_results_single['ch_name'], categories=desired_order, ordered=True)
  650. # Sort the DataFrame based on the new 'ch_name' order
  651. model_results_single = model_results_single.sort_values('ch_name')
  652. model_results_single['ch_name'] = model_results_single.index.str.replace('ch_name', '', regex=False).str.replace(':ConditionSingle', '', regex=False)
  653. model_results_single['ch_name'] = pd.Categorical(model_results_single['ch_name'], categories=desired_order, ordered=True)
  654. # Sort the DataFrame based on the new 'ch_name' order
  655. model_results_single = model_results_single.sort_values('ch_name')
  656. # To view the model summary
  657. print(model_results_single)
  658. significant_results_single = model_results_single.copy()
  659. # # Set values to 0 where p-value is >= 0.05
  660. significant_results_single.loc[significant_results_single['P-val'] > 0.051, 'T-stat'] = 0 # Ensure correct column name
  661. significant_results_single.loc[significant_results_single['T-stat'] < 0, 'T-stat'] = 0
  662. # == Combine results
  663. combined_result = significant_results_interacting.copy()
  664. significant_results_single.index = significant_results_single.index.str[:16]
  665. significant_results_interacting.index = significant_results_interacting.index.str[:16]
  666. # Now check if they match
  667. print(significant_results_interacting.index.equals(significant_results_single.index)) # Should be True
  668. # A trick to be able to plot the two contrasts on the same brain. Only t-value is relevant in this dataframe
  669. combined_result['T-stat'] = significant_results_interacting['T-stat'] - significant_results_single['T-stat']
  670. ## Plot results with a custom color bar to be able to show significant channels from both contrasts
  671. n_colors_half = 128 # number of colors per half
  672. viridis = plt.get_cmap('PiYG')
  673. rdbu_r = plt.get_cmap('RdBu_r')
  674. # Extract upper half of viridis and reverse it
  675. viridis_upper = viridis(np.linspace(0.5, 1.0, n_colors_half))[::-1]
  676. # Extract upper half of RdBu_r as is
  677. rdbu_r_upper = rdbu_r(np.linspace(0.5, 1.0, n_colors_half))
  678. # Concatenate both halves
  679. combined_colors = np.vstack((viridis_upper, rdbu_r_upper))
  680. # Define vmin/vmax
  681. vmin, vmax = -4, 4
  682. values = np.linspace(vmin, vmax, combined_colors.shape[0])
  683. # Define fading range
  684. fade_halfwidth = 0.6 # adjust for smoother fade
  685. fade_start_neg, fade_end_neg = -1.96 - fade_halfwidth, -1.96 + fade_halfwidth
  686. fade_start_pos, fade_end_pos = 1.96 - fade_halfwidth, 1.96 + fade_halfwidth
  687. # White color
  688. white = np.array([1, 1, 1, 1])
  689. # Apply fading into white
  690. for i, val in enumerate(values):
  691. if -1.96 <= val <= 1.96:
  692. combined_colors[i] = white
  693. elif fade_start_neg < val < fade_end_neg:
  694. t = (val - fade_start_neg) / (fade_end_neg - fade_start_neg)
  695. combined_colors[i] = (1-t) * combined_colors[i] + t * white
  696. elif fade_start_pos < val < fade_end_pos:
  697. t = (val - fade_start_pos) / (fade_end_pos - fade_start_pos)
  698. combined_colors[i] = (1-t) * white + t * combined_colors[i]
  699. # Create custom colormap
  700. custom_cmap = LinearSegmentedColormap.from_list(
  701. "custom_with_white_fading_center", combined_colors
  702. )
  703. ## Plot results!
  704. brain = mne_nirs.visualisation.plot_glm_surface_projection(
  705. raw_haemo_b.copy().pick("hbo"),
  706. combined_result,
  707. clim=clim,
  708. view='lateral',
  709. colormap=custom_cmap,
  710. colorbar=True,
  711. size=(800, 700),
  712. value='T-stat',
  713. distance=0.015
  714. )
  715. ## == Conjunction analysis
  716. # Ensure both have consistent ch_name as index
  717. interacting_df = model_results_interacting.set_index('ch_name')
  718. single_df = model_results_single.set_index('ch_name')
  719. # Select relevant columns (t-statistics)
  720. interacting_t = interacting_df[['T-stat']].rename(columns={'T-stat': 't_interacting'})
  721. single_t = single_df[['T-stat']].rename(columns={'T-stat': 't_single'})
  722. # Merge on ch_name
  723. conj_df = interacting_t.join(single_t, how='inner')
  724. # Compute minimum t-statistics per channel (conjunction)
  725. conj_df['t_min'] = conj_df[['t_interacting', 't_single']].min(axis=1)
  726. # Convert min t-stat to one-sided p-value using standard normal survival function
  727. conj_df['P_conjunction'] = norm.sf(conj_df['t_min']) # sf = 1 - cdf
  728. # Mark significant conjunctions
  729. alpha = 0.05
  730. conj_df['Significant'] = conj_df['P_conjunction'] < alpha
  731. # List channels passing conjunction test
  732. significant_conjunction_channels = conj_df[conj_df['Significant']].index.tolist()
  733. print("Conjunction-significant channels:", significant_conjunction_channels)
  734. # Optional: display summary table
  735. print(conj_df.sort_values('P_conjunction'))

fNIRS_analyses_shared.py, no license · at the source

Overview

Authors: Manuel Mello1, Emilie Serraille1, Jean-Rémy Hochmann1, Liuba Papeo1
ORCID iDs: Manuel Mello
  1. Institut des Sciences Cognitives—Marc Jeannerod, UMR5229, Centre National de La Recherche Scientifique (CNRS) and Université Claude Bernard Lyon 1, 67 Bd. Pinel, 69675 Bron, France
Journal: iScience, volume 29, issue 6, article 116287
Dates: received 16 October 2025; accepted 21 May 2026; published online 8 June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.isci.2026.116287 · PMID 42291211 · PMCID PMC13264107 · OpenAlex W4414426746
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), developmental (subfield)
Methods: Spectral & time-frequency, Statistics, Preprocessing, fMRI & imaging
Keywords: health sciences, medicine, neurology, pediatrics, human physiology, natural sciences, biological sciences, neuroscience, developmental neuroscience, cognitive neuroscience
Topic: Child and Animal Learning Development (Developmental and Educational Psychology, Psychology), according to OpenAlex
Funding: ANR-DFG Franco-German (FRAL_RELATIONS_268980); Fondation de France (00134704/WB-2022-46104); fellowship of the LabEx CORTEX of the University of Lyon (ANR-11-LABX-0042)
Citations: not cited yet (Europe PMC); 94 references in the paper
Research resources: R – version 4.3.3 RRID:SCR_001905, MNE Python – version 1.5.0 RRID:SCR_005972, Python – version 3.8.10 RRID:SCR_008394

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

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Languages: Python (1)
Size: 12 files, 1 script
Software Heritage: not checked
Found in: “Data and code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (1 file), MNE-Python (1 file), NumPy (1 file), pandas (1 file), PyWavelets (1 file), SciPy (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
1 file

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://doi.org/10.17605/OSF.IO/874RW. • The original programming scripts used to pre-process and analyze the data of this study are publicly available on the Open Science Framework. https://doi.org/10.17605/OSF.IO/874RW. • Any additional information required to re-analyze the data reported in this paper are available from the lead contact upon request.

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://doi.org/10.1016/j.isci.2026.116287

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/j.isci.2026.116287},
url = {https://doi.org/10.1016/j.isci.2026.116287},
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/06/08
VL - 29
IS - 6
SP - 116287
SN - 2589-0042
PB - Elsevier
DO - 10.1016/j.isci.2026.116287
UR - https://doi.org/10.1016/j.isci.2026.116287
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.isci.2026.116287",
"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": "iScience",
"volume": "29",
"issue": "6",
"page": "116287",
"DOI": "10.1016/j.isci.2026.116287",
"PMID": "42291211",
"PMCID": "PMC13264107",
"ISSN": "2589-0042",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.isci.2026.116287",
"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 neuroscience
In 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 communications
In 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: iScience
In 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: Neurophotonics
In 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 biology
In 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 communications
In 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: Neurophotonics
In 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 one
In 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.

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.