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.

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

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

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

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

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

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

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

In [8]:
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; depth is 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-09020601 and sub-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.

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

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

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

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

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

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

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

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

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

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.

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

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

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

In [25]:
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}
In [26]:
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
In [27]:
# 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).

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

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.

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

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

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.

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

In [33]:
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-range start 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-orientation hold, and the t-tests of conj-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.