2637 字
13 分钟
Python 数据处理实战:Pandas 核心 API 速查 + 大文件分块读取 + 内存优化技巧
Pandas 是 Python 数据处理的核心库,几乎每个数据分析、机器学习项目都离不开它。但很多教程只讲基础用法,遇到真实的”脏数据”和大文件场景时往往束手无策。
本文聚焦实战场景:从最常用的 DataFrame 操作速查,到处理 GB 级大文件的分块技巧,再到内存优化和 Polars 对比,帮你在真实项目中用好 Pandas。
一、安装与版本说明
# 安装(推荐 uv)uv add pandas numpy openpyxl
# 或 pippip install pandas numpy openpyxl
import pandas as pdimport numpy as np
print(pd.__version__) # 本文基于 Pandas 2.xPandas 2.0 引入了 Copy-on-Write(CoW)机制,链式赋值行为有所改变,本文代码基于 Pandas 2.x 编写。
二、DataFrame 核心操作速查表
2.1 创建与读取
import pandas as pdimport 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],})
# 读取 CSVdf = pd.read_csv('data.csv', encoding='utf-8')
# 读取 Excel(需要 openpyxl)df = pd.read_excel('data.xlsx', sheet_name='Sheet1')
# 读取 JSONdf = pd.read_json('data.json', orient='records')
# 读取数据库(需要 sqlalchemy)from sqlalchemy import create_engineengine = 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'] # 单列 → Seriesdf[['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:合并数据
# 假设有两个 DataFrameemployees = 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_000agg_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
# 转为 DataFrameresult = 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 pdimport 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 categoryimport pandas as pdimport numpy as np
# 模拟 100 万行,只有 5 种城市n = 1_000_000cities = ['北京', '上海', '广州', '深圳', '成都']df = pd.DataFrame({'city': np.random.choice(cities, n)})
# object 类型(默认)mem_object = df['city'].memory_usage(deep=True) / 1024 / 1024print(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 / 1024print(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-5x5.3 内存节省对比总结
| 原类型 | 优化后 | 节省 | 说明 |
|---|---|---|---|
int64 | int8 | 87.5% | 数值范围在 -128~127 |
int64 | int16 | 75% | 数值范围在 -32768~32767 |
int64 | int32 | 50% | 数值在 ±21 亿以内 |
float64 | float32 | 50% | 精度降至约 7 位有效数字 |
object(低基数) | category | 80-98% | 唯一值少的字符串列 |
object(日期字符串) | datetime64 | 节省+功能↑ | 转日期后支持时间序列操作 |
六、Pandas vs Polars 简要对比
Polars 是 2023-2026 年快速崛起的 DataFrame 库,在大规模数据处理上有明显优势:
6.1 速度对比(100万行数据)
# 测试:读取 CSV + groupby + filter 整体流程
# Pandas(单线程)import pandas as pdimport 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) | Pandas | API 更直观,教程/文档丰富 |
| 与 sklearn/matplotlib 集成 | Pandas | 生态成熟,无需额外转换 |
| 大规模 ETL / 数据管道 | Polars | 多线程、惰性求值,内存效率高 |
| 超大文件(> 2GB) | Polars / DuckDB | Pandas 内存可能不够 |
| 新项目起点 | 按需 | 两者 API 可以共存,互转 |
# Pandas ↔ Polars 互转import pandas as pdimport polars as pl
pandas_df = pd.DataFrame({'a': [1, 2, 3]})
# Pandas → Polarspolars_df = pl.from_pandas(pandas_df)
# Polars → Pandaspandas_df2 = polars_df.to_pandas()七、常用操作速查备忘表
# ── 时间序列操作(pd.Timestamp / datetime 列)───────────────df['date'] = pd.to_datetime(df['date'])df['year'] = df['date'].dt.yeardf['month'] = df['date'].dt.monthdf['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)相关文章:
- Python 性能优化实战:cProfile + NumPy 向量化 + functools.cache
- Python 类型注解完全指南:从基础标注到 Pydantic 数据验证
- Python 并发编程完全指南:GIL / threading / multiprocessing
- Python 项目工程化实战(一):pyproject.toml + uv + Ruff
本文代码基于 Pandas 2.2 / Polars 0.20 版本验证,接口以官方文档为准。
Python 数据处理实战:Pandas 核心 API 速查 + 大文件分块读取 + 内存优化技巧
https://971918.xyz/posts/python-guide/python-pandas-data-processing/