Buckets:

illusion-brain/brain-domains / scripts /exp43_tau_behavior.py
redmingwei's picture
download
raw
12.7 kB
"""Phase 43: τ 与行为的相关 —— 逐试次设计(DMFC ready-set-go)。
## 为什么换设计
exp42 用跨被试相关(n=10)检验 τ 与解码准确率,结果 ρ=−0.10。
但那个设计有两个问题:
1. **用解码代替行为** —— 文献的"整合窗口"是关于行为的
2. **n=10 功效不足** —— 需 |ρ|>0.65
DMFC_RSG 数据集解决了第一个问题:它有**真实的计时行为**。
但只有 1 个被试(sub-Haydn),所以不能做跨被试。
改用**逐试次设计**:
每个试次单独估 τ(该试次神经活动的衰减速率)
每个试次有计时误差 |t2 - t1|
看 τ 是否预测该试次的计时准确度(n = 1029)
## 数据(DANDI 000130)
intervals/trials 里的字段:
ready_time, set_time, go_time
→ 目标间隔 t1 = set - ready
→ 产生间隔 t2 = go - set
→ 计时误差 = t2 - t1(中位 −2 ms,SD 147 ms)
→ |误差| 中位 69 ms(9% 相对误差)
units/spike_times: 54 个 unit 的绝对时间戳
## 分析窗口
ready-set-go 的关键是 **set→go 的等待期**(被试必须在这段时间内
维持 t1 的记忆并倒计时)。取 set 之后到 go 之前作为主要窗口。
但注意:不同试次的这个窗口长度不同(因为 t1 不同)。
所以必须做 **时间归一化**,或者只取固定长度(如 set 后 400ms)。
## 三种 τ 估计
1. ACF 指数拟合(exp41 验证过,5/5 通过分辨率检验)
2. 积分自相关 τ_int(若窗口足够)
3. 1/f 指数(作为对照,非 τ)
## 判据
Spearman ρ(τ, |计时误差|) 显著负 → τ 长 = 更准 → 支持"整合窗口"假设
ρ ≈ 0 → τ 与行为无关
显著正 → 反方向,需解释
## 必须做的对照
1. **试次级打乱**:把 τ 与行为随机配对,看 ρ 的零分布
2. **控制反应时**:t2 本身与 τ 可能都受"唤醒度"影响
3. **控制 unit 数变异**:某些试次 spike 计数少,τ 估不准
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.optimize import curve_fit
from scipy.stats import spearmanr
OUT = Path("/domains/runs/tau-behavior")
NWB_TRAIN = "/domains/primate/dmfc_rsg/sub-Haydn_desc-train_ecephys.nwb"
FS = 250.0 # 统一到 250 Hz(4ms bin),与 exp41 一致
BIN_MS = 4.0
WIN_AFTER_SET_MS = 600.0 # set 之后的窗口(固定长度,避免随 t1 变)
N_PCS = 16
MAX_LAG_MS = 400.0
# ------------------------------------------------------------ NWB 读取
def load_trials(nwb_path):
import h5py
with h5py.File(nwb_path, "r") as f:
t = f["intervals"]["trials"]
d = {k: t[k][:] for k in
["ready_time", "set_time", "go_time", "start_time", "stop_time",
"is_outlier", "is_short"]}
u = f["units"]
spike_times = u["spike_times"][:]
st_idx = u["spike_times_index"][:]
# 按 unit 切分
bounds = np.concatenate([[0], st_idx])
units = [spike_times[bounds[i]:bounds[i + 1]] for i in range(len(st_idx))]
return d, units
def bin_spikes(units, t0, t1, bin_ms=BIN_MS):
"""把 [t0, t1) 内的 spike 转成 (n_bins, n_units) 计数矩阵。"""
edges = np.arange(t0, t1 + bin_ms / 1000, bin_ms / 1000)
if len(edges) < 4:
return None
M = np.zeros((len(edges) - 1, len(units)), dtype=np.float32)
for j, sp in enumerate(units):
if len(sp) == 0:
continue
s = sp[(sp >= t0) & (sp < t1)]
if len(s) == 0:
continue
idx = np.clip(((s - t0) / (bin_ms / 1000)).astype(int), 0, M.shape[0] - 1)
np.add.at(M[:, j], idx, 1.0)
return M
# ------------------------------------------------------------ τ 估计
def acf_fft(x, max_lag):
T = x.shape[0]
xc = x - x.mean(axis=0, keepdims=True)
n = 1
while n < 2 * T:
n <<= 1
f = np.fft.rfft(xc, n=n, axis=0)
acf = np.fft.irfft(f * np.conj(f), n=n, axis=0)[:max_lag + 1]
norms = np.arange(T, T - max_lag - 1, -1)[:, None]
acf = acf / np.maximum(norms, 1)
return acf / np.maximum(acf[0:1], 1e-12)
def estimate_tau(P, dt_ms, max_lag):
if P.shape[0] < 8:
return None
a = np.median(acf_fft(P, max_lag), axis=1)
lags = np.arange(len(a))
ok = np.isfinite(a) & (a > 0.02) & (a < 0.99)
if ok.sum() < 4:
return None
def f(t, tau):
return np.exp(-t / np.maximum(tau, 1e-3))
try:
popt, _ = curve_fit(f, lags[ok] * dt_ms, a[ok], p0=[100.0],
bounds=([2.0], [5000.0]), maxfev=6000)
return float(popt[0])
except Exception:
return None
def integrated_tau(a, dt_ms, c=5.0):
if len(a) < 6:
return None
taus = 1.0 + 2.0 * np.cumsum(a[1:])
Ms = np.arange(1, len(a))
m = Ms >= c * taus
if np.any(m):
return float(taus[int(np.argmax(m))] * dt_ms)
return None
# ------------------------------------------------------------ 主分析
def main():
OUT.mkdir(parents=True, exist_ok=True)
print("=" * 82, flush=True)
print("Phase 43 — τ vs 计时行为(逐试次,DMFC ready-set-go)", flush=True)
print("=" * 82, flush=True)
if not Path(NWB_TRAIN).exists():
print(f"缺 {NWB_TRAIN}", flush=True)
return
t0 = time.time()
trials, units = load_trials(NWB_TRAIN)
n_trial = len(trials["go_time"])
print(f" 试次 {n_trial} | unit {len(units)} | 载入 {time.time()-t0:.1f}s",
flush=True)
# ---- 行为
rt = trials["ready_time"]; st_ = trials["set_time"]; gt = trials["go_time"]
t1 = st_ - rt # 目标间隔
t2 = gt - st_ # 产生间隔
ok_beh = (np.isfinite(t1) & np.isfinite(t2) & (t1 > 0.1) &
(t2 > 0.05) & (~trials["is_outlier"]) & (~trials["is_short"]))
print(f" 行为有效试次 {ok_beh.sum()}", flush=True)
err = t2 - t1
print(f" 计时误差: 中位 {np.nanmedian(err[ok_beh]):+.3f}s, "
f"|误差| 中位 {np.nanmedian(np.abs(err[ok_beh])):.3f}s", flush=True)
# ---- 逐试次 τ
rows = []
win_s = WIN_AFTER_SET_MS / 1000.0
dt_ms = BIN_MS
max_lag = int(MAX_LAG_MS / BIN_MS)
for i in range(n_trial):
if not ok_beh[i]:
continue
a0 = st_[i]
a1 = a0 + win_s
M = bin_spikes(units, a0, a1)
if M is None or M.shape[0] < 20:
continue
# 逐 unit z-score 后 PCA
mu = M.mean(axis=0, keepdims=True)
sd = M.std(axis=0, keepdims=True) + 1e-6
Z = (M - mu) / sd
d = min(N_PCS, Z.shape[1] - 1)
try:
U, S, _ = np.linalg.svd(Z, full_matrices=False)
except np.linalg.LinAlgError:
continue
P = (U[:, :d] * S[:d]).astype(np.float64)
a = np.median(acf_fft(P, max_lag), axis=1)
tau = estimate_tau(P, dt_ms, max_lag)
ti = integrated_tau(a, dt_ms)
rows.append({
"trial": i,
"t1_ms": round(float(t1[i] * 1000), 1),
"t2_ms": round(float(t2[i] * 1000), 1),
"err_ms": round(float(err[i] * 1000), 1),
"abs_err_ms": round(float(abs(err[i]) * 1000), 1),
"rel_err": round(float(abs(err[i]) / t1[i]), 4),
"tau_ms": round(tau, 1) if tau else None,
"tau_int_ms": round(ti, 1) if ti else None,
"n_spikes": int(M.sum()),
"acf1": round(float(a[1]), 4) if len(a) > 1 else None,
"n_bins": int(M.shape[0]),
})
if len(rows) % 100 == 0:
print(f" {len(rows)} 试次 ({time.time()-t0:.0f}s)", flush=True)
df = pd.DataFrame(rows)
df.to_csv(OUT / "results.csv", index=False)
print(f"\n 完成 {len(df)} 试次 | 耗时 {time.time()-t0:.0f}s", flush=True)
d = df.dropna(subset=["tau_ms", "abs_err_ms"])
if len(d) < 30:
print("有效试次不足", flush=True)
return
print("\n" + "=" * 82, flush=True)
print(f"相关分析(n={len(d)} 试次)", flush=True)
print("=" * 82, flush=True)
def rep(x, y, name):
r, p = spearmanr(x, y)
print(f" {name:<34} ρ = {r:+.4f} p = {p:.2e}", flush=True)
return float(r), float(p)
r1, p1 = rep(d.tau_ms, d.abs_err_ms, "τ vs |计时误差|")
r2, p2 = rep(d.tau_ms, d.rel_err, "τ vs 相对误差")
r3, p3 = rep(d.tau_ms, d.t2_ms, "τ vs 产生间隔 t2")
r4, p4 = rep(d.tau_ms, d.n_spikes, "τ vs spike 总数")
r5, p5 = rep(d.n_spikes, d.abs_err_ms, "spike 总数 vs |误差|")
# ---- 置换对照
print("\n" + "-" * 82, flush=True)
print("置换对照(1000 次打乱 τ 与行为的配对)", flush=True)
print("-" * 82, flush=True)
rng = np.random.default_rng(0)
null = []
x = d.tau_ms.to_numpy()
y = d.abs_err_ms.to_numpy()
for _ in range(1000):
null.append(spearmanr(rng.permutation(x), y)[0])
null = np.array(null)
p_perm = float((np.abs(null) >= abs(r1)).mean())
print(f" 零分布: 中位 {np.median(null):+.4f} 95%区间 "
f"[{np.percentile(null,2.5):+.4f}, {np.percentile(null,97.5):+.4f}]",
flush=True)
print(f" 观测 ρ={r1:+.4f} 的置换 p = {p_perm:.4f}", flush=True)
# ---- 分 t1 层(控制目标间隔)
print("\n" + "-" * 82, flush=True)
print("按目标间隔 t1 分层(控制任务难度)", flush=True)
print("-" * 82, flush=True)
d2 = d.copy()
d2["t1_bin"] = pd.qcut(d2.t1_ms, 4, labels=["Q1(短)", "Q2", "Q3", "Q4(长)"],
duplicates="drop")
for lab, g in d2.groupby("t1_bin", observed=True):
if len(g) < 20:
continue
rr, pp = spearmanr(g.tau_ms, g.abs_err_ms)
print(f" {lab:<10} n={len(g):>4} t1中位={g.t1_ms.median():>6.0f}ms "
f"ρ = {rr:+.4f} p = {pp:.3f}", flush=True)
# ---- 判定
print("\n" + "=" * 82, flush=True)
print("判定", flush=True)
print("=" * 82, flush=True)
if p_perm < 0.05 and r1 < 0:
verdict = (f"✅ τ 与计时误差显著负相关 (ρ={r1:+.3f}, 置换 p={p_perm:.4f}) "
f"—— τ 长 = 更准,支持「整合窗口」假设")
elif p_perm < 0.05 and r1 > 0:
verdict = (f"⚠️ τ 与计时误差显著正相关 (ρ={r1:+.3f}) "
f"—— 与「整合窗口」假设方向相反")
elif abs(r1) < 0.05:
verdict = (f"❌ 无相关 (ρ={r1:+.3f}, 置换 p={p_perm:.4f}) "
f"—— τ 不预测计时准确度")
else:
verdict = (f"⚠️ 弱相关但不显著 (ρ={r1:+.3f}, p={p_perm:.4f}) "
f"—— n={len(d)} 下功效不足")
print(f" {verdict}", flush=True)
print(f"\n 注:单被试(sub-Haydn),只能做逐试次分析。", flush=True)
print(f" 窗口:set 后 {WIN_AFTER_SET_MS:.0f} ms(固定,避免随 t1 变化)",
flush=True)
(OUT / "report.json").write_text(json.dumps({
"task": "tau_vs_behavior",
"dataset": "DANDI 000130 DMFC_RSG (sub-Haydn)",
"design": "single-subject, trial-wise",
"n_trials_total": int(n_trial),
"n_trials_used": int(len(d)),
"window_after_set_ms": WIN_AFTER_SET_MS,
"n_pcs": N_PCS, "bin_ms": BIN_MS,
"behavior": {
"t1_median_ms": round(float(d.t1_ms.median()), 1),
"t2_median_ms": round(float(d.t2_ms.median()), 1),
"abs_err_median_ms": round(float(d.abs_err_ms.median()), 1),
"rel_err_median": round(float(d.rel_err.median()), 4),
},
"correlations": {
"tau_vs_abs_err": {"rho": round(r1, 4), "p": float(p1), "p_perm": p_perm},
"tau_vs_rel_err": {"rho": round(r2, 4), "p": float(p2)},
"tau_vs_t2": {"rho": round(r3, 4), "p": float(p3)},
"tau_vs_nspikes": {"rho": round(r4, 4), "p": float(p4)},
"nspikes_vs_abs_err": {"rho": round(r5, 4), "p": float(p5)},
},
"permutation": {"median": round(float(np.median(null)), 4),
"ci95": [round(float(np.percentile(null, 2.5)), 4),
round(float(np.percentile(null, 97.5)), 4)],
"n": 1000},
"verdict": verdict,
"elapsed_s": round(time.time() - t0, 1),
}, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\n 耗时 {time.time()-t0:.0f}s", flush=True)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
12.7 kB
·
Xet hash:
0fde009ecb72c47b5dd381cd62ddb42ba4946dfa62e4fc2c965c487efdcf72e5

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