最后更新:2026-03-27
官方文档:https://pandas.pydata.org/docs/
适用版本:pandas 2.x(2026-05-08 核实)
相关文档:SQLAlchemy完全指南 MySQL基础完全指南
目录
- Pandas完全指南 · 安装
- Pandas完全指南 · 核心数据结构
- Pandas完全指南 · 数据读写
- Pandas完全指南 · 数据查看
- Pandas完全指南 · 数据选取
- Pandas完全指南 · 数据清洗
- Pandas完全指南 · 数据变换
- Pandas完全指南 · 字符串操作
- Pandas完全指南 · 日期时间操作
- Pandas完全指南 · 分组聚合
- Pandas完全指南 · 合并与连接
- Pandas完全指南 · 透视与重塑
- Pandas完全指南 · 排序
- Pandas完全指南 · 窗口函数
- Pandas完全指南 · 性能优化
- Pandas完全指南 · 常用配合库
- Pandas完全指南 · 最佳实践
- Pandas完全指南 · 应用场景
- Pandas完全指南 · 常见陷阱与注意事项
安装
# 基础安装
pip install pandas
# 常用可选依赖
pip install openpyxl # Excel 读写(.xlsx)
pip install xlrd # Excel 读取旧格式(.xls)
pip install pyarrow # Parquet 读写 / Arrow backend(pandas 2.0+)
pip install fastparquet # Parquet 读写(另一实现)
pip install sqlalchemy # read_sql / to_sql
pip install lxml # read_html / XML 解析
pip install beautifulsoup4 # read_html HTML 解析备选
pip install tables # HDF5 读写(PyTables)
pip install fsspec # 远程文件系统(S3/GCS)
# 一次性安装所有常用依赖
pip install "pandas[all]"
版本确认:
import pandas as pd
print(pd.__version__) # 例:2.2.0
核心数据结构
Series
Series 是带标签的一维数组,可存储任意类型数据。
创建 Series
import pandas as pd
import numpy as np
# 从列表创建
s = pd.Series([10, 20, 30, 40])
# 从字典创建(键成为索引)
s = pd.Series({"a": 1, "b": 2, "c": 3})
# 指定索引
s = pd.Series([10, 20, 30], index=["x", "y", "z"], name="score")
# 从标量广播
s = pd.Series(5, index=range(5))
# 从 NumPy 数组
s = pd.Series(np.arange(5), dtype="float32")
常用属性
| 属性 |
类型 |
说明 |
index |
Index |
索引对象 |
values |
ndarray 或 ExtensionArray |
底层数据数组 |
dtype |
dtype |
元素数据类型 |
shape |
tuple |
形状,如 (5,) |
name |
hashable |
Series 名称,合并时会成为列名 |
size |
int |
元素总数 |
nbytes |
int |
占用内存字节数 |
is_monotonic_increasing |
bool |
是否单调递增 |
is_unique |
bool |
是否所有值唯一 |
s = pd.Series([10, 20, 30], index=["a", "b", "c"], name="score")
print(s.index) # Index(['a', 'b', 'c'], dtype='object')
print(s.values) # [10 20 30]
print(s.dtype) # int64
print(s.shape) # (3,)
print(s.name) # score
DataFrame
DataFrame 是带标签的二维表格结构,每列可以是不同类型。
创建 DataFrame
# 从字典(最常用)
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"age": [25, 30, 35],
"score": [88.5, 92.0, 78.3],
})
# 从列表套字典
df = pd.DataFrame([
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30},
])
# 从二维列表(需指定列名)
df = pd.DataFrame(
[[1, 2, 3], [4, 5, 6]],
columns=["a", "b", "c"],
index=["row1", "row2"],
)
# 从 NumPy ndarray
arr = np.random.randn(4, 3)
df = pd.DataFrame(arr, columns=["x", "y", "z"])
# 从 records(列表套元组)
df = pd.DataFrame.from_records(
[(1, "Alice"), (2, "Bob")],
columns=["id", "name"],
)
# 从 CSV 文件(见数据读写章节)
df = pd.read_csv("data.csv")
常用属性
| 属性/方法 |
说明 |
columns |
列标签 Index 对象 |
index |
行标签 Index 对象 |
dtypes |
每列的数据类型(Series) |
shape |
(行数, 列数) |
size |
元素总数(行数 × 列数) |
ndim |
维度,始终为 2 |
info() |
打印摘要信息(列名、非空数、类型、内存) |
describe() |
数值列统计摘要(count/mean/std/min/25%/50%/75%/max) |
memory_usage() |
每列内存占用(字节) |
df.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 3 entries, 0 to 2
# Data columns (total 3 columns): ...
df.describe()
# age score
# count 3.0 3.000000
# mean 30.0 86.266667
# ...
print(df.dtypes)
# name object
# age int64
# score float64
数据读写
read_csv()
读取 CSV 或文本文件。
df = pd.read_csv("data.csv")
df = pd.read_csv("data.csv", sep="\t", encoding="utf-8", nrows=1000)
| 参数 |
类型 |
默认值 |
说明 |
filepath_or_buffer |
str / path / IO |
必填 |
文件路径、URL 或文件对象 |
sep |
str |
',' |
字段分隔符,支持正则;\t 为制表符 |
delimiter |
str |
None |
sep 的别名 |
header |
int / list / None |
'infer' |
作为列名的行号;None 表示无表头 |
names |
list |
None |
自定义列名列表;配合 header=0 可替换原表头 |
index_col |
int / str / list / False |
None |
作为行索引的列;False 强制不使用索引列 |
usecols |
list / callable |
None |
只读取指定列,可减少内存占用 |
dtype |
dict / str |
None |
指定各列类型,如 {"id": int, "name": str} |
parse_dates |
bool / list / dict |
False |
解析日期列;True 尝试解析索引 |
infer_datetime_format |
bool |
False |
自动推断日期格式(已在 2.0 废弃) |
nrows |
int |
None |
只读取前 N 行 |
skiprows |
int / list / callable |
None |
跳过指定行(行号列表或前 N 行) |
skipfooter |
int |
0 |
跳过末尾 N 行 |
encoding |
str |
None |
文件编码,如 'utf-8'、'gbk' |
na_values |
str / list / dict |
None |
额外视为 NaN 的值 |
keep_default_na |
bool |
True |
是否保留默认 NaN 识别集 |
chunksize |
int |
None |
分块读取,返回 TextFileReader 迭代器 |
compression |
str |
'infer' |
压缩格式:'gzip'、'bz2'、'zip'、'xz' |
thousands |
str |
None |
千位分隔符,如 ',' |
decimal |
str |
'.' |
小数点符号 |
comment |
str |
None |
注释行前缀字符 |
low_memory |
bool |
True |
分块推断类型(可能导致混合类型),建议指定 dtype |
engine |
str |
None |
解析引擎:'c'、'python'、'pyarrow' |
# 分块读取大文件
for chunk in pd.read_csv("big.csv", chunksize=10000):
process(chunk)
# 只读取指定列,并指定类型
df = pd.read_csv(
"sales.csv",
usecols=["date", "amount", "region"],
dtype={"amount": "float32", "region": "category"},
parse_dates=["date"],
)
read_excel()
读取 Excel 文件(.xlsx 需要 openpyxl,.xls 需要 xlrd)。
df = pd.read_excel("data.xlsx", sheet_name="Sheet1")
| 参数 |
类型 |
默认值 |
说明 |
io |
str / path / IO |
必填 |
文件路径或文件对象 |
sheet_name |
str / int / list / None |
0 |
工作表名或序号;None 读取全部,返回 dict |
header |
int / list |
0 |
表头行号 |
names |
list |
None |
自定义列名 |
index_col |
int / list |
None |
索引列 |
usecols |
str / list / callable |
None |
指定列,支持 Excel 列字母如 "A:D" |
dtype |
dict |
None |
各列类型 |
nrows |
int |
None |
读取行数 |
skiprows |
int / list |
None |
跳过行 |
na_values |
str / list / dict |
None |
额外 NaN 值 |
parse_dates |
bool / list |
False |
解析日期列 |
engine |
str |
None |
'openpyxl'(xlsx)、'xlrd'(xls) |
# 读取多个 sheet
sheets = pd.read_excel("data.xlsx", sheet_name=None) # 返回 {name: df}
# 读取指定列范围
df = pd.read_excel("data.xlsx", usecols="A:E", nrows=100)
read_json()
读取 JSON 文件或字符串。
df = pd.read_json("data.json")
df = pd.read_json('{"a":[1,2],"b":[3,4]}')
| 参数 |
类型 |
默认值 |
说明 |
path_or_buf |
str / path / IO |
必填 |
文件路径、URL 或 JSON 字符串 |
orient |
str |
None |
JSON 结构方向:'split'、'records'、'index'、'columns'、'values'、'table' |
dtype |
bool / dict |
True |
类型推断或指定类型 |
lines |
bool |
False |
每行一个 JSON 对象(JSONL/NDJSON 格式) |
chunksize |
int |
None |
配合 lines=True 分块读取 |
encoding |
str |
'utf-8' |
文件编码 |
compression |
str |
'infer' |
压缩格式 |
# 读取 JSONL 格式(每行一个 JSON 对象)
df = pd.read_json("data.jsonl", lines=True)
# orient='records' 对应列表套字典格式
df = pd.read_json('[{"name":"Alice","age":25}]', orient="records")
read_parquet()
读取 Parquet 列式存储文件(需要 pyarrow 或 fastparquet)。
df = pd.read_parquet("data.parquet")
| 参数 |
类型 |
默认值 |
说明 |
path |
str / path |
必填 |
文件路径或目录 |
engine |
str |
'auto' |
'pyarrow' 或 'fastparquet' |
columns |
list |
None |
只读取指定列 |
filters |
list |
None |
行级过滤,格式 [("col", "op", value)] |
storage_options |
dict |
None |
远程存储选项(S3 等) |
# 只读取部分列(Parquet 支持列裁剪,效率高)
df = pd.read_parquet("data.parquet", columns=["id", "amount"])
# 带过滤条件(分区裁剪)
df = pd.read_parquet("data.parquet", filters=[("year", "==", 2024)])
read_sql() / read_sql_query()
从数据库读取数据(需要 SQLAlchemy)。
from sqlalchemy import create_engine
engine = create_engine("postgresql+psycopg2://user:pass@localhost/db")
df = pd.read_sql("SELECT * FROM users", con=engine)
df = pd.read_sql_query("SELECT id, name FROM users WHERE age > 25", con=engine)
df = pd.read_sql_table("users", con=engine)
| 参数 |
类型 |
默认值 |
说明 |
sql |
str / SQLAlchemy Selectable |
必填 |
SQL 查询语句或表名 |
con |
SQLAlchemy engine / DBAPI2 connection |
必填 |
数据库连接 |
index_col |
str / list |
None |
作为索引的列 |
coerce_float |
bool |
True |
将非字符串数值转为浮点 |
params |
list / dict |
None |
SQL 参数绑定 |
parse_dates |
list / dict |
None |
解析日期列 |
columns |
list |
None |
仅对 read_sql_table 有效,指定列 |
chunksize |
int |
None |
分块读取 |
写出函数
to_csv()
| 参数 |
类型 |
默认值 |
说明 |
path_or_buf |
str / path / IO |
None |
输出路径;None 返回字符串 |
sep |
str |
',' |
分隔符 |
na_rep |
str |
'' |
NaN 的替换字符串 |
columns |
list |
None |
指定输出列 |
header |
bool / list |
True |
是否写出列名 |
index |
bool |
True |
是否写出行索引 |
index_label |
str / list |
None |
索引列的列名 |
mode |
str |
'w' |
写入模式,'a' 追加 |
encoding |
str |
'utf-8' |
文件编码 |
compression |
str / dict |
'infer' |
压缩格式 |
float_format |
str |
None |
浮点数格式,如 '%.2f' |
date_format |
str |
None |
日期格式,如 '%Y-%m-%d' |
chunksize |
int |
None |
分块写出 |
df.to_csv("output.csv", index=False, encoding="utf-8-sig") # utf-8-sig 避免 Excel 乱码
to_excel()
| 参数 |
类型 |
默认值 |
说明 |
excel_writer |
str / ExcelWriter |
必填 |
文件路径或 ExcelWriter 对象 |
sheet_name |
str |
'Sheet1' |
工作表名 |
na_rep |
str |
'' |
NaN 替换 |
columns |
list |
None |
指定列 |
header |
bool / list |
True |
是否写出表头 |
index |
bool |
True |
是否写出索引 |
startrow |
int |
0 |
起始行(0-based) |
startcol |
int |
0 |
起始列(0-based) |
engine |
str |
None |
'openpyxl'、'xlsxwriter' |
float_format |
str |
None |
浮点数格式 |
freeze_panes |
tuple |
None |
冻结窗格,如 (1, 0) 冻结首行 |
# 多 sheet 写出
with pd.ExcelWriter("output.xlsx", engine="openpyxl") as writer:
df1.to_excel(writer, sheet_name="销售", index=False)
df2.to_excel(writer, sheet_name="库存", index=False)
to_parquet()
| 参数 |
类型 |
默认值 |
说明 |
path |
str / path |
None |
输出路径;None 返回字节 |
engine |
str |
'auto' |
'pyarrow' 或 'fastparquet' |
compression |
str |
'snappy' |
压缩:'snappy'、'gzip'、'brotli'、None |
index |
bool |
None |
是否写出索引 |
partition_cols |
list |
None |
按列分区存储 |
storage_options |
dict |
None |
远程存储选项 |
to_json()
| 参数 |
类型 |
默认值 |
说明 |
path_or_buf |
str / path / IO |
None |
输出路径;None 返回字符串 |
orient |
str |
视对象而定 |
JSON 结构:'split'、'records'、'index'、'columns'、'values'、'table' |
date_format |
str |
'epoch' |
日期格式:'epoch'(毫秒时间戳)或 'iso' |
double_precision |
int |
10 |
浮点数精度 |
force_ascii |
bool |
True |
强制 ASCII 编码;False 保留 Unicode |
indent |
int |
None |
JSON 缩进空格数 |
lines |
bool |
False |
每行一个 JSON 对象(JSONL) |
compression |
str / dict |
'infer' |
压缩格式 |
# 输出 JSONL 格式(常用于流式处理)
df.to_json("output.jsonl", orient="records", lines=True, force_ascii=False)
数据查看
head() / tail() / sample()
df.head(5) # 前 5 行(默认)
df.tail(10) # 后 10 行
df.sample(5) # 随机 5 行
df.sample(frac=0.1, random_state=42) # 随机 10%,固定随机种子
| 方法 |
参数 |
默认值 |
说明 |
head(n) |
n: int |
5 |
前 N 行 |
tail(n) |
n: int |
5 |
后 N 行 |
sample(n) |
n: int |
1 |
随机 N 行 |
sample(frac) |
frac: float |
None |
随机比例,与 n 互斥 |
sample(replace) |
replace: bool |
False |
是否有放回抽样 |
sample(random_state) |
random_state: int |
None |
随机种子 |
info() / describe() / value_counts()
df.info() # 列名、非空数、类型、内存
df.info(memory_usage="deep") # 精确内存统计
df.describe() # 数值列统计摘要
df.describe(include="all") # 包含字符串/category 列
df.describe(include="object")
df["city"].value_counts() # 频次统计,降序
df["city"].value_counts(normalize=True) # 比例
df["city"].value_counts(dropna=False) # 包含 NaN
shape / dtypes / columns / index
df.shape # (100, 5) — (行数, 列数)
df.dtypes # 每列数据类型
df.columns # Index(['a', 'b', 'c'])
df.index # RangeIndex(start=0, stop=100, step=1)
# 修改列名
df.columns = ["col1", "col2", "col3"]
# 列名转列表
cols = df.columns.tolist()
数据选取
这是 pandas 使用最频繁的核心章节。
[] 基础选取
# 选取单列 -> 返回 Series
s = df["name"]
# 选取多列 -> 返回 DataFrame
sub = df"name", "age"
# 行切片(仅支持整数或标签切片)
df[0:5] # 前 5 行(整数切片,不含第 5 行)
df["a":"c"] # 标签切片,含端点(适用于有字符串索引的 df)
注意:[] 用于行切片时是基于位置(整数)或标签,行为依索引类型而定,容易混淆,建议优先使用 loc / iloc。
loc[]
基于标签(行索引名和列名)选取数据。
# 单行
df.loc[0]
df.loc["alice"]
# 单个值
df.loc[0, "name"]
# 行范围(含端点)
df.loc[0:5]
df.loc["2024-01-01":"2024-01-31"]
# 行 + 列
df.loc[0:5, "name":"age"]
df.loc[[0, 2, 4], ["name", "score"]]
# 布尔数组
mask = df["age"] > 25
df.loc[mask]
df.loc[mask, "name"]
# 赋值(推荐方式)
df.loc[0, "score"] = 99.0
df.loc[df["age"] > 30, "grade"] = "senior"
| 参数 |
类型 |
说明 |
row_indexer |
标签 / 标签列表 / 标签切片 / 布尔数组 / callable |
行选择器 |
col_indexer |
标签 / 标签列表 / 标签切片 / 布尔数组 / callable |
列选择器(可省略) |
iloc[]
基于整数位置选取数据(0-based,不含末端)。
# 单行
df.iloc[0]
df.iloc[-1] # 最后一行
# 单个值
df.iloc[0, 2] # 第 0 行第 2 列
# 行范围(不含末端)
df.iloc[0:5]
df.iloc[::2] # 每隔一行
# 行 + 列
df.iloc[0:5, 0:3]
df.iloc[[0, 2, 4], [1, 3]]
# 赋值
df.iloc[0, 1] = 100
| 参数 |
类型 |
说明 |
row_indexer |
int / int 列表 / int 切片 / 布尔数组 / callable |
行位置选择器 |
col_indexer |
int / int 列表 / int 切片 / 布尔数组 / callable |
列位置选择器(可省略) |
at[] / iat[]
单值快速访问,比 loc/iloc 更快(无需额外开销)。
df.at[0, "name"] # 标签访问单值
df.iat[0, 2] # 位置访问单值
df.at[0, "score"] = 95 # 赋值
df.iat[0, 2] = 95
| 方法 |
参数 |
说明 |
at[row_label, col_label] |
行标签, 列标签 |
基于标签的单值访问 |
iat[row_pos, col_pos] |
行位置, 列位置 |
基于整数位置的单值访问 |
布尔索引
# 单条件
df[df["age"] > 25]
# 多条件(必须用括号和位运算符)
df[(df["age"] > 25) & (df["city"] == "北京")]
df[(df["score"] < 60) | (df["score"] > 90)]
df[~(df["city"] == "北京")] # 取反
# isin(IN 过滤)
df[df["city"].isin(["北京", "上海", "广州"])]
# between(区间过滤,含端点)
df[df["age"].between(20, 30)]
# str.contains(模糊匹配)
df[df["name"].str.contains("张")]
# notna / isna
df[df["score"].notna()]
query()
使用字符串表达式过滤,语法更简洁,支持链式调用。
df.query("age > 25")
df.query("city == '北京' and score >= 80")
df.query("city in ['北京', '上海']")
df.query("age.between(20, 30)") # 需要 numexpr
# 引用外部变量(加 @ 前缀)
threshold = 25
df.query("age > @threshold")
# 列名含空格时用反引号
df.query("`first name` == 'Alice'")
| 参数 |
类型 |
默认值 |
说明 |
expr |
str |
必填 |
过滤表达式字符串 |
inplace |
bool |
False |
是否原地修改 |
**kwargs |
- |
- |
传递给 numexpr.evaluate 的额外参数 |
多条件过滤总结
# 方法一:布尔索引(最通用)
result = df[(df["age"] > 25) & (df["score"] > 80) & (df["city"] == "北京")]
# 方法二:query(最简洁)
result = df.query("age > 25 and score > 80 and city == '北京'")
# 方法三:np.logical_and.reduce(多条件组合)
import numpy as np
conditions = [df["age"] > 25, df["score"] > 80, df["city"] == "北京"]
result = df[np.logical_and.reduce(conditions)]
数据清洗
缺失值处理
isna() / notna()
df.isna() # 逐元素 True/False
df.isna().sum() # 每列缺失数量
df.isna().sum() / len(df) # 缺失比例
df["score"].isna()
df["score"].notna()
fillna()
填充缺失值。
| 参数 |
类型 |
默认值 |
说明 |
value |
scalar / dict / Series / DataFrame |
None |
填充值;dict 可按列指定不同值 |
method |
str |
None |
'ffill'(前向填充)、'bfill'(后向填充);pandas 2.0 建议用 ffill()/bfill() |
axis |
int / str |
0 |
0/'index' 按行,1/'columns' 按列 |
inplace |
bool |
False |
是否原地修改 |
limit |
int |
None |
最多连续填充 N 个缺失值 |
df.fillna(0)
df.fillna({"age": 0, "score": df["score"].mean()})
df["score"].fillna(df["score"].median(), inplace=True)
df.ffill() # 前向填充(推荐替代 method='ffill')
df.bfill() # 后向填充
dropna()
删除含缺失值的行或列。
| 参数 |
类型 |
默认值 |
说明 |
axis |
int / str |
0 |
0 删行,1 删列 |
how |
str |
'any' |
'any':含任意 NaN 则删;'all':全为 NaN 才删 |
thresh |
int |
None |
至少有 N 个非 NaN 才保留 |
subset |
list |
None |
只检查指定列 |
inplace |
bool |
False |
是否原地修改 |
df.dropna() # 删除含任意 NaN 的行
df.dropna(subset=["name", "score"]) # 只检查这两列
df.dropna(how="all") # 全为 NaN 才删
df.dropna(thresh=3) # 保留至少有 3 个非 NaN 的行
重复值处理
duplicated()
| 参数 |
类型 |
默认值 |
说明 |
subset |
list / str |
None |
用于判断重复的列;None 使用全部列 |
keep |
str / False |
'first' |
'first':保留首次;'last':保留末次;False:标记所有重复 |
df.duplicated() # 返回布尔 Series
df.duplicated(subset=["name"]) # 只按 name 判断重复
df[df.duplicated()] # 查看重复行
df.duplicated().sum() # 重复行数量
drop_duplicates()
| 参数 |
类型 |
默认值 |
说明 |
subset |
list / str |
None |
判断重复的列 |
keep |
str / False |
'first' |
保留策略 |
inplace |
bool |
False |
是否原地修改 |
ignore_index |
bool |
False |
是否重置索引 |
df.drop_duplicates()
df.drop_duplicates(subset=["name", "date"], keep="last")
类型转换
astype()
| 参数 |
类型 |
默认值 |
说明 |
dtype |
str / dtype / dict |
必填 |
目标类型;dict 可按列指定 |
copy |
bool |
True |
是否返回副本(pandas 2.0 CoW 下效果变化) |
errors |
str |
'raise' |
'raise':报错;'ignore':出错时返回原数据 |
df["age"].astype(int)
df["price"].astype("float32")
df["category"].astype("category")
df.astype({"age": int, "score": "float32"})
pd.to_numeric()
| 参数 |
类型 |
默认值 |
说明 |
arg |
scalar / list / Series |
必填 |
要转换的数据 |
errors |
str |
'raise' |
'raise'、'coerce'(转 NaN)、'ignore'(返回原值) |
downcast |
str |
None |
'integer'、'signed'、'unsigned'、'float' 降级类型 |
pd.to_numeric(df["score"], errors="coerce") # 无法转换的变为 NaN
pd.to_datetime()
| 参数 |
类型 |
默认值 |
说明 |
arg |
str / int / list / Series / DataFrame |
必填 |
要转换的数据 |
errors |
str |
'raise' |
'raise'、'coerce'、'ignore' |
format |
str |
None |
日期格式字符串,如 '%Y-%m-%d';pandas 2.0+ 推荐显式指定 |
unit |
str |
None |
时间戳单位:'s'、'ms'、'us'、'ns' |
utc |
bool |
False |
是否转为 UTC 时区 |
dayfirst |
bool |
False |
是否优先解析为日在前的格式 |
pd.to_datetime(df["date"])
pd.to_datetime(df["date"], format="%Y-%m-%d")
pd.to_datetime(df["ts"], unit="s") # Unix 时间戳(秒)
pd.to_datetime(df["date"], errors="coerce") # 非法日期转 NaT
重命名
rename()
| 参数 |
类型 |
默认值 |
说明 |
mapper |
dict / function |
None |
应用于 axis 的映射或函数 |
index |
dict / function |
None |
行索引重命名 |
columns |
dict / function |
None |
列名重命名 |
axis |
int / str |
0 |
配合 mapper 使用,0/'index' 或 1/'columns' |
inplace |
bool |
False |
是否原地修改 |
errors |
str |
'ignore' |
'raise' 在键不存在时报错 |
df.rename(columns={"old_name": "new_name", "age": "年龄"})
df.rename(columns=str.lower) # 全部列名转小写
df.rename(index={0: "first_row"}) # 重命名行索引
重置索引
reset_index()
| 参数 |
类型 |
默认值 |
说明 |
level |
int / str / list |
None |
重置的层级(多层索引时使用) |
drop |
bool |
False |
True 则丢弃原索引,不加入列 |
inplace |
bool |
False |
是否原地修改 |
col_level |
int / str |
0 |
多层列时,将索引插入的层级 |
df.reset_index() # 原索引成为列,新增 RangeIndex
df.reset_index(drop=True) # 丢弃原索引,常用于过滤后重置行号
set_index()
| 参数 |
类型 |
默认值 |
说明 |
keys |
str / list / Series / Index |
必填 |
作为索引的列名或列名列表 |
drop |
bool |
True |
是否从列中删除选为索引的列 |
append |
bool |
False |
是否追加到现有索引(构建多层索引) |
inplace |
bool |
False |
是否原地修改 |
verify_integrity |
bool |
False |
是否检查索引唯一性 |
df.set_index("id")
df.set_index(["year", "month"]) # 多层索引
df.set_index("date", drop=True)
数据变换
apply()
对 DataFrame 的行或列(或 Series 的每个元素)应用函数。
| 参数 |
类型 |
默认值 |
说明 |
func |
callable |
必填 |
要应用的函数 |
axis |
int / str |
0 |
0/'index':对每列应用;1/'columns':对每行应用 |
raw |
bool |
False |
True 则传入 ndarray 而非 Series,可提升性能 |
result_type |
str |
None |
仅 axis=1 时有效:'expand'、'reduce'、'broadcast' |
args |
tuple |
() |
传递给 func 的额外位置参数 |
**kwargs |
- |
- |
传递给 func 的关键字参数 |
# 对每列应用
df.apply(lambda col: col.max() - col.min())
# 对每行应用(axis=1)
df.apply(lambda row: row["score"] * 1.1 if row["grade"] == "A" else row["score"], axis=1)
# result_type='expand' 将返回的 list 展开为多列
df.apply(lambda row: [row["x"] + 1, row["y"] * 2], axis=1, result_type="expand")
# 传额外参数
def scale(col, factor):
return col * factor
df.apply(scale, args=(2.0,))
df.apply(scale, factor=2.0)
map() / applymap() / pipe()
# Series.map():逐元素映射(替代值 or 应用函数)
df["grade"].map({"A": 4.0, "B": 3.0, "C": 2.0})
df["score"].map(lambda x: round(x, 1))
# DataFrame.map()(pandas 2.1+,之前叫 applymap):逐元素应用函数
df.map(lambda x: str(x).strip())
# applymap():pandas < 2.1 的写法(2.1 起已弃用,改用 map)
df.applymap(lambda x: x ** 2)
# pipe():链式传入整个 DataFrame,用于 method chaining
def normalize(df, col):
df[col] = (df[col] - df[col].mean()) / df[col].std()
return df
df.pipe(normalize, "score").pipe(normalize, "age")
向量化操作
pandas 支持直接在列之间做算术和比较运算,底层由 NumPy 加速,避免使用循环。
df["total"] = df["price"] * df["quantity"]
df["discount_price"] = df["price"] * (1 - df["discount_rate"])
df["is_adult"] = df["age"] >= 18
df["z_score"] = (df["score"] - df["score"].mean()) / df["score"].std()
# 字符串拼接
df["full_name"] = df["first_name"] + " " + df["last_name"]
# 条件赋值:np.where
import numpy as np
df["label"] = np.where(df["score"] >= 60, "pass", "fail")
# 多条件赋值:np.select
conditions = [df["score"] >= 90, df["score"] >= 60]
choices = ["优秀", "合格"]
df["grade"] = np.select(conditions, choices, default="不合格")
assign()
返回新 DataFrame(不修改原数据),适合链式操作。
df2 = (
df
.assign(total=df["price"] * df["quantity"])
.assign(tax=lambda x: x["total"] * 0.13)
.assign(final=lambda x: x["total"] + x["tax"])
)
| 参数 |
类型 |
说明 |
**kwargs |
scalar / callable |
列名=值或列名=函数;函数接收 df 作为参数,可引用刚新建的列 |
列的增删改
# 新增列
df["new_col"] = 0
df["new_col"] = df["a"] + df["b"]
# 修改列
df["score"] = df["score"] * 1.1
# 删除列
df.drop(columns=["col1", "col2"]) # 返回新 df
df.drop(columns="col1", inplace=True) # 原地删除
del df["col1"] # 直接删除(原地)
df.pop("col1") # 删除并返回该列
# 插入列到指定位置
df.insert(loc=2, column="middle_col", value=0)
字符串操作
通过 Series.str 访问器对字符串列进行操作,自动处理 NaN(返回 NaN 而不是报错)。
str.contains()
| 参数 |
类型 |
默认值 |
说明 |
pat |
str |
必填 |
匹配模式(默认正则) |
case |
bool |
True |
是否区分大小写 |
flags |
int |
0 |
re 模块的标志,如 re.IGNORECASE |
na |
scalar |
None |
NaN 的填充值 |
regex |
bool |
True |
False 则作为字面字符串匹配 |
df[df["name"].str.contains("张", na=False)]
df[df["email"].str.contains(r"@gmail\.com", regex=True, na=False)]
str.startswith() / str.endswith()
| 参数 |
类型 |
默认值 |
说明 |
pat |
str / tuple |
必填 |
前缀/后缀字符串或字符串元组 |
na |
scalar |
None |
NaN 的填充值 |
df[df["filename"].str.endswith((".csv", ".xlsx"), na=False)]
str.replace()
| 参数 |
类型 |
默认值 |
说明 |
pat |
str / re.Pattern |
必填 |
匹配模式(默认正则) |
repl |
str / callable |
必填 |
替换字符串或函数 |
n |
int |
-1 |
最多替换次数;-1 全部替换 |
case |
bool |
None |
是否区分大小写 |
flags |
int |
0 |
re 标志 |
regex |
bool |
True |
是否使用正则 |
df["phone"].str.replace(r"\D", "", regex=True) # 去除非数字
df["text"].str.replace("旧词", "新词", regex=False) # 字面替换
str.strip() / str.lstrip() / str.rstrip()
| 参数 |
类型 |
默认值 |
说明 |
to_strip |
str |
None |
要去除的字符;None 去除空白 |
df["name"].str.strip()
df["code"].str.strip("0") # 去除前后的 "0"
str.split()
| 参数 |
类型 |
默认值 |
说明 |
pat |
str |
None |
分隔符(默认空白字符) |
n |
int |
-1 |
最多分割次数 |
expand |
bool |
False |
True 返回 DataFrame,False 返回 Series of lists |
regex |
bool |
None |
是否使用正则 |
df["name"].str.split(" ") # 返回 Series of lists
df["name"].str.split(" ", expand=True) # 展开为多列
df["date"].str.split("-", expand=True, n=2) # 最多分 2 次
| 方法 |
参数 |
说明 |
str.extract(pat, flags, expand) |
pat: 含捕获组的正则 |
提取第一个匹配;expand=True 返回 DataFrame |
str.extractall(pat, flags) |
pat: 含捕获组的正则 |
提取所有匹配,返回 MultiIndex DataFrame |
str.findall(pat, flags) |
pat: 正则 |
返回所有匹配列表 |
# extract:提取捕获组
df["date"].str.extract(r"(\d{4})-(\d{2})-(\d{2})") # 返回 3 列
df["email"].str.extract(r"(?P<user>\w+)@(?P<domain>\w+)") # 命名捕获组
# findall:返回所有匹配
df["text"].str.findall(r"\d+")
str.upper() / str.lower() / str.len()
df["name"].str.upper()
df["name"].str.lower()
df["name"].str.title() # 首字母大写
df["text"].str.len() # 字符串长度(NaN 返回 NaN)
df["text"].str.count("a") # 字符出现次数
| 方法 |
说明 |
str.upper() |
全部大写 |
str.lower() |
全部小写 |
str.title() |
标题格式(每词首字母大写) |
str.capitalize() |
首字母大写,其余小写 |
str.len() |
字符串长度 |
str.count(pat) |
子串出现次数 |
str.get(i) |
获取第 i 个字符 |
str.pad(width, side, fillchar) |
填充到指定宽度 |
str.zfill(width) |
左侧补零 |
str.cat(others, sep) |
字符串连接 |
日期时间操作
pd.to_datetime()
见 Pandas完全指南 · 类型转换 章节中的参数表。
dt 访问器属性
通过 Series.dt 访问器操作 datetime 类型列。
# 先转换为 datetime 类型
df["date"] = pd.to_datetime(df["date"])
# 提取日期部分
df["date"].dt.year
df["date"].dt.month
df["date"].dt.day
df["date"].dt.hour
df["date"].dt.minute
df["date"].dt.second
df["date"].dt.microsecond
df["date"].dt.weekday # 0=周一, 6=周日
df["date"].dt.day_name() # 星期名称(英文)
df["date"].dt.month_name()
df["date"].dt.dayofyear # 一年中的第几天
df["date"].dt.quarter # 季度(1-4)
df["date"].dt.is_leap_year
df["date"].dt.days_in_month
| 属性 |
说明 |
dt.year |
年 |
dt.month |
月(1-12) |
dt.day |
日(1-31) |
dt.hour |
时(0-23) |
dt.minute |
分(0-59) |
dt.second |
秒(0-59) |
dt.weekday |
星期几(0=周一) |
dt.dayofweek |
同 weekday |
dt.dayofyear |
一年中的第几天 |
dt.quarter |
季度 |
dt.is_month_end |
是否月末 |
dt.is_month_start |
是否月初 |
dt.is_year_end |
是否年末 |
dt.is_leap_year |
是否闰年 |
dt.days_in_month |
当月天数 |
dt.strftime() / dt.floor() / dt.ceil() / dt.normalize()
df["date"].dt.strftime("%Y年%m月%d日")
df["date"].dt.floor("H") # 向下取整到小时
df["date"].dt.ceil("T") # 向上取整到分钟
df["date"].dt.normalize() # 截断到日(时间部分归零)
df["date"].dt.tz_localize("Asia/Shanghai") # 本地化时区
df["date"].dt.tz_convert("UTC") # 转换时区
| 方法 |
参数 |
说明 |
dt.strftime(date_format) |
date_format: str |
格式化为字符串 |
dt.floor(freq) |
freq: str |
向下取整,freq 如 'H'、'T'、'D' |
dt.ceil(freq) |
freq: str |
向上取整 |
dt.round(freq) |
freq: str |
四舍五入取整 |
dt.normalize() |
- |
归零时间部分 |
DateOffset / Timedelta
from pandas.tseries.offsets import MonthEnd, BDay
# Timedelta(固定时间差)
pd.Timedelta("1 days 2 hours")
pd.Timedelta(days=1, hours=2)
df["date"] + pd.Timedelta(days=7)
# DateOffset(日历感知)
df["date"] + pd.DateOffset(months=1) # 下个月同日
df["date"] + MonthEnd(1) # 下个月末
df["date"] + BDay(5) # 5 个工作日后
resample()
对时间序列进行重采样(必须以 datetime 为索引)。
| 参数 |
类型 |
默认值 |
说明 |
rule |
str / DateOffset |
必填 |
重采样频率:'D'、'W'、'M'、'Q'、'Y'、'H'、'T'(分钟)等 |
axis |
int / str |
0 |
沿哪个轴重采样(pandas 2.0 废弃,改用 .T.resample()...T) |
closed |
str |
None |
区间哪端闭合:'left' 或 'right' |
label |
str |
None |
用区间哪端标签:'left' 或 'right' |
convention |
str |
'start' |
对 PeriodIndex 有效 |
origin |
str / Timestamp |
'start_day' |
起始点对齐 |
offset |
Timedelta / str |
None |
起始偏移 |
# 设置时间索引
df = df.set_index("date")
# 按月聚合
df.resample("M").sum()
df.resample("M").agg({"amount": "sum", "count": "count"})
# 按周求均值
df["price"].resample("W").mean()
# 降采样(粗粒度)
df.resample("Q").last() # 每季度最后一个值
# 升采样(细粒度)后填充
df.resample("D").ffill() # 日频,前向填充
date_range()
生成日期范围。
| 参数 |
类型 |
默认值 |
说明 |
start |
str / datetime |
None |
起始日期 |
end |
str / datetime |
None |
结束日期 |
periods |
int |
None |
生成点数;与 end 二选一 |
freq |
str / DateOffset |
'D' |
频率 |
tz |
str / tzinfo |
None |
时区 |
normalize |
bool |
False |
是否归零时间部分 |
name |
str |
None |
结果 Index 的名称 |
inclusive |
str |
'both' |
'both'、'left'、'right'、'neither' 端点包含 |
pd.date_range("2024-01-01", "2024-12-31", freq="D") # 全年每天
pd.date_range("2024-01-01", periods=12, freq="MS") # 12 个月初
pd.date_range("2024-01-01", periods=5, freq="B") # 5 个工作日
分组聚合
groupby()
| 参数 |
类型 |
默认值 |
说明 |
by |
str / list / callable / dict |
必填 |
分组依据:列名、列名列表、函数或字典 |
axis |
int / str |
0 |
分组轴(pandas 2.0 废弃列方向分组) |
level |
int / str |
None |
MultiIndex 层级 |
as_index |
bool |
True |
False 则分组键不作为索引,而是普通列 |
sort |
bool |
True |
是否对分组键排序 |
group_keys |
bool |
True |
apply 时是否在结果中添加分组键 |
observed |
bool |
False |
仅对 Categorical 有效;True 只显示实际出现的分组 |
dropna |
bool |
True |
是否丢弃分组键为 NaN 的组 |
g = df.groupby("city")
g = df.groupby(["city", "year"])
g = df.groupby("city", as_index=False)
g = df.groupby("city", sort=False, observed=True)
agg() / aggregate()
| 参数 |
类型 |
默认值 |
说明 |
func |
str / callable / list / dict |
必填 |
聚合函数;dict 格式 {列名: 函数或函数列表} |
*args |
- |
- |
传递给函数的位置参数 |
**kwargs |
- |
- |
传递给函数的关键字参数 |
# 单函数
df.groupby("city")["score"].agg("mean")
# 多函数
df.groupby("city")["score"].agg(["mean", "std", "count"])
# 按列指定不同函数
df.groupby("city").agg({
"score": ["mean", "max"],
"age": "median",
})
# 命名聚合(pandas 0.25+,推荐)
df.groupby("city").agg(
avg_score=("score", "mean"),
max_score=("score", "max"),
count=("id", "count"),
)
对每组应用函数,返回与原 DataFrame 等长的结果(不降维)。
| 参数 |
类型 |
默认值 |
说明 |
func |
str / callable / list / dict |
必填 |
转换函数;需返回与输入等长的结果 |
*args |
- |
- |
额外参数 |
**kwargs |
- |
- |
关键字参数 |
# 组内标准化(z-score)
df["score_z"] = df.groupby("city")["score"].transform(
lambda x: (x - x.mean()) / x.std()
)
# 用组均值填充缺失值
df["score"] = df.groupby("city")["score"].transform(lambda x: x.fillna(x.mean()))
# 添加组统计到原 df
df["city_avg"] = df.groupby("city")["score"].transform("mean")
df["city_rank"] = df.groupby("city")["score"].transform("rank", ascending=False)
filter()
过滤满足条件的组(整组保留或整组丢弃)。
| 参数 |
类型 |
默认值 |
说明 |
func |
callable |
必填 |
接收子 DataFrame/Series,返回 bool |
dropna |
bool |
True |
是否丢弃过滤后的 NaN 行 |
# 保留组内记录数 >= 10 的组
df.groupby("city").filter(lambda g: len(g) >= 10)
# 保留组均值 > 80 的组
df.groupby("city").filter(lambda g: g["score"].mean() > 80)
apply() 在 GroupBy 上的用法
# 每组取 score 最高的 3 条
df.groupby("city").apply(lambda g: g.nlargest(3, "score"))
# 每组自定义处理
def process_group(g):
g = g.copy()
g["rank"] = g["score"].rank(ascending=False)
return g
df.groupby("city").apply(process_group)
常用聚合函数汇总
| 函数 |
说明 |
sum() |
求和 |
mean() |
均值 |
median() |
中位数 |
count() |
非 NaN 数量 |
size() |
总行数(含 NaN) |
nunique() |
唯一值数量 |
first() |
第一个值 |
last() |
最后一个值 |
min() |
最小值 |
max() |
最大值 |
std() |
标准差(ddof=1) |
var() |
方差 |
sem() |
标准误 |
prod() |
乘积 |
cumsum() |
累计和 |
cumprod() |
累计积 |
cummax() |
累计最大 |
cummin() |
累计最小 |
合并与连接
merge()
类似 SQL JOIN,默认内连接。
| 参数 |
类型 |
默认值 |
说明 |
left |
DataFrame |
必填 |
左表 |
right |
DataFrame |
必填 |
右表 |
how |
str |
'inner' |
连接方式:'inner'、'left'、'right'、'outer'、'cross' |
on |
str / list |
None |
连接键(两表中同名列);与 left_on/right_on 互斥 |
left_on |
str / list |
None |
左表连接键 |
right_on |
str / list |
None |
右表连接键 |
left_index |
bool |
False |
用左表索引作为连接键 |
right_index |
bool |
False |
用右表索引作为连接键 |
suffixes |
tuple |
('_x', '_y') |
重名列的后缀 |
validate |
str |
None |
校验连接类型:'1:1'、'1:m'、'm:1'、'm:m' |
indicator |
bool / str |
False |
添加 _merge 列标识来源;字符串可自定义列名 |
sort |
bool |
False |
是否按连接键排序结果 |
# 基本用法
result = pd.merge(df_left, df_right, on="id")
# 不同列名的连接键
result = pd.merge(df_orders, df_users, left_on="user_id", right_on="id")
# 用索引连接
result = pd.merge(df_left, df_right, left_index=True, right_index=True)
# 左连接,重名列加后缀,添加来源标识
result = pd.merge(
df_left, df_right,
on="id",
how="left",
suffixes=("_left", "_right"),
indicator=True,
)
# _merge 列值:'left_only' / 'right_only' / 'both'
# 验证连接基数
result = pd.merge(df_left, df_right, on="id", validate="m:1")
各种 how 的对比
| how |
保留行 |
说明 |
'inner' |
两表均匹配的行 |
交集,最常用 |
'left' |
左表全部行 |
左外连接,右表无匹配时为 NaN |
'right' |
右表全部行 |
右外连接,左表无匹配时为 NaN |
'outer' |
两表所有行 |
全外连接,无匹配时为 NaN |
'cross' |
笛卡尔积 |
两表行数之积,无需指定 on |
# 找出左表独有的行(反连接)
result = pd.merge(df_left, df_right, on="id", how="left", indicator=True)
left_only = result[result["_merge"] == "left_only"].drop(columns="_merge")
join()
基于索引的连接,是 merge 的简化版。
| 参数 |
类型 |
默认值 |
说明 |
other |
DataFrame / Series / list |
必填 |
要连接的对象 |
on |
str / list |
None |
左表中用于连接的列(右表用索引) |
how |
str |
'left' |
连接方式 |
lsuffix |
str |
'' |
左表重名列后缀 |
rsuffix |
str |
'' |
右表重名列后缀 |
sort |
bool |
False |
是否排序 |
validate |
str |
None |
基数校验 |
df1.join(df2, how="inner")
df1.join(df2, on="id", rsuffix="_right")
concat()
沿轴拼接多个对象(行拼接或列拼接)。
| 参数 |
类型 |
默认值 |
说明 |
objs |
list / dict |
必填 |
要拼接的 DataFrame 或 Series 列表 |
axis |
int / str |
0 |
0/'index':纵向拼接(增加行);1/'columns':横向拼接(增加列) |
join |
str |
'outer' |
'outer':保留所有列;'inner':只保留共同列 |
ignore_index |
bool |
False |
True 则重置索引 |
keys |
list |
None |
添加多层索引标识来源 |
levels |
list |
None |
多层索引的层级 |
names |
list |
None |
多层索引的层名 |
verify_integrity |
bool |
False |
是否检查索引是否有重复 |
sort |
bool |
False |
是否对列排序 |
copy |
bool |
True |
pandas 2.0 起忽略此参数(CoW) |
# 纵向拼接(行堆叠)
df_all = pd.concat([df1, df2, df3], ignore_index=True)
# 带来源标识
df_all = pd.concat([df_2023, df_2024], keys=["2023", "2024"])
# 横向拼接(列合并)
df_wide = pd.concat([df_a, df_b], axis=1)
# 只保留共同列
df_all = pd.concat([df1, df2], join="inner", ignore_index=True)
透视与重塑
pivot_table()
类似 Excel 数据透视表,可处理重复值(通过聚合)。
| 参数 |
类型 |
默认值 |
说明 |
data |
DataFrame |
必填 |
数据源(也可作为 df 方法调用) |
values |
str / list |
None |
聚合的值列;None 则聚合所有数值列 |
index |
str / list / callable |
None |
行方向的分组键 |
columns |
str / list / callable |
None |
列方向的分组键 |
aggfunc |
str / callable / list / dict |
'mean' |
聚合函数 |
fill_value |
scalar |
None |
填充缺失单元格 |
margins |
bool |
False |
是否添加"合计"行列 |
margins_name |
str |
'All' |
合计标签 |
dropna |
bool |
True |
是否丢弃全为 NaN 的列 |
observed |
bool |
False |
仅对 Categorical 有效 |
sort |
bool |
True |
是否排序 |
pd.pivot_table(
df,
values="sales",
index="region",
columns="product",
aggfunc="sum",
fill_value=0,
margins=True,
)
pivot()
将长格式转宽格式,不做聚合(值必须唯一,否则报错)。
| 参数 |
类型 |
默认值 |
说明 |
index |
str |
None |
行索引列名 |
columns |
str / list |
必填 |
要展开为列的列名 |
values |
str / list |
None |
值列名;None 使用其余所有列 |
df.pivot(index="date", columns="city", values="temperature")
melt()
将宽格式转长格式(逆透视)。
| 参数 |
类型 |
默认值 |
说明 |
id_vars |
str / list |
None |
保持不变的标识列 |
value_vars |
str / list |
None |
要"融化"的列;None 使用除 id_vars 外所有列 |
var_name |
str |
'variable' |
变量列的列名 |
value_name |
str |
'value' |
值列的列名 |
col_level |
int / str |
None |
多层列时使用的层级 |
ignore_index |
bool |
True |
是否重置索引 |
pd.melt(
df,
id_vars=["id", "name"],
value_vars=["2022_sales", "2023_sales", "2024_sales"],
var_name="year",
value_name="sales",
)
stack() / unstack()
# stack():将最内层列转为行(宽转长)
df.stack()
# unstack():将最内层行索引转为列(长转宽)
df.unstack()
df.unstack(level=0) # 指定层级
df.unstack(level="city")
crosstab()
交叉频率表,统计两个或多个因子的频次。
| 参数 |
类型 |
默认值 |
说明 |
index |
array / list |
必填 |
行分组因子 |
columns |
array / list |
必填 |
列分组因子 |
values |
array |
None |
聚合的值;需配合 aggfunc |
aggfunc |
callable |
None |
聚合函数 |
rownames |
list |
None |
行维度名称 |
colnames |
list |
None |
列维度名称 |
margins |
bool |
False |
是否添加合计 |
margins_name |
str |
'All' |
合计标签 |
normalize |
bool / str |
False |
标准化:True/'all'、'index'、'columns' |
dropna |
bool |
True |
是否丢弃全 NaN 列 |
pd.crosstab(df["gender"], df["grade"])
pd.crosstab(df["gender"], df["grade"], normalize="index") # 行比例
pd.crosstab(df["city"], df["grade"], margins=True)
排序
sort_values()
| 参数 |
类型 |
默认值 |
说明 |
by |
str / list |
必填 |
排序依据的列名或列名列表 |
axis |
int / str |
0 |
0 对行排序 |
ascending |
bool / list |
True |
升序/降序;列表时与 by 一一对应 |
inplace |
bool |
False |
是否原地修改 |
kind |
str |
'quicksort' |
排序算法:'quicksort'、'mergesort'(稳定)、'heapsort'、'stable' |
na_position |
str |
'last' |
NaN 位置:'first' 或 'last' |
ignore_index |
bool |
False |
是否重置索引 |
key |
callable |
None |
排序前对每列应用的转换函数 |
df.sort_values("score", ascending=False)
df.sort_values(["city", "score"], ascending=[True, False])
df.sort_values("name", key=lambda s: s.str.lower()) # 忽略大小写排序
df.sort_values("score", na_position="first")
sort_index()
| 参数 |
类型 |
默认值 |
说明 |
axis |
int / str |
0 |
0 对行排序,1 对列排序 |
level |
int / str / list |
None |
指定多层索引的排序层级 |
ascending |
bool / list |
True |
升序/降序 |
inplace |
bool |
False |
是否原地修改 |
kind |
str |
'quicksort' |
排序算法 |
na_position |
str |
'last' |
NaN 位置 |
sort_remaining |
bool |
True |
多层索引时是否对剩余层级也排序 |
ignore_index |
bool |
False |
是否重置索引 |
key |
callable |
None |
排序前转换函数 |
df.sort_index() # 按行索引升序
df.sort_index(ascending=False)
df.sort_index(axis=1) # 按列名排序
窗口函数
rolling()
滑动窗口计算。
| 参数 |
类型 |
默认值 |
说明 |
window |
int / timedelta / offset |
必填 |
窗口大小(行数或时间范围) |
min_periods |
int |
None |
窗口内最少非 NaN 数,默认等于 window |
center |
bool |
False |
True 则将当前点放在窗口中心 |
win_type |
str |
None |
窗口类型(Gaussian 等),配合 scipy |
on |
str |
None |
对哪列计算(用于 datetime 时间窗口) |
axis |
int / str |
0 |
pandas 2.1 废弃 |
closed |
str |
None |
区间闭合方式:'right'、'left'、'both'、'neither' |
step |
int |
None |
步长(每隔 N 步计算一次) |
method |
str |
'single' |
'table' 启用表级优化 |
df["price"].rolling(window=5).mean() # 5 日移动平均
df["price"].rolling(window=5, min_periods=1).mean() # 不足 5 个也计算
df["price"].rolling(window=5, center=True).mean() # 居中窗口
# 时间窗口(需 datetime 索引)
df["price"].rolling("7D").mean() # 过去 7 天均值
# 多列同时计算
df"price", "volume".rolling(5).sum()
expanding()
扩张窗口(从起点到当前行,窗口不断增大)。
df["price"].expanding().mean() # 累计均值
df["price"].expanding(min_periods=5).std() # 至少 5 个点才开始计算
| 参数 |
类型 |
默认值 |
说明 |
min_periods |
int |
1 |
最少行数 |
method |
str |
'single' |
'table' 启用表级优化 |
ewm()
指数加权移动(指数衰减权重)。
| 参数 |
类型 |
默认值 |
说明 |
com |
float |
None |
质心,alpha = 1/(1+com) |
span |
float |
None |
跨度,alpha = 2/(span+1) |
halflife |
float / timedelta |
None |
半衰期 |
alpha |
float |
None |
平滑因子,0 < alpha <= 1;以上四选一 |
min_periods |
int |
0 |
最少行数 |
adjust |
bool |
True |
是否使用调整型加权(处理初始值) |
ignore_na |
bool |
False |
是否在计算权重时忽略 NaN |
df["price"].ewm(span=20).mean() # EMA-20(指数移动平均)
df["price"].ewm(halflife=5).std()
窗口函数常用聚合
roll = df["price"].rolling(20)
roll.mean() # 移动平均
roll.sum() # 移动求和
roll.std() # 移动标准差
roll.min() # 移动最小值
roll.max() # 移动最大值
roll.var() # 移动方差
roll.corr(df["volume"]) # 移动相关系数
roll.cov(df["volume"]) # 移动协方差
roll.apply(lambda x: x.iloc[-1] / x.iloc[0] - 1) # 自定义(N 日涨幅)
性能优化
选择合适的数据类型
不合理的类型是内存浪费的主要来源。
# 查看内存占用
df.info(memory_usage="deep")
df.memory_usage(deep=True).sum() / 1024**2 # MB
# 整数降级
df["age"] = df["age"].astype("int8") # -128 ~ 127
df["count"] = df["count"].astype("uint16") # 0 ~ 65535
# 浮点降级
df["ratio"] = df["ratio"].astype("float32") # 精度约 7 位
# 字符串列转 category(低基数列效果显著)
df["city"] = df["city"].astype("category")
df["status"] = df["status"].astype("category")
使用 category 类型节省内存
适用于基数(唯一值数量)远小于行数的列。
# 转为 category
df["region"] = df["region"].astype("category")
# 内存对比
import sys
s_str = pd.Series(["北京", "上海", "广州"] * 100000)
s_cat = s_str.astype("category")
print(s_str.memory_usage(deep=True)) # ~2.4 MB
print(s_cat.memory_usage(deep=True)) # ~0.3 MB
# category 排序
from pandas.api.types import CategoricalDtype
cat_type = CategoricalDtype(categories=["低", "中", "高"], ordered=True)
df["level"] = df["level"].astype(cat_type)
df.sort_values("level") # 按自定义顺序排序
eval() / query() 替代向量运算
使用 numexpr 引擎,对大 DataFrame 可节省内存并提速。
# 普通写法(会创建中间数组)
result = df["a"] + df["b"] * df["c"]
# eval 写法(原地计算,节省内存)
result = df.eval("a + b * c")
df.eval("total = a + b * c", inplace=True)
# 引用外部变量
factor = 1.1
df.eval("score_adj = score * @factor", inplace=True)
适用场景:行数 > 10 万、表达式涉及多列运算时效果明显;简单操作反而因解析开销更慢。
chunksize 处理大文件
def process_large_csv(filepath, chunksize=50000):
results = []
for chunk in pd.read_csv(filepath, chunksize=chunksize):
# 每块独立处理
filtered = chunk[chunk["amount"] > 100]
agg = filtered.groupby("city")["amount"].sum()
results.append(agg)
return pd.concat(results).groupby(level=0).sum()
避免 iterrows()
iterrows() 逐行迭代,性能极差(每行创建 Series,开销大)。
# 错误示范(慢)
for idx, row in df.iterrows():
df.at[idx, "total"] = row["price"] * row["qty"]
# 正确做法:向量化
df["total"] = df["price"] * df["qty"]
# 如必须遍历,用 itertuples(比 iterrows 快 10x 以上)
for row in df.itertuples(index=True, name="Row"):
print(row.price, row.qty)
# 或用 apply(次优)
df["total"] = df.apply(lambda r: r["price"] * r["qty"], axis=1)
inplace=True 的真实效果
inplace=True 并不总是节省内存,也不总是更快。
# inplace=True 实际上仍会在内部创建新对象再赋值回去
df.drop(columns=["col"], inplace=True)
# 等效于
df = df.drop(columns=["col"])
# pandas 2.0+ CoW(写时复制)机制下,inplace 行为更一致
# 建议:明确赋值,避免依赖 inplace 的副作用
df = df.dropna().reset_index(drop=True)
使用 PyArrow backend(pandas 2.0+)
# 创建时指定 ArrowDtype
import pyarrow as pa
df = pd.read_csv("data.csv", dtype_backend="pyarrow")
# 手动转换
df = df.convert_dtypes(dtype_backend="pyarrow")
# PyArrow 类型的优势:更低内存占用、更快 IO、原生支持 null(非 NaN)
Copy-on-Write(pandas 2.0+ CoW 机制)
pandas 2.0 引入 CoW,2.0 起可选,3.0 起强制启用。
# 启用 CoW(pandas 2.0/2.1 中需手动开启)
pd.options.mode.copy_on_write = True
# CoW 下,所有切片都是写时复制视图
subset = df"a", "b" # 不立即复制
subset["a"] = 0 # 触发复制,不影响原 df
# 明确需要独立副本时
copy = df.copy()
# CoW 的主要影响:彻底消除 SettingWithCopyWarning
# 不再需要 df.loc[mask, "col"] = val 来避免链式赋值警告
常用配合库
NumPy
import numpy as np
# DataFrame -> ndarray
arr = df.to_numpy()
arr = df.values # 旧写法,不推荐
arr = df["score"].to_numpy()
# ndarray -> DataFrame
df = pd.DataFrame(arr, columns=["a", "b", "c"])
# NumPy ufunc 直接应用于 Series
np.sqrt(df["score"])
np.log(df["price"])
np.clip(df["age"], a_min=18, a_max=65)
# NumPy 函数与 pandas 互操作
np.where(df["score"] >= 60, "pass", "fail")
np.select([df["score"] >= 90, df["score"] >= 60], ["A", "B"], default="C")
Matplotlib / Seaborn
df.plot() 基于 Matplotlib,是快速可视化的便捷方式。
| 参数 |
类型 |
默认值 |
说明 |
kind |
str |
'line' |
图表类型:'line'、'bar'、'barh'、'hist'、'box'、'kde'、'area'、'pie'、'scatter'、'hexbin' |
x |
str |
None |
x 轴列名(scatter 等需要) |
y |
str / list |
None |
y 轴列名 |
figsize |
tuple |
None |
图表尺寸 (width, height) |
title |
str |
None |
标题 |
xlabel / ylabel |
str |
None |
轴标签 |
legend |
bool |
True |
是否显示图例 |
grid |
bool |
False |
是否显示网格 |
ax |
matplotlib Axes |
None |
指定 Axes 对象 |
subplots |
bool |
False |
每列一个子图 |
rot |
int |
None |
x 轴标签旋转角度 |
import matplotlib.pyplot as plt
df["score"].plot(kind="hist", bins=20, title="Score Distribution")
plt.show()
df.plot(kind="scatter", x="age", y="score", figsize=(8, 5))
plt.show()
# 多列折线图
df"price", "cost".plot(figsize=(12, 4), grid=True)
plt.tight_layout()
plt.show()
# Seaborn
import seaborn as sns
sns.heatmap(df.corr(), annot=True, fmt=".2f", cmap="coolwarm")
sns.boxplot(data=df, x="city", y="score")
SQLAlchemy
见 SQLAlchemy完全指南。
from sqlalchemy import create_engine
engine = create_engine("mysql+pymysql://user:pass@localhost/db")
# 读取
df = pd.read_sql("SELECT * FROM orders WHERE status = 'paid'", con=engine)
# 写入
df.to_sql(
name="orders_backup",
con=engine,
if_exists="replace", # 'fail' / 'replace' / 'append'
index=False,
chunksize=1000,
method="multi", # 批量插入,更快
)
to_sql() 重点参数:
| 参数 |
类型 |
默认值 |
说明 |
name |
str |
必填 |
目标表名 |
con |
SQLAlchemy engine |
必填 |
数据库连接 |
schema |
str |
None |
数据库 schema |
if_exists |
str |
'fail' |
'fail'、'replace'(删表重建)、'append' |
index |
bool |
True |
是否写出 df 索引 |
index_label |
str / list |
None |
索引的列名 |
chunksize |
int |
None |
分批写入行数 |
dtype |
dict |
None |
指定各列的 SQLAlchemy 类型 |
method |
str / callable |
None |
None(逐行)、'multi'(批量 INSERT)、或自定义函数 |
PyArrow
import pyarrow as pa
import pyarrow.parquet as pq
# pandas -> Arrow Table
table = pa.Table.from_pandas(df)
# Arrow Table -> pandas
df = table.to_pandas()
# 直接读写 Parquet
pq.write_table(table, "data.parquet", compression="snappy")
table = pq.read_table("data.parquet", columns=["id", "amount"])
df = table.to_pandas()
# 使用 pandas 接口(底层走 pyarrow)
df.to_parquet("data.parquet", engine="pyarrow", compression="snappy")
df = pd.read_parquet("data.parquet", engine="pyarrow")
Dask
Dask 提供与 pandas 几乎相同的 API,支持并行和大于内存的数据集。
import dask.dataframe as dd
# 读取(懒执行)
ddf = dd.read_csv("large_*.csv")
ddf = dd.from_pandas(df, npartitions=4)
# 操作(与 pandas 语法基本相同)
result = ddf[ddf["score"] > 80].groupby("city")["score"].mean()
# 触发计算
result_df = result.compute()
# 转回 pandas
df = ddf.compute()
Polars
Polars 是 Rust 编写的高性能 DataFrame 库,与 pandas 互转方便。
import polars as pl
# pandas -> Polars
pl_df = pl.from_pandas(df)
# Polars -> pandas
df = pl_df.to_pandas()
# 通过 Arrow 互转(零拷贝,更高效)
arrow_table = df_pandas.to_arrow() # pandas 2.0+ 方法(实验性)
pl_df = pl.from_arrow(arrow_table)
详见 Polars完全指南。
最佳实践
链式操作(Method Chaining)
链式写法让数据处理流程更清晰,避免中间变量。
result = (
pd.read_csv("sales.csv", parse_dates=["date"])
.rename(columns=str.lower)
.dropna(subset=["amount", "product"])
.assign(
month=lambda df: df["date"].dt.to_period("M"),
amount_rmb=lambda df: df["amount"] * 7.2,
)
.query("amount > 100")
.groupby(["month", "product"])
.agg(total=("amount_rmb", "sum"), cnt=("amount_rmb", "count"))
.reset_index()
.sort_values("total", ascending=False)
)
链式操作中,pipe() 可插入自定义函数:
def add_rank(df, col):
df = df.copy()
df["rank"] = df[col].rank(ascending=False, method="dense").astype(int)
return df
result = df.query("score > 60").pipe(add_rank, "score")
避免 SettingWithCopyWarning
该警告提示你正在对切片的副本赋值,修改可能不会反映到原 df。
# 错误示范(链式赋值,触发警告)
df[df["score"] > 60]["grade"] = "pass" # 不生效
# 正确做法:用 loc 赋值
df.loc[df["score"] > 60, "grade"] = "pass"
# 需要修改子集时,显式 copy
subset = df[df["city"] == "北京"].copy()
subset["discount"] = 0.9
# pandas 2.0+ 启用 CoW 后,上述问题从根本上消除
pd.options.mode.copy_on_write = True
使用 pd.api.types 做类型检查
from pandas.api.types import (
is_numeric_dtype,
is_string_dtype,
is_datetime64_any_dtype,
is_categorical_dtype,
is_integer_dtype,
is_float_dtype,
)
for col in df.columns:
if is_numeric_dtype(df[col]):
df[col] = df[col].fillna(0)
elif is_string_dtype(df[col]):
df[col] = df[col].fillna("")
大数据集分块处理模式
import pandas as pd
def etl_pipeline(input_path, output_path, chunksize=100_000):
first = True
for chunk in pd.read_csv(input_path, chunksize=chunksize):
# 处理每块
chunk = (
chunk
.dropna(subset=["id", "amount"])
.assign(amount=pd.to_numeric(chunk["amount"], errors="coerce"))
.dropna(subset=["amount"])
)
# 追加写出
chunk.to_csv(
output_path,
mode="w" if first else "a",
header=first,
index=False,
)
first = False
明确使用 copy() 避免隐式引用
# 切片是视图,修改会影响原 df(pandas CoW 前的行为)
subset = df"a", "b"
subset["a"] = 0 # 可能触发警告,且行为不确定
# 明确复制
subset = df"a", "b".copy()
subset["a"] = 0 # 安全,不影响原 df
应用场景
数据清洗流水线示例
import pandas as pd
def clean_user_data(filepath):
df = (
pd.read_csv(filepath, dtype={"phone": str})
.rename(columns=str.strip) # 去除列名空白
.rename(columns=str.lower) # 列名小写
.drop_duplicates(subset=["user_id"]) # 去重
.dropna(subset=["user_id", "email"]) # 删除关键字段为空的行
.assign(
email=lambda df: df["email"].str.strip().str.lower(),
phone=lambda df: df["phone"].str.replace(r"\D", "", regex=True),
age=lambda df: pd.to_numeric(df["age"], errors="coerce"),
created_at=lambda df: pd.to_datetime(df["created_at"], errors="coerce"),
)
.loc[lambda df: df["age"].between(0, 150)] # 过滤异常年龄
.reset_index(drop=True)
)
return df
日志分析示例
import pandas as pd
# 读取 Nginx 日志
df = pd.read_csv(
"access.log",
sep=r"\s+",
header=None,
names=["ip", "dash1", "user", "time", "request", "status", "size"],
usecols=["ip", "time", "request", "status", "size"],
engine="python",
)
df["time"] = pd.to_datetime(df["time"], format="[%d/%b/%Y:%H:%M:%S")
df["status"] = pd.to_numeric(df["status"], errors="coerce")
df["size"] = pd.to_numeric(df["size"].replace("-", "0"), errors="coerce")
# 按小时统计请求量
hourly = df.set_index("time").resample("H").agg(
requests=("status", "count"),
errors=("status", lambda x: (x >= 500).sum()),
traffic_mb=("size", lambda x: x.sum() / 1024**2),
)
# 找出访问量最多的 IP
top_ips = df["ip"].value_counts().head(10)
时序数据处理示例
import pandas as pd
df = pd.read_csv("stock.csv", parse_dates=["date"])
df = df.set_index("date").sort_index()
# 计算技术指标
df["ma5"] = df["close"].rolling(5).mean()
df["ma20"] = df["close"].rolling(20).mean()
df["ema12"] = df["close"].ewm(span=12).mean()
df["ema26"] = df["close"].ewm(span=26).mean()
df["macd"] = df["ema12"] - df["ema26"]
df["signal"] = df["macd"].ewm(span=9).mean()
# 月度统计
monthly = df["close"].resample("M").agg(
open="first",
high="max",
low="min",
close="last",
)
# 计算月涨跌幅
monthly["return"] = monthly["close"].pct_change()
报表生成示例
import pandas as pd
df = pd.read_excel("sales.xlsx", parse_dates=["order_date"])
# 销售汇总透视表
pivot = pd.pivot_table(
df,
values=["amount", "qty"],
index=pd.Grouper(key="order_date", freq="Q"),
columns="product_category",
aggfunc={"amount": "sum", "qty": "sum"},
fill_value=0,
margins=True,
margins_name="合计",
)
# 格式化并导出
with pd.ExcelWriter("report.xlsx", engine="openpyxl") as writer:
pivot.to_excel(writer, sheet_name="季度销售", float_format="%.2f")
df.groupby("region")["amount"].describe().to_excel(writer, sheet_name="区域统计")
常见陷阱与注意事项
SettingWithCopyWarning
# 触发警告的典型写法(链式赋值)
df[df["score"] > 60]["grade"] = "pass" # 无效赋值,且触发警告
# 正确写法
df.loc[df["score"] > 60, "grade"] = "pass"
原因:df[df["score"] > 60] 可能返回视图也可能返回副本,pandas 无法确定,因此发出警告。
Chained Indexing 问题
# 危险:连续两次 [],行为不确定
df["city"]["Beijing"] # 不推荐
# 安全:一次性用 loc 完成
df.loc["Beijing", "city"]
inplace 不总是节省内存
# 错误认知:inplace=True 会在原地修改,节省内存
df.sort_values("score", inplace=True)
# 实际:pandas 内部仍创建新对象再替换,内存峰值不变
# 另外 inplace=True 阻断 method chaining,建议放弃使用
df = df.sort_values("score")
groupby 后 index 变化
# 默认 as_index=True,分组键成为索引
g = df.groupby("city")["score"].mean()
print(g.index) # Index(['北京', '上海', ...], name='city')
# 解决方法一:reset_index()
result = df.groupby("city")["score"].mean().reset_index()
# 解决方法二:as_index=False
result = df.groupby("city", as_index=False)["score"].mean()
# 多级索引时
result = df.groupby(["city", "year"])["score"].mean()
result.xs("北京", level="city") # 取某级索引
merge 后列名冲突
# 两表都有 name 列,合并后自动加后缀
result = pd.merge(df_left, df_right, on="id")
# 产生 name_x, name_y
# 指定自定义后缀
result = pd.merge(df_left, df_right, on="id", suffixes=("_customer", "_order"))
# 提前重命名,避免冲突
df_right = df_right.rename(columns={"name": "order_name"})
datetime 时区问题
# 无时区 datetime(naive)
dt = pd.Timestamp("2024-01-01 08:00:00")
# 附加时区(localize)
dt_tz = dt.tz_localize("Asia/Shanghai")
# 转换时区
dt_utc = dt_tz.tz_convert("UTC")
# 时区比较:naive 与 tz-aware 不能直接比较
# 统一策略:所有 datetime 都存储为 UTC,展示时转为本地时区
# 读取时指定 UTC
df["ts"] = pd.to_datetime(df["ts"], utc=True)
# 混合时区列会被存为 object 类型,需统一处理
浮点数精度
# 浮点数比较不可靠
0.1 + 0.2 == 0.3 # False
# 安全比较方式
import numpy as np
np.isclose(df["a"], df["b"]) # 元素级
np.allclose(df["a"].values, df["b"].values) # 全部
# 金融场景建议用 Decimal 或存整数(分)
# 或转为 string 再比较哈希
# pandas 浮点显示精度
pd.set_option("display.float_format", "{:.4f}".format)
参见
Polars完全指南
SQLAlchemy完全指南
FastAPI完全指南