|
1 | 1 | # -*- coding: utf-8 -*-
|
2 | 2 | import os
|
| 3 | +from functools import lru_cache |
3 | 4 | import numpy as np
|
4 | 5 | from numpy import ones, kron, mean, eye, hstack, tile
|
5 | 6 | from numpy.linalg import pinv
|
@@ -86,44 +87,61 @@ def _list_outputs(self):
|
86 | 87 | return outputs
|
87 | 88 |
|
88 | 89 |
|
89 |
| -def ICC_rep_anova(Y): |
| 90 | +@lru_cache(maxsize=1) |
| 91 | +def ICC_projection_matrix(shape): |
| 92 | + nb_subjects, nb_conditions = shape |
| 93 | + |
| 94 | + x = kron(eye(nb_conditions), ones((nb_subjects, 1))) # sessions |
| 95 | + x0 = tile(eye(nb_subjects), (nb_conditions, 1)) # subjects |
| 96 | + X = hstack([x, x0]) |
| 97 | + return X @ pinv(X.T @ X, hermitian=True) @ X.T |
| 98 | + |
| 99 | + |
| 100 | +def ICC_rep_anova(Y, projection_matrix=None): |
90 | 101 | """
|
91 | 102 | the data Y are entered as a 'table' ie subjects are in rows and repeated
|
92 | 103 | measures in columns
|
93 | 104 |
|
94 | 105 | One Sample Repeated measure ANOVA
|
95 | 106 |
|
96 | 107 | Y = XB + E with X = [FaTor / Subjects]
|
97 |
| - """ |
98 | 108 |
|
| 109 | + ``ICC_rep_anova`` involves an expensive operation to compute a projection |
| 110 | + matrix, which depends only on the shape of ``Y``, which is computed by |
| 111 | + calling ``ICC_projection_matrix(Y.shape)``. If arrays of multiple shapes are |
| 112 | + expected, it may be worth pre-computing and passing directly as an |
| 113 | + argument to ``ICC_rep_anova``. |
| 114 | +
|
| 115 | + If only one ``Y.shape`` will occur, you do not need to explicitly handle |
| 116 | + these, as the most recently calculated matrix is cached automatically. |
| 117 | + For example, if you are running the same computation on every voxel of |
| 118 | + an image, you will see significant speedups. |
| 119 | +
|
| 120 | + If a ``Y`` is passed with a new shape, a new matrix will be calculated |
| 121 | + automatically. |
| 122 | + """ |
99 | 123 | [nb_subjects, nb_conditions] = Y.shape
|
100 | 124 | dfc = nb_conditions - 1
|
101 |
| - dfe = (nb_subjects - 1) * dfc |
102 | 125 | dfr = nb_subjects - 1
|
| 126 | + dfe = dfr * dfc |
103 | 127 |
|
104 | 128 | # Compute the repeated measure effect
|
105 | 129 | # ------------------------------------
|
106 | 130 |
|
107 | 131 | # Sum Square Total
|
108 |
| - mean_Y = mean(Y) |
109 |
| - SST = ((Y - mean_Y) ** 2).sum() |
110 |
| - |
111 |
| - # create the design matrix for the different levels |
112 |
| - x = kron(eye(nb_conditions), ones((nb_subjects, 1))) # sessions |
113 |
| - x0 = tile(eye(nb_subjects), (nb_conditions, 1)) # subjects |
114 |
| - X = hstack([x, x0]) |
| 132 | + demeaned_Y = Y - mean(Y) |
| 133 | + SST = np.sum(demeaned_Y**2) |
115 | 134 |
|
116 | 135 | # Sum Square Error
|
117 |
| - predicted_Y = X @ (pinv(X.T @ X, hermitian=True) @ (X.T @ Y.flatten("F"))) |
118 |
| - residuals = Y.flatten("F") - predicted_Y |
119 |
| - SSE = (residuals**2).sum() |
120 |
| - |
121 |
| - residuals.shape = Y.shape |
| 136 | + if projection_matrix is None: |
| 137 | + projection_matrix = ICC_projection_matrix(Y.shape) |
| 138 | + residuals = Y.flatten("F") - (projection_matrix @ Y.flatten("F")) |
| 139 | + SSE = np.sum(residuals**2) |
122 | 140 |
|
123 | 141 | MSE = SSE / dfe
|
124 | 142 |
|
125 |
| - # Sum square session effect - between colums/sessions |
126 |
| - SSC = ((mean(Y, 0) - mean_Y) ** 2).sum() * nb_subjects |
| 143 | + # Sum square session effect - between columns/sessions |
| 144 | + SSC = np.sum(mean(demeaned_Y, 0) ** 2) * nb_subjects |
127 | 145 | MSC = SSC / dfc / nb_subjects
|
128 | 146 |
|
129 | 147 | session_effect_F = MSC / MSE
|
|
0 commit comments