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.

In [1]:
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.

In [2]:
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.

In [3]:
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-chance change.

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.

In [4]:
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
No description has been provided for this image
In [5]:
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
No description has been provided for this image

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.

In [6]:
# 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.

In [7]:
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.

In [8]:
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.

In [9]:
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
In [10]:
# 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
No description has been provided for this image
In [11]:
# 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.

In [12]:
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).

In [13]:
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.

In [14]:
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.

In [15]:
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
No description has been provided for this image

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.

In [16]:
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.

In [17]:
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.

In [18]:
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.

In [19]:
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.

In [20]:
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.

In [21]:
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.

In [22]:
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.

In [23]:
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.