Reproducing Sargolini et al. (2006) from dandiset 000582¶
Paper: Sargolini F, Fyhn M, Hafting T, McNaughton BL, Witter MP, Moser M-B, Moser EI (2006). Conjunctive representation of position, direction, and velocity in entorhinal cortex. Science 312:758-762. doi:10.1126/science.1125572
Data: DANDI:000582, version 0.251111.2151, streamed from the archive. Nothing is downloaded to disk.
Why this version. The paper cites no version of the dandiset. 0.251111.2151 is the only published
version, and the draft (last modified 2026-05-28) holds the same 118 assets, with the same sizes
and asset identifiers, so there is no newer content to test. The earlier attempt,
000582-sargolini-2006-01, used the same version.
Claims. The claims file is claims/000582/10.1126-science.1125572.yaml, written by a reader from
the version of record and its Supporting Online Material (SOM), and checked by a second reader. It
replaces a file written from the abstract only. The three claims of the earlier attempt keep their
ids (layer2-grid, deep-hd-conjunctive, speed) and now carry the paper's numbers and criteria.
This notebook tests the claims file as it stands; where it leaves a detail open, every reading it
names is run. This attempt asked five questions about the claims file (questions/000582.yaml);
a reader answered them on 2026-10-20, the claims file now carries the answers, and this notebook
follows the updated criteria. This version also answers the verifier's first-round report
(transcripts/000582/verifier-1-report.md): it adds the outputs the report asked for, and one
outcome changed (grid-phase-orientation, see its section). It then answers the second-round
report (transcripts/000582/verifier-2-report.md), which agreed with every outcome: it corrects
the conclusions the report quoted, and adds the outputs it asked for. No outcome changes.
| Claim | Tested here |
|---|---|
layer2-grid |
tested |
grid-all-layers |
tested |
gridness-range-similar |
tested |
grid-stable |
tested |
spacing-dorsoventral |
blocked: no distance from the postrhinal border in the dandiset |
fieldsize-dorsoventral |
blocked: no distance from the postrhinal border in the dandiset |
grid-phase-orientation |
tested |
hd-layers |
tested |
hd-with-grid-colocalized |
tested |
hd-tuning-breadth-layers |
tested |
hd-full-range |
tested |
hd-colocalized-distributed |
tested |
hd-stable-trials |
tested |
hd-rates |
tested |
deep-hd-conjunctive |
tested |
gridness-hd-continuous |
tested |
conj-tuning-similar |
tested |
cue-rotation-coherent |
blocked: no file marks a cue-card rotation |
speed |
tested |
speed-slope-steeper |
tested |
speed-intercept |
tested |
The measures follow the SOM as the claims file quotes it, and are defined in sargolini.py
next to this notebook, with the source of each parameter.
Readings. The claims file names readings for details the paper leaves open (O1 to O4): trials pooled or the first trial (O1), 3 cm maps with peaks of 25 bins or 1.5 cm maps with peaks of 100 pixels (O2), the gridness circle to the outermost peak's edge or its centre, and the central peak removed as its pixels or as a disc (O3), with a peak's centre its pixel of highest correlation or its centroid and its radius that of a disc of the same area or the largest distance from its centre to one of its pixels (O3, as answered), and a significance level of 0.05 or 0.01 for the head-direction tests (O4). The peak radius is not used when the circle runs to the outer peak's centre and the central peak is removed as its pixels, so that pair of readings is run once. That gives 56 combinations for grid cells, 4 for head-direction cells and 112 for conjunctive cells; every claim is shown under all of them.
import os
os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import hashlib
import warnings
from collections import Counter
from concurrent.futures import ProcessPoolExecutor
from itertools import combinations, product
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats
import sargolini as sg
warnings.filterwarnings("ignore")
pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 200)
plt.rcParams.update({"figure.dpi": 100, "axes.spines.top": False, "axes.spines.right": False})
LAYERS = ["II", "III", "V", "VI"]
DEEP = ["III", "V", "VI"]
def within(value, expected, tolerance):
return bool(abs(value - expected) <= tolerance + 1e-12)
def outcome_of(outcomes):
"""The outcome all readings give, or methods_underspecified when they differ."""
found = sorted(set(outcomes))
return found[0] if len(found) == 1 else "methods_underspecified (" + ", ".join(found) + ")"
Data check¶
Each file is one recording: the positions of one or two tracking LEDs and the spike times of the cells recorded. The units table gives each cell's name (tetrode and cell), layer, electrode depth and hemisphere. The electrodes table has one row per file, with location "MEC" and no coordinates. There is no trial table and no other annotation.
sessions = sg.load_sessions()
units = pd.DataFrame([
dict(subject=s.subject, path=s.path, session=s.session_id, unit=u.name, layer=u.layer or "not given",
depth_mm=u.depth_mm, hemisphere=u.hemisphere or "not given", two_leds=s.green is not None,
minutes=int(round(s.duration / 60)), spikes=len(u.spike_times),
rate_hz=len(u.spike_times) / s.duration)
for s in sessions for u in s.units])
print(f"{len(sessions)} files, {units.subject.nunique()} rats, {len(units)} cell-sessions\n")
print("Files and cells by number of LEDs and file length:")
print(units.groupby(["two_leds", "minutes"]).agg(files=("path", "nunique"), cells=("unit", "size"),
rats=("subject", "nunique")).to_string())
print("\nCells by rat and layer:")
print(pd.crosstab(units.subject, units.layer, margins=True).to_string())
dts = np.concatenate([np.diff(s.t) for s in sessions])
print(f"\nPosition sampling interval: {dts.min():.4f} to {dts.max():.4f} s (median {np.median(dts):.4f}); "
f"file lengths {min(s.duration for s in sessions):.1f} to {max(s.duration for s in sessions):.1f} s")
red = np.concatenate([s.red for s in sessions])
print(f"LED1 positions: x {np.nanmin(red[:, 0]):.1f} to {np.nanmax(red[:, 0]):.1f} cm, "
f"y {np.nanmin(red[:, 1]):.1f} to {np.nanmax(red[:, 1]):.1f} cm; "
f"missing samples {np.isnan(red).any(axis=1).mean():.2%}")
first_spike = min(u.spike_times.min() for s in sessions for u in s.units if len(u.spike_times))
outside = sum(int(np.sum((u.spike_times < s.t[0]) | (u.spike_times > s.t[-1] + s.dt)))
for s in sessions for u in s.units)
print(f"Spikes per cell {units.spikes.min()} to {units.spikes.max()} (median {units.spikes.median():.0f}); "
f"mean rate {units.rate_hz.min():.2f} to {units.rate_hz.max():.2f} Hz; "
f"first spike at {first_spike:.3f} s; spikes outside the tracked period: {outside}")
print(f"Electrode depth {units.depth_mm.min():.2f} to {units.depth_mm.max():.2f} mm "
f"(missing for {units.depth_mm.isna().sum()} cells); hemisphere missing for "
f"{(units.hemisphere == 'not given').sum()} cells; layer missing for {(units.layer == 'not given').sum()} cells")
print("\nDistinct session descriptions:", Counter(s.description for s in sessions).most_common())
print("Electrodes table, columns (besides the group reference):", dict(Counter(tuple(s.electrodes) for s in sessions)),
"; rows per file:", dict(Counter(len(s.electrodes["id"]) for s in sessions)),
"; locations:", dict(Counter(v for s in sessions for v in s.electrodes["location"])))
118 files, 15 rats, 620 cell-sessions
Files and cells by number of LEDs and file length:
files cells rats
two_leds minutes
False 10 47 167 8
True 10 48 297 8
20 23 156 4
Cells by rat and layer:
layer II III V VI not given All
subject
10073 8 0 0 0 0 8
10697 10 0 0 0 0 10
10704 0 0 0 44 0 44
10884 10 37 0 0 0 47
10938 0 8 0 0 0 8
10962 0 0 0 11 0 11
11016 0 0 13 0 0 13
11025 0 38 0 0 0 38
11084 0 0 53 0 1 54
11138 0 46 0 0 0 46
11207 0 0 52 143 5 200
11265 26 85 0 0 0 111
11278 17 0 0 0 0 17
11340 0 10 0 0 0 10
11343 0 3 0 0 0 3
All 71 227 118 198 6 620
Position sampling interval: 0.0200 to 0.0200 s (median 0.0200); file lengths 600.0 to 1200.0 s
LED1 positions: x -102.6 to 104.4 cm, y -104.6 to 102.7 cm; missing samples 0.01%
Spikes per cell 106 to 14696 (median 1127); mean rate 0.18 to 16.00 Hz; first spike at 0.001 s; spikes outside the tracked period: 10
Electrode depth 1.70 to 3.40 mm (missing for 36 cells); hemisphere missing for 230 cells; layer missing for 6 cells
Distinct session descriptions: [('This session includes spike and position times for recorded cells from a Long Evans rat that was running in a 1 x 1 meter enclosure. The cells were recorded in the dorsocaudal 25% portion of the medial entorhinal cortex (MEC).Position is given for two LEDs to enable calculation of head direction.', 118)]
Electrodes table, columns (besides the group reference): {('group_name', 'id', 'location'): 118} ; rows per file: {1: 118} ; locations: {'MEC': 118}
The enclosure and the scale of the positions¶
Every file says the rat ran in a 1 x 1 m enclosure. The SOM lists four enclosures: circles of 90 and 180 cm and squares of 100 and 150 cm. Below, for each file: the span of the tracked positions, whether they are centred so that the minimum is exactly minus half the larger span (which would mean they were rescaled), the distance between the two LEDs (7 cm in the SOM, a check of the scale that does not depend on the enclosure), and the fraction of samples in the corners outside the inscribed circle (near zero in a circular enclosure). For the two-LED files, the fraction of samples with the LEDs more than 15 cm apart, over twice the SOM's 7 cm, flags files where head direction from the two LEDs is doubtful (15 cm is a display threshold, not a selection).
rows = []
for s in sessions:
span = np.nanmax(s.red, axis=0) - np.nanmin(s.red, axis=0)
big = span.max()
centred = np.isclose(np.nanmin(s.red[:, np.argmax(span)]), -big / 2, atol=0.01)
centre = (np.nanmax(s.red, axis=0) + np.nanmin(s.red, axis=0)) / 2
r = np.hypot(*(s.red - centre).T)
sep = np.hypot(*(s.green - s.red).T) if s.green is not None else np.array([np.nan])
sep_ok = sep[~np.isnan(sep)]
rows.append(dict(path=s.path, subject=s.subject, minutes=int(round(s.duration / 60)),
span_x=span[0], span_y=span[1], larger_span=round(big, 1), centred=centred,
led_separation=np.nanmedian(sep),
led_over_15cm=np.mean(sep_ok > 15) if s.green is not None else np.nan,
outside_circle=np.nanmean(r > big / 2 + 1)))
extent = pd.DataFrame(rows)
print("Files by larger span of positions (cm) and file length:")
print(extent.groupby(["larger_span", "minutes"]).agg(
files=("path", "size"), rats=("subject", "nunique"), all_centred=("centred", "all"),
median_led_separation_cm=("led_separation", "median"),
median_outside_circle=("outside_circle", "median"), max_outside_circle=("outside_circle", "max")).round(3).to_string())
sep = extent.led_separation.dropna()
print(f"\nLED separation, median per file: {sep.min():.1f} to {sep.max():.1f} cm (median {sep.median():.1f}); SOM: 7 cm")
print(f"Files whose positions span exactly 100, 150 or 200 cm on the larger axis: "
f"{extent.larger_span.isin([100.0, 150.0, 200.0]).sum()} of {len(extent)}")
print("Files with fewer than 1% of samples outside the inscribed circle (a circular enclosure), by span:",
extent[extent.outside_circle < 0.01].groupby("larger_span").size().to_dict(), "of",
extent.groupby("larger_span").size().to_dict())
print("Two-LED files with the LEDs more than 15 cm apart (over twice the SOM's 7 cm) in over 10% of samples:")
print(extent[extent.led_over_15cm > 0.1][["path", "larger_span", "led_separation", "led_over_15cm"]].round(3).to_string(index=False))
print(f"Other two-LED files: at most {extent.led_over_15cm[extent.led_over_15cm <= 0.1].max():.3f} of samples above 15 cm")
others = extent[extent.led_over_15cm <= 0.5].led_separation
print(f"The other {len(others)} two-LED files (LEDs over 15 cm apart in at most half of the samples): median separation "
f"{others.min():.1f} to {others.max():.1f} cm")
Files by larger span of positions (cm) and file length:
files rats all_centred median_led_separation_cm median_outside_circle max_outside_circle
larger_span minutes
100.0 10 95 15 True 5.760 0.160 0.293
20 1 1 False 2.559 0.015 0.015
150.0 20 2 1 False 3.802 0.117 0.134
200.0 20 20 3 False 4.742 0.002 0.013
LED separation, median per file: 2.6 to 36.0 cm (median 5.2); SOM: 7 cm
Files whose positions span exactly 100, 150 or 200 cm on the larger axis: 118 of 118
Files with fewer than 1% of samples outside the inscribed circle (a circular enclosure), by span: {200.0: 19} of {100.0: 96, 150.0: 2, 200.0: 20}
Two-LED files with the LEDs more than 15 cm apart (over twice the SOM's 7 cm) in over 10% of samples:
path larger_span led_separation led_over_15cm
sub-11025/sub-11025_ses-06050501_behavior+ecephys.nwb 200.0 5.844 0.178
sub-11265/sub-11265_ses-09020601_behavior+ecephys.nwb 100.0 35.954 0.713
sub-11278/sub-11278_ses-31080502_behavior+ecephys.nwb 100.0 22.714 0.622
Other two-LED files: at most 0.039 of samples above 15 cm
The other 69 two-LED files (LEDs over 15 cm apart in at most half of the samples): median separation 2.6 to 8.4 cm
Trials¶
The SOM says every rat ran two 10-minute trials, 10 minutes apart. 95 files last 10 minutes and 23 last 20 minutes. Below, the step between successive position samples at 600 s in the 20-minute files, against the 99th percentile of all steps in the same file: a jump at 600 s marks two trials joined end to end.
rows = []
for s in sessions:
if len(s.trials) == 2:
ok = ~np.isnan(s.red).any(axis=1)
before = s.red[ok & (s.t < sg.TRIAL_S - 0.1)][-1]
after = s.red[ok & (s.t >= sg.TRIAL_S + 0.1)][0]
steps = np.hypot(*(s.red[10:] - s.red[:-10]).T) # displacements over 0.2 s
first = (s.t >= s.trials[0][0]) & (s.t < sg.TRIAL_S)
centre_1 = (np.nanmax(s.red[first], axis=0) + np.nanmin(s.red[first], axis=0)) / 2
centre_2 = (np.nanmax(s.red[~first], axis=0) + np.nanmin(s.red[~first], axis=0)) / 2
rows.append(dict(file=s.path.split("/")[1][:22], step_599_9_to_600_1_cm=np.hypot(*(after - before)),
p99_9_of_0_2s_steps_cm=np.nanpercentile(steps, 99.9),
trial1_centre_offset_cm=np.hypot(*centre_1), trial2_centre_offset_cm=np.hypot(*centre_2)))
joins = pd.DataFrame(rows)
print(joins.round(2).to_string())
print(f"\n{(joins.step_599_9_to_600_1_cm > joins.p99_9_of_0_2s_steps_cm).sum()} of {len(joins)} files move farther "
f"from 599.9 to 600.1 s than in 99.9% of their 0.2-s steps; the first trial's positions are centred "
f"on zero (within 0.1 cm) in {(joins.trial1_centre_offset_cm < 0.1).sum()}, the second's in "
f"{(joins.trial2_centre_offset_cm < 0.1).sum()}.")
same_day = units.assign(day=units.session.str[:6]).groupby(["subject", "day"]).path.nunique()
print(f"Days with more than one file: {(same_day > 1).sum()}; the files of a day are listed below.")
file step_599_9_to_600_1_cm p99_9_of_0_2s_steps_cm trial1_centre_offset_cm trial2_centre_offset_cm 0 sub-11025_ses-06050501 98.48 33.78 0.0 0.58 1 sub-11025_ses-11040501 117.74 13.11 0.0 8.65 2 sub-11025_ses-14050501 41.13 15.33 0.0 1.01 3 sub-11025_ses-16050501 148.52 15.48 0.0 4.35 4 sub-11138_ses-11040504 31.95 7.39 0.0 10.30 5 sub-11138_ses-15040504 145.23 24.29 0.0 3.50 6 sub-11207_ses-03060501 88.54 12.86 0.0 2.64 7 sub-11207_ses-04070501 48.95 14.83 0.0 3.28 8 sub-11207_ses-06070501 130.46 12.95 0.0 1.77 9 sub-11207_ses-07070501 116.10 13.28 0.0 3.81 10 sub-11207_ses-08070504 19.35 13.62 0.0 2.71 11 sub-11207_ses-09060501 147.42 12.78 0.0 3.74 12 sub-11207_ses-09070505 40.94 13.55 0.0 4.35 13 sub-11207_ses-10070503 63.90 14.52 0.0 7.31 14 sub-11207_ses-11060503 23.53 13.89 0.0 1.29 15 sub-11207_ses-17060501 127.80 14.91 0.0 3.66 16 sub-11207_ses-18060501 106.12 16.32 0.0 1.86 17 sub-11207_ses-21060501 100.60 13.86 0.0 1.52 18 sub-11207_ses-24060501 176.08 14.66 0.0 3.66 19 sub-11207_ses-27060504 142.61 12.41 0.0 3.73 20 sub-11207_ses-30060503 23.93 15.39 0.0 5.51 21 sub-11265_ses-16030601 63.14 12.16 0.0 1.45 22 sub-11265_ses-16030604 60.79 14.44 0.0 1.16 23 of 23 files move farther from 599.9 to 600.1 s than in 99.9% of their 0.2-s steps; the first trial's positions are centred on zero (within 0.1 cm) in 23, the second's in 0. Days with more than one file: 15; the files of a day are listed below.
Recordings deposited more than once, and the identity of cells across files¶
Identical spike trains or identical position tracks in two files would be a recording deposited twice. Cells are listed per file; no identifier links a cell across files. Cells of the same rat with the same electrode depth, hemisphere and tetrode-cell name in several files may be the same cell recorded on several days.
spike_hash = Counter(hashlib.md5(np.round(u.spike_times, 6).tobytes()).hexdigest()
for s in sessions for u in s.units)
track_hash = Counter(hashlib.md5(np.nan_to_num(s.red).tobytes()).hexdigest() for s in sessions)
print(f"Spike trains deposited more than once: {sum(v > 1 for v in spike_hash.values())}; "
f"position tracks deposited more than once: {sum(v > 1 for v in track_hash.values())}")
key = ["subject", "depth_mm", "hemisphere", "unit"]
repeats = units[units.depth_mm.notna()].groupby(key).path.nunique()
print(f"Cell-sessions with depth given: {units.depth_mm.notna().sum()}; distinct (rat, depth, hemisphere, name): "
f"{len(repeats)}; of these, {(repeats > 1).sum()} appear in 2 to {repeats.max()} files")
same_day = units.assign(day=units.session.str[:6])
for (subject, day), g in same_day.groupby(["subject", "day"]):
if g.path.nunique() > 1:
print(f" rat {subject}, day {day}: " + "; ".join(
f"{p.split('_ses-')[1][:8]} depth {h.depth_mm.iloc[0]:.2f} mm, {len(h)} cells"
for p, h in g.groupby("path")))
Spike trains deposited more than once: 0; position tracks deposited more than once: 0 Cell-sessions with depth given: 584; distinct (rat, depth, hemisphere, name): 425; of these, 99 appear in 2 to 7 files rat 10704, day 070704: 07070402 depth 2.55 mm, 4 cells; 07070407 depth 2.55 mm, 6 cells rat 10884, day 030804: 03080402 depth 3.00 mm, 6 cells; 03080405 depth 3.00 mm, 4 cells; 03080409 depth 3.05 mm, 4 cells rat 10884, day 080704: 08070402 depth 2.20 mm, 4 cells; 08070405 depth 2.20 mm, 1 cells rat 10938, day 121004: 12100406 depth 2.70 mm, 4 cells; 12100410 depth 2.70 mm, 1 cells rat 10962, day 281104: 28110402 depth 2.55 mm, 2 cells; 28110406 depth 2.55 mm, 3 cells rat 11084, day 090305: 09030501 depth 2.05 mm, 6 cells; 09030503 depth nan mm, 1 cells rat 11138, day 060405: 06040501 depth 1.75 mm, 3 cells; 06040507 depth 1.75 mm, 5 cells rat 11138, day 110405: 11040501 depth 2.05 mm, 3 cells; 11040504 depth 2.20 mm, 4 cells; 11040509 depth 2.10 mm, 5 cells rat 11207, day 090705: 09070501 depth 2.70 mm, 1 cells; 09070505 depth 2.80 mm, 2 cells rat 11207, day 100705: 10070501 depth 2.85 mm, 4 cells; 10070503 depth 2.90 mm, 3 cells rat 11207, day 110605: 11060501 depth 2.00 mm, 12 cells; 11060502 depth 2.00 mm, 1 cells; 11060503 depth 2.15 mm, 4 cells rat 11207, day 210605: 21060501 depth 2.30 mm, 5 cells; 21060503 depth 2.15 mm, 13 cells rat 11207, day 270605: 27060501 depth 2.15 mm, 14 cells; 27060504 depth 2.35 mm, 3 cells rat 11207, day 300605: 30060501 depth 2.30 mm, 10 cells; 30060503 depth 2.50 mm, 1 cells rat 11265, day 160306: 16030601 depth 3.20 mm, 12 cells; 16030604 depth 3.25 mm, 14 cells
Cells by layer against table S1¶
Table S1A gives the cells sorted per layer for all 17 rats, table S1B for the nine two-LED rats.
S1A = {"II": 108, "III": 223, "V": 110, "VI": 186} # table S1A, cells sampled (paper)
S1B = {"II": 64, "III": 175, "V": 74, "VI": 136} # table S1B, two-LED rats (paper)
per_layer = pd.DataFrame({
"dandiset, all": units.layer.value_counts(), "paper S1A": pd.Series(S1A),
"dandiset, two LEDs": units[units.two_leds].layer.value_counts(), "paper S1B": pd.Series(S1B)}).fillna(0).astype(int)
per_layer.loc["total"] = per_layer.sum()
print(per_layer.to_string())
leds = units.groupby("subject").two_leds.agg(["min", "max"])
print(f"\nRats with two-LED files only: {int((leds['min']).sum())}; one LED only: {int((~leds['max']).sum())}; "
f"both: {list(leds[leds['min'] != leds['max']].index)} (the SOM: nine rats had two LEDs, eight one)")
print(f"Rats: {units.subject.nunique()} (the paper: 17)")
dandiset, all paper S1A dandiset, two LEDs paper S1B II 71 108 43 64 III 227 223 182 175 V 118 110 79 74 VI 198 186 143 136 not given 6 0 6 0 total 620 627 453 449 Rats with two-LED files only: 7; one LED only: 7; both: ['11084'] (the SOM: nine rats had two LEDs, eight one) Rats: 15 (the paper: 17)
Measures for every cell¶
The next cell computes, for every cell, the rate maps, autocorrelograms and gridness, the
head-direction tuning and its tests, and the speed regressions, under every reading, and for every
pair of cells recorded together the offset of their rate-map crosscorrelogram (sargolini.py).
with ProcessPoolExecutor(min(8, os.cpu_count() or 1)) as pool:
results = list(pool.map(sg.session_measures, sessions, chunksize=1))
cells = pd.DataFrame([c for cs, _ in results for c in cs])
pairs = pd.DataFrame([p for _, ps in results for p in ps])
by_path = {s.path: s for s in sessions}
print(f"{len(cells)} cells and {len(pairs)} pairs of simultaneously recorded cells")
no_score = {g: int(cells[f"grid|{t}|{g}"].isna().sum()) for t in sg.TRIALS for g in sg.GRID_READINGS}
print(f"Cells without a gridness (no autocorrelogram peak besides the central one): "
f"{min(no_score.values())} to {max(no_score.values())} of {len(cells)}, by reading")
two_led = cells[cells.two_leds]
print(f"Two-LED cells without a Watson U2 or block correlation: "
f"{int(two_led['u2|pooled'].isna().sum())} and {int(two_led['block_r|pooled'].isna().sum())} of {len(two_led)}")
620 cells and 2142 pairs of simultaneously recorded cells Cells without a gridness (no autocorrelogram peak besides the central one): 14 to 130 of 620, by reading Two-LED cells without a Watson U2 or block correlation: 0 and 0 of 453
Do spikes and positions share a clock?¶
There are no trials or events to align to, so the check shifts each cell's spike train against the tracking by -2 to +2 s and recomputes the measures the claims rest on: Watson's U2 for the 30 two-LED cells with the highest U2, and the spatial information of the 3 cm rate map for the 30 cells with the highest gridness. If the clocks agree, both peak at a shift near zero; an offset between them would move the peak.
lags = np.round(np.arange(-2, 2.01, 0.25), 2)
top_hd = cells[cells.two_leds].nlargest(30, "u2|pooled")
top_grid = cells.nlargest(30, f"grid|pooled|{sg.GRID_READINGS[0]}")
unit_of = {(s.path, u.name): (s, u) for s in sessions for u in s.units}
u2_lag = np.array([sg.lagged_u2(*unit_of[(r.path, r.unit)], lags) for r in top_hd.itertuples()])
info_lag = np.array([sg.lagged_map_info(*unit_of[(r.path, r.unit)], lags) for r in top_grid.itertuples()])
clock = pd.DataFrame({"shift_s": lags,
"median U2 / U2 at 0": np.median(u2_lag / u2_lag[:, [list(lags).index(0)]], axis=0),
"median info / info at 0": np.median(info_lag / info_lag[:, [list(lags).index(0)]], axis=0)})
print(clock.round(3).to_string(index=False))
print(f"\nShift of the peak, per cell: U2 {Counter(lags[np.argmax(u2_lag, axis=1)]).most_common()}; "
f"spatial information {Counter(lags[np.argmax(info_lag, axis=1)]).most_common()}")
print(f"Cells: {top_hd.subject.nunique()} rats and {top_hd.path.nunique()} files (U2); "
f"{top_grid.subject.nunique()} rats and {top_grid.path.nunique()} files (information)")
shift_s median U2 / U2 at 0 median info / info at 0
-2.00 0.205 0.429
-1.75 0.242 0.463
-1.50 0.284 0.463
-1.25 0.326 0.476
-1.00 0.393 0.516
-0.75 0.492 0.561
-0.50 0.619 0.687
-0.25 0.829 0.863
0.00 1.000 1.000
0.25 0.863 0.901
0.50 0.681 0.676
0.75 0.569 0.571
1.00 0.482 0.521
1.25 0.401 0.493
1.50 0.325 0.473
1.75 0.278 0.462
2.00 0.248 0.449
Shift of the peak, per cell: U2 [(np.float64(0.0), 29), (np.float64(0.25), 1)]; spatial information [(np.float64(0.0), 28), (np.float64(0.25), 2)]
Cells: 4 rats and 20 files (U2); 8 rats and 23 files (information)
The findings of attempt 000582-sargolini-2006-01, and new ones¶
The version is the same as the earlier attempt's, and the data are unchanged, so each finding is re-checked on the same files with the outputs above.
Finding of -01 |
Kind | Status | Shown by |
|---|---|---|---|
| Every file says the enclosure was 1 x 1 m, but in 23 sessions the positions span 1.5 to 2.5 m | metadata_error | Still present | Enclosure table: 2 files span 150 cm and 20 span 200 cm on the larger axis, 19 of the 20 circular (under 1% of samples outside the inscribed circle) |
| The descriptions say position is given for two LEDs, but 47 of 118 files have one | metadata_error | Still present | First table: 47 files, 167 cell-sessions with one LED; rat 11084 has files of both kinds |
| No identifier links a cell across files | identifier_missing | Still present | 584 cell-sessions with a depth form 425 distinct (rat, depth, hemisphere, name), 99 of them in 2 to 7 files |
| The dandiset is a sample whose selection is not described | selection_undocumented | Still present | 620 cell-sessions against the paper's 627 cells, 15 rats against 17, layer II 71 against 108 while layers III, V and VI hold slightly more than table S1A |
| The paper is not open access and the dandiset gives no analysis parameters | methods_unavailable | Partly fixed | This attempt read the methods in the SOM, supplied by a person; the paper is still not open access, the dandiset states no parameter, and no analysis code is public |
New findings:
- The positions are rescaled. In all 118 files the positions span exactly 100, 150 or 200 cm on the larger axis, and the first trial is centred on zero. Of the 20 files spanning 200 cm, 19 look circular; the SOM's large circle is 180 cm across, so in those files distances and speeds are about 11% too large. The distance between the two LEDs (median 5.2 cm per file, 2.6 to 36 cm, against 7 cm in the SOM) is too variable to fix the scale. The files do not say that the positions were rescaled.
- The 23 twenty-minute files hold two 10-minute trials joined near 600 s: in all 23 the position jumps between 599.9 and 600.1 s by more than 99.9% of the file's 0.2-s steps, and only the first trial is centred. There is no trial table, and nothing says which 10-minute files, if any, are the two trials of one recording; the files of a day differ in depth or cells (list above).
- No file gives the recording sites' distance from the postrhinal border, which the paper's
dorsoventral analyses use;
depthis the depth the electrodes were lowered to. - No file names its enclosure, a condition or the cue-card position, so the cue-rotation trials cannot be found.
- Six cells have no layer, 36 no depth and 230 no hemisphere.
- The descriptions disagree with the files: the experiment description says the sample holds "conjunctive cells and head direction cells from layers III and V", the dandiset description that it holds spike times "for recorded grid cells"; the files hold cells of layers II, III, V and VI and of every class.
- The electrodes table holds one row per file, location "MEC", with no coordinates, so it does not give the recording sites' distance from the postrhinal border either.
- In two two-LED files,
sub-11265_ses-09020601andsub-11278_ses-31080502, the LEDs are more than 15 cm apart in 71% and 62% of samples (median 36.0 and 22.7 cm, against 2.6 to 8.4 cm in the other two-LED files), so head direction there is doubtful; nothing in the files flags it. They hold 13 layer III and 11 layer II cells. A third file,sub-11025_ses-06050501, has 17.8% of samples above 15 cm; the others at most 3.9%. - The files do not say which LED is the red one, whose positions the SOM uses. LED1 is the only LED in the single-LED files, and the SOM says single-LED rats carried a red LED, so LED1 is taken as red.
Spikes and positions are consistent with a shared clock: Watson's U2 and the spatial information peak at zero shift for 29 and 28 of 30 cells, and the 10 spikes outside the tracked period fall after the last sample.
Claim layer2-grid¶
The proportion of cells with grid structure depended on layer. Most well-separated layer II cells were grid cells (66 of 108), against a smaller proportion in layers III (78 of 223), V (17 of 110) and VI (42 of 186).
Criterion: all cells of all rats, by layer; a grid cell has gridness above 0, under readings O1 to O3. Reproduced when the layer II fraction is above 0.5 and above that of each deeper layer, and the four fractions are within tolerance (0.611 +/- 0.061, 0.350 +/- 0.035, 0.155 +/- 0.015, 0.226 +/- 0.023); partially reproduced when the order holds but a fraction is outside tolerance. Each cell-session counts as a cell, as the paper's 627 cells are about as many as the dandiset's 620 cell-sessions (data check). The six cells with no layer are left out. Two variants, which do not decide the outcome, follow the main table: the gridness of each cell's spike train shifted in time, a control for how often gridness above 0 arises without a grid, and the fractions with each (rat, depth, hemisphere, name) counted once.
EXPECTED_GRID = {"II": (0.611, 0.061), "III": (0.350, 0.035), "V": (0.155, 0.015), "VI": (0.226, 0.023)}
known = cells[cells.layer.isin(LAYERS)].copy()
n_layer = known.layer.value_counts()
rows = []
for T, G, _ in sg.readings(with_level=False):
grid = sg.classify(known, T, G, 0.05).grid
frac = grid.groupby(known.layer).mean()
order = frac["II"] > 0.5 and all(frac["II"] > frac[l] for l in DEEP)
tol = all(within(frac[l], *EXPECTED_GRID[l]) for l in LAYERS)
rows.append(dict(trials=T, gridness=G, **{l: frac[l] for l in LAYERS},
**{f"n {l}": int(grid[known.layer == l].sum()) for l in LAYERS}, total=int(grid.sum()),
order=order, in_tolerance=[l for l in LAYERS if within(frac[l], *EXPECTED_GRID[l])],
outcome="reproduced" if order and tol else "partially_reproduced" if order else "not_reproduced"))
grid_table = pd.DataFrame(rows)
print("Cells per layer:", n_layer[LAYERS].to_dict())
print(grid_table.round(3).to_string())
print(f"\nRange over the {len(grid_table)} readings:", {l: f"{grid_table[l].min():.3f} to {grid_table[l].max():.3f}" for l in LAYERS})
print("Expected:", {l: f"{v - t:.3f} to {v + t:.3f}" for l, (v, t) in EXPECTED_GRID.items()})
print("Readings with each layer within tolerance:", {l: int(grid_table.in_tolerance.apply(lambda x: l in x).sum()) for l in LAYERS})
print("Outcomes:", grid_table.outcome.value_counts().to_dict(), "->", outcome_of(grid_table.outcome))
Cells per layer: {'II': 71, 'III': 227, 'V': 118, 'VI': 198}
trials gridness II III V VI n II n III n V n VI total order in_tolerance outcome
0 pooled 3cm|enclose|blob|max|area 0.634 0.559 0.314 0.359 45 127 37 71 280 True [II] partially_reproduced
1 pooled 3cm|enclose|blob|max|farthest 0.634 0.568 0.322 0.354 45 129 38 70 282 True [II] partially_reproduced
2 pooled 3cm|enclose|blob|centroid|area 0.634 0.568 0.339 0.394 45 129 40 78 292 True [II] partially_reproduced
3 pooled 3cm|enclose|blob|centroid|farthest 0.620 0.568 0.347 0.354 44 129 41 70 284 True [II] partially_reproduced
4 pooled 3cm|enclose|disc|max|area 0.662 0.577 0.331 0.379 47 131 39 75 292 True [II] partially_reproduced
5 pooled 3cm|enclose|disc|max|farthest 0.620 0.555 0.356 0.374 44 126 42 74 286 True [II] partially_reproduced
6 pooled 3cm|enclose|disc|centroid|area 0.648 0.581 0.356 0.409 46 132 42 81 301 True [II] partially_reproduced
7 pooled 3cm|enclose|disc|centroid|farthest 0.606 0.559 0.373 0.364 43 127 44 72 286 True [II] partially_reproduced
8 pooled 3cm|centre|blob|max|area 0.662 0.568 0.322 0.394 47 129 38 78 292 True [II] partially_reproduced
9 pooled 3cm|centre|blob|centroid|area 0.718 0.577 0.347 0.419 51 131 41 83 306 True [] partially_reproduced
10 pooled 3cm|centre|disc|max|area 0.676 0.590 0.356 0.429 48 134 42 85 309 True [] partially_reproduced
11 pooled 3cm|centre|disc|max|farthest 0.648 0.555 0.339 0.394 46 126 40 78 290 True [II] partially_reproduced
12 pooled 3cm|centre|disc|centroid|area 0.718 0.604 0.373 0.444 51 137 44 88 320 True [] partially_reproduced
13 pooled 3cm|centre|disc|centroid|farthest 0.690 0.564 0.347 0.399 49 128 41 79 297 True [] partially_reproduced
14 pooled 1.5cm|enclose|blob|max|area 0.662 0.551 0.347 0.359 47 125 41 71 284 True [II] partially_reproduced
15 pooled 1.5cm|enclose|blob|max|farthest 0.648 0.551 0.347 0.369 46 125 41 73 285 True [II] partially_reproduced
16 pooled 1.5cm|enclose|blob|centroid|area 0.620 0.564 0.373 0.374 44 128 44 74 290 True [II] partially_reproduced
17 pooled 1.5cm|enclose|blob|centroid|farthest 0.634 0.559 0.364 0.364 45 127 43 72 287 True [II] partially_reproduced
18 pooled 1.5cm|enclose|disc|max|area 0.676 0.568 0.364 0.374 48 129 43 74 294 True [] partially_reproduced
19 pooled 1.5cm|enclose|disc|max|farthest 0.648 0.551 0.364 0.389 46 125 43 77 291 True [II] partially_reproduced
20 pooled 1.5cm|enclose|disc|centroid|area 0.634 0.581 0.407 0.394 45 132 48 78 303 True [II] partially_reproduced
21 pooled 1.5cm|enclose|disc|centroid|farthest 0.606 0.555 0.390 0.389 43 126 46 77 292 True [II] partially_reproduced
22 pooled 1.5cm|centre|blob|max|area 0.648 0.568 0.339 0.389 46 129 40 77 292 True [II] partially_reproduced
23 pooled 1.5cm|centre|blob|centroid|area 0.676 0.573 0.347 0.404 48 130 41 80 299 True [] partially_reproduced
24 pooled 1.5cm|centre|disc|max|area 0.662 0.577 0.381 0.399 47 131 45 79 302 True [II] partially_reproduced
25 pooled 1.5cm|centre|disc|max|farthest 0.634 0.546 0.347 0.384 45 124 41 76 286 True [II] partially_reproduced
26 pooled 1.5cm|centre|disc|centroid|area 0.704 0.599 0.390 0.424 50 136 46 84 316 True [] partially_reproduced
27 pooled 1.5cm|centre|disc|centroid|farthest 0.662 0.555 0.322 0.374 47 126 38 74 285 True [II] partially_reproduced
28 first 3cm|enclose|blob|max|area 0.662 0.546 0.305 0.384 47 124 36 76 283 True [II] partially_reproduced
29 first 3cm|enclose|blob|max|farthest 0.648 0.546 0.297 0.364 46 124 35 72 277 True [II] partially_reproduced
30 first 3cm|enclose|blob|centroid|area 0.676 0.551 0.322 0.424 48 125 38 84 295 True [] partially_reproduced
31 first 3cm|enclose|blob|centroid|farthest 0.648 0.555 0.322 0.384 46 126 38 76 286 True [II] partially_reproduced
32 first 3cm|enclose|disc|max|area 0.690 0.559 0.322 0.404 49 127 38 80 294 True [] partially_reproduced
33 first 3cm|enclose|disc|max|farthest 0.634 0.533 0.322 0.394 45 121 38 78 282 True [II] partially_reproduced
34 first 3cm|enclose|disc|centroid|area 0.690 0.559 0.339 0.439 49 127 40 87 303 True [] partially_reproduced
35 first 3cm|enclose|disc|centroid|farthest 0.634 0.542 0.339 0.399 45 123 40 79 287 True [II] partially_reproduced
36 first 3cm|centre|blob|max|area 0.704 0.564 0.314 0.434 50 128 37 86 301 True [] partially_reproduced
37 first 3cm|centre|blob|centroid|area 0.718 0.573 0.373 0.460 51 130 44 91 316 True [] partially_reproduced
38 first 3cm|centre|disc|max|area 0.718 0.577 0.347 0.470 51 131 41 93 316 True [] partially_reproduced
39 first 3cm|centre|disc|max|farthest 0.690 0.546 0.339 0.409 49 124 40 81 294 True [] partially_reproduced
40 first 3cm|centre|disc|centroid|area 0.718 0.595 0.398 0.485 51 135 47 96 329 True [] partially_reproduced
41 first 3cm|centre|disc|centroid|farthest 0.690 0.559 0.364 0.424 49 127 43 84 303 True [] partially_reproduced
42 first 1.5cm|enclose|blob|max|area 0.676 0.542 0.339 0.364 48 123 40 72 283 True [] partially_reproduced
43 first 1.5cm|enclose|blob|max|farthest 0.648 0.529 0.339 0.364 46 120 40 72 278 True [II] partially_reproduced
44 first 1.5cm|enclose|blob|centroid|area 0.648 0.555 0.347 0.384 46 126 41 76 289 True [II] partially_reproduced
45 first 1.5cm|enclose|blob|centroid|farthest 0.620 0.542 0.322 0.374 44 123 38 74 279 True [II] partially_reproduced
46 first 1.5cm|enclose|disc|max|area 0.690 0.555 0.356 0.389 49 126 42 77 294 True [] partially_reproduced
47 first 1.5cm|enclose|disc|max|farthest 0.648 0.529 0.364 0.399 46 120 43 79 288 True [II] partially_reproduced
48 first 1.5cm|enclose|disc|centroid|area 0.662 0.568 0.381 0.404 47 129 45 80 301 True [II] partially_reproduced
49 first 1.5cm|enclose|disc|centroid|farthest 0.592 0.542 0.347 0.404 42 123 41 80 286 True [II] partially_reproduced
50 first 1.5cm|centre|blob|max|area 0.690 0.564 0.339 0.414 49 128 40 82 299 True [] partially_reproduced
51 first 1.5cm|centre|blob|centroid|area 0.690 0.573 0.364 0.444 49 130 43 88 310 True [] partially_reproduced
52 first 1.5cm|centre|disc|max|area 0.704 0.564 0.390 0.419 50 128 46 83 307 True [] partially_reproduced
53 first 1.5cm|centre|disc|max|farthest 0.676 0.537 0.347 0.389 48 122 41 77 288 True [] partially_reproduced
54 first 1.5cm|centre|disc|centroid|area 0.718 0.595 0.415 0.465 51 135 49 92 327 True [] partially_reproduced
55 first 1.5cm|centre|disc|centroid|farthest 0.676 0.546 0.339 0.389 48 124 40 77 289 True [] partially_reproduced
Range over the 56 readings: {'II': '0.592 to 0.718', 'III': '0.529 to 0.604', 'V': '0.297 to 0.415', 'VI': '0.354 to 0.485'}
Expected: {'II': '0.550 to 0.672', 'III': '0.315 to 0.385', 'V': '0.140 to 0.170', 'VI': '0.203 to 0.249'}
Readings with each layer within tolerance: {'II': 32, 'III': 0, 'V': 0, 'VI': 0}
Outcomes: {'partially_reproduced': 56} -> partially_reproduced
READING = ("pooled", sg.GRID_READINGS[0], 0.05) # the first reading, used for figures and per-rat tables
known["grid"] = sg.classify(known, READING[0], READING[1], 0.05).grid
per_rat = known.groupby(["subject", "layer"]).grid.agg(grid_cells="sum", cells="size")
per_rat["fraction"] = (per_rat.grid_cells / per_rat.cells).round(2)
print(f"Per rat, reading {READING[:2]}:")
print(per_rat.unstack("layer").to_string())
dedup = known[known.depth_mm.notna()].drop_duplicates(["subject", "depth_mm", "hemisphere", "unit"])
print("\nVariant, one cell-session per (rat, depth, hemisphere, name), first file kept:",
{l: f"{int(g.grid.sum())}/{len(g)} = {g.grid.mean():.3f}" for l, g in dedup.groupby("layer")})
fig, ax = plt.subplots(figsize=(6, 3.2))
col = f"grid|{READING[0]}|{READING[1]}"
bins = np.linspace(-1.5, 1.8, 34)
for l in LAYERS:
ax.hist(known[known.layer == l][col].dropna(), bins=bins, histtype="step", density=True, label=f"layer {l}")
ax.axvline(0, color="k", lw=1)
ax.set_xlabel("Gridness"); ax.set_ylabel("Density"); ax.legend(frameon=False, fontsize=8)
print(f"Gridness plotted: {known[col].min():.2f} to {known[col].max():.2f}; none outside the axes "
f"({int(((known[col] < bins[0]) | (known[col] > bins[-1])).sum())} cells outside)")
Per rat, reading ('pooled', '3cm|enclose|blob|max|area'):
grid_cells cells fraction
layer II III V VI II III V VI II III V VI
subject
10073 7.0 NaN NaN NaN 8.0 NaN NaN NaN 0.88 NaN NaN NaN
10697 9.0 NaN NaN NaN 10.0 NaN NaN NaN 0.90 NaN NaN NaN
10704 NaN NaN NaN 24.0 NaN NaN NaN 44.0 NaN NaN NaN 0.55
10884 6.0 16.0 NaN NaN 10.0 37.0 NaN NaN 0.60 0.43 NaN NaN
10938 NaN 5.0 NaN NaN NaN 8.0 NaN NaN NaN 0.62 NaN NaN
10962 NaN NaN NaN 4.0 NaN NaN NaN 11.0 NaN NaN NaN 0.36
11016 NaN NaN 8.0 NaN NaN NaN 13.0 NaN NaN NaN 0.62 NaN
11025 NaN 22.0 NaN NaN NaN 38.0 NaN NaN NaN 0.58 NaN NaN
11084 NaN NaN 7.0 NaN NaN NaN 53.0 NaN NaN NaN 0.13 NaN
11138 NaN 21.0 NaN NaN NaN 46.0 NaN NaN NaN 0.46 NaN NaN
11207 NaN NaN 22.0 43.0 NaN NaN 52.0 143.0 NaN NaN 0.42 0.30
11265 16.0 57.0 NaN NaN 26.0 85.0 NaN NaN 0.62 0.67 NaN NaN
11278 7.0 NaN NaN NaN 17.0 NaN NaN NaN 0.41 NaN NaN NaN
11340 NaN 6.0 NaN NaN NaN 10.0 NaN NaN NaN 0.60 NaN NaN
11343 NaN 0.0 NaN NaN NaN 3.0 NaN NaN NaN 0.00 NaN NaN
Variant, one cell-session per (rat, depth, hemisphere, name), first file kept: {'II': '34/50 = 0.680', 'III': '68/133 = 0.511', 'V': '31/87 = 0.356', 'VI': '51/155 = 0.329'}
Gridness plotted: -0.77 to 1.40; none outside the axes (0 cells outside)
# Variants that do not decide the outcome. (1) A control for gridness above 0: the same gridness
# for each cell's spike train shifted in time (sargolini.shifted_gridness), first reading.
seeds = np.random.default_rng(5820).integers(2**32, size=len(sessions)) # this section's own generator
with ProcessPoolExecutor(min(8, os.cpu_count() or 1)) as pool:
shifted = pd.DataFrame([r for rs in pool.map(sg.shifted_gridness, sessions, seeds, chunksize=1) for r in rs])
shifted = shifted.merge(known[["path", "unit", "layer"]], on=["path", "unit"])
print(f"Spike trains shifted in time, 5 shifts of 20 s or more per cell: {len(shifted)} maps of {shifted[['path', 'unit']].drop_duplicates().shape[0]} cells, "
f"{int(shifted.gridness.isna().sum())} without a gridness")
print(f" gridness above 0: {(shifted.gridness > 0).mean():.3f} of shifted maps; by layer "
f"{(shifted.gridness > 0).groupby(shifted.layer).mean().round(3).to_dict()}")
print(f" observed, gridness above 0: {known.grid.groupby(known.layer).mean().round(3).to_dict()}")
for q in (0.95, 0.99):
cut = shifted.gridness.quantile(q)
above = (known[col] > cut).groupby(known.layer).mean()
print(f" cells above the {q:.0%} point of the shifted gridness ({cut:.3f}): {above.round(3).to_dict()}; "
f"layer II above every deeper layer: {all(above['II'] > above[l] for l in DEEP)}")
# (2) Each (rat, depth, hemisphere, name) counted once, first file kept, under every reading.
first_copy = known.depth_mm.notna() & ~known.duplicated(["subject", "depth_mm", "hemisphere", "unit"])
once = known[first_copy]
rows, per_rat_rows = [], []
for T, G, _ in sg.readings(with_level=False):
grid = sg.classify(once, T, G, 0.05).grid
frac = grid.groupby(once.layer).mean()
rows.append(dict(trials=T, gridness=G, **{l: frac[l] for l in LAYERS},
order=frac["II"] > 0.5 and all(frac["II"] > frac[l] for l in DEEP),
layers_in_tolerance=sum(within(frac[l], *EXPECTED_GRID[l]) for l in LAYERS)))
per_rat_rows.append(grid.groupby([once.subject, once.layer]).sum().rename("grid"))
once_table = pd.DataFrame(rows)
print(f"\nEach (rat, depth, hemisphere, name) once: {len(once)} of {len(known)} cells "
f"({int(known.depth_mm.isna().sum())} without a depth left out), over the {len(once_table)} readings:")
print(" fractions:", {l: f"{once_table[l].min():.3f} to {once_table[l].max():.3f}" for l in LAYERS},
f"order holds under {int(once_table.order.sum())}; readings by number of layers within tolerance:",
once_table.layers_in_tolerance.value_counts().sort_index().to_dict())
per_rat_once = pd.concat(per_rat_rows, axis=1)
per_rat_once = pd.DataFrame({"cells": once.groupby(["subject", "layer"]).size(),
"grid cells, fewest": per_rat_once.min(axis=1), "grid cells, most": per_rat_once.max(axis=1)})
print("Per rat and layer, each cell once, grid cells over the readings:")
print(per_rat_once.unstack("layer").fillna("").to_string())
Spike trains shifted in time, 5 shifts of 20 s or more per cell: 3070 maps of 614 cells, 1 without a gridness
gridness above 0: 0.297 of shifted maps; by layer {'II': 0.307, 'III': 0.288, 'V': 0.293, 'VI': 0.306}
observed, gridness above 0: {'II': 0.634, 'III': 0.559, 'V': 0.314, 'VI': 0.359}
cells above the 95% point of the shifted gridness (0.216): {'II': 0.408, 'III': 0.419, 'V': 0.186, 'VI': 0.197}; layer II above every deeper layer: False
cells above the 99% point of the shifted gridness (0.362): {'II': 0.296, 'III': 0.348, 'V': 0.153, 'VI': 0.192}; layer II above every deeper layer: False
Each (rat, depth, hemisphere, name) once: 425 of 614 cells (30 without a depth left out), over the 56 readings:
fractions: {'II': '0.680 to 0.780', 'III': '0.451 to 0.564', 'V': '0.333 to 0.437', 'VI': '0.316 to 0.458'} order holds under 56; readings by number of layers within tolerance: {0: 56}
Per rat and layer, each cell once, grid cells over the readings:
cells grid cells, fewest grid cells, most
layer II III V VI II III V VI II III V VI
subject
10073 8.0 7.0 7.0
10697 6.0 4.0 5.0
10704 34.0 14.0 22.0
10884 10.0 30.0 6.0 11.0 8.0 18.0
10938 7.0 3.0 4.0
10962 9.0 2.0 4.0
11016 13.0 7.0 9.0
11025 21.0 9.0 13.0
11084 27.0 3.0 7.0
11138 36.0 12.0 19.0
11207 47.0 112.0 17.0 29.0 25.0 49.0
11265 26.0 39.0 16.0 21.0 19.0 24.0
Result: partially reproduced. Under all 56 readings layer II has the largest fraction of grid cells, 0.592 to 0.718, above 0.5 and above layers III (0.529 to 0.604), V (0.297 to 0.415) and VI (0.354 to 0.485), so the order holds. The layer II fraction is within its tolerance under 32 of the 56 readings, but no deeper layer is within tolerance under any reading: each is well above the paper's fraction. Gridness above 0 makes 277 to 329 cells grid cells, against the paper's 203 of 627. Per rat, the layer II fractions are 0.41 to 0.90 and the deeper ones 0 to 0.67; of the two rats recorded in layers II and III, layer II is higher in 10884 (0.60 against 0.43) and lower in 11265 (0.62 against 0.67).
Gridness above 0 is a lax criterion on these data. With each spike train shifted in time, 0.297 of the shifted maps have gridness above 0 (0.288 to 0.307 by layer), against observed fractions of 0.634, 0.559, 0.314 and 0.359 in layers II, III, V and VI under the first reading. In layers V and VI the observed fractions are barely above those of the shifted spike trains. Counting only cells above the 95% point of the shifted gridness (0.216) gives 0.408, 0.419, 0.186 and 0.197, and above the 99% point (0.362) 0.296, 0.348, 0.153 and 0.192. Under either stricter cut layer III is above layer II, so the layer order on which the partial outcome rests does not survive a stricter grid threshold, and no layer has a majority of grid cells. This is not the paper's criterion, and the outcome rests on gridness above 0, as the claim says; but it limits what the partial outcome shows.
Counting each (rat, depth, hemisphere, name) once (425 of 614 cells; the 30 without a depth left out), the order holds under all 56 readings (layer II 0.680 to 0.780, III 0.451 to 0.564, V 0.333 to 0.437, VI 0.316 to 0.458), and no reading has all four layers within tolerance, so this variant gives the same outcome.
The answer on a peak's centre and radius adds the radius as the largest distance to a pixel of the peak. The outcome is the same under every reading, as before; the ranges widen a little (layer II 0.620 to 0.718 before).
The earlier attempt also called this claim partially reproduced, for another reason: it used a grid score of its own with a shuffled-data threshold, under which a third of layer II cells were grid cells. With the paper's criterion, gridness above 0, most layer II cells are grid cells, and the claim falls short on the deeper layers' fractions.
Claim grid-all-layers¶
Grid cells with tessellating firing fields were found in all four principal cell layers of MEC (II, III, V and VI).
Criterion: grid cells as in layer2-grid; reproduced when at least one grid cell is found in each
layer.
counts = grid_table[[f"n {l}" for l in LAYERS]]
print(f"Fewest grid cells per layer over the {len(counts)} readings:", counts.min().to_dict())
print("Rats contributing grid cells per layer (first reading):",
known[known.grid].groupby("layer").subject.nunique().to_dict())
all_layers = (counts > 0).all(axis=1)
print(f"Readings with a grid cell in every layer: {int(all_layers.sum())} of {len(all_layers)}")
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once (see layer2-grid).
once_counts = pd.DataFrame([sg.classify(once, T, G, 0.05).grid.groupby(once.layer).sum().reindex(LAYERS, fill_value=0)
for T, G, _ in sg.readings(with_level=False)])
print("Variant, each (rat, depth, hemisphere, name) once: fewest grid cells per layer",
once_counts.min().astype(int).to_dict(), f"; readings with a grid cell in every layer {int((once_counts > 0).all(axis=1).sum())} of {len(once_counts)}")
print("Outcome:", "reproduced" if all_layers.all() else "methods_underspecified" if all_layers.any() else "not_reproduced")
Fewest grid cells per layer over the 56 readings: {'n II': 42, 'n III': 120, 'n V': 35, 'n VI': 70}
Rats contributing grid cells per layer (first reading): {'II': 5, 'III': 6, 'V': 3, 'VI': 3}
Readings with a grid cell in every layer: 56 of 56
Variant, each (rat, depth, hemisphere, name) once: fewest grid cells per layer {'II': 34, 'III': 60, 'V': 29, 'VI': 49} ; readings with a grid cell in every layer 56 of 56
Outcome: reproduced
Result: reproduced. Every layer has grid cells under all 56 readings: at least 42 in layer II, 120 in III, 35 in V and 70 in VI, from 5, 6, 3 and 3 rats under the first reading.
Counting each (rat, depth, hemisphere, name) once, every layer still has grid cells under all 56 readings, at least 34, 60, 29 and 49.
Claim gridness-range-similar¶
Among deeper-layer cells with periodic firing, the range of gridness did not differ from that of layer II cells.
Criterion: a claim of absence with no test named by the paper. Grid cells of each layer (readings O1 to O3). Readings of the test: (a) two-sided Mann-Whitney, layer II against layers III to VI pooled; (b) Kruskal-Wallis across the four layers; each at p < 0.05. Reproduced when no reading finds a difference, not reproduced when all do, methods_underspecified when they disagree.
def range_tests(frame):
rows = []
for T, G, _ in sg.readings(with_level=False):
col = f"grid|{T}|{G}"
g = frame[sg.classify(frame, T, G, 0.05).grid]
by = {l: g[g.layer == l][col] for l in LAYERS}
mw = stats.mannwhitneyu(by["II"], g[g.layer != "II"][col], alternative="two-sided").pvalue
kw = stats.kruskal(*by.values()).pvalue
pairwise = {f"pair {x}-{y} p": stats.mannwhitneyu(by[x], by[y], alternative="two-sided").pvalue
for x, y in combinations(LAYERS, 2)}
rows.append(dict(trials=T, gridness=G, mann_whitney_p=mw, kruskal_wallis_p=kw,
**{f"median {l}": by[l].median() for l in LAYERS},
**{f"range {l}": f"{by[l].min():.2f} to {by[l].max():.2f}" for l in LAYERS}, **pairwise))
return pd.DataFrame(rows)
range_table = range_tests(known)
print(range_table.drop(columns=[c for c in range_table if c.startswith("pair ")]).round(4).to_string())
tests = pd.concat([range_table.mann_whitney_p, range_table.kruskal_wallis_p])
print(f"\nTests finding a difference (p < 0.05): {int((tests < 0.05).sum())} of {len(tests)} "
f"(Mann-Whitney {int((range_table.mann_whitney_p < 0.05).sum())} of {len(range_table)}, Kruskal-Wallis "
f"{int((range_table.kruskal_wallis_p < 0.05).sum())} of {len(range_table)}); p from {tests.min():.2g} to {tests.max():.2g}")
print(f"Mann-Whitney p {range_table.mann_whitney_p.min():.3f} to {range_table.mann_whitney_p.max():.3f}; "
f"Kruskal-Wallis p {range_table.kruskal_wallis_p.min():.4f} to {range_table.kruskal_wallis_p.max():.4f}")
print("Median gridness of grid cells, range over readings:",
{l: f"{range_table[f'median {l}'].min():.2f} to {range_table[f'median {l}'].max():.2f}" for l in LAYERS})
# Where the Kruskal-Wallis difference lies: two-sided Mann-Whitney tests between pairs of layers. These are
# not the criterion's tests, are uncorrected, and do not decide the outcome.
print("\nPairs of layers, Mann-Whitney (not the criterion's tests, uncorrected), readings with p < 0.05 and p range:")
for x, y in combinations(LAYERS, 2):
p = range_table[f"pair {x}-{y} p"]
print(f" {x} against {y}: {int((p < 0.05).sum())} of {len(p)}, p {p.min():.2g} to {p.max():.2g}; median {x} above median {y} "
f"in {int((range_table[f'median {x}'] > range_table[f'median {y}']).sum())}")
found = tests < 0.05
print("Outcome:", "not_reproduced" if found.all() else "methods_underspecified" if found.any() else "reproduced")
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
rt = range_tests(once)
print(f"Variant, each (rat, depth, hemisphere, name) once: Mann-Whitney finds a difference in "
f"{int((rt.mann_whitney_p < 0.05).sum())} of {len(rt)} (p {rt.mann_whitney_p.min():.3f} to {rt.mann_whitney_p.max():.3f}), "
f"Kruskal-Wallis in {int((rt.kruskal_wallis_p < 0.05).sum())} of {len(rt)} (p {rt.kruskal_wallis_p.min():.4f} to {rt.kruskal_wallis_p.max():.4f})")
trials gridness mann_whitney_p kruskal_wallis_p median II median III median V median VI range II range III range V range VI
0 pooled 3cm|enclose|blob|max|area 0.0660 0.0470 0.3157 0.6324 0.3110 0.4619 0.02 to 1.33 0.01 to 1.40 0.01 to 1.35 0.00 to 1.38
1 pooled 3cm|enclose|blob|max|farthest 0.0311 0.0263 0.2831 0.5177 0.3050 0.4576 0.00 to 1.32 0.00 to 1.38 0.00 to 1.35 0.00 to 1.38
2 pooled 3cm|enclose|blob|centroid|area 0.2355 0.0398 0.3353 0.5387 0.2816 0.2856 0.02 to 1.33 0.00 to 1.37 0.00 to 1.32 0.00 to 1.38
3 pooled 3cm|enclose|blob|centroid|farthest 0.0734 0.0411 0.2813 0.4745 0.2521 0.4699 0.01 to 1.33 0.00 to 1.36 0.00 to 1.30 0.00 to 1.37
4 pooled 3cm|enclose|disc|max|area 0.0735 0.0225 0.3076 0.5791 0.2730 0.3896 0.02 to 1.33 0.00 to 1.40 0.01 to 1.35 0.00 to 1.38
5 pooled 3cm|enclose|disc|max|farthest 0.0237 0.0070 0.2955 0.5397 0.3702 0.3939 0.00 to 1.32 0.00 to 1.38 0.01 to 1.35 0.00 to 1.38
6 pooled 3cm|enclose|disc|centroid|area 0.2550 0.0135 0.3262 0.5510 0.2502 0.3056 0.02 to 1.33 0.00 to 1.37 0.00 to 1.32 0.00 to 1.38
7 pooled 3cm|enclose|disc|centroid|farthest 0.0509 0.0246 0.2940 0.4887 0.3535 0.4703 0.01 to 1.33 0.00 to 1.36 0.00 to 1.30 0.00 to 1.38
8 pooled 3cm|centre|blob|max|area 0.2348 0.0150 0.4381 0.7089 0.4177 0.3492 0.01 to 1.17 0.00 to 1.36 0.00 to 1.15 0.00 to 1.22
9 pooled 3cm|centre|blob|centroid|area 0.5684 0.0029 0.5648 0.7627 0.4260 0.3800 0.00 to 1.25 0.00 to 1.49 0.00 to 1.18 0.01 to 1.22
10 pooled 3cm|centre|disc|max|area 0.4142 0.0026 0.4355 0.6838 0.3566 0.3056 0.01 to 1.17 0.01 to 1.36 0.01 to 1.15 0.00 to 1.22
11 pooled 3cm|centre|disc|max|farthest 0.1732 0.0056 0.4536 0.7206 0.4183 0.3888 0.01 to 1.17 0.00 to 1.36 0.00 to 1.15 0.00 to 1.23
12 pooled 3cm|centre|disc|centroid|area 0.8949 0.0010 0.5655 0.7211 0.3547 0.3880 0.00 to 1.25 0.01 to 1.49 0.00 to 1.18 0.00 to 1.22
13 pooled 3cm|centre|disc|centroid|farthest 0.5906 0.0088 0.5752 0.7840 0.4707 0.4779 0.00 to 1.25 0.01 to 1.49 0.00 to 1.18 0.01 to 1.23
14 pooled 1.5cm|enclose|blob|max|area 0.0133 0.0085 0.2990 0.4898 0.2472 0.4597 0.01 to 1.35 0.00 to 1.39 0.01 to 1.35 0.00 to 1.39
15 pooled 1.5cm|enclose|blob|max|farthest 0.0203 0.0085 0.2275 0.4374 0.2258 0.3840 0.00 to 1.34 0.00 to 1.36 0.00 to 1.31 0.00 to 1.38
16 pooled 1.5cm|enclose|blob|centroid|area 0.1847 0.0122 0.3116 0.4981 0.2320 0.4148 0.00 to 1.35 0.01 to 1.38 0.00 to 1.33 0.01 to 1.39
17 pooled 1.5cm|enclose|blob|centroid|farthest 0.0537 0.0304 0.2548 0.4248 0.2260 0.4284 0.00 to 1.34 0.00 to 1.36 0.00 to 1.31 0.00 to 1.38
18 pooled 1.5cm|enclose|disc|max|area 0.0244 0.0067 0.2768 0.4796 0.2354 0.4036 0.02 to 1.35 0.01 to 1.39 0.01 to 1.35 0.00 to 1.40
19 pooled 1.5cm|enclose|disc|max|farthest 0.0087 0.0042 0.2278 0.4744 0.3396 0.3842 0.00 to 1.34 0.01 to 1.49 0.00 to 1.31 0.00 to 1.38
20 pooled 1.5cm|enclose|disc|centroid|area 0.2650 0.0023 0.3042 0.4888 0.2067 0.3307 0.00 to 1.35 0.00 to 1.38 0.00 to 1.33 0.01 to 1.39
21 pooled 1.5cm|enclose|disc|centroid|farthest 0.0911 0.0253 0.2934 0.4305 0.2730 0.3842 0.01 to 1.34 0.00 to 1.49 0.01 to 1.31 0.00 to 1.38
22 pooled 1.5cm|centre|blob|max|area 0.1968 0.0401 0.4200 0.6090 0.3757 0.3244 0.00 to 1.14 0.00 to 1.39 0.00 to 1.15 0.01 to 1.25
23 pooled 1.5cm|centre|blob|centroid|area 0.6376 0.0081 0.5668 0.7457 0.3983 0.3890 0.00 to 1.26 0.01 to 1.49 0.00 to 1.19 0.00 to 1.21
24 pooled 1.5cm|centre|disc|max|area 0.1975 0.0043 0.4091 0.5856 0.3231 0.3186 0.00 to 1.14 0.02 to 1.39 0.01 to 1.15 0.01 to 1.25
25 pooled 1.5cm|centre|disc|max|farthest 0.0963 0.0080 0.4332 0.6524 0.4278 0.3995 0.00 to 1.14 0.02 to 1.49 0.01 to 1.16 0.00 to 1.26
26 pooled 1.5cm|centre|disc|centroid|area 0.6949 0.0012 0.5539 0.7175 0.3314 0.3827 0.00 to 1.26 0.01 to 1.49 0.01 to 1.19 0.00 to 1.21
27 pooled 1.5cm|centre|disc|centroid|farthest 0.4600 0.0194 0.5736 0.7726 0.4689 0.5347 0.00 to 1.26 0.01 to 1.49 0.00 to 1.19 0.00 to 1.22
28 first 3cm|enclose|blob|max|area 0.1321 0.0450 0.3157 0.5710 0.3050 0.3864 0.02 to 1.33 0.01 to 1.40 0.03 to 1.35 0.00 to 1.38
29 first 3cm|enclose|blob|max|farthest 0.0314 0.0333 0.2290 0.5215 0.2759 0.3488 0.00 to 1.32 0.00 to 1.38 0.00 to 1.35 0.00 to 1.38
30 first 3cm|enclose|blob|centroid|area 0.2814 0.0186 0.3275 0.5387 0.2778 0.2842 0.02 to 1.33 0.00 to 1.37 0.01 to 1.32 0.00 to 1.38
31 first 3cm|enclose|blob|centroid|farthest 0.1722 0.0817 0.2905 0.4719 0.2693 0.3072 0.01 to 1.33 0.00 to 1.36 0.00 to 1.30 0.01 to 1.37
32 first 3cm|enclose|disc|max|area 0.1234 0.0172 0.3076 0.5501 0.2666 0.3533 0.02 to 1.33 0.00 to 1.40 0.01 to 1.35 0.00 to 1.38
33 first 3cm|enclose|disc|max|farthest 0.0263 0.0066 0.2335 0.5363 0.3544 0.3066 0.00 to 1.32 0.00 to 1.38 0.03 to 1.35 0.00 to 1.38
34 first 3cm|enclose|disc|centroid|area 0.2754 0.0049 0.3181 0.5557 0.2458 0.3056 0.02 to 1.33 0.00 to 1.37 0.01 to 1.32 0.00 to 1.38
35 first 3cm|enclose|disc|centroid|farthest 0.1188 0.0417 0.3028 0.4887 0.3497 0.3058 0.02 to 1.33 0.00 to 1.36 0.00 to 1.30 0.00 to 1.38
36 first 3cm|centre|blob|max|area 0.2205 0.0087 0.4363 0.6950 0.3928 0.2883 0.01 to 1.17 0.00 to 1.34 0.00 to 1.16 0.00 to 1.20
37 first 3cm|centre|blob|centroid|area 0.7152 0.0003 0.5209 0.7274 0.3142 0.3150 0.00 to 1.18 0.00 to 1.43 0.00 to 1.18 0.01 to 1.22
38 first 3cm|centre|disc|max|area 0.3401 0.0008 0.4349 0.6802 0.2732 0.2910 0.02 to 1.17 0.01 to 1.33 0.01 to 1.16 0.00 to 1.21
39 first 3cm|centre|disc|max|farthest 0.1172 0.0035 0.4432 0.7113 0.3938 0.3827 0.00 to 1.17 0.00 to 1.34 0.00 to 1.16 0.00 to 1.21
40 first 3cm|centre|disc|centroid|area 0.9930 0.0001 0.5207 0.7189 0.2619 0.3096 0.00 to 1.18 0.00 to 1.43 0.01 to 1.18 0.00 to 1.22
41 first 3cm|centre|disc|centroid|farthest 0.6893 0.0034 0.5261 0.7332 0.3239 0.3768 0.00 to 1.18 0.00 to 1.43 0.01 to 1.18 0.00 to 1.23
42 first 1.5cm|enclose|blob|max|area 0.0729 0.0281 0.3043 0.4796 0.2259 0.4605 0.00 to 1.35 0.00 to 1.39 0.01 to 1.35 0.01 to 1.39
43 first 1.5cm|enclose|blob|max|farthest 0.0834 0.0161 0.2275 0.4518 0.1688 0.3259 0.00 to 1.34 0.00 to 1.36 0.01 to 1.31 0.00 to 1.38
44 first 1.5cm|enclose|blob|centroid|area 0.2984 0.0375 0.3101 0.4887 0.2286 0.3907 0.03 to 1.35 0.00 to 1.38 0.01 to 1.33 0.00 to 1.39
45 first 1.5cm|enclose|blob|centroid|farthest 0.2285 0.1563 0.2913 0.4248 0.2537 0.3151 0.00 to 1.34 0.00 to 1.36 0.00 to 1.31 0.01 to 1.38
46 first 1.5cm|enclose|disc|max|area 0.1317 0.0193 0.2993 0.4612 0.2245 0.4006 0.00 to 1.35 0.00 to 1.39 0.01 to 1.35 0.01 to 1.40
47 first 1.5cm|enclose|disc|max|farthest 0.0575 0.0056 0.2278 0.4770 0.2670 0.2919 0.00 to 1.34 0.01 to 1.49 0.00 to 1.31 0.00 to 1.38
48 first 1.5cm|enclose|disc|centroid|area 0.3837 0.0065 0.3042 0.4796 0.2155 0.3724 0.03 to 1.35 0.00 to 1.38 0.00 to 1.33 0.00 to 1.39
49 first 1.5cm|enclose|disc|centroid|farthest 0.3879 0.1590 0.3071 0.4290 0.2788 0.2986 0.03 to 1.34 0.00 to 1.49 0.00 to 1.31 0.00 to 1.38
50 first 1.5cm|centre|blob|max|area 0.2175 0.0341 0.4323 0.5456 0.3315 0.3058 0.00 to 1.14 0.00 to 1.31 0.00 to 1.15 0.01 to 1.25
51 first 1.5cm|centre|blob|centroid|area 0.7466 0.0013 0.5340 0.7263 0.2879 0.2981 0.00 to 1.18 0.00 to 1.42 0.00 to 1.19 0.00 to 1.21
52 first 1.5cm|centre|disc|max|area 0.1758 0.0012 0.4201 0.5484 0.2330 0.3177 0.00 to 1.14 0.00 to 1.31 0.01 to 1.15 0.01 to 1.25
53 first 1.5cm|centre|disc|max|farthest 0.0711 0.0081 0.4338 0.6483 0.3661 0.4067 0.00 to 1.14 0.00 to 1.49 0.01 to 1.16 0.00 to 1.26
54 first 1.5cm|centre|disc|centroid|area 0.7600 0.0001 0.5330 0.7024 0.2492 0.3024 0.00 to 1.18 0.00 to 1.43 0.01 to 1.19 0.00 to 1.21
55 first 1.5cm|centre|disc|centroid|farthest 0.4188 0.0042 0.5497 0.7466 0.2948 0.4572 0.00 to 1.18 0.01 to 1.43 0.00 to 1.19 0.01 to 1.22
Tests finding a difference (p < 0.05): 61 of 112 (Mann-Whitney 8 of 56, Kruskal-Wallis 53 of 56); p from 8.3e-05 to 0.99
Mann-Whitney p 0.009 to 0.993; Kruskal-Wallis p 0.0001 to 0.1590
Median gridness of grid cells, range over readings: {'II': '0.23 to 0.58', 'III': '0.42 to 0.78', 'V': '0.17 to 0.47', 'VI': '0.28 to 0.53'}
Pairs of layers, Mann-Whitney (not the criterion's tests, uncorrected), readings with p < 0.05 and p range:
II against III: 46 of 56, p 0.00049 to 0.092; median II above median III in 0
II against V: 2 of 56, p 0.027 to 1; median II above median V in 48
II against VI: 0 of 56, p 0.094 to 0.97; median II above median VI in 29
III against V: 49 of 56, p 7.3e-05 to 0.08; median III above median V in 56
III against VI: 25 of 56, p 0.00048 to 0.55; median III above median VI in 55
V against VI: 2 of 56, p 0.02 to 0.87; median V above median VI in 13
Outcome: methods_underspecified
Variant, each (rat, depth, hemisphere, name) once: Mann-Whitney finds a difference in 3 of 56 (p 0.030 to 0.884), Kruskal-Wallis in 10 of 56 (p 0.0194 to 0.5808)
Result: blocked, methods_underspecified. The two tests the criterion names disagree. The Mann-Whitney test of layer II against the deeper layers finds a difference under 8 of the 56 readings (p 0.009 to 0.993), while the Kruskal-Wallis test across the four layers finds one under 53 of 56 (p 0.0001 to 0.159). The median gridness of grid cells is 0.42 to 0.78 in layer III, 0.23 to 0.58 in layer II, 0.28 to 0.53 in layer VI and 0.17 to 0.47 in layer V, and the layer III median is above the layer II median under all 56 readings. Pairwise Mann-Whitney tests, which are not the criterion's tests and are uncorrected, find layer II different from layer III under 46 of 56 readings, layer III from layer V under 49 and from layer VI under 25, but layer II from layer V under 2 and from layer VI under none. So the Kruskal-Wallis difference comes mostly from layer III, whose grid cells have higher gridness than those of layer II as well as those of layers V and VI; pooling layer III with layers V and VI hides this from the Mann-Whitney test. The ranges themselves are alike: in every layer they run from about 0 to between 1.14 and 1.49. With the radius reading added the tests still disagree (2 of 32 and 32 of 32 before). Counting each (rat, depth, hemisphere, name) once, the Mann-Whitney test finds a difference under 3 of 56 readings and the Kruskal-Wallis test under 10 of 56, so the tests still disagree.
Claim grid-stable¶
All 203 grid cells had stable periodic firing patterns within and between trials.
Criterion: grid cells as in layer2-grid. Between trials, for cells recorded in two trials (the
20-minute files): (a) a positive map correlation with Pearson p < 0.05, or (b) gridness above 0
in each trial's map. Within a trial: the same tests on the maps of (a) the first and second halves,
(b) odd and even minutes, of each trial. Reproduced when every grid cell passes under every
reading; partially reproduced when at least 90% do; methods_underspecified when the readings
disagree. A cell passes a combination when it passes the within-trial reading in each of its
trials and, if it was recorded in two trials, the between-trial reading. Under O1 (b) only the
first trial is used within trials.
def stability(frame, T, G):
bname, rest = G.split("|", 1)
g = frame[sg.classify(frame, T, G, 0.05).grid]
within_tests = {}
for split, test in product(("halves", "minutes"), ("correlation", "gridness")):
ok = pd.Series(True, index=g.index)
for k in ([0] if T == "first" else [0, 1]):
has = g.trials > k
if test == "correlation":
passed = (g[f"within|{k}|{split}|{bname}|r"] > 0) & (g[f"within|{k}|{split}|{bname}|p"] < 0.05)
else:
passed = (g[f"within|{k}|{split}|{bname}|0|{rest}"] > 0) & (g[f"within|{k}|{split}|{bname}|1|{rest}"] > 0)
ok &= passed | ~has
within_tests[(split, test)] = ok
two = g.trials == 2
between_tests = {
"correlation": (g[f"between|{bname}|r"] > 0) & (g[f"between|{bname}|p"] < 0.05),
"gridness": (g[f"grid|first|{G}"] > 0) & (g[f"between|{bname}|trial2|{rest}"] > 0),
}
rows = []
for (split, wt), w in within_tests.items():
for bt, b in between_tests.items():
stable = w & (b | ~two)
rows.append(dict(trials=T, gridness=G, within=f"{split}, {wt}", between=bt, grid_cells=len(g),
two_trial_cells=int(two.sum()), within_pass=w.mean(),
between_pass=b[two].mean() if two.any() else np.nan,
stable=stable.mean()))
return rows
stab = pd.DataFrame([r for T, G, _ in sg.readings(with_level=False) for r in stability(known, T, G)])
stab["outcome"] = np.select([stab.stable >= 1, stab.stable >= 0.9], ["reproduced", "partially_reproduced"], "not_reproduced")
print("Under the first gridness reading (all eight test combinations):")
print(stab[(stab.trials == "pooled") & (stab.gridness == sg.GRID_READINGS[0])].round(3).to_string())
summary = stab.groupby(["within", "between"]).agg(stable_min=("stable", "min"), stable_max=("stable", "max"),
within_min=("within_pass", "min"), within_max=("within_pass", "max"),
between_min=("between_pass", "min"), between_max=("between_pass", "max"))
print(f"\nRange over the {stab.groupby(['trials', 'gridness']).ngroups} grid readings:")
print(summary.round(3).to_string())
print(f"\nGrid cells: {stab.grid_cells.min()} to {stab.grid_cells.max()} (paper 203 +/- 20); in two-trial files "
f"{stab.two_trial_cells.min()} to {stab.two_trial_cells.max()}")
print(f"Outcomes over {len(stab)} combinations:", stab.outcome.value_counts().to_dict(), "->", outcome_of(stab.outcome))
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
stab_once = pd.DataFrame([r for T, G, _ in sg.readings(with_level=False) for r in stability(once, T, G)])
stab_once["outcome"] = np.select([stab_once.stable >= 1, stab_once.stable >= 0.9], ["reproduced", "partially_reproduced"], "not_reproduced")
print(f"Variant, each (rat, depth, hemisphere, name) once: grid cells {stab_once.grid_cells.min()} to {stab_once.grid_cells.max()}, "
f"stable {stab_once.stable.min():.3f} to {stab_once.stable.max():.3f}; outcomes",
stab_once.outcome.value_counts().to_dict(), "->", outcome_of(stab_once.outcome))
Under the first gridness reading (all eight test combinations):
trials gridness within between grid_cells two_trial_cells within_pass between_pass stable outcome
0 pooled 3cm|enclose|blob|max|area halves, correlation correlation 280 75 0.882 0.867 0.882 not_reproduced
1 pooled 3cm|enclose|blob|max|area halves, correlation gridness 280 75 0.882 0.600 0.829 not_reproduced
2 pooled 3cm|enclose|blob|max|area halves, gridness correlation 280 75 0.575 0.867 0.575 not_reproduced
3 pooled 3cm|enclose|blob|max|area halves, gridness gridness 280 75 0.575 0.600 0.575 not_reproduced
4 pooled 3cm|enclose|blob|max|area minutes, correlation correlation 280 75 0.918 0.867 0.918 partially_reproduced
5 pooled 3cm|enclose|blob|max|area minutes, correlation gridness 280 75 0.918 0.600 0.871 not_reproduced
6 pooled 3cm|enclose|blob|max|area minutes, gridness correlation 280 75 0.607 0.867 0.607 not_reproduced
7 pooled 3cm|enclose|blob|max|area minutes, gridness gridness 280 75 0.607 0.600 0.604 not_reproduced
Range over the 56 grid readings:
stable_min stable_max within_min within_max between_min between_max
within between
halves, correlation correlation 0.871 0.930 0.872 0.930 0.836 0.949
gridness 0.804 0.867 0.872 0.930 0.494 0.703
halves, gridness correlation 0.491 0.617 0.491 0.617 0.836 0.949
gridness 0.485 0.605 0.491 0.617 0.494 0.703
minutes, correlation correlation 0.902 0.963 0.902 0.971 0.836 0.949
gridness 0.836 0.904 0.902 0.971 0.494 0.703
minutes, gridness correlation 0.510 0.652 0.510 0.655 0.836 0.949
gridness 0.507 0.638 0.510 0.655 0.494 0.703
Grid cells: 277 to 329 (paper 203 +/- 20); in two-trial files 71 to 85
Outcomes over 448 combinations: {'not_reproduced': 364, 'partially_reproduced': 84} -> methods_underspecified (not_reproduced, partially_reproduced)
Variant, each (rat, depth, hemisphere, name) once: grid cells 179 to 214, stable 0.458 to 0.974; outcomes {'not_reproduced': 377, 'partially_reproduced': 71} -> methods_underspecified (not_reproduced, partially_reproduced)
Result: blocked, methods_underspecified. No combination of readings has every grid cell stable, and the readings split between the two lower outcomes: 84 of the 448 combinations give partially reproduced (at least 90% stable) and 364 give not reproduced. With map correlations within trials (odd and even minutes) and between trials, 90.2% to 96.3% of grid cells are stable; with gridness above 0 required in both parts of each trial, 49% to 65% are. Between trials, 84% to 95% of the 71 to 85 grid cells in two-trial files have a significant positive map correlation, and 49% to 70% have gridness above 0 in both trials. The number of grid cells, 277 to 329, is outside 203 +/- 20 under every reading. With the radius reading added the split is the same as before (51 partially reproduced and 205 not reproduced of 256).
Counting each (rat, depth, hemisphere, name) once (179 to 214 grid cells), 71 combinations give partially reproduced and 377 not reproduced, so the readings still disagree.
Claims spacing-dorsoventral and fieldsize-dorsoventral: blocked¶
Both need each recording site's distance from the postrhinal border, measured in the histology
(SOM, Histology; fig. S3). The units table has depth ("the depth of the inserted electrodes in
meters"), the depth the tetrodes were lowered to, which is not that distance, and no other column
or file gives it; the electrodes table has one row per file, location "MEC", with no coordinates
(data check). Both claims are blocked with data_missing.
Claim grid-phase-orientation¶
In all layers the spatial phase of nearby grid cells was distributed (their vertices were offset), while grid orientation was consistent across simultaneously recorded cells.
Criterion: pairs of grid cells recorded together (same file and trial). Orientation, compared modulo 60 degrees, is consistent when the within-file pairwise differences are smaller than those between files of different rats, by a one-sided Mann-Whitney test at p < 0.05 (a standard choice named in the claim). The pairs of different rats are taken (a) within the same layer, (b) from all layers (readings, as answered). Phase is distributed when for more than half the pairs the peak of the rate-map crosscorrelogram nearest the centre is displaced from the centre by (a) more than a fifth of the grid spacing (the mean of the two cells'), (b) more than the 3 cm bin. Reproduced when both parts hold under every reading, readings O1 to O3 included, in each layer with at least one simultaneous pair. As the criterion judges each layer, pairs are of cells of the same layer; pairs of cells of different layers are counted below and left out. The criterion names no outcome but reproduced, and the claims file's rule for readings applies: a claim that holds under some readings and not under others is methods_underspecified.
def phase_orientation(T, G, frame=known):
bname, _, _, loc, _ = G.split("|")
g = frame[sg.classify(frame, T, G, 0.05).grid]
info = g.set_index(["path", "unit"])[[f"orientation|{T}|{bname}|{loc}", f"spacing|{T}|{bname}|{loc}", "layer", "subject"]]
info.columns = ["orientation", "spacing", "layer", "subject"]
p = pairs.join(info.add_suffix("_a"), on=["path", "unit_a"], how="inner").join(info.add_suffix("_b"), on=["path", "unit_b"], how="inner")
p = p[p.layer_a == p.layer_b]
d = np.abs(p.orientation_a - p.orientation_b) % 60
p = p.assign(d_orient=np.minimum(d, 60 - d), offset=p[f"offset|{T}|{bname}|{loc}"],
fifth=(p.spacing_a + p.spacing_b) / 2 / 5)
rows = []
for l in LAYERS:
q = p[p.layer_a == l]
if len(q) == 0:
continue
between = {}
for pool, cl in (("layer", g[g.layer == l]), ("all layers", g)):
o = cl[f"orientation|{T}|{bname}|{loc}"].to_numpy() % 60
s = cl.subject.to_numpy()
i, j = np.triu_indices(len(cl), 1)
dd = np.abs(o[i] - o[j])[s[i] != s[j]]
between[pool] = np.minimum(dd, 60 - dd)
p_of = {pool: stats.mannwhitneyu(q.d_orient, b, alternative="less").pvalue if len(b) else np.nan
for pool, b in between.items()}
rows.append(dict(trials=T, gridness=G, layer=l, pairs=len(q), files=q.path.nunique(), rats=q.subject.nunique(),
median_within=q.d_orient.median(), median_between_rats=np.median(between["layer"]),
orientation_p=p_of["layer"], orientation_p_all_layers=p_of["all layers"],
displaced_fifth=(q.offset > q.fifth).mean(), displaced_3cm=(q.offset > 3).mean(),
no_peak=int(q.offset.isna().sum())))
return rows
po = pd.DataFrame([r for T, G, _ in sg.readings(with_level=False) for r in phase_orientation(T, G)])
po["orientation_holds"] = po.orientation_p < 0.05
po["orientation_holds_all_layers"] = po.orientation_p_all_layers < 0.05
po["phase_holds"] = (po.displaced_fifth > 0.5) & (po.displaced_3cm > 0.5)
print("First gridness reading, by layer:")
print(po[(po.trials == "pooled") & (po.gridness == sg.GRID_READINGS[0])].round(3).to_string())
print(f"\nBy layer over the {po.groupby(['trials', 'gridness']).ngroups} readings:")
print(po.groupby("layer").agg(pairs_min=("pairs", "min"), pairs_max=("pairs", "max"), rats=("rats", "max"),
orientation_p_min=("orientation_p", "min"), orientation_p_max=("orientation_p", "max"),
p_all_layers_max=("orientation_p_all_layers", "max"),
orientation_holds=("orientation_holds", "mean"),
displaced_fifth_min=("displaced_fifth", "min"), displaced_fifth_max=("displaced_fifth", "max"),
displaced_3cm_min=("displaced_3cm", "min"), displaced_3cm_max=("displaced_3cm", "max"),
median_within=("median_within", "median"), median_between=("median_between_rats", "median")).round(3).to_string())
po["phase_fifth"] = po.displaced_fifth > 0.5
po["phase_3cm"] = po.displaced_3cm > 0.5
def judge_readings(po):
rows = []
for (T, G), x in po.groupby(["trials", "gridness"]):
for pool, col in (("same layer", "orientation_holds"), ("all layers", "orientation_holds_all_layers")):
for phase in ("phase_fifth", "phase_3cm"):
rows.append(dict(trials=T, gridness=G, pool=pool, phase=phase, both_every_layer=bool((x[col] & x[phase]).all()),
phase_every_layer=bool(x[phase].all()),
orientation_layers=",".join(x[x[col]].layer), orientation_fails=",".join(x[~x[col]].layer)))
return pd.DataFrame(rows)
po_readings = judge_readings(po)
print(f"\nReadings (trials, gridness, pool, phase): {len(po_readings)}; both parts hold in every layer under "
f"{int(po_readings.both_every_layer.sum())}; phase holds in every layer under {int(po_readings.phase_every_layer.sum())}")
print(po_readings.groupby(["pool", "phase"]).both_every_layer.agg(["sum", "size"]).to_string())
print("Layers where orientation fails, readings:", po_readings.orientation_fails.str.split(",").explode().replace("", np.nan).dropna().value_counts().to_dict())
for l in LAYERS:
for pool, col in (("same layer", "orientation_holds"), ("all layers", "orientation_holds_all_layers")):
x = po[po.layer == l]
print(f" layer {l}, other rats from {pool}: orientation holds under {int(x[col].sum())} of {len(x)} readings")
# Reproduced when both parts hold in every layer under every reading (the criterion). The criterion names no
# other outcome, and a claim that holds under some readings and not others is methods_underspecified (claims file).
print("Outcome:", "reproduced" if po_readings.both_every_layer.all() else
"methods_underspecified" if po_readings.both_every_layer.any() else "not_reproduced")
first_grid = known[sg.classify(known, READING[0], READING[1], 0.05).grid].set_index(["path", "unit"]).layer
pl = (pairs.join(first_grid.rename("layer_a"), on=["path", "unit_a"], how="inner")
.join(first_grid.rename("layer_b"), on=["path", "unit_b"], how="inner"))
print(f"Simultaneous grid pairs, first reading: {len(pl)}, of which {int((pl.layer_a != pl.layer_b).sum())} pair cells of "
f"different layers and are not counted in any layer")
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
po_once = pd.DataFrame([r for T, G, _ in sg.readings(with_level=False) for r in phase_orientation(T, G, once)])
po_once = po_once.assign(orientation_holds=po_once.orientation_p < 0.05, orientation_holds_all_layers=po_once.orientation_p_all_layers < 0.05,
phase_fifth=po_once.displaced_fifth > 0.5, phase_3cm=po_once.displaced_3cm > 0.5)
por_once = judge_readings(po_once)
print("\nVariant, each (rat, depth, hemisphere, name) once, by layer:")
print(po_once.groupby("layer").agg(pairs_min=("pairs", "min"), pairs_max=("pairs", "max"), readings=("pairs", "size"),
orientation_holds=("orientation_holds", "sum"), orientation_holds_all_layers=("orientation_holds_all_layers", "sum"),
phase_fifth=("phase_fifth", "sum"), phase_3cm=("phase_3cm", "sum")).to_string())
print(f" both parts hold in every layer under {int(por_once.both_every_layer.sum())} of {len(por_once)} readings; outcome:",
"reproduced" if por_once.both_every_layer.all() else "methods_underspecified" if por_once.both_every_layer.any() else "not_reproduced")
First gridness reading, by layer:
trials gridness layer pairs files rats median_within median_between_rats orientation_p orientation_p_all_layers displaced_fifth displaced_3cm no_peak orientation_holds orientation_holds_all_layers phase_holds
0 pooled 3cm|enclose|blob|max|area II 108 10 5 7.882 14.866 0.00 0.000 0.815 0.981 0 True True True
1 pooled 3cm|enclose|blob|max|area III 267 27 6 7.474 14.621 0.00 0.000 0.850 0.959 0 True True True
2 pooled 3cm|enclose|blob|max|area V 22 8 3 3.963 14.843 0.00 0.000 0.864 1.000 0 True True True
3 pooled 3cm|enclose|blob|max|area VI 70 21 3 14.293 14.940 0.14 0.236 0.929 1.000 0 False False True
By layer over the 56 readings:
pairs_min pairs_max rats orientation_p_min orientation_p_max p_all_layers_max orientation_holds displaced_fifth_min displaced_fifth_max displaced_3cm_min displaced_3cm_max median_within median_between
layer
II 103 140 5 0.0 0.000 0.000 1.000 0.782 0.856 0.976 0.993 6.150 14.995
III 216 283 7 0.0 0.000 0.000 1.000 0.825 0.870 0.956 0.989 7.885 14.358
V 22 45 3 0.0 0.503 0.520 0.750 0.750 0.941 0.926 1.000 8.858 15.962
VI 67 156 3 0.0 0.377 0.516 0.625 0.868 0.985 0.947 1.000 13.597 15.371
Readings (trials, gridness, pool, phase): 224; both parts hold in every layer under 88; phase holds in every layer under 224
sum size
pool phase
all layers phase_3cm 19 56
phase_fifth 19 56
same layer phase_3cm 25 56
phase_fifth 25 56
Layers where orientation fails, readings: {'VI': 106, 'V': 58}
layer II, other rats from same layer: orientation holds under 56 of 56 readings
layer II, other rats from all layers: orientation holds under 56 of 56 readings
layer III, other rats from same layer: orientation holds under 56 of 56 readings
layer III, other rats from all layers: orientation holds under 56 of 56 readings
layer V, other rats from same layer: orientation holds under 42 of 56 readings
layer V, other rats from all layers: orientation holds under 41 of 56 readings
layer VI, other rats from same layer: orientation holds under 35 of 56 readings
layer VI, other rats from all layers: orientation holds under 24 of 56 readings
Outcome: methods_underspecified
Simultaneous grid pairs, first reading: 487, of which 20 pair cells of different layers and are not counted in any layer
Variant, each (rat, depth, hemisphere, name) once, by layer:
pairs_min pairs_max readings orientation_holds orientation_holds_all_layers phase_fifth phase_3cm
layer
II 87 117 56 56 56 56 56
III 53 74 56 47 54 56 56
V 19 28 56 43 39 56 56
VI 35 110 56 38 21 56 56
both parts hold in every layer under 66 of 224 readings; outcome: methods_underspecified
Result: blocked, methods_underspecified. The phase part holds in every layer under every reading: 75% to 98.5% of simultaneous pairs have the crosscorrelogram peak nearest the centre displaced by more than a fifth of the spacing, and 92.6% to 100% by more than 3 cm. Orientation is more alike within files than between rats in layers II and III under every reading (p below 0.001), but not under every reading in layers V (p up to 0.50 with the layer's own rats and 0.52 with all layers; holds under 42 and 41 of 56 readings) and VI (p up to 0.34 and 0.50; 35 and 23 of 56), which come from three rats each. Both parts hold in every layer under 90 of the 224 readings (26 of 56 with the layer's own rats and 19 of 56 with all layers, under each phase reading), and not under the other 134. These counts rest on p-values near 0.05, and runs of the same code on other machines have differed from them by a reading or two (not run here); the outcome does not depend on them. The criterion names no outcome for this case, so the claims file's rule applies and the readings' disagreement blocks the claim. 20 of the 487 simultaneous grid pairs of the first reading pair cells of different layers and are not counted.
The previous version called this claim partially reproduced, an outcome the criterion does not name; the verifier pointed this out, and the outcome is now the one the claims file's rule gives. It is the outcome this claim had before the answer, for the same reason.
Counting each (rat, depth, hemisphere, name) once, the phase part still holds in every layer under every reading, orientation holds under 56, 47, 43 and 38 of 56 readings in layers II, III, V and VI with the layer's own rats (56, 54, 39 and 21 with all layers), and both parts hold in every layer under 66 of 224 readings, so the readings still disagree.
Claim hd-layers¶
No head-direction cells were found in layer II, whereas a large proportion of the cells in layers III to VI had directional preferences (273 of 385 cells, counting conjunctive cells).
Criterion: cells of two-LED files only; head-direction cells by the SOM criteria (Watson U2 significant and the odd/even-minute correlation significant) with readings O1 and O4; directional cells are head-direction plus conjunctive cells. Reproduced when no layer II cell passes and the fractions are within tolerance (deeper 0.709 +/- 0.071; III 0.669 +/- 0.067; V 0.878 +/- 0.088; VI 0.669 +/- 0.067); partially reproduced when no layer II cell passes and the deeper fraction is above 0.5 but a number is outside tolerance. The dandiset holds the non-grid cells too (data check), so the fractions can be formed.
EXPECTED_HD = {"III to VI": (0.709, 0.071), "III": (0.669, 0.067), "V": (0.878, 0.088), "VI": (0.669, 0.067)}
tracked = known[known.two_leds].copy()
rows = []
for T, _, a in sg.readings(with_grid=False):
hd = sg.classify(tracked, T, sg.GRID_READINGS[0], a).hd
frac = {l: hd[tracked.layer == l].mean() for l in LAYERS}
frac["III to VI"] = hd[tracked.layer != "II"].mean()
n_ii = int(hd[tracked.layer == "II"].sum())
tol = all(within(frac[k], *EXPECTED_HD[k]) for k in EXPECTED_HD)
rows.append(dict(trials=T, level=a, layer_II=f"{n_ii}/{(tracked.layer == 'II').sum()}",
**{k: frac[k] for k in ["III to VI", "III", "V", "VI"]},
deeper=f"{int(hd[tracked.layer != 'II'].sum())}/{(tracked.layer != 'II').sum()}",
u2_significant_II=int((tracked[f"u2_p|{T}"] < a)[tracked.layer == "II"].sum()),
block_significant_II=int(((tracked[f"block_r|{T}"] > 0) & (tracked[f"block_p|{T}"] < a))[tracked.layer == "II"].sum()),
outcome="reproduced" if n_ii == 0 and tol else
"partially_reproduced" if n_ii == 0 and frac["III to VI"] > 0.5 else "not_reproduced"))
hd_table = pd.DataFrame(rows)
print("Two-LED cells per layer:", tracked.layer.value_counts()[LAYERS].to_dict())
print(hd_table.round(3).to_string())
print("Outcomes:", hd_table.outcome.value_counts().to_dict(), "->", outcome_of(hd_table.outcome))
for T, _, a in sg.readings(with_grid=False):
hd = sg.classify(tracked, T, sg.GRID_READINGS[0], a).hd
passing = tracked[hd & (tracked.layer == "II")]
print(f" layer II cells passing, {T}, {a}: rats {passing.subject.value_counts().to_dict()}, "
f"files {sorted(passing.session.unique())}, U2 {passing[f'u2|{T}'].min():.2f} to {passing[f'u2|{T}'].max():.2f}")
Two-LED cells per layer: {'II': 43, 'III': 182, 'V': 79, 'VI': 143}
trials level layer_II III to VI III V VI deeper u2_significant_II block_significant_II outcome
0 pooled 0.05 9/43 0.733 0.599 0.937 0.790 296/404 41 9 not_reproduced
1 pooled 0.01 7/43 0.678 0.527 0.924 0.734 274/404 39 7 not_reproduced
2 first 0.05 4/43 0.723 0.599 0.937 0.762 292/404 41 4 not_reproduced
3 first 0.01 2/43 0.671 0.527 0.924 0.713 271/404 41 2 not_reproduced
Outcomes: {'not_reproduced': 4} -> not_reproduced
layer II cells passing, pooled, 0.05: rats {'11265': 9}, files ['16030601', '16030604'], U2 0.86 to 9.77
layer II cells passing, pooled, 0.01: rats {'11265': 7}, files ['16030601', '16030604'], U2 1.38 to 9.77
layer II cells passing, first, 0.05: rats {'11265': 4}, files ['16030601', '16030604'], U2 0.36 to 4.99
layer II cells passing, first, 0.01: rats {'11265': 2}, files ['16030604'], U2 4.51 to 4.99
tracked["hd"] = sg.classify(tracked, READING[0], READING[1], READING[2]).hd
ii = tracked[(tracked.layer == "II")]
print(f"Layer II cells passing, reading {READING[0]}, {READING[2]}:")
print(ii[ii.hd][["subject", "session", "unit", "u2|pooled", "block_r|pooled", "block_p|pooled",
"grid|pooled|" + READING[1], "mv_r|pooled", "peak_rate|pooled"]].round(4).to_string())
print("\nPer rat and layer (two-LED cells):")
print(tracked.groupby(["subject", "layer"]).hd.agg(directional="sum", cells="size").unstack("layer").to_string())
print("\nLayer II, Watson U2 (median, range):", f"{ii['u2|pooled'].median():.1f}, {ii['u2|pooled'].min():.1f} to {ii['u2|pooled'].max():.1f};",
"deeper head-direction cells:", f"{tracked[tracked.hd & (tracked.layer != 'II')]['u2|pooled'].median():.1f}")
Layer II cells passing, reading pooled, 0.05:
subject session unit u2|pooled block_r|pooled block_p|pooled grid|pooled|3cm|enclose|blob|max|area mv_r|pooled peak_rate|pooled
560 11265 16030601 t2c3 1.4878 0.5345 0.0000 -0.0665 0.0840 3.4389
564 11265 16030601 t3c4 1.3793 0.3586 0.0049 0.1321 0.0580 9.2998
568 11265 16030604 t1c1 1.3811 0.3761 0.0031 0.3380 0.0895 3.2931
573 11265 16030604 t3c2 2.3738 0.3149 0.0143 0.8002 0.1501 2.7027
574 11265 16030604 t3c3 7.9196 0.5311 0.0000 0.8955 0.2023 6.7749
576 11265 16030604 t3c5 1.8648 0.5563 0.0000 0.3856 0.0818 3.1955
577 11265 16030604 t4c1 9.0384 0.6079 0.0000 0.1323 0.4851 1.9656
579 11265 16030604 t4c3 0.8640 0.2620 0.0432 -0.4388 0.1019 1.8479
580 11265 16030604 t4c4 9.7706 0.5038 0.0000 0.1770 0.1956 7.6364
Per rat and layer (two-LED cells):
directional cells
layer II III V VI II III V VI
subject
11025 NaN 8.0 NaN NaN NaN 38.0 NaN NaN
11084 NaN NaN 23.0 NaN NaN NaN 27.0 NaN
11138 NaN 26.0 NaN NaN NaN 46.0 NaN NaN
11207 NaN NaN 51.0 113.0 NaN NaN 52.0 143.0
11265 9.0 65.0 NaN NaN 26.0 85.0 NaN NaN
11278 0.0 NaN NaN NaN 17.0 NaN NaN NaN
11340 NaN 7.0 NaN NaN NaN 10.0 NaN NaN
11343 NaN 3.0 NaN NaN NaN 3.0 NaN NaN
Layer II, Watson U2 (median, range): 1.4, 0.1 to 9.8; deeper head-direction cells: 13.1
# The layer II cells that pass, their files, and variants that do not decide the outcome.
ii_files = sorted(set().union(*(tracked[sg.classify(tracked, T, sg.GRID_READINGS[0], a).hd & (tracked.layer == "II")].path
for T, _, a in sg.readings(with_grid=False))))
two = extent[extent.led_separation.notna()].assign(rank=lambda x: x.led_separation.rank().astype(int))
print(f"Files of the layer II cells passing under any reading ({len(ii_files)}), against all {len(two)} two-LED files:")
print(two[two.path.isin(ii_files)][["path", "minutes", "larger_span", "centred", "led_separation", "rank", "led_over_15cm"]].round(3).to_string(index=False))
print(f"Two-LED files: median LED separation {two.led_separation.min():.1f} to {two.led_separation.max():.1f} cm (rank 1 the smallest); "
f"separation above 15 cm in {two.led_over_15cm.min():.3f} to {two.led_over_15cm.max():.3f} of samples")
print("Two-LED files of rat 11265:")
print(two[two.subject == "11265"][["path", "minutes", "larger_span", "led_separation", "rank", "led_over_15cm"]].round(3).to_string(index=False))
odd_led = extent[extent.led_over_15cm > 0.5].path
variants = {"all files": pd.Series(True, index=tracked.index),
"without the files of the layer II cells": ~tracked.path.isin(ii_files),
"without the files with LEDs over 15 cm apart in most samples": ~tracked.path.isin(odd_led),
"without the files of the layer II cells or with LEDs over 15 cm apart": ~tracked.path.isin(ii_files) & ~tracked.path.isin(odd_led),
"each (rat, depth, hemisphere, name) once": first_copy.reindex(tracked.index)}
rows = []
for name, keep in variants.items():
sub = tracked[keep]
for T, _, a in sg.readings(with_grid=False):
hd = sg.classify(sub, T, sg.GRID_READINGS[0], a).hd
rows.append(dict(variant=name, trials=T, level=a,
layer_II=f"{int(hd[sub.layer == 'II'].sum())}/{int((sub.layer == 'II').sum())}",
**{l: hd[sub.layer == l].mean() for l in DEEP}, **{"III to VI": hd[sub.layer != "II"].mean()}))
print("\nHead-direction and conjunctive cells by variant:")
print(pd.DataFrame(rows).round(3).to_string(index=False))
print("Files with LEDs over 15 cm apart in most samples, cells by layer:",
tracked[tracked.path.isin(odd_led)].groupby(["path", "layer"]).size().to_dict())
left = tracked[~tracked.path.isin(ii_files) & (tracked.layer == "II")]
print("Layer II cells left without the files of the passing cells, by file:",
left.groupby("path").size().to_dict(), "; of them in files with LEDs over 15 cm apart in most samples:",
int(left.path.isin(odd_led).sum()))
Files of the layer II cells passing under any reading (2), against all 71 two-LED files:
path minutes larger_span centred led_separation rank led_over_15cm
sub-11265/sub-11265_ses-16030601_behavior+ecephys.nwb 20 150.0 False 3.680 4 0.0
sub-11265/sub-11265_ses-16030604_behavior+ecephys.nwb 20 150.0 False 3.925 6 0.0
Two-LED files: median LED separation 2.6 to 36.0 cm (rank 1 the smallest); separation above 15 cm in 0.000 to 0.713 of samples
Two-LED files of rat 11265:
path minutes larger_span led_separation rank led_over_15cm
sub-11265/sub-11265_ses-01020602_behavior+ecephys.nwb 10 100.0 4.381 10 0.005
sub-11265/sub-11265_ses-02020601_behavior+ecephys.nwb 10 100.0 4.924 26 0.002
sub-11265/sub-11265_ses-03020601_behavior+ecephys.nwb 10 100.0 5.593 42 0.039
sub-11265/sub-11265_ses-06020601_behavior+ecephys.nwb 10 100.0 4.777 20 0.017
sub-11265/sub-11265_ses-07020602_behavior+ecephys.nwb 10 100.0 5.086 33 0.010
sub-11265/sub-11265_ses-09020601_behavior+ecephys.nwb 10 100.0 35.954 71 0.713
sub-11265/sub-11265_ses-13020601_behavior+ecephys.nwb 10 100.0 4.773 19 0.013
sub-11265/sub-11265_ses-16030601_behavior+ecephys.nwb 20 150.0 3.680 4 0.000
sub-11265/sub-11265_ses-16030604_behavior+ecephys.nwb 20 150.0 3.925 6 0.000
sub-11265/sub-11265_ses-31010601_behavior+ecephys.nwb 10 100.0 5.555 40 0.006
Head-direction and conjunctive cells by variant:
variant trials level layer_II III V VI III to VI
all files pooled 0.05 9/43 0.599 0.937 0.790 0.733
all files pooled 0.01 7/43 0.527 0.924 0.734 0.678
all files first 0.05 4/43 0.599 0.937 0.762 0.723
all files first 0.01 2/43 0.527 0.924 0.713 0.671
without the files of the layer II cells pooled 0.05 0/17 0.599 0.937 0.790 0.733
without the files of the layer II cells pooled 0.01 0/17 0.527 0.924 0.734 0.678
without the files of the layer II cells first 0.05 0/17 0.599 0.937 0.762 0.723
without the files of the layer II cells first 0.01 0/17 0.527 0.924 0.713 0.671
without the files with LEDs over 15 cm apart in most samples pooled 0.05 9/32 0.627 0.937 0.790 0.749
without the files with LEDs over 15 cm apart in most samples pooled 0.01 7/32 0.556 0.924 0.734 0.696
without the files with LEDs over 15 cm apart in most samples first 0.05 4/32 0.627 0.937 0.762 0.739
without the files with LEDs over 15 cm apart in most samples first 0.01 2/32 0.556 0.924 0.713 0.688
without the files of the layer II cells or with LEDs over 15 cm apart pooled 0.05 0/6 0.627 0.937 0.790 0.749
without the files of the layer II cells or with LEDs over 15 cm apart pooled 0.01 0/6 0.556 0.924 0.734 0.696
without the files of the layer II cells or with LEDs over 15 cm apart first 0.05 0/6 0.627 0.937 0.762 0.739
without the files of the layer II cells or with LEDs over 15 cm apart first 0.01 0/6 0.556 0.924 0.713 0.688
each (rat, depth, hemisphere, name) once pooled 0.05 9/26 0.594 0.963 0.795 0.756
each (rat, depth, hemisphere, name) once pooled 0.01 7/26 0.542 0.963 0.723 0.706
each (rat, depth, hemisphere, name) once first 0.05 4/26 0.615 0.963 0.759 0.748
each (rat, depth, hemisphere, name) once first 0.01 2/26 0.552 0.963 0.696 0.698
Files with LEDs over 15 cm apart in most samples, cells by layer: {('sub-11265/sub-11265_ses-09020601_behavior+ecephys.nwb', 'III'): 13, ('sub-11278/sub-11278_ses-31080502_behavior+ecephys.nwb', 'II'): 11}
Layer II cells left without the files of the passing cells, by file: {'sub-11278/sub-11278_ses-30080505_behavior+ecephys.nwb': 6, 'sub-11278/sub-11278_ses-31080502_behavior+ecephys.nwb': 11} ; of them in files with LEDs over 15 cm apart in most samples: 11
Result: not reproduced. Under every reading some layer II cells pass both head-direction
criteria: 9, 7, 4 and 2 of 43. All of them are from rat 11265, in its two 20-minute files of
16 March 2006 (16030601 and 16030604), and their U2 (0.36 to 9.8) is below the median of the
deeper head-direction cells (13.1). Watson's U2 alone is significant for 39 to 41 of the 43 layer II
cells, so the block correlation decides which pass. In the deeper layers 0.671 to 0.733 of cells are
directional, within 0.709 +/- 0.071, and layer V (0.924 to 0.937) is within its range; layer III
(0.527 to 0.599) is below its range under every reading and layer VI (0.713 to 0.790) above it
under two of four. The claim needs no layer II cell to pass even for a partial reproduction.
The two files show no sign of bad tracking: their median LED separation, 3.7 and 3.9 cm, ranks 4th
and 6th smallest of the 71 two-LED files, and fewer than 0.05% of their samples have the LEDs more
than 15 cm apart. They are the only two files whose positions span 150 cm. Without them no layer II cell passes (0 of 17) and the deeper fractions are unchanged,
which would be partially reproduced; this variant selects files after seeing the result and does
not decide the outcome. It is also weaker than its count suggests: the 17 layer II cells left are
all from rat 11278, and 11 of them are in sub-11278_ses-31080502, one of the two files whose
LEDs are more than 15 cm apart in most samples. Leaving out that file as well, 0 of 6 layer II
cells pass, so the "none" of this variant rests on 6 layer II cells with plausible tracking. Leaving out the two files with LEDs more than 15 cm apart in most samples
(13 layer III and 11 layer II cells) changes nothing in layer II (9, 7, 4 and 2 of 32), and counting
each (rat, depth, hemisphere, name) once leaves 9, 7, 4 and 2 of 26.
Claim hd-with-grid-colocalized¶
Beneath layer II, head-direction cells were usually recorded together with grid cells.
Criterion: two-LED files, layers III to VI, cell classes as in hd-layers and layer2-grid. The
unit of colocalization is (a) the file (one recording), (b) the tetrode within a file. The
head-direction cells are the pure head-direction cells of table S1B, and the cell found with them is
another cell of the unit: (a) a pure grid cell, (b) a grid or conjunctive cell (readings, as
answered). Reproduced when, under every reading, more than half of the units holding a
head-direction cell also hold such a cell; methods_underspecified when the readings disagree.
deep = tracked[tracked.layer.isin(DEEP)]
deep_once = deep[first_copy.reindex(deep.index)] # each (rat, depth, hemisphere, name) once, for variants
def coloc_of(cells_):
rows = []
for T, G, a in sg.readings():
c = sg.classify(cells_, T, G, a)
for companion, other in (("pure grid", c.grid & ~c.hd), ("grid or conjunctive", c.grid)):
frame = cells_.assign(pure_hd=c.pure_hd, other=other)
for unit_name, keys in (("file", ["path"]), ("tetrode", ["path", "tetrode"])):
g = frame.groupby(keys).agg(hd=("pure_hd", "any"), other=("other", "any"))
with_hd = g[g.hd]
rows.append(dict(trials=T, gridness=G, level=a, companion=companion, unit=unit_name, units_with_hd=len(with_hd),
also_grid=int(with_hd.other.sum()), fraction=with_hd.other.mean()))
out = pd.DataFrame(rows)
out["more_than_half"] = out.fraction > 0.5
return out
coloc = coloc_of(deep)
print(coloc.groupby(["companion", "unit"]).agg(units_min=("units_with_hd", "min"), units_max=("units_with_hd", "max"),
fraction_min=("fraction", "min"), fraction_median=("fraction", "median"),
fraction_max=("fraction", "max")).round(3).to_string())
print(coloc[(coloc.trials == READING[0]) & (coloc.gridness == READING[1]) & (coloc.level == READING[2])].round(3).to_string())
print(f"\nReadings (trials, gridness, level, companion, unit) with more than half: {int(coloc.more_than_half.sum())} of {len(coloc)}")
print(coloc.groupby(["companion", "unit"]).more_than_half.agg(["sum", "size"]).to_string())
print("Outcome:", outcome_of(np.where(coloc.more_than_half, "reproduced", "not_reproduced")))
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
coloc_once = coloc_of(deep_once)
print("Variant, each (rat, depth, hemisphere, name) once, readings with more than half:")
print(coloc_once.groupby(["companion", "unit"]).agg(units_min=("units_with_hd", "min"), units_max=("units_with_hd", "max"),
fraction_min=("fraction", "min"), fraction_max=("fraction", "max"),
more_than_half=("more_than_half", "sum"), readings=("more_than_half", "size")).round(3).to_string())
print(" outcome:", outcome_of(np.where(coloc_once.more_than_half, "reproduced", "not_reproduced")))
units_min units_max fraction_min fraction_median fraction_max
companion unit
grid or conjunctive file 49 57 0.750 0.852 0.909
tetrode 107 128 0.362 0.428 0.477
pure grid file 49 57 0.472 0.533 0.600
tetrode 107 128 0.145 0.168 0.194
trials gridness level companion unit units_with_hd also_grid fraction more_than_half
0 pooled 3cm|enclose|blob|max|area 0.05 pure grid file 55 28 0.509 True
1 pooled 3cm|enclose|blob|max|area 0.05 pure grid tetrode 126 21 0.167 False
2 pooled 3cm|enclose|blob|max|area 0.05 grid or conjunctive file 55 49 0.891 True
3 pooled 3cm|enclose|blob|max|area 0.05 grid or conjunctive tetrode 126 54 0.429 False
Readings (trials, gridness, level, companion, unit) with more than half: 195 of 448
sum size
companion unit
grid or conjunctive file 112 112
tetrode 0 112
pure grid file 83 112
tetrode 0 112
Outcome: methods_underspecified (not_reproduced, reproduced)
Variant, each (rat, depth, hemisphere, name) once, readings with more than half:
units_min units_max fraction_min fraction_max more_than_half readings
companion unit
grid or conjunctive file 42 47 0.667 0.841 112 112
tetrode 81 94 0.308 0.459 0 112
pure grid file 42 47 0.302 0.477 0 112
tetrode 81 94 0.098 0.184 0 112
outcome: methods_underspecified (not_reproduced, reproduced)
Result: blocked, methods_underspecified. Counted by file, 75% to 91% of the 49 to 57 files with a pure head-direction cell in layers III to VI also hold a grid or conjunctive cell, and 47% to 60% hold a pure grid cell (more than half under 83 of 112 readings). Counted by tetrode, 36% to 48% of 107 to 128 tetrodes hold a grid or conjunctive cell, and 14.5% to 19.4% a pure grid cell, below half under every reading. The readings disagree. The answer confirmed the pure head-direction cells and another cell, which this attempt had used, and added the pure grid cell as a reading; it lowers the fractions but does not change the outcome.
Counting each (rat, depth, hemisphere, name) once, by file 67% to 84% of the 42 to 47 files hold a grid or conjunctive cell (more than half under all 112 readings) and 30% to 48% a pure grid cell (under none); by tetrode both are below half under every reading. The readings still disagree.
Claim hd-tuning-breadth-layers¶
The breadth of directional tuning did not differ between layers III, V and VI (mean U2 18.2, 25.2 and 18.3; mean angular standard deviation 55.9, 52.2 and 58.2 degrees; P > 0.05).
Criterion: a claim of absence; tests (a) one-way ANOVA, (b) Kruskal-Wallis, each on U2 and on the angular standard deviation; cells (a) pure head-direction cells, (b) with conjunctive cells. The angular standard deviation is (a) Batschelet's angular deviation, sqrt(2(1 - r)), (b) his circular standard deviation, sqrt(-2 ln r) (readings, as answered). Reproduced when no reading finds a difference at p < 0.05; not reproduced when all do; methods_underspecified when they disagree; a reproduced absence with means outside tolerance is partially reproduced.
EXPECTED_U2 = {"III": (18.2, 1.82), "V": (25.2, 2.52), "VI": (18.3, 1.83)}
EXPECTED_ASD = {"III": (55.9, 5.59), "V": (52.2, 5.22), "VI": (58.2, 5.82)}
def breadth_of(cells_):
rows = []
for T, G, a in sg.readings():
c = sg.classify(cells_, T, G, a)
for cell_reading, sel in (("pure head-direction", c.pure_hd), ("with conjunctive", c.hd)):
if cell_reading == "with conjunctive" and G != sg.GRID_READINGS[0]:
continue # does not depend on the gridness reading
g = cells_[sel]
for measure, col, exp in (("U2", f"u2|{T}", EXPECTED_U2), ("angular deviation", f"ang_dev|{T}", EXPECTED_ASD),
("circular sd", f"circ_sd|{T}", EXPECTED_ASD)):
groups = [g[g.layer == l][col] for l in DEEP]
means = {l: x.mean() for l, x in zip(DEEP, groups)}
rows.append(dict(trials=T, gridness=G if cell_reading.startswith("pure") else "any", level=a, cells=cell_reading,
measure=measure, **{f"n {l}": len(x) for l, x in zip(DEEP, groups)},
**{f"mean {l}": means[l] for l in DEEP},
anova_p=stats.f_oneway(*groups).pvalue, kruskal_p=stats.kruskal(*groups).pvalue,
means_in_tolerance=all(within(means[l], *exp[l]) for l in DEEP)))
return pd.DataFrame(rows)
breadth = breadth_of(deep)
print(breadth.groupby(["cells", "measure"]).agg(
readings=("anova_p", "size"), anova_p_min=("anova_p", "min"), anova_p_max=("anova_p", "max"),
kruskal_p_min=("kruskal_p", "min"), kruskal_p_max=("kruskal_p", "max"),
**{f"mean {l} min": (f"mean {l}", "min") for l in DEEP}, **{f"mean {l} max": (f"mean {l}", "max") for l in DEEP},
in_tolerance=("means_in_tolerance", "sum")).round(3).T.to_string())
print("\nWith conjunctive cells, every trial and level reading:")
print(breadth[breadth.cells == "with conjunctive"].round(3).to_string())
ps = pd.concat([breadth.anova_p, breadth.kruskal_p])
for m in ("U2", "angular deviation", "circular sd"):
sub = breadth[breadth.measure == m]
print(f"{m}: ANOVA finds a difference in {int((sub.anova_p < 0.05).sum())} of {len(sub)}, Kruskal-Wallis in "
f"{int((sub.kruskal_p < 0.05).sum())} of {len(sub)}; means within tolerance in {int(sub.means_in_tolerance.sum())}")
print(f"\nTests finding a difference: {int((ps < 0.05).sum())} of {len(ps)}; "
f"U2 tests {int((pd.concat([breadth[breadth.measure == 'U2'].anova_p, breadth[breadth.measure == 'U2'].kruskal_p]) < 0.05).sum())} "
f"of {2 * (breadth.measure == 'U2').sum()}")
found = ps < 0.05
print("Outcome:", "not_reproduced" if found.all() else "methods_underspecified" if found.any() else
"reproduced" if breadth.means_in_tolerance.any() else "partially_reproduced")
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
b1 = breadth_of(deep_once)
print("Variant, each (rat, depth, hemisphere, name) once, tests finding a difference:",
{m: f"ANOVA {int((s.anova_p < 0.05).sum())}, Kruskal-Wallis {int((s.kruskal_p < 0.05).sum())} of {len(s)}" for m, s in b1.groupby("measure")})
cells pure head-direction with conjunctive
measure U2 angular deviation circular sd U2 angular deviation circular sd
readings 112.000 112.000 112.000 4.000 4.000 4.000
anova_p_min 0.144 0.003 0.001 0.063 0.001 0.000
anova_p_max 0.986 0.474 0.685 0.379 0.012 0.010
kruskal_p_min 0.007 0.002 0.002 0.004 0.000 0.000
kruskal_p_max 0.974 0.438 0.438 0.278 0.006 0.006
mean III min 16.324 46.684 58.111 15.987 52.651 65.863
mean V min 19.598 44.252 51.025 20.006 46.251 53.880
mean VI min 17.151 50.080 59.365 15.840 52.622 63.823
mean III max 25.673 56.504 76.209 19.256 54.973 70.823
mean V max 26.861 47.360 56.460 24.593 46.636 54.782
mean VI max 27.009 53.652 65.815 24.078 54.528 68.407
in_tolerance 4.000 2.000 5.000 0.000 0.000 0.000
With conjunctive cells, every trial and level reading:
trials gridness level cells measure n III n V n VI mean III mean V mean VI anova_p kruskal_p means_in_tolerance
3 pooled any 0.05 with conjunctive U2 109 74 113 17.256 24.292 22.615 0.063 0.004 False
4 pooled any 0.05 with conjunctive angular deviation 109 74 113 54.972 46.595 54.528 0.001 0.000 False
5 pooled any 0.05 with conjunctive circular sd 109 74 113 70.823 54.507 68.407 0.000 0.000 False
9 pooled any 0.01 with conjunctive U2 96 73 105 19.256 24.593 24.078 0.195 0.055 False
10 pooled any 0.01 with conjunctive angular deviation 96 73 105 52.651 46.251 53.117 0.008 0.003 False
11 pooled any 0.01 with conjunctive circular sd 96 73 105 65.898 53.880 65.136 0.005 0.003 False
177 first any 0.05 with conjunctive U2 109 74 109 15.987 20.006 15.840 0.193 0.075 False
178 first any 0.05 with conjunctive angular deviation 109 74 109 54.973 46.636 53.914 0.001 0.001 False
179 first any 0.05 with conjunctive circular sd 109 74 109 70.774 54.782 66.835 0.001 0.001 False
183 first any 0.01 with conjunctive U2 96 73 102 17.821 20.248 16.653 0.379 0.278 False
184 first any 0.01 with conjunctive angular deviation 96 73 102 52.656 46.292 52.622 0.012 0.006 False
185 first any 0.01 with conjunctive circular sd 96 73 102 65.863 54.159 63.823 0.010 0.006 False
U2: ANOVA finds a difference in 0 of 116, Kruskal-Wallis in 10 of 116; means within tolerance in 4
angular deviation: ANOVA finds a difference in 35 of 116, Kruskal-Wallis in 45 of 116; means within tolerance in 2
circular sd: ANOVA finds a difference in 61 of 116, Kruskal-Wallis in 45 of 116; means within tolerance in 5
Tests finding a difference: 196 of 696; U2 tests 10 of 232
Outcome: methods_underspecified
Variant, each (rat, depth, hemisphere, name) once, tests finding a difference: {'U2': 'ANOVA 0, Kruskal-Wallis 0 of 116', 'angular deviation': 'ANOVA 1, Kruskal-Wallis 2 of 116', 'circular sd': 'ANOVA 2, Kruskal-Wallis 2 of 116'}
Result: blocked, methods_underspecified. On U2, no ANOVA and 10 of 116 Kruskal-Wallis tests find a difference between layers III, V and VI; on the angular standard deviation many do, under either formula (angular deviation: ANOVA 35 and Kruskal-Wallis 43 of 116; circular standard deviation: 61 and 43 of 116); 192 of the 696 tests find a difference. Layer V cells are the more sharply tuned: with conjunctive cells, their angular deviation is 46.3 to 46.6 degrees against 52.6 to 55.0 in layers III and VI. The means are within tolerance in 11 of 348 rows. The answer made both formulas readings, which this attempt already ran; the readings still disagree, so the outcome is unchanged.
Counting each (rat, depth, hemisphere, name) once, no U2 test finds a difference, and the angular measures find one in 1 or 2 of 116 per test (angular deviation: ANOVA 1, Kruskal-Wallis 2; circular standard deviation: 2 and 2). The readings still disagree, though the differences are much rarer when repeated cells are left out.
Claim hd-full-range¶
The entire range of head directions was represented by the preferred directions of head-direction cells in each of layers III, V and VI.
Criterion: head-direction and conjunctive cells of each layer; preferred direction is the bin of peak rate. Reproduced when each (a) 30-degree, (b) 60-degree, (c) 90-degree sector holds the preferred direction of at least one cell of the layer, in all three layers; not reproduced when no reading holds in some layer; otherwise methods_underspecified. Sectors start at 0 degrees of the camera's frame (a standard choice).
rows = []
for T, _, a in sg.readings(with_grid=False):
hd = sg.classify(deep, T, sg.GRID_READINGS[0], a).hd
for l in DEEP:
pref = np.degrees(deep[hd & (deep.layer == l)][f"peak_dir|{T}"]) % 360
row = dict(trials=T, level=a, layer=l, cells=len(pref))
for width in (30, 60, 90):
filled = np.unique((pref // width).astype(int))
row[f"{width} deg sectors filled"] = f"{len(filled)}/{360 // width}"
row[f"{width} ok"] = len(filled) == 360 // width
rows.append(row)
cover = pd.DataFrame(rows)
print(cover.to_string())
fig, axes = plt.subplots(1, 3, figsize=(9, 2.6), subplot_kw=dict(projection="polar"))
for ax, l in zip(axes, DEEP):
pref = deep[tracked.loc[deep.index, "hd"] & (deep.layer == l)][f"peak_dir|{READING[0]}"]
ax.hist(pref, bins=np.linspace(-np.pi, np.pi, 13))
ax.set_title(f"layer {l}, n = {len(pref)}", fontsize=9); ax.set_yticklabels([])
print("Polar histograms: preferred directions, 30-degree bins, first reading; every cell is within the axes.")
by_reading = cover.groupby(["trials", "level"])[["30 ok", "60 ok", "90 ok"]].all()
layer_any = cover.groupby("layer")[["30 ok", "60 ok", "90 ok"]].any().any(axis=1)
print("Outcome:", "reproduced" if by_reading.values.all() else "not_reproduced" if not layer_any.all() else "methods_underspecified")
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
filled_once = []
for T, _, a in sg.readings(with_grid=False):
hd = sg.classify(deep_once, T, sg.GRID_READINGS[0], a).hd
for l in DEEP:
pref = np.degrees(deep_once[hd & (deep_once.layer == l)][f"peak_dir|{T}"]) % 360
filled_once.append(dict(trials=T, level=a, layer=l, cells=len(pref), sectors_30=len(np.unique((pref // 30).astype(int)))))
filled_once = pd.DataFrame(filled_once)
print("Variant, each (rat, depth, hemisphere, name) once, 30-degree sectors filled (of 12):")
print(filled_once.to_string(index=False))
trials level layer cells 30 deg sectors filled 30 ok 60 deg sectors filled 60 ok 90 deg sectors filled 90 ok 0 pooled 0.05 III 109 12/12 True 6/6 True 4/4 True 1 pooled 0.05 V 74 12/12 True 6/6 True 4/4 True 2 pooled 0.05 VI 113 12/12 True 6/6 True 4/4 True 3 pooled 0.01 III 96 12/12 True 6/6 True 4/4 True 4 pooled 0.01 V 73 12/12 True 6/6 True 4/4 True 5 pooled 0.01 VI 105 12/12 True 6/6 True 4/4 True 6 first 0.05 III 109 12/12 True 6/6 True 4/4 True 7 first 0.05 V 74 12/12 True 6/6 True 4/4 True 8 first 0.05 VI 109 12/12 True 6/6 True 4/4 True 9 first 0.01 III 96 12/12 True 6/6 True 4/4 True 10 first 0.01 V 73 12/12 True 6/6 True 4/4 True 11 first 0.01 VI 102 12/12 True 6/6 True 4/4 True Polar histograms: preferred directions, 30-degree bins, first reading; every cell is within the axes. Outcome: reproduced Variant, each (rat, depth, hemisphere, name) once, 30-degree sectors filled (of 12): trials level layer cells sectors_30 pooled 0.05 III 57 12 pooled 0.05 V 52 12 pooled 0.05 VI 89 12 pooled 0.01 III 52 12 pooled 0.01 V 52 12 pooled 0.01 VI 81 12 first 0.05 III 59 12 first 0.05 V 52 12 first 0.05 VI 85 12 first 0.01 III 53 12 first 0.01 V 52 12 first 0.01 VI 78 12
Result: reproduced. Under all four readings the preferred directions of head-direction and conjunctive cells fill all twelve 30-degree sectors, and so all 60- and 90-degree sectors, in each of layers III (96 to 109 cells), V (73 to 74) and VI (102 to 113).
Counting each (rat, depth, hemisphere, name) once (52 to 89 cells per layer), all twelve 30-degree sectors are still filled in every layer under every reading.
Claim hd-colocalized-distributed¶
Preferred directions of simultaneously recorded head-direction cells in the same area were widely distributed; in most ensembles of five or more cells (8 of 12) they showed no significant clustering.
Criterion: every file with 5 or more simultaneously recorded head-direction cells; the directions of the cells' mean vectors tested for clustering with Watson's one-sample U2 test of uniformity at p < 0.05, against the critical value for the number of cells (Stephens' small-sample form, which gives the paper's 0.177 to 0.182 for 5 to 15 cells). Cells (a) pure head-direction cells, (b) with conjunctive cells. Reproduced when, under a reading, fewer than half of the ensembles show clustering; methods_underspecified when the readings disagree. Expected 4 +/- 0.4 clustered of 12 +/- 1.2 ensembles.
rows, ensembles = [], {}
for T, G, a in sg.readings():
c = sg.classify(deep, T, G, a)
for cell_reading, sel in (("pure head-direction", c.pure_hd), ("with conjunctive", c.hd)):
if cell_reading == "with conjunctive" and G != sg.GRID_READINGS[0]:
continue
found = []
for path, g in deep[sel].groupby("path"):
if len(g) >= 5:
u2, crit = sg.watson_u2_uniformity(g[f"mv_dir|{T}"].to_numpy())
found.append(dict(path=path, layer="/".join(sorted(g.layer.unique())), cells=len(g), u2=u2, critical=crit,
clustered=u2 > crit))
found = pd.DataFrame(found, columns=["path", "layer", "cells", "u2", "critical", "clustered"])
ensembles[(T, G if cell_reading.startswith("pure") else "any", a, cell_reading)] = found
rows.append(dict(trials=T, gridness=G if cell_reading.startswith("pure") else "any", level=a, cells=cell_reading,
ensembles=len(found), clustered=int(found.clustered.sum()),
fewer_than_half=found.clustered.sum() < len(found) / 2 if len(found) else np.nan))
distributed = pd.DataFrame(rows)
print(distributed.groupby("cells").agg(readings=("ensembles", "size"), ensembles_min=("ensembles", "min"),
ensembles_max=("ensembles", "max"), clustered_min=("clustered", "min"),
clustered_max=("clustered", "max"),
readings_fewer_than_half=("fewer_than_half", "sum")).to_string())
print("\nWith conjunctive cells:")
print(distributed[distributed.cells == "with conjunctive"].to_string())
print("Ensembles per rat, with conjunctive cells, first reading:",
ensembles[(READING[0], "any", READING[2], "with conjunctive")].path.str[4:9].value_counts().to_dict())
print("\nEnsembles, first reading with conjunctive cells:")
print(ensembles[(READING[0], "any", READING[2], "with conjunctive")].round(3).to_string())
print("Outcome:", outcome_of(np.where(distributed.fewer_than_half.astype(bool), "reproduced", "not_reproduced")))
ens = ensembles[(READING[0], "any", READING[2], "with conjunctive")]
members = deep[deep.path.isin(ens.path) & tracked.loc[deep.index, "hd"]]
repeated = members.depth_mm.notna() & members.duplicated(["subject", "depth_mm", "hemisphere", "unit"], keep=False)
print(f"\nCells of these ensembles found, by (rat, depth, hemisphere, name), in another of them: {int(repeated.sum())} of "
f"{len(members)}, in {members[repeated].path.nunique()} of {len(ens)} ensembles")
rows = []
for T, G, a in sg.readings(with_grid=False):
hd = sg.classify(deep, T, sg.GRID_READINGS[0], a).hd & first_copy.reindex(deep.index)
found = [sg.watson_u2_uniformity(g[f"mv_dir|{T}"].to_numpy()) for _, g in deep[hd].groupby("path") if len(g) >= 5]
rows.append(dict(trials=T, level=a, ensembles=len(found), clustered=sum(u2 > crit for u2, crit in found)))
print("Variant, each (rat, depth, hemisphere, name) once, with conjunctive cells:")
print(pd.DataFrame(rows).to_string(index=False))
readings ensembles_min ensembles_max clustered_min clustered_max readings_fewer_than_half
cells
pure head-direction 112 9 16 0 3 112
with conjunctive 4 26 28 2 4 4
With conjunctive cells:
trials gridness level cells ensembles clustered fewer_than_half
1 pooled any 0.05 with conjunctive 28 3 True
3 pooled any 0.01 with conjunctive 26 4 True
59 first any 0.05 with conjunctive 28 2 True
61 first any 0.01 with conjunctive 27 2 True
Ensembles per rat, with conjunctive cells, first reading: {'11207': 18, '11265': 6, '11084': 3, '11138': 1}
Ensembles, first reading with conjunctive cells:
path layer cells u2 critical clustered
0 sub-11084/sub-11084_ses-03020501_behavior+ecephys.nwb V 5 0.078 0.177 False
1 sub-11084/sub-11084_ses-08030506_behavior+ecephys.nwb V 6 0.112 0.179 False
2 sub-11084/sub-11084_ses-10030502_behavior+ecephys.nwb V 8 0.258 0.181 True
3 sub-11138/sub-11138_ses-06040507_behavior+ecephys.nwb III 5 0.096 0.177 False
4 sub-11207/sub-11207_ses-03060501_behavior+ecephys.nwb VI 7 0.112 0.180 False
5 sub-11207/sub-11207_ses-04070501_behavior+ecephys.nwb V/VI 7 0.187 0.180 True
6 sub-11207/sub-11207_ses-06070501_behavior+ecephys.nwb VI 6 0.040 0.179 False
7 sub-11207/sub-11207_ses-07070501_behavior+ecephys.nwb VI 6 0.078 0.179 False
8 sub-11207/sub-11207_ses-08070504_behavior+ecephys.nwb VI 9 0.134 0.182 False
9 sub-11207/sub-11207_ses-09060501_behavior+ecephys.nwb V/VI 9 0.057 0.182 False
10 sub-11207/sub-11207_ses-11060501_behavior+ecephys.nwb V/VI 9 0.074 0.182 False
11 sub-11207/sub-11207_ses-14060501_behavior+ecephys.nwb V/VI 9 0.089 0.182 False
12 sub-11207/sub-11207_ses-16060501_behavior+ecephys.nwb V/VI 9 0.096 0.182 False
13 sub-11207/sub-11207_ses-17060501_behavior+ecephys.nwb V/VI 6 0.034 0.179 False
14 sub-11207/sub-11207_ses-18060501_behavior+ecephys.nwb V/VI 14 0.198 0.184 True
15 sub-11207/sub-11207_ses-20060501_behavior+ecephys.nwb VI 5 0.039 0.177 False
16 sub-11207/sub-11207_ses-21060501_behavior+ecephys.nwb V 5 0.163 0.177 False
17 sub-11207/sub-11207_ses-21060503_behavior+ecephys.nwb V/VI 11 0.110 0.183 False
18 sub-11207/sub-11207_ses-23060501_behavior+ecephys.nwb VI 6 0.069 0.179 False
19 sub-11207/sub-11207_ses-24060501_behavior+ecephys.nwb V/VI 8 0.071 0.181 False
20 sub-11207/sub-11207_ses-27060501_behavior+ecephys.nwb V/VI 11 0.176 0.183 False
21 sub-11207/sub-11207_ses-30060501_behavior+ecephys.nwb V/VI 6 0.079 0.179 False
22 sub-11265/sub-11265_ses-01020602_behavior+ecephys.nwb III 9 0.062 0.182 False
23 sub-11265/sub-11265_ses-02020601_behavior+ecephys.nwb III 10 0.087 0.182 False
24 sub-11265/sub-11265_ses-03020601_behavior+ecephys.nwb III 11 0.052 0.183 False
25 sub-11265/sub-11265_ses-06020601_behavior+ecephys.nwb III 16 0.150 0.184 False
26 sub-11265/sub-11265_ses-07020602_behavior+ecephys.nwb III 6 0.075 0.179 False
27 sub-11265/sub-11265_ses-31010601_behavior+ecephys.nwb III 6 0.070 0.179 False
Outcome: reproduced
Cells of these ensembles found, by (rat, depth, hemisphere, name), in another of them: 92 of 225, in 16 of 28 ensembles
Variant, each (rat, depth, hemisphere, name) once, with conjunctive cells:
trials level ensembles clustered
pooled 0.05 19 3
pooled 0.01 16 3
first 0.05 19 2
first 0.01 17 1
Result: reproduced. Under every reading fewer than half of the ensembles of five or more simultaneous head-direction cells show clustering by Watson's U2 test: 0 to 3 of 9 to 16 ensembles of pure head-direction cells, and 2 to 4 of 26 to 28 with conjunctive cells. The counts differ from the paper's (12 ensembles, 4 clustered); the clustered count is within 4 +/- 0.4 under one reading with conjunctive cells (4 of 26). Most ensembles come from rats 11207 (18 of 28) and 11265 (6). The ensembles share cells: 92 of their 225 cells (first reading, with conjunctive cells) match, by rat, depth, hemisphere and name, a cell of another ensemble, in 16 of the 28. Counting each such cell once leaves 16 to 19 ensembles, of which 1 to 3 are clustered, so the outcome is the same.
Claim hd-stable-trials¶
Directional tuning curves were always stable across the two trials (circular correlation r = 0.84, P < 0.001).
Criterion: head-direction and conjunctive cells recorded in both trials (the 20-minute two-LED files). Readings: (a) across cells, the circular-circular correlation of the preferred direction in trial 1 with that in trial 2 (Jammalamadaka's coefficient, a standard choice); (b) per cell, the Pearson correlation of the two trials' 60-bin tuning curves, averaged over cells, its mean tested against 0 by a one-sample t-test. Reproduced when, under a reading, r is within 0.84 +/- 0.084 with p < 0.001; methods_underspecified when one reading reproduces and another fails.
two_trial = deep[deep.trials == 2]
def stable_of(frame):
rows = []
for T, _, a in sg.readings(with_grid=False):
g = frame[sg.classify(frame, T, sg.GRID_READINGS[0], a).hd]
silent = g["trial_peak_dir|0"].isna() | g["trial_peak_dir|1"].isna()
g = g[~silent]
rcc, pcc = sg.circular_correlation(g["trial_peak_dir|0"].to_numpy(float), g["trial_peak_dir|1"].to_numpy(float))
per_cell = []
for t0, t1 in zip(g["trial_tuning|0"], g["trial_tuning|1"]):
ok = ~np.isnan(t0) & ~np.isnan(t1)
per_cell.append(stats.pearsonr(t0[ok], t1[ok])[0])
per_cell = np.array(per_cell)
rows.append(dict(trials=T, level=a, cells=len(g), dropped_silent_in_a_trial=int(silent.sum()),
files=g.path.nunique(), rats=g.subject.nunique(),
circular_r=rcc, circular_p=pcc, mean_curve_r=per_cell.mean(),
curve_r_range=f"{per_cell.min():.2f} to {per_cell.max():.2f}",
t_test_p=stats.ttest_1samp(per_cell, 0).pvalue,
cells_with_curve_r_below_0_5=int((per_cell < 0.5).sum())))
out = pd.DataFrame(rows)
out["a_reproduces"] = out.circular_r.between(0.756, 0.924) & (out.circular_p < 0.001)
out["b_reproduces"] = out.mean_curve_r.between(0.756, 0.924) & (out.t_test_p < 0.001)
return out
stable_hd = stable_of(two_trial)
print(stable_hd.round(4).to_string())
diff = pd.Series(np.degrees(np.angle(np.exp(1j * (two_trial["trial_peak_dir|1"] - two_trial["trial_peak_dir|0"]).astype(float)))),
index=two_trial.index)
hd_two = sg.classify(two_trial, READING[0], READING[1], READING[2]).hd & diff.notna()
print(f"\nChange of preferred direction between trials, first reading: median |change| "
f"{np.median(np.abs(diff[hd_two])):.0f} degrees; within 30 degrees: {(np.abs(diff[hd_two]) <= 30).mean():.2f}; "
f"by rat: {two_trial[hd_two].assign(d=np.abs(diff[hd_two])).groupby('subject').d.median().round(0).to_dict()}")
print("Outcome:", outcome_of(np.where(pd.concat([stable_hd.a_reproduces, stable_hd.b_reproduces]), "reproduced", "not_reproduced")))
# Variants that do not decide the outcome: the two-trial file with the LEDs more than 15 cm apart in 17.8% of
# samples (data check) left out, and each (rat, depth, hemisphere, name) once.
doubt = extent[(extent.led_over_15cm > 0.1) & (extent.led_over_15cm <= 0.5)].path
print("\nTwo-trial head-direction and conjunctive cells in files with the LEDs over 15 cm apart in 10% to 50% of samples, by reading:")
for T, _, a in sg.readings(with_grid=False):
g = two_trial[sg.classify(two_trial, T, sg.GRID_READINGS[0], a).hd & two_trial.path.isin(doubt)]
print(f" {T}, {a}:", [(p.split("/")[0], u, round(float(np.degrees(d0)) % 360), round(float(np.degrees(d1)) % 360))
for p, u, d0, d1 in zip(g.path, g.unit, g["trial_peak_dir|0"], g["trial_peak_dir|1"])],
"(file, cell, preferred direction in trials 1 and 2, degrees)")
for name, frame in (("without that file", two_trial[~two_trial.path.isin(doubt)]),
("each (rat, depth, hemisphere, name) once", two_trial[first_copy.reindex(two_trial.index)])):
v = stable_of(frame)
print(f"Variant, {name}:")
print(v[["trials", "level", "cells", "files", "circular_r", "circular_p", "mean_curve_r", "t_test_p", "a_reproduces", "b_reproduces"]].round(4).to_string(index=False))
print(f" over the readings: cells {v.cells.min()} to {v.cells.max()}, circular r {v.circular_r.min():.3f} to {v.circular_r.max():.3f} "
f"(p at most {v.circular_p.max():.2g}), mean curve r {v.mean_curve_r.min():.3f} to {v.mean_curve_r.max():.3f} "
f"(p at most {v.t_test_p.max():.2g}); outcome:",
outcome_of(np.where(pd.concat([v.a_reproduces, v.b_reproduces]), "reproduced", "not_reproduced")))
trials level cells dropped_silent_in_a_trial files rats circular_r circular_p mean_curve_r curve_r_range t_test_p cells_with_curve_r_below_0_5 a_reproduces b_reproduces 0 pooled 0.05 96 2 20 3 0.7805 0.0 0.7711 -0.07 to 0.98 0.0 14 True True 1 pooled 0.01 91 2 19 3 0.8517 0.0 0.8040 0.15 to 0.98 0.0 9 True True 2 first 0.05 92 2 19 3 0.7812 0.0 0.7810 -0.07 to 0.98 0.0 11 True True 3 first 0.01 88 2 19 3 0.7772 0.0 0.8065 -0.07 to 0.98 0.0 7 True True
Change of preferred direction between trials, first reading: median |change| 9 degrees; within 30 degrees: 0.83; by rat: {'11025': 15.0, '11138': 9.0, '11207': 6.0}
Outcome: reproduced
Two-trial head-direction and conjunctive cells in files with the LEDs over 15 cm apart in 10% to 50% of samples, by reading:
pooled, 0.05: [] (file, cell, preferred direction in trials 1 and 2, degrees)
pooled, 0.01: [] (file, cell, preferred direction in trials 1 and 2, degrees)
first, 0.05: [('sub-11025', 't1c2', 111, 291), ('sub-11025', 't2c1', 15, 321)] (file, cell, preferred direction in trials 1 and 2, degrees)
first, 0.01: [('sub-11025', 't2c1', 15, 321)] (file, cell, preferred direction in trials 1 and 2, degrees)
Variant, without that file:
trials level cells files circular_r circular_p mean_curve_r t_test_p a_reproduces b_reproduces
pooled 0.05 96 20 0.7805 0.0 0.7711 0.0 True True
pooled 0.01 91 19 0.8517 0.0 0.8040 0.0 True True
first 0.05 90 18 0.5380 0.0 0.7988 0.0 False True
first 0.01 87 18 0.6447 0.0 0.8154 0.0 False True
over the readings: cells 87 to 96, circular r 0.538 to 0.852 (p at most 1.7e-07), mean curve r 0.771 to 0.815 (p at most 7.7e-54); outcome: methods_underspecified (not_reproduced, reproduced)
Variant, each (rat, depth, hemisphere, name) once: trials level cells files circular_r circular_p mean_curve_r t_test_p a_reproduces b_reproduces pooled 0.05 78 17 0.8615 0.0 0.7805 0.0 True True pooled 0.01 74 17 0.8765 0.0 0.8115 0.0 True True first 0.05 76 18 0.8412 0.0 0.7748 0.0 True True first 0.01 72 18 0.8972 0.0 0.8057 0.0 True True over the readings: cells 72 to 78, circular r 0.841 to 0.897 (p at most 4.9e-10), mean curve r 0.775 to 0.812 (p at most 3.1e-42); outcome: reproduced
Result: reproduced. For the 88 to 96 head-direction and conjunctive cells in the 19 or 20 two-trial files (3 rats), the circular correlation of the preferred direction between trials is 0.78 to 0.85 and the mean correlation of the two trials' tuning curves 0.77 to 0.81, all with p below 0.001 and within 0.84 +/- 0.084, under both readings and all four classifications. The statement's "always" holds less well: 7 to 14 of these cells have tuning curves correlated below 0.5 between trials. Two cells silent in one trial are left out under each reading.
The third two-LED file with questionable LED geometry, sub-11025_ses-06050501 (LEDs more than
15 cm apart in 17.8% of samples), holds no head-direction or conjunctive cell under the pooled
readings and 2 and 1 under the first-trial readings, whose preferred directions change by 180 and
54 degrees between trials. Without that file the tuning-curve reading still holds under all four
readings (0.771 to 0.815), but the circular correlation falls to 0.538 and 0.645 under the
first-trial readings, outside 0.84 +/- 0.084, although the cells left out are unstable ones. The
circular correlation is centred on the circular means of the preferred directions, which fill the
circle (hd-full-range), so leaving out one or two cells can move it far. Under this variant the
readings would disagree; the criterion leaves no file out, so the variant does not decide the
outcome, but it shows that the circular-correlation reading is fragile here. Counting each (rat,
depth, hemisphere, name) once (72 to 78 cells), the circular correlation is 0.841 to 0.897 and the
mean curve correlation 0.775 to 0.812, which reproduces under both readings.
Claim hd-rates¶
In head-direction cells, firing rose from a low background rate (typically below 0.5 Hz) to a high maximum rate (5 to 40 Hz) when the rat's head faced the cell's range of directions.
Criterion: head-direction cells of the two-LED files, (a) pure, (b) with conjunctive cells. Peak rate: the highest bin of the unsmoothed 60-bin tuning curve. Background rate: (a) the lowest bin, (b) the mean over the bins more than 90 degrees from the preferred direction; "typically" is the median over cells. Reproduced when, under a reading, the median background rate is below 0.55 Hz and at least 90% of cells have peak rates from 4.5 to 44 Hz; methods_underspecified when readings disagree.
def rates_of(frame):
rows = []
for T, G, a in sg.readings():
c = sg.classify(frame, T, G, a)
for cell_reading, sel in (("pure head-direction", c.pure_hd), ("with conjunctive", c.hd)):
if cell_reading == "with conjunctive" and G != sg.GRID_READINGS[0]:
continue
g = frame[sel]
peak_ok = g[f"peak_rate|{T}"].between(4.5, 44).mean()
for bg in ("bg_min", "bg_far"):
med = g[f"{bg}|{T}"].median()
rows.append(dict(trials=T, gridness=G if cell_reading.startswith("pure") else "any", level=a, cells=cell_reading,
background="lowest bin" if bg == "bg_min" else "beyond 90 deg", n=len(g),
median_background=med, peak_in_range=peak_ok,
median_peak=g[f"peak_rate|{T}"].median(),
reproduces=(med < 0.55) and (peak_ok >= 0.9)))
return pd.DataFrame(rows)
rates = rates_of(deep)
print(rates.groupby(["cells", "background"]).agg(
readings=("n", "size"), median_background_min=("median_background", "min"),
median_background_max=("median_background", "max"), peak_in_range_min=("peak_in_range", "min"),
peak_in_range_max=("peak_in_range", "max"), median_peak_min=("median_peak", "min"),
median_peak_max=("median_peak", "max"), reproducing=("reproduces", "sum")).round(3).to_string())
g = deep[tracked.loc[deep.index, "hd"]]
print(f"\nFirst reading, with conjunctive cells: peak rates {g['peak_rate|pooled'].min():.1f} to "
f"{g['peak_rate|pooled'].max():.1f} Hz; below 4.5 Hz: {(g['peak_rate|pooled'] < 4.5).mean():.2f}; "
f"lowest-bin rate {g['bg_min|pooled'].min():.2f} to {g['bg_min|pooled'].max():.2f} Hz")
print("Outcome:", outcome_of(np.where(rates.reproduces, "reproduced", "not_reproduced")))
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
r1 = rates_of(deep_once)
print(f"Variant, each (rat, depth, hemisphere, name) once: median background {r1.median_background.min():.2f} to "
f"{r1.median_background.max():.2f} Hz, peak in range {r1.peak_in_range.min():.3f} to {r1.peak_in_range.max():.3f}; outcome:",
outcome_of(np.where(r1.reproduces, "reproduced", "not_reproduced")))
readings median_background_min median_background_max peak_in_range_min peak_in_range_max median_peak_min median_peak_max reproducing
cells background
pure head-direction beyond 90 deg 112 0.315 0.471 0.659 0.712 6.645 6.927 0
lowest bin 112 0.000 0.000 0.659 0.712 6.645 6.927 0
with conjunctive beyond 90 deg 4 0.410 0.501 0.655 0.679 5.870 6.097 0
lowest bin 4 0.000 0.000 0.655 0.679 5.870 6.097 0
First reading, with conjunctive cells: peak rates 0.9 to 31.5 Hz; below 4.5 Hz: 0.34; lowest-bin rate 0.00 to 5.59 Hz
Outcome: not_reproduced
Variant, each (rat, depth, hemisphere, name) once: median background 0.00 to 0.57 Hz, peak in range 0.636 to 0.701; outcome: not_reproduced
Result: not reproduced. The background rate is low as stated: its median over cells is 0 Hz in the lowest bin and 0.32 to 0.50 Hz beyond 90 degrees of the preferred direction, below 0.55 Hz under every reading. But only 65.5% to 71.2% of head-direction cells peak between 4.5 and 44 Hz, where the criterion needs 90%; the median peak is 5.9 to 6.9 Hz, and 34% of cells (first reading, with conjunctive cells) peak below 4.5 Hz.
Counting each (rat, depth, hemisphere, name) once, 63.6% to 70.1% of head-direction cells peak between 4.5 and 44 Hz, and the claim is not reproduced under any reading.
Claim deep-hd-conjunctive¶
The proportion of grid cells with conjunctive head-direction tuning depended on layer (66% in layer III, 90% in layer V, 28% in layer VI, none in layer II); the difference was significant across all four layers and across layers III to VI.
Criterion: two-LED files; grid and conjunctive cells by the SOM, readings O1 to O4. Chi-square readings: (a) conjunctive against non-conjunctive grid cells per layer; (b) conjunctive cells against all other cells per layer. Reproduced when, under a reading, both tests are significant (p < 0.001 for four layers, p < 0.05 for III to VI) and the four fractions are within tolerance (0, 0.66 +/- 0.066, 0.90 +/- 0.09, 0.28 +/- 0.028); partially reproduced when no layer II cell is conjunctive and the four-layer test is significant but a fraction or the three-layer test fails.
EXPECTED_CONJ = {"II": (0.0, 0.0), "III": (0.66, 0.066), "V": (0.90, 0.09), "VI": (0.28, 0.028)}
rows = []
for T, G, a in sg.readings():
c = sg.classify(tracked, T, G, a)
grid_cells = tracked[c.grid]
frac = c.conj[c.grid].groupby(grid_cells.layer).mean().reindex(LAYERS)
n_conj = c.conj.groupby(tracked.layer).sum().reindex(LAYERS)
n_grid = c.grid.groupby(tracked.layer).sum().reindex(LAYERS)
tables = {"a": pd.crosstab(grid_cells.layer, c.conj[c.grid]).reindex(index=LAYERS, columns=[False, True], fill_value=0),
"b": pd.crosstab(tracked.layer, c.conj).reindex(index=LAYERS, columns=[False, True], fill_value=0)}
tol = all(within(frac[l], *EXPECTED_CONJ[l]) for l in LAYERS)
for chi, table in tables.items():
chi4, p4 = stats.chi2_contingency(table, correction=False)[:2]
chi3, p3 = stats.chi2_contingency(table.loc[DEEP], correction=False)[:2]
outcome = ("reproduced" if p4 < 0.001 and p3 < 0.05 and tol else
"partially_reproduced" if n_conj["II"] == 0 and p4 < 0.001 else "not_reproduced")
rows.append(dict(trials=T, gridness=G, level=a, chi_square=chi,
**{l: f"{n_conj[l]}/{n_grid[l]} = {frac[l]:.2f}" for l in LAYERS},
chi2_4=chi4, p_4=p4, chi2_3=chi3, p_3=p3, outcome=outcome))
conj_table = pd.DataFrame(rows)
print(conj_table[(conj_table.gridness == READING[1])].round(4).to_string())
fr = {l: conj_table[l].str.split("= ").str[1].astype(float) for l in LAYERS}
print(f"\nConjunctive fraction of grid cells, range over the {len(conj_table) // 2} readings:", {l: f"{v.min():.2f} to {v.max():.2f}" for l, v in fr.items()})
n_ii = conj_table.II.str.split("/").str[0].astype(int)
print(f"Layer II conjunctive cells: {n_ii.min()} to {n_ii.max()}; readings with none: {int((n_ii == 0).sum() / 2)} of {len(conj_table) // 2}")
print("chi-square p, four layers:", f"{conj_table.p_4.min():.2g} to {conj_table.p_4.max():.2g};",
"layers III to VI:", f"{conj_table.p_3.min():.2g} to {conj_table.p_3.max():.2g}")
print(f"Outcomes over {len(conj_table)} rows:", conj_table.outcome.value_counts().to_dict(), "->", outcome_of(conj_table.outcome))
ii_rats = Counter()
for T, G, a in sg.readings():
c = sg.classify(tracked, T, G, a)
ii_rats.update(tracked[c.conj & (tracked.layer == "II")].subject)
print("Rats of the layer II conjunctive cells, summed over readings:", dict(ii_rats))
trials gridness level chi_square II III V VI chi2_4 p_4 chi2_3 p_3 outcome
0 pooled 3cm|enclose|blob|max|area 0.05 a 7/23 = 0.30 58/106 = 0.55 25/27 = 0.93 30/43 = 0.70 23.3214 0.0000 14.1983 0.0008 not_reproduced
1 pooled 3cm|enclose|blob|max|area 0.05 b 7/23 = 0.30 58/106 = 0.55 25/27 = 0.93 30/43 = 0.70 8.2153 0.0418 5.3714 0.0682 not_reproduced
2 pooled 3cm|enclose|blob|max|area 0.01 a 6/23 = 0.26 50/106 = 0.47 25/27 = 0.93 24/43 = 0.56 25.2436 0.0000 18.0456 0.0001 not_reproduced
3 pooled 3cm|enclose|blob|max|area 0.01 b 6/23 = 0.26 50/106 = 0.47 25/27 = 0.93 24/43 = 0.56 10.2848 0.0163 7.6526 0.0218 not_reproduced
112 first 3cm|enclose|blob|max|area 0.05 a 3/25 = 0.12 57/103 = 0.55 24/26 = 0.92 32/48 = 0.67 35.8999 0.0000 12.5178 0.0019 not_reproduced
113 first 3cm|enclose|blob|max|area 0.05 b 3/25 = 0.12 57/103 = 0.55 24/26 = 0.92 32/48 = 0.67 12.5415 0.0057 3.4605 0.1772 not_reproduced
114 first 3cm|enclose|blob|max|area 0.01 a 1/25 = 0.04 50/103 = 0.49 24/26 = 0.92 28/48 = 0.58 41.1330 0.0000 16.2967 0.0003 not_reproduced
115 first 3cm|enclose|blob|max|area 0.01 b 1/25 = 0.04 50/103 = 0.49 24/26 = 0.92 28/48 = 0.58 15.7864 0.0013 4.0133 0.1344 not_reproduced
Conjunctive fraction of grid cells, range over the 112 readings: {'II': '0.04 to 0.32', 'III': '0.46 to 0.59', 'V': '0.86 to 0.95', 'VI': '0.53 to 0.74'}
Layer II conjunctive cells: 1 to 8; readings with none: 0 of 112
chi-square p, four layers: 1.9e-11 to 0.23; layers III to VI: 1.6e-05 to 0.78
Outcomes over 224 rows: {'not_reproduced': 224} -> not_reproduced
Rats of the layer II conjunctive cells, summed over readings: {'11265': 488}
c = sg.classify(tracked, *READING)
s1b = pd.crosstab(tracked.layer, np.select([c.conj, c.grid, c.pure_hd], ["conjunctive", "grid", "head direction"], "other"))
s1b = s1b.reindex(index=LAYERS, columns=["grid", "head direction", "conjunctive", "other"], fill_value=0)
print("Table S1B, first reading (the paper: II 34/0/0/30, III 19/80/37/39, V 1/56/9/8, VI 18/84/7/27):")
print(s1b.to_string())
print("\nConjunctive / grid cells per rat and layer, first reading:")
print(tracked.assign(conj=c.conj, grid=c.grid).groupby(["subject", "layer"]).agg(conj=("conj", "sum"), grid=("grid", "sum")).unstack("layer").to_string())
Table S1B, first reading (the paper: II 34/0/0/30, III 19/80/37/39, V 1/56/9/8, VI 18/84/7/27):
col_0 grid head direction conjunctive other
layer
II 16 2 7 18
III 48 51 58 25
V 2 49 25 3
VI 13 83 30 17
Conjunctive / grid cells per rat and layer, first reading:
conj grid
layer II III V VI II III V VI
subject
11025 NaN 4.0 NaN NaN NaN 22.0 NaN NaN
11084 NaN NaN 3.0 NaN NaN NaN 5.0 NaN
11138 NaN 6.0 NaN NaN NaN 21.0 NaN NaN
11207 NaN NaN 22.0 30.0 NaN NaN 22.0 43.0
11265 7.0 44.0 NaN NaN 16.0 57.0 NaN NaN
11278 0.0 NaN NaN NaN 7.0 NaN NaN NaN
11340 NaN 4.0 NaN NaN NaN 6.0 NaN NaN
11343 NaN 0.0 NaN NaN NaN 0.0 NaN NaN
# Variants that do not decide the outcome (see hd-layers): the two files of rat 11265 left out, the two
# files with implausible LED geometry left out, and each (rat, depth, hemisphere, name) counted once.
rows = []
for name, keep in variants.items():
sub = tracked[keep]
n_ii, fr, p4s, p4b = [], {l: [] for l in LAYERS}, [], []
for T, G, a in sg.readings():
c = sg.classify(sub, T, G, a)
n_ii.append(int((c.conj & (sub.layer == "II")).sum()))
f = c.conj[c.grid].groupby(sub[c.grid].layer).mean().reindex(LAYERS)
for l in LAYERS:
fr[l].append(f[l])
table = pd.crosstab(sub[c.grid].layer, c.conj[c.grid]).reindex(index=LAYERS, columns=[False, True], fill_value=0)
p4s.append(stats.chi2_contingency(table, correction=False)[1])
table_b = pd.crosstab(sub.layer, c.conj).reindex(index=LAYERS, columns=[False, True], fill_value=0)
p4b.append(stats.chi2_contingency(table_b, correction=False)[1])
rows.append(dict(variant=name, layer_II_conjunctive=f"{min(n_ii)} to {max(n_ii)}",
**{l: f"{np.nanmin(v):.2f} to {np.nanmax(v):.2f}" for l, v in fr.items()},
p_4_reading_a=f"{min(p4s):.2g} to {max(p4s):.2g}", p_4_reading_b=f"{min(p4b):.2g} to {max(p4b):.2g}",
readings_p_4_below_0_001=f"a {sum(p < 0.001 for p in p4s)}, b {sum(p < 0.001 for p in p4b)} of {len(p4s)}"))
print("Conjunctive fraction of grid cells, range over the readings, by variant:")
print(pd.DataFrame(rows).to_string(index=False))
Conjunctive fraction of grid cells, range over the readings, by variant:
variant layer_II_conjunctive II III V VI p_4_reading_a p_4_reading_b readings_p_4_below_0_001
all files 1 to 8 0.04 to 0.32 0.46 to 0.59 0.86 to 0.95 0.53 to 0.74 1.9e-11 to 8.4e-05 4.1e-05 to 0.23 a 112, b 23 of 112
without the files of the layer II cells 0 to 0 0.00 to 0.00 0.46 to 0.59 0.86 to 0.95 0.53 to 0.74 2.4e-07 to 0.00033 0.00014 to 0.054 a 112, b 8 of 112
without the files with LEDs over 15 cm apart in most samples 1 to 8 0.05 to 0.41 0.49 to 0.63 0.86 to 0.95 0.53 to 0.74 7e-10 to 0.0026 0.00018 to 0.35 a 106, b 4 of 112
without the files of the layer II cells or with LEDs over 15 cm apart 0 to 0 0.00 to 0.00 0.49 to 0.63 0.86 to 0.95 0.53 to 0.74 3.6e-05 to 0.034 0.00079 to 0.34 a 60, b 1 of 112
each (rat, depth, hemisphere, name) once 1 to 8 0.05 to 0.44 0.42 to 0.62 0.90 to 0.96 0.50 to 0.79 3.3e-08 to 0.006 9.5e-05 to 0.39 a 101, b 6 of 112
Result: not reproduced. Layer II has conjunctive cells under every reading (1 to 8, all from rat 11265, 0.04 to 0.32 of layer II grid cells), and the claim needs none even for a partial reproduction. The conjunctive fraction of grid cells is 0.86 to 0.95 in layer V, within 0.90 +/- 0.09, but 0.46 to 0.59 in layer III (paper 0.66) and 0.53 to 0.74 in layer VI (paper 0.28). The four-layer chi-square has p from 1.9e-11 to 0.23 and the three-layer one from 1.6e-05 to 0.78, depending on the reading. The radius reading added by the answer changes none of this.
The layer II conjunctive cells are in the two 150-cm files of rat 11265 discussed under
hd-layers. Without those files layer II has none. The four-layer chi-square then has p 2.4e-07
to 0.00033 under reading (a), below 0.001 under all 112 readings, but 0.00014 to 0.054 under
reading (b), below 0.001 under only 8 of 112. So that variant would be partially reproduced under
reading (a) and not reproduced under most readings of (b): its readings disagree, and it would be
methods_underspecified, not partially reproduced. Leaving out also the two files with implausible
LED geometry, p is 3.6e-05 to 0.034 under (a) (below 0.001 under 60 of 112) and 0.00079 to 0.34
under (b) (1 of 112). Like the variant under hd-layers, these select files after seeing the
result and do not decide the outcome. Leaving out only the two files with implausible LED
geometry, or counting each (rat, depth, hemisphere, name) once, leaves 1 to 8 layer II
conjunctive cells.
The earlier attempt called this claim reproduced. Its claim was the abstract's sentence, that grid, head-direction and conjunctive cells occur together in the deeper layers, which the data still show (table S1B above). The claim now carries the paper's per-layer fractions and requires no conjunctive cell in layer II, and the data meet neither.
Claim gridness-hd-continuous¶
Grid cells and head-direction cells formed overlapping populations; the joint distribution of gridness and directional tuning was continuous, with some grid cells directionally tuned and some head-direction cells having grid structure.
Criterion: two-LED cells, gridness against Watson U2 (log scale) as in Fig. 3E. Reproduced when conjunctive cells exist and, under readings O1 to O4, the cells passing either criterion form no separate clusters, read as no gap along either axis wider than a quarter of that axis's range among them (a standard choice).
def continuity_of(frame):
rows = []
for T, G, a in sg.readings():
c = sg.classify(frame, T, G, a)
either = frame[c.grid | c.hd]
row = dict(trials=T, gridness=G, level=a, conjunctive=int(c.conj.sum()), cells=len(either))
for axis, values in (("gridness", either[f"grid|{T}|{G}"].dropna()), ("log10 U2", np.log10(either[f"u2|{T}"].dropna()))):
v = np.sort(values.to_numpy())
gap = np.diff(v).max() / (v[-1] - v[0])
row[f"largest gap, {axis} (fraction of range)"] = gap
rows.append(row)
out = pd.DataFrame(rows)
out["continuous"] = (out.conjunctive > 0) & (out.filter(like="largest gap").max(axis=1) <= 0.25)
return out
cont = continuity_of(tracked)
print(cont.describe().round(3).to_string())
print(f"\nReadings with conjunctive cells and no gap above a quarter: {int(cont.continuous.sum())} of {len(cont)}")
fig, ax = plt.subplots(figsize=(5, 3.6))
col, u2 = f"grid|{READING[0]}|{READING[1]}", f"u2|{READING[0]}"
c = sg.classify(tracked, *READING)
for label, sel, style in (("conjunctive", c.conj, "o"), ("other", ~c.conj, ".")):
ax.scatter(tracked[sel][col], tracked[sel][u2], s=10, marker=style, label=label)
ax.set_yscale("log"); ax.set_xlabel("Gridness"); ax.set_ylabel("Watson U2"); ax.legend(frameon=False, fontsize=8)
print(f"Plotted: gridness {tracked[col].min():.2f} to {tracked[col].max():.2f}, U2 {tracked[u2].min():.2g} to {tracked[u2].max():.1f}; "
f"{int(tracked[col].isna().sum())} cells without a gridness are not shown")
print("Outcome:", outcome_of(np.where(cont.continuous, "reproduced", "not_reproduced")))
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
tracked_once = tracked[first_copy.reindex(tracked.index)]
c1 = continuity_of(tracked_once)
print(f"Variant, each (rat, depth, hemisphere, name) once: conjunctive cells {c1.conjunctive.min()} to {c1.conjunctive.max()}, "
f"largest gap at most {c1.filter(like='largest gap').max().max():.3f} of the range; outcome:",
outcome_of(np.where(c1.continuous, "reproduced", "not_reproduced")))
level conjunctive cells largest gap, gridness (fraction of range) largest gap, log10 U2 (fraction of range) count 112.00 112.000 112.000 112.000 112.000 mean 0.03 117.580 380.330 0.088 0.080 std 0.02 9.423 6.252 0.035 0.026 min 0.01 98.000 366.000 0.039 0.042 25% 0.01 110.000 375.750 0.067 0.058 50% 0.03 117.000 381.000 0.081 0.069 75% 0.05 125.000 385.000 0.095 0.108 max 0.05 141.000 395.000 0.209 0.126 Readings with conjunctive cells and no gap above a quarter: 112 of 112 Plotted: gridness -0.77 to 1.40, U2 0.07 to 136.6; 8 cells without a gridness are not shown Outcome: reproduced
Variant, each (rat, depth, hemisphere, name) once: conjunctive cells 62 to 92, largest gap at most 0.213 of the range; outcome: reproduced
Result: reproduced. Under all 112 readings there are conjunctive cells (98 to 141), and among the 366 to 395 cells passing either criterion the largest gap is at most 0.209 of the range of gridness and 0.126 of the range of log U2, below a quarter.
Counting each (rat, depth, hemisphere, name) once, there are 62 to 92 conjunctive cells and the largest gap is at most 0.213 of a range, so the outcome is the same.
Claim conj-tuning-similar¶
Directional tuning of conjunctive cells did not differ from that of pure head-direction cells (mean U2 18.0 against 19.4, P > 0.05).
Criterion: a claim of absence; (a) two-sided t-test, (b) two-sided Mann-Whitney test of U2, conjunctive against pure head-direction cells, at p < 0.05. Reproduced when no reading finds a difference; not reproduced when all do; methods_underspecified when they disagree; a reproduced absence with means outside tolerance (18.0 +/- 1.8, 19.4 +/- 1.94) is partially reproduced. All two-LED cells are used, as the paper's 53 and 220 cells span all layers.
def conj_u2_of(frame):
rows = []
for T, G, a in sg.readings():
c = sg.classify(frame, T, G, a)
x, y = frame[c.conj][f"u2|{T}"], frame[c.pure_hd][f"u2|{T}"]
rows.append(dict(trials=T, gridness=G, level=a, conjunctive=len(x), pure_hd=len(y), mean_conj=x.mean(), mean_hd=y.mean(),
median_conj=x.median(), median_hd=y.median(), t_p=stats.ttest_ind(x, y).pvalue,
mw_p=stats.mannwhitneyu(x, y, alternative="two-sided").pvalue))
return pd.DataFrame(rows)
conj_u2 = conj_u2_of(tracked)
conj_u2["in_tolerance"] = conj_u2.mean_conj.between(16.2, 19.8) & conj_u2.mean_hd.between(17.46, 21.34)
print(conj_u2.describe().round(4).to_string())
print(f"\nt-tests finding a difference: {int((conj_u2.t_p < 0.05).sum())} of {len(conj_u2)}; Mann-Whitney: {int((conj_u2.mw_p < 0.05).sum())} of {len(conj_u2)}; "
f"means within tolerance: {int(conj_u2.in_tolerance.sum())} of {len(conj_u2)}")
print(conj_u2[(conj_u2.gridness == READING[1])].round(4).to_string())
found = pd.concat([conj_u2.t_p, conj_u2.mw_p]) < 0.05
print("Outcome:", "not_reproduced" if found.all() else "methods_underspecified" if found.any() else
"reproduced" if conj_u2.in_tolerance.any() else "partially_reproduced")
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
cu1 = conj_u2_of(tracked_once)
print(f"Variant, each (rat, depth, hemisphere, name) once: t-tests finding a difference {int((cu1.t_p < 0.05).sum())} of {len(cu1)}, "
f"Mann-Whitney {int((cu1.mw_p < 0.05).sum())} of {len(cu1)}")
level conjunctive pure_hd mean_conj mean_hd median_conj median_hd t_p mw_p
count 112.0000 112.0000 112.0000 112.0000 112.0000 112.0000 112.0000 112.0000 112.0000
mean 0.0300 117.5804 171.1696 15.7585 21.7664 11.6550 13.0789 0.0220 0.1525
std 0.0201 9.4226 8.0078 1.7942 2.4631 1.5249 1.9364 0.0333 0.1785
min 0.0100 98.0000 146.0000 11.9541 18.2209 8.7721 10.5305 0.0001 0.0008
25% 0.0100 110.0000 165.7500 14.2703 19.3464 10.1398 11.3597 0.0031 0.0288
50% 0.0300 117.0000 171.0000 15.7010 21.6891 11.3875 12.6781 0.0081 0.0821
75% 0.0500 125.0000 177.2500 17.1950 24.0245 12.7976 14.8967 0.0225 0.1994
max 0.0500 141.0000 187.0000 19.4344 25.3571 15.4947 17.3972 0.1636 0.8471
t-tests finding a difference: 96 of 112; Mann-Whitney: 42 of 112; means within tolerance: 3 of 112
trials gridness level conjunctive pure_hd mean_conj mean_hd median_conj median_hd t_p mw_p in_tolerance
0 pooled 3cm|enclose|blob|max|area 0.05 120 185 15.4387 23.8781 11.4422 13.8633 0.0008 0.0090 False
1 pooled 3cm|enclose|blob|max|area 0.01 105 176 17.2848 24.9435 13.6882 15.9459 0.0045 0.0658 False
56 first 3cm|enclose|blob|max|area 0.05 116 180 12.6252 19.4223 9.4471 11.5754 0.0006 0.0066 False
57 first 3cm|enclose|blob|max|area 0.01 103 170 13.8862 20.3928 11.2358 12.4484 0.0019 0.0231 False
Outcome: methods_underspecified
Variant, each (rat, depth, hemisphere, name) once: t-tests finding a difference 71 of 112, Mann-Whitney 26 of 112
Result: blocked, methods_underspecified. The t-test finds a difference in U2 between conjunctive and pure head-direction cells under 98 of 112 readings (mean U2 12.0 to 19.4 against 18.2 to 25.4), the Mann-Whitney test under 42 of 112 (medians 8.8 to 15.5 against 10.5 to 17.4). The means are within tolerance under 3 of 112 readings.
Counting each (rat, depth, hemisphere, name) once, the t-test finds a difference under 71 of 112 readings and the Mann-Whitney test under 26, so the tests still disagree.
Claim cue-rotation-coherent: blocked¶
The claim needs the cue-rotation trials of five rats in the small circular box, and the position of
the cue card in each. No file names its enclosure, its condition or a cue rotation, and there is no
trial table; the description says only that the rat ran in a 1 x 1 m enclosure. The small circle is not
there in another guise either: every circular file spans 200 cm (data check). The rotated
trials cannot be found, so the claim is blocked with data_missing.
Claim speed¶
Regression showed a consistent positive speed-rate relation in nearly all grid cells (141 of 150), head-direction cells (153 of 220) and conjunctive cells (45 of 53).
Criterion: per cell, firing rate in 1-s bins regressed on speed over the trial, all bins kept. Counting: (a) slope positive with P < 0.001; (b) slope positive, the count then tested against half by a one-sided binomial sign test at p < 0.001. Grid cells are the grid cells that are not conjunctive, from all rats; head-direction (pure) and conjunctive cells from two-LED files. Speed from (a) path length, (b) displacement over the bin; positions (a) smoothed, (b) raw; trials as O1; significance from (a) the slope, (b) Pearson's r (the same test as the slope for a simple regression), (c) Spearman's rank correlation. As answered, the counting and significance readings are readings of the reported number, and for each combination of method details (speed measure, positions, trials and cell classes) the best of them is taken: reproduced when under one of them all three fractions are within tolerance (0.94 +/- 0.094, 0.695 +/- 0.070, 0.849 +/- 0.085); partially reproduced when none does but under one the grid and conjunctive fractions exceed the head-direction fraction and all exceed one half. The claim takes that outcome when every combination of method details gives it; methods_underspecified when they disagree.
EXPECTED_SPEED = {"grid_only": (0.94, 0.094), "pure_hd": (0.695, 0.070), "conj": (0.849, 0.085)}
def speed_table_of(frame):
rows = []
for T, G, a in sg.readings():
c = sg.classify(frame, T, G, a)
for sr in sg.SPEED_READINGS:
slope = frame[f"slope|{T}|{sr}"]
for count, sig in (("P < 0.001, slope", f"p_linear|{T}|{sr}"), ("P < 0.001, Spearman", f"p_spearman|{T}|{sr}"),
("positive slope", None)):
passed = (slope > 0) & ((frame[sig] < 0.001) if sig else True)
row = dict(trials=T, gridness=G, level=a, speed=sr, count=count)
for cls in EXPECTED_SPEED:
sel = c[cls]
row[f"{cls} n"] = int(sel.sum())
row[cls] = passed[sel].mean()
if sig is None:
row[f"{cls} sign p"] = stats.binomtest(int(passed[sel].sum()), int(sel.sum()), 0.5, alternative="greater").pvalue
tol = all(within(row[k], *v) for k, v in EXPECTED_SPEED.items())
order = row["grid_only"] > row["pure_hd"] and row["conj"] > row["pure_hd"] and min(row[k] for k in EXPECTED_SPEED) > 0.5
# under the positive-slope count, the count of each class must also pass the sign test at p < 0.001
sign_ok = sig is not None or all(row[f"{k} sign p"] < 0.001 for k in EXPECTED_SPEED)
row["sign_test_ok"] = sign_ok
row["outcome without the sign test"] = "reproduced" if tol else "partially_reproduced" if order else "not_reproduced"
row["outcome"] = row["outcome without the sign test"] if sign_ok else "not_reproduced"
rows.append(row)
return pd.DataFrame(rows)
speed_table = speed_table_of(known)
print(speed_table.groupby(["count", "speed"]).agg(
grid_min=("grid_only", "min"), grid_max=("grid_only", "max"), hd_min=("pure_hd", "min"), hd_max=("pure_hd", "max"),
conj_min=("conj", "min"), conj_max=("conj", "max"),
reproduced=("outcome", lambda o: int((o == "reproduced").sum())),
partial=("outcome", lambda o: int((o == "partially_reproduced").sum())),
readings=("outcome", "size")).round(3).to_string())
print("\nCells per class over the readings:", {k: f"{speed_table[f'{k} n'].min()} to {speed_table[f'{k} n'].max()}" for k in EXPECTED_SPEED})
sign = speed_table[speed_table["count"] == "positive slope"]
print("Sign test p (positive slope against half), largest over readings:",
{k: f"{sign[f'{k} sign p'].max():.2g}" for k in EXPECTED_SPEED})
print(f"Positive-slope rows failing the sign test: {int((~sign.sign_test_ok).sum())} of {len(sign)}; by class: "
f"{ {k: int((sign[f'{k} sign p'] >= 0.001).sum()) for k in EXPECTED_SPEED} }")
print("Positive-slope rows, outcomes with and without the sign test:",
sign.outcome.value_counts().to_dict(), sign["outcome without the sign test"].value_counts().to_dict())
print("Outcomes over all rows:", speed_table.outcome.value_counts().to_dict())
# The counting and significance readings are readings of the reported number: for each combination of
# method details (trials, gridness, level, speed measure and positions) the best of them is taken.
RANK = {"reproduced": 0, "partially_reproduced": 1, "not_reproduced": 2}
DETAILS = ["trials", "gridness", "level", "speed"]
best = (speed_table.assign(rank=speed_table.outcome.map(RANK)).sort_values("rank", kind="stable")
.drop_duplicates(DETAILS).reset_index(drop=True))
print(f"\nBest reading of the number per combination of method details ({len(best)} combinations):",
best.outcome.value_counts().to_dict(), "->", outcome_of(best.outcome))
best_without = (speed_table.assign(rank=speed_table["outcome without the sign test"].map(RANK)).sort_values("rank", kind="stable")
.drop_duplicates(DETAILS))
print("Variant, the sign test not applied:", best_without["outcome without the sign test"].value_counts().to_dict())
worst = best[best.outcome == "not_reproduced"][DETAILS]
print("Combinations not reproduced under any reading of the number, every reading:")
print(speed_table.merge(worst, on=DETAILS)[DETAILS + ["count", "grid_only", "pure_hd", "conj", "outcome"]].round(3).to_string())
print(best.groupby(["speed", "trials"]).outcome.value_counts().unstack(fill_value=0).to_string())
print(best.groupby("level").outcome.value_counts().unstack(fill_value=0).to_string())
print(best.assign(bin=best.gridness.str.split("|").str[0]).groupby("bin").outcome.value_counts().unstack(fill_value=0).to_string())
first = speed_table[(speed_table.trials == READING[0]) & (speed_table.gridness == READING[1]) & (speed_table.level == READING[2])]
print("\nFirst classification reading, every speed and count reading:")
print(first[["speed", "count", "grid_only n", "grid_only", "pure_hd n", "pure_hd", "conj n", "conj", "outcome"]].round(3).to_string())
# Variant that does not decide the outcome: each (rat, depth, hemisphere, name) once.
st1 = speed_table_of(once)
best1 = st1.assign(rank=st1.outcome.map(RANK)).sort_values("rank", kind="stable").drop_duplicates(DETAILS)
print(f"\nVariant, each (rat, depth, hemisphere, name) once: best reading of the number per combination ({len(best1)}):",
best1.outcome.value_counts().to_dict(), "->", outcome_of(best1.outcome))
print(" fractions by count:", {k: {cls: f"{x[cls].min():.3f} to {x[cls].max():.3f}" for cls in EXPECTED_SPEED} for k, x in st1.groupby("count")})
grid_min grid_max hd_min hd_max conj_min conj_max reproduced partial readings
count speed
P < 0.001, Spearman displacement|raw 0.342 0.428 0.135 0.205 0.195 0.341 0 0 112
displacement|smoothed 0.342 0.421 0.145 0.211 0.204 0.333 0 0 112
path|raw 0.345 0.446 0.200 0.265 0.274 0.414 0 0 112
path|smoothed 0.345 0.446 0.211 0.282 0.304 0.415 0 0 112
P < 0.001, slope displacement|raw 0.129 0.241 0.094 0.161 0.081 0.197 0 0 112
displacement|smoothed 0.135 0.241 0.094 0.168 0.081 0.197 0 0 112
path|raw 0.159 0.259 0.135 0.224 0.117 0.254 0 0 112
path|smoothed 0.165 0.253 0.129 0.202 0.135 0.254 0 0 112
positive slope displacement|raw 0.827 0.874 0.628 0.708 0.686 0.797 6 105 112
displacement|smoothed 0.833 0.880 0.622 0.708 0.686 0.797 13 96 112
path|raw 0.815 0.880 0.660 0.705 0.739 0.822 44 68 112
path|smoothed 0.827 0.886 0.641 0.692 0.726 0.814 56 56 112
Cells per class over the readings: {'grid_only': '157 to 205', 'pure_hd': '146 to 187', 'conj': '98 to 141'}
Sign test p (positive slope against half), largest over readings: {'grid_only': '5.5e-17', 'pure_hd': '0.0015', 'conj': '4.5e-05'}
Positive-slope rows failing the sign test: 2 of 448; by class: {'grid_only': 0, 'pure_hd': 2, 'conj': 0}
Positive-slope rows, outcomes with and without the sign test: {'partially_reproduced': 325, 'reproduced': 119, 'not_reproduced': 4} {'partially_reproduced': 327, 'reproduced': 119, 'not_reproduced': 2}
Outcomes over all rows: {'not_reproduced': 900, 'partially_reproduced': 325, 'reproduced': 119}
Best reading of the number per combination of method details (448 combinations): {'partially_reproduced': 325, 'reproduced': 119, 'not_reproduced': 4} -> methods_underspecified (not_reproduced, partially_reproduced, reproduced)
Variant, the sign test not applied: {'partially_reproduced': 327, 'reproduced': 119, 'not_reproduced': 2}
Combinations not reproduced under any reading of the number, every reading:
trials gridness level speed count grid_only pure_hd conj outcome
0 pooled 1.5cm|enclose|disc|centroid|area 0.01 displacement|smoothed P < 0.001, slope 0.208 0.168 0.142 not_reproduced
1 pooled 1.5cm|enclose|disc|centroid|area 0.01 displacement|smoothed P < 0.001, Spearman 0.393 0.211 0.267 not_reproduced
2 pooled 1.5cm|enclose|disc|centroid|area 0.01 displacement|smoothed positive slope 0.836 0.708 0.700 not_reproduced
3 pooled 1.5cm|enclose|disc|centroid|area 0.01 displacement|raw P < 0.001, slope 0.208 0.161 0.142 not_reproduced
4 pooled 1.5cm|enclose|disc|centroid|area 0.01 displacement|raw P < 0.001, Spearman 0.399 0.205 0.275 not_reproduced
5 pooled 1.5cm|enclose|disc|centroid|area 0.01 displacement|raw positive slope 0.831 0.708 0.700 not_reproduced
6 first 1.5cm|enclose|disc|centroid|farthest 0.01 displacement|smoothed P < 0.001, slope 0.183 0.099 0.135 not_reproduced
7 first 1.5cm|enclose|disc|centroid|farthest 0.01 displacement|smoothed P < 0.001, Spearman 0.360 0.154 0.243 not_reproduced
8 first 1.5cm|enclose|disc|centroid|farthest 0.01 displacement|smoothed positive slope 0.880 0.623 0.748 not_reproduced
9 first 1.5cm|centre|blob|centroid|area 0.01 displacement|smoothed P < 0.001, slope 0.150 0.115 0.111 not_reproduced
10 first 1.5cm|centre|blob|centroid|area 0.01 displacement|smoothed P < 0.001, Spearman 0.352 0.160 0.231 not_reproduced
11 first 1.5cm|centre|blob|centroid|area 0.01 displacement|smoothed positive slope 0.865 0.622 0.744 not_reproduced
outcome not_reproduced partially_reproduced reproduced
speed trials
displacement|raw first 0 53 3
pooled 1 52 3
displacement|smoothed first 2 52 2
pooled 1 44 11
path|raw first 0 19 37
pooled 0 49 7
path|smoothed first 0 23 33
pooled 0 33 23
outcome not_reproduced partially_reproduced reproduced
level
0.01 4 170 50
0.05 0 155 69
outcome not_reproduced partially_reproduced reproduced
bin
1.5cm 4 157 63
3cm 0 168 56
First classification reading, every speed and count reading:
speed count grid_only n grid_only pure_hd n pure_hd conj n conj outcome
0 path|smoothed P < 0.001, slope 160 0.238 185 0.184 120 0.242 not_reproduced
1 path|smoothed P < 0.001, Spearman 160 0.431 185 0.265 120 0.392 not_reproduced
2 path|smoothed positive slope 160 0.838 185 0.681 120 0.767 partially_reproduced
3 path|raw P < 0.001, slope 160 0.231 185 0.200 120 0.242 not_reproduced
4 path|raw P < 0.001, Spearman 160 0.431 185 0.249 120 0.400 not_reproduced
5 path|raw positive slope 160 0.825 185 0.697 120 0.767 partially_reproduced
6 displacement|smoothed P < 0.001, slope 160 0.219 185 0.151 120 0.175 not_reproduced
7 displacement|smoothed P < 0.001, Spearman 160 0.412 185 0.195 120 0.308 not_reproduced
8 displacement|smoothed positive slope 160 0.844 185 0.686 120 0.750 partially_reproduced
9 displacement|raw P < 0.001, slope 160 0.219 185 0.146 120 0.175 not_reproduced
10 displacement|raw P < 0.001, Spearman 160 0.425 185 0.195 120 0.308 not_reproduced
11 displacement|raw positive slope 160 0.838 185 0.692 120 0.742 partially_reproduced
Variant, each (rat, depth, hemisphere, name) once: best reading of the number per combination (448): {'partially_reproduced': 201, 'reproduced': 125, 'not_reproduced': 122} -> methods_underspecified (not_reproduced, partially_reproduced, reproduced)
fractions by count: {'P < 0.001, Spearman': {'grid_only': '0.354 to 0.495', 'pure_hd': '0.139 to 0.318', 'conj': '0.205 to 0.413'}, 'P < 0.001, slope': {'grid_only': '0.168 to 0.327', 'pure_hd': '0.115 to 0.287', 'conj': '0.082 to 0.293'}, 'positive slope': {'grid_only': '0.805 to 0.896', 'pure_hd': '0.581 to 0.695', 'conj': '0.688 to 0.829'}}
c = sg.classify(known, *READING)
cls = pd.Series(np.select([c.conj, c.grid_only, c.pure_hd], ["conjunctive", "grid", "head direction"], ""), index=known.index)
col = f"slope|{READING[0]}|path|smoothed"
sp = known.assign(cls=cls, positive_p=(known[col] > 0) & (known[f"p_linear|{READING[0]}|path|smoothed"] < 0.001))
print("Per rat, first reading, path length from smoothed positions, P < 0.001 and positive:")
print(sp[sp.cls != ""].groupby(["subject", "cls"]).positive_p.agg(["sum", "size"]).unstack("cls").to_string())
print(f"\nCorrelation of rate with speed per cell (median by class): "
f"{sp[sp.cls != ''].groupby('cls')[f'r|{READING[0]}|path|smoothed'].median().round(3).to_dict()}")
fig, ax = plt.subplots(figsize=(5, 3))
for k in ["grid", "head direction", "conjunctive"]:
ax.hist(sp[sp.cls == k][col], bins=np.linspace(-0.2, 0.5, 36), histtype="step", label=k, density=True)
ax.axvline(0, color="k", lw=1); ax.set_xlabel("Slope (Hz per cm/s)"); ax.legend(frameon=False, fontsize=8)
out = sp[sp.cls != ""][col]
print(f"Slopes {out.min():.3f} to {out.max():.3f} Hz per cm/s; outside the axes: {int(((out < -0.2) | (out > 0.5)).sum())}")
Per rat, first reading, path length from smoothed positions, P < 0.001 and positive:
sum size
cls conjunctive grid head direction conjunctive grid head direction
subject
10073 NaN 6.0 NaN NaN 7.0 NaN
10697 NaN 3.0 NaN NaN 9.0 NaN
10704 NaN 6.0 NaN NaN 24.0 NaN
10884 NaN 1.0 NaN NaN 22.0 NaN
10938 NaN 1.0 NaN NaN 5.0 NaN
10962 NaN 1.0 NaN NaN 4.0 NaN
11016 NaN 2.0 NaN NaN 8.0 NaN
11025 2.0 8.0 1.0 4.0 18.0 4.0
11084 1.0 0.0 1.0 3.0 4.0 20.0
11138 2.0 0.0 2.0 6.0 15.0 20.0
11207 16.0 2.0 25.0 52.0 13.0 112.0
11265 7.0 7.0 3.0 51.0 22.0 23.0
11278 NaN 1.0 NaN NaN 7.0 NaN
11340 1.0 0.0 0.0 4.0 2.0 3.0
11343 NaN NaN 2.0 NaN NaN 3.0
Correlation of rate with speed per cell (median by class): {'conjunctive': 0.052, 'grid': 0.078, 'head direction': 0.05}
Slopes -0.352 to 0.241 Hz per cm/s; outside the axes: 1
Result: blocked, methods_underspecified. Counted as cells with a positive slope at P < 0.001, 0.129 to 0.446 of grid cells, 0.094 to 0.282 of head-direction cells and 0.081 to 0.415 of conjunctive cells pass, against 0.94, 0.695 and 0.849, under every combination of readings. Counted by a positive slope, 0.815 to 0.886 of grid cells, 0.622 to 0.708 of head-direction cells and 0.686 to 0.822 of conjunctive cells pass. Under that count the criterion also asks that each class's count pass a one-sided sign test against half at p < 0.001; the head-direction count fails it in 2 of the 448 positive-slope rows (largest p 0.0015), which are then not reproduced. Taking the best reading of the number for each of the 448 combinations of method details, 119 give reproduced (all three fractions within tolerance), 325 partially reproduced (the order holds) and 4 not reproduced. The combinations disagree, so the claim is methods_underspecified. Without the sign test the split is 119, 327 and 2, as in the previous version, which computed the test but did not apply it. Reproduced combinations are most frequent with path-length speeds and the first trial (37 and 33 of 56) and rare with displacement speeds (2 to 11 of 56). Counting each (rat, depth, hemisphere, name) once, the best reading per combination gives 125 reproduced, 201 partially reproduced and 122 not reproduced, so the method details still disagree.
The question asked how to read the criterion, which called the claim reproduced "under a combination of readings" and also methods_underspecified "when readings disagree". The answer separates readings of the number, of which the best counts, from method details, which must agree. The outcome is the same as before the answer, now for a stated reason: the method details disagree.
The earlier attempt called this claim reproduced from a different measure (the correlation of rate with speed over 5 cm/s bins, against shuffled spike trains) and a different claim (the abstract's "all cell types were modulated by running speed").
Claim speed-slope-steeper¶
The slope of the speed-rate regression was steeper for conjunctive cells and grid cells than for head-direction cells, and did not differ between conjunctive and grid cells.
Criterion: slopes from the speed claim's regressions, its readings; one-way ANOVA and Tukey HSD. Reproduced when the ANOVA has p < 0.005, Tukey gives conjunctive and grid each steeper than head-direction at p < 0.05, and conjunctive against grid p > 0.05; partially reproduced when the ANOVA is significant at p < 0.05 and one of the two comparisons with head-direction is.
rows = []
for T, G, a in sg.readings():
c = sg.classify(known, T, G, a)
for sr in sg.SPEED_READINGS:
col = f"slope|{T}|{sr}"
g, h, j = known[c.grid_only][col], known[c.pure_hd][col], known[c.conj][col]
anova = stats.f_oneway(g, h, j)
tukey = stats.tukey_hsd(g, h, j).pvalue
conj_hd = tukey[2, 1] < 0.05 and j.mean() > h.mean()
grid_hd = tukey[0, 1] < 0.05 and g.mean() > h.mean()
outcome = ("reproduced" if anova.pvalue < 0.005 and conj_hd and grid_hd and tukey[2, 0] > 0.05 else
"partially_reproduced" if anova.pvalue < 0.05 and (conj_hd or grid_hd) else "not_reproduced")
rows.append(dict(trials=T, gridness=G, level=a, speed=sr, df=f"F(2,{len(g) + len(h) + len(j) - 3})", F=anova.statistic,
anova_p=anova.pvalue, mean_grid=g.mean(), mean_hd=h.mean(), mean_conj=j.mean(),
tukey_conj_hd=tukey[2, 1], tukey_grid_hd=tukey[0, 1], tukey_conj_grid=tukey[2, 0], outcome=outcome))
slopes = pd.DataFrame(rows)
print(slopes.groupby("speed").agg(F_min=("F", "min"), F_max=("F", "max"), anova_p_max=("anova_p", "max"),
conj_hd_p_max=("tukey_conj_hd", "max"), grid_hd_p_max=("tukey_grid_hd", "max"),
conj_grid_p_min=("tukey_conj_grid", "min"), conj_grid_p_max=("tukey_conj_grid", "max"),
mean_grid=("mean_grid", "median"), mean_hd=("mean_hd", "median"), mean_conj=("mean_conj", "median")).round(4).to_string())
print(f"\nOver all readings: F from {slopes.F.min():.1f} to {slopes.F.max():.1f} with {sorted(slopes.df.unique())[0]} to "
f"{sorted(slopes.df.unique())[-1]}; ANOVA p at most {slopes.anova_p.max():.3f}; Tukey p conjunctive against head-direction "
f"{slopes.tukey_conj_hd.min():.2f} to {slopes.tukey_conj_hd.max():.2f}, grid against head-direction at most "
f"{slopes.tukey_grid_hd.max():.4f}, conjunctive against grid {slopes.tukey_conj_grid.min():.4f} to {slopes.tukey_conj_grid.max():.2f}")
print("\nFirst classification reading:")
print(slopes[(slopes.trials == READING[0]) & (slopes.gridness == READING[1]) & (slopes.level == READING[2])].round(4).to_string())
print("\nOutcomes:", slopes.outcome.value_counts().to_dict(), "->", outcome_of(slopes.outcome))
F_min F_max anova_p_max conj_hd_p_max grid_hd_p_max conj_grid_p_min conj_grid_p_max mean_grid mean_hd mean_conj
speed
displacement|raw 6.3166 13.6427 0.0020 0.9901 0.0012 0.0004 0.1935 0.0311 0.0129 0.0187
displacement|smoothed 6.2127 13.4503 0.0022 0.9877 0.0014 0.0004 0.1904 0.0314 0.0129 0.0189
path|raw 4.5012 10.9820 0.0116 0.9909 0.0084 0.0031 0.2946 0.0334 0.0158 0.0214
path|smoothed 5.4781 12.0085 0.0044 0.9827 0.0030 0.0022 0.2069 0.0340 0.0150 0.0212
Over all readings: F from 4.5 to 13.6 with F(2,444) to F(2,488); ANOVA p at most 0.012; Tukey p conjunctive against head-direction 0.06 to 0.99, grid against head-direction at most 0.0084, conjunctive against grid 0.0004 to 0.29
First classification reading:
trials gridness level speed df F anova_p mean_grid mean_hd mean_conj tukey_conj_hd tukey_grid_hd tukey_conj_grid outcome
0 pooled 3cm|enclose|blob|max|area 0.05 path|smoothed F(2,462) 6.7644 0.0013 0.0337 0.0162 0.0213 0.5933 0.0009 0.0577 partially_reproduced
1 pooled 3cm|enclose|blob|max|area 0.05 path|raw F(2,462) 5.8685 0.0030 0.0331 0.0168 0.0216 0.6338 0.0023 0.0843 partially_reproduced
2 pooled 3cm|enclose|blob|max|area 0.05 displacement|smoothed F(2,462) 7.1355 0.0009 0.0310 0.0143 0.0193 0.5603 0.0006 0.0522 partially_reproduced
3 pooled 3cm|enclose|blob|max|area 0.05 displacement|raw F(2,462) 7.1664 0.0009 0.0307 0.0142 0.0191 0.5661 0.0006 0.0504 partially_reproduced
Outcomes: {'partially_reproduced': 448} -> partially_reproduced
Result: blocked, methods_underspecified, because it depends on speed. The analysis ran:
under all 448 combinations the ANOVA is significant (F 4.5 to 13.6, p at most 0.012) and grid
cells have steeper slopes than head-direction cells (Tukey p at most 0.0084), but conjunctive
cells do not differ from head-direction cells (Tukey p 0.06 to 0.99), which on its own would make
the claim partially reproduced under every reading. Conjunctive and grid cells differ under some
readings (Tukey p 0.0004 to 0.29). The degrees of freedom are F(2,444) to F(2,488). The verifier would call this claim partially
reproduced, on these numbers; it stays blocked because the claims file lists speed under its
depends_on, and the protocol (Author, step 1.5) says that "a claim that depends on a blocked
claim is blocked for the same reason". That is why blocked_by is methods_underspecified, the
reason speed is blocked. The disagreement is recorded.
Claim speed-intercept¶
The intercept of the speed-rate regression was positive in all cells but one (422 of 423), and mean rates at the lowest speeds (0 to 3 cm/s) were about 2.3 to 2.5 Hz in each cell class, so firing persisted during brief stops.
Criterion: regressions and readings as in the speed claim. Averages: (a) the mean over cells of each cell's mean rate in the 1-s bins from 0 to 3 cm/s; (b) the mean over all such bins of the class pooled. Reproduced when, under a reading, the fraction is within 0.998 +/- 0.0998 and the rates within 2.26 +/- 0.03, 2.50 +/- 0.02 and 2.39 +/- 0.05 Hz; partially reproduced when the fraction holds and the rates are within 10% of the printed values.
EXPECTED_LOW = {"grid_only": 2.26, "pure_hd": 2.50, "conj": 2.39}
SPREAD_LOW = {"grid_only": 0.03, "pure_hd": 0.02, "conj": 0.05}
rows = []
for T, G, a in sg.readings():
c = sg.classify(known, T, G, a)
sel = c.grid_only | c.pure_hd | c.conj
for sr in sg.SPEED_READINGS:
positive = (known[f"intercept|{T}|{sr}"] > 0)[sel]
for avg in ("mean of cells", "pooled bins"):
row = dict(trials=T, gridness=G, level=a, speed=sr, average=avg, cells=int(sel.sum()),
positive_intercept=positive.mean(), negative=int((~positive).sum()))
for k in EXPECTED_LOW:
g = known[c[k]]
if avg == "mean of cells":
row[k] = g[f"low_rate|{T}|{sr}"].mean()
else:
row[k] = g[f"low_spikes|{T}|{sr}"].sum() / g[f"low_bins|{T}|{sr}"].sum()
fraction_ok = within(row["positive_intercept"], 0.998, 0.0998)
exact = all(within(row[k], EXPECTED_LOW[k], SPREAD_LOW[k]) for k in EXPECTED_LOW)
ten = all(within(row[k], EXPECTED_LOW[k], 0.1 * EXPECTED_LOW[k]) for k in EXPECTED_LOW)
row["outcome"] = ("reproduced" if fraction_ok and exact else
"partially_reproduced" if fraction_ok and ten else "not_reproduced")
rows.append(row)
low = pd.DataFrame(rows)
print(low.groupby(["speed", "average"]).agg(
positive_min=("positive_intercept", "min"), positive_max=("positive_intercept", "max"),
negative_min=("negative", "min"), negative_max=("negative", "max"), grid_min=("grid_only", "min"), grid_max=("grid_only", "max"),
hd_min=("pure_hd", "min"), hd_max=("pure_hd", "max"), conj_min=("conj", "min"), conj_max=("conj", "max"),
reproduced=("outcome", lambda o: int((o == "reproduced").sum())),
partial=("outcome", lambda o: int((o == "partially_reproduced").sum())), readings=("outcome", "size")).round(3).to_string())
lowbins = known[f"low_bins|{READING[0]}|path|smoothed"]
print(f"\n1-s bins at 0 to 3 cm/s per cell (first reading, path length, smoothed): median {lowbins.median():.0f}, "
f"range {lowbins.min()} to {lowbins.max()}")
print("Outcomes:", low.outcome.value_counts().to_dict())
# Variant that does not decide the outcome (rescaling): in the 19 circular files spanning 200 cm the low-speed bin
# taken up to 3 cm/s times 200/180, as if their positions were scaled to the SOM's 180 cm circle; other files as deposited.
circular = set(extent[(extent.larger_span == 200) & (extent.outside_circle < 0.01)].path)
in_circle = known.path.isin(circular)
rows = []
for T, G, a in sg.readings():
c = sg.classify(known, T, G, a)
for sr in sg.SPEED_READINGS:
row = dict(trials=T, gridness=G, level=a, speed=sr)
for k in EXPECTED_LOW:
g = known[c[k]]
row[k] = g[f"low_rate|{T}|{sr}"].where(~in_circle[g.index], g[f"low_rate_rescaled|{T}|{sr}"]).mean()
row["within 10%"] = all(within(row[k], EXPECTED_LOW[k], 0.1 * EXPECTED_LOW[k]) for k in EXPECTED_LOW)
rows.append(row)
low_rescaled = pd.DataFrame(rows)
print(f"\nVariant, the circular files rescaled to 180 cm ({len(circular)} files, {int(in_circle.sum())} cells), mean of cells at 0 to 3 cm/s:",
{k: f"{low_rescaled[k].min():.2f} to {low_rescaled[k].max():.2f} Hz" for k in EXPECTED_LOW},
f"; rows with all three within 10%: {int(low_rescaled['within 10%'].sum())} of {len(low_rescaled)}")
m = known[in_circle]
print(f" in the circular files alone, first reading, path length, smoothed: {m[f'low_rate|{READING[0]}|path|smoothed'].mean():.2f} Hz as deposited, "
f"{m[f'low_rate_rescaled|{READING[0]}|path|smoothed'].mean():.2f} Hz rescaled")
positive_min positive_max negative_min negative_max grid_min grid_max hd_min hd_max conj_min conj_max reproduced partial readings
speed average
displacement|raw mean of cells 0.996 0.998 1 2 1.664 1.904 1.421 1.755 1.202 1.527 0 0 112
pooled bins 0.996 0.998 1 2 1.558 1.752 1.257 1.598 1.227 1.614 0 0 112
displacement|smoothed mean of cells 0.996 0.998 1 2 1.642 1.880 1.429 1.746 1.213 1.525 0 0 112
pooled bins 0.996 0.998 1 2 1.552 1.742 1.259 1.590 1.219 1.605 0 0 112
path|raw mean of cells 0.982 0.992 4 8 1.228 1.413 1.181 1.584 0.939 1.258 0 0 112
pooled bins 0.982 0.992 4 8 1.284 1.518 1.077 1.420 1.009 1.528 0 0 112
path|smoothed mean of cells 0.991 0.996 2 4 1.313 1.579 1.232 1.555 1.019 1.337 0 0 112
pooled bins 0.991 0.996 2 4 1.282 1.459 1.121 1.469 1.114 1.522 0 0 112
1-s bins at 0 to 3 cm/s per cell (first reading, path length, smoothed): median 32, range 3 to 443
Outcomes: {'not_reproduced': 896}
Variant, the circular files rescaled to 180 cm (19 files, 120 cells), mean of cells at 0 to 3 cm/s: {'grid_only': '1.22 to 1.90 Hz', 'pure_hd': '1.18 to 1.76 Hz', 'conj': '0.93 to 1.53 Hz'} ; rows with all three within 10%: 0 of 448
in the circular files alone, first reading, path length, smoothed: 0.99 Hz as deposited, 0.97 Hz rescaled
Result: blocked, methods_underspecified, because it depends on speed. The analysis ran: the
intercept is positive in 98.2% to 99.8% of cells (at most 8 negative), within tolerance, but the mean
rates at 0 to 3 cm/s are 1.2 to 1.9 Hz for grid cells, 1.1 to 1.8 Hz for head-direction cells and
0.9 to 1.6 Hz for conjunctive cells, more than 10% below the paper's 2.26, 2.50 and 2.39 Hz under
every reading, which on its own would make the claim not reproduced. Speeds depend on the scale
of the positions, which the files rescale (data check); the rescaling is about 11% in the 19
circular files. Taking in those files the bins up to 3 cm/s times 200/180, as if their positions
were scaled to the SOM's 180 cm circle, the class means are 1.22 to 1.90, 1.18 to 1.76 and 0.93
to 1.53 Hz, and no row has all three within 10%; in the 120 cells of those files the mean moves
from 0.99 to 0.97 Hz under the first reading. The rescaling does not explain the low rates. (A
constant scale within a file multiplies the slopes by a constant, so it changes neither their sign
nor their p-values, and the speed counts do not depend on it.) The verifier would call this
claim not reproduced; it stays blocked because it depends on speed, for the same reason
(Author, step 1.5), and the disagreement is recorded.
Summary¶
| Claim | Outcome | Earlier attempt (-01) |
|---|---|---|
layer2-grid |
partially reproduced | partially reproduced |
grid-all-layers |
reproduced | not tested |
gridness-range-similar |
blocked: methods_underspecified | not tested |
grid-stable |
blocked: methods_underspecified | not tested |
spacing-dorsoventral |
blocked: data_missing | not tested |
fieldsize-dorsoventral |
blocked: data_missing | not tested |
grid-phase-orientation |
blocked: methods_underspecified | not tested |
hd-layers |
not reproduced | not tested |
hd-with-grid-colocalized |
blocked: methods_underspecified | not tested |
hd-tuning-breadth-layers |
blocked: methods_underspecified | not tested |
hd-full-range |
reproduced | not tested |
hd-colocalized-distributed |
reproduced | not tested |
hd-stable-trials |
reproduced | not tested |
hd-rates |
not reproduced | not tested |
deep-hd-conjunctive |
not reproduced | reproduced |
gridness-hd-continuous |
reproduced | not tested |
conj-tuning-similar |
blocked: methods_underspecified | not tested |
cue-rotation-coherent |
blocked: data_missing | not tested |
speed |
blocked: methods_underspecified | reproduced |
speed-slope-steeper |
blocked: methods_underspecified (depends on speed) |
not tested |
speed-intercept |
blocked: methods_underspecified (depends on speed) |
not tested |
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.
The five questions this attempt asked were answered on 2026-10-20; the answers changed no outcome
in the end. After them, grid-phase-orientation was called partially reproduced, an outcome its
criterion does not name; the verifier's first round pointed this out, and it is again blocked as
methods_underspecified, since both of its parts hold in every layer under 88 of 224 readings and
not under the others.
The verifier's first round also asked for outputs, which were added: the gridness of time-shifted
spike trains (0.297 of shifted maps have gridness above 0, so the paper's criterion is lax on
these data; above the 95% point of the shifted gridness layer III has more grid cells than layer
II), the tracking of the two files of rat 11265 that hold every layer II head-direction and
conjunctive cell (normal LED geometry; without them layer II has none), the LED geometry of every
two-LED file (two files have the LEDs more than 15 cm apart in most samples), the sign test applied
in the speed outcome and the electrodes table. The second round agreed with every outcome and
asked for more, now added: each (rat, depth, hemisphere, name) counted once for every tested claim
but the two that depend on speed (no outcome changes under it), an outcome line in the cells of
grid-all-layers, gridness-range-similar and hd-tuning-breadth-layers, which lacked one,
pairwise tests locating the gridness-range-similar difference (it lies mostly between layer III
and the others), reading (b) of the chi-square for the deep-hd-conjunctive variants, the layer
II cells left in the hd-layers variant (6 with plausible tracking), hd-stable-trials without
the third file with questionable LED geometry (the circular-correlation reading then fails under
two of four readings), and the low-speed rates with the circular files rescaled (no change). The
verifier would call speed-slope-steeper partially reproduced and speed-intercept not
reproduced; both stay blocked because they depend on speed, as the protocol says, and the
disagreement is recorded.
How the analysis differs from the paper's:
- Each cell-session counts as a cell; the paper's count of 627 cells is about the dandiset's 620 cell-sessions, but some cells may appear in several files.
- Trials: the 20-minute files are split at the jump in position near 600 s; the 10-minute files are taken as single trials. Positions are smoothed over the whole file, so the 15-point filter spans the join of the two trials for 7 samples on each side, and the pooled maps join two trials whose positions are not centred alike; the first-trial reading of O1 avoids both.
- Positions are as deposited, rescaled (see the data check); LED1 is taken as the red LED.
- Details the claims file leaves open and that this attempt fixed as standard choices: contiguous
means 4-connected; the block correlation
passes when r > 0 with a two-sided p below the level; the small-sample critical value of
Watson's one-sample U2 is Stephens'; the circular correlation is Jammalamadaka and SenGupta's;
sectors for
hd-full-rangestart at 0 degrees; a spike takes the position of the nearest sample. - A peak's centre (its maximum or its centroid) and its radius (of a disc of the same area, or the largest distance to a pixel of the peak) are run as the readings the claims file now names; the central peak takes the same centre and radius.
- Under the paper's criterion, gridness above 0, this dandiset has more grid cells than the paper reports (277 to 329 against 203), and more in the deeper layers; this shifts every claim built on the grid class. Time-shifted spike trains reach gridness above 0 in 0.297 of maps.
- Some counts rest on p-values near 0.05 and have differed by a reading or two between runs of
the same code on different machines (for example the readings under which both parts of
grid-phase-orientationhold, and the t-tests ofconj-tuning-similar); no outcome depends on them. The numbers here are this run's.
What this attempt found about the dandiset is listed in the data check: the five findings of the earlier attempt are still present (one, the missing methods, partly fixed by the supplied paper), and the new ones are the rescaled positions, the two trials joined in the 20-minute files with no trial table, the missing distance from the postrhinal border (the electrodes table gives only "MEC"), the missing enclosure and cue-card labels, two two-LED files with the LEDs more than 15 cm apart in most samples, cells without layer, depth or hemisphere, descriptions that disagree with the files, and no statement of which LED is red.