OSCR

Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior.

Code ↔ Paper

11 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 11 matches
  1. [1] § Methods › Social maps ↔ umap/umap_.py, lines 1485–1737 · score 0.79 · low dimensional space, dimensional embedding, dimensionality reduction, UMAP embedded, uniformly, clustered
  2. [2] § Methods › Social maps ↔ social_behavior_umap.ipynb, lines 357–403 · score 0.78 · watershed segmentation, kernel density, UMAP embedded, stereotypic interaction, dimensional space, clustered
  3. [3] § Methods › Pose tracking and processing ↔ social_behavior_umap.ipynb, lines 177–241 · score 0.71 · relative orientation, relative angle, velocities, Lateral, speed, frames
  4. [4] § Results › Male- and female-directed courtship-like interactions exhibit different dynamics ↔ social_behavior_umap.ipynb, lines 357–403 · score 0.70 · kernel density, low density, stereotypical interaction, social modes, watershed, UMAP
  5. [5] § Methods › Maps of behavioral responses to song ↔ social_behavior_umap.ipynb, lines 177–241 · score 0.66 · wavelet transform, UMAP embedding, watershed, spectrogram, pose, signal
  6. [6] § Methods › Maps of behavioral responses to song ↔ umap/umap_.py, lines 1485–1737 · score 0.65 · dimensional space, UMAP embedding, manifold, uniformly, density, behavioral
  7. [7] § Methods › HMM-GLM modeling › Generalized linear model (GLM) ↔ demo/basics.ipynb, lines 100–127 · score 0.58 · raised cosine, small delays, broaden, GLM, transformation, filters
  8. [8] § Methods › Pose tracking and processing › Quantifying courter position during interactions ↔ social_behavior_umap.ipynb, lines 632–688 · score 0.57 · fly length, polar, histogram, angle, binned, courter
  9. [9] § Methods › Pose tracking and processing ↔ umap/spectral.py, lines 163–279 · score 0.55 · metrics function, relative positioning, Euclidean, vector, component, connecting
  10. [10] § Methods › HMM-GLM modeling ↔ ssm/transitions.py, lines 213–263 · score 0.53 · hidden Markov model, generalized linear model, transitioning, probability
  11. [11] § Results › Male- and female-directed courtship-like interactions exhibit different dynamics ↔ social_behavior_umap.ipynb, lines 841–898 · score 0.52 · male female, female directed, spent, edges, social, Ratio

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

Jupyter notebook · 985 lines · 41 KB · no license · 6 matches

  1. # %% [markdown]
  2. # # Social UMAP
  3. # This notebook contains the code to generate the social UMAPs from Nair et al. 2025, "Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior" (bioRxiv, May 2025).
  4. # %%
  5. # imports and configurations
  6. import os
  7. import pandas as pd
  8. import numpy as np
  9. from scipy import signal
  10. from sklearn.preprocessing import MinMaxScaler, StandardScaler
  11. from embedding_utils import (get_freq_scales, compute_wavelet,
  12. umap_embedding,
  13. time_delay_embedding,
  14. SpatialClustering,
  15. umap_multiple_fits,
  16. basis_transformation,
  17. kernel_density_estimation)
  18. from glm_utils import bases
  19. import matplotlib.pyplot as plt
  20. import seaborn as sns
  21. import matplotlib.colors as mcolors
  22. from scipy.stats import mannwhitneyu, binned_statistic_2d
  23. plt.style.use('ncb.mplstyle')
  24. colors = [mcolors.CSS4_COLORS["violet"], mcolors.CSS4_COLORS["royalblue"],
  25. mcolors.CSS4_COLORS["thistle"], mcolors.CSS4_COLORS["lightsteelblue"]]
  26. color_palette = sns.set_palette(sns.color_palette(colors))
  27. # %% [markdown]
  28. # ## How to arrange the data and specify parameters
  29. #
  30. # The data should be stored in the `dat` folder as one CSV file per trial. The columns of each CSV file are used as input features for the UMAP.
  31. #
  32. # The `trial_names` indicate the trials belonging to different groups, namely male-female and male-male interactions. The `features` list indicates the input features used to fit the UMAP, and `condition` indicates the condition on which the UMAPs are compared.
  33. #
  34. # - `start_frame_idx`: index of the first frame of each trial to be used in the analysis
  35. #
  36. # - `end_frame_idx`: index of the last frame of each trial to be used in the analysis
  37. #
  38. # - `confidence_threshold`: confidence level of tracked poses below which frames are ignored in the analysis
  39. #
  40. # ### Lowpass filtering
  41. # - `required_sampling_rate`: sampling rate of the data
  42. #
  43. # - `filter_features`: flag indicating whether to lowpass filter UMAP input features
  44. #
  45. # - `filter_cutoff`: cutoff frequency if lowpass filtering is used
  46. #
  47. # - `filter_ord`: filter order
  48. #
  49. # ### Normalization of input features
  50. # - `normalize_features`: whether to normalize the input features
  51. #
  52. # - `normalization`: what type of normalization is used, "standardize", "minmax", or `None`
  53. #
  54. # - `features_range`: for minmax normalization, the feature range to be used as minimum and maximum values. If used, this should be a dictionary whose keys are any or some of the feature names (`features`) and values are a list of two elements: the minimum and maximum value for the feature
  55. #
  56. # ### Time delay embedding parameters
  57. # - `time_delay_poses`: whether to use time-delay embedding of input features
  58. #
  59. # - `nb_delays`: if `time_delay_poses` is `True`, specify the number of delays to embed
  60. #
  61. # - `basis_transform`: whether to transform the basis vectors of time-delay embedded features
  62. #
  63. # - `basis_function`: if `basis_transform` is `True`, the function to be used for basis transformation
  64. #
  65. # ### Wavelet transform parameters
  66. # - `wavelet_transform`: whether to wavelet transform the input features. Either time-delay embedding or wavelet transform is used
  67. #
  68. # - `min_freq`: minimum frequency used for wavelet transform
  69. #
  70. # - `max_freq`: maximum frequency used for wavelet transform
  71. #
  72. # - `nfreqs`: number of frequency components to be used for wavelet transform
  73. #
  74. # - `freq_spacing`: frequency spacing to be used: 'dyadic', 'log', or 'uniform'
  75. #
  76. # ### UMAP parameters
  77. #
  78. # - `train_size`: ratio of data to be used for fitting the UMAP
  79. #
  80. # - `min_dist`: UMAP hyperparameter `min_dist` (https://umap-learn.readthedocs.io/en/latest/parameters.html)
  81. #
  82. # - `n_neighbors`: UMAP hyperparameter `n_neighbors` (https://umap-learn.readthedocs.io/en/latest/parameters.html)
  83. #
  84. # - `random_state`: random seed used to fit UMAP reproducibly
  85. #
  86. # - `nan_handling`: whether to 'remove' or 'interpolate' NaN values before analysis
  87. #
  88. # ### kernel density estimation (KDE) and watershed segmentation parameters
  89. # - `bw`: bandwidth to be used for KDE (https://kdepy.readthedocs.io/en/latest/introduction.html#Selecting-a-suitable-bandwidth)
  90. #
  91. # - `nb_gridpoints`: number of grid points on each 2D axis to be used for KDE
  92. #
  93. # - `watershed_threshold`: threshold to be used for watershed segmentation algorithm (see `watershed_segmentation` in `embedding_utils.py`)
  94. #
  95. # - `results_save_path`: path to save social UMAP results
  96. #
  97. # The notebook reads data from the CSV files and stores them in `preprocessed_data_path`.
  98. # %%
  99. data_save_path = "dat"
  100. preprocessed_data_path = os.path.join(data_save_path, "preprocessed_data.npz")
  101. # parameters to get data
  102. trial_names = {}
  103. trial_names['male-female'] = \
  104. ['localhost-20200706_122314',
  105. 'localhost-20200708_122011',
  106. 'localhost-20200707_113229',
  107. 'localhost-20200710_120433',
  108. 'localhost-20200710_134643',
  109. 'localhost-20200709_123809',
  110. 'localhost-20200709_115547',
  111. 'localhost-20200710_131755',
  112. 'localhost-20220214_114900',
  113. 'localhost-20220214_122614',
  114. 'localhost-20220214_130838',
  115. 'localhost-20220214_132737',
  116. 'localhost-20220216_093349',
  117. 'localhost-20220216_101942',
  118. 'localhost-20220218_114657',
  119. 'localhost-20220224_120331',
  120. 'localhost-20220225_121823',
  121. 'localhost-20220302_113245',
  122. 'localhost-20220302_125321',
  123. 'localhost-20220502_115227',
  124. 'localhost-20210127_145608',
  125. 'localhost-20210210_094512',
  126. 'localhost-20210210_100441',
  127. 'localhost-20210617_121041',
  128. 'localhost-20210617_123045',
  129. 'localhost-20220502_123531',
  130. 'localhost-20220506_130520',
  131. 'localhost-20220509_115048',
  132. 'localhost-20220509_124215',
  133. 'localhost-20220511_115515',
  134. ]
  135. trial_names['male-male'] = \
  136. ['localhost-20200706_132556',
  137. 'localhost-20200708_120051',
  138. 'localhost-20200812_143257',
  139. 'localhost-20200709_111319',
  140. 'localhost-20200812_155855',
  141. 'localhost-20210222_144954',
  142. 'localhost-20210222_152829',
  143. 'localhost-20210308_120623',
  144. 'localhost-20210308_122507',
  145. 'localhost-20220214_120801',
  146. 'localhost-20220214_135209',
  147. 'localhost-20220222_122109',
  148. 'localhost-20220223_121135',
  149. 'localhost-20220224_124055',
  150. 'localhost-20220224_130031',
  151. 'localhost-20220225_123838',
  152. 'localhost-20220225_125855',
  153. 'localhost-20220302_115211',
  154. 'localhost-20220302_131417',
  155. 'localhost-20220302_133433',
  156. 'localhost-20220502_125611',
  157. 'localhost-20210201_124220',
  158. 'localhost-20210201_132121',
  159. 'localhost-20210209_114709',
  160. 'localhost-20210209_122417',
  161. 'localhost-20220304_121729',
  162. 'localhost-20220304_123618',
  163. 'localhost-20220502_133927',
  164. 'localhost-20220509_121352',
  165. 'localhost-20220509_130538'
  166. ]
  167. features = [
  168. 'courter_velocity_forward',
  169. 'target_velocity_forward',
  170. 'courter_abs_velocity_lateral',
  171. 'target_abs_velocity_lateral',
  172. 'courter_angles_speed',
  173. 'target_angles_speed',
  174. 'distance_target',
  175. 'relative_angle_abs_target',
  176. 'relative_angle_abs_courter',
  177. 'relative_orientation_abs_target_wrap',
  178. ]
  179. condition = 'current_song'
  180. start_frame_idx = 2000
  181. end_frame_idx = -2000
  182. # confidence values not calibrated in SLEAP
  183. confidence_threshold = 0.7 #None if sleap
  184. # filtering
  185. required_sampling_rate = 30#Hz
  186. filter_features = False
  187. filter_cutoff = 10#Hz
  188. filter_ord = 4
  189. assert filter_cutoff < required_sampling_rate/2, (
  190. "filter_cutoff should be less than %0.2f Hz"
  191. %(required_sampling_rate/2))
  192. b, a = signal.butter(filter_ord, filter_cutoff/(0.5*required_sampling_rate))
  193. # use normalize_features = True only for wavelet spectrogram approach
  194. normalize_features = True
  195. normalization = "standardize" #"minmax" or None
  196. features_range = None
  197. # time delay works better than wavelet spectrogram for now
  198. time_delay_poses = True
  199. nb_delays = 15
  200. basis_transform = False
  201. basis_function = bases.raised_cosine(0, 12, [0, 12], 10, nb_delays)
  202. wavelet_transform = not time_delay_poses
  203. min_freq = 1
  204. max_freq = 25
  205. nfreqs = 25
  206. freq_spacing = 'dyadic'
  207. freqs, scales_cwt = get_freq_scales(
  208. min_freq, max_freq, nfreqs, required_sampling_rate,
  209. spacing = freq_spacing)
  210. # umap parameters
  211. train_size = 0.1
  212. min_dist = 0.0
  213. n_neighbors = 100
  214. random_state = 42
  215. nan_handling = "remove" # "interpolate" to interpolate nans or "remove" to remove nans
  216. if wavelet_transform:
  217. assert nan_handling == "interpolate", "use nan_handling as interpolate if wavelet_transform is True"
  218. # kde and watershed parameters
  219. bw = 0.5
  220. nb_gridpoints = 256
  221. watershed_threshold = 0.001
  222. results_save_path = f"res/umap_embedding"
  223. if not os.path.exists(results_save_path):
  224. os.makedirs(results_save_path)
  225. # %%
  226. # save umap parameters
  227. umap_params = {}
  228. umap_params = dict(
  229. trial_names=trial_names,
  230. features=features,
  231. condition=condition,
  232. required_sampling_rate=required_sampling_rate,
  233. data_save_path=data_save_path,
  234. start_frame_idx=start_frame_idx,
  235. end_frame_idx=end_frame_idx,
  236. confidence_threshold=confidence_threshold,
  237. filter_features=filter_features,
  238. filter_cutoff=filter_cutoff,
  239. filter_ord=filter_ord,
  240. normalize_features=normalize_features,
  241. normalization=normalization,
  242. features_range=features_range,
  243. #additional_features=additional_features,
  244. time_delay_poses=time_delay_poses,
  245. nb_delays=nb_delays,
  246. basis_transform = basis_transform,
  247. basis_function = basis_function,
  248. wavelet_transform=wavelet_transform,
  249. min_freq=min_freq,
  250. max_freq=max_freq,
  251. nfreqs=nfreqs,
  252. freq_spacing=freq_spacing,
  253. train_size=train_size,
  254. min_dist=min_dist,
  255. n_neighbors=n_neighbors,
  256. random_state=random_state,
  257. nan_handling=nan_handling,
  258. bw=bw,
  259. nb_gridpoints=nb_gridpoints,
  260. watershed_threshold=watershed_threshold,
  261. results_save_path=results_save_path)
  262. # %% [markdown]
  263. # ## Prepare the data
  264. #
  265. # Read the data from `preprocessed_data_path` if available. Otherwise, read the trial-wise CSV files in the `dat` folder. Lowpass-filter the data, remove frames below the confidence threshold, and normalize.
  266. # %%
  267. # get data
  268. all_trial_names = list(np.concatenate(list(trial_names.values())))
  269. X, y = [], []
  270. if os.path.exists(preprocessed_data_path):
  271. data = np.load(preprocessed_data_path, allow_pickle=True)
  272. X = list(data['X'])
  273. y = list(data['y'])
  274. else:
  275. for trial_name in all_trial_names:
  276. print(trial_name)
  277. df_trial_data = pd.read_csv(f'{data_save_path}/{trial_name}.csv')
  278. trial_X = df_trial_data[features].values[start_frame_idx:end_frame_idx]
  279. trial_y = df_trial_data[condition].values[start_frame_idx:end_frame_idx]
  280. # low pass filter data
  281. trial_X = signal.filtfilt(b, a, trial_X, axis=0)
  282. # filter out non confident frames
  283. if confidence_threshold is not None and 'poses_confidence' in df_trial_data:
  284. poses_confidence = df_trial_data['poses_confidence'].values
  285. confident_frames = poses_confidence>confidence_threshold
  286. trial_X[~confident_frames] = np.nan
  287. trial_y[~confident_frames] = np.nan
  288. # normalization
  289. if normalize_features:
  290. if normalization == 'standardize':
  291. trial_X = StandardScaler().fit_transform(trial_X)
  292. elif normalization == 'minmax':
  293. trial_X = MinMaxScaler().fit_transform(trial_X)
  294. X.append(trial_X)
  295. y.append(trial_y)
  296. np.savez(preprocessed_data_path, X=X, y=y)
  297. # %% [markdown]
  298. # ## Data processing
  299. #
  300. # Extract wavelet spectrograms or perform time-delay embedding of input features for each trial. If using wavelet spectrograms, the processed data for each trial is a 2D array of shape (`N_trial`, `len(features)` x `nfreqs`), where `N_trial` is the number of samples for the trial. If using time-delay embedding, the processed data for each trial is a 2D array of shape (`N_trial`, `len(features)` x `nb_delays`). The processed data from all trials are then pooled to form `X_proc`.
  301. #
  302. # %%
  303. # process data
  304. X_proc, y_proc = [], []
  305. list_timestamps = []
  306. for trial_X, trial_y in zip(X, y):
  307. N = trial_X.shape[0]
  308. t0=0
  309. dt=1/required_sampling_rate
  310. timestamps = np.arange(0, N, dtype="float32") * dt + t0
  311. if wavelet_transform:
  312. # Compute wavelet spectrogram
  313. print('Computing wavelet spectrograms ...')
  314. trial_X_cwt = []
  315. for feat_idx in range(trial_X.shape[1]):
  316. [P, t, f] = compute_wavelet(timestamps, trial_X[:, feat_idx],
  317. scales_cwt)
  318. trial_X_cwt.extend((P))
  319. trial_X_cwt = np.array(trial_X_cwt, dtype='float32').T
  320. X_proc.append(trial_X_cwt)
  321. y_proc.append(trial_y)
  322. print('done')
  323. elif time_delay_poses:
  324. # time delay embedding
  325. X_td, y_td = time_delay_embedding(
  326. trial_X, trial_y, nb_delays = nb_delays, multi_features=True,
  327. padding='same', remove_nans=True if nan_handling == "remove" else False)
  328. if basis_transform:
  329. X_td, basis_projection = basis_transformation(
  330. X_td, nb_delays, basis_function, multi_features=True, nb_stim=len(features))
  331. X_proc.append(np.array(X_td, dtype="float32"))
  332. y_proc.append(y_td)
  333. list_timestamps.append(timestamps)
  334. # %% [markdown]
  335. # ## UMAP embedding and spatial clustering using KDE and watershed segmentation
  336. #
  337. # First, the time-delayed inputs `X_proc` are embedded into a two-dimensional manifold `X_embedded`. A kernel density estimation is performed on the two-dimensional space, followed by watershed segmentation, which creates spatial clusters centered at local peaks of the kernel density estimates. From a behavioral perspective, the local peaks in the KDE correspond to stereotyped interactions between the organisms, and regions of low density correspond to transitions between stereotyped interactions. Thus, each spatial cluster is assigned to a particular interaction prototype called a social mode.
  338. # %%
  339. if os.path.exists(f'{results_save_path}/embedding_results.npz'):
  340. # load data
  341. results = np.load(f'{results_save_path}/embedding_results.npz', allow_pickle=True)
  342. X_proc = results['X_proc']
  343. y_proc = results['y_proc']
  344. X_embedded = results['X_embedded']
  345. X_embedded_kde = results['X_embedded_kde']
  346. X_embedded_kde_positions = results['X_embedded_kde_positions']
  347. X_embedded_segments = results['X_embedded_segments']
  348. labels = results['labels']
  349. positions = results['positions']
  350. labels_edge_positions = results['labels_edge_positions']
  351. else:
  352. #%% umap embedding
  353. X_embedded, reducer = umap_embedding(
  354. X_proc, min_dist, n_neighbors,
  355. n_components=2,
  356. train_size=train_size,
  357. random_state=random_state,
  358. )
  359. # kernel density estimation and watershed segmentation
  360. spatial_clustering = SpatialClustering(bw, nb_gridpoints, watershed_threshold)
  361. embedded_features_kde, positions, labels, labels_edge, labels_edge_positions = spatial_clustering.fit(X_embedded)
  362. X_embedded_kde, X_embedded_kde_positions, X_embedded_segments = spatial_clustering.transform(
  363. X_embedded, positions, labels)
  364. # save results
  365. results = {}
  366. results['X_proc'] = X_proc
  367. results['y_proc'] = y_proc
  368. results['timestamps'] = list_timestamps
  369. results['reducer'] = reducer
  370. results['X_embedded'] = X_embedded
  371. results['X_embedded_kde'] = X_embedded_kde
  372. results['X_embedded_kde_positions'] = X_embedded_kde_positions
  373. results['X_embedded_segments'] = X_embedded_segments
  374. results['labels'] = labels
  375. results['positions'] = positions
  376. results['labels_edge_positions'] = labels_edge_positions
  377. results['umap_params'] = umap_params
  378. np.savez(f'{results_save_path}/embedding_results.npz', results)
  379. # %% [markdown]
  380. # ## Meaning of each spatial segment (social modes)
  381. #
  382. # To understand what each spatial segment means, we first plot the time-delay embedded features for each social mode separately for male-female and male-male interactions.
  383. # %%
  384. # time-delay embedded feature values for each segment
  385. X_proc_mean = {}
  386. n_samples_label = {}
  387. for analysis_group in trial_names:
  388. X_proc_mean[analysis_group] = {}
  389. n_samples_label[analysis_group] = {}
  390. for label in np.unique(labels):
  391. if label == 0:
  392. continue
  393. X_proc_mean[analysis_group][label] = []
  394. n_samples_label[analysis_group][label] = 0
  395. for x_proc, x_seg, trial_id in zip(X_proc, X_embedded_segments, all_trial_names):
  396. if trial_id not in trial_names[analysis_group]: continue
  397. X_proc_mean[analysis_group][label].append(
  398. np.sum(x_proc[x_seg==label], axis=0)/\
  399. np.sum(x_seg==label))
  400. n_samples_label[analysis_group][label] += np.sum(x_seg==label)
  401. X_proc_mean[analysis_group][label] = np.array(
  402. X_proc_mean[analysis_group][label])
  403. if time_delay_poses:
  404. X_proc_mean[analysis_group][label] = np.reshape(
  405. X_proc_mean[analysis_group][label],
  406. (X_proc_mean[analysis_group][label].shape[0], -1, nb_delays))
  407. elif wavelet_transform:
  408. X_proc_mean[analysis_group][label] = np.reshape(
  409. X_proc_mean[analysis_group][label],
  410. (X_proc_mean[analysis_group][label].shape[0], -1, nfreqs))
  411. num_clusters = 12
  412. fig, ax = plt.subplots(
  413. len(X_proc_mean),
  414. num_clusters,
  415. sharex=True, sharey=True,
  416. num="cluster_means", figsize=(num_clusters*2, 2*len(X_proc_mean)))
  417. for g, group in enumerate(X_proc_mean):
  418. for i, (label, features_mean) in enumerate(X_proc_mean[group].items()):
  419. ax[g, i].set_title(f"{label} ({n_samples_label[group][label]})")
  420. ax[g, i].matshow(np.nanmean(features_mean, 0), vmin=-1, vmax=1, cmap="bwr")
  421. ax[g, i].set_yticks(np.arange(features_mean.shape[1]))
  422. ax[g, i].set_yticklabels(features)
  423. ax[g, 0].set_ylabel(group)
  424. plt.tight_layout()
  425. plt.show()
  426. # %% [markdown]
  427. # Next, we plot the average values of each input feature within each segment. Based on the speed components of each fly and their relative positioning with respect to each other, we name the modes:
  428. # 1. `Behind and idle`: the courter is behind the target and idle
  429. # 2. `Behind and close`: the courter is behind the target and close
  430. # 3. `Behind and chasing`: the courter is behind the target and chasing
  431. # 4. `Behind and circling`: the courter is behind the target and circling
  432. # 5. `Uninterested`: the courter and target are distant and facing away from each other
  433. # 6. `Front and circling`: the courter is in front of the target and circling
  434. # 7. `Front and close`: the courter is in front of the target and close
  435. # 8. `Front and idle`: the courter is in front of the target and idle
  436. #
  437. # Some segments contain only a few frames or are noisy, which we ignore.
  438. # %%
  439. # feature means for each segment
  440. filter_by_song = False
  441. ignore_states = [0, 1, 5, 9, 12]
  442. state_names = [
  443. 'Behind idle',
  444. 'Behind close',
  445. 'Behind chasing',
  446. 'Behind circling',
  447. 'Uninterested',
  448. 'Front circling',
  449. 'Front close',
  450. 'Front idle'
  451. ]
  452. rows = []
  453. for analysis_group in trial_names:
  454. for trial_name in trial_names[analysis_group]:
  455. trial_idx = trial_names[analysis_group].index(trial_name)
  456. trial_feature_values = StandardScaler().fit_transform(X[trial_idx])
  457. trial_segments = X_embedded_segments[trial_idx]
  458. for state in np.unique(trial_segments):
  459. if state in ignore_states: continue
  460. for f, feat in enumerate(features):
  461. trial_feat_segment_mean = np.nanmean(
  462. trial_feature_values[trial_segments==state, f])
  463. rows.append(
  464. {"trial_name": trial_name,
  465. "analysis_group": analysis_group,
  466. "state": state,
  467. "feature": feat,
  468. "zscore": trial_feat_segment_mean}
  469. )
  470. df_mean_feature_values_segments = pd.DataFrame(rows)
  471. # plots
  472. figname = 'cluster_feature_means'
  473. fig, ax = plt.subplots(
  474. 1, len(np.unique(df_mean_feature_values_segments.state)),
  475. figsize=(3*(len(np.unique(df_mean_feature_values_segments.state))),
  476. len(np.unique(df_mean_feature_values_segments.feature))),
  477. sharey=True,
  478. num=figname)
  479. for s, state in enumerate(np.unique(df_mean_feature_values_segments.state)):
  480. if state in ignore_states: continue
  481. df_mean_feature_values = df_mean_feature_values_segments[
  482. df_mean_feature_values_segments.state==state]
  483. sns.barplot(data=df_mean_feature_values,
  484. y="feature",
  485. x="zscore",
  486. ax=ax[s],
  487. dodge=True,
  488. #palette=["k"],
  489. orient='h')
  490. #sns.pointplot(data=df_mean_feature_values,
  491. # y="feature",
  492. # x="zscore",
  493. # dodge=0.5,
  494. # ax=ax[s],
  495. # orient='h',
  496. # join=False, palette=['k'])
  497. ax[s].set_title(f"{state_names[s]}")
  498. ax[s].set_yticklabels(features)
  499. ax[s].legend([])
  500. plt.tight_layout()
  501. plt.show()
  502. # %% [markdown]
  503. # Next, we visualize the feature values directly on the social UMAP embedding.
  504. # %%
  505. # color code embedding by feature values
  506. binned = True
  507. filter_song = False
  508. groupwise=False
  509. figname = "embedding_feat_values"
  510. if binned: figname = figname + "_binned"
  511. if groupwise: figname = figname + "_groupwise"
  512. if groupwise:
  513. feat_values_binned = {}
  514. fig, ax = plt.subplots(
  515. len(trial_names), len(features), sharex=True, sharey=True,
  516. num='embedding_feature_values'+("_binned" if binned else "_"),
  517. figsize=(len(features)*2.5, 6))
  518. for g, analysis_group in enumerate(trial_names):
  519. feat_values_binned[analysis_group] = {}
  520. X_embedded_concat_group = np.concatenate(
  521. [X_embedded[i] for i in range(len(X_embedded))
  522. if all_trial_names[i] in trial_names[analysis_group]], axis=0)
  523. X_raw_concat_group = np.concatenate(
  524. [X[i] for i in range(len(X))
  525. if all_trial_names[i] in trial_names[analysis_group]], axis=0)
  526. X_raw_concat_group = StandardScaler().fit_transform(X_raw_concat_group)
  527. for i, feature in enumerate(features):
  528. if binned:
  529. ret = binned_statistic_2d(
  530. X_embedded_concat_group[:, 1],
  531. X_embedded_concat_group[:, 0],
  532. X_raw_concat_group[:, i],
  533. np.nanmean, bins=128,
  534. range=[[0, 18], [0, 18]])
  535. feat_values_binned[analysis_group][feature] = ret
  536. im = ax[g, i].imshow(
  537. ret.statistic,
  538. extent=[ret.x_edge[0], ret.x_edge[-1],
  539. ret.y_edge[0], ret.y_edge[-1]],
  540. cmap='bwr', zorder=1, vmin=-1, vmax=1,)
  541. else:
  542. ax[g, i].scatter(
  543. *X_embedded_concat_group[::10].T, s=0.1, vmin=-1, vmax=1,
  544. alpha=0.25, c=X_raw_concat_group[::10, i], cmap='bwr', zorder=1)
  545. ax[g, i].scatter(*labels_edge_positions.T, s=0.2, c='k', zorder=2)
  546. ax[g, i].set_title(feature)
  547. ax[g, i].set_xlim(0, 18)
  548. ax[g, i].set_ylim(0, 18)
  549. ax[g, 0].set_ylabel(analysis_group)
  550. ax[g, i].axis("off")
  551. fig.colorbar(
  552. im, ax=ax[-1, -1], shrink=0.25, aspect=10, ticks=[-1, 0, 1])
  553. # both groups together
  554. else:
  555. fig, ax = plt.subplots(
  556. 1, len(features), sharex=True, sharey=True,
  557. num='embedding_feature_values'+("_binned" if binned else "_"),
  558. figsize=(len(features)*2.5, 3))
  559. feat_values_binned = {}
  560. if not filter_song:
  561. X_embedded_concat_group = np.concatenate(
  562. [X_embedded[i] for i in range(len(X_embedded))], axis=0)
  563. X_raw_concat_group = np.concatenate(
  564. [X[i] for i in range(len(X))], axis=0)
  565. else:
  566. X_embedded_concat_group = np.concatenate(
  567. [X_embedded[i][y[i].ravel()!=0]
  568. for i in range(len(X_embedded))], axis=0)
  569. X_raw_concat_group = np.concatenate(
  570. [X[i][y[i].ravel()!=0] for i in range(len(X))], axis=0)
  571. X_raw_concat_group = StandardScaler().fit_transform(X_raw_concat_group)
  572. for i, feature in enumerate(features):
  573. if binned:
  574. ret = binned_statistic_2d(
  575. X_embedded_concat_group[:, 1],
  576. X_embedded_concat_group[:, 0],
  577. X_raw_concat_group[:, i],
  578. np.nanmean, bins=128,
  579. range=[[-18, 18], [-18, 18]])
  580. feat_values_binned[feature] = ret
  581. im = ax[i].imshow(
  582. ret.statistic,
  583. extent=[ret.x_edge[0], ret.x_edge[-1],
  584. ret.y_edge[0], ret.y_edge[-1]],
  585. cmap='bwr', zorder=1, vmin=-1, vmax=1,)
  586. else:
  587. im = ax[i].scatter(
  588. *X_embedded_concat_group[::10].T, s=0.1, vmin=-1, vmax=1,
  589. alpha=0.25, c=X_raw_concat_group[::10, i], cmap='bwr', zorder=1)
  590. ax[i].scatter(*labels_edge_positions.T, s=0.2, c='k', zorder=2)
  591. ax[i].set_title(feature, fontsize=10)
  592. ax[i].set_xlim(0, 20)
  593. ax[i].set_ylim(0, 20)
  594. ax[i].axis("off")
  595. fig.colorbar(im, ax=ax[-1], shrink=0.25, aspect=10, ticks=[-1, 0, 1])
  596. plt.tight_layout()
  597. plt.show()
  598. # %% [markdown]
  599. # Finally, we plot the courting male's position around the partner during each social mode. We normalize the distance between flies by the length of the target and limit the visualization to three fly lengths.
  600. # %%
  601. if os.path.exists(f'{results_save_path}/courter_positions.npz'):
  602. with np.load(f'{results_save_path}/courter_positions.npz', allow_pickle=True) as data:
  603. dict_courter_positions = data['arr_0'].item()
  604. else:
  605. dict_courter_positions = {}
  606. for trial_idx, trial_name in enumerate(all_trial_names):
  607. df_trial_data = pd.read_csv(f'{data_save_path}/{trial_name}.csv')
  608. courter_position = df_trial_data[['relative_angle_courter', 'distance_target']].values
  609. # normalize distance by target length
  610. courter_position[:, 1] = courter_position[:, 1]/df_trial_data["target_length"].values
  611. trial_segments = X_embedded_segments[trial_idx]
  612. for state in np.unique(trial_segments):
  613. if state in ignore_states: continue
  614. else:
  615. if state not in dict_courter_positions:
  616. dict_courter_positions[state] = []
  617. dict_courter_positions[state].extend(courter_position[:len(trial_segments)][trial_segments==state])
  618. np.savez(f'{results_save_path}/courter_positions.npz', dict_courter_positions)
  619. rbins = np.linspace(0, 3, 31)
  620. abins = np.linspace(-np.pi, np.pi, 73)
  621. courter_position_hist = {}
  622. for state in dict_courter_positions:
  623. dict_courter_positions[state]=np.array(dict_courter_positions[state])
  624. azimut = np.array(dict_courter_positions[state][:, 0])
  625. azimut_rad = np.deg2rad(azimut)
  626. radius = np.array(dict_courter_positions[state][:, 1])
  627. #calculate histogram
  628. hist, _, _ = np.histogram2d(azimut_rad, radius, bins=(abins, rbins),
  629. density=True)
  630. courter_position_hist[state] = hist
  631. # plot
  632. A, R = np.meshgrid(abins, rbins)
  633. figname = 'courter_position_states'
  634. fig, ax = plt.subplots(
  635. 1, len(courter_position_hist), subplot_kw=dict(projection="polar"),
  636. figsize=(3*len(courter_position_hist), 3), num=figname)
  637. for s, (segment, hist) in enumerate(courter_position_hist.items()):
  638. pc = ax[s].pcolormesh(
  639. A, R, (hist).T,
  640. cmap="Reds",
  641. vmin=0, vmax=1)
  642. ax[s].set_title(state_names[s], fontsize=15)
  643. ax[s].set_xticks([-np.pi*(3/4), -np.pi/2, -np.pi/4,
  644. 0, np.pi/4, np.pi/2, np.pi*(3/4), np.pi])
  645. ax[s].set_xlim(-np.pi, np.pi)
  646. ax[s].set_theta_zero_location("N")
  647. plt.subplots_adjust(bottom=0.4, right=0.9, top=0.6)
  648. cax = plt.axes([0.85, 0.1, 0.01, 0.2])
  649. plt.colorbar(pc, cax=cax)
  650. plt.tight_layout()
  651. plt.show()
  652. # %% [markdown]
  653. # ## Comparison between male-female and male-male interactions
  654. #
  655. # Once we have the social UMAP embedding, we can compare them across different experimental conditions to understand differences in their interactions. Here we plot the UMAPs for male-female and male-male interactions and visualize their differences.
  656. # %%
  657. # comparison between groups
  658. kde_mean_group = {}
  659. X_embedded_kde_group = {}
  660. groups = list(trial_names.keys())
  661. for group in groups:
  662. X_embedded_kde_group[group] = []
  663. for trial_name in trial_names[group]:
  664. trial_idx = all_trial_names.index(trial_name)
  665. X_embedded_kde_group[group].append(X_embedded_kde[trial_idx])
  666. group_kde_mean = np.mean(X_embedded_kde_group[group], 0)
  667. group_kde_mean[group_kde_mean<1e-3]=0
  668. kde_mean_group[group] = group_kde_mean
  669. fig, ax = plt.subplots(1, 3, sharex=True, sharey=True, figsize=(15, 5))
  670. xmin = positions[:,0].min()
  671. xmax = positions[:,0].max()
  672. ymin = positions[:,1].min()
  673. ymax = positions[:,1].max()
  674. for g, group in enumerate(kde_mean_group):
  675. vmin = 0 #np.round(np.nanpercentile(kde_mean_group[group], 2.5), 2)
  676. vmax = 0.03 #np.round(np.nanpercentile(kde_mean_group[group], 97.5), 2)
  677. kde_plot = ax[g].imshow(
  678. kde_mean_group[group],
  679. origin='lower',
  680. extent=[xmin, xmax, ymin, ymax],
  681. vmin=vmin, vmax=vmax,
  682. cmap="Reds")
  683. ax[g].scatter(*labels_edge_positions.T, s=0.2, c='k')
  684. ax[g].set_title(group)
  685. ax[g].axis('off')
  686. ax[g].set_xlim(0, 20)
  687. ax[g].set_ylim(0, 20)
  688. fig.colorbar(kde_plot, ax=ax[g], shrink=0.25, aspect=10, ticks=[vmin, vmax])
  689. # difference
  690. statistical_test = True
  691. X_embedded_kde_diff = kde_mean_group[groups[0]] - kde_mean_group[groups[1]]
  692. vmin=-0.03
  693. vmax=0.03
  694. if statistical_test:
  695. statistic, p_value = mannwhitneyu(X_embedded_kde_group[groups[0]], X_embedded_kde_group[groups[1]])
  696. X_embedded_kde_diff = X_embedded_kde_diff * (p_value<0.05)
  697. diff_lim = np.max(np.abs(X_embedded_kde_diff))
  698. kde_diff = ax[2].imshow(
  699. X_embedded_kde_diff,
  700. cmap='bwr', origin='lower',
  701. extent=[xmin, xmax, ymin, ymax],
  702. vmin=vmin, vmax=vmax)
  703. ax[2].scatter(*labels_edge_positions.T, s=0.2, c='k')
  704. ax[2].set_title("difference")
  705. ax[2].set_xlim(0, 20)
  706. ax[2].set_ylim(0, 20)
  707. ax[2].axis('off')
  708. fig.colorbar(kde_diff, ax=ax[2], shrink=0.25, aspect=10, ticks=[vmin, 0, vmax])
  709. plt.tight_layout()
  710. plt.show()
  711. plt.tight_layout()
  712. plt.show()
  713. # %% [markdown]
  714. # Next, we quantify the differences in social modes during male-female and male-male interactions.
  715. # %%
  716. rows = []
  717. for analysis_group in trial_names:
  718. for trial_name in trial_names[analysis_group]:
  719. trial_idx = all_trial_names.index(trial_name)
  720. trial_segments = X_embedded_segments[trial_idx]
  721. for s, state in enumerate(np.unique(trial_segments)):
  722. if s in ignore_states: continue
  723. state_ratio = np.sum(trial_segments==state)/len(trial_segments)
  724. rows.append(
  725. {"trial_name": trial_name,
  726. "analysis_group": analysis_group,
  727. "state": s,
  728. "ratio": state_ratio}
  729. )
  730. df_states_time_spent = pd.DataFrame(rows)
  731. fig, ax = plt.subplots(1, 1, figsize=(2*len(np.unique(df_states_time_spent.state)), 4))
  732. sns.barplot(data=df_states_time_spent, x="state", y="ratio", hue="analysis_group")
  733. sns.stripplot(data=df_states_time_spent, x="state", y="ratio", hue="analysis_group", dodge=True, alpha=0.25, palette=["k"])
  734. sns.despine()
  735. ax.set_ylim(0, 1)
  736. ax.set_xticklabels(state_names)
  737. plt.tight_layout()
  738. plt.show()
  739. # %% [markdown]
  740. # ## Comparisons between different song contexts
  741. #
  742. # To compare interactions in different singing conditions, we condition our UMAPs on different song contexts and compare song versus silence and pulse versus sine.
  743. # %%
  744. unique_segments = np.unique(np.concatenate(X_embedded_segments))
  745. song_conditioned_kde = {}
  746. rows = []
  747. for analysis_group in trial_names:
  748. song_conditioned_kde[analysis_group] = {}
  749. for song_type in ['song', 'silence', 'pulse', 'sine']:
  750. song_conditioned_kde[analysis_group][song_type] = []
  751. for datename in trial_names[analysis_group]:
  752. trial_idx = all_trial_names.index(datename)
  753. X_embedded_trial = X_embedded[trial_idx][nb_delays:]
  754. X_embedded_segments_trail = X_embedded_segments[trial_idx][nb_delays:]
  755. y_trial = y_proc[trial_idx].ravel()
  756. # song
  757. if song_type == 'song':
  758. trial_song_embedding = X_embedded_trial[(y_trial!=0)]
  759. trial_song_segments = X_embedded_segments_trail[y_trial!=0]
  760. elif song_type == 'silence':
  761. trial_song_embedding = X_embedded_trial[(y_trial==0)]
  762. trial_song_segments = X_embedded_segments_trail[y_trial==0]
  763. elif song_type == 'pulse':
  764. trial_song_embedding = X_embedded_trial[(y_trial==1)]
  765. trial_song_segments = X_embedded_segments_trail[y_trial==1]
  766. elif song_type == 'sine':
  767. trial_song_embedding = X_embedded_trial[(y_trial==2)]
  768. trial_song_segments = X_embedded_segments_trail[y_trial==2]
  769. _, trial_song_kde = kernel_density_estimation(
  770. trial_song_embedding, bw, positions)
  771. song_conditioned_kde[analysis_group][song_type].append(trial_song_kde)
  772. for state in unique_segments:
  773. if state not in ignore_states:
  774. song_state_time_trial = (
  775. np.sum(trial_song_segments==state) / len(trial_song_segments)
  776. )
  777. rows.append(
  778. {"analysis_group": analysis_group,
  779. "song": song_type,
  780. "trial": datename,
  781. "state": state,
  782. "time_spent": np.sum(
  783. trial_song_segments==state),
  784. "time_spent_ratio": song_state_time_trial}
  785. )
  786. song_conditioned_kde[analysis_group][song_type] = np.array(
  787. song_conditioned_kde[analysis_group][song_type])
  788. df_song_states_time = pd.DataFrame(rows)
  789. # %%
  790. # song vs silence
  791. rows = []
  792. for analysis_group in np.unique(df_song_states_time.analysis_group):
  793. for trial in np.unique(df_song_states_time.trial):
  794. for state in np.unique(df_song_states_time.state):
  795. trial_song_states_time = df_song_states_time[
  796. (df_song_states_time.analysis_group==analysis_group)&
  797. (df_song_states_time.trial==trial)&
  798. (df_song_states_time.state==state)
  799. ]
  800. if not len(trial_song_states_time): continue
  801. song_silence_diff = trial_song_states_time[
  802. (trial_song_states_time.song=="song")].time_spent_ratio.values[0] - trial_song_states_time[
  803. (trial_song_states_time.song=="silence")].time_spent_ratio.values[0]
  804. pulse_sine_diff = trial_song_states_time[
  805. (trial_song_states_time.song=="pulse")].time_spent_ratio.values[0] - trial_song_states_time[
  806. (trial_song_states_time.song=="sine")].time_spent_ratio.values[0]
  807. rows.append(
  808. {"analysis_group": analysis_group,
  809. "trial": trial,
  810. "state": state,
  811. "p(song)-p(silence)": song_silence_diff,
  812. "p(pulse)-p(sine)": pulse_sine_diff}
  813. )
  814. df_song_diff = pd.DataFrame(rows)
  815. fig, ax = plt.subplots(1, 3, figsize=(15, 3), gridspec_kw={'width_ratios': [1, 1, 3]})
  816. mean_kde_mf_diff = np.mean(song_conditioned_kde['male-female']['song'], 0) - \
  817. np.mean(song_conditioned_kde['male-female']['silence'], 0)
  818. mean_kde_mm_diff = np.mean(song_conditioned_kde['male-male']['song'], 0) - \
  819. np.mean(song_conditioned_kde['male-male']['silence'], 0)
  820. mf_kde = ax[0].matshow(mean_kde_mf_diff,
  821. cmap='bwr', origin='lower',
  822. extent=[xmin, xmax, ymin, ymax],
  823. vmin=-0.03, vmax=0.03)
  824. ax[0].set_xlim(0, 20)
  825. ax[0].set_ylim(0, 20)
  826. ax[0].set_ylabel("song - silence")
  827. ax[0].set_title("female-directed")
  828. ax[0].axis("off")
  829. ax[0].scatter(*labels_edge_positions.T, s=0.2, c='k')
  830. fig.colorbar(mf_kde, ax=ax[0], shrink=0.25, aspect=10, ticks=[-0.03, 0, 0.03])
  831. mm_kde = ax[1].matshow(mean_kde_mm_diff,
  832. cmap='bwr', origin='lower',
  833. extent=[xmin, xmax, ymin, ymax],
  834. vmin=-0.03, vmax=0.03)
  835. ax[1].set_xlim(0, 20)
  836. ax[1].set_ylim(0, 20)
  837. ax[1].axis("off")
  838. ax[1].set_title("male-directed")
  839. ax[1].scatter(*labels_edge_positions.T, s=0.2, c='k')
  840. #fig.colorbar(mm_kde, ax=ax[1], shrink=0.25, aspect=10, ticks=[-0.03, 0, 0.03])
  841. sns.barplot(data=df_song_diff, x="state", y="p(song)-p(silence)", hue="analysis_group", ax=ax[2])
  842. sns.stripplot(data=df_song_diff, x="state", y="p(song)-p(silence)", hue="analysis_group", ax=ax[2], alpha=0.25, dodge=True, palette=["k"])
  843. ax[2].set_xticklabels(state_names, rotation=45)
  844. plt.suptitle("song - silence")
  845. #plt.tight_layout()
  846. # %%
  847. # pulse vs sine
  848. fig, ax = plt.subplots(1, 3, figsize=(15, 3), gridspec_kw={'width_ratios': [1, 1, 3]})
  849. mean_kde_mf_diff = np.mean(song_conditioned_kde['male-female']['pulse'], 0) - \
  850. np.mean(song_conditioned_kde['male-female']['sine'], 0)
  851. mean_kde_mm_diff = np.mean(song_conditioned_kde['male-male']['pulse'], 0) - \
  852. np.mean(song_conditioned_kde['male-male']['sine'], 0)
  853. mf_kde = ax[0].matshow(mean_kde_mf_diff,
  854. cmap='bwr', origin='lower',
  855. extent=[xmin, xmax, ymin, ymax],
  856. vmin=-0.03, vmax=0.03)
  857. ax[0].set_xlim(0, 20)
  858. ax[0].set_ylim(0, 20)
  859. ax[0].set_title("female-directed")
  860. ax[0].axis("off")
  861. ax[0].scatter(*labels_edge_positions.T, s=0.2, c='k')
  862. fig.colorbar(mf_kde, ax=ax[0], shrink=0.25, aspect=10, ticks=[-0.03, 0, 0.03])
  863. mm_kde = ax[1].matshow(mean_kde_mm_diff,
  864. cmap='bwr', origin='lower',
  865. extent=[xmin, xmax, ymin, ymax],
  866. vmin=-0.03, vmax=0.03)
  867. ax[1].set_xlim(0, 20)
  868. ax[1].set_ylim(0, 20)
  869. ax[1].axis("off")
  870. ax[1].set_title("male-directed")
  871. ax[1].scatter(*labels_edge_positions.T, s=0.2, c='k')
  872. #fig.colorbar(mm_kde, ax=ax[1], shrink=0.25, aspect=10, ticks=[-0.03, 0, 0.03])
  873. sns.barplot(data=df_song_diff, x="state", y="p(pulse)-p(sine)", hue="analysis_group", ax=ax[2])
  874. sns.stripplot(data=df_song_diff, x="state", y="p(pulse)-p(sine)", hue="analysis_group", ax=ax[2], alpha=0.25, dodge=True, palette=["k"])
  875. ax[2].set_xticklabels(state_names, rotation=45)
  876. plt.suptitle("pulse - sine")
  877. #plt.tight_layout()
  878. # %% [markdown]
  879. # ## Hyperparameter tuning
  880. #
  881. # We used three important hyperparameters: two for fitting the UMAP, `n_neighbors` and `min_dist`, and one for time-delay embedding of features, `nb_delays`.
  882. #
  883. # The `n_neighbors` parameter specifies the size of the local neighborhood UMAP looks at when fitting the manifold. This parameter controls how UMAP balances local versus global structure in the data. Low values make the UMAP fit the local structure well, whereas large values focus on global structure but can lose fine details.
  884. #
  885. # `min_dist` specifies the minimum distance between the embedded points in the low-dimensional manifold. Thus, low values result in points being embedded densely together.
  886. #
  887. # `nb_delays` specifies the history information to be included when embedding the features into low dimensions. Small values focus on immediate history.
  888. #
  889. # To find out the optimal values of these hyperparameters for our data, we performed a hyperparameter optimization. This was done by fitting the UMAP with different hyperparameter combinations and validating performance by the reconstruction error. From the fitted UMAP, an `inverse_transform` is applied on an embedded validation set (not used for fitting), and the mean squared error between the original validation data and reconstructed validation data is quantified.
  890. # %%
  891. #UMAP hyperparameter tuning
  892. n_neighbors_grid=[10, 50, 100, 200]
  893. min_dist_grid=[0.0, 0.1, 0.2, 0.5]
  894. nb_delays_grid=[15, 30, 60, 120]#s
  895. fit_results = umap_multiple_fits(
  896. X,
  897. n_neighbors_grid=n_neighbors_grid,
  898. min_dist_grid=min_dist_grid,
  899. nb_delays_grid=nb_delays_grid)
  900. # plots
  901. val_score_matrices = {}
  902. for nb_delays in nb_delays_grid:
  903. val_score_matrices[nb_delays] = np.zeros((len(min_dist_grid), len(n_neighbors_grid)))
  904. for m, min_dist in enumerate(min_dist_grid):
  905. for n, n_neighbors in enumerate(n_neighbors_grid):
  906. val_score_matrices[nb_delays][m, n] = fit_results[
  907. f'delay: {nb_delays}, '+\
  908. f'n_components: 2, ' +\
  909. f'n_neighbors: {n_neighbors}, ' +\
  910. f'min_dist: {min_dist}, '+\
  911. f'metric: euclidean'
  912. ]['val_score']
  913. fig, ax = plt.subplots(1, len(nb_delays_grid), figsize=(len(nb_delays_grid)*3, 3))
  914. for i, nb_delays in enumerate(nb_delays_grid):
  915. im = ax[i].matshow(val_score_matrices[nb_delays])
  916. ax[i].set_xlabel("n_neighbors")
  917. ax[i].set_xticklabels(n_neighbors_grid)
  918. ax[i].set_ylabel("min_dist")
  919. ax[i].set_yticklabels(min_dist_grid)
  920. fig.colorbar(im, ax=ax[i], shrink=0.25, aspect=10)
  921. plt.tight_layout()
  922. plt.show()
  923. # %%

social_behavior_umap.ipynb at commit db64ff6, no license · at the source

Overview

Authors: Sarath Ravindran Nair1, Adrián Palacios-Muñoz1,2,3, Sage Martineau2,4, Malak Nasr2,5,6, Jan Clemens1,2,7
  1. ENI-G, a Joint Initiative of the University Medical Center Göttingen and the Max Planck Institute for Multidisciplinary Sciences, Göttingen, Germany
  2. IMPRS Neuroscience, Göttingen, Germany
  3. Present Address: Charité Universitätsmedizin Berlin, Berlin, Germany
  4. Institute of Computer Science, University of Göttingen, Göttingen, Germany
  5. CERVO Brain Research Centre, Québec City, QC Canada
  6. Faculty of Medicine, Université Laval, Québec City, QC Canada
  7. Department of Neuroscience, Faculty VI, University of Oldenburg, Oldenburg, Germany
Journal: Nature communications, volume 17, issue 1, article 4026
Dates: received 21 July 2025; accepted 2 April 2026; published online 4 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41467-026-72057-9 · PMID 42082469 · PMCID PMC13139495 · OpenAlex W4410238817
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: drosophila (organism), systems (subfield)
Methods: Statistics, Smoothing, state filtering, decompositions, Spectral & time-frequency, Preprocessing, Evoked potentials, fMRI & imaging
Keywords: Social behaviour, Decision, Sexual behaviour
MeSH: Drosophila melanogaster*, Sexual Behavior, Animal*, Social Behavior*, Animals, Brain, Courtship, Female, Male, Optogenetics, Vocalization, Animal (* major topic)
Topic: Neuroendocrine regulation and behavior (Social Psychology, Psychology), according to OpenAlex
Funding: European Research Council (851210); Deutsche Forschungsgemeinschaft (German Research Foundation) (329518246)
Citations: not cited yet (Europe PMC); 138 references in the paper

Abstract

How the brain enables individuals to adapt behavior to their partner is key to understanding social exchange. For example, courtship behavior involves sensorimotor processing of signals that can result in behavioral dialog between partners, such as stereotyped movements and singing. The courtship behavior of Drosophila melanogaster males with their partners, which are usually female but can also be male, involves singing. To investigate how behavioral feedback and sensorimotor processing contribute to flexible social interactions, we compared the courtship behavior and singing of male D. melanogaster towards males and females. Quantitative analysis of their interactions revealed that while underlying courtship and song rules are unaffected by the sex of the partner, the behavioral dynamics and song sequences differ by partner sex. This divergence stems from sex-specific behavioral feedback: females decelerate to song, while males orient towards the singer. Moreover, optogenetic manipulations reveal that the partners’ responses are driven by sex-specific neural circuits that link song detection with arousal and social decisions. Our findings demonstrate that flexible social behaviors can arise from fixed sensorimotor rules through a context-dependent selection facilitated by the partner’s behavioral feedback. More broadly, our results reveal compositionality as a key mechanism for achieving behavioral flexibility during complex social interactions such as courtship.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 11 matches between paragraphs and lines of code.

janclemenslab/socialUMAP

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: db64ff6a2513c17db493d9c7d931daea4bce087b, 26 January 2026
Languages: Python (1), Jupyter (1)
Size: 10 files, 2 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, environment (requirements.txt), 1 notebook
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: Matplotlib (2 files), NumPy (2 files), pandas (2 files), scikit-learn (2 files), SciPy (2 files), PyWavelets (1 file), scikit-image (1 file), seaborn (1 file), UMAP (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
3 files

jgraving/DeepPoseKit

License: Apache-2.0
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: cecdb0c8c364ea049a3b705275ae71a2f366d4da, 9 June 2020
Languages: Python (52), Jupyter (7)
Size: 66 files, 59 scripts
Software Heritage: not archived
Found in: “Software and algorithms used”
Holds: README, license file, environment (setup.py), 7 notebooks
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (35 files), TensorFlow (30 files), Keras (26 files), OpenCV (11 files), Matplotlib (9 files), h5py (8 files), pandas (5 files), scikit-learn (1 file), SciPy (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
61 files

janclemenslab/das

License: Apache-2.0
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 9a7b5b5ac850a7f85d05bd0f88477ea2991f46d2, 15 September 2026
Languages: Python (48), Jupyter (11)
Size: 125 files, 59 scripts
Software Heritage: archived
Found in: “Software and algorithms used”
Holds: README, license file, environment (pyproject.toml), tests, continuous integration, documentation, 11 notebooks
Not found: CITATION.cff
Tools: NumPy (38 files), Keras (20 files), Matplotlib (10 files), SciPy (9 files), h5py (7 files), pandas (6 files), scikit-learn (6 files), UMAP (3 files), PyTorch (2 files), xarray (2 files), seaborn (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
61 files

janclemenslab/glm_utils

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 65b048a1d9f2aa0a8f21be2d78ab154ecd05f241, 8 June 2021
Languages: Jupyter (7), Python (6)
Size: 19 files, 13 scripts
Software Heritage: archived
Found in: “Software and algorithms used”
Holds: README, license file, environment (pyproject.toml, setup.py), 7 notebooks
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (12 files), scikit-learn (8 files), Matplotlib (6 files), SciPy (6 files), h5py (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
15 files

lindermanlab/ssm

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: eb6c8aa33e5311d3564075807dec340759dd8081, 9 May 2025
Languages: Python (51), Jupyter (14)
Size: 93 files, 65 scripts
Software Heritage: archived
Found in: “Software and algorithms used”
Holds: README, license file, CITATION.cff, environment (setup.cfg, setup.py), tests, continuous integration, documentation, 14 notebooks
Tools: Matplotlib (36 files), seaborn (25 files), SciPy (17 files), NumPy (11 files), scikit-learn (11 files), Numba (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
67 files

lmcinnes/umap

License: BSD-3-Clause
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 1180b785023ff8e8eb071de56c581d1dc8bec04e, 24 September 2026
Languages: Python (48), Jupyter (11), Shell (3)
Size: 319 files, 62 scripts
Software Heritage: archived
Found in: “Software and algorithms used”
Holds: README, license file, environment (pyproject.toml, setup.py), tests, continuous integration, documentation, 11 notebooks
Not found: CITATION.cff
Tools: UMAP (50 files), NumPy (44 files), scikit-learn (38 files), Matplotlib (20 files), Numba (20 files), SciPy (15 files), TensorFlow (10 files), Keras (9 files), pandas (6 files), seaborn (6 files), h5py (1 file), Pillow (1 file), PyTorch (1 file), scikit-image (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
64 files

tommyod/KDEpy

License: BSD-3-Clause
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: cdeca713299f17ab0a55365c761c1999dc0421a0, 6 May 2026
Languages: Python (26), MATLAB (9), Jupyter (6)
Size: 76 files, 41 scripts
Software Heritage: not archived
Found in: “Software and algorithms used”
Holds: README, license file, environment (pyproject.toml, setup.py), tests, continuous integration, documentation, 6 notebooks
Not found: CITATION.cff
Tools: NumPy (30 files), SciPy (13 files), Matplotlib (12 files), scikit-learn (3 files), statsmodels (3 files), Numba (1 file), pandas (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
43 files

trevismd/statannotations

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 3f020ae631ca88a091b6ee3e9a9fd32158920879, 22 June 2025
Languages: Python (37), JavaScript (9), Shell (2), Jupyter (1)
Size: 119 files, 49 scripts
Software Heritage: archived
Found in: “Software and algorithms used”
Holds: README, license file, environment (requirements.txt, setup.py, docs/requirements.in, docs/requirements.txt), tests, continuous integration, documentation, 1 notebook
Not found: CITATION.cff
Tools: statannotations (30 files), pandas (14 files), seaborn (14 files), NumPy (13 files), Matplotlib (7 files), SciPy (5 files), statsmodels (2 files)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
51 files

pywavelets.readthedocs.io

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: the link answers
Software Heritage: not checked
Found in: “Software and algorithms used”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 28 September 2026: the link answers (HTTP 200)
  • 28 September 2026: the link answers (HTTP 200)

Code availability

All data analyses were performed using the software listed in Table 2. Code for generating the social maps is deposited at https://github.com/janclemenslab/socialUMAP.

Reproduced under the paper's license (CC BY), from the paper cited above.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 9 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 350 scripts, each with its path and the digest of its content;
  • 11 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability

All data supporting the findings of this study are available within the paper, its Supplementary Information or a public repository. Source data are provided with this paper as a Source Data file. Raw experimental data generated in this study have been deposited in the Göttingen Research Online database (10.25625/PTKYTD). Source data are provided with this paper.

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, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 3 keywords, 10 MeSH terms, 2 funders, 113 references.

Cite

This paper

Ravindran Nair, S., Palacios-Muñoz, A., Martineau, S., Nasr, M., & Clemens, J. (2026). Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior. Nature communications, 17(1), 4026. https://doi.org/10.1038/s41467-026-72057-9

BibTeX

@article{ravindrannair2026sex,
author = {Ravindran Nair, Sarath and Palacios-Muñoz, Adrián and Martineau, Sage and Nasr, Malak and Clemens, Jan},
title = {{Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior}},
journal = {Nature communications},
year = {2026},
month = may,
volume = {17},
number = {1},
pages = {4026},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-72057-9},
url = {https://doi.org/10.1038/s41467-026-72057-9},
pmid = {42082469},
pmcid = {PMC13139495}
}

RIS

TY - JOUR
AU - Ravindran Nair, Sarath
AU - Palacios-Muñoz, Adrián
AU - Martineau, Sage
AU - Nasr, Malak
AU - Clemens, Jan
TI - Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/05/04
VL - 17
IS - 1
SP - 4026
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-72057-9
UR - https://doi.org/10.1038/s41467-026-72057-9
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-72057-9",
"type": "article-journal",
"title": "Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior",
"container-title": "Nature communications",
"author": [
{
"family": "Ravindran Nair",
"given": "Sarath"
},
{
"family": "Palacios-Muñoz",
"given": "Adrián"
},
{
"family": "Martineau",
"given": "Sage"
},
{
"family": "Nasr",
"given": "Malak"
},
{
"family": "Clemens",
"given": "Jan"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "4026",
"DOI": "10.1038/s41467-026-72057-9",
"PMID": "42082469",
"PMCID": "PMC13139495",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-72057-9",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
4
]
]
}
}

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.3389/fnsys.2026.1822122 [code]
Convergence-divergence circuits for multimodal integration of innate and learned opponent valences.
Journal: Frontiers in systems neuroscience
In common: Keras, UMAP, Numba, 11 other tools, systems, 13 references
[2] doi:10.1038/s41586-026-10735-w [code]
Distributed control circuits across a brain-and-cord connectome.
Journal: Nature
In common: UMAP, scikit-image, PyTorch, 6 other tools, drosophila, 9 references
[3] doi:10.1093/g3journal/jkag037 [code]
Visual recognition of the anteroposterior female body axis drives spatial elements of male courtship in Drosophila.
Journal: G3 (Bethesda, Md.)
In common: scikit-image, h5py, scikit-learn, 4 other tools, drosophila, 8 references
[4] doi:10.1038/s41592-026-03057-2 [code]
CREsted: modeling genomic and synthetic cell-type-specific enhancers across tissues and species.
Journal: Nature methods
In common: PyWavelets, Keras, UMAP, 12 other tools
[5] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In common: statannotations, Keras, Numba, 12 other tools
[6] doi:10.1371/journal.pcbi.1014571 [code]
SynAPSeg: A novel dataset and image analysis framework for deep learning-based synapse detection and quantification.
Journal: PLoS computational biology
In common: Keras, Numba, TensorFlow, 11 other tools, 2 references
[7] doi:10.1038/s41467-026-72437-1 [code]
High-speed whole-brain imaging in Drosophila.
Journal: Nature communications
In common: OpenCV, scikit-image, h5py, 7 other tools, drosophila, systems, 4 references
[8] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: Keras, UMAP, Numba, 12 other tools
[9] doi:10.1038/s41467-026-72152-x [code]
Centralized brain networks controlling antennal grooming coordination.
Journal: Nature communications
In common: TensorFlow, OpenCV, h5py, 7 other tools, drosophila, systems, 4 references
[10] doi:10.1038/s41593-026-02232-0 [code]
Entorhinal cortex represents task-relevant remote locations independently of CA1.
Journal: Nature neuroscience
In common: xarray, Numba, OpenCV, 11 other tools, systems

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.