In-scanner thoughts contribute to resting-state functional connectivity.
The 15 matches · 3 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
- [1] § Methods › In-scanner experience reports analyses › Clustering ↔ notebooks/S10_SNYCQ_TSNE_Clustering.ipynb, lines 415–442 · score 0.94 · Gaussian Mixture Modeling, Python library scikit, soft clustering nature, spherical clusters, provides membership probabilities, neuroimaging literature
- [2] § Methods › In-scanner experience reports analyses › Dimensionality reduction: T-SNE ↔ notebooks/S10_SNYCQ_TSNE_Clustering.ipynb, lines 277–310 · score 0.91 · draw biplot arrows, NSE embedding space, SNE dimensions relate, linear function, maximal change, SNE coordinates
- [3] § Methods › In-scanner experience reports analyses › Dimensionality reduction: T-SNE ↔ notebooks/S10_SNYCQ_TSNE_Clustering.ipynb, lines 218–253 · score 0.85 · explored hyper parameter, embedding preserves local, SNE algorithm, t-SNE, trustworthiness, perplexity
- [4] § Methods › fMRI Data Pre-processing ↔ bash/S06_NuissanceRegression.sh, lines 86–173 · score 0.79 · nuisance regression, noise regressors, 0.15 Hz, derivative, filtering, AFNI
- [5] § Methods › In-scanner experience reports analyses › Data scaling ↔ notebooks/S10_SNYCQ_TSNE_Clustering.ipynb, lines 66–75 · score 0.79 · RobustScaler, inter quantile range, excessive influence, scikit learn, median, outliers
- [6] § Methods › In-scanner experience reports analyses › Clustering ↔ notebooks/S10_SNYCQ_TSNE_Clustering.ipynb, lines 415–442 · score 0.79 · low dimensional space, clustering technique, clustering algorithms, inner experience, separate scans, robustness
- [7] § Methods › In-scanner experience reports analyses › Outlier detection ↔ notebooks/S10_SNYCQ_TSNE_Clustering.ipynb, lines 85–101 · score 0.65 · squared Mahalanobis distance, Outlier detection, quantile, scan
- [8] § Methods › fMRI Data Pre-processing ↔ bash/S04_TransformToMNI.pass01.sh, the whole file · a weak match · score 0.63 · MNI space, motion correction, Pre processing, AFNI, scans
- [9] § Methods › Literature search › Connectome-based predictive modeling studies ↔ notebooks/S16_CPM.Development.ipynb, lines 257–315 · score 0.60 · feature selection, cross validation, Fold, CPM, predicted, models
- [10] § Methods › Functional connectivity analyses - statistical differences across scan sets › Parcellation ↔ bash/S08_ExtractROIts.sh, the whole file · a weak match · score 0.57 · dNetCorr, pre processing, FC matrices, ROIs, AFNI, atlas
- [11] § Methods › Functional connectivity analyses – connectome-based predictive modeling ↔ notebooks/S16_CPM.Development.ipynb, lines 257–315 · score 0.56 · cross validation, gather, training, fold, CPM, metric
- [12] § Methods › fMRI Data Pre-processing ↔ bash/S04_TransformToMNI.pass01.sh, the whole file · a weak match · score 0.55 · ANTs, pre processing, MNI152, scan
- [13] § Methods › In-scanner experience reports analyses › Clustering ↔ notebooks/S10_SNYCQ_TSNE_Clustering.ipynb, lines 479–515 · score 0.53 · Adjusted Rand, subsamples, ARI, bootstrapping, clusters, SI
- [14] § Results › Self-report annotations of resting-state scans ↔ notebooks/S10_SNYCQ_TSNE_Clustering.ipynb, lines 479–515 · score 0.52 · adjusted Rand, subsampling, ARI, bootstrapping, fit, GMM
- [15] § Methods › Functional connectivity analyses - statistical differences across scan sets › Parcellation ↔ notebooks/S07_PrepareAtlas.ipynb, lines 1–32 · score 0.52 · Limbic network, subcortical regions, pallidum, AAL, putamen, ROIs
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 · 1,101 lines · 40 KB · CC0-1.0 · 8 matches
- # %% [markdown]
- # # Description: SNYCQ Initial Exploration, Clustering and TSNE
- #
- # This notebook contains the following analytical steps associated with the in-scanner experience data
- #
- # 1. Data scaling: this is accomplished using skicit-learn [```RobustScaler```](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.RobustScaler.html)
- #
- # 2. Outlier detection
- #
- # 3. Creates figure looking a potential correlations between sNYCQ items
- #
- # 4. Dimensionality reduction with T-SNE
- #
- # 5. Clustering analysis in original 11D space
- # %%
- from utils.basics import get_sbj_scan_list, RESOURCES_DINFO_DIR, RESOURCES_SNYCQ_DIR, ORIG_DEMO_PATH
- from utils.plotting import show_correlations_with_statistics
- import os.path as osp
- from scipy.stats import ttest_ind, mannwhitneyu, wilcoxon
- import hvplot.pandas
- import holoviews as hv
- import seaborn as sns
- from tqdm.notebook import tqdm
- import warnings
- warnings.simplefilter(action='ignore', category=FutureWarning)
- # %%
- import numpy as np
- import pandas as pd
- import matplotlib.pyplot as plt
- import plotly.graph_objects as go
- import panel as pn
- from scipy.stats import pearsonr
- from sklearn.preprocessing import RobustScaler
- from sklearn.covariance import MinCovDet
- from sklearn.mixture import GaussianMixture
- from sklearn.cluster import KMeans
- from sklearn.metrics import silhouette_score, adjusted_rand_score
- from sklearn.manifold import TSNE, trustworthiness
- from sklearn.linear_model import LinearRegression
- # %% [markdown]
- # Configurations for outlier detection, ambigous clusters and random seed
- # %%
- OUTLIER_Q = 0.997 # robust cutoff on squared Mahalanobis distances
- AMBIG_THRESH = 0.8 # ambiguous if max(prob) < 0.8
- RANDOM_STATE = 42
- # %% [markdown]
- # ***
- # # 1. Load In-scanner Experience Data (SNYCQ)
- #
- # We load this data only for the scans that have passed our QA for the imaging data
- # %%
- SBJs, SCANs, SNYCQ_wVigilance = get_sbj_scan_list(when='post_motion', return_snycq=True)
- SNYCQ = SNYCQ_wVigilance.drop('Vigilance',axis=1)
- Nscans, Nquestions = SNYCQ.shape
- print(SNYCQ.shape)
- SNYCQ_items = SNYCQ.columns
- # %% [markdown]
- # ***
- # # 2. Data Scaling
- # Answers to all questions, except wakefulness, were scaled using scikit-learn ```RobustScaler```. This scaler object removes the median and scales the data by the inter-quantile range. This form of scaling was performed to avoid excessive influence of outliers in the scaling process.
- # %%
- X_raw_df = SNYCQ[SNYCQ_items].replace([np.inf, -np.inf], np.nan).dropna(axis=0)
- idx = X_raw_df.index
- X_scaled = RobustScaler().fit_transform(X_raw_df.values)
- X_scaled_df = pd.DataFrame(X_scaled,index=idx, columns=X_raw_df.columns)
- # %% [markdown]
- # We look at the distributions of the sNYCQ data before and after scaling
- # %%
- plot_dist = X_raw_df.hvplot.hist(title='SNYC-Q pre scaling') + X_scaled_df.hvplot.hist(title='SNYC-Q post scaling', shared_axes=False)
- hv.save(plot_dist, osp.join('figures', 'FigureXX-SNYCQ_histograms_pre_post_scaling.html'))
- plot_dist
- # %% [markdown]
- # 
- # %% [markdown]
- # ***
- # # 3. Outlier Detection
- #
- # We relied on the Mahalanobis distance of each scan to the sample's mean in order to detect outlier scans. We set the threshold to the top 3% quantile
- # %%
- mcd = MinCovDet().fit(X_scaled_df.values)
- # squared Mahalanobis distances for the training set
- md2 = mcd.mahalanobis(X_scaled_df.values) if hasattr(mcd, "mahalanobis") else mcd.dist_
- # Threshold
- thr = np.quantile(md2, OUTLIER_Q)
- keep = md2 < thr
- # %%
- md2_df = pd.DataFrame(md2, index=idx)
- md2_df.hvplot(hover_cols=['Subject','Run'], title='Mahalanobis ditance',ylabel='Mahalanobis distance') *hv.HLine(thr).opts(line_width=0.5, line_dash='dashed', line_color='k')
- # %% [markdown]
- # Now that we have identified two outlier scans, we create a new version of the data where those scans have been removed (e.g., ```X_$$$$_kept```)
- # %%
- idx_kept = idx[keep]
- X_scaled_kept_df = X_scaled_df.loc[idx_kept] # Scaled data for scans not marked as outliers
- X_raw_kept_df = X_raw_df.loc[idx_kept] # Original data for scans not marked as outliers
- print(f"[Outliers] Flagged {(~keep).sum()} / {len(md2)}; keeping {keep.sum()}")
- # %% [markdown]
- # # 4. Correlation between SNYCQ items
- #
- # To explore the structure of in-scanner experience reports, we first computed the Pearson’s correlation between the 11 in-scanner experience items.
- # %%
- # Compute correlation matrix
- X_raw_kept_corr_df = X_raw_kept_df.corr()
- # %%
- # Estimate P-value matrix (Pearson)
- cols = X_raw_kept_df.columns
- pval_df = pd.DataFrame(np.zeros((len(cols), len(cols))),
- index=cols, columns=cols)
- for i, c1 in enumerate(cols):
- for j, c2 in enumerate(cols):
- if i <= j:
- r, p = pearsonr(X_raw_kept_df[c1], X_raw_kept_df[c2])
- pval_df.loc[c1, c2] = p
- pval_df.loc[c2, c1] = p
- # %%
- # Get clustering order from seaborn
- clustergrid = sns.clustermap(X_raw_kept_corr_df)
- plt.close()
- row_order = clustergrid.dendrogram_row.reordered_ind
- ordered = X_raw_kept_corr_df.index[row_order]
- corr_ord = X_raw_kept_corr_df.round(2).loc[ordered, ordered]
- pval_ord = pval_df.loc[ordered, ordered]
- # %%
- # Make sure names exist (used by show_results)
- corr_ord.index.name = 'index'
- corr_ord.columns.name = 'col'
- corr_ord.name = 'corr'
- pval_ord.index.name = 'index'
- pval_ord.columns.name = 'col'
- pval_ord.name = 'pval'
- # %%
- # Calcualte the number of unique entries to do Bonferroni correction
- n_comps = corr_ord.shape[0]*(corr_ord.shape[0]-1)/2
- # %%
- # Plot: correlation heatmap with bold black outline for pBonf < 0.05
- plot = show_correlations_with_statistics(
- data_val=corr_ord,
- data_pval=pval_ord,
- pval_thr=0.05/n_comps,
- clabel='Pearson correlation',
- height=700,
- width=800,
- cmap='RdBu_r',
- fontscale=1.5,
- clim=(-0.7, 0.7)
- )
- # %% [markdown]
- # 
- #
- # ### Saving Publication ready format data and figure panel
- #
- # We will save now the panel as HTML in a way that we can then export to PDF via the "Print..." option of the browser
- # %%
- import holoviews as hv
- from bokeh.io import save
- from bokeh.models.plots import Plot
- from bokeh.resources import INLINE
- hv.extension("bokeh")
- def svg_backend(hv_plot, element):
- hv_plot.state.output_backend = "svg"
- svg_plot = plot.opts(hooks=[svg_backend])
- bokeh_obj = hv.render(svg_plot, backend="bokeh")
- # Extra safety for layouts / overlays
- if isinstance(bokeh_obj, Plot):
- bokeh_obj.output_backend = "svg"
- for p in bokeh_obj.select({"type": Plot}):
- p.output_backend = "svg"
- save(
- bokeh_obj,
- filename="./figures/Figure01_B-SNYCQcorr.html",
- resources=INLINE,
- title="Figure01_B",
- )
- # %%
- corr_ord.to_csv('./source_data_files/figure_01_b.csv',float_format='%.2f')
- print('Saving source data for Figure 01B: SNYCQ correlation matrix with significant correlations outlined in black [./source_data_files/figure_01_b.csv]')
- # %% [markdown]
- # ***
- # # 4. Dimensionality Reduction with T-SNE
- # ## 4.1. Hyper-parameter Optimization
- # Two key hyper-parameters of the T-SNE algorithm are dimensionality and perplexity. We relied on [trustworthiness](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.trustworthiness.html)—an estimate of how well a given embedding preserves local distances—to select these two hyper-parameters on a data-driven manner. The explored hyper-parameter space was:
- #
- # | Hyper-parameter | Space Explored |
- # |:----------------|:---------------|
- # |dimensionality| {1,2,3}|
- # | perplexity | {5,9,10,13,15,18,20,30,40,50} |
- # %%
- def choose_tsne_perplexity(X, random_state=RANDOM_STATE,n_components=[1,2,3]):
- n = X.shape[0]
- # Theoretical upper bound: perplexity < (n - 1); practical upper ~ n/3
- max_perp = max(5, min(60, (n - 1) // 3))
- base = min(max_perp, max(5, int(round(n / 50)))) # ~ n/50 baseline, clamped [5, 60]
- # Candidate set around 'base' plus some classics
- cands = sorted(set([
- 5, 10, 15, 20, 30, 40, 50,
- base, int(base*1.5), int(base*2)
- ]))
- cands = [p for p in cands if 5 <= p <= max_perp and p < (n - 1)]
- scores = []
- for nc in tqdm(n_components, position=0, desc='Num Components'):
- for p in tqdm(cands, position=1, desc='Perplexity', leave=False):
- tsne_tmp = TSNE(n_components=nc, perplexity=p, learning_rate='auto', init='pca',
- n_iter_without_progress=600, early_exaggeration=12.0, random_state=random_state)
- Ytmp = tsne_tmp.fit_transform(X)
- tw = trustworthiness(X, Ytmp, n_neighbors=10, metric='euclidean')
- scores.append((nc,p, tw))
- # pick the perplexity with max trustworthiness; break ties by preferring mid-range
- scores.sort(key=lambda t: (-t[2], abs(t[0] - 30)))
- return scores[0][0], scores[0][1], pd.DataFrame(scores, columns=["num_components","perplexity", "trustworthiness"])
- best_nc, best_perp, tw_table = choose_tsne_perplexity(X_scaled_kept_df.values, RANDOM_STATE)
- # %% [markdown]
- # Now we show what were the values that maximized the trustworthiness of the T-SNE embeddings
- # %%
- print(f"[t-SNE tuning] Selected perplexity = {best_perp}")
- print(f"[t-SNE tuning] Selected dimensionaity = {best_nc}")
- print(f"[t-SNE tuning] Trustworthiness = {tw_table['trustworthiness'].max()}")
- # %% [markdown]
- # ## 4.2. Compute T-SNE with optimal dimensionality and perplexity
- # %%
- tsne = TSNE(
- n_components=best_nc, perplexity=best_perp, learning_rate='auto', init='pca',
- n_iter_without_progress=1000, early_exaggeration=12.0, random_state=RANDOM_STATE
- )
- Y = tsne.fit_transform(X_scaled_kept_df.values)
- if best_nc == 2:
- emb = pd.DataFrame(Y, index=idx_kept, columns=["TSNE1", "TSNE2"])
- else:
- emb = pd.DataFrame(Y, index=idx_kept, columns=["TSNE1", "TSNE2","TSNE3"])
- # %% [markdown]
- # ## 4.3. Compute Biplot Arrows inidicating directions of maximal variance for each SNYCQ item
- #
- # To aid with the interpretation of how T-SNE dimensions relate to the original items in the SNYC survey, we modeled each of the SNYCQ items as a linear function of the three T-SNE coordinates. We then used these coefficients to draw biplot arrows depicting the directions of maximal change for each SNYCQ item in the 3D T-NSE embedding space.
- # %%
- def compute_biplot_arrows(emb_df, features_df, feature_names):
- """
- For each feature f: fit f ~ a*X + b*Y on the 2D embedding; return direction (a,b) and R^2.
- features_df should be standardized/robust-scaled (we use Xr_kept).
- """
- Xy = emb_df.values # shape (n,2)
- n_components = Xy.shape[1]
- arrows = []
- reg = LinearRegression()
- for j, f in enumerate(feature_names):
- y = features_df.iloc[:, j].values
- reg.fit(Xy, y)
- if n_components == 2:
- a, b = reg.coef_
- r2 = reg.score(Xy, y)
- arrows.append((f, a, b, r2))
- elif n_components == 3:
- a,b,c = reg.coef_
- r2 = reg.score(Xy,y)
- arrows.append((f,a,b,c,r2))
- else:
- print('++ ERROR: This function only works with 2D and 3D embeddings')
- return None
- if n_components==2:
- arrows_df = pd.DataFrame(arrows, columns=["feature", "beta_x", "beta_y", "R2"]).sort_values("R2", ascending=False)
- elif n_components == 3:
- arrows_df = pd.DataFrame(arrows, columns=["feature", "beta_x", "beta_y", "beta_z","R2"]).sort_values("R2", ascending=False)
- return arrows_df
- # %%
- tsne_arrows = compute_biplot_arrows(emb,X_raw_kept_df,SNYCQ_items)
- # Scaling so that they are clearly visible in the plot
- tsne_arrows['beta_x'] = tsne_arrows['beta_x'] * 5
- tsne_arrows['beta_y'] = tsne_arrows['beta_y'] * 5
- tsne_arrows['beta_z'] = tsne_arrows['beta_z'] * 5
- # %% [markdown]
- # ## 4.4. Plot the T-SNE embedding colored by one SNYCQ item
- # %%
- # Create a new DF with both th original values and the dimensions in T-SNE (for plotting purposes)
- emb_plus = pd.concat([emb, X_raw_kept_df], axis=1)
- # %%
- tsne_camera_object = dict(
- center=dict(x=6.661338147750939e-16, y=-1.942890293094024e-16, z=8.881784197001252e-16),
- eye=dict(x=0.4505864572659788, y=1.5176226342505497, z=-1.6428294069570146),
- up=dict(x=-0.7264163680411282, y=0.05191238226958195, z=0.6852914451596732)
- )
- # Create the scatter trace
- scatter_trace = go.Scatter3d(
- x=emb_plus['TSNE1'],
- y=emb_plus['TSNE2'],
- z=emb_plus['TSNE3'],
- mode='markers',
- marker=dict(
- size=5, # Fixed small size
- color=emb_plus['People'], # Color mapped to 'People'
- colorscale='viridis', # Viridis colormap
- opacity=0.8,
- colorbar=dict(title='People') # Optional colorbar
- ),
- text=[
- "<br>".join(f"{col}: {row[col]}" for col in emb_plus.columns if col not in ['TSNE1', 'TSNE2', 'TSNE3'])
- for _, row in emb_plus.iterrows()
- ],
- hoverinfo='text'
- )
- # === 2. Arrows from (0, 0, 0) to (beta_x, beta_y, beta_z) ===
- arrow_traces = []
- for i, row in tsne_arrows.iterrows():
- # Line (arrow body)
- if row['feature'] == 'People':
- arrow_color = 'red'
- else:
- arrow_color = 'black'
- arrow_trace = go.Scatter3d(
- x=[0, row['beta_x']],
- y=[0, row['beta_y']],
- z=[0, row['beta_z']],
- mode='lines',
- line=dict(
- color=arrow_color,
- width=max(1, row['R2'] * 10), # Scale width by R2
- ),
- showlegend=False
- )
- # Text label at arrow tip
- label_trace = go.Scatter3d(
- x=[row['beta_x']],
- y=[row['beta_y']],
- z=[row['beta_z']],
- mode='text',
- text=[row['feature']],
- textposition='top center',
- showlegend=False
- )
- arrow_traces.extend([arrow_trace, label_trace])
- # === 3. Combine all traces ===
- fig = go.Figure(data=[scatter_trace] + arrow_traces)
- # === 4. Update layout ===
- fig.update_layout(
- scene=dict(
- xaxis_title='TSNE1',
- yaxis_title='TSNE2',
- zaxis_title='TSNE3',
- aspectmode='data',
- camera=tsne_camera_object
- ),
- title='3D t-SNE Plot with Feature Arrows',
- margin=dict(l=0, r=0, b=0, t=40),
- height=700,
- showlegend=False
- )
- fig.show()
- # %%
- fig.write_html(osp.join('figures', 'FigureXX-SNYCQtsne_colorby_People.html'))
- # %% [markdown]
- # 
- # %% [markdown]
- # ***
- # # 5. Clustering Analyses
- #
- # ## 5.1 Apply K-means and Gaussian Mixture Modeling to the data (K=2)
- #
- # To separate scans into two sets with well-differentiated inner-experience, we decided to apply two different clustering algorithms to the in-experience data (11 questions) following robust scaling.
- #
- # >NOTE: To be clear, clustering was performed in the original 11D space, not the low dimensional space generated by T-SNE.
- #
- # Working with two different clustering methods allows to evaluate the robustness of results against clustering technique.
- #
- # The two selected methods were [K-Means](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html) and [Gaussian Mixture Modeling](https://scikit-learn.org/stable/modules/generated/sklearn.mixture.GaussianMixture.html); both as implemented in the python library scikit-learn.
- #
- # K-Means was chosen as a representative hard-clustering method often used in the neuroimaging literature.
- #
- # Gaussian Mixture Modeling was chosen because of its soft-clustering nature (i.e., it provides membership probabilities) and because it allows non-spherical clusters. In both instances, we set k=2.
- #
- # The next cell computes the GMM clustering for K = 2
- # %%
- K = 2
- gm = GaussianMixture(n_components=K, covariance_type="spherical", random_state=RANDOM_STATE).fit(X_scaled_kept_df.values)
- proba = gm.predict_proba(X_scaled_kept_df.values)
- labels_gmm = proba.argmax(axis=1)
- # %% [markdown]
- # Now we do the same using KMeans for comparison
- # %%
- km = KMeans(n_clusters=K, random_state=RANDOM_STATE, n_init=10).fit(X_scaled_kept_df.values)
- labels_km = km.predict(X_scaled_kept_df.values)
- # %% [markdown]
- # ## 5.2. Cluster method comparison with ARI and Silhouette Index
- #
- # We now check for the consistency of the clustering results across both methods using the Asjusted Rand Index (ARI), and also for their quality, separately, using the Silhouette Index (SI)
- #
- # 1. Compute the SI for each clustering result
- # %%
- sil_gmm = silhouette_score(X_scaled_kept_df.values, labels_gmm)
- sil_km = silhouette_score(X_scaled_kept_df.values, labels_km)
- # %% [markdown]
- # 2. Compute the ARI comparing both methods
- # %%
- ari_km_gmm = adjusted_rand_score(labels_km, labels_gmm)
- # %% [markdown]
- # 3. Print the computed statistics
- # %%
- print(f"[GMM k=2 sph] silhouette={sil_gmm:.3f}")
- print(f"[K-M k=2 ] silhouette={sil_km:.3f}")
- print(f"[Agreement] ARI(KMeans vs GMM) = {ari_km_gmm:.3f}")
- print(f"[Sizes] GMM clusters: {np.bincount(labels_gmm)}")
- # %% [markdown]
- # Based on the SI, we decided to move forward with the GMM solustion, which we explore in further detail.
- # %% [markdown]
- # ## 5.3. Bootstraing Analysis for GMM
- # %%
- def subsample_labels(model, X, frac=0.8, seed=0):
- rng = np.random.RandomState(seed)
- n = X.shape[0]
- take = np.sort(rng.choice(n, int(frac*n), replace=False)) # sort for convenience
- # clone model with same hyperparams
- m2 = type(model)(**model.get_params())
- if hasattr(m2, "random_state"):
- m2.random_state = seed
- # (KMeans has fit_predict; GMM needs fit + predict)
- if hasattr(m2, "fit_predict"):
- labels = m2.fit_predict(X[take])
- else:
- m2.fit(X[take]); labels = m2.predict(X[take])
- return take, labels
- def bootstrap_ari(model, X, n_runs=20, frac=0.8, base_seed=0):
- aris = []
- for s in range(n_runs):
- i1, l1 = subsample_labels(model, X, frac=frac, seed=base_seed + 2*s)
- i2, l2 = subsample_labels(model, X, frac=frac, seed=base_seed + 2*s + 1)
- # align to the same samples, same order
- common = np.intersect1d(i1, i2)
- # positions of the common indices in each subsample
- pos1 = np.searchsorted(i1, common)
- pos2 = np.searchsorted(i2, common)
- aris.append(adjusted_rand_score(l1[pos1], l2[pos2]))
- return float(np.mean(aris)), float(np.std(aris))
- gm_mean_ari, gm_sd_ari = bootstrap_ari(gm, X_scaled_kept_df.values, n_runs=20, frac=0.8, base_seed=100)
- print("Bootstrap ARI (GMM) mean±sd: %.2f +/- %.2f" %(gm_mean_ari, gm_sd_ari))
- # %% [markdown]
- # ***
- # ## 5.4. Detection of scans with ambigous cluster membership
- #
- # One additional bonus of GMM over K-means is that GMM not being a hard clustering algorithm, it outputs cluster membershup probabilities. We will use these to detect scans with ambiguous membership. Such scans will not be included in the population differences analyses later on.
- # %%
- proba_df = pd.DataFrame(proba, index=idx_kept, columns=['P(c1)','P(c2)'])
- proba_df.hvplot.hist('P(c1)', title='Probability Distribution for Membership in Cluster 1')
- # %%
- # store probabilities & an ambiguity flag
- p1 = proba[:, 1]
- ambiguity = np.maximum(1 - p1, p1) < (AMBIG_THRESH)
- # %% [markdown]
- # Save final cluster/sets labels in a new pandas dataframe: ```group_info_df```
- # %%
- # Store that information with clear labels in a pandas Dataframe
- group_info_df = pd.DataFrame(index=idx_kept, columns=['Set Label', 'Group Probability'])
- for i,scan in enumerate(idx_kept):
- if ambiguity[i]:
- group_info_df.loc[scan,"Set Label"] = "Ambiguous"
- group_info_df.loc[scan,"Group Probability"] = np.max([p1[i],1-p1[i]])
- else:
- if p1[i] > 1 - p1[i]:
- group_info_df.loc[scan,"Set Label"] = "Set B"
- group_info_df.loc[scan,"Group Probability"] = p1[i]
- else:
- group_info_df.loc[scan,"Set Label"] = "Set A"
- group_info_df.loc[scan,"Group Probability"] = 1 - p1[i]
- group_info_df['Set Label'].value_counts()
- # %% [markdown]
- # Add cluster membership information to the TSNE embedding information, and save two versions to disk: one with the original SNYCQ items, one with their scaled values.
- # %%
- emb_plus = pd.concat([emb_plus, group_info_df], axis=1)
- emb_plus.to_csv(osp.join(RESOURCES_SNYCQ_DIR, 'SNYCQ_tsne_embeddings_plus.csv'))
- print('++ INFO: Saved t-SNE embeddings with original features and group info to CSV: %s' % osp.join(RESOURCES_SNYCQ_DIR, 'SNYCQ_tsne_embeddings_plus.csv'))
- emb_plus_scaled = pd.concat([emb, X_scaled_kept_df, group_info_df], axis=1)
- emb_plus_scaled.to_csv(osp.join(RESOURCES_SNYCQ_DIR, 'SNYCQ_tsne_embeddings_plus_scaled.csv'))
- print('++ INFO: Saved t-SNE embeddings with scaled features and group info to CSV: %s' % osp.join(RESOURCES_SNYCQ_DIR, 'SNYCQ_tsne_embeddings_plus_scaled.csv'))
- # %% [markdown]
- # ## 5.5. Plot the T-SNE embedding again, but this time with scans colored according to set membership
- # %%
- tsne_camera_object = dict(
- center=dict(x=0.0, y=0.0, z=0.0),
- eye=dict(x=1.25, y=-1.25, z=1.25),
- up=dict(x=0.0, y=0.0, z=0.0)
- )
- scatter_traces = []
- group_colors = {
- "Set A": "#1f77b4", # light blue
- "Set B": "#ff7f0e", # orange
- "Ambiguous": "#ffffff" # white fill
- }
- for group, color in group_colors.items():
- group_data = emb_plus[emb_plus["Set Label"] == group]
- scatter_traces.append(
- go.Scatter3d(
- x=group_data["TSNE1"],
- y=group_data["TSNE2"],
- z=group_data["TSNE3"],
- mode='markers',
- name=group,
- marker=dict(
- size=5,
- color=color,
- opacity=0.9,
- line=dict(
- color='black' if group == "Ambiguous" else color,
- width=2 if group == "Ambiguous" else 0
- )
- ),
- text=[
- "<br>".join(
- f"{col}: {row[col]}"
- for col in emb_plus.columns if col not in ['TSNE1', 'TSNE2', 'TSNE3']
- )
- for _, row in group_data.iterrows()
- ],
- hoverinfo='text'
- )
- )
- # === 3. Combine all traces ===
- fig = go.Figure(data=scatter_traces + arrow_traces)
- fig.update_layout(
- scene=dict(
- xaxis_title='TSNE1',
- yaxis_title='TSNE2',
- zaxis_title='TSNE3',
- aspectmode='data',
- camera=tsne_camera_object
- ),
- legend=dict(
- title='Group:',
- itemsizing='constant',
- x=0.55, # move horizontally (1.0 is far right, 0.5 is middle)
- y=0.9, # move vertically (1.0 is top)
- xanchor='left',
- yanchor='top',
- bgcolor='rgba(255,255,255,0.7)', # optional background for readability
- bordercolor='rgba(0,0,0,0.2)',
- borderwidth=1
- ),
- title='3D t-SNE Plot with Feature Arrows',
- margin=dict(l=0, r=0, b=0, t=40),
- height=700,
- showlegend=True
- )
- fig.show()
- # %%
- fig.write_html(osp.join('figures', 'Figure01_K_SNYCQtsneWclusters.html'))
- # %% [markdown]
- # 
- # %% [markdown]
- # ### Save Publication Ready Figure Panel and Data source
- # %%
- fig.write_image(osp.join('figures', 'Figure01_K_SNYCQtsneWclusters.svg'))
- # %%
- emb_plus[['TSNE1', 'TSNE2', 'TSNE3','Set Label']].to_csv('./source_data_files/figure_01_k.csv', float_format='%.3f', index=False)
- # %% [markdown]
- # ***
- # ## 6. Explore how Sets A and B differ in terms of SNYCQ items, vigilance, head motion and basic demographics
- #
- # We will seek for potential differences across groups in the following variables:
- #
- # * All entries in the SNYCQ, including wakefulness
- # * Mean head motion
- # * Age distribution
- # * Gender distribution
- #
- # We will do this using Cohen's d, MannWhitney tests and Wilconxon tests
- # %% [markdown]
- # ## 6.1. Examination of differences in age distribution
- # %%
- # Load Demographic Data
- demographics = pd.read_csv(ORIG_DEMO_PATH, index_col=0,sep='\t')
- demographics = demographics.loc[list(SBJs)]
- # Load Demographic Data
- #demographics = pd.read_csv(osp.join(RESOURCES_SNYCQ_DIR,'participants_post_motion_QA.csv'), index_col=0)
- # Extract information about age
- age_per_scan = pd.DataFrame(index=idx_kept, columns=['Set Label','Age (5-year bins)'])
- for sbj,run in tqdm(idx_kept):
- age_range = demographics.loc[sbj,'age (5-year bins)']
- group_label = emb_plus.loc[(sbj,run),'Set Label']
- age_per_scan.loc[(sbj,run),'Age (5-year bins)'] = age_range
- age_per_scan.loc[(sbj,run),'Set Label'] = group_label
- # Remove entries for ambiguous scans
- age_per_scan = age_per_scan[age_per_scan['Set Label']!='Ambiguous']
- # Get counts of scas in each age range
- age_counts_per_group = age_per_scan.groupby('Set Label').value_counts()
- age_counts_per_group = age_counts_per_group.infer_objects()
- # Prepare Dataframe for plotting with hvplot
- age_counts_per_group = age_counts_per_group.reset_index()
- age_counts_per_group.columns = ['Group','Age Range','# Scans']
- age_counts_per_group.replace({'Set A':'A', 'Set B':'B'}, inplace=True)
- age_counts_per_group['color'] = '#ffffff'
- age_counts_per_group.loc[age_counts_per_group['Group']=='A','color'] = group_colors['Set A']
- age_counts_per_group.loc[age_counts_per_group['Group']=='B','color'] = group_colors['Set B']
- age_counts_per_group = age_counts_per_group.infer_objects()
- age_counts_per_group = age_counts_per_group.sort_values(by='Age Range', ascending=True)
- # %%
- A = age_counts_per_group.set_index(['Group','Age Range']).loc['A',:]['# Scans']
- B = age_counts_per_group.set_index(['Group','Age Range']).loc['B',:]['# Scans']
- W, w_p = wilcoxon(A,B, alternative='two-sided', method='exact')
- print('++ AGE ACROSS SETS: Wilcoxon = %.2f (p = %.2f)' % (W,w_p))
- # %%
- # Generate graph that will get later added to a Grid with information about all variables
- age_bar_plot = age_counts_per_group.hvplot.bar(x='Age Range',by='Group', alpha=0.5, xlabel='Age',cmap=["#1f77b4","#ff7f0e"]).opts(toolbar=None, xrotation=90, width=250, height=200, fontscale=1)
- # %% [markdown]
- # 
- # %% [markdown]
- # ### Save publication ready and source data for Figure 01I
- # %%
- age_counts_per_group.to_csv('./source_data_files/figure_01_i.csv', index=False)
- # %%
- svg_plot = age_bar_plot.opts(hooks=[svg_backend])
- bokeh_obj = hv.render(svg_plot, backend="bokeh")
- # Extra safety for layouts / overlays
- if isinstance(bokeh_obj, Plot):
- bokeh_obj.output_backend = "svg"
- for p in bokeh_obj.select({"type": Plot}):
- p.output_backend = "svg"
- save(
- bokeh_obj,
- filename="./figures/Figure01_I-AgePerSet.html",
- resources=INLINE,
- title="Figure01_I",
- )
- # %% [markdown]
- # ## 6.2. Examination of differneces in gender distribution
- # %%
- # Extract information about age
- sex_per_scan = pd.DataFrame(index=idx_kept, columns=['Set Label','Sex'])
- for sbj,run in tqdm(idx_kept):
- sex = demographics.loc[sbj,'gender']
- if sex == 'M':
- sex = 'Male'
- else:
- sex = 'Female'
- group_label = emb_plus.loc[(sbj,run),'Set Label']
- sex_per_scan.loc[(sbj,run),'Sex'] = sex
- sex_per_scan.loc[(sbj,run),'Set Label'] = group_label
- # Remove entries for ambiguous scans
- sex_per_scan = sex_per_scan[sex_per_scan['Set Label']!='Ambiguous']
- # Get counts of scas in each age range
- sex_counts_per_group = sex_per_scan.groupby('Set Label').value_counts()
- sex_counts_per_group = sex_counts_per_group.infer_objects()
- sex_counts_per_group
- # %%
- sex_bar_plot = sex_counts_per_group.hvplot.bar(stacked=True, xlabel='', legend='top_left', title='', ylabel='# Scans', color=['white','gray']).opts(toolbar=None, width=250, height=200, fontscale=1)
- #hv.save(sex_bar_plot, osp.join('figures', 'Figure01_J-SexPerSet.html'))
- # %% [markdown]
- # 
- #
- # ### Save Publication Ready and Data Source
- # %%
- svg_plot = sex_bar_plot.opts(hooks=[svg_backend])
- bokeh_obj = hv.render(svg_plot, backend="bokeh")
- # Extra safety for layouts / overlays
- if isinstance(bokeh_obj, Plot):
- bokeh_obj.output_backend = "svg"
- for p in bokeh_obj.select({"type": Plot}):
- p.output_backend = "svg"
- save(
- bokeh_obj,
- filename="./figures/Figure01_J-SexPerSet.html",
- resources=INLINE,
- title="Figure01_J",
- )
- # %%
- sex_counts_per_group.to_csv('./source_data_files/figure_01_j.csv')
- # %% [markdown]
- # ## 6.3. Examination of diffrences in head motion
- # %%
- # Load motion information for each scan
- mot_info = pd.read_csv(osp.join(RESOURCES_DINFO_DIR,'motion_confounds.csv'),index_col=['Subject','Run'])
- scans_in_A = emb_plus[emb_plus['Set Label'] == 'Set A'].index
- scans_in_B = emb_plus[emb_plus['Set Label'] == 'Set B'].index
- mot_A = mot_info.loc[scans_in_A,'Mean Rel Motion'].values
- mot_B = mot_info.loc[scans_in_B,'Mean Rel Motion'].values
- U, u_p = mannwhitneyu(mot_A,mot_B,alternative='two-sided')
- print('++ AGE ACROSS SETS: Mann-Whiteney U = %.2f (p = %.2f)' % (U,u_p))
- # %%
- mot_A = mot_info.loc[scans_in_A,'Mean Rel Motion']
- mot_B = mot_info.loc[scans_in_B,'Mean Rel Motion']
- overlay = mot_A.hvplot.hist(label='Set A', c=group_colors['Set A'], title='', width=250, height=200, alpha=0.5, shared_axes=False, bins=20, normed=True).opts(toolbar=None) * \
- mot_B.hvplot.hist(label='Set B', c=group_colors['Set B'], alpha=0.5, shared_axes=False, bins=20, normed=True) * \
- mot_A.hvplot.kde(label='Set A', c=group_colors['Set A'], alpha=0.5, shared_axes=False) * \
- mot_B.hvplot.kde(label='Set B', c=group_colors['Set B'], alpha=0.5, shared_axes=False)
- # %% [markdown]
- # 
- #
- # ### Save Publication ready figure and Source Data
- # %%
- svg_plot = overlay.opts(hooks=[svg_backend])
- bokeh_obj = hv.render(svg_plot, backend="bokeh")
- # Extra safety for layouts / overlays
- if isinstance(bokeh_obj, Plot):
- bokeh_obj.output_backend = "svg"
- for p in bokeh_obj.select({"type": Plot}):
- p.output_backend = "svg"
- save(
- bokeh_obj,
- filename="./figures/Figure01_H-MotionPerSet.html",
- resources=INLINE,
- title="Figure01_H",
- )
- # %%
- pd.concat([mot_A.reset_index(drop=True), mot_B.reset_index(drop=True)], axis=1, keys=['Set A','Set B']).to_csv('./source_data_files/figure_01_h.csv', index=False)
- # %% [markdown]
- # ## 6.4 Examination of Vigilance
- # %%
- vigilance = SNYCQ_wVigilance['Vigilance']
- vigilance.name = 'Wakefulness'
- scans_in_A = emb_plus[emb_plus['Set Label'] == 'Set A'].index
- scans_in_B = emb_plus[emb_plus['Set Label'] == 'Set B'].index
- vigilance_A = vigilance.loc[scans_in_A].values
- vigilance_B = vigilance.loc[scans_in_B].values
- U, u_p = mannwhitneyu(vigilance_A,vigilance_B,alternative='two-sided')
- print('++ VIGILANCE ACROSS SETS: Mann-Whiteney U = %.2f (p = %.2f)' % (U,u_p))
- # %%
- vigilance_A = vigilance.loc[scans_in_A]
- vigilance_B = vigilance.loc[scans_in_B]
- overlay = vigilance_A.hvplot.hist(label='Set A', c=group_colors['Set A'], title='', width=250, height=200, alpha=0.5, shared_axes=False, bins=[0,10,20,30,40,50,60,70,80,90,100], normed=True, legend=False).opts(toolbar=None) * \
- vigilance_B.hvplot.hist(label='Set B', c=group_colors['Set B'], alpha=0.5, shared_axes=False, bins=[0,10,20,30,40,50,60,70,80,90,100], normed=True) * \
- vigilance_A.hvplot.kde(label='Set A', c=group_colors['Set A'], alpha=0.5, shared_axes=False) * \
- vigilance_B.hvplot.kde(label='Set B', c=group_colors['Set B'], alpha=0.5, shared_axes=False)
- #hv.save(overlay, osp.join('figures', 'Figure01_G-VigilancePerSet.html'))
- # %% [markdown]
- # 
- # ### Save Publication Ready Panel and Source Data
- # %%
- svg_plot = overlay.opts(hooks=[svg_backend])
- bokeh_obj = hv.render(svg_plot, backend="bokeh")
- # Extra safety for layouts / overlays
- if isinstance(bokeh_obj, Plot):
- bokeh_obj.output_backend = "svg"
- for p in bokeh_obj.select({"type": Plot}):
- p.output_backend = "svg"
- save(
- bokeh_obj,
- filename="./figures/Figure01_G-VigilancePerSet.html",
- resources=INLINE,
- title="Figure01_G",
- )
- # %%
- pd.concat([vigilance_A.reset_index(drop=True), vigilance_B.reset_index(drop=True)], axis=1, keys=['Set A','Set B']).to_csv('./source_data_files/figure_01_g.csv', index=False)
- # %% [markdown]
- # ## 6.5. Examination of differences in SNYCQ items
- # %%
- def cohens_d(x0, x1):
- m0, m1 = np.nanmean(x0), np.nanmean(x1)
- s0, s1 = np.nanstd(x0, ddof=1), np.nanstd(x1, ddof=1)
- n0, n1 = np.sum(~np.isnan(x0)), np.sum(~np.isnan(x1))
- sp = np.sqrt(((n0-1)*s0**2 + (n1-1)*s1**2) / (n0 + n1 - 2))
- return (m1 - m0) / sp if sp > 0 else np.nan
- def calculate_stats_per_set(df, items, label_col='Set Label', method="bootstrap",
- B=2000, # bootstrap reps
- ci=95,random_state=123):
- rng = np.random.default_rng(random_state)
- alpha_low = (100 - ci) / 2.0
- alpha_high = 100 - alpha_low
- out = []
- for f in items:
- x0 = df.loc[df[label_col] == "Set A", f].dropna().values
- x1 = df.loc[df[label_col] == "Set B", f].dropna().values
- m0 = float(np.mean(x0)) if x0.size else np.nan
- m1 = float(np.mean(x1)) if x1.size else np.nan
- d = cohens_d(x0,x1)
- u, p = mannwhitneyu(x0,x1,alternative='two-sided')
- if method == "bootstrap":
- # nonparametric bootstrap of the mean per cluster
- if x0.size >= 2:
- boots0 = [np.mean(x0[rng.integers(0, x0.size, x0.size)]) for _ in range(B)]
- lo0, hi0 = np.percentile(boots0, [alpha_low, alpha_high])
- else:
- lo0 = hi0 = np.nan
- if x1.size >= 2:
- boots1 = [np.mean(x1[rng.integers(0, x1.size, x1.size)]) for _ in range(B)]
- lo1, hi1 = np.percentile(boots1, [alpha_low, alpha_high])
- else:
- lo1 = hi1 = np.nan
- elif method == "analytic":
- # mean ± z * SE; SE = s / sqrt(n)
- z = 1.96 if ci == 95 else None
- if z is None:
- from scipy.stats import norm
- z = float(norm.ppf(0.5 + ci/200.0))
- if x0.size >= 2:
- se0 = float(np.std(x0, ddof=1) / np.sqrt(x0.size))
- lo0, hi0 = m0 - z*se0, m0 + z*se0
- else:
- lo0 = hi0 = np.nan
- if x1.size >= 2:
- se1 = float(np.std(x1, ddof=1) / np.sqrt(x1.size))
- lo1, hi1 = m1 - z*se1, m1 + z*se1
- else:
- lo1 = hi1 = np.nan
- else:
- raise ValueError("method must be 'bootstrap' or 'analytic'.")
- out.append((f, d, u,p, m0, lo0, hi0, m1, lo1, hi1))
- out_df = pd.DataFrame(out, columns=["Item", "d(Set B - Set A)", "MW (U)","MW (p)","Set A (mean)", "lo (Set A)", "hi (Set A)", "Set B (mean)", "lo (Set B)", "hi (Set B)"]).set_index("Item")
- return out_df
- # %%
- non_ambiguous_scans = emb_plus[emb_plus['Set Label']!='Ambiguous'].index
- data_items = SNYCQ_wVigilance.loc[non_ambiguous_scans,:]
- data_labels = emb_plus.loc[non_ambiguous_scans,'Set Label']
- data = pd.concat([data_items,data_labels],axis=1)
- stats_per_set = calculate_stats_per_set(data,[c for c in data_items.columns if c !='Vigilance'],'Set Label')
- table_02 = stats_per_set.sort_values(by="d(Set B - Set A)", ascending=False).round(2)[['Set A (mean)','Set B (mean)','d(Set B - Set A)','MW (U)','MW (p)']]
- table_02
- # %%
- table_02.to_csv('./source_data_files/table_02.csv', float_format='%.2f')
- # %%
- layout = pn.GridBox(ncols=4)
- items_in_descending_d = stats_per_set.sort_values(by="d(Set B - Set A)", ascending=False).index
- set_a_scans = emb_plus[emb_plus['Set Label']=='Set A'].index
- set_b_scans = emb_plus[emb_plus['Set Label']=='Set B'].index
- for item in items_in_descending_d:
- bins = [0,10,20,30,40,50,60,70,80,90,100]
- this_pval = stats_per_set.loc[item,"MW (p)"]
- if this_pval >= 0.01:
- title = "Cohen d=%.2f | n.s." % stats_per_set.loc[item]["d(Set B - Set A)"]
- else:
- title = "Cohen d=%.2f | p < 0.01" % stats_per_set.loc[item]["d(Set B - Set A)"]
- overlay = SNYCQ.loc[set_a_scans,item].hvplot.hist(label='Set A', c=group_colors['Set A'], title=title, width=225, height=200, alpha=0.5, shared_axes=False, bins=bins, normed=True) * \
- SNYCQ.loc[set_b_scans,item].hvplot.hist(label='Set B', c=group_colors['Set B'], alpha=0.5, shared_axes=False, bins=bins, normed=True) * \
- SNYCQ.loc[set_a_scans,item].hvplot.kde(label='Set A', c=group_colors['Set A'], title=title, width=300, height=200, alpha=0.5, shared_axes=False) * \
- SNYCQ.loc[set_b_scans,item].hvplot.kde(label='Set B', c=group_colors['Set B'], alpha=0.5, shared_axes=False)
- overlay = overlay.opts(show_legend=False,shared_axes=False, toolbar=None)
- layout.append(overlay)
- layout.save( osp.join('figures', 'Supplementary_Figure02.html'))
- # %%
- SNYCQ.loc[set_a_scans].to_csv('./source_data_files/suppfig_02_parta.csv',index=False)
- SNYCQ.loc[set_b_scans].to_csv('./source_data_files/suppfig_02_partb.csv',index=False)
- # %% [markdown]
- # 
- # %% [markdown]
- # Provide the same information in more concise manner in the form of a radar plot
- # %%
- def plot_radar_means_with_ci(
- stats_df, # output of cluster_means_ci (indexed by feature)
- order=None, # optional ordering of features
- title="Per-cluster item means (with 95% CI)",
- ci_label="95% CI"
- ):
- """
- Draws radar with Cluster 0 & 1 mean lines and shaded CI bands.
- """
- # order features
- if order is None:
- features = list(stats_df.index)
- else:
- features = list(order)
- stats_df = stats_df.loc[features]
- # angles for axes + close the loop
- angles = np.linspace(0, 2*np.pi, len(features), endpoint=False).tolist()
- angles += angles[:1]
- # extract arrays and close loops
- m0 = stats_df["Set A (mean)"].to_numpy().tolist(); m0 += m0[:1]
- m1 = stats_df["Set B (mean)"].to_numpy().tolist(); m1 += m1[:1]
- lo0 = stats_df["lo (Set A)"].to_numpy().tolist(); lo0 += lo0[:1]
- hi0 = stats_df["hi (Set A)"].to_numpy().tolist(); hi0 += hi0[:1]
- lo1 = stats_df["lo (Set B)"].to_numpy().tolist(); lo1 += lo1[:1]
- hi1 = stats_df["hi (Set B)"].to_numpy().tolist(); hi1 += hi1[:1]
- # limits based on CI envelopes
- all_vals = np.array((lo0 + hi0 + lo1 + hi1), dtype=float)
- finite_vals = all_vals[np.isfinite(all_vals)]
- if finite_vals.size:
- rmin, rmax = float(np.nanmin(finite_vals)), float(np.nanmax(finite_vals))
- else:
- rmin, rmax = 0.0, 1.0
- pad = max(1.0, 0.05 * (rmax - rmin))
- # plot
- fig = plt.figure(figsize=(7, 7))
- ax = plt.subplot(111, polar=True)
- # mean lines
- l0, = ax.plot(angles, m0, linewidth=2, label="Set A")
- l1, = ax.plot(angles, m1, linewidth=2, label="Set B")
- # shaded CI polygons (build as upper path + reversed lower path)
- # cluster 0
- ang_np = np.array(angles)
- poly0_ang = np.concatenate([ang_np, ang_np[::-1]])
- poly0_rad = np.concatenate([np.array(hi0), np.array(lo0)[::-1]])
- s0 = ax.fill(poly0_ang, poly0_rad, alpha=0.15, label=f"Set A {ci_label}")
- # cluster 1
- poly1_ang = np.concatenate([ang_np, ang_np[::-1]])
- poly1_rad = np.concatenate([np.array(hi1), np.array(lo1)[::-1]])
- s1 = ax.fill(poly1_ang, poly1_rad, alpha=0.15, label=f"Set B {ci_label}")
- # axes & labels
- ax.set_xticks(angles[:-1])
- ax.set_xticklabels(features)
- ax.set_ylim(rmin - pad, rmax + pad)
- ax.set_title(title)
- ax.legend(loc="upper right", bbox_to_anchor=(0.1, 1.0))
- plt.tight_layout()
- plt.show()
- return fig
- # %%
- # Plot (keep your preferred order of spokes)
- plot = plot_radar_means_with_ci(stats_per_set, order=list(items_in_descending_d),
- title="SNYCQ per-cluster means with 95% CI")
- # %% [markdown]
- # ### Save Publication Ready Panel and Source Data for the Radar Plot
- # %%
- stats_per_set.to_csv('./source_data_files/figure_01_f.csv', float_format='%.2f')
- # %%
- plot.savefig('./figures/Figure01_F.svg', format='svg', bbox_inches='tight')
- # %% [markdown]
- # ***
- #
- # # Distribution of SNYCQ values (Supplementary Figure 1)
- # %%
- layout = None
- for q in SNYCQ.columns:
- plot = SNYCQ[q].reset_index(drop=True).hvplot.hist(bins=np.linspace(0,100,20), width=250, height=200, normed=True, ylabel='Density', fontsize=12) * SNYCQ[q].reset_index(drop=True).hvplot.kde()
- if layout is None:
- layout = plot
- else:
- layout = layout + plot
- layout = layout.cols(3).opts(toolbar=None)
- hv.save(layout, osp.join('figures', 'Supplementary_Figure01.html'))
- # %% [markdown]
- # 
- # %%
- SNYCQ.to_csv('./source_data_files/suppfig_01.csv',index=False)
- # %% [markdown]
S10_SNYCQ_TSNE_Clustering.ipynb at commit 2aa1bb1, under CC0-1.0 · at the source
Overview
- Section on Functional Imaging Methods, NIMH, NIH,Bethesda, MA USA
- Department of Psychology, Northwestern University,Evanston, IL USA
- Machine Learning Team, NIMH, NIH,Bethesda, MA USA
- Department of Psychology, University of Calgary,Calgary, AL Canada
- Hotchkiss Brain Institute, University of Calgary,Calgary, AL Canada
- Functional MRI Core, NIMH, NIH,Bethesda, MA USA
Abstract
Resting-state fMRI (rsfMRI) scans—acquired in the absence of experimentally controlled stimuli or task demands—are widely used to identify aberrant patterns of functional connectivity (FC) in clinical populations. To minimize interpretational uncertainty, researchers routinely control for across-cohort disparities in age, gender, comorbidities, and head motion. Yet, studies rarely consider the possibility that systematic differences in inner experience (i.e., how subjects think and feel during the scan) directly affect FC measures. Here, using an rsfMRI dataset comprising 469 scans with retrospective experiential annotations, we show that summary descriptors of in-scanner experience are reproducible across visits and subject-specific, consistent with trait-like characteristics. We further show that widespread significant differences in FC are observed between scans that are associated with different reported experiential profiles, and that FC can predict specific experiential dimensions with performance comparable to that reported for demographic, cognitive, and clinical variables. Together, these findings highlight the key role that in-scanner experience should play when interpreting FC in the context of rsfMRI. Given that the available experiential measures are retrospective summaries, these results speak to stable experiential tendencies rather than potential moment-to-moment, state-dependent relationships between ongoing experience and concurrent brain activity.
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 15 matches between paragraphs and lines of code.
nimh-sfim/fc_introspection
2aa1bb1b653111e0486001780837e9734d90eaf3, 1 June 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
47 files
- bash/
S01_NC_run_structural.sh , Shell, 42 lines - bash/
S02_NC_run_func_preproc. , Shell, 52 linessh - bash/
S04_TransformToMNI.pass0 , Shell, 76 lines, 2 matches1.sh - bash/
S04_TransformToMNI.pass0 , Shell, 43 lines2.sh - bash/
S04_TransformToMNI.pass0 , Shell, 67 lines3.sh - bash/
S05_SegmentT1.sh , Shell, 44 lines - bash/
S06_NuissanceRegression. , Shell, 173 lines, 1 matchsh - bash/
S08_ExtractROIts.sh , Shell, 69 lines, 1 match - bash/
S16_cpm_batch.sh , Shell, 53 lines - matlab/
CONN_CPM_BothContrasts_o , MATLAB, 95 linesnBrain.m - matlab/
CONN_NBS_IndividualContr , MATLAB, 75 linesast_onBrain.m - matlab/
NBS2BrainViewer.m , MATLAB, 44 lines - notebooks/
S00_OrganizeData.ipynb , Jupyter, 228 lines - notebooks/
S00_OrganizeData.py , Python, 267 lines - notebooks/
S01_NC_run_structural.Cr , Jupyter, 94 lineseateSwarm.ipynb - notebooks/
S01_NC_run_structural.Cr , Python, 119 lineseateSwarm.py - notebooks/
S02_NC_run_func_preproc. , Jupyter, 84 linesCreateSwarm.ipynb - notebooks/
S02_NC_run_func_preproc. , Python, 110 linesCreateSwarm.py - notebooks/
S03_QA_ExcessiveMotion.i , Jupyter, 115 linespynb - notebooks/
S03_QA_ExcessiveMotion.p , Python, 151 linesy - notebooks/
S04_TransformToMNI.Creat , Jupyter, 152 lineseSwarm.ipynb - notebooks/
S04_TransformToMNI.Creat , Python, 180 lineseSwarm.py - notebooks/
S05_SegmentT1.ipynb , Jupyter, 72 lines - notebooks/
S05_SegmentT1.py , Python, 90 lines - notebooks/
S06_NuissanceRegression. , Jupyter, 82 linesipynb - notebooks/
S06_NuissanceRegression. , Python, 100 linespy - notebooks/
S07_PrepareAtlas.ipynb , Jupyter, 506 lines, 1 match - notebooks/
S07_PrepareAtlas.py , Python, 560 lines - notebooks/
S08_Extract_ROI_ts.ipynb , Jupyter, 85 lines - notebooks/
S08_Extract_ROI_ts.py , Python, 110 lines - notebooks/
S09_Dashboard_ExploreFCp , Jupyter, 125 lineserScan.ipynb - notebooks/
S09_Dashboard_ExploreFCp , Python, 156 lineserScan.py - notebooks/
S10_SNYCQ_TSNE_Clusterin , Jupyter, 1,101 lines, 8 matchesg.ipynb - notebooks/
S10_SNYCQ_TSNE_Clusterin , Python, 1,257 linesg.py - notebooks/
S11_SNYCQ_Consistency_Id , Jupyter, 599 linesentifiability.ipynb - notebooks/
S11_SNYCQ_Consistency_Id , Python, 731 linesentifiability.py - notebooks/
S12_SNYCQ_ICQF.ipynb , Jupyter, 565 lines - notebooks/
S12_SNYCQ_ICQF.py , Python, 684 lines - notebooks/
S13_NBS_prepare.ipynb , Jupyter, 260 lines - notebooks/
S13_NBS_prepare.py , Python, 284 lines - notebooks/
S14_NBS_view.ipynb , Jupyter, 265 lines - notebooks/
S14_NBS_view.py , Python, 321 lines - notebooks/
S15_NBS_Literature_Searc , Jupyter, 62 linesh_Results.ipynb - notebooks/
S15_NBS_Literature_Searc , Python, 97 linesh_Results.py - notebooks/
S16_CPM.Development.ipyn , Jupyter, 538 lines, 2 matchesb - repository limit reached (2,000 files or 30 MB): the rest is at the source (27 files)
- LICENSE, License, 121 lines
- README.md, Text, 16 lines
Code availability
Code used in this study is available at https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 45 scripts, each with its path and the digest of its content;
- 15 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
Datasets cited
- dataverse.harvard.edu/
dataset.xhtml , at dataverse.harvard.edu; found in “Data availability”
Data availability
This study is based on publicly available data as described in Mendes et al35 Imaging data can be accessed at ftp://
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, 8 authors, 3 keywords, 10 MeSH terms, 1 funder, 93 references.
Cite
This paper
Gonzalez-Castillo, J., Spurney, M. A., Lam, K. C., Gephart, I. S., Pereira, F., Handwerker, D. A., Kam, J. W. Y., & Bandettini, P. A. (2026). In-scanner thoughts contribute to resting-state functional connectivity. Nature communications, 17(1), 8058. https://
BibTeX
@article{gonzalezcastill
author = {Gonzalez-Castillo, Javier and Spurney, Megan A. and Lam, Ka Chun and Gephart, Isabel S. and Pereira, Francisco and Handwerker, Daniel A. and Kam, Julia W. Y. and Bandettini, Peter A.},
title = {{In-scanner thoughts contribute to resting-state functional connectivity}},
journal = {Nature communications},
year = {2026},
month = jun,
volume = {17},
number = {1},
pages = {8058},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/
url = {https://
pmid = {42365014},
pmcid = {PMC13454257}
}
RIS
TY - JOUR
AU - Gonzalez-Castillo, Javier
AU - Spurney, Megan A.
AU - Lam, Ka Chun
AU - Gephart, Isabel S.
AU - Pereira, Francisco
AU - Handwerker, Daniel A.
AU - Kam, Julia W. Y.
AU - Bandettini, Peter A.
TI - In-scanner thoughts contribute to resting-state functional connectivity
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/
VL - 17
IS - 1
SP - 8058
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"type": "article-journal",
"title": "In-scanner thoughts contribute to resting-state functional connectivity",
"container-title": "Nature communications",
"author": [
{
"family": "Gonzalez-Castillo",
"given": "Javier"
},
{
"family": "Spurney",
"given": "Megan A."
},
{
"family": "Lam",
"given": "Ka Chun"
},
{
"family": "Gephart",
"given": "Isabel S."
},
{
"family": "Pereira",
"given": "Francisco"
},
{
"family": "Handwerker",
"given": "Daniel A."
},
{
"family": "Kam",
"given": "Julia W. Y."
},
{
"family": "Bandettini",
"given": "Peter A."
}
],
"container-title-short":
"volume": "17",
"issue": "1",
"page": "8058",
"DOI": "10.1038/
"PMID": "42365014",
"PMCID": "PMC13454257",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
27
]
]
}
}
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.7554/elife.104053 [code]
- Brain cognition gaps reveal associations with dopamine and factors related to brain health through artificial intelligence prediction of functional connectome.Journal: eLifeIn common: scikit-learn, pandas, SciPy, 1 other tool, 10 references
- [2] doi:10.1002/epi.70323 [code]
- Individual-specific resting-state networks predict language dominance in drug-resistant epilepsy.Journal: EpilepsiaIn common: AFNI, ANTs, FreeSurfer, 5 other tools, fMRI, 4 references
- [3] doi:10.21203/rs.3.rs-9326213/v1 [code]
- Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brainJournal: Research Square (preprint)In common: ANTs, FreeSurfer, FSL, 7 other tools, fMRI, 3 references
- [4] doi:10.64898/2026.03.09.710558 [code]
- Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brainJournal: bioRxiv (preprint)In common: ANTs, FreeSurfer, FSL, 7 other tools, fMRI, 3 references
- [5] doi:10.1162/imag.a.1222 [code]
- Network-based near-scalp personalized brain stimulation targets.Journal: Imaging neuroscience (Cambridge, Mass.)In common: AFNI, ANTs, FreeSurfer, 5 other tools, 3 references
- [6] doi:10.1038/s41467-026-73668-y [code]
- Convergent and divergent brain-cognition development in early adolescence.Journal: Nature communicationsIn common: AFNI, ANTs, FreeSurfer, 7 other tools, fMRI, 2 references
- [7] doi:10.1016/j.neuron.2026.04.011 [code]
- Precision fMRI reveals densely interdigitated network patches with conserved motifs in the lateral prefrontal cortex.Journal: NeuronIn common: AFNI, ANTs, FreeSurfer, 5 other tools, fMRI, 3 references
- [8] doi:10.1162/imag.a.1336 [code]
- Brain network dynamics in the wake-sleep transition reorganize according to task engagement.Journal: Imaging neuroscience (Cambridge, Mass.)In common: 7 references, author Javier Gonzalez-Castillo
- [9] doi:10.1038/s41398-026-04025-2 [code]
- Brain energetic landscapes shape state dysregulation in major depressive disorder: a morphological network controllability perspective.Journal: Translational psychiatryIn common: AFNI, ANTs, FreeSurfer, 7 other tools, 2 references
- [10] doi:10.1016/j.xcrm.2026.102943 [code]
- Parent-of-origin effects in Alzheimer's liability dissociate neurocognitive and cardiovascular traits in at-risk individuals.Journal: Cell reports. MedicineIn common: AFNI, ANTs, FreeSurfer, 8 other tools, clinical / translational
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 45 scripts, and 15 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:ad3aac1bee0c0346…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
