📖 本篇简介:上一篇讲了 Excel"功能按钮"与 pandas 的对照,本篇讲"公式"的对照——把 SUMIF、SUMIFS、COUNTIF、COUNTIFS、IF、IFS、INDEX/MATCH、LEFT/RIGHT/MID、IFERROR、RANK、ROUND、CONCATENATE 这些天天写的 Excel 公式,逐一翻译成 pandas 表达式。每节先给 Excel 公式写法,再给 pandas 等价代码(已实跑),最后列注意事项。前置知识:第 49 章 Excel 常用功能与 pandas 对照、第 13 章 分组聚合:groupby、第 48 章 NumPy 与 pandas 协同。学完后你拿到一张带公式的 Excel 报表,能立刻用 pandas 复现全部计算逻辑。
Excel 公式是"单元格里写一段代码,逐格求值";pandas 是"整列向量化,一次算完"。理解这个差异,是理解所有公式对照的钥匙。
目录
1. 开篇:Excel 公式 ↔ pandas 总对照表
2. SUMIF / SUMIFS ↔ where + sum / groupby + sum
3. COUNTIF / COUNTIFS ↔ 布尔计数 / value_counts
4. IF / IFS ↔ np.where / np.select
5. SUM 整列 ↔ Series.sum
6. INDEX / MATCH ↔ loc
7. LEFT / RIGHT / MID ↔ str.slice
8. IFERROR ↔ fillna / where / 掩码
9. RANK ↔ rank
10. ROUND ↔ round
11. CONCATENATE / TEXT ↔ str 拼接与格式化
12. VLOOKUP 模糊匹配 ↔ merge_asof / pd.cut
13. 综合案例:按部门汇总超预算金额
14. 常见坑
15. 本章小结与练习题
1. 开篇:Excel 公式 ↔ pandas 总对照表
刚从 Excel 转 pandas 的人,最常问的一句话是:"我在 Excel 里写了 5 层嵌套的 IF,pandas 里怎么写?"这张总表先把地图给你:
| Excel 公式 | pandas 等价写法 | 说明 |
|------------|-----------------|------|
| =SUMIF(A:A, "销售", B:B) | df.loc[df.dept=="销售", "amount"].sum() | 条件求和 |
| =SUMIFS(C:C, A:A, "销售", B:B, "差旅") | df.loc[(df.dept=="销售")&(df.item=="差旅"), "amount"].sum() | 多条件求和 |
| =COUNTIF(C:C, ">60") | (df.score>=60).sum() | 条件计数 |
| =IF(A2>=60, "及格", "不及格") | np.where(df.score>=60, "及格", "不及格") | 二分支 |
| =IFS(A2>=90,"优",A2>=60,"良",TRUE,"差") | np.select([c1,c2,c3], ["优","良","差"]) | 多分支 |
| =SUM(B:B) | df["col"].sum() | 整列求和 |
| =INDEX(B:B, MATCH("A002", A:A, 0)) | df.set_index("code").loc["A002", "price"] | 双向查找 |
| =LEFT(A2, 3) / =RIGHT(A2, 3) / =MID(A2, 2, 3) | s.str[:3] / s.str[-3:] / s.str[1:4] | 字符串切片 |
| =IFERROR(A2/B2, 0) | (a/b).where(b!=0, other=np.nan).fillna(0) | 错误兜底 |
| =RANK(B2, B:B, 0) | s.rank(ascending=False, method="average") | 排名 |
| =ROUND(A2, 2) | s.round(2) | 四舍五入 |
| =A2&"-"&B2 / =TEXT(A2,"¥#,##0.00") | s1.str.cat(s2, sep="-") / s.map(lambda x: f"¥{x:,.2f}") | 拼接 / 格式化 |
| =VLOOKUP(A2, 区间, 2, TRUE) 模糊匹配 | pd.cut(...) 或 pd.merge_asof(...) | 分段 / 有序关联 |
💡 核心差异:Excel 公式写在单个单元格里,向下拖拽时行号自动变化;pandas 写在整列上,一次算完全表。所以 pandas 代码更短,但心智模型要从"格子"切换到"列"。
2. SUMIF / SUMIFS ↔ where + sum / groupby + sum
Excel 里 SUMIF 是"单条件求和",SUMIFS 是"多条件求和"。pandas 里等价写法有两种:布尔索引 + sum(一次性)和 groupby + sum(按组汇总)。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =SUMIF(A:A, "销售", C:C) | df.loc[df.dept=="销售", "amount"].sum() | 单条件 |
| =SUMIFS(C:C, A:A, "销售", B:B, "差旅") | df.loc[(df.dept=="销售")&(df.item=="差旅"), "amount"].sum() | 多条件 |
| 按部门汇总 | df.groupby("dept")["amount"].sum() | 一次出所有组 |
import numpy as np
import pandas as pd
exp = pd.DataFrame({
"dept": ["销售", "销售", "技术", "技术", "销售", "技术"],
"item": ["差旅", "招待", "差旅", "设备", "办公", "差旅"],
"amount": [1200, 800, 3000, 5000, 200, 1500],
})
# SUMIF: 销售部门的支出总和
total_sales = exp.loc[exp["dept"] == "销售", "amount"].sum()
print("SUMIF(销售) =", total_sales)
# SUMIFS: 销售部门 + 差旅 类
total_sales_travel = exp.loc[(exp["dept"] == "销售") & (exp["item"] == "差旅"), "amount"].sum()
print("SUMIFS(销售+差旅) =", total_sales_travel)
# 等价 groupby:一次得到所有部门×类别的汇总
g = exp.groupby(["dept", "item"])["amount"].sum()
print("groupby 透视:\n", g)
SUMIF(销售) = 2200
SUMIFS(销售+差旅) = 1200
groupby 透视:
dept item
技术 差旅 4500
设备 5000
销售 办公 200
差旅 1200
招待 800
Name: amount, dtype: int64
注意事项:
- 多条件之间用
&,每个条件必须加圆括号。 - 如果你要的是"每个部门各自的 SUMIF",用
groupby比写 N 次布尔索引快得多,也更不容易错。
3. COUNTIF / COUNTIFS ↔ 布尔计数 / value_counts
COUNTIF 是"数满足条件的行数"。pandas 里布尔 Series 求和就是计数(True=1,False=0)。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =COUNTIF(C:C, ">60") | (df.score>=60).sum() | 条件计数 |
| =COUNTIFS(A:A, "销售", C:C, ">=60") | ((df.dept=="销售")&(df.score>=60)).sum() | 多条件 |
| =COUNTIF(A:A, "销售") | (df.dept=="销售").sum() 或 df.dept.value_counts()["销售"] | 频数 |
scores = pd.DataFrame({
"name": ["张三", "李四", "王五", "赵六", "钱七"],
"dept": ["销售", "技术", "销售", "技术", "销售"],
"score": [90, 55, 72, 40, 88],
})
# COUNTIF: 成绩 >= 60 的人数
pass_cnt = (scores["score"] >= 60).sum()
print("及格人数 =", pass_cnt)
# COUNTIFS: 销售部门 且 及格
cnt = ((scores["dept"] == "销售") & (scores["score"] >= 60)).sum()
print("销售部门及格人数 =", cnt)
# value_counts:一次性数出每个部门出现次数
print("按部门计数:\n", scores["dept"].value_counts())
及格人数 = 3
销售部门及格人数 = 3
按部门计数:
dept
销售 3
技术 2
Name: count, dtype: int64
⚠️ 常见坑:(df.x>0).sum()得到的是 int,不是 DataFrame;不要写成df[(df.x>0)].count(),那会返回每列的非空行数,不是你想要的。
4. IF / IFS ↔ np.where / np.select
Excel 的 IF 是二分支,IFS 是多分支(Excel 2019+ 才有)。pandas 对应 np.where(二分支)和 np.select(多分支)。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =IF(A2>=60, "及格", "不及格") | np.where(s>=60, "及格", "不及格") | 二分支 |
| =IFS(A2>=90,"优",A2>=60,"良",TRUE,"差") | np.select([s>=90,s>=60], ["优","良"], default="差") | 多分支,从上往下匹配 |
s = pd.Series([95, 55, 72, 30, 88])
# IF: >=60 及格 否则 不及格
label = np.where(s >= 60, "及格", "不及格")
print("IF:\n", label)
# IFS: 多分支 np.select
conds = [s >= 90, s >= 60, s >= 0]
choices = ["优", "良", "差"]
grade = np.select(conds, choices, default="缺考")
print("IFS:\n", grade)
IF:
['及格' '不及格' '及格' '不及格' '及格']
IFS:
['优' '差' '良' '差' '良']
注意事项:
np.select的条件列表从上往下匹配,命中第一个就停——这和 ExcelIFS完全一致。- 不要用
df.apply(lambda row: ...)写条件分支,np.where/np.select是向量化的,快几十倍(见第 48 章)。
5. SUM 整列 ↔ Series.sum
最简单的一个:=SUM(B:B) 就是 df["col"].sum()。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =SUM(B:B) | df["col"].sum() | 整列求和 |
| =AVERAGE(B:B) | df["col"].mean() | 平均 |
| =MAX(B:B) / =MIN(B:B) | df["col"].max() / df["col"].min() | 最大最小 |
print("总和 =", s.sum())
print("平均 =", s.mean())
总和 = 340
平均 = 68.0
💡 pandas 默认会跳过 NaN 再聚合(skipna=True),Excel 的SUM也会自动忽略空单元格,行为一致。
6. INDEX / MATCH ↔ loc
Excel 里 INDEX + MATCH 是比 VLOOKUP 更灵活的"双向查找":MATCH 找到行号,INDEX 取值。pandas 里就是 loc——按索引标签或布尔掩码取数。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =INDEX(B:B, MATCH("A002", A:A, 0)) | df.set_index("code").loc["A002", "price"] | 按键查值 |
| =INDEX(2:2, 3) 取第 2 行第 3 列 | df.iloc[1, 2] | 按位置查 |
| 按列名取整列 | df["price"] | |
lookup = pd.DataFrame({
"code": ["A001", "A002", "A003"],
"name": ["苹果", "香蕉", "橙子"],
"price": [5, 3, 6],
}).set_index("code")
# MATCH 找到 code=A002 的行, INDEX 取 price
print("A002 价格 =", lookup.loc["A002", "price"])
# 按列名取整列
print("price 列 =", lookup["price"].tolist())
A002 价格 = 3
price 列 = [5, 3, 6]
注意事项:
loc按标签索引,iloc按位置索引。Excel 的MATCH(..., 0)(精确匹配)对应loc。- 如果 code 列有重复,
loc["A002"]会返回多行,需要再用.iloc[0]取第一个。
7. LEFT / RIGHT / MID ↔ str.slice
Excel 的 LEFT(A2, n) 取前 n 个字符,RIGHT(A2, n) 取后 n 个,MID(A2, start, n) 从第 start 位取 n 个。pandas 里直接用 Python 切片语法。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =LEFT(A2, 3) | s.str[:3] | 前 3 个 |
| =RIGHT(A2, 3) | s.str[-3:] | 后 3 个 |
| =MID(A2, 2, 3) 从第 2 位起取 3 个 | s.str[1:4] | Python 切片从 0 开始,左闭右开 |
txt = pd.Series(["abcdefg", "1234567", "Python"])
print("LEFT 3:\n", txt.str[:3])
print("RIGHT 3:\n", txt.str[-3:])
print("MID 2,3 (从第2位取3个):\n", txt.str[1:4])
LEFT 3: RIGHT 3: MID 2,3:
0 abc 0 efg 0 bcd
1 123 1 567 1 234
2 Pyt 2 hon 2 yth
dtype: str dtype: str dtype: str
⚠️ 常见坑:Excel 的MID(A2, 2, 3)是"从第 2 位开始取 3 个"(1-based),pandas 的s.str[1:4]是"从索引 1 取到索引 4"(0-based,左闭右开)。换算公式:Excel MID(text, start, n) ≈ pandas s.str[start-1:start-1+n]。
8. IFERROR ↔ fillna / where / 掩码
Excel 的 =IFERROR(A2/B2, 0) 意思是:算 A2/B2,如果出错就返回 0。pandas 里除法本身不会"报错",而是产生 NaN 或 inf(旧版),用 fillna 兜底。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =IFERROR(A2/B2, 0) | (a/b).fillna(0) | 出错填 0 |
| =IFERROR(A2/B2, "") | (a/b).astype(object).where(b!=0, other="") | 填字符串 |
| 屏蔽 0 除 | (a/b).where(b!=0) | 保持 NaN |
mixed = pd.Series([10, 0, 20, 0, 30], dtype="float64")
b = pd.Series([2, 0, 4, 0, 5], dtype="float64")
# pandas 3.0 中 Series 除以 0 直接得到 NaN(旧版会得到 inf)
ratio = mixed / b
print("原始除法:\n", ratio)
# 用 where 屏蔽 0,再 fillna 兜底
safe = ratio.where(b != 0, other=np.nan).fillna(0)
print("IFERROR 处理后:\n", safe)
原始除法:
0 5.0
1 NaN
2 5.0
3 NaN
4 6.0
dtype: float64
IFERROR 处理后:
0 5.0
1 0.0
2 5.0
3 0.0
4 6.0
dtype: float64
⚠️ 版本差异:pandas < 2.0 里Series / 0会得到inf(无穷大),需要replace([np.inf,-np.inf], np.nan).fillna(0);pandas 3.0 起 Series 除以 0 直接给 NaN,省一步。
9. RANK ↔ rank
Excel 的 =RANK(B2, B:B, 0) 是降序排名,=RANK(B2, B:B, 1) 是升序。pandas 里 s.rank() 有多个 method 控制并列值的处理方式。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =RANK(B2, B:B, 0) 降序,并列取平均 | s.rank(ascending=False, method="average") | Excel 默认 |
| 并列名次连续(dense rank) | s.rank(ascending=False, method="dense") | 1,2,2,3 而不是 1,2,2,4 |
| 并列取最小名次 | s.rank(ascending=False, method="min") | |
r = pd.Series([90, 85, 90, 70, 100])
print("降序 rank (method=average):\n", r.rank(ascending=False, method="average"))
print("降序 rank (method=dense):\n", r.rank(ascending=False, method="dense"))
print("降序 rank (method=min):\n", r.rank(ascending=False, method="min"))
降序 rank (method=average): [2.5, 4.0, 2.5, 5.0, 1.0]
降序 rank (method=dense): [2.0, 3.0, 2.0, 4.0, 1.0]
降序 rank (method=min): [2.0, 4.0, 2.0, 5.0, 1.0]
dtype 均为 float64
注意事项:
- Excel 的
RANK默认是"并列取平均"(两个 90 分并列第 2,下一个是第 4),对应method="average"。 method="dense"是比赛排名(1,2,2,3),method="min"是中式排名(1,2,2,4),按业务需要选。
10. ROUND ↔ round
最简单的一个:=ROUND(A2, 2) 就是 s.round(2)。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =ROUND(A2, 2) | s.round(2) | 保留 2 位 |
| =ROUND(A2, 0) | s.round(0) | 取整 |
| =ROUNDUP(A2, 0) / =ROUNDDOWN(A2, 0) | np.ceil(s) / np.floor(s) | 向上 / 向下 |
print(pd.Series([3.14159, 2.71828, 1.445]).round(2))
0 3.14
1 2.72
2 1.44
dtype: float64
⚠️ 常见坑:round(2)用的是"银行家舍入"(四舍六入五成双),不是 Excel 的"四舍五入"。1.445在上面结果是1.44而不是1.45——因为浮点数1.445实际存储是1.4449999...。要严格四舍五入用Decimal或np.floor(x*100+0.5)/100。
11. CONCATENATE / TEXT ↔ str 拼接与格式化
Excel 的 =A2&"-"&B2 拼接,=TEXT(A2,"¥#,##0.00") 格式化。pandas 里是 str.cat 和 f-string。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =A2&"-"&B2 | s1.str.cat(s2, sep="-") | 两列拼接 |
| =TEXT(A2, "¥#,##0.00") | s.map(lambda x: f"¥{x:,.2f}") | 货币格式化 |
| =TEXT(A2, "0.0%") | (s*100).round(2).astype(str) + "%" | 百分比 |
name = pd.Series(["张三", "李四"])
dept = pd.Series(["销售", "技术"])
print("拼接:\n", name.str.cat(dept, sep="-"))
money = pd.Series([1234.5, 6789.0])
print("格式化:\n", money.map(lambda x: f"¥{x:,.2f}"))
拼接:
0 张三-销售
1 李四-技术
dtype: str
格式化:
0 ¥1,234.50
1 ¥6,789.00
dtype: str
12. VLOOKUP 模糊匹配 ↔ merge_asof / pd.cut
Excel 的 =VLOOKUP(分数, 等级表!A:B, 2, TRUE) 是"按区间近似匹配"。pandas 里有两种场景:分段打标用 pd.cut,有序关联用 pd.merge_asof。
| Excel 公式写法 | pandas 等价代码 | 说明 |
|----------------|-----------------|------|
| =VLOOKUP(分数, 等级表, 2, TRUE) 分段 | pd.cut(s, bins=[0,60,80,100], labels=["不及格","及格","优秀"]) | 等宽分段 |
| 按时间近似关联 | pd.merge_asof(left, right, on="时间", direction="backward") | 时间序列常用 |
# 场景一:分数分段
score_bin = pd.Series([95, 55, 72, 40, 88])
bins = [0, 60, 80, 100]
labels = ["不及格", "及格", "优秀"]
cat = pd.cut(score_bin, bins=bins, labels=labels, include_lowest=True)
print("pd.cut 分级:\n", cat)
# 场景二:merge_asof 按时间近似匹配
left = pd.DataFrame({"time": [1, 3, 5, 7], "val": [10, 20, 30, 40]})
right = pd.DataFrame({"time": [1, 4, 6], "price": [100, 200, 300]})
m = pd.merge_asof(left, right, on="time", direction="backward")
print("merge_asof:\n", m)
pd.cut 分级:
0 优秀
1 不及格
2 及格
3 不及格
4 优秀
dtype: category
Categories (3, str): ['不及格' < '及格' < '优秀']
merge_asof:
time val price
0 1 10 100
1 3 20 100
2 5 30 200
3 7 40 300
💡 注意 merge_asof 的结果:time=3 时,right 里没有 time=3,向后找最近的 time=1 → price=100;time=7 时向后找 time=6 → price=300。这正是 Excel VLOOKUP 近似匹配的行为。
13. 综合案例:按部门汇总超预算金额
最后用一个综合案例把上面所有公式串起来。需求:
财务给了两张表:① 各部门月度预算;② 本月每笔实际支出。要求用 pandas 算出每个部门"实际支出 - 预算 = 超支金额",并标记"超支 / 节余"。
# 预算表
budget = pd.DataFrame({
"dept": ["销售", "技术", "行政"],
"budget": [5000, 8000, 2000],
})
# 实际支出表
actual = pd.DataFrame({
"dept": ["销售", "销售", "技术", "技术", "技术", "行政", "行政"],
"amount": [1200, 4500, 3000, 5000, 200, 1500, 800],
})
# 步骤 1: 按部门汇总实际支出(等价 SUMIFS)
act_sum = actual.groupby("dept", as_index=False)["amount"].sum().rename(columns={"amount": "actual"})
print("各部门实际支出:\n", act_sum)
# 步骤 2: 与预算合并(等价 VLOOKUP 把预算贴到实际表)
report = budget.merge(act_sum, on="dept", how="left").fillna({"actual": 0})
# 步骤 3: 计算超支 + 打标(等价 IF)
report["over"] = report["actual"] - report["budget"]
report["status"] = np.where(report["over"] > 0, "超支", "节余")
print("最终报表:\n", report)
# 步骤 4: 只看超支部门(等价 COUNTIF 的筛选版)
print("超支部门:\n", report[report["over"] > 0])
各部门实际支出:
dept actual
0 技术 8200
1 行政 2300
2 销售 5700
最终报表:
dept budget actual over status
0 销售 5000 5700 700 超支
1 技术 8000 8200 200 超支
2 行政 2000 2300 300 超支
超支部门(即 over>0 的行): 销售 700 / 技术 200 / 行政 300
💡 这个案例里:groupby+sum替代了 SUMIFS,merge替代了 VLOOKUP,np.where替代了 IF,布尔索引替代了筛选——四步拼起来就是一张完整的财务报表。
14. 常见坑
⚠️ 坑 1:Excel MID 与 pandas 切片的偏移
ExcelMID(text, start, n)是 1-based,pandass.str[a:b]是 0-based 且左闭右开。换算:Excel MID(t, s, n) ≈ pandas t.str[s-1 : s-1+n]。
>
⚠️ 坑 2:pandas 3.0 除法不再返回 inf
旧版 pandas 里Series / 0会得到inf,需要replace([np.inf,-np.inf], np.nan)清洗;pandas 3.0 起直接给 NaN,fillna(0)一步到位。写兼容代码建议统一用(a/b).replace([np.inf,-np.inf], np.nan).fillna(0)。
>
⚠️ 坑 3:np.where 返回 numpy 数组,不是 Series
np.where(s>0, 1, 0)返回numpy.ndarray,赋给新列没问题,但要继续调用.str/.dt等 pandas 访问器会报错。需要时用pd.Series(..., index=s.index)包回去。
>
⚠️ 坑 4:rank 的 method 选错导致排名错
ExcelRANK默认是method="average"(并列取平均,如 2.5),不是dense。如果你想要 1,2,2,3 这种比赛排名,必须显式写method="dense"。
>
⚠️ 坑 5:GROUPBY 后列名变了
df.groupby("dept")["amount"].sum()返回的是 Series,名字叫amount;df.groupby("dept", as_index=False)["amount"].sum()才是 DataFrame。要继续 merge 时记得加as_index=False。
15. 本章小结与练习题
小结
1. Excel 公式在 pandas 里几乎都有一一对应:SUMIF/SUMIFS↔布尔索引+sum / groupby、COUNTIF↔布尔求和、IF↔np.where、IFS↔np.select、INDEX/MATCH↔loc、LEFT/RIGHT/MID↔str 切片、IFERROR↔fillna、RANK↔rank、ROUND↔round。
2. 核心差异:Excel 公式逐格求值,pandas 整列向量化;写 pandas 代码要从"格子思维"切换到"列思维"。
3. 多分支用 np.select,不要用 apply(lambda row: ...)——向量化快几十倍。
4. 分段打标用 pd.cut,时间近似关联用 pd.merge_asof,是 Excel VLOOKUP 模糊匹配的两个标准替代。
练习题
1. 给定 df = pd.DataFrame({"dept":["销售","技术","销售"],"amount":[100,200,300]}),分别用 (a) 布尔索引+sum、(b) groupby+sum 两种方式算出"销售部门总金额",并对比代码长度。
2. 用 np.select 实现:成绩 ≥90 为"优",≥75 为"良",≥60 为"及格",否则为"不及格"。写出完整代码并测试 [95, 80, 65, 50]。
3. 把 Excel 公式 =IFERROR(VLOOKUP(A2, 客户表!A:B, 2, FALSE), "未知") 翻译成 pandas 写法(提示:merge 后 fillna)。
4. 给定 s = pd.Series([88, 92, 88, 70, 100]),分别用 method="average"、"dense"、"min" 计算降序排名,解释三个结果的差异。
5. 用 pd.cut 把 [25, 45, 60, 75, 90] 分成三段:<30 青年、30~60 中年、>60 老年,并写出 labels。
6. (综合题)扩展第 13 节的财务案例:再加一列"预算使用率 = actual / budget",用 map 格式化成百分比字符串(保留 1 位小数),并按使用率降序排序输出。
下一篇预告:第 51 章 LeetCode pandas 题库精讲(一) —— 用 LeetCode DataFrame 真题把前面学过的过滤、合并、聚合练到肌肉记忆。
文章回复
0 条公开回复