1  Python与大数据分析导论

1.1 引言与学习目标

在当今的金融行业和学术研究中,数据分析能力已经成为一项核心竞争力。无论是量化投资策略的开发、上市公司财务报表的分析,还是宏观经济趋势的预测,都离不开对海量数据的高效处理和深入挖掘。Python 凭借其简洁的语法、强大的生态系统和卓越的数据处理能力,已成为全球金融从业者和研究者的首选编程语言。

学习目标

完成本章后,你应能:

  • 用“数据规模—算子—内存—结果物化”四个维度解释一个金融数据任务为何需要 Pandas、Polars、DuckDB 或 Dask;
  • 在终端创建或更新 peter 环境、注册同名 Jupyter 内核,并打印解释器与核心库版本作为验证证据;
  • 运行教师提供的 Python 循环与 NumPy 向量化脚手架,核对同一计算的结果一致性,并把计时结论限定在本机、本次输入与具体算子;
  • 运行教师提供的 Pandas 真实数据脚手架,依据输出识别交易所分布、字段合同与最早上市公司;
  • 按 PEP 8 与意图导向原则检查一段代码的命名、缩进和常量写法。

目标—活动—核心练习/答案映射

正式目标 正文活动 核心评价证据
条件化选择 Pandas、Polars、DuckDB 或 Dask 小节 1.2.2小节 1.5.1 的任务边界比较 练习 1.4 及参考答案中的四维选择理由
建立并核验统一环境 小节 1.3.1小节 1.3.2 的环境操作 练习 1.1 及参考答案打印的解释器路径、Python 与三项核心库版本
运行并解释循环与向量化脚手架 小节 1.4.1 的等价计算与计时 练习 1.2 对既有输出的一致性核验和条件化解释;不评分 NumPy API
运行并解释真实数据脚手架 小节 1.6.2 的字段、类型与地域检查 练习 1.3 对教师生成表的字段、交易所计数与上市日期解释;不评分 Pandas API
按规范审查代码 小节 1.4.3 的 PEP 8 与意图导向示例 练习 1.5 及参考答案中的问题清单和改写代码

前置知识要求

  • 具备基本的计算机操作能力(文件管理、命令行基础)
  • 不要求已有编程经验;本章只要求能复制终端命令,并会识别文件、文件夹与表格中的行和列
  • 具备基础的金融知识(股票、债券、收益率等概念)

若尚未接触变量、循环或函数,可以先把它们分别理解为“带名字的值”“重复步骤”和“可复用步骤”。第 2 章会正式讲授这些概念,第 3 章讲 NumPy,第 5 章讲 Pandas。本章提前出现的 NumPy/Pandas 数据准备均为教师提供的脚手架:学生只运行、观察和核对,不要求独立编写,也不计入 API 评分。本章评分只落在已经解释的环境命令、概念边界、输出证据与代码规范。

1.2 大数据分析的演进:从 Hadoop 生态到现代 Python 接口数据栈

1.2.1 传统大数据技术栈的局限

在 2010 年代,大数据处理的主流方案是以 Java/JVM 为基础的 Hadoop 生态系统,包括 HDFS、MapReduce、Hive、Spark 等组件。这些工具虽然能够处理 PB 级别的数据,但对于商学院学生和金融分析师而言,存在显著的使用门槛:

  • 部署复杂:需要配置分布式集群环境,单机学习成本极高
  • 语言壁垒:核心框架基于 Java/Scala,学习曲线陡峭
  • 资源消耗大:JVM 的内存开销和启动时间对个人电脑不友好
  • 迭代效率低:从代码编写到结果查看的反馈周期过长

1.2.2 现代 Python 数据栈的崛起

近年来,得益于 Apache Arrow 等内存标准以及 C++、Rust 等原生执行引擎的发展,一批提供 Python API 的数据处理工具改变了大数据分析的工作方式:

工具 特点 典型应用场景
Pandas 成熟的表格分析库,API 丰富 能否适用取决于数据布局、算子和内存预算
Polars Rust 执行引擎、表达式 API 与多线程 列式查询、惰性执行与流式处理
DuckDB 进程内分析型数据库,支持 SQL 单机大数据 SQL 查询
Dask Python 原生分布式计算框架 超大规模数据集并行计算
Apache Arrow 列式内存格式标准 在类型和缓冲区兼容时减少跨工具复制

这些工具的共同特点是:

  1. 统一的 Python 接口:多数工具可通过 pipconda 安装;Python 是操作界面,底层实现语言因工具而异
  2. 单机高性能:充分利用现代 CPU 的多核架构和向量化指令集
  3. 内存策略多样:部分工具支持惰性求值或流式处理;能否处理超过内存的数据仍取决于算子、分区、临时结果与输出物化方式
  4. 互操作性较强:Apache Arrow 提供共同的列式表示;是否零拷贝取决于数据类型、缓冲区所有权、分块方式和目标工具

中国金融市场的数据规模参考

以中国 A 股市场为例,数据规模覆盖了从中小型到大规模的各个层次:

  • 日频行情数据:约 5000 只股票 × 20 年 × 250 交易日 ≈ 2500 万行;Pandas 是否适用要结合列数、dtype、可用内存和具体算子评估
  • 分钟频行情数据:约 5000 只 × 20 年 × 250 天 × 240 分钟 ≈ 60 亿行,通常需要列投影、分区和按需计算,而非仅凭格式指定唯一工具
  • Tick 级行情数据:每只股票每日可达数万条,全市场数据通常需要分块、分区或分布式执行,并严格控制最终结果大小

本教材将循序渐进地引导你掌握处理上述各种规模数据的能力。

1.2.3 技术演进时间线

图 1.1 展示本节讨论对象的可视化结果,读图时应结合正文给出的口径与限制。

图中的年份统一表示可由项目方资料核验的具体里程碑,而不是笼统的“技术成熟年”。Apache Spark 的项目历史记录其 2009 年始于研究项目、2010 年初开源;Apache Arrow 的 0.1.0 发布页记录首个版本发布于 2016 年;DuckDB 项目历史区分了 2018 年开始开发与 2019 年公开 v0.1;Polars 的项目回顾把首次提交定在 2020 年 6 月 23 日;Pandas 2.0 的 release notes则记录了 2023 年版本里程碑。Hadoop 在这里作为更早的分布式生态背景出现,不再用一个未经本节来源定义的年份概括整个项目。

大数据分析技术演进 2009 Spark 在 Berkeley AMPLab 启动 2010 Spark 开源,研究原型进入社区 2016 Apache Arrow 发布 0.1.0 2018/19 DuckDB 开始开发并公开 v0.1 2020 Polars 首次提交,Rust 表格引擎起步 2023 Pandas 2.0 扩展 Arrow-backed dtype JVM 生态 Python 生态 现代融合
图 1.1: 大数据分析技术从 Hadoop 生态到现代 Python 接口数据栈的演进历程

1.3 基础开发环境搭建

1.3.1 Conda 包管理与虚拟环境

Conda 是 Python 数据科学领域最流行的包管理和环境管理工具。与 pip 相比,Conda 能够管理非 Python 依赖(如 C/C++ 库),并提供了强大的虚拟环境隔离机制。

安装 Miniconda

推荐使用 Miniconda(Conda 的最小安装版本),它只包含 Conda 本身和 Python,体积小巧:

  1. 访问 Miniconda 官网 下载适合你操作系统的安装包
  2. 按照安装向导完成安装
  3. 打开终端(Windows 用户打开 Anaconda Prompt),验证安装:
import platform  # 识别操作系统,以便全章后续真实数据示例复用统一数据根路径
from pathlib import Path  # 为小型快照哈希验证构造跨平台文件路径
DATA_ROOT = 'C:/qiufei/data' if platform.system() == 'Windows' else '/home/ubuntu/r2_data_mount/data'  # 为跨平台本地数据读取建立唯一入口
# 在终端中执行以下命令验证 Conda 是否正确安装
# conda --version  # 检查 conda 版本号
# python --version  # 检查 Python 版本号
print('Conda 环境已正确配置')  # 在 Python 中确认环境就绪
Conda 环境已正确配置

创建和管理虚拟环境

虚拟环境是 Python 开发的最佳实践,它能隔离不同项目的依赖,避免版本冲突:

# 以下为终端命令,非 Python 代码
# conda env create --file environment.yml  # 首次部署时按仓库清单创建 peter
# conda env update --name peter --file environment.yml --prune  # 已存在时按同一清单更新;与上一行二选一
# conda run --no-capture-output --name peter python -m ipykernel install --user --name peter --display-name peter  # 将同名内核指向当前环境
# conda run --no-capture-output --name peter jupyter kernelspec list --json  # 核对 peter.spec.argv[0] 的解释器路径

本教材的环境配置

本教材以 environment.yml 创建或更新名为 peter 的 Conda 环境。只有完成项目规定的解释器、依赖和内核预检后,下列导入与版本输出才代表本书的目标环境:

import sys  # 导入系统模块,用于获取 Python 解释器信息
print(f'Python 版本: {sys.version}')  # 打印当前 Python 版本
print(f'Python 路径: {sys.executable}')  # 打印 Python 解释器的完整路径
Python 版本: 3.10.20 (main, Mar 11 2026, 17:46:40) [GCC 14.3.0]
Python 路径: /home/ubuntu/miniconda3/envs/peter/bin/python
import pandas as pd  # 导入 Pandas 库,Python 最核心的数据分析工具
import numpy as np  # 导入 NumPy 库,提供高性能数值计算支持
import matplotlib  # 导入 Matplotlib 库,Python 基础绑图引擎
print(f'Pandas 版本: {pd.__version__}')  # 确认 Pandas 版本
print(f'NumPy 版本: {np.__version__}')  # 确认 NumPy 版本
print(f'Matplotlib 版本: {matplotlib.__version__}')  # 确认 Matplotlib 版本
Pandas 版本: 2.3.3
NumPy 版本: 1.26.4
Matplotlib 版本: 3.10.8

1.3.2 Jupyter 生态系统

Jupyter 是交互式数据分析的标准工具,它提供了”代码-输出-文档”三位一体的工作环境。

Jupyter Notebook vs JupyterLab

特性 Jupyter Notebook JupyterLab
界面 单文档 多标签页,类 IDE
文件管理 基础 完善的文件浏览器
扩展性 有限 丰富的插件生态
终端 不支持 内置终端
适用场景 快速原型开发 完整的分析项目

Jupyter 的核心概念

Jupyter Notebook 由一系列”单元格(Cell)“组成,每个单元格可以是:

  • 代码单元格:编写和执行 Python 代码
  • Markdown 单元格:编写格式化文档和数学公式
  • 输出区域:展示代码运行结果(表格、图形等)

教师脚手架(只运行核对,不评分)

下面的 DataFrame 只帮助初学者认识 Jupyter 的表格输出。Pandas 将在第 5 章正式讲授;本章不要求解释或独立编写字典转表、列选择或聚合 API。

import pandas as pd  # 导入 Pandas 用于创建示例数据框
sample_data = {  # 构建一个简化的 A 股行情数据字典
    '股票代码': ['600276', '002415', '600104'],  # 采用三家长三角上市公司的代码作为标签
    '股票名称': ['恒瑞医药', '海康威视', '上汽集团'],  # 公司分别位于江苏、浙江和上海
    '收盘价': [45.0, 32.0, 15.0],  # 设置仅用于 DataFrame 展示的假设价格,不作行情事实
    '涨跌幅': [0.015, -0.008, 0.023]  # 设置仅用于语法演示的假设日涨跌幅
}  # 完成假设行情字典,避免把机制常量解释为真实观测
stock_overview = pd.DataFrame(sample_data)  # 将字典转换为 Pandas 数据框
stock_overview  # 在 Jupyter 中直接显示数据框,自动渲染为格式化表格
股票代码 股票名称 收盘价 涨跌幅
0 600276 恒瑞医药 45.0 0.015
1 002415 海康威视 32.0 -0.008
2 600104 上汽集团 15.0 0.023

1.3.3 开发工具推荐

除了 Jupyter 之外,以下工具也是数据分析工作中的得力助手:

  • VS Code:微软出品的免费代码编辑器,支持 Python 和 Jupyter Notebook 的原生编辑,配合 Copilot AI 插件可以大幅提升编程效率
  • Quarto:下一代科技出版系统,支持将包含代码的文档渲染为 HTML、PDF、Word 等多种格式,本教材即使用 Quarto 构建

1.4 大数据分析的基本思维与代码规范

1.4.1 向量化思维

在同质数值数组的逐元素计算中,一个常见的编程思维转变是从 Python 层循环转向向量化操作。它常能减少解释器开销,但收益大小取决于数组规模、dtype、内存布局、具体内核和临时数组;含分支、字符串、I/O 或难以向量化的算法不一定更快。

为何向量化如此重要?

Python 层 for 循环会逐次承担解释器开销;NumPy 的许多数值内核由编译代码实现,并可能利用 SIMD(单指令多数据)指令集。Pandas 的高层操作还可能受索引对齐、缺失值、对象 dtype 和内存复制影响,因此应以同一输入上的结果核对与实测为准。

教师脚手架(只运行核对,不评分)

以下三个代码块由教师提供。学生只需依次运行,核对两条路径结果一致,并用条件化语言解释本次计时;数组创建、切片、np.diff()np.allclose() 将在第 3 章讲授,不作为本章评分点。

import numpy as np  # 导入 NumPy 用于向量化计算
import time  # 导入时间模块用于计时
stock_prices = np.linspace(10.0, 200.0, num=1_000_000, dtype=np.float64)  # 用确定性正值序列承载计时,不把题设数列冒充市场观测
assert stock_prices.shape == (1_000_000,) and stock_prices[0] == 10.0 and stock_prices[-1] == 200.0  # 固定输入规模与端点,防止两种实现比较不同数据
start_time = time.time()  # 记录循环开始时间
returns_loop = []  # 初始化空列表存储逐个计算的收益率
for i in range(1, len(stock_prices)):  # 遍历每个价格(从第2个开始)
    daily_return = (stock_prices[i] - stock_prices[i-1]) / stock_prices[i-1]  # 计算日收益率
    returns_loop.append(daily_return)  # 将收益率追加到列表中
loop_elapsed_seconds = time.time() - start_time  # 计算循环方式的总耗时
print(f'循环方式耗时: {loop_elapsed_seconds:.4f} 秒')  # 输出循环耗时
循环方式耗时: 0.3988 秒
start_time = time.time()  # 记录向量化开始时间
returns_vectorized = np.diff(stock_prices) / stock_prices[:-1]  # NumPy向量化计算日收益率
assert np.allclose(np.asarray(returns_loop), returns_vectorized)  # 两种实现必须在同一固定输入上逐项一致
vectorized_elapsed_seconds = time.time() - start_time  # 计算向量化方式的总耗时
vectorized_elapsed_seconds = max(vectorized_elapsed_seconds, 1e-9)  # 防止除以零(向量化极快时耗时可能为0)
print(f'向量化方式耗时: {vectorized_elapsed_seconds:.4f} 秒')  # 输出向量化耗时
speedup_ratio = loop_elapsed_seconds / vectorized_elapsed_seconds  # 计算加速比
print(f'本机本次加速比: {speedup_ratio:.1f} 倍')  # 仅报告当前硬件、输入规模和具体算子的观测结果
向量化方式耗时: 0.0457 秒
本机本次加速比: 8.7 倍

1.4.2 惰性求值与流式处理

面对 GB 级甚至 TB 级的数据,将所有数据一次性加载到内存中是不现实的。现代数据处理框架普遍采用两种策略来应对这一挑战:

惰性求值(Lazy Evaluation)

惰性求值的核心思想是:先构建计算图(描述”要做什么”),再统一执行(实际”去做”)。这允许框架在执行前进行全局优化,例如谓词下推(Predicate Pushdown)、投影下推(Projection Pushdown)等。

Polars 的 Lazy API 是惰性求值的典型代表:

# 以下为 Polars 的惰性求值示范(伪代码,后续章节将详细讲解)
# import polars as pl  # 导入 Polars 库
# lazy_frame = pl.scan_parquet('huge_stock_data.parquet')  # 扫描文件,不立即加载到内存
# result = (
#     lazy_frame
#     .filter(pl.col('exchange') == 'SSE')  # 过滤条件:仅保留上交所股票
#     .group_by('industry')  # 按行业分组
#     .agg(pl.col('market_cap').mean())  # 计算各行业平均市值
#     .collect()  # 此时才真正执行所有计算
# )
print('惰性求值:先描述计算,再统一执行')  # 说明核心思想
惰性求值:先描述计算,再统一执行

流式处理(Streaming)

流式处理将大文件分成小块(Chunk),逐块处理并汇总结果,内存占用始终保持在可控范围内:

# Pandas 的分块读取示范
# import pandas as pd  # 导入 Pandas 库
# chunk_reader = pd.read_csv('huge_file.csv', chunksize=100000)  # 每次读取10万行
# total_volume = 0  # 初始化成交量累计值
# for chunk in chunk_reader:  # 逐块迭代处理
#     total_volume += chunk['volume'].sum()  # 累加每块的成交量
# print(f'总成交量: {total_volume}')  # 输出最终的成交量汇总结果
print('流式处理:分块读取,逐块计算,汇总结果')  # 说明核心思想
流式处理:分块读取,逐块计算,汇总结果

1.4.3 Python 代码规范

编写规范、可读性强的代码是数据分析的重要素养。本教材遵循以下规范:

PEP 8 核心规范

  • 命名规范:变量和函数使用 snake_case(小写下划线),类名使用 CamelCase(驼峰命名),常量使用 UPPER_CASE(全大写)
  • 缩进:统一使用 4 个空格
  • 行长度:每行不超过 79 个字符
  • 空行:函数之间空两行,方法之间空一行

数据分析代码的额外规范

import pandas as pd  # 导入 Pandas,注意按字母顺序排列 import 语句

TRADING_DAYS_PER_YEAR = 252  # 定义教学年化约定;实际年份的交易日数应按日历核验

def calculate_annualized_return(daily_returns_series):  # 函数名清晰描述其功能
    """计算年化收益率。

    Args:
        daily_returns_series: 包含日收益率的 Pandas Series

    Returns:
        float: 年化收益率
    """
    mean_daily_return = daily_returns_series.mean()  # 计算日均收益率
    annualized_return = mean_daily_return * TRADING_DAYS_PER_YEAR  # 年化(简单年化法)
    return annualized_return  # 返回年化收益率

print(f'A股年交易日数: {TRADING_DAYS_PER_YEAR}')  # 展示常量的使用
A股年交易日数: 252

变量命名的核心原则——意图导向

变量名应当精准传达其业务含义,而非使用无意义的泛型名称:

不规范 规范 说明
x stock_price 明确表示这是股价
data daily_return_series 说明数据的具体内容
temp filtered_sse_stocks 描述过滤条件和结果
flag is_trading_day 布尔值使用 is_ 前缀
list1 yangtze_delta_tickers 体现业务领域语境

1.5 本教材的结构与学习路径

本教材按照由浅入深、循序渐进的原则,将内容组织为五个篇章:

1.5.1 教材内容架构

图 1.2 展示本节讨论对象的可视化结果,读图时应结合正文给出的口径与限制。

《Python在大数据分析中的应用》教材结构 第一篇:基础与工具准备 第1章 导论 → 第2章 数据结构 → 第3章 NumPy 第二篇:结构化数据处理与分析核心(Pandas) 第4章 数据IO → 第5章 Pandas → 第6章 清洗 → 第7章 规整 第8章 聚合 → 第9章 时间序列 → 第10章 可视化 第三篇:现代 Python 大数据工具栈 第11章 内存管理 → 第12章 Polars → 第13章 DuckDB → 第14章 Dask 第四篇:特征工程与数据预处理 第15章 特征工程 → 第16章 大规模流水线 → 第17章 文本数据 第五篇:综合实战案例 第18章 财务+行情综合分析 → 第19章 高频数据特征提取
图 1.2: 本教材五篇内容的层次递进关系

1.5.2 学习路径建议

根据你的学习目标和时间安排,推荐以下学习路径:

路径一:数据分析基础(4-6 周)

适合刚接触 Python 的商学院学生,重点掌握 Pandas 的核心操作:

第 1 章 → 第 2 章 → 第 3 章 → 第 5 章 → 第 6 章 → 第 10 章

路径二:完整数据分析(8-12 周)

适合有一定编程基础的学生,全面掌握结构化数据分析:

第 1-10 章(按顺序学习)

路径三:大数据与量化金融(16 周完整课程)

适合对量化金融和大数据技术感兴趣的学生:

第 1-19 章(完整学习)

1.6 中国 A 股市场数据概览

本教材的案例和练习大量使用中国 A 股市场的真实数据。在正式开始学习之前,让我们先了解一下这些数据的基本特征。

1.6.1 数据来源与存储

本教材实际读取的数据族、相对路径、覆盖期、schema/复权、SHA-256、提供方证据和再分发边界统一记录在项目 readme.md 的『实际读取数据族 manifest』中。普通渲染只对小文件做完整哈希,并对巨型文件做 key、schema、行数或固定日期样本检查;发布审计才重算巨型文件哈希。

教师脚手架(只运行核对,不评分)

以下目录表和真实数据读取由教师提供。学生本章只需识别文件名、覆盖期、行列与字段输出;Pandas 的 DataFrame、HDF5 读取、筛选和分组不计分。

print(f'本章统一数据根路径: {DATA_ROOT}')  # 复用章首跨平台入口,避免不同示例悄然切换数据源
本章统一数据根路径: /home/ubuntu/r2_data_mount/data
import pandas as pd  # 导入 Pandas 以读取 HDF 节点元数据并展示目录
catalog_records = [  # 逐项抄录 readme manifest 的路径、键、覆盖期与精确行数
    ('公司基本信息', 'stock/stock_basic_data.h5', '/stock_basic_info', '静态证券截面', 5_524),  # 固定公司表合同
    ('季度财务报表', 'stock/financial_statement.h5', '/financial_data', '2005Q1—2026Q1', 306_019),  # 固定财务表合同
    ('前复权日行情', 'stock/stock_price_pre_adjusted.h5', '/data', '2005-01-04—2025-12-31', 15_368_843),  # 固定前复权合同
    ('后复权日行情', 'stock/stock_price_post_adjusted.h5', '/data', '2005-01-04—2025-12-31', 15_368_843),  # 固定后复权合同
    ('不复权日行情', 'stock/stock_price_no_adjust.h5', '/data', '2005-01-04—2025-12-31', 15_368_843),  # 固定原始价格合同
    ('季度估值因子', 'stock/valuation_factors_quarterly_15_years.h5', '/valuation_factors', '2011-03-31—2025-12-31', 207_600),  # 固定估值表合同
    ('指数日行情', 'index/indexes.h5', '/indexes', '2005-01-04—2026-01-30', 22_408_975),  # 使用实际单一指数文件路径
]  # 完成七个实际 HDF 数据族的权威目录
data_catalog = pd.DataFrame(catalog_records, columns=['数据集', '相对路径', 'HDF key', '覆盖期', '预期行数'])  # 形成可审计目录
data_catalog  # 展示与 readme manifest 同源的精确目录
表 1.1: 由 manifest 固定并以 HDF 元数据核验的核心数据目录
数据集 相对路径 HDF key 覆盖期 预期行数
0 公司基本信息 stock/stock_basic_data.h5 /stock_basic_info 静态证券截面 5524
1 季度财务报表 stock/financial_statement.h5 /financial_data 2005Q1—2026Q1 306019
2 前复权日行情 stock/stock_price_pre_adjusted.h5 /data 2005-01-04—2025-12-31 15368843
3 后复权日行情 stock/stock_price_post_adjusted.h5 /data 2005-01-04—2025-12-31 15368843
4 不复权日行情 stock/stock_price_no_adjust.h5 /data 2005-01-04—2025-12-31 15368843
5 季度估值因子 stock/valuation_factors_quarterly_15_years.h5 /valuation_factors 2011-03-31—2025-12-31 207600
6 指数日行情 index/indexes.h5 /indexes 2005-01-04—2026-01-30 22408975
catalog_layouts = []  # 保存每个节点的 table 或 fixed 布局以展示 schema 入口
for catalog_row in data_catalog.itertuples(index=False):  # 逐项核验七个目录记录
    catalog_path = Path(DATA_ROOT) / catalog_row.相对路径  # 从统一数据根拼接相对路径
    assert catalog_path.is_file(), f'缺少目录文件: {catalog_path}'  # 缺文件时立即失败而非静默跳过
    with pd.HDFStore(catalog_path, mode='r') as catalog_store:  # 只读打开本地 HDF 文件
        assert catalog_row._2 in catalog_store.keys(), (catalog_path, catalog_store.keys())  # 核验 HDF key;_2 对应“HDF key”列
        catalog_storer = catalog_store.get_storer(catalog_row._2)  # 取得节点布局和形状元数据
        fixed_value_nodes = [node for name, node in catalog_storer.group._v_children.items() if name.startswith('block') and name.endswith('_values')]  # 找出 fixed 节点的数据块
        catalog_rows = catalog_storer.nrows if catalog_storer.format_type == 'table' else max(node.shape[0] for node in fixed_value_nodes)  # 从 table 行数或 fixed 数据块首轴取得记录数
        assert catalog_rows == catalog_row.预期行数, (catalog_path, catalog_rows, catalog_row.预期行数)  # 阻止目录与快照漂移
        catalog_layouts.append(catalog_storer.format_type)  # 记录节点布局供目录展示
catalog_audit = data_catalog.assign(节点布局=catalog_layouts)  # 合并路径、键、覆盖期、行数与布局
catalog_audit  # 输出核验后的目录;字段明细以各章数据合同为准
列表 1.1
数据集 相对路径 HDF key 覆盖期 预期行数 节点布局
0 公司基本信息 stock/stock_basic_data.h5 /stock_basic_info 静态证券截面 5524 table
1 季度财务报表 stock/financial_statement.h5 /financial_data 2005Q1—2026Q1 306019 table
2 前复权日行情 stock/stock_price_pre_adjusted.h5 /data 2005-01-04—2025-12-31 15368843 table
3 后复权日行情 stock/stock_price_post_adjusted.h5 /data 2005-01-04—2025-12-31 15368843 table
4 不复权日行情 stock/stock_price_no_adjust.h5 /data 2005-01-04—2025-12-31 15368843 table
5 季度估值因子 stock/valuation_factors_quarterly_15_years.h5 /valuation_factors 2011-03-31—2025-12-31 207600 fixed
6 指数日行情 index/indexes.h5 /indexes 2005-01-04—2026-01-30 22408975 fixed

表 1.1 给出与项目 manifest 同源的目录;代码块 列表 1.1 随后只读取 HDF 元数据,逐项验证文件、key、布局和行数。覆盖期来自同一 manifest,不因文件存在就被重新解释为可公开取得的数据。

1.6.2 快速预览 A 股数据

让我们先快速预览一下上市公司基本信息数据:

import pandas as pd  # 导入 Pandas 用于数据读取
import hashlib  # 对体量较小的公司基本表执行完整字节哈希
stock_basic_path = f'{DATA_ROOT}/stock/stock_basic_data.h5'  # 拼接上市公司基本信息文件路径
stock_basic_sha256 = hashlib.sha256(Path(stock_basic_path).read_bytes()).hexdigest()  # 在固定输出计算前验证 1.6 MB 快照
assert stock_basic_sha256 == '15a3be1d6bb949e4b202741ca6e4b17a8a6f2fffee6e24726f5738f5e4e162be'  # 阻止基础表静默漂移
stock_basic_data = pd.read_hdf(stock_basic_path, key='/stock_basic_info')  # 按 manifest 的精确 HDF key 读取公司基本信息
assert stock_basic_data.shape[0] == 5_524  # 核验公司表与 manifest 的精确记录数一致
print(f'数据维度: {stock_basic_data.shape}')  # 输出数据的行数和列数
print(f'数据列名: {list(stock_basic_data.columns)}')  # 输出所有字段名称
数据维度: (5524, 24)
数据列名: ['order_book_id', 'industry_code', 'market_tplus', 'symbol', 'special_type', 'exchange', 'status', 'type', 'de_listed_date', 'listed_date', 'sector_code_name', 'abbrev_symbol', 'sector_code', 'round_lot', 'trading_hours', 'board_type', 'industry_name', 'issue_price', 'trading_code', 'office_address', 'province', 'purchasedate', 'citics_2019_l1_code', 'citics_2019_l1_name']

表 1.2 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。

stock_basic_data.head()  # 显示前5行数据,快速了解数据结构
表 1.2: 上市公司基本信息数据前5行
order_book_id industry_code market_tplus symbol special_type exchange status type de_listed_date listed_date ... trading_hours board_type industry_name issue_price trading_code office_address province purchasedate citics_2019_l1_code citics_2019_l1_name
0 000001.XSHE J66 1 平安银行 Normal XSHE Active CS 0000-00-00 1991-04-03 ... 09:31-11:30,13:01-15:00 MainBoard 货币金融服务 40.0 000001 中国广东省深圳市深南东路5047号;中国广东省深圳市福田区益田路5023号平安金融中心B座 广东省 NaN 40 银行
1 000002.XSHE K70 1 万科A Normal XSHE Active CS 0000-00-00 1991-01-29 ... 09:31-11:30,13:01-15:00 MainBoard 房地产业 1.0 000002 中国广东省深圳市福田区梅林路63号万科大厦 广东省 NaN 42 房地产
2 000003.XSHE Unknown 1 PT金田A PT XSHE Delisted CS 2002-06-14 1991-07-03 ... 09:31-11:30,13:01-15:00 MainBoard 未知 10.0 000003 深圳市罗湖区嘉宾路深华商业大厦19楼1909 广东省 NaN NaN NaN
3 000004.XSHE I65 1 *ST国华 StarST XSHE Active CS 0000-00-00 1990-12-01 ... 09:31-11:30,13:01-15:00 MainBoard 软件和信息技术服务业 1.0 000004 深圳市福田区梅林街道孖岭社区凯丰路10号翠林大厦12层 广东省 NaN 62 计算机
4 000005.XSHE Unknown 1 ST星源 Other XSHE Delisted CS 2024-04-26 1990-12-10 ... 09:31-11:30,13:01-15:00 MainBoard 未知 10.0 000005 深圳市罗湖区深南东路2017号华乐大厦3F 广东省 NaN NaN NaN

5 rows × 24 columns

长三角地区上市公司概览

本教材优先使用长三角地区(上海、江苏、浙江、安徽)的上市公司作为案例。让我们看看这些地区有多少上市公司:

import pandas as pd  # 导入 Pandas 用于数据处理
stock_basic_data = pd.read_hdf(f'{DATA_ROOT}/stock/stock_basic_data.h5', key='/stock_basic_info')  # 按 manifest key 读取公司基本信息
required_stock_fields = {'order_book_id', 'province'}  # 声明地域统计不可缺少的 schema 字段
missing_stock_fields = required_stock_fields.difference(stock_basic_data.columns)  # 计算实际 schema 的缺失字段
assert not missing_stock_fields, f'公司表缺少必要字段: {sorted(missing_stock_fields)}'  # 合同不满足时显式失败
yangtze_delta_provinces = ['上海市', '江苏省', '浙江省', '安徽省']  # 使用公司表 province 字段的真实四省市取值
assert set(yangtze_delta_provinces).issubset(set(stock_basic_data['province'].dropna()))  # 核验四个地域值确实存在

表 1.3 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。

表 1.3: 长三角地区上市公司数量统计
yrd_filter_mask = stock_basic_data['province'].isin(yangtze_delta_provinces)  # 按真实 province 合同创建地域掩码
yrd_stock_count = (  # 生成顺序稳定且包含全部四省市的统计表
    stock_basic_data.loc[yrd_filter_mask].groupby('province').size()  # 按省市统计公司记录数
    .reindex(yangtze_delta_provinces, fill_value=0).rename('上市公司数量')  # 固定展示顺序并保留零计数
    .rename_axis('地区').reset_index()  # 把地域索引恢复为普通列
)  # 完成长三角公司计数
print(yrd_stock_count.to_string(index=False))  # 以无索引格式输出统计结果
print(f'\n长三角地区上市公司总数: {int(yrd_filter_mask.sum())}')  # 输出可复核的四省市总数
 地区  上市公司数量
上海市     464
江苏省     691
浙江省     720
安徽省     180

长三角地区上市公司总数: 2055

1.7 本章小结

本章介绍了以下核心内容:

  1. 大数据分析的技术演进:从 Hadoop/JVM 生态到具有 Python API 的现代数据栈。Pandas 主要由 C/Cython 加速,Polars 由 Rust 实现,DuckDB 由 C++ 实现,Dask 负责任务调度;“可以从 Python 调用”不等于“底层由纯 Python 执行”
  2. 开发环境搭建:Conda 虚拟环境管理和 Jupyter 生态系统是 Python 数据分析的标准工具链
  3. 大数据分析思维:向量化操作、惰性求值和流式处理是高效处理大规模数据的三大核心理念
  4. 代码规范:遵循 PEP 8 标准和意图导向的命名规范,编写可读性强、可维护的分析代码
  5. 教材结构:本书分为五篇十九章,从基础到进阶,从单机到分布式,覆盖完整的数据分析技术栈

从下一章开始,我们将正式进入 Python 编程的世界,深入学习内置数据结构、函数和文件操作。

1.8 练习题

练习 1.1:安装 Miniconda,根据仓库 environment.yml 新建或更新本教材统一使用的 peter 环境,注册同名内核,然后在 Jupyter Notebook 中打印当前解释器路径、Python 版本、三项核心库的完整版本号和模块路径,作为内核指向正确环境的验证证据。

解答

# 在终端中执行以下命令:
# conda env create --file environment.yml  # peter 不存在时按权威清单新建环境
# conda env update --name peter --file environment.yml --prune  # peter 已存在时更新;与上一行二选一
# conda run --no-capture-output --name peter python -m ipykernel install --user --name peter --display-name peter  # 注册同名内核并绑定 peter 解释器
# conda run --no-capture-output --name peter jupyter notebook  # 从同一环境启动 Jupyter Notebook

# 在 notebook 中运行以下代码,核对内核实际指向的解释器和依赖版本:
import sys  # 读取当前 Jupyter 内核实际使用的 Python 解释器与版本
import pandas as pd  # 读取本教材表格计算核心库的版本
import numpy as np  # 读取本教材数组计算核心库的版本
import matplotlib  # 读取本教材绘图核心库的版本而不依赖 pyplot 状态
print(f'解释器路径: {sys.executable}')  # 路径应落在 peter 环境中,否则需重新选择内核
print(f'Python: {sys.version.split()[0]}')  # 报告可复核的完整 Python 语义版本号
print(f'Pandas: {pd.__version__}')  # 报告 Pandas 完整版本号以固定运行环境
print(f'NumPy: {np.__version__}')  # 报告 NumPy 完整版本号以固定数值语义
print(f'Matplotlib: {matplotlib.__version__}')  # 报告 Matplotlib 完整版本号以固定绘图环境
print(f'Pandas 模块路径: {pd.__file__}')  # 证明 Pandas 与解释器来自同一 peter 环境
print(f'NumPy 模块路径: {np.__file__}')  # 证明 NumPy 没有从用户目录或旧环境串入
print(f'Matplotlib 模块路径: {matplotlib.__file__}')  # 证明绘图库与已注册内核一致
解释器路径: /home/ubuntu/miniconda3/envs/peter/bin/python
Python: 3.10.20
Pandas: 2.3.3
NumPy: 1.26.4
Matplotlib: 3.10.8
Pandas 模块路径: /home/ubuntu/miniconda3/envs/peter/lib/python3.10/site-packages/pandas/__init__.py
NumPy 模块路径: /home/ubuntu/miniconda3/envs/peter/lib/python3.10/site-packages/numpy/__init__.py
Matplotlib 模块路径: /home/ubuntu/miniconda3/envs/peter/lib/python3.10/site-packages/matplotlib/__init__.py

练习 1.2:依次运行教师提供的两个代码块,让 Python 循环和 NumPy 向量化计算同一组 100 万个确定性数值的平方和。提交两项结果的一致性证据、本机两段耗时,并用一句话说明为何该耗时比不能推广到所有任务。评分只看运行证据与解释,不考查 NumPy API 的独立编写。

解答

教师脚手架(只运行核对,不评分 API)

import numpy as np  # 导入 NumPy 用于生成随机数和向量化计算
import time  # 导入 time 模块用于计时
numeric_values = np.linspace(-1.0, 1.0, 1_000_000)  # 构造可复现的确定性数值网格
start_time = time.time()  # 记录循环开始时间
sum_of_squares_loop = 0  # 初始化平方和为0
for number in numeric_values:  # 遍历每个数值
    sum_of_squares_loop += number ** 2  # 累加平方值
loop_elapsed = time.time() - start_time  # 计算循环耗时
print(f'循环结果: {sum_of_squares_loop:.2f}, 耗时: {loop_elapsed:.4f}秒')  # 输出结果和耗时
循环结果: 333334.00, 耗时: 0.2039秒
start_time = time.time()  # 记录向量化开始时间
sum_of_squares_vectorized = np.sum(numeric_values ** 2)  # 一行代码完成向量化平方和计算
vectorized_elapsed = time.time() - start_time  # 计算向量化耗时
vectorized_elapsed = max(vectorized_elapsed, 1e-9)  # 防止除以零(向量化极快时耗时可能为0)
np.testing.assert_allclose(sum_of_squares_loop, sum_of_squares_vectorized, rtol=1e-10)  # 先核对两种求和路径在浮点容差内一致
print(f'向量化结果: {sum_of_squares_vectorized:.2f}, 耗时: {vectorized_elapsed:.6f}秒')  # 输出结果和耗时
print(f'本机本次算子的耗时比: {loop_elapsed / vectorized_elapsed:.0f}倍')  # 只描述当前输入、硬件与平方和算子的测量结果
向量化结果: 333334.00, 耗时: 0.001032秒
本机本次算子的耗时比: 198倍

练习 1.3:运行教师提供的 stock_basic_data.h5 读取与汇总脚手架。根据输出报告各交易所普通股数量、最早上市的 5 家公司,并指出支撑答案的四个字段。评分只看是否正确读取输出、区分普通股口径并引用字段证据,不考查 read_hdf()、布尔筛选、日期转换或 Pandas 聚合 API。

解答

教师脚手架(只运行核对,不评分 API)

import pandas as pd  # 导入 Pandas 用于数据操作
stock_basic_data = pd.read_hdf(f'{DATA_ROOT}/stock/stock_basic_data.h5')  # 读取上市公司基本信息
required_columns = {'exchange', 'listed_date', 'symbol', 'order_book_id'}  # 声明完成题目所必需的字段集合
assert required_columns.issubset(stock_basic_data.columns)  # 在统计前验证本地数据合同,避免静默漏列

common_stock_df = stock_basic_data.loc[  # 仅保留普通股和题目所需字段以控制内存
    stock_basic_data['type'].eq('CS'),  # 用本地类型字段排除基金、指数等非普通股对象
    ['exchange', 'listed_date', 'symbol', 'order_book_id']  # 保留交易所、日期、名称与证券代码
].copy()  # 创建独立结果,避免后续日期转换修改原始表
common_stock_df['listed_date'] = pd.to_datetime(  # 统一上市日期类型以支持可靠排序
    common_stock_df['listed_date'], errors='coerce'  # 将无法解析的日期显式标为缺失,便于随后排除
)  # 完成上市日期的统一类型转换

exchange_counts = (  # 构造按交易所汇总的可读结果表
    common_stock_df['exchange']  # 选择交易所分类字段
    .value_counts()  # 统计每个交易所的普通股公司数
    .rename_axis('exchange')  # 为分类索引赋予业务名称
    .to_frame('company_count')  # 将计数 Series 转为便于展示的 DataFrame
)  # 完成交易所计数表
earliest_five = common_stock_df.dropna(subset=['listed_date']).nsmallest(  # 排除无日期记录后选择最早公司
    5, 'listed_date'  # 按上市日期选出最早的五条有效记录
)  # 完成最早上市公司结果表

print(exchange_counts)  # 输出各交易所普通股公司数以回答第一问
earliest_five  # 展示最早上市五家公司及其证券代码和日期以回答第二问
          company_count
exchange               
XSHE               3075
XSHG               2449
exchange listed_date symbol order_book_id
3 XSHE 1990-12-01 *ST国华 000004.XSHE
4 XSHE 1990-12-10 ST星源 000005.XSHE
3589 XSHG 1990-12-19 方正科技 600601.XSHG
3590 XSHG 1990-12-19 云赛智联 600602.XSHG
3639 XSHG 1990-12-19 飞乐音响 600651.XSHG

exchange_counts 给出各交易所普通股公司数;earliest_five 同时保留公司名称、证券代码和上市日期,因此完整回答了题目的两个子问。由于数据快照会更新,教材不硬编码某个计数,而要求代码在当前本地数据上重新计算。

练习 1.4:分别为下列任务选择首要工具,并从“数据规模—算子—内存—结果物化”四个维度说明理由。任务 A 是内存可容纳的 20 万行交互式清洗;任务 B 是对多份 Parquet 做列投影、过滤和 SQL 聚合且只返回百行结果;任务 C 是单机上对可流式执行的宽表做惰性表达式计算;任务 D 是已有 Python 函数必须跨多个分区并行执行。工具候选为 Pandas、DuckDB、Polars 与 Dask。

解答:任务 A 首选 Pandas,因为数据可放入内存、算子以交互式清洗为主且结果规模小;任务 B 首选 DuckDB,因为 SQL 聚合可直接下推到 Parquet,避免物化全部输入;任务 C 首选 Polars,因为惰性计划和可流式算子有机会控制单机内存,但仍须确认算子可流式且最终结果可容纳;任务 D 首选 Dask,因为任务需要保留 Python 函数并进行分区调度。四项选择都不是仅由行数决定:若算子不可下推、分区严重倾斜或最终结果本身过大,就必须重新设计任务并实测峰值内存。

练习 1.5:审查语句 TotalValue=price*100。指出命名、运算符空格、常量含义和业务意图方面的问题,并改写为能让读者理解“100”来源的代码。

解答TotalValue 不符合 PEP 8 的 snake_case 命名,运算符两侧缺空格,裸常量 100 没有单位或业务来源,变量名也没有说明计算的是持仓市值。可改写为:

share_price_cny = 12.50  # 用人民币计价的题设每股价格,明确数值单位
position_shares = 100  # 记录题设持股数量,避免无法解释的裸常量
position_value_cny = share_price_cny * position_shares  # 计算持仓市值并在名称中保留币种与业务含义
print(f'持仓市值: {position_value_cny:.2f} 元')  # 输出可人工复核的题设计算结果
持仓市值: 1250.00 元