Buckets:

redmingwei's picture
download
raw
13.6 kB
#!/usr/bin/env python3
"""exp56b — β 的稳定性与零模型(修正归一化)。
## 上一版的错误
exp56 用 ρ(τ) = cross(τ)/cross(0)。但零模型(每单元独立循环移位)破坏跨单元
对齐后 cross(0) 趋于 0,除一个接近零的数会放大噪声,null 的 β 变成
0.032 / 0.08 甚至负值——这不是"零模型也有 β",而是归一化失效。
## 本版
改为按总方差归一:
total(0) = Σ_{ij} <x_i x_j> = 全部方差(含对角)
cross(τ) = total(τ) − diag(τ) = 只含 i≠j
ρ(τ) = cross(τ) / total(0) ∈ [−1, 1],有绝对尺度
shared_frac = cross(0) / total(0) = 总方差里跨单元共享的比例
白噪声下 ρ(τ) ≈ 0;共享潜变量下 ρ(τ) > 0 且衰减。
噪声底由零模型的 ρ(τ) 在 τ∈[2,500] 上的 RMS 给出,SNR = ρ(1)/噪声底。
β 从 log ρ(τ) 对 log τ 拟合(τ ∈ [2, min(500, T/4)]),仅在 ρ(τ) > 2×噪声底
的点上拟合,点数不足 8 个则报 None。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import kruskal, mannwhitneyu, spearmanr
V3 = Path("/domains/counts-v3")
OUT = Path("/domains/runs/beta2")
OUT.mkdir(parents=True, exist_ok=True)
DT = 20.0
MAX_LAG = 500
TRUNC = 3000
FIT_RANGES = [100, 250, 500]
LOCAL = [(2, 5), (5, 10), (10, 25), (25, 50), (50, 100), (100, 250), (250, 500)]
N_NULL = 3
SEED = 0
def n2(n):
k = 1
while k < n:
k <<= 1
return k
def acf_pair(x, max_lag):
"""返回 (total, diag) 未归一化 ACF,无偏除以 (T−τ)。"""
T, d = x.shape
xc = x - x.mean(axis=0, keepdims=True)
N = n2(2 * T)
F = np.fft.rfft(xc, n=N, axis=0)
Fs = F.sum(axis=1)
total = np.fft.irfft(Fs * np.conj(Fs), n=N)[:max_lag + 1]
diag = np.fft.irfft((np.abs(F) ** 2).sum(axis=1), n=N)[:max_lag + 1]
nrm = np.arange(T, T - max_lag - 1, -1)
return total / np.maximum(nrm, 1), diag / np.maximum(nrm, 1)
def rho_curve(x, max_lag):
"""ρ(τ) = (total(τ) − diag(τ)) / total(0)。"""
total, diag = acf_pair(x, max_lag)
t0 = max(total[0], 1e-12)
cross = total - diag
return cross / t0, float(cross[0] / t0)
def fit_beta(rho, noise, hi=MAX_LAG, lo=2):
hi = min(hi, len(rho) - 1)
if hi - lo < 8:
return None
t = np.arange(lo, hi + 1) * DT
y = rho[lo:hi + 1]
ok = np.isfinite(y) & (y > max(2 * noise, 1e-9))
if ok.sum() < 8:
return None
b = -np.polyfit(np.log(t[ok]), np.log(y[ok]), 1)[0]
return float(b)
def local_slopes(rho, noise):
out = {}
for a, b in LOCAL:
if b >= len(rho) or rho[a] <= 1e-9 or rho[b] <= 1e-9:
continue
if rho[a] < 2 * noise or rho[b] < 2 * noise:
continue
out[f"{a}-{b}"] = round(float(
-(np.log(rho[b]) - np.log(rho[a])) / (np.log(b) - np.log(a))), 3)
return out
def agg(x, f):
T = (x.shape[0] // f) * f
return x[:T].reshape(T // f, f, x.shape[1]).sum(axis=1)
def top_stats(x):
xc = x - x.mean(axis=0, keepdims=True)
s = np.linalg.svd(xc, compute_uv=False) ** 2
return (float(s[0] / max(s.sum(), 1e-12)),
float((s.sum() ** 2) / max((s ** 2).sum(), 1e-12)))
def task_of(source, fname):
if source == "human_intracortical":
return "H1_reach_grasp" if fname.startswith("h1") else "H2_handwriting"
if "churchland" in fname:
return "reach"
if "dmfc" in fname:
return "timing"
if "area2" in fname:
return "bump"
return "?"
def main():
t0 = time.time()
W = np.load(V3 / "counts.npy", mmap_mode="r")
idx = pd.read_parquet(V3 / "counts_index.parquet")
rng = np.random.default_rng(SEED)
rows = []
for n, (_, r) in enumerate(idx.iterrows()):
x = np.asarray(W[r.bin_start:r.bin_end, :r.n_units], dtype=np.float32)
if x.shape[0] < 1200 or x.shape[1] < 8:
continue
ml = min(MAX_LAG, x.shape[0] - 2)
# ---- 零模型:独立循环移位,拿噪声底
null_prof = []
for _ in range(N_NULL):
xs = np.empty_like(x)
for j in range(x.shape[1]):
xs[:, j] = np.roll(x[:, j], int(rng.integers(1, x.shape[0])))
rn, _ = rho_curve(xs, ml)
null_prof.append(rn)
null_prof = np.array(null_prof)
noise = float(np.sqrt((null_prof[:, 2:] ** 2).mean()))
null_beta = [fit_beta(p, noise) for p in null_prof]
rho, shared = rho_curve(x, ml)
top1, eff = top_stats(x)
b_full = fit_beta(rho, noise)
ls = local_slopes(rho, noise)
# 稳定性
range_b = {str(k): fit_beta(rho, noise, hi=min(k, ml)) for k in FIT_RANGES}
h = x.shape[0] // 2
r1, s1 = rho_curve(x[:h], ml)
r2, s2 = rho_curve(x[h:], ml)
q = x.shape[0] // 4
qs = []
for i in range(4):
rq, _ = rho_curve(x[i * q:(i + 1) * q], ml)
qs.append(fit_beta(rq, noise))
bws = {}
for f in (1, 2, 5):
xf = x if f == 1 else agg(x, f)
if xf.shape[0] > 100:
rf, _ = rho_curve(xf, min(MAX_LAG, xf.shape[0] - 2))
bws[str(f * 20)] = fit_beta(rf, noise)
b_tr = None
if x.shape[0] >= TRUNC:
rt, _ = rho_curve(x[:TRUNC], min(MAX_LAG, TRUNC - 2))
b_tr = fit_beta(rt, noise)
rows.append({
"source": r.source, "task": task_of(r.source, r.file),
"file": r.file, "region": r.region,
"n_units": int(x.shape[1]), "n_bins": int(x.shape[0]),
"hours": round(x.shape[0] * DT / 3.6e6, 3),
"rate": float(x.mean()), "fano": float(x.var() / max(x.mean(), 1e-9)),
"top1": round(top1, 4), "eff_dim": round(eff, 2),
"shared_frac": round(float(shared), 5),
"rho1": round(float(rho[1]), 5), "rho2": round(float(rho[2]), 5),
"noise_floor": round(noise, 6),
"snr_rho1": round(float(rho[1] / max(noise, 1e-12)), 1),
"beta": b_full,
"beta_null": json.dumps([round(v, 3) if v else None for v in null_beta]),
"beta_h1": fit_beta(r1, noise), "beta_h2": fit_beta(r2, noise),
"shared_h1": round(s1, 5), "shared_h2": round(s2, 5),
"beta_q": json.dumps([round(v, 3) if v else None for v in qs]),
"beta_rng": json.dumps({k: (round(v, 3) if v else None)
for k, v in range_b.items()}),
"beta_bw": json.dumps({k: (round(v, 3) if v else None)
for k, v in bws.items()}),
"beta_trunc": b_tr,
"local_slopes": json.dumps(ls),
})
if n % 10 == 0:
print(f" [{n+1}/{len(idx)}] {r.file} β={b_full and round(b_full,3)} "
f"null={[v and round(v,3) for v in null_beta]} "
f"shared={shared:.4f} noise={noise:.2e} [{time.time()-t0:.0f}s]",
flush=True)
del x
df = pd.DataFrame(rows)
df.to_csv(OUT / "per_segment.csv", index=False)
rep = {"task": "beta_stability_v2", "dt_ms": DT, "n_segments": int(len(df)),
"normalization": "rho(tau) = cross(tau)/total(0)", "n_null": N_NULL}
def med(c, g=df):
v = g[c].dropna()
return None if not len(v) else {
"n": int(len(v)), "median": round(float(v.median()), 4),
"p10": round(float(v.quantile(.1)), 4),
"p90": round(float(v.quantile(.9)), 4)}
rep["shared_frac"] = med("shared_frac")
rep["rho1"] = med("rho1")
rep["snr_rho1"] = med("snr_rho1")
rep["noise_floor"] = med("noise_floor")
rep["beta"] = med("beta")
# Q4 零模型
NB = pd.DataFrame([json.loads(s) for s in df.beta_null])
nbmed = NB.mean(axis=1)
rep["Q4_null_beta"] = {
"n": int(nbmed.notna().sum()),
"null_median": round(float(nbmed.median()), 3),
"null_p10": round(float(nbmed.quantile(.1)), 3),
"null_p90": round(float(nbmed.quantile(.9)), 3),
"real_median": round(float(df.beta.median()), 3),
"frac_null_lt_half_real": round(float(
(nbmed < 0.5 * df.beta).mean()), 3),
"frac_null_ge_0.2": round(float((nbmed >= 0.2).mean()), 3)}
# Q1
LS = pd.DataFrame([json.loads(s) for s in df.local_slopes])
rep["Q1_local_slope"] = {c: {"median": round(float(LS[c].median()), 3),
"p10": round(float(LS[c].quantile(.1)), 3),
"p90": round(float(LS[c].quantile(.9)), 3),
"n": int(LS[c].notna().sum())}
for c in LS.columns}
if len(LS.columns) >= 3:
cols = list(LS.columns)
inc = LS[cols].apply(lambda r: bool(np.all(np.diff(r.dropna().values) > -0.05))
if r.notna().sum() == len(cols) else False, axis=1)
rep["Q1_slope_increases_with_lag"] = round(float(inc.mean()), 3)
# Q2
m = df.dropna(subset=["beta_h1", "beta_h2"])
if len(m) > 5:
r_ = spearmanr(m.beta_h1, m.beta_h2)
rep["Q2_split_half_beta"] = {
"n": int(len(m)), "rho": round(float(r_[0]), 3), "p": float(r_[1])}
ms = df.dropna(subset=["shared_h1", "shared_h2"])
if len(ms) > 5:
r_ = spearmanr(ms.shared_h1, ms.shared_h2)
rep["Q2_split_half_shared"] = {
"n": int(len(ms)), "rho": round(float(r_[0]), 3), "p": float(r_[1]),
"ratio_median": round(float((ms.shared_h1 / ms.shared_h2).median()), 3)}
RNGD = pd.DataFrame([json.loads(s) for s in df.beta_rng])
rep["Q2_fit_range"] = {c: med(c, RNGD) if c in RNGD else None for c in RNGD.columns}
BWD = pd.DataFrame([json.loads(s) for s in df.beta_bw])
rep["Q2_bin_width"] = {c: med(c, BWD) if c in BWD else None for c in BWD.columns}
m2 = df.dropna(subset=["beta", "beta_trunc"])
if len(m2) > 5:
rep["Q2_truncation"] = {
"n": int(len(m2)),
"full": round(float(m2.beta.median()), 3),
"trunc": round(float(m2.beta_trunc.median()), 3),
"rho": round(float(spearmanr(m2.beta, m2.beta_trunc)[0]), 3)}
QD = pd.DataFrame([json.loads(s) for s in df.beta_q])
rep["Q2_quarters"] = {"range_median": round(float((QD.max(axis=1) - QD.min(axis=1)).median()), 3),
"sd_median": round(float(QD.std(axis=1).median()), 3)}
# Q3
rep["Q3_correlates"] = {}
for c in ["n_units", "rate", "fano", "top1", "eff_dim", "hours", "shared_frac"]:
mm = df[["beta", c]].dropna()
if len(mm) > 5:
r_ = spearmanr(mm.beta, mm[c])
rep["Q3_correlates"][c] = {"rho": round(float(r_[0]), 3),
"p": float(r_[1]), "n": int(len(mm))}
# shared_frac 与什么相关(它是 β 的主要混淆候选)
rep["Q3_shared_correlates"] = {}
for c in ["n_units", "rate", "top1", "eff_dim", "hours"]:
mm = df[["shared_frac", c]].dropna()
if len(mm) > 5:
r_ = spearmanr(mm.shared_frac, mm[c])
rep["Q3_shared_correlates"][c] = {"rho": round(float(r_[0]), 3),
"p": float(r_[1]), "n": int(len(mm))}
# 任务 / 物种
rep["by_task"] = {}
for (src, tk), g in df.groupby(["source", "task"]):
rep["by_task"][f"{src}:{tk}"] = {
"n_segments": int(len(g)),
"beta": med("beta", g), "shared_frac": med("shared_frac", g),
"rho1": med("rho1", g), "top1": med("top1", g)}
mac = df[df.source == "macaque_nlb"]
groups = [g.beta.dropna().values for _, g in mac.groupby("task") if len(g) >= 2]
if len(groups) >= 2:
kw = kruskal(*groups)
rep["macaque_task_test"] = {
"kruskal_H": round(float(kw.statistic), 3), "p": float(kw.pvalue),
"medians": {tk: round(float(g.beta.median()), 3)
for tk, g in mac.groupby("task") if len(g) >= 2}}
hum = df[df.source == "human_intracortical"]
g1 = hum[hum.task == "H1_reach_grasp"].beta.dropna()
g2 = hum[hum.task == "H2_handwriting"].beta.dropna()
if len(g1) > 2 and len(g2) > 2:
rep["human_task_test"] = {
"H1": round(float(g1.median()), 3), "H2": round(float(g2.median()), 3),
"p": float(mannwhitneyu(g1, g2).pvalue), "n": [int(len(g1)), int(len(g2))]}
a = df[(df.source == "macaque_nlb") & (df.region == "M1")].beta_trunc.dropna()
b = hum.beta_trunc.dropna()
if len(a) > 2 and len(b) > 2:
rep["species_truncated"] = {
"macaque": round(float(a.median()), 3), "human": round(float(b.median()), 3),
"p": float(mannwhitneyu(a, b).pvalue), "n": [int(len(a)), int(len(b))]}
# shared_frac 的物种比较(截断后没有 shared,用全段)
a2 = df[(df.source == "macaque_nlb") & (df.region == "M1")].shared_frac.dropna()
b2 = hum.shared_frac.dropna()
if len(a2) > 2 and len(b2) > 2:
rep["species_shared"] = {
"macaque": round(float(a2.median()), 5), "human": round(float(b2.median()), 5),
"p": float(mannwhitneyu(a2, b2).pvalue)}
(OUT / "report.json").write_text(json.dumps(rep, indent=2, ensure_ascii=False),
encoding="utf-8")
print("\n" + json.dumps(rep, indent=2, ensure_ascii=False), flush=True)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
13.6 kB
·
Xet hash:
ea22d8a2d1f48962534b3e84a0574092a8c14ec702c09cf3fb195365b462c126

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.