☰
传递熵实战:从原理到Python实现与参数调优
2026/10/1 19:03:02 网站建设 项目流程

简介:这份MATLAB脚本实现了双向传递熵计算,面向需要量化时间序列间信息流动方向的复杂系统研究者、数据科学从业者及工程技术人员,可配合widelymfx等分析框架,用于神经科学中脑区通信分析、金融市场变量间领先滞后检测、生物物理等场景。传递熵相比互信息更能体现变量间的定向影响,脚本基于香农信息熵框架,完整覆盖滑动窗口预处理、概率分布估计、条件概率计算等关键步骤,可输出A→B与B→A的传递熵值,帮助识别系统内谁在驱动谁、谁是被驱动者,并配有结果可视化环节,便于直观观察信息流变化,所得结果可直接用于因果推断和动态交互分析。资源为1个m文件,压缩包仅1KB,轻量无需额外依赖,可直接在MATLAB中运行,也可作为算法教学示例或二次开发基础;计算时需根据数据长度、时间延迟与窗口大小调整参数,以保证结果稳健。已有453人学习下载,适合希望快速上手传递熵分析与应用的开发者。

1. 传递熵的第一个门槛叫方向,第二个门槛叫显著性

做时间序列分析的人多半有过这种经历:两条曲线走势高度同步,领导、客户或审稿人问“是不是 A 带动了 B”。相关系数只能说“高度相关”,互信息也只能说“存在依赖”。传递熵(transfer entropy)做的是另一件事:在已知 B 自身历史预测能力的前提下,看 A 的历史还能为 B 的未来降低多少不确定性。能降低,才谈得上方向性。这一指标把问题从“像不像”推到“传不传”,常被用在脑电通道间的信息流动、设备振动故障溯源、期货与现货价格引导关系这类场景里。适合谁:手里有同步采样的多通道序列、需要判断单向或双向传导方向的工程师与分析师。这篇笔记的目标很直接:概念过关、代码能跑、参数不翻车、结论拿得出手。

2. 传递熵的数学直觉与估计器选型:条件互信息如何给“相关”标方向

2.1 从互信息到传递熵:一条不对称的条件互信息

互信息 I(X;Y) 衡量两个变量的共同信息量,但它是完全对称的,交换 X 和 Y 结果一样。相关性、互信息这类对称度量永远回答不了“谁影响谁”。传递熵把方向这一个问题改写成“可预测性提升”:

TE_{X→Y} = H(Y_next | Y_past) − H(Y_next | Y_past, X_past)

这里的 H 是条件熵,Y_past 是 Y 在过去一段窗口内的取值,X_past 同理。公式说的是:先用 Y 自己的历史去预测 Y 的未来,得到一个不确定性;再额外把 X 的历史也放进去,再看不确定性下降了多少。如果下降明显,说明 X 的过去携带了关于 Y 未来的信息,而且这部分信息没有被 Y 自身的历史覆盖。把这个量记为 X 到 Y 的传递熵。

关键就在“条件”两个字。没有条件,式子退化成普通互信息;有了条件,才能区分“同步相关”和“方向性信息流”。一个典型的例子是共同驱动场景:两个序列同时受第三个变量影响,互信息一定很大,但传递熵里 Y 自身历史已经把趋势解释得差不多,X 能贡献的增量信息很小。这一步先把因果直觉立住,后续的代码和参数才谈得上有意义。

2.2 直方图分箱估计:最直观但最吃参数的路径

要把上面的公式变成可计算的统计量,最直接的做法是离散化。把连续序列映射到有限个符号区间,然后统计联合概率分布。这是直方图分箱估计,也是许多人第一次实现传递熵时选择的方法。它的优点是逻辑透明:分箱、计数、算熵,每一步都能对照公式检查;缺点也很明显,高维下状态空间迅速膨胀。

举个例子:嵌入维度取 3,分箱数取 16,只算目标侧条件变量就有 16 的三次方量级的状态空间,再乘上待预测的未来值,联合分布单元数上万。样本量只有几千时绝大多数格子是空的,概率估计偏置大。所以直方图法适合做两件事:验证自己的代码逻辑没有写反方向,以及处理符号序列或低嵌入维度的快速筛查。正式出结论前,必须换更稳的估计器复核。

2.3 k 近邻与高斯估计器:为什么工具箱默认不选直方图

连续序列的传递熵估计,业内常见做法是 k 近邻类估计器,典型代表是基于 Kraskov–Stögbauer–Grassberger 互信息估计扩展出来的 KSG 传递熵估计器。它不依赖把数据切进固定格子,而是通过每个样本点在联合空间里的近邻距离估计概率密度,对连续分布和非线性耦合更友好。许多信息动力学工具箱默认使用这类估计器。

高斯解析估计器是另一个常见选项。它假设变量服从联合高斯分布,把熵写成协方差矩阵行列式的解析形式,计算快,但在非线性耦合下会系统性低估传递熵。如果你处理的是脑电、金融高频、机械振动这类大概率非线性的信号,高斯估计只配做快速摸底。

名字层面的坑也在这里。你在不同仓库里会看到 transfer_entropy、transferentropy 甚至带着用户名前缀的变体写法,本质是同一个量的命名习惯差异。选工具箱时不要被名字迷惑,优先看底层估计器类型和置换检验实现。

三类估计器选型对比:

估计器适用输入主要优势主要代价建议场景
直方图分箱连续序列需离散化实现透明、易调试高维稀疏、对箱子数敏感验证代码、符号序列
高斯解析连续序列速度快、无分箱参数只适合近似高斯分布快速摸底、大量通道预筛
k 近邻 KSG连续序列能捕捉非线性耦合速度慢、对邻域 k 敏感正式分析、论文级结果

3. 用 Python 把传递熵跑通:从分箱估计器到工具箱复核

3.1 先造一份“已知因果”的合成数据

跑任何估计器之前,手里必须有一份真值已知的数据。否则代码跑出结果,你也不知道它算得对不对。常见做法是造一个单向耦合的向量自回归过程:X 独立演化,Y 受 X 的滞后值影响,但 Y 不反馈回 X。

import numpy as np def make_coupled_series(n=4000, delay=2, noise=0.1): """生成 X -> Y 单向耦合的合成序列,用于验证传递熵代码。""" x = np.zeros(n) y = np.zeros(n) x[0] = 0.5 y[0] = 0.5 for t in range(1, n): x[t] = 0.8 * x[t - 1] + 0.2 * noise * np.random.randn() y[t] = 0.5 * y[t - 1] + 0.35 * x[t - delay] + 0.2 * noise * np.random.randn() return x, y

这份数据里 X 是一阶自回归,Y 的一阶自回归之外增加了来自 X 的滞后贡献。delay 参数控制真实耦合延迟,噪声项让估计不会完美到失真。为什么一定要先造这份数据:它保证你知道“真实答案”是 X→Y 成立、Y→X 不成立。后面无论自写代码还是调工具箱,第一步都用它验证方向,再上真实数据。

3.2 分箱法估计器:从公式到可跑代码

下面这段代码是直方图分箱传递熵的最小实现。它固定嵌入维度为 1,只计算 Y 的最近过去和 X 的最近过去对 Y 未来的信息贡献,但结构完整,可以直接替换成更高维度。

import math from collections import defaultdict import numpy as np def discretize(u, nbins=8): """用分位数边界把连续序列离散化为符号。""" edges = np.quantile(u, np.linspace(0, 1, nbins + 1)) edges[0] -= 1e-12 # 防止最小值被分到第 0 个箱子外 return np.clip(np.digitize(u, edges) - 1, 0, nbins - 1) def transfer_entropy_hist(x, y, nbins=8, delay=1): """直方图分箱法估计 T_{X->Y},熵单位为 nat。""" xq = discretize(x, nbins) yq = discretize(y, nbins) n = len(yq) # 三元组结构:(Y_{t+delay}, Y_t, X_t) triples = [(yq[t + delay], yq[t], xq[t]) for t in range(n - delay)] c_joint3 = defaultdict(int) # (Y_future, Y_past, X_past) c_joint2 = defaultdict(int) # (Y_future, Y_past) c_dep2 = defaultdict(int) # (Y_past, X_past) c_dep1 = defaultdict(int) # (Y_past) for yf, yp, xp in triples: c_joint3[(yf, yp, xp)] += 1 c_joint2[(yf, yp)] += 1 c_dep2[(yp, xp)] += 1 c_dep1[yp] += 1 total = len(triples) def cond_entropy(joint_counts, dep_counts, total): """由联合计数与条件变量计数计算 H(Y_future | 条件变量)。""" h = 0.0 for key, cnt in joint_counts.items(): p_joint = cnt / total p_dep = dep_counts[key[1:]] / total h -= p_joint * math.log(p_joint / p_dep) return h h_y_given_yp = cond_entropy(c_joint2, c_dep1, total) h_y_given_yp_xp = cond_entropy(c_joint3, c_dep2, total) return h_y_given_yp - h_y_given_yp_xp

代码逻辑分四步。第一步把连续序列离散化,np.quantile 按分位数生成箱子边界,比固定等宽边界更能适应数据分布。第二步构建三元组,每条三元组对应“未来的 Y、过去的 Y、过去的 X”。第三步统计四类计数,分别是三元联合、二元联合,以及两个条件变量各自的分布。第四步用条件熵函数计算两类条件熵,相减得到传递熵。

注意 cond_entropy 里 key[1:] 的用法:joint_counts 的键第一位是待预测的 Y_future,剩余部分是条件变量。比如 c_joint3 的键是 (yf, yp, xp),条件变量计数就取 (yp, xp)。这个细节最容易写错,一旦写错方向,输出直接失真。想换成比特为单位,把 math.log 改成 math.log2 即可。

参数上,nbins=8 和 delay=1 只是起点。跑合成数据时,delay 设成和真实耦合延迟一致,TE 值应当明显大于 0;反向 Y→X 的估计值应接近 0。如果分箱估计器和真实答案对不上,先怀疑离散化边界,再检查三元组的构建顺序。

3.3 用工具箱做显著性检验:设置与输出判读

自写估计器只能给出一个点估计值,给不出显著性。真实场景里你必须回答一个问题:这个 TE 值是不是噪声碰出来的?常见做法是换用专门的信息动力学工具箱,例如 Python 生态里的 IDTxl,它内置多变量传递熵分析和基于置换检验的显著性推断。

from idtxl.multivariate_te import MultivariateTE from idtxl.data import Data data = Data(np.vstack([x, y]), dim_order='sp') settings = { 'cmi_estimator': 'Jidt_GaussianCMI', 'max_lag_sources': 5, 'min_lag_sources': 1, 'max_lag_target': 5, 'min_lag_target': 1, 'n_perm_min': 200, 'n_perm_max': 500, 'alpha': 0.05, } analyser = MultivariateTE() result = analyser.analyse_network( settings=settings, data=data, sources='all', targets='all' ) print(result.get_network_statistics())

这段代码是把两行序列组成多变量数据对象,然后做全网络分析。settings 里的参数是真正影响结果的部分:min_lag_sources 和 max_lag_sources 控制 X 侧历史窗口的扫描范围,min_lag_target 和 max_lag_target 控制 Y 自回归侧的范围;cmi_estimator 指定条件互信息估计器,这个示例用的是高斯估计,想捕捉非线性就把名字换成 KSG 类估计器;n_perm_min 和 n_perm_max 是置换检验的最小最大置换次数;alpha 是显著性水平。

工具箱版本不同,结果对象的方法名可能有差异。拿到 result 之后先跑一次 dir(result),再看可用的属性和方法名,这是最稳妥的做法。

输出判读按这个表来:

输出字段含义判读基准
TE 值条件互信息估计值越大信息流越强,但受估计器影响
p 值置换检验显著性p < 0.05 才承认连接存在
z 分数相对零分布的偏离程度经验上绝对值大于 2 值得关注

判读时还有一条容易被忽略:p < 0.05 但 TE 值只有千分位,说明效应量极小,结论要谨慎。显著性回答“是不是噪声”,TE 值大小回答“信息流强不强”,两者必须一起报告。

4. 传递熵参数怎么定:嵌入维数、延迟与分箱数的经验区间

4.1 嵌入维数:先把 Y 的“自记忆”选对

嵌入维数指的是把过去多少个时刻纳入条件变量。TE 的定义里“Y 自己过去能解释多少”这一步全靠嵌入维数支撑。维数太小,Y 的自回归记忆没被完全控制住,X 的历史可能会替 Y 的历史背锅,产生伪双向结果;维数太大,条件变量维度暴涨,联合分布稀疏,估计方差变大。

我一般不会拍脑袋选维数。常见做法是先建一个 Y 的自回归模型,从 1 阶逐阶往上试,看 AIC 或 BIC 下降到哪个阶数开始平稳,把那个阶数作为嵌入维数的起点。再用格子搜索在起点附近试两个值,比较 TE 方向和 p 值是否稳定。方向没变、量级没崩,这个维数就算可接受;方向翻转则需要警惕。

4.2 延迟扫描:不要只试 delay=1

真实系统的耦合很少是恰好滞后一个采样周期。脑电跨通道传播通常有几个毫秒延迟,金融价格引导关系可能滞后几十个 tick。如果只把 delay 设成 1,真实滞后在第 5 步,第 5 步的 X 历史根本不会进入条件窗口,TE 自然测不出来。

正确做法是做一个延迟扫描:让 min_lag_sources 从 1 开始,max_lag_sources 设到你认为合理的最大滞后,看 TE 值随延迟的变化曲线。曲线在某个延迟处出现峰值,那个位置就是耦合延迟的经验估计。注意这个峰值只能作为“滞后结构”的参考,不能直接当因果证据——因果判断还要依赖显著性检验。另一个细节:min_lag_sources 和 min_lag_target 尽量从 1 开始,不要用 0。0 表示同时刻,两个序列的同时关联可以是共同驱动造成的,没有时间先后,放进 TE 里会污染方向判断。

4.3 分箱数与邻域大小:分辨率与样本量的博弈

直方图分箱法里,nbins 是一个必须交代的参数。分箱数太小,非线性结构被粗暴平均,TE 被压到零附近;分箱数太大,联合分布单元数暴涨,每个格子里样本稀少,估计偏置反而推高虚假值。这个矛盾没有固定解,只能按样本量找平衡点。

k 近邻类估计器的对应参数是邻域 k。k 小,对局部结构更敏感,但方差大;k 大,估计平滑,但可能抹掉细微的信息流。我一般先 k=4 起步,再跑一次 k=2 和 k=8 的稳定性检查,看 TE 值的量级和方向是否保持稳定。

参数快速参考:

参数常见范围起点建议什么时候需要动它
嵌入维数 d1~8按 AR 模型的 BIC 选伪双向出现时加大
延迟 τ1~max_lag做延迟扫描确定序列存在明显滞后时
分箱数 nbins4~328样本量小用 4~8
k 近邻数 k2~104结果波动大时上下试探
置换次数100~1000200p 值接近 0.05 时加大

4.4 置换检验:显著性结论的最后一道闸门

置换检验的思想是把 X 的历史随机打乱,破坏它与 Y 未来的关联,然后重新计算 TE,重复上百次得到零分布。真实的 TE 值如果远大于零分布的绝大多数值,就说明结果不太可能是偶然。这也意味着置换次数直接决定 p 值的稳定性。

经验值是 200 次起步,如果 p 值在 0.05 附近徘徊,加到 500 甚至 1000。多目标同时检验时要留意多重比较问题,常见做法是把 alpha 按比较次数做校正,或者用 FDR 控制错误发现率。还有一个容易被忽视的问题:时间序列本身存在自相关,有效样本量远小于序列长度。置换检验把样本当作独立来打乱时,零分布可能过窄,p 值虚低。严谨的场景用块置换,按时间块整体打乱,保留块内的时间结构。

5. 跑传递熵的五个坑:从伪双向到虚假显著

5.1 伪双向:X→Y 和 Y→X 都显著

现象:合成数据明明只设了 X→Y 单向耦合,跑出来两个方向都是 p < 0.05。

原因:最常见的是共同驱动。X 和 Y 同时受某个外部变量 W 影响时,两者的历史互相能预测对方的未来,伪双向就这么出来的。另一个原因是嵌入维数太小,Y 自回归记忆没控制干净。

解决:先把外部变量 W 作为条件变量纳入计算,做条件传递熵;或者至少把 W 从 X 和 Y 中回归掉再算。合成数据场景可以先检查一下数据生成函数:X 的独立演化是否真的只依赖自己。在工具箱里,把 W 加进 data 矩阵并把 sources 或 targets 的索引配好,是这类问题最直接的修正。

5.2 延迟设成 1,真实耦合在第 10 步

现象:TE 值接近零,所有 p 值都不显著,但你在把 X 整体平移 10 步后重新对齐时,TE 突然变得显著。

原因:真实耦合延迟超过了扫描窗口。delay=1 只让最近 1 步的 X 历史进入条件变量,第 10 步的滞后贡献完全没被模型看到。

解决:做延迟扫描网格,从 1 到最大合理滞后逐个算 TE,观察峰值位置。这个坑在金融数据和设备振动数据里特别常见。不要用“试了几个延迟都不显著”来判断没有因果,先确认你的 max_lag_sources 覆盖了物理过程可能的传播时间。

5.3 分箱数过多或过少,方向直接翻转

现象:nbins=4 时 X→Y 方向几乎为零,nbins=32 时两个方向都显著,换 nbins=16 又一个结果。人还没开始分析,参数先替你做了一轮翻车。

原因:分箱数过少时非线性结构被抹平,真实的信息流被平均掉;分箱数过多时联合概率分布单元数爆炸,有限样本下统计偏置主导,虚假显著随之出现。

解决:先用 k 近邻类估计器复核,它不需要分箱这一步。如果必须用直方图法,把 nbins 从 4 到 32 逐个扫描,看方向是否稳定。方向随参数翻转的结果,一律按不可信处理。

5.4 非平稳数据直接上 TE,整段结果和分段结果打架

现象:整段数据算出来 X→Y 显著,但把数据切成四段逐段算,两段显著、两段不显著,甚至有一段方向反了。

原因:序列存在均值漂移或方差变化。非平稳过程里,条件熵的估计被趋势部分主导,真正的信息流被淹没或污染。这在这个指标的场景里尤其要命,因为脑电、金融 tick、设备振动天然带有长时间漂移。

解决:先做平稳性检验,差分或去趋势后再算。如果非平稳本身就是研究对象,那就做滑窗传递熵,报告 TE 随时间的变化曲线,不要把一个平均值的结论当成全时段真理。

5.5 置换次数太少,p 值在下次跑就换脸

现象:第一次跑 p=0.049,勉强显著;重复一遍同样的分析,p=0.07,结论翻了。

原因:置换次数不够,零分布本身就带噪声。200 次置换在临界值附近的分辨率不足;时间序列的自相关又让有效样本量远小于名义样本量,零分布被压窄,p 值虚低。

解决:提高置换次数,临界结论至少 500 次以上;怀疑自相关时改用块置换。报告结论时把 TE 值、置换次数和零分布的分位数一起贴出来,别只报一个 p 值。

6. 合成对照与灵敏度检查:传递熵上线前的最后一关

我自己在真实数据上翻过车,后来养成一个习惯:每条传递熵结论的背后,都先跑一份合成对照脚本。这份脚本不用复杂,固定包含三组测试。第一组是单向耦合数据,确认 X→Y 显著、Y→X 不显著;第二组是完全独立的两条随机序列,确认 TE 不虚高,p 值落在预期区间;第三组是共享驱动数据,确认条件化后能识别出伪关系。

然后是参数灵敏度检查。把延迟从 1 扫到真实滞后附近,把分箱数或邻域 k 各换两档,看方向和量级是否稳定。稳定性比单个数值重要得多。

# 滑窗 TE:检查信息流是否随时间变化 te_window = [] for start in range(0, len(x) - 1000, 200): xw = x[start:start + 1000] yw = y[start:start + 1000] te_window.append(transfer_entropy_hist(xw, yw, nbins=8, delay=2))

这几十行代码价值很高。窗口大小卡在 1000 个点,步长 200,跑完看 TE 曲线是否平滑。如果某个窗口突然跳出一根尖峰,优先检查那段数据里有没有异常事件,而不是急着下因果结论。

最后说一个我自己的教训:早期做通道有效连接分析时,我交出一张“X→Y 显著”的图,后来发现 X 和 Y 只是共同受一个外部驱动影响,嵌入维度加两阶之后结论就消失了。从那以后,合成对照脚本成了固定动作。它不花多少时间,但能在你对着显著性结果下结论之前,先把“方向对不对”“参数稳不稳”“显著是真还是假”这三关过掉。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询