Python实战:5分钟用NumPy和Matplotlib生成时间序列的MTF热力图(附完整代码)
从金融数据到视觉洞察:用Python构建时间序列的马尔可夫转移场热力图
你是否曾面对一长串股票价格数据,感觉它们就像一串难以捉摸的密码?传统的折线图能告诉你趋势,但隐藏在价格跳动背后的状态转移规律——比如价格从“小幅上涨”状态转移到“大幅下跌”状态的可能性有多大——却往往被忽略。今天,我想和你分享一种名为马尔可夫转移场的可视化技术,它能将时间序列中这种动态的、概率性的状态转移关系,编码成一张信息丰富的热力图。这不仅仅是画个图那么简单,它更像是在为你的时间序列数据拍摄一张“状态关系X光片”。
在金融数据分析、设备状态监测甚至行为模式识别中,理解一个状态如何演变为下一个状态至关重要。MTF正是为此而生。它基于马尔可夫过程的思想,假设下一个状态只与当前状态有关。通过计算和可视化状态间的转移概率,MTF热力图能直观揭示序列的平稳性、周期性以及异常突变点。对于数据分析师和算法工程师而言,这不仅是探索性数据分析的利器,更能为后续的特征工程和模型构建提供全新的视角。接下来,我将手把手带你,用NumPy和Matplotlib,从一个真实的股票价格案例开始,一步步生成并解读属于你自己的MTF热力图。
1. 理解核心:马尔可夫转移场究竟是什么?
在深入代码之前,我们有必要先厘清MTF的概念。它听起来有些学术化,但核心思想非常直观。想象一下,你正在观察一只股票每日的收盘价。为了简化分析,我们可以将连续的价格波动离散化成几个“状态”,例如:“暴跌”、“小幅下跌”、“横盘”、“小幅上涨”、“暴涨”。马尔可夫转移矩阵描述的是,在已知今天处于某个状态(比如“小幅上涨”)的条件下,明天转移到各个状态的概率分别是多少。这是一个N×N的方阵(N为状态数),每一行的概率之和为1。
那么,马尔可夫转移场又是什么呢?MTF将这个二维的转移矩阵,“映射”回原始时间序列的一维时间轴上,形成了一个N×N的矩阵(这里N是时间序列的长度,而非状态数)。MTF矩阵中位于第i行、第j列的元素 M[i, j],其含义是:在时间点i的状态,转移到时间点j的状态的概率。也就是说,它不再只关注相邻时间步的转移,而是构建了任意两个时间点状态之间的转移概率关系。这相当于把一维的时间序列,“展开”成了一个二维的概率关系图。
为什么这么做有价值?至少有三个层面的洞察:
- 揭示序列的宏观模式:如果热力图呈现出明显的对角线或块状结构,说明序列具有记忆性或周期性。例如,在股票数据中,这可能对应着某种趋势的持续性或均值回归的周期。
- 识别状态转移的异常:热力图中突然出现的亮斑或暗斑,可能对应着历史中不常见的状态跳变,这往往是异常事件或结构性断点的信号。
- 作为机器学习特征:MTF矩阵本身可以作为图像输入到卷积神经网络中,用于时间序列分类或预测任务,这是一种将时序问题转化为图像识别问题的巧妙思路。
为了更清晰地对比传统分析与MTF分析的视角差异,我们可以看下面这个表格:
| 分析维度 | 传统时间序列分析 (如折线图) | 马尔可夫转移场分析 |
|---|---|---|
| 关注焦点 | 数据点的绝对数值、趋势、季节性 | 数据点所属状态之间的转移概率关系 |
| 数据形态 | 一维序列 | 二维矩阵(图像) |
| 核心输出 | 统计指标、拟合曲线 | 概率热力图 |
| 优势 | 直观展示变化过程,易于理解 | 揭示隐藏的状态动力学和长期依赖关系 |
| 典型应用 | 趋势预测、异常检测 | 模式识别、序列分类、特征提取 |
理解了MTF是什么以及为什么有用之后,我们就可以开始动手实践了。整个过程可以清晰地分为几个步骤,我们接下来会逐一拆解。
2. 实战准备:环境搭建与数据获取
工欲善其事,必先利其器。我们首先需要确保Python环境中安装了必要的库。对于这个项目,核心依赖是三个:NumPy 用于高效的数值计算和矩阵操作,Matplotlib 用于绘图和可视化,SciPy 的可选模块用于后续的图像平滑处理。如果你使用Anaconda,这些库通常已经安装。若未安装,一条命令即可搞定:
pip install numpy matplotlib scipy
提示:建议在Jupyter Notebook或类似交互式环境中进行后续操作,这样可以实时看到每一步的结果,尤其是图像输出。
接下来是数据。为了让分析更有实感,我们不使用随机数,而是获取真实的股票价格数据。这里,我们可以使用 yfinance 库来方便地获取雅虎财经的历史数据。首先安装它:
pip install yfinance
然后,让我们获取苹果公司过去一年的日线收盘价数据:
import yfinance as yf
import numpy as np
# 定义股票代码和时间范围
ticker = "AAPL"
start_date = "2023-01-01"
end_date = "2024-01-01"
# 下载历史数据
stock_data = yf.download(ticker, start=start_date, end=end_date)
# 提取收盘价序列,并转换为NumPy数组
close_prices = stock_data['Close'].values
print(f"获取到 {len(close_prices)} 个交易日的数据。")
print(f"收盘价序列示例(前5个): {close_prices[:5]}")
print(f"收盘价范围: {close_prices.min():.2f} ~ {close_prices.max():.2f}")
运行这段代码,你将得到一个一维的 close_prices 数组,这就是我们后续分析的基础原材料。如果网络请求遇到问题,也可以先用本地CSV文件或一个模拟的合成数据序列来替代,确保流程能跑通。有了数据,我们的MTF构建之旅就正式开始了。
3. 构建MTF的核心三步:离散化、计算转移矩阵、生成场
MTF的生成是一个清晰的流水线过程。我们将把上百个收盘价数值,最终转换成一幅色彩斑斓的热力图。这个过程包含三个关键函数,我会逐一解释其原理并给出代码。
3.1 第一步:将连续价格离散化为状态
股票价格是连续值,但马尔可夫链处理的是离散状态。因此,我们需要将连续的收盘价“分箱”到有限个区间中。这里,我们采用等宽分箱法。假设我们想将价格波动划分为10个状态(从状态0到状态9)。
def discretize_signal(signal, n_bins):
"""
将连续信号离散化为n_bins个状态。
参数:
signal (np.ndarray): 输入的一维时间序列数据。
n_bins (int): 离散化的状态数量。
返回:
np.ndarray: 离散化后的信号,每个元素是对应原始值所属的状态索引(0到n_bins-1)。
"""
# 在信号最小值和最大值之间创建n_bins个等间距的边界
bin_edges = np.linspace(np.min(signal), np.max(signal), n_bins + 1)
# 使用digitize函数将每个信号值分配到对应的箱子里,返回索引从1开始
digitized = np.digitize(signal, bin_edges)
# 调整索引从0开始,并且确保最大值落在最后一个箱子内
digitized = digitized - 1
digitized[digitized == n_bins] = n_bins - 1 # 处理边界情况
return digitized
# 应用离散化
n_states = 10
price_states = discretize_signal(close_prices, n_states)
print(f"离散化后的状态序列(前10个): {price_states[:10]}")
这个函数做了两件事:首先确定价格的上下界并均匀分割;然后将每个实际价格“归类”到对应的区间。输出 price_states 是一个与 close_prices 等长的数组,但里面的数字不再是美元价格,而是0到9之间的整数,代表当天的价格处于哪个“状态层”。
3.2 第二步:计算状态转移概率矩阵
现在我们知道每一天处于什么状态了。接下来,我们要统计状态之间是如何转换的。具体来说,就是统计今天处于状态A、明天处于状态B的情况发生了多少次,然后将其转化为概率。这就是一阶马尔可夫转移矩阵。
def compute_transition_matrix(digitized_signal, n_states):
"""
根据离散化的信号计算一阶马尔可夫转移矩阵。
参数:
digitized_signal (np.ndarray): 离散化后的状态序列。
n_states (int): 状态的总数。
返回:
np.ndarray: 一个n_states x n_states的矩阵,其中元素[i, j]表示从状态i转移到状态j的概率。
"""
# 初始化一个全零的转移计数矩阵
count_matrix = np.zeros((n_states, n_states), dtype=int)
# 遍历序列,统计相邻状态转移的次数
for t in range(len(digitized_signal) - 1):
i = digitized_signal[t] # 当前时刻状态
j = digitized_signal[t+1] # 下一时刻状态
count_matrix[i, j] += 1
# 将计数矩阵转换为概率矩阵(行归一化)
# 防止某行总和为0导致除零错误,使用maximum函数设置最小分母为1
row_sums = count_matrix.sum(axis=1, keepdims=True)
prob_matrix = count_matrix / np.maximum(row_sums, 1)
return prob_matrix
# 计算转移矩阵
trans_mat = compute_transition_matrix(price_states, n_states)
print("状态转移概率矩阵(部分,前5x5):")
print(trans_mat[:5, :5])
print(f"\n矩阵形状: {trans_mat.shape}")
print("检查:每行概率之和应接近1。示例第一行和: {:.6f}".format(trans_mat[0].sum()))
得到的 trans_mat 是一个10x10的矩阵。你可以查看 trans_mat[2, 5] 的值,它代表当价格处于状态2时,下一次转移到状态5的估计概率。这个矩阵是MTF的“心脏”。
3.3 第三步:从转移矩阵生成马尔可夫转移场
这是最巧妙的一步。我们要创建一个大小为 (T, T) 的矩阵,其中T是时间序列的长度。对于这个矩阵中的每一个位置 (i, j),我们填入的值是 trans_mat[ price_states[i], price_states[j] ]。也就是说,用第i天的状态和第j天的状态作为索引,去查第一步得到的转移概率矩阵,把那个概率值填进去。
def create_markov_transition_field(transition_matrix, digitized_signal):
"""
根据转移矩阵和离散信号创建马尔可夫转移场。
参数:
transition_matrix (np.ndarray): 状态转移概率矩阵。
digitized_signal (np.ndarray): 离散化后的状态序列。
返回:
np.ndarray: 马尔可夫转移场矩阵,形状为 (len(signal), len(signal))。
"""
T = len(digitized_signal) # 时间序列长度
mtf = np.zeros((T, T))
# 遍历MTF矩阵的每一个位置
for i in range(T):
for j in range(T):
# 获取时间点i和j的状态
state_i = digitized_signal[i]
state_j = digitized_signal[j]
# 填入从状态i转移到状态j的概率
mtf[i, j] = transition_matrix[state_i, state_j]
return mtf
# 生成MTF矩阵
mtf_matrix = create_markov_transition_field(trans_mat, price_states)
print(f"MTF矩阵形状: {mtf_matrix.shape}")
print(f"MTF矩阵左上角4x4区域:\n{mtf_matrix[:4, :4]}")
现在,mtf_matrix 就是我们的原始马尔可夫转移场。它是一个二维方阵,对角线上的元素 M[i, i] 表示同一天状态“转移”到自身的概率(这通常由转移矩阵中对角线元素决定)。整个矩阵蕴含了任意两天之间状态转移的概率关系。
4. 可视化与增强:从矩阵到洞察热力图
生成了一个数值矩阵还不够,我们需要将其可视化,并通过对图像的处理和解读来提取信息。这里主要用到Matplotlib的 imshow 函数。
4.1 基础可视化与色彩映射
首先,我们绘制原始的MTF矩阵。
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 8))
# 使用‘hot’色彩映射,从暗(低概率)到亮(高概率)
im = plt.imshow(mtf_matrix, cmap='hot', origin='lower', aspect='auto')
plt.colorbar(im, label='转移概率')
plt.title('苹果股价马尔可夫转移场 (原始)')
plt.xlabel('时间点 j')
plt.ylabel('时间点 i')
plt.tight_layout()
plt.show()
这张热力图可能看起来颗粒感比较强,因为我们的时间序列有200多个点,矩阵就是200x200,每个像素代表一个概率值。图中亮黄色的区域代表高转移概率,深色区域代表低概率。
注意:
origin='lower'参数确保了y轴(i轴)的原点在左下角,这与矩阵索引和通常的时间序列起点认知一致。aspect='auto'让图像填充整个坐标区域,避免正方形变形。
4.2 应用高斯模糊平滑图像
原始的MTF可能包含大量细节和噪声,不利于观察宏观模式。我们可以应用一个轻微的高斯模糊来平滑图像,让模式更突出。这类似于在图像处理中降噪。
from scipy.ndimage import gaussian_filter
# 应用高斯模糊,sigma值控制平滑程度
sigma = 1.5
mtf_smoothed = gaussian_filter(mtf_matrix, sigma=sigma)
# 并排对比原始与平滑后的MTF
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
im0 = axes[0].imshow(mtf_matrix, cmap='hot', origin='lower', aspect='auto')
axes[0].set_title('原始 MTF')
axes[0].set_xlabel('时间点 j')
axes[0].set_ylabel('时间点 i')
plt.colorbar(im0, ax=axes[0])
im1 = axes[1].imshow(mtf_smoothed, cmap='hot', origin='lower', aspect='auto')
axes[1].set_title(f'平滑后 MTF (sigma={sigma})')
axes[1].set_xlabel('时间点 j')
axes[1].set_ylabel('时间点 i')
plt.colorbar(im1, ax=axes[1])
plt.tight_layout()
plt.show()
平滑之后,图像中的块状或带状结构通常会变得更加清晰。Sigma参数是关键:值太小平滑效果不明显,值太大会过度模糊丢失信息。通常需要在0.5到3之间尝试。
4.3 解读热力图:寻找金融数据的模式
现在,面对这张平滑后的热力图,我们能看出什么?以下是一些常见的模式及其在金融时间序列中的可能含义:
- 明亮的主对角线:如果对角线(i=j)是一条亮线,说明序列具有很高的自相关性,即当天的状态很大程度上决定了当天的状态(这是显然的),但也暗示了状态的持续性。在股价中,这可能意味着趋势的延续。
- 平行于对角线的亮带:如果除了主对角线,还有一条或多条与主对角线平行的亮带,这通常表明序列具有周期性。亮带距离主对角线的距离就是周期长度。在股票数据中,这可能对应着以周、月或季度为单位的某种循环模式。
- 均匀的方格图案:如果图像看起来像均匀的棋盘格,说明状态转移是随机的、无记忆的,符合随机游走的特征。
- 明显的亮块或暗块:图像中某个矩形区域特别亮或特别暗,表示在对应的那段时间区间内,状态转移模式发生了显著变化。这可能是结构性突变的信号,比如市场机制的改变、重大政策发布或公司财报发布前后。
- 不对称性:理论上,MTF矩阵不一定对称。如果图像关于对角线明显不对称,说明从状态A到状态B的概率与从B到A的概率不同,这揭示了状态转移的方向性偏好。例如,股价从“暴跌”状态恢复到“横盘”的概率,可能远高于从“横盘”直接进入“暴涨”的概率。
为了将视觉观察量化,我们可以计算一些辅助指标。例如,计算MTF矩阵的平均值和标准差,可以了解整体转移概率的水平和波动情况。计算对角线元素的平均值,可以量化序列的自持续性强度。
# 一些简单的量化分析
print("MTF矩阵统计信息:")
print(f" 平均值: {mtf_matrix.mean():.4f}")
print(f" 标准差: {mtf_matrix.std():.4f}")
print(f" 对角线均值: {np.diag(mtf_matrix).mean():.4f}")
print(f" 最大值位置: {np.unravel_index(mtf_matrix.argmax(), mtf_matrix.shape)}")
结合量化指标和视觉模式,你就能对这段苹果股价序列的状态动力学有一个超越简单涨跌的、更深层次的理解。
5. 高级技巧与参数调优实战
掌握了基础流程后,我们可以探索一些进阶操作,让MTF分析更具针对性和洞察力。
5.1 状态数量与分箱策略的选择
n_states(状态数)是一个至关重要的超参数。太少的状态会丢失信息,使MTF过于粗糙;太多的状态则可能导致转移矩阵稀疏,MTF噪声过大。
- 经验法则:对于长度T的时间序列,状态数可以设置在
sqrt(T)到T/10之间。对于200多天的日线数据,选择8到15个状态通常是合理的起点。 - 基于数据分布的分箱:除了等宽分箱,还可以尝试等频分箱,确保每个状态包含大致相同数量的数据点,这对于分布不均匀的数据(如股价)有时效果更好。
def discretize_quantile(signal, n_bins):
"""使用分位数进行离散化(等频分箱)"""
# 计算分位数点
quantiles = np.percentile(signal, np.linspace(0, 100, n_bins + 1))
# 去除可能重复的边界值
quantiles = np.unique(quantiles)
# 使用digitize,注意边界处理
digitized = np.digitize(signal, quantiles) - 1
digitized[digitized == len(quantiles)-1] = len(quantiles)-2 # 处理右边界
return digitized
# 尝试等频分箱
price_states_eqfreq = discretize_quantile(close_prices, n_states)
trans_mat_eqfreq = compute_transition_matrix(price_states_eqfreq, len(np.unique(price_states_eqfreq)))
mtf_eqfreq = create_markov_transition_field(trans_mat_eqfreq, price_states_eqfreq)
# 可以绘制对比两种分箱方法产生的MTF
5.2 处理更复杂的数据:收益率序列
收盘价序列通常是非平稳的,其统计特性(如均值、方差)会随时间变化。直接分析价格构建的MTF可能被长期趋势主导。一个常见的做法是分析对数收益率序列,它通常更接近平稳。
# 计算对数收益率
log_returns = np.diff(np.log(close_prices))
# 注意:收益率序列比价格序列少一个数据点
print(f"对数收益率序列长度: {len(log_returns)}")
# 对收益率序列进行MTF分析(需要重新调整状态划分,因为值域变了)
n_states_ret = 8 # 收益率的状态数可以设少一些
return_states = discretize_signal(log_returns, n_states_ret)
trans_mat_ret = compute_transition_matrix(return_states, n_states_ret)
mtf_returns = create_markov_transition_field(trans_mat_ret, return_states)
# 可视化收益率的MTF
plt.figure(figsize=(8, 6))
plt.imshow(gaussian_filter(mtf_returns, sigma=1.0), cmap='coolwarm', origin='lower', aspect='auto')
plt.colorbar(label='转移概率')
plt.title('苹果股价对数收益率的马尔可夫转移场')
plt.xlabel('时间点 j')
plt.ylabel('时间点 i')
plt.show()
分析收益率MTF,我们可能更关注市场波动状态的转移,例如从“低波动”到“高波动”的转移概率,这对于风险管理非常有价值。
5.3 将MTF作为特征用于机器学习
MTF矩阵可以看作是一张单通道的灰度图像。这意味着我们可以直接利用成熟的图像处理技术和卷积神经网络来处理时间序列。一个简单的应用流程如下:
- 数据准备:为多个时间序列样本(例如不同股票、不同时间段)分别生成MTF图像。
- 构建数据集:将MTF图像(可调整为统一尺寸,如64x64)和对应的标签(如股票涨跌类别、行业分类)组成数据集。
- 模型训练:使用一个简单的CNN(如LeNet、ResNet变体)对图像进行分类或回归训练。
# 伪代码示例:将MTF调整为固定尺寸并归一化,准备输入CNN
from skimage.transform import resize
def prepare_mtf_for_cnn(mtf_matrix, target_size=(64, 64)):
"""将MTF矩阵调整为固定尺寸并归一化到[0,1]"""
mtf_resized = resize(mtf_matrix, target_size, anti_aliasing=True)
# 归一化
mtf_normalized = (mtf_resized - mtf_resized.min()) / (mtf_resized.max() - mtf_resized.min() + 1e-8)
return mtf_normalized
# 假设我们有一个MTF列表和对应的标签列表
# mtf_list = [mtf1, mtf2, ...]
# labels = [label1, label2, ...]
# X_processed = np.array([prepare_mtf_for_cnn(mtf) for mtf in mtf_list])
# 然后可以将X_processed输入到Keras/Torch的CNN模型中
这种方法将时序分类问题转化为了图像分类问题,有时能捕捉到传统时序特征无法捕获的复杂模式。
在实际项目中,我经常需要对比不同参数(状态数、分箱方法、平滑系数)下生成的MTF,观察其对最终模式识别或分类任务效果的影响。没有一成不变的“最佳参数”,它高度依赖于你的数据特性和分析目标。多尝试、多对比,并结合业务知识进行解读,才是用好MTF的关键。
更多推荐

所有评论(0)