01 数据的管理与探索

本章会用到的数据

  • 公司信息公司基本信息

  • 财务数据财务报表

  • 本地缓存:首次运行时下载,并保存到 data/course/

  • 这些数据能做什么:认识公司、财务指标、日期和表格结构,并练习筛选、汇总与可视化。

  • 分析时注意:财务信息从 info_date 起才视为可用,不能用尚未披露的数据解释更早的决策。

  • 示例说明:模拟数据只用于解释方法,不代表真实市场表现。

【课堂核心】2 学时学习安排

学习内容 分钟
动机与先修 15
概念与公式 25
跟做示例 30
形成性检查 15
独立任务与反馈 25
本章小结 10

【可选拓展】:数据库史、完整图库、境外/生成数据机制例与额外 API 示例,安排课后阅读,不占核心时间。

今日议题:我们如何从数据海洋中淘金?

我们的核心挑战,是如何不被海量数据淹没,并从中提取出能够驱动决策的 α (Alpha) 信号。

从数据噪声中识别金融信号原始数据经过筛选与验证后形成可用于决策的信号。 原始数据行情 · 财报 · 文本清洗与验证信息时点 · 样本外α

基本数据说明:七项信息缺一不可

说明项 作用与最小记录
文件、key 确定数据文件与逻辑数据表,避免读取同名异表
字段、单位 确定变量含义与量纲,防止元/万元或价格/收益率混用
频率、样本期 确定观察粒度与覆盖范围,防止日频、季频或区间错配
可得日 记录信息真正可用于决策的日期,而不是它描述的财年

反例:财年早于决策日,不等于信息已经可得。

  • 决策时点:研究者在 2024-03-31 做预测。
  • 实际披露:2023 年报中的该字段到 2024-04-30 才披露。
  • 错误性质:可得日晚于决策日,构成前瞻偏差。
  • 修正方法:删除该字段,或把决策日推迟到披露之后。

本章目标:掌握数据分析的“三板斧”

今天课程结束时,你将掌握从原始数据中提取价值的核心三步。这三步构成了我们数据分析步骤的基石。

1. 理解数据存储

辨析不同存储介质与文件格式的优劣,为数据选择合适的“家”。

2. 掌握数据操纵

使用 Pandas 对数据进行读取、清洗、筛选、聚合与合并。

3. 实现数据可视化

Seaborn 将枯燥的数据转化为直观、有洞察力的图表。

本章三项学习目标依次学习数据存储、数据操纵与数据可视化。 数据存储格式 · 容量 · 读取数据操纵清洗 · 筛选 · 合并数据表达图表 · 口径 · 结论

金融数据管理的挑战与机遇

金融数据不仅量大,而且复杂,这既是挑战也是机遇。高效的数据管理能力是区分业余和专业的关键。

挑战

  • 如何高效地存储、读取和处理 TB 甚至 PB 级别的数据?
  • 如何保证数据质量,避免“垃圾进,垃圾出”?
  • 如何在噪音中识别出微弱的信号?

机遇

  • 投资决策: 股市指标预测公司基本面。
  • 风险管理: 银行转账信息评估信用风险。
  • 监管科技: 识别异常交易模式。

本章学习路线图

我们将遵循一条从理论到工具,再到实践的清晰路径,确保你系统地掌握本章知识。

Chapter Roadmap 路线图依次展示理论、工具与实践三个学习阶段。 1. 理论基础 存储 & 格式 2. 核心工具 Pandas & Seaborn 3. 动手实践 数据探索 & 可视化

第一部分:数据住在哪里?

分析师的困境:速度 vs. 空间

在数据分析中,我们始终在两个核心存储设备之间进行权衡:内存 (RAM)硬盘 (Hard Drive/SSD)。理解它们的区别,是高效处理数据的第一步。

一个生动的类比:工作台 vs. 仓库

将内存和硬盘想象成你的工作台仓库,这能帮助你直观地理解它们各自的角色、优缺点和协同工作的模式。

内存与持久存储的取舍内存速度快但容量有限,持久存储容量大但读取较慢,数据分析需要在两者之间分块交换。 内存 RAM速度快 · 容量有限硬盘 SSD/HDD容量大 · 读取较慢按需读取 · 分块处理 · 及时释放

内存与硬盘的协同步骤

数据分析的典型步骤完美体现了二者的协同:从硬盘(仓库)中调取数据到内存(工作台)进行处理,然后将结果存回硬盘。

Data Workflow between RAM and HDD 流程图展示数据从硬盘长期存储进入内存处理,再把结果写回硬盘的分析过程。 硬盘 (仓库) 长期存储海量数据 内存 (工作台) 高速计算与分析 2. 读入数据进行分析 4. 将结果写回保存 1. 原始数据 3. 程序执行

内存 vs. 硬盘:核心特性对比

特性 内存 (RAM) 硬盘 (Hard Drive/SSD)
角色 工作区 (Workspace) 存储区 (Storage)
速度 极快 相对较慢
容量 较小 (8GB - 64GB) 巨大 (256GB - 数TB)
持久性 易失性 (断电即清空) 非易失性 (长期保存)
成本 每GB成本高 每GB成本低

数据格式:为数据选择合适的“容器”

数据不仅要选择存储设备,还要选择合适的文件格式。这决定了数据的结构、性能和通用性。我们将重点关注四种主流格式。

Four Key Data Formats 四个图标分别表示 CSV、XLSX、JSON 与 Parquet 数据格式。 CSV 通用文本 XLSX 电子表格 { } 网络数据 [ C ] 大数据

文件格式 1: CSV (逗号分隔值)

  • 结构: 纯文本,以逗号分隔值的表格数据。
  • 优点: 格式简单,人类可读性高,通用性极强。
  • 缺点: 无数据类型信息,处理大文件时读写慢,占用空间较大。
  • 金融用例: 分享小规模的日度收盘价数据。

文件格式 2: XLSX (Excel 工作簿)

  • 结构: 微软Excel的专有格式,支持多工作表、公式、图表。
  • 优点: 商业分析中最常用,支持丰富的数据类型和格式化。
  • 缺点: 格式复杂导致读写较慢,非纯文本,依赖特定库。
  • 金融用例: 制作需要格式化和图表的财务报告。

文件格式 3: JSON (JavaScript 对象表示法)

  • 结构: 纯文本,基于键值对的层次化数据结构。
  • 优点: 结构灵活清晰,人类可读性高,是网络API数据交换的标准。
  • 缺点: 对于大型表格数据,冗余信息多,效率不高。
  • 金融用例: 从券商API获取账户信息、订单状态等嵌套数据。

文件格式 4: Parquet (列式存储)

  • 结构: 二进制,列式存储,专为大数据分析设计。
  • 优点: 读写速度极快(特别是读取特定列时),压缩率高,自带数据类型。
  • 缺点: 二进制格式,人类不可直接阅读。
  • 金融用例: 存储海量历史高频交易数据、行情数据版本。

什么是“列式存储”?

行式存储(如CSV)像读书一样,一行一行存数据。列式存储(如Parquet)像查字典一样,一列一列存数据,这使其在分析查询时效率极高。

行式与列式存储查询比较左侧行式存储为计算价格均值读取每行全部字段,右侧列式存储只扫描价格列,用高亮数据块对比分析查询的I/O范围。 行式存储 (Row-Oriented) 分析查询: 计算平均价格 SELECT AVG(Price) 硬盘读取操作 (Disk Read) ... 效率低下 必须读取包含无关列的整行数据 列式存储 (Columnar) 分析查询: 计算平均价格 SELECT AVG(Price) 硬盘读取操作 (Disk Read) 非常高效 只需精确读取“价格”列的数据

四种数据格式的横向比较

文件类型 数据结构 读/写速度 (大数据) 人类可读性 典型用例
CSV 平面表格 高 (文本) 简单表格数据交换
XLSX 电子表格 中 (需软件) 商业、财务报告
JSON 层次化/键值对 高 (文本) 网络API、配置文件
Parquet 列式存储 极快 低 (二进制) 大数据分析、高效存储

第二部分:我们的核心工具

我们的核心工具(一): Pandas 简介

Pandas 用带行列标签的 DataFrame 串起一条最小可检查结果程:

操作 核心表达 必查证据
选择 df.loc[rows, columns] 行筛选条件、字段清单与样本数
清洗 dropnaastype、主键去重 缺失率、类型、重复键与行数变化
聚合 groupby(...).agg(...) 分组键、统计量、分母与单位

合并时再用 merge(..., validate='one_to_one') 声明键关系;任何重复键、未匹配或意外增删行都必须先停止并修正。

Pandas 的核心:DataFrame 对象

一个DataFrame是一个二维的、大小可变的、异构的表格数据结构,带有标记的轴(行和列)。

Anatomy of a Pandas DataFrame 示意图把 DataFrame 分为索引、列名与数据区三个组成部分。 600104 150.5 1.2M 600276 2730.1 800K 603195 780.0 2.5M Ticker Price Volume 0 1 2 Columns (列) Index (索引) Data (数据)

Pandas 的核心优势:直观且强大

  • 标签对齐

    操作自动按行和列的标签对齐,避免了手动管理的麻烦。

  • 轻松处理缺失值

    提供了简单易用的方法来处理 NaN (Not a Number)。

  • 功能丰富

    groupby, merge, pivot_table 等高级功能让复杂的数据操作变得简单。

  • 生态系统

    Matplotlib, Seaborn, Scikit-learn 等库无缝集成。

我们也必须了解Pandas的局限性

了解工具的边界同样重要。

1. 默认内存步骤的限制

  • 默认行为:eager DataFrame 步骤通常把当前分区物化到内存。
  • 失败条件:单次全量读取超过可用内存时会失败。
  • 本章策略:使用 HDF5 选择性读取、chunksize 分块或流式迭代。
  • 规模继续上升:转向第 18 章的分区并行框架。
Out of Memory Error大数据集无法装入容量较小内存的示意图。 50GB Dataset vs. 16GB RAMRAM (16GB)Data (50GB)内存溢出

2. 非原生SQL支持

Pandas 本身不执行SQL查询。要在DataFrame上使用SQL语法,需要借助 pandasql 等第三方库。

我们的核心工具(二): Matplotlib 简介

Matplotlib 是 Python 数据可视化的奠基者

  • 定位: 一个功能强大但相对底层的绘图库,提供了对图表每个元素的完全控制能力。
  • 优点: 灵活性极高,应用广泛,图表类型丰富。
  • 类比: Matplotlib 就像是画家的画笔和颜料,你可以画出任何你想要的东西,但这需要你手动调配。

我们的核心工具(三): Seaborn 简介

Seaborn 是基于 Matplotlib 的高级统计数据可视化库

  • 定位: 专注于绘制美观且信息丰富的统计图表。
  • 优点: 高级接口,美观默认值,与Pandas完美集成。
  • 类比: Seaborn 就像一个专业的图表设计师,你告诉他数据和主题,他就能迅速生成专业、美观的图表。

Matplotlib vs. Seaborn

Matplotlib vs. Seaborn Analogy 类比图将 Matplotlib 比作可完全控制的调色板,将 Seaborn 比作快速统一样式的设计模板。 Matplotlib 画家的颜料盘:完全控制 Seaborn 设计师的模板:快速专业

第三部分:Pandas 实战演练

实战场景

我们将通过一个实际案例,一步步学习Pandas的核心操作。

场景: 我们有一份虚拟的投资者信息数据,包含ID、收入、性别和股票投资组合金额。

目标: 对这份数据进行读取、检查、筛选、扩展、聚合、合并,并最终保存。

步骤0: 准备工作环境和数据

在开始之前,我们需要导入Pandas库,并创建一个模拟的数据文件 income_stock.csv

为了保证结果可复现,我们使用 np.random.seed(42) 固定随机数。

代码
# 为“步骤0: 准备工作环境和数据”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
# 为“步骤0: 准备工作环境和数据”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“步骤0: 准备工作环境和数据”导入 `os`,用于检查练习文件是否存在。
import os

# 为“步骤0: 准备工作环境和数据”,设置随机种子以保证结果可复现
np.random.seed(42)

# 构造十名教学用户的固定字段字典,供 DataFrame 创建、文件读写与合并机制演示。
data = {
    # 生成十个唯一用户编号,作为两张演示表的合并键。
    'id': [f'i_{i+1}' for i in range(10)],
    # 生成十个整数收入值,用于筛选与分组统计演示。
    'income': np.random.randint(25000, 500000, 10),
    # 固定十条性别标签,保证分组与条件筛选都有样本。
    'gender': ['F', 'M', 'M', 'F', 'M', 'F', 'M', 'F', 'F', 'M'],
    # 生成十个持股金额,用于计算持股收入比。
    'stock': np.random.uniform(15000, 250000, 10)
}
# 将用户字段整理为收入与持股演示表。
df_demo = pd.DataFrame(data)

# 构造同一用户主键对应的 GPA 字典,作为后续一对一合并的右表输入。
gpa_data = {
    # 复用相同十个用户编号,使 GPA 可按编号并入主表。
    'id': [f'i_{i+1}' for i in range(10)],
    # 生成两位小数 GPA,作为右表待合并字段。
    'gpa': np.round(np.random.uniform(2.0, 4.0, 10), 2)
}
# 将用户编号与 GPA 整理为合并演示右表。
df_gpa_demo = pd.DataFrame(gpa_data)

# 仅在输出目录缺失时创建目录,避免改动已有练习文件。
if not os.path.exists('data'):
    # 当输出目录不存在时创建该目录。
    os.makedirs('data')

# 保存表格数据版本,供后续读取示例复用。
df_demo.to_csv('data/income_stock.csv', index=False)
# 保存表格数据版本,供后续读取示例复用。
df_gpa_demo.to_csv('data/gpa.csv', index=False)

# 报告两份演示 CSV 已写入公开数据目录。
print('演示数据 "income_stock.csv" 和 "gpa.csv" 已创建。')
演示数据 "income_stock.csv" 和 "gpa.csv" 已创建。

步骤1: 将数据从CSV文件读入内存

我们的第一个任务是将硬盘上的 income_stock.csv 文件加载到内存中的 Pandas DataFrame 中。

我们使用 pd.read_csv() 函数。

代码
# 读取固定公开数据版本并保留来源口径。
df = pd.read_csv('data/income_stock.csv')
# 报告 CSV 已读入 `df`,提示学生继续检查字段与类型。
print('数据成功读入名为 df 的 DataFrame 中。')
数据成功读入名为 df 的 DataFrame 中。

步骤2: 对数据进行第一次“侦察”

数据读入后,绝不能假设它是完美的。首先要快速检查它的基本情况。

  • 问题1: 数据长什么样?(预览)
  • 问题2: 数据有多大?(维度)
  • 问题3: 每列存储的是什么类型的数据?(类型)

侦察工具1: .head() - 快速预览数据

.head() 方法可以显示 DataFrame 的前5行(默认),让我们对数据结构有一个直观的认识。

代码
# 为“侦察工具1: `.head()` - 快速预览数据”,显示前几条记录以核对字段与取值。
df.head()
id income gender stock
0 i_1 146958 F 51658.712279
1 i_2 171867 M 28649.648860
2 i_3 156932 M 218551.394257
3 i_4 390838 F 156262.027760
4 i_5 284178 M 181397.055782

侦察工具2: .shape - 查看数据维度

.shape 属性返回一个元组,表示 (行数, 列数)。

代码
# 显示 `df` 的行数与列数,检查“侦察工具2: .shape - 查看数据维度”的数据规模。
df.shape
(10, 4)

解读: 这个输出 (10, 4) 告诉我们,这个数据集有 10 个观测样本(行),每个样本有 4 个特征(列)。

侦察工具3: .dtypes - 检查数据类型

.dtypes 属性告诉我们每一列存储的数据是什么格式。这是至关重要的一步,因为错误的类型会导致计算错误。

代码
# 列出 `df` 各字段的数据类型,发现读取或转换造成的类型偏差。
df.dtypes
id         object
income      int64
gender     object
stock     float64
dtype: object

解读:

  • incomeint64 (整数),正确。
  • stockfloat64 (浮点数/小数),正确。
  • idgenderobject,这通常表示文本(字符串),也正确。

步骤3: 数据选择与切片 - 获取你关心的部分

我们很少需要一次性处理整个数据集。更常见的操作是选择特定的列或行。

  • 选择列: 分析特定变量。
  • 选择行: 分析特定观测样本。

选择单列:使用方括号 []

要获取一个完整的列,可以使用 df['列名'] 的语法。这将返回一个 Pandas Series 对象。

代码
# 从数据框提取持股金额列为 `stock_series`,演示单列索引返回 Series。
stock_series = df['stock']
# 为“选择单列:使用方括号 `[]`”,显示前几条记录以核对字段与取值。
stock_series.head()
0     51658.712279
1     28649.648860
2    218551.394257
3    156262.027760
4    181397.055782
Name: stock, dtype: float64

选择多列:传递一个列表

要选择多个列,向方括号中传递一个包含列名的列表。

代码
# 为“选择多列:传递一个列表”,显示前几条记录以核对字段与取值。
df[['id', 'income']].head()
id income
0 i_1 146958
1 i_2 171867
2 i_3 156932
3 i_4 390838
4 i_5 284178

选择单行:使用 .iloc[] 按位置索引

要根据行号(从0开始)选择一行,使用 .iloc[行号]

代码
# 按位置取得第一条用户记录,而不是保留尚未调用的索引器。
user_0 = df.iloc[0]
# 输出第 1 行记录,核对 `.iloc[0]` 的位置索引结果。
print(user_0)
id                 i_1
income          146958
gender               F
stock     51658.712279
Name: 0, dtype: object

步骤4: 数据筛选 - 根据条件过滤数据

筛选是数据分析中最核心的操作之一。我们提出问题,然后用条件来过滤出答案。

核心思想:布尔掩码 (Boolean Masking)

布尔掩码 Part 1: 创建条件

首先,我们创建一个布尔条件。Pandas会逐行判断这个条件,返回一个由 True / False 组成的 Series。

问题: 哪些用户是男性?

代码
# 创建一个布尔掩码
mask_male = (df['gender'] == 'M')
# 输出性别条件生成的布尔序列,检查每行是否满足男性筛选条件。
print(mask_male)
0    False
1     True
2     True
3    False
4     True
5    False
6     True
7    False
8    False
9     True
Name: gender, dtype: bool

布尔掩码 Part 2: 应用掩码

然后,将这个布尔 Series 放入 df[] 中,Pandas 会返回所有对应值为 True 的行。

代码
# 从上一步获取掩码
mask_male = (df['gender'] == 'M')

# 将掩码应用于DataFrame
df_male = df[mask_male]
# 为“布尔掩码 Part 2: 应用掩码”,显示前几条记录以核对字段与取值。
df_male.head()
id income gender stock
1 i_2 171867 M 28649.648860
2 i_3 156932 M 218551.394257
4 i_5 284178 M 181397.055782
6 i_7 135268 M 242928.815258
9 i_10 162337 M 57728.867294

多条件筛选:使用 & (与) 和 | (或)

当有多个条件时,需要用 & (and) 或 | (or) 连接。

重要: 每个条件都必须用括号 () 括起来!

问题: 筛选出性别为男 并且 收入 > 300,000 的用户。

代码
# 同时应用男性与高收入条件,得到满足两项标准的记录子集。
df_male_highinc = df[(df['gender'] == 'M') & (df['income'] > 300000)]
# 输出双条件筛选后的记录,验证布尔条件按行同时成立。
print(df_male_highinc)
Empty DataFrame
Columns: [id, income, gender, stock]
Index: []

步骤5: 创建新变量 - 特征工程的基础

通常,原始数据本身不足以回答问题。我们需要基于现有变量创建新的、更有意义的变量(特征)。

问题: 用户的投资额占其收入的比例是多少?

创建新列:直接赋值

我们可以像给字典赋值一样,直接创建一个新列。

代码
# 用持股金额除以收入,生成衡量资产相对收入规模的比例列。
df['stock_income_ratio'] = df['stock'] / df['income']
# 为“创建新列:直接赋值”,显示前几条记录以核对字段与取值。
df.head()
id income gender stock stock_income_ratio
0 i_1 146958 F 51658.712279 0.351520
1 i_2 171867 M 28649.648860 0.166697
2 i_3 156932 M 218551.394257 1.392650
3 i_4 390838 F 156262.027760 0.399813
4 i_5 284178 M 181397.055782 0.638322

解读: 一个名为 stock_income_ratio 的新列被创建出来,它的值是每一行 stock 除以 income 的结果。

步骤6: 数据汇总与聚合 - 从细节到宏观

我们经常需要从个体数据中计算出汇总统计量,以了解数据的宏观特征。

  • 问题1: 数据集的整体统计特征是怎样的?
  • 问题2: 不同性别的用户在收入和投资上有什么差异?

汇总工具1: .describe() - 获取描述性统计

.describe() 方法可以快速计算出所有数值型列的常用统计指标。

代码
# 汇总 `df` 各数值列的计数、位置与离散统计量。
df.describe()
income stock stock_income_ratio
count 10.000000 10.000000 10.000000
mean 190603.500000 123253.758925 0.695423
std 89590.564097 86907.971239 0.544712
min 79886.000000 19837.356160 0.136924
25% 145398.750000 53176.251033 0.352543
50% 159634.500000 110580.859385 0.519067
75% 217635.750000 203317.279387 0.881390
max 390838.000000 242928.815258 1.795907

汇总工具2: groupby() - 分组聚合的神器

groupby() 是 Pandas 中最强大的功能之一。它遵循 “拆分-应用-合并” (Split-Apply-Combine) 的思想。

Split-Apply-Combine Diagram 示意图展示 groupby 将表格拆成多组、逐组应用函数,再把结果合并成新表格。 Key Data A 1 B 5 A 3 A 2 B 4 DataFrame 拆分 1 3 2 5 4 应用 (Apply) sum() 6 9 合并 Key Sum A 6 B 9 Result

groupby() 实战:按性别分析收入和投资

问题: 计算男性和女性用户各自的平均收入/投资额、中位数和标准差。

我们可以使用 .agg() 方法一次性应用多个聚合函数。

代码
# 我们想对 'income' 和 'stock' 这两列进行操作
# 我们想计算 'mean', 'median', 'std' 这三个统计量
grouped_stats = df.groupby('gender')[['income', 'stock']].agg(['mean', 'median', 'std'])
# 输出性别分组统计,比较各组收入与持股金额的聚合结果。
print(grouped_stats)
          income                                   stock                 \
            mean    median            std           mean         median   
gender                                                                    
F       199090.6  146958.0  120183.237462  100656.361559   64899.691009   
M       182116.4  162337.0   58612.864725  145851.156290  181397.055782   

                      
                 std  
gender                
F       79680.450713  
M       96791.836696  

步骤7: 数据合并 - 整合多个数据源

在现实世界中,数据往往来自不同的文件或数据库。我们需要将它们整合起来进行统一分析。

场景: 我们有另一份 gpa.csv 文件,包含了用户的大学GPA信息。

合并工具: pd.merge() - 类似SQL的连接操作

pd.merge() 函数可以根据一个或多个共同的键(key)将两个 DataFrame 连接在一起。

代码
# 先读入gpa数据
df_gpa = pd.read_csv('data/gpa.csv')
# 标出合并前的 GPA 右表预览。
print('GPA 数据预览:')
# 输出 GPA 表前两行,确认合并前右表的主键与字段。
print(df_gpa.head(2))

# 按 'id' 列合并两个DataFrame
df_merged = pd.merge(df, df_gpa, on='id')

# 标出按用户编号合并后的表格预览。
print('\n合并后的数据预览:')
# 输出合并表前两行,确认 `id` 匹配后收入与 GPA 位于同一记录。
print(df_merged.head(2))
GPA 数据预览:
    id   gpa
0  i_1  2.37
1  i_2  2.61

合并后的数据预览:
    id  income gender         stock  stock_income_ratio   gpa
0  i_1  146958      F  51658.712279            0.351520  2.37
1  i_2  171867      M  28649.648860            0.166697  2.61

步骤8: 数据存储 - 保存你的分析结果

分析处理完成后,通常需要将最终的干净、完整的数据集保存到硬盘,以备后续使用。

我们将把合并后的 df_merged 保存为一个新的CSV文件。

保存工具: .to_csv()

.to_csv() 方法可以将 DataFrame 写入一个CSV文件。

最佳实践: 设置 index=False,避免将Pandas的行索引写入文件,防止下次读入时多出一列。

代码
# 保存表格数据版本,供后续读取示例复用。
df_merged.to_csv('data/merged.csv', index=False)
# 报告合并结果已经写入 CSV,便于核对输出位置。
print('文件 "merged.csv" 已成功保存。')
文件 "merged.csv" 已成功保存。

第四部分:用数据讲故事:可视化探索

实战演练:数据可视化与探索

数字是抽象的,而图形是直观的。数据可视化是理解数据、发现模式、沟通见解的最有效方式。

工具: 我们将主要使用 Seaborn,并用 Matplotlib 进行一些定制。

数据: 我们将使用公开 stock/financial_statement.h5(key=financial_data)中的中国上市公司年度财报样本。

步骤0: 准备可视化环境和数据

  • 字段:公司、季度、披露日、总资产、营业收入与归母净利润。
  • 样本:2018—2024 年 Q4;每个公司—财年保留最后披露版本。
  • 任务边界:这是披露后探索,不是预测任务。
  • 信息边界:任何特征只有在披露日之后才可使用。
代码
# 为“步骤0: 准备可视化环境和数据”导入 `seaborn` 并绑定 `sns`,用于编码当前任务的统计分布或分组关系。
import seaborn as sns
# 为“步骤0: 准备可视化环境和数据”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 导入 Matplotlib 字体管理器,用于在绘图前检查项目指定的中文字体确已安装。
from matplotlib import font_manager

# 设置一个美观的绘图风格
sns.set_theme(style="whitegrid")
# 固定项目的已安装衬线字体栈,使思源宋体优先渲染中文标题与坐标轴。
project_serif_font_families = ["Source Han Serif SC", "DejaVu Serif"]
# 禁用默认回退并定位思源宋体文件,避免只声明一个未安装的字体名。
source_han_serif_path = font_manager.findfont(project_serif_font_families[0], fallback_to_default=False)
# 从字体文件反查实际家族名,作为渲染环境的字体解析证据。
resolved_source_han_family = font_manager.FontProperties(fname=source_han_serif_path).get_name()
# 在字体解析被替换时立即失败,防止中文标签以缺字方框静默输出。
assert resolved_source_han_family == "Source Han Serif SC", f"中文字体解析异常: {resolved_source_han_family}"
# 将经实测可用的思源宋体置于字体栈首位,并保留已安装的西文衬线回退。
plt.rcParams["font.family"] = project_serif_font_families
# 保留 Unicode 减号 U+2212,使负值刻度与中文标签在同一字体说明下正确呈现。
plt.rcParams["axes.unicode_minus"] = True

# 优先使用 Linux 或 Windows 的本地数据,读者环境再使用章节缓存或公开下载。
from pathlib import Path
from urllib.request import urlretrieve  # 复用本章隐藏设置单元安装的浏览器标识下载器
# 按本地共享数据、项目缓存的顺序选择已存在文件,全部缺失时才准备下载。
finance_path = next((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5'), Path('C:/qiufei/data/stock/financial_statement.h5'), Path('data/course/financial_statement.h5')] if candidate_path.exists()), Path('data/course/financial_statement.h5'))
if not finance_path.exists():
    finance_path.parent.mkdir(parents=True, exist_ok=True)
    urlretrieve('https://assets.qiufei.site/data/stock/financial_statement.h5', finance_path)
# 确定年度 Q4 行条件,使大型 HDF5 在读取前先完成行筛选。
finance_quarters = [f'{year}q4' for year in range(2018, 2025)]
# 只读取探索图所需的公司、时点与三项财务字段,避免全量载入宽表。
finance_columns = ['order_book_id', 'quarter', 'info_date', 'total_assets', 'operating_revenue', 'net_profit_parent_company']
# 从表式 HDF5 键同时选择年度行与必要字段,保持数据读取与绘图逻辑分离。
finance_raw = pd.read_hdf(finance_path, key='financial_data', where='quarter in finance_quarters', columns=finance_columns)
# 统一披露日期类型,为公司—财年版本选择提供可复算时点。
finance_raw['info_date'] = pd.to_datetime(finance_raw['info_date'])
# 检查 HDF 行过滤确实只返回事先确定年度,阻止季度口径静默漂移。
assert finance_raw['quarter'].isin(finance_quarters).all(), 'HDF 行筛选返回了事先确定范围外季度'
# 从季度字段提取财年,供版本去重和分期比较。
finance_raw['fiscal_year'] = finance_raw['quarter'].str[:4].astype(int)
# 对公司—财年保留最后披露版本,防止同一报告修订重复计数。
finance_sample = finance_raw.sort_values(['order_book_id', 'fiscal_year', 'info_date']).drop_duplicates(['order_book_id', 'fiscal_year'], keep='last')
# 删除三项绘图指标缺失的记录,确保各图使用一致完整样本。
finance_sample = finance_sample.dropna(subset=['total_assets', 'operating_revenue', 'net_profit_parent_company']).copy()
# 将大额人民币字段转换为亿元,提升坐标轴可读性。
finance_sample[['assets_yi', 'revenue_yi', 'net_profit_yi']] = finance_sample[['total_assets', 'operating_revenue', 'net_profit_parent_company']].div(1e8).to_numpy()
# 把财年分为三个连续阶段,用于箱形图比较而不暗示随机分组。
finance_sample['period_group'] = pd.cut(finance_sample['fiscal_year'], bins=[2017, 2020, 2022, 2024], labels=['2018—2020', '2021—2022', '2023—2024'])
# 按归母净利润正负建立经营结果组,只作描述性比较。
finance_sample['profit_status'] = np.where(finance_sample['net_profit_yi'].ge(0), '盈利', '亏损')
# 显示公开财务样本前五行,核对公司、财年、披露日与亿元口径。
finance_sample[['order_book_id', 'fiscal_year', 'info_date', 'assets_yi', 'revenue_yi', 'net_profit_yi']].rename(columns={
    'order_book_id': '证券代码', 'fiscal_year': '财年', 'info_date': '披露日',
    'assets_yi': '总资产(亿元)', 'revenue_yi': '营业收入(亿元)', 'net_profit_yi': '归母净利润(亿元)'
}).head()
证券代码 财年 披露日 总资产(亿元) 营业收入(亿元) 归母净利润(亿元)
3 000001.XSHE 2018 2021-02-02 34185.92 1167.16 248.18
3 000001.XSHE 2019 2022-03-10 39390.70 1379.58 281.95
3 000001.XSHE 2020 2023-03-09 44685.14 1535.42 289.28
3 000001.XSHE 2021 2024-03-15 49213.80 1693.83 363.36
3 000001.XSHE 2022 2025-03-15 53215.14 1798.95 455.16

可视化任务1: 理解单个变量的分布

研究问题: 中国上市公司年度归母净利润(亿元)的分布怎样?是否偏态并存在极端观测?

适用图表: 直方图 (Histogram)

直方图:观察数据分布的形状

直方图通过将数据分组成等宽的“箱子”(bins),并统计落在每个箱子内的数据点数量,来展示数据的分布情况。

代码
plt.figure(figsize=(9, 5)) # 设置画布大小
sns.histplot(data=finance_sample, x='net_profit_yi', kde=True, color='dodgerblue') # 展示公开公司财年利润分布
# 将图题设为描述性问题,避免在执行前硬编码分布结论。
plt.title('年度归母净利润的分布与尾部观测')
# 将横轴标为年度归母净利润及亿元单位。
plt.xlabel('归母净利润(亿元)')
# 为“直方图:观察数据分布的形状”,将纵轴标为“频数”,明确纵向编码的变量。
plt.ylabel('频数')
# 显示利润直方图与核密度曲线,检查集中区间、偏态和极端公司财年。
plt.show()
直方图以年度归母净利润(亿元)为横轴、公司财年观测频数为纵轴,并叠加核密度曲线。
图 1: 中国上市公司年度归母净利润分布

可视化任务2: 比较不同组别的分布

研究问题: 2018—2020、2021—2022 与 2023—2024 三个阶段的归母净利润分布有何不同?

适用图表: 箱形图 (Box Plot)

箱形图:简洁地展示五数概括

箱形图(又称盒须图)用一种简洁的方式展示了数据的关键统计特征,对于比较多个组的分布和识别异常值尤其有效。

Anatomy of a Box Plot 箱线图结构标出最小值、第一四分位数、中位数、第三四分位数、最大值与离群点。 异常值 (Outlier) 最大值 (Maximum) 第三四分位数 (Q3) 中位数 (Median) 第一四分位数 (Q1) 最小值 (Minimum)

绘制箱形图:使用 sns.boxplot()

代码
# 为“绘制箱形图:使用 `sns.boxplot()`”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(9, 5))
# 用 `sns.boxplot` 绘制分组四分位数、须与离群点。
sns.boxplot(data=finance_sample, x='period_group', y='net_profit_yi', palette='pastel', order=['2018—2020', '2021—2022', '2023—2024'])
# 用描述性图题提示比较中位数、离散度和离群点。
plt.title('三个财年阶段的利润分布比较')
# 将横轴标为财年阶段,明确分组的时间顺序。
plt.xlabel('财年阶段')
# 将纵轴标为归母净利润及亿元单位。
plt.ylabel('归母净利润(亿元)')
# 显示按时间阶段分组的利润箱形图,比较中位数、四分位距与离群点。
plt.show()
横轴按三个连续财年阶段分组,纵轴为归母净利润(亿元);每个箱体给出中位数、四分位距、须线和离群点。
图 2: 按财年阶段比较归母净利润的箱形图

解读: 箱体中线、四分位距与离群点分别回答典型水平、离散程度与尾部公司财年;阶段差异是描述性关联,不能归因于某项政策或冲击。

可视化任务3: 比较不同类别的平均值

研究问题: 当前样本中盈利与亏损公司财年的平均营业收入有差异吗?

适用图表: 条形图 (Bar Plot)

绘制条形图:使用 sns.barplot()

Seaborn的条形图默认计算的是每个类别的均值,并用误差棒(error bars)表示该均值的95%置信区间。

代码
# 为“绘制条形图:使用 `sns.barplot()`”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(9, 3.0))
# 按盈利状态分组,以营业收入为柱高,比较中国上市公司财年的组均值。
sns.barplot(data=finance_sample, x='profit_status', y='revenue_yi', palette='coolwarm')
# 用描述性图题提示读取组均值与误差线,不硬编码执行结果。
plt.title('盈利状态与平均营业收入的样本比较', fontsize=18)
# 将横轴标为盈利状态,明确横向分组变量。
plt.xlabel('公司财年盈利状态', fontsize=16)
# 将纵轴标为平均营业收入及亿元单位。
plt.ylabel('平均营业收入(亿元)', fontsize=16)
plt.xticks(fontsize=16)  # 保持分组标签达到投影可读字号
plt.yticks(fontsize=16)  # 保持数值刻度达到投影可读字号
# 显示盈利与亏损组平均营业收入及误差线,检查组间差异与不确定性。
plt.show()
横轴区分盈利与亏损公司财年,纵轴为组内平均营业收入(亿元);柱高和误差线用于描述组间差异与不确定性。
图 3: 按盈利状态比较平均营业收入

解读: 柱高是当前样本组均值;误差棒是否重叠不能替代预先定义的统计检验。若要推断,应报告均值差、区间、公司内依赖与抽样边界,并避免因果措辞。

可视化任务4: 探索两个连续变量的关系

研究问题: 营业收入与归母净利润之间存在怎样的样本内关系?

适用图表: 散点图 (Scatter Plot)

绘制散点图:使用 sns.scatterplot()

散点图在二维平面上用点来表示两个数值变量的值,用于揭示变量间的相关性。

代码
# 为“绘制散点图:使用 `sns.scatterplot()`”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(9, 5))
# 以营业收入为横轴、归母净利润为纵轴绘制公司财年散点。
sns.scatterplot(data=finance_sample, x='revenue_yi', y='net_profit_yi', alpha=0.6)
# 用描述性图题引导观察方向与离散程度,不先写死相关结论。
plt.title('营业收入与归母净利润的样本内关系')
# 将横轴标为营业收入及亿元单位。
plt.xlabel('营业收入(亿元)')
# 将纵轴标为归母净利润及亿元单位。
plt.ylabel('归母净利润(亿元)')
# 显示收入与利润散点关系,检查样本内方向、离散程度与极端观测。
plt.show()
横轴为营业收入(亿元)、纵轴为归母净利润(亿元);每个点是一个中国上市公司财年,点云显示样本内方向、离散程度与极端观测。
图 4: 营业收入与归母净利润的样本关系

散点图解读边界

解读: 点云只描述当前披露样本的关联方向、非线性与异方差;公司规模、行业和重复公司观测都会影响图形,相关不等于因果。

可视化任务5: 量化多个变量间的关系

研究问题: 总资产、营业收入与归母净利润三个规模指标的样本相关性是多少?

适用图表: 热图 (Heatmap)

绘制热图:先算相关性,再用 sns.heatmap()

这个过程分两步:

  1. 使用 Pandas 的 .corr() 方法计算相关系数矩阵。
  2. 将这个矩阵传入 sns.heatmap() 进行可视化。
代码
# 步骤1: 选取数值列并计算相关系数矩阵
corr = finance_sample[['assets_yi', 'revenue_yi', 'net_profit_yi']].corr()  # 计算公开财务规模指标的样本相关矩阵

# 步骤2: 绘制热图
plt.figure(figsize=(8.5, 3.4))
# 把 `corr` 编码为热图色块,比较“核心数值变量的相关性热图”中的成对关系。
sns.heatmap(corr, annot=True, cmap='vlag', center=0, fmt='.2f', annot_kws={'size': 16})
# annot=True 在格子上显示数值
# cmap='vlag' 使用一个在0点发散的色板
# fmt='.2f' 格式化数值为两位小数
plt.title('资产、收入与利润的样本相关矩阵', fontsize=18)
plt.xticks(fontsize=16)  # 保持横轴财务字段标签投影可读
plt.yticks(fontsize=16)  # 保持纵轴财务字段标签投影可读
# 显示公开财务字段相关系数热图,核对线性关联方向和强度。
plt.show()
核心数值变量的相关性热图:行列对应变量,色块的方向与深浅表示相关系数正负和绝对值;对角线为 1,非对角格用于识别共同变化。
图 5: 核心数值变量的相关性热图

热图解读

热图数值由当前公开数据版本实时计算;同公司跨年依赖与共同规模因子会影响相关系数,相关不等于因果,也不等于样本外预测价值。

阶段小结

今天,我们从理论到实践,学习了数据管理与探索的全过程:

数据存储

理解了内存与硬盘的权衡,以及CSV、Parquet等格式的选择。

数据管理 (Pandas)

掌握了数据读写、查询、筛选、聚合、合并等一系列核心操作。

数据探索 (Seaborn)

学会了使用多种图表来发现数据背后的故事和模式。

这些技能是进行任何严肃的金融数据分析的基石

检查:输入说明能否复现?

给出文件、key、字段、单位、频率、样本期与可得日;缺任何一项时,说明为何不能进入模型。独立作答 2 分钟。

反馈:七项输入说明缺一不可

  • 对象:文件与 key 确定读取哪个逻辑数据表。
  • 含义:字段与单位固定变量定义和量纲。
  • 范围:频率与样本期固定观察粒度和覆盖区间。
  • 时间边界:可得日用于排除未来信息。

缺任一项都不能复现同一输入或排除前瞻偏差。答漏可得日者回看 相关内容,修正七项说明并重新提交;七项齐全者进入 相关内容

动手实践:课堂练习

使用本章生成的 income_stock.csvgpa.csv,独立完成并提交八项结果:

  1. 两表字段与主键检查;
  2. id 合并与行数检查;
  3. 女性子样本描述统计;
  4. 女性子样本 Parquet 导出与回读检查;
  5. 按性别分组均值;
  6. 收入—投资散点图;
  7. 性别分组柱状图;
  8. 三变量相关矩阵与热图。

查看顺序:先提交八项结果,再开放答案区。

练习:先完成再查看答案:先交作业结果

提交代码、8 项输出和一句失败诊断;缺主键唯一性检查或行数检查者先修正再进入反馈。自查标准为输出可复现、合并不增删用户、统计口径与题意一致。

练习1-2:数据读入与合并

  1. 读入数据: 读入 income_stock.csv 数据。
  2. 合并数据: 将其与 gpa.csv 数据以用户 id 为键进行合并。

练习1-2参考答案

代码
# 读取收入与投资练习表,恢复用户主表的字段和行顺序。
df_income = pd.read_csv('data/income_stock.csv')

# 读取 GPA 练习表,随后按用户主键与收入表合并。
df_gpa = pd.read_csv('data/gpa.csv')
# 统计收入表重复主键,作为一对一连接前的硬门禁。
income_duplicate_keys = int(df_income['id'].duplicated().sum())
# 统计 GPA 表重复主键,避免多对多连接静默放大样本。
gpa_duplicate_keys = int(df_gpa['id'].duplicated().sum())
# 用断言阻止任一输入表主键不唯一时继续合并。
assert income_duplicate_keys == 0 and gpa_duplicate_keys == 0, '重复 id:返回输入表去重并核对业务主键'
# 按唯一 `id` 执行一对一连接,使错误连接基数立即失败。
df_full = pd.merge(df_income, df_gpa, on='id', how='inner', validate='one_to_one')
# 统计收入表中无法匹配 GPA 的用户,防止内连接静默丢行。
income_unmatched_rows = int((~df_income['id'].isin(df_gpa['id'])).sum())
# 统计 GPA 表中无法匹配收入记录的用户,完整检查左右未匹配。
gpa_unmatched_rows = int((~df_gpa['id'].isin(df_income['id'])).sum())
# 汇总两表键唯一性、连接前后行数与左右未匹配证据。
merge_audit = pd.DataFrame([{'收入表行数': len(df_income), 'GPA表行数': len(df_gpa), '收入表重复键': income_duplicate_keys, 'GPA表重复键': gpa_duplicate_keys, '左未匹配': income_unmatched_rows, '右未匹配': gpa_unmatched_rows, '合并后行数': len(df_full)}])
# 展示连接检查表,使第 1—2 项作业结果可以直接自查。
display(merge_audit)
# 强制本题的一对一完整匹配要求,行数异常时停止后续统计。
assert income_unmatched_rows == 0 and gpa_unmatched_rows == 0 and len(df_full) == len(df_income) == len(df_gpa), '合并行数或未匹配异常:返回检查 id 口径'
# 展示合并后前五行,核对字段和值未因连接发生错位。
display(df_full.head())
收入表行数 GPA表行数 收入表重复键 GPA表重复键 左未匹配 右未匹配 合并后行数
0 10 10 0 0 0 0 10
id income gender stock gpa
0 i_1 146958 F 51658.712279 2.37
1 i_2 171867 M 28649.648860 2.61
2 i_3 156932 M 218551.394257 3.05
3 i_4 390838 F 156262.027760 2.86
4 i_5 284178 M 181397.055782 2.58

练习3-4:数据筛选与汇总

  1. 选取子集: 选取所有女性用户的数据,并存入一个新的数据帧。
  2. 数据总结: 对这个新的女性用户数据帧进行总结,计算 income, stock, 和 gpa 的描述性统计信息。

练习3-4参考答案

代码
# 按性别字段筛选女性用户,形成练习描述统计与导出的目标子样本。
df_female = df_full[df_full['gender'] == 'F'].copy() # 使用 .copy() 避免 SettingWithCopyWarning
# 投影只展示六项核心统计量;完整 describe 可在折叠代码中复算。
female_summary = df_female[['income', 'stock', 'gpa']].describe().loc[['count', 'mean', 'std', 'min', '50%', 'max']]
# 用紧凑表核对筛选后的样本量、中心、离散度与范围。
display(female_summary.round(2))
income stock gpa
count 5.00 5.00 5.00
mean 199090.60 100656.36 2.75
std 120183.24 79680.45 0.32
min 79886.00 19837.36 2.37
50% 146958.00 64899.69 2.73
max 390838.00 210624.02 3.22

练习5:数据存储为Parquet格式

  1. 存为Parquet: 将上一步创建的女性用户数据帧 df_female 存为一个 female_data.parquet 文件。

练习5参考答案

代码
# 将女性样本写入指定 Parquet 文件,保留列类型供后续回读检查。
df_female.to_parquet('data/female_data.parquet')
# 重新读取刚写出的文件,验证行数、列名与女性子样本完全一致。
female_roundtrip = pd.read_parquet('data/female_data.parquet')
# 用断言阻止“文件写出但内容或模式变化”被误报为完成。
assert female_roundtrip.shape == df_female.shape and female_roundtrip.columns.equals(df_female.columns)

练习6:重新读入并可视化关系

  1. 重新读入: 重新读入原始的 income_stock.csv 数据。
  2. 散点图: 使用散点图来可视化 incomestock 之间的关系。

练习6参考答案

代码
df_income_reload = pd.read_csv('data/income_stock.csv')  # 重新读取练习保存的收入与投资记录,核对持久化结果

# 为“练习6参考答案”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(9, 5))
# 以 `'income'` 为横轴、`'stock'` 为纵轴绘制散点,数据来自 `df_income_reload`,展示“收入与股票投资关系”。
sns.scatterplot(data=df_income_reload, x='income', y='stock')
# 将图题设为“收入与股票投资额关系”,直接说明当前图形的比较目的。
plt.title('收入与股票投资额关系')
# 将横轴标为“收入”,明确横向编码的变量。
plt.xlabel('收入')
# 将纵轴标为“股票投资额”,明确纵向编码的变量。
plt.ylabel('股票投资额')
# 显示收入与股票投资额散点图,检查两者的方向、离散程度和高收入样本位置。
plt.show()
横轴为学生收入、纵轴为股票投资额;每个点代表一名学生,趋势线用于判断收入提高时投资额是否同步变化。
图 6: 收入与股票投资关系

练习7:可视化性别差异

  1. 条形图: 使用条形图来可视化和比较男性与女性在平均投资总额 (stock) 上的区别。

练习7参考答案

代码
# 按性别显式计算样本数与股票投资均值,提供可复算的第 5 项数值产出。
gender_stock_means = df_full.groupby('gender', as_index=False).agg(样本数=('stock', 'size'), 平均股票投资额=('stock', 'mean'))
# 展示分组均值表,使柱高可以回查到精确数值与分母。
display(gender_stock_means.round(2))
gender 样本数 平均股票投资额
0 F 5 100656.36
1 M 5 145851.16

练习7参考答案:柱图

代码
plt.figure(figsize=(8, 5))  # 创建适合比较两组均值的紧凑画布
# 以 `'gender'` 分组、`'stock'` 为柱高(数据 `df_full`),比较“男女平均股票投资额对比”。
sns.barplot(data=df_full, x='gender', y='stock', palette=['lightblue', 'salmon'])
# 将图题设为“男性与女性的平均股票投资额对比”,直接说明当前图形的比较目的。
plt.title('男性与女性的平均股票投资额对比')
# 将横轴标为“性别”,明确横向编码的变量。
plt.xlabel('性别')
# 将纵轴标为“平均股票投资额”,明确纵向编码的变量。
plt.ylabel('平均股票投资额')
# 显示按性别分组的平均股票投资额及误差线,比较两组均值与不确定性。
plt.show()
横轴为性别组、纵轴为平均股票投资额;两根柱的高度给出样本内男女均值差异,不表达因果。
图 7: 男女平均股票投资额对比

练习8:可视化多变量相关性

  1. 热图: 使用热图来可视化 income, stock, 以及 gpa 三个变量之间的相关性。

练习8参考答案:相关矩阵与热图

代码
# 首先计算相关性矩阵
corr_matrix_ex = df_full[['income', 'stock', 'gpa']].corr()

# 然后绘制热图
plt.figure(figsize=(8, 5))
# 把 `corr_matrix_ex` 编码为热图色块,比较“收入、投资与GPA的相关性”中的成对关系。
sns.heatmap(corr_matrix_ex, annot=True, cmap='coolwarm', center=0, fmt='.2f')
# 将图题设为“收入、股票投资与GPA的相关性热图”,直接说明当前图形的比较目的。
plt.title('收入、股票投资与GPA的相关性热图')
# 显示收入、股票投资额与 GPA 的相关系数矩阵,核对三组两两线性关联。
plt.show()
收入、投资与GPA的相关性:行列对应变量,色块的方向与深浅表示相关系数正负和绝对值;对角线为 1,非对角格用于识别共同变化。
图 8: 收入、投资与GPA的相关性

八项作业结果自查

检查组 通过标准
数据结构 展示字段与两表主键;重复键为 0
合并完整性 一对一合并前后行数一致;左右未匹配均为 0
子样本统计 女性描述统计包含样本数、中心与离散度
文件回读 Parquet 回读后的形状与列一致
分组汇总 分组均值表同时给出各组分母与均值
两幅比较图 散点图字段、轴名正确;柱图与分组均值同源
相关分析 相关矩阵与热图同源

通过条件:八项全部满足。若出现重复键、未匹配或合并行数变化,返回 相关内容 修复;若图表与数值表不一致,返回相应答案页复算。

公开中国数据练习:上市公司覆盖

代码
from pathlib import Path  # 管理公开公司基础信息路径
from urllib.request import urlretrieve  # 复用本章已安装的浏览器标识下载器
import pandas as pd  # 读取并汇总中国上市公司数据版本
# 按本地共享数据、项目缓存的顺序选择公司基础信息文件。
basic_path = next((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/stock/stock_basic_data.h5'), Path('C:/qiufei/data/stock/stock_basic_data.h5'), Path('data/course/stock_basic_data.h5')] if candidate_path.exists()), Path('data/course/stock_basic_data.h5'))
if not basic_path.exists():
    basic_path.parent.mkdir(parents=True, exist_ok=True)
    urlretrieve('https://assets.qiufei.site/data/stock/stock_basic_data.h5', basic_path)
basic_columns = ['order_book_id', 'symbol', 'province', 'industry_name', 'status']  # 限定覆盖检查所需字段
basic_companies = pd.read_hdf(basic_path, key='stock_basic_info', columns=basic_columns)  # 选择性读取小型基础信息表
yangtze_provinces = ['上海市', '江苏省', '浙江省', '安徽省']  # 确定长三角地区口径
yangtze_companies = basic_companies.query('province in @yangtze_provinces and status == "Active"').copy()  # 筛选当前上市的长三角公司
coverage_table = yangtze_companies.groupby('province').agg(公司数=('order_book_id', 'nunique'), 行业数=('industry_name', 'nunique')).reset_index()  # 汇总地区覆盖
print({'文件': basic_path.name, '键': 'stock_basic_info', '全表行数': len(basic_companies), '长三角公司数': len(yangtze_companies)})  # 输出数据来源与样本数量变化
display(coverage_table)  # 展示真实中国公司覆盖而非生成案例
{'文件': 'stock_basic_data.h5', '键': 'stock_basic_info', '全表行数': 5524, '长三角公司数': 1974}
表 1: 公开上市公司数据版本中的长三角省市覆盖
province 公司数 行业数
0 上海市 438 62
1 安徽省 174 46
2 江苏省 664 58
3 浙江省 698 57

本章小结

  • 能提交含文件、key、字段、单位、频率、样本期与可得日的数据字典。
  • 选择存储格式取决于访问模式、类型与规模,不由“流行”决定。
  • 本章图形只描述样本,不建立因果或交易结论。
  • 下一章学习向量、矩阵与统计量,为后续模型公式建立可复算基础。