Buckets:
| #!/usr/bin/env python3 | |
| """exp56 — 幂律指数 β 的稳定性、相关物与零模型。 | |
| exp55 得到 β = 0.371(猕猴 0.338,人类 0.434,p = 0.0016)。 | |
| 本脚本回答四个问题: | |
| Q1 真的是幂律吗? | |
| 算局域斜率 d log R / d log τ 在 7 个区间上的值。 | |
| 幂律要求各区间斜率大致相同;若随 lag 单调变化,则是 crossover,不是幂律。 | |
| Q2 β 稳不稳? | |
| - 分裂信度:前半 / 后半 / 四等分 | |
| - 拟合区间:lag ≤ 100 / 250 / 500 / 1000 | |
| - bin 宽:20 / 40 / 100 ms(计数可加,重采样无损) | |
| - 截断到相同长度:全部截到 3000 bin(60 s) | |
| - 随机子采样窗(只保留一半时间点) | |
| Q3 β 与什么相关? | |
| n_units、放电率、Fano、有效维度、top-1 占比、片段时长 | |
| Q4 β 需要真实的跨单元结构吗? | |
| 零模型:每个单元独立循环移位(保留各自的边际分布与自相关, | |
| 破坏跨单元时间对齐)。若 β 仍在,则 β 不来自群体耦合。 | |
| 关键对照:猕猴内部三个任务(伸手 / 计时 / 触碰)与人类内部两个任务 | |
| (抓取 / 手写)。物种与任务不混淆。 | |
| """ | |
| 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/beta") | |
| OUT.mkdir(parents=True, exist_ok=True) | |
| DT = 20.0 | |
| MAX_LAG = 500 | |
| TRUNC = 3000 # 60 s,用于时长控制 | |
| FIT_RANGES = [100, 250, 500, 1000] | |
| LOCAL = [(2, 5), (5, 10), (10, 25), (25, 50), (50, 100), (100, 250), (250, 500)] | |
| N_SHUFFLE = 2 | |
| SEED = 0 | |
| def n2(n): | |
| k = 1 | |
| while k < n: | |
| k <<= 1 | |
| return k | |
| def cross_acf(x, max_lag): | |
| """Σ_{i≠j}<x_i x_j(τ)> / Σ_{i≠j}<x_i x_j(0)>。""" | |
| T, d = x.shape | |
| xc = x - x.mean(axis=0, keepdims=True) | |
| F = np.fft.rfft(xc, n=n2(2 * T), axis=0) | |
| Fs = F.sum(axis=1) | |
| total = np.fft.irfft(Fs * np.conj(Fs), n=n2(2 * T))[:max_lag + 1] | |
| diag = np.fft.irfft((np.abs(F) ** 2).sum(axis=1), n=n2(2 * T))[:max_lag + 1] | |
| nrm = np.arange(T, T - max_lag - 1, -1) | |
| cross = (total - diag) / np.maximum(nrm, 1) | |
| return cross / max(cross[0], 1e-12) | |
| def beta_of(R, hi=MAX_LAG, lo=2): | |
| hi = min(hi, len(R) - 1) | |
| if hi - lo < 5: | |
| return None | |
| t = np.arange(lo, hi + 1) * DT | |
| y = R[lo:hi + 1] | |
| ok = np.isfinite(y) & (y > 1e-6) | |
| if ok.sum() < 5: | |
| return None | |
| b = -np.polyfit(np.log(t[ok]), np.log(y[ok]), 1)[0] | |
| return float(b) | |
| def local_slopes(R): | |
| out = {} | |
| for a, b in LOCAL: | |
| if b >= len(R) or R[a] <= 1e-9 or R[b] <= 1e-9: | |
| continue | |
| out[f"{a}-{b}"] = round(float( | |
| -(np.log(R[b]) - np.log(R[a])) / (np.log(b) - np.log(a))), 3) | |
| return out | |
| def agg(x, f): | |
| """时间轴按 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] < 5: | |
| continue | |
| ml = min(MAX_LAG, x.shape[0] - 2) | |
| R = cross_acf(x, ml) | |
| top1, eff = top_stats(x) | |
| # Q1 局域斜率 | |
| ls = local_slopes(R) | |
| # Q2 稳定性 | |
| b_full = beta_of(R) | |
| # 拟合区间 | |
| range_b = {str(k): beta_of(R, hi=min(k, ml)) for k in FIT_RANGES} | |
| # 分裂 | |
| h = x.shape[0] // 2 | |
| b_h1 = beta_of(cross_acf(x[:h], ml)) | |
| b_h2 = beta_of(cross_acf(x[h:], ml)) | |
| q = x.shape[0] // 4 | |
| b_q = [beta_of(cross_acf(x[i * q:(i + 1) * q], ml)) for i in range(4)] | |
| # bin 宽 | |
| bw = {} | |
| for f in (1, 2, 5): | |
| xf = x if f == 1 else agg(x, f) | |
| if xf.shape[0] > 60: | |
| Rf = cross_acf(xf, min(MAX_LAG, xf.shape[0] - 2)) | |
| bw[str(f * 20)] = beta_of(Rf, hi=min(MAX_LAG, xf.shape[0] - 2)) | |
| # 截断到 TRUNC | |
| b_tr = beta_of(cross_acf(x[:TRUNC], min(MAX_LAG, TRUNC - 2))) if x.shape[0] >= TRUNC else None | |
| # 半数时间点(随机抽 1/2 的 bin,破坏局部结构,作为对照) | |
| sel = np.sort(rng.choice(x.shape[0], x.shape[0] // 2, replace=False)) | |
| b_sub = beta_of(cross_acf(x[sel], ml)) | |
| # Q4 零模型:每列独立循环移位 | |
| nulls = [] | |
| for _ in range(N_SHUFFLE): | |
| xs = x.copy() | |
| for j in range(xs.shape[1]): | |
| xs[:, j] = np.roll(xs[:, j], int(rng.integers(1, x.shape[0]))) | |
| nulls.append(beta_of(cross_acf(xs, ml))) | |
| 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), | |
| "beta": b_full, | |
| "beta_h1": b_h1, "beta_h2": b_h2, | |
| "beta_q": json.dumps([round(v, 3) if v else None for v in b_q]), | |
| "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 bw.items()}), | |
| "beta_trunc": b_tr, "beta_sub": b_sub, | |
| "beta_null": json.dumps([round(v, 3) if v else None for v in nulls]), | |
| "local_slopes": json.dumps(ls), | |
| "R1": round(float(R[1]), 4), "R2": round(float(R[2]), 4), | |
| }) | |
| if n % 10 == 0: | |
| print(f" [{n+1}/{len(idx)}] {r.file} β={b_full and round(b_full,3)} " | |
| f"null={nulls and [round(v,3) if v else None for v in nulls]} " | |
| f"[{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", "dt_ms": DT, "n_segments": int(len(df)), | |
| "trunc_bins": TRUNC, "n_shuffle": N_SHUFFLE} | |
| 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()), 3), | |
| "p10": round(float(v.quantile(.1)), 3), | |
| "p90": round(float(v.quantile(.9)), 3)} | |
| # ---------- Q1 | |
| ls_cols = [f"{a}-{b}" for a, b in LOCAL] | |
| rep["Q1_local_slope"] = {c: med(c) for c in []} # placeholder | |
| 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} | |
| rep["Q1_monotone_increase"] = round(float( | |
| LS[[c for c in LS.columns]].apply( | |
| lambda r: bool(np.all(np.diff(r.dropna().values) > -0.05)), axis=1).mean()), 3) | |
| # ---------- Q2 | |
| rep["Q2_beta"] = med("beta") | |
| rep["Q2_split_half"] = None | |
| m = df.dropna(subset=["beta_h1", "beta_h2"]) | |
| if len(m) > 5: | |
| r = spearmanr(m.beta_h1, m.beta_h2) | |
| rep["Q2_split_half"] = { | |
| "n": int(len(m)), "rho": round(float(r[0]), 3), "p": float(r[1]), | |
| "half_ratio_median": round(float((m.beta_h1 / m.beta_h2).median()), 3), | |
| "half_ratio_iqr": round(float((m.beta_h1 / m.beta_h2).quantile(.75) | |
| - (m.beta_h1 / m.beta_h2).quantile(.25)), 3)} | |
| BQ = pd.DataFrame([json.loads(s) for s in df.beta_q]) | |
| rep["Q2_quarters"] = {"n": int(BQ.notna().any(axis=1).sum()), | |
| "sd_median": round(float(BQ.std(axis=1).median()), 3), | |
| "range_median": round(float((BQ.max(axis=1) - BQ.min(axis=1)).median()), 3)} | |
| RNG = pd.DataFrame([json.loads(s) for s in df.beta_rng]) | |
| rep["Q2_fit_range"] = {c: {"median": round(float(RNG[c].median()), 3), | |
| "spread_p90_p10": round(float(RNG[c].quantile(.9) | |
| - RNG[c].quantile(.1)), 3)} | |
| for c in RNG.columns} | |
| rep["Q2_fit_range_ratio"] = { | |
| "median_max_over_min": round(float((RNG.max(axis=1) / RNG.min(axis=1)).median()), 2)} | |
| BW = pd.DataFrame([json.loads(s) for s in df.beta_bw]) | |
| rep["Q2_bin_width"] = {c: med(c, BW) if c in BW else None for c in BW.columns} | |
| m2 = df.dropna(subset=["beta", "beta_trunc"]) | |
| if len(m2) > 5: | |
| rep["Q2_truncation"] = { | |
| "n": int(len(m2)), | |
| "beta_full_median": round(float(m2.beta.median()), 3), | |
| "beta_trunc_median": round(float(m2.beta_trunc.median()), 3), | |
| "rho": round(float(spearmanr(m2.beta, m2.beta_trunc)[0]), 3), | |
| "ratio_median": round(float((m2.beta_trunc / m2.beta).median()), 3)} | |
| m3 = df.dropna(subset=["beta", "beta_sub"]) | |
| if len(m3) > 5: | |
| rep["Q2_subsample"] = {"n": int(len(m3)), | |
| "ratio_median": round(float((m3.beta_sub / m3.beta).median()), 3)} | |
| # ---------- Q3 | |
| rep["Q3_correlates"] = {} | |
| for c in ["n_units", "rate", "fano", "top1", "eff_dim", "hours"]: | |
| 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))} | |
| # ---------- Q4 零模型 | |
| NULL = pd.DataFrame([json.loads(s) for s in df.beta_null]) | |
| rep["Q4_null"] = { | |
| "beta_real_median": round(float(df.beta.median()), 3), | |
| "beta_null_median": round(float(NULL.mean(axis=1).median()), 3), | |
| "beta_null_p10": round(float(NULL.mean(axis=1).quantile(.1)), 3), | |
| "beta_null_p90": round(float(NULL.mean(axis=1).quantile(.9)), 3), | |
| "frac_null_ge_half_real": round(float( | |
| (NULL.mean(axis=1) >= 0.5 * df.beta).mean()), 3)} | |
| # ---------- 任务对照 | |
| 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), | |
| "beta_trunc": med("beta_trunc", g), | |
| "top1": med("top1", g), | |
| "rate": med("rate", 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"] = { | |
| "n_groups": len(groups), "kruskal_H": round(float(kw.statistic), 3), | |
| "p": float(kw.pvalue), | |
| "group_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_median": round(float(g1.median()), 3), | |
| "H2_median": 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 = df[(df.source == "human_intracortical")].beta_trunc.dropna() | |
| if len(a) > 2 and len(b) > 2: | |
| rep["species_truncated"] = { | |
| "macaque_median": round(float(a.median()), 3), | |
| "human_median": round(float(b.median()), 3), | |
| "p": float(mannwhitneyu(a, b).pvalue), | |
| "n": [int(len(a)), int(len(b))]} | |
| (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:
- 12.9 kB
- Xet hash:
- d631b87ddc6038e7ab6ce8bf8f6ff1f3a8dfe90176870000287fd45fac8e3fd2
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.