Reproducing Fouke et al. (2025) from dandiset 001453¶
Paper. Fouke KE, He Z, Loring MD, Naumann EA (2025). Neural circuits underlying divergent visuomotor strategies of zebrafish and Danionella cerebrum. Current Biology. doi:10.1016/j.cub.2025.04.027. This attempt used the full text of the accepted author manuscript in PubMed Central (PMC12218792); the version of record is not open access.
Dandiset. DANDI:001453, version
0.250518.1950. The paper cites no DANDI version (its data statement points to a GitLab repository
that holds code only). This is the only published version. The draft holds the same 37 files
(same blobs and sizes); it differs only in metadata added later (authors, keywords, and a
relatedResource link to the paper), none of which the analysis uses.
What the dandiset holds. Freely swimming tracking of 25 zebrafish (ZF) and 12 D. cerebrum (DC) larvae during the optomotor protocol of Figure 1 and Figure S1. The two-photon imaging and the head-fixed tail tracking of Figures 2 to 4 are not in it.
Outside inputs. The stimulus codes in the files have no names. The names come from the paper's
analysis notebook Fig1-S1.ipynb (GitLab ean26/danionella_omr_neuralcircuits2025, commit
33a5c29) and the protocol file of the stimulus software pandastim (GitHub
Naumann-Lab/pandastim, commit 7d9d161). The spatial scale (10 pixels per mm) is the factor in
Fig1-S1.ipynb. Claims marked "with inputs" use these and depend on the link claim.
No random numbers are drawn anywhere in this notebook.
| Claim | Data | Status |
|---|---|---|
| stimulus-code-link | with inputs | tested |
| fig1d-dc-faster-all-codes | dandiset | tested |
| fig1d-median-velocity | with inputs | tested |
| fig1d-zf-forward-faster-than-static | with inputs | tested |
| fig1d-dc-no-modulation | with inputs | tested |
| fig1d-dc-faster-oblique | with inputs | tested |
| fig1g-follow-direction | with inputs | tested |
| fig1h-zf-higher-angular-acceleration | with inputs | tested |
| figs1e-dc-respond-less-monocular | with inputs | tested |
| fig1c-continuous-vs-bouts | dandiset | tested |
| fig1cef-representative-values | dandiset | blocked: the representative fish and trials cannot be identified |
| fig2d, fig2g, fig2h, fig2k, fig2l (imaging) | dandiset | blocked: data missing |
| fig3e, fig3cd (head-fixed tail tracking) | dandiset | blocked: data missing |
| fig4f, fig4hi (imaging with tail tracking) | dandiset | blocked: data missing |
import time
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import statsmodels.api as sm
from scipy import stats
from statsmodels.formula.api import ols
from statsmodels.stats.multicomp import pairwise_tukeyhsd
import omr
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 200)
warnings.filterwarnings("ignore", category=RuntimeWarning)
started = time.time()
sessions = omr.load_all()
print(f"loaded {len(sessions)} files from dandiset {omr.DANDISET_ID} version {omr.DANDISET_VERSION} "
f"in {time.time() - started:.0f} s")
loaded 37 files from dandiset 001453 version 0.250518.1950 in 92 s
Data check¶
Each check of the metadata against the values is stated under its output.
info = pd.DataFrame([{
"fish": omr.fish_key(s), "species": s["species"], "subject_id": s["subject_id"], "age": s["age"],
"session_start": s["session_start"][:19], "frames": s["n_frames"], "tracked": s["n_tracked"],
"frame_hz": round(s["frame_hz"], 1), "x_max": round(s["x_range"][1]), "y_max": round(s["y_range"][1]),
"has_dist": s["has_dist"], "same_timestamps": s["same_timestamps"]} for s in sessions])
print("Files per species:")
print(info.species.value_counts().to_string())
print("\nSubject ids used by both species:",
sorted(set(info[info.species == "Danio rerio"].subject_id) & set(info[info.species != "Danio rerio"].subject_id), key=int))
print("Distinct subject ids:", info.subject_id.nunique(), "; distinct fish (species and id):", info.fish.nunique())
print("Age field values:", info.age.unique().tolist())
print("Paper, Figure 1D legend: N = ZF 26, DC 12 fish.")
print("\nEvery series in a file shares one timestamp array:", bool(info.same_timestamps.all()))
print("\nStated units:", sessions[0]["units"])
print("\n", info.sort_values(["species", "frame_hz"]).to_string(index=False))
Files per species:
species
Danio rerio 25
Danionella cerebrum 12
Subject ids used by both species: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
Distinct subject ids: 25 ; distinct fish (species and id): 37
Age field values: ['P5D/P9D']
Paper, Figure 1D legend: N = ZF 26, DC 12 fish.
Every series in a file shares one timestamp array: True
Stated units: {'f0_theta': 'radians', 'f0_vtheta': 'radians/s', 'f0_vx': 'pixels/s', 'f0_vy': 'pixels/s', 'f0_x': 'pixels', 'f0_y': 'pixels', 'stim_index': 'a.u.', 'stimulus': 'a.u.'}
fish species subject_id age session_start frames tracked frame_hz x_max y_max has_dist same_timestamps
ZF-sub-12 Danio rerio 12 P5D/P9D 2023-06-14T17:10:42 261221 225233 32.5 1120 1112 False True
ZF-sub-13 Danio rerio 13 P5D/P9D 2023-06-15T10:58:05 336304 177571 32.5 1114 1108 False True
ZF-sub-14 Danio rerio 14 P5D/P9D 2023-06-14T16:15:58 377090 285286 32.5 1117 1109 False True
ZF-sub-16 Danio rerio 16 P5D/P9D 2023-07-25T17:51:46 335384 245033 32.5 1118 1120 False True
ZF-sub-17 Danio rerio 17 P5D/P9D 2023-07-25T17:52:10 257365 90910 32.5 1109 1116 False True
ZF-sub-18 Danio rerio 18 P5D/P9D 2023-07-25T17:52:21 336498 289231 32.5 1106 1115 False True
ZF-sub-19 Danio rerio 19 P5D/P9D 2023-07-25T17:52:42 257126 164421 32.5 1108 1122 False True
ZF-sub-7 Danio rerio 7 P5D/P9D 2023-06-14T16:15:40 289185 196772 32.5 1125 1114 False True
ZF-sub-8 Danio rerio 8 P5D/P9D 2023-06-15T10:57:49 297959 191485 32.5 1116 1111 False True
ZF-sub-11 Danio rerio 11 P5D/P9D 2023-06-20T11:18:55 2106421 1523624 34.4 1117 1119 False True
ZF-sub-10 Danio rerio 10 P5D/P9D 2023-06-20T11:16:37 219788 191617 34.6 1116 1110 False True
ZF-sub-15 Danio rerio 15 P5D/P9D 2023-06-20T11:19:27 499605 261639 34.6 1117 1114 False True
ZF-sub-22 Danio rerio 22 P5D/P9D 2023-07-25T17:55:47 213430 170901 34.6 1119 1114 False True
ZF-sub-23 Danio rerio 23 P5D/P9D 2023-07-25T17:56:02 195013 179448 34.6 1115 1121 False True
ZF-sub-20 Danio rerio 20 P5D/P9D 2023-07-25T17:55:23 260498 193449 34.7 1117 1123 False True
ZF-sub-21 Danio rerio 21 P5D/P9D 2023-07-25T17:55:36 297073 204713 34.7 1107 1121 False True
ZF-sub-24 Danio rerio 24 P5D/P9D 2023-07-25T17:56:21 296522 240050 34.7 1107 1119 False True
ZF-sub-9 Danio rerio 9 P5D/P9D 2023-06-15T10:57:55 133180 108757 34.7 1116 1110 False True
ZF-sub-1 Danio rerio 1 P5D/P9D 2023-06-14T16:31:57 642085 424726 167.2 1113 1106 False True
ZF-sub-3 Danio rerio 3 P5D/P9D 2023-06-15T10:57:40 901227 678420 167.2 1107 1124 False True
ZF-sub-4 Danio rerio 4 P5D/P9D 2023-06-20T11:16:09 1108079 543629 167.2 1128 1115 False True
ZF-sub-5 Danio rerio 5 P5D/P9D 2023-06-20T11:21:23 324257 317560 167.2 1106 1105 False True
ZF-sub-6 Danio rerio 6 P5D/P9D 2023-06-14T16:15:31 1709839 1528467 167.2 1123 1118 False True
ZF-sub-0 Danio rerio 0 P5D/P9D 2023-06-20T11:08:54 9842588 5765859 170.7 1123 1114 False True
ZF-sub-2 Danio rerio 2 P5D/P9D 2023-06-15T10:57:22 8646552 2903548 170.7 1114 1118 False True
DC-sub-4 Danionella cerebrum 4 P5D/P9D 2023-10-31T15:50:00 55697 55697 34.1 1113 1113 True True
DC-sub-11 Danionella cerebrum 11 P5D/P9D 2024-05-22T00:24:07 633139 633139 128.0 1012 1011 True True
DC-sub-5 Danionella cerebrum 5 P5D/P9D 2023-10-31T15:58:36 67372 67372 128.0 1456 1490 True True
DC-sub-8 Danionella cerebrum 8 P5D/P9D 2024-05-21T16:31:07 775349 775349 128.0 1013 1013 True True
DC-sub-0 Danionella cerebrum 0 P5D/P9D 2023-10-31T13:55:10 85373 85373 146.3 1106 1109 True True
DC-sub-1 Danionella cerebrum 1 P5D/P9D 2023-10-31T13:55:58 254638 254638 146.3 1107 1105 True True
DC-sub-10 Danionella cerebrum 10 P5D/P9D 2024-05-03T19:06:31 532103 532103 146.3 1016 1017 True True
DC-sub-9 Danionella cerebrum 9 P5D/P9D 2024-05-03T17:13:07 587016 587016 146.3 1012 1010 True True
DC-sub-2 Danionella cerebrum 2 P5D/P9D 2023-10-31T15:47:56 143633 143633 170.7 1107 1105 True True
DC-sub-3 Danionella cerebrum 3 P5D/P9D 2023-10-31T15:48:51 383384 383384 170.7 1107 1108 True True
DC-sub-6 Danionella cerebrum 6 P5D/P9D 2024-05-21T13:07:12 189534 189534 170.7 1009 946 True True
DC-sub-7 Danionella cerebrum 7 P5D/P9D 2024-05-21T14:33:06 1961958 1961958 170.7 1011 1011 True True
The archive's summary says 25 subjects and lists only Danio rerio. The files hold 37 fish:
subject ids 0 to 11 are each used for one zebrafish and one D. cerebrum, so subject_id alone
does not identify an animal. Fish are named here by species and subject id. The 25 ZF are one fewer
than the 26 of the Figure 1D legend; the paper's notebook (healthy_zf_fish) lists 25 ZF and 12 DC,
and its ANOVA has 305 residual degrees of freedom, which fits 25 and 12 fish over nine stimuli
less ten empty cells. The age is given as a range (P5D/P9D) for every fish.
fast = info.frame_hz > 100
print("Files by frame rate (Methods: tracking at more than 150 Hz):")
print(pd.crosstab(info.species, np.where(fast, "128-171 Hz", "32-35 Hz")).to_string())
dts = pd.DataFrame([{"fish": omr.fish_key(s), **{f"dt_{k}": v for k, v in s["dt_ms"].items()}} for s in sessions])
print("\nTimestamp intervals per file (ms), over all frames; within_10pct is the fraction of intervals within 10% of the median:")
print(dts.sort_values("dt_median").round(3).to_string(index=False))
print("Files whose median interval is below 6.67 ms (above 150 Hz):", int((dts.dt_median < 1000 / 150).sum()), "of", len(dts))
# What f0_vtheta is: compare its sum over a trial with the trial's change of f0_theta.
rows = []
for s in sessions:
ratio_sum, ratio_int, vx_frame, vx_sec = [], [], [], []
for number, c, idx in omr.trials(s):
if len(idx) < 50:
continue
th, vth, t = s["f0_theta"][idx], s["f0_vtheta"][idx], s["t"][idx]
if abs(th[-1] - th[0]) > 0.5:
ratio_sum.append(vth.sum() / (th[-1] - th[0]))
ratio_int.append(np.trapezoid(vth, t) / (th[-1] - th[0]))
dx = np.diff(s["f0_x"][idx])
vx = s["f0_vx"][idx][1:]
ok = np.abs(dx) > 0.5
if ok.sum() > 10:
vx_frame.append(np.median(vx[ok] / dx[ok]))
vx_sec.append(np.median(vx[ok] / (dx[ok] / np.diff(t)[ok])))
rows.append({"fish": omr.fish_key(s), "frame_hz": round(s["frame_hz"]),
"sum_vtheta/dtheta": np.median(ratio_sum), "integral_vtheta_dt/dtheta": np.median(ratio_int),
"vx/(dx per frame)": np.median(vx_frame), "vx/(dx per s)": np.median(vx_sec)})
units = pd.DataFrame(rows)
print("\nMedian over trials, per file:")
print(units.round(3).to_string(index=False))
print("\nAcross files, median of sum(vtheta)/dtheta:", round(units["sum_vtheta/dtheta"].median(), 3),
"; of integral(vtheta dt)/dtheta:", round(units["integral_vtheta_dt/dtheta"].median(), 4))
Files by frame rate (Methods: tracking at more than 150 Hz):
col_0 128-171 Hz 32-35 Hz
species
Danio rerio 7 18
Danionella cerebrum 11 1
Timestamp intervals per file (ms), over all frames; within_10pct is the fraction of intervals within 10% of the median:
fish dt_median dt_p5 dt_p95 dt_max dt_within_10pct
ZF-sub-0 5.859 3.906 7.812 656.250 0.427
DC-sub-6 5.859 3.906 13.672 4088667.969 0.354
DC-sub-2 5.859 5.859 7.812 2887641.602 0.748
ZF-sub-2 5.859 3.906 7.812 19.531 0.461
DC-sub-3 5.859 5.859 7.080 1699309.326 0.770
DC-sub-7 5.859 4.883 6.836 329480.469 0.634
ZF-sub-5 5.981 5.005 7.019 14.038 0.714
ZF-sub-1 5.981 5.005 7.080 16.846 0.720
ZF-sub-6 5.981 5.127 7.080 13.672 0.712
ZF-sub-3 5.981 5.005 7.080 17.090 0.717
ZF-sub-4 5.981 5.005 7.080 16.968 0.721
DC-sub-0 6.836 5.859 7.812 1412770.020 0.633
DC-sub-10 6.836 5.859 7.812 750478.027 0.485
DC-sub-1 6.836 5.859 7.812 2814250.000 0.598
DC-sub-9 6.836 5.859 7.812 687657.227 0.648
DC-sub-11 7.812 3.906 7.812 6377695.312 0.612
DC-sub-5 7.812 6.836 10.254 2621780.273 0.459
DC-sub-8 7.812 3.906 7.812 5828884.766 0.614
ZF-sub-24 28.809 27.832 29.907 58.105 0.992
ZF-sub-20 28.809 27.832 30.029 71.777 0.972
ZF-sub-9 28.809 27.832 29.785 58.105 0.993
ZF-sub-21 28.809 27.832 29.907 59.570 0.992
ZF-sub-10 28.900 27.832 29.907 58.167 0.992
ZF-sub-22 28.915 27.832 29.922 85.938 0.985
ZF-sub-15 28.923 27.832 30.029 74.707 0.988
ZF-sub-23 28.931 27.832 30.029 3762.205 0.962
ZF-sub-11 29.053 27.344 31.250 3786.110 0.991
DC-sub-4 29.297 27.832 30.273 956103.027 0.984
ZF-sub-19 30.762 29.785 31.738 3717.298 0.991
ZF-sub-18 30.762 29.785 31.860 3748.039 0.989
ZF-sub-17 30.762 29.785 31.860 46.875 0.991
ZF-sub-16 30.762 29.785 31.921 2948.212 0.988
ZF-sub-7 30.762 29.785 31.738 53.864 0.993
ZF-sub-12 30.762 29.785 31.738 46.387 0.996
ZF-sub-13 30.762 29.785 31.738 47.607 0.995
ZF-sub-14 30.762 29.785 31.738 46.875 0.992
ZF-sub-8 30.762 29.785 31.738 45.898 0.995
Files whose median interval is below 6.67 ms (above 150 Hz): 11 of 37
Median over trials, per file:
fish frame_hz sum_vtheta/dtheta integral_vtheta_dt/dtheta vx/(dx per frame) vx/(dx per s)
ZF-sub-0 171 0.987 0.006 0.991 0.006
DC-sub-0 146 0.889 0.005 0.764 0.005
ZF-sub-1 167 0.933 0.006 0.990 0.006
DC-sub-1 146 0.612 0.006 0.727 0.005
ZF-sub-10 35 0.988 0.029 1.069 0.031
DC-sub-10 146 0.508 0.007 0.837 0.006
ZF-sub-11 34 0.903 0.026 1.066 0.031
DC-sub-11 128 0.090 0.005 0.823 0.005
ZF-sub-12 33 0.953 0.029 1.057 0.032
ZF-sub-13 33 0.998 0.031 1.068 0.033
ZF-sub-14 33 0.997 0.031 1.069 0.033
ZF-sub-15 35 0.980 0.029 1.084 0.031
ZF-sub-16 33 0.958 0.029 0.974 0.030
ZF-sub-17 33 0.476 0.015 1.046 0.032
ZF-sub-18 33 0.996 0.031 0.927 0.028
ZF-sub-19 33 0.991 0.030 1.008 0.031
ZF-sub-2 171 0.919 0.006 0.981 0.006
DC-sub-2 171 0.469 0.005 0.733 0.004
ZF-sub-20 35 0.980 0.028 1.087 0.032
ZF-sub-21 35 0.997 0.029 1.110 0.032
ZF-sub-22 35 0.624 0.023 1.073 0.031
ZF-sub-23 35 0.998 0.029 1.056 0.031
ZF-sub-24 35 0.994 0.029 1.035 0.030
ZF-sub-3 167 0.984 0.006 1.000 0.006
DC-sub-3 171 0.794 0.004 0.733 0.005
ZF-sub-4 167 0.843 0.005 0.983 0.006
DC-sub-4 34 0.729 0.019 0.985 0.028
ZF-sub-5 167 0.938 0.006 1.002 0.006
DC-sub-5 128 0.488 0.006 0.807 0.006
ZF-sub-6 167 0.933 0.006 1.000 0.006
DC-sub-6 171 0.512 0.007 0.810 0.004
ZF-sub-7 33 -0.050 0.003 1.098 0.034
DC-sub-7 171 0.877 0.005 0.867 0.005
ZF-sub-8 33 0.982 0.030 1.084 0.033
DC-sub-8 128 0.322 0.004 0.633 0.004
ZF-sub-9 35 0.998 0.029 1.021 0.029
DC-sub-9 146 0.889 0.007 0.816 0.005
Across files, median of sum(vtheta)/dtheta: 0.933 ; of integral(vtheta dt)/dtheta: 0.0072
f0_vtheta is stated in radians/s. Summed over a trial's frames it gives the trial's change of
f0_theta (median ratio near 1 in most files), while its time integral gives about 1/frame rate
of it. It is the heading change per frame, in radians. The paper's code uses it that way (it sums
it to get the cumulative angle), so the analysis is unaffected, but the stated unit is wrong.
f0_vx and f0_vy are stated in pixels/s; the ratio to the per-frame displacement, printed above,
is the check: per frame it is 0.63 to 1.11, per second about the frame interval, so these are
pixels per frame. The paper's code converts them with the frame interval, which treats them as per
frame. They are not used below.
18 of 25 ZF files and 1 of 12 DC files are sampled at 32 to 35 Hz, against the Methods' "more than
150 Hz". The raw timestamp intervals show these are the rates of acquisition: in those files the
median interval is 28.8 to 30.8 ms and 96% to 99.6% of intervals are within 10% of it, and their
trials last a median 34.96 s (printed below), as the 35 s protocol has, so the timestamps are in
seconds. The other files have median intervals of 5.9 to 7.8 ms (128 to 171 Hz), quantised in
steps of about 1 ms, and only 11 of 37 files have a median interval below 6.67 ms (above 150 Hz).
The largest intervals in DC files (minutes to hours) are the gaps between trials. The ratio of f0_vtheta's sum to the change of f0_theta is lower in most DC files
(0.09 to 0.89) than in most ZF files, so in DC files the two series do not agree over a trial; the
analysis uses f0_vtheta, as the paper's code does. The measures below are reported by frame-rate group where it could matter.
# Spatial scale: trials start when the fish is in the central area, and end when it leaves the
# 45 mm active area. Positions are in pixels; no scale is given in the files.
rows = []
for s in sessions:
starts = np.array([idx[0] for _, c, idx in omr.trials(s) if c != 20])
inside = np.concatenate([idx for _, c, idx in omr.trials(s) if c != 20])
x, y = s["f0_x"], s["f0_y"]
rows.append({"fish": omr.fish_key(s),
"start_x_5_95": np.ptp(np.percentile(x[starts], [5, 95])),
"start_y_5_95": np.ptp(np.percentile(y[starts], [5, 95])),
"trial_x_1_99": np.ptp(np.percentile(x[inside], [1, 99])),
"trial_y_1_99": np.ptp(np.percentile(y[inside], [1, 99])),
"x_max": s["x_range"][1], "y_max": s["y_range"][1]})
extent = pd.DataFrame(rows).round(0)
print(extent.to_string(index=False))
print("\nMedian extent of trial start positions (px):", extent[["start_x_5_95", "start_y_5_95"]].median().to_dict())
print("Median extent of positions in trials (px):", extent[["trial_x_1_99", "trial_y_1_99"]].median().to_dict())
print("Files with any position beyond 1200 px:", extent[(extent.x_max > 1200) | (extent.y_max > 1200)].fish.tolist())
fish start_x_5_95 start_y_5_95 trial_x_1_99 trial_y_1_99 x_max y_max
ZF-sub-0 541.0 516.0 957.0 888.0 1123.0 1114.0
DC-sub-0 454.0 516.0 879.0 1025.0 1106.0 1109.0
ZF-sub-1 517.0 481.0 981.0 879.0 1113.0 1106.0
DC-sub-1 518.0 523.0 957.0 935.0 1107.0 1105.0
ZF-sub-10 526.0 517.0 919.0 900.0 1116.0 1110.0
DC-sub-10 533.0 519.0 951.0 944.0 1016.0 1017.0
ZF-sub-11 511.0 515.0 895.0 886.0 1117.0 1119.0
DC-sub-11 527.0 537.0 952.0 956.0 1012.0 1011.0
ZF-sub-12 847.0 850.0 833.0 840.0 1120.0 1112.0
ZF-sub-13 514.0 533.0 918.0 844.0 1114.0 1108.0
ZF-sub-14 921.0 781.0 994.0 965.0 1117.0 1109.0
ZF-sub-15 784.0 769.0 946.0 972.0 1117.0 1114.0
ZF-sub-16 527.0 469.0 975.0 827.0 1118.0 1120.0
ZF-sub-17 537.0 504.0 919.0 1003.0 1109.0 1116.0
ZF-sub-18 479.0 471.0 663.0 819.0 1106.0 1115.0
ZF-sub-19 529.0 537.0 910.0 712.0 1108.0 1122.0
ZF-sub-2 532.0 481.0 911.0 887.0 1114.0 1118.0
DC-sub-2 474.0 491.0 641.0 643.0 1107.0 1105.0
ZF-sub-20 541.0 449.0 963.0 982.0 1117.0 1123.0
ZF-sub-21 545.0 515.0 886.0 874.0 1107.0 1121.0
ZF-sub-22 944.0 779.0 1020.0 970.0 1119.0 1114.0
ZF-sub-23 518.0 524.0 889.0 768.0 1115.0 1121.0
ZF-sub-24 522.0 403.0 786.0 826.0 1107.0 1119.0
ZF-sub-3 536.0 499.0 906.0 943.0 1107.0 1124.0
DC-sub-3 537.0 506.0 805.0 839.0 1107.0 1108.0
ZF-sub-4 537.0 498.0 958.0 871.0 1128.0 1115.0
DC-sub-4 538.0 538.0 965.0 990.0 1113.0 1113.0
ZF-sub-5 534.0 399.0 924.0 746.0 1106.0 1105.0
DC-sub-5 497.0 548.0 1205.0 1156.0 1456.0 1490.0
ZF-sub-6 485.0 394.0 879.0 749.0 1123.0 1118.0
DC-sub-6 530.0 511.0 825.0 788.0 1009.0 946.0
ZF-sub-7 1039.0 962.0 1051.0 1037.0 1125.0 1114.0
DC-sub-7 543.0 541.0 937.0 892.0 1011.0 1011.0
ZF-sub-8 1007.0 1021.0 1043.0 1041.0 1116.0 1111.0
DC-sub-8 513.0 521.0 868.0 813.0 1013.0 1013.0
ZF-sub-9 541.0 468.0 828.0 824.0 1116.0 1110.0
DC-sub-9 532.0 522.0 904.0 905.0 1012.0 1010.0
Median extent of trial start positions (px): {'start_x_5_95': 532.0, 'start_y_5_95': 516.0}
Median extent of positions in trials (px): {'trial_x_1_99': 919.0, 'trial_y_1_99': 887.0}
Files with any position beyond 1200 px: ['DC-sub-5']
Trial start positions spread over about 500 pixels, and positions in trials over about 900 pixels, with a maximum near 1100 in almost every file. At the code's 10 pixels per mm this is a start area of about 50 mm and a field of about 110 mm, close to the 45 mm active area and the 100 mm dish of the Methods. The scale is consistent with the data, not shown by it. One DC file has positions up to about 1490 pixels, so its camera field, and possibly its scale, differs from the others.
trial_rows = []
for s in sessions:
for number, c, idx in omr.trials(s):
t = s["t"][idx]
trial_rows.append({"fish": omr.fish_key(s), "species": omr.fish_key(s)[:2], "code": c,
"trial": number, "duration_s": t[-1] - t[0]})
trial_table = pd.DataFrame(trial_rows)
print("Stimulus codes present:", sorted(trial_table.code.unique()))
counts = trial_table.pivot_table(index="fish", columns="code", values="trial", aggfunc="count", fill_value=0)
print("\nTrials per fish and stimulus code:")
print(counts.to_string())
print("\nFish without code 20 (static):", counts.index[counts[20] == 0].tolist())
blocks = pd.DataFrame([{"fish": omr.fish_key(s), "static_blocks": s["static_blocks"], "static_minutes": s["static_s"] / 60}
for s in sessions if s["static_blocks"]])
print("Code 20: trial numbers (stim_index) per fish:", blocks.static_blocks.value_counts().to_dict(),
"; length of the block (min):", blocks.static_minutes.min().round(1), "to", blocks.static_minutes.max().round(1))
print("\nTrial duration (s) by species, codes 0-19 (code 20 is cut to its first 60 s on loading):")
print(trial_table[trial_table.code != 20].groupby("species").duration_s.describe().round(1).to_string())
zf_rate = trial_table[(trial_table.species == "ZF") & (trial_table.code != 20)].fish.map(lambda f: "128-171 Hz" if info.set_index("fish").frame_hz[f] > 100 else "32-35 Hz")
print("\nZF trial duration (s) by frame rate of the file (protocol: 35 s per trial):")
print(trial_table[(trial_table.species == "ZF") & (trial_table.code != 20)].groupby(zf_rate).duration_s.describe().round(2).to_string())
print("\nFraction of trial frames tracked (positions present), by species:")
print(info.assign(sp=info.fish.str[:2], frac=info.tracked / info.frames).groupby("sp").frac.describe().round(3).to_string())
print("\nFiles with a `dist` series:", info.groupby(info.fish.str[:2]).has_dist.sum().to_dict())
Stimulus codes present: [np.int64(0), np.int64(1), np.int64(2), np.int64(3), np.int64(4), np.int64(5), np.int64(6), np.int64(7), np.int64(8), np.int64(9), np.int64(10), np.int64(11), np.int64(12), np.int64(13), np.int64(14), np.int64(15), np.int64(16), np.int64(17), np.int64(18), np.int64(19), np.int64(20)]
Trials per fish and stimulus code:
code 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
fish
DC-sub-0 2 1 2 3 3 2 3 2 2 3 3 2 2 1 2 2 3 2 3 3 0
DC-sub-1 3 5 4 5 5 4 5 3 4 4 4 5 5 3 4 5 5 3 5 5 0
DC-sub-10 6 7 7 7 7 7 7 5 7 5 8 7 7 7 7 6 6 6 7 8 1
DC-sub-11 7 5 7 8 5 6 8 9 7 8 6 8 7 8 7 3 4 4 7 6 1
DC-sub-2 3 2 4 3 3 5 5 4 4 4 1 3 3 5 3 4 2 2 3 2 1
DC-sub-3 5 6 6 7 6 5 7 6 6 7 7 6 7 6 6 5 5 6 5 6 0
DC-sub-4 5 5 6 5 5 5 5 5 5 5 5 5 5 6 5 5 5 6 5 5 1
DC-sub-5 1 2 1 1 2 1 2 1 1 2 2 1 1 1 2 1 1 1 1 1 1
DC-sub-6 3 7 4 6 5 8 5 6 7 6 4 7 3 8 8 5 6 4 8 4 1
DC-sub-7 8 10 10 10 10 10 10 10 9 10 10 10 10 10 10 9 10 9 10 10 1
DC-sub-8 7 6 8 8 7 8 6 8 7 6 9 6 7 7 6 5 8 6 4 8 1
DC-sub-9 6 7 7 8 7 7 7 7 7 6 8 7 7 7 7 7 7 7 7 8 1
ZF-sub-0 9 9 9 10 9 9 8 9 9 9 9 9 10 9 9 9 10 9 9 10 1
ZF-sub-1 4 1 3 2 6 4 5 4 5 4 3 3 4 4 3 3 3 4 5 2 0
ZF-sub-10 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-11 20 21 20 19 19 19 20 20 18 20 19 21 20 17 19 21 19 19 21 21 1
ZF-sub-12 7 7 7 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-13 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 6 6 0
ZF-sub-14 7 7 7 7 7 7 6 6 7 6 7 7 7 7 7 7 7 7 7 6 1
ZF-sub-15 7 7 7 6 6 7 5 6 7 6 6 7 7 7 6 7 6 7 7 7 1
ZF-sub-16 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-17 2 3 2 3 3 2 2 2 2 2 3 3 3 2 3 3 3 2 2 3 0
ZF-sub-18 7 7 7 7 7 7 6 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-19 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 0
ZF-sub-2 5 4 5 7 4 7 4 5 7 6 5 5 6 4 5 8 4 9 7 6 1
ZF-sub-20 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-21 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-22 7 7 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-23 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-24 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 1
ZF-sub-3 6 7 6 6 6 7 6 7 7 7 6 6 5 7 7 7 7 6 7 3 1
ZF-sub-4 3 4 4 5 5 6 5 4 6 3 3 4 4 6 4 3 4 4 5 3 1
ZF-sub-5 1 2 1 2 2 1 2 1 1 1 2 2 2 1 2 2 2 1 1 2 0
ZF-sub-6 7 7 7 6 7 7 6 7 6 5 6 6 6 6 7 7 6 7 7 7 1
ZF-sub-7 4 6 5 5 5 5 4 5 5 5 5 5 5 5 5 6 6 4 5 4 0
ZF-sub-8 6 6 5 6 5 4 6 6 5 6 7 7 6 6 6 6 7 6 5 7 0
ZF-sub-9 3 2 3 3 3 3 3 3 3 3 3 3 3 3 2 3 3 3 3 3 1
Fish without code 20 (static): ['DC-sub-0', 'DC-sub-1', 'DC-sub-3', 'ZF-sub-1', 'ZF-sub-13', 'ZF-sub-17', 'ZF-sub-19', 'ZF-sub-5', 'ZF-sub-7', 'ZF-sub-8']
Code 20: trial numbers (stim_index) per fish: {1: 25, 2: 2} ; length of the block (min): 0.2 to 787.3
Trial duration (s) by species, codes 0-19 (code 20 is cut to its first 60 s on loading):
count mean std min 25% 50% 75% max
species
DC 1297.0 67.4 817.5 1.7 7.0 13.3 32.4 14890.8
ZF 3198.0 26.3 11.0 0.3 15.8 35.0 35.0 41.2
ZF trial duration (s) by frame rate of the file (protocol: 35 s per trial):
count mean std min 25% 50% 75% max
fish
128-171 Hz 740.0 24.19 11.67 3.00 12.65 29.35 35.01 35.04
32-35 Hz 2458.0 26.98 10.67 0.28 17.30 34.96 34.99 41.22
Fraction of trial frames tracked (positions present), by species:
count mean std min 25% 50% 75% max
sp
DC 12.0 1.000 0.000 1.000 1.000 1.000 1.000 1.000
ZF 25.0 0.706 0.167 0.336 0.639 0.731 0.817 0.979
Files with a `dist` series: {'DC': 12, 'ZF': 0}
All files use codes 0 to 19, and 27 of 37 also code 20. The protocol file of the stimulus software
has 140 motion trials (20 stimuli, 7 repeats, each with a 3 s stationary phase and 35 s in all)
followed by a "blank" block of 4 hours (the files hold 0.2 to 787.3 min of it), which is code 20: the "static" condition of Figure 1D is the start of that block, one period per fish, after all motion trials (25 fish have one trial
number for code 20, two have two). Fig1-S1.ipynb handles code 20 like any trial number: its
pass_criteria keeps the first 35 s, and the measures use 3 to 35 s. The loader's 60 s cut keeps
that window, so static here is measured over the same frames as in the code, apart from the code's
filter on the DC vel column. 10 fish have no code 20. The
trials of DC are shorter than those of ZF (DC more often leave the active area, which ends a
trial). ZF files hold every frame, with untracked frames as NaN and frames between trials; DC files
hold only tracked trial frames, and carry an extra dist series. The two species were evidently
exported differently.
# Recordings deposited twice: compare frame counts, the sequence of stimulus codes, and the first
# tracked positions of every pair of files.
sig = []
for s in sessions:
seq = tuple(c for _, c, _ in omr.trials(s))
xs = s["f0_x"][~np.isnan(s["f0_x"])][:500]
sig.append((omr.fish_key(s), s["n_frames"], seq, xs))
pairs = []
for i in range(len(sig)):
for j in range(i + 1, len(sig)):
same_n = sig[i][1] == sig[j][1]
same_seq = sig[i][2] == sig[j][2]
n = min(len(sig[i][3]), len(sig[j][3]))
same_x = n > 0 and np.allclose(sig[i][3][:n], sig[j][3][:n])
if same_n or same_x or (same_seq and len(sig[i][2]) > 20):
pairs.append((sig[i][0], sig[j][0], same_n, same_seq, same_x))
print("Pairs sharing frame count, stimulus sequence or first positions:", pairs if pairs else "none")
print("Distinct stimulus sequences:", len({s[2] for s in sig}), "of", len(sig))
Pairs sharing frame count, stimulus sequence or first positions: [('ZF-sub-10', 'ZF-sub-16', False, True, False), ('ZF-sub-10', 'ZF-sub-20', False, True, False), ('ZF-sub-10', 'ZF-sub-21', False, True, False), ('ZF-sub-10', 'ZF-sub-23', False, True, False), ('ZF-sub-10', 'ZF-sub-24', False, True, False), ('ZF-sub-16', 'ZF-sub-20', False, True, False), ('ZF-sub-16', 'ZF-sub-21', False, True, False), ('ZF-sub-16', 'ZF-sub-23', False, True, False), ('ZF-sub-16', 'ZF-sub-24', False, True, False), ('ZF-sub-20', 'ZF-sub-21', False, True, False), ('ZF-sub-20', 'ZF-sub-23', False, True, False), ('ZF-sub-20', 'ZF-sub-24', False, True, False), ('ZF-sub-21', 'ZF-sub-23', False, True, False), ('ZF-sub-21', 'ZF-sub-24', False, True, False), ('ZF-sub-23', 'ZF-sub-24', False, True, False)]
Distinct stimulus sequences: 32 of 37
No two files share a frame count or their first positions. The pairs listed share only the sequence of stimulus codes: six ZF files ran the full fixed protocol of the stimulus software in the same order. No recording appears to be deposited twice.
Clock of the stimulus labels. The stimulus code and trial number are series on the same timestamps as the positions, so they share one clock by construction. Whether the labels are aligned with what the fish saw is checked with a contrast: turning to code 3 minus turning to code 2 (right and left motion, by the outside mapping; the check itself does not need the names), in 0.5 s bins from the trial start. If the labels are aligned, the difference should be near zero in the 3 s stationary phase and grow once the grating moves. With each trial given the code of the trial before it, the difference should vanish.
bins = np.arange(0, 20.5, 0.5)
def turning_course(shift):
out = {}
for s in sessions:
tr = list(omr.trials(s))
for k, (number, c, idx) in enumerate(tr):
label = tr[k - shift][1] if k - shift >= 0 else None
if label not in (2, 3):
continue
t = s["t"][idx] - s["t"][idx][0]
deg = np.degrees(s["f0_vtheta"][idx])
h = np.histogram(t, bins, weights=deg)[0] / 0.5
out.setdefault((omr.fish_key(s)[:2], label), []).append(h)
return out
rows = []
fig, axes = plt.subplots(1, 2, figsize=(10, 3.5), sharey=True)
for ax, shift in zip(axes, (0, 1)):
course = turning_course(shift)
for sp, colour in (("ZF", "0.4"), ("DC", "k")):
diff = np.mean(course[(sp, 3)], axis=0) - np.mean(course[(sp, 2)], axis=0)
ax.plot(bins[:-1] + 0.25, diff, color=colour, label=sp)
rows.append({"labels": "true" if shift == 0 else "previous trial", "species": sp,
"0-3 s": diff[bins[:-1] < 3].mean(), "3-20 s": diff[bins[:-1] >= 3].mean(),
"min": diff.min(), "max": diff.max()})
ax.axvline(3, ls="--", color="0.6")
ax.axhline(0, color="0.8")
ax.set_title("true labels" if shift == 0 else "labels of the previous trial")
ax.set_xlabel("time from trial start (s)")
axes[0].set_ylabel("turning, code 3 minus code 2 (deg/s)")
axes[0].legend()
plt.tight_layout()
plt.show()
print("Mean difference in turning (deg/s), code 3 minus code 2; axes autoscaled, all points shown:")
print(pd.DataFrame(rows).round(2).to_string(index=False))
course = turning_course(0)
zf_diff = np.mean(course[("ZF", 3)], axis=0) - np.mean(course[("ZF", 2)], axis=0)
print("\nZF, true labels, by 0.5 s bin from 0 to 6 s:", np.round(zf_diff[:12], 1).tolist())
Mean difference in turning (deg/s), code 3 minus code 2; axes autoscaled, all points shown:
labels species 0-3 s 3-20 s min max
true ZF 13.16 34.810001 2.580000 49.560001
true DC 9.81 4.560000 -15.790000 29.840000
previous trial ZF 7.85 6.730000 0.220000 16.700001
previous trial DC 14.98 0.120000 -17.889999 43.450001
ZF, true labels, by 0.5 s bin from 0 to 6 s: [16.799999237060547, 18.0, 16.5, 19.100000381469727, 2.5999999046325684, 5.900000095367432, 14.899999618530273, 19.0, 19.700000762939453, 40.20000076293945, 32.79999923706055, 36.29999923706055]
For ZF the contrast with the true labels is 13.2 deg/s in the stationary phase and 34.8 deg/s from 3 to 20 s; in 0.5 s bins it falls to 2.6 and 5.9 deg/s between 2 and 3 s and rises from 3 s on, to 40.2 deg/s at 4.5 to 5 s; with the labels of the previous trial it is 7.9 and 6.7 deg/s. The offset before 3 s is consistent with turning carried over from the previous trial (in the fixed protocol, right motion follows medial-right motion, and the trials after a code 3 trial start with a positive offset too). For ZF the stimulus labels are consistent with the timing of the response. For DC the contrast is small (4.6 deg/s from 3 to 20 s with true labels, 0.1 with shifted labels) and noisier in the stationary phase (9.8 and 15.0 deg/s); the timing of the DC labels could not be established by this check beyond that difference.
Trial measures¶
Every trial is processed as in Fig1-S1.ipynb (see the helper's docstring). The counts of trials
dropped, by reason, follow.
started = time.time()
measures = pd.concat([omr.trial_measures(s) for s in sessions], ignore_index=True)
measures["name"] = measures.code.map(omr.STIMULUS_NAMES)
hz = {omr.fish_key(s): s["frame_hz"] for s in sessions}
measures["rate"] = np.where(measures.fish.map(hz) > 100, "fast", "slow")
print(f"{len(measures)} trials measured in {time.time() - started:.0f} s")
print("\nTrials kept and dropped, by species and reason:")
print(measures.assign(reason=measures.reason.fillna("").replace("", "kept")).groupby(["species", "reason"]).size().to_string())
print("\nPassing trials per species:", measures[measures.passes].groupby("species").size().to_dict())
ok = measures[measures.passes]
print("Range of trial speed (mm/s) in passing trials:", ok.speed_mm_s.min().round(2), "to", ok.speed_mm_s.max().round(2))
print("Range of trial angular velocity (deg/s) in passing trials:", ok.angvel_deg_s.min().round(1), "to", ok.angvel_deg_s.max().round(1))
fast_trials = ok[ok.speed_mm_s > omr.MAX_SPEED_MM_S]
print("Passing trials whose measured speed exceeds 100 mm/s (the code's check uses 0 to 35 s and, for DC, "
"the stored `dist`):", fast_trials.groupby("species").size().to_dict())
print("\nThose trials: the code's check speed from `dist`, the same check from smoothed positions, and the "
"measured speed (3 to 35 s) and the length of the motion phase it is measured over (duration_s, s), mm/s:")
print(fast_trials[["fish", "code", "name", "trial", "n_frames", "duration_s", "check_speed_mm_s", "check_speed_pos_mm_s", "speed_mm_s"]]
.round(1).to_string(index=False))
# Variant rejection: the speed check from smoothed positions in DC files too.
measures["passes_pos"] = measures.speed_mm_s.notna() & (measures.cum10_deg <= omr.MAX_CUM_ANGLE_10S) & \
(measures.cum35_deg <= omr.MAX_CUM_ANGLE_35S) & (measures.check_speed_pos_mm_s <= omr.MAX_SPEED_MM_S)
print("\nTrials passing with the speed check from positions, by species:", measures[measures.passes_pos].groupby("species").size().to_dict(),
"; passing one check and not the other:", measures[measures.passes != measures.passes_pos].groupby("species").size().to_dict())
dc_m = measures[(measures.species == "DC") & measures.check_speed_mm_s.notna()]
ratio_check = dc_m.check_speed_mm_s / dc_m.check_speed_pos_mm_s
print("DC trials, ratio of the `dist` check speed to the position check speed: median",
round(ratio_check.median(), 3), "; 5th to 95th percentile", ratio_check.quantile([0.05, 0.95]).round(3).tolist())
4522 trials measured in 3 s
Trials kept and dropped, by species and reason:
species reason
DC criteria 27
kept 1264
no motion phase 15
ZF criteria 2
kept 3194
no motion phase 16
too short 4
Passing trials per species: {'DC': 1264, 'ZF': 3194}
Range of trial speed (mm/s) in passing trials: 0.01 to 1057.13
Range of trial angular velocity (deg/s) in passing trials: -422.5 to 764.6
Passing trials whose measured speed exceeds 100 mm/s (the code's check uses 0 to 35 s and, for DC, the stored `dist`): {'DC': 11, 'ZF': 1}
Those trials: the code's check speed from `dist`, the same check from smoothed positions, and the measured speed (3 to 35 s) and the length of the motion phase it is measured over (duration_s, s), mm/s:
fish code name trial n_frames duration_s check_speed_mm_s check_speed_pos_mm_s speed_mm_s
ZF-sub-0 16 forward_backward 102 633 0.9 26.3 26.3 100.6
DC-sub-1 5 forward_right 14 368 0.0 14.2 31.4 694.7
DC-sub-10 2 left 37 154 0.0 8.0 33.1 163.5
DC-sub-11 8 diverging 53 124 0.4 4.8 35.4 114.8
DC-sub-11 5 forward_right 54 156 0.0 5.0 41.7 1057.1
DC-sub-11 14 forward_x 68 168 0.1 5.0 32.1 179.2
DC-sub-11 0 forward 79 97 0.2 4.9 42.9 203.8
DC-sub-11 11 lateral_right 91 176 0.1 6.3 31.5 248.4
DC-sub-2 3 right 81 440 0.7 13.1 50.1 176.7
DC-sub-2 16 forward_backward 82 1332 7.3 8.5 96.5 132.1
DC-sub-2 17 backward_forward 138 423 0.4 8.6 50.8 109.1
DC-sub-2 20 static 140 332 14.6 21.9 92.5 106.5
Trials passing with the speed check from positions, by species: {'DC': 1264, 'ZF': 3194} ; passing one check and not the other: {}
DC trials, ratio of the `dist` check speed to the position check speed: median 1.112 ; 5th to 95th percentile [0.312, 3.792]
Twelve passing trials have a measured speed above 100 mm/s, 11 of them DC. For DC the code's
check uses the stored dist, which does not agree with the positions (the ratio of the two check
speeds has a median of 1.11 and a 5th to 95th percentile of 0.31 to 3.79). Computing the check from
positions does not change which trials pass: the 11 DC trials have position-based check speeds of
31.4 to 96.5 mm/s, under the 100 mm/s limit. Nine of them have a motion phase under 1 s, so their
speed is measured over a short stretch. Two do not: DC-sub-2 forward_backward, 132.1 mm/s over
7.3 s, and DC-sub-2 static, 106.5 mm/s over 14.6 s. The code's check is taken over 0 to 35 s, not
over the 3 to 35 s the speed is measured on, and with dist or with positions it does not catch
these trials.
stimulus-code-link¶
The dandiset's stimulus codes can be linked to the stimuli the paper names, using the paper's analysis code and the protocol file of its stimulus software, and the values computed through that link agree with those the paper's code printed.
Reference values: the per-stimulus medians printed by Fig1-S1.ipynb (commit 33a5c29), cells
for Figure 1D (speed) and Figure 1G (angular velocity).
CODE_PRINTED_SPEED = { # mm/s, Fig1-S1.ipynb output, codes in WHOLE_FIELD order
"DC": [11.9, 9.7, 10.0, 10.5, 10.5, 10.4, 8.8, 9.4, 9.0],
"ZF": [5.1, 8.8, 8.4, 6.7, 4.9, 4.3, 5.2, 6.4, 8.3]}
CODE_PRINTED_ANGVEL = { # deg/s
"DC": [-1.8, -2.0, 1.8, 8.7, 3.4, 5.0, -2.3, 0.3, 1.5],
"ZF": [2.3, 0.6, 6.6, 17.2, 7.7, 0.0, -21.5, -22.3, -1.4]}
used = [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 13, 20]
present = set(trial_table.code.unique())
print("Codes the analysis uses that are missing from the dandiset:", sorted(set(used) - present) or "none")
print("Fish per species here:", info.fish.str[:2].value_counts().to_dict(),
"; in Fig1-S1.ipynb's lists: ZF 25, DC 12; in the paper: ZF 26, DC 12")
speed_fish = omr.fish_medians(measures, "speed_mm_s")
angvel_fish = omr.fish_medians(measures, "angvel_deg_s")
rows = []
for sp in ("ZF", "DC"):
for k, c in enumerate(omr.WHOLE_FIELD):
sv = speed_fish[(speed_fish.species == sp) & (speed_fish.code == c)].speed_mm_s
av = angvel_fish[(angvel_fish.species == sp) & (angvel_fish.code == c)].angvel_deg_s
rows.append({"species": sp, "code": c, "name": omr.STIMULUS_NAMES[c], "n_fish": len(sv),
"speed_here": sv.median(), "speed_code": CODE_PRINTED_SPEED[sp][k],
"angvel_here": av.median(), "angvel_code": CODE_PRINTED_ANGVEL[sp][k]})
link = pd.DataFrame(rows)
link["speed_ratio"] = link.speed_here / link.speed_code
link["within_10pct"] = (link.speed_ratio - 1).abs() <= 0.10
print(link.round(2).to_string(index=False))
n_within = int(link.within_10pct.sum())
print(f"\nSpeed medians within 10% of the code's printed values: {n_within} of {len(link)}")
print("Median absolute difference in angular velocity (deg/s):",
round((link.angvel_here - link.angvel_code).abs().median(), 2))
# Variant: Fig1-S1.ipynb sets fish medians of exactly 0 to NaN before its statistics and printed medians.
nz = speed_fish[speed_fish.speed_mm_s != 0]
print("\nFish x stimulus speed medians of exactly 0 (whole-field stimuli):",
speed_fish[(speed_fish.speed_mm_s == 0) & speed_fish.code.isin(omr.WHOLE_FIELD)][["fish", "code"]].values.tolist())
link_nz = link[["species", "code", "name", "speed_code"]].copy()
link_nz["speed_here_no_zeros"] = [nz[(nz.species == r.species) & (nz.code == r.code)].speed_mm_s.median() for r in link_nz.itertuples()]
link_nz["within_10pct"] = (link_nz.speed_here_no_zeros / link_nz.speed_code - 1).abs() <= 0.10
print("With zero medians left out, speed medians within 10% of the printed values:", int(link_nz.within_10pct.sum()), "of 18;",
"DC static", round(link_nz[(link_nz.species == "DC") & (link_nz.code == 20)].speed_here_no_zeros.iloc[0], 2), "mm/s")
Codes the analysis uses that are missing from the dandiset: none
Fish per species here: {'ZF': 25, 'DC': 12} ; in Fig1-S1.ipynb's lists: ZF 25, DC 12; in the paper: ZF 26, DC 12
species code name n_fish speed_here speed_code angvel_here angvel_code speed_ratio within_10pct
ZF 20 static 18 4.84 5.1 2.33 2.3 0.95 True
ZF 0 forward 25 8.78 8.8 0.62 0.6 1.00 True
ZF 5 forward_right 25 8.38 8.4 6.56 6.6 1.00 True
ZF 3 right 25 6.71 6.7 17.16 17.2 1.00 True
ZF 4 backward_right 25 4.84 4.9 7.73 7.7 0.99 True
ZF 1 backward 25 4.33 4.3 0.03 0.0 1.01 True
ZF 7 backward_left 25 5.16 5.2 -21.51 -21.5 0.99 True
ZF 2 left 25 6.39 6.4 -22.30 -22.3 1.00 True
ZF 6 forward_left 25 8.23 8.3 -1.42 -1.4 0.99 True
DC 20 static 9 10.52 11.9 -1.81 -1.8 0.88 False
DC 0 forward 12 9.69 9.7 -1.98 -2.0 1.00 True
DC 5 forward_right 12 10.02 10.0 1.79 1.8 1.00 True
DC 3 right 12 10.29 10.5 8.67 8.7 0.98 True
DC 4 backward_right 12 10.42 10.5 3.39 3.4 0.99 True
DC 1 backward 12 10.33 10.4 5.05 5.0 0.99 True
DC 7 backward_left 12 8.81 8.8 -2.28 -2.3 1.00 True
DC 2 left 12 9.36 9.4 0.25 0.3 1.00 True
DC 6 forward_left 12 8.97 9.0 1.51 1.5 1.00 True
Speed medians within 10% of the code's printed values: 17 of 18
Median absolute difference in angular velocity (deg/s): 0.02
Fish x stimulus speed medians of exactly 0 (whole-field stimuli): []
With zero medians left out, speed medians within 10% of the printed values: 17 of 18; DC static 10.52 mm/s
# Codes 9 and 11: the figure code calls 9 lateral right, the protocol conversion calls 11 lateral right.
rows = []
for c in (10, 13, 9, 11, 3, 2):
for sp in ("ZF", "DC"):
v = angvel_fish[(angvel_fish.species == sp) & (angvel_fish.code == c)].angvel_deg_s
rows.append({"code": c, "protocol name": omr.STIMULUS_NAMES[c],
"figure-code name": omr.LATERAL_FROM_FIGURE_CODE.get(c, omr.STIMULUS_NAMES[c]),
"species": sp, "n_fish": len(v), "median_deg_s": v.median(),
"fish_turning_right": int((v > 0).sum())})
print(pd.DataFrame(rows).round(2).to_string(index=False))
code protocol name figure-code name species n_fish median_deg_s fish_turning_right
10 medial_right medial_right ZF 25 17.94 23
10 medial_right medial_right DC 12 3.26 8
13 medial_left medial_left ZF 25 -11.18 3
13 medial_left medial_left DC 12 -2.85 2
9 lateral_left lateral_right ZF 25 0.02 14
9 lateral_left lateral_right DC 12 -0.49 5
11 lateral_right lateral_left ZF 25 1.59 16
11 lateral_right lateral_left DC 12 1.12 9
3 right right ZF 25 17.16 23
3 right right DC 12 8.67 8
2 left left ZF 25 -22.30 3
2 left left DC 12 0.25 6
# Codes 9 and 11 against the medial codes 10 and 13: turning (deg/s) per fish, mean over trials in
# 4 s bins from the trial start (0-3 s is the stationary phase), passing trials only.
tbins = np.array([0, 3, 7, 11, 15, 19, 23, 27, 31, 35])
course_rows = []
passing_trials = set(zip(measures[measures.passes].path, measures[measures.passes].trial))
for s in sessions:
for number, c, idx in omr.trials(s):
if c not in (9, 10, 11, 13) or (s["path"], number) not in passing_trials:
continue
t = s["t"][idx] - s["t"][idx][0]
deg = np.degrees(s["f0_vtheta"][idx])
summed = np.histogram(t, tbins, weights=deg)[0]
course_rows.append({"fish": omr.fish_key(s), "species": omr.fish_key(s)[:2], "code": c,
**{f"{a}-{b}s": v / (b - a) for a, b, v in zip(tbins[:-1], tbins[1:], summed)}})
courses = pd.DataFrame(course_rows)
cols = [c for c in courses.columns if c.endswith("s") and "-" in c]
per_fish_course = courses.groupby(["species", "fish", "code"])[cols].mean()
print("Species mean of fish means, by code (deg/s; positive is rightward):")
print(per_fish_course.groupby(["species", "code"]).mean().round(1).to_string())
sustained = per_fish_course[cols[1:]].mean(axis=1).unstack("code")
print("\nPer fish, mean turning from 3 to 35 s (deg/s), codes 9, 10, 11, 13:")
print(sustained.round(1).to_string())
for sp in ("ZF", "DC"):
d = (sustained.loc[sp][9] - sustained.loc[sp][11]).dropna()
print(f"{sp}: code 9 minus code 11, median {d.median():.2f} deg/s over {len(d)} fish, {int((d > 0).sum())} positive, "
f"Wilcoxon p = {stats.wilcoxon(d).pvalue:.3g}; code 10 minus 13, median "
f"{(sustained.loc[sp][10] - sustained.loc[sp][13]).dropna().median():.2f}")
Species mean of fish means, by code (deg/s; positive is rightward):
0-3s 3-7s 7-11s 11-15s 15-19s 19-23s 23-27s 27-31s 31-35s
species code
DC 9 1.3 -2.4 -4.3 -0.0 -3.0 -5.6 -1.3 0.0 -0.0
10 1.1 3.6 4.0 1.7 1.6 -0.3 -1.4 -2.6 -1.4
11 5.6 6.0 3.3 3.6 7.0 3.4 0.4 0.8 3.7
13 10.7 -3.1 -1.1 0.6 -0.2 -1.0 0.4 -0.2 0.5
ZF 9 1.6 0.2 -1.7 -1.7 -0.7 -1.2 -2.0 -0.6 -2.0
10 3.2 8.5 15.4 13.1 12.9 11.1 12.9 10.7 8.0
11 2.4 2.7 3.5 2.8 3.3 1.1 0.6 1.2 2.2
13 0.1 -9.0 -9.0 -10.0 -13.1 -13.9 -11.9 -8.8 -8.3
Per fish, mean turning from 3 to 35 s (deg/s), codes 9, 10, 11, 13:
code 9 10 11 13
species fish
DC DC-sub-0 -10.1 1.6 0.8 -0.8
DC-sub-1 -0.1 -1.5 7.1 7.0
DC-sub-10 -2.4 1.5 6.6 0.4
DC-sub-11 1.0 2.0 -0.2 -0.1
DC-sub-2 -1.7 -0.8 0.9 -6.3
DC-sub-3 -2.2 -1.7 5.8 -1.5
DC-sub-4 -4.5 2.1 -0.0 -1.2
DC-sub-5 5.9 11.0 0.1 -1.0
DC-sub-6 -1.6 -0.1 2.6 -0.2
DC-sub-7 4.6 0.7 7.6 8.7
DC-sub-8 -4.5 -4.5 3.3 -7.7
DC-sub-9 -9.5 -2.4 7.9 -3.6
ZF ZF-sub-0 0.6 14.5 -0.1 -14.1
ZF-sub-1 1.7 5.9 2.5 -12.9
ZF-sub-10 -6.7 26.1 5.5 -22.1
ZF-sub-11 -5.9 12.6 5.5 -9.2
ZF-sub-12 -11.5 16.9 8.7 -15.7
ZF-sub-13 -4.1 24.0 -5.4 -38.8
ZF-sub-14 6.5 1.9 5.3 -8.0
ZF-sub-15 3.0 24.0 -4.0 -13.7
ZF-sub-16 -1.2 2.7 0.4 -5.4
ZF-sub-17 5.8 6.6 -9.5 -5.0
ZF-sub-18 -0.9 11.4 3.3 -9.8
ZF-sub-19 -1.4 11.0 -1.0 -10.2
ZF-sub-2 0.0 5.4 -2.7 -3.7
ZF-sub-20 0.8 4.1 3.6 -11.1
ZF-sub-21 2.5 14.0 12.0 -1.3
ZF-sub-22 5.5 2.9 2.8 1.3
ZF-sub-23 -3.5 17.7 9.5 -22.2
ZF-sub-24 -1.6 21.2 1.1 -15.5
ZF-sub-3 -20.6 13.1 11.0 -8.2
ZF-sub-4 -4.2 7.4 14.0 -9.8
ZF-sub-5 8.3 2.1 -7.6 7.0
ZF-sub-6 0.4 25.5 6.5 -16.4
ZF-sub-7 0.4 -2.0 1.2 -1.5
ZF-sub-8 -3.8 -2.8 1.8 2.9
ZF-sub-9 -0.6 22.9 -10.7 -18.9
ZF: code 9 minus code 11, median -1.66 deg/s over 25 fish, 9 positive, Wilcoxon p = 0.141; code 10 minus 13, median 21.16
DC: code 9 minus code 11, median -5.82 deg/s over 12 fish, 2 positive, Wilcoxon p = 0.00928; code 10 minus 13, median 1.66
Result: reproduced. Every code the analysis uses is in the dandiset. Through the link, 17 of
18 species and stimulus speed medians are within 10% of the values printed by the paper's code (the
exception is DC static, 10.52 against 11.9 mm/s), and the angular velocities differ from the
printed ones by a median of 0.02 deg/s. The fish counts (25 ZF, 12 DC) match the code's lists, not
the paper's 26 ZF. Fish cannot be matched one by one. No fish x stimulus speed median is exactly 0, so the
code's exclusion of zero medians changes nothing, and DC static stays at 10.52 mm/s. The static
window is the same as the code's (see the data check), which leaves the code's filter on the DC
vel column as the remaining difference. On codes 9 and 11, the ZF medians for both are near zero
(0.02 and 1.59 deg/s), while the medial codes 10 and 13 give 17.94 and -11.18 deg/s. Over 3 to 35 s,
DC turn more to the right for code 11 than for code 9 (median difference 5.82 deg/s, 10 of 12 fish,
Wilcoxon p = 0.0093); ZF go the same way but not significantly (1.66 deg/s, 16 of 25 fish,
p = 0.141). If the fish turn with lateral motion as they do with medial motion, this points to
code 11 being lateral right, as the protocol file has it. The data do not show that they do, so
the assignment is not settled.
fig1d-dc-faster-all-codes¶
D. cerebrum (DC) swim at a higher median velocity than zebrafish (ZF) across the visual stimuli.
Tested without the stimulus names, over all 21 codes, in pixels per second (no scale needed).
def two_way(df, value, factor="code"):
model = ols(f"{value} ~ C(species) + C({factor}) + C(species):C({factor})", data=df).fit()
return sm.stats.anova_lm(model, typ=2)
all_codes = speed_fish.assign(speed_px_s=speed_fish.speed_mm_s * omr.PX_PER_MM)
table = two_way(all_codes, "speed_px_s")
print(table.to_string(float_format=lambda v: f"{v:.4g}"))
med = all_codes.groupby("species").speed_px_s.median()
print("\nMedian over fish and code values (px/s):", med.round(1).to_dict(),
"; n values:", all_codes.groupby("species").size().to_dict())
per_fish = all_codes.groupby(["species", "fish"]).speed_px_s.median()
print("\nPer fish, median over codes (px/s):")
print(per_fish.round(1).to_string())
p_fish = stats.mannwhitneyu(per_fish.loc["DC"], per_fish.loc["ZF"]).pvalue
print(f"\nOne value per fish (median over codes): DC {per_fish.loc['DC'].median():.1f} against ZF {per_fish.loc['ZF'].median():.1f} px/s, "
f"Mann-Whitney p = {p_fish:.2g} (the ANOVA above treats each fish x code value as independent)")
sum_sq df F PR(>F)
C(species) 2.694e+05 1 78.44 6.272e-18
C(code) 1.405e+05 20 2.045 0.004612
C(species):C(code) 9.16e+04 20 1.333 0.1495
Residual 2.49e+06 725 NaN NaN
Median over fish and code values (px/s): {'DC': 94.0, 'ZF': 55.2} ; n values: {'DC': 249, 'ZF': 518}
Per fish, median over codes (px/s):
species fish
DC DC-sub-0 76.4
DC-sub-1 61.1
DC-sub-10 123.5
DC-sub-11 137.8
DC-sub-2 44.7
DC-sub-3 69.9
DC-sub-4 106.6
DC-sub-5 120.5
DC-sub-6 27.1
DC-sub-7 115.3
DC-sub-8 28.0
DC-sub-9 98.4
ZF ZF-sub-0 56.9
ZF-sub-1 69.0
ZF-sub-10 48.0
ZF-sub-11 41.7
ZF-sub-12 52.9
ZF-sub-13 43.2
ZF-sub-14 40.1
ZF-sub-15 52.0
ZF-sub-16 32.1
ZF-sub-17 72.6
ZF-sub-18 1.4
ZF-sub-19 46.6
ZF-sub-2 39.6
ZF-sub-20 76.0
ZF-sub-21 76.3
ZF-sub-22 64.3
ZF-sub-23 44.5
ZF-sub-24 45.0
ZF-sub-3 74.8
ZF-sub-4 56.7
ZF-sub-5 58.0
ZF-sub-6 60.2
ZF-sub-7 73.5
ZF-sub-8 55.1
ZF-sub-9 52.8
One value per fish (median over codes): DC 87.4 against ZF 52.9 px/s, Mann-Whitney p = 0.02 (the ANOVA above treats each fish x code value as independent)
# Variants: speed from positions resampled to 30 Hz; ZF split by frame rate.
s30 = omr.fish_medians(measures, "speed30_mm_s").assign(speed_px_s=lambda d: d.speed30_mm_s * omr.PX_PER_MM)
t30 = two_way(s30, "speed_px_s")
print("30 Hz variant, species effect: F =", round(t30.loc["C(species)", "F"], 2), "p =", f"{t30.loc['C(species)', 'PR(>F)']:.2g}",
"; medians (px/s):", s30.groupby("species").speed_px_s.median().round(1).to_dict())
rate = all_codes.fish.map(lambda f: "fast" if hz[f] > 100 else "slow")
print("\nMedian over fish and code values (px/s) by species and frame rate:")
print(all_codes.assign(rate=rate).groupby(["species", "rate"]).speed_px_s.agg(["median", "size"]).round(1).to_string())
print("\nThe same with 30 Hz resampled positions:")
print(s30.assign(rate=s30.fish.map(lambda f: "fast" if hz[f] > 100 else "slow")).groupby(["species", "rate"]).speed_px_s.agg(["median", "size"]).round(1).to_string())
30 Hz variant, species effect: F = 33.13 p = 1.3e-08 ; medians (px/s): {'DC': 95.2, 'ZF': 53.8}
Median over fish and code values (px/s) by species and frame rate:
median size
species rate
DC fast 86.4 228
slow 106.6 21
ZF fast 59.9 145
slow 52.9 373
The same with 30 Hz resampled positions:
median size
species rate
DC fast 89.4 228
slow 112.4 21
ZF fast 59.3 145
slow 51.6 373
Result: reproduced. Over all 21 codes DC swim faster than ZF: median 94.0 against 55.2 pixels/s over fish and code values, species effect F = 78.4, p = 6.3e-18. With positions resampled to 30 Hz the result is the same (95.2 against 53.8 pixels/s, p = 1.3e-08), and DC are faster within each frame-rate group (fast files 86.4 against 59.9, slow files 106.6 against 52.9). The ANOVA treats each fish x code value as independent; with one value per fish (the median over codes), DC 87.4 against ZF 52.9 pixels/s, Mann-Whitney p = 0.02.
fig1d-median-velocity¶
Across the whole-field stimuli DC have a higher median velocity than ZF (9.8 against 6.4 mm/s), with a significant main effect of species in a two-way ANOVA.
wf = speed_fish[speed_fish.code.isin(omr.WHOLE_FIELD)].copy()
wf["name"] = wf.code.map(omr.STIMULUS_NAMES)
wide = wf.pivot_table(index=["species", "fish"], columns="name", values="speed_mm_s")
print("Per fish median speed (mm/s), whole-field stimuli:")
print(wide.round(1).to_string())
def readings(fish_values, trials_values, column):
out = {}
for sp in ("DC", "ZF"):
f = fish_values[fish_values.species == sp]
out[sp] = {
"a: median of stimulus medians": f.groupby("code")[column].median().median(),
"b: median of fish x stimulus values": f[column].median(),
"c: median over fish of fish medians": f.groupby("fish")[column].median().median(),
"d: median of passing trials": trials_values[trials_values.species == sp][column].median()}
return pd.DataFrame(out)
wf_trials = measures[measures.passes & measures.code.isin(omr.WHOLE_FIELD)]
read = readings(wf, wf_trials, "speed_mm_s")
read["DC within 9.8 +/- 0.98"] = (read.DC - 9.8).abs() <= 0.98
read["ZF within 6.4 +/- 0.64"] = (read.ZF - 6.4).abs() <= 0.64
print("\nOverall medians (mm/s) under each reading:")
print(read.round(2).to_string())
anova_wf = two_way(wf, "speed_mm_s")
print("\nTwo-way ANOVA (type II) on fish x stimulus medians:")
print(anova_wf.to_string(float_format=lambda v: f"{v:.4g}"))
Per fish median speed (mm/s), whole-field stimuli:
name backward backward_left backward_right forward forward_left forward_right left right static
species fish
DC DC-sub-0 11.0 8.5 9.9 6.3 13.3 7.2 7.9 6.9 NaN
DC-sub-1 8.5 5.3 6.9 5.4 4.2 10.2 5.6 6.5 NaN
DC-sub-10 11.1 9.0 12.3 12.2 11.3 12.2 16.8 12.8 19.8
DC-sub-11 19.1 9.9 18.2 6.8 15.8 18.5 5.4 10.2 0.0
DC-sub-2 6.5 0.5 1.2 8.9 4.4 1.0 0.2 10.4 106.5
DC-sub-3 3.8 13.0 4.9 10.5 8.7 9.8 17.1 7.8 NaN
DC-sub-4 9.7 11.7 12.7 12.3 10.8 12.1 12.3 10.5 10.5
DC-sub-5 12.1 17.5 12.7 19.3 8.8 22.8 24.2 17.7 11.9
DC-sub-6 3.4 1.2 3.9 2.1 3.9 2.7 0.7 7.1 0.0
DC-sub-7 11.0 13.3 11.0 11.0 13.4 12.8 10.8 13.4 13.9
DC-sub-8 4.4 4.6 1.9 10.6 5.0 2.6 2.8 4.9 4.8
DC-sub-9 12.0 8.6 12.3 7.8 9.2 7.8 12.8 13.6 9.5
ZF ZF-sub-0 4.6 4.2 5.2 8.8 8.8 8.0 5.8 7.2 5.3
ZF-sub-1 6.5 6.0 6.0 9.2 10.8 8.4 8.4 7.3 NaN
ZF-sub-10 4.2 3.8 4.3 10.4 8.2 10.4 4.9 5.9 4.2
ZF-sub-11 3.8 3.1 4.2 10.3 9.1 10.2 5.3 7.0 5.6
ZF-sub-12 4.1 5.4 4.8 5.5 8.2 8.7 10.4 8.6 5.0
ZF-sub-13 4.1 5.8 4.7 7.4 9.6 6.9 6.4 6.3 NaN
ZF-sub-14 2.5 3.0 4.8 4.4 3.2 4.7 5.4 3.3 4.5
ZF-sub-15 4.3 5.7 5.8 5.7 4.7 5.9 3.7 3.8 2.1
ZF-sub-16 1.6 1.7 1.6 12.1 11.3 10.6 4.9 3.2 1.2
ZF-sub-17 5.6 7.1 10.1 12.7 10.8 7.8 6.4 8.1 NaN
ZF-sub-18 0.1 1.2 0.1 2.6 0.2 0.6 0.8 0.7 0.9
ZF-sub-19 0.0 3.5 0.0 8.7 5.1 8.6 4.7 6.5 NaN
ZF-sub-2 4.0 2.6 2.2 8.9 6.6 5.8 3.9 4.6 4.5
ZF-sub-20 7.4 8.5 7.4 11.6 8.9 9.9 9.1 9.1 7.9
ZF-sub-21 5.7 6.0 7.3 11.3 9.7 9.9 9.2 9.8 4.5
ZF-sub-22 5.8 6.8 7.0 6.4 6.7 7.0 5.6 5.9 6.9
ZF-sub-23 3.5 4.4 4.2 9.7 8.3 9.8 7.4 6.4 3.3
ZF-sub-24 3.3 3.2 3.3 8.7 7.5 7.8 6.1 6.7 4.7
ZF-sub-3 7.5 6.8 8.9 11.3 12.8 9.3 10.7 7.9 12.4
ZF-sub-4 5.5 4.9 4.4 9.4 8.6 13.0 7.0 7.1 8.3
ZF-sub-5 5.4 3.6 6.7 7.2 6.2 6.5 7.2 2.8 NaN
ZF-sub-6 10.0 9.8 9.0 7.9 4.2 4.9 4.6 6.9 47.9
ZF-sub-7 8.1 10.8 7.3 7.2 5.6 8.5 7.3 7.0 NaN
ZF-sub-8 5.8 5.5 5.5 7.3 8.1 5.3 6.7 5.3 NaN
ZF-sub-9 4.1 5.2 4.4 12.4 9.1 11.2 7.1 8.0 6.6
Overall medians (mm/s) under each reading:
DC ZF DC within 9.8 +/- 0.98 ZF within 6.4 +/- 0.64
a: median of stimulus medians 10.02 6.39 True True
b: median of fish x stimulus values 9.87 6.41 True True
c: median over fish of fish medians 9.36 6.33 True True
d: median of passing trials 9.88 6.28 True True
Two-way ANOVA (type II) on fish x stimulus medians:
sum_sq df F PR(>F)
C(species) 966 1 20.78 7.477e-06
C(code) 715.4 8 1.923 0.05616
C(species):C(code) 572.5 8 1.539 0.1429
Residual 1.418e+04 305 NaN NaN
# Variants: 30 Hz speeds; no trial rejection; by ZF frame-rate group.
wf30 = s30[s30.code.isin(omr.WHOLE_FIELD)]
r30 = readings(wf30, measures[measures.passes & measures.code.isin(omr.WHOLE_FIELD)], "speed30_mm_s")
a30 = two_way(wf30, "speed30_mm_s")
print("30 Hz positions: overall medians (mm/s)")
print(r30.round(2).to_string())
print("species effect p =", f"{a30.loc['C(species)', 'PR(>F)']:.2g}")
no_reject = measures.copy()
no_reject["passes"] = no_reject.speed_mm_s.notna()
nr = omr.fish_medians(no_reject, "speed_mm_s")
nr = nr[nr.code.isin(omr.WHOLE_FIELD)]
print("\nWithout the code's trial rejection (trials with a measure:", int(no_reject.passes.sum()), "):")
print(readings(nr, no_reject[no_reject.passes & no_reject.code.isin(omr.WHOLE_FIELD)], "speed_mm_s").round(2).to_string())
print("species effect p =", f"{two_way(nr, 'speed_mm_s').loc['C(species)', 'PR(>F)']:.2g}")
wf_rate = wf.assign(rate=wf.fish.map(lambda f: "fast" if hz[f] > 100 else "slow"))
print("\nReading (b) by species and frame rate (mm/s):")
print(wf_rate.groupby(["species", "rate"]).speed_mm_s.agg(["median", "size"]).round(2).to_string())
30 Hz positions: overall medians (mm/s)
DC ZF
a: median of stimulus medians 9.80 6.07
b: median of fish x stimulus values 9.79 6.08
c: median over fish of fish medians 9.43 6.30
d: median of passing trials 10.15 6.12
species effect p = 0.0011
Without the code's trial rejection (trials with a measure: 4487 ):
DC ZF
a: median of stimulus medians 10.02 6.39
b: median of fish x stimulus values 9.87 6.41
c: median over fish of fish medians 9.36 6.33
d: median of passing trials 9.85 6.30
species effect p = 6.7e-06
Reading (b) by species and frame rate (mm/s):
median size
species rate
DC fast 9.09 96
slow 11.69 9
ZF fast 6.98 61
slow 5.88 157
fig, ax = plt.subplots(figsize=(10, 3.5))
for k, c in enumerate(omr.WHOLE_FIELD):
for dx, sp, colour in ((-0.15, "ZF", "0.5"), (0.15, "DC", "k")):
v = wf[(wf.species == sp) & (wf.code == c)].speed_mm_s
ax.scatter(np.full(len(v), k + dx), v, s=10, color=colour, label=sp if k == 0 else None)
ax.hlines(v.median(), k + dx - 0.1, k + dx + 0.1, color="r")
ax.set_xticks(range(len(omr.WHOLE_FIELD)), [omr.STIMULUS_NAMES[c] for c in omr.WHOLE_FIELD], rotation=30)
ax.set_ylabel("fish median speed (mm/s)")
ax.legend()
plt.tight_layout()
plt.show()
print("Plotted range (mm/s):", wf.speed_mm_s.min().round(2), "to", wf.speed_mm_s.max().round(2), "; axes autoscaled, all points shown")
Plotted range (mm/s): 0.01 to 106.46 ; axes autoscaled, all points shown
Result: reproduced. Under every reading both overall medians are within tolerance of 9.8 and 6.4 mm/s: reading (a) gives DC 10.02 and ZF 6.39, (b) 9.87 and 6.41, (c) 9.36 and 6.33, (d) 9.88 and 6.28. The species effect of the two-way ANOVA is F = 20.8, p = 7.5e-06. With positions resampled to 30 Hz, DC give 9.43 to 10.15 and ZF 6.07 to 6.30, still within tolerance (p = 0.0011); without the code's trial rejection (4487 trials in place of 4458) the readings are unchanged except (d), DC 9.85 and ZF 6.30, and p = 6.7e-06. ZF from 32 to 35 Hz files are slower (5.88 mm/s, reading b) than ZF from 167 to 171 Hz files (6.98).
fig1d-zf-forward-faster-than-static¶
ZF modulate their velocity with the direction of motion; forward motion increases their median velocity (static 5.0 mm/s, forward 8.8 mm/s).
for c in (20, 0):
v = wf[(wf.species == "ZF") & (wf.code == c)].speed_mm_s
print(f"ZF {omr.STIMULUS_NAMES[c]:8s} median {v.median():.2f} mm/s over {len(v)} fish "
f"(interquartile {v.quantile(0.25):.2f} to {v.quantile(0.75):.2f})")
paired = wide.loc["ZF"][["static", "forward"]].dropna()
print(f"\nZF fish with both: {len(paired)}; forward faster in {(paired.forward > paired.static).sum()}; "
f"median of forward minus static {np.median(paired.forward - paired.static):.2f} mm/s; "
f"Wilcoxon p = {stats.wilcoxon(paired.forward, paired.static).pvalue:.3g}")
print("\nBy ZF frame-rate group (median mm/s, n fish):")
print(wf_rate[(wf_rate.species == "ZF") & wf_rate.code.isin([20, 0])].groupby(["rate", "code"]).speed_mm_s.agg(["median", "size"]).round(2).to_string())
v30 = wf30[(wf30.species == "ZF")].groupby("code").speed30_mm_s.median()
print("\n30 Hz variant, ZF static and forward (mm/s):", round(v30[20], 2), round(v30[0], 2))
ZF static median 4.84 mm/s over 18 fish (interquartile 4.29 to 6.84)
ZF forward median 8.78 mm/s over 25 fish (interquartile 7.25 to 10.37)
ZF fish with both: 18; forward faster in 14; median of forward minus static 3.67 mm/s; Wilcoxon p = 0.00769
By ZF frame-rate group (median mm/s, n fish):
median size
rate code
fast 0 8.94 7
20 8.31 5
slow 0 8.69 18
20 4.50 13
30 Hz variant, ZF static and forward (mm/s): 4.64 8.65
Result: reproduced. ZF median speed is 4.84 mm/s for static (18 fish) and 8.78 mm/s for forward (25 fish), against 5.0 and 8.8. Of 18 fish with both, 14 are faster to forward motion (median difference 3.67 mm/s, Wilcoxon p = 0.0077). The static value depends on frame rate: 8.31 mm/s in the 5 fish from fast files, 4.50 in the 13 from slow files; forward does not (8.94 and 8.69).
fig1d-dc-no-modulation¶
DC lack the direction-dependent velocity modulation seen in ZF.
The paper's test: Tukey's HSD over all 18 species and stimulus groups of fish medians.
groups = wf.species + "_" + wf.name
tukey = pairwise_tukeyhsd(wf.speed_mm_s, groups, alpha=0.05)
res = pd.DataFrame(tukey.summary().data[1:], columns=tukey.summary().data[0])
dc_pairs = res[res.group1.str.startswith("DC") & res.group2.str.startswith("DC")]
zf_pairs = res[res.group1.str.startswith("ZF") & res.group2.str.startswith("ZF")]
print(f"DC pairs: {len(dc_pairs)}, significant: {int(dc_pairs.reject.sum())}; smallest adjusted p {dc_pairs['p-adj'].min():.3f}")
print(f"ZF pairs: {len(zf_pairs)}, significant: {int(zf_pairs.reject.sum())}; smallest adjusted p {zf_pairs['p-adj'].min():.3f}")
print(res[res.reject].to_string(index=False) if res.reject.any() else "no significant pair")
dc_med = wf[wf.species == "DC"].groupby("name").speed_mm_s.median()
zf_med = wf[wf.species == "ZF"].groupby("name").speed_mm_s.median()
print(f"\nRange of DC per-stimulus medians: {dc_med.min():.2f} to {dc_med.max():.2f} mm/s (ratio {dc_med.max() / dc_med.min():.2f})")
print(f"Range of ZF per-stimulus medians: {zf_med.min():.2f} to {zf_med.max():.2f} mm/s (ratio {zf_med.max() / zf_med.min():.2f})")
for sp in ("DC", "ZF"):
sub = wf[wf.species == sp]
print(f"{sp}: one-way ANOVA across the nine stimuli, p = {stats.f_oneway(*[g.speed_mm_s for _, g in sub.groupby('code')]).pvalue:.3g}; "
f"Friedman over fish with all nine: ", end="")
w = sub.pivot_table(index="fish", columns="code", values="speed_mm_s").dropna()
print(f"{len(w)} fish, p = {stats.friedmanchisquare(*[w[c] for c in w.columns]).pvalue:.3g}" if len(w) > 2 else "too few")
print("\nDC static, per fish (mm/s):", wf[(wf.species == 'DC') & (wf.code == 20)].set_index('fish').speed_mm_s.round(1).to_dict())
DC pairs: 36, significant: 3; smallest adjusted p 0.030
ZF pairs: 36, significant: 0; smallest adjusted p 0.835
group1 group2 meandiff p-adj lower upper reject
DC_backward_left DC_static 11.0686 0.0297 0.4873 21.6499 True
DC_backward_right DC_static 10.6616 0.0460 0.0803 21.2429 True
DC_forward_left DC_static 10.6049 0.0488 0.0236 21.1863 True
DC_static ZF_backward -14.9510 0.0000 -24.2791 -5.6230 True
DC_static ZF_backward_left -14.5063 0.0000 -23.8344 -5.1783 True
DC_static ZF_backward_right -14.4723 0.0000 -23.8004 -5.1443 True
DC_static ZF_forward -10.9704 0.0057 -20.2984 -1.6424 True
DC_static ZF_forward_left -11.9582 0.0012 -21.2862 -2.6301 True
DC_static ZF_forward_right -11.6658 0.0020 -20.9938 -2.3378 True
DC_static ZF_left -13.2870 0.0001 -22.6150 -3.9589 True
DC_static ZF_right -13.4435 0.0001 -22.7715 -4.1154 True
DC_static ZF_static -12.1059 0.0025 -21.9023 -2.3095 True
Range of DC per-stimulus medians: 8.81 to 10.52 mm/s (ratio 1.19)
Range of ZF per-stimulus medians: 4.33 to 8.78 mm/s (ratio 2.03)
DC: one-way ANOVA across the nine stimuli, p = 0.479; Friedman over fish with all nine: 9 fish, p = 0.622
ZF: one-way ANOVA across the nine stimuli, p = 0.00082; Friedman over fish with all nine: 18 fish, p = 3.76e-10
DC static, per fish (mm/s): {'DC-sub-10': 19.8, 'DC-sub-11': 0.0, 'DC-sub-2': 106.5, 'DC-sub-4': 10.5, 'DC-sub-5': 11.9, 'DC-sub-6': 0.0, 'DC-sub-7': 13.9, 'DC-sub-8': 4.8, 'DC-sub-9': 9.5}
CODE_PRINTED_TUKEY_DC = { # Fig1-S1.ipynb, Tukey over the 18 groups: DC pairs with adjusted p < 0.05
("backward_left", "static"): 0.0236, ("backward_right", "static"): 0.0373, ("forward_left", "static"): 0.0397}
here = {tuple(sorted((r.group1[3:], r.group2[3:]))): r["p-adj"] for _, r in dc_pairs[dc_pairs.reject].iterrows()}
print("DC pairs significant in Fig1-S1.ipynb's printed output:", CODE_PRINTED_TUKEY_DC)
print("DC pairs significant here:", {k: round(v, 4) for k, v in here.items()})
def tukey_counts(sub):
t = pairwise_tukeyhsd(sub.speed_mm_s, sub.species + "_" + sub.name)
r = pd.DataFrame(t.summary().data[1:], columns=t.summary().data[0])
dcp = r[r.group1.str.startswith("DC") & r.group2.str.startswith("DC")]
zfp = r[r.group1.str.startswith("ZF") & r.group2.str.startswith("ZF")]
return (f"DC pairs significant {int(dcp.reject.sum())} of {len(dcp)} (smallest p {dcp['p-adj'].min():.3f}); "
f"ZF pairs significant {int(zfp.reject.sum())} of {len(zfp)} (smallest p {zfp['p-adj'].min():.3f})")
# Variants: the eight motion directions only; fish-stimulus values above 100 mm/s left out; fish medians
# of exactly 0 left out, as Fig1-S1.ipynb does; the speed check computed from positions in DC files too.
pos = omr.fish_medians(measures.assign(passes=measures.passes_pos), "speed_mm_s")
pos = pos[pos.code.isin(omr.WHOLE_FIELD)].assign(name=lambda d: d.code.map(omr.STIMULUS_NAMES))
for label, sub in (("directions only", wf[wf.code != 20]), ("values above 100 mm/s left out", wf[wf.speed_mm_s <= 100]),
("zero medians left out", wf[wf.speed_mm_s != 0]), ("speed check from positions", pos)):
print(f"\n{label}: {tukey_counts(sub)}")
print("\nDC static, per fish, speed check from positions (mm/s):", pos[(pos.species == 'DC') & (pos.code == 20)].set_index('fish').speed_mm_s.round(1).to_dict())
DC pairs significant in Fig1-S1.ipynb's printed output: {('backward_left', 'static'): 0.0236, ('backward_right', 'static'): 0.0373, ('forward_left', 'static'): 0.0397}
DC pairs significant here: {('backward_left', 'static'): 0.0297, ('backward_right', 'static'): 0.046, ('forward_left', 'static'): 0.0488}
directions only: DC pairs significant 0 of 28 (smallest p 0.999); ZF pairs significant 3 of 28 (smallest p 0.009)
values above 100 mm/s left out: DC pairs significant 0 of 36 (smallest p 1.000); ZF pairs significant 0 of 36 (smallest p 0.110)
zero medians left out: DC pairs significant 3 of 36 (smallest p 0.030); ZF pairs significant 0 of 36 (smallest p 0.835)
speed check from positions: DC pairs significant 3 of 36 (smallest p 0.030); ZF pairs significant 0 of 36 (smallest p 0.835)
DC static, per fish, speed check from positions (mm/s): {'DC-sub-10': 19.8, 'DC-sub-11': 0.0, 'DC-sub-2': 106.5, 'DC-sub-4': 10.5, 'DC-sub-5': 11.9, 'DC-sub-6': 0.0, 'DC-sub-7': 13.9, 'DC-sub-8': 4.8, 'DC-sub-9': 9.5}
Result: not reproduced. By the paper's test, Tukey's HSD over the 18 groups, 3 of 36 DC
pairs differ (static against backward left, backward right and forward left, adjusted p 0.030 to
0.049), so the test finds a difference within DC. All three involve static, whose DC values come
from 9 fish and include one at 106.5 mm/s and two near 0 (shown as 0.0 when rounded). Among the eight motion directions no DC
pair differs (smallest adjusted p 0.999); the DC per-stimulus medians span 8.81 to 10.52 mm/s
(ratio 1.19, against 2.03 in ZF), and a one-way ANOVA across the nine DC stimuli gives p = 0.479
(ZF p = 0.00082). With values above 100 mm/s left out, no DC pair differs. The same Tukey test
finds no ZF pair over the 18 groups (smallest adjusted p 0.835), and 3 of 28 among the directions
alone. The output of Fig1-S1.ipynb itself shows the same three DC pairs as significant (adjusted p
0.0236, 0.0373 and 0.0397), so the paper's own test disagrees with the claim, and the result here
matches the paper's computation. Computing the speed check from positions, or leaving out zero
medians as the code does, gives the same 3 pairs. The DC-sub-2 static value of 106.5 mm/s comes from
one trial over a 14.6 s motion phase in a 100 mm dish, which is likely a tracking artefact (see
the trial measures), and without values above 100 mm/s no DC pair differs. The outcome is a
property of the paper's test on this data; it is not evidence that DC modulate their speed with
stimulus direction.
fig1d-dc-faster-oblique¶
DC swim at a significantly higher velocity than ZF for oblique stimuli.
rows = []
for c in omr.WHOLE_FIELD:
sub = wf[wf.code == c]
t = pairwise_tukeyhsd(sub.speed_mm_s, sub.species)
rows.append({"code": c, "name": omr.STIMULUS_NAMES[c],
"DC_median": sub[sub.species == "DC"].speed_mm_s.median(),
"ZF_median": sub[sub.species == "ZF"].speed_mm_s.median(),
"mean_DC_minus_ZF": sub[sub.species == "DC"].speed_mm_s.mean() - sub[sub.species == "ZF"].speed_mm_s.mean(),
"p": t.pvalues[0], "n_DC": int((sub.species == "DC").sum()), "n_ZF": int((sub.species == "ZF").sum())})
per_stim = pd.DataFrame(rows)
per_stim["significant_at_0.05/9"] = per_stim.p < 0.05 / 9
print(per_stim.round(4).to_string(index=False))
reading_a = ["forward_right", "forward_left", "backward_right", "backward_left"]
reading_b = ["right", "left", "backward_right", "backward_left"]
for label, names in (("a, intercardinal", reading_a), ("b, 90 degrees or more", reading_b)):
sel = per_stim[per_stim.name.isin(names)]
n = int((sel["significant_at_0.05/9"] & (sel.mean_DC_minus_ZF > 0)).sum())
print(f"Reading {label}: {n} of 4 stimuli with DC significantly faster")
code name DC_median ZF_median mean_DC_minus_ZF p n_DC n_ZF significant_at_0.05/9
20 static 10.5207 4.8415 12.1059 0.1633 9 18 False
0 forward 9.6858 8.7799 0.7501 0.5140 12 25 False
5 forward_right 10.0216 8.3811 1.9967 0.1838 12 25 False
3 right 10.2944 6.7082 3.9483 0.0002 12 25 True
4 backward_right 10.4239 4.8425 3.8107 0.0047 12 25 True
1 backward 10.3299 4.3323 4.6720 0.0002 12 25 True
7 backward_left 8.8098 5.1609 3.4377 0.0071 12 25 False
2 left 9.3607 6.3879 3.3426 0.0411 12 25 False
6 forward_left 8.9690 8.2308 1.3532 0.2411 12 25 False
Reading a, intercardinal: 1 of 4 stimuli with DC significantly faster
Reading b, 90 degrees or more: 2 of 4 stimuli with DC significantly faster
Result: reproduced. Under reading (b) DC are significantly faster than ZF for right (p = 0.0002) and backward right (p = 0.0047), 2 of 4; under reading (a) for 1 of 4 (backward right). Backward motion, which is not oblique, also differs (p = 0.0002).
fig1g-follow-direction¶
Both species follow the direction of optic flow, turning right (positive angular velocity) to rightward motion and left to leftward motion.
awf = angvel_fish[angvel_fish.code.isin(omr.WHOLE_FIELD)].copy()
awf["name"] = awf.code.map(omr.STIMULUS_NAMES)
print("Species medians of fish median angular velocity (deg/s), and fish turning right:")
summary = awf.groupby(["name", "species"]).angvel_deg_s.agg(["median", "size", lambda v: int((v > 0).sum())])
summary.columns = ["median", "n_fish", "n_positive"]
print(summary.round(2).unstack("species").to_string())
aw = awf.pivot_table(index=["species", "fish"], columns="code", values="angvel_deg_s")
score = (aw[omr.RIGHTWARD].mean(axis=1) - aw[omr.LEFTWARD].mean(axis=1)).rename("right_minus_left")
print("\nPer fish, mean over rightward minus mean over leftward stimuli (deg/s):")
print(score.round(2).to_string())
for sp in ("ZF", "DC"):
v = score.loc[sp].dropna()
p = stats.wilcoxon(v, alternative="greater").pvalue
print(f"{sp}: {len(v)} fish, median {v.median():.2f} deg/s, {int((v > 0).sum())} positive, one-sided Wilcoxon p = {p:.3g}")
Species medians of fish median angular velocity (deg/s), and fish turning right:
median n_fish n_positive
species DC ZF DC ZF DC ZF
name
backward 5.05 0.03 12 25 11 14
backward_left -2.28 -21.51 12 25 3 3
backward_right 3.39 7.73 12 25 9 22
forward -1.98 0.62 12 25 5 17
forward_left 1.51 -1.42 12 25 8 9
forward_right 1.79 6.56 12 25 8 21
left 0.25 -22.30 12 25 6 3
right 8.67 17.16 12 25 8 23
static -1.81 2.33 9 18 4 10
Per fish, mean over rightward minus mean over leftward stimuli (deg/s):
species fish
DC DC-sub-0 -2.07
DC-sub-1 5.38
DC-sub-10 -2.88
DC-sub-11 1.39
DC-sub-2 12.33
DC-sub-3 30.94
DC-sub-4 44.08
DC-sub-5 42.24
DC-sub-6 -6.55
DC-sub-7 -4.07
DC-sub-8 -0.44
DC-sub-9 0.38
ZF ZF-sub-0 33.05
ZF-sub-1 26.01
ZF-sub-10 49.14
ZF-sub-11 28.37
ZF-sub-12 45.92
ZF-sub-13 40.85
ZF-sub-14 1.48
ZF-sub-15 14.03
ZF-sub-16 23.02
ZF-sub-17 4.28
ZF-sub-18 7.80
ZF-sub-19 35.78
ZF-sub-2 37.73
ZF-sub-20 43.98
ZF-sub-21 48.41
ZF-sub-22 -6.59
ZF-sub-23 47.79
ZF-sub-24 52.60
ZF-sub-3 29.51
ZF-sub-4 38.79
ZF-sub-5 -2.87
ZF-sub-6 22.85
ZF-sub-7 5.63
ZF-sub-8 2.30
ZF-sub-9 39.47
ZF: 25 fish, median 29.51 deg/s, 23 positive, one-sided Wilcoxon p = 9.83e-07
DC: 12 fish, median 0.88 deg/s, 7 positive, one-sided Wilcoxon p = 0.151
# Variant: by frame-rate group, and the binocular pair alone (right minus left).
rate_aw = score.reset_index().assign(rate=lambda d: d.fish.map(lambda f: "fast" if hz[f] > 100 else "slow"))
print(rate_aw.groupby(["species", "rate"]).right_minus_left.agg(["median", "size"]).round(2).to_string())
pair = (aw[3] - aw[2]).rename("right_minus_left_binocular")
for sp in ("ZF", "DC"):
v = pair.loc[sp].dropna()
print(f"{sp}, codes 3 minus 2 only: median {v.median():.2f} deg/s over {len(v)} fish, p = {stats.wilcoxon(v, alternative='greater').pvalue:.3g}")
median size
species rate
DC fast 0.38 11
slow 44.08 1
ZF fast 29.51 7
slow 32.08 18
ZF, codes 3 minus 2 only: median 45.08 deg/s over 25 fish, p = 2.62e-06
DC, codes 3 minus 2 only: median 3.10 deg/s over 12 fish, p = 0.133
Result: partially reproduced. ZF follow the direction of motion: the rightward minus leftward difference is positive in 23 of 25 fish (median 29.51 deg/s, one-sided Wilcoxon p = 9.8e-07). DC do not by this test: 7 of 12 fish are positive, median 0.88 deg/s, p = 0.151, and the DC median for left motion is +0.25 deg/s. Codes 3 minus 2 alone give the same (ZF 45.08 deg/s, p = 2.6e-06; DC 3.10 deg/s, p = 0.133). DC turn right to right motion (median 8.67 deg/s), and four DC fish have differences above 12 deg/s.
fig1h-zf-higher-angular-acceleration¶
Across directional stimuli ZF have higher angular acceleration than DC, and the distributions of the two species differ for every stimulus.
started = time.time()
passing = set(zip(measures[measures.passes].path, measures[measures.passes].trial))
accel = pd.concat([omr.acceleration_frames(s, passing) for s in sessions], ignore_index=True)
accel = accel[accel.code.isin(omr.WHOLE_FIELD)]
print(f"angular acceleration frames computed in {time.time() - started:.0f} s; frames kept:")
print(accel.groupby(["kind", "species"]).size().to_string())
def accel_tests(kind):
rows = []
for c in omr.WHOLE_FIELD:
sub = accel[(accel.kind == kind) & (accel.code == c)]
z, d = sub[sub.species == "ZF"].accel, sub[sub.species == "DC"].accel
fz = sub[sub.species == "ZF"].groupby("fish").accel.median()
fd = sub[sub.species == "DC"].groupby("fish").accel.median()
rows.append({"name": omr.STIMULUS_NAMES[c], "ZF_median": z.median(), "DC_median": d.median(),
"n_ZF": len(z), "n_DC": len(d), "kruskal_p": stats.kruskal(z, d).pvalue,
"ZF_fish_median": fz.median(), "DC_fish_median": fd.median(),
"fish_mannwhitney_p": stats.mannwhitneyu(fz, fd).pvalue})
out = pd.DataFrame(rows)
out["zf_higher_and_sig"] = (out.ZF_median > out.DC_median) & (out.kruskal_p < 0.05 / 9)
return out
main = accel_tests("methods")
print("\nMethods definition (deg/s^2, heading at 30 Hz), frames 10 to 1000:")
print(main.to_string(index=False, float_format=lambda v: f"{v:.3g}"))
print(f"\nStimuli with ZF higher and Kruskal-Wallis p < 0.05/9: {int(main.zf_higher_and_sig.sum())} of 9")
print(f"Per-fish variant, ZF fish median higher and Mann-Whitney p < 0.05/9: "
f"{int(((main.ZF_fish_median > main.DC_fish_median) & (main.fish_mannwhitney_p < 0.05 / 9)).sum())} of 9")
angular acceleration frames computed in 18 s; frames kept:
kind species
code DC 471552
ZF 869982
methods DC 36895
ZF 246834
theta DC 32619
ZF 188787
Methods definition (deg/s^2, heading at 30 Hz), frames 10 to 1000:
name ZF_median DC_median n_ZF n_DC kruskal_p ZF_fish_median DC_fish_median fish_mannwhitney_p zf_higher_and_sig
static 55.4 179 3720 823 0 58.1 188 5.12e-06 False
forward 57.3 183 14729 3490 0 55.5 171 3.23e-06 False
forward_right 56.5 143 20871 3890 0 59.4 156 7.01e-06 False
right 66.4 187 37350 5597 0 63.8 204 1.45e-06 False
backward_right 63.2 175 39015 5155 0 64.2 174 1.71e-06 False
backward 58.4 170 36839 4647 0 59.7 199 2.76e-06 False
backward_left 62.4 188 38625 5429 0 60.8 164 2.76e-06 False
left 64.7 191 38207 4131 0 68.5 187 2e-06 False
forward_left 58.2 180 17478 3733 0 57.8 177 3.23e-06 False
Stimuli with ZF higher and Kruskal-Wallis p < 0.05/9: 0 of 9
Per-fish variant, ZF fish median higher and Mann-Whitney p < 0.05/9: 0 of 9
coded = accel_tests("code")
print("Variant: Fig1-S1.ipynb's quantity (per-frame heading change / file mean frame interval):")
print(coded.to_string(index=False, float_format=lambda v: f"{v:.3g}"))
print(f"\nStimuli with ZF higher and p < 0.05/9: {int(coded.zf_higher_and_sig.sum())} of 9")
ratio = pd.DataFrame([{"fish": omr.fish_key(s), "frame_interval_median_ms": 1000 / s["frame_hz"], "code_dt_ms": 1000 * s["code_dt"]} for s in sessions])
ratio["code_dt_over_frame_interval"] = ratio.code_dt_ms / ratio.frame_interval_median_ms
fast_fish = {f for f, v in hz.items() if v > 100}
sub = accel[(accel.kind == "methods") & accel.fish.isin(fast_fish)]
fm = sub.groupby(["species", "fish"]).accel.median()
print("\nMethods definition, files at 128-171 Hz only: per-fish median over all whole-field frames (deg/s^2):",
f"ZF {fm.loc['ZF'].median():.1f} (n {len(fm.loc['ZF'])}), DC {fm.loc['DC'].median():.1f} (n {len(fm.loc['DC'])}),",
f"Mann-Whitney p = {stats.mannwhitneyu(fm.loc['ZF'], fm.loc['DC']).pvalue:.3g}")
print("\nThe code's mean interval against the median frame interval, by species:")
print(ratio.groupby(ratio.fish.str[:2]).code_dt_over_frame_interval.describe().round(2).to_string())
# Noise floor: the stationary phase (0 to 3 s, grating still) against the motion phase, and the
# measure from the unwrapped f0_theta in place of the cumulative sum of f0_vtheta.
floor = accel[accel.kind.isin(["methods", "theta"])].groupby(["kind", "species", "stationary"]).accel.agg(["median", "size"])
print("\nMedian absolute angular acceleration (deg/s^2) by phase (stationary = first 3 s), frames 10 to 1000:")
print(floor.round(1).to_string())
fish_floor = accel[accel.kind == "methods"].groupby(["species", "fish", "stationary"]).accel.median().unstack()
fish_floor.columns = ["motion", "stationary"]
print("\nPer fish (Methods definition): median in the stationary phase against the motion phase:")
print(fish_floor.round(1).to_string())
for sp in ("ZF", "DC"):
f = fish_floor.loc[sp].dropna()
print(f"{sp}: stationary over motion, median ratio {(f.stationary / f.motion).median():.2f} over {len(f)} fish; "
f"Wilcoxon p = {stats.wilcoxon(f.stationary, f.motion).pvalue:.3g}")
theta = accel_tests("theta")
print("\nVariant: heading from the unwrapped f0_theta:")
print(theta[["name", "ZF_median", "DC_median", "kruskal_p", "ZF_fish_median", "DC_fish_median", "fish_mannwhitney_p"]]
.to_string(index=False, float_format=lambda v: f"{v:.3g}"))
print(f"Stimuli with ZF higher and p < 0.05/9: {int(theta.zf_higher_and_sig.sum())} of 9")
Variant: Fig1-S1.ipynb's quantity (per-frame heading change / file mean frame interval):
name ZF_median DC_median n_ZF n_DC kruskal_p ZF_fish_median DC_fish_median fish_mannwhitney_p zf_higher_and_sig
static 29.5 33.5 11123 8724 0.0148 21.7 19.1 0.338 False
forward 27.2 22.1 62566 47939 0 18 16.7 0.14 True
forward_right 24.4 21.8 81283 48494 0 18.8 16.9 0.046 True
right 26.1 22.4 120287 68546 0 22.2 17 0.00242 True
backward_right 28 24.3 135332 70136 0 22.5 16.6 0.00453 True
backward 28.2 24.2 124936 63830 0 22.5 16.5 0.0143 True
backward_left 28.6 26.7 131434 62142 1.02e-20 24.6 16.7 0.00332 True
left 26.7 25.7 121937 58593 0.724 22.7 16.6 0.00744 False
forward_left 26.3 21.9 81084 43148 0 19.4 15.8 0.0577 True
Stimuli with ZF higher and p < 0.05/9: 7 of 9
Methods definition, files at 128-171 Hz only: per-fish median over all whole-field frames (deg/s^2): ZF 84.9 (n 7), DC 178.6 (n 11), Mann-Whitney p = 6.28e-05
The code's mean interval against the median frame interval, by species:
count mean std min 25% 50% 75% max
fish
DC 12.0 10.9 9.25 1.83 6.52 8.73 13.59 37.31
ZF 25.0 1.5 0.51 1.05 1.20 1.37 1.54 3.12
Median absolute angular acceleration (deg/s^2) by phase (stationary = first 3 s), frames 10 to 1000:
median size
kind species stationary
methods DC False 171.300003 28478
True 205.000000 8417
ZF False 60.799999 215075
True 66.900002 31759
theta DC False 235.800003 25357
True 270.200012 7262
ZF False 66.400002 162345
True 83.599998 26442
Per fish (Methods definition): median in the stationary phase against the motion phase:
motion stationary
species fish
DC DC-sub-0 188.800003 235.600006
DC-sub-1 320.799988 271.799988
DC-sub-10 214.899994 232.300003
DC-sub-11 108.199997 169.399994
DC-sub-2 120.500000 210.199997
DC-sub-3 169.800003 213.300003
DC-sub-4 137.399994 122.800003
DC-sub-5 155.600006 210.399994
DC-sub-6 100.300003 291.899994
DC-sub-7 249.399994 282.799988
DC-sub-8 169.500000 196.199997
DC-sub-9 171.300003 212.399994
ZF ZF-sub-0 62.000000 81.800003
ZF-sub-1 97.199997 116.300003
ZF-sub-10 61.200001 61.299999
ZF-sub-11 45.700001 49.299999
ZF-sub-12 58.200001 62.400002
ZF-sub-13 54.799999 54.900002
ZF-sub-14 42.700001 48.500000
ZF-sub-15 43.500000 50.900002
ZF-sub-16 39.599998 47.200001
ZF-sub-17 52.799999 54.500000
ZF-sub-18 31.700001 37.200001
ZF-sub-19 65.000000 65.500000
ZF-sub-2 68.900002 116.500000
ZF-sub-20 76.000000 81.699997
ZF-sub-21 81.599998 86.900002
ZF-sub-22 70.800003 65.900002
ZF-sub-23 76.500000 62.599998
ZF-sub-24 72.500000 74.800003
ZF-sub-3 123.300003 132.000000
ZF-sub-4 78.599998 129.500000
ZF-sub-5 62.099998 68.099998
ZF-sub-6 83.800003 116.500000
ZF-sub-7 53.500000 57.299999
ZF-sub-8 52.500000 58.200001
ZF-sub-9 63.299999 64.500000
ZF: stationary over motion, median ratio 1.07 over 25 fish; Wilcoxon p = 0.00012
DC: stationary over motion, median ratio 1.24 over 12 fish; Wilcoxon p = 0.0161
Variant: heading from the unwrapped f0_theta:
name ZF_median DC_median kruskal_p ZF_fish_median DC_fish_median fish_mannwhitney_p
static 61.8 278 0 63.1 253 0.00268
forward 77.3 233 0 78 222 2.76e-06
forward_right 66.4 218 0 67.6 225 2e-06
right 73.3 239 0 70.9 219 1.23e-06
backward_right 66.3 261 0 69.6 250 1.23e-06
backward 64.5 251 0 64.1 263 2.76e-06
backward_left 67.4 222 0 67.8 202 3.78e-06
left 69 252 0 74.5 243 1.23e-06
forward_left 71.5 273 0 75.8 283 2e-06
Stimuli with ZF higher and p < 0.05/9: 0 of 9
# Noise floor, direct test: angular acceleration (Methods definition) in frames where the fish is
# still, by species. Speed is from raw positions at 30 Hz with the 7-sample rolling median.
m = accel[accel.kind == "methods"].dropna(subset=["speed_mm_s"])
bands = pd.cut(m.speed_mm_s, [0, 1, 2, 5, np.inf], right=False, labels=["<1", "1-2", "2-5", ">=5"])
print("Median absolute angular acceleration (deg/s^2) by speed band (mm/s), with frames and fish:")
print(m.groupby(["species", bands], observed=True).agg(median=("accel", "median"), frames=("accel", "size"),
fish=("fish", "nunique")).round(1).to_string())
print("\nFraction of frames below 1 mm/s:", m.assign(still=m.speed_mm_s < 1).groupby("species").still.mean().round(3).to_dict())
for limit in (1, 2):
still = m[m.speed_mm_s < limit].groupby(["species", "fish"]).accel.median()
z, d = still.loc["ZF"], still.loc["DC"]
print(f"Still below {limit} mm/s, per-fish medians: ZF {z.median():.1f} (n {len(z)}), DC {d.median():.1f} (n {len(d)}), "
f"Mann-Whitney p = {stats.mannwhitneyu(z, d).pvalue:.3g}")
Median absolute angular acceleration (deg/s^2) by speed band (mm/s), with frames and fish:
median frames fish
species speed_mm_s
DC <1 70.900002 4824 12
1-2 143.899994 2323 12
2-5 210.500000 4399 12
>=5 207.300003 25349 12
ZF <1 34.099998 68177 25
1-2 59.299999 46200 25
2-5 84.500000 70500 25
>=5 85.800003 61957 25
Fraction of frames below 1 mm/s: {'DC': 0.131, 'ZF': 0.276}
Still below 1 mm/s, per-fish medians: ZF 34.9 (n 25), DC 72.4 (n 12), Mann-Whitney p = 0.000138
Still below 2 mm/s, per-fish medians: ZF 43.4 (n 25), DC 96.8 (n 12), Mann-Whitney p = 4.67e-05
Result: not reproduced. With angular acceleration as the Methods define it, DC are higher than ZF for all nine stimuli (medians 143 to 191 against 55.4 to 66.4 deg/s², Kruskal-Wallis p below 0.05/9 for each), the reverse of the claim, so 0 of 9 stimuli meet the criterion. Per-fish medians give the same, and so do the files at 128 to 171 Hz alone (ZF 84.9, DC 178.6 deg/s², p = 6.3e-05). The quantity Fig1-S1.ipynb computes gives ZF higher with p below 0.05/9 for 7 of 9 stimuli. That quantity divides each frame's heading change by the file's mean frame interval, which in DC files is a median 8.73 times the frame interval (ZF 1.37), because DC files hold only trial frames and the gaps between trials enter the mean.
The DC values may be partly tracking noise. In the stationary phase (first 3 s, grating still)
the DC median is 205 deg/s², higher than the 171 during motion (per fish, a median ratio of 1.24,
Wilcoxon p = 0.016); ZF are also higher at rest, 66.9 against 60.8 (ratio 1.07, p = 0.00012), so
this comparison does not separate the species. In frames where the fish is still (speed below
1 mm/s), DC are still higher than ZF (per-fish medians 72.4 against 34.9 deg/s², Mann-Whitney
p = 0.00014; below 2 mm/s, 96.8 against 43.4). In both species the values rise with speed, and DC
are higher in every speed band (moving at 5 mm/s or more, 207 against 85.8). So DC heading is
noisier when still, and DC are also higher when moving; these outputs cannot say whether the
higher values in moving DC are noise or turning during continuous swimming. With the heading from the unwrapped
f0_theta in place of the summed f0_vtheta, DC are higher still (218 to 278 against ZF 61.8 to
77.3 deg/s², 0 of 9 stimuli meet the criterion). Both heading series give DC the higher values, and
neither shows a DC response to motion, so the reversal should not be read as evidence against the
paper.
figs1e-dc-respond-less-monocular¶
DC generally respond less than ZF to monocular stimuli, suggesting that they need more visual motion input to drive the optomotor response.
def monocular_table(lateral_right, lateral_left):
cats = {"static": [(20, 1)], "medial": [(10, 1), (13, -1)],
"lateral": [(lateral_right, 1), (lateral_left, -1)], "binocular": [(3, 1), (2, -1)]}
rows = []
for cat, members in cats.items():
for c, sign in members:
sub = angvel_fish[angvel_fish.code == c]
for _, r in sub.iterrows():
rows.append({"category": cat, "species": r.species, "fish": r.fish, "code": c,
"toward_deg_s": sign * r.angvel_deg_s})
return pd.DataFrame(rows)
def monocular_tests(df):
model = ols("toward_deg_s ~ C(species) + C(category) + C(species):C(category)", data=df).fit()
table = sm.stats.anova_lm(model, typ=2)
rows = []
for cat in ("static", "medial", "lateral", "binocular"):
sub = df[df.category == cat]
t = pairwise_tukeyhsd(sub.toward_deg_s, sub.species)
rows.append({"category": cat, "ZF_median": sub[sub.species == "ZF"].toward_deg_s.median(),
"DC_median": sub[sub.species == "DC"].toward_deg_s.median(),
"n_ZF": int((sub.species == "ZF").sum()), "n_DC": int((sub.species == "DC").sum()),
"p": t.pvalues[0], "significant_at_0.05/4": t.pvalues[0] < 0.05 / 4})
return table, pd.DataFrame(rows)
mono = monocular_table(9, 11)
table, per_cat = monocular_tests(mono)
print("Figure-code assignment (9 lateral right, 11 lateral left). Two-way ANOVA (type II):")
print(table.to_string(float_format=lambda v: f"{v:.4g}"))
print()
print(per_cat.round(4).to_string(index=False))
print("\nPer fish, angular velocity toward the stimulus (deg/s), mean of the two sides:")
print(mono.groupby(["species", "fish", "category"]).toward_deg_s.mean().unstack().round(1).to_string())
Figure-code assignment (9 lateral right, 11 lateral left). Two-way ANOVA (type II):
sum_sq df F PR(>F)
C(species) 3411 1 23.26 2.508e-06
C(category) 1.308e+04 3 29.73 2.166e-16
C(species):C(category) 1745 3 3.966 0.008737
Residual 3.534e+04 241 NaN NaN
category ZF_median DC_median n_ZF n_DC p significant_at_0.05/4
static 2.3291 -1.8061 18 9 0.2046 False
medial 13.4811 3.2584 50 24 0.0003 True
lateral -0.1256 -1.0730 50 24 0.9586 False
binocular 21.8698 2.8671 50 24 0.0002 True
Per fish, angular velocity toward the stimulus (deg/s), mean of the two sides:
category binocular lateral medial static
species fish
DC DC-sub-0 0.6 -4.6 13.3 NaN
DC-sub-1 2.5 -0.1 -4.4 NaN
DC-sub-10 -4.8 -6.0 2.5 -17.9
DC-sub-11 2.7 4.2 2.7 0.5
DC-sub-2 8.4 2.7 3.5 -6.6
DC-sub-3 13.0 -6.0 2.2 NaN
DC-sub-4 17.7 -6.6 9.3 2.3
DC-sub-5 24.0 3.8 11.6 -53.2
DC-sub-6 -1.3 0.3 0.8 -1.8
DC-sub-7 -11.4 -7.4 -4.4 27.5
DC-sub-8 -1.4 -0.3 4.6 4.8
DC-sub-9 -0.2 -4.5 6.4 -48.9
ZF ZF-sub-0 20.9 0.5 19.5 -4.1
ZF-sub-1 31.2 4.7 12.7 NaN
ZF-sub-10 36.4 -8.1 29.4 7.2
ZF-sub-11 24.2 -2.8 14.9 -5.7
ZF-sub-12 32.8 -12.1 19.2 7.4
ZF-sub-13 33.9 -1.1 31.7 NaN
ZF-sub-14 0.8 2.0 3.8 -3.9
ZF-sub-15 9.2 2.4 18.0 6.7
ZF-sub-16 12.5 -0.1 5.7 3.4
ZF-sub-17 9.6 7.4 7.9 NaN
ZF-sub-18 4.0 -0.2 13.5 9.3
ZF-sub-19 30.9 0.1 10.8 NaN
ZF-sub-2 14.8 0.5 6.2 -16.5
ZF-sub-20 23.6 2.0 8.7 5.0
ZF-sub-21 30.9 -8.4 11.4 12.6
ZF-sub-22 -2.6 1.5 3.0 -5.3
ZF-sub-23 30.9 -8.2 20.4 1.3
ZF-sub-24 34.9 -1.6 18.4 17.3
ZF-sub-3 26.1 -25.0 12.7 24.7
ZF-sub-4 22.5 -12.7 17.8 -25.6
ZF-sub-5 -11.1 7.9 -0.5 NaN
ZF-sub-6 19.3 -2.6 20.8 -18.6
ZF-sub-7 -0.1 1.6 -1.2 NaN
ZF-sub-8 1.8 -3.5 -5.3 NaN
ZF-sub-9 24.5 2.2 19.2 -23.5
swapped = monocular_table(11, 9)
t2, p2 = monocular_tests(swapped)
print("Variant, protocol-file assignment (11 lateral right, 9 lateral left):")
print("species effect p =", f"{t2.loc['C(species)', 'PR(>F)']:.3g}")
print(p2.round(4).to_string(index=False))
Variant, protocol-file assignment (11 lateral right, 9 lateral left): species effect p = 2.11e-06 category ZF_median DC_median n_ZF n_DC p significant_at_0.05/4 static 2.3291 -1.8061 18 9 0.2046 False medial 13.4811 3.2584 50 24 0.0003 True lateral 0.1256 1.0730 50 24 0.9586 False binocular 21.8698 2.8671 50 24 0.0002 True
Result: reproduced. The species effect is significant (F = 23.3, p = 2.5e-06), and DC turn less toward the stimulus than ZF for medial stimuli (median 3.26 against 13.48 deg/s, p = 0.0003) and binocular ones (2.87 against 21.87 deg/s, p = 0.0002). For lateral stimuli DC are below ZF only nominally (-1.07 against -0.13 deg/s, p = 0.96): neither species turns toward lateral motion, and the criterion's lateral part holds by 0.95 deg/s. With the protocol file's assignment of codes 9 and 11 the lateral medians change sign (ZF 0.13, DC 1.07 deg/s), DC are no longer below ZF there, and that variant would be partially reproduced. The DC turning time courses (under the link claim) lean toward the protocol file's assignment if the fish turn with lateral motion, so the lateral part of this result is fragile.
fig1c-continuous-vs-bouts¶
ZF swim in discrete bouts separated by glides, while DC swim continuously at a relatively constant velocity.
Measure fixed in the claims file: the ratio of median to mean instantaneous speed (positions at 30 Hz, 3 to 35 s), per fish the median over all trials of all codes.
steady = measures[measures.passes].groupby(["species", "fish"]).steadiness.median()
print("Per fish median steadiness (median / mean instantaneous speed):")
print(steady.round(3).to_string())
z, d = steady.loc["ZF"].dropna(), steady.loc["DC"].dropna()
p = stats.mannwhitneyu(z, d, alternative="two-sided").pvalue
print(f"\nZF median {z.median():.3f} over {len(z)} fish; DC median {d.median():.3f} over {len(d)} fish; Mann-Whitney p = {p:.3g}")
by_rate = steady.reset_index().assign(rate=lambda df: df.fish.map(lambda f: "fast" if hz[f] > 100 else "slow"))
print("\nBy frame rate of the file:")
print(by_rate.groupby(["species", "rate"]).steadiness.agg(["median", "size"]).round(3).to_string())
zs = by_rate[(by_rate.species == "ZF") & (by_rate.rate == "fast")].steadiness
ds = by_rate[(by_rate.species == "DC") & (by_rate.rate == "fast")].steadiness
print(f"Fast files only: ZF {zs.median():.3f} (n {len(zs)}) against DC {ds.median():.3f} (n {len(ds)}), p = {stats.mannwhitneyu(zs, ds).pvalue:.3g}")
fwd = measures[measures.passes & (measures.code == 0)].groupby(["species", "fish"]).steadiness.median()
print(f"Forward trials only (code 0): ZF {fwd.loc['ZF'].median():.3f}, DC {fwd.loc['DC'].median():.3f}, p = {stats.mannwhitneyu(fwd.loc['ZF'].dropna(), fwd.loc['DC'].dropna()).pvalue:.3g}")
Per fish median steadiness (median / mean instantaneous speed):
species fish
DC DC-sub-0 0.958
DC-sub-1 0.917
DC-sub-10 0.974
DC-sub-11 0.884
DC-sub-2 0.848
DC-sub-3 0.898
DC-sub-4 0.999
DC-sub-5 0.896
DC-sub-6 0.140
DC-sub-7 0.985
DC-sub-8 0.741
DC-sub-9 0.968
ZF ZF-sub-0 0.298
ZF-sub-1 0.392
ZF-sub-10 0.292
ZF-sub-11 0.306
ZF-sub-12 0.273
ZF-sub-13 0.302
ZF-sub-14 0.371
ZF-sub-15 0.293
ZF-sub-16 0.276
ZF-sub-17 0.267
ZF-sub-18 0.580
ZF-sub-19 0.435
ZF-sub-2 0.244
ZF-sub-20 0.359
ZF-sub-21 0.365
ZF-sub-22 0.426
ZF-sub-23 0.320
ZF-sub-24 0.339
ZF-sub-3 0.414
ZF-sub-4 0.339
ZF-sub-5 0.263
ZF-sub-6 0.283
ZF-sub-7 0.281
ZF-sub-8 0.342
ZF-sub-9 0.302
ZF median 0.306 over 25 fish; DC median 0.908 over 12 fish; Mann-Whitney p = 5.36e-05
By frame rate of the file:
median size
species rate
DC fast 0.898 11
slow 0.999 1
ZF fast 0.298 7
slow 0.313 18
Fast files only: ZF 0.298 (n 7) against DC 0.898 (n 11), p = 0.00283
Forward trials only (code 0): ZF 0.376, DC 0.956, p = 1.23e-06
Result: reproduced. The median-to-mean ratio of instantaneous speed is 0.306 in ZF (25 fish, 0.244 to 0.580) and 0.908 in DC (12 fish, 11 of them above 0.74 and one at 0.140), Mann-Whitney p = 5.4e-05. The difference holds in the files at 128 to 171 Hz alone (0.298 against 0.898, p = 0.0028) and in forward trials alone (0.376 against 0.956, p = 1.2e-06).
Summary¶
A failure to reproduce here may be a failure of this attempt, not of the paper: see the reasoning and the differences from the paper's method.
| Claim | Data | Outcome |
|---|---|---|
| stimulus-code-link | with inputs | reproduced |
| fig1d-dc-faster-all-codes | dandiset | reproduced |
| fig1d-median-velocity | with inputs | reproduced |
| fig1d-zf-forward-faster-than-static | with inputs | reproduced |
| fig1d-dc-no-modulation | with inputs | not reproduced |
| fig1d-dc-faster-oblique | with inputs | reproduced |
| fig1g-follow-direction | with inputs | partially reproduced |
| fig1h-zf-higher-angular-acceleration | with inputs | not reproduced |
| figs1e-dc-respond-less-monocular | with inputs | reproduced |
| fig1c-continuous-vs-bouts | dandiset | reproduced |
| fig1cef-representative-values | dandiset | blocked: metadata insufficient |
| fig2d, fig2g, fig2h, fig2k, fig2l, fig3e, fig3cd, fig4f, fig4hi | dandiset | blocked: data missing |
How the analysis differs from the paper's. The stimulus names come from the paper's code and
the stimulus software, not from the dandiset. The trial processing follows the paper's notebook
Fig1-S1.ipynb, except that DC files have no vel column, so the code's filter on it is not
applied. The dandiset has 25 ZF, not 26. Angular acceleration follows the Methods' definition, on
heading resampled to 30 Hz; the code's quantity is a variant. The test of "following the direction"
and the measure of continuous swimming are choices made here, as the paper gives no test for the
first and bases the second on bout files that are not in the dandiset.
What was found about the dandiset.
- It holds only the freely swimming behaviour of Figure 1 and Figure S1. The imaging and head-fixed tail tracking of Figures 2 to 4 are not in it, and the GitLab repository the paper names for "all behavioral and imaging data" holds notebooks only.
- The stimulus codes 0 to 20 have no names or description in the files or the metadata. The paper's code and the stimulus software disagree on which of codes 9 and 11 is lateral right.
- Subject ids 0 to 11 are each used for one ZF and one DC; the archive's summary counts 25 subjects and lists only Danio rerio.
f0_vthetais a heading change per frame, not radians/s as stated; in most DC files its sum does not match the change off0_theta.- 18 of 25 ZF files and 1 DC file are at 32 to 35 Hz, not above 150 Hz as the Methods say; their timestamp intervals and trial lengths show these are the real acquisition rates.
- No spatial scale is given; positions in pixels are consistent with the code's 10 pixels per mm, except one DC file whose positions reach 1490 pixels.
- ZF and DC files were exported differently: ZF files keep untracked and between-trial frames,
DC files hold only tracked trial frames and an extra
distseries. - The paper's own Tukey output finds three DC pairs significant (static against backward left, backward right and forward left), as here, while the paper states that DC do not modulate speed.
- The stored
distseries of DC files does not agree with the positions. The code's speed check uses it, but the check from positions passes the same trials; 11 DC trials above 100 mm/s pass because the check covers 0 to 35 s, not the 3 to 35 s the speed is measured on. - The paper's code computes "angular acceleration" as heading change per frame divided by the file's mean frame interval, which the gaps between trials inflate in DC files; the Figure 1H result depends on it. With the Methods' definition, DC values are higher than ZF at every speed, including when the fish is still (72.4 against 34.9 deg/s² below 1 mm/s), so part of the DC values may be tracking noise; the outputs cannot say how much.