Top-ranked units (Gerken, Darcher et al. 2025, dandiset 001616)¶
Paper: Gerken, Darcher et al. (2025), eLife reviewed preprint 106758 v1, doi:10.7554/eLife.106758.1, full text. Dandiset: DANDI:001616, version 0.260702.0824.
Companion to reproduce_gerken_2025.ipynb, with the same loading, samples, folds and logistic
regression (summer.py). It holds two claims:
| Claim | Status |
|---|---|
| fig7b-top500-overlap | tested |
| fig7a-500-neurons | tested, with the logistic regression in place of the LSTM |
The notebook takes about 8 minutes.
import os
for var in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
os.environ.setdefault(var, "1")
import time
import numpy as np
import pandas as pd
import summer
pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 40)
started_all = started = time.time()
sessions = summer.load_all()
units = summer.unit_table(sessions)
spikes = summer.movie_spikes(sessions)
print(f"loaded {len(sessions)} files, {len(units)} units, from dandiset {summer.DANDISET_ID} "
f"version {summer.DANDISET_VERSION} in {time.time() - started:.0f} s")
loaded 29 files, 2286 units, from dandiset 001616 version 0.260702.0824 in 116 s
edges = sessions[0]["edges_s"]
S = summer.sample_windows(spikes, edges)
n = S.shape[0]
y = summer.bin_labels(sessions[0], "summer")
print(f"patients {units.patient.nunique()}, units {len(units)}; by region {units.region.value_counts().to_dict()}")
print(f"samples x units: {S.shape}; Summer positive in {np.mean(y == 1):.3f} of the samples, values {sorted(np.unique(y).tolist())}")
for f in range(5):
print(f"test fold {f + 1}: " + "; ".join(f"validation fold {v + 1} train {len(a)}, validation {len(b)}, test {len(c)}"
for v in range(5) if v != f for a, b, c in [summer.split(n, f, val_fold=v)]))
patients 29, units 2286; by region {'H': 846, 'A': 559, 'EC': 493, 'PHC': 293, 'PIC': 68, 'other': 27}
samples x units: (59080, 2286); Summer positive in 0.527 of the samples, values [0, 1]
test fold 1: validation fold 2 train 30822, validation 7194, test 7194; validation fold 3 train 26202, validation 7194, test 7194; validation fold 4 train 26202, validation 7194, test 7194; validation fold 5 train 30822, validation 7194, test 7194
test fold 2: validation fold 1 train 30822, validation 7194, test 7194; validation fold 3 train 30822, validation 7194, test 7194; validation fold 4 train 26202, validation 7194, test 7194; validation fold 5 train 26202, validation 7194, test 7194
test fold 3: validation fold 1 train 26202, validation 7194, test 7194; validation fold 2 train 30822, validation 7194, test 7194; validation fold 4 train 30822, validation 7194, test 7194; validation fold 5 train 26202, validation 7194, test 7194
test fold 4: validation fold 1 train 26202, validation 7194, test 7194; validation fold 2 train 26202, validation 7194, test 7194; validation fold 3 train 30822, validation 7194, test 7194; validation fold 5 train 30822, validation 7194, test 7194
test fold 5: validation fold 1 train 30822, validation 7194, test 7194; validation fold 2 train 26202, validation 7194, test 7194; validation fold 3 train 26202, validation 7194, test 7194; validation fold 4 train 30822, validation 7194, test 7194
fig7b-top500-overlap¶
Across the five rankings of neurons by logistic regression weights for Summer, the top 500 neurons share 78 neurons, against about 1 expected by chance.
rng_rank = np.random.default_rng(37)
started = time.time()
keys, jobs = [], []
for f in range(5):
for v in [j for j in range(5) if j != f]:
keys.append((f, v))
jobs.append((summer.fit_logreg, (S, y, *summer.split(n, f, val_fold=v))))
kfits = dict(zip(keys, summer.run_jobs(jobs)))
print(f"{len(jobs)} sub-split fits in {time.time() - started:.0f} s")
rankings, zero_in_top = [], []
for f in range(5):
coefs = [kfits[(f, v)]["coef"] for v in range(5) if v != f]
order = summer.ranking(coefs, rng_rank)
rankings.append(order)
nz = (np.abs(np.array(coefs)) > 0).any(0)
zero_in_top.append(int((~nz[order[:500]]).sum()))
print(f"test fold {f + 1}: C {[kfits[(f, v)]['C'] for v in range(5) if v != f]}, units with a non-zero coefficient in any of the 4 models "
f"{int(nz.sum())}, in all 4 {int((np.abs(np.array(coefs)) > 0).all(0).sum())}; top 500 tied at zero {zero_in_top[-1]}; "
f"sub-split test kappa {np.round([kfits[(f, v)]['kappa'] for v in range(5) if v != f], 3).tolist()}")
N = S.shape[1]
rows = []
for k_ in (100, 350, 500, 750, 1000):
common = set(rankings[0][:k_])
for o in rankings[1:]:
common &= set(o[:k_])
sims = [len(set.intersection(*[set(rng_rank.choice(N, k_, replace=False)) for _ in range(5)])) for _ in range(200)]
rows.append({"top": k_, "common": len(common), "chance_formula": N * (k_ / N) ** 5, "chance_simulated": np.mean(sims),
"paper_chance": {100: 0, 350: 0, 500: 1, 750: 9, 1000: 37}[k_]})
ov = pd.DataFrame(rows)
print(ov.round(2).to_string(index=False))
common500 = set(rankings[0][:500]).intersection(*[set(o[:500]) for o in rankings[1:]])
print("common 500 by region:", units.loc[sorted(common500)].region.value_counts().to_dict(),
"; patients:", units.loc[sorted(common500)].patient.nunique())
# Variant: zero-tied units ordered by unit index instead of a random draw (same order in every ranking).
order_idx = [np.lexsort((np.arange(N), -np.abs(np.array([kfits[(f, v)]["coef"] for v in range(5) if v != f])).mean(0),
-(np.abs(np.array([kfits[(f, v)]["coef"] for v in range(5) if v != f])) > 0).sum(0))) for f in range(5)]
print("variant, ties by unit index: common in top 500:", len(set(order_idx[0][:500]).intersection(*[set(o[:500]) for o in order_idx[1:]])))
20 sub-split fits in 73 s test fold 1: C [0.003, 0.01, 0.003, 0.03], units with a non-zero coefficient in any of the 4 models 1281, in all 4 72; top 500 tied at zero 0; sub-split test kappa [0.459, 0.424, 0.488, 0.434] test fold 2: C [0.001, 0.01, 0.01, 0.03], units with a non-zero coefficient in any of the 4 models 1291, in all 4 47; top 500 tied at zero 0; sub-split test kappa [0.258, 0.299, 0.439, 0.288] test fold 3: C [0.01, 0.01, 0.003, 0.01], units with a non-zero coefficient in any of the 4 models 1195, in all 4 95; top 500 tied at zero 0; sub-split test kappa [0.302, 0.296, 0.316, 0.376] test fold 4: C [0.003, 0.003, 0.03, 0.03], units with a non-zero coefficient in any of the 4 models 1437, in all 4 78; top 500 tied at zero 0; sub-split test kappa [0.26, 0.397, 0.406, 0.108] test fold 5: C [0.03, 0.001, 0.003, 0.003], units with a non-zero coefficient in any of the 4 models 1101, in all 4 28; top 500 tied at zero 0; sub-split test kappa [0.324, 0.422, 0.51, 0.335]
top common chance_formula chance_simulated paper_chance
100 20 0.00 0.00 0
350 65 0.19 0.20 0
500 95 1.14 1.12 1
750 156 8.69 8.81 9
1000 252 36.62 35.70 37
common 500 by region: {'A': 34, 'H': 21, 'EC': 20, 'PHC': 13, 'other': 4, 'PIC': 3} ; patients: 26
variant, ties by unit index: common in top 500: 95
Result: partially reproduced. The five rankings share 95 units in their top 500, against 78 in the paper (tolerance 70.2 to 85.8) and 1.14 expected by chance (1.12 simulated), 83 times chance. No unit in a top 500 was tied at zero, so the tie-break does not matter (95 with ties by unit index). The overlaps for the top 100, 350, 750 and 1000 are 20, 65, 156 and 252, against chance values of 0.00, 0.19, 8.69 and 36.6 that match the paper's (0, 0, 9, 37). The 95 units come from 26 patients and every region.
fig7a-500-neurons¶
Decoding Summer from the 500 top-ranked neurons gives the peak performance (kappa 0.38), above that of the full population.
For each test fold, the logistic regression is refitted on each of its four sub-splits with only the top k units of that fold's ranking, and scored on the shared test fold. Kappa is averaged over the four sub-splits, then over the five test folds (Methods); the SEM is over the five fold means.
rng_top = np.random.default_rng(39)
SIZES = (23, 114, 150, 229, 500, 750, 1000, 1500)
started = time.time()
keys = [(k_, f, v) for k_ in SIZES for f in range(5) for v in range(5) if v != f]
tfits = dict(zip(keys, summer.run_jobs([(summer.fit_logreg, (S, y, *summer.split(n, f, val_fold=v), rankings[f][:k_]))
for k_, f, v in keys])))
tfits |= {(N, f, v): r for (f, v), r in kfits.items()}
print(f"{len(keys)} fits on top-ranked units in {time.time() - started:.0f} s")
rows = []
for k_ in SIZES + (N,):
per_fold = np.array([np.mean([tfits[(k_, f, v)]["kappa"] for v in range(5) if v != f]) for f in range(5)])
pmax = max(summer.permutation_p(tfits[(k_, f, v)], rng_top) for f in range(5) for v in range(5) if v != f)
rows.append({"units": k_, "percent": round(100 * k_ / N, 1), "kappa": per_fold.mean(), "sem": per_fold.std(ddof=1) / np.sqrt(5),
"fold_means": np.round(per_fold, 3).tolist(), "max_perm_p": pmax})
top = pd.DataFrame(rows).set_index("units")
print(top.round(4).to_string())
print("paper (LSTM): peak 0.38 at 500 units; 0.52 at 150 units with a ranking that saw the test data")
best = top.kappa.idxmax()
print(f"500 units {top.kappa[500]:.4f}; all {N} units {top.kappa[N]:.4f}; difference {top.kappa[500] - top.kappa[N]:.4f}; "
f"highest at {best} units ({top.kappa[best]:.4f})")
160 fits on top-ranked units in 162 s
percent kappa sem fold_means max_perm_p units 23 1.0 0.3410 0.0288 [0.424, 0.345, 0.29, 0.266, 0.38] 0.001 114 5.0 0.3779 0.0228 [0.428, 0.321, 0.329, 0.425, 0.387] 0.001 150 6.6 0.3892 0.0164 [0.441, 0.37, 0.345, 0.406, 0.384] 0.001 229 10.0 0.3939 0.0194 [0.455, 0.354, 0.351, 0.397, 0.413] 0.001 500 21.9 0.3751 0.0262 [0.429, 0.34, 0.35, 0.312, 0.445] 0.001 750 32.8 0.3713 0.0260 [0.43, 0.336, 0.344, 0.309, 0.437] 0.001 1000 43.7 0.3629 0.0274 [0.434, 0.325, 0.335, 0.298, 0.422] 0.001 1500 65.6 0.3621 0.0285 [0.428, 0.331, 0.334, 0.287, 0.429] 0.001 2286 100.0 0.3570 0.0293 [0.451, 0.321, 0.322, 0.293, 0.398] 0.001 paper (LSTM): peak 0.38 at 500 units; 0.52 at 150 units with a ranking that saw the test data 500 units 0.3751; all 2286 units 0.3570; difference 0.0181; highest at 229 units (0.3939)
Result: partially reproduced. With the logistic regression, the top 500 units decode Summer with kappa 0.375, above all 2286 units (0.357), by 0.018, less than the SEM over folds (0.026 and 0.029). The peak is not at 500: 229 units give 0.394 and 150 units 0.389; from 229 units upwards the kappa falls steadily, and the top 23 units still give 0.341. Every fit is above chance by the test-label shuffle (p = 0.001). The paper's LSTM peaks at 0.38 with 500 units.
print(f"total run time {(time.time() - started_all) / 60:.1f} min")
total run time 6.2 min