4. 数据读取实战:用Pandas读取CSV格式Tick数据、处理大文件的分块读取策略

好,咱们进入实战环节。

前面聊了那么多理论,什么Tick数据结构、存储格式、字段含义……说实话,纸上谈兵没意思。真正干活的时候,你面对的第一个问题往往是:这文件怎么读进来?

尤其是高频Tick数据,一天的数据量动不动就几个GB。我见过不少新手,上来就pd.read_csv()一把梭,然后内存爆了,电脑卡死,心态崩了。

嗯,今天咱们就把这个坎儿迈过去。

4.1 为什么Tick数据不能直接全量读取?

先说说原因。

一个典型的沪深300股指期货Tick文件,一天大概有20万到30万条记录。如果存成CSV,大概200MB到500MB。看起来不大对吧?

但你要知道,回测不是只跑一天。你跑一个月、一年、甚至跨品种,数据量轻松上10GB。这时候pd.read_csv()默认行为是把整个文件塞进内存。

我曾在项目中遇到过这种情况:服务器64GB内存,跑一个三年的全品种Tick回测,数据加载阶段直接OOM(内存溢出)。后来查了半天,发现是读取策略没优化。

说白了,内存不是无限的,Tick数据也不是一次性消费品。我们需要分块、按需、流式地读取。

4.2 基础读取:Pandas read_csv 核心参数

先看最基础的用法。假设你有一个文件 tick_data_20240101.csv,结构如下:

timestamp symbol price volume direction
2024-01-01 09:30:00.123 IF2401 3850.2 5 B
2024-01-01 09:30:00.456 IF2401 3850.4 3 S

最基本的读取代码:

import pandas as pd

df = pd.read_csv('tick_data_20240101.csv')
print(df.head())
print(df.info())

这样写没问题,但仅限于小文件。我个人习惯,不管文件大小,先看一眼文件的行数和大小

import os

file_path = 'tick_data_20240101.csv'
file_size = os.path.getsize(file_path) / (1024 * 1024)  # MB
print(f"文件大小: {file_size:.2f} MB")

# 快速统计行数(不加载数据)
with open(file_path, 'r') as f:
    line_count = sum(1 for _ in f)
print(f"总行数: {line_count}")

为什么要这么做?因为你要心里有数。如果文件超过1GB,直接全量读就是找死。

4.3 分块读取:chunksize 参数

Pandas 提供了一个非常实用的参数——chunksize。它不会一次性返回DataFrame,而是返回一个迭代器,每次读取指定行数。

看代码:

chunk_size = 50000  # 每次读5万行
chunks = []

for chunk in pd.read_csv('tick_data_20240101.csv', chunksize=chunk_size):
    # 在这里可以对每个chunk做预处理
    # 比如过滤、转换类型、去重等
    chunk['timestamp'] = pd.to_datetime(chunk['timestamp'])
    chunk = chunk[chunk['price'] > 0]  # 过滤异常价格
    chunks.append(chunk)

# 最后合并
df_full = pd.concat(chunks, ignore_index=True)
print(f"最终数据量: {len(df_full)}")

这里有个细节:chunksize 不是越大越好。设得太小,比如1000行,循环次数太多,I/O开销大;设得太大,比如100万行,内存压力又上来了。

我一般怎么选?看单行数据的大小。Tick数据每行大概100-200字节,5万行大约5-10MB。这个量级对内存很友好,处理速度也快。

小技巧: 如果你不确定chunksize设多少,可以先读1000行,用df.memory_usage(deep=True).sum()算一下内存占用,然后反推合适的chunk大小。

4.4 按需读取:usecols 和 dtype 优化

Tick数据字段很多。你想想看,回测时你真的需要所有字段吗?

比如有些CSV里包含exchangetrade_idbid_price1ask_price1……但你的策略只需要timestamppricevolume。那就别读多余的字段。

# 只读需要的列
cols = ['timestamp', 'symbol', 'price', 'volume']
df = pd.read_csv('tick_data_20240101.csv', usecols=cols)

另一个容易被忽略的点是数据类型。Pandas默认会把所有数字列读成float64,但Tick数据里的volume是整数,price用float32精度也够了。

dtype_dict = {
    'symbol': 'category',   # 品种代码用category,节省内存
    'price': 'float32',
    'volume': 'int32',
    'direction': 'category'
}

df = pd.read_csv('tick_data_20240101.csv', 
                 usecols=cols,
                 dtype=dtype_dict)

我曾经对比过:一个500MB的Tick文件,用默认类型读进来占1.2GB内存;优化dtype后只占400MB。差距就是这么大。

注意: 使用dtype时,要确保数据里没有空值或异常值。比如某列声明为int32,但数据里有NaN,就会报错。建议先做数据探查,再定类型。

4.4 实战:分块读取 + 增量处理

真正的回测场景,我们往往不需要一次性加载所有数据。而是按时间窗口滑动处理

比如,我要回测一个基于过去1分钟Tick数据的策略。我可以这样设计:

import pandas as pd
from datetime import timedelta

chunk_size = 10000
window = timedelta(minutes=1)

# 用一个缓冲区来维护滑动窗口
buffer = pd.DataFrame()

for chunk in pd.read_csv('tick_data_20240101.csv', 
                         chunksize=chunk_size,
                         parse_dates=['timestamp'],
                         dtype={'symbol': 'category', 'price': 'float32', 'volume': 'int32'}):
    
    # 将新数据追加到缓冲区
    buffer = pd.concat([buffer, chunk], ignore_index=True)
    
    # 只保留最近1分钟的数据
    latest_time = buffer['timestamp'].max()
    cutoff_time = latest_time - window
    buffer = buffer[buffer['timestamp'] >= cutoff_time]
    
    # 在这里执行你的策略逻辑
    # 比如计算VWAP、订单簿不平衡等
    if len(buffer) > 0:
        vwap = (buffer['price'] * buffer['volume']).sum() / buffer['volume'].sum()
        # ... 策略逻辑 ...
    
    # 可选:定期清理缓冲区,防止无限增长
    if len(buffer) > 100000:
        buffer = buffer.iloc[-50000:]  # 只保留最近5万条

这个模式的好处是:内存占用恒定,不管文件多大,缓冲区大小可控。而且天然支持流式处理,适合实盘模拟。

4.5 知识体系图

下面这张图总结了本章的核心逻辑,我建议你保存下来,以后做数据读取时对照着看:

Tick数据读取策略决策树 CSV Tick数据 小文件 (< 500MB) 大文件 (> 500MB) 直接 read_csv() 优化 dtype + usecols chunksize 分块读取 滑动窗口增量处理 内存可控 · 性能最优 推荐做法 需谨慎处理

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 时间解析慢:Tick数据的时间戳精度到毫秒甚至微秒。用parse_dates=['timestamp']解析很慢。我建议先读成字符串,然后用pd.to_datetime(..., format='%Y-%m-%d %H:%M:%S.%f')指定格式,速度能快5-10倍。
  • CSV引号问题:有些Tick数据里字段带引号,比如"IF2401"。默认read_csv会处理,但如果数据不规范,记得加quoting=csv.QUOTE_NONEescapechar='\\'
  • 空行和注释行:有些数据文件头部有注释行(以#开头)。用comment='#'参数可以自动跳过。
  • 编码问题:国内数据源常见GBK编码。用encoding='gbk'encoding='utf-8',错了会报UnicodeDecodeError。我一般先试UTF-8,不行再换GBK。

好了,数据读取这块就讲到这里。记住一个原则:能少读就少读,能分批就不一次。下一节咱们聊聊数据清洗中最头疼的部分——缺失值和异常值处理。


专注资料整理