11  Pandas 数据框内部操作

11.1 引言数据框操作的数据清洗价值

数据清洗(Data Cleaning)是数据分析中最耗时的环节,通常占据60-80%的时间。Pandas提供了丰富的数据框内部操作方法,使得数据清洗工作变得高效而可靠。

11.2 本章学习目标

通过本章学习,你将能够:

  1. 使用 read_csvindex_colusecols 参数高效读取外部数据,并在读取后用 dropna 完成第一道清洗
  2. 使用 shapeindexcolumnsdtypesmemory_usage 快速诊断一张新读入的数据框
  3. 区分 .loc(按标签,切片含端点)与 .iloc(按位置,切片不含端点),完成单值、切片与多行多列取数
  4. 使用 sort_indexsort_values(含多列、多方向)排序,使用布尔索引、isinquery(含 @ 变量引用)筛选数据
  5. 使用 applymap 对行、列与逐元素应用自定义函数

先修内容:第 章节 10 章的 Series 与 DataFrame 基础。

11.3 数据读取与基础操作

11.3.1 read_csv/read_excel参数详解

任务要求:从给定 CSV 地址读取 2022 年三只股票(syzg 三一重工、zgpa 中国平安、zgjz 中国建筑)的日度数据,把 date 列设为行索引、只读取所需列并丢弃缺失行;随后完成三问:输出全年交易日总数;用 .loc 取出 2022 年 3 月第一个交易日中国建筑的价格;按日期升序排序后输出前 8 行。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 11.1: 平台原始代码
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
#将第一列(date)设置为索引,只读取指定的列usecols
df=pd.read_csv("https://huoran.oss-cn-shenzhen.aliyuncs.com/20230306/csv/1632645776084066304.csv",index_col=0,usecols=['date','syzg','zgpa','zgjz']).dropna()

Trade_days=df.shape[0]  # 获取数据维度信息
print(f'2022年共有{Trade_days}个交易日')  # 输出2022年共有

stock_price=df.loc['2022-03-01','zgjz']  # 按标签索引提取数据
print(f'3月第一个交易日中国建筑价格为{stock_price}元')  # 输出3月第一个交易日中国建筑价格为

df_asc=df.sort_index(ascending=True)  # 按索引排序
print(df_asc.head(8))  # 输出前几行数据

预期输出(数据来自教学平台的 OSS 直链,本机实测判读要点如下;具体数值以平台运行结果为准):

  • 第一行输出 2022年共有N个交易日,N 为剔除缺失后的全年交易日数(作者实测环境为 242)
  • 第二行输出 3月第一个交易日中国建筑价格为X元,X 为 2022-03-01 的 zgjz 收盘价(作者实测为 5.16)
  • 随后输出按日期升序排列的前 8 行数据表,含 syzgzgpazgjz 三列,行索引为日期字符串

代码深度解析:

  1. index_col的作用:

    • 将某列设为行索引(通常是日期)
    • 便于时间序列分析
    • 减少内存占用(索引不重复存储)
  2. usecols的优势:

    • 内存效率: 只加载需要的列
    • 读取速度: 减少I/O操作
    • 代码清晰: 明确分析目标
  3. 链式操作:

    df = (pd.read_csv(...)
           .dropna()
           .sort_index())

11.3.2 数据框基础属性

列表 11.2: 查看DataFrame的常用属性
# =============================================================================
# 题目:查看DataFrame的基本属性和结构信息
# =============================================================================
# 本代码块演示如何快速了解DataFrame的基本情况。
# 金融应用场景:数据探索性分析(EDA),检查数据质量,确认数据覆盖范围。
# =============================================================================

# ==================== 数据框形状 ====================
print(f"数据框形状: {df.shape}")  # 返回元组(行数, 列数)
print(f"  - 行数(交易日): {df.shape[0]}")  # 交易日数量
print(f"  - 列数(股票): {df.shape[1]}")  # 股票数量

# ==================== 交易日数量 ====================
Trade_days = df.shape[0]  # 保存交易日数量到变量
print(f"\n2022年共有{Trade_days}个交易日")  # 显示交易日统计

# ==================== 索引信息 ====================
print(f"\n索引类型: {type(df.index)}")  # 显示索引类型(DatetimeIndex)
print(f"索引范围: {df.index.min()}{df.index.max()}")  # 显示日期范围
# 输出解释:显示数据涵盖的起始日期和结束日期

# ==================== 列名 ====================
print(f"\n列名(股票代码):")
print(df.columns.tolist())  # 将列名转换为列表显示
# 输出解释:syzg(三一重工)、zgpa(中国平安)、zgjz(中国建筑)

# ==================== 数据类型 ====================
print(f"\n数据类型:")
print(df.dtypes)  # 显示每列的数据类型
# 输出解释:所有列都是float64类型,表示价格数据

# ==================== 内存使用 ====================
print(f"\n内存使用:")
print(df.memory_usage(deep=True))  # 显示每列的内存占用
# 输出解释:deep=True深入计算字符串等对象的内存使用

11.4 数据选择与索引

11.4.1 loc基于标签的索引

loc使用标签(行/列名)进行选择,包含末端

列表 11.3: 使用loc进行标签索引
# =============================================================================
# 题目:使用loc基于标签选择数据
# =============================================================================
# 本代码块演示如何使用loc方法根据标签(日期、列名)选择数据。
# 金融应用场景:查询特定日期的股价,提取特定日期范围的数据,条件筛选。
# =============================================================================

# ==================== 获取特定日期的单个值 ====================
stock_price = df.loc['2022-03-01', 'zgjz']  # 使用loc选择特定行和列的交叉点
print(f"3月第一个交易日中国建筑价格: {stock_price}元")
# 输出解释:显示2022-03-01这一天zgjz(中国建筑)的收盘价

# ==================== 选择多行多列(切片) ====================
subset = df.loc[  # 使用loc选择数据子集
    '2022-03-01':'2022-03-05',  # 行切片:日期范围(包含两端)
    ['syzg', 'zgpa']  # 列选择:指定列名列表
]
print(f"\n3月1-5日,三一重工和中国平安的价格:")
print(subset)
# 输出解释:显示3月1日至5日(共5行),syzg和zgpa两列的数据
# 注意:loc的标签切片包含两端(与Python切片不同)

# ==================== 布尔条件(条件筛选) ====================
print(f"\n三一重工价格>20的交易日:")
syzg_high = df.loc[df.syzg > 20]  # 使用布尔条件选择行
print(syzg_high)
# 输出解释:显示所有syzg列价格大于20的行
# df.syzg > 20返回布尔Series,loc用它来筛选行

11.4.2 iloc基于位置的索引

iloc使用整数位置进行选择,不包含末端

列表 11.4: 使用iloc进行位置索引
# =============================================================================
# 题目:使用iloc基于整数位置选择数据
# =============================================================================
# 本代码块演示如何使用iloc方法根据整数位置选择数据。
# 金融应用场景:获取前N行数据,跳过表头,随机采样,批量处理。
# =============================================================================

# ==================== 前5行前2列 ====================
print("前5行前2列:")
print(df.iloc[:5, :2])  # 行位置0-4,列位置0-1
# 输出解释:显示前5个交易日,前2列股票的价格
# 注意:iloc的切片不包含末端(标准Python切片规则)

# ==================== 特定位置的单个值 ====================
print(f"\n第1行第1列: {df.iloc[0, 0]}")  # 第1行(位置0),第1列(位置0)
# 输出解释:显示第1个交易日,第1列股票的价格

# ==================== 负索引(倒数) ====================
print(f"\n倒数第3行:")
print(df.iloc[-3])  # 倒数第3行(所有列)
# 输出解释:显示倒数第3个交易日的所有股票价格
# 负数从末尾开始计数:-1是最后1行,-2是倒数第2行

11.5 数据排序

11.5.1 sort_index按索引排序

列表 11.5: 按索引排序DataFrame
# =============================================================================
# 题目:按索引(日期)排序DataFrame
# =============================================================================
# 本代码块演示如何按行索引排序数据。
# 金融应用场景:确保时间序列数据按时间顺序排列,这是时间序列分析的前提。
# =============================================================================

# ==================== 升序排序(默认) ====================
df_asc = df.sort_index(ascending=True)  # 按索引升序排序(早到晚)
print("升序排序(默认):")
print(df_asc.head(8))  # 显示前8行
# 输出解释:数据按日期从早到晚排列(2022-01-04在最前)

# ==================== 降序排序 ====================
df_desc = df.sort_index(ascending=False)  # 按索引降序排序(晚到早)
print(f"\n降序排序:")
print(df_desc.head(3))  # 显示前3行(实际是最晚的3天)
# 输出解释:数据按日期从晚到早排列(最新的日期在最前)

金融应用: 确保时间序列数据按时间顺序排列

11.5.2 sort_values按值排序

列表 11.6: 按列值排序DataFrame
# =============================================================================
# 题目:按列值排序DataFrame
# =============================================================================
# 本代码块演示如何按某列或多列的值排序。
# 金融应用场景:找出股价最高/最低的交易日,多维度排序分析。
# =============================================================================

# ==================== 按单列排序 ====================
print("按三一重工价格降序:")
df_sorted = df.sort_values('syzg', ascending=False)  # 按syzg列降序排序
print(df_sorted.head())  # 显示前几行
# 输出解释:syzg价格最高的交易日排在最前面

# ==================== 按多列排序 ====================
print(f"\n先按syzg升序,再按zgpa降序:")
df_multi_sort = df.sort_values(  # 按多列排序
    ['syzg', 'zgpa'],  # 排序依据的列
    ascending=[True, False]  # 分别指定排序方向
)
print(df_multi_sort.head())
# 输出解释:
# - 先按syzg升序(低到高)
# - syzg相同时,按zgpa降序(高到低)
# - 适用于多层级排序需求

11.6 数据筛选与查询

理论背景:数据质量原则

高质量数据分析的前提是高质量数据。数据清洗的目标:

  1. 完整性: 无缺失值或合理处理
  2. 一致性: 格式统一,单位一致
  3. 准确性: 值在合理范围内
  4. 唯一性: 无重复记录

本节的筛选与查询操作,正是逐一核对这四项目标的基本手段,例如用条件筛选找出取值异常的记录、用索引定位重复行。

11.6.1 条件筛选

列表 11.7: 多条件筛选DataFrame
# =============================================================================
# 题目:使用条件表达式筛选DataFrame数据
# =============================================================================
# 本代码块演示如何使用单个或多个条件筛选数据。
# 金融应用场景:找出股价突破关键点的交易日,多指标联合筛选。
# =============================================================================

# ==================== 单条件筛选 ====================
print("三一重工>20:")
print(df[df.syzg > 20].head())  # 使用布尔条件筛选
# 输出解释:显示syzg>20的所有行(前几行)
# df.syzg > 20返回布尔Series,用于筛选行

# ==================== 多条件(and) ====================
print(f"\n三一重工>20 且 中国平安>40:")
print(df[(df.syzg > 20) & (df.zgpa > 40)].head())
# 输出解释:显示同时满足两个条件的行
# &表示逻辑与(AND),两边条件必须同时满足
# 注意:每个条件必须用括号括起来

# ==================== 多条件(or) ====================
print(f"\n三一重工>30 或 中国平安>45:")
print(df[(df.syzg > 30) | (df.zgpa > 45)].head())
# 输出解释:显示满足至少一个条件的行
# |表示逻辑或(OR),两边条件满足一个即可

# ==================== 使用isin方法(成员检查) ====================
print(f"\n特定日期的数据:")
dates_to_check = ['2022-03-01', '2022-03-02', '2022-03-03']  # 日期列表
print(df.loc[dates_to_check])  # 选择索引在列表中的行
# 输出解释:显示这3个特定日期的数据
# 等价于df[df.index.isin(dates_to_check)]

11.6.2 query方法

query方法提供了更直观的查询语法:

列表 11.8: 使用query方法筛选数据
# =============================================================================
# 题目:使用query方法进行更直观的数据查询
# =============================================================================
# 本代码块演示query方法,它提供了类似SQL的查询语法。
# 金融应用场景:编写复杂的多条件查询,提高代码可读性。
# =============================================================================

# ==================== 简单条件 ====================
result1 = df.query('syzg > 20')  # 使用query方法筛选
print("syzg > 20:")
print(result1.head())
# 输出解释:与df[df.syzg > 20]结果相同
# 优势:语法更直观,不需要重复写df前缀

# ==================== 多条件 ====================
result2 = df.query('syzg > 20 and zgpa < 40')  # 多条件查询
print(f"\nsyzg > 20 and zgpa < 40:")
print(result2.head())
# 输出解释:显示同时满足两个条件的行
# 优势:使用and/or/not代替&/|/~,更符合自然语言

# ==================== 引用变量 ====================
threshold = 25  # 定义阈值变量
result3 = df.query('syzg > @threshold')  # 使用@引用外部变量
print(f"\nsyzg > {threshold}:")
print(result3.head())
# 输出解释:显示syzg > 25的行
# @符号告诉query在Python环境中查找变量,而不是列名

11.7 数据变换

11.7.1 apply与map

列表 11.9: 使用apply和map进行数据变换
# =============================================================================
# 题目:使用apply和map方法进行数据变换
# =============================================================================
# 本代码块演示如何对DataFrame和Series应用自定义函数。
# 金融应用场景:计算价格区间、分类评级、应用复杂业务逻辑。
# =============================================================================

# ==================== apply:对行或列应用函数 ====================
print("每列的极差:")
print(df.apply(lambda x: x.max() - x.min()))  # 对每列应用lambda函数
# 输出解释:显示每列的最大值与最小值之差(价格波动范围)
# apply默认对列操作(axis=0)

print(f"\n每行的均值:")
print(df.apply(lambda x: x.mean(), axis=1).head())  # 对每行应用函数
# 输出解释:显示每个交易日3只股票的平均价格
# axis=1表示沿行方向操作(跨列)

# ==================== map:对Series的每个元素应用函数 ====================
print(f"\n将价格分类:")

# ------------------- 定义分类函数 -------------------
def categorize_price(price):  # 定义价格分类函数
    """
    根据价格将股票分类为高、中、低三档
    参数:
        price: 股票价格
    返回:
        '高'/'中'/'低': 价格等级
    """
    if price > 30:  # 价格大于30
        return '高'  # 高价股
    elif price > 20:  # 价格在20-30之间
        return '中'  # 中价股
    else:  # 价格小于等于20
        return '低'  # 低价股

# ------------------- 应用分类函数 -------------------
syzg_category = df['syzg'].map(categorize_price)  # 对syzg列的每个元素应用函数
print(syzg_category.head(10))  # 显示前10个分类结果
# 输出解释:每个价格被转换为'高'/'中'/'低'三类之一
# map只用于Series,对每个元素逐个应用函数

11.8 本章小结

要点:

  • index_col=0 把首列设为行索引,usecols 只加载需要的列,链式 .dropna() 完成读取后清洗
  • .loc 按标签取数且切片含端点;.iloc 按位置取数且切片不含端点
  • sort_index 排时间轴,sort_values 排数值,多列排序可分别指定方向
  • 多条件布尔筛选用 &/| 且每个条件要加括号;query 支持 and/or@变量 引用

易错点:

  • (df.syzg > 20) & (df.zgpa > 40) 少写括号会因运算优先级报错
  • df.query('syzg > @threshold') 漏写 @ 会把 threshold 当作列名查找
  • 日期以字符串存储时,df.loc['2022-03-01'] 依赖标签精确匹配,索引未排序或格式不符都取不到

11.9 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。

    import pandas as pd
    
    df = pd.DataFrame({'price': [10, 11, 12, 13]}, index=['d1', 'd2', 'd3', 'd4'])
    print(df.loc['d2'])
    print(df.iloc[1])
    print(df.loc['d1':'d3', 'price'])

    参考答案(先写下你的预测再点开)

    解题思路:逐行推演。第 1 行 df.loc['d2'] 按标签取一,注意取行的结果不再是 DataFrame 而是一个 Series:索引变成原来的列名 price,值为 11,行标签 d2 成为该 Series 的名字。第 2 行 df.iloc[1] 按位置取第 2 行(位置 1),同样是 d2 这一行,输出与第 1 行完全一样——标签 d2 恰好位于位置 1。第 3 行 df.loc['d1':'d3', 'price'] 同时给出行切片与列名:.loc 的标签切片含两端,取 d1d2d3 三行的 price 列,结果是名为 price 的 Series(索引是行标签)。若把这行改成 df.iloc[0:3, 0],位置切片不含末端,同样取位置 0、1、2 三行——两种切片规则不同、结果恰好一致,依据却完全不同。

    # 验证脚本:逐行输出三个表达式的求值结果
    import pandas as pd  # 导入Pandas库
    
    df = pd.DataFrame({'price': [10, 11, 12, 13]}, index=['d1', 'd2', 'd3', 'd4'])  # 构造带标签索引的单列数据框
    print(df.loc['d2'])  # 按标签取一行,返回Series
    print(df.iloc[1])  # 按位置取第2行,返回Series
    print(df.loc['d1':'d3', 'price'])  # 标签切片含两端,取出三行的price列

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    price    11
    Name: d2, dtype: int64
    price    11
    Name: d2, dtype: int64
    d1    10
    d2    11
    d3    12
    Name: price, dtype: int64

    回扣本章:对应本章小结“要点”第 2 条——.loc 按标签取数且切片含端点,.iloc 按位置取数且切片不含端点,两套规则不要混。

  2. 概念辨析:df.loc['d1':'d3']df.iloc[0:3] 的端点语义有何不同?df[df.syzg > 20]df.query('syzg > 20') 两种筛选写法各有什么优劣?

    参考答案(点开前请先独立完成)

    解题思路:逐点作答。第一,端点语义:df.loc['d1':'d3']标签切片,右端点 d3 取得到(含两端,共 3 行);df.iloc[0:3]位置切片,右端点位置 3 取不到(标准 Python 切片,取位置 0、1、2,同为 3 行)。两者在本例结果相同,但依据不同:loc 到索引里“查名字”,iloc 按“第几个”数位置;一旦行索引乱序或标签是整数,两种写法就可能取到不同的行。第二,两种筛选写法:df[df.syzg > 20] 是原生布尔索引,条件本身是普通的 pandas 表达式,调试直观;但多条件必须用 &| 且每个条件加括号,条件一长可读性下降。df.query('syzg > 20') 用类 SQL 的字符串语法,多条件可写 and/or/not,还支持 @变量 引用外部阈值(如 df.query('syzg > @threshold')),长条件与参数化场景更清晰;代价是条件写在字符串里,列名或变量名拼错要到运行时才报错。结论:单条件两者皆可;多条件、需要把阈值抽成变量时 query 更合适。

    回扣本章:对应本章小结“要点”第 4 条与“易错点”第 1、2 条——多条件布尔筛选用 &/| 且每个条件要加括号;query 支持 and/or@变量 引用。

  3. 变式任务(平台任务同型改造):在平台数据上用一条链式语句完成”筛出三一重工大于 20 且中国平安大于 40 的交易日,按 zgjz 降序排序,只保留 syzgzgjz 两列”。

    参考答案(点开前请先独立完成)

    解题思路:在 列表 11.1 读入的数据框 df 上操作,一条链式语句分三步:(df['syzg'] > 20) & (df['zgpa'] > 40) 两个布尔条件用 & 连接(每个条件都加括号,避免运算优先级报错);把布尔条件放在 .loc[条件, 列列表] 的行位置,列位置给 ['syzg', 'zgjz'] 完成选列;最后接 .sort_values('zgjz', ascending=False) 按中国建筑价格降序。筛选后的行数用 .shape[0] 核对。

    # 变式程序:一条链式语句完成筛选、选列与排序(先读入与 @lst-ch11-platform-task 同源的 OSS 数据,需联网)
    import pandas as pd  # 导入Pandas库
    df = pd.read_csv('https://huoran.oss-cn-shenzhen.aliyuncs.com/20230306/csv/1632645776084066304.csv', index_col=0, usecols=['date', 'syzg', 'zgpa', 'zgjz']).dropna()  # 读入中国建筑2022年日线数据
    result = df.loc[(df['syzg'] > 20) & (df['zgpa'] > 40), ['syzg', 'zgjz']].sort_values('zgjz', ascending=False)  # 布尔条件筛行,列表选列,再按zgjz降序
    print(result.head())  # 查看zgjz最高的前5个交易日
    print('共筛选出{}个交易日'.format(result.shape[0]))  # 输出满足条件的交易日总数

    预期输出(OSS 直链本机实测,拉取日期 2026-08-27;若链接失效以教学平台内置数据为准,具体数值以平台运行结果为准):

                 syzg  zgjz
    date
    2022-02-11  20.82  5.60
    2022-02-18  20.89  5.50
    2022-02-08  21.25  5.49
    2022-02-07  20.74  5.48
    2022-02-10  21.21  5.46
    共筛选出34个交易日

    注意:以上为本题变式的独立代码;列表 11.1 对应的平台原始代码仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应本章小结“要点”第 4 条与“易错点”第 1 条——多条件筛选用 & 且每个条件加括号,.loc[条件, 列列表] 一句完成筛行选列。

  4. 动手验证:构造一个 4 行 2 列、含两个缺失值的小数据框,分别运行 dropna()dropna(how='all')dropna(subset=[某一列]),比较三者的删除行为。

    参考答案(点开前请先独立完成)

    解题思路:构造 4 行 2 列小表,让两列各缺 1 个值且错开位置(d2 缺成交量、d3 缺收盘价),这样三种删除行为能清楚区分。dropna() 默认 how='any':行内任一列缺失即删,d2d3 都被删掉,只剩 d1d4dropna(how='all'):只有整行全缺才删,本例没有全空行,4 行全部保留——这是最宽松的口径,常用于剔除全空行。dropna(subset=['收盘价']):只看指定列的缺失,其他列不影响行去留——d3 因收盘价缺失被删,d2 虽缺成交量但被保留。三者的差别在于“看哪些列、缺到什么程度才删”。

    # 验证脚本:对比三种dropna口径的删除行为
    import numpy as np  # 导入NumPy库,用于生成NaN缺失值
    import pandas as pd  # 导入Pandas库
    
    df_check = pd.DataFrame({'收盘价': [10.0, 11.0, np.nan, 13.0], '成交量': [100.0, np.nan, 300.0, 400.0]}, index=['d1', 'd2', 'd3', 'd4'])  # 4行2列,收盘价与成交量各缺1个
    print(df_check)  # 先看原始数据:d2缺成交量,d3缺收盘价
    print(df_check.dropna())  # 默认how='any':任一列缺失的行都被删除
    print(df_check.dropna(how='all'))  # how='all':只有整行全缺才删除,本例4行都保留
    print(df_check.dropna(subset=['收盘价']))  # subset:只按收盘价列判断,d3被删,d2保留

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

         收盘价    成交量
    d1  10.0  100.0
    d2  11.0    NaN
    d3   NaN  300.0
    d4  13.0  400.0
         收盘价    成交量
    d1  10.0  100.0
    d4  13.0  400.0
         收盘价    成交量
    d1  10.0  100.0
    d2  11.0    NaN
    d3   NaN  300.0
    d4  13.0  400.0
         收盘价    成交量
    d1  10.0  100.0
    d2  11.0    NaN
    d4  13.0  400.0

    回扣本章:对应本章小结“要点”第 1 条——链式 .dropna() 是读取后的第一道清洗,howsubset 共同决定删除口径。