nltools 0.6.0.dev0__py3-none-any.whl

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 (95) hide show
  1. nltools/__init__.py +55 -0
  2. nltools/algorithms/__init__.py +90 -0
  3. nltools/algorithms/alignment/__init__.py +21 -0
  4. nltools/algorithms/alignment/procrustes.py +565 -0
  5. nltools/algorithms/alignment/srm.py +758 -0
  6. nltools/algorithms/backends.py +1059 -0
  7. nltools/algorithms/corrections.py +177 -0
  8. nltools/algorithms/decoding.py +327 -0
  9. nltools/algorithms/inference/__init__.py +50 -0
  10. nltools/algorithms/inference/bootstrap.py +1386 -0
  11. nltools/algorithms/inference/correlation.py +373 -0
  12. nltools/algorithms/inference/intersubject.py +422 -0
  13. nltools/algorithms/inference/isc.py +1554 -0
  14. nltools/algorithms/inference/matrix.py +602 -0
  15. nltools/algorithms/inference/one_sample.py +288 -0
  16. nltools/algorithms/inference/random.py +122 -0
  17. nltools/algorithms/inference/timeseries.py +347 -0
  18. nltools/algorithms/inference/two_sample.py +212 -0
  19. nltools/algorithms/inference/utils.py +58 -0
  20. nltools/algorithms/inference/validation.py +282 -0
  21. nltools/algorithms/neighborhoods.py +207 -0
  22. nltools/algorithms/outliers.py +308 -0
  23. nltools/algorithms/regression.py +83 -0
  24. nltools/algorithms/signal.py +303 -0
  25. nltools/algorithms/similarity.py +234 -0
  26. nltools/algorithms/validation.py +151 -0
  27. nltools/cross_validation.py +72 -0
  28. nltools/data/__init__.py +30 -0
  29. nltools/data/adjacency/__init__.py +875 -0
  30. nltools/data/adjacency/io.py +111 -0
  31. nltools/data/adjacency/modeling.py +569 -0
  32. nltools/data/adjacency/plotting.py +174 -0
  33. nltools/data/adjacency/state.py +349 -0
  34. nltools/data/adjacency/stats.py +596 -0
  35. nltools/data/adjacency/utils.py +79 -0
  36. nltools/data/atlases/__init__.py +23 -0
  37. nltools/data/atlases/labeling.py +158 -0
  38. nltools/data/atlases/loading.py +76 -0
  39. nltools/data/atlases/registry.py +96 -0
  40. nltools/data/atlases/reporting.py +456 -0
  41. nltools/data/braindata/__init__.py +2170 -0
  42. nltools/data/braindata/analysis.py +1381 -0
  43. nltools/data/braindata/bootstrap.py +398 -0
  44. nltools/data/braindata/io.py +896 -0
  45. nltools/data/braindata/modeling.py +594 -0
  46. nltools/data/braindata/plotting.py +501 -0
  47. nltools/data/braindata/prediction.py +1250 -0
  48. nltools/data/braindata/utils.py +348 -0
  49. nltools/data/braindata/validation.py +197 -0
  50. nltools/data/braindata/viewer.js +266 -0
  51. nltools/data/braindata/viewer.py +770 -0
  52. nltools/data/combine.py +27 -0
  53. nltools/data/designmatrix/__init__.py +1032 -0
  54. nltools/data/designmatrix/append.py +518 -0
  55. nltools/data/designmatrix/diagnostics.py +248 -0
  56. nltools/data/designmatrix/io.py +356 -0
  57. nltools/data/designmatrix/plotting.py +291 -0
  58. nltools/data/designmatrix/regressors.py +463 -0
  59. nltools/data/designmatrix/transforms.py +200 -0
  60. nltools/data/designmatrix/utils.py +350 -0
  61. nltools/data/ownership.py +129 -0
  62. nltools/data/results.py +291 -0
  63. nltools/data/roc/__init__.py +398 -0
  64. nltools/data/simulator/__init__.py +927 -0
  65. nltools/data/simulator/haxby.py +124 -0
  66. nltools/data/validation.py +83 -0
  67. nltools/datasets.py +218 -0
  68. nltools/io/__init__.py +10 -0
  69. nltools/io/events.py +67 -0
  70. nltools/io/h5.py +246 -0
  71. nltools/mask.py +403 -0
  72. nltools/models/__init__.py +11 -0
  73. nltools/models/glm.py +543 -0
  74. nltools/models/results.py +49 -0
  75. nltools/models/ridge.py +1303 -0
  76. nltools/models/validation.py +26 -0
  77. nltools/plotting/__init__.py +32 -0
  78. nltools/plotting/adjacency.py +421 -0
  79. nltools/plotting/brain.py +669 -0
  80. nltools/plotting/decomposition.py +111 -0
  81. nltools/plotting/prediction.py +110 -0
  82. nltools/resources/covariates_example.csv +161 -0
  83. nltools/resources/onsets_example.csv +40 -0
  84. nltools/templates/__init__.py +51 -0
  85. nltools/templates/config.py +144 -0
  86. nltools/templates/fetch.py +260 -0
  87. nltools/templates/matching.py +183 -0
  88. nltools/templates/paths.py +106 -0
  89. nltools/templates/registry.py +25 -0
  90. nltools/utils.py +230 -0
  91. nltools/version.py +13 -0
  92. nltools-0.6.0.dev0.dist-info/METADATA +95 -0
  93. nltools-0.6.0.dev0.dist-info/RECORD +95 -0
  94. nltools-0.6.0.dev0.dist-info/WHEEL +4 -0
  95. nltools-0.6.0.dev0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,875 @@
1
+ """Provide data structures for working with similarity and dissimilarity matrices."""
2
+
3
+ from copy import deepcopy
4
+ import numpy as np
5
+ import polars as pl
6
+ from sklearn.metrics.pairwise import pairwise_distances
7
+
8
+ from nltools.utils import _attempt_to_import
9
+ from .utils import _apply_stat, _perform_arithmetic
10
+
11
+
12
+ # Optional dependencies
13
+ nx = _attempt_to_import("networkx")
14
+
15
+ MAX_INT = np.iinfo(np.int32).max
16
+
17
+
18
+ class Adjacency:
19
+ """Represent adjacency matrices in vectorized form.
20
+
21
+ Store distance/similarity matrices as strict upper triangles and directed
22
+ matrices as full row-major vectors. Symmetric reconstruction always has a
23
+ zero diagonal; input diagonals are discarded. Flat rectangular stacks require
24
+ an explicit `*_flat` matrix type. A list or 2-D flat array retains stack rank,
25
+ including one matrix. A zero-length symmetric vector represents one node.
26
+ Construction and result methods return independently owned mutable state.
27
+
28
+ Args:
29
+ data (Adjacency | np.ndarray | pd.DataFrame | pl.DataFrame | str | Path | list): A square
30
+ matrix, a flattened vector, a `.csv`/`.h5` path, or a list of
31
+ matrices/`Adjacency` instances/`.csv` paths to stack.
32
+ Y (pd.DataFrame | pl.DataFrame, optional): Matrix metadata, one row per matrix.
33
+ None inherits metadata during copy construction.
34
+ matrix_type (str, optional): Type of matrix. One of `'distance'`, `'similarity'`,
35
+ `'directed'`, `'distance_flat'`, `'similarity_flat'`, `'directed_flat'`.
36
+ For copy construction, this confirms the existing kind without reinterpreting it.
37
+ labels (list, optional): Shared node labels, or a nested matrix-by-node
38
+ label grid for a stack. None inherits labels during copy construction.
39
+
40
+ Attributes:
41
+ data (np.ndarray): Vectorized matrix values. Shape `(vector_length,)` for a
42
+ single matrix or `(n_matrices, vector_length)` for a stack; symmetric
43
+ matrices store only the upper triangle without the diagonal.
44
+ matrix_type (str): One of `'distance'`, `'similarity'`, `'directed'`, or
45
+ `'empty'` (the `'_flat'` input variants are normalized to their base type).
46
+ is_single_matrix (bool): True for single storage; a one-row stack is False.
47
+ issymmetric (bool): True for distance/similarity matrices, False for directed.
48
+ labels (list): Node labels (empty list when none were given).
49
+ Y (pl.DataFrame): Training labels as a polars DataFrame (possibly empty).
50
+ is_empty (bool): True if the instance holds no matrices.
51
+ n_nodes (int): Number of nodes `n` for an `(n, n)` matrix.
52
+ shape (tuple): Logical shape — `(n_nodes, n_nodes)` for a single matrix,
53
+ `(n_matrices, n_nodes, n_nodes)` for a stack, including typed empty stacks;
54
+ `(0, 0)` for an untyped empty constructor.
55
+ vector_shape (tuple): Shape of the internal vectorized storage (`data.shape`).
56
+ """
57
+
58
+ def __init__(self, data=None, *, Y=None, matrix_type=None, labels=None):
59
+ from .state import _initialize
60
+
61
+ _initialize(self, data, matrix_type=matrix_type, labels=labels, Y=Y)
62
+
63
+ # ── Dunders (alphabetical) ──────────────────────────────────────────
64
+
65
+ def __add__(self, y):
66
+ return _perform_arithmetic(self, y, np.add, "add")
67
+
68
+ def __copy__(self):
69
+ from nltools.data.ownership import _copy_complete
70
+
71
+ return _copy_complete(self)
72
+
73
+ def __deepcopy__(self, memo):
74
+ from nltools.data.ownership import _copy_complete
75
+
76
+ return _copy_complete(self, memo)
77
+
78
+ def __getitem__(self, index):
79
+ from .state import _select
80
+
81
+ return _select(self, index)
82
+
83
+ def __iter__(self):
84
+ for x in range(len(self)):
85
+ yield self[x]
86
+
87
+ def __len__(self):
88
+ if self.is_single_matrix:
89
+ return 1
90
+ return self.data.shape[0]
91
+
92
+ def __mul__(self, y):
93
+ return _perform_arithmetic(self, y, np.multiply, "multiply")
94
+
95
+ def __radd__(self, y):
96
+ return _perform_arithmetic(self, y, np.add, "add", reverse=True)
97
+
98
+ def __repr__(self):
99
+ return f"{self.__class__.__module__}.{self.__class__.__name__}(shape={self.shape}, Y={self.Y.shape}, is_symmetric={self.issymmetric}, matrix_type={self.matrix_type})"
100
+
101
+ def __rmul__(self, y):
102
+ return _perform_arithmetic(self, y, np.multiply, "multiply", reverse=True)
103
+
104
+ def __rsub__(self, y):
105
+ return _perform_arithmetic(self, y, np.subtract, "subtract", reverse=True)
106
+
107
+ def __sub__(self, y):
108
+ return _perform_arithmetic(self, y, np.subtract, "subtract")
109
+
110
+ def __truediv__(self, y):
111
+ return _perform_arithmetic(self, y, np.divide, "divide")
112
+
113
+ # ── Properties (alphabetical) ───────────────────────────────────────
114
+
115
+ @property
116
+ def Y(self) -> pl.DataFrame:
117
+ """Training labels as a polars DataFrame (possibly empty)."""
118
+ return self._Y
119
+
120
+ @Y.setter
121
+ def Y(self, value) -> None:
122
+ from .state import _owned_frame
123
+
124
+ self._Y = _owned_frame(value, len(self))
125
+
126
+ @property
127
+ def is_empty(self) -> bool:
128
+ """Check if Adjacency object is empty.
129
+
130
+ Returns:
131
+ bool: True if the adjacency matrix is empty, False otherwise.
132
+ """
133
+ return len(self) == 0
134
+
135
+ @property
136
+ def n_nodes(self):
137
+ """Return the number of nodes in the adjacency matrix.
138
+
139
+ Returns:
140
+ int: Number of nodes (n) for an (n, n) matrix.
141
+ """
142
+ return self._n_nodes
143
+
144
+ @property
145
+ def shape(self):
146
+ """Return the logical shape of the adjacency matrix.
147
+
148
+ Returns:
149
+ tuple: `(n_nodes, n_nodes)` for a single matrix, `(n_matrices, n_nodes,
150
+ n_nodes)` for stacked matrices, including typed empty stacks; `(0, 0)`
151
+ for an untyped empty constructor.
152
+
153
+ Note:
154
+ Use `.vector_shape` to get the internal vectorized representation shape.
155
+ """
156
+ if self.matrix_type == "empty":
157
+ return (0, 0)
158
+
159
+ if self.is_single_matrix:
160
+ return (self.n_nodes, self.n_nodes)
161
+ return (len(self), self.n_nodes, self.n_nodes)
162
+
163
+ @property
164
+ def vector_shape(self):
165
+ """Return shape of internal vectorized representation.
166
+
167
+ Returns:
168
+ tuple: `(vector_length,)` for a single matrix, `(n_matrices,
169
+ vector_length)` for stacked matrices.
170
+
171
+ Note:
172
+ This is the raw shape of the internal data storage.
173
+ Use `.shape` for the logical (n_nodes, n_nodes) shape.
174
+ """
175
+ return self.data.shape
176
+
177
+ # ── Public methods (alphabetical) ───────────────────────────────────
178
+
179
+ def append(self, data):
180
+ """Append data to an Adjacency instance.
181
+
182
+ Args:
183
+ data (Adjacency): Adjacency instance to append.
184
+
185
+ Returns:
186
+ Adjacency: New appended Adjacency instance.
187
+ """
188
+ from .state import _append
189
+
190
+ return _append(self, data)
191
+
192
+ def bootstrap(
193
+ self,
194
+ statistic,
195
+ *,
196
+ n_samples=5000,
197
+ confidence_level=0.95,
198
+ return_samples=False,
199
+ n_jobs=-1,
200
+ random_state=None,
201
+ progress_bar: bool = False,
202
+ ):
203
+ """Bootstrap an aggregate statistic across a stack of matrices.
204
+
205
+ Resamples matrices with replacement and aggregates the replicates as
206
+ they complete, so what the run holds is the retained tail — about
207
+ ``(1 - confidence_level)`` of the replicates per edge — plus one
208
+ dispatch window, rather than all ``n_samples`` matrices.
209
+
210
+ Args:
211
+ statistic (str): Statistic to bootstrap: `'mean'`, `'median'`,
212
+ `'std'`, `'sum'`, `'min'`, or `'max'` — each the corresponding
213
+ NumPy reduction over matrices, with `'std'` at ``ddof=0``.
214
+ n_samples (int): Number of bootstrap replicates, at least two.
215
+ Default 5000.
216
+ confidence_level (float): Confidence level of the reported
217
+ interval, strictly between zero and one. Default 0.95. The
218
+ bounds are the central percentile interval, elementwise
219
+ marginal per edge.
220
+ return_samples (bool): Retain and return every replicate. Default
221
+ False.
222
+ n_jobs (int): CPU worker ceiling. -1 (default) means all cores.
223
+ random_state (int | None): Random seed for reproducibility.
224
+ progress_bar (bool): If True, show a progress bar. Default False.
225
+
226
+ Returns:
227
+ BootstrapResult: ``estimate`` (the statistic on the unresampled
228
+ stack), ``standard_error``, ``ci_lower`` and ``ci_upper`` as
229
+ single-matrix `Adjacency` objects, plus ``samples`` as a NumPy
230
+ array with the bootstrap axis first when
231
+ ``return_samples=True``.
232
+
233
+ Examples:
234
+ ```python
235
+ boot = adj.bootstrap("mean", n_samples=1000)
236
+ boot.estimate # → Adjacency
237
+ ```
238
+ """
239
+ from .modeling import _bootstrap
240
+
241
+ return _bootstrap(
242
+ self,
243
+ statistic,
244
+ n_samples=n_samples,
245
+ confidence_level=confidence_level,
246
+ return_samples=return_samples,
247
+ n_jobs=n_jobs,
248
+ random_state=random_state,
249
+ progress_bar=progress_bar,
250
+ )
251
+
252
+ def cluster_summary(self, *, clusters=None, summary="mean", scope="within"):
253
+ """Provide summaries of clusters within Adjacency matrices.
254
+
255
+ Computes mean/median of within and between cluster values. Requires a
256
+ list of cluster ids indicating the row/column of each cluster.
257
+
258
+ Args:
259
+ clusters (list): Cluster label for each row/column.
260
+ summary (str | None): Central tendency, `'mean'` or `'median'`. If None,
261
+ return all values instead of a summary.
262
+ scope (str): Summarize `'within'` cluster or `'between'` clusters.
263
+
264
+ Returns:
265
+ dict: Per-cluster summaries keyed by cluster label.
266
+ """
267
+ from .stats import _cluster_summary
268
+
269
+ return _cluster_summary(self, clusters=clusters, summary=summary, scope=scope)
270
+
271
+ def copy(self):
272
+ """Return an independently owned copy, preserving internal aliases and cycles."""
273
+ return deepcopy(self)
274
+
275
+ def distance( # nosemgrep: kwargs-internal-forwarding # forwards to sklearn.metrics.pairwise_distances
276
+ self, metric="correlation", *, include_diag=False, **kwargs
277
+ ):
278
+ """Calculate distance between images within an Adjacency() instance.
279
+
280
+ Args:
281
+ metric (str): Distance metric; any metric accepted by
282
+ `sklearn.metrics.pairwise_distances` (scikit-learn or scipy).
283
+ include_diag (bool): Whether to include the main diagonal when
284
+ computing distances between adjacency matrices. Only applies
285
+ to symmetric matrices. Default False (consistent with how
286
+ symmetric matrices are stored without the diagonal).
287
+ **kwargs (dict): Forwarded to `sklearn.metrics.pairwise_distances`.
288
+
289
+ Returns:
290
+ Adjacency: A 2D distance matrix.
291
+ """
292
+ if include_diag and self.issymmetric:
293
+ # Get square form and extract upper triangle WITH diagonal
294
+ squares = self.squareform()
295
+ if self.is_single_matrix:
296
+ squares = [squares]
297
+ # Extract upper triangle including diagonal for each matrix
298
+ data_with_diag = []
299
+ for sq in squares:
300
+ mask = np.triu(
301
+ np.ones_like(sq, dtype=bool), k=0
302
+ ) # k=0 includes diagonal
303
+ data_with_diag.append(sq[mask])
304
+ data = np.array(data_with_diag)
305
+ else:
306
+ data = np.atleast_2d(self.data)
307
+
308
+ return Adjacency(
309
+ pairwise_distances(data, metric=metric, **kwargs),
310
+ matrix_type="distance",
311
+ )
312
+
313
+ def distance_to_similarity(self, metric="correlation", beta=1):
314
+ """Convert distance matrix to similarity matrix.
315
+
316
+ Currently only implemented for the 'correlation' and 'euclidean' metrics.
317
+
318
+ Args:
319
+ metric (str): Either 'correlation' or 'euclidean'.
320
+ beta (float): Scale parameter of the exponential used for 'euclidean' (default: 1).
321
+
322
+ Returns:
323
+ Adjacency: The converted similarity matrix.
324
+ """
325
+ from .state import _distance_to_similarity
326
+
327
+ return _distance_to_similarity(self, metric, beta)
328
+
329
+ def generate_permutations(self, n_permute, random_state=None):
330
+ """Generate permuted versions of an Adjacency instance lazily.
331
+
332
+ Args:
333
+ n_permute (int): Number of permutations.
334
+ random_state (int | np.random.RandomState, optional): Random seed for
335
+ reproducibility.
336
+
337
+ Yields:
338
+ Adjacency: Permuted version of self.
339
+
340
+ Examples:
341
+ ```python
342
+ for perm in adj.generate_permutations(1000):
343
+ out = neural_distance_mat.similarity(perm)
344
+ ```
345
+ """
346
+ from .modeling import _generate_permutations
347
+
348
+ return _generate_permutations(self, n_permute, random_state)
349
+
350
+ def mean(self, axis=0):
351
+ """Calculate mean of Adjacency.
352
+
353
+ Args:
354
+ axis (int): Calculate mean over matrices (0) or upper triangle (1).
355
+
356
+ Returns:
357
+ float | Adjacency | np.ndarray: A float for a single matrix; an
358
+ Adjacency when `axis=0`; an array when `axis=1`.
359
+ """
360
+ return _apply_stat(self, np.nanmean, axis)
361
+
362
+ def median(self, axis=0):
363
+ """Calculate median of Adjacency.
364
+
365
+ Args:
366
+ axis (int): Calculate median over matrices (0) or upper triangle (1).
367
+
368
+ Returns:
369
+ float | Adjacency | np.ndarray: A float for a single matrix; an
370
+ Adjacency when `axis=0`; an array when `axis=1`.
371
+ """
372
+ return _apply_stat(self, np.nanmedian, axis)
373
+
374
+ def plot( # nosemgrep: kwargs-internal-forwarding # forwards to seaborn via _plot_adjacency
375
+ self, *, limit=3, ax=None, **kwargs
376
+ ):
377
+ """Create a heatmap of an Adjacency matrix.
378
+
379
+ Args:
380
+ limit (int): Number of heatmaps to plot if the object contains multiple
381
+ matrices. Default 3.
382
+ ax (matplotlib.axes.Axes, optional): Axis to draw on (single matrix only).
383
+ **kwargs (dict): Forwarded to `seaborn.heatmap`.
384
+ """
385
+ from .plotting import _plot_adjacency
386
+
387
+ return _plot_adjacency(self, limit=limit, ax=ax, **kwargs)
388
+
389
+ def plot_between_label_distance( # nosemgrep: kwargs-internal-forwarding # forwards to seaborn via stats.plot_between_label_distance
390
+ self, *, labels=None, ax=None, permutation_test=True, n_permute=5000, **kwargs
391
+ ):
392
+ """Create a heatmap of the average distance between every pair of labels.
393
+
394
+ Args:
395
+ labels (np.ndarray, optional): Group label per node; defaults to the
396
+ stored labels.
397
+ ax (matplotlib.axes.Axes, optional): Axis to draw on.
398
+ permutation_test (bool): Also compute the mean-difference and p-value
399
+ matrices from a two-sample permutation test comparing each
400
+ group's within-label distances against its distances to each
401
+ other group. Default True.
402
+ n_permute (int): Number of permutations for the test. Default 5000.
403
+ **kwargs (dict): Forwarded to `seaborn.heatmap`.
404
+
405
+ Returns:
406
+ tuple[pl.DataFrame, ...]: `(long_df, within_mean_df)` without the
407
+ permutation test, or `(long_df, within_mean_df, mean_diff_df,
408
+ p_df)` with it. `long_df` holds every pairwise distance with its
409
+ `Group` and `Comparison` labels; the others are long-format
410
+ label-pair frames.
411
+ """
412
+ from .stats import _plot_between_label_distance
413
+
414
+ return _plot_between_label_distance(
415
+ self,
416
+ labels=labels,
417
+ ax=ax,
418
+ permutation_test=permutation_test,
419
+ n_permute=n_permute,
420
+ **kwargs,
421
+ )
422
+
423
+ def plot_label_distance( # nosemgrep: kwargs-internal-forwarding # forwards to seaborn via stats.plot_label_distance
424
+ self, labels=None, ax=None, *, permutation_test=False, n_permute=5000, **kwargs
425
+ ):
426
+ """Create a violin plot of within- and between-label distances.
427
+
428
+ Args:
429
+ labels (np.ndarray, optional): Group label per node; defaults to the
430
+ stored labels.
431
+ ax (matplotlib.axes.Axes, optional): Axis to draw on.
432
+ permutation_test (bool): Run a two-sample permutation test of within
433
+ against between distance for each group. Default False.
434
+ n_permute (int): Number of permutations for the test. Default 5000.
435
+ **kwargs (dict): Forwarded to `seaborn.violinplot`, plus `fontsize`
436
+ for the axis label and title (default 18).
437
+
438
+ Returns:
439
+ pl.DataFrame | tuple[pl.DataFrame, dict]: The long-format frame with
440
+ columns `Distance`, `Type`, `Group`, or `(long_df, stats)` when
441
+ `permutation_test=True`, where `stats` maps each group label to
442
+ its permutation-test result.
443
+ """
444
+ from .stats import _plot_label_distance
445
+
446
+ return _plot_label_distance(
447
+ self,
448
+ labels,
449
+ ax,
450
+ permutation_test=permutation_test,
451
+ n_permute=n_permute,
452
+ **kwargs,
453
+ )
454
+
455
+ def plot_mds( # nosemgrep: kwargs-internal-forwarding # forwards to matplotlib via plotting.plot_mds
456
+ self,
457
+ *,
458
+ n_components=2,
459
+ metric_mds=True,
460
+ labels=None,
461
+ labels_color=None,
462
+ cmap=None,
463
+ view=(30, 20),
464
+ figsize=None,
465
+ ax=None,
466
+ n_jobs=-1,
467
+ **kwargs,
468
+ ):
469
+ """Plot multidimensional scaling.
470
+
471
+ Args:
472
+ n_components (int): Number of dimensions to project (2 or 3).
473
+ metric_mds (bool): Perform metric (True) or non-metric (False) scaling.
474
+ Default True.
475
+ labels (list, optional): Overrides the labels stored on the instance.
476
+ labels_color (list, optional): One color per label.
477
+ cmap (matplotlib.colors.Colormap, optional): Colormap. Default `plt.cm.hot_r`.
478
+ view (tuple): Elevation/azimuth for a 3-D plot. Default (30, 20).
479
+ figsize (list): Figure size. Default [12, 8].
480
+ ax (matplotlib.axes.Axes, optional): Axis to draw on.
481
+ n_jobs (int): Number of parallel jobs.
482
+ **kwargs (dict): Forwarded to `sklearn.manifold.MDS`.
483
+ """
484
+ from .plotting import _plot_mds
485
+
486
+ return _plot_mds(
487
+ self,
488
+ n_components=n_components,
489
+ metric_mds=metric_mds,
490
+ labels=labels,
491
+ labels_color=labels_color,
492
+ cmap=cmap,
493
+ view=view,
494
+ figsize=figsize,
495
+ ax=ax,
496
+ n_jobs=n_jobs,
497
+ **kwargs,
498
+ )
499
+
500
+ def plot_silhouette(
501
+ self,
502
+ *,
503
+ labels=None,
504
+ ax=None,
505
+ permutation_test=True,
506
+ n_permute=5000,
507
+ colors=None,
508
+ figsize=(6, 4),
509
+ ):
510
+ """Create a silhouette plot.
511
+
512
+ Args:
513
+ labels (np.ndarray, optional): Cluster/group label per node (overrides
514
+ stored labels).
515
+ ax (matplotlib.axes.Axes, optional): Axis to draw on.
516
+ permutation_test (bool): Whether to run a permutation test. Default True.
517
+ n_permute (int): Number of permutations for the test. Default 5000.
518
+ colors (list, optional): RGB triplets, one per cluster. Default: seaborn
519
+ `'hls'` palette.
520
+ figsize (tuple): Figure size. Default (6, 4).
521
+
522
+ Returns:
523
+ pl.DataFrame: Columns `label` and `mean_silhouette`, plus `p` when
524
+ `permutation_test=True`.
525
+ """
526
+ from .stats import _plot_silhouette
527
+
528
+ return _plot_silhouette(
529
+ self,
530
+ labels=labels,
531
+ ax=ax,
532
+ permutation_test=permutation_test,
533
+ n_permute=n_permute,
534
+ colors=colors,
535
+ figsize=figsize,
536
+ )
537
+
538
+ def r_to_z(self):
539
+ """Apply Fisher's r-to-z transformation to each data element."""
540
+ from .stats import _r_to_z
541
+
542
+ return _r_to_z(self)
543
+
544
+ def regress(self, X, *, tail=2):
545
+ """Run a regression on an adjacency instance.
546
+
547
+ Pass an `Adjacency` as `X` to decompose this matrix with other matrices, or a
548
+ `DesignMatrix` to regress each cell across a stack of matrices.
549
+
550
+ Args:
551
+ X (Adjacency | DesignMatrix): Design matrix.
552
+ tail (int | str): `2`/`'two'` (two-tailed, default) or `1`/`'one'`
553
+ (one-tailed: beta > 0; negate a regressor for the other direction).
554
+
555
+ Returns:
556
+ dict: Keys `beta`, `sigma` (coefficient standard error), `t`, `p`,
557
+ `df`, and `residual`. With DesignMatrix predictors, coefficient
558
+ fields are Adjacency maps per predictor (single for one predictor).
559
+ With Adjacency predictors, a single response is required and
560
+ coefficient fields are native predictor arrays or scalars.
561
+ `df` is a scalar; `residual` retains response shape and metadata.
562
+ """
563
+ from .modeling import _regress
564
+
565
+ return _regress(self, X, tail=tail)
566
+
567
+ def similarity(
568
+ self,
569
+ data,
570
+ *,
571
+ plot=False,
572
+ method="2d",
573
+ n_permute=5000,
574
+ metric="spearman",
575
+ include_diag=False,
576
+ nan_policy="omit",
577
+ tail=2,
578
+ return_null=False,
579
+ n_jobs=-1,
580
+ random_state=None,
581
+ progress_bar: bool = False,
582
+ ):
583
+ """Calculate similarity between two Adjacency matrices.
584
+
585
+ The default uses Spearman correlation and a permutation test.
586
+
587
+ Args:
588
+ data (Adjacency | np.ndarray): Adjacency to compare against, or a 1-D array
589
+ the same size as `self.data`.
590
+ plot (bool): Plot the two stacked adjacency matrices being compared.
591
+ Default False.
592
+ method (str | None): Permutation scheme, `'1d'`, `'2d'`, or None (no
593
+ permutation test).
594
+ n_permute (int): Number of permutations for the p-value. Default 5000.
595
+ metric (str): `'spearman'`, `'pearson'`, or `'kendall'`.
596
+ include_diag (bool): Only applies to `'directed'` matrices with
597
+ `method=None` or `method='1d'`. Default False (self-similarity is
598
+ uninformative). Symmetric matrices never store the diagonal, so this
599
+ flag is a no-op for them.
600
+ nan_policy (str): How to handle NaN values on the 1-D paths
601
+ (`method='1d'` or `method=None`): `'omit'` removes NaN pairwise
602
+ before computing the correlation (default), `'propagate'` lets NaN
603
+ flow through, `'raise'` errors if any NaN is present.
604
+ `method='2d'` raises on any NaN whatever the policy.
605
+ tail (int | str): `2`/`'two'` (two-tailed, default) or `1`/`'one'`
606
+ (one-tailed, positive direction).
607
+ return_null (bool): If True, also return the null distribution. Default False.
608
+ n_jobs (int): Number of parallel jobs. Default -1 (all cores).
609
+ random_state (int, optional): Random seed for reproducibility.
610
+ progress_bar (bool): If True, show a progress bar. Default False.
611
+
612
+ Returns:
613
+ dict | list[dict]: A correlation result dict with keys
614
+ 'correlation' and 'p' for a single matrix, or a list of these
615
+ dicts for a stack.
616
+
617
+ Note:
618
+ `metric` and `method` are easy to mix up. `metric` is the
619
+ correlation used to compare the two matrices. `method` is the
620
+ permutation scheme: `'2d'` shuffles rows and columns together (the
621
+ Mantel test, the right null for a symmetric RDM), `'1d'` shuffles
622
+ the vectorized entries, and None skips the test entirely.
623
+ """
624
+ from .stats import _similarity
625
+
626
+ return _similarity(
627
+ self,
628
+ data,
629
+ plot=plot,
630
+ method=method,
631
+ n_permute=n_permute,
632
+ metric=metric,
633
+ include_diag=include_diag,
634
+ nan_policy=nan_policy,
635
+ tail=tail,
636
+ return_null=return_null,
637
+ n_jobs=n_jobs,
638
+ random_state=random_state,
639
+ progress_bar=progress_bar,
640
+ )
641
+
642
+ def social_relations_model(self, summarize_results=True, nan_replace=True):
643
+ """Estimate the social relations model from a matrix for a round-robin design.
644
+
645
+ $$X_{ij} = m + \\alpha_i + \\beta_j + g_{ij} + \\epsilon_{ijl}$$
646
+
647
+ where $X_{ij}$ is the score for person i rating person j, $m$ is the group mean,
648
+ $\\alpha_i$ is person i's actor effect, $\\beta_j$ is person j's partner effect, $g_{ij}$
649
+ is the relationship effect and $\\epsilon_{ijl}$ is the error in measure l for actor i and partner j.
650
+
651
+ This model is primarily concerned with partitioning the variance of the various
652
+ effects. The implementation follows Chapter 8 of Kenny, Kashy, & Cook (2006) and
653
+ the tests replicate the book's examples. Actor scores are rows (lower triangle)
654
+ and partner scores are columns (upper triangle). The minimal sample size to
655
+ estimate these effects is 4.
656
+
657
+ **Model assumptions:** social interactions are exclusively dyadic; people are
658
+ randomly sampled from the population; there are no order effects; the effects
659
+ combine additively and relationships are linear.
660
+
661
+ Args:
662
+ summarize_results (bool): If True, print a formatted summary of model results.
663
+ nan_replace (bool): If True, replace NaN values with row and column means.
664
+
665
+ Returns:
666
+ pd.Series | pd.DataFrame: All of the effects estimated using SRM, as a
667
+ Series (single matrix) or DataFrame (one row per matrix).
668
+
669
+ References:
670
+ Kenny, D. A., Kashy, D. A., & Cook, W. L. (2006). *Dyadic data analysis*.
671
+ Guilford Press.
672
+ """
673
+ from .modeling import _social_relations_model
674
+
675
+ return _social_relations_model(self, summarize_results, nan_replace)
676
+
677
+ def squareform(self):
678
+ """Convert adjacency data back to square form.
679
+
680
+ Returns:
681
+ np.ndarray | list[np.ndarray]: Detached square matrix, or a list of
682
+ detached matrices for a stack. Symmetric diagonals are zero.
683
+ """
684
+ from .state import _to_square
685
+
686
+ return _to_square(self)
687
+
688
+ def stats_label_distance(self, *, labels=None, n_permute=5000, n_jobs=-1):
689
+ """Calculate permutation tests on within and between label distance.
690
+
691
+ Args:
692
+ labels (np.ndarray, optional): Group label per node; defaults to the
693
+ stored labels.
694
+ n_permute (int): Number of permutations to run. Default 5000.
695
+ n_jobs (int): Number of parallel jobs. Default -1 (all cores).
696
+
697
+ Returns:
698
+ dict: Per-group within-vs-between distance differences and p-values, keyed
699
+ by group label.
700
+ """
701
+ from .stats import _stats_label_distance
702
+
703
+ return _stats_label_distance(
704
+ self, labels=labels, n_permute=n_permute, n_jobs=n_jobs
705
+ )
706
+
707
+ def std(self, axis=0):
708
+ """Calculate standard deviation of Adjacency.
709
+
710
+ Args:
711
+ axis (int): Calculate std over matrices (0) or upper triangle (1).
712
+
713
+ Returns:
714
+ float | Adjacency | np.ndarray: A float for a single matrix; an
715
+ Adjacency when `axis=0`; an array when `axis=1`.
716
+ """
717
+ return _apply_stat(self, np.nanstd, axis)
718
+
719
+ def sum(self, axis=0):
720
+ """Calculate sum of Adjacency.
721
+
722
+ Args:
723
+ axis (int): Calculate sum over matrices (0) or upper triangle (1).
724
+
725
+ Returns:
726
+ float | Adjacency | np.ndarray: A float for a single matrix; an
727
+ Adjacency when `axis=0`; an array when `axis=1`.
728
+ """
729
+ return _apply_stat(self, np.nansum, axis)
730
+
731
+ def threshold(self, *, upper=None, lower=None, binarize=False):
732
+ """Threshold an Adjacency instance.
733
+
734
+ Provide upper and lower values or percentages to perform two-sided
735
+ thresholding. Binarize will return a mask image respecting thresholds
736
+ if provided, otherwise respecting every non-zero value.
737
+
738
+ Args:
739
+ upper (float | str, optional): Upper cutoff. A string such as `'95%'` is
740
+ interpreted as a percentile; None for one-sided thresholding.
741
+ lower (float | str, optional): Lower cutoff. A string such as `'5%'` is
742
+ interpreted as a percentile; None for one-sided thresholding.
743
+ binarize (bool): Return a binarized matrix respecting the thresholds if
744
+ provided, otherwise binarize on every non-zero value. Default False.
745
+
746
+ Returns:
747
+ Adjacency: Thresholded Adjacency instance.
748
+ """
749
+ from .stats import _threshold
750
+
751
+ return _threshold(self, upper=upper, lower=lower, binarize=binarize)
752
+
753
+ def to_graph(self):
754
+ """Convert a single Adjacency matrix into a NetworkX graph.
755
+
756
+ This currently works only when `is_single_matrix` is True.
757
+
758
+ Returns:
759
+ networkx.Graph | networkx.DiGraph: `DiGraph` for directed matrices,
760
+ `Graph` otherwise; nodes are relabeled with `labels` when set.
761
+ """
762
+ from .io import _to_graph
763
+
764
+ return _to_graph(self)
765
+
766
+ def to_square(self):
767
+ """Convert adjacency back to square matrix format.
768
+
769
+ This is an alias for `squareform`.
770
+
771
+ Returns:
772
+ np.ndarray | list[np.ndarray]: Square matrix representation, or a list
773
+ of them if this object contains multiple adjacency matrices.
774
+ """
775
+ return self.squareform()
776
+
777
+ def ttest(
778
+ self,
779
+ *,
780
+ popmean=0.0,
781
+ permutation=False,
782
+ n_permute=5000,
783
+ tail=2,
784
+ return_null=False,
785
+ n_jobs=-1,
786
+ random_state=None,
787
+ progress_bar: bool = False,
788
+ ):
789
+ """Run a one-sample t-test across stacked matrices.
790
+
791
+ Tests every stored edge against `popmean` across the matrices in the
792
+ stack.
793
+
794
+ Args:
795
+ popmean (float): Population mean to test against. Default 0.0.
796
+ permutation (bool): If True, take p from a sign-flip permutation
797
+ test on `matrices - popmean`. The reported `t` stays the
798
+ observed parametric statistic. Default False.
799
+ n_permute (int): Number of permutations, used only when
800
+ `permutation=True`. Default 5000.
801
+ tail (int | str): `2`/`'two'` (two-tailed, default) or `1`/`'one'`
802
+ (one-tailed: mean > `popmean`). Applies to both paths.
803
+ return_null (bool): If True, also return the permutation null. Has
804
+ no effect on the parametric path, which computes no null.
805
+ Default False.
806
+ n_jobs (int): Number of parallel jobs. Default -1 (all cores).
807
+ random_state (int, optional): Random seed for reproducibility.
808
+ progress_bar (bool): If True, show a progress bar. Default False.
809
+
810
+ Returns:
811
+ dict: `'mean'`, `'t'`, `'z'` and `'p'` as independent single-matrix
812
+ `Adjacency` results that retain the node count, storage kind
813
+ (including directed) and shared node labels, with matrix
814
+ metadata cleared. `'mean'` is the edgewise mean minus `popmean`;
815
+ `'t'` is the observed one-sample t-statistic on both paths;
816
+ `'p'` is parametric, or the empirical sign-flip p-value when
817
+ `permutation=True`; `'z'` is the tail-aware normal score of `p`.
818
+ With `permutation=True` and `return_null=True` the dict also
819
+ holds `'null_dist'`, an owned `(n_permute, n_edges)` array of
820
+ centered means in flat storage order and in the units of
821
+ `'mean'`. Maps are unthresholded. Apply a cutoff or a
822
+ multiple-comparison correction afterwards.
823
+
824
+ Raises:
825
+ ValueError: If this Adjacency holds fewer than two matrices.
826
+
827
+ Examples:
828
+ ```python
829
+ result = stacked.ttest()
830
+ result["mean"] # effect size per edge
831
+ result["t"].squareform() # back to a node-by-node matrix
832
+
833
+ # Threshold after testing, never inside it
834
+ import numpy as np
835
+
836
+ significant = result["t"].copy()
837
+ significant.data = np.where(result["p"].data < 0.05, result["t"].data, 0.0)
838
+ ```
839
+ """
840
+ from .stats import _ttest
841
+
842
+ return _ttest(
843
+ self,
844
+ popmean=popmean,
845
+ permutation=permutation,
846
+ n_permute=n_permute,
847
+ tail=tail,
848
+ return_null=return_null,
849
+ n_jobs=n_jobs,
850
+ random_state=random_state,
851
+ progress_bar=progress_bar,
852
+ )
853
+
854
+ def write(self, file_name, method="long"):
855
+ """Write the Adjacency to a `.csv` or `.h5` file.
856
+
857
+ HDF5 is the round-trip format: values, matrix kind, node labels, and
858
+ `Y`. CSV stores values only, so a CSV read back loses the labels, `Y`,
859
+ and the matrix kind, and needs an explicit `matrix_type` wherever the
860
+ flat layout is ambiguous. Square CSV output is single-matrix only.
861
+
862
+ Args:
863
+ file_name (str | Path): Output path; an `.h5`/`.hdf5` suffix writes HDF5.
864
+ method (str): Layout for CSV output, `'long'` (vectorized rows) or
865
+ `'square'` (single matrix only).
866
+ """
867
+ from .io import _write
868
+
869
+ return _write(self, file_name, method)
870
+
871
+ def z_to_r(self):
872
+ """Convert each z score back into an r value."""
873
+ from .stats import _z_to_r
874
+
875
+ return _z_to_r(self)