查看全课大纲(第 48 / 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入门:从咖啡馆到华尔街

Pandas 是量化交易员最核心的武器。本章你将通过 Series 和 DataFrame 深度处理时间序列行情数据,掌握 loc/iloc 定位筛选、缺失值与重复值清洗、以及最大回撤与年化波动率等核心指标统计计算。

💡 核心导读

本节将带领读者使用 Pandas 对全球食品营养数据进行实战分析。通过本小节的学习,您将掌握:如何加载和清洗真实世界中的复杂数据集、使用分组聚合进行多维度分析、可视化食品营养指标分布,以及构建基础的数据透视表。核心技能包括:缺失值处理技巧、groupby高级用法、pd.cut分箱操作,以及 Seaborn 可视化集成。


数据集准备与清洗

数据加载与初探

我们使用 Open Food Facts 数据集,包含来自全球的食品营养信息:

import pandas as pd
import numpy as np

# 构造示例食品数据,模拟原数据形态
data = {
    'product_name': ['Cookie A', 'Chips B', 'Milk C', 'Bread D', 'Yogurt E', 'Juice F', 'Cereal G', 'Cheese H'],
    'countries_en': ['United States,Canada', 'France,Germany', 'Italy', 'Japan,Korea', 'United Kingdom', 'Australia', 'Spain', 'Brazil'],
    'energy_100g': [2100.5, 1850.2, 650.8, 1200.0, 850.3, 950.7, 1350.1, 1750.9],
    'fat_100g': [25.3, 30.1, 3.5, 15.2, 5.8, 0.5, 8.9, 28.7],
    'carbohydrates_100g': [65.2, 55.8, 48.3, 70.1, 12.5, 23.4, 75.0, 2.1],
    'sugars_100g': [35.5, 2.3, 12.8, 25.6, 9.2, 21.0, 30.5, 1.2],
    'proteins_100g': [5.2, 6.8, 3.5, 8.1, 4.3, 0.8, 10.2, 22.5],
    'salt_100g': [1.2, 1.8, 0.3, 2.1, 0.9, 0.1, 1.5, 1.9],
    'nutriscore_grade': ['d', 'e', 'b', 'c', 'a', 'b', 'c', 'd']
}
food_df = pd.DataFrame(data)

print(f"数据集形状:{food_df.shape}")
print("字段示例:")
print(food_df.iloc[:, :5].columns.tolist())
数据集形状:(8, 9)
字段示例:
['product_name', 'countries_en', 'energy_100g', 'fat_100g', 'carbohydrates_100g']

关键字段清洗

清洗核心分析字段并处理缺失值:

import pandas as pd
import numpy as np

# 重新构造数据,确保本块自包含
data = {
    'product_name': ['Cookie A', 'Chips B', 'Milk C', 'Bread D', 'Yogurt E', 'Juice F', 'Cereal G', 'Cheese H'],
    'countries_en': ['United States,Canada', 'France,Germany', 'Italy', 'Japan,Korea', 'United Kingdom', 'Australia', 'Spain', 'Brazil'],
    'energy_100g': [2100.5, 1850.2, 650.8, 1200.0, 850.3, 950.7, 1350.1, 1750.9],
    'fat_100g': [25.3, 30.1, 3.5, 15.2, 5.8, 0.5, 8.9, 28.7],
    'carbohydrates_100g': [65.2, 55.8, 48.3, 70.1, 12.5, 23.4, 75.0, 2.1],
    'sugars_100g': [35.5, 2.3, 12.8, 25.6, 9.2, 21.0, 30.5, 1.2],
    'proteins_100g': [5.2, 6.8, 3.5, 8.1, 4.3, 0.8, 10.2, 22.5],
    'salt_100g': [1.2, 1.8, 0.3, 2.1, 0.9, 0.1, 1.5, 1.9],
    'nutriscore_grade': ['d', 'e', 'b', 'c', 'a', 'b', 'c', 'd']
}
food_df = pd.DataFrame(data)

cols = [
    'product_name', 'countries_en', 'energy_100g', 
    'fat_100g', 'carbohydrates_100g', 'sugars_100g',
    'proteins_100g', 'salt_100g', 'nutriscore_grade'
]
food_df = food_df[cols].copy()

food_df['nutriscore_grade'] = food_df['nutriscore_grade'].fillna('Unknown')
food_df = food_df.dropna(subset=['energy_100g', 'fat_100g'])

food_df['countries_en'] = food_df['countries_en'].str.split(',').str[0]

print("清洗后数据形状:", food_df.shape)
print("国家字段处理示例:")
print(food_df[['product_name', 'countries_en']].head())

清洗前后数据结构对比:

清洗后数据形状: (8, 9)
国家字段处理示例:
  product_name    countries_en
0     Cookie A   United States
1      Chips B          France
2       Milk C           Italy
3      Bread D           Japan
4     Yogurt E  United Kingdom

营养指标分析实战

国家维度营养对比

使用分组聚合计算各国食品平均营养值:

import pandas as pd
import numpy as np

# 重新构造数据,确保本块自包含
data = {
    'product_name': ['Cookie A', 'Chips B', 'Milk C', 'Bread D', 'Yogurt E', 'Juice F', 'Cereal G', 'Cheese H'],
    'countries_en': ['United States', 'France', 'Italy', 'Japan', 'United Kingdom', 'Australia', 'Spain', 'Brazil'],
    'energy_100g': [2100.5, 1850.2, 650.8, 1200.0, 850.3, 950.7, 1350.1, 1750.9],
    'fat_100g': [25.3, 30.1, 3.5, 15.2, 5.8, 0.5, 8.9, 28.7],
    'carbohydrates_100g': [65.2, 55.8, 48.3, 70.1, 12.5, 23.4, 75.0, 2.1],
    'sugars_100g': [35.5, 2.3, 12.8, 25.6, 9.2, 21.0, 30.5, 1.2],
    'proteins_100g': [5.2, 6.8, 3.5, 8.1, 4.3, 0.8, 10.2, 22.5],
    'salt_100g': [1.2, 1.8, 0.3, 2.1, 0.9, 0.1, 1.5, 1.9],
    'nutriscore_grade': ['d', 'e', 'b', 'c', 'a', 'b', 'c', 'd']
}
food_df = pd.DataFrame(data)

country_stats = (
    food_df.groupby('countries_en')
    .agg({
        'energy_100g': 'mean',
        'fat_100g': ['mean', 'median'],
        'sugars_100g': lambda x: np.percentile(x.dropna(), 75)
    })
    .sort_values(('energy_100g', 'mean'), ascending=False)
    .head(10)
)

print(country_stats.round(1))

终端输出示例:

energy_100g fat_100g        sugars_100g
                      mean     mean median    <lambda>
countries_en
United States       2100.5     25.3   25.3        35.5
France              1850.2     30.1   30.1         2.3
Brazil              1750.9     28.7   28.7         1.2
Spain               1350.1      8.9    8.9        30.5
Japan               1200.0     15.2   15.2        25.6
Australia            950.7      0.5    0.5        21.0
United Kingdom       850.3      5.8    5.8         9.2
Italy                650.8      3.5    3.5        12.8

能量等级分箱分析

使用pd.cut进行能量等级分箱:

import pandas as pd
import numpy as np

# 重新构造数据,确保本块自包含
data = {
    'product_name': ['Cookie A', 'Chips B', 'Milk C', 'Bread D', 'Yogurt E', 'Juice F', 'Cereal G', 'Cheese H'],
    'countries_en': ['United States', 'France', 'Italy', 'Japan', 'United Kingdom', 'Australia', 'Spain', 'Brazil'],
    'energy_100g': [2100.5, 1850.2, 650.8, 1200.0, 850.3, 950.7, 1350.1, 1750.9],
    'fat_100g': [25.3, 30.1, 3.5, 15.2, 5.8, 0.5, 8.9, 28.7],
    'carbohydrates_100g': [65.2, 55.8, 48.3, 70.1, 12.5, 23.4, 75.0, 2.1],
    'sugars_100g': [35.5, 2.3, 12.8, 25.6, 9.2, 21.0, 30.5, 1.2],
    'proteins_100g': [5.2, 6.8, 3.5, 8.1, 4.3, 0.8, 10.2, 22.5],
    'salt_100g': [1.2, 1.8, 0.3, 2.1, 0.9, 0.1, 1.5, 1.9],
    'nutriscore_grade': ['d', 'e', 'b', 'c', 'a', 'b', 'c', 'd']
}
food_df = pd.DataFrame(data)

bins = [0, 800, 1500, 2500, np.inf]
labels = ['低能量', '中能量', '高能量', '超高能量']
food_df['energy_level'] = pd.cut(food_df['energy_100g'], bins=bins, labels=labels)

energy_grade = pd.crosstab(
    food_df['energy_level'],
    food_df['nutriscore_grade'],
    normalize='index'
)

print("能量等级与营养评分交叉表(行归一化):")
print(energy_grade.round(3))
能量等级与营养评分交叉表(行归一化):
nutriscore_grade     a     b    c      d      e
energy_level
低能量               0.00  1.00  0.0  0.000  0.000
中能量               0.25  0.25  0.5  0.000  0.000
高能量               0.00  0.00  0.0  0.667  0.333


可视化实战

营养指标分布矩阵

使用 Seaborn 绘制核心指标关系:

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

# 重新构造数据,确保本块自包含
data = {
    'product_name': ['Cookie A', 'Chips B', 'Milk C', 'Bread D', 'Yogurt E', 'Juice F', 'Cereal G', 'Cheese H'],
    'countries_en': ['United States', 'France', 'Italy', 'Japan', 'United Kingdom', 'Australia', 'Spain', 'Brazil'],
    'energy_100g': [2100.5, 1850.2, 650.8, 1200.0, 850.3, 950.7, 1350.1, 1750.9],
    'fat_100g': [25.3, 30.1, 3.5, 15.2, 5.8, 0.5, 8.9, 28.7],
    'carbohydrates_100g': [65.2, 55.8, 48.3, 70.1, 12.5, 23.4, 75.0, 2.1],
    'sugars_100g': [35.5, 2.3, 12.8, 25.6, 9.2, 21.0, 30.5, 1.2],
    'proteins_100g': [5.2, 6.8, 3.5, 8.1, 4.3, 0.8, 10.2, 22.5],
    'salt_100g': [1.2, 1.8, 0.3, 2.1, 0.9, 0.1, 1.5, 1.9],
    'nutriscore_grade': ['d', 'e', 'b', 'c', 'a', 'b', 'c', 'd']
}
food_df = pd.DataFrame(data)

sns.set(style="ticks", font_scale=0.8)
g = sns.pairplot(
    food_df[['fat_100g', 'sugars_100g', 'proteins_100g', 'nutriscore_grade']],
    hue='nutriscore_grade',
    palette="Set2",
    height=2,
    corner=True
)
g.fig.suptitle("营养指标分布矩阵(按NutriScore分级)", y=1.02)

print("绘图完成(演示用,实际环境中会显示图形)")
plt.close(g.fig)
绘图完成(演示用,实际环境中会显示图形)

国家营养雷达图

展示前5国的营养指标对比:

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from math import pi

# 重新构造聚合统计数据,确保本块自包含
index_data = ['United States', 'France', 'Italy', 'Japan', 'United Kingdom']
multi_col = pd.MultiIndex.from_tuples([
    ('energy_100g', 'mean'),
    ('fat_100g', 'mean'),
    ('fat_100g', 'median'),
    ('sugars_100g', '<lambda>')
])
data_values = [
    [2100.5, 25.3, 25.3, 35.5],
    [1850.2, 30.1, 30.1, 2.3],
    [650.8, 3.5, 3.5, 12.8],
    [1200.0, 15.2, 15.2, 25.6],
    [850.3, 5.8, 5.8, 9.2]
]
country_stats = pd.DataFrame(data_values, index=index_data, columns=multi_col)

radar_df = country_stats.head(5).copy()
categories = radar_df.columns.get_level_values(0).unique()

radar_df = (radar_df - radar_df.min()) / (radar_df.max() - radar_df.min())

fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, polar=True)
for idx, country in enumerate(radar_df.index):
    values = radar_df.loc[country].values.flatten().tolist()
    values += values[:1]  # 闭合图形
    ax.plot(np.linspace(0, 2*pi, len(values)), values, label=country)

ax.set_xticks(np.linspace(0, 2*pi, len(categories)))
ax.set_xticklabels(categories)
plt.legend()
plt.title("五国核心营养指标对比(标准化)")

print("雷达图绘制完成(演示用,实际环境中会显示图形)")
plt.close(fig)
雷达图绘制完成(演示用,实际环境中会显示图形)

📝 动手练一练

第 1 题 · 概念应用

你正在分析食品营养数据集 df,其中每一行是一款食品,含 energy_100g(每 100 克能量,千焦)与 sodium_100g(每 100 克钠含量,毫克)两列。你想找出高能量(> 1700)且低钠(< 100)的食品,用于后续的标签分析。以下哪种写法是正确的?

  • A. df[df['energy_100g'] > 1700 and df['sodium_100g'] < 100]
  • B. df[(df['energy_100g'] > 1700) & (df['sodium_100g'] < 100)]
  • C. df[(df['energy_100g'] > 1700) | (df['sodium_100g'] < 100)]
  • D. df[df['energy_100g'] > 1700 & df['sodium_100g'] < 100]
👉 点击查看参考答案

答案:B

推理过程:

  • 选项 A 错误:Python 的 and 关键字只能作用于单个布尔值,无法对两个布尔 Series 逐元素做“与”运算,会抛出真值不明确的 ValueError;向量化的逻辑与必须用 &。
  • 选项 B 正确:两个比较各自生成布尔 Series,外层加括号后用 & 逐元素求与,恰好筛出“高能量且低钠”的行。括号不能省,因为 & 的优先级高于比较运算符。
  • 选项 C 错误:| 是逐元素“或”,会把“高能量但不低钠”和“低钠但不高能量”的食品也选进来,不符合“且”的要求。
  • 选项 D 错误:缺少括号时 & 先与相邻数字结合,被解释成 energy_100g > (1700 & df['sodium_100g']) < 100 这类错误结构,要么直接报错要么得到无意义结果。

第 2 题 · 代码填空

将连续的数值指标(如能量值 energy_100g)转换为离散等级后,分组统计会方便很多。下面的代码把食品按每 100 克能量划分为“低”、“中”、“高”三个等级。请补全完成分箱操作的函数名。

import pandas as pd
import numpy as np

# 示例数据
data = {
    'product': ['Cookie', 'Chips', 'Milk', 'Bread', 'Yogurt'],
    'energy_100g': [2100, 1850, 650, 1200, 850]
}
df = pd.DataFrame(data)

# 定义分箱边界和标签
bins = [0, 800, 1500, np.inf]
labels = ['低', '中', '高']

# 使用 pd.cut 进行分箱操作
df['energy_level'] = ______(df['energy_100g'],
                            bins=bins,
                            labels=labels,
                            include_lowest=True)

print(df[['product', 'energy_100g', 'energy_level']])
👉 点击查看参考答案

空位应填:pd.cut

完整可运行代码:

import pandas as pd
import numpy as np

# 示例数据
data = {
    'product': ['Cookie', 'Chips', 'Milk', 'Bread', 'Yogurt'],
    'energy_100g': [2100, 1850, 650, 1200, 850]
}
df = pd.DataFrame(data)

# 定义分箱边界和标签
bins = [0, 800, 1500, np.inf]
labels = ['低', '中', '高']

# 使用 pd.cut 进行分箱操作
df['energy_level'] = pd.cut(df['energy_100g'],
                            bins=bins,
                            labels=labels,
                            include_lowest=True)

print(df[['product', 'energy_100g', 'energy_level']])

运行结果:

  product  energy_100g energy_level
0  Cookie         2100            高
1   Chips         1850            高
2    Milk          650            低
3   Bread         1200            中
4  Yogurt          850            中

解释:

  • 空位填 pd.cut:它按给定边界把连续值切分为若干区间,并可通过 labels 直接贴上分类标签(需要“等频分箱”时则改用 pd.qcut)。
  • bins=[0, 800, 1500, np.inf] 与 include_lowest=True 把区间定为 [0, 800]、(800, 1500]、(1500, +∞),依次对应低、中、高;所以能量 650 的牛奶落入“低”,850 的酸奶落入“中”。
  • 分箱后的列是 category 类型,既方便做 groupby 等级统计,也比连续值更适合做报告中的分层对比。

本章小结

通过本实战练习,我们完成了从原始数据加载到多维分析的完整流程。关键操作包括:

  1. 使用low_memory=False处理大型CSV文件
  2. 通过groupby+agg实现多维度聚合
  3. 利用pd.cut进行智能分箱
  4. 结合Seaborn实现高级可视化

📋 行动清单

  • 尝试修改pd.cut的分箱边界,观察能量等级分级对NutriScore分布的影响
  • 使用pivot_table重构国家营养数据,比较与groupby结果的差异
  • 为雷达图添加新的营养指标(如蛋白质或膳食纤维),重新分析国家特征

—— 小象教研组