2637 字
13 分钟

Python 数据处理实战:Pandas 核心 API 速查 + 大文件分块读取 + 内存优化技巧

Pandas 是 Python 数据处理的核心库,几乎每个数据分析、机器学习项目都离不开它。但很多教程只讲基础用法,遇到真实的”脏数据”和大文件场景时往往束手无策。

本文聚焦实战场景:从最常用的 DataFrame 操作速查,到处理 GB 级大文件的分块技巧,再到内存优化和 Polars 对比,帮你在真实项目中用好 Pandas。


一、安装与版本说明#

Terminal window
# 安装(推荐 uv)
uv add pandas numpy openpyxl
# 或 pip
pip install pandas numpy openpyxl
import pandas as pd
import numpy as np
print(pd.__version__) # 本文基于 Pandas 2.x

Pandas 2.0 引入了 Copy-on-Write(CoW)机制,链式赋值行为有所改变,本文代码基于 Pandas 2.x 编写。


二、DataFrame 核心操作速查表#

2.1 创建与读取#

import pandas as pd
import numpy as np
# 从字典创建
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'age': [25, 30, 35, 28],
'city': ['北京', '上海', '北京', '广州'],
'salary': [8000, 12000, 15000, 9500],
'score': [88.5, 92.0, 78.3, 95.1],
})
# 读取 CSV
df = pd.read_csv('data.csv', encoding='utf-8')
# 读取 Excel(需要 openpyxl)
df = pd.read_excel('data.xlsx', sheet_name='Sheet1')
# 读取 JSON
df = pd.read_json('data.json', orient='records')
# 读取数据库(需要 sqlalchemy)
from sqlalchemy import create_engine
engine = create_engine('sqlite:///data.db')
df = pd.read_sql('SELECT * FROM users LIMIT 1000', engine)

2.2 基础信息查看#

df.shape # (行数, 列数)
df.dtypes # 各列数据类型
df.info() # 综合信息:类型 + 非空数量 + 内存占用
df.describe() # 数值列统计(均值/标准差/分位数)
df.head(5) # 前 5 行
df.tail(3) # 后 3 行
df.sample(10) # 随机抽 10 行
df.isnull().sum() # 各列缺失值数量
df.duplicated().sum() # 重复行数量
df['city'].value_counts() # 某列的频次统计
df['salary'].describe() # 单列统计

2.3 选取数据#

# ── 按列选取 ──────────────────────────────────────────────────
df['name'] # 单列 → Series
df[['name', 'salary']] # 多列 → DataFrame
# ── 按行选取 ──────────────────────────────────────────────────
df.loc[0] # 按索引标签选行
df.loc[0:2] # 按标签切片(包含末端!)
df.iloc[0] # 按位置选行
df.iloc[0:3] # 按位置切片(不包含末端)
df.iloc[[0, 2, 4]] # 按位置列表选行
# ── 行列同时选取 ──────────────────────────────────────────────
df.loc[0:2, ['name', 'salary']] # 按标签:前3行的 name/salary 列
df.iloc[0:3, [0, 3]] # 按位置:前3行的第0和第3列
# ── 条件过滤(最常用)──────────────────────────────────────────
df[df['age'] > 28] # 单条件
df[(df['age'] > 25) & (df['city'] == '北京')] # 多条件(用 & 而非 and)
df[df['city'].isin(['北京', '上海'])] # isin 过滤
df[df['name'].str.contains('li', case=False)] # 字符串包含
# query 语法(更简洁)
df.query('age > 28 and city == "北京"')
df.query('salary > @threshold', threshold=10000) # 引用外部变量

2.4 数据清洗#

# ── 缺失值处理 ────────────────────────────────────────────────
df.dropna() # 删除含任意缺失值的行
df.dropna(subset=['salary']) # 只删除 salary 为空的行
df.dropna(thresh=3) # 至少有 3 个非空值才保留
df.fillna(0) # 用 0 填充所有缺失值
df['salary'].fillna(df['salary'].mean()) # 用均值填充
df['city'].fillna(method='ffill') # 前向填充(用上一个非空值)
# ── 重复值处理 ────────────────────────────────────────────────
df.drop_duplicates() # 删除完全重复的行
df.drop_duplicates(subset=['name', 'city']) # 按指定列判断重复
# ── 类型转换 ──────────────────────────────────────────────────
df['age'] = df['age'].astype(int)
df['date'] = pd.to_datetime(df['date']) # 字符串转日期
df['salary'] = pd.to_numeric(df['salary'], errors='coerce') # 转数值,错误置 NaN
# ── 字符串操作(str accessor)────────────────────────────────
df['name'].str.upper() # 转大写
df['name'].str.strip() # 去除首尾空格
df['city'].str.replace('北京', 'Beijing') # 替换
df['email'].str.split('@').str[0] # 按分隔符拆分取第一部分
df['name'].str.len() # 字符串长度

2.5 排序与排名#

df.sort_values('salary', ascending=False) # 按薪资降序
df.sort_values(['city', 'salary'], ascending=[True, False]) # 多列排序
df['salary'].rank(method='dense') # 排名(dense = 无间断)
df.nlargest(5, 'salary') # 薪资前5名
df.nsmallest(3, 'score') # 分数最低3名

三、groupby / merge / pivot_table 实战#

3.1 groupby:分组聚合#

# ── 基础分组聚合 ──────────────────────────────────────────────
df.groupby('city')['salary'].mean() # 各城市平均薪资
df.groupby('city')['salary'].agg(['mean', 'max', 'min', 'count'])
# ── 多列分组 ──────────────────────────────────────────────────
df.groupby(['city', 'department'])['salary'].mean()
# ── 多个聚合指标 ──────────────────────────────────────────────
result = df.groupby('city').agg(
avg_salary = ('salary', 'mean'),
max_salary = ('salary', 'max'),
employee_cnt = ('name', 'count'),
avg_score = ('score', 'mean'),
).round(2)
# ── 自定义聚合函数 ────────────────────────────────────────────
# 计算每个城市薪资的"极差"(最大值 - 最小值)
df.groupby('city')['salary'].agg(lambda x: x.max() - x.min())
# ── transform:保持原 DataFrame 行数(不合并)──────────────────
# 给每行加一列"所在城市平均薪资"
df['city_avg_salary'] = df.groupby('city')['salary'].transform('mean')
# ── filter:过滤整个分组 ──────────────────────────────────────
# 只保留城市平均薪资 > 10000 的行
df.groupby('city').filter(lambda x: x['salary'].mean() > 10000)

3.2 merge / join:合并数据#

# 假设有两个 DataFrame
employees = pd.DataFrame({
'emp_id': [1, 2, 3, 4],
'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'dept_id': [10, 20, 10, 30],
})
departments = pd.DataFrame({
'dept_id': [10, 20, 40],
'dept_name': ['Engineering', 'Marketing', 'HR'],
})
# ── 内连接(交集,类似 SQL INNER JOIN)──────────────────────
pd.merge(employees, departments, on='dept_id', how='inner')
# 结果:只保留两个表都有的 dept_id(10, 20)
# ── 左连接(保留左表全部行)────────────────────────────────────
pd.merge(employees, departments, on='dept_id', how='left')
# 结果:Diana 的 dept_id=30 在 departments 中不存在,dept_name 为 NaN
# ── 外连接(并集)────────────────────────────────────────────
pd.merge(employees, departments, on='dept_id', how='outer')
# ── 不同列名的连接 ────────────────────────────────────────────
pd.merge(employees, departments,
left_on='dept_id', right_on='dept_id') # 明确指定
# ── 多键连接 ──────────────────────────────────────────────────
pd.merge(df1, df2, on=['city', 'year'])
# ── concat:纵向/横向拼接 ─────────────────────────────────────
pd.concat([df1, df2], axis=0, ignore_index=True) # 纵向拼接(叠加行)
pd.concat([df1, df2], axis=1) # 横向拼接(叠加列)

3.3 pivot_table:数据透视表#

import pandas as pd
# 示例数据:销售记录
sales = pd.DataFrame({
'month': ['Jan', 'Jan', 'Feb', 'Feb', 'Mar', 'Mar'],
'city': ['北京', '上海', '北京', '上海', '北京', '上海'],
'product': ['A', 'B', 'A', 'B', 'A', 'B'],
'revenue': [1000, 1500, 1200, 1800, 900, 2000],
'quantity': [10, 15, 12, 18, 9, 20],
})
# ── 基础透视:各月各城市的总收入 ─────────────────────────────
pd.pivot_table(
sales,
values='revenue', # 汇总的值
index='month', # 行(纵轴维度)
columns='city', # 列(横轴维度)
aggfunc='sum', # 聚合方式
fill_value=0, # 空值填充为 0
)
# 输出:
# city 北京 上海
# month
# Feb 1200 1800
# Jan 1000 1500
# Mar 900 2000
# ── 多个聚合指标 ──────────────────────────────────────────────
pd.pivot_table(
sales,
values=['revenue', 'quantity'],
index=['month', 'city'],
aggfunc={'revenue': 'sum', 'quantity': 'mean'},
margins=True, # 添加合计行/列
margins_name='总计',
)

3.4 apply:逐行/逐列自定义运算#

# 逐行运算(axis=1)
df['bonus'] = df.apply(
lambda row: row['salary'] * 0.2 if row['score'] >= 90 else row['salary'] * 0.1,
axis=1
)
# 逐列运算(axis=0,用于统计)
df.apply(lambda col: col.max() - col.min())
# ⚠️ 注意:apply 是 Python 循环,速度远慢于向量化操作
# 能用向量化的就不要用 apply:
# 不好:df.apply(lambda row: row['salary'] * 1.1, axis=1)
# 好: df['salary'] * 1.1

四、大文件处理:分块读取#

当 CSV 文件超过几百 MB 时,直接 pd.read_csv() 可能导致内存溢出。分块读取(chunked processing)是核心解决方案:

4.1 基础分块读取#

import pandas as pd
chunk_size = 100_000 # 每次读取 10 万行
# 方式一:显式迭代
results = []
for chunk in pd.read_csv('large_file.csv', chunksize=chunk_size):
# 对每个 chunk 进行处理
filtered = chunk[chunk['status'] == 'active']
results.append(filtered)
# 合并结果
df_result = pd.concat(results, ignore_index=True)

4.2 实战场景:统计大文件中各类型的总量#

import pandas as pd
chunk_size = 200_000
agg_result = {}
for chunk in pd.read_csv('orders.csv', chunksize=chunk_size,
usecols=['order_type', 'amount'], # 只读取需要的列
dtype={'amount': 'float32'}): # 优化 dtype
# 按 order_type 分组求和
group_sum = chunk.groupby('order_type')['amount'].sum()
for key, val in group_sum.items():
agg_result[key] = agg_result.get(key, 0) + val
# 转为 DataFrame
result = pd.Series(agg_result).sort_values(ascending=False)
print(result)

4.3 进阶:指定列和数据类型(减少读取时间)#

# 只读取需要的列(usecols),指定 dtype,大幅提升速度和减少内存
df = pd.read_csv(
'large_data.csv',
usecols=['user_id', 'event_type', 'timestamp', 'amount'], # 只读4列
dtype={
'user_id': 'int32', # 默认 int64,节省一半
'event_type': 'category', # 字符串 → category,节省 90%
'amount': 'float32', # 默认 float64,节省一半
},
parse_dates=['timestamp'], # 直接解析为 datetime
nrows=None, # None = 读全部;设数字 = 只读前 N 行(用于抽样调试)
)

五、内存优化:让 DataFrame 瘦身#

5.1 dtype 优化#

import pandas as pd
import numpy as np
def optimize_dtypes(df: pd.DataFrame) -> pd.DataFrame:
"""自动优化 DataFrame 的数据类型,减少内存占用"""
for col in df.columns:
col_type = df[col].dtype
# 整数列:降级到最小可用整数类型
if col_type in ['int64', 'int32', 'int16']:
col_min = df[col].min()
col_max = df[col].max()
if col_min >= np.iinfo(np.int8).min and col_max <= np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
elif col_min >= np.iinfo(np.int16).min and col_max <= np.iinfo(np.int16).max:
df[col] = df[col].astype(np.int16)
elif col_min >= np.iinfo(np.int32).min and col_max <= np.iinfo(np.int32).max:
df[col] = df[col].astype(np.int32)
# 浮点列:float64 → float32(精度略降,但通常够用)
elif col_type == 'float64':
df[col] = df[col].astype(np.float32)
# 字符串/object 列:低基数的转 category
elif col_type == 'object':
num_unique = df[col].nunique()
if num_unique / len(df) < 0.5: # 唯一值少于 50%,转 category
df[col] = df[col].astype('category')
return df
# 使用示例
print(f"优化前内存:{df.memory_usage(deep=True).sum() / 1024**2:.1f} MB")
df = optimize_dtypes(df)
print(f"优化后内存:{df.memory_usage(deep=True).sum() / 1024**2:.1f} MB")

5.2 category 类型的威力#

# 对比:object vs category
import pandas as pd
import numpy as np
# 模拟 100 万行,只有 5 种城市
n = 1_000_000
cities = ['北京', '上海', '广州', '深圳', '成都']
df = pd.DataFrame({'city': np.random.choice(cities, n)})
# object 类型(默认)
mem_object = df['city'].memory_usage(deep=True) / 1024 / 1024
print(f"object 类型内存:{mem_object:.1f} MB") # 约 62 MB
# category 类型
df['city_cat'] = df['city'].astype('category')
mem_cat = df['city_cat'].memory_usage(deep=True) / 1024 / 1024
print(f"category 类型内存:{mem_cat:.1f} MB") # 约 1 MB(节省 98%!)
# category 类型在 groupby 时也更快
%timeit df.groupby('city')['city'].count()
%timeit df.groupby('city_cat')['city'].count()
# category groupby 通常快 2-5x

5.3 内存节省对比总结#

原类型优化后节省说明
int64int887.5%数值范围在 -128~127
int64int1675%数值范围在 -32768~32767
int64int3250%数值在 ±21 亿以内
float64float3250%精度降至约 7 位有效数字
object(低基数)category80-98%唯一值少的字符串列
object(日期字符串)datetime64节省+功能↑转日期后支持时间序列操作

六、Pandas vs Polars 简要对比#

Polars 是 2023-2026 年快速崛起的 DataFrame 库,在大规模数据处理上有明显优势:

6.1 速度对比(100万行数据)#

# 测试:读取 CSV + groupby + filter 整体流程
# Pandas(单线程)
import pandas as pd
import time
t0 = time.perf_counter()
df = pd.read_csv('data_1M.csv')
result = df[df['status'] == 'active'].groupby('city')['amount'].sum()
print(f"Pandas: {time.perf_counter() - t0:.3f}s") # 约 1.2s
# Polars(多线程 + 惰性求值)
import polars as pl
t0 = time.perf_counter()
result = (
pl.scan_csv('data_1M.csv') # scan = 惰性,不立即读取
.filter(pl.col('status') == 'active')
.group_by('city')
.agg(pl.col('amount').sum())
.collect() # collect = 触发实际计算
)
print(f"Polars: {time.perf_counter() - t0:.3f}s") # 约 0.15s(快约 8x)

6.2 选型建议#

场景推荐原因
探索性数据分析(EDA)PandasAPI 更直观,教程/文档丰富
与 sklearn/matplotlib 集成Pandas生态成熟,无需额外转换
大规模 ETL / 数据管道Polars多线程、惰性求值,内存效率高
超大文件(> 2GB)Polars / DuckDBPandas 内存可能不够
新项目起点按需两者 API 可以共存,互转
# Pandas ↔ Polars 互转
import pandas as pd
import polars as pl
pandas_df = pd.DataFrame({'a': [1, 2, 3]})
# Pandas → Polars
polars_df = pl.from_pandas(pandas_df)
# Polars → Pandas
pandas_df2 = polars_df.to_pandas()

七、常用操作速查备忘表#

# ── 时间序列操作(pd.Timestamp / datetime 列)───────────────
df['date'] = pd.to_datetime(df['date'])
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['weekday'] = df['date'].dt.day_name()
df.set_index('date').resample('M').sum() # 按月重采样求和
# ── 窗口函数(rolling)──────────────────────────────────────
df['7day_avg'] = df['sales'].rolling(window=7).mean() # 7 日移动平均
df['7day_max'] = df['sales'].rolling(window=7).max() # 7 日滚动最大值
# ── 字符串列的高效处理 ────────────────────────────────────────
# 多列合并为字符串
df['full_addr'] = df['province'] + ' ' + df['city']
# 正则提取
df['area_code'] = df['phone'].str.extract(r'^(\d{3})')
# ── 导出 ──────────────────────────────────────────────────────
df.to_csv('output.csv', index=False, encoding='utf-8-sig') # utf-8-sig 防止 Excel 乱码
df.to_excel('output.xlsx', index=False, sheet_name='Data')
df.to_parquet('output.parquet') # Parquet 格式:压缩更好,读取更快
df.to_json('output.json', orient='records', force_ascii=False)

相关文章

本文代码基于 Pandas 2.2 / Polars 0.20 版本验证,接口以官方文档为准。

Python 数据处理实战:Pandas 核心 API 速查 + 大文件分块读取 + 内存优化技巧
https://971918.xyz/posts/python-guide/python-pandas-data-processing/
作者
九所长
发布于
2026-07-15
许可协议
CC BY-NC-SA 4.0