查看全课大纲(第 15 / 52 节)
  1. 1.量化分析基本概念:从咖啡馆到华尔街
  2. 2.量化交易员学习路线:从零基础到策略开发
  3. 3.量化策略生命周期:数据分析的五大流程
  4. 4.量化数据类型:数值型与类别型数据
  5. 5.量化沙盒搭建(1):Python与Anaconda安装
  6. 6.量化沙盒搭建(2):Jupyter Notebook与库配置
  7. 7.本地多源数据:量化历史回测的基石
  8. 8.TXT数据流操作:处理金融交易日志
  9. 9.JSON数据流操作:解析股票行情API响应
  10. 10.CSV行情文件操作:读取与存储历史K线
  11. 11.Excel报表操作:清洗财报与财务指标
  12. 12.量化数据库:SQL常用查询语法
  13. 13.量化数据库:Python连接与写入
  14. 14.量化多表连接:关联股票信息与行情
  15. 15.实战:小象咖啡店营业数据与个股财务指标的多源数据分析
  16. 16.量化数据采集:爬虫工作原理与合规
  17. 17.量化爬虫核心:URL管理与去重策略
  18. 18.量化网页下载:防反爬与请求头伪装
  19. 19.网页解析(1):使用XPath定位财经新闻
  20. 20.网页解析(2):使用CSS选择器与ItemLoader
  21. 21.Scrapy框架:构建工业级量化爬虫
  22. 22.Scrapy实战(1):新建项目与数据模型定义
  23. 23.Scrapy实战(2):编写财经爬虫逻辑
  24. 24.Scrapy实战(3):启用数据管道清洗行情
  25. 25.Scrapy实战(4):防反爬中间件配置
  26. 26.实战:构建每日热门个股行情自动采集系统
  27. 27.NumPy与SciPy:加速量化矩阵计算
  28. 28.NumPy数组:构建股票价格矩阵
  29. 29.数组切片:提取特定股票与交易日数据
  30. 30.NumPy常用方法:计算均值与波动率
  31. 31.向量化计算:摆脱缓慢的For循环
  32. 32.通用函数ufunc:计算对数收益率与累计收益
  33. 33.实战:股票组合资产收益率与协方差矩阵计算
  34. 34.Pandas Series:管理单只股票收盘价
  35. 35.Pandas DataFrame:构建多只股票行情面板
  36. 36.Pandas Index:时间序列索引与对齐
  37. 37.DataFrame索引操作:loc与iloc定位行情
  38. 38.索引总结:量化多字段筛选与定位
  39. 39.运算与对齐:合并不同交易日的数据
  40. 40.Pandas map:对股票代码与状态做映射
  41. 41.Pandas apply:滚动计算技术指标
  42. 42.排序操作:按市盈率与收益率对股票排名
  43. 43.数据清洗:填充与剔除停牌股票缺失值
  44. 44.数据清洗:剔除重复报价与异常交易数据
  45. 45.常用统计:量化最大回撤与分位数风险评估
  46. 46.极值定位:寻找历史最高价与最低价的发生日
  47. 47.风险与累计收益:计算年化波动率与累计回报
  48. 48.实战:小象咖啡店营业数据与热门个股时间序列清洗分析
  49. 49.Series索引操作:按位置与标签访问行情数据
  50. 50.文件读写操作:CSV行情数据导入导出
  51. 51.数据清洗:替换异常报价数据
  52. 52.常用统计:sum/mean/median/count聚合指标

实战:小象咖啡店营业数据与个股财务指标的多源数据分析

约 12 分钟

实战:欧洲职业足球比赛数据的多源数据分析

小象实战讲义 · 量化交易Python入门:从咖啡馆到华尔街

在量化交易中,历史回测数据往往以多种异构本地文件(TXT、CSV、JSON、Excel)和关系数据库形式存在。本章你将编写数据接口,读取小象咖啡的门店营收数据并解析股票行情API响应,打通量化回测的数据通道!

💡 核心导读

本节将带领读者完成欧洲职业足球数据的采集、清洗与分析全流程。通过Python数据科学工具链操作本地存储的SQLite数据库与CSV文件,掌握多源数据合并、特征工程与可视化分析技巧。核心技能包括:1) 使用Pandas进行多表关联查询 2) 构建比赛结果预测特征 3) 制作热力图展示球队表现矩阵。


数据准备与环境配置

数据库连接与探查

import sqlite3
import pandas as pd
from pathlib import Path

# 在内存中创建数据库并填充示例数据
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

# 创建示例表结构并插入少量数据
cursor.execute('''
CREATE TABLE Match (
    match_id INTEGER PRIMARY KEY,
    season TEXT,
    home_team_api_id INTEGER,
    away_team_api_id INTEGER,
    home_team_goal INTEGER,
    away_team_goal INTEGER,
    B365H REAL,
    B365D REAL,
    B365A REAL,
    league_id INTEGER
)
''')

matches_data = [
    (1, '2015/2016', 8634, 10281, 2, 1, 1.85, 3.60, 4.20, 1),
    (2, '2015/2016', 9991, 9993, 1, 1, 2.10, 3.30, 3.50, 1),
    (3, '2015/2016', 8635, 10282, 3, 0, 1.50, 4.00, 6.50, 2),
    (4, '2014/2015', 8634, 10281, 1, 2, 2.00, 3.40, 3.80, 1),
]
cursor.executemany('INSERT INTO Match VALUES (?,?,?,?,?,?,?,?,?,?)', matches_data)

cursor.execute('''
CREATE TABLE Team (
    team_api_id INTEGER PRIMARY KEY,
    team_long_name TEXT
)
''')

teams_data = [
    (8634, 'Real Madrid CF'),
    (10281, 'FC Barcelona'),
    (9991, 'Manchester United'),
    (9993, 'Chelsea'),
    (8635, 'FC Bayern Munich'),
    (10282, 'Borussia Dortmund'),
]
cursor.executemany('INSERT INTO Team VALUES (?,?)', teams_data)

conn.commit()

# 查询表名
tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)
print(tables)
name
0  Match
1   Team

多源数据加载

import pandas as pd
import sqlite3

# 重新建立连接并查询
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

# 重建Match表和数据
cursor.execute('''
CREATE TABLE Match (
    match_id INTEGER PRIMARY KEY,
    season TEXT,
    home_team_api_id INTEGER,
    away_team_api_id INTEGER,
    home_team_goal INTEGER,
    away_team_goal INTEGER,
    B365H REAL,
    B365D REAL,
    B365A REAL,
    league_id INTEGER
)
''')
matches_data = [
    (1, '2015/2016', 8634, 10281, 2, 1, 1.85, 3.60, 4.20, 1),
    (2, '2015/2016', 9991, 9993, 1, 1, 2.10, 3.30, 3.50, 1),
    (3, '2015/2016', 8635, 10282, 3, 0, 1.50, 4.00, 6.50, 2),
]
cursor.executemany('INSERT INTO Match VALUES (?,?,?,?,?,?,?,?,?,?)', matches_data)

# 查询比赛数据
matches = pd.read_sql("SELECT * FROM Match WHERE season='2015/2016'", conn)

# 构造球员评分数据(模拟读取CSV)
ratings_data = {
    'player_id': [101, 102, 103],
    'player_name': ['Lionel Messi', 'Cristiano Ronaldo', 'Robert Lewandowski'],
    'overall_rating': [94, 93, 91]
}
ratings = pd.DataFrame(ratings_data)

print("比赛数据字段示例:")
print(matches[['home_team_api_id', 'away_team_api_id', 'home_team_goal']].head(2))
print("\n球员评分数据示例:")
print(ratings.head(2))
比赛数据字段示例:
   home_team_api_id  away_team_api_id  home_team_goal
0              8634             10281               2
1              9991              9993               1

球员评分数据示例:
   player_id        player_name  overall_rating
0        101       Lionel Messi              94
1        102  Cristiano Ronaldo              93

核心分析流程

数据关联与特征工程

import pandas as pd
import sqlite3

# 重新建立连接并查询
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

# 重建Match表和数据
cursor.execute('''
CREATE TABLE Match (
    match_id INTEGER PRIMARY KEY,
    season TEXT,
    home_team_api_id INTEGER,
    away_team_api_id INTEGER,
    home_team_goal INTEGER,
    away_team_goal INTEGER,
    B365H REAL,
    B365D REAL,
    B365A REAL,
    league_id INTEGER
)
''')
matches_data = [
    (1, '2015/2016', 8634, 10281, 2, 1, 1.85, 3.60, 4.20, 1),
    (2, '2015/2016', 9991, 9993, 1, 1, 2.10, 3.30, 3.50, 1),
    (3, '2015/2016', 8635, 10282, 3, 0, 1.50, 4.00, 6.50, 2),
]
cursor.executemany('INSERT INTO Match VALUES (?,?,?,?,?,?,?,?,?,?)', matches_data)

# 重建Team表和数据
cursor.execute('''
CREATE TABLE Team (
    team_api_id INTEGER PRIMARY KEY,
    team_long_name TEXT
)
''')
teams_data = [
    (8634, 'Real Madrid CF'),
    (10281, 'FC Barcelona'),
    (9991, 'Manchester United'),
    (9993, 'Chelsea'),
    (8635, 'FC Bayern Munich'),
    (10282, 'Borussia Dortmund'),
]
cursor.executemany('INSERT INTO Team VALUES (?,?)', teams_data)
conn.commit()

# 查询数据
matches = pd.read_sql("SELECT * FROM Match WHERE season='2015/2016'", conn)
teams = pd.read_sql("SELECT * FROM Team", conn)

# 数据增强
enhanced_matches = matches.merge(
    teams[['team_api_id', 'team_long_name']],
    left_on='home_team_api_id',
    right_on='team_api_id'
).rename(columns={'team_long_name': 'home_team_name'})

enhanced_matches['result'] = enhanced_matches.apply(
    lambda row: 'home_win' if row['home_team_goal'] > row['away_team_goal'] else 
               ('draw' if row['home_team_goal'] == row['away_team_goal'] else 'away_win'),
    axis=1
)

features = enhanced_matches[[
    'home_team_name', 
    'B365H',
    'B365D',
    'B365A',
    'result'
]].dropna()

print("增强后的比赛数据特征:")
print(features)
增强后的比赛数据特征:
      home_team_name  B365H  B365D  B365A    result
0     Real Madrid CF   1.85    3.6    4.2  home_win
1  Manchester United   2.10    3.3    3.5      draw
2   FC Bayern Munich   1.50    4.0    6.5  home_win

可视化分析

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

# 构造比赛数据
matches_data = {
    'match_id': [1, 2, 3, 4, 5],
    'league_id': [1, 1, 2, 2, 1],
    'home_team_goal': [2, 1, 3, 0, 2],
    'away_team_goal': [1, 1, 0, 2, 0]
}
matches = pd.DataFrame(matches_data)
matches['total_goals'] = matches['home_team_goal'] + matches['away_team_goal']

plt.figure(figsize=(12,6))
sns.boxplot(
    x='league_id',
    y='total_goals',
    data=matches
)
plt.title('2015/2016赛季各联赛进球数分布')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
plt.close()


高级分析技巧

球队对战热力图

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

# 构造增强的比赛数据
data = {
    'home_team_name': ['Real Madrid CF', 'Manchester United', 'FC Bayern Munich', 
                       'Real Madrid CF', 'Manchester United'],
    'away_team_name': ['FC Barcelona', 'Chelsea', 'Borussia Dortmund', 
                       'Chelsea', 'Borussia Dortmund'],
    'home_team_goal': [2, 1, 3, 1, 2],
    'away_team_goal': [1, 1, 0, 0, 1]
}
enhanced_matches = pd.DataFrame(data)

# 计算主队场均进球对阵表现
team_pairs = enhanced_matches.groupby([
    'home_team_name', 
    'away_team_name'
])['home_team_goal'].mean().unstack()

# 填充缺失值为0用于热力图
team_pairs_filled = team_pairs.fillna(0)

plt.figure(figsize=(10, 8))
sns.heatmap(
    team_pairs_filled,
    cmap='YlOrRd',
    annot=True,
    fmt='.1f',
    linewidths=.5,
    cbar_kws={'label': '场均进球'}
)
plt.title('主队场均进球对阵表现')
plt.tight_layout()
plt.show()
plt.close()

比赛结果预测

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

# 构造特征数据
features_data = {
    'home_team_name': ['Real Madrid CF', 'Manchester United', 'FC Bayern Munich', 
                       'Real Madrid CF', 'Manchester United'],
    'B365H': [1.85, 2.10, 1.50, 1.90, 2.05],
    'B365D': [3.60, 3.30, 4.00, 3.50, 3.40],
    'B365A': [4.20, 3.50, 6.50, 4.00, 3.60],
    'result': ['home_win', 'draw', 'home_win', 'home_win', 'home_win']
}
features = pd.DataFrame(features_data)

X = features[['B365H', 'B365D', 'B365A']]
y = features['result']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=42)

model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
accuracy = model.score(X_test, y_test)
print(f"模型准确率:{accuracy:.2%}")
模型准确率:50.00%

📝 动手练一练

第 1 题 · 概念应用

在量化策略开发中,你正在构建一个多因子模型。其中一个因子是“动量因子”,定义为过去 20 个交易日的累计收益率。你从两个数据源获取数据:price_df(CSV 文件,包含每日收盘价)和 benchmark_df(SQLite 数据库查询结果,包含每日基准指数收益率)。你需要计算个股动量因子与基准动量因子的相关性。

以下关于数据对齐和处理的步骤描述中,哪一项最合理?

  • A. 由于两个数据源都包含日期,可以直接使用 pd.concat([price_df, benchmark_df], axis=1) 进行横向拼接,然后计算相关性。
  • B. 先把两个 DataFrame 的日期列统一转换为 datetime 类型,再使用 pd.merge(price_df, benchmark_df, on='date', how='inner') 进行内连接对齐,确保分析基于同一组交易日。
  • C. 基准在周末没有数据,就用上周五的基准收益率填充周末,并把这些填充出来的周末行当作真实交易日参与 20 日累计收益率的计算。
  • D. 计算 20 日累计收益率时,应使用原始价格序列直接计算,即使中间有缺失的交易日(如停牌),也无需进行填充或特殊处理。
👉 点击查看参考答案

答案:B

推理过程:

  • A 错误:pd.concat 默认按索引拼接,如果两个 DataFrame 的索引(日期)顺序或数量不完全一致,会错误地对齐数据,导致“日期错位”问题,相关性计算无效。
  • B 正确:统一为 datetime 后再用 inner 连接,只保留两个数据源共有的交易日,这是同期相关性分析的标准流程(同格式的日期字符串也能合并,但先转 datetime 更稳妥、便于后续时间序列运算)。
  • C 错误:把上周五的收益率填到周末、再把这些并不存在的交易日当作真实样本计入 20 日累计收益率,等于人为伪造了交易数据,会系统性扭曲因子与相关性。正确做法是先统一到交易日频率(如 .asfreq('B')),只在真实交易日上对齐。
  • D 错误:股票停牌会导致价格序列出现缺失值,如果不处理(如向前填充),计算出的收益率序列会出现 NaN,进而影响后续相关性计算。通常需要对缺失价格进行填充(如停牌期间价格不变),或确保在计算收益率时,使用的价格序列是连续的。

💡 小象贴士:在量化分析中,确保不同数据源在时间维度上的严格对齐是避免“前视偏差”的关键一步。错误的日期对齐会使得回测结果完全失真。


第 2 题 · 代码填空

在分析中,你需要将球队信息表 (team_df) 与比赛数据表 (match_df) 进行关联,以获取主队和客队的完整名称。已知两个表的关联键名称不同:match_df 中使用 home_id 和 away_id,而 team_df 中使用 team_id 和 name。

请补全下面代码中 pd.merge 函数的参数,实现正确的关联,生成包含 home_team_name 和 away_team_name 的新 DataFrame enhanced_df。

import pandas as pd

# 模拟比赛数据
match_data = {
    'match_id': [1, 2],
    'home_id': [8634, 9991],
    'away_id': [10281, 9993],
    'home_goals': [2, 1],
    'away_goals': [1, 1]
}
match_df = pd.DataFrame(match_data)

# 模拟球队数据
team_data = {
    'team_id': [8634, 10281, 9991, 9993],
    'name': ['Real Madrid CF', 'FC Barcelona', 'Manchester United', 'Chelsea']
}
team_df = pd.DataFrame(team_data)

# 关联获取主队名称
enhanced_df = pd.merge(match_df, team_df, left_on=______, right_on='team_id')
enhanced_df = enhanced_df.rename(columns={'name': 'home_team_name'})

# 关联获取客队名称
enhanced_df = pd.merge(enhanced_df, team_df, left_on=______, right_on='team_id')
enhanced_df = enhanced_df.rename(columns={'name': 'away_team_name'})

# 选择并展示关键列
result = enhanced_df[['match_id', 'home_team_name', 'away_team_name', 'home_goals', 'away_goals']]
print(result)
👉 点击查看参考答案

空位应填:‘home_id’ 和 ‘away_id’

补全后的完整可运行代码:

import pandas as pd

# 模拟比赛数据
match_data = {
    'match_id': [1, 2],
    'home_id': [8634, 9991],
    'away_id': [10281, 9993],
    'home_goals': [2, 1],
    'away_goals': [1, 1]
}
match_df = pd.DataFrame(match_data)

# 模拟球队数据
team_data = {
    'team_id': [8634, 10281, 9991, 9993],
    'name': ['Real Madrid CF', 'FC Barcelona', 'Manchester United', 'Chelsea']
}
team_df = pd.DataFrame(team_data)

# 关联获取主队名称
enhanced_df = pd.merge(match_df, team_df, left_on='home_id', right_on='team_id')
enhanced_df = enhanced_df.rename(columns={'name': 'home_team_name'})

# 关联获取客队名称
enhanced_df = pd.merge(enhanced_df, team_df, left_on='away_id', right_on='team_id')
enhanced_df = enhanced_df.rename(columns={'name': 'away_team_name'})

# 选择并展示关键列
result = enhanced_df[['match_id', 'home_team_name', 'away_team_name', 'home_goals', 'away_goals']]
print(result)
match_id     home_team_name away_team_name  home_goals  away_goals
0         1     Real Madrid CF   FC Barcelona           2           1
1         2  Manchester United        Chelsea           1           1

本章小结

📋 行动清单

  • 在本地SQLite数据库中执行跨表查询,合并Match表与Team表的主客场球队名称
  • 使用Seaborn绘制两支特定球队的历史对战进球数折线图
  • 修改预测模型,加入球队近期比赛结果作为时序特征

—— 小象教研组