📖 本篇简介:本篇把 Excel 里你天天在用的操作——VLOOKUP、透视表、筛选、排序、去重、条件格式、分列、查找替换——逐一翻译成 pandas 写法。每节先给一张"Excel 操作 | pandas 代码 | 说明"三栏对照表,再给一段可运行示例,并指出 Excel 里有、pandas 里没有(或反过来)的能力边界。前置知识:第 04 章 数据读取与写入、第 11 章 数据合并:concat-merge-join、第 12 章 数据重塑与透视表。学完后你看到任何一张 Excel 表,都能立刻想到对应的 pandas 一行代码。
Excel 是"所见即所得"的电子表格,pandas 是"代码即数据"的列式分析框架。对照学习的价值,不是把 pandas 当成 Excel 的替代品,而是知道哪些操作该交给 Excel、哪些该交给 pandas。
目录
1. 开篇:Excel 功能 ↔ pandas 总对照表
2. VLOOKUP ↔ merge(精确 / 近似匹配)
3. 透视表 ↔ pivot_table
4. 筛选 ↔ 布尔索引 / query
5. 排序 ↔ sort_values
6. 去重 ↔ drop_duplicates
7. 条件格式 ↔ Styler(background_gradient / apply)
8. 数据验证 ↔ 清洗约束(范围 / 枚举检查)
9. 分列 ↔ str.split(expand=True)
10. 数字格式 ↔ dtype / round
11. 冻结窗格与打印设置(pandas 无直接对应)
12. 合并单元格 ↔ MultiIndex
13. 查找替换 ↔ replace
14. Excel 文件读写基础
15. 常见坑
16. 本章小结与练习题
1. 开篇:Excel 功能 ↔ pandas 总对照表
刚从 Excel 转 pandas 的同学,最大的痛苦不是"语法记不住",而是"我不知道 pandas 里有没有这个东西"。这张总表先把地图给你,后面再逐项展开:
| Excel 功能 | pandas 写法 | 说明 |
|------------|-------------|------|
| VLOOKUP / XLOOKUP | df.merge(other, on=..., how=...) | 列关联,最常用 left/inner |
| 数据透视表 | pd.pivot_table(df, index=, columns=, values=, aggfunc=) | 行列任意组合聚合 |
| 自动筛选(下拉箭头) | 布尔索引 df[(df.x>0) & (df.y=='a')] 或 df.query(...) | 比 Excel 更灵活,可保存成脚本 |
| 排序 | df.sort_values(by=, ascending=) | 多列排序传列表 |
| 删除重复项 | df.drop_duplicates(subset=, keep=) | 按列去重 |
| 条件格式(色阶 / 高亮) | df.style.background_gradient(...) | 仅渲染,不改数据 |
| 数据验证(下拉框 / 范围限制) | 事后清洗:df[~df.x.isin(allowed)] | pandas 不"拦截"输入,只"检查"结果 |
| 分列 | df["col"].str.split(",", expand=True) | 一行展开成多列 |
| 设置单元格数字格式 | df["col"].round(2) / .map(lambda x: f"¥{x:,.2f}") | pandas 只存数值,展示在 Styler |
| 冻结首行 / 打印区域 | 无直接对应 | 这些是 Excel 的"展示层"概念 |
| 合并单元格 | pd.MultiIndex.from_tuples(...) | pandas 用层级索引表达"分组头" |
| 查找替换 | df.replace({"旧":"新"}) | 整列甚至整表批量替换 |
| 读取 .xlsx | pd.read_excel(path, sheet_name=, engine="openpyxl") | 需装 openpyxl |
| 导出 .xlsx | df.to_excel(path, index=False) | 同样依赖 openpyxl |
💡 一句话心智模型:Excel 把"数据 + 展示 + 交互"揉在一张表里;pandas 只负责"数据 + 计算",展示层(颜色、冻结、打印、公式栏)基本交给 Styler 或 Excel 自己。
2. VLOOKUP ↔ merge(精确 / 近似匹配)
Excel 里最高频的函数之一是 =VLOOKUP(查找值, 查找区域, 返回列号, FALSE)。pandas 对应的不是某个函数,而是 pd.merge——它把"按 key 关联两张表"这件事做成了一等公民。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| =VLOOKUP(B2, 客户表!A:C, 2, FALSE) | orders.merge(cust, on="cust_id", how="left") | 精确匹配,左连接保留订单全部行 |
| =VLOOKUP(B2, 区间!A:B, 2, TRUE) 近似匹配 | pd.merge_asof(left, right, on="时间列") 或 pd.cut(...) | 要求右表 key 已排序 |
| INDEX + MATCH 双向查找 | df.set_index("code").loc["A002", "price"] | 见第 50 章 |
import pandas as pd
# 模拟两张表:订单表 + 客户表
orders = pd.DataFrame({
"order_id": [101, 102, 103, 104],
"cust_id": [1, 2, 1, 3],
"amount": [120, 200, 80, 350],
})
cust = pd.DataFrame({
"cust_id": [1, 2, 3],
"cust_name": ["张三", "李四", "王五"],
"city": ["北京", "上海", "广州"],
})
# Excel: =VLOOKUP(B2, 客户表!A:C, 2, FALSE)
# pandas: on 是连接键,how="left" 等价于"左表全保留"
merged = orders.merge(cust, on="cust_id", how="left")
print(merged)
order_id cust_id amount cust_name city
0 101 1 120 张三 北京
1 102 2 200 李四 上海
2 103 1 80 张三 北京
3 104 3 350 王五 广州
精确 vs 近似的区别:
- 精确匹配(VLOOKUP 第四参数
FALSE/0):键必须完全相等,对应merge(on=...)。 - 近似匹配(VLOOKUP 第四参数
TRUE/1):右表要先排序,找"小于等于查找值的最大键",对应pd.merge_asof(...)(时间序列场景)或pd.cut(...)(分段场景)。
⚠️ 常见坑:Excel 的 VLOOKUP 只能从左向右查;pandas 的 merge 没有这个限制,但连接键在两张表里的名字必须一致,不一致用left_on=/right_on=。
3. 透视表 ↔ pivot_table
Excel 插入"数据透视表"对话框里拖拖拽拽的那套,pandas 里是一行 pd.pivot_table。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 行:部门;列:季度;值:销售额;汇总方式:求和 | pd.pivot_table(df, index="部门", columns="季度", values="销售额", aggfunc="sum") | 行列任意拖 |
| 汇总方式改成"计数" | aggfunc="count" | 也可以是 np.mean / "max" |
| 勾选"总计" | margins=True, margins_name="合计" | 行末加合计行 |
| 空单元格填 0 | fill_value=0 | 避免 NaN |
df = pd.DataFrame({
"部门": ["销售", "销售", "技术", "技术", "销售", "技术"],
"季度": ["Q1", "Q2", "Q1", "Q2", "Q2", "Q1"],
"销售额": [100, 150, 200, 180, 90, 220],
})
# 等价于 Excel 透视表:行=部门,列=季度,值=销售额,求和 + 总计
pivot = df.pivot_table(index="部门", columns="季度", values="销售额",
aggfunc="sum", fill_value=0,
margins=True, margins_name="合计")
print(pivot)
季度 Q1 Q2 合计
部门
技术 420 180 600
销售 100 240 340
合计 520 420 940
💡 Excel 透视表拖完后"结果是一张新表",pivot_table 返回的也是一张新 DataFrame,可以继续 to_excel 写回 Excel。4. 筛选 ↔ 布尔索引 / query
Excel 表头那排小箭头叫"自动筛选"。pandas 没有 UI,但布尔索引是它的超集——你可以把任意复杂条件写在一行里。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 筛选 dept = 销售 | df[df["dept"] == "销售"] | 布尔掩码 |
| 筛选 amount ≥ 1000 | df[df["amount"] >= 1000] | 比较运算 |
| 多条件(且) | df[(df.a==1) & (df.b>2)] | 每个条件加括号 |
| 多条件(或) | df[(df.a==1) \| (df.b>2)] | 竖线 \| |
| 包含某关键词 | df[df["name"].str.contains("张")] | str 访问器 |
| 类 SQL 写法 | df.query("dept == '销售' and amount >= 1000") | 字符串更简洁 |
sales = pd.DataFrame({
"name": ["张三", "李四", "王五", "赵六"],
"dept": ["销售", "技术", "销售", "技术"],
"amount": [1200, 800, 1500, 600],
})
# 方式一:布尔索引(最常用)
m1 = sales[(sales["dept"] == "销售") & (sales["amount"] >= 1000)]
print("布尔索引:\n", m1)
# 方式二:query(表达式更接近 Excel 的筛选条件对话框)
m2 = sales.query("dept == '销售' and amount >= 1000")
print("query:\n", m2)
布尔索引:
name dept amount
0 张三 销售 1200
2 王五 销售 1500
query:
name dept amount
0 张三 销售 1200
2 王五 销售 1500
⚠️ 常见坑:每个布尔条件都要用圆括号括起来,否则&的优先级会让表达式先于==计算,得到ValueError。
5. 排序 ↔ sort_values
Excel 工具栏上的 A→Z / Z→A 按钮,对应 sort_values。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 按单列升序 | df.sort_values("col") | 默认 ascending=True |
| 按单列降序 | df.sort_values("col", ascending=False) | |
| 多列排序(先按部门,部门内按金额降序) | df.sort_values(by=["dept","amount"], ascending=[True,False]) | 传列表 |
| 按索引排序 | df.sort_index() | |
print(sales.sort_values(by=["dept", "amount"], ascending=[True, False]))
name dept amount
1 李四 技术 800
3 赵六 技术 600
2 王五 销售 1500
0 张三 销售 1200
💡 pandas 3.0 起sort_values等方法的inplace=True参数已被弃用/移除,直接接收返回值即可:df = df.sort_values(...)。
6. 去重 ↔ drop_duplicates
Excel 的"数据 → 删除重复项"按钮,对应 drop_duplicates。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 按整行去重 | df.drop_duplicates() | 默认保留第一条 |
| 按某列去重(同 id 只留一行) | df.drop_duplicates(subset=["id"]) | subset 指定列 |
| 保留最后一条 | df.drop_duplicates(subset=["id"], keep="last") | keep="last" |
| 完全删除重复(不保留) | df.drop_duplicates(keep=False) | |
| 只看哪些行重复了 | df.duplicated(keep=False) | 返回布尔 Series |
dup = pd.DataFrame({
"id": [1, 1, 2, 3, 3, 3],
"name": ["A", "A", "B", "C", "C", "C"],
})
print("原始:\n", dup)
print("按 id 去重保留第一条:\n", dup.drop_duplicates(subset=["id"], keep="first"))
原始:
id name
0 1 A
1 1 A
2 2 B
3 3 C
4 3 C
5 3 C
按 id 去重保留第一条:
id name
0 1 A
2 2 B
3 3 C
7. 条件格式 ↔ Styler(background_gradient / apply)
Excel 的"开始 → 条件格式 → 色阶"可以把数值按大小染成红黄绿色。pandas 里对应的是 df.style,但它只负责渲染 HTML,不改 DataFrame 本身。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 色阶(红-黄-绿) | df.style.background_gradient(cmap="RdYlGn") | 数值列自动按大小上色 |
| 大于某值标红 | df.style.applymap(lambda v: "color:red" if v>90 else "") | 逐格返回 CSS |
| 导出带格式的 Excel | df.style.to_excel("out.xlsx") | 需要 openpyxl |
score = pd.DataFrame({
"姓名": ["张三", "李四", "王五"],
"数学": [90, 55, 72],
"英语": [60, 88, 40],
})
# 仅对"数学""英语"两列做红黄绿色阶
sty = score.style.background_gradient(cmap="RdYlGn", subset=["数学", "英语"])
print("Styler 对象类型:", type(sty).__name__)
# Styler 本身不打印数据,要导出 HTML 或 Excel 才能看到颜色
html = sty.to_html()
print("HTML 长度:", len(html), "包含 background:", "background" in html.lower())
Styler 对象类型: Styler
HTML 长度: 1583 包含 background: True
⚠️ 常见坑:
1.df.style是Styler对象,不是 DataFrame,不能继续做 merge/groupby。
2. pandas 3.0 起applymap已被重命名为map(DataFrame.style 里仍叫applymap,但 DataFrame 本身的applymap已移除)。
3. Styler 依赖jinja2,没装会报The '.style' accessor requires jinja2。
8. 数据验证 ↔ 清洗约束(范围 / 枚举检查)
Excel 的"数据 → 数据验证"是输入时拦截(不合法的值根本输不进去)。pandas 没有"输入拦截"这个概念,但它有更强大的事后检查——把不合法的行筛出来。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 限制 0 ≤ age ≤ 120 | df[(df.age<0) \| (df.age>120)] | 找出越界行 |
| 下拉框只能选"男/女" | df[~df.gender.isin({"男","女"})] | 枚举外的行 |
| 必填项不能为空 | df[df.col.isna()] | 找出缺失行 |
| 保存合法数据 | df_clean = df[df.age.between(0,120)] | 反向筛选 |
raw = pd.DataFrame({
"age": [20, 35, 150, -5, 40],
"gender": ["男", "女", "未知", "男", "其他"],
})
# 范围检查
bad_age = raw[(raw["age"] < 0) | (raw["age"] > 120)]
print("非法年龄行:\n", bad_age)
# 枚举检查
allowed = {"男", "女"}
bad_gender = raw[~raw["gender"].isin(allowed)]
print("非法性别行:\n", bad_gender)
非法年龄行:
age gender
2 150 未知
3 -5 男
非法性别行:
age gender
2 150 未知
4 40 其他
💡 工作流建议:Excel 适合"少量人工录入",pandas 适合"批量已有数据的清洗"。两者配合:Excel 录数据时开数据验证,导到 pandas 后再用上面的代码做二次兜底检查。
9. 分列 ↔ str.split(expand=True)
Excel"数据 → 分列"按分隔符把一列拆成多列。pandas 里是 str.split,关键参数 expand=True 让它返回 DataFrame 而不是列表。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 按"-"分列 | df["col"].str.split("-", expand=True) | expand=True 才会变成多列 |
| 限制拆成 2 段 | str.split("-", n=1, expand=True) | n=1 只切一刀 |
| 按固定宽度切 | str.slice(0,3) / str[0:3] | 见第 50 章 LEFT/RIGHT/MID |
addr = pd.DataFrame({"full": ["北京-朝阳区-望京", "上海-浦东新区-陆家嘴", "广东-深圳-南山"]})
parts = addr["full"].str.split("-", expand=True)
parts.columns = ["省", "市", "区"]
print(parts)
省 市 区
0 北京 朝阳区 望京
1 上海 浦东新区 陆家嘴
2 广东 深圳 南山
⚠️ 常见坑:忘了expand=True,返回的是 Series of list,不能直接赋给多列;赋值时要先给列命名,再pd.concat回原表。
10. 数字格式 ↔ dtype / round
Excel 单元格格式里设"保留 2 位小数""加千分位""加货币符号"——这些只是显示效果,底层数值不变。pandas 更纯粹:它只存数值,展示格式交给 Styler 或字符串格式化。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 保留 2 位小数 | df["col"].round(2) | 返回新 Series |
| 百分比显示 | (df["rate"]*100).round(2).astype(str) + "%" | 自己拼 |
| 货币格式 ¥1,234.50 | df["price"].map(lambda x: f"¥{x:,.2f}") | f-string 格式化 |
| 单元格显示 0.00 | Styler: df.style.format("{:.2f}") | 不改底层值 |
money = pd.DataFrame({"price": [19.999, 3.14159, 100.0]})
money["price_2f"] = money["price"].round(2)
print(money)
price price_2f
0 19.99900 20.00
1 3.14159 3.14
2 100.00000 100.00
⚠️ 常见坑:round(2)是"四舍五入到 2 位",但底层还是 float,参与后续计算时会重新出现多位小数。要做"货币精确计算"应改用Decimal或pandas的Float64(仍非精确),金融场景建议用整数"分"存储。
11. 冻结窗格与打印设置(pandas 无直接对应)
这一节专门说清楚:pandas 不做"展示层"的事。
| Excel 操作 | pandas 是否有对应 | 说明 |
|------------|-------------------|------|
| 视图 → 冻结首行 | ❌ 无 | 这是 Excel 打开文件后的浏览体验 |
| 页面布局 → 打印区域 | ❌ 无 | pandas 不生成"打印版式" |
| 插入图表(图表向导) | ✅ df.plot() / matplotlib | 见第 15 章 |
| 插入迷你图(Sparkline) | ⚠️ 需自己用 matplotlib 画 | pandas 无内置 |
| 设置列宽 / 行高 | ❌ 无 | 导出 Excel 后再用 openpyxl 调 |
| 自动换行 / 合并标题 | ❌ 无 | 同上 |
为什么 pandas 不做这些? 因为它的定位是"数据处理引擎",不是"展示软件"。正确协作方式:用 pandas 清洗计算 → df.to_excel("result.xlsx", index=False) 导出 → 在 Excel 里调冻结窗格、列宽、打印区域。需要自动化时可用 openpyxl 直接操作 xlsx(详见 第 22 章 数据读写进阶)。
12. 合并单元格 ↔ MultiIndex
Excel 里"合并居中"是为了做表头分组,比如:
| 销售 | 技术 |
| Q1 | Q2 | Q1 | Q2 |
| 100 | 150 | 200 | 180 |
pandas 不用合并单元格,而是用 MultiIndex(多层索引) 表达这种"分组头"。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 合并单元格做表头 | pd.MultiIndex.from_tuples([("销售","Q1"),...], names=["部门","季度"]) | 行/列都能多层 |
| 透视表产生多层列 | pivot_table(...) 默认就是 MultiIndex | 见第 3 节 |
midx = pd.MultiIndex.from_tuples([
("销售", "Q1"), ("销售", "Q2"),
("技术", "Q1"), ("技术", "Q2"),
], names=["部门", "季度"])
df2 = pd.DataFrame({"销售额": [100, 150, 200, 180]}, index=midx)
print(df2)
销售额
部门 季度
销售 Q1 100
Q2 150
技术 Q1 200
Q2 180
💡 pandas 的 MultiIndex 在"选择分组"上比 Excel 合并单元格强得多:df2.loc["销售"] 就能取出整个销售部门,合并单元格在 Excel 里反而是筛选排序的天敌。13. 查找替换 ↔ replace
Excel 的 Ctrl+H 替换对话框,对应 df.replace。
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 把 "N/A" 替换成空 | df.replace("N/A", np.nan) | 全表扫描 |
| 多值映射 | df.replace({"旧1":"新1", "旧2":"新2"}) | 字典映射 |
| 正则替换 | df.replace({"col": r"^\s+$"}, np.nan, regex=True) | regex=True |
| 只替换某一列 | df["col"] = df["col"].replace(...) | 局部替换 |
s = pd.Series(["N/A", "正常", "缺失", "正常", "N/A"])
print(s.replace({"N/A": np.nan, "缺失": np.nan}))
0 NaN
1 正常
2 NaN
3 正常
4 NaN
dtype: str
💡 注意最后一行dtype: str——pandas 3.0 默认字符串列就是str类型(旧版是object),缺失值自动变成pd.NA或np.nan,与字符串列共存。
14. Excel 文件读写基础
最基础的读写两行代码:
| Excel 操作 | pandas 代码 | 说明 |
|------------|-------------|------|
| 打开 .xlsx | pd.read_excel(path, sheet_name="Sheet1", engine="openpyxl") | 必须装 openpyxl |
| 另存为 .xlsx | df.to_excel(path, index=False, engine="openpyxl") | index=False 不导出行号 |
| 读所有 sheet | pd.read_excel(path, sheet_name=None) | 返回 {sheet名: DataFrame} |
| 写多个 sheet | with pd.ExcelWriter(path) as w: df1.to_excel(w, "A"); df2.to_excel(w, "B") | 上下文管理器 |
import os
out_xlsx = r"C:\Users\Administrator\AppData\Local\Temp\pandas_ch49_50\demo.xlsx"
df3 = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
df3.to_excel(out_xlsx, sheet_name="Sheet1", index=False, engine="openpyxl")
back = pd.read_excel(out_xlsx, sheet_name="Sheet1", engine="openpyxl")
print(back)
print("文件存在:", os.path.exists(out_xlsx))
a b
0 1 x
1 2 y
2 3 z
文件存在: True
💡 进阶玩法(指定列类型、跳过表头、读取合并单元格、写公式、用 xlsxwriter 加格式)见 第 22 章 数据读写进阶,这里不展开。
15. 常见坑
⚠️ 坑 1:Excel 日期序列号
Excel 用"1900 年 1 月 1 日 = 1"的整数存日期(而且还多算了 1900-02-29 这个不存在的日期)。如果你读到一列"看起来像日期"的整数(比如 44927),用:
pd.to_datetime(pd.Series([44927]), origin="1899-12-30", unit="D")
注意 origin 要写1899-12-30而不是1900-01-01,这是为了兼容 Excel 的闰年 bug。
>
⚠️ 坑 2:公式单元格读出来是缓存值
pd.read_excel默认读到的是 Excel 上次保存时的计算结果(缓存值),不是公式本身。如果 Excel 没打开重算过,缓存值可能是旧的。要读公式本身得用openpyxl直接操作cell.value加data_only=False。
>
⚠️ 坑 3:多 sheet 读取
sheet_name=0读第一个 sheet;sheet_name="销售表"按名字读;sheet_name=None一次读全部 sheet,返回dict[str, DataFrame]。忘了写 sheet_name 默认只读第一个。
>
⚠️ 坑 4:中文路径
pd.read_excel("D:\\资料\\数据.xlsx")在 Windows 上一般没问题,但如果遇到FileNotFoundError,先用os.path.exists(path)验证路径是否正确,再检查反斜杠是否被转义(建议用原始字符串r"...")。
>
⚠️ 坑 5:dtype 读取
Excel 里"数字 + 空单元格"混着的列,pandas 可能读成float64(因为 NaN 是浮点),导致整数列多出.0。用pd.read_excel(..., dtype={"id": "Int64"})指定可空整数类型。
16. 本章小结与练习题
小结
1. Excel 高频操作在 pandas 里都有一一对应:VLOOKUP↔merge、透视表↔pivot_table、筛选↔布尔索引/query、排序↔sort_values、去重↔drop_duplicates。
2. 展示层(条件格式、冻结窗格、打印、合并单元格)pandas 只做一半:Styler 管颜色,其余交给 Excel。
3. pd.read_excel / to_excel 是基础读写,进阶(多 sheet、公式、合并单元格、样式)在第 22 章。
4. 日期序列号、公式缓存值、中文路径、dtype 推断是读 Excel 时的四大经典坑。
练习题
1. 给定 orders 和 cust 两张表(见第 2 节),用 merge 写出"只保留有客户信息的订单"(inner join)和"保留所有客户,即使没订单"(right join)两种写法,并说明结果行数差异。
2. 用 pivot_table 对第 3 节的销售数据做一次"部门 × 季度"的平均销售额透视(aggfunc="mean"),并加上 margins。
3. 用布尔索引和 query 两种方式,分别筛选出 sales 表中"销售部门 且 amount 大于 1000"的行,对比代码长度。
4. 解释:为什么 df.style.background_gradient(...) 之后,df 本身的数值没有任何变化?如果想让颜色真的导出到 Excel 文件里,应该怎么做?
5. 一段数据 age = [20, 200, -5, 30],请写一段 pandas 代码:把越界(<0 或 >120)的行替换成 NaN,然后用 dropna() 得到清洗后的 DataFrame。
6. (思考)Excel 的"合并单元格"在 pandas 里被 MultiIndex 取代。请说出 MultiIndex 相比合并单元格的两个优势(提示:从筛选、排序角度想)。
下一篇预告:第 50 章 Excel 公式与 pandas 对照 —— 把 SUMIF、COUNTIF、IF、VLOOKUP、RANK 这些 Excel 公式逐一翻译成 pandas 表达式。
文章回复
0 条公开回复