Skip to content

Commit 66e3ef5

Browse files
refactor: don't re-calculate side bias
1 parent 39e8a52 commit 66e3ef5

6 files changed

Lines changed: 33 additions & 94 deletions

File tree

src/dynamic_foraging_processing/qc/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
ProcessedQC,
3030
behavior_qc_results,
3131
calculate_lick_intervals,
32-
compute_rolling_bias,
33-
compute_side_bias,
3432
lick_interval_results,
3533
plot_lick_intervals,
3634
plot_side_bias,
@@ -53,8 +51,6 @@
5351
"bool_to_status",
5452
"build_quality_control",
5553
"calculate_lick_intervals",
56-
"compute_rolling_bias",
57-
"compute_side_bias",
5854
"contract_qc_metrics",
5955
"lick_interval_results",
6056
"make_metric",

src/dynamic_foraging_processing/qc/processed/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
from dynamic_foraging_processing.qc.processed.behavior import (
44
calculate_lick_intervals,
5-
compute_rolling_bias,
6-
compute_side_bias,
75
lick_interval_results,
86
side_bias_result,
97
)
@@ -18,8 +16,6 @@
1816
"ProcessedQC",
1917
"behavior_qc_results",
2018
"calculate_lick_intervals",
21-
"compute_rolling_bias",
22-
"compute_side_bias",
2319
"lick_interval_results",
2420
"plot_lick_intervals",
2521
"plot_side_bias",

src/dynamic_foraging_processing/qc/processed/behavior.py

Lines changed: 15 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717
SIDE_BIAS_PLOT = "side_bias.png"
1818
LICK_INTERVALS_PLOT = "lick_intervals.png"
1919

20-
#: ``animal_response`` choice codes.
21-
_LEFT, _RIGHT, _IGNORE = 0, 1, 2
22-
2320

2421
def calculate_lick_intervals(
2522
left_lick_times: np.ndarray, right_lick_times: np.ndarray
@@ -105,92 +102,36 @@ def calculate_lick_intervals(
105102
}
106103

107104

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.
110107
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.
114110
115111
Parameters
116112
----------
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``.
179116
180117
Returns
181118
-------
182119
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
185122
referencing ``side_bias.png``.
186123
"""
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))
188129
name = "average side bias"
189130
return QCResult(
190131
name=name,
191132
value=mean_bias,
192133
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).",
194135
reference=SIDE_BIAS_PLOT,
195136
tags={"behavior": name},
196137
)

src/dynamic_foraging_processing/qc/processed/plots.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from dynamic_foraging_processing.qc.processed.behavior import (
1919
LICK_INTERVALS_PLOT,
2020
SIDE_BIAS_PLOT,
21-
compute_rolling_bias,
2221
)
2322

2423

@@ -86,18 +85,17 @@ def plot_lick_intervals(
8685
return LICK_INTERVALS_PLOT
8786

8887

89-
def _add_bias_plot(ax: plt.Axes, animal_response: np.ndarray, window: int) -> None:
90-
"""Draw the rolling side-bias trace with its confidence band."""
88+
def _add_bias_plot(ax: plt.Axes, side_bias: np.ndarray) -> None:
89+
"""Draw the per-trial side-bias trace from the trial-table column."""
9190
ax.set_xlabel("Trial #")
9291
ax.set_ylabel("Side Bias")
9392
ax.axhline(+0.7, color="r", linestyle="--")
9493
ax.axhline(-0.7, color="r", linestyle="--")
9594
ax.axhline(0, color="k", linestyle="--")
9695
ax.set_ylim([-1, +1])
9796

98-
bias, ci = compute_rolling_bias(animal_response, window=window)
97+
bias = np.asarray(side_bias, dtype=float)
9998
trials = np.arange(len(bias))
100-
ax.fill_between(trials, ci[:, 0], ci[:, 1], color="gray", alpha=0.5)
10199
ax.plot(trials, bias, "k", linewidth=2)
102100
if len(bias):
103101
ax.set_xlim([0, len(bias)])
@@ -236,6 +234,7 @@ def _add_reward_probabilities(
236234

237235
def plot_side_bias(
238236
animal_response: np.ndarray,
237+
side_bias: np.ndarray,
239238
results_folder: str,
240239
*,
241240
lickspout_x: t.Optional[np.ndarray] = None,
@@ -251,17 +250,18 @@ def plot_side_bias(
251250
autowater_right: t.Optional[np.ndarray] = None,
252251
manual_left_times: t.Optional[np.ndarray] = None,
253252
manual_right_times: t.Optional[np.ndarray] = None,
254-
bias_window: int = 20,
255253
) -> str:
256254
"""Save the four-panel side-bias figure.
257255
258-
Panels: rolling side bias (with CI), lickspout position, behavior raster,
259-
and reward probabilities.
256+
Panels: per-trial side bias, lickspout position, behavior raster, and
257+
reward probabilities.
260258
261259
Parameters
262260
----------
263261
animal_response : numpy.ndarray
264262
Per-trial choice codes (``0`` left, ``1`` right, ``2`` ignore).
263+
side_bias : numpy.ndarray
264+
Per-trial side bias from the trial table (right minus left).
265265
results_folder : str
266266
Directory to write ``side_bias.png`` into.
267267
lickspout_x, lickspout_y1, lickspout_y2, lickspout_z : numpy.ndarray, optional
@@ -276,8 +276,6 @@ def plot_side_bias(
276276
Per-trial autowater indicator arrays.
277277
manual_left_times, manual_right_times : numpy.ndarray, optional
278278
Manual-water delivery timestamps (s).
279-
bias_window : int, optional
280-
Trailing window for the rolling bias. Defaults to ``20``.
281279
282280
Returns
283281
-------
@@ -290,7 +288,7 @@ def plot_side_bias(
290288
axis.spines["top"].set_visible(False)
291289
axis.spines["right"].set_visible(False)
292290

293-
_add_bias_plot(ax[0], animal_response, bias_window)
291+
_add_bias_plot(ax[0], side_bias)
294292
_add_lickspout_position_plot(ax[1], lickspout_x, lickspout_y1, lickspout_y2, lickspout_z)
295293
_add_behavior_plot(
296294
ax[2],

src/dynamic_foraging_processing/qc/processed/results.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
def behavior_qc_results(
2323
animal_response: np.ndarray,
24+
side_bias: np.ndarray,
2425
left_lick_times: np.ndarray,
2526
right_lick_times: np.ndarray,
2627
results_folder: t.Optional[str] = None,
@@ -50,6 +51,8 @@ def behavior_qc_results(
5051
----------
5152
animal_response : numpy.ndarray
5253
Per-trial choice codes (``0`` left, ``1`` right, ``2`` ignore).
54+
side_bias : numpy.ndarray
55+
Per-trial side bias from the trial table (right minus left).
5356
left_lick_times, right_lick_times : numpy.ndarray
5457
Timestamps (s) of left/right-port licks.
5558
results_folder : str, optional
@@ -68,12 +71,13 @@ def behavior_qc_results(
6871
The average-side-bias result followed by the four lick-interval results.
6972
"""
7073
results = [
71-
side_bias_result(animal_response),
74+
side_bias_result(side_bias),
7275
*lick_interval_results(left_lick_times, right_lick_times),
7376
]
7477
if results_folder is not None:
7578
plot_side_bias(
7679
animal_response,
80+
side_bias,
7781
results_folder,
7882
lickspout_x=lickspout_x,
7983
lickspout_y1=lickspout_y1,

src/dynamic_foraging_processing/qc/processed/stage.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ProcessedQC(BaseQC):
2121
def run(
2222
self,
2323
animal_response: np.ndarray,
24+
side_bias: np.ndarray,
2425
left_lick_times: np.ndarray,
2526
right_lick_times: np.ndarray,
2627
results_folder: t.Optional[str] = None,
@@ -49,6 +50,8 @@ def run(
4950
----------
5051
animal_response : numpy.ndarray
5152
Per-trial choice codes (``0`` left, ``1`` right, ``2`` ignore).
53+
side_bias : numpy.ndarray
54+
Per-trial side bias from the trial table (right minus left).
5255
left_lick_times, right_lick_times : numpy.ndarray
5356
Timestamps (s) of left/right-port licks.
5457
results_folder : str, optional
@@ -67,6 +70,7 @@ def run(
6770
"""
6871
results = behavior_qc_results(
6972
animal_response,
73+
side_bias,
7074
left_lick_times,
7175
right_lick_times,
7276
results_folder,

0 commit comments

Comments
 (0)