@yibeichan/claude-skills 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/cli.js +272 -0
  4. package/install.py +240 -0
  5. package/package.json +44 -0
  6. package/skills/bidsapp-nidm-standards/SKILL.md +202 -0
  7. package/skills/bidsapp-nidm-standards/references/babs_config.md +20 -0
  8. package/skills/bidsapp-nidm-standards/references/cli_arguments.md +76 -0
  9. package/skills/bidsapp-nidm-standards/references/container_patterns.md +53 -0
  10. package/skills/bidsapp-nidm-standards/references/nidm_integration.md +403 -0
  11. package/skills/bidsapp-nidm-standards/references/repo_structure.md +121 -0
  12. package/skills/bidsapp-nidm-standards/references/testing_patterns.md +82 -0
  13. package/skills/dicom2fmriprep/SKILL.md +377 -0
  14. package/skills/dicom2fmriprep/evals/evals.json +26 -0
  15. package/skills/dicom2fmriprep/references/babs-details.md +407 -0
  16. package/skills/dicom2fmriprep/references/fmriprep-details.md +250 -0
  17. package/skills/dicom2fmriprep/references/heudiconv-details.md +243 -0
  18. package/skills/fmri-ssm/SKILL.md +317 -0
  19. package/skills/fmri-ssm/references/code_templates.md +1570 -0
  20. package/skills/fmri-ssm/references/downstream_analysis.md +680 -0
  21. package/skills/fmri-ssm/references/group_inference.md +608 -0
  22. package/skills/fmri-ssm/references/hrf_modeling.md +447 -0
  23. package/skills/fmri-ssm/references/model_catalog.md +436 -0
  24. package/skills/fmri-ssm/references/paradigm_guide.md +406 -0
  25. package/skills/fmri-ssm/references/preprocessing.md +614 -0
  26. package/skills/fmri-ssm.zip +0 -0
  27. package/skills/neuroimaging-qc/SKILL.md +203 -0
  28. package/skills/neuroimaging-qc/references/eeg_qc.md +400 -0
  29. package/skills/neuroimaging-qc/references/fmri_qc.md +343 -0
  30. package/skills/neuroimaging-qc/references/fnirs_qc.md +430 -0
  31. package/skills/neuroimaging-qc/references/structural_qc.md +454 -0
  32. package/skills/neuroimaging-qc/scripts/parse_fmriprep_confounds.py +153 -0
  33. package/skills/neuroimaging-qc/scripts/parse_mriqc.py +114 -0
  34. package/skills/neuroimaging-qc/scripts/qc_report.py +295 -0
  35. package/skills/scientific-writer/SKILL.md +202 -0
  36. package/skills/scientific-writer/references/citation_styles.md +163 -0
  37. package/skills/scientific-writer/references/field_conventions.md +245 -0
  38. package/skills/scientific-writer/references/figures_tables.md +225 -0
  39. package/skills/scientific-writer/references/reporting_guidelines.md +225 -0
  40. package/skills.json +54 -0
@@ -0,0 +1,608 @@
1
+ # Group-Level Inference for SSMs in fMRI
2
+
3
+ ## Table of Contents
4
+ 1. [When Single-Subject vs. Group Models](#when-to-use)
5
+ 2. [Approach 1: Concatenation (Group HMM)](#concatenation)
6
+ 3. [Approach 2: Two-Stage (Fit per Subject, Aggregate)](#two-stage)
7
+ 4. [Approach 3: Hierarchical Bayesian Models](#hierarchical)
8
+ 5. [State Alignment Across Subjects](#alignment)
9
+ 6. [Comparing Groups (Clinical, Behavioral)](#group-comparison)
10
+ 7. [Deep Data vs. Wide Data Strategies](#deep-vs-wide)
11
+ 8. [Statistical Testing on SSM-Derived Metrics](#stats)
12
+
13
+ ---
14
+
15
+ ## 1. When Single-Subject vs. Group Models {#when-to-use}
16
+
17
+ **Single-subject models (fit each subject independently):**
18
+ - Deep data: many runs or long scans per subject (>30 min total per subject)
19
+ - When individual differences in state structure are the focus
20
+ - When subjects may have genuinely different numbers of states
21
+ - Precision medicine / individual-level inference
22
+
23
+ **Group models (pool across subjects):**
24
+ - Shallow data: short scans, few runs per subject (<15 min total)
25
+ - When you want to define common states shared across subjects
26
+ - When group-level transition dynamics are the primary question
27
+ - Standard clinical comparison designs (patients vs. controls)
28
+
29
+ **Hybrid: group-defined states, subject-specific dynamics**
30
+ - Define states from group model, then estimate subject-specific transition matrices
31
+ - Good balance for most studies
32
+
33
+ ---
34
+
35
+ ## 2. Approach 1: Concatenation (Group HMM) {#concatenation}
36
+
37
+ The simplest group approach: concatenate all subjects' data and fit a single HMM.
38
+ The model learns states shared across all subjects.
39
+
40
+ ```python
41
+ """Group HMM via concatenation."""
42
+ import numpy as np
43
+ from hmmlearn import hmm
44
+
45
+ def fit_group_hmm_concat(subject_data, K, covariance_type='full',
46
+ n_restarts=50, n_iter=200):
47
+ """Fit a single HMM on concatenated multi-subject data.
48
+
49
+ Parameters
50
+ ----------
51
+ subject_data : dict
52
+ {subject_id: list_of_run_arrays} where each array is (T, n_features)
53
+ K : int
54
+ Number of states
55
+
56
+ Returns
57
+ -------
58
+ group_model : fitted HMM
59
+ subject_states : dict of {subject_id: list_of_state_arrays}
60
+ """
61
+ # Concatenate all runs from all subjects
62
+ all_data = []
63
+ all_lengths = []
64
+ subject_run_map = [] # track which segment belongs to which subject/run
65
+
66
+ for sub_id, runs in subject_data.items():
67
+ for run_idx, run_data in enumerate(runs):
68
+ all_data.append(run_data)
69
+ all_lengths.append(run_data.shape[0])
70
+ subject_run_map.append((sub_id, run_idx))
71
+
72
+ data_concat = np.vstack(all_data)
73
+ print(f"Total data: {data_concat.shape[0]} TRs from {len(subject_data)} subjects")
74
+
75
+ # Z-score globally (or per-subject — see note below)
76
+ data_concat = (data_concat - data_concat.mean(axis=0)) / data_concat.std(axis=0)
77
+
78
+ # Fit group HMM (inline; see code_templates.md for the full fit_gaussian_hmm helper)
79
+ from hmmlearn import hmm
80
+ from sklearn.cluster import KMeans
81
+ best_model = None
82
+ best_score = -np.inf
83
+ for restart in range(n_restarts):
84
+ model = hmm.GaussianHMM(
85
+ n_components=K, covariance_type=covariance_type,
86
+ n_iter=n_iter, tol=1e-4, random_state=42 + restart,
87
+ )
88
+ if restart == 0:
89
+ km = KMeans(n_clusters=K, random_state=42, n_init=10).fit(data_concat)
90
+ model.means_init = km.cluster_centers_
91
+ try:
92
+ model.fit(data_concat, lengths=all_lengths)
93
+ score = model.score(data_concat, lengths=all_lengths)
94
+ if score > best_score:
95
+ best_score = score
96
+ best_model = model
97
+ except Exception:
98
+ continue
99
+ group_model = best_model
100
+
101
+ # Decode per subject
102
+ states = group_model.predict(data_concat, all_lengths)
103
+ subject_states = {}
104
+ offset = 0
105
+ for (sub_id, run_idx), length in zip(subject_run_map, all_lengths):
106
+ if sub_id not in subject_states:
107
+ subject_states[sub_id] = []
108
+ subject_states[sub_id].append(states[offset:offset + length])
109
+ offset += length
110
+
111
+ return group_model, subject_states
112
+
113
+
114
+ # IMPORTANT: Z-scoring considerations
115
+ # Option A: Global z-score (above) — treats all subjects as coming from same distribution
116
+ # Good when subjects are similar (same scanner, similar demographics)
117
+ # Bad when there are systematic between-subject differences in signal level
118
+ #
119
+ # Option B: Per-subject z-score — normalize each subject's data independently
120
+ # Good when between-subject variability in signal level is a nuisance
121
+ # Bad when between-subject differences in mean activation are meaningful
122
+ #
123
+ # Recommendation: Per-subject z-scoring is usually safer for group HMMs
124
+ def zscore_per_subject(subject_data):
125
+ """Z-score each subject's data independently."""
126
+ for sub_id in subject_data:
127
+ all_runs = np.vstack(subject_data[sub_id])
128
+ mu = all_runs.mean(axis=0)
129
+ sigma = all_runs.std(axis=0)
130
+ sigma[sigma < 1e-6] = 1.0 # avoid division by zero
131
+ subject_data[sub_id] = [(run - mu) / sigma for run in subject_data[sub_id]]
132
+ return subject_data
133
+ ```
134
+
135
+ **Limitations of concatenation:**
136
+ - Assumes all subjects share the same state definitions AND transition dynamics
137
+ - Subjects with more data have more influence on state definitions
138
+ - Cannot capture individual differences in state structure
139
+ - Large datasets: concatenating 100+ subjects creates very large matrices
140
+
141
+ ---
142
+
143
+ ## 3. Approach 2: Two-Stage (Fit per Subject, Aggregate) {#two-stage}
144
+
145
+ Fit individual subject models, then align states across subjects and aggregate statistics.
146
+
147
+ ```python
148
+ """Two-stage group analysis: fit per-subject, then align and aggregate."""
149
+ import numpy as np
150
+ from scipy.optimize import linear_sum_assignment
151
+ from scipy.spatial.distance import cdist
152
+
153
+ def fit_per_subject(subject_data, K, **hmm_kwargs):
154
+ """Stage 1: Fit HMM to each subject independently."""
155
+ subject_models = {}
156
+ subject_states = {}
157
+
158
+ for sub_id, runs in subject_data.items():
159
+ data_concat = np.vstack(runs)
160
+ lengths = [r.shape[0] for r in runs]
161
+
162
+ # Z-score within subject
163
+ data_concat = (data_concat - data_concat.mean(0)) / data_concat.std(0)
164
+
165
+ model, score, _ = fit_gaussian_hmm(
166
+ data_concat, lengths, K, **hmm_kwargs
167
+ )
168
+
169
+ subject_models[sub_id] = model
170
+ states = model.predict(data_concat, lengths)
171
+
172
+ # Split states back into runs
173
+ subject_states[sub_id] = []
174
+ offset = 0
175
+ for length in lengths:
176
+ subject_states[sub_id].append(states[offset:offset + length])
177
+ offset += length
178
+
179
+ print(f"Subject {sub_id}: LL={score:.1f}")
180
+
181
+ return subject_models, subject_states
182
+
183
+
184
+ def align_subjects_to_reference(subject_models, subject_data, reference_sub=None):
185
+ """Stage 2: Align state labels across subjects using Hungarian algorithm.
186
+
187
+ Parameters
188
+ ----------
189
+ subject_models : dict of {sub_id: fitted_model}
190
+ subject_data : dict of {sub_id: list_of_run_arrays}
191
+ Raw data per subject (needed to score models when reference_sub is None)
192
+ reference_sub : str or None
193
+ Subject to use as reference. If None, use the subject with highest LL.
194
+ """
195
+ sub_ids = list(subject_models.keys())
196
+
197
+ if reference_sub is None:
198
+ # Use subject with best model fit as reference
199
+ scores = {s: subject_models[s].score(
200
+ np.vstack(subject_data[s]),
201
+ [r.shape[0] for r in subject_data[s]]
202
+ ) for s in sub_ids}
203
+ reference_sub = max(scores, key=scores.get)
204
+
205
+ ref_means = subject_models[reference_sub].means_
206
+ alignments = {reference_sub: {k: k for k in range(ref_means.shape[0])}}
207
+
208
+ for sub_id in sub_ids:
209
+ if sub_id == reference_sub:
210
+ continue
211
+
212
+ target_means = subject_models[sub_id].means_
213
+ cost = cdist(ref_means, target_means, metric='correlation')
214
+ row_ind, col_ind = linear_sum_assignment(cost)
215
+
216
+ alignments[sub_id] = {col: row for row, col in zip(row_ind, col_ind)}
217
+
218
+ # Report alignment quality
219
+ match_cost = cost[row_ind, col_ind].mean()
220
+ print(f"Subject {sub_id}: mean alignment cost = {match_cost:.3f}")
221
+
222
+ return alignments, reference_sub
223
+
224
+
225
+ def aggregate_metrics(subject_states, subject_models, alignments, tr):
226
+ """Stage 3: Compute group-level metrics from aligned subject results.
227
+
228
+ Returns per-subject metrics that can be used for group statistics.
229
+ """
230
+ K = subject_models[list(subject_models.keys())[0]].n_components
231
+
232
+ metrics = {}
233
+ for sub_id in subject_states:
234
+ alignment = alignments[sub_id]
235
+
236
+ # Re-label states using alignment
237
+ aligned_states = []
238
+ for run_states in subject_states[sub_id]:
239
+ aligned = np.array([alignment[s] for s in run_states])
240
+ aligned_states.append(aligned)
241
+ all_states = np.concatenate(aligned_states)
242
+
243
+ # Fractional occupancy
244
+ frac_occ = np.array([(all_states == k).sum() / len(all_states) for k in range(K)])
245
+
246
+ # Mean dwell time per state
247
+ mean_dwell = {}
248
+ for k in range(K):
249
+ dwells = []
250
+ for run_states in aligned_states:
251
+ current = 0
252
+ for t in range(1, len(run_states)):
253
+ if run_states[t] == run_states[t-1] == k:
254
+ current += 1
255
+ elif run_states[t-1] == k:
256
+ dwells.append((current + 1) * tr)
257
+ current = 0
258
+ else:
259
+ current = 0
260
+ mean_dwell[k] = np.mean(dwells) if dwells else 0
261
+
262
+ # Transition rates (number of transitions per minute)
263
+ total_time = len(all_states) * tr / 60 # minutes
264
+ n_transitions = np.sum(np.diff(all_states) != 0)
265
+ transition_rate = n_transitions / total_time
266
+
267
+ metrics[sub_id] = {
268
+ 'fractional_occupancy': frac_occ,
269
+ 'mean_dwell_time': mean_dwell,
270
+ 'transition_rate': transition_rate,
271
+ }
272
+
273
+ return metrics
274
+ ```
275
+
276
+ ---
277
+
278
+ ## 4. Approach 3: Hierarchical Bayesian Models {#hierarchical}
279
+
280
+ The most principled approach: subject parameters are drawn from group-level priors.
281
+
282
+ ```python
283
+ """Hierarchical HMM using pyhsmm (Bayesian approach).
284
+
285
+ This learns both group-level state definitions and subject-specific variations.
286
+ The Dirichlet prior on transitions links subjects together.
287
+ """
288
+ import pyhsmm
289
+ import pyhsmm.basic.distributions as distributions
290
+
291
+ def fit_hierarchical_hmm(subject_data, K, alpha_a0=1.0, alpha_b0=1.0):
292
+ """Fit hierarchical Bayesian HMM with shared state definitions.
293
+
294
+ Uses MCMC sampling — slower but provides uncertainty estimates.
295
+ """
296
+ n_features = subject_data[list(subject_data.keys())[0]][0].shape[1]
297
+
298
+ obs_distns = [
299
+ distributions.Gaussian(
300
+ mu_0=np.zeros(n_features),
301
+ sigma_0=np.eye(n_features),
302
+ kappa_0=0.1,
303
+ nu_0=n_features + 2,
304
+ )
305
+ for _ in range(K)
306
+ ]
307
+
308
+ model = pyhsmm.models.WeakLimitStickyHDPHMM(
309
+ kappa=50, # stickiness
310
+ alpha_a_0=alpha_a0,
311
+ alpha_b_0=alpha_b0,
312
+ gamma_a_0=1.0,
313
+ gamma_b_0=1.0,
314
+ init_state_concentration=1.0,
315
+ obs_distns=obs_distns,
316
+ )
317
+
318
+ # Add each subject's data as a separate sequence
319
+ for sub_id, runs in subject_data.items():
320
+ for run_data in runs:
321
+ model.add_data(run_data)
322
+
323
+ # MCMC sampling
324
+ n_samples = 500
325
+ n_burnin = 200
326
+
327
+ for i in range(n_samples):
328
+ model.resample_model()
329
+ if i >= n_burnin and i % 10 == 0:
330
+ print(f"Sample {i}: {model.num_states()} active states")
331
+
332
+ return model
333
+ ```
334
+
335
+ **For a more scalable hierarchical approach, consider `glhmm`:**
336
+
337
+ ```python
338
+ """Hierarchical HMM using glhmm (Vidaurre's library).
339
+
340
+ Supports group-level inference with subject-specific transition matrices.
341
+ """
342
+ from glhmm import glhmm # import the class directly (not 'from glhmm import glhmm as gl')
343
+ from glhmm import preproc
344
+
345
+ def fit_group_glhmm(subject_data, K):
346
+ """Fit group-level HMM allowing subject-specific transitions."""
347
+
348
+ # Prepare data
349
+ all_data = []
350
+ T_list = []
351
+ for sub_id in sorted(subject_data.keys()):
352
+ for run in subject_data[sub_id]:
353
+ all_data.append(run)
354
+ T_list.append(run.shape[0])
355
+
356
+ data_concat = np.vstack(all_data)
357
+ indices = preproc.build_indices(T_list)
358
+
359
+ model = glhmm(
360
+ K=K,
361
+ covtype='full',
362
+ model_mean='state',
363
+ model_beta='no',
364
+ )
365
+
366
+ model.train(data_concat, indices=indices, maxiter=200)
367
+
368
+ return model
369
+ ```
370
+
371
+ ---
372
+
373
+ ## 5. State Alignment Across Subjects {#alignment}
374
+
375
+ When fitting per-subject models, states are arbitrarily labeled. Alignment is needed.
376
+
377
+ **Hungarian algorithm** (shown above): optimal 1-to-1 assignment based on state similarity.
378
+ Works when all subjects have the same K and similar states.
379
+
380
+ **K-means on state parameters:** Cluster all subjects' state means into K clusters. Each
381
+ cluster defines a group state, and subjects' states are assigned to the nearest cluster.
382
+
383
+ **Correlation-based alignment:** Compute spatial correlation between each subject's state
384
+ means and a reference set. Assign based on maximum correlation.
385
+
386
+ **When alignment fails:** If subjects genuinely have different state structures (different K
387
+ or qualitatively different states), forced alignment is inappropriate. Strategies:
388
+
389
+ 1. **Correlation-based alignment with a quality threshold.** Compute the max correlation
390
+ between each subject's state and the reference. If the best match is below r = 0.5,
391
+ flag that subject's state as "unaligned" and exclude it from group averages for that state.
392
+
393
+ 2. **Reduce K.** Often alignment failure signals over-fitting. Try K-1 or K-2; finer
394
+ distinctions may be subject-specific and unreliable at the group level.
395
+
396
+ 3. **K-means on pooled state means.** Cluster all subjects' K state means together into K
397
+ clusters (or use the silhouette score to find the optimal group K). Each cluster is a
398
+ group state; subjects whose state mean falls in a cluster are "aligned" to that state.
399
+
400
+ 4. **Treat as separate analyses.** When populations genuinely differ (e.g., controls vs.
401
+ patients with vastly altered dynamics), fit separate group models rather than forcing
402
+ alignment. Compare models by BIC or by the interpretability of aligned vs. separate fits.
403
+
404
+ 5. **HDP-HMM alignment.** If using HDP-HMM (where K varies by subject), align only the
405
+ shared "active" states using the mode K across subjects. For subjects with extra states,
406
+ treat the extras as subject-specific and exclude from group comparisons. Cap the group K
407
+ at the minimum K seen across subjects to avoid extrapolation.
408
+
409
+ ---
410
+
411
+ ## 6. Comparing Groups {#group-comparison}
412
+
413
+ ### SSM-derived metrics for group comparison
414
+
415
+ | Metric | Description | Statistical test |
416
+ |--------|-------------|-----------------|
417
+ | Fractional occupancy | Proportion of time in each state | t-test or Wilcoxon per state |
418
+ | Mean dwell time | Average duration of state visits | t-test or Wilcoxon per state |
419
+ | Transition probability | A[i,j] matrix | Permutation test on matrix elements |
420
+ | Transition rate | Total transitions per minute | t-test |
421
+ | State-specific FC | Covariance matrix per state | Network-based statistic, NBS |
422
+ | Switching entropy | Entropy of the transition matrix | t-test |
423
+
424
+ ### Example: patient vs. control comparison
425
+
426
+ ```python
427
+ """Compare SSM metrics between two groups."""
428
+ from scipy import stats
429
+
430
+ def compare_groups(metrics_group1, metrics_group2, K, alpha=0.05,
431
+ correction='fdr'):
432
+ """Compare SSM-derived metrics between two groups.
433
+
434
+ Parameters
435
+ ----------
436
+ metrics_group1, metrics_group2 : list of dicts
437
+ Each dict has 'fractional_occupancy', 'mean_dwell_time', 'transition_rate'
438
+ K : int
439
+ Number of states
440
+ correction : str
441
+ 'bonferroni' or 'fdr' for multiple comparison correction
442
+ """
443
+ results = {}
444
+ p_values = []
445
+
446
+ # Fractional occupancy per state
447
+ for k in range(K):
448
+ occ1 = [m['fractional_occupancy'][k] for m in metrics_group1]
449
+ occ2 = [m['fractional_occupancy'][k] for m in metrics_group2]
450
+
451
+ stat, p = stats.mannwhitneyu(occ1, occ2, alternative='two-sided')
452
+ results[f'frac_occ_state{k}'] = {
453
+ 'group1_mean': np.mean(occ1), 'group2_mean': np.mean(occ2),
454
+ 'U': stat, 'p': p
455
+ }
456
+ p_values.append(p)
457
+
458
+ # Dwell time per state
459
+ for k in range(K):
460
+ dwell1 = [m['mean_dwell_time'][k] for m in metrics_group1]
461
+ dwell2 = [m['mean_dwell_time'][k] for m in metrics_group2]
462
+
463
+ stat, p = stats.mannwhitneyu(dwell1, dwell2, alternative='two-sided')
464
+ results[f'dwell_state{k}'] = {
465
+ 'group1_mean': np.mean(dwell1), 'group2_mean': np.mean(dwell2),
466
+ 'U': stat, 'p': p
467
+ }
468
+ p_values.append(p)
469
+
470
+ # Transition rate
471
+ rate1 = [m['transition_rate'] for m in metrics_group1]
472
+ rate2 = [m['transition_rate'] for m in metrics_group2]
473
+ stat, p = stats.mannwhitneyu(rate1, rate2, alternative='two-sided')
474
+ results['transition_rate'] = {
475
+ 'group1_mean': np.mean(rate1), 'group2_mean': np.mean(rate2),
476
+ 'U': stat, 'p': p
477
+ }
478
+ p_values.append(p)
479
+
480
+ # Multiple comparison correction
481
+ from statsmodels.stats.multitest import multipletests
482
+ reject, p_corrected, _, _ = multipletests(p_values, method='fdr_bh')
483
+
484
+ # Attach corrected p-values
485
+ for i, key in enumerate(results):
486
+ results[key]['p_corrected'] = p_corrected[i]
487
+ results[key]['significant'] = reject[i]
488
+
489
+ return results
490
+ ```
491
+
492
+ ---
493
+
494
+ ## 7. Deep Data vs. Wide Data Strategies {#deep-vs-wide}
495
+
496
+ ### Deep data (few subjects, many runs/long scans)
497
+ - Example: 5 subjects, each with 10 hours of scanning (Midnight Scan Club, MyConnectome)
498
+ - Strategy: fit rich per-subject models (HMM-MAR, rSLDS, many states)
499
+ - Cross-validate within-subject (train on 80% of runs, test on 20%)
500
+ - Compare subjects qualitatively — each subject gets a full characterization
501
+ - Can detect rare or idiosyncratic states that group models would miss
502
+
503
+ ### Wide data (many subjects, short scans)
504
+ - Example: 1000 subjects from UK Biobank, each with 6 min of resting-state
505
+ - Strategy: group-level HMM or concatenation approach with simple model (Gaussian HMM)
506
+ - K=4-8 is typical — not enough per-subject data for more
507
+ - Power is in between-subject comparisons, not within-subject dynamics
508
+ - Use diagonal covariance or low-dimensional features to keep parameter count manageable
509
+
510
+ ### Mixed strategies
511
+ - Fit group model on all subjects to define states
512
+ - Then, for each subject, fix state definitions and estimate only the transition matrix
513
+ and initial state distribution (fewer parameters, estimable from short scans)
514
+ - This gives subject-specific dynamics with group-defined states
515
+
516
+ ```python
517
+ def refit_transitions_per_subject(group_model, subject_data):
518
+ """Fix emission parameters from group model, refit transitions per subject.
519
+
520
+ This is the hybrid approach: group-defined states, subject-specific dynamics.
521
+ """
522
+ from hmmlearn import hmm
523
+
524
+ K = group_model.n_components
525
+ subject_transitions = {}
526
+
527
+ for sub_id, runs in subject_data.items():
528
+ data = np.vstack(runs)
529
+ lengths = [r.shape[0] for r in runs]
530
+
531
+ # Create new model with fixed emissions
532
+ sub_model = hmm.GaussianHMM(
533
+ n_components=K,
534
+ covariance_type=group_model.covariance_type,
535
+ n_iter=100,
536
+ params='st', # only update startprob and transmat
537
+ init_params='', # don't reinitialize anything
538
+ )
539
+
540
+ # Copy fixed emissions from group model
541
+ sub_model.means_ = group_model.means_.copy()
542
+ sub_model.covars_ = group_model.covars_.copy()
543
+ sub_model.startprob_ = group_model.startprob_.copy()
544
+ sub_model.transmat_ = group_model.transmat_.copy()
545
+
546
+ sub_model.fit(data, lengths=lengths)
547
+ subject_transitions[sub_id] = sub_model.transmat_.copy()
548
+
549
+ return subject_transitions
550
+ ```
551
+
552
+ ---
553
+
554
+ ## 8. Statistical Testing on SSM-Derived Metrics {#stats}
555
+
556
+ ### Permutation testing (recommended for SSM-derived statistics)
557
+
558
+ SSM-derived metrics (dwell times, transition probabilities) often violate assumptions of
559
+ parametric tests. Permutation testing is distribution-free and appropriate.
560
+
561
+ ```python
562
+ def permutation_test_groups(metric_group1, metric_group2, n_permutations=10000,
563
+ random_state=42):
564
+ """Two-sample permutation test.
565
+
566
+ Parameters
567
+ ----------
568
+ metric_group1, metric_group2 : array-like
569
+ Per-subject metric values
570
+
571
+ Returns
572
+ -------
573
+ observed_diff : float
574
+ p_value : float (two-sided)
575
+ """
576
+ rng = np.random.RandomState(random_state)
577
+
578
+ g1 = np.array(metric_group1)
579
+ g2 = np.array(metric_group2)
580
+ all_data = np.concatenate([g1, g2])
581
+ n1 = len(g1)
582
+
583
+ observed_diff = g1.mean() - g2.mean()
584
+
585
+ perm_diffs = np.zeros(n_permutations)
586
+ for i in range(n_permutations):
587
+ perm = rng.permutation(all_data)
588
+ perm_diffs[i] = perm[:n1].mean() - perm[n1:].mean()
589
+
590
+ p_value = np.mean(np.abs(perm_diffs) >= np.abs(observed_diff))
591
+
592
+ return observed_diff, p_value
593
+ ```
594
+
595
+ ### Effect sizes
596
+
597
+ Always report effect sizes alongside p-values:
598
+ - Cohen's d for mean differences
599
+ - Eta-squared for ANOVA-style comparisons
600
+ - For transition matrices: Frobenius norm of the difference matrix
601
+
602
+ ### Confounds in group comparisons
603
+
604
+ When comparing clinical groups, control for:
605
+ - Head motion (mean FD) — correlates with many SSM metrics
606
+ - Scan length (if variable) — affects estimation quality
607
+ - Age, sex — standard demographic confounds
608
+ - Scanner/site (for multi-site studies) — use as covariate or ComBat harmonization