ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用 transfer_entropy 计算传递熵:识别时间序列间的信息流动方向

用 transfer_entropy 计算传递熵:识别时间序列间的信息流动方向

简介:面向时间序列分析与信息论应用开发者,这份Python资源实现两个随机过程之间的传递熵(Transfer Entropy)统计计算,可量化从X到Y或Y到X的非对称信息流,适用于因果关系推断、神经科学信号分析和金融时序建模等场景。包体紧凑,共2个文件,核心为一个Python脚本,负责根据给定历史观测计算条件概率与Kullback-Leibler散度,从而得到方向性信息传递量;另附Markdown格式说明文档,对算法的数学定义、输入输出格式和调用方式作简要梳理。压缩包整体约2KB,轻量易用,无需复杂环境依赖。目前已有2394人学习下载,适合具备基础Python知识、希望快速集成传递熵指标的算法工程师或研究人员。通过这份资源可获取可直接运行的Python脚本及配套说明,既可对照验证双向信息流计算结果,也可作为扩展实现多元传递熵或嵌入维度选择的起点。

1. 两个时间序列的驱动方向:TE 一算就清楚

手里有两路传感器信号,波形几乎一样、只是错开了半个周期,相关系数给到 0.9,你却说不清谁在驱动谁。transfer_entropy 这个 Python 库解决的就是这个问题:计算两个时间序列之间的传递熵(Transfer Entropy,TE),用条件互信息给出有方向、非线性的信息流动度量。TE(X→Y) 显著大于反向,说明 X 的过去对 Y 的未来有 Y 自身历史给不了的预测力。门槛很低,但参数选择和结果解读才是真正的坎。这篇文章从概念、最小代码、关键参数到踩坑记录,给你一条能直接照做的路径。

2. 先立住概念:TE 为什么能测「信息流动」

2.1 从熵到条件熵:信息论的两把尺子

要理解传递熵,先得理解香农熵。一个离散随机变量 X 的熵定义为 H(X) = −Σ p(x)·log p(x),它度量的是这个变量的不确定性:概率分布越平坦,熵越大;越集中,熵越小。单位由 log 的底数决定,底数是 2 就是比特(bit),底数是 e 就是奈特(nat)。在时间序列里,我们面对的不是单个变量而是一串变量,所以要引入联合熵和条件熵。联合熵 H(X, Y) 描述两个变量的共同不确定性,条件熵 H(Y|X) = H(X, Y) − H(X) 描述的是在已经知道 X 的前提下,Y 还剩多少不确定性。

这个差值很关键,它衡量的正是知识带来的不确定性削减。放到时间序列的语境里:如果已经知道 Y 的过去,再去考察"知道了 X 的过去之后,Y 的现在还能削减多少不确定性",这个削减量就是信息流动的候选度量。为什么强调"在知道 Y 的过去之后"?因为时间序列自身有自相关,Y 的很多变化本来就能被自己的历史解释。如果不去掉这部分,任何与 Y 相关的 X 都会被误判成驱动源,这就是下一节条件化的意义。

条件熵的直觉用一个具体场景就能讲清楚。假设一支标的的涨跌方向很难预测,历史数据里 Y 自身的序列相关很弱,但它的走势和另一支标的 X 的前一天状态强相关。那么 H(Y_t | Y 的历史) 仍然很大,因为 Y 自己的历史给不了多少信息;而 H(Y_t | Y 的历史, X 的历史) 会显著变小。这个显著的变小,就是 TE 要捕捉的东西。信息论方法的好处是不关心函数形式,只关心概率分布,因此理论上能捕捉任意的非线性依赖,不需要你事先指定 y = f(x) 长什么样。

2.2 TE 的定义与直觉:比预测改善更进一步的度量

传递熵的标准定义是 T_{X→Y} = I(X_{t-L:t-1}; Y_t | Y_{t-L:t-1}),也就是在 Y 自身过去已知的条件下,X 的过去与 Y 的当前之间的条件互信息。L 是滞后阶数,决定我们考虑多长的历史。写成概率形式是:

T_{X→Y} = Σ p(y_t, y_{t-1:L}, x_{t-1:L}) · log [ p(y_t | y_{t-1:L}, x_{t-1:L}) / p(y_t | y_{t-1:L}) ]

这个式子拆开就一句话:分母是只看 Y 的历史来预测 Y_t 的概率,分子是额外看了 X 的历史之后预测 Y_t 的概率。比值大于 1 说明 X 的历史确实改善了预测,取对数再按概率加权平均,得到的就是平均信息增益。TE 是非负的,而且天然不满足对称性:T_{X→Y} 和 T_{Y→X} 是两个完全独立的量。不对称,正是方向性度量的核心要求。

有一个特例值得记住:在系统是线性高斯的情况下,TE 和 Granger 因果在数学上是等价的,两者只差一个常数因子。这条性质给了我们一个锚点——当面对线性系统时,TE 与 Granger 不会给出矛盾结论。但反过来,当系统是非线性时,Granger 基于线性自回归的框架可能完全失效,而 TE 不需要任何函数形式假设,直接从联合概率密度出发。这是 TE 在复杂系统分析里不可替代的根本原因,也是后面避坑章节里"TE 和 Granger 结论相反"那一条的理论基础。

实操中,TE 的大小还依赖几个具体选择:用什么熵估计器、数据如何离散化、滞后取几阶。这些选择不改变 TE 的定义,但会改变数值。所以我建议把 TE 理解成一个"在同一设置下做相对比较"的度量,而不是一个普适常数。不同论文里 TE 数值不可直接比较,除非它们用了完全相同的估计流程,这个意识越早建立越好。

2.3 为什么常用方法不够:方向、非线性与滞后缺一不可

先说 Pearson 相关系数。它天然对称,corr(X, Y) = corr(Y, X),根本回答不了方向问题;它只度量线性关系,两个强非线性同步的信号可能算出接近 0 的相关系数;它还默认同时刻对齐,现实中驱动关系的传导往往有延迟,不做移位的相关会严重低估耦合。Granger 因果检验解决了方向问题,通过比较嵌套线性回归的残差方差判断 X 能否改善对 Y 的预测,但它的前提是真实关系是线性的。遇到平方耦合、阈值触发这类非线性,线性系数会被平均成不显著。互信息不做线性假设,但依然是对称的,只告诉你共享多少信息,不告诉方向。把这几个方法放一起对比:

方法方向性非线性滞后建模输出
Pearson 相关无无无对称标量
Granger 因果有无有检验统计量
互信息 MI无有无对称标量
传递熵 TE有有有有方向标量

TE 同时满足方向性、非线性和滞后建模三个条件,代价是它需要通过高维概率估计来获取这些能力。维度随滞后阶数和离散分箱数指数增长,对样本量的需求远高于相关系数。这个代价具体有多大、怎么控制,就是后两章参数部分要解决的事。理解了这个取舍,你就明白为什么 TE 不能像相关系数那样"几十个点就敢算"——算出来的不是信息流,是噪声。

3. 用 transfer_entropy 包跑通最小示例:代码与输出解读

3.1 安装与数据准备

这个库的安装没有任何特殊之处,直接走 pip。装完之后最常用的导入是from transfer_entropy import DiscreteTE,它是面向连续时间序列的接口:先把连续值离散化成 bin,再在离散概率表上估计 TE。如果你面对的是已经是离散类别标签的数据,可以绕过分箱这一步,但绝大多数传感器、金融行情、生理信号都是连续值,所以 DiscreteTE 是最合适的起点。

动手算真数据之前,我强烈建议先构造一组已知方向的数据来验证代码和环境。最常见的做法是构造一个"X → Y"的单向驱动系统:X 是独立白噪声,Y 在每一时刻等于 X 上一时刻乘以一个系数,再加上 Y 自身的白噪声项。这样真实方向是唯一确定的,如果算法给出的方向和构造不一致,说明是代码或环境的问题,而不是数据的玄学问题。

import numpy as np np.random.seed(42) n = 5000 # X: 独立标准正态白噪声,作为信息源 x = np.random.normal(0.0, 1.0, n) # Y: 由 x 的滞后一期驱动,系数 0.6,叠加独立噪声 y = np.zeros(n) for t in range(1, n): y[t] = 0.6 * x[t-1] + np.random.normal(0.0, 0.5)

这里有两个参数决定生成的难度:驱动系数 0.6 控制耦合强度,系数越大 TE 越高;噪声标准差 0.5 控制信噪比,噪声越大 TE 越难被检测。如果你想模拟"弱耦合、难检测"的场景,把系数降到 0.2、噪声升到 1.0 即可。np.random.seed(42)保证每次运行生成同一份数据,后续所有参数实验都能在这份数据上对比,不用反复重建基线。循环生成 5000 个点只需要几十毫秒,但这套数据可以作为整个项目早期的标准验证集。

3.2 正向与反向:一个最小 TE 计算

有了数据就直接算 TE。下面这个例子完整覆盖正向、反向两个方向,这是跑 TE 的基本素养——永远同时看两个方向,只看一边的结论没有意义。

from transfer_entropy import DiscreteTE # 正向: X 的过去对 Y 现在的信息 te_forward = DiscreteTE(x, y, lag=1, bins=3) res_forward = te_forward.compute() # 反向: Y 的过去对 X 现在的信息 te_backward = DiscreteTE(y, x, lag=1, bins=3) res_backward = te_backward.compute() print("X -> Y:", res_forward) print("Y -> X:", res_backward)

DiscreteTE(source, target, lag, bins)的第一个参数是信息源,第二个是目标,方向以"谁对谁"为准,别传反了。lag=1表示只用过去 1 个时刻参与计算,bins=3表示把连续值离散化成 3 个等级。构造完成后调用.compute(),内部会完成概率统计、条件熵计算,再做替代数据生成和显著性检验。在 5000 个采样点、bins=3 的设置下,每个方向的运行时间通常在一秒以内,可以放心做参数扫描。

跑完后的res_forward和res_backward是字典结构,里面是 TE 数值、p 值、替代数据的统计量等字段。res['te']这个键在各版本里基本没变过,其余字段名不同版本可能有差异,打印出来看一眼即可。以我们构造的数据,正向 TE 会明显大于 0,p 值小于 0.05;反向 TE 会落在替代数据基线附近,p 值不显著。如果连这种构造明确的单向驱动都测不出来,问题基本出在参数选择上,往下读第 4 章。

提示:TE 的数值单位取决于 log 底数,用自然对数得到 nats,用 2 为底得到 bits。报告结果时务必写明单位,否则和别人的结果对比数值对不上,一开始就在注释里记录单位习惯是最省事的做法。

3.3 读懂输出:方向、幅度与显著性

输出结果的第一件事是看方向性,而不是盯着绝对数值。TE 的绝对值受 bin 数、数据长度、单位选择影响很大,不同系统之间横向比较绝对值意义有限;但在同一数据集上,两个方向的对比是相对可靠的。判断准则一般是:如果正向 TE 明显大于反向,且正向显著、反向不显著,就是清晰的单向驱动证据;如果两个方向都显著,说明可能存在反馈回路或共同驱动源,这种情况不能直接下单向因果结论,后面第 5 章会专门展开。

第二件事是看替代数据均值。替代数据是打乱或重排后的序列算出来的 TE,代表了"没有真实耦合时 TE 能达到的基线水平"。如果观测 TE 只有 0.02,替代均值是 0.018,即使方向上有差别,这个差别的实际意义也很有限。我一般把观测值与替代均值的差值当作净信息流来报告,这个差值才是超出随机水平的那部分。

第三件事是看 p 值。库内部会生成一批替代序列,p 值就是替代 TE 大于等于观测 TE 的比例。p 值显著只代表"TE 大于随机水平",不代表因果成立——任何遗漏的共同驱动变量都可能造成虚假的显著。这是 TE 使用中最容易被误会的一环,也直接引出第 4 章和第 5 章的内容。

4. 三个必调参数:lag、bins 与替代数据检验

这一章是 TE 实战里最值得花时间的部分,也是我用血泪经验换来的结论:TE 好不好用,八成取决于参数。默认参数能跑通流程,但跑通和跑对是两回事。下面三个参数,每个都必须理解它的含义再动手调。

4.1 lag:信息传递的时滞必须试出来

lag 是 TE 公式里最敏感的参数,它决定"X 的历史到底往前看几步"。lag 设小了,真实延迟超过窗口长度,TE 被低估甚至接近 0;lag 设大了,联合概率的维度爆炸,每个格子里的样本数急剧下降,估计方差变大。默认值 lag=1 只在"影响当天就到"的场景里正确:金融高频数据里信息传导以毫秒计,lag=1 常常够用;但日频数据里板块间的传导可能滞后 3 到 5 天,脑电信号传导滞后也在几十毫秒到几百毫秒的尺度,不同场景对 lag 的要求差一个数量级以上。

我常用的做法是在候选范围内扫描 lag,比如 1 到 10,画 TE 随 lag 变化的曲线,取峰值对应的 lag。扫描不追求精确,追求方向稳定:真正有驱动关系的 pair,TE-lag 曲线通常有一个明显峰值;没有关系的 pair,曲线是一条接近水平的噪声线。如果峰值不明显,或者不同 lag 下方向结论反转,基本可以判断这对序列的 TE 关系不可靠,宁可不下结论,也不要挑一个好看的 lag 报结果——这是以后被人质疑时最难解释的地方。

扫描时要固定其他变量。bins 固定在同一值,替代数据次数也固定,保证横轴上只有 lag 在变化。另外注意 TE(lag=2) 和 TE(lag=1) 不是嵌套关系,不能说"看两期包含了看一期",因为条件集合不同,含义本身就不同。选定 lag 之后,最好单独做一次显著性检验确认结果,而不是直接沿用扫描里那个 p 值。

注意:扫描 lag 本身有多重比较的问题,最优 lag 处看到的 p 值是被选择过程放大过的,不能直接当作最终证据。严谨的做法是选定 lag 后,用一份独立的验证集重新计算 p 值。

4.2 bins:离散化粒度直接决定偏差与方差

DiscreteTE 面对连续数据时必须先分箱。bins 的含义是把值域等分成多少个区间,等宽分箱是最常见的做法。bin 数少了,比如 2 或 3,数据被粗粒化,非线性细节被抹平,TE 偏低;bin 数多了,比如 10,每个格子期望样本数指数下降,概率估计的方差暴涨,TE 虚高。这就是信息论估计里最经典的偏差-方差权衡,而 bins 就是你手里的旋钮。

经验法则和样本量强相关。对 n 个样本,每维 bin 数通常在 n 的三分之一次方到平方根之间取值,这是直方图估计的常用结论。5000 个样本取 3 到 5 个 bin,20000 个样本可以取 5 到 8 个,少于 1000 个样本就老老实实取 2 到 3。注意 TE 的条件化涉及多维联合分布,实际维度是 lag 乘 2 还要多,所以真实样本需求比直觉更大,宁少勿多是这个参数的正确态度。

bins 的另一个隐藏问题在异常值上。等宽分箱对异常值非常敏感:一个远在 10 倍标准差外的点会把分箱边界整体带偏,大量样本挤进同一个 bin,概率表失真。实际使用前我一般先对序列做分位数裁剪或标准化,把极端值压回去。如果装到的版本支持分位数分箱,优先用分位数分箱,它按排序位置切边界,对分布形状更不敏感。选好 bins 后做一个简单的鲁棒性检查:把 bins 加减 1 重新算,方向结论不翻转,结果才敢往下游用。

4.3 替代数据检验:不过显著性就不算数

TE 的数值本身受有限样本偏差影响,即使两个完全独立的序列也能算出非零的 TE。所以显著性检验不是可选项,而是必选。做法是替代数据检验:把 source 序列打乱或重排,target 保持不变,重新计算 TE,重复多次得到零假设分布,再看观测 TE 在分布里的位置。p 值就是替代 TE 大于等于观测 TE 的比例。库的.compute()内部默认会做这个检验,替代序列的条数一般可以配置。

替代次数太少,p 值的分辨率不够,比如 100 次替代能给出的最小非零 p 值就是 0.01,想要更精细的显著性刻度就得增加次数。常规做法是 200 到 1000 次。另一个容易被忽略的细节是打乱方式:完全随机打乱会破坏所有时间结构,如果原序列有强自相关性,替代分布会失真,得到不可靠的 p 值。更稳妥的做法是按块打乱(block shuffle),把序列切成若干块再重排,保留块内的时间结构。如果库支持块打乱,对自相关较强的序列优先选它。

显著性结论的正确表述要非常小心:p 值显著只能说明"TE 大于随机水平",不能说明"因果成立"。任何遗漏的共同驱动变量都可能造成虚假的 TE,这正是第 5 章第一个坑展开的内容。严谨的报告习惯是同时给出观测 TE、替代均值、p 值和样本量,这样别人复现时能判断你的结果可靠程度。

5. 避坑记录:TE 计算常见的五个翻车现场

5.1 现象一:TE 值永远接近 0,方向全无

现象:数据肉眼可见强相关,波形都对得上,但 TE 输出几乎为 0,两个方向都不显著。

原因:最常见的是 lag 没选对,真实传导延迟比默认值大,TE 的窗口根本没覆盖到有效滞后;其次是序列不平稳,带趋势或周期成分的数据会干扰概率估计;还有一种可能是耦合其实是瞬时的——X 和 Y 同时被某个外部源驱动,并没有真正的时间差信息流动。TE 定义里隐含一个前提:预测 Y 的未来需要利用过去的滞后信息,瞬时同步耦合不在它的测量范围内。

解决:按顺序排查。先扫描 lag 画 TE-lag 曲线,看峰值是否存在;再检查序列平稳性,有明显趋势就先差分或去趋势;最后回到数据生成逻辑,问自己一句"驱动关系真的有时间差吗"。如果没有时间差,TE 这个工具就不适合当前问题,换互信息或同步性度量更合理。

5.2 现象二:双向都显著,判断不出主驱动

现象:正向反向的 TE 都显著,说不清到底谁驱动谁。

原因:第一种是真实的反馈回路,X 影响 Y 的同时 Y 也影响 X,两个方向都存在信息流,TE 双向显著是正确反映;第二种是共同驱动源,存在第三个变量 Z 同时驱动 X 和 Y,造成虚假的 TE,这种情况下方向判断完全不可信。

解决:先分清楚属于哪种。反馈回路的情况下,双向显著本身就是有价值的结论,可以比较两个方向的净信息流大小,谁大谁主导;想提取单向因果就要引入条件化,把 Z 一起放进去算条件 TE。共同驱动源的情况下,最实用的一招是引入已知的混淆变量做条件化,或者设计控制场景对照。实在没有第三个变量的信息,就承认"存在混淆可能",不下硬性因果结论。

5.3 现象三:样本长度一变,TE 大幅振荡

现象:同一批数据截短一半或翻倍延长,TE 数值明显漂移,甚至方向反转。

原因:样本量不足加 bins 过大,这是最常见的组合。TE 要填的高维概率表大小随 lag 和 bins 指数增长,样本不够时每个格子只有几个样本,直方图估计方差极大,一点数据变化就能让概率分布面目全非。

解决:先降复杂度再谈精度。把 bins 降到 2 或 3,lag 收敛到最小有效值,观察结果是否稳定。我习惯做一个样本量扫描实验:从 500 开始翻倍到 20000,看 TE 是否收敛到某个平台。如果 TE 随样本量增加持续漂移,说明复杂度还是太高,5000 以下样本量还在用 bins=5、lag=3,基本是在自欺欺人。

5.4 现象四:TE 不小但 p 值过不了关

现象:观测 TE 明显大于 0,可替代数据检验就是不显著。

原因:最常见于强自相关的时间序列。如果替代数据用完全随机打乱生成,替代序列变成白噪声,和原序列的自相关结构不匹配,零分布本身就是错的;另一种可能是替代次数太少,p 值分辨率不够,结果恰好在临界边缘。

解决:优先改用块打乱,块长建议取序列自相关长度的 2 到 3 倍,保留块内结构;其次增加替代次数到 500 以上。如果块打乱之后仍不显著,反向怀疑初始 TE:是不是 bins 太大导致的虚假高值。我在这个问题上的教训是:显著性没过,宁可报"未检测到信息流",也不要为了得到好看的结果去调参数调到过——那只是让代码骗你自己。

5.5 现象五:TE 结论和 Granger 相反

现象:同一组数据,Granger 检验说 X 不是 Y 的因,TE 却给出显著的正向结果;或者反过来,Granger 显著而 TE 不显著。

原因:大多数情况是系统存在 Granger 模型捕捉不到的非线性耦合。Granger 基于线性自回归,遇到平方耦合、阈值效应、符号非对称的依赖关系,线性系数会被平均掉,得到不显著的结论。反过来,如果数据经过滤波或差分破坏了时间结构,TE 也可能失效。

解决:用已知的非线性系统做模拟对照。比如构造 y[t] = a·x[t−1]² + 噪声,这种场景下 Granger 大概率失效而 TE 能检出。确认真实数据属于非线性驱动,那就信任 TE;确认系统是线性的,结论矛盾就要去查预处理步骤。方法结论不同不是 bug,先搞清楚数据满足哪种假设,再决定信任哪个结果。

6. 一个进阶技巧:用滑窗算时变 TE

静态 TE 假设整段数据的信息流动结构不变,但很多真实场景并不满足。金融市场里板块间的主导关系会随行情切换,生理信号里信息流向随状态变化,工业设备的故障传播路径也会变。处理这类问题,我习惯把 TE 计算放进滑窗里,得到一条随时间变化的 TE 曲线。

def rolling_te(x, y, win=1000, step=200, lag=1, bins=3): """滑窗 TE: 返回 (窗口中心下标列表, TE 值列表)""" centers, values = [], [] for start in range(0, len(x) - win + 1, step): xw = x[start:start + win] yw = y[start:start + win] res = DiscreteTE(xw, yw, lag=lag, bins=bins).compute() centers.append(start + win // 2) values.append(res["te"]) return centers, values

窗口大小要和数据的平稳尺度匹配:窗口内数据必须足够长才能稳定估计概率表,至少是 bin 数个数的十倍以上,否则每个窗口都在重复前面的样本量坑。步长决定时间分辨率和计算量的平衡,步长越小分辨率越高,但计算量线性放大。一个实用的起点是 win=1000、step=200,配对数据量在 5 万以内跑完只需要几分钟。

滑窗结果的解读逻辑和静态不一样:方向判断要看整条曲线的主导区间。如果大多数窗口正向 TE 高于反向,少数窗口方向反转,那么反转的区间往往对应状态切换,这是最值得深挖的信号。

我自己每次做滑窗都会跑一对无耦合的数据做对照,把基线曲线画在同一个坐标轴里。凡是信号曲线穿不过基线带上界的窗口,一律不算显著。这个习惯帮我避免了好几次把噪声当信号的事故,也让滑窗结果在面对质疑时有据可查。希望这一套概念、代码、参数和避坑经验能帮到你,至少让 TE 不再是个结果全靠猜的黑匣子,希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进