Reproducing Geiller, Vancura et al. (2020) from dandiset 000625¶
Paper: Geiller T, Vancura B, Terada S, et al. (2020). Large-Scale 3D Two-Photon Imaging of Molecularly Identified CA1 Interneuron Dynamics in Behaving Mice. Neuron 108(5):968-983. doi:10.1016/j.neuron.2020.09.013. The full text was used: the NIH author manuscript on PubMed Central (PMC7736348), with its STAR Methods and figure legends. The supplementary figures were not available, so claims about them rest on the Results text.
Data: DANDI:000625, version 0.231114.0019, streamed from the archive. The paper cites no version of the dandiset (it says the data "will be made available soon"), so the most recent published version is pinned. The only other published version, 0.231114.0017, has the same three assets; the two differ only in the description text (checked below).
What can and cannot be tested¶
The paper imaged 3626 interneurons in 16 mice across random foraging, novel contexts, goal-oriented learning, sharp-wave ripple recordings and two further staining strategies. The dandiset holds one VGAT-Cre mouse (tg19) in three random-foraging sessions: dF/F of 231 ROIs, the subtype of each ROI, belt position, and reward and water times. It holds no cell depth, LFP, licking, novel-context or goal-oriented sessions, and none of the VIP-Cre or strategy 3 mice.
| Claim | Paper | Here |
|---|---|---|
fig1-cells-per-mouse |
About 227 interneurons imaged and 121 identified per mouse | Tested |
fig1-depth-distribution |
Layer distribution of each subtype | Blocked: no cell depth or coordinates |
fig2c-most-positive |
Most interneurons positively correlated with velocity, about 10% negatively | Tested |
fig2c-cck-negative |
Negative correlations enriched in CCKC, nearly half of them | Tested |
fig2c-subtypes-differ |
Velocity correlation differs between subtypes | Tested |
fig2c-ngfc-less-modulated |
IvC/NGFC less modulated by locomotion | Tested |
fig2d-negative-cells-in-oriens |
Negatively correlated cells in stratum oriens | Blocked: no cell depth |
s2-lags |
Negatively correlated cells more tightly locked in time to velocity | Tested |
s2-cck-run-stop |
Immobility-triggered activation specific to CCKC | Tested |
s3-subtype-classification |
Deep network classifies four groups at 59 to 71% | Blocked: one mouse of the training set, no depth |
fig3-swr-pvbc-vs-somc, fig3-swr-bimodal |
Sharp-wave ripple modulation | Blocked: no LFP or 40 Hz sessions |
fig4-fraction-spatial |
17.5% of interneurons spatially selective | Tested |
fig4-fraction-negative |
12.7% with negative fields | Tested |
fig4d-decoding |
Spatially selective subset decodes position about as well as all cells | Tested |
fig4e-bic-recurrence |
BiC more often spatially modulated again | Blocked: 3 BiC |
fig4f-selectivity-no-difference |
Selectivity index does not differ between subtypes | Tested |
fig4h-pvbc-more-stable |
Spatial PVBC more stable within session than spatial SomC | Tested |
s6-within-session-order |
PVBC most and CCKC least stable within session | Tested |
s6-later-lower-above-chance |
Stability after 1 h and 24 h lower but above chance | Tested, with stand-in intervals |
fig5-context-decorrelation, fig5-novel-context-decrease |
Novel context | Blocked: no novel-context sessions |
fig6-reward-somc-bic, fig6-old-zone |
Goal-oriented learning | Blocked: no goal-oriented sessions or licking |
fig7-vip-subtypes-immobility, fig7-isi-correlations |
VIP and ISI subtypes | Blocked: those mice are not in the dandiset |
The paper's code (losonczylab/Geiller_Vancura_Neuron2020,
commit c37f37c) holds only the deep-network classifier of Figure S3 and its preprocessing. From it
this notebook takes the lap definition (a lap starts where position wraps to the start of the
belt). Everything else follows the STAR Methods, with the choices they leave open stated in
interneurons.py.
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats
from threadpoolctl import threadpool_limits
import interneurons as inn
warnings.filterwarnings("ignore", category=RuntimeWarning)
threadpool_limits(1)
plt.rcParams.update({"figure.dpi": 100, "axes.spines.top": False, "axes.spines.right": False})
pd.set_option("display.precision", 3)
pd.set_option("display.width", 160)
def rng_for(claim):
"""A generator of its own for each claim's section, seeded from the claim id."""
return np.random.default_rng(list(claim.encode()))
Versions¶
The two published versions list the same assets, with the same asset identifiers.
old, new = inn.assets("0.231114.0017"), inn.assets(inn.VERSION)
print(pd.DataFrame(new, columns=["path", "asset id"]).to_string(index=False))
print(f"\nThe assets of 0.231114.0017 and {inn.VERSION} are the same: {old == new}")
path asset id sub-tg19/sub-tg19_ses-21322_behavior+ophys.nwb 714411fc-fbd1-4ce2-8d57-0fe9376f9292 sub-tg19/sub-tg19_ses-21324_behavior+ophys.nwb 274df0e2-a678-4d74-8e6a-99953eefbb47 sub-tg19/sub-tg19_ses-21333_behavior+ophys.nwb 3ab398ea-dbff-4978-b432-6fc2f5ee3aeb The assets of 0.231114.0017 and 0.231114.0019 are the same: True
Load the data and check it against its description¶
interneurons.load_session streams each file and reads the dF/F traces, the subtype table, the
belt position, the reward and water intervals and the ROI masks. The raw image series are not
read.
sessions = {s.session_id: s for s in (inn.load_session(p) for p, _ in new)}
order = sorted(sessions, key=lambda k: sessions[k].start_time) # chronological
rows = []
for sid in order:
s = sessions[sid]
L = inn.belt_length(s)
finite = s.dff[np.isfinite(s.dff)]
rows.append({
"session": sid, "start (as stored)": s.start_time, "description": s.description,
"frames": s.dff.shape[0], "ROIs": s.dff.shape[1], "ROIs imaged": int((~np.isnan(s.dff).all(0)).sum()),
"ROIs partly NaN": int((np.isnan(s.dff).any(0) & ~np.isnan(s.dff).all(0)).sum()),
"stated rate (Hz)": s.stated_rate, "imaging s at stated rate": s.dff.shape[0] / s.stated_rate,
"behaviour s": s.pos_t[-1], "position samples": len(s.pos),
"longest gap between samples (s)": np.diff(s.pos_t).max(),
"position min (m)": s.pos.min(), "position max (m)": s.pos.max(), "belt length (m)": L,
"laps (wraps)": int((np.diff(s.pos) < -L / 2).sum()),
"reward intervals": len(s.reward), "median reward s": np.median(np.diff(s.reward, axis=1)),
"water intervals": len(s.water), "median water s": np.median(np.diff(s.water, axis=1)),
"dF/F min": finite.min(), "dF/F median": np.median(finite), "dF/F 99th pct": np.percentile(finite, 99),
"dF/F max": finite.max(),
})
print(pd.DataFrame(rows).set_index("session").T.to_string())
first = sessions[order[0]]
print("\nLabels as stored, ROIs per label (the same in every session: "
f"{all((sessions[k].label == first.label).all() for k in order)})")
print(pd.Series(first.label).value_counts().to_string())
print("\nThe paper's six subtypes, ROIs imaged per session")
print(pd.DataFrame({sid: pd.Series(sessions[sid].subtype[~np.isnan(sessions[sid].dff).all(0)])
.value_counts().reindex(inn.ORDER) for sid in order}).to_string())
print(f"\nMetadata read from the files, session {order[0]}")
for k, v in first.meta.items():
print(f" {k}: {v}")
for sid in order[1:]:
diff = {k: v for k, v in sessions[sid].meta.items() if v != first.meta[k]}
print(f"Session {sid} differs in: {diff if diff else 'nothing'}")
session 21333 21324 21322
start (as stored) 2019-06-11T09:45:26+00:00 2019-06-13T15:35:02+00:00 2019-06-13T18:02:27+00:00
description Random foraging: session 1 of 4. Experiment group: imvgat. Random foraging: session 1 of 4. Experiment group: imvgat. Random foraging: session 1 of 4. Experiment group: imvgat.
frames 5211 3945 3944
ROIs 231 231 231
ROIs imaged 215 216 224
ROIs partly NaN 0 0 0
stated rate (Hz) 5.798 5.798 5.798
imaging s at stated rate 898.743 680.395 680.223
behaviour s 897.408 899.987 879.099
position samples 26131 45609 26701
longest gap between samples (s) 25.522 65.784 36.116
position min (m) 0.0 0.0 0.0
position max (m) 2.11 2.11 2.11
belt length (m) 2.11 2.11 2.11
laps (wraps) 9 37 17
reward intervals 46 112 48
median reward s 1.0 0.5 0.659
water intervals 97 113 49
median water s 0.049 0.049 0.049
dF/F min -0.152 -0.086 -0.084
dF/F median 0.113 0.129 0.122
dF/F 99th pct 0.615 0.849 1.038
dF/F max 7.428 8.312 8.81
Labels as stored, ROIs per label (the same in every session: True)
UNIDENTIFIED 148
PVBC 24
CCK 15
NGFF 13
SOM 10
NOTHING 9
AAC 8
BISTR 3
AAC (RIGHT CELL) 1
The paper's six subtypes, ROIs imaged per session
21333 21324 21322
PVBC 24 24 24
SomC 10 9 10
BiC 3 3 3
AAC 9 8 9
CCKC 15 14 14
IvC/NGFC 12 12 13
Metadata read from the files, session 21333
dff unit: lumens
DfOverF and Fluorescence are one HDF5 object: True
subtype rows: 232
ROIs: 231
subtype ids without a ROI: [np.int64(231)]
subtype column description: Recording location in Middle Earth
position unit: meters
position reference frame: Position offset: .220 meters
imaging rate (plane): 5.7981
indicator: GCaMP6f
excitation (nm): 920.0
emission (nm): 920.0
raw image series: {'TwoPhotonSeries2': (5211, 25, 46)}
subject: {'age': 'P60D', 'sex': 'M', 'species': 'Mus musculus', 'subject_id': 'tg19'}
experiment description: Head-fixed mouse running on a treadmill belt during 4 random foraging sessions, the objective being to label the most numerous interneuron subtypes.
PlaneSegmentation columns: ['id', 'pixel_mask', 'pixel_mask_index']
pixel_mask fields: ['x', 'y', 'weight']
pixel_mask x range: (0, 10625)
pixel_mask y range: (0, 24)
reference images: []
imaging plane fields: ['Green', 'description', 'device', 'excitation_lambda', 'grid_spacing', 'imaging_rate', 'indicator', 'location', 'origin_coords']
imaging plane location: CA1
imaging plane origin_coords (um): [0.0, 0.0]
imaging plane grid_spacing (um): [50.0, 50.0]
acquisition: ['Position/SpatialSeries/data (26131,)', 'Position/SpatialSeries/reference_frame ()', 'Position/SpatialSeries/timestamps (26131,)', 'TwoPhotonSeries2/data (5211, 25, 46)', 'TwoPhotonSeries2/imaging_plane/Green/description ()', 'TwoPhotonSeries2/imaging_plane/Green/emission_lambda ()', 'TwoPhotonSeries2/imaging_plane/description ()', 'TwoPhotonSeries2/imaging_plane/excitation_lambda ()', 'TwoPhotonSeries2/imaging_plane/grid_spacing (2,)', 'TwoPhotonSeries2/imaging_plane/imaging_rate ()', 'TwoPhotonSeries2/imaging_plane/indicator ()', 'TwoPhotonSeries2/imaging_plane/location ()', 'TwoPhotonSeries2/imaging_plane/origin_coords (2,)', 'TwoPhotonSeries2/starting_time ()']
processing/behavior: ['BehavioralEpochs/reward/data (92,)', 'BehavioralEpochs/reward/timestamps (92,)', 'BehavioralEpochs/water/data (194,)', 'BehavioralEpochs/water/timestamps (194,)']
raw image series starting_time (s): {'TwoPhotonSeries2': 0.0}
file_create_date: ['2023-11-13T18:16:37.067111+00:00']
timestamps_reference_time: 2019-06-11T09:45:26+00:00
Session 21324 differs in: {'raw image series': {'TwoPhotonSeries2': (3945, 33, 46)}, 'pixel_mask y range': (0, 32), 'acquisition': ['Position/SpatialSeries/data (45609,)', 'Position/SpatialSeries/reference_frame ()', 'Position/SpatialSeries/timestamps (45609,)', 'TwoPhotonSeries2/data (3945, 33, 46)', 'TwoPhotonSeries2/imaging_plane/Green/description ()', 'TwoPhotonSeries2/imaging_plane/Green/emission_lambda ()', 'TwoPhotonSeries2/imaging_plane/description ()', 'TwoPhotonSeries2/imaging_plane/excitation_lambda ()', 'TwoPhotonSeries2/imaging_plane/grid_spacing (2,)', 'TwoPhotonSeries2/imaging_plane/imaging_rate ()', 'TwoPhotonSeries2/imaging_plane/indicator ()', 'TwoPhotonSeries2/imaging_plane/location ()', 'TwoPhotonSeries2/imaging_plane/origin_coords (2,)', 'TwoPhotonSeries2/starting_time ()'], 'processing/behavior': ['BehavioralEpochs/reward/data (224,)', 'BehavioralEpochs/reward/timestamps (224,)', 'BehavioralEpochs/water/data (226,)', 'BehavioralEpochs/water/timestamps (226,)'], 'file_create_date': ['2023-11-13T18:16:41.840769+00:00'], 'timestamps_reference_time': '2019-06-13T15:35:02+00:00'}
Session 21322 differs in: {'raw image series': {'TwoPhotonSeries2': (3944, 33, 46)}, 'pixel_mask y range': (0, 32), 'acquisition': ['Position/SpatialSeries/data (26701,)', 'Position/SpatialSeries/reference_frame ()', 'Position/SpatialSeries/timestamps (26701,)', 'TwoPhotonSeries2/data (3944, 33, 46)', 'TwoPhotonSeries2/imaging_plane/Green/description ()', 'TwoPhotonSeries2/imaging_plane/Green/emission_lambda ()', 'TwoPhotonSeries2/imaging_plane/description ()', 'TwoPhotonSeries2/imaging_plane/excitation_lambda ()', 'TwoPhotonSeries2/imaging_plane/grid_spacing (2,)', 'TwoPhotonSeries2/imaging_plane/imaging_rate ()', 'TwoPhotonSeries2/imaging_plane/indicator ()', 'TwoPhotonSeries2/imaging_plane/location ()', 'TwoPhotonSeries2/imaging_plane/origin_coords (2,)', 'TwoPhotonSeries2/starting_time ()'], 'processing/behavior': ['BehavioralEpochs/reward/data (96,)', 'BehavioralEpochs/reward/timestamps (96,)', 'BehavioralEpochs/water/data (98,)', 'BehavioralEpochs/water/timestamps (98,)'], 'file_create_date': ['2023-11-13T18:16:46.969234+00:00'], 'timestamps_reference_time': '2019-06-13T18:02:27+00:00'}
Checks of the description against the values:
- Subject and sessions. One mouse, tg19, which the paper lists under staining strategy 1. Three sessions, two on 2019-06-13 about 2.5 h apart and one two days earlier. All three are described as "session 1 of 4", and the experiment description mentions four random-foraging sessions; one is not in the dandiset.
- ROIs and subtypes. 231 ROIs, of which 215, 216 and 224 have traces in the three sessions; the others are entirely NaN. The subtype table has 232 rows; the row with id 231 has no ROI and is left out, and the others are matched to ROIs by id. 74 ROIs carry one of the paper's six subtypes (24 PVBC, 10 SOM, 3 BISTR, 9 AAC counting "AAC (RIGHT CELL)", 15 CCK, 13 NGFF). The labels "UNIDENTIFIED" and "NOTHING" are not explained. The column's description reads "Recording location in Middle Earth".
- dF/F. The values run from about -0.15 to 8.8 with a median near 0.12, so they are fractions, not percent. The stated unit, lumens, is wrong. The DfOverF and Fluorescence series are the same HDF5 object, so the raw fluorescence the description lists is not there.
- Position. Stated in metres, it runs from 0 to 2.11 m, so the belt is 2.11 m long here, where the paper says 2 m. Position is stored only when it changes (gaps of up to 66 s), so the value at a frame is the last stored sample.
- Imaging rate. Every file states 5.7981 Hz. For session 21333 that gives 899 s of imaging against 897 s of behaviour. For 21324 and 21322 it gives 680 s against 900 s and 879 s. This is checked next.
- Raw images. One cropped series per file (33 x 46 or 25 x 46 pixels), where the description says "individual cropped ROIs". The emission wavelength is given as 920 nm, the same as the excitation. The subject's age is P60D in all three sessions, where the paper gives 3 to 5 months.
- Depth and coordinates. The plane segmentation has only the columns id, pixel_mask and pixel_mask_index, and the pixel masks have the fields x, y and weight, with no z. The masks run over x from 0 to 10625 and y from 0 to 24 or 32, which fits 231 crops of 46 pixels side by side, so they give no position in the field of view. The imaging plane has origin_coords (0, 0) and grid_spacing (50, 50) um, which are placeholders, and there are no reference images. Nothing in the files gives a cell's depth or layer.
- Other series. Acquisition holds the position and the one cropped image series; the behaviour module holds only the reward and water intervals. There is no lick record.
- Session order. The session ids run 21322, 21324, 21333, but the start times put 21333 first,
two days before the other two. The raw image series all start at 0 s, and the files were all
created on 2023-11-13, so nothing else in the files dates the sessions. This notebook orders
the sessions by their stated start times; if those are wrong, the stand-ins for 1 h and 24 h
in
s6-later-lower-above-chancechange.
Do imaging and behaviour share a clock?¶
The files give no frame timestamps, only a rate. For each session, fit_frame_rate places the
frames at i / rate for rates from 3.5 to 6.5 Hz and scores each rate by the best correlation (over
lags within 5 s) between the mean z-scored dF/F and running speed. Then, as a contrast, running
(speed above 5 cm/s) is decoded from the population with the labels shifted in time, on the stated
and on the fitted clock. With the right clock, decoding peaks near zero shift and falls to chance
far from it.
rates = np.arange(3.5, 6.5, 0.005)
clock, scores, rows = {}, {}, []
for sid in order:
s = sessions[sid]
clock[sid], scores[sid] = inn.fit_frame_rate(s, rates)
rows.append({"session": sid, "stated rate": s.stated_rate, "fitted rate": clock[sid],
"score at stated rate": scores[sid][np.argmin(abs(rates - s.stated_rate))],
"score at fitted rate": scores[sid].max(),
"best rate > 0.1 Hz from fit": rates[abs(rates - clock[sid]) > 0.1][np.argmax(scores[sid][abs(rates - clock[sid]) > 0.1])],
"its score": scores[sid][abs(rates - clock[sid]) > 0.1].max(),
"imaging s at fitted rate": s.dff.shape[0] / clock[sid], "behaviour s": s.pos_t[-1]})
print(pd.DataFrame(rows).set_index("session").to_string())
fig, ax = plt.subplots(figsize=(6, 3))
for sid in order:
ax.plot(rates, scores[sid], label=sid)
ax.axvline(5.7981, color="k", ls="--", lw=1, label="stated")
ax.set_xlabel("Frame rate (Hz)"); ax.set_ylabel("Best r, population vs speed"); ax.legend(fontsize=8)
print(f"Plotted scores range from {min(v.min() for v in scores.values()):.2f} to {max(v.max() for v in scores.values()):.2f}")
stated rate fitted rate score at stated rate score at fitted rate best rate > 0.1 Hz from fit its score imaging s at fitted rate behaviour s session 21333 5.798 5.800 0.795 0.795 5.905 0.508 898.448 897.408 21324 5.798 4.395 0.156 0.723 4.500 0.392 897.611 899.987 21322 5.798 4.385 0.087 0.870 4.490 0.518 899.430 879.099 Plotted scores range from -0.20 to 0.87
shifts = np.arange(-60, 61, 5.0)
rows, curves = [], {}
for sid in order:
s = sessions[sid]
for name, rate in (("stated", s.stated_rate), ("fitted", clock[sid])):
b = inn.behaviour(s, rate)
acc = inn.shifted_label_decoding(s.dff, b["speed"] > inn.RUN_SPEED, b["covered"], rate, shifts)
curves[sid, name] = acc
rows.append({"session": sid, "clock": name, "rate": rate, "accuracy at 0 s": acc[shifts == 0][0],
"best shift (s)": shifts[np.argmax(acc)], "best accuracy": acc.max(),
"mean accuracy at |shift| >= 30 s": acc[abs(shifts) >= 30].mean()})
print("Balanced accuracy of decoding running (> 5 cm/s) from the population; chance is 0.5")
print(pd.DataFrame(rows).to_string(index=False))
fig, axes = plt.subplots(1, 3, figsize=(10, 2.8), sharey=True)
for ax, sid in zip(axes, order):
for name in ("stated", "fitted"):
ax.plot(shifts, curves[sid, name], marker=".", label=name)
ax.axhline(0.5, color="0.7", lw=1); ax.set_title(sid, fontsize=10); ax.set_xlabel("Label shift (s)")
axes[0].set_ylabel("Balanced accuracy"); axes[0].legend(fontsize=8)
print(f"Plotted accuracies range from {min(c.min() for c in curves.values()):.2f} to {max(c.max() for c in curves.values()):.2f}")
Balanced accuracy of decoding running (> 5 cm/s) from the population; chance is 0.5 session clock rate accuracy at 0 s best shift (s) best accuracy mean accuracy at |shift| >= 30 s 21333 stated 5.798 0.905 0.0 0.905 0.498 21333 fitted 5.800 0.904 0.0 0.904 0.498 21324 stated 5.798 0.514 15.0 0.560 0.475 21324 fitted 4.395 0.941 0.0 0.941 0.485 21322 stated 5.798 0.435 20.0 0.606 0.486 21322 fitted 4.385 0.886 -5.0 0.941 0.499 Plotted accuracies range from 0.40 to 0.94
For 21333 the fit recovers the stated rate (5.800 against 5.798 Hz). For 21324 and 21322 the stated rate gives a population-speed correlation of 0.16 and 0.09 and decoding at chance at zero shift (0.51 and 0.44), while the fitted rates, 4.395 and 4.385 Hz, give 0.72 and 0.87 and decoding of 0.94 and 0.89 at zero shift, falling to chance (0.49 and 0.50) beyond 30 s. The best score more than 0.1 Hz from the fitted rate is lower in every session (0.51, 0.39 and 0.52 against 0.80, 0.72 and 0.87), so the fitted rate is unique within the range searched. The fitted rates also make all three sessions about 898 s long (898, 898 and 899 s), as session 21333's stated rate does, which is evidence independent of the neural data. The data are trusted: the rest of the notebook uses the fitted rates, with frames starting at time 0 as the files state. The fit uses the same population-speed relation that some claims test, so the correlation of the population with speed is in part fitted; the stated clock is shown as a variant where it matters. In 21322, imaging continues about 20 s after the position record ends; those frames are left out.
# Behaviour on the fitted clock, and each ROI's velocity correlation and tuning curve, per session.
B, RUN, BINS, TC, R, LAG, IMAGED = {}, {}, {}, {}, {}, {}, {}
for sid in order:
s = sessions[sid]
b = B[sid] = inn.behaviour(s, clock[sid])
IMAGED[sid] = ~np.isnan(s.dff).all(0)
R[sid], LAG[sid] = inn.velocity_correlation(s.dff, b["speed"], clock[sid], b["covered"])
RUN[sid] = b["covered"] & (b["speed"] > inn.RUN_SPEED)
BINS[sid] = inn.position_bins(b["pos"][RUN[sid]], b["length"])
TC[sid] = inn.tuning_curves(s.dff[RUN[sid]], BINS[sid])
TC[sid][~IMAGED[sid]] = np.nan
print(pd.DataFrame({sid: {"running frames": RUN[sid].sum(), "fraction running": RUN[sid][B[sid]["covered"]].mean(),
"laps": B[sid]["lap"].max(), "speed max (m/s)": B[sid]["speed"].max(),
"speed min (m/s)": B[sid]["speed"].min()} for sid in order}).to_string())
21333 21324 21322 running frames 1213.000 2031.000 1161.000 fraction running 0.233 0.515 0.301 laps 9.000 36.000 17.000 speed max (m/s) 0.168 0.299 0.234 speed min (m/s) -0.008 -0.007 -0.010
Repeated deposits and cell identity across sessions¶
The three files are checked for duplicated recordings (identical traces or behaviour), and for whether the ROI with the same index is the same cell in every session, as the dandiset's description implies ("231 ROIs were observed over 3 separate imaging sessions") and as the paper's averaging over sessions requires. The tests compare, for the same ROI in two sessions, mask area, velocity correlation and tuning curve, against the same quantity with ROIs shuffled.
rng = rng_for("identity")
pairs = [(a, b) for i, a in enumerate(order) for b in order[i + 1:]]
rows = []
for a, b in pairs:
both = IMAGED[a] & IMAGED[b]
n = min(len(sessions[a].dff), len(sessions[b].dff))
same_trace = inn.rowwise_corr(sessions[a].dff[:n, both].T, sessions[b].dff[:n, both].T)
pos_r = np.corrcoef(B[a]["pos"][:n], B[b]["pos"][:n])[0, 1]
ma, mb = sessions[a].mask_area[both], sessions[b].mask_area[both]
mask_r = np.corrcoef(ma, mb)[0, 1]
mask_null = [np.corrcoef(ma, rng.permutation(mb))[0, 1] for _ in range(1000)]
ta, tb = TC[a][both], TC[b][both]
tune = np.nanmean(inn.rowwise_corr(ta, tb))
tune_null = [np.nanmean(inn.rowwise_corr(ta, tb[rng.permutation(len(tb))])) for _ in range(200)]
vel_r = np.corrcoef(R[a][both], R[b][both])[0, 1]
vel_null = [np.corrcoef(R[a][both], rng.permutation(R[b][both]))[0, 1] for _ in range(1000)]
rows.append({"pair": f"{a}-{b}", "ROIs in both": both.sum(),
"max |r| same ROI, raw traces": np.nanmax(abs(same_trace)), "r of positions": pos_r,
"mask area r": mask_r, "mask area null 95th": np.percentile(mask_null, 95),
"velocity r, r": vel_r, "velocity r null 95th": np.percentile(vel_null, 95),
"mean tuning r same ROI": tune, "tuning null mean": np.mean(tune_null),
"tuning null 95th": np.percentile(tune_null, 95)})
print(pd.DataFrame(rows).set_index("pair").T.to_string())
pair 21333-21324 21333-21322 21324-21322 ROIs in both 206.000 211.000 214.000 max |r| same ROI, raw traces 0.224 0.190 0.190 r of positions 0.111 0.100 0.015 mask area r 0.134 -0.048 0.214 mask area null 95th 0.118 0.113 0.113 velocity r, r 0.155 0.162 0.410 velocity r null 95th 0.116 0.112 0.111 mean tuning r same ROI 0.035 0.105 0.188 tuning null mean -0.018 0.108 0.147 tuning null 95th 0.026 0.146 0.184
No two sessions duplicate each other: the largest correlation of a ROI's trace with itself in another session is 0.22, and the position traces correlate at 0.11 or less. Identity across sessions is consistent with the data but not shown. The velocity correlation of the same ROI agrees across sessions above the shuffled 95th percentile in all three pairs (0.16, 0.16 and 0.41 against about 0.11). Mask areas agree above it in two pairs of three (0.13 and 0.21 against 0.12 and 0.11, and -0.05 for 21333-21322). Tuning curves agree above it only for 21333-21324 (0.035 against 0.026). The ROI index is taken as the cell, as the dandiset describes; claims that average over sessions are also shown per session.
Cells per mouse (fig1-cells-per-mouse)¶
About 227 interneurons were imaged per mouse (226.6 ± 27.3, mean ± std over 16 mice), and 121.0 ± 28.4 per mouse were assigned a putative subtype.
s0 = sessions[order[0]]
imaged_any = np.any([IMAGED[sid] for sid in order], axis=0)
table = pd.DataFrame({
"ROIs in the files": [len(s0.roi_ids)],
"ROIs imaged in at least one session": [imaged_any.sum()],
"in the six subtypes": [(s0.subtype != "").sum()],
"not labelled UNIDENTIFIED": [(s0.label != "UNIDENTIFIED").sum()],
}, index=["mouse tg19"])
print(table.T.to_string())
print("Paper: 226.6 +/- 27.3 imaged and 121.0 +/- 28.4 assigned per mouse (mean +/- std, 16 mice)")
print(f"Ranges: imaged {226.6 - 27.3:.1f} to {226.6 + 27.3:.1f}, assigned {121.0 - 28.4:.1f} to {121.0 + 28.4:.1f}")
mouse tg19 ROIs in the files 231 ROIs imaged in at least one session 228 in the six subtypes 74 not labelled UNIDENTIFIED 83 Paper: 226.6 +/- 27.3 imaged and 121.0 +/- 28.4 assigned per mouse (mean +/- std, 16 mice) Ranges: imaged 199.3 to 253.9, assigned 92.6 to 149.4
Result: partially reproduced. The 231 ROIs of mouse tg19 are within the paper's spread of 226.6 ± 27.3. The ROIs assigned a subtype, 74 in the six subtypes or 83 including "NOTHING", are below the range of 121.0 ± 28.4 (92.6 to 149.4) under both readings.
Correlation with velocity (fig2c-most-positive)¶
The majority of interneurons were positively correlated with running velocity, and about 10% were negatively correlated, across all subtypes.
Each ROI's correlation is the signed Pearson r of largest absolute value over lags from -5 to +5 s, averaged over the sessions in which it was imaged, as in the paper's methods.
cells = pd.DataFrame({"subtype": s0.subtype, "label": s0.label,
"r": np.nanmean([R[sid] for sid in order], axis=0),
"lag": np.nanmean([LAG[sid] for sid in order], axis=0),
"sessions": np.sum([IMAGED[sid] for sid in order], axis=0)})
cells = cells[cells.sessions > 0]
six = cells[cells.subtype != ""]
def summary(r):
return {"n": len(r), "fraction positive": np.mean(r > 0), "fraction negative": np.mean(r < 0),
"median r": np.median(r)}
rows = {"all imaged ROIs": summary(cells.r), "six subtypes": summary(six.r)}
for sid in order:
rows[f"session {sid}, all"] = summary(R[sid][IMAGED[sid]])
m = IMAGED[sid] & (s0.subtype != "")
rows[f"session {sid}, six subtypes"] = summary(R[sid][m])
print(pd.DataFrame(rows).T.to_string())
print(f"Paper: 156 of 1585 negative ({156 / 1585:.3f}); with a tolerance of 10%, {0.098 * 0.9:.3f} to {0.098 * 1.1:.3f}")
n fraction positive fraction negative median r all imaged ROIs 228.0 0.965 0.035 0.503 six subtypes 74.0 0.905 0.095 0.498 session 21333, all 215.0 0.935 0.065 0.468 session 21333, six subtypes 73.0 0.945 0.055 0.473 session 21324, all 216.0 0.889 0.111 0.485 session 21324, six subtypes 70.0 0.857 0.143 0.507 session 21322, all 224.0 0.911 0.089 0.652 session 21322, six subtypes 73.0 0.836 0.164 0.609 Paper: 156 of 1585 negative (0.098); with a tolerance of 10%, 0.088 to 0.108
# Variants: the smoothing of velocity, and the stated clock.
rows = {}
for label, sigma, use_stated in (("velocity unsmoothed", 0.0, False), ("velocity smoothed 0.5 s (main)", 0.5, False),
("velocity smoothed 1 s", 1.0, False), ("stated clock, 0.5 s", 0.5, True)):
rs = []
for sid in order:
rate = sessions[sid].stated_rate if use_stated else clock[sid]
b = inn.behaviour(sessions[sid], rate, sigma)
rs.append(inn.velocity_correlation(sessions[sid].dff, b["speed"], rate, b["covered"])[0])
r = np.nanmean(rs, axis=0)
sub = s0.subtype != ""
rows[label] = {**summary(r[~np.isnan(r)]),
**{f"negative in {sid}": np.mean(x[~np.isnan(x)] < 0) for sid, x in zip(order, rs)},
"six subtypes, negative": np.mean(r[sub & ~np.isnan(r)] < 0),
**{f"six subtypes, negative in {sid}": np.mean(x[sub & ~np.isnan(x)] < 0) for sid, x in zip(order, rs)}}
print(pd.DataFrame(rows).T.to_string())
fig, ax = plt.subplots(figsize=(5, 3))
ax.hist(cells.r, bins=np.linspace(-1, 1, 41), color="0.4")
ax.axvline(0, color="k", lw=1); ax.set_xlabel("Correlation with velocity"); ax.set_ylabel("ROIs")
print(f"Plotted r ranges from {cells.r.min():.2f} to {cells.r.max():.2f}")
n fraction positive fraction negative median r negative in 21333 negative in 21324 negative in 21322 six subtypes, negative six subtypes, negative in 21333 six subtypes, negative in 21324 six subtypes, negative in 21322 velocity unsmoothed 228.0 0.965 0.035 0.481 0.065 0.111 0.089 0.095 0.055 0.143 0.164 velocity smoothed 0.5 s (main) 228.0 0.965 0.035 0.503 0.065 0.111 0.089 0.095 0.055 0.143 0.164 velocity smoothed 1 s 228.0 0.965 0.035 0.515 0.070 0.111 0.094 0.095 0.055 0.129 0.164 stated clock, 0.5 s 228.0 0.947 0.053 0.199 0.065 0.106 0.281 0.041 0.055 0.071 0.315 Plotted r ranges from -0.27 to 0.79
# The 74 subtyped ROIs: r in each session, and whether its sign changes between sessions.
per_roi = pd.DataFrame({"subtype": s0.subtype, **{sid: R[sid] for sid in order}}).loc[six.index]
per_roi["mean r"] = six.r
signs = np.sign(per_roi[order])
per_roi["sessions"] = signs.notna().sum(axis=1)
per_roi["sign flips"] = (signs.max(axis=1) != signs.min(axis=1)) & (per_roi["sessions"] > 1)
print(per_roi.to_string())
multi = per_roi[per_roi.sessions > 1]
print(f"\nSubtyped ROIs imaged in 2 or more sessions: {len(multi)}; sign of r the same in all: "
f"{(~multi['sign flips']).sum()}; sign changes: {multi['sign flips'].sum()}")
print("Sign changes by subtype:", multi.groupby("subtype")["sign flips"].sum().reindex(inn.ORDER).to_dict())
print(f"Subtyped ROIs negative on average: {(per_roi['mean r'] < 0).sum()}; "
f"negative in at least one session: {(per_roi[order] < 0).any(axis=1).sum()}")
subtype 21333 21324 21322 mean r sessions sign flips
19 PVBC 0.562 0.645 0.511 0.573 3 False
23 PVBC 0.426 0.192 -0.299 0.106 3 True
24 PVBC 0.511 0.605 0.479 0.532 3 False
26 PVBC 0.765 0.719 0.847 0.777 3 False
28 SomC 0.596 0.697 0.606 0.633 3 False
29 SomC 0.654 0.627 0.731 0.671 3 False
34 BiC 0.466 0.550 0.353 0.456 3 False
41 PVBC 0.436 0.560 0.821 0.606 3 False
43 SomC 0.692 0.556 0.757 0.669 3 False
46 SomC 0.360 NaN 0.255 0.308 2 False
53 BiC 0.333 0.580 0.764 0.559 3 False
56 SomC -0.277 0.542 0.514 0.259 3 True
57 IvC/NGFC 0.501 -0.222 0.530 0.270 3 True
58 CCKC 0.190 -0.283 -0.181 -0.091 3 True
59 SomC 0.621 0.503 0.190 0.438 3 False
60 PVBC 0.555 0.489 0.678 0.574 3 False
61 PVBC 0.656 0.677 0.651 0.661 3 False
62 IvC/NGFC 0.705 0.492 0.848 0.682 3 False
64 PVBC 0.494 0.705 0.648 0.615 3 False
65 AAC 0.227 0.623 0.821 0.557 3 False
67 PVBC 0.201 0.518 0.444 0.388 3 False
76 PVBC -0.124 0.551 0.645 0.357 3 True
81 AAC 0.537 0.562 0.739 0.613 3 False
82 SomC 0.565 0.564 0.795 0.641 3 False
85 PVBC 0.232 0.594 0.664 0.497 3 False
88 AAC 0.108 NaN 0.338 0.223 2 False
90 AAC 0.480 0.569 -0.178 0.290 3 True
92 AAC 0.043 0.641 -0.238 0.148 3 True
93 PVBC 0.152 0.569 0.813 0.512 3 False
97 PVBC 0.412 0.511 -0.160 0.255 3 True
98 BiC 0.515 0.557 0.739 0.604 3 False
101 PVBC 0.469 0.610 0.864 0.648 3 False
104 IvC/NGFC 0.591 0.415 0.819 0.609 3 False
105 PVBC 0.669 0.661 0.470 0.600 3 False
107 PVBC 0.674 0.536 0.473 0.561 3 False
108 PVBC 0.684 0.620 0.747 0.684 3 False
110 IvC/NGFC 0.144 0.610 0.800 0.518 3 False
111 PVBC 0.617 0.559 0.819 0.665 3 False
112 AAC 0.627 0.418 0.850 0.632 3 False
117 PVBC 0.581 0.086 0.687 0.451 3 False
121 PVBC 0.249 0.547 0.390 0.395 3 False
122 PVBC 0.640 0.644 0.691 0.658 3 False
123 AAC 0.660 0.435 0.790 0.628 3 False
125 CCKC 0.473 0.310 0.575 0.453 3 False
126 SomC 0.522 0.457 0.295 0.425 3 False
131 AAC 0.165 0.624 0.706 0.499 3 False
134 IvC/NGFC 0.734 0.424 0.671 0.610 3 False
136 PVBC 0.182 0.464 -0.149 0.166 3 True
138 AAC 0.155 0.541 0.456 0.384 3 False
145 CCKC 0.629 NaN 0.605 0.617 2 False
146 CCKC -0.087 0.344 NaN 0.129 2 True
147 CCKC 0.206 0.641 0.579 0.475 3 False
148 CCKC 0.575 -0.082 0.759 0.417 3 True
149 PVBC 0.328 0.380 0.526 0.411 3 False
154 PVBC 0.417 0.433 0.738 0.529 3 False
158 IvC/NGFC 0.653 0.487 0.590 0.577 3 False
160 CCKC 0.243 -0.555 -0.364 -0.225 3 True
162 IvC/NGFC 0.586 0.199 0.642 0.476 3 False
164 IvC/NGFC 0.327 0.496 0.628 0.484 3 False
165 CCKC 0.108 -0.257 -0.100 -0.083 3 True
167 IvC/NGFC 0.405 0.498 0.617 0.507 3 False
170 CCKC 0.316 -0.454 -0.216 -0.118 3 True
171 IvC/NGFC 0.451 -0.058 0.209 0.200 3 True
175 CCKC 0.312 0.379 0.609 0.433 3 False
176 CCKC 0.262 0.393 0.647 0.434 3 False
178 CCKC 0.462 0.395 0.640 0.499 3 False
179 CCKC -0.334 -0.323 -0.165 -0.274 3 False
180 SomC 0.574 0.475 0.383 0.477 3 False
182 CCKC 0.559 -0.654 -0.504 -0.200 3 True
183 CCKC 0.526 0.484 0.695 0.569 3 False
184 SomC 0.403 -0.308 -0.271 -0.059 3 True
197 IvC/NGFC 0.609 NaN 0.165 0.387 2 False
211 IvC/NGFC 0.730 0.362 0.519 0.537 3 False
224 IvC/NGFC NaN 0.718 0.866 0.792 2 False
Subtyped ROIs imaged in 2 or more sessions: 74; sign of r the same in all: 57; sign changes: 17
Sign changes by subtype: {'PVBC': 4, 'SomC': 2, 'BiC': 0, 'AAC': 2, 'CCKC': 7, 'IvC/NGFC': 2}
Subtyped ROIs negative on average: 7; negative in at least one session: 18
Result: reproduced. Over the sessions, 96.5% of the 228 imaged ROIs and 90.5% of the 74 ROIs in the six subtypes are positively correlated with velocity. The fraction negative is 0.095 among the six subtypes, inside the tolerance of 0.088 to 0.108, and 0.035 among all imaged ROIs, outside it. The six-subtype reading passes only because the correlations are averaged over sessions: per session its fraction negative is 0.055, 0.143 and 0.164, all outside the range. Per session the fraction negative among all ROIs is 0.065, 0.111 and 0.089, so that reading is in range in session 21322 only. The sign of r changes between sessions in 17 of the 74 subtyped ROIs (7 of the 15 CCKC); 18 are negative in at least one session and 7 on average. One ROI more or fewer among the 7 would move the result out of range. Smoothing velocity over 0 or 1 s changes the averaged fractions by no more than 0.005 under either reading, and a per-session fraction by at most 0.014. On the stated clock, 28% of all ROIs in session 21322 are negatively correlated, and 0.041 of the six subtypes on average.
Negative correlations in CCKC (fig2c-cck-negative)¶
Negatively correlated cells were significantly enriched in CCKC, where nearly half of cells were more active during immobility.
by_type = six.groupby("subtype").r.agg(n="count", mean="mean", median="median",
fraction_negative=lambda x: np.mean(x < 0)).reindex(inn.ORDER)
print(by_type.to_string())
per_session = pd.DataFrame({sid: pd.Series(R[sid][IMAGED[sid] & (s0.subtype != "")],
index=s0.subtype[IMAGED[sid] & (s0.subtype != "")])
.groupby(level=0).apply(lambda x: np.mean(x < 0)).reindex(inn.ORDER) for sid in order})
print("\nFraction negative per session")
print(per_session.to_string())
cck = six.subtype == "CCKC"
table = [[(six.r[cck] < 0).sum(), (six.r[cck] >= 0).sum()], [(six.r[~cck] < 0).sum(), (six.r[~cck] >= 0).sum()]]
odds, p = stats.fisher_exact(table, alternative="greater")
print(f"\nCCKC negative {table[0][0]} of {sum(table[0])}; other five subtypes {table[1][0]} of {sum(table[1])}; "
f"one-sided Fisher exact p = {p:.2g}, odds ratio {odds:.1f}")
n mean median fraction_negative
subtype
PVBC 24 0.509 0.546 0.0
SomC 10 0.446 0.458 0.1
BiC 3 0.540 0.559 0.0
AAC 9 0.441 0.499 0.0
CCKC 15 0.202 0.417 0.4
IvC/NGFC 13 0.511 0.518 0.0
Fraction negative per session
21333 21324 21322
PVBC 0.042 0.000 0.125
SomC 0.100 0.111 0.100
BiC 0.000 0.000 0.000
AAC 0.000 0.000 0.222
CCKC 0.133 0.500 0.429
IvC/NGFC 0.000 0.167 0.000
CCKC negative 6 of 15; other five subtypes 1 of 59; one-sided Fisher exact p = 0.00017, odds ratio 38.7
Result: reproduced. 6 of 15 CCKC (0.40) are negatively correlated, against 1 of 59 cells in the other five subtypes (one-sided Fisher exact p = 0.00017). By session the CCKC fraction is 0.13, 0.50 and 0.43.
Differences between subtypes (fig2c-subtypes-differ)¶
The correlation with velocity differs between the six subtypes (one-way ANOVA, p < 1e-10).
groups = [six.r[six.subtype == t].to_numpy() for t in inn.ORDER]
F, p = stats.f_oneway(*groups)
print(f"One-way ANOVA across the six subtypes: F = {F:.2f}, p = {p:.2g} (n = {[len(g) for g in groups]})")
for sid in order:
m = IMAGED[sid] & (s0.subtype != "")
g = [R[sid][m & (s0.subtype == t)] for t in inn.ORDER]
print(f" session {sid}: F = {stats.f_oneway(*g)[0]:.2f}, p = {stats.f_oneway(*g)[1]:.2g}")
tk = stats.tukey_hsd(*groups)
rows = [{"pair": f"{inn.ORDER[i]} - {inn.ORDER[j]}", "difference": tk.statistic[i, j], "p": tk.pvalue[i, j]}
for i in range(6) for j in range(i + 1, 6)]
print("\nTukey's test")
print(pd.DataFrame(rows).to_string(index=False))
One-way ANOVA across the six subtypes: F = 4.41, p = 0.0015 (n = [24, 10, 3, 9, 15, 13]) session 21333: F = 1.90, p = 0.11 session 21324: F = 7.63, p = 1.1e-05 session 21322: F = 1.77, p = 0.13
Tukey's test
pair difference p
PVBC - SomC 0.063 9.726e-01
PVBC - BiC -0.030 9.999e-01
PVBC - AAC 0.068 9.680e-01
PVBC - CCKC 0.307 8.673e-04
PVBC - IvC/NGFC -0.002 1.000e+00
SomC - BiC -0.093 9.867e-01
SomC - AAC 0.005 1.000e+00
SomC - CCKC 0.244 8.165e-02
SomC - IvC/NGFC -0.065 9.806e-01
BiC - AAC 0.098 9.843e-01
BiC - CCKC 0.337 1.574e-01
BiC - IvC/NGFC 0.028 1.000e+00
AAC - CCKC 0.239 1.126e-01
AAC - IvC/NGFC -0.070 9.767e-01
CCKC - IvC/NGFC -0.309 5.077e-03
Result: reproduced. The one-way ANOVA across the six subtypes gives p = 0.0015. By Tukey's test the difference lies with CCKC, lower than PVBC (p = 0.0009) and IvC/NGFC (p = 0.005). Within single sessions the ANOVA is significant in one of three (21324, p = 1.1e-05).
IvC/NGFC and locomotion (fig2c-ngfc-less-modulated)¶
IvC/NGFC were less modulated by locomotion than other subtypes.
ngfc = six.r[six.subtype == "IvC/NGFC"]
others = six.r[six.subtype.isin(["PVBC", "SomC", "BiC", "AAC"])]
rows = []
for name, f in (("r", lambda x: x), ("|r|", np.abs)):
p = stats.mannwhitneyu(f(ngfc), f(others), alternative="less").pvalue
rows.append({"reading": name, "IvC/NGFC mean": f(ngfc).mean(), "IvC/NGFC n": len(ngfc),
"PVBC, SomC, BiC, AAC mean": f(others).mean(), "n": len(others), "one-sided Mann-Whitney p": p})
for sid in order:
m = IMAGED[sid]
a = f(R[sid][m & (s0.subtype == "IvC/NGFC")])
o = f(R[sid][m & np.isin(s0.subtype, ["PVBC", "SomC", "BiC", "AAC"])])
rows.append({"reading": f"{name}, session {sid}", "IvC/NGFC mean": a.mean(), "IvC/NGFC n": len(a),
"PVBC, SomC, BiC, AAC mean": o.mean(), "n": len(o),
"one-sided Mann-Whitney p": stats.mannwhitneyu(a, o, alternative="less").pvalue})
print(pd.DataFrame(rows).to_string(index=False))
reading IvC/NGFC mean IvC/NGFC n PVBC, SomC, BiC, AAC mean n one-sided Mann-Whitney p
r 0.511 13 0.484 46 0.576
r, session 21333 0.536 12 0.431 46 0.921
r, session 21324 0.368 12 0.525 44 0.005
r, session 21322 0.608 13 0.509 46 0.759
|r| 0.511 13 0.487 46 0.576
|r|, session 21333 0.536 12 0.448 46 0.918
|r|, session 21324 0.415 12 0.539 44 0.005
|r|, session 21322 0.608 13 0.565 46 0.718
Result: not reproduced. Averaged over sessions, IvC/NGFC have a mean correlation of 0.511 against 0.484 for PVBC, SomC, BiC and AAC (one-sided p = 0.58), and the same holds for the absolute correlation. Only session 21324 shows the effect (0.368 against 0.525, p = 0.005).
Lags (s2-lags)¶
The activity of positively correlated cells followed changes in velocity by up to several seconds, while the activity of negatively correlated cells was more tightly linked in time.
def lag_test(r, lag):
pos, neg = lag[r > 0], lag[r < 0]
return {"positive n": len(pos), "positive median lag (s)": np.median(pos),
"positive median |lag| (s)": np.median(abs(pos)), "negative n": len(neg),
"negative median lag (s)": np.median(neg), "negative median |lag| (s)": np.median(abs(neg)),
"one-sided Mann-Whitney p (|lag| neg < pos)": stats.mannwhitneyu(abs(neg), abs(pos), alternative="less").pvalue}
rows = {"all imaged ROIs, averaged over sessions": lag_test(cells.r.to_numpy(), cells.lag.to_numpy()),
"six subtypes, averaged over sessions": lag_test(six.r.to_numpy(), six.lag.to_numpy())}
consistent = [i for i in cells.index if np.unique(np.sign([R[sid][i] for sid in order if IMAGED[sid][i]])).size == 1]
rows["all imaged ROIs, same sign of r in every session"] = lag_test(cells.r[consistent].to_numpy(), cells.lag[consistent].to_numpy())
for sid in order:
rows[f"session {sid}"] = lag_test(R[sid][IMAGED[sid]], LAG[sid][IMAGED[sid]])
print(pd.DataFrame(rows).T.to_string())
print("Negative lag: activity lags velocity.")
fig, ax = plt.subplots(figsize=(5, 3))
ax.scatter(cells.lag, cells.r, s=10, color="0.3")
ax.axhline(0, color="0.7", lw=1); ax.axvline(0, color="0.7", lw=1)
ax.set_xlabel("Lag of peak correlation (s)"); ax.set_ylabel("Correlation with velocity")
print(f"Plotted lags range from {cells.lag.min():.2f} to {cells.lag.max():.2f} s")
positive n positive median lag (s) positive median |lag| (s) negative n negative median lag (s) negative median |lag| (s) one-sided Mann-Whitney p (|lag| neg < pos) all imaged ROIs, averaged over sessions 220.0 -1.674 1.674 8.0 -2.063 2.063 0.596 six subtypes, averaged over sessions 67.0 -1.848 1.848 7.0 -1.989 1.989 0.379 all imaged ROIs, same sign of r in every session 185.0 -1.583 1.583 1.0 -2.137 2.137 0.755 session 21333 201.0 -1.207 1.207 14.0 -1.207 1.379 0.816 session 21324 192.0 -0.910 0.910 24.0 -1.479 1.934 0.995 session 21322 204.0 -2.509 2.509 20.0 -3.991 4.561 0.993 Negative lag: activity lags velocity. Plotted lags range from -4.81 to 1.15 s
Result: not reproduced. Positively correlated ROIs lag velocity, with a median lag of -1.67 s, as the paper says. The 8 negatively correlated ROIs have a larger median absolute lag (2.06 s), not a smaller one (one-sided p = 0.60), and so do the negatively correlated ROIs of each session taken alone. Keeping only ROIs whose r has the same sign in every session leaves 1 negatively correlated ROI, too few to test. The lags depend on frames starting at time 0, which the files state but which could not be checked independently.
Run-stop responses (s2-cck-run-stop)¶
Run-start and run-stop responses show that immobility-triggered activation is specific to CCKC.
Events are crossings of 0.2 cm/s separated by at least 5 s from the previous event; the response is the mean dF/F in the 3 s after the event minus the 3 s before.
def run_responses(separation):
start, stop, counts = [], [], {}
for sid in order:
s, b = sessions[sid], B[sid]
ev = inn.run_events(b["speed"], clock[sid], b["covered"], separation)
counts[sid] = {k: len(v) for k, v in ev.items()}
start.append(inn.event_response(s.dff, ev["start"], clock[sid]))
stop.append(inn.event_response(s.dff, ev["stop"], clock[sid]))
return np.nanmean(start, axis=0), np.nanmean(stop, axis=0), counts
rows, events = [], {}
for sep in (3.0, 5.0, 10.0):
start, stop, events[sep] = run_responses(sep)
df = pd.DataFrame({"subtype": s0.subtype, "start": start, "stop": stop}).loc[cells.index]
df = df[df.subtype != ""]
c, o = df.stop[df.subtype == "CCKC"].dropna(), df.stop[df.subtype != "CCKC"].dropna()
means = df.groupby("subtype")[["start", "stop"]].mean().reindex(inn.ORDER)
rows.append({"separation (s)": sep, **{f"stop {t}": means.loc[t, "stop"] for t in inn.ORDER},
"CCKC n": len(c), "others n": len(o),
"one-sided Mann-Whitney p (CCKC stop > others)": stats.mannwhitneyu(c, o, alternative="greater").pvalue})
if sep == 5.0:
main = means
print("Events kept per session:", events)
print("\nMean run-start and run-stop responses (dF/F, 3 s after minus 3 s before), separation 5 s")
print(main.to_string())
start, stop, _ = run_responses(5.0)
neg_cck = cells.index[(cells.subtype == "CCKC") & (cells.r < 0)]
pos_cck = cells.index[(cells.subtype == "CCKC") & (cells.r > 0)]
print(f"\nCCKC run-stop response (separation 5 s): negatively correlated ({len(neg_cck)}) "
f"{np.nanmean(stop[neg_cck]):.3f}, positively correlated ({len(pos_cck)}) {np.nanmean(stop[pos_cck]):.3f}")
per_sess = {}
for sid in order:
s, b = sessions[sid], B[sid]
ev = inn.run_events(b["speed"], clock[sid], b["covered"], 5.0)
d = pd.DataFrame({"subtype": s0.subtype, "r": R[sid], "stop": inn.event_response(s.dff, ev["stop"], clock[sid])})
d = d[IMAGED[sid] & (s0.subtype != "")]
c = d[d.subtype == "CCKC"]
per_sess[sid] = {**d.groupby("subtype").stop.mean().reindex(inn.ORDER).to_dict(),
"CCKC n": len(c), "CCKC rising": int((c.stop > 0).sum()),
"CCKC negatively correlated, n": int((c.r < 0).sum()),
"CCKC negatively correlated, mean": c.stop[c.r < 0].mean(), "stop events": len(ev["stop"])}
print("\nRun-stop responses per session (separation 5 s)")
print(pd.DataFrame(per_sess).to_string())
print("\nRun-stop responses by separation")
print(pd.DataFrame(rows).set_index("separation (s)").T.to_string())
Events kept per session: {3.0: {'21333': {'start': 29, 'stop': 32}, '21324': {'start': 22, 'stop': 25}, '21322': {'start': 16, 'stop': 17}}, 5.0: {'21333': {'start': 24, 'stop': 29}, '21324': {'start': 22, 'stop': 25}, '21322': {'start': 16, 'stop': 16}}, 10.0: {'21333': {'start': 22, 'stop': 21}, '21324': {'start': 11, 'stop': 22}, '21322': {'start': 15, 'stop': 16}}}
Mean run-start and run-stop responses (dF/F, 3 s after minus 3 s before), separation 5 s
start stop
subtype
PVBC 0.041 -0.083
SomC 0.034 -0.063
BiC 0.036 -0.081
AAC 0.057 -0.099
CCKC 0.011 -0.003
IvC/NGFC 0.058 -0.086
CCKC run-stop response (separation 5 s): negatively correlated (6) 0.084, positively correlated (9) -0.061
Run-stop responses per session (separation 5 s)
21333 21324 21322
PVBC -0.037 -0.127 -0.085
SomC -0.051 -0.086 -0.061
BiC -0.043 -0.135 -0.067
AAC -0.021 -0.173 -0.120
CCKC -0.005 0.024 -0.023
IvC/NGFC -0.077 -0.064 -0.110
CCKC n 15.000 14.000 14.000
CCKC rising 5.000 7.000 5.000
CCKC negatively correlated, n 2.000 7.000 6.000
CCKC negatively correlated, mean 0.019 0.123 0.102
stop events 29.000 25.000 16.000
Run-stop responses by separation
separation (s) 3.0 5.0 10.0
stop PVBC -7.972e-02 -8.282e-02 -8.419e-02
stop SomC -5.982e-02 -6.347e-02 -6.570e-02
stop BiC -7.747e-02 -8.149e-02 -8.357e-02
stop AAC -9.520e-02 -9.875e-02 -9.581e-02
stop CCKC -2.683e-03 -3.381e-03 -1.549e-03
stop IvC/NGFC -7.814e-02 -8.624e-02 -8.550e-02
CCKC n 1.500e+01 1.500e+01 1.500e+01
others n 5.900e+01 5.900e+01 5.900e+01
one-sided Mann-Whitney p (CCKC stop > others) 8.267e-04 8.655e-04 8.655e-04
Result: partially reproduced. At run stops every other subtype drops by 0.06 to 0.10 dF/F, while CCKC stay flat on average (-0.003), a significant difference (one-sided p = 0.0009, the same at separations of 3 and 10 s). The criterion also asks that CCKC rise on average, and they do not. Only the 6 negatively correlated CCKC rise (0.084, against -0.061 for the other 9; shown above, not part of the criterion). By session the mean CCKC run-stop response is -0.005, 0.024 and -0.023, while every other subtype falls in every session (-0.021 to -0.173); the CCKC that are negatively correlated in a session rise in all three (0.019, 0.123 and 0.102).
Spatially selective interneurons (fig4-fraction-spatial)¶
17.5 ± 6.4% of interneurons, blind of subtype, passed the test for significant spatial selectivity.
The paper's rule, on frames with speed above 5 cm/s: tuning curves of 100 bins smoothed with a Gaussian of 3 bins; 1000 shuffles rotating position against dF/F; a field is 10 consecutive bins above the shuffles' 95th percentile, with a peak above 100% dF/F. Fields may wrap around the end of the belt. As a null, the same procedure is run once per session on a surrogate in which dF/F is first rotated against the behaviour by at least 60 s.
rng = rng_for("fig4-fraction-spatial")
BOUNDS, rows = {}, []
for sid in order:
s, m = sessions[sid], IMAGED[sid]
BOUNDS[sid] = inn.shuffle_bounds(s.dff[RUN[sid]][:, m], BINS[sid], rng)
# The same procedure on a surrogate: dF/F rotated against the behaviour by at least 60 s first.
shift = rng.integers(int(60 * clock[sid]), RUN[sid].sum() - int(60 * clock[sid]))
surrogate = inn.shuffle_bounds(np.roll(s.dff[RUN[sid]][:, m], shift, axis=0), BINS[sid], rng)
for name, kw in (("peak > 100% dF/F", {}), ("no peak rule", {"peak": None}),
("peak > 50% dF/F", {"peak": 0.5}), ("no peak rule, no wrap", {"peak": None, "wrap": False}),
("no peak rule, 15 bins", {"peak": None, "field_bins": 15})):
f, fs = inn.place_fields(BOUNDS[sid], **kw), inn.place_fields(surrogate, **kw)
rows.append({"session": sid, "rule": name, "ROIs": m.sum(), "positive": f["positive"].mean(),
"negative": f["negative"].mean(), "positive, surrogate": fs["positive"].mean(),
"negative, surrogate": fs["negative"].mean()})
spatial = pd.DataFrame(rows)
print(spatial.to_string(index=False))
print("\nMean over the three sessions")
print(spatial.groupby("rule", sort=False)[["positive", "negative", "positive, surrogate", "negative, surrogate"]].mean().to_string())
print("Paper: positive 0.175 +/- 0.064, negative 0.127 +/- 0.083 (mean +/- sem)")
peaks = np.concatenate([np.nanmax(BOUNDS[sid]["tc"], 1) for sid in order])
print(f"\nPeak of the session tuning curve per ROI: median {np.median(peaks):.2f}, 90th percentile "
f"{np.percentile(peaks, 90):.2f}, max {peaks.max():.2f} dF/F; above 1.0 in {np.mean(peaks > 1):.3f} of ROIs")
session rule ROIs positive negative positive, surrogate negative, surrogate
21333 peak > 100% dF/F 215 0.005 0.470 0.000 0.079
21333 no peak rule 215 0.549 0.470 0.056 0.079
21333 peak > 50% dF/F 215 0.065 0.470 0.009 0.079
21333 no peak rule, no wrap 215 0.488 0.437 0.047 0.079
21333 no peak rule, 15 bins 215 0.307 0.181 0.005 0.005
21324 peak > 100% dF/F 216 0.000 0.745 0.000 0.208
21324 no peak rule 216 0.750 0.745 0.162 0.208
21324 peak > 50% dF/F 216 0.083 0.745 0.023 0.208
21324 no peak rule, no wrap 216 0.741 0.727 0.139 0.208
21324 no peak rule, 15 bins 216 0.565 0.565 0.069 0.060
21322 peak > 100% dF/F 224 0.027 0.777 0.000 0.036
21322 no peak rule 224 0.795 0.777 0.040 0.036
21322 peak > 50% dF/F 224 0.241 0.777 0.004 0.036
21322 no peak rule, no wrap 224 0.750 0.777 0.040 0.031
21322 no peak rule, 15 bins 224 0.621 0.603 0.009 0.004
Mean over the three sessions
positive negative positive, surrogate negative, surrogate
rule
peak > 100% dF/F 0.010 0.664 0.000 0.108
no peak rule 0.698 0.664 0.086 0.108
peak > 50% dF/F 0.130 0.664 0.012 0.108
no peak rule, no wrap 0.660 0.647 0.075 0.106
no peak rule, 15 bins 0.497 0.450 0.028 0.023
Paper: positive 0.175 +/- 0.064, negative 0.127 +/- 0.083 (mean +/- sem)
Peak of the session tuning curve per ROI: median 0.26, 90th percentile 0.57, max 1.60 dF/F; above 1.0 in 0.011 of ROIs
Result: not reproduced. With the paper's peak rule, 1.0% of ROIs are selected on average over the sessions, because only 1.1% of ROIs have a tuning curve that reaches 1.0 dF/F. Without the peak rule, 69.8% are selected, against 8.6% on the surrogate. Neither is within 17.5 ± 6.4%. A peak rule of 50% dF/F gives 13.0%, inside the range, but that threshold is not the paper's.
print(spatial[spatial.rule == "no peak rule"][["session", "negative", "negative, surrogate"]].to_string(index=False))
print(f"Mean over sessions: {spatial[spatial.rule == 'no peak rule'].negative.mean():.3f}")
# Overlap of positive and negative fields under the rule without the peak (the paper sets no peak for negative fields).
rows = []
for sid in order:
f = inn.place_fields(BOUNDS[sid], peak=None)
fp = inn.place_fields(BOUNDS[sid])
both = f["positive"] & f["negative"]
rows.append({"session": sid, "ROIs": len(both), "positive and negative": both.mean(),
"negative only": (f["negative"] & ~f["positive"]).mean(),
"positive only": (f["positive"] & ~f["negative"]).mean(),
"negative among ROIs positive": f["negative"][f["positive"]].mean(),
"negative among ROIs not positive": f["negative"][~f["positive"]].mean(),
"negative, excluding positive with peak rule": (f["negative"] & ~fp["positive"]).mean()})
overlap = pd.DataFrame(rows).set_index("session")
print("\nOverlap of positive and negative fields (rule without the peak), fraction of imaged ROIs")
print(overlap.to_string())
print("Mean over sessions:")
print(overlap.drop(columns="ROIs").mean().to_string())
# The coupling on synthetic cells: noise around a baseline of 0.12 dF/F on the real position of the first session,
# with a field of 10 bins added (positive) or subtracted (negative) in one cell each, and one cell without a field.
rng = rng_for("fig4-fraction-negative")
bins = BINS[order[0]]
in_field = (bins >= 40) & (bins < 50)
syn = 0.12 + 0.05 * rng.standard_normal((len(bins), 3))
syn[in_field, 0] += 1.0
syn[in_field, 1] -= 0.1
b = inn.shuffle_bounds(syn, bins, rng)
print("\nSynthetic cells")
for name, kw in (("rule without the peak", {"peak": None}), ("peak > 100% dF/F", {})):
f = inn.place_fields(b, **kw)
print(pd.DataFrame({"positive": f["positive"], "negative": f["negative"]},
index=["positive field only", "negative field only", "no field"]).to_string(), f"({name})")
session negative negative, surrogate
21333 0.470 0.079
21324 0.745 0.208
21322 0.777 0.036
Mean over sessions: 0.664
Overlap of positive and negative fields (rule without the peak), fraction of imaged ROIs
ROIs positive and negative negative only positive only negative among ROIs positive negative among ROIs not positive negative, excluding positive with peak rule
session
21333 215 0.349 0.121 0.200 0.636 0.268 0.465
21324 216 0.644 0.102 0.106 0.858 0.407 0.745
21322 224 0.683 0.094 0.112 0.860 0.457 0.750
Mean over sessions:
positive and negative 0.558
negative only 0.106
positive only 0.139
negative among ROIs positive 0.784
negative among ROIs not positive 0.377
negative, excluding positive with peak rule 0.653
Synthetic cells
positive negative
positive field only True True
negative field only True True
no field False False (rule without the peak)
positive negative
positive field only True True
negative field only False True
no field False False (peak > 100% dF/F)
Result: not reproduced. On average 66.4% of ROIs have a negative field (47.0%, 74.5% and 77.7% by session), against 10.8% on the surrogate and 12.7 ± 8.3% in the paper. Under the positive rule without the peak, 55.8% of ROIs on average have both kinds of field, 10.6% only a negative field and 13.9% only a positive one. With rotated shuffles a strong field raises the shuffled curves at every bin, so the two kinds of field are coupled: on synthetic cells, a cell with only a positive field is also flagged negative, and a cell with only a negative field is flagged positive under the rule without the peak. The coupling explains part of the 66.4%, not all of it. A negative field is found in 78.4% of ROIs with a positive field, but also in 37.7% of ROIs without one, about 3.5 times the surrogate rate. Counting only ROIs with a negative field and no positive field gives 10.6%, inside the paper's range, but that variant depends on the positive rule without the peak (with the peak rule it gives 65.3%), it excludes real negative cells that are flagged positive, and it was chosen after the result, so it does not change the outcome.
Position decoding (fig4d-decoding)¶
Position decoded from the small subset of spatially selective interneurons was about as accurate as from all interneurons, and more accurate than from as many randomly chosen interneurons or with position shuffled.
A linear SVM decodes the position bin from lap-wise tuning curves, leaving one lap out. The selected cells follow the peak rule where it gives at least 5 cells (only in 21322) and otherwise the rule without the peak. Shuffled position is dF/F rotated against position by at least 60 s (three draws); random subsets are ten draws of the same size.
rng = rng_for("fig4d-decoding")
SEL, rows = {}, []
for sid in order:
s, m = sessions[sid], IMAGED[sid]
peak = inn.place_fields(BOUNDS[sid])["positive"]
nopeak = inn.place_fields(BOUNDS[sid], peak=None)["positive"]
rule = "peak > 100% dF/F" if peak.sum() >= 5 else "no peak rule"
sel = peak if peak.sum() >= 5 else nopeak
SEL[sid] = sel
X = s.dff[RUN[sid]][:, m]
laps = B[sid]["lap"][RUN[sid]]
Xl, y, g = inn.lap_tuning(X, BINS[sid], laps)
err = lambda cols: inn.decode_position(Xl[:, cols], y, g, B[sid]["length"])
e_all, e_sel = err(np.arange(m.sum())), err(np.flatnonzero(sel))
e_rand = [err(rng.choice(m.sum(), sel.sum(), replace=False)) for _ in range(10)]
e_shuf = []
for _ in range(3):
Xs = np.roll(X[:, sel], rng.integers(int(60 * clock[sid]), len(X) - int(60 * clock[sid])), axis=0)
e_shuf.append(inn.decode_position(*inn.lap_tuning(Xs, BINS[sid], laps), B[sid]["length"]))
if peak.sum() >= 1:
e_peak = err(np.flatnonzero(peak))
else:
e_peak = np.nan
rows.append({"session": sid, "laps": len(np.unique(g)), "rule": rule, "ROIs": m.sum(), "selected": sel.sum(),
"selected with peak rule": peak.sum(), "error all (cm)": e_all, "error selected": e_sel,
"error peak-rule cells": e_peak, "error random, mean": np.mean(e_rand),
"error random, min": np.min(e_rand), "error shuffled, mean": np.mean(e_shuf),
"chance (cm)": B[sid]["length"] * 100 / 4})
dec = pd.DataFrame(rows).set_index("session")
dec["selected / all"] = dec["error selected"] / dec["error all (cm)"]
dec["holds"] = (dec["selected / all"] <= 1.25) & (dec["error selected"] < dec["error random, mean"]) & \
(dec["error selected"] < dec["error shuffled, mean"])
print(dec.T.to_string())
print("Chance is the mean error of a uniform guess around a loop, a quarter of the belt.")
session 21333 21324 21322 laps 10 37 18 rule no peak rule no peak rule peak > 100% dF/F ROIs 215 216 224 selected 118 162 6 selected with peak rule 1 0 6 error all (cm) 21.127 13.219 15.363 error selected 25.212 13.845 30.329 error peak-rule cells 43.838 NaN 30.329 error random, mean 24.835 14.441 31.824 error random, min 19.926 13.759 25.03 error shuffled, mean 53.386 48.674 51.802 chance (cm) 52.738 52.738 52.738 selected / all 1.193 1.047 1.974 holds False True False Chance is the mean error of a uniform guess around a loop, a quarter of the belt.
Result: not reproduced. The criterion holds in one session of three. In 21324 the 162 selected ROIs decode with an error of 13.8 cm, against 13.2 cm from all ROIs, 14.4 cm from random subsets and 48.7 cm with position shuffled. In 21333 the selected ROIs do no better than random subsets (25.2 against 24.8 cm). In 21322 the 6 ROIs of the peak rule give twice the error of all ROIs (30.3 against 15.4 cm). In every session shuffling position brings the error to about chance (49 to 53 cm, chance 52.7 cm), and all ROIs decode position well (13 to 21 cm).
Selectivity index (fig4f-selectivity-no-difference)¶
The selectivity index of spatially selective cells did not differ between subtypes (one-way ANOVA, p = 0.35).
The cells are those selected for the decoding above. fig4e-bic-recurrence is blocked: the
dandiset has 3 BiC.
si = np.full((len(order), len(s0.roi_ids)), np.nan)
for k, sid in enumerate(order):
m = np.flatnonzero(IMAGED[sid])
idx = m[SEL[sid]]
si[k, idx] = inn.sparsity_index(BOUNDS[sid]["tc"][SEL[sid]])
sel_cells = pd.DataFrame({"subtype": s0.subtype, "si": np.nanmean(si, 0), "times selected": (~np.isnan(si)).sum(0)})
sel_cells = sel_cells[(sel_cells.subtype != "") & (sel_cells["times selected"] > 0)]
print(sel_cells.groupby("subtype").si.agg(["count", "mean", "median"]).reindex(inn.ORDER).to_string())
use = [t for t in inn.ORDER if (sel_cells.subtype == t).sum() >= 3]
F, p = stats.f_oneway(*[sel_cells.si[sel_cells.subtype == t] for t in use])
print(f"\nOne-way ANOVA over {use}: F = {F:.2f}, p = {p:.2g}")
print(f"Effect size: subtype means range from {sel_cells.groupby('subtype').si.mean()[use].min():.3f} "
f"to {sel_cells.groupby('subtype').si.mean()[use].max():.3f}; overall SD {sel_cells.si.std():.3f}")
# The subtyped selected cells, one tuning curve per cell and session.
tc_sel = np.concatenate([BOUNDS[sid]["tc"][SEL[sid] & (s0.subtype[IMAGED[sid]] != "")] for sid in order])
base = np.nanmin(tc_sel, 1)
print(f"Tuning curves of the selected cells: minimum over bins, median {np.median(base):.3f}; "
f"mean over bins, median {np.median(np.nanmean(tc_sel, 1)):.3f}; maximum, median {np.median(np.nanmax(tc_sel, 1)):.3f} dF/F")
print(f"Selectivity index, median {np.median(inn.sparsity_index(tc_sel)):.3f}; after subtracting each curve's "
f"minimum, median {np.median(inn.sparsity_index(tc_sel - base[:, None])):.3f}")
count mean median subtype PVBC 22 0.020 0.017 SomC 9 0.031 0.026 BiC 3 0.028 0.034 AAC 9 0.012 0.011 CCKC 13 0.020 0.020 IvC/NGFC 12 0.033 0.015 One-way ANOVA over ['PVBC', 'SomC', 'BiC', 'AAC', 'CCKC', 'IvC/NGFC']: F = 1.60, p = 0.17 Effect size: subtype means range from 0.012 to 0.033; overall SD 0.020 Tuning curves of the selected cells: minimum over bins, median 0.158; mean over bins, median 0.205; maximum, median 0.255 dF/F Selectivity index, median 0.016; after subtracting each curve's minimum, median 0.307
Result: reproduced. The one-way ANOVA across the six subtypes gives p = 0.17, so no difference is found. Subtype means range from 0.012 to 0.033, with a standard deviation of 0.020 across cells. The tuning curves of the selected subtyped cells have a median minimum of 0.158 dF/F and a median maximum of 0.255, so they vary little around their level; subtracting each curve's minimum raises the median index from 0.016 to 0.307. The test therefore compares near-flat curves and has little power.
Stability of spatially selective PVBC and SomC (fig4h-pvbc-more-stable)¶
Spatially selective PVBC had a more stable within-session recruitment than spatially selective SomC.
Stability is the correlation between the tuning curves of odd and even laps.
OE = {sid: np.full(len(s0.roi_ids), np.nan) for sid in order}
for sid in order:
m = IMAGED[sid]
OE[sid][m] = inn.odd_even_correlation(sessions[sid].dff[RUN[sid]][:, m], BINS[sid], B[sid]["lap"][RUN[sid]])
oe_sel = np.full((len(order), len(s0.roi_ids)), np.nan)
for k, sid in enumerate(order):
idx = np.flatnonzero(IMAGED[sid])[SEL[sid]]
oe_sel[k, idx] = OE[sid][idx]
per_cell = pd.DataFrame({"subtype": s0.subtype, "r": np.nanmean(oe_sel, 0)}).dropna()
spatial_pv_som = {t: per_cell.r[per_cell.subtype == t].to_numpy() for t in ("PVBC", "SomC")}
print({t: len(v) for t, v in spatial_pv_som.items()}, "spatially selective cells (odd-even r averaged over the sessions in which each was selected)")
if min(len(v) for v in spatial_pv_som.values()) >= 5:
a, b = spatial_pv_som["PVBC"], spatial_pv_som["SomC"]
print(f"PVBC mean {a.mean():.3f}, SomC mean {b.mean():.3f}, difference {a.mean() - b.mean():.3f}, "
f"unpaired t-test p = {stats.ttest_ind(a, b).pvalue:.2g}")
for sid in order:
idx = np.flatnonzero(IMAGED[sid])[SEL[sid]]
v = {t: OE[sid][idx][s0.subtype[idx] == t] for t in ("PVBC", "SomC")}
print(f" session {sid}: PVBC {v['PVBC'].mean():.3f} (n = {len(v['PVBC'])}), SomC {v['SomC'].mean():.3f} (n = {len(v['SomC'])})")
{'PVBC': 22, 'SomC': 9} spatially selective cells (odd-even r averaged over the sessions in which each was selected)
PVBC mean 0.678, SomC mean 0.736, difference -0.059, unpaired t-test p = 0.42
session 21333: PVBC 0.672 (n = 15), SomC 0.693 (n = 5)
session 21324: PVBC 0.683 (n = 17), SomC 0.765 (n = 9)
session 21322: PVBC 0.662 (n = 1), SomC nan (n = 0)
Result: not reproduced. 22 spatially selective PVBC and 9 SomC qualify. PVBC are less stable than SomC, not more (mean odd-even correlation 0.678 against 0.736, unpaired t-test p = 0.42), and the same holds in each session with both subtypes present.
Within-session stability by subtype (s6-within-session-order)¶
Within-session stability of tuning curves differed between subtypes, PVBC showing the highest and CCKC the lowest odd-even correlations.
All imaged ROIs of the six subtypes, regardless of spatial selectivity.
oe = pd.DataFrame({"subtype": s0.subtype, **{sid: OE[sid] for sid in order}})
oe["mean"] = oe[order].mean(axis=1)
oe = oe[(oe.subtype != "") & oe["mean"].notna()]
table = oe.groupby("subtype")[["mean", *order]].mean().reindex(inn.ORDER)
table.insert(0, "n", oe.groupby("subtype").size().reindex(inn.ORDER))
print(table.to_string())
eligible = table[table.n >= 5]
print(f"\nAmong subtypes with at least 5 cells: highest {eligible['mean'].idxmax()}, lowest {eligible['mean'].idxmin()}")
F, p = stats.f_oneway(*[oe["mean"][oe.subtype == t] for t in eligible.index])
print(f"One-way ANOVA across them: F = {F:.2f}, p = {p:.2g}")
n mean 21333 21324 21322 subtype PVBC 24 0.598 0.566 0.573 0.654 SomC 10 0.606 0.377 0.765 0.686 BiC 3 0.490 0.193 0.709 0.568 AAC 9 0.570 0.355 0.777 0.613 CCKC 15 0.500 0.437 0.475 0.586 IvC/NGFC 13 0.593 0.391 0.641 0.728 Among subtypes with at least 5 cells: highest SomC, lowest CCKC One-way ANOVA across them: F = 0.76, p = 0.56
Result: partially reproduced. CCKC have the lowest mean odd-even correlation (0.500). PVBC (0.598) are second to SomC (0.606), not first. The ANOVA across the five subtypes with at least 5 cells gives p = 0.56, so the subtypes do not differ significantly here.
Stability across sessions (s6-later-lower-above-chance)¶
Tuning curve correlations of the same cells after 1 hour and after 24 hours were lower than within-session correlations, and still higher than chance.
Sessions are ordered by their stated start times, which disagree with the order of their ids (see the data check). The same-day pair (21324 then 21322, starts 2.5 h apart) stands in for 1 hour, and the two pairs with 21333 (54 and 56 h) for 24 hours. The ROIs in the six subtypes imaged in both sessions.
rng = rng_for("s6-later-lower-above-chance")
rows = []
for a, b in [(order[1], order[2]), (order[0], order[1]), (order[0], order[2])]:
m = IMAGED[a] & IMAGED[b] & (s0.subtype != "")
across = inn.rowwise_corr(TC[a][m], TC[b][m])
within = OE[a][m]
ok = ~np.isnan(across) & ~np.isnan(within)
obs = across[ok].mean()
idx, types = np.flatnonzero(m)[ok], s0.subtype[m][ok]
null_all, null_type = [], []
for _ in range(1000):
null_all.append(np.nanmean(inn.rowwise_corr(TC[a][idx], TC[b][rng.permutation(idx)])))
perm = idx.copy()
for t in np.unique(types):
perm[types == t] = rng.permutation(idx[types == t])
null_type.append(np.nanmean(inn.rowwise_corr(TC[a][idx], TC[b][perm])))
gap_h = (pd.Timestamp(sessions[b].start_time) - pd.Timestamp(sessions[a].start_time)).total_seconds() / 3600
rows.append({"pair": f"{a}-{b}", "hours between starts": gap_h, "cells": ok.sum(),
"within-session r (earlier)": within[ok].mean(), "across r": obs,
"Wilcoxon p (across < within)": stats.wilcoxon(across[ok], within[ok], alternative="less").pvalue,
"null mean, all shuffled": np.mean(null_all),
"p, all shuffled": (1 + np.sum(np.array(null_all) >= obs)) / 1001,
"null mean, within subtype": np.mean(null_type),
"p, within subtype": (1 + np.sum(np.array(null_type) >= obs)) / 1001})
print(pd.DataFrame(rows).set_index("pair").T.to_string())
pair 21324-21322 21333-21324 21333-21322 hours between starts 2.457e+00 5.383e+01 5.628e+01 cells 6.900e+01 6.900e+01 7.200e+01 within-session r (earlier) 6.148e-01 4.359e-01 4.526e-01 across r 2.176e-01 6.351e-02 1.424e-01 Wilcoxon p (across < within) 4.247e-07 1.143e-06 6.481e-05 null mean, all shuffled 1.403e-01 -1.131e-04 1.464e-01 p, all shuffled 4.695e-02 7.493e-02 5.255e-01 null mean, within subtype 1.605e-01 1.473e-02 1.656e-01 p, within subtype 1.029e-01 1.039e-01 7.253e-01
Result: partially reproduced. Correlations across sessions are lower than within the earlier session in every pair (0.22 against 0.61 for the same-day pair, Wilcoxon p = 4e-07; 0.06 and 0.14 for the two-day pairs). Above chance holds only for the same-day pair with identities shuffled among all cells (p = 0.047), not with shuffles within subtype (p = 0.10), and for neither two-day pair (p = 0.075 and 0.53 with all cells shuffled). As cell identity across sessions is not established, this may reflect the ROIs as much as the cells. The order of the sessions rests on their stated start times, which conflict with the order of their ids.
Summary¶
| Claim | Outcome | Note |
|---|---|---|
fig1-cells-per-mouse |
Partially reproduced | 231 ROIs, in range; 74 or 83 identified, below 92.6 to 149.4 |
fig1-depth-distribution |
Blocked: data missing | No cell depth or coordinates in the files |
fig2c-most-positive |
Reproduced | 0.095 negative among the six subtypes, averaged over sessions; no session in range alone |
fig2c-cck-negative |
Reproduced | 6 of 15 CCKC negative against 1 of 59 others |
fig2c-subtypes-differ |
Reproduced | ANOVA p = 0.0015, driven by CCKC |
fig2c-ngfc-less-modulated |
Not reproduced | IvC/NGFC 0.511 against 0.484, p = 0.58 |
fig2d-negative-cells-in-oriens |
Blocked: data missing | No cell depth |
s2-lags |
Not reproduced | Negatively correlated ROIs not more tightly locked |
s2-cck-run-stop |
Partially reproduced | CCKC flat at run stops while others drop; no rise on average |
s3-subtype-classification |
Blocked: data missing | One mouse of the training set, no depth |
fig3-swr-pvbc-vs-somc, fig3-swr-bimodal |
Blocked: data missing | No LFP or 40 Hz sessions |
fig4-fraction-spatial |
Not reproduced | 1.0% with the peak rule, 69.8% without |
fig4-fraction-negative |
Not reproduced | 66.4%; coupled to positive fields, but 37.7% of ROIs without one |
fig4d-decoding |
Not reproduced | Criterion holds in 1 session of 3 |
fig4e-bic-recurrence |
Blocked: other | 3 BiC |
fig4f-selectivity-no-difference |
Reproduced | ANOVA p = 0.17 |
fig4h-pvbc-more-stable |
Not reproduced | PVBC 0.678, SomC 0.736 |
s6-within-session-order |
Partially reproduced | CCKC lowest; PVBC second to SomC; ANOVA p = 0.56 |
s6-later-lower-above-chance |
Partially reproduced | Lower in every pair; above chance only same-day, one reading |
fig5-context-decorrelation, fig5-novel-context-decrease |
Blocked: data missing | No novel-context sessions |
fig6-reward-somc-bic, fig6-old-zone |
Blocked: data missing | No goal-oriented sessions or licking |
fig7-vip-subtypes-immobility, fig7-isi-correlations |
Blocked: data missing | VIP-Cre and strategy 3 mice not included |
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 this differs from the paper's analysis:
- One mouse with 74 identified interneurons, where the paper pools 6 or 7 mice per analysis.
- The frame rates are fitted from the data for two sessions, as the stated rate does not fit.
- The ROI with the same index is taken as the same cell in every session; the data are consistent with this but do not show it.
- The 1 hour and 24 hour comparisons use sessions 2.5 h and about two days apart.
- Choices the methods leave open: velocity smoothed over 0.5 s, run events at least 5 s apart, place fields allowed to wrap around the belt, a one-vs-one linear SVM, shuffled position made by rotating dF/F against position.
What was found about the dandiset:
- The stated imaging rate, 5.7981 Hz, is wrong for sessions 21324 and 21322, whose frames fit about 4.39 Hz; on the stated clock their activity does not follow running.
- The dF/F unit is given as lumens; the Fluorescence series is a link to the dF/F series, so raw fluorescence is missing; the subtype column's description is a placeholder ("Recording location in Middle Earth"), and the table has one row more than there are ROIs.
- The labels "NOTHING" and "UNIDENTIFIED" are not explained, nor are the reward and water intervals; there is no cell depth, no mapping of ROIs across sessions, and no lick record.
- All three sessions are called "session 1 of 4", and the fourth session is not included. The session ids and the start times give different orders.
- The pixel masks are stored on a strip of the 231 cropped frames side by side, and the imaging plane's origin and grid spacing are placeholders, so ROIs have no position in the field of view.
- The dandiset holds one of the paper's 16 mice and only its random-foraging sessions, without saying which part of the paper it covers.
- With the paper's place-field rule and a peak above 100% dF/F, almost no ROI qualifies, as only 1.1% of tuning curves reach 1.0 dF/F; the dF/F here may be scaled differently from the paper's.