Shin et al. (2025) from dandiset 000248: decoding¶

The decoding claims of the Neuropixels data (Figs. 3a, 3c, 3d; Extended Data Fig. 7), on DANDI:000248 version 0.240502.2344. The header of reproduce_shin_2025.ipynb gives the paper, the text the claims file was written from, the choice of version and the other claims. Loading and the decoder are in illusion.py.

Claim Tested here
fig3a-npx-v1-decoding yes
fig3c-npx-v1-inference yes (further seeds in decoding_checks_shin_2025.ipynb, reading a, and decoding_seed1_shin_2025.ipynb, decoding_seed2_shin_2025.ipynb, reading b)
fig3d-npx-areas-inference yes (further seeds as for fig3c)
ed7b-npx-xre-inference yes
ed7a-npx-no-area-above-v1 yes
In [1]:
import os
os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import time
import warnings

import numpy as np
import pandas as pd
from joblib import Parallel, delayed
from scipy import stats

import illusion as il

warnings.filterwarnings("ignore", category=RuntimeWarning)
pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 200)
N_JOBS = 4

started = time.time()
sessions = il.load_all(ccg=False)
print(f"loaded {len(sessions)} sessions of dandiset {il.DANDISET_ID} version {il.DANDISET_VERSION} "
      f"in {time.time() - started:.0f} s")
loaded 12 sessions of dandiset 000248 version 0.240502.2344 in 132 s

Data check¶

RS units per area and session, and ICwcfg1 trials of the decoded images.

In [2]:
rows = []
for s in sessions:
    u, b = s["units"], s["blocks"]["ICwcfg1"]
    n = {c: int(np.sum(b["codes"] == c)) for c in [il.IC1, il.LC1, il.LC2, il.IC2, il.TRE1, il.TRE2, il.XRE1, il.XRE2]}
    rows.append({"subject": s["subject"], **{f"{a}_RS": int(((u.area == a) & u.rs).sum()) for a in il.AREAS},
                 "trials IC1/LC1/LC2/IC2": "/".join(str(n[c]) for c in [il.IC1, il.LC1, il.LC2, il.IC2]),
                 "TRE1/TRE2": f"{n[il.TRE1]}/{n[il.TRE2]}", "XRE1/XRE2": f"{n[il.XRE1]}/{n[il.XRE2]}"})
units_tab = pd.DataFrame(rows)
print(units_tab.to_string(index=False))
allR = np.concatenate([s["blocks"]["ICwcfg1"]["R"].ravel() for s in sessions])
print(f"IC response (spikes/s): min {allR.min():.1f}, median {np.median(allR):.1f}, max {allR.max():.1f}")
print("Session-areas with >= 50 RS units:", {a: int((units_tab[f"{a}_RS"] >= 50).sum()) for a in il.AREAS},
      "(paper 12, 8, 11, 10, 12, 11)")
subject  V1_RS  LM_RS  RL_RS  AL_RS  PM_RS  AM_RS trials IC1/LC1/LC2/IC2 TRE1/TRE2 XRE1/XRE2
 619293     89      0    156    106    151    107        400/400/400/400   400/400   400/400
 619296    168      4     87     94     95     93        400/400/400/400   400/400   400/400
 620333    114    119    128     91    102    129        400/400/400/400   400/400   400/400
 620334     68    183    100      0     90     82        400/400/400/400   400/400   400/400
 625545    190    181    122    160    173    134        400/400/400/400   400/400   400/400
 625554    179      0    136    139    193    183        400/400/400/400   400/400   400/400
 625555    314      0    208    159    214    198        400/400/400/400   400/400   400/400
 630506     58    131    140    121    208      0        400/400/400/400   400/400   400/400
 631510    129    105     75     87     70     96        400/400/400/400   400/400   400/400
 631570    142     74      0    105    158    109        400/400/400/400   400/400   400/400
 633229    181    172    130    179    210    142        400/400/400/400   400/400   400/400
 637484    172    122    279      0     67     96        400/400/400/400   400/400   400/400
IC response (spikes/s): min 0.0, median 0.0, max 525.0
Session-areas with >= 50 RS units: {'V1': 12, 'LM': 8, 'RL': 11, 'AL': 10, 'PM': 12, 'AM': 11} (paper 12, 8, 11, 10, 12, 11)

The counts of RS units and trials are those of the main notebook's data check (400 trials of each decoded image per session). The clock of spike times and trial table is checked there.

Decoder¶

The decoder of the claims file: ICwcfg1, RS units of one area, sessions with at least 50 RS units in the area; IC response; classes IC1, LC1, LC2, IC2 balanced to the smallest class by its first trials; stratified tenfold partition; z-scoring by the training trials; linear SVM, one-vs-one. Reading (a): C = 1; reading (b): C chosen by an inner threefold cross-validation on the training trials over 10^-3 to 1, the Python counterpart of the code's hyperparameter search. The search over C above 1, the one-vs-all coding and the kernel scale (equivalent to C for a linear kernel) is examined in decoding_checks_shin_2025.ipynb on V1, PM and AM. TRE trials are decoded by each fold model and the fractions averaged. The XRE decoders are trained on XRE1 against XRE2 and probed with IC1 and IC2. Each (session, area) has its own seed. Both readings are run on every area.

In [3]:
t0 = time.time()
jobs = []
for i, s in enumerate(sessions):
    u = s["units"]
    for j, a in enumerate(il.AREAS):
        m = ((u.area == a) & u.rs).values
        if m.sum() < 50:
            continue
        b = s["blocks"]["ICwcfg1"]
        jobs.append((s["subject"], a, b["R"][:, m], b["codes"], 1000 * i + j))

def run_decoders(subject, a, R, codes, seed):
    b = {"R": R, "codes": codes}
    m = np.ones(R.shape[1], bool)
    out = []
    for rd in ["a", "b"]:
        r = il.decode(b["R"][:, m], b["codes"], [il.IC1, il.LC1, il.LC2, il.IC2], [il.TRE1, il.TRE2], seed=seed, reading=rd)
        score, icv, lcv = il.inference_score(r["probe"])
        x = il.decode(b["R"][:, m], b["codes"], [il.XRE1, il.XRE2], [il.IC1, il.IC2], seed=seed, reading=rd)
        out.append({"subject": subject, "area": a, "n_units": int(m.sum()), "reading": rd,
                    "acc": r["acc"], "tre_ic": icv, "tre_lc": lcv, "inference": score, "n_per_class": r["n_per_class"],
                    "xre_acc": x["acc"], "xre_score": (x["probe"][0, 0] + x["probe"][1, 1]) / 2,
                    "C_tre": r["C_chosen"], "C_xre": x["C_chosen"], "unconverged": r["n_unconverged"] + x["n_unconverged"]})
    return out

dec = pd.DataFrame([r for rs in Parallel(n_jobs=N_JOBS)(delayed(run_decoders)(*j) for j in jobs) for r in rs])
warnings.filterwarnings("ignore")
print(f"{len(jobs)} session-area decoders, both readings, in {time.time() - t0:.0f} s")
print("Sessions per area:", dec[dec.reading == "a"].groupby("area").size().reindex(il.AREAS).to_dict(),
      "(paper 12, 8, 11, 10, 12, 11)")
print("Trials per class:", sorted(dec.n_per_class.unique()))
print("Fits stopped at max_iter without converging:", dec.groupby("reading").unconverged.sum().to_dict(),
      "of", len(jobs) * 2 * 10, "outer fits per reading (reading b: each with 12 inner fits and a refit)")
cb = dec[dec.reading == "b"]
print("Reading (b), C chosen per fold, TRE decoder:", pd.Series(np.concatenate(cb.C_tre.values)).value_counts().sort_index().to_dict(),
      "; XRE decoder:", pd.Series(np.concatenate(cb.C_xre.values)).value_counts().sort_index().to_dict())
64 session-area decoders, both readings, in 1023 s
Sessions per area: {'V1': 12, 'LM': 8, 'RL': 11, 'AL': 10, 'PM': 12, 'AM': 11} (paper 12, 8, 11, 10, 12, 11)
Trials per class: [np.int64(400)]
Fits stopped at max_iter without converging: {'a': 0, 'b': 2} of 1280 outer fits per reading (reading b: each with 12 inner fits and a refit)
Reading (b), C chosen per fold, TRE decoder: {0.001: 41, 0.01: 489, 0.1: 84, 1.0: 26} ; XRE decoder: {0.001: 87, 0.01: 395, 0.1: 89, 1.0: 69}

fig3a-npx-v1-decoding¶

A linear SVM decodes the four images IC1, LC1, LC2 and IC2 from V1 activity in the Neuropixels data with tenfold cross-validation accuracy above the chance of 0.25.

In [4]:
for rd in ["a", "b"]:
    x = dec[(dec.reading == rd)]
    v = x[x.area == "V1"].acc
    print(f"reading ({rd}) V1: mean accuracy {v.mean():.3f} +/- {v.sem():.3f} (s.e.m.), range {v.min():.3f} to {v.max():.3f}, "
          f"n = {len(v)}, signed-rank against 0.25 p = {il.signrank_p(v - 0.25):.4g}; sessions above 0.25: {(v > 0.25).sum()}")
    if rd == "a":
        print("   other areas:", {a: (round(x[x.area == a].acc.mean(), 3), round(il.signrank_p(x[x.area == a].acc - 0.25), 4))
                                 for a in il.AREAS[1:]})
print(dec[(dec.reading == "a") & (dec.area == "V1")][["subject", "n_units", "acc"]].round(3).to_string(index=False))
reading (a) V1: mean accuracy 0.697 +/- 0.052 (s.e.m.), range 0.268 to 0.887, n = 12, signed-rank against 0.25 p = 0.0004883; sessions above 0.25: 12
   other areas: {'LM': (np.float64(0.709), 0.0078), 'RL': (np.float64(0.543), 0.001), 'AL': (np.float64(0.648), 0.002), 'PM': (np.float64(0.506), 0.0005), 'AM': (np.float64(0.48), 0.001)}
reading (b) V1: mean accuracy 0.719 +/- 0.056 (s.e.m.), range 0.264 to 0.922, n = 12, signed-rank against 0.25 p = 0.0004883; sessions above 0.25: 12
subject  n_units   acc
 619293       89 0.268
 619296      168 0.887
 620333      114 0.682
 620334       68 0.694
 625545      190 0.781
 625554      179 0.781
 625555      314 0.486
 630506       58 0.755
 631510      129 0.821
 631570      142 0.798
 633229      181 0.856
 637484      172 0.553

Result: reproduced. V1 decoding accuracy is 0.697 under reading (a) and 0.719 under reading (b), against the chance of 0.25, with a signed-rank p = 0.00049 under both (12 sessions, every one above 0.25).

fig3c-npx-v1-inference¶

In Neuropixels V1, TRE trials are decoded as the matching IC image more often than as the matching LC image (0.51 against 0.22), so IC inference is represented in V1.

In [5]:
for rd in ["a", "b"]:
    x = dec[(dec.reading == rd) & (dec.area == "V1")]
    print(f"reading ({rd}): P(TRE -> IC) {x.tre_ic.mean():.3f} +/- {x.tre_ic.sem():.3f}, P(TRE -> LC) {x.tre_lc.mean():.3f} +/- "
          f"{x.tre_lc.sem():.3f}; IC > LC in {(x.tre_ic > x.tre_lc).sum()} of {len(x)} sessions; "
          f"signed-rank IC vs LC p = {il.signrank_p(x.tre_ic, x.tre_lc):.4g}")
print("Paper: 0.51 (tolerance 0.051) and 0.22 (0.022), P < 0.05")
print(dec[(dec.area == "V1")].pivot_table(index="subject", columns="reading", values=["tre_ic", "tre_lc"]).round(3).to_string())
reading (a): P(TRE -> IC) 0.516 +/- 0.062, P(TRE -> LC) 0.197 +/- 0.026; IC > LC in 10 of 12 sessions; signed-rank IC vs LC p = 0.002441
reading (b): P(TRE -> IC) 0.518 +/- 0.067, P(TRE -> LC) 0.210 +/- 0.033; IC > LC in 10 of 12 sessions; signed-rank IC vs LC p = 0.002441
Paper: 0.51 (tolerance 0.051) and 0.22 (0.022), P < 0.05
        tre_ic        tre_lc       
reading      a      b      a      b
subject                            
619293   0.254  0.245  0.257  0.270
619296   0.901  0.935  0.033  0.024
620333   0.408  0.366  0.273  0.318
620334   0.435  0.426  0.139  0.166
625545   0.611  0.672  0.250  0.234
625554   0.791  0.785  0.177  0.183
625555   0.301  0.292  0.217  0.222
630506   0.335  0.322  0.208  0.201
631510   0.610  0.684  0.062  0.023
631570   0.719  0.722  0.143  0.162
633229   0.549  0.482  0.309  0.424
637484   0.280  0.289  0.294  0.293

Result: reproduced. Under reading (b), TRE is decoded as the matching IC with probability 0.518 and as the matching LC with 0.210, both within 0.51 +/- 0.051 and 0.22 +/- 0.022, with IC above LC at signed-rank p = 0.0024. Under reading (a), IC is 0.516 and LC 0.197, just below the lower bound 0.198, with the same p. With two further cross-validation seeds, both values are within tolerance under both readings: reading (a) gives IC 0.513 and 0.511, LC 0.198 and 0.199, p = 0.0024 (decoding_checks_shin_2025.ipynb); reading (b) gives IC 0.519 and 0.514, LC 0.209 and 0.215, p = 0.0015 and 0.0068 (decoding_seed1_shin_2025.ipynb, decoding_seed2_shin_2025.ipynb).

fig3d-npx-areas-inference¶

In the Neuropixels data almost every visual area decodes TRE as IC more than as LC, significantly in V1, LM, RL, AL and PM and not in AM, with V1, LM and AL the strongest.

In [6]:
paper_p = {"V1": 0.0132, "LM": 0.0156, "RL": 0.0244, "AL": 0.0039, "PM": 0.0342, "AM": 0.1748}
for rd in ["a", "b"]:
    rows = []
    for a in il.AREAS:
        x = dec[(dec.reading == rd) & (dec.area == a)]
        rows.append({"area": a, "sessions": len(x), "IC": x.tre_ic.mean(), "LC": x.tre_lc.mean(), "inference_mean": x.inference.mean(),
                     "inference_sem": x.inference.sem(), "p": il.signrank_p(x.inference), "paper_p": paper_p[a]})
    t = pd.DataFrame(rows)
    t["sig_IC_above_LC"] = (t.p < 0.05) & (t.inference_mean > 0)
    print(f"reading ({rd})")
    print(t.round(4).to_string(index=False))
    print(f"  {int(t.sig_IC_above_LC.sum())} areas significant")
    print("  significant areas:", t[t.sig_IC_above_LC].area.tolist(),
          "; three largest mean scores:", t.nlargest(3, "inference_mean").area.tolist(),
          "; three smallest p:", t.nsmallest(3, "p").area.tolist())
print("Per-session inference scores, reading (a) and (b):")
print(dec.pivot_table(index="subject", columns=["reading", "area"], values="inference").round(3).to_string())
reading (a)
area  sessions     IC     LC  inference_mean  inference_sem      p  paper_p  sig_IC_above_LC
  V1        12 0.5162 0.1966          0.3195         0.0805 0.0024   0.0132             True
  LM         8 0.5371 0.2356          0.3015         0.0798 0.0156   0.0156             True
  RL        11 0.4220 0.3306          0.0914         0.0250 0.0020   0.0244             True
  AL        10 0.5111 0.2526          0.2585         0.0747 0.0020   0.0039             True
  PM        12 0.3690 0.3111          0.0578         0.0255 0.0923   0.0342            False
  AM        11 0.3926 0.3319          0.0607         0.0138 0.0029   0.1748             True
  5 areas significant
  significant areas: ['V1', 'LM', 'RL', 'AL', 'AM'] ; three largest mean scores: ['V1', 'LM', 'AL'] ; three smallest p: ['RL', 'AL', 'V1']
reading (b)
area  sessions     IC     LC  inference_mean  inference_sem      p  paper_p  sig_IC_above_LC
  V1        12 0.5184 0.2100          0.3084         0.0907 0.0024   0.0132             True
  LM         8 0.5654 0.2195          0.3459         0.0859 0.0156   0.0156             True
  RL        11 0.4248 0.3407          0.0841         0.0264 0.0049   0.0244             True
  AL        10 0.5366 0.2513          0.2853         0.0830 0.0020   0.0039             True
  PM        12 0.3810 0.3091          0.0718         0.0347 0.0771   0.0342            False
  AM        11 0.3958 0.3465          0.0494         0.0279 0.0830   0.1748            False
  4 areas significant
  significant areas: ['V1', 'LM', 'RL', 'AL'] ; three largest mean scores: ['LM', 'V1', 'AL'] ; three smallest p: ['AL', 'V1', 'RL']
Per-session inference scores, reading (a) and (b):
reading      a                                         b                                   
area        AL     AM     LM     PM     RL     V1     AL     AM     LM     PM     RL     V1
subject                                                                                    
619293   0.423  0.029    NaN -0.009  0.118 -0.003  0.491 -0.166    NaN -0.125  0.109 -0.025
619296   0.716  0.071    NaN  0.164 -0.009  0.867  0.755  0.072    NaN  0.179 -0.033  0.911
620333   0.136  0.104  0.542 -0.015  0.251  0.135  0.151  0.087  0.550 -0.033  0.253  0.048
620334     NaN  0.055  0.545  0.195  0.228  0.296    NaN -0.008  0.605  0.207  0.225  0.260
625545   0.066  0.060  0.352  0.053  0.046  0.361  0.116  0.092  0.327  0.046  0.035  0.438
625554   0.565  0.115    NaN  0.141  0.048  0.614  0.667  0.146    NaN  0.259  0.089  0.602
625555   0.228  0.009    NaN  0.150  0.054  0.084  0.271  0.024    NaN  0.149  0.032  0.070
630506   0.258    NaN  0.448 -0.075  0.113  0.128  0.217    NaN  0.613 -0.080  0.101  0.121
631510   0.063  0.117  0.110 -0.032  0.052  0.549  0.046  0.172  0.071 -0.033  0.034  0.661
631570   0.014 -0.023 -0.019 -0.005    NaN  0.576  0.002 -0.006 -0.017  0.111    NaN  0.559
633229   0.117  0.097  0.042  0.051  0.009  0.240  0.137  0.108  0.202  0.108  0.010  0.059
637484     NaN  0.032  0.392  0.076  0.094 -0.013    NaN  0.023  0.416  0.076  0.070 -0.003

Result: reproduced, resting on two assumptions. The readings differ on the seed of this notebook. Under reading (a), five areas decode TRE as IC significantly more than as LC (V1, LM, RL, AL and AM; PM p = 0.092), and V1, LM and AL have the three largest mean scores, which meets the criterion; the two further seeds give the same five areas (decoding_checks_shin_2025.ipynb). Under reading (b), adopted, four areas are significant (V1, LM, RL and AL; PM p = 0.077, AM p = 0.083), and LM, V1 and AL have the three largest mean scores, a partial reproduction. Reading (b) is adopted, as the paper's code chooses the SVM hyperparameters by a search, which reading (a) leaves out. With the two further seeds, reading (b) gives five significant areas each, with LM, V1 and AL the three largest: V1, LM, RL, AL and PM with seed +100000 (PM p = 0.0425, AM p = 0.083), and V1, LM, RL, AL and AM with seed +200000 (AM p = 0.0186, PM p = 0.052) (decoding_seed1_shin_2025.ipynb, decoding_seed2_shin_2025.ipynb). The criterion is therefore met in five of the six runs (two readings, three seeds); only the seed of this notebook under reading (b), where PM and AM fall just above 0.05, does not meet it. The outcome follows the majority of the seeds under reading (b), a rule chosen after the first seed's result and not part of the criterion (assumption); a rule requiring every seed would give a partial reproduction. V1, LM, RL and AL are significant and LM, V1 and AL the three largest in all six runs. Which of PM and AM is significant changes with the seed under reading (b); under reading (a), AM is significant and PM is not with every seed, the reverse of the paper.

ed7b-npx-xre-inference¶

Decoders trained on the XRE real-bar images classify IC1 and IC2 trials as the XRE image whose real bar matches the illusory bar, above chance in all six Neuropixels areas.

XRE inference score (P(IC1 -> XRE1) + P(IC2 -> XRE2))/2, signed-rank against 0.5, the chance of a two-class decoder.

In [7]:
paper_xp = {"V1": 0.0005, "LM": 0.0078, "RL": 0.0049, "AL": 0.0020, "PM": 0.0005, "AM": 0.0137}
for rd in ["a", "b"]:
    rows = []
    for a in il.AREAS:
        x = dec[(dec.reading == rd) & (dec.area == a)]
        rows.append({"area": a, "sessions": len(x), "xre_acc": x.xre_acc.mean(), "xre_score_mean": x.xre_score.mean(),
                     "xre_score_sem": x.xre_score.sem(), "p_vs_0.5": il.signrank_p(x.xre_score - 0.5), "paper_p": paper_xp[a]})
    t = pd.DataFrame(rows)
    t["sig_above_0.5"] = (t["p_vs_0.5"] < 0.05) & (t.xre_score_mean > 0.5)
    print(f"reading ({rd}): areas significant above 0.5: {int(t['sig_above_0.5'].sum())} of 6")
    print(t.round(4).to_string(index=False))
reading (a): areas significant above 0.5: 6 of 6
area  sessions  xre_acc  xre_score_mean  xre_score_sem  p_vs_0.5  paper_p  sig_above_0.5
  V1        12   0.8624          0.7184         0.0328    0.0005   0.0005           True
  LM         8   0.8125          0.7131         0.0364    0.0078   0.0078           True
  RL        11   0.6439          0.6086         0.0268    0.0020   0.0049           True
  AL        10   0.7644          0.6924         0.0397    0.0020   0.0020           True
  PM        12   0.6520          0.5983         0.0253    0.0010   0.0005           True
  AM        11   0.6331          0.5764         0.0251    0.0137   0.0137           True
reading (b): areas significant above 0.5: 6 of 6
area  sessions  xre_acc  xre_score_mean  xre_score_sem  p_vs_0.5  paper_p  sig_above_0.5
  V1        12   0.8811          0.7621         0.0345    0.0005   0.0005           True
  LM         8   0.8270          0.7241         0.0384    0.0078   0.0078           True
  RL        11   0.6530          0.6068         0.0297    0.0049   0.0049           True
  AL        10   0.7749          0.7053         0.0403    0.0020   0.0020           True
  PM        12   0.6632          0.5984         0.0272    0.0010   0.0005           True
  AM        11   0.6274          0.5847         0.0304    0.0137   0.0137           True

Result: reproduced. The XRE inference score is above 0.5 with signed-rank p < 0.05 in all six areas under both readings, with p from 0.0005 to 0.0137, close to the paper's values (0.0005 to 0.0137).

ed7a-npx-no-area-above-v1¶

No higher visual area shows a substantially larger inference performance than V1, although decoding accuracy and inference scores differ across areas.

Kruskal-Wallis across the six areas of the per-session values, then (a) two-sided signed-rank of V1 against each HVA over the sessions with both, and (b) Tukey-Kramer comparisons after the Kruskal-Wallis. A claim of absence: reproduced when no HVA is significantly larger than V1.

In [8]:
def tukey_vs_v1(vals_by_area):
    # Tukey-Kramer on mean ranks after Kruskal-Wallis (MATLAB multcompare default), HVA minus V1
    allv = np.concatenate([vals_by_area[a] for a in il.AREAS])
    lab = np.concatenate([[a] * len(vals_by_area[a]) for a in il.AREAS])
    r = stats.rankdata(allv)
    N = len(allv)
    _, c = np.unique(allv, return_counts=True)
    var = N * (N + 1) / 12 * (1 - np.sum(c ** 3 - c) / (N ** 3 - N))
    qc = il.tukey_qcrit(6)
    out = {}
    for a in il.AREAS[1:]:
        d = r[lab == a].mean() - r[lab == "V1"].mean()
        se = np.sqrt(var * (1 / np.sum(lab == a) + 1 / np.sum(lab == "V1")) / 2)
        out[a] = (round(d, 1), bool(abs(d) / se > qc))
    return out

for rd in ["a", "b"]:
    x = dec[dec.reading == rd]
    print(f"reading ({rd})")
    for meas, paper in [("acc", 0.0019), ("inference", 0.0249), ("xre_acc", 0.0005), ("xre_score", 0.0022)]:
        vals = {a: x[x.area == a][meas].values for a in il.AREAS}
        kwp = stats.kruskal(*vals.values()).pvalue
        piv = x.pivot(index="subject", columns="area", values=meas)
        pair = {}
        for a in il.AREAS[1:]:
            both = piv[["V1", a]].dropna()
            pair[a] = (round(both[a].mean() - both["V1"].mean(), 3), round(il.signrank_p(both[a], both["V1"]), 4))
        print(f"  {meas}: Kruskal-Wallis p = {kwp:.4f} (paper {paper}); means", {a: round(v.mean(), 3) for a, v in vals.items()})
        print(f"     (a) HVA minus V1 and signed-rank p:", pair)
        print(f"     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant:", tukey_vs_v1(vals))
reading (a)
  acc: Kruskal-Wallis p = 0.0006 (paper 0.0019); means {'V1': np.float64(0.697), 'LM': np.float64(0.709), 'RL': np.float64(0.543), 'AL': np.float64(0.648), 'PM': np.float64(0.506), 'AM': np.float64(0.48)}
     (a) HVA minus V1 and signed-rank p: {'LM': (np.float64(-0.033), 0.8438), 'RL': (np.float64(-0.144), 0.0869), 'AL': (np.float64(-0.063), 0.3223), 'PM': (np.float64(-0.191), 0.0024), 'AM': (np.float64(-0.211), 0.001)}
     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant: {'LM': (np.float64(0.9), False), 'RL': (np.float64(-19.4), False), 'AL': (np.float64(-5.7), False), 'PM': (np.float64(-23.1), True), 'AM': (np.float64(-25.6), True)}
  inference: Kruskal-Wallis p = 0.0071 (paper 0.0249); means {'V1': np.float64(0.32), 'LM': np.float64(0.302), 'RL': np.float64(0.091), 'AL': np.float64(0.258), 'PM': np.float64(0.058), 'AM': np.float64(0.061)}
     (a) HVA minus V1 and signed-rank p: {'LM': (np.float64(0.018), 1.0), 'RL': (np.float64(-0.205), 0.123), 'AL': (np.float64(-0.097), 0.375), 'PM': (np.float64(-0.262), 0.0049), 'AM': (np.float64(-0.276), 0.0098)}
     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant: {'LM': (np.float64(-2.2), False), 'RL': (np.float64(-16.8), False), 'AL': (np.float64(-2.0), False), 'PM': (np.float64(-21.3), False), 'AM': (np.float64(-20.5), False)}
  xre_acc: Kruskal-Wallis p = 0.0005 (paper 0.0005); means {'V1': np.float64(0.862), 'LM': np.float64(0.812), 'RL': np.float64(0.644), 'AL': np.float64(0.764), 'PM': np.float64(0.652), 'AM': np.float64(0.633)}
     (a) HVA minus V1 and signed-rank p: {'LM': (np.float64(-0.102), 0.0781), 'RL': (np.float64(-0.215), 0.0137), 'AL': (np.float64(-0.094), 0.1934), 'PM': (np.float64(-0.21), 0.0015), 'AM': (np.float64(-0.221), 0.0029)}
     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant: {'LM': (np.float64(-5.5), False), 'RL': (np.float64(-26.0), True), 'AL': (np.float64(-9.4), False), 'PM': (np.float64(-25.4), True), 'AM': (np.float64(-26.3), True)}
  xre_score: Kruskal-Wallis p = 0.0077 (paper 0.0022); means {'V1': np.float64(0.718), 'LM': np.float64(0.713), 'RL': np.float64(0.609), 'AL': np.float64(0.692), 'PM': np.float64(0.598), 'AM': np.float64(0.576)}
     (a) HVA minus V1 and signed-rank p: {'LM': (np.float64(-0.023), 0.7422), 'RL': (np.float64(-0.096), 0.1016), 'AL': (np.float64(-0.031), 0.625), 'PM': (np.float64(-0.12), 0.0034), 'AM': (np.float64(-0.148), 0.0049)}
     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant: {'LM': (np.float64(0.5), False), 'RL': (np.float64(-16.1), False), 'AL': (np.float64(-3.7), False), 'PM': (np.float64(-19.3), False), 'AM': (np.float64(-21.8), False)}
reading (b)
  acc: Kruskal-Wallis p = 0.0017 (paper 0.0019); means {'V1': np.float64(0.719), 'LM': np.float64(0.738), 'RL': np.float64(0.556), 'AL': np.float64(0.677), 'PM': np.float64(0.525), 'AM': np.float64(0.498)}
     (a) HVA minus V1 and signed-rank p: {'LM': (np.float64(-0.028), 0.7422), 'RL': (np.float64(-0.152), 0.123), 'AL': (np.float64(-0.059), 0.3223), 'PM': (np.float64(-0.193), 0.0034), 'AM': (np.float64(-0.218), 0.002)}
     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant: {'LM': (np.float64(1.4), False), 'RL': (np.float64(-19.4), False), 'AL': (np.float64(-5.4), False), 'PM': (np.float64(-20.6), False), 'AM': (np.float64(-24.0), True)}
  inference: Kruskal-Wallis p = 0.0156 (paper 0.0249); means {'V1': np.float64(0.308), 'LM': np.float64(0.346), 'RL': np.float64(0.084), 'AL': np.float64(0.285), 'PM': np.float64(0.072), 'AM': np.float64(0.049)}
     (a) HVA minus V1 and signed-rank p: {'LM': (np.float64(0.078), 0.8438), 'RL': (np.float64(-0.201), 0.2061), 'AL': (np.float64(-0.059), 0.7695), 'PM': (np.float64(-0.237), 0.0122), 'AM': (np.float64(-0.276), 0.0186)}
     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant: {'LM': (np.float64(5.8), False), 'RL': (np.float64(-13.2), False), 'AL': (np.float64(3.5), False), 'PM': (np.float64(-13.4), False), 'AM': (np.float64(-16.4), False)}
  xre_acc: Kruskal-Wallis p = 0.0002 (paper 0.0005); means {'V1': np.float64(0.881), 'LM': np.float64(0.827), 'RL': np.float64(0.653), 'AL': np.float64(0.775), 'PM': np.float64(0.663), 'AM': np.float64(0.627)}
     (a) HVA minus V1 and signed-rank p: {'LM': (np.float64(-0.108), 0.0234), 'RL': (np.float64(-0.224), 0.0098), 'AL': (np.float64(-0.1), 0.1934), 'PM': (np.float64(-0.218), 0.001), 'AM': (np.float64(-0.245), 0.002)}
     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant: {'LM': (np.float64(-7.2), False), 'RL': (np.float64(-26.2), True), 'AL': (np.float64(-10.7), False), 'PM': (np.float64(-25.4), True), 'AM': (np.float64(-30.0), True)}
  xre_score: Kruskal-Wallis p = 0.0015 (paper 0.0022); means {'V1': np.float64(0.762), 'LM': np.float64(0.724), 'RL': np.float64(0.607), 'AL': np.float64(0.705), 'PM': np.float64(0.598), 'AM': np.float64(0.585)}
     (a) HVA minus V1 and signed-rank p: {'LM': (np.float64(-0.061), 0.4609), 'RL': (np.float64(-0.143), 0.0244), 'AL': (np.float64(-0.051), 0.2754), 'PM': (np.float64(-0.164), 0.0005), 'AM': (np.float64(-0.178), 0.0029)}
     (b) Tukey-Kramer mean-rank difference HVA minus V1, significant: {'LM': (np.float64(-5.1), False), 'RL': (np.float64(-22.2), True), 'AL': (np.float64(-7.8), False), 'PM': (np.float64(-24.2), True), 'AM': (np.float64(-25.5), True)}

Result: reproduced. No higher visual area has a significantly larger TRE or XRE inference score than V1 under either pairwise test or either decoder reading: every significant difference has the HVA below V1. These are PM and AM in several measures, RL in XRE accuracy (both readings; signed-rank and Tukey-Kramer) and XRE score (reading b; signed-rank p = 0.0244 and Tukey-Kramer), and LM in XRE accuracy under reading (b) (signed-rank p = 0.0234). The Kruskal-Wallis tests across areas are significant, as in the paper (reading a: p = 0.0006, 0.0071, 0.0005 and 0.0077 for accuracy, inference score, XRE accuracy and XRE score; paper 0.0019, 0.0249, 0.0005, 0.0022).

In [9]:
print(f"Notebook ran in {(time.time() - started) / 60:.1f} min")
Notebook ran in 19.3 min

Summary¶

Claim Outcome
fig3a-npx-v1-decoding reproduced
fig3c-npx-v1-inference reproduced (within tolerance under reading b with three seeds; under reading a with two of three seeds)
fig3d-npx-areas-inference reproduced (assumptions: decoder reading b, and the majority of three seeds; reading a meets the criterion with every seed, reading b with two of three)
ed7b-npx-xre-inference reproduced
ed7a-npx-no-area-above-v1 reproduced

A failure to reproduce here may be a failure of this attempt, not of the paper: see the reasoning and the differences from the paper's method.

How the analysis differs from the paper's: the decoders use scikit-learn's libsvm SVM in place of MATLAB's fitcecoc. The code's Bayesian hyperparameter search is replaced by a grid search over C from 10^-3 to 1 with one-vs-one coding (reading b); decoding_checks_shin_2025.ipynb shows, on the first outer fold of V1, PM and AM, that C above 1 gave a higher inner accuracy in 1 of 35 sessions (by 0.001, from fits that mostly did not converge) and that the one-vs-all coding changed the best inner accuracy by -0.026 to 0.027. Extended Data Fig. 7c pools Neuropixels and 2p areas; its Kruskal-Wallis values are compared here on the six Neuropixels areas only.