Gillon, Lecoq et al. (2023) and dandisets 000336 and 000037¶
Paper: Gillon CJ, Lecoq JA, et al. "Responses of pyramidal cell somata and apical dendrites in mouse
visual cortex over multiple days." Scientific Data 10, 287 (2023), doi:10.1038/s41597-023-02214-y.
The claims file (claims/000336/10.1038-s41597-023-02214-y.yaml) was written from the version of
record as served by Europe PMC (PMC10192216), with its tables and figure legends; Supplementary Table 1
was not available.
Target dataset: DANDI:000336, version 0.260325.1802, the only published version (the other is the
draft). It is the OpenScope "Dendritic Coupling" project. It names the paper among its related
resources, but the paper's data are on DANDI:000037 (OpenScope "Credit Assignment"), the dandiset the
paper's Data Records cite. The first claim tests whether 000336 holds any of the paper's sessions; all
other claims are marked data: elsewhere in the claims file and are tested on DANDI:000037.
Data used: DANDI:000037, version 0.230426.0054 (doi:10.48324/DANDI.000037/0.230426.0054). The paper
cites the dandiset without a version. The claims file names this version for every claim, and it is the
version published shortly before the paper (April 2023, paper published May 2023). The later version
0.240209.1623 adds only a CHANGES file (shown below), so the choice does not change the data read.
Only the basic files (_behavior+ophys.nwb) are read, streamed from the archive.
Outside input: the session table of the paper's code, mouse_df.csv of
github.com/colleenjg/OpenScope_CA_Analysis at commit 046e7fef5f452cdc0775fe54040b0f13aeae1e57, for the
session numbers (1 to 6 per mouse) and the QC status, which the files do not carry. The paper's Tables 1
and 2, copied from its text into the helper module, are used only as reference values.
| Claim | Tested here |
|---|---|
| dataset-holds-paper-sessions | yes, on 000336 |
| table1-subjects | yes |
| table2-sessions | yes |
| session-schedule | yes |
| imaging-planes | yes |
| roi-counts-per-session | yes |
| tracked-rois-per-plane | yes |
| fig8-example-matches | yes |
| fig8-ordering-variation | yes, in roi_matching_orderings.ipynb |
| stimulus-durations | yes |
| inconsistent-event-fraction | yes |
| fig5-stimulus-responses | yes |
| fig6-behaviour | yes |
| fig7a-snr-high-stable | yes |
| fig7b-signal-stable | yes |
| fig7c-correlations-similar | yes |
| high-correlation-pairs | yes |
| fig9-onset-offset-transients | yes |
import os
os.environ.setdefault("OMP_NUM_THREADS", "4")
import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats
import h5py, remfile, pynwb
import credassign as ca
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 200)
T0 = time.time()
The target, DANDI:000336¶
The archive's metadata, the file list, and one file opened and listed in full, including every column of every table.
m336 = ca.version_metadata(ca.TARGET_DANDISET, ca.TARGET_VERSION)
print(m336["name"])
print("published versions:", [v["version"] for v in ca.get_json(f"{ca.API}/dandisets/{ca.TARGET_DANDISET}/versions/")["results"]])
print("subjects (archive summary):", m336["assetsSummary"]["numberOfSubjects"], " files:", m336["assetsSummary"]["numberOfFiles"],
" bytes:", m336["assetsSummary"]["numberOfBytes"])
for r in m336["relatedResource"]:
print("related:", r["relation"], r.get("identifier", r["url"]), "|", r["name"])
a336 = ca.list_assets(ca.TARGET_DANDISET, ca.TARGET_VERSION)
a336["subject"] = a336.path.str.extract(r"sub-(\d+)/")[0]
a336["session"] = a336.path.str.extract(r"_ses-(\d+)")[0]
a336["kind"] = a336.path.str.extract(r"_([a-z+]+)\.nwb$")[0]
print(len(a336), "assets;", a336.subject.nunique(), "subjects;", a336.session.nunique(), "session IDs")
print(a336.groupby("kind").size().to_string())
print("subjects:", sorted(a336.subject.unique()))
Allen Institute Openscope - Dendritic Coupling project
published versions: ['draft', '0.260325.1802'] subjects (archive summary): 16 files: 2356 bytes: 42229353097330 related: dcite:IsDocumentedBy https://alleninstitute.github.io/openscope_databook/projects/dendritic_coupling.html | OpenScope Databook - Dendritic Coupling project documentation related: dcite:IsDocumentedBy https://github.com/AllenInstitute/openscope_dendritic_coupling | Public code repository for Dendritic Coupling project related: dcite:Describes 10.1016/j.tins.2012.11.006 | A cellular mechanism for cortical associations: an organizing principle for the cerebral cortex. related: dcite:Describes 1810.11393 | Dendritic cortical microcircuits approximate the backpropagation algorithm related: dcite:Describes 10.1016/j.neuron.2018.10.003 | Predictive Processing: A Canonical Cortical Computation related: dcite:Describes 10.1038/s41597-023-02214-y | Responses of pyramidal cell somata and apical dendrites in mouse visual cortex over multiple days
2356 assets; 16 subjects; 104 session IDs kind image 2 image+ophys 362 ophys 1992 subjects: ['621602', '621603', '631188', '634402', '635489', '641331', '644972', '647715', '655042', '655580', '656228', '660986', '661749', '662470', '690226', '698808']
# One file of 000336, the smallest processed ophys file, listed in full.
small = a336[a336.kind == "ophys"].sort_values("size").iloc[0]
print(small.path, small["size"])
with pynwb.NWBHDF5IO(file=ca.open_h5(small.asset_id), load_namespaces=True) as io:
nwb = io.read()
print("session_start_time:", nwb.session_start_time, "| session_id:", nwb.session_id, "| identifier:", nwb.identifier)
s = nwb.subject
print("subject:", s.subject_id, s.genotype, s.sex, s.age, "|", s.description)
for k, p in nwb.imaging_planes.items():
print("imaging plane", k, "|", p.description, "|", p.location, "| rate", p.imaging_rate)
print("acquisition:", {k: type(v).__name__ for k, v in nwb.acquisition.items()})
for name, mod in nwb.processing.items():
for dn, d in mod.data_interfaces.items():
print("processing", name, dn, type(d).__name__)
if hasattr(d, "colnames"):
print(" columns:", list(d.colnames))
for attr in ("roi_response_series", "time_series", "plane_segmentations"):
for sn, ser in (getattr(d, attr, None) or {}).items():
print(" ", sn, getattr(getattr(ser, "data", None), "shape", None), list(getattr(ser, "colnames", [])))
for k, t in nwb.intervals.items():
print("intervals", k, len(t), "rows; columns:", list(t.colnames))
sub-662470/sub-662470_ses-1253666600-acq-1253834948_ophys.nwb 89216248
session_start_time: 2023-03-13 15:58:48.095000+00:00 | session_id: None | identifier: 1253834948
subject: 662470 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt F P84.0D | external: 662470 donor_id: 1244572630 specimen_id: [(1244572640,)]
imaging plane imaging_plane | Failed Cell Segmentation | Failed Cell Segmentation | rate 10.71
imaging plane imaging_plane_1 | (512, 512) field of view in VISl at depth 16 um | VISl | rate 10.0
acquisition: {'EyeTracking': 'EllipseEyeTracking', 'v_in': 'TimeSeries', 'v_sig': 'TimeSeries'}
processing ophys images Images
processing ophys ophys_motion_correction_x TimeSeries
processing ophys ophys_motion_correction_y TimeSeries
processing running dx TimeSeries
processing running speed TimeSeries
processing running speed_unfiltered TimeSeries
processing stimulus timestamps TimeSeries
processing stimulus_ophys timestamps TimeSeries
intervals fixed_gabors_presentations 3000 rows; columns: ['start_time', 'stop_time', 'stimulus_name', 'stimulus_block', 'units', 'stimulus_index', 'OriSurp', 'PosSizesAll', 'colors', 'contrs', 'depths', 'elementMask', 'elementTex', 'fieldDepth', 'fieldPos', 'fieldShape', 'fieldSize', 'nElements', 'opacities', 'oris', 'phases', 'rgbs', 'sfs', 'sizes', 'xys', 'tags', 'timeseries']
intervals gratings_presentations 624 rows; columns: ['start_time', 'stop_time', 'stimulus_name', 'stimulus_block', 'color', 'contrast', 'mask', 'opacity', 'ori', 'size', 'units', 'stimulus_index', 'spatial_frequency', 'phase', 'tags', 'timeseries']
intervals movie_flower_fwd_presentations 16200 rows; columns: ['start_time', 'stop_time', 'stimulus_name', 'stimulus_block', 'frame', 'color', 'contrast', 'opacity', 'ori', 'size', 'units', 'stimulus_index', 'tags', 'timeseries']
intervals movie_touch_of_evil_fwd_presentations 16200 rows; columns: ['start_time', 'stop_time', 'stimulus_name', 'stimulus_block', 'frame', 'color', 'contrast', 'opacity', 'ori', 'size', 'units', 'stimulus_index', 'tags', 'timeseries']
intervals movie_worms_fwd_presentations 16200 rows; columns: ['start_time', 'stop_time', 'stimulus_name', 'stimulus_block', 'frame', 'color', 'contrast', 'opacity', 'ori', 'size', 'units', 'stimulus_index', 'tags', 'timeseries']
intervals rotate_gabors_presentations 3000 rows; columns: ['start_time', 'stop_time', 'stimulus_name', 'stimulus_block', 'units', 'stimulus_index', 'OriSurp', 'PosSizesAll', 'colors', 'contrs', 'depths', 'elementMask', 'elementTex', 'fieldDepth', 'fieldPos', 'fieldShape', 'fieldSize', 'nElements', 'opacities', 'oris', 'phases', 'rgbs', 'sfs', 'sizes', 'xys', 'tags', 'timeseries']
intervals spontaneous_presentations 183 rows; columns: ['start_time', 'stop_time', 'stimulus_name', 'tags', 'timeseries']
The paper's data, DANDI:000037¶
The files list, the difference between its published versions, and one basic file listed in full.
v37 = [v["version"] for v in ca.get_json(f"{ca.API}/dandisets/{ca.DANDISET}/versions/")["results"]]
print("versions of 000037:", v37)
a37 = ca.list_assets()
later = ca.list_assets(ca.DANDISET, "0.240209.1623")
both = a37.merge(later, on="path", how="outer", suffixes=("", "_later"), indicator=True)
print("only in the pinned version:", both.loc[both._merge == "left_only", "path"].tolist())
print("only in 0.240209.1623:", both.loc[both._merge == "right_only", "path"].tolist())
print("paths in both with a different size:", int((both.loc[both._merge == "both", "size"] != both.loc[both._merge == "both", "size_later"]).sum()))
a37["subject"] = a37.path.str.extract(r"sub-(\d+)/")[0]
a37["session"] = a37.path.str.extract(r"_ses-(\d+)_")[0]
a37["kind"] = a37.path.str.extract(r"_ses-\d+_(.*)\.nwb$")[0]
print(a37.groupby("kind").agg(n=("path", "size"), min_gb=("size", lambda x: x.min() / 1e9), max_gb=("size", lambda x: x.max() / 1e9)))
basic = a37[a37.kind == "behavior+ophys"].sort_values(["subject", "session"]).reset_index(drop=True)
print(len(basic), "basic files,", basic.session.nunique(), "sessions,", basic.subject.nunique(), "subjects")
print("each session has all three versions:", (a37.groupby("session").size() == 3).all())
versions of 000037: ['draft', '0.230426.0054', '0.240209.1623']
only in the pinned version: []
only in 0.240209.1623: ['CHANGES']
paths in both with a different size: 0
n min_gb max_gb
kind
behavior+image+ophys 50 1.589581 3.188968
behavior+ophys 50 0.136659 1.703330
obj-raw_behavior+image+ophys 50 42.864406 49.504858
50 basic files, 50 sessions, 13 subjects
each session has all three versions: True
# One basic file of 000037 listed in full (subject 418779, session 777496949).
row = basic[basic.session == "777496949"].iloc[0]
with pynwb.NWBHDF5IO(file=ca.open_h5(row.asset_id), load_namespaces=True) as io:
nwb = io.read()
print("session_start_time:", nwb.session_start_time, "| session_id:", nwb.session_id)
print("subject:", nwb.subject.subject_id, nwb.subject.genotype, nwb.subject.sex, nwb.subject.age)
for k, p in nwb.imaging_planes.items():
print("imaging plane", k, "| location", p.location, "| rate", p.imaging_rate, "| indicator", p.indicator)
for name, mod in nwb.processing.items():
for dn, d in mod.data_interfaces.items():
for attr in ("roi_response_series", "time_series", "plane_segmentations", "images"):
for sn, ser in (getattr(d, attr, None) or {}).items():
print(name, dn, sn, getattr(ser, "data", None).shape if hasattr(ser, "data") else "",
getattr(ser, "unit", ""), list(getattr(ser, "colnames", [])), getattr(ser, "description", ""))
for col in nwb.trials.columns:
print("trials column:", col.name, "|", col.description)
tr = nwb.trials.to_dataframe()
print(tr.groupby(["stimulus_type", "stimulus_template_name"]).size())
print("acquisition:", list(nwb.acquisition), "| stimulus:", list(nwb.stimulus))
session_start_time: 2018-11-13 01:51:48.328000-08:00 | session_id: 777496949
subject: 418779 Rbp4-Cre_KL100;Camk2a-tTA;Ai93 F P84D
imaging plane ImagingPlane | location {'area': 'VISp', 'depth': '375'} | rate 30.0 | indicator GCaMP6f
behavior BehavioralTimeSeries running_velocity (251999,) cm/s [] Velocity of the mouse on a rotating disc.
behavior PupilTracking pupil_diameter (126727,) mm [] Diameter of the mouse pupil (right) facing the stimulus presentation screen.
behavior PupilTracking pupil_position_x (126727,) mm [] Position in x of the center of the mouse pupil (right) facing the stimulus presentation screen.
behavior PupilTracking pupil_position_y (126727,) mm [] Position in y of the center of the mouse pupil (right) facing the stimulus presentation screen.
ophys DfOverF RoiResponseSeries (126727, 15) Normalized fluorescence (A.U.) [] ROI traces
ophys ImageSegmentation PlaneSegmentation ['image_mask', 'tracking_id'] Segmentation for imaging plane (de Vries et al., 2019, Nat Neurosci)
ophys PlaneImages max_projection (512, 512) [] Normalized maximum projection of the motion corrected z-stack, averaged over 8 frames (hei x wid).
trials column: start_time | Start time of epoch, in seconds
trials column: stop_time | Stop time of epoch, in seconds
trials column: stimulus_type | Type of stimulus presented during the trial.
trials column: stimulus_template_name | Name of the stimulus template for the trial.
trials column: unexpected | Whether the stimulus features are expected (0) or unexpected (1).
trials column: gabor_frame | Gabor frame or set, defined by Gabor patch locations and sizes (A, B, C, D or U, G).
trials column: gabor_kappa | Kappa (dispersion) of the von Mises distribution used to sample Gabor patch orientations (degrees).
trials column: gabor_mean_orientation | Mean orientation of the von Mises distribution used to sample Gabor patch orientations (degrees, [0, 360].
trials column: gabor_number | Number of Gabor patches on-screen (before warping mask is applied).
trials column: gabor_locations_x_index | Index for VectorData 'gabor_locations_x'
trials column: gabor_locations_x | Locations along the x dimension (width) of the Gabor patch centers (in pixels), centered on the middle of the screen. All locations are for the unwarped stimulus.
trials column: gabor_locations_y_index | Index for VectorData 'gabor_locations_y'
trials column: gabor_locations_y | Locations along the y dimension (height) of Gabor patch centers (in pixels), centered on the middle of the screen. All locations are for the unwarped stimulus.
trials column: gabor_sizes_index | Index for VectorData 'gabor_sizes'
trials column: gabor_sizes | Gabor patch sizes (in pixels, with patch width defined, as in psychopy, as width across 6 standard deviations).
trials column: gabor_orientations_index | Index for VectorData 'gabor_orientations'
trials column: gabor_orientations | Orientations of the Gabor patches (degrees, [0, 360]).
trials column: main_flow_direction | Main direction of flow for the squares (all moving at 50 visual degrees/second). Flow directions are either rightward (nasal-to-temporal) or leftward (temporal-to-nasal).
trials column: num_frames_stim | Stop frame number for the stimulus frames (excluded).
trials column: num_frames_twop | Stop frame number for the two-photon frames (excluded).
trials column: square_size | Size of the squares (in pixels).
trials column: square_number | Total number of squares present, both in the off and on-screen areas (before warping mask is applied).
trials column: square_proportion_flipped | Proportion of squares moving in the direction opposite to the main flow (all moving at 50 visual degrees/second).
trials column: square_locations_x_index | Index for VectorData 'square_locations_x'
trials column: square_locations_x | Locations along the x dimension (width) of square centers for each frame of the trial (in pixels), (square x stimulus frame), centered on the middle of the screen. Note that squares disappear a bit off-screen, and are (re)initialized a bit off-screen. All locations are for the unwarped stimulus.
trials column: square_locations_y_index | Index for VectorData 'square_locations_y'
trials column: square_locations_y | Locations along the y dimension (height) of square centers for each frame of the trial (in pixels), (square x stimulus frame), centered on the middle of the screen. Note that squares are (re)initialized a bit off-screen area. All locations are for the unwarped stimulus.
trials column: start_frame_stim_template | Start frame number for the trial in the corresponding template.
trials column: start_frame_stim | Start frame number for the stimulus frames.
trials column: start_frame_twop | Start frame number for the two-photon frames.
trials column: stop_frame_stim | Stop frame number for the stimulus frames (excluded).
trials column: stop_frame_twop | Stop frame number for the two-photon frames (excluded).
stimulus_type stimulus_template_name
gabors gabors 6800
grayscreen grayscreen 4
visflow visflow_left 1020
visflow_right 1020
dtype: int64
acquisition: [] | stimulus: []
Data check¶
All 50 basic files are opened for their metadata (subject, session, plane, stream lengths and the trial
table). The ROI traces of the 33 analysed sessions (sessions 1 to 3 that passed QC, by the paper's
code table) are read once, and every measure the claims need is computed as they load (helper,
roi_measures). This takes about eight minutes.
mouse_df = ca.read_mouse_df()
mouse_df["sessid"] = mouse_df.sessid.astype(str)
mouse_df["mouseid"] = mouse_df.mouseid.astype(str)
table1, table2 = ca.paper_table1(), ca.paper_table2()
analysed = mouse_df[(mouse_df.pass_fail == "P") & mouse_df.sess_n.between(1, 3)]
ANALYSED = set(analysed.sessid)
SESS1 = set(analysed.loc[analysed.sess_n == 1, "sessid"])
print(len(ANALYSED), "analysed sessions from", analysed.mouseid.nunique(), "mice;", len(SESS1), "session-1 files")
meta, trials, roi, beh = {}, {}, {}, {}
for r in basic.itertuples():
f = ca.open_h5(r.asset_id)
meta[r.session] = ca.session_metadata(f)
trials[r.session] = ca.trials_table(f)
if r.session in ANALYSED:
roi[r.session] = ca.roi_measures(f, trials[r.session], fig5=r.session in SESS1)
beh[r.session] = ca.behaviour(f)
f.close()
print(f"loaded in {(time.time() - T0) / 60:.1f} min")
33
analysed sessions from 11 mice; 11 session-1 files
loaded in 12.0 min
md_ = pd.DataFrame([{k: v for k, v in m.items() if k not in ("tracking_ids", "twop_timestamps")} for m in meta.values()])
md_["session"] = list(meta)
md_["depth"] = md_.location.str.extract(r"'depth': '(\d+)'")[0].astype(int)
md_["area"] = md_.location.str.extract(r"'area': '(\w+)'")[0]
md_["line"] = np.where(md_.genotype.str.startswith("Cux2"), "L2/3", np.where(md_.genotype.str.startswith("Rbp4"), "L5", "?"))
md_["plane"] = np.where(md_.depth.isin([175, 375]), "soma", "dend")
md_ = md_.merge(mouse_df[["sessid", "sess_n", "pass_fail", "full_timestamp", "date", "plane", "line", "nrois", "nrois_tracked"]]
.rename(columns={"sessid": "session", "plane": "plane_code", "line": "line_code"}), on="session", how="left")
md_["analysed"] = md_.session.isin(ANALYSED)
print("files:", len(md_), "| subjects:", md_.subject_id.nunique(), "| sessions:", md_.session_id.nunique())
print("session_id equals the file name's session:", (md_.session_id == md_.session).all())
print("subject_id equals the file name's subject:", (md_.subject_id.values == basic.subject.values).all())
print(md_.groupby(["line", "plane"]).agg(mice=("subject_id", "nunique"), sessions=("session", "size"), analysed=("analysed", "sum")))
print("plane from depth agrees with the code table:", (md_.plane == md_.plane_code).all(),
"| line from genotype agrees:", ((md_.line == "L2/3") == md_.line_code.str.startswith("L23")).all())
print("areas:", md_.area.unique(), "| imaging rates (metadata):", md_.imaging_rate.unique())
print("2p frame interval, s: median over files", md_.twop_median_dt.median(), "range", md_.twop_median_dt.min(), md_.twop_median_dt.max())
print("2p frames per session:", md_.twop_n.min(), "to", md_.twop_n.max(), "| dF/F shape frames equal timestamps:",
all(m["dff_shape"][0] == m["twop_n"] for m in meta.values()), "| dF/F unit:", md_.dff_unit.unique())
print("running unit:", md_.run_unit.unique(), "| running sample interval (s):", md_.run_median_dt.min(), "to", md_.run_median_dt.max())
print("pupil unit:", md_.pupil_unit.unique(), "| pupil samples equal 2p frames:", (md_.pupil_n == md_.twop_n).all())
print("max projection shapes:", md_.max_projection_shape.astype(str).unique())
print("files whose pupil samples differ from their 2p frames:", md_.loc[md_.pupil_n != md_.twop_n, ["session", "pupil_n", "twop_n"]].to_dict("records"))
print("files without a tracking_id column:", int((~md_.has_tracking_column).sum()), "| their tracked count in Table 2:",
sorted(set(table2.set_index("session_id").loc[md_.loc[~md_.has_tracking_column, "session"], "n_tracked"])))
files: 50 | subjects: 13 | sessions: 50
session_id equals the file name's session: True
subject_id equals the file name's subject: True
mice sessions analysed
line plane
L2/3 dend 3 12 6
soma 3 12 9
L5 dend 4 16 9
soma 3 10 9
plane from depth agrees with the code table: True | line from genotype agrees: True
areas: <StringArray>
['VISp']
Length: 1, dtype: str | imaging rates (metadata): [30.]
2p frame interval, s: median over files 0.033249999999952706 range 0.03316999999969994 0.033249999999952706
2p frames per session: 126721 to 127588 | dF/F shape frames equal timestamps: True | dF/F unit: <StringArray>
['Normalized fluorescence (A.U.)']
Length: 1, dtype: str
running unit: <StringArray>
['cm/s']
Length: 1, dtype: str | running sample interval (s): 0.016679999999723805 to 0.01667999999995118
pupil unit: <StringArray>
['mm']
Length: 1, dtype: str | pupil samples equal 2p frames: False
max projection shapes: <StringArray>
['(512, 512)']
Length: 1, dtype: str
files whose pupil samples differ from their 2p frames: [{'session': '759872185', 'pupil_n': 61134, 'twop_n': 127588}]
files without a tracking_id column: 11 | their tracked count in Table 2: [0]
# The paper's code table (mouse_df.csv), the columns used, for the 50 sessions, beside Table 2.
cols = ["mouseid", "sessid", "sess_n", "date", "pass_fail", "depth", "plane", "line", "nrois", "nrois_tracked"]
mdf = mouse_df[mouse_df.sessid.isin(table2.session_id)][cols].sort_values(["mouseid", "sess_n"])
print(mdf.to_string(index=False))
t2_ = table2.set_index("session_id").loc[mdf.sessid]
print("code table rows for the 50 sessions of Table 2:", len(mdf))
print("agrees with Table 2 on: QC", ((mdf.pass_fail.values == "P") == (t2_.qc.values == "passed")).sum(),
"| date", (mdf.date.astype(str).values == t2_.date.values).sum(), "| depth", (mdf.depth.values == t2_.depth.values).sum(),
"| ROIs", (mdf.nrois.values == t2_.n_rois.values).sum(), "| tracked ROIs", (mdf.nrois_tracked.clip(lower=0).values == t2_.n_tracked.values).sum(), "of 50",
"(the code table gives -1 where Table 2 gives 0)")
# Session numbers: within each mouse, sess_n runs 1, 2, ... in order of Table 2's dates.
order_ok = all((g.sort_values("date").sess_n.values == np.arange(1, len(g) + 1)).all() for _, g in mdf.groupby("mouseid"))
print("sess_n numbers each mouse's sessions 1, 2, ... in date order:", order_ok)
mouseid sessid sess_n date pass_fail depth plane line nrois nrois_tracked 408021 758519303 1 20180926 P 175 soma L23-Cux2 96 59 408021 759189643 2 20180927 P 175 soma L23-Cux2 74 59 408021 759660390 3 20181001 P 175 soma L23-Cux2 107 59 411400 759666166 1 20181001 F 20 dend L5-Rbp4 942 -1 411400 759872185 2 20181002 F 20 dend L5-Rbp4 289 -1 411400 760269100 3 20181003 F 20 dend L5-Rbp4 524 -1 411400 761730740 4 20181009 P 20 dend L5-Rbp4 630 162 411400 762415169 5 20181011 P 20 dend L5-Rbp4 637 162 411400 763646681 6 20181015 P 20 dend L5-Rbp4 597 162 411424 761624763 1 20181009 P 175 soma L23-Cux2 87 55 411424 761944562 2 20181010 P 175 soma L23-Cux2 90 55 411424 762250376 3 20181011 P 175 soma L23-Cux2 80 55 411771 760260459 1 20181003 P 375 soma L5-Rbp4 90 47 411771 760659782 2 20181004 P 375 soma L5-Rbp4 70 47 411771 761269197 3 20181008 P 375 soma L5-Rbp4 79 47 412933 763949859 1 20181016 F 75 dend L23-Cux2 1041 -1 412933 764897534 2 20181017 F 75 dend L23-Cux2 948 -1 412933 765427689 3 20181018 F 75 dend L23-Cux2 836 -1 412933 766755831 4 20181022 P 50 dend L23-Cux2 344 98 412933 767254594 5 20181023 P 50 dend L23-Cux2 168 98 412933 768807532 6 20181025 P 50 dend L23-Cux2 250 98 413663 764704289 1 20181017 P 50 dend L23-Cux2 628 136 413663 765193831 2 20181018 P 50 dend L23-Cux2 348 136 413663 766502238 3 20181022 P 50 dend L23-Cux2 512 136 418779 777496949 1 20181112 P 375 soma L5-Rbp4 15 12 418779 778374308 2 20181113 P 375 soma L5-Rbp4 26 12 418779 779152062 3 20181114 P 375 soma L5-Rbp4 29 12 420011 777914830 1 20181113 P 20 dend L5-Rbp4 205 51 420011 778864809 2 20181114 P 20 dend L5-Rbp4 159 51 420011 779650018 3 20181115 P 20 dend L5-Rbp4 182 51 433414 826187862 1 20190220 P 75 dend L23-Cux2 727 118 433414 826773996 2 20190221 P 75 dend L23-Cux2 300 118 433414 827833392 3 20190222 P 75 dend L23-Cux2 333 118 433448 826338612 1 20190220 P 20 dend L5-Rbp4 1636 112 433448 826819032 2 20190221 P 20 dend L5-Rbp4 445 112 433448 828816509 3 20190225 P 20 dend L5-Rbp4 496 112 433448 829283315 4 20190226 P 20 dend L5-Rbp4 436 -1 433451 823453391 1 20190215 P 20 dend L5-Rbp4 966 353 433451 824434038 2 20190218 P 20 dend L5-Rbp4 1029 353 433451 825180479 3 20190219 P 20 dend L5-Rbp4 986 353 433458 826659257 1 20190221 P 375 soma L5-Rbp4 99 70 433458 827300090 2 20190222 P 375 soma L5-Rbp4 87 70 433458 828475005 3 20190225 P 375 soma L5-Rbp4 97 70 433458 829520904 4 20190227 P 375 soma L5-Rbp4 88 -1 440889 832883243 1 20190306 P 175 soma L23-Cux2 224 147 440889 833704570 2 20190307 P 175 soma L23-Cux2 224 147 440889 834403597 3 20190308 P 175 soma L23-Cux2 210 147 440889 836968429 4 20190314 P 175 soma L23-Cux2 205 -1 440889 837360280 5 20190315 F 175 soma L23-Cux2 217 -1 440889 838633305 6 20190318 F 175 soma L23-Cux2 227 -1 code table rows for the 50 sessions of Table 2: 50 agrees with Table 2 on: QC 50 | date 50 | depth 50 | ROIs 50 | tracked ROIs 50 of 50 (the code table gives -1 where Table 2 gives 0) sess_n numbers each mouse's sessions 1, 2, ... in date order: True
# Ranges of the variables used, analysed sessions.
r_ = pd.DataFrame({s: {"dff_min": v["dff_min"], "dff_max": v["dff_max"], "rois": v["n_rois"], "rois_nan_or_inf": v["n_bad"]}
for s, v in roi.items()}).T
print("dF/F over all ROIs and frames:", r_.dff_min.min(), "to", r_.dff_max.max(), "| sessions with NaN or Inf ROIs:", int((r_.rois_nan_or_inf > 0).sum()))
b_ = pd.DataFrame({s: {"run_min": np.min(v["run"]), "run_max": np.max(v["run"]), "run_dropped": v["run_dropped"],
"pup_min": np.min(v["pupil"]), "pup_max": np.max(v["pupil"]), "pup_nan": v["pupil_nan"],
"pup_dropped": v["pupil_dropped"]} for s, v in beh.items()}).T
print(b_.describe().loc[["min", "50%", "max"]])
# Duplicates: frame and ROI counts, and a fingerprint of the first 1000 frames of every ROI.
dup = md_.groupby(["twop_n", "n_rois"]).session.apply(list)
print("sessions sharing frame and ROI counts:", [v for v in dup if len(v) > 1])
fp = pd.Series({s: v["fingerprint"] for s, v in roi.items()})
print("analysed sessions sharing a trace fingerprint:", int(fp.duplicated(keep=False).sum()))
dF/F over all ROIs and frames: -10.346665467865007 to 30.209230369583594 | sessions with NaN or Inf ROIs: 0
run_min run_max run_dropped pup_min pup_max pup_nan pup_dropped
min -28.410246 11.064477 0.0 0.033900 0.736593 79.0 1.0
50% -10.081872 51.221538 0.0 0.218147 0.877274 504.0 9.0
max -1.899914 83.848043 12.0 0.530078 1.021494 2906.0 23.0
sessions sharing frame and ROI counts: []
analysed sessions sharing a trace fingerprint: 0
Clocks¶
The trial table gives each stimulus row its 2p frames (start_frame_twop), and the 2p, pupil and
running series carry timestamps. The check below compares the table's frame numbers with the 2p
timestamps, and then uses a contrast: the population response at the stimulus onsets of the table,
against the same frames shifted by 5 to 20 s, where no onset is.
lag = []
for s in sorted(ANALYSED):
t, ts = trials[s], meta[s]["twop_timestamps"]
fr = t.start_frame_twop.values.astype(int)
d = ts[fr] - t.start_time.values # 2p timestamp of each row's start frame minus the row's start time
lag.append({"session": s, "median_s": np.median(d), "min_s": d.min(), "max_s": d.max()})
lag = pd.DataFrame(lag)
print("2p timestamp of each row's start frame - row start time, s: medians", lag.median_s.min().round(4), "to", lag.median_s.max().round(4),
"| extremes", lag.min_s.min().round(4), "to", lag.max_s.max().round(4))
print("running timestamps span the stimuli in every session:",
all((meta[s]["run_first"] <= trials[s].start_time.min() + 1) and (meta[s]["run_last"] >= trials[s].stop_time.max() - 1) for s in ANALYSED))
print("pupil timestamps equal the 2p timestamps in every analysed session:",
all(meta[s]["pupil_n"] == meta[s]["twop_n"] and meta[s]["pupil_first"] == meta[s]["twop_first"] for s in ANALYSED))
2p timestamp of each row's start frame - row start time, s: medians -0.0174 to 0.0147 | extremes -0.0434 to 0.0363 running timestamps span the stimuli in every session: True pupil timestamps equal the 2p timestamps in every analysed session: True
# Contrast: per session, the mean over ROIs of (mean dF/F 0-2 s after - 2 s before) at the table's
# onset frames, and at the same frames shifted by -20, -10, -5, +5, +10 and +20 s, in units of its SEM.
z = pd.DataFrame({s: {sh: v["onset_shift"][sh][0] / v["onset_shift"][sh][1] for sh in v["onset_shift"]} for s, v in roi.items()}).T
z.columns = [f"{c / ca.FPS:+.0f} s" for c in z.columns]
print(z.describe().loc[["min", "50%", "max"]].round(2).to_string())
true_col = "+0 s"
best = z[true_col].abs() >= z.drop(columns=true_col).abs().max(axis=1)
print("sessions where the table's onsets give the largest |mean/SEM| of all shifts:", int(best.sum()), "of", len(z))
print("sessions where they do not:")
print(z[~best].round(2).to_string())
-20 s -10 s -5 s +0 s +5 s +10 s +20 s
min -5.15 -5.27 -5.74 1.63 -8.78 -8.33 -8.96
50% -0.74 -0.71 -0.08 9.28 -0.70 0.33 -0.11
max 4.82 6.62 2.91 31.28 5.31 4.93 4.77
sessions where the table's onsets give the largest |mean/SEM| of all shifts: 32 of 33
sessions where they do not:
-20 s -10 s -5 s +0 s +5 s +10 s +20 s
758519303 -2.36 -0.31 2.15 1.63 2.52 1.2 -0.42
The trial table's frame numbers agree with the 2p timestamps to within about one frame (printed above), the pupil series shares the 2p timestamps, and the running series spans the stimuli. In 32 of 33 sessions the population response at the table's stimulus onsets is larger, in units of its SEM, than at any shifted time; in the other, 758519303 (408021, session 1), the responses are weak at every shift (1.6 SEM at the onsets, 2.5 at +5 s), so the check is not informative there. The running series could not be checked by a contrast, as the claims use no running response.
dataset-holds-paper-sessions¶
The paper's data are the 50 sessions of Table 2, deposited on DANDI as dandiset 000037, with files named by subject and session ID. This claim checks whether dandiset 000336 holds any of those sessions.
Sessions are matched by subject and session ID from the file names of 000336 and the subject metadata of its files, against the IDs of Table 2.
paper_subjects, paper_sessions = set(table2.subject_id), set(table2.session_id)
print("Table 2:", len(paper_subjects), "subjects,", len(paper_sessions), "sessions")
print("000336 subjects in Table 2:", sorted(set(a336.subject) & paper_subjects))
print("000336 session IDs in Table 2:", sorted(set(a336.session.dropna()) & paper_sessions))
# subject metadata of one file per 000336 subject: the smallest processed (ophys) file, or for the
# subjects without one, the smallest raw (image+ophys) file
subj = []
for s, g in a336.groupby("subject"):
kind = "ophys" if (g.kind == "ophys").any() else "image+ophys"
r = g[g.kind == kind].sort_values("size").iloc[0]
with h5py.File(remfile.File(ca.s3_url(r.asset_id)), "r") as f:
subj.append({"subject": s, "file_kind": kind, "subject_id": f["general/subject/subject_id"][()].decode(),
"genotype": f["general/subject/genotype"][()].decode(),
"start": f["session_start_time"][()].decode()[:10]})
subj = pd.DataFrame(subj)
print(subj.to_string(index=False))
print("subjects opened:", len(subj), "of", a336.subject.nunique())
print("subject_id metadata in Table 2:", sorted(set(subj.subject_id) & paper_subjects))
print("000336 session start dates:", subj.start.min(), "to", subj.start.max(), "| Table 2 dates:", table2.date.min(), "to", table2.date.max())
print("000037 holds the 50 sessions of Table 2:", set(basic.session) == paper_sessions)
Table 2: 13 subjects, 50 sessions 000336 subjects in Table 2: [] 000336 session IDs in Table 2: []
subject file_kind subject_id genotype start 621602 ophys 621602 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2022-07-25 621603 ophys 621603 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2022-07-27 631188 ophys 631188 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2022-09-15 634402 ophys 634402 Cux2-CreERT2/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2022-10-04 635489 ophys 635489 Cux2-CreERT2/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2022-10-18 641331 ophys 641331 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2022-12-13 644972 ophys 644972 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-01-04 647715 ophys 647715 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-01-24 655042 ophys 655042 Cux2-CreERT2/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-01-26 655580 ophys 655580 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-02-08 656228 ophys 656228 Cux2-CreERT2/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-02-10 660986 ophys 660986 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-02-23 661749 ophys 661749 Cux2-CreERT2/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-03-07 662470 ophys 662470 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-03-13 690226 image+ophys 690226 Cux2-CreERT2/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-11-10 698808 image+ophys 698808 Rbp4-Cre_KL100/wt;Camk2a-tTA/wt;Ai93(TITL-GCaMP6f)/wt 2023-11-15 subjects opened: 16 of 16 subject_id metadata in Table 2: [] 000336 session start dates: 2022-07-25 to 2023-11-15 | Table 2 dates: 20180926 to 20190318 000037 holds the 50 sessions of Table 2: True
Result: not reproduced. None of the 16 subjects or 104 session IDs in the file names of DANDI:000336 is among the 13 subjects and 50 sessions of Table 2, nor is any subject ID in the metadata of the 16 subjects (one processed file each for 14, one raw file each for the 2 without processed files), and their sessions start between 2022-07-25 and 2023-11-15, against 2018 and 2019 for the paper's. 000336 holds a different project. All 50 sessions of Table 2 are in DANDI:000037, where the other claims are tested.
table1-subjects¶
13 mice were imaged, 3 in L2/3-D, 3 in L2/3-S, 4 in L5-D and 3 in L5-S, 7 male and 6 female, each with at least three imaging sessions.
Line from the genotype (Cux2 is L2/3, Rbp4 is L5), plane from the imaging plane depth, sex from the subject metadata, per subject, checked against Table 1. The date of birth is checked as the session date minus the stated age.
subj37 = md_.groupby("subject_id").agg(sex=("sex", "unique"), line=("line", "unique"), plane=("plane", "unique"),
sessions=("session", "size"), genotype=("genotype", "first"))
for c in ["sex", "line", "plane"]:
assert subj37[c].map(len).eq(1).all(), c
subj37[c] = subj37[c].str[0]
# date of birth as start date (local, see session-schedule) minus age in days
md_["start_utc"] = pd.to_datetime(md_.session_start_time.str[:19])
md_["local_date"] = md_.start_utc.dt.tz_localize("UTC").dt.tz_convert("America/Los_Angeles").dt.strftime("%Y%m%d")
md_["age_days"] = md_.age.str.extract(r"P(\d+)D")[0].astype(int)
dob = (pd.to_datetime(md_.local_date) - pd.to_timedelta(md_.age_days, unit="D")).dt.strftime("%Y%m%d")
subj37["dob_from_age"] = dob.groupby(md_.subject_id).agg(lambda x: sorted(set(x)))
t1 = table1.set_index("subject_id")
t1["plane"] = t1.plane.map({"somata": "soma", "dendrites": "dend"})
cmp1 = subj37.merge(t1, left_index=True, right_index=True, suffixes=("", "_table1"))
print(cmp1[["genotype", "sex", "sex_table1", "line", "line_table1", "plane", "plane_table1", "sessions", "dob_from_age", "dob"]].to_string())
print("sex, line and plane agree with Table 1 for every subject:",
all((cmp1[c] == cmp1[c + "_table1"]).all() for c in ["sex", "line", "plane"]))
grp = subj37.groupby(["line", "plane"]).size()
print(grp.to_string())
print("mice:", len(subj37), "| male:", (subj37.sex == "M").sum(), "| female:", (subj37.sex == "F").sum(),
"| fewest sessions per mouse:", subj37.sessions.min())
genotype sex sex_table1 line line_table1 plane plane_table1 sessions dob_from_age dob
subject_id
408021 Cux2-CreERT2;Camk2a-tTA;Ai93 M M L2/3 L2/3 soma soma 3 [20180623] 20180623
411400 Rbp4-Cre_KL100;Camk2a-tTA;Ai93 F F L5 L5 dend dend 6 [20180711] 20180711
411424 Cux2-CreERT2;Camk2a-tTA;Ai93 F F L2/3 L2/3 soma soma 3 [20180711] 20180711
411771 Rbp4-Cre_KL100;Camk2a-tTA;Ai93 M M L5 L5 soma soma 3 [20180713] 20180713
412933 Cux2-CreERT2;Camk2a-tTA;Ai93 M M L2/3 L2/3 dend dend 6 [20180718] 20180718
413663 Cux2-CreERT2;Camk2a-tTA;Ai93 M M L2/3 L2/3 dend dend 3 [20180721] 20180721
418779 Rbp4-Cre_KL100;Camk2a-tTA;Ai93 F F L5 L5 soma soma 3 [20180820] 20180820
420011 Rbp4-Cre_KL100;Camk2a-tTA;Ai93 F F L5 L5 dend dend 3 [20180826] 20180826
433414 Cux2-CreERT2;Camk2a-tTA;Ai93 F F L2/3 L2/3 dend dend 3 [20181106] 20181106
433448 Rbp4-Cre_KL100;Camk2a-tTA;Ai93 M M L5 L5 dend dend 4 [20181106] 20181106
433451 Rbp4-Cre_KL100;Camk2a-tTA;Ai93 M M L5 L5 dend dend 3 [20181106] 20181106
433458 Rbp4-Cre_KL100;Camk2a-tTA;Ai93 M M L5 L5 soma soma 4 [20181106] 20181106
440889 Cux2-CreERT2;Camk2a-tTA;Ai93 F F L2/3 L2/3 soma soma 6 [20181212] 20181212
sex, line and plane agree with Table 1 for every subject: True
line plane
L2/3 dend 3
soma 3
L5 dend 4
soma 3
mice: 13 | male: 7 | female: 6 | fewest sessions per mouse: 3
Result: reproduced. The files hold 13 mice: 3 L2/3-D, 3 L2/3-S, 4 L5-D and 3 L5-S, 7 male and 6 female, each with at least 3 sessions. Sex, line and plane agree with Table 1 for every subject, and the date of birth implied by each file's age agrees with Table 1.
table2-sessions¶
50 imaging sessions were recorded from the 13 mice. Two mice have sessions that failed QC; the sessions 1 to 3 that passed QC are 33 files.
The files carry no QC status; it comes from the paper's code table (mouse_df.csv), checked here
against Table 2. Readings for the mice with failed sessions: (a) mice whose sessions 1 to 3 include a
failed session, (b) mice with any failed session.
t2 = table2.set_index("session_id")
qc = mouse_df.set_index("sessid")
print("sessions in the files:", md_.session.nunique(), "| in Table 2:", len(t2), "| same set:", set(md_.session) == set(t2.index))
print("code table QC agrees with Table 2:", ((qc.loc[t2.index, "pass_fail"] == "P") == (t2.qc == "passed")).all())
failed = t2[t2.qc == "failed"]
print("failed sessions:", failed.groupby("subject_id").size().to_dict())
s123 = mouse_df[mouse_df.sess_n.between(1, 3)]
read_a = sorted(s123.loc[s123.pass_fail == "F", "mouseid"].unique())
read_b = sorted(failed.subject_id.unique())
print("(a) mice with a failed session among sessions 1 to 3:", len(read_a), read_a)
print("(b) mice with any failed session:", len(read_b), read_b)
print("sessions 1 to 3 that passed QC:", len(ANALYSED), "files, from", analysed.mouseid.nunique(), "mice")
print("sessions per mouse in the files:", md_.groupby("subject_id").size().to_dict())
sessions in the files: 50 | in Table 2: 50 | same set: True
code table QC agrees with Table 2: True
failed sessions: {'411400': 3, '412933': 3, '440889': 2}
(a) mice with a failed session among sessions 1 to 3: 2 ['411400', '412933']
(b) mice with any failed session: 3 ['411400', '412933', '440889']
sessions 1 to 3 that passed QC: 33 files, from 11 mice
sessions per mouse in the files: {'408021': 3, '411400': 6, '411424': 3, '411771': 3, '412933': 6, '413663': 3, '418779': 3, '420011': 3, '433414': 3, '433448': 4, '433451': 3, '433458': 4, '440889': 6}
Result: reproduced. The files hold 50 sessions, the set of Table 2. Under reading (a), 2 mice (411400 and 412933) have a failed session among sessions 1 to 3; under reading (b) the count is 3, with 440889, whose sessions 5 and 6 failed. 33 sessions 1 to 3 passed QC. The QC status is not in the files; it comes from the paper's code table, which agrees with Table 2.
session-schedule¶
After habituation each mouse had three 70-minute imaging sessions within three to six days, at most one per day.
The files' session_start_time is stored with a -08:00 offset, but its clock time equals the UTC start
time of the code table (full_timestamp), so the stored offset is wrong: the start times are UTC. Local
dates are taken by converting them from UTC to Pacific time, and compared with Table 2's dates. Readings:
which sessions, (a) sessions 1 to 3 as the code table numbers them, (b) the three tracked sessions (a
nonzero tracked count in the files); span, (i) days from first to third, (ii) calendar days inclusive;
duration, (x) first to last 2p frame, (y) first stimulus row start to last stop, grey screen included.
md_["start_clock"] = md_.session_start_time.str[:19].str.replace("-", "").str.replace(":", "")
print("stored offsets:", md_.session_start_time.str[-6:].unique())
print("start clock time equals the code table's UTC timestamp in every file:", (md_.start_clock == md_.full_timestamp).all())
md_["utc_date"] = md_.start_utc.dt.strftime("%Y%m%d")
print("local dates equal Table 2's dates:", (md_.set_index("session").local_date == t2.date.reindex(md_.session).values).all(),
"| UTC dates equal them:", (md_.set_index("session").utc_date == t2.date.reindex(md_.session).values).sum(), "of", len(md_))
md_["dur_x"] = (md_.twop_last - md_.twop_first) / 60
md_["dur_y"] = [(trials[s].stop_time.max() - trials[s].start_time.min()) / 60 for s in md_.session]
print("duration (x), min:", md_.dur_x.min().round(2), "to", md_.dur_x.max().round(2),
"| (y):", md_.dur_y.min().round(2), "to", md_.dur_y.max().round(2))
def spans(sel, datecol):
out = {}
for mouse, g in sel.groupby("subject_id"):
d = pd.to_datetime(g[datecol]).sort_values()
out[mouse] = {"n": len(d), "distinct": d.nunique() == len(d), "days_i": (d.iloc[-1] - d.iloc[0]).days,
"days_ii": (d.iloc[-1] - d.iloc[0]).days + 1, "dates": " ".join(d.dt.strftime("%m%d"))}
return pd.DataFrame(out).T
sel_a = md_[md_.sess_n.between(1, 3)]
sel_b = md_[md_.n_tracked > 0]
for name, sel in (("a", sel_a), ("b", sel_b)):
for datecol in ("local_date", "utc_date"):
sp = spans(sel, datecol)
ok_i = sp.distinct & sp.days_i.between(3, 6) & (sp.n == 3)
ok_ii = sp.distinct & sp.days_ii.between(3, 6) & (sp.n == 3)
print(f"\nreading ({name}), {datecol}: mice {len(sp)}, three sessions each: {(sp.n == 3).all()}; "
f"span (i) in 3-6 for {ok_i.sum()}, span (ii) in 3-6 for {ok_ii.sum()}")
if datecol == "local_date":
print(sp.to_string())
for name, sel in (("a", sel_a), ("b", sel_b)):
print(f"durations under ({name}): (x) {sel.dur_x.min():.2f} to {sel.dur_x.max():.2f} min (median {sel.dur_x.median():.2f}), "
f"(y) {sel.dur_y.min():.2f} to {sel.dur_y.max():.2f} min (median {sel.dur_y.median():.2f})")
stored offsets: <StringArray>
['-07:00', '-08:00']
Length: 2, dtype: str
start clock time equals the code table's UTC timestamp in every file: True
local dates equal Table 2's dates: True | UTC dates equal them: 46 of 50
duration (x), min: 70.23 to 70.53 | (y): 70.06 to 70.37
reading (a), local_date: mice 13, three sessions each: True; span (i) in 3-6 for 6, span (ii) in 3-6 for 13
n distinct days_i days_ii dates
408021 3 True 5 6 0926 0927 1001
411400 3 True 2 3 1001 1002 1003
411424 3 True 2 3 1009 1010 1011
411771 3 True 5 6 1003 1004 1008
412933 3 True 2 3 1016 1017 1018
413663 3 True 5 6 1017 1018 1022
418779 3 True 2 3 1112 1113 1114
420011 3 True 2 3 1113 1114 1115
433414 3 True 2 3 0220 0221 0222
433448 3 True 5 6 0220 0221 0225
433451 3 True 4 5 0215 0218 0219
433458 3 True 4 5 0221 0222 0225
440889 3 True 2 3 0306 0307 0308
reading (a), utc_date: mice 13, three sessions each: True; span (i) in 3-6 for 7, span (ii) in 3-6 for 13
reading (b), local_date: mice 13, three sessions each: True; span (i) in 3-6 for 8, span (ii) in 3-6 for 12
n distinct days_i days_ii dates
408021 3 True 5 6 0926 0927 1001
411400 3 True 6 7 1009 1011 1015
411424 3 True 2 3 1009 1010 1011
411771 3 True 5 6 1003 1004 1008
412933 3 True 3 4 1022 1023 1025
413663 3 True 5 6 1017 1018 1022
418779 3 True 2 3 1112 1113 1114
420011 3 True 2 3 1113 1114 1115
433414 3 True 2 3 0220 0221 0222
433448 3 True 5 6 0220 0221 0225
433451 3 True 4 5 0215 0218 0219
433458 3 True 4 5 0221 0222 0225
440889 3 True 2 3 0306 0307 0308
reading (b), utc_date: mice 13, three sessions each: True; span (i) in 3-6 for 8, span (ii) in 3-6 for 12
durations under (a): (x) 70.23 to 70.53 min (median 70.23), (y) 70.06 to 70.37 min (median 70.06)
durations under (b): (x) 70.23 to 70.32 min (median 70.23), (y) 70.06 to 70.16 min (median 70.06)
Result: reproduced. Under readings (a) and (ii), sessions 1 to 3 of each of the 13 mice fall on three distinct local dates within 3 to 6 calendar days, and every session lasts 70.2 to 70.5 minutes under duration reading (x) and 70.1 to 70.4 under (y). Under span (i) (days from the first session to the third) only 6 of 13 mice are within 3 to 6 days, as 7 had their sessions on three consecutive days (2 days). With the tracked sessions (b), 411400 spans 7 calendar days. The local dates match Table 2 for all 50 sessions once the stored start times are read as UTC; the UTC dates match for 46.
imaging-planes¶
Somata were imaged at 175 um (L2/3) and 375 um (L5) and distal apical dendrites at 50 to 75 um (L2/3) and 20 um (L5), at 30 Hz with 512 x 512 pixels over a 400 um field of view.
d = md_.set_index("session")
print("depth equals Table 2 in", (d.depth == t2.depth.reindex(d.index)).sum(), "of", len(d), "sessions")
mis = d[d.depth != t2.depth.reindex(d.index)]
print("sessions whose depth differs from Table 2:")
print(pd.DataFrame({"subject": mis.subject_id, "sess_n": mis.sess_n, "file_depth": mis.depth, "table2_depth": t2.depth.reindex(mis.index),
"code_table_depth": mouse_df.set_index("sessid").depth.reindex(mis.index)}).to_string())
print("raw imaging plane location strings of 433414:")
for s, loc in md_.loc[md_.subject_id == "433414", ["session", "location"]].values:
print(" ", s, loc)
code_depth = mouse_df.set_index("sessid").depth.reindex(d.index)
print("code table depth equals the file depth in", (code_depth == d.depth).sum(), "of", len(d),
"sessions, and equals Table 2 in", (code_depth == t2.depth.reindex(d.index)).sum())
print(md_.groupby(["line", "plane"]).depth.unique().to_string())
print("imaging rate metadata:", md_.imaging_rate.unique(), "Hz | from timestamps:",
(1 / md_.twop_median_dt).min().round(3), "to", (1 / md_.twop_median_dt).max().round(3), "Hz")
print("max projection image sizes:", md_.max_projection_shape.astype(str).value_counts().to_dict())
print("field of view in um: not stated in the files (imaging plane grid spacing not set)")
depth equals Table 2 in 47 of 50 sessions
sessions whose depth differs from Table 2:
subject sess_n file_depth table2_depth code_table_depth
session
826187862 433414 1 60 75 75
826773996 433414 2 60 75 75
827833392 433414 3 60 75 75
raw imaging plane location strings of 433414:
826187862 {'area': 'VISp', 'depth': '60'}
826773996 {'area': 'VISp', 'depth': '60'}
827833392 {'area': 'VISp', 'depth': '60'}
code table depth equals the file depth in 47 of 50 sessions, and equals Table 2 in 50
line plane
L2/3 dend [75, 50, 60]
soma [175]
L5 dend [20]
soma [375]
imaging rate metadata: [30.] Hz | from timestamps: 30.075 to 30.148 Hz
max projection image sizes: {'(512, 512)': 50}
field of view in um: not stated in the files (imaging plane grid spacing not set)
Result: partially reproduced. The imaging rate is 30 Hz in the metadata and 30.08 to 30.15 Hz from
the timestamps, and every plane image is 512 x 512 pixels. The depth equals Table 2 in 47 of 50
sessions: the three sessions of 433414 (L2/3-D) are at 60 um in the files and at 75 um in Table 2 and
in the paper's code table. The 400 um field of view is not in the files. The raw location string of each of the three 433414 files reads
'depth': '60', so 60 um is the deposited value, not a parsing artefact; the code table gives 75 um
there and equals Table 2 in all 50 sessions. The code table (printed in the data check) also agrees with
Table 2 on QC, date, ROI count and tracked ROI count in all 50 sessions, and its session numbers run 1,
2, ... in date order within each mouse.
roi-counts-per-session¶
15 to 224 ROIs per session were retained in the somatic planes and 159 to 1636 in the dendritic planes.
ROI count per session from the files, against Table 2. Readings: (a) the 33 analysed sessions, (b) every session that passed QC, (c) all 50 sessions. Variant: ROIs left after removing those with NaN or Inf values (traces read for the 33 analysed sessions only).
d = md_.set_index("session")
print("ROI count equals Table 2 in", (d.n_rois == t2.n_rois.reindex(d.index)).sum(), "of", len(d), "sessions")
passed = set(t2.index[t2.qc == "passed"])
for name, sel in (("a", d[d.index.isin(ANALYSED)]), ("b", d[d.index.isin(passed)]), ("c", d)):
g = sel.groupby("plane").n_rois.agg(["min", "max", "size"])
print(f"({name})", g.to_dict("index"))
clean = pd.Series({s: v["n_rois"] - v["n_bad"] for s, v in roi.items()})
print("variant, after NaN/Inf removal (a):", clean.groupby(d.loc[clean.index, "plane"]).agg(["min", "max"]).to_dict("index"),
"| ROIs removed:", int(sum(v["n_bad"] for v in roi.values())))
ROI count equals Table 2 in 50 of 50 sessions
(a) {'dend': {'min': 159, 'max': 1636, 'size': 15}, 'soma': {'min': 15, 'max': 224, 'size': 18}}
(b) {'dend': {'min': 159, 'max': 1636, 'size': 22}, 'soma': {'min': 15, 'max': 224, 'size': 20}}
(c) {'dend': {'min': 159, 'max': 1636, 'size': 28}, 'soma': {'min': 15, 'max': 227, 'size': 22}}
variant, after NaN/Inf removal (a): {'dend': {'min': 159, 'max': 1636}, 'soma': {'min': 15, 'max': 224}} | ROIs removed: 0
Result: reproduced. Every session's ROI count equals Table 2. Under readings (a) and (b) the somatic counts run from 15 to 224 and the dendritic from 159 to 1636, the paper's ranges exactly; under reading (c) the largest somatic count is 227, inside the widened range. No ROI of the 33 analysed sessions has NaN or Inf values, so the variant gives the same counts.
tracked-rois-per-plane¶
ROI matching across the three sessions gave 254 tracked ROIs in L2/3-D, 261 in L2/3-S, 516 in L5-D and 129 in L5-S, about 32 to 75% of the theoretical maximum.
Tracked ROIs are those with a tracking index (tracking_id not NaN). Per mouse, the tracked count must
be the same in its three tracked sessions, and the indices must run over the same set. Theoretical
maximum readings: (a) per plane, the sum over mice of the smallest ROI count among the three sessions;
(b) per plane, the sum of the mean count; (c) per mouse, as (a).
trk = []
for s, m in meta.items():
ids = m["tracking_ids"]
trk.append({"session": s, "subject_id": m["subject_id"], "n_tracked": m["n_tracked"], "n_rois": m["n_rois"],
"ids": tuple(sorted(ids[~np.isnan(ids)].astype(int)))})
trk = pd.DataFrame(trk).merge(md_[["session", "line", "plane"]], on="session")
trk["group"] = trk.line + "-" + trk.plane.map({"soma": "S", "dend": "D"})
print("tracked count equals Table 2 in", (trk.set_index("session").n_tracked == t2.n_tracked.reindex(trk.session).values).sum(), "of", len(trk))
tracked_sess = trk[trk.n_tracked > 0]
per_mouse = tracked_sess.groupby("subject_id").agg(group=("group", "first"), sessions=("session", "size"),
counts=("n_tracked", lambda x: sorted(set(x))),
same_ids=("ids", lambda x: len(set(x)) == 1),
min_rois=("n_rois", "min"), mean_rois=("n_rois", "mean"))
per_mouse["n_tracked"] = per_mouse.counts.str[0]
per_mouse["analysed"] = per_mouse.index.isin(analysed.mouseid)
per_mouse["tracked_in_sessions_1_3"] = [set(tracked_sess.loc[tracked_sess.subject_id == m, "session"]) <= set(analysed.sessid)
for m in per_mouse.index]
print(per_mouse.to_string())
pm = per_mouse[per_mouse.analysed]
by_plane = pm.groupby("group").agg(tracked=("n_tracked", "sum"), max_a=("min_rois", "sum"), max_b=("mean_rois", "sum"))
by_plane["frac_a_%"] = 100 * by_plane.tracked / by_plane.max_a
by_plane["frac_b_%"] = 100 * by_plane.tracked / by_plane.max_b
print(by_plane.round(1).to_string())
frac_c = 100 * pm.n_tracked / pm.min_rois
print("(c) per mouse:", frac_c.round(1).to_dict())
for name, x in (("a", by_plane["frac_a_%"]), ("b", by_plane["frac_b_%"]), ("c", frac_c)):
print(f"reading ({name}): {x.min():.1f} to {x.max():.1f} %, inside 28.8 to 82.5: {x.min() >= 28.8 and x.max() <= 82.5}")
tracked count equals Table 2 in 50 of 50
group sessions counts same_ids min_rois mean_rois n_tracked analysed tracked_in_sessions_1_3
subject_id
408021 L2/3-S 3 [59] True 74 92.333333 59 True True
411400 L5-D 3 [162] True 597 621.333333 162 False False
411424 L2/3-S 3 [55] True 80 85.666667 55 True True
411771 L5-S 3 [47] True 70 79.666667 47 True True
412933 L2/3-D 3 [98] True 168 254.000000 98 False False
413663 L2/3-D 3 [136] True 348 496.000000 136 True True
418779 L5-S 3 [12] True 15 23.333333 12 True True
420011 L5-D 3 [51] True 159 182.000000 51 True True
433414 L2/3-D 3 [118] True 300 453.333333 118 True True
433448 L5-D 3 [112] True 445 859.000000 112 True True
433451 L5-D 3 [353] True 966 993.666667 353 True True
433458 L5-S 3 [70] True 87 94.333333 70 True True
440889 L2/3-S 3 [147] True 210 219.333333 147 True True
tracked max_a max_b frac_a_% frac_b_%
group
L2/3-D 254 648 949.3 39.2 26.8
L2/3-S 261 364 397.3 71.7 65.7
L5-D 516 1570 2034.7 32.9 25.4
L5-S 129 172 197.3 75.0 65.4
(c) per mouse: {'408021': 79.7, '411424': 68.8, '411771': 67.1, '413663': 39.1, '418779': 80.0, '420011': 32.1, '433414': 39.3, '433448': 25.2, '433451': 36.5, '433458': 80.5, '440889': 70.0}
reading (a): 32.9 to 75.0 %, inside 28.8 to 82.5: True
reading (b): 25.4 to 65.7 %, inside 28.8 to 82.5: False
reading (c): 25.2 to 80.5 %, inside 28.8 to 82.5: False
Result: reproduced. Summed over the 11 analysed mice, the files hold 254 (L2/3-D), 261 (L2/3-S), 516 (L5-D) and 129 (L5-S) tracked ROIs, the paper's counts, and each mouse has the same tracking indices in its three tracked sessions. Under reading (a) of the theoretical maximum the fractions run from 32.9 to 75.0%, inside the widened range; under (b) they run from 25.4 to 65.7% and under (c) from 25.2 to 80.5%, both outside it.
fig8-example-matches¶
In the example L2/3-D mouse, taking the union of matches over all session orderings and removing four conflicting matches gave 136 final matches; in the example L5-S mouse, removing two conflicts gave 47.
The examples are mice 413663 (L2/3-D) and 411771 (L5-S), as the claims file gives them from the paper's code. The conflict counts cannot be tested from the files.
for mouse in ("413663", "411771"):
r = per_mouse.loc[mouse]
print(mouse, r.group, "tracked ROIs:", r.n_tracked, "| sessions:", r.sessions, "| same indices:", r.same_ids)
413663 L2/3-D tracked ROIs: 136 | sessions: 3 | same indices: True 411771 L5-S tracked ROIs: 47 | sessions: 3 | same indices: True
Result: reproduced. The files give 136 tracked ROIs for 413663 and 47 for 411771, the paper's final match counts. The conflicts removed (4 and 2) are not in the files and are not tested.
fig8-ordering-variation¶
The number of matched ROIs depends on the order in which sessions are aligned, and this variation is far smaller for somata than for dendrites.
Tested in the second notebook, roi_matching_orderings.ipynb, which reruns the matching with the public
n-way matching package over the six session orderings of each tracked mouse.
stimulus-durations¶
Each imaging session lasts 70 minutes, with 34 minutes of Gabor sequences and 17 minutes of visual flow in each direction, each stimulus period flanked by 30 s of grey screen.
From the trial table of each analysed session: each stimulus period is its first row start to its last row stop, per Gabor and per flow direction; grey periods are the grey-screen rows.
dur = []
for s in sorted(ANALYSED):
t = trials[s]
row = {"session": s}
for name, sel in (("gabors", t.stimulus_type == "gabors"),
("flow_left", t.stimulus_template_name == "visflow_left"),
("flow_right", t.stimulus_template_name == "visflow_right")):
row[name + "_min"] = (t.stop_time[sel].max() - t.start_time[sel].min()) / 60
g = t[t.stimulus_type == "grayscreen"]
row["grey_n"] = len(g)
row["grey_min_s"], row["grey_max_s"] = (g.stop_time - g.start_time).min(), (g.stop_time - g.start_time).max()
order = t.stimulus_template_name.values
row["order"] = " ".join(pd.Series(order)[pd.Series(order) != pd.Series(order).shift()].str.replace("visflow_", "").str.replace("grayscreen", "grey"))
dur.append(row)
dur = pd.DataFrame(dur).set_index("session")
print(dur.drop(columns="order").describe().loc[["min", "50%", "max"]].round(3))
print(dur.order.value_counts().to_string())
ok = (dur.gabors_min.sub(34).abs() <= 3.4) & dur.flow_left_min.sub(17).abs().le(1.7) & dur.flow_right_min.sub(17).abs().le(1.7) \
& dur.grey_min_s.sub(30).abs().le(3) & dur.grey_max_s.sub(30).abs().le(3)
print("sessions within tolerance:", ok.sum(), "of", len(ok))
gabors_min flow_left_min flow_right_min grey_n grey_min_s grey_max_s min 34.027 17.013 17.013 4.0 29.983 30.029 50% 34.028 17.014 17.014 4.0 29.983 30.031 max 34.127 17.043 17.016 4.0 29.983 30.039 order grey gabors grey right grey left grey 11 grey gabors grey left grey right grey 9 grey left grey right grey gabors grey 7 grey right grey left grey gabors grey 6 sessions within tolerance: 33 of 33
Result: reproduced. In all 33 sessions the Gabor period lasts 34.03 to 34.13 minutes, each flow direction 17.01 to 17.04 minutes, and each of the four grey periods 29.98 to 30.04 s, one before and after each stimulus period.
inconsistent-event-fraction¶
Inconsistent sequences are about 7% of Gabor sequences and 5% of visual flow time, in about 30 blocks per session, each 30 to 90 s of consistent stimulus followed by 3 to 6 s (Gabor) or 2 to 4 s (visual flow) of inconsistent stimulus.
Gabor fraction: sequences with a U image over all sequences (D or U). Flow fraction: time of rows with reversed squares over all flow time. A block is a run of consistent rows followed by a run of inconsistent rows, within the Gabor period and within each flow direction (the stimulus code samples them so). Durations are from row start and stop times. Readings: (a) mean of per-session fractions, (b) pooled; blocks (i) per stimulus, (ii) per session.
def runs(t):
# consistent and inconsistent run durations, in order, for one stimulus period (rows in time order)
u = t.unexpected.values == 1
edges = np.flatnonzero(np.diff(u.astype(int))) + 1
bounds = np.r_[0, edges, len(u)]
out = []
for a, b in zip(bounds[:-1], bounds[1:]):
out.append((bool(u[a]), t.stop_time.values[b - 1] - t.start_time.values[a]))
return out
inc = []
cons_d, inc_g, inc_f, block_runs = [], [], [], []
for s in sorted(ANALYSED):
t = trials[s]
gab = t[t.stimulus_type == "gabors"]
n_d, n_u = (gab.gabor_frame == "D").sum(), (gab.gabor_frame == "U").sum()
vf = t[t.stimulus_type == "visflow"]
vf_dur = vf.stop_time - vf.start_time
blocks = {}
for name, sel in (("gabors", gab), ("visflow_left", vf[vf.stimulus_template_name == "visflow_left"]),
("visflow_right", vf[vf.stimulus_template_name == "visflow_right"])):
r = runs(sel)
# a block: a consistent run followed by an inconsistent one; a trailing consistent run is not a block
nb = 0
for (u1, d1), (u2, d2) in zip(r[:-1], r[1:]):
if not u1 and u2:
nb += 1
cons_d.append((name, d1))
(inc_g if name == "gabors" else inc_f).append(d2)
block_runs += [(s, name, "consistent", d1), (s, name, "inconsistent", d2)]
blocks[name] = nb
blocks[name + "_trailing_s"] = r[-1][1] if not r[-1][0] else 0.0
blocks[name + "_first_inconsistent"] = r[0][0]
inc.append({"session": s, "gabor_U": n_u, "gabor_seq": n_d + n_u, "gabor_frac_%": 100 * n_u / (n_d + n_u),
"flow_incons_s": vf_dur[vf.unexpected == 1].sum(), "flow_s": vf_dur.sum(),
"flow_frac_%": 100 * vf_dur[vf.unexpected == 1].sum() / vf_dur.sum(),
"blocks_gabor": blocks["gabors"], "blocks_flow": blocks["visflow_left"] + blocks["visflow_right"],
"trailing_gabor_s": blocks["gabors_trailing_s"],
"starts_inconsistent": any(blocks[k + "_first_inconsistent"] for k in ("gabors", "visflow_left", "visflow_right"))})
inc = pd.DataFrame(inc).set_index("session")
inc["blocks_session"] = inc.blocks_gabor + inc.blocks_flow
print(inc.describe().loc[["mean", "min", "max"]].round(2).to_string())
print("(a) mean of per-session fractions: Gabor %.2f%%, flow %.2f%%" % (inc["gabor_frac_%"].mean(), inc["flow_frac_%"].mean()))
print("(b) pooled: Gabor %.2f%%, flow %.2f%%" % (100 * inc.gabor_U.sum() / inc.gabor_seq.sum(), 100 * inc.flow_incons_s.sum() / inc.flow_s.sum()))
print("(i) per stimulus, mean blocks: Gabor %.1f, flow %.1f; (ii) per session %.1f" % (inc.blocks_gabor.mean(), inc.blocks_flow.mean(), inc.blocks_session.mean()))
print("blocks per session (reading i): Gabor", inc.blocks_gabor.min(), "to", inc.blocks_gabor.max(),
"| flow", inc.blocks_flow.min(), "to", inc.blocks_flow.max())
print("sessions outside 27 to 33 blocks: Gabor", int((~inc.blocks_gabor.between(27, 33)).sum()),
"| flow", int((~inc.blocks_flow.between(27, 33)).sum()), "of", len(inc))
print(inc[["blocks_gabor", "blocks_flow"]].T.to_string())
cd = pd.DataFrame(cons_d, columns=["stim", "s"])
print("consistent run before each inconsistent run, s:", cd.groupby(cd.stim.str[:7]).s.agg(["min", "max", "size"]).round(2).to_dict("index"))
print("inconsistent runs, s: Gabor %.2f to %.2f (n %d); flow %.2f to %.2f (n %d)" % (min(inc_g), max(inc_g), len(inc_g), min(inc_f), max(inc_f), len(inc_f)))
print("Gabor inconsistent run lengths (s, rounded to 0.1):", pd.Series(np.round(inc_g, 1)).value_counts().sort_index().to_dict())
print("flow inconsistent run lengths (s, rounded to 0.1):", pd.Series(np.round(inc_f, 1)).value_counts().sort_index().to_dict())
out_c = cd[(cd.s < 30) | (cd.s > 90)]
print("consistent runs outside 30-90 s:", len(out_c), "of", len(cd), "| furthest outside by",
round(max((30 - out_c.s).max(), (out_c.s - 90).max()), 3) if len(out_c) else 0, "s")
out_g = [x for x in inc_g if not 3 <= x <= 6]
out_f = [x for x in inc_f if not 2 <= x <= 4]
print("inconsistent runs outside the stated range: Gabor", len(out_g), "of", len(inc_g), "(largest", round(max(out_g, default=0), 3), "s)",
"| flow", len(out_f), "of", len(inc_f), "(range", round(min(out_f, default=0), 3), "to", round(max(out_f, default=0), 3), "s)")
# The Methods sample durations on a grid (multiples of 1.5 s for Gabor, 1 s for flow); on that grid:
grid = np.where(cd.stim == "gabors", 1.5, 1.0)
cd_grid = np.round(cd.s / grid) * grid
print("on the sampling grid: consistent runs outside 30-90 s:", int(((cd_grid < 30) | (cd_grid > 90)).sum()),
"| Gabor inconsistent outside 3-6 s:", int(sum(not 3 <= np.round(x / 1.5) * 1.5 <= 6 for x in inc_g)),
"| flow inconsistent outside 2-4 s:", int(sum(not 2 <= np.round(x) <= 4 for x in inc_f)))
far = pd.DataFrame(block_runs, columns=["session", "stim", "run", "s"])
step = np.where(far.stim == "gabors", 1.5, 1.0)
far["grid"] = np.round(far.s / step) * step
far["off_s"] = far.s - far.grid
far["off_frames_60Hz"] = far.off_s * 60
print("runs furthest from their grid value:")
print(far.reindex(far.off_s.abs().sort_values(ascending=False).index).head(5).round(3).to_string(index=False))
print("runs more than 0.25 s from their grid value:", int((far.off_s.abs() > 0.25).sum()), "of", len(far),
"| by session:", far[far.off_s.abs() > 0.25].session.value_counts().to_dict())
# Measured over grid duration, per session (consistent runs): a session whose display ran slow
# stretches every run in proportion to its length.
far["stretch_per_mille"] = 1000 * far.off_s / far.grid
st_ = far[far.run == "consistent"].groupby("session").stretch_per_mille.agg(["median", "min", "max"])
print("consistent runs, measured over grid duration minus 1, per mille: median over sessions",
round(st_["median"].median(), 2), "| sessions with the largest median:")
print(st_.sort_values("median", ascending=False).head(4).round(2).to_string())
print("largest distance of a measured run from its grid value, s:",
round(max(np.abs(cd.s - cd_grid).max(), max(abs(x - np.round(x / 1.5) * 1.5) for x in inc_g), max(abs(x - np.round(x)) for x in inc_f)), 3))
print("per session: Gabor fraction", inc["gabor_frac_%"].min().round(2), "to", inc["gabor_frac_%"].max().round(2),
"%; flow fraction", inc["flow_frac_%"].min().round(2), "to", inc["flow_frac_%"].max().round(2), "%")
gabor_U gabor_seq gabor_frac_% flow_incons_s flow_s flow_frac_% blocks_gabor blocks_flow trailing_gabor_s blocks_session
mean 94.21 1360.0 6.93 94.98 2041.71 4.65 31.48 32.18 0.0 63.67
min 82.00 1360.0 6.03 80.07 2041.61 3.92 29.00 29.00 0.0 59.00
max 108.00 1360.0 7.94 112.09 2043.38 5.49 34.00 35.00 0.0 68.00
(a) mean of per-session fractions: Gabor 6.93%, flow 4.65%
(b) pooled: Gabor 6.93%, flow 4.65%
(i) per stimulus, mean blocks: Gabor 31.5, flow 32.2; (ii) per session 63.7
blocks per session (reading i): Gabor 29 to 34 | flow 29 to 35
sessions outside 27 to 33 blocks: Gabor 2 | flow 11 of 33
session 758519303 759189643 759660390 760260459 760659782 761269197 761624763 761944562 762250376 764704289 765193831 766502238 777496949 777914830 778374308 778864809 779152062 779650018 823453391 824434038 825180479 826187862 826338612 826659257 826773996 826819032 827300090 827833392 828475005 828816509 832883243 833704570 834403597
blocks_gabor 32 29 34 31 30 30 33 33 33 31 31 32 34 30 33 30 33 32 33 31 32 32 32 30 30 33 31 32 31 30 31 30 30
blocks_flow 31 34 33 32 29 34 34 30 35 33 34 29 30 32 31 29 30 32 30 34 34 34 35 31 33 31 33 31 33 34 30 35 32
consistent run before each inconsistent run, s: {'gabors': {'min': 30.02, 'max': 90.21, 'size': 1039}, 'visflow': {'min': 30.02, 'max': 90.07, 'size': 1062}}
inconsistent runs, s: Gabor 2.99 to 6.06 (n 1039); flow 1.99 to 4.01 (n 1062)
Gabor inconsistent run lengths (s, rounded to 0.1): {3.0: 349, 4.5: 348, 4.6: 1, 6.0: 339, 6.1: 2}
flow inconsistent run lengths (s, rounded to 0.1): {2.0: 387, 3.0: 342, 4.0: 333}
consistent runs outside 30-90 s: 49 of 2101 | furthest outside by 0.215 s
inconsistent runs outside the stated range: Gabor 377 of 1039 (largest 6.057 s) | flow 347 of 1062 (range 1.994 to 4.014 s)
on the sampling grid: consistent runs outside 30-90 s: 0 | Gabor inconsistent outside 3-6 s: 0 | flow inconsistent outside 2-4 s: 0
runs furthest from their grid value:
session stim run s grid off_s off_frames_60Hz
832883243 gabors consistent 88.887 88.5 0.387 23.225
833704570 gabors consistent 73.852 73.5 0.352 21.091
833704570 gabors consistent 79.836 79.5 0.336 20.151
833704570 gabors consistent 81.331 81.0 0.331 19.878
833704570 gabors consistent 72.316 72.0 0.316 18.941
runs more than 0.25 s from their grid value: 22 of 4202 | by session: {'833704570': 18, '832883243': 3, '826187862': 1}
consistent runs, measured over grid duration minus 1, per mille: median over sessions 0.81 | sessions with the largest median:
median min max
session
826187862 1.24 0.78 3.23
832883243 1.23 0.68 5.21
833704570 0.82 0.77 5.51
761944562 0.81 -2.18 1.10
largest distance of a measured run from its grid value, s: 0.387
per session: Gabor fraction 6.03 to 7.94 %; flow fraction 3.92 to 5.49 %
Result: reproduced. Inconsistent sequences are 6.93% of Gabor sequences and 4.65% of flow time (readings a and b agree; per session 6.03 to 7.94% and 3.92 to 5.49%). There are on average 31.5 Gabor blocks and 32.2 flow blocks per session (reading i, within 30 +- 3; reading ii, 63.7 per session, is not). Measured from row times, 49 of 2101 consistent runs exceed 90 s by up to 0.22 s, and 377 of 1039 Gabor and 347 of 1062 flow inconsistent runs fall outside 3 to 6 s and 2 to 4 s by at most 0.06 s; on the sampling grid of the Methods (1.5 s and 1 s steps) every run is within the stated ranges. The block count is judged on the mean per session: per session it runs from 29 to 34 for Gabor and 29 to 35 for flow, so 2 of 33 sessions (Gabor) and 11 of 33 (flow) fall outside 27 to 33. The run furthest from its grid value, a consistent Gabor run of 88.887 s in session 832883243 (440889), lies 0.387 s from 88.5 s; consistent runs are longer than their grid values by a median of 0.81 per mille, and the 22 runs more than 0.25 s off all lie in the three sessions with the largest stretch (1.24, 1.23 and 0.82 per mille medians, up to 5.5 per mille), consistent with the display running slightly slow in those sessions, not with a different design. The outcome rests on two choices the criterion does not fix: durations are judged on the sampling grid of the Methods, and the block count on its mean over sessions. On row times, or with the block count required in every session, the claim would not hold.
fig5-stimulus-responses¶
Somatic and dendritic ROIs respond to both the Gabor and the visual flow stimuli, many with increased fluorescence at stimulus onset, and their responses differ between consistent and inconsistent stimuli.
Session 1 of the 11 analysed mice, ROIs pooled per plane. Gabor: per ROI, mean dF/F over 0 to 0.6 s after D against after U. Flow, each direction: mean over 0 to 2 s after against 2 s before inconsistent flow onset. ROI selection (a) all ROIs, (b) the Fig. 5 legend's criteria. Tests (i) two-sided Wilcoxon signed-rank across ROIs, p < 0.05, (ii) |mean difference| > 2 SEM. Unscaled dF/F as the claims file says; the code's scaled traces are shown as a variant.
def two_tests(x, y):
dlt = np.asarray(y) - np.asarray(x)
n = len(dlt)
if n < 2:
return n, np.nan, np.nan, np.nan, False, False
p = scipy.stats.wilcoxon(dlt).pvalue if np.any(dlt != 0) else 1.0
mean, sem = dlt.mean(), dlt.std(ddof=1) / np.sqrt(n)
return n, mean, sem, p, p < 0.05, abs(mean) > 2 * sem
rows5 = []
for key in ("fig5", "fig5_scaled"):
for s in sorted(SESS1):
m = meta[s]
grp = md_.set_index("session").loc[s]
for stim, v in roi[s][key].items():
for sel_name in ("a", "b", "b_u") if stim == "gabors" else ("a", "b"):
keep = {"a": np.ones(len(v["consistent"]), bool), "b": v["selected"], "b_u": v.get("selected_u")}[sel_name]
rows5.append({"traces": key, "plane": grp.line + "-" + ("S" if grp.plane == "soma" else "D"),
"mouse": m["subject_id"], "stim": stim, "selection": sel_name,
"consistent": v["consistent"][keep], "inconsistent": v["inconsistent"][keep]})
r5 = pd.DataFrame(rows5)
res5 = []
for (traces, plane, stim, sel), g in r5.groupby(["traces", "plane", "stim", "selection"]):
x, y = np.concatenate(g.consistent.values), np.concatenate(g.inconsistent.values)
n, mean, sem, p, wil, sem2 = two_tests(x, y)
res5.append({"traces": traces, "plane": plane, "stim": stim, "selection": sel, "n_rois": n,
"mean_incons_minus_cons": mean, "sem": sem, "wilcoxon_p": p, "i_wilcoxon": wil, "ii_2sem": sem2})
res5 = pd.DataFrame(res5)
with pd.option_context("display.float_format", "{:.3g}".format):
print(res5[res5.traces == "fig5"].drop(columns="traces").to_string(index=False))
summary5 = res5[res5.selection != "b_u"].groupby(["traces", "selection", "plane"]).agg(any_i=("i_wilcoxon", "any"), any_ii=("ii_2sem", "any"))
print(summary5.to_string())
print("variant b_u (Gabor selection on inconsistent sequences, Fig. 5b legend), Gabor only:")
with pd.option_context("display.float_format", "{:.3g}".format):
print(res5[res5.selection == "b_u"].drop(columns=["selection"]).to_string(index=False))
# With b_u in place of b for Gabor: does every plane still have a difference under both tests?
alt = res5[(res5.selection == "b_u") | ((res5.selection == "b") & (res5.stim != "gabors"))]
print("selection b with b_u for Gabor, every plane has a difference:",
alt.groupby(["traces", "plane"]).agg(any_i=("i_wilcoxon", "any"), any_ii=("ii_2sem", "any")).all().to_dict())
for traces in ("fig5", "fig5_scaled"):
s5 = summary5.loc[traces]
print(traces, "| every reading pair has a difference in all four planes:", bool(s5.all().all()))
plane stim selection n_rois mean_incons_minus_cons sem wilcoxon_p i_wilcoxon ii_2sem
L2/3-D gabors a 1355 -0.00245 0.000376 3.06e-14 True True
L2/3-D gabors b 54 -0.0224 0.00454 3.37e-07 True True
L2/3-D gabors b_u 35 -0.0121 0.0078 0.368 False False
L2/3-D visflow left (nasal) a 1355 0.0081 0.000449 2.9e-74 True True
L2/3-D visflow left (nasal) b 56 0.0374 0.00392 7.55e-11 True True
L2/3-D visflow right (temp) a 1355 0.00743 0.000489 1.49e-60 True True
L2/3-D visflow right (temp) b 66 0.0354 0.00268 1.64e-12 True True
L2/3-S gabors a 407 0.00118 0.00185 0.717 False False
L2/3-S gabors b 16 0.0054 0.00752 0.159 False False
L2/3-S gabors b_u 20 0.00442 0.0146 0.784 False False
L2/3-S visflow left (nasal) a 407 0.00823 0.00226 1.06e-14 True True
L2/3-S visflow left (nasal) b 23 0.0808 0.0148 2.38e-07 True True
L2/3-S visflow right (temp) a 407 0.00773 0.00187 3.31e-17 True True
L2/3-S visflow right (temp) b 23 0.0686 0.0117 2.38e-07 True True
L5-D gabors a 2807 -0.000229 0.000338 0.883 False False
L5-D gabors b 124 -0.0104 0.00379 0.0255 True True
L5-D gabors b_u 95 0.0042 0.00419 0.543 False False
L5-D visflow left (nasal) a 2807 0.0014 0.000395 2.71e-08 True True
L5-D visflow left (nasal) b 110 0.0371 0.00314 8.75e-20 True True
L5-D visflow right (temp) a 2807 -7.22e-05 0.00039 0.464 False False
L5-D visflow right (temp) b 109 0.0273 0.00181 1.28e-19 True True
L5-S gabors a 204 0.0173 0.00589 0.000394 True True
L5-S gabors b 8 0.0691 0.0518 0.547 False False
L5-S gabors b_u 13 0.141 0.0749 0.0105 True False
L5-S visflow left (nasal) a 204 0.00696 0.00281 4.12e-08 True True
L5-S visflow left (nasal) b 13 0.0786 0.0146 0.000244 True True
L5-S visflow right (temp) a 204 -0.00173 0.00168 0.751 False False
L5-S visflow right (temp) b 10 0.0155 0.0019 0.00195 True True
any_i any_ii
traces selection plane
fig5 a L2/3-D True True
L2/3-S True True
L5-D True True
L5-S True True
b L2/3-D True True
L2/3-S True True
L5-D True True
L5-S True True
fig5_scaled a L2/3-D True True
L2/3-S True True
L5-D True True
L5-S True True
b L2/3-D True True
L2/3-S True True
L5-D True True
L5-S True True
variant b_u (Gabor selection on inconsistent sequences, Fig. 5b legend), Gabor only:
traces plane stim n_rois mean_incons_minus_cons sem wilcoxon_p i_wilcoxon ii_2sem
fig5 L2/3-D gabors 35 -0.0121 0.0078 0.368 False False
fig5 L2/3-S gabors 20 0.00442 0.0146 0.784 False False
fig5 L5-D gabors 95 0.0042 0.00419 0.543 False False
fig5 L5-S gabors 13 0.141 0.0749 0.0105 True False
fig5_scaled L2/3-D gabors 62 -0.00975 0.00641 0.53 False False
fig5_scaled L2/3-S gabors 21 0.0496 0.0534 0.946 False False
fig5_scaled L5-D gabors 116 -0.00355 0.00486 0.572 False False
fig5_scaled L5-S gabors 13 0.428 0.261 0.00464 True False
selection b with b_u for Gabor, every plane has a difference: {'any_i': True, 'any_ii': True}
fig5 | every reading pair has a difference in all four planes: True
fig5_scaled | every reading pair has a difference in all four planes: True
# Selection (b) per session: ROIs selected out of all, and the session thresholds (75th percentiles;
# SNR median). Unscaled traces.
selrows = []
for s in sorted(SESS1):
grp = md_.set_index("session").loc[s]
for stim, v in roi[s]["fig5"].items():
for name, sel_key, thr_key in (("b", "selected", "thresholds"), ("b_u", "selected_u", "thresholds_u")):
if sel_key not in v:
continue
selrows.append({"session": s, "mouse": grp.subject_id, "plane": grp.line + "-" + ("S" if grp.plane == "soma" else "D"),
"stim": stim, "selection": name, "selected": int(v[sel_key].sum()), "rois": len(v[sel_key]),
**{f"thr_{k}": t for k, t in v[thr_key].items()}})
selrows = pd.DataFrame(selrows)
with pd.option_context("display.float_format", "{:.3g}".format):
print(selrows.to_string(index=False))
print("sessions x stimuli with no ROI selected:", selrows.groupby("selection").apply(lambda g: int((g.selected == 0).sum())).to_dict())
session mouse plane stim selection selected rois thr_snr thr_corr thr_sd thr_skew thr_diff
758519303 408021 L2/3-S gabors b 2 96 3.85 0.0567 0.0227 1 NaN
758519303 408021 L2/3-S gabors b_u 2 96 3.85 0.0941 0.0396 0.649 NaN
758519303 408021 L2/3-S visflow left (nasal) b 4 96 3.85 0.0107 NaN NaN 0.00688
758519303 408021 L2/3-S visflow right (temp) b 8 96 3.85 0.0137 NaN NaN 0.0203
760260459 411771 L5-S gabors b 4 90 3.65 0.0336 0.0165 0.731 NaN
760260459 411771 L5-S gabors b_u 5 90 3.65 0.073 0.0259 0.841 NaN
760260459 411771 L5-S visflow left (nasal) b 3 90 3.65 0.00696 NaN NaN 0.0158
760260459 411771 L5-S visflow right (temp) b 2 90 3.65 0.00804 NaN NaN 0.00557
761624763 411424 L2/3-S gabors b 3 87 3.01 0.0604 0.0239 0.824 NaN
761624763 411424 L2/3-S gabors b_u 6 87 3.01 0.0689 0.0263 0.791 NaN
761624763 411424 L2/3-S visflow left (nasal) b 8 87 3.01 0.0305 NaN NaN 0.0183
761624763 411424 L2/3-S visflow right (temp) b 5 87 3.01 0.0226 NaN NaN 0.0201
764704289 413663 L2/3-D gabors b 25 628 2.39 0.00631 0.0108 0.498 NaN
764704289 413663 L2/3-D gabors b_u 17 628 2.39 0.00541 0.0252 0.313 NaN
764704289 413663 L2/3-D visflow left (nasal) b 27 628 2.39 0.0159 NaN NaN 0.0165
764704289 413663 L2/3-D visflow right (temp) b 31 628 2.39 0.0109 NaN NaN 0.013
777496949 418779 L5-S gabors b 0 15 3.55 0.0102 0.0108 0.564 NaN
777496949 418779 L5-S gabors b_u 1 15 3.55 0.00873 0.0114 0.371 NaN
777496949 418779 L5-S visflow left (nasal) b 3 15 3.55 0.0262 NaN NaN 0.000536
777496949 418779 L5-S visflow right (temp) b 1 15 3.55 0.00646 NaN NaN 0.0041
777914830 420011 L5-D gabors b 13 205 2.54 0.0019 0.0146 0.406 NaN
777914830 420011 L5-D gabors b_u 5 205 2.54 0.00386 0.0423 0.335 NaN
777914830 420011 L5-D visflow left (nasal) b 5 205 2.54 0.0104 NaN NaN 0.00794
777914830 420011 L5-D visflow right (temp) b 7 205 2.54 0.0208 NaN NaN 0.0097
823453391 433451 L5-D gabors b 54 966 2.58 0.0125 0.018 0.696 NaN
823453391 433451 L5-D gabors b_u 49 966 2.58 0.00845 0.0293 0.408 NaN
823453391 433451 L5-D visflow left (nasal) b 33 966 2.58 0.0108 NaN NaN 0.0067
823453391 433451 L5-D visflow right (temp) b 30 966 2.58 0.0137 NaN NaN 0.0107
826187862 433414 L2/3-D gabors b 29 727 2.39 0.0127 0.0171 0.405 NaN
826187862 433414 L2/3-D gabors b_u 18 727 2.39 0.0119 0.033 0.33 NaN
826187862 433414 L2/3-D visflow left (nasal) b 29 727 2.39 0.0144 NaN NaN 0.0147
826187862 433414 L2/3-D visflow right (temp) b 35 727 2.39 0.0119 NaN NaN 0.0157
826338612 433448 L5-D gabors b 57 1636 2.54 0.00399 0.0143 0.271 NaN
826338612 433448 L5-D gabors b_u 41 1636 2.54 0.00556 0.0397 0.315 NaN
826338612 433448 L5-D visflow left (nasal) b 72 1636 2.54 0.0112 NaN NaN 0.0108
826338612 433448 L5-D visflow right (temp) b 72 1636 2.54 0.0116 NaN NaN 0.00686
826659257 433458 L5-S gabors b 4 99 3 0.0328 0.0162 0.838 NaN
826659257 433458 L5-S gabors b_u 7 99 3 0.101 0.0287 0.886 NaN
826659257 433458 L5-S visflow left (nasal) b 7 99 3 0.0396 NaN NaN 0.0223
826659257 433458 L5-S visflow right (temp) b 7 99 3 0.00621 NaN NaN 0.0107
832883243 440889 L2/3-S gabors b 11 224 2.89 0.045 0.0117 0.72 NaN
832883243 440889 L2/3-S gabors b_u 12 224 2.89 0.0454 0.0156 0.543 NaN
832883243 440889 L2/3-S visflow left (nasal) b 11 224 2.89 0.0221 NaN NaN 0.0115
832883243 440889 L2/3-S visflow right (temp) b 10 224 2.89 0.0176 NaN NaN 0.00955
sessions x stimuli with no ROI selected: {'b': 1, 'b_u': 0}
# By mouse: Gabor U minus D (all ROIs, unscaled), and the per-mouse Wilcoxon p.
bym = []
for _, g in r5[(r5.traces == "fig5") & (r5.selection == "a")].iterrows():
n, mean, sem, p, wil, sem2 = two_tests(g.consistent, g.inconsistent)
bym.append({"plane": g.plane, "mouse": g.mouse, "stim": g.stim, "n": n, "mean": mean, "sem": sem, "p": p})
with pd.option_context("display.float_format", "{:.3g}".format):
print(pd.DataFrame(bym).pivot_table(index=["plane", "mouse"], columns="stim", values=["mean", "p"]).to_string())
mean p
stim gabors visflow left (nasal) visflow right (temp) gabors visflow left (nasal) visflow right (temp)
plane mouse
L2/3-D 413663 -0.00664 0.00981 0.00686 5.53e-50 3.64e-50 3.38e-23
433414 0.00117 0.00662 0.00793 0.000455 3.04e-28 3.21e-39
L2/3-S 408021 0.00546 -0.00358 0.0117 0.00245 0.31 0.00197
411424 -0.00424 0.0173 0.0137 9.53e-07 5.06e-10 8.81e-08
440889 0.00145 0.00975 0.0037 0.967 1.17e-08 1.21e-09
L5-D 420011 -6.62e-05 0.00221 0.00231 0.524 0.0248 0.134
433448 0.00521 0.00242 -0.00178 1.01e-60 1.7e-08 5.5e-08
433451 -0.00948 -0.000488 0.00232 6.61e-78 0.287 2.73e-14
L5-S 411771 0.0221 0.00786 -0.00127 0.00204 5.51e-07 0.774
418779 -0.00249 -0.00943 -0.00814 0.151 0.0554 0.277
433458 0.016 0.00862 -0.00118 0.00841 0.000276 0.219
Result: reproduced. Under all four reading pairs (selection a or b, test i or ii), each of the four planes has at least one stimulus with a difference between consistent and inconsistent responses, and the same holds with the code's scaled traces. With all ROIs (a), the Gabor difference is significant in L2/3-D and L5-S but not in L2/3-S or L5-D, where only the flow differs. Under selection (b) the flow selection is made on the after-minus-before difference itself, so its test is not independent of the selection; that selection is the claims file's own. Selecting Gabor ROIs on their inconsistent (U) sequence responses, as the Fig. 5b legend can be read, gives no Gabor difference in L2/3-D, L2/3-S or L5-D under either test (L5-S, 13 ROIs, Wilcoxon p = 0.011, mean within 2 SEM), but every plane keeps a difference through flow, so the outcome is the same. Selection (b) picks 0 to 72 ROIs per session and stimulus; one session (418779, Gabor) selects none.
fig6-behaviour¶
Running velocity and pupil diameter were recorded in every analysed session, for the 11 mice whose data are shown aggregated in Fig. 6b.
Each analysed session must have a running velocity trace (cm/s) and a pupil diameter trace (mm) that span the session, here the stimulus period from the first to the last trial-table row. Running is median-filtered (5 frames) and single-frame changes of at least 50 cm/s are dropped; pupil frames with a single-frame change of at least 0.05 mm are dropped (Methods).
cov = []
for s in sorted(ANALYSED):
m, t, b = meta[s], trials[s], beh[s]
t0, t1 = t.start_time.min(), t.stop_time.max()
cov.append({"session": s, "mouse": m["subject_id"], "run_unit": m["run_unit"], "pupil_unit": m["pupil_unit"],
"run_spans": m["run_first"] <= t0 + 1 and m["run_last"] >= t1 - 1,
"pupil_spans": m["pupil_first"] <= t0 + 1 and m["pupil_last"] >= t1 - 1,
"run_n": m["run_n"], "run_kept_%": 100 * len(b["run"]) / m["run_n"],
"pupil_nan_%": 100 * b["pupil_nan"] / m["pupil_n"], "pupil_kept_%": 100 * len(b["pupil"]) / m["pupil_n"],
"run_median": np.median(b["run"]), "pupil_median": np.median(b["pupil"])})
cov = pd.DataFrame(cov)
print(cov.drop(columns=["session"]).describe().loc[["min", "50%", "max"]].round(3).to_string())
print("units:", cov.run_unit.unique(), cov.pupil_unit.unique())
ok6 = cov[cov.run_spans & cov.pupil_spans & (cov.run_unit == "cm/s") & (cov.pupil_unit == "mm")]
print("sessions with both traces spanning the session:", len(ok6), "of", len(cov), "| mice:", ok6.mouse.nunique())
print(cov.groupby("mouse")[["pupil_nan_%", "run_median", "pupil_median"]].median().round(3).to_string())
run_n run_kept_% pupil_nan_% pupil_kept_% run_median pupil_median
min 251999.0 99.995 0.062 97.699 0.006 0.314
50% 251999.0 100.000 0.396 99.601 0.130 0.487
max 251999.0 100.000 2.293 99.937 33.141 0.678
units: <StringArray>
['cm/s']
Length: 1, dtype: str <StringArray>
['mm']
Length: 1, dtype: str
sessions with both traces spanning the session: 33 of 33 | mice: 11
pupil_nan_% run_median pupil_median
mouse
408021 0.986 0.191 0.509
411424 0.381 3.543 0.643
411771 0.331 0.019 0.318
413663 0.247 0.078 0.487
418779 1.038 0.125 0.447
420011 0.660 0.159 0.464
433414 0.122 0.015 0.487
433448 0.133 19.983 0.629
433451 1.004 0.055 0.425
433458 0.499 4.198 0.612
440889 0.389 0.196 0.405
fig, ax = plt.subplots(2, 3, figsize=(11, 5), sharey="row")
for col, sn in enumerate((1, 2, 3)):
ss = analysed.loc[analysed.sess_n == sn, "sessid"]
run = [beh[s]["run"] for s in ss]
pup = [beh[s]["pupil"] for s in ss]
ax[0, col].hist(run, bins=np.linspace(-20, 80, 51), stacked=True, color=plt.cm.Greys(np.linspace(0.3, 0.9, len(run))))
ax[1, col].hist(pup, bins=np.linspace(0, 1.5, 51), stacked=True, color=plt.cm.Greys(np.linspace(0.3, 0.9, len(pup))))
ax[0, col].set_title(f"session {sn}")
out_r = sum(int(((x < -20) | (x > 80)).sum()) for x in run)
out_p = sum(int(((x < 0) | (x > 1.5)).sum()) for x in pup)
print(f"session {sn}: running {min(x.min() for x in run):.1f} to {max(x.max() for x in run):.1f} cm/s, {out_r} values off the axes; "
f"pupil {min(x.min() for x in pup):.3f} to {max(x.max() for x in pup):.3f} mm, {out_p} off the axes")
ax[0, 0].set_ylabel("running frames"); ax[1, 0].set_ylabel("pupil frames")
for a in ax[0]: a.set_xlabel("running velocity (cm/s)")
for a in ax[1]: a.set_xlabel("pupil diameter (mm)")
fig.tight_layout()
session 1: running -28.4 to 74.1 cm/s, 147 values off the axes; pupil 0.082 to 1.021 mm, 0 off the axes
session 2: running -22.1 to 69.1 cm/s, 44 values off the axes; pupil 0.034 to 0.962 mm, 0 off the axes
session 3: running -18.4 to 83.8 cm/s, 146 values off the axes; pupil 0.102 to 0.919 mm, 0 off the axes
Result: reproduced. All 33 analysed sessions, from 11 mice, have a running velocity trace in cm/s and a pupil diameter trace in mm that span the stimulus period; 0.06 to 2.3% of pupil frames are NaN.
fig7a-snr-high-stable¶
ROI SNR is relatively high, above 1, in every plane and session, and stable over the three days.
SNR per ROI as in the paper's code, ROIs pooled across mice within each line, plane and session number (12 groups). "Above 1": (a) group median > 1, (b) group 25th percentile > 1. "Stable": (i) within each plane, each session's median lies inside every other session's 25th to 75th percentile box; (ii) each session's median within 10% of the mean of the three medians.
def per_roi(key):
rows = []
for s, v in roi.items():
m = md_.set_index("session").loc[s]
rows.append(pd.DataFrame({"value": v[key], "session": s, "mouse": m.subject_id, "sess_n": m.sess_n,
"plane": m.line + "-" + ("S" if m.plane == "soma" else "D")}))
return pd.concat(rows, ignore_index=True)
def group_stats(df):
g = df.groupby(["plane", "sess_n"]).value
return pd.DataFrame({"n": g.size(), "q25": g.quantile(0.25), "median": g.median(), "q75": g.quantile(0.75),
"q05": g.quantile(0.05), "q95": g.quantile(0.95)})
def stability(st):
out = {}
for plane, g in st.groupby(level="plane"):
med, q25, q75 = g["median"].values, g.q25.values, g.q75.values
inside = all(q25[j] <= med[i] <= q75[j] for i in range(3) for j in range(3) if i != j)
within = bool(np.all(np.abs(med - med.mean()) <= 0.1 * med.mean()))
out[plane] = {"i_inside_boxes": inside, "ii_within_10%": within,
"max_dev_from_mean_%": 100 * np.max(np.abs(med - med.mean())) / med.mean()}
return pd.DataFrame(out).T
snr = per_roi("snr")
st = group_stats(snr)
print(st.round(3).to_string())
print("(a) every group median > 1:", bool((st["median"] > 1).all()), "| (b) every group 25th percentile > 1:", bool((st.q25 > 1).all()),
"| smallest 25th percentile:", st.q25.min().round(3))
print(stability(st).to_string())
print("ROIs with SNR <= 1:", int((snr.value <= 1).sum()), "of", len(snr))
print(snr.groupby(["plane", "mouse", "sess_n"]).value.median().unstack().round(2).to_string())
n q25 median q75 q05 q95
plane sess_n
L2/3-D 1 1355 2.337 2.388 2.470 2.284 2.925
2 648 2.335 2.382 2.460 2.292 3.075
3 845 2.318 2.372 2.442 2.271 2.885
L2/3-S 1 407 2.529 3.090 4.593 2.285 9.067
2 388 2.479 2.924 4.173 2.273 7.947
3 397 2.475 2.831 3.769 2.273 7.605
L5-D 1 2807 2.455 2.552 2.688 2.367 3.178
2 1633 2.438 2.540 2.707 2.356 3.341
3 1664 2.431 2.519 2.660 2.350 3.132
L5-S 1 204 2.793 3.430 4.632 2.411 8.130
2 183 2.673 3.226 4.161 2.359 6.885
3 205 2.606 3.188 4.138 2.308 7.467
(a) every group median > 1: True | (b) every group 25th percentile > 1: True | smallest 25th percentile: 2.318
i_inside_boxes ii_within_10% max_dev_from_mean_%
L2/3-D True True 0.365022
L2/3-S True True 4.799678
L5-D True True 0.70787
L5-S True True 4.533112
ROIs with SNR <= 1: 0 of 10736
sess_n 1 2 3
plane mouse
L2/3-D 413663 2.39 2.39 2.36
433414 2.39 2.38 2.38
L2/3-S 408021 3.85 3.80 3.01
411424 3.01 2.97 2.84
440889 2.89 2.75 2.73
L5-D 420011 2.54 2.52 2.51
433448 2.54 2.47 2.48
433451 2.58 2.58 2.54
L5-S 411771 3.65 3.38 3.41
418779 3.55 3.13 2.95
433458 3.00 3.10 2.99
# The SNR of pure Gaussian noise (B1 of the first verification): unit-SD noise traces of a session's
# length with mean 0, +0.5 and -0.5 SD. For Gaussian noise of mean m and SD sd the SNR is about
# m / sd + E[Z | Z > 1.645] = m / sd + 2.063, so the measure exceeds 1 for noise alone unless the
# trace's median lies below about -1.06 noise SD.
rng_snr = np.random.default_rng(71)
n_frames = int(md_[md_.analysed].twop_n.median())
floor = []
for m0 in (0.0, 0.5, -0.5):
x = rng_snr.normal(m0, 1.0, size=(50, n_frames))
v = np.array([ca.calculate_snr(r)[0] for r in x])
floor.append({"noise_mean_sd": m0, "snr_median": np.median(v), "snr_min": v.min(), "snr_max": v.max(),
"analytic": m0 + scipy.stats.norm.pdf(1.645) / 0.05})
print(pd.DataFrame(floor).round(3).to_string(index=False))
# Each ROI's own noise floor: m / sd + 2.063, with m its median and sd its noise SD (signal / SNR).
fl = []
for s, v in roi.items():
sd = v["signal"] / v["snr"]
m = md_.set_index("session").loc[s]
fl.append(pd.DataFrame({"plane": m.line + "-" + ("S" if m.plane == "soma" else "D"), "sess_n": m.sess_n,
"snr": v["snr"], "floor": v["median"] / sd + scipy.stats.norm.pdf(1.645) / 0.05}))
fl = pd.concat(fl, ignore_index=True)
fl["above_floor"] = fl.snr - fl.floor
print(fl.groupby(["plane", "sess_n"]).agg(n=("snr", "size"), snr_median=("snr", "median"), floor_median=("floor", "median"),
above_floor_median=("above_floor", "median"),
frac_above_floor_by_1=("above_floor", lambda x: (x > 1).mean())).round(3).to_string())
noise_mean_sd snr_median snr_min snr_max analytic
0.0 2.063 2.050 2.080 2.062
0.5 2.563 2.549 2.579 2.562
-0.5 1.562 1.547 1.574 1.562
n snr_median floor_median above_floor_median frac_above_floor_by_1
plane sess_n
L2/3-D 1 1355 2.388 2.062 0.326 0.041
2 648 2.382 2.062 0.319 0.051
3 845 2.372 2.062 0.309 0.031
L2/3-S 1 407 3.090 2.062 1.028 0.506
2 388 2.924 2.062 0.862 0.448
3 397 2.831 2.062 0.769 0.398
L5-D 1 2807 2.552 2.062 0.489 0.070
2 1633 2.540 2.062 0.477 0.088
3 1664 2.519 2.062 0.456 0.064
L5-S 1 204 3.430 2.062 1.368 0.618
2 183 3.226 2.062 1.164 0.568
3 205 3.188 2.062 1.126 0.546
# The helper's calculate_snr and rolling_mean against the functions of the paper's code
# (github.com/colleenjg/util, util/math_util.py, at ca.UTIL_COMMIT), and its pairwise correlations against
# the code's own correlation step (get_correlation, analysis/misc_analys.py at ca.CODE_COMMIT), on every
# ROI of one somatic and one dendritic session-1 file (418779 and 420011).
paper_fn = ca.paper_code_functions()
cmp_rows = []
for s in sorted(SESS1):
if meta[s]["subject_id"] not in ("418779", "420011"):
continue
with ca.open_h5(basic.loc[basic.session == s, "asset_id"].iloc[0]) as f:
dff = f["processing/ophys/DfOverF/RoiResponseSeries/data"][:].T
dff = dff[np.all(np.isfinite(dff), axis=1)]
snr_paper = np.array([paper_fn["calculate_snr"](x) for x in dff])
rm_paper, rm_here = paper_fn["rolling_mean"](dff, win=4), ca.rolling_mean(dff, 4)
c_paper = ca.paper_get_correlation(dff)
cmp_rows.append({"session": s, "mouse": meta[s]["subject_id"], "rois": len(dff),
"max_abs_diff_snr": np.abs(snr_paper - roi[s]["snr"]).max(),
"max_abs_diff_rolling_mean": np.abs(rm_paper - rm_here).max(),
"pairs": len(c_paper), "max_abs_diff_corr": np.abs(c_paper - roi[s]["corr"]).max()})
del dff, rm_paper, rm_here
print(pd.DataFrame(cmp_rows).to_string(index=False))
session mouse rois max_abs_diff_snr max_abs_diff_rolling_mean pairs max_abs_diff_corr 777496949 418779 15 0.0 8.881784e-16 105 3.445616e-09 777914830 420011 205 0.0 1.776357e-15 20910 2.898118e-08
Result: reproduced. Every group's median and 25th percentile of SNR are above 1 (smallest 25th percentile 2.32; no ROI has SNR at or below 1), and in all four planes each session's median lies within every other session's interquartile box and within 4.8% of the mean of the three medians, so every reading pair holds. The criterion's threshold of 1 is low for this measure: pure Gaussian noise gives an SNR of 2.06 (1.56 and 2.56 with the median 0.5 SD below or above the noise mean), matching the analytic m / sd + 2.063. Every group's median SNR lies above its ROIs' own Gaussian noise floor (median floor 2.062), by 0.31 to 0.33 in L2/3-D, 0.46 to 0.49 in L5-D and 0.77 to 1.37 in the somatic planes; 3 to 9% of dendritic and 40 to 62% of somatic ROIs exceed it by more than 1. The outcome follows the criterion, but SNR above 1 alone would hold for noise. The helper's calculate_snr and rolling mean equal those of the paper's code (util/math_util.py at commit 2b05f954), and its pairwise correlations equal those of the code's own correlation step (get_correlation, analysis/misc_analys.py at commit 046e7fe), on every ROI and pair of 418779 and 420011 session 1 (105 and 20910 pairs; largest differences 0, under 2e-15 and under 3e-8, the last from storing correlations as float32).
fig7b-signal-stable¶
The mean dF/F signal of the ROIs is stable over days.
Signal is the mean of an ROI's dF/F values above its noise threshold. Readings (i) and (ii) as for SNR.
sig = per_roi("signal")
st_b = group_stats(sig)
print(st_b.round(4).to_string())
print(stability(st_b).to_string())
print(sig.groupby(["plane", "mouse", "sess_n"]).value.median().unstack().round(3).to_string())
n q25 median q75 q05 q95
plane sess_n
L2/3-D 1 1355 0.3616 0.4674 0.5749 0.2551 0.7414
2 648 0.3524 0.4481 0.5357 0.2440 0.6903
3 845 0.3334 0.4323 0.5345 0.2418 0.7068
L2/3-S 1 407 0.1601 0.2015 0.2839 0.1328 0.6628
2 388 0.1573 0.1908 0.2653 0.1299 0.5760
3 397 0.1620 0.1871 0.2424 0.1297 0.5403
L5-D 1 2807 0.3749 0.5168 0.7086 0.2512 0.9884
2 1633 0.3515 0.4461 0.5815 0.2511 0.8850
3 1664 0.3537 0.4541 0.6041 0.2554 0.9197
L5-S 1 204 0.2137 0.2529 0.3551 0.1671 0.5970
2 183 0.1857 0.2216 0.2891 0.1311 0.4606
3 205 0.1954 0.2367 0.3090 0.1620 0.5594
i_inside_boxes ii_within_10% max_dev_from_mean_%
L2/3-D True True 4.02721
L2/3-S True True 4.350542
L5-D True True 9.413938
L5-S True True 6.694551
sess_n 1 2 3
plane mouse
L2/3-D 413663 0.405 0.417 0.392
433414 0.522 0.482 0.490
L2/3-S 408021 0.248 0.218 0.186
411424 0.213 0.212 0.200
440889 0.185 0.182 0.184
L5-D 420011 0.521 0.526 0.557
433448 0.592 0.443 0.444
433451 0.440 0.435 0.444
L5-S 411771 0.240 0.188 0.217
418779 0.226 0.221 0.262
433458 0.278 0.252 0.257
Result: reproduced. Both stability readings hold in all four planes: each session's median signal lies inside the other sessions' boxes and within 9.4% (L5-D, the largest) of the mean of the three medians.
fig7c-correlations-similar¶
The distributions of pairwise ROI correlations are very similar for apical dendrite ROIs and for somatic ROIs, so the dendrites are unlikely to be heavily over-segmented.
Pearson correlations over all ROI pairs of each session (4-sample rolling mean), pooled across the mice of each line, plane and session. Six comparisons (line x session), dendritic against somatic: (i) the dendritic median inside the somatic 25th to 75th percentile range; (ii) the dendritic 99th percentile at most 10% above the somatic 99th percentile.
corr_rows = []
pooled = {}
for (line, plane, sn), g in md_[md_.analysed].groupby(["line", "plane", "sess_n"]):
c = np.concatenate([roi[s]["corr"] for s in g.session])
pooled[(line, plane, sn)] = c
q = np.quantile(c, [0.25, 0.5, 0.75, 0.99])
corr_rows.append({"line": line, "plane": plane, "sess_n": sn, "pairs": c.size, "q25": q[0], "median": q[1], "q75": q[2],
"q99": q[3], "frac_above_0.8_%": 100 * np.mean(c > 0.8)})
cr = pd.DataFrame(corr_rows).set_index(["line", "sess_n", "plane"])
with pd.option_context("display.float_format", "{:.4g}".format):
print(cr.to_string())
comp = []
for (line, sn), g in cr.groupby(level=["line", "sess_n"]):
d, s = g.xs("dend", level="plane").iloc[0], g.xs("soma", level="plane").iloc[0]
comp.append({"line": line, "sess_n": sn, "dend_median": d["median"], "soma_q25": s.q25, "soma_q75": s.q75,
"i": s.q25 <= d["median"] <= s.q75, "dend_q99": d.q99, "soma_q99": s.q99, "ii": d.q99 <= 1.1 * s.q99,
"ks_stat": scipy.stats.ks_2samp(pooled[(line, "dend", sn)], pooled[(line, "soma", sn)]).statistic})
comp = pd.DataFrame(comp)
with pd.option_context("display.float_format", "{:.4g}".format):
print(comp.to_string(index=False))
print("(i) holds in", comp.i.sum(), "of 6; (ii) holds in", comp.ii.sum(), "of 6")
pairs q25 median q75 q99 frac_above_0.8_%
line sess_n plane
L2/3 1 dend 460779 0.01972 0.02773 0.036 0.08179 0.02105
2 dend 105228 0.01795 0.0245 0.03172 0.1007 0.02756
3 dend 186094 0.01918 0.02692 0.03533 0.0957 0.004299
1 soma 33277 0.01484 0.0337 0.05675 0.1746 0.01503
2 soma 31682 0.02195 0.04531 0.07191 0.1774 0.009469
3 soma 30776 0.02558 0.04791 0.07344 0.1867 0.009748
L5 1 dend 1824435 0.009622 0.01619 0.02392 0.08058 0.00581
2 dend 640257 0.01112 0.01881 0.0285 0.1185 0.0114
3 dend 624836 0.01111 0.01856 0.028 0.1138 0.006242
1 soma 8961 0.007197 0.02341 0.04338 0.1791 0
2 soma 6481 0.01538 0.03058 0.05013 0.1897 0
3 soma 8143 0.01898 0.04351 0.08251 0.2067 0
line sess_n dend_median soma_q25 soma_q75 i dend_q99 soma_q99 ii ks_stat L2/3 1 0.02773 0.01484 0.05675 True 0.08179 0.1746 True 0.2705 L2/3 2 0.0245 0.02195 0.07191 True 0.1007 0.1774 True 0.4548 L2/3 3 0.02692 0.02558 0.07344 True 0.0957 0.1867 True 0.4364 L5 1 0.01619 0.007197 0.04338 True 0.08058 0.1791 True 0.2641 L5 2 0.01881 0.01538 0.05013 True 0.1185 0.1897 True 0.2871 L5 3 0.01856 0.01898 0.08251 False 0.1138 0.2067 True 0.4325 (i) holds in 5 of 6; (ii) holds in 6 of 6
fig, ax = plt.subplots(1, 2, figsize=(10, 3.5), sharey=True)
bins = np.linspace(-0.3, 1, 66)
for k, line in enumerate(("L2/3", "L5")):
for plane, ls in (("dend", "-"), ("soma", "--")):
for sn in (1, 2, 3):
c = pooled[(line, plane, sn)]
ax[k].hist(c, bins=bins, histtype="step", density=True, ls=ls, label=f"{plane} s{sn}")
print(line, plane, sn, "range", c.min().round(3), "to", c.max().round(3), "| pairs off the axes:", int(((c < -0.3) | (c > 1)).sum()))
ax[k].set_yscale("log"); ax[k].set_title(line); ax[k].set_xlabel("pairwise correlation")
ax[0].legend(fontsize=7); ax[0].set_ylabel("density")
fig.tight_layout()
L2/3 dend 1 range -0.263 to 0.953 | pairs off the axes: 0 L2/3 dend 2 range -0.107 to 0.904 | pairs off the axes: 0 L2/3 dend 3 range -0.433 to 0.886 | pairs off the axes: 1 L2/3 soma 1 range -0.087 to 0.899 | pairs off the axes: 0 L2/3 soma 2 range -0.1 to 0.938 | pairs off the axes: 0 L2/3 soma 3 range -0.091 to 0.897 | pairs off the axes: 0 L5 dend 1 range -0.295 to 0.955 | pairs off the axes: 0 L5 dend 2 range -0.318 to 0.948 | pairs off the axes: 1 L5 dend 3 range -0.531 to 0.919 | pairs off the axes: 4 L5 soma 1 range -0.107 to 0.603 | pairs off the axes: 0 L5 soma 2 range -0.062 to 0.578 | pairs off the axes: 0 L5 soma 3 range -0.13 to 0.618 | pairs off the axes: 0
Result: blocked (methods underspecified). Reading (ii) holds in all six comparisons: the dendritic 99th percentile is below the somatic one in each (0.081 to 0.119 against 0.175 to 0.207). Reading (i) holds in 5 of 6: in L5 session 3 the dendritic median, 0.0186, is just below the somatic 25th percentile, 0.0190. The criterion asks both readings to hold in all six for a reproduction and blocks the claim when they disagree.
high-correlation-pairs¶
About 0.01% of possible pairs of L2/3 dendritic ROIs have correlations above 0.8.
Readings for the denominator: (a) pairs pooled over the L2/3-D sessions 1 to 3 of the analysed mice; (b) mean of per-session fractions; (c) session 1 only (pooled over its mice).
l23d = md_[md_.analysed & (md_.line == "L2/3") & (md_.plane == "dend")]
per_s = pd.DataFrame({s: {"mouse": meta[s]["subject_id"], "sess_n": l23d.set_index("session").loc[s, "sess_n"],
"pairs": roi[s]["corr"].size, "above": int(np.sum(roi[s]["corr"] > 0.8))} for s in l23d.session}).T
per_s["frac_%"] = 100 * per_s.above.astype(float) / per_s.pairs.astype(float)
print(per_s.to_string())
a = 100 * per_s.above.sum() / per_s.pairs.sum()
b = per_s["frac_%"].mean()
s1 = per_s[per_s.sess_n == 1]
c = 100 * s1.above.sum() / s1.pairs.sum()
bymouse = per_s.groupby("mouse")[["pairs", "above"]].sum()
bymouse["frac_%"] = 100 * bymouse.above.astype(float) / bymouse.pairs.astype(float)
print(bymouse.to_string())
for m_out in bymouse.index:
rest = per_s[per_s.mouse != m_out]
print(f"pooled without {m_out}: {100 * rest.above.sum() / rest.pairs.sum():.4f}%")
print(f"(a) pooled {a:.4f}% (b) mean of sessions {b:.4f}% (c) session 1 {c:.4f}% | tolerance 0.005 to 0.015%")
other = cr["frac_above_0.8_%"].groupby(level=["line", "plane"]).mean()
print("mean over sessions of the pooled fraction above 0.8 (%), other planes:", other.round(4).to_dict())
mouse sess_n pairs above frac_%
764704289 413663 1 196878 90 0.045714
765193831 413663 2 60378 26 0.043062
766502238 413663 3 130816 7 0.005351
826187862 433414 1 263901 7 0.002653
826773996 433414 2 44850 3 0.006689
827833392 433414 3 55278 1 0.001809
pairs above frac_%
mouse
413663 388072 123 0.031695
433414 364029 11 0.003022
pooled without 413663: 0.0030%
pooled without 433414: 0.0317%
(a) pooled 0.0178% (b) mean of sessions 0.0175% (c) session 1 0.0211% | tolerance 0.005 to 0.015%
mean over sessions of the pooled fraction above 0.8 (%), other planes: {('L2/3', 'dend'): 0.0176, ('L2/3', 'soma'): 0.0114, ('L5', 'dend'): 0.0078, ('L5', 'soma'): 0.0}
Result: not reproduced. 0.0178% of L2/3-D pairs have correlations above 0.8 when pooled over the six sessions (reading a), 0.0175% as a mean of sessions (b), and 0.0211% in session 1 (c), all above the 0.005 to 0.015% the claim allows. Per session the fraction ranges from 0.0018 to 0.046%. By mouse, 413663 gives 0.032% (123 of the 134 pairs above 0.8) and 433414 0.0030%; without 413663 the pooled fraction is 0.0030%, without 433414 0.032%, both outside the tolerance as well.
fig9-onset-offset-transients¶
Mean dF/F traces show distinct transients aligned with the stimulus onsets and offsets (grey screen to stimulus and back), in every plane and session.
Per ROI, the mean over events of (mean dF/F 0 to 2 s after - mean over 2 s before), for onsets (grey to stimulus) and offsets (stimulus to grey), both stimuli. Readings (i) |mean across ROIs| > 2 SEM, (ii) two-sided Wilcoxon signed-rank across ROIs, p < 0.05. Variants: Gabor onsets and offsets only, and the code's scaled traces.
res9 = []
for key in ("onset_diff", "offset_diff", "onset_diff_gabor", "offset_diff_gabor", "onset_diff_scaled", "offset_diff_scaled"):
for (line, plane, sn), g in md_[md_.analysed].groupby(["line", "plane", "sess_n"]):
dlt = np.concatenate([roi[s][key] for s in g.session])
n, mean, sem, p, wil, sem2 = two_tests(np.zeros_like(dlt), dlt)
res9.append({"measure": key, "group": f"{line}-{'S' if plane == 'soma' else 'D'}", "sess_n": sn, "n_rois": n,
"mean": mean, "sem": sem, "wilcoxon_p": p, "i_2sem": sem2, "ii_wilcoxon": wil})
res9 = pd.DataFrame(res9)
with pd.option_context("display.float_format", "{:.3g}".format):
print(res9[res9.measure.isin(["onset_diff", "offset_diff"])].to_string(index=False))
print(res9.groupby("measure")[["i_2sem", "ii_wilcoxon"]].sum().astype(int).astype(str).add(" of 12").to_string())
measure group sess_n n_rois mean sem wilcoxon_p i_2sem ii_wilcoxon
onset_diff L2/3-D 1 1355 0.051 0.00153 3.99e-193 True True
onset_diff L2/3-D 2 648 0.0517 0.0019 6.29e-93 True True
onset_diff L2/3-D 3 845 0.0647 0.00251 3.51e-131 True True
onset_diff L2/3-S 1 407 0.0487 0.00508 9.33e-46 True True
onset_diff L2/3-S 2 388 0.0601 0.00537 6.15e-55 True True
onset_diff L2/3-S 3 397 0.0862 0.00615 6.86e-64 True True
onset_diff L5-D 1 2807 0.103 0.00283 0 True True
onset_diff L5-D 2 1633 0.0615 0.00248 1.92e-213 True True
onset_diff L5-D 3 1664 0.0691 0.00292 5.03e-194 True True
onset_diff L5-S 1 204 0.0738 0.00754 4.85e-30 True True
onset_diff L5-S 2 183 0.0622 0.00678 8.1e-29 True True
onset_diff L5-S 3 205 0.0726 0.00746 6.49e-29 True True
offset_diff L2/3-D 1 1355 -0.0132 0.000904 3.87e-69 True True
offset_diff L2/3-D 2 648 -0.0248 0.00123 1.39e-69 True True
offset_diff L2/3-D 3 845 -0.0161 0.00121 4.34e-62 True True
offset_diff L2/3-S 1 407 -0.0278 0.00328 2.75e-46 True True
offset_diff L2/3-S 2 388 -0.0227 0.00413 1.02e-38 True True
offset_diff L2/3-S 3 397 -0.0289 0.00425 2.64e-45 True True
offset_diff L5-D 1 2807 -0.0163 0.000993 6.13e-109 True True
offset_diff L5-D 2 1633 -0.0243 0.00108 6.4e-159 True True
offset_diff L5-D 3 1664 -0.0144 0.00119 1.52e-88 True True
offset_diff L5-S 1 204 -0.0348 0.00368 1.17e-29 True True
offset_diff L5-S 2 183 -0.0193 0.00863 3.16e-25 True True
offset_diff L5-S 3 205 -0.0244 0.00603 3.73e-26 True True
i_2sem ii_wilcoxon
measure
offset_diff 12 of 12 12 of 12
offset_diff_gabor 7 of 12 9 of 12
offset_diff_scaled 12 of 12 12 of 12
onset_diff 12 of 12 12 of 12
onset_diff_gabor 12 of 12 12 of 12
onset_diff_scaled 12 of 12 12 of 12
# By mouse (unscaled, both stimuli): mean difference and Wilcoxon p.
bym9 = []
for key in ("onset_diff", "offset_diff"):
for s in sorted(ANALYSED):
m = md_.set_index("session").loc[s]
n, mean, sem, p, wil, sem2 = two_tests(np.zeros(len(roi[s][key])), roi[s][key])
bym9.append({"measure": key, "plane": m.line + "-" + m.plane, "mouse": m.subject_id, "sess_n": m.sess_n, "mean": mean, "p": p})
bym9 = pd.DataFrame(bym9)
with pd.option_context("display.float_format", "{:.2g}".format):
print(bym9.pivot_table(index=["plane", "mouse"], columns=["measure", "sess_n"], values="mean").to_string())
print("mouse-sessions with p < 0.05: onset", int((bym9[bym9.measure == "onset_diff"].p < 0.05).sum()),
"| offset", int((bym9[bym9.measure == "offset_diff"].p < 0.05).sum()), "of", len(ANALYSED))
measure offset_diff onset_diff
sess_n 1 2 3 1 2 3
plane mouse
L2/3-dend 413663 -0.013 -0.033 -0.019 0.064 0.05 0.067
433414 -0.013 -0.016 -0.012 0.04 0.054 0.06
L2/3-soma 408021 -0.029 -0.062 -0.046 0.026 0.069 0.062
411424 -0.041 -0.032 -0.041 0.062 0.033 0.074
440889 -0.022 -0.0057 -0.015 0.053 0.068 0.1
L5-dend 420011 -0.0059 -0.01 -0.0049 0.04 0.07 0.074
433448 -0.0097 -0.014 -0.013 0.14 0.11 0.12
433451 -0.03 -0.031 -0.017 0.058 0.039 0.041
L5-soma 411771 -0.036 -0.0046 -0.0032 0.042 0.073 0.044
418779 -0.045 -0.023 -0.017 0.033 0.055 0.075
433458 -0.032 -0.03 -0.044 0.11 0.056 0.095
mouse-sessions with p < 0.05: onset 33 | offset 33 of 33
# Mean traces (+-SEM across ROIs) around onsets and offsets, per group, unscaled, both stimuli.
tt = np.arange(-ca.PRE, ca.POST) / ca.FPS
fig, ax = plt.subplots(2, 4, figsize=(13, 5), sharex=True)
for k, (line, plane) in enumerate([("L2/3", "dend"), ("L2/3", "soma"), ("L5", "dend"), ("L5", "soma")]):
for r, key in enumerate(("onset_trace", "offset_trace")):
for sn in (1, 2, 3):
ss = md_[md_.analysed & (md_.line == line) & (md_.plane == plane) & (md_.sess_n == sn)].session
tr_ = np.concatenate([roi[s][key] for s in ss])
mu, se = tr_.mean(0), tr_.std(0, ddof=1) / np.sqrt(len(tr_))
ax[r, k].plot(tt, mu, label=f"session {sn}"); ax[r, k].fill_between(tt, mu - se, mu + se, alpha=0.3)
print(line, plane, key, sn, "mean trace range", mu.min().round(4), "to", mu.max().round(4))
ax[r, k].axvline(0, ls="--", c="k"); ax[r, k].set_title(f"{line} {plane} {key.split('_')[0]}", fontsize=9)
ax[0, 0].legend(fontsize=7)
for a in ax[1]: a.set_xlabel("time from event (s)")
for a in ax[:, 0]: a.set_ylabel("dF/F")
fig.tight_layout()
L2/3 dend onset_trace 1 mean trace range -0.026 to 0.1326 L2/3 dend onset_trace 2 mean trace range -0.0299 to 0.1137 L2/3 dend onset_trace 3 mean trace range -0.0455 to 0.1298 L2/3 dend offset_trace 1 mean trace range -0.0075 to 0.0346 L2/3 dend offset_trace 2 mean trace range -0.0159 to 0.0461 L2/3 dend offset_trace 3 mean trace range -0.0135 to 0.0551 L2/3 soma onset_trace 1 mean trace range -0.0244 to 0.107 L2/3 soma onset_trace 2 mean trace range -0.024 to 0.1036
L2/3 soma onset_trace 3 mean trace range -0.031 to 0.1431 L2/3 soma offset_trace 1 mean trace range -0.0138 to 0.041 L2/3 soma offset_trace 2 mean trace range -0.0013 to 0.0431 L2/3 soma offset_trace 3 mean trace range -0.0116 to 0.0538 L5 dend onset_trace 1 mean trace range -0.0321 to 0.219 L5 dend onset_trace 2 mean trace range -0.0313 to 0.1137 L5 dend onset_trace 3 mean trace range -0.0223 to 0.1311 L5 dend offset_trace 1 mean trace range 0.017 to 0.0545 L5 dend offset_trace 2 mean trace range 0.0003 to 0.0538 L5 dend offset_trace 3 mean trace range -0.0042 to 0.0578 L5 soma onset_trace 1 mean trace range -0.0153 to 0.1124 L5 soma onset_trace 2 mean trace range -0.0179 to 0.0944 L5 soma onset_trace 3 mean trace range -0.0237 to 0.1151 L5 soma offset_trace 1 mean trace range -0.0166 to 0.0493 L5 soma offset_trace 2 mean trace range -0.012 to 0.0426 L5 soma offset_trace 3 mean trace range -0.0204 to 0.0483
Result: reproduced. Both readings find a transient at onset and at offset in all 12 groups. The
mean dF/F rises after stimulus onsets (mean differences 0.049 to 0.103 dF/F) and is lower after offsets
than before them (-0.013 to -0.035). With Gabor events only, onsets give a difference in 12 of 12
groups and offsets in 7 (i) and 9 (ii) of 12. The Fig. 9 legends name the Gabor sequence onset and
offset only, while the text names both stimuli; whether the claim should be judged on Gabor events alone
is asked of a reader (questions/000336.yaml). On that variant both readings agree that some groups
lack an offset transient, which the criterion calls partially reproduced.
Summary¶
| Claim | Outcome |
|---|---|
| dataset-holds-paper-sessions | not reproduced: 000336 holds none of the paper's sessions |
| table1-subjects | reproduced (on 000037) |
| table2-sessions | reproduced (on 000037) |
| session-schedule | reproduced (on 000037), under readings (a) and (ii) |
| imaging-planes | partially reproduced (on 000037): 433414 is at 60 um in the files, 75 um in Table 2 |
| roi-counts-per-session | reproduced (on 000037) |
| tracked-rois-per-plane | reproduced (on 000037), fractions under reading (a) |
| fig8-example-matches | reproduced (on 000037) |
| fig8-ordering-variation | reproduced (on 000037), in roi_matching_orderings.ipynb, resting on assumptions |
| stimulus-durations | reproduced (on 000037) |
| inconsistent-event-fraction | reproduced (on 000037), blocks under reading (i), resting on assumptions (durations on the sampling grid, block count as a mean over sessions) |
| fig5-stimulus-responses | reproduced (on 000037) |
| fig6-behaviour | reproduced (on 000037) |
| fig7a-snr-high-stable | reproduced (on 000037); pure Gaussian noise has SNR about 2.06 on this measure |
| fig7b-signal-stable | reproduced (on 000037) |
| fig7c-correlations-similar | blocked, methods underspecified: the criterion's two readings disagree in one of six comparisons |
| high-correlation-pairs | not reproduced (on 000037): about 0.018% of pairs, against 0.01 +- 0.005% |
| fig9-onset-offset-transients | reproduced (on 000037) |
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.
How the analysis differs from the paper's: the target dandiset, 000336, does not hold the paper's data, so every finding was tested on DANDI:000037 (version 0.230426.0054). Session numbers and QC come from the paper's code table, as the files carry neither. Onset, offset and Fig. 5 measures use unscaled dF/F, as the claims file says, with the code's scaled traces as a variant. The Fig. 5 test and ROI selection, the stability and similarity readings and the theoretical maximum of tracked ROIs are the claims file's readings, since the paper gives no tests. The ordering analysis uses the public matching package, not the paper's modified one, with the overlap constraint read as intersection over union.
What was found about the datasets:
- DANDI:000336 names the paper as a related resource but holds a different OpenScope project (16 other mice, recorded 2022 to 2023); the paper's data are in DANDI:000037.
- In DANDI:000037 the stored
session_start_timecarries a Pacific offset (-07:00 or -08:00) on a clock time that is the UTC start time, so the stored times are 7 to 8 hours late; read as written, 4 of 50 sessions fall on a different date from Table 2. - The three sessions of 433414 give a depth of 60 um, where Table 2 and the paper's code table give 75 um.
- The files carry no QC status or session number, which Table 2 and the paper's code give.
- Session 759872185 (411400, failed QC) has 61134 pupil samples for 127588 imaging frames.
Checks added after the first verification: the subject metadata of all 16 subjects of 000336; the paper's
code table in full beside Table 2; the raw depth strings of 433414; per-session block counts and the runs
furthest from the sampling grid; the Fig. 5 selection counts and thresholds, and a Gabor selection on
inconsistent sequences; a pure-noise control for SNR and a comparison of the helper's SNR, rolling mean and
correlations with the paper's code; and the L2/3-D high-correlation fraction by mouse. In
roi_matching_orderings.ipynb: the package's own run on all orderings of two mice, and the matches lost
only for lacking a direct pairwise link. After the second verification: the correlations checked against
the paper's own correlation step (get_correlation), and the ordering analysis run on all 13 tracked mice
as well as the 11 analysed ones.
print(f"notebook ran in {(time.time() - T0) / 60:.1f} min")
notebook ran in 12.7 min