Pandas 系统整理

适用场景:结构化表格数据的 ETL 及常规数据分析。
阅读方式:先看全景图建立心智模型 → 快速上手跑通全流程 → 按需深入分层详解。


一、数据流全景图

flowchart LR
    %% 定义样式
    classDef io fill:#fff3e0,stroke:#e65100,stroke-width:2px
    classDef core fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef op fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
    classDef adv fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    classDef ts fill:#fce4ec,stroke:#c62828,stroke-width:2px
    classDef eng fill:#f5f5f5,stroke:#616161,stroke-width:2px
    classDef out fill:#e0f7fa,stroke:#006064,stroke-width:2px

    SRC["📄 原始数据文件
CSV / Excel / Parquet / JSON / SQL"]:::io --> IO["📥 接入层 · 读取
read_csv / read_parquet / read_excel
参数:dtype · parse_dates · na_values"]:::io IO --> CORE["🔷 核心层 · 内存模型
DataFrame(二维表)
Series(一维序列)
Index(轴标签系统)
类型:Int64 / string / category / datetime"]:::core CORE --> IDX["🎯 索引定位
选行选列 × 标签/位置/条件"]:::op IDX --> CLN["🧹 数据清洗
缺失值 · 去重 · 类型治理"]:::op CLN --> TRF["🛠 数据变换
向量化运算 · map · apply · str/dt"]:::op TRF --> AGG["📊 分组聚合
GroupBy · agg · transform"]:::adv TRF --> RSP["🔄 重塑合并
pivot · melt · merge · concat"]:::adv subgraph 持久化[" "] AGG --> OUT["📤 写回文件
to_csv / to_parquet / to_excel"]:::out RSP --> OUT end CORE -.-> TS["⏱ 时间序列
resample · rolling · shift
—— 专项能力"]:::ts TS -.-> OUT TRF -.->|查手册| REF["📚 参考手册
参数详解 · 速查表 · 报错排查"]:::eng AGG -.->|查手册| REF:::eng OUT -.->|查手册| REF:::eng

阅读指引:实线箭头是日常主线路径,虚线箭头是专项或查阅路径。每层的详细内容在三、分层详解中展开。


二、快速上手:零售交易数据分析全流程

StringIO 模拟一份含各种脏数据的零售交易 CSV,零依赖跑完全流程。每行代码右侧标注该操作所属的系统层(👉 核心层 / 🎯 索引定位 / 🧹 数据清洗 / 🛠 数据变换 / 📊 分组聚合 / 📤 持久化),跑完后你就建立了"功能 → 层级"的映射感。

2.1 模拟数据

import pandas as pd
import numpy as np
from io import StringIO

csv_data = """
TransactionID,Date,Product,Category,UnitPrice,Quantity,CustomerType,Region,Discount,PaymentMethod
,2025-01-05,Widget A,Gadgets,100.5,5,Regular,North,0.1,Credit Card
1002,2025-01-06,Widget B,Gadgets,200.0,,VIP,South,0.15, 
1003,invalid_date,Widget C,Accessories,50.25,2,Regular,East,,PayPal
1004,2025-01-08,Widget D,Gadgets,300.0,10,VIP,North,0.2,Credit Card
1005,2025-01-09,Widget E ,Accessories,75.0,3,Regular,West,0.05, 
1006,2025-01-10,Widget F,Gadgets,150.0,8,Regular,South,,Cash
1007,2025-01-11,Widget G,Accessories,25.0,1, ,East,0,
1008,2025-01-12,Widget H,Gadgets,500.0,4,VIP,North,0.3,Credit Card
1009,2025-01-13,Widget I,Accessories,60.0,6,Regular,West,0.1,PayPal
1010,2025-01-14,Widget A,Gadgets,100.5,2,,North,,Credit Card
"""

2.2 全流程代码

# ========================================================
# Step 1: 读取(接入层)
# ========================================================
df = pd.read_csv(
    StringIO(csv_data),
    sep=',',
    dtype={
        'TransactionID': 'Int64',    # 👉 核心层:空值友好的整数类型
        'Quantity': 'Int64',
    },
    na_values=['', 'NA', 'null'],
    on_bad_lines='warn',             # 遇到坏行跳过并警告
)
# ========================================================
# Step 2: 探查(贯穿整个流程,属于核心层的数据审视)
# ========================================================
print("=== 概览 ===")
df.info()
print("=== 统计 ===")
df.describe(include='all')

# ========================================================
# Step 3: 清洗
# ========================================================

# 3a 日期清洗 → 无效日期变 NaT          🧹 清洗
df['Date'] = pd.to_datetime(df['Date'], errors='coerce')

# 3b Region 列的空格去掉                🧹 清洗(str 访问器)
df['Region'] = df['Region'].str.strip()

# 3c Product 列空格 + CustomerType 空值  🧹 清洗
df['Product'] = df['Product'].str.strip()
df['CustomerType'] = df['CustomerType'].str.strip().fillna('Regular')

# 3d 统一填充缺失值                      🧹 清洗
df = df.fillna({
    'TransactionID': -1,
    'Quantity': 0,
    'Discount': 0.0,
    'PaymentMethod': 'Unknown',
})

# 3e 去重(假设 TransactionID 是唯一键)  🧹 清洗
df.drop_duplicates(subset=['TransactionID'], keep='first', inplace=True)

# ========================================================
# Step 4: 处理
# ========================================================

# 4a 派生列:收入 = 单价 × 数量          🛠 变换(向量化运算)
df['Revenue'] = df['UnitPrice'] * df['Quantity']

# 4b 分类:根据收入打标签               🛠 变换(apply)
df['RevenueLevel'] = df['Revenue'].apply(
    lambda x: 'High' if x >= 1000 else ('Mid' if x >= 500 else 'Low')
)

# 4c 客户类型列转为 category 类型        👉 核心层:类型优化
df['CustomerType'] = df['CustomerType'].astype('category')

# ========================================================
# Step 5: 分析
# ========================================================

# 5a 按产品类别分组汇总                  📊 进阶层:分组聚合
summary = (
    df.groupby('Category')
    .agg(
        TotalRevenue=('Revenue', 'sum'),
        AvgUnitPrice=('UnitPrice', 'mean'),
        OrderCount=('TransactionID', 'count'),
        AvgDiscount=('Discount', 'mean'),
    )
    .reset_index()
)

# 5b 按区域和客户类型做透视              📊 进阶层:重塑
pivot = df.pivot_table(
    index='Region',
    columns='CustomerType',
    values='Revenue',
    aggfunc='sum',
    fill_value=0,
)

# ========================================================
# Step 6: 窗口计算
# ========================================================

# 按日期排序后,计算销售额的 3 期移动平均  ⏱ 时间序列
df_sorted = df.sort_values('Date').reset_index(drop=True)
df_sorted['Revenue_MA3'] = df_sorted['Revenue'].rolling(window=3).mean()

# ========================================================
# Step 7: 持久化                          📤 持久化
# ========================================================
summary.to_csv('retail_summary.csv', index=False, encoding='utf-8-sig')
df_sorted.to_parquet('retail_full.parquet', index=False)

print("全流程结束,已输出 retail_summary.csv 和 retail_full.parquet")

三、分层详解

3.1 核心层 — 数据模型

一句话:这一层回答"pandas 的数据在内存里到底长什么样"——理解了它,后面所有的操作错误(链式赋值、整数变浮点、视图修改污染)都能从根源上避免。

3.1.1 三大核心对象

对象 维度 类比 关键点
DataFrame 二维(行 × 列) Excel 表格、SQL 表 每一列是一个 Series,列之间共享同一个 Index
Series 一维(值 + 标签) 带索引的数组 / 单列 values 存数据(numpy 数组),index 存标签
Index 一维(纯标签) 行名的集合、字典的 key 集合 不可变、可重复、可排序——是 DataFrame 行/列对齐的纽带

三者关系:

DataFrame
├── columns: Index(['TransactionID', 'Date', 'Product', ...])   ← 列名也是 Index
├── index: Index([0, 1, 2, 3, ...])                             ← 行标签
├── 'TransactionID': Series (Int64)                              ← 每一列是 Series
├── 'Product': Series (object)
├── 'Date': Series (datetime64)
└── ...

Index 的关键行为:两个 DataFrame 做运算时,按 Index 对齐而非按位置——这是 pandas 和 numpy 最大的区别。

a = pd.DataFrame({'val': [1, 2, 3]}, index=['x', 'y', 'z'])
b = pd.DataFrame({'val': [10, 20, 30]}, index=['z', 'y', 'x'])
a + b  # 结果按 x/y/z 对齐,而不是逐行加
# Output:
#    val
# x   31  ← a的1 + b的30 = 31
# y   22  ← 2 + 20 = 22
# z   13  ← 3 + 10 = 13

3.1.2 视图(View)vs 副本(Copy)

这是 pandas 半数以上 bug 的根源,核心就一句话:

pandas 默认"能省则省"——它尽量返回原始数据的视图(view),只有在明确要求复制或操作无法原地执行时才返回副本(copy)。

df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})

# 返回视图的情况:选取单列、切片行
view = df['A']       # view 是 df['A'] 的视图
view.iloc[0] = 999   # ⚠️ 会修改原始 df!因为没复制

# 安全的做法:显式复制
safe_view = df['A'].copy()
safe_view.iloc[0] = 999  # ✅ 不影响原 df

# SettingWithCopyWarning 的经典场景
df[df['A'] > 1]['B'] = 10   # ❌ 链式取 → 无法确定是视图还是副本,pandas 报警告
df.loc[df['A'] > 1, 'B'] = 10  # ✅ 单次操作,pandas 明确知道要修改原对象

实战判断法则

操作 返回值 是否修改原数据
df['col'] 视图或副本 可能(赋值需谨慎)
df[['col1', 'col2']] 副本 不修改
df.loc[:] 视图或副本 可能
df.iloc[:3] 视图(切片) 会修改原数据
.copy() 明确副本 不修改

3.1.3 类型系统

pandas 的类型体系比 numpy 更丰富,很多 bug 源于不理解这一点。

类型 说明 何时用
int64 / Int64(大写 I) 小写=标准 numpy,不支持 NaN;大写=pandas 扩展,支持 NaN 列中有空值的整数用 Int64
float64 默认浮点,支持 NaN 常规小数
object Python 对象指针,通常存字符串,但性能差 字符串较多时考虑换 string
string pandas 1.0+ 的专用字符串类型,比 object 更安全 纯文本列
category 枚举型,只有有限几种取值,节省内存 性别/省份/状态等低基数列
datetime64[ns] 时间戳,纳秒精度 日期时间列
timedelta[ns] 时间差 两日期之差

实战:整数变浮点的根源

# 问题:列中有 NaN 时,int64 无法存 NaN,自动转 float64
df = pd.DataFrame({'id': [1, None, 3]})
print(df['id'].dtype)  # float64 ← 1.0, NaN, 3.0,连整数都变成了浮点

# 方案:用可空整数类型 Int64(大写 I)
df['id'] = df['id'].astype('Int64')
print(df['id'].dtype)  # Int64 ← 1, <NA>, 3,保持整数形态

# 读取时直接指定
df = pd.read_csv('file.csv', dtype={'id': 'Int64'})

3.2 操作层 — 日常 80%

一句话:你每天在 pandas 里做的事情——选数据、改数据、清洗数据——都在这一层。

3.2.1 索引定位 — 选数据的三种方式

# 🎯 场景 1:按标签选(.loc)
df.loc[0:3]                        # 标签 0 到 3(包含两端)
df.loc[df['Region'] == 'North']    # 条件筛选
df.loc[df['Revenue'] > 500, ['Product', 'Revenue']]  # 行条件 + 指定列(最常用写法)

# 🎯 场景 2:按位置选(.iloc)
df.iloc[0:3]                       # 前 3 行(不包含 3,和 Python 切片一致)
df.iloc[:, [0, 2, 4]]              # 第 0/2/4 列

# 🎯 场景 3:标量快速取值(.at / .iat)
df.at[2, 'Product']                # 第 2 行 Product 列的值(比 .loc 快)
df.iat[2, 2]                       # 第 2 行第 2 列的值(比 .iloc 快)

# 🎯 推荐用法总结
df[df['A'] > 0]['B']               # ❌ 链式索引 → 可能 SettingWithCopyWarning
df.loc[df['A'] > 0, 'B']           # ✅ 一步到位,清晰安全

3.2.2 数据变换 — 修改和派生数据

# 🛠 选择 1:向量化运算(最推荐,性能最好)
df['Revenue'] = df['UnitPrice'] * df['Quantity']      # 四则运算
df['HighValue'] = df['Revenue'] > 1000                 # 布尔列
df['DiscountedPrice'] = df['UnitPrice'] * (1 - df['Discount'])  # 复合运算

# 🛠 选择 2:map — Series 的一对一映射(适用于枚举翻译)
df['RegionCN'] = df['Region'].map({
    'North': '华北', 'South': '华南', 'East': '华东', 'West': '西部'
})

# 🛠 选择 3:apply — 函数应用(灵活但非向量化)
# 场景 A:单列传入,返回标量
df['RevenueLevel'] = df['Revenue'].apply(lambda x: 'High' if x > 800 else 'Low')

# 场景 B:多列传入,返回标量(axis=1 表示按行)
df['Score'] = df.apply(
    lambda row: row['UnitPrice'] * row['Quantity'] * (1 - row['Discount']),
    axis=1,
)

# 🛠 选择 4:访问器 — .str 和 .dt
df['Product_Upper'] = df['Product'].str.upper()           # 字符串向量化
df['YearMonth'] = df['Date'].dt.strftime('%Y-%m')         # 日期字段提取
df['Region'] = df['Region'].str.strip()                   # 去空格

# 🛠 选择 5:管道(pipe)— 函数式组合
def add_score(df, multiplier=1):
    df['Score'] = df['Revenue'] * multiplier
    return df

df = df.pipe(add_score, multiplier=1.2)

apply 的定位:它很灵活但不是核武器——能用向量化运算解决的优先用向量化,apply 本质是循环,数据量大时性能差距显著。

3.2.3 数据清洗 — 脏数据治理

# 🧹 缺失值检测
df.isna().sum()                          # 每列缺失数量
df.isna().mean() * 100                   # 每列缺失百分比

# 🧹 缺失值删除
df.dropna(subset=['TransactionID'])      # 关键列有缺失 → 直接删行
df.dropna(thresh=5)                      # 一行有 5 个以上非空才保留

# 🧹 缺失值填充
df.fillna(0)                                    # 全局填 0
df.fillna({'Quantity': 0, 'Discount': 0.0})      # 逐列填不同值
df.fillna(method='ffill')                        # 前向填充(时间序列常用)
df.fillna(method='bfill')                        # 后向填充

# 🧹 重复值
df.duplicated(subset=['TransactionID'])          # 标记重复
df.drop_duplicates(subset=['TransactionID'], keep='first')  # 删除重复,保留首个

# 🧹 类型治理
df['col'] = df['col'].astype('Int64')            # 类型转换
df['col'] = pd.to_numeric(df['col'], errors='coerce')  # 强制数字,无效变 NaN
df['col'] = pd.to_datetime(df['col'], errors='coerce')  # 强制日期,无效变 NaT

# 🧹 异常值治理
df['Price'].clip(lower=0, upper=10000)           # 截断异常值(如价格负数)
df['Region'] = df['Region'].replace({'': 'Unknown', ' ': 'Unknown'})  # 替换特定值

3.3 进阶层 — 分组聚合与重塑合并

一句话:当你不满足于"只看单行"时——你需要按组统计(GroupBy)、把表转来转去(pivot/melt)、或把多张表拼一起(merge/concat)——这就是进阶层。

3.3.1 分组聚合(GroupBy)

核心范式:Split → Apply → Combine

# 按单个字段分组
df.groupby('Category')['Revenue'].sum()

# 按多个字段分组
df.groupby(['Category', 'Region'])['Revenue'].sum()

# 命名聚合(一次性做多件事,列名自解释)
summary = (
    df.groupby('Category')
    .agg(
        TotalRevenue=('Revenue', 'sum'),
        AvgPrice=('UnitPrice', 'mean'),
        OrderCount=('TransactionID', 'count'),
        MaxQty=('Quantity', 'max'),
    )
    .reset_index()  # 把 Category 从索引变回普通列
)

# transform:把聚合结果广播回原始形状(保持行数不变)
df['CategoryAvgRevenue'] = df.groupby('Category')['Revenue'].transform('mean')

# filter:筛选整个组
df.groupby('Category').filter(lambda g: g['Revenue'].sum() > 2000)

# 窗口函数(rolling):依赖排序,常用于时间序列
df_sorted['Revenue_MA3'] = df_sorted['Revenue'].rolling(window=3).mean()
df_sorted['Revenue_Cumsum'] = df_sorted['Revenue'].cumsum()

3.3.2 重塑合并

# 🔄 宽表化(pivot):行×列×值 = 三维透视
# 把 Category 展开为列名,查看每个 Region 的各类别收入
pivot = df.pivot_table(
    index='Region',
    columns='Category',
    values='Revenue',
    aggfunc='sum',
    fill_value=0,
)

# 🔄 长表化(melt):把宽表还原回长表
melted = pivot.reset_index().melt(
    id_vars=['Region'],
    var_name='Category',
    value_name='Revenue',
)

# 🔗 横向合并(merge):类似 SQL JOIN
left = df[['TransactionID', 'Product', 'Revenue']]
right = pd.DataFrame({
    'Product': ['Widget A', 'Widget B'],
    'ProductGroup': ['Premium', 'Standard'],
})
merged = pd.merge(left, right, on='Product', how='left')

# 🔗 纵向拼接(concat):类似 UNION ALL
# 模拟两份数据
q1 = df[df['Date'].dt.quarter == 1].copy()
q2 = df[df['Date'].dt.quarter == 2].copy()
all_data = pd.concat([q1, q2], axis=0, ignore_index=True)

# 🔗 索引连接(join):按索引对齐,两个索引相同的 df 合并
df_with_index = df.set_index('TransactionID')
meta = pd.DataFrame({'ProductGroup': ['Gadget', 'Accessory']}, index=[1002, 1003])
meta.index.name = 'TransactionID'
joined = df_with_index.join(meta)

3.4 专业层 — 时间序列

一句话:pandas 最强势的领域之一,比 Excel 和 SQL 都灵活。这里只演示最常用的。

# 确保日期列是 datetime 类型
df['Date'] = pd.to_datetime(df['Date'], errors='coerce')

# 排序(时间序列操作大多依赖有序索引)
df = df.sort_values('Date').reset_index(drop=True)

# ⏱ 移动平均(rolling)
df['Revenue_MA3'] = df['Revenue'].rolling(window=3).mean()

# ⏱ 环比差异(shift:把上一行的值拿到当前行)
df['Revenue_Change'] = df['Revenue'] - df['Revenue'].shift(1)

# ⏱ 累计求和(cumsum)
df['Revenue_Cum'] = df['Revenue'].cumsum()

# ⏱ 重采样(resample):改变时间频率 — 需要时间作为索引
df_ts = df.set_index('Date')
# 按周聚合
weekly = df_ts.resample('W')['Revenue'].sum()
# 按月聚合
monthly = df_ts.resample('M')['Revenue'].agg(['sum', 'mean', 'count'])

不常用但提一嘴:时区转换(tz_localize / tz_convert)、时间差计算(Timedelta)、日期偏移(DateOffset)、merge_asof(按时间最近匹配),有需要时再查参考手册。


3.5 工程层 — 性能优化与常见陷阱

一句话:从"能跑"到"高效且不出错"——这是区分新手和熟手的分水岭。

3.5.1 性能优化

# ⚡ 原则 1:向量化 > apply > 循环
# 这是一个数量级的差距
df['Revenue'] = df['UnitPrice'] * df['Quantity']            # ✅ 向量化(最快)
df['RevenueLevel'] = df['Revenue'].apply(lambda x: ...)       # ✅ apply(灵活但慢)
for i in range(len(df)): df.iloc[i, ...] = ...               # ❌ 循环(最慢)

# ⚡ 原则 2:category 类型优化内存
# 例:Region 列只有 4 种取值
df['Region'] = df['Region'].astype('category')
# 内存可以从 object 的 ~300MB 降到 category 的 ~5MB(数据量百万行时差异明显)

# ⚡ 原则 3:只读需要的列
df = pd.read_csv('large.csv', usecols=['Product', 'Sales'])  # ✅ 避免读入无用列

# ⚡ 原则 4:分块处理超大文件
chunks = pd.read_csv('huge_file.csv', chunksize=10000)
result = pd.concat([
    chunk.groupby('Category')['Sales'].sum() for chunk in chunks
]).groupby(level=0).sum()

# ⚡ 原则 5:inplace=True 不一定更快
# inplace 的本意是"就地修改",但对大部分操作来说 pandas 内部仍然会复制
# 新版 pandas 已标记 inplace 为 deprecated,推荐显式赋值
df = df.drop_duplicates()                # ✅ 推荐
df.drop_duplicates(inplace=True)         # ⚠️ 不推荐(inplace 将被废弃)

3.5.2 常见陷阱(每个陷阱配"为什么会发生")


陷阱 1:SettingWithCopyWarning

# 现象
df.loc[df['Region'] == 'North', 'Revenue']  = 0  # 正常
df[df['Region'] == 'North']['Revenue'] = 0        # ⚠️ SettingWithCopyWarning

# 为什么会发生?
# 链式索引 df[A][B] 等价于:(df[A])[B] → 中间变量 df[A] 可能是视图也可能是副本
# pandas 无法判断你是想改原数据还是改副本 → 于是警告

# 方案:永远用 .loc 单步操作
df.loc[df['Region'] == 'North', 'Revenue'] = 0   # 一步到位,语义明确

陷阱 2:整数变浮点

# 现象
df = pd.DataFrame({'id': [1, None, 3]})
print(df['id'].dtype)  # float64 → 1.0, NaN, 3.0(整数变浮点)

# 为什么会发生?
# numpy 的 int64 不支持 NaN,pandas 自动提升为 float64(float64 支持 NaN)
# 这是一种"类型提升"行为

# 方案
df['id'] = df['id'].astype('Int64')                # 大写 I — pandas 扩展类型,支持 NaN
df = pd.read_csv('file.csv', dtype={'id': 'Int64'})   # 读取时直接指定

陷阱 3:视图修改污染源数据

# 现象
orders = pd.DataFrame({'product': ['A', 'B', 'C'], 'price': [10, 20, 30]})
subset = orders[orders['product'] != 'C']   # 取了前两行
subset['price'] = subset['price'] * 2       # 想改 subset
print(orders['price'].values)               # ⚠️ 可能也变了(如果是视图)

# 为什么会发生?
# 布尔索引返回的可能是原始数据的视图(取决于 pandas 内部优化)
# 视图本身是原始数据的一段内存指针,改视图可能改到原数据

# 方案:对子集操作前明确复制
subset = orders[orders['product'] != 'C'].copy()   # ✅ 明确是副本
subset['price'] = subset['price'] * 2               # ✅ 只改副本,不影响原数据

陷阱 4:链式赋值被忽略

# 现象
df['Category'] = df['Category'].str.strip().str.lower()  # 正常
# 但某个操作后,数据看起来变了,写入文件时发现没变

# 方案:每做完一步检查一下类型
print(df['Category'].dtype)            # 确认类型
print(df[['Category']].head())         # 确认内容

四、参考手册

4.1 常用方法详解(用法 + 为什么用 + 不用会怎样)

每条都回答三个问题:怎么用(代码示范)、为什么用(解决什么问题)、不用会怎样(后果是什么)。


读取 & 写入

pd.read_csv(path)
- 👉 怎么用:df = pd.read_csv('data.csv', dtype={'id': 'Int64'})
- 💡 为什么用:把 CSV 文件读进内存变成 DataFrame,是 pandas 最重要的入口函数
- ⚠️ 不用会怎样:数据进不来,后续所有操作无法开始。不指定 dtype 的话,含空值的整数字段会自动变 float

pd.read_excel(path)
- 👉 怎么用:df = pd.read_excel('data.xlsx', sheet_name='Sheet1')
- 💡 为什么用:读 Excel 文件,支持多 sheet
- ⚠️ 不用会怎样:只能用 CSV 代替,但 Excel 多 sheet 和单元格格式信息会丢失

pd.read_parquet(path)
- 👉 怎么用:df = pd.read_parquet('data.parquet')
- 💡 为什么用:读 Parquet,速度极快、体积小、自动保留所有数据类型(不会把日期转成字符串)
- ⚠️ 不用会怎样:不是必须的,但 Parquet 在读大文件和跨语言交换时比 CSV 好用得多

pd.read_sql(query, connection)
- 👉 怎么用:df = pd.read_sql('SELECT * FROM orders', engine)
- 💡 为什么用:直接从数据库读表到 DataFrame,跳过导出 CSV 的中间步骤
- ⚠️ 不用会怎样:只能先导出 CSV 再读,多了一步操作

df.to_csv(path)
- 👉 怎么用:df.to_csv('out.csv', index=False, encoding='utf-8-sig')
- 💡 为什么用:把处理完的数据写出为通用格式,给 Excel 或其他工具用
- ⚠️ 不设 index=False:文件里会多出一列无意义的序号(0,1,2,...);不设 encoding='utf-8-sig':Excel 打开 CSV 中文会乱码

df.to_parquet(path)
- 👉 怎么用:df.to_parquet('out.parquet', index=False)
- 💡 为什么用:比 CSV 体积小 5~10 倍,读写快 3~5 倍,且所有列的数据类型原样保留(不用下次再设 dtype)
- ⚠️ 不用会怎样:不影响功能,但在处理大文件时 CSV 的读写慢和类型丢失问题会反复折磨你

df.to_excel(path)
- 👉 怎么用:df.to_excel('out.xlsx', index=False)
- 💡 为什么用:业务方只要 Excel 格式时用
- ⚠️ 不设 index=False:和 CSV 同理,多一列序号


索引 & 选择

.loc[行条件, 列名]
- 👉 怎么用:df.loc[df['Region'] == 'North', ['Product', 'Revenue']]
- 💡 为什么用:按标签定位,是 pandas 最推荐的选数方式——条件筛选和列选择一步完成,不会触发 SettingWithCopyWarning
- ⚠️ 不用会怎样:如果用链式 df[条件]['列'] 替代,会遇到 SettingWithCopyWarning,且语义不清晰

.iloc[行位置, 列位置]
- 👉 怎么用:df.iloc[0:5, [0, 2]](前 5 行的第 0 和 2 列)
- 💡 为什么用:按位置定位,不知道列名只知道第几列时用,或取前 N 行看看数据时用
- ⚠️ 不用会怎样:没有替代品,当你只需要"前 3 行"或"第 5 列"时只能用 iloc

.at[行标签, 列名]
- 👉 怎么用:df.at[3, 'Product'] → 返回第 3 行 Product 列的具体值
- 💡 为什么用:比 .loc 快 2~3 倍,适合只取一个标量值时用
- ⚠️ 不用会怎样:用 .loc 也能取,只是慢一点

.reset_index()
- 👉 怎么用:
python summary = df.groupby('Category')['Revenue'].sum() # summary 的索引是 Category(原列变成了索引) summary = summary.reset_index() # 现在 Category 变回普通列,索引恢复为 0,1,2,...
- 💡 为什么用:groupby()set_index()pivot_table() 等操作会把某列"升"为索引。reset_index 把它"降"回普通列,让你能继续用 df['Category'] 来访问
- ⚠️ 不用会怎样:如果你分组后不 reset_index,Category 这一列是索引而不是 DataFrame 的列——后续 df['Category'] 会报 KeyError,写 to_csv() 时还会多写一列索引到文件里

.set_index('col')
- 👉 怎么用:df.set_index('TransactionID', inplace=True)
- 💡 为什么用:把某列设为行索引后,可以用 .loc 按该列的值快速定位行(类似字典的 key)
- ⚠️ 不用会怎样:不设索引也能正常操作,但需要频繁按某列值取行时没有 .loc 方便


清洗 & 变换

df.fillna(value)
- 👉 怎么用:df.fillna({'Quantity': 0, 'Note': '无'})
- 💡 为什么用:真实数据一定有缺失值,不处理的话后续计算(sum/mean 等)会得到 NaN 或报错
- ⚠️ 不用会怎样:df['Revenue'].sum() 遇到 NaN 会返回 NaN,而不是你期望的数字

df.dropna()
- 👉 怎么用:df.dropna(subset=['TransactionID']) → 删除 TransactionID 为空的整行
- 💡 为什么用:关键字段缺失的数据没有分析价值,直接删掉比胡乱填充更安全
- ⚠️ 不用会怎样:缺失关键字段的行会带进去,污染下游分析结果

df.drop_duplicates()
- 👉 怎么用:df.drop_duplicates(subset=['TransactionID'], keep='first')
- 💡 为什么用:数据采集或拼接时常出现重复行,不剔除会导致统计结果翻倍(比如计数变多、求和变大)
- ⚠️ 不用会怎样:df.groupby('Category')['Revenue'].sum() 的数值会偏高——重复的行被重复计算了

df['col'].astype('Int64')(注意是大写 I)
- 👉 怎么用:df['id'] = df['id'].astype('Int64')
- 💡 为什么用:列中有空值时,小写 int64 没法存 NaN,会自动提升为 float64(1 → 1.0),astype('Int64') 用 pandas 的可空整数类型,既保留整数形态又支持空值
- ⚠️ 不用会怎样:含空值的整数字段会变 float,写文件时多了小数点,读回时还要再转,而且后续做 groupby 时浮点效率低于整数

pd.to_numeric(col, errors='coerce')
- 👉 怎么用:df['Price'] = pd.to_numeric(df['Price'], errors='coerce')
- 💡 为什么用:某列本应是数字但混入了字符串(如 "100 元"、"N/A"),to_numeric 把这些脏数据转成 NaN,留下干净的数值
- ⚠️ 不用会怎样:该列会保持 object 类型,后续 df['Price'].mean() 会报错,因为字符串没法求均值

pd.to_datetime(col, errors='coerce')
- 👉 怎么用:df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
- 💡 为什么用:日期列常常混入无效值(如 "2025-13-01"、"invalid_date"),to_datetime 把这些统一变成 NaT(时间空值),保证整列是 datetime 类型
- ⚠️ 不用会怎样:日期列保持 object/string 类型,后续 .dt.yearsort_values() 按日期排、resample() 全都不支持

df['col'].apply(fn)
- 👉 怎么用:df['Level'] = df['Revenue'].apply(lambda x: 'High' if x > 500 else 'Low')
- 💡 为什么用:当向量化运算无法满足时(如 if-else 分支逻辑、调用外部库),apply 提供一个灵活的兜底方案
- ⚠️ 不用会怎样:一定要用 for 循环逐个处理,代码长 3 倍、性能还差。但注意:如果能用向量化就优先向量化,apply 本质是循环,数据量大时慢

df['col'].map(dict)
- 👉 怎么用:df['RegionCN'] = df['Region'].map({'North': '华北', 'South': '华南'})
- 💡 为什么用:做枚举值的翻译/映射时最简洁,比 apply 快
- ⚠️ 不用会怎样:可以用 apply 代替,但 map 更直观——本身就是"把 A 换成 B",没有误用的空间

df['col'].str.xxx
- 👉 怎么用:df['Product'].str.strip()df['Product'].str.contains('Widget')
- 💡 为什么用:pandas 把 Python 的字符串方法向量化了,一行代码处理整列,比 for 循环快一个数量级
- ⚠️ 不用会怎样:只能写 for 循环逐个处理,代码丑、性能差

df['col'].dt.xxx
- 👉 怎么用:df['YearMonth'] = df['Date'].dt.strftime('%Y-%m')
- 💡 为什么用:从日期列提取年/月/日/季度等字段时用,类似 str 访问器但针对时间类型
- ⚠️ 不用会怎样:不用的话要从日期字符串手动 split('-') 提取,麻烦且容易出错


分组 & 聚合

df.groupby('col').agg(...)
- 👉 怎么用:
python df.groupby('Category').agg( Total=('Revenue', 'sum'), Avg=('UnitPrice', 'mean'), Count=('TransactionID', 'count'), ).reset_index()
- 💡 为什么用:按类别分组,每个组做统计(求和、平均、计数等)——这是数据分析最核心的操作,等同于 SQL 的 GROUP BY
- ⚠️ 不用会怎样:你只能跑 df['Revenue'].sum() 得到全局总和,看不到"每个类别的总和"。不接 .reset_index() 的话,Category 列变成了索引

.transform('mean')
- 👉 怎么用:df['CategoryAvg'] = df.groupby('Category')['Revenue'].transform('mean')
- 💡 为什么用:把统计结果广播回原始数据的每一行,保持行数不变——常用于"计算每个值与其组均值的差异"
- ⚠️ 不用会怎样:如果用 .agg 代替,得到的是压缩后的分组表,还需要再 merge 回原表,多一步操作

.rolling(window).mean()
- 👉 怎么用:df['Sales_MA3'] = df_sorted['Sales'].rolling(window=3).mean()
- 💡 为什么用:消除短期波动,看趋势。常用的技术指标(移动平均线)就是它
- ⚠️ 不用会怎样:肉眼从原始数据看趋势很困难,特别是数据噪声大时

.shift(n)
- 👉 怎么用:df['Sales_Change'] = df['Sales'] - df['Sales'].shift(1)
- 💡 为什么用:把前一行的值拿到当前行,用来算环比差值(今天 - 昨天)
- ⚠️ 不用会怎样:你只能写个循环 for i in range(1, len(df)) 手动算

.cumsum()
- 👉 怎么用:df['Revenue_Cum'] = df['Revenue'].cumsum()
- 💡 为什么用:算累计值,比如"到每天为止的总销售额"
- ⚠️ 不用会怎样:写循环逐个累加,或者用 rolling(len(df), min_periods=1).sum() 替代

df.sort_values('col')
- 👉 怎么用:df.sort_values('Date', ascending=True, inplace=True)
- 💡 为什么用:rolling/shift/cumsum 等操作依赖数据顺序(上一行 / 下一行),必须先按时间排好序
- ⚠️ 不用会怎样:不排序就做 rolling,移动平均计算的是"当前行的前 N 行"——如果数据不是按时间排列的,算出来的移动平均毫无意义


重塑 & 合并

df.pivot_table(index, columns, values, aggfunc)
- 👉 怎么用:
python df.pivot_table(index='Region', columns='Category', values='Revenue', aggfunc='sum', fill_value=0)
- 💡 为什么用:把"长表"转成"宽表"——把某个字段的值展开为多列,每个格子是聚合后的数值。Excel 透视表就是这个功能
- ⚠️ 不用会怎样:只能用 groupby 然后手动整理成宽表格式,比较麻烦。Excel 用户拿到长表看不懂

df.melt(id_vars, var_name, value_name)
- 👉 怎么用:
python melted = pivot.reset_index().melt( id_vars=['Region'], var_name='Category', value_name='Revenue', )
- 💡 为什么用:pivot 的逆操作——把一张宽表变回长表,因为大多数机器学习模型和图表库都期望长表格式
- ⚠️ 不用会怎样:手动把几十列名和值拆成"变量-值"对,极其繁琐

pd.merge(left, right, on, how)
- 👉 怎么用:pd.merge(df, lookup_table, on='Product', how='left')
- 💡 为什么用:把两张表按某个键拼起来,等同 SQL 的 JOIN。real work 中天天用——数据永远不会只在一张表里
- ⚠️ 不用会怎样:数据散落在多张表,你只能手动 VLOOKUP 或者写复杂的笛卡尔积操作

pd.concat([df1, df2], axis=0)
- 👉 怎么用:all_data = pd.concat([q1_data, q2_data], axis=0, ignore_index=True)
- 💡 为什么用:纵向拼接多份结构相同的 DataFrame(如 1 月数据 + 2 月数据),多数据源整合时必用
- ⚠️ 不设 ignore_index=True:拼完后索引保持原来的,可能重复(0,1,2,0,1,2),后续 .loc[0] 会匹配到多行

4.2 读取参数详解

pd.read_csv() 是日常最常接触的函数,参数虽多但可归为几类:

pd.read_csv(
    path,

    # ── 文件结构 ──
    sep=',',            # 分隔符(CSV 用逗号,TSV 用 '\t')
    header=0,           # 表头在第几行(按 0 计)
    names=None,         # 自定义列名(配合 header=0 可跳过原表头)
    index_col=None,     # 指定某列作为行索引
    skiprows=[],        # 跳过前 N 行或指定行
    nrows=None,         # 只读前 N 行(测试数据时有用)

    # ── 类型控制(最易出错的区域)──
    dtype={},           # 强制指定列类型:dtype={'id': 'Int64', 'price': 'float64'}
    parse_dates=[],     # 需要解析为日期的列:parse_dates=['Date', 'CreatedAt']
    converters={},      # 自定义解析函数:converters={'col': lambda x: x.strip()}

    # ── 缺失值处理 ──
    na_values=[],       # 指定哪些值视为 NaN:na_values=['', 'NA', 'null', 'N/A']
    keep_default_na=True,  # 是否保留默认的 NaN 标记

    # ── 容错 ──
    on_bad_lines='warn',    # 'error'(抛异常,默认)| 'warn'(警告并跳过)| 'skip'(静默跳过)
    encoding='utf-8',       # 文件编码
    encoding_errors='strict',  # 'strict' | 'ignore' | 'replace'

    # ── 性能 ──
    usecols=[],         # 只读指定列,节省内存
    chunksize=None,     # 分块读取整型,每次返回迭代器
)

4.3 经典报错排查

报错信息 常见原因 解决方案
SettingWithCopyWarning 链式索引后赋值 改用 .loc 单步操作
ValueError: cannot convert float NaN to integer 试图把含 NaN 的列转为 int64 改用 Int64(大写 I)
PerformanceWarning: DataFrame is highly fragmented 频繁插入/删除列导致内存碎片 一次性创建列或 .copy() 后再操作
FutureWarning: Downcasting object dtype arrays on ... 新版 pandas 行为变更 按提示改用新 API
KeyError: 'ColumnName' 列名不存在 检查列名是否含空格/大小写差异
ParserError: Error tokenizing data. C error CSV 中某行列数不匹配 检查分隔符是否正确,或 on_bad_lines='warn' 跳过
AttributeError: 'DataFrame' object has no attribute 'append' 旧版用 append,新版已移除 改用 pd.concat([df1, df2])
ValueError: cannot reindex from a duplicate axis 索引有重复值,无法对齐 df = df[~df.index.duplicated()] 去重索引