|
17 | 17 | SIDE_BIAS_PLOT = "side_bias.png" |
18 | 18 | LICK_INTERVALS_PLOT = "lick_intervals.png" |
19 | 19 |
|
20 | | -#: ``animal_response`` choice codes. |
21 | | -_LEFT, _RIGHT, _IGNORE = 0, 1, 2 |
22 | | - |
23 | 20 |
|
24 | 21 | def calculate_lick_intervals( |
25 | 22 | left_lick_times: np.ndarray, right_lick_times: np.ndarray |
@@ -105,92 +102,36 @@ def calculate_lick_intervals( |
105 | 102 | } |
106 | 103 |
|
107 | 104 |
|
108 | | -def compute_side_bias(animal_response: np.ndarray) -> float: |
109 | | - """Compute the average side bias over responded trials. |
| 105 | +def side_bias_result(side_bias: np.ndarray) -> QCResult: |
| 106 | + """Build the average-side-bias ``QCResult`` from the trial-table column. |
110 | 107 |
|
111 | | - Bias is ``mean(is_right) - mean(is_left)`` across trials where the animal |
112 | | - responded (``ignore`` trials excluded). The result lies in ``[-1, +1]``; |
113 | | - positive means a rightward bias. |
| 108 | + The per-trial side bias is read directly from the trial table rather than |
| 109 | + recomputed; this check averages it over the session. |
114 | 110 |
|
115 | 111 | Parameters |
116 | 112 | ---------- |
117 | | - animal_response : numpy.ndarray |
118 | | - Per-trial choice codes (``0`` left, ``1`` right, ``2`` ignore). |
119 | | -
|
120 | | - Returns |
121 | | - ------- |
122 | | - float |
123 | | - The average side bias, or ``nan`` if no trial was responded to. |
124 | | - """ |
125 | | - responses = np.asarray(animal_response) |
126 | | - responded = responses != _IGNORE |
127 | | - if not responded.any(): |
128 | | - return float("nan") |
129 | | - chosen = responses[responded] |
130 | | - return float(np.mean(chosen == _RIGHT) - np.mean(chosen == _LEFT)) |
131 | | - |
132 | | - |
133 | | -def compute_rolling_bias( |
134 | | - animal_response: np.ndarray, window: int = 20 |
135 | | -) -> t.Tuple[np.ndarray, np.ndarray]: |
136 | | - """Compute a trailing-window side-bias trace with a normal-approx CI. |
137 | | -
|
138 | | - For each trial, the bias is ``p_right - p_left`` over responded trials in |
139 | | - the trailing ``window``; the confidence band is a 95% normal approximation |
140 | | - for that difference. Replaces the GUI-precomputed ``B_Bias`` / ``B_Bias_CI``. |
141 | | -
|
142 | | - Parameters |
143 | | - ---------- |
144 | | - animal_response : numpy.ndarray |
145 | | - Per-trial choice codes (``0`` left, ``1`` right, ``2`` ignore). |
146 | | - window : int, optional |
147 | | - Number of trailing trials in the rolling window. Defaults to ``20``. |
148 | | -
|
149 | | - Returns |
150 | | - ------- |
151 | | - tuple of numpy.ndarray |
152 | | - ``(bias, ci)`` where ``bias`` has shape ``(n_trials,)`` and ``ci`` has |
153 | | - shape ``(n_trials, 2)``. Trials with no responses are ``nan``. |
154 | | - """ |
155 | | - responses = np.asarray(animal_response) |
156 | | - n = len(responses) |
157 | | - bias = np.full(n, np.nan) |
158 | | - ci = np.full((n, 2), np.nan) |
159 | | - for i in range(n): |
160 | | - window_slice = responses[max(0, i - window + 1) : i + 1] |
161 | | - chosen = window_slice[window_slice != _IGNORE] |
162 | | - if len(chosen) == 0: |
163 | | - continue |
164 | | - p_right = float(np.mean(chosen == _RIGHT)) |
165 | | - b = 2.0 * p_right - 1.0 # p_right - p_left, since p_left + p_right == 1 |
166 | | - se = 2.0 * np.sqrt(p_right * (1.0 - p_right) / len(chosen)) |
167 | | - bias[i] = b |
168 | | - ci[i] = (b - 1.96 * se, b + 1.96 * se) |
169 | | - return bias, ci |
170 | | - |
171 | | - |
172 | | -def side_bias_result(animal_response: np.ndarray) -> QCResult: |
173 | | - """Build the average-side-bias ``QCResult``. |
174 | | -
|
175 | | - Parameters |
176 | | - ---------- |
177 | | - animal_response : numpy.ndarray |
178 | | - Per-trial choice codes (``0`` left, ``1`` right, ``2`` ignore). |
| 113 | + side_bias : numpy.ndarray |
| 114 | + Per-trial side bias from the trial table (right minus left); positive |
| 115 | + means a rightward bias. Trials with no response are ``nan``. |
179 | 116 |
|
180 | 117 | Returns |
181 | 118 | ------- |
182 | 119 | QCResult |
183 | | - Passes when ``abs(mean_bias) < 0.5``. Fails when no trial was responded |
184 | | - to (``mean_bias`` is ``nan``). Tagged ``{"behavior": ...}`` and |
| 120 | + Passes when ``abs(mean_bias) < 0.5``. Fails when the column is empty or |
| 121 | + all ``nan`` (``mean_bias`` is ``nan``). Tagged ``{"behavior": ...}`` and |
185 | 122 | referencing ``side_bias.png``. |
186 | 123 | """ |
187 | | - mean_bias = compute_side_bias(animal_response) |
| 124 | + values = np.asarray(side_bias, dtype=float) |
| 125 | + if values.size == 0 or np.all(np.isnan(values)): |
| 126 | + mean_bias = float("nan") |
| 127 | + else: |
| 128 | + mean_bias = float(np.nanmean(values)) |
188 | 129 | name = "average side bias" |
189 | 130 | return QCResult( |
190 | 131 | name=name, |
191 | 132 | value=mean_bias, |
192 | 133 | passed=bool(abs(mean_bias) < 0.5), # nan comparisons are False -> fails |
193 | | - description="Average side bias over responded trials (right minus left).", |
| 134 | + description="Average of the per-trial side bias (right minus left).", |
194 | 135 | reference=SIDE_BIAS_PLOT, |
195 | 136 | tags={"behavior": name}, |
196 | 137 | ) |
|
0 commit comments