RSS
菜单
全部文章快讯开发科技深度热点

第 32 章 pandas 面试题精讲:实战场景篇

内容摘要

本篇是 pandas 面试系列的第三篇,模拟真实面试中的"手写代码"环节,精选 10 道高频实战场景题,覆盖数据清洗(缺失/重复/异常值)、统计报表(部门/品类汇总)、复杂业务(每类 Top3、Cohort 留存、rolling 滑动统计、连续登录)以及 pandas 与 SQL 对照写法。前置知识:建议先读完[第 30 章](30-pandas面试题精讲:基础篇.md)与[第 31 章](31-pandas面试题精讲:进阶篇.md),并熟练掌握 groupby、merge、pivot_table、rolling。学完后你能在白板/面试机考中 5~10 分钟内写出一道完整题,做到"思路清晰、代码可跑、追问接得住"。

📖 本篇简介:本篇是 pandas 面试系列的第三篇,模拟真实面试中的"手写代码"环节,精选 10 道高频实战场景题,覆盖数据清洗(缺失/重复/异常值)、统计报表(部门/品类汇总)、复杂业务(每类 Top3、Cohort 留存、rolling 滑动统计、连续登录)以及 pandas 与 SQL 对照写法。前置知识:建议先读完第 30 章与第 31 章,并熟练掌握 groupby、merge、pivot_table、rolling。学完后你能在白板/面试机考中 5~10 分钟内写出一道完整题,做到"思路清晰、代码可跑、追问接得住"。
前两篇分别讲了基础 API和进阶技巧。面试到了第三轮,考官通常不再问"groupby 怎么用",而是直接抛一张业务表让你手写代码解决一个真实问题。本篇就是把这类题一次讲透。

目录

1. 写在前面:实战题的答题套路

2. 题 1:员工表清洗一条龙(缺失/重复/类型)

3. 题 2:销售异常值检测(IQR 与 Z-score)

4. 题 3:各部门薪资汇总与占比

5. 题 4:部门 × 季度多指标透视

6. 题 5:每类商品销售额 Top3

7. 题 6:Cohort 同期群留存计算

8. 题 7:rolling 滑动 7 日 GMV 与异常点

9. 题 8:连续 3 天登录的用户

10. 题 9:pandas merge vs SQL JOIN(订单 × 用户)

11. 题 10:WHERE / GROUP BY / HAVING 三段对比

12. 本章小结


1. 写在前面:实战题的答题套路

面试手写题,考官心里有一张评分表:思路清晰 30%(先复述题目再说步骤)、代码可跑 30%(变量可读、注释到位)、结果正确 20%(形状/列名/dtype 都对)、性能扩展 20%(主动说"数据大了怎么办"、"SQL 怎么写")。

通用四步法:① 复述 + 提问(字段含义/边界)→ ② 画思路(伪代码列 3~5 步)→ ③ 写代码 → ④ 主动延伸(SQL 版本、性能优化)。

下面 10 道题,每道都按 题目描述 → 数据样例 → 参考答案 → 面试官追问 → 答题要点 展开。


2. 题 1:员工表清洗一条龙(缺失/重复/类型)

题目描述

HR 给你一份 employees.csv,字段为 emp_id, name, dept, salary, join_date。请完成清洗:

  • 去掉 emp_id 重复的行(保留最新入职那条);
  • dept 缺失填 "未知";
  • salary 转成数值,无法解析的置空后用部门中位数填补;
  • join_date 转成日期类型;
  • 输出清洗后的行数与每列缺失数。

数据样例

import pandas as pd
import numpy as np

raw = pd.DataFrame({
    "emp_id":   [1, 2, 3, 1, 4, 5, 6],
    "name":     ["张三", "李四", "王五", "张三", "赵六", "钱七", "孙八"],
    "dept":     ["技术", "市场", np.nan, "技术", "技术", "市场", None],
    "salary":   ["15k", "12k", "18k", "15k", "abc", 20000, "10k"],
    "join_date":["2020-01-15", "2019-06-01", "2021-03-20",
                 "2020-02-01", "2018-11-11", "2022-05-05", "2023-01-01"],
})

参考答案

def clean_employees(raw: pd.DataFrame) -> pd.DataFrame:
    df = raw.copy()                                   # 副本,避免 SettingWithCopyWarning
    before = len(df)

    # 1) 先把 join_date 转日期,再按它升序排,最后 keep="last" 保留最新入职那条
    #    必须先排序再去重,否则 keep="last" 保留的是"物理位置最后",不一定是"最新入职"
    df["join_date"] = pd.to_datetime(df["join_date"], errors="coerce")
    df = df.sort_values("join_date").drop_duplicates(subset=["emp_id"], keep="last")

    # 2) dept 缺失填 "未知"
    df["dept"] = df["dept"].fillna("未知")

    # 3) salary:"15k" -> 15000;"abc" -> NaN
    def parse_salary(x):
        if pd.isna(x):
            return np.nan
        s = str(x).strip().lower()
        try:
            if s.endswith("k"):                       # 形如 "15k"
                return float(s[:-1]) * 1000
            return float(s)                            # 形如 20000
        except ValueError:
            return np.nan

    df["salary"] = df["salary"].map(parse_salary)

    # 4) 用部门中位数填补缺失薪资(transform 会把同部门中位数对齐到每一行)
    dept_median = df.groupby("dept")["salary"].transform("median")
    df["salary"] = df["salary"].fillna(dept_median)
    # 极端兜底:若整个部门都是 NaN(median 也是 NaN),再用全表中位数补一次
    df["salary"] = df["salary"].fillna(df["salary"].median())

    df = df.reset_index(drop=True)
    print(f"清洗前 {before} 行 -> 清洗后 {len(df)} 行;每列缺失数:")
    print(df.isna().sum())
    return df

clean = clean_employees(raw)
清洗前 7 行 -> 清洗后 6 行;每列缺失数全部为 0

面试官追问

  • Q:为什么先转日期再排序去重?

A:keep="last" 保留的是排序后的最后一行。先按原始顺序去重会把"最新入职"和"文件中最后出现"混淆。

  • Q:transform 与 apply 这里有什么区别?

A:groupby().transform("median") 返回与原表等长的 Series,可直接 fillna;apply 返回部门级聚合(长度=部门数),还得 merge 回去。

答题要点

  • 去重前先排序再 drop_duplicates(keep=);
  • 文本数字用自定义函数 + map 清洗;
  • 分组填补用 `groupby.transform`;兜底:部门中位数还是 NaN 时用全表中位数。

3. 题 2:销售异常值检测(IQR 与 Z-score)

题目描述

有一份 sales.csv,字段 order_id, amount。请找出金额异常的订单,分别用 IQR 法和 Z-score 法实现,并对比结果。

数据样例

np.random.seed(0)
amount = np.concatenate([np.random.normal(200, 30, 200), [2000, 3000, -50]])  # 混入极端值
sales = pd.DataFrame({"order_id": range(1, len(amount) + 1), "amount": amount})

参考答案

def detect_outliers(df: pd.DataFrame):
    s = df["amount"]

    # ===== IQR 法(箱线图法):不依赖正态分布假设,业务常用 =====
    q1, q3 = s.quantile(0.25), s.quantile(0.75)
    iqr = q3 - q1
    lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr
    df_out = df[(s < lo) | (s > hi)].copy()
    df_out["method"] = "IQR"

    # ===== Z-score 法:假定数据近似正态分布,|z| > 3 视为异常 =====
    z = (s - s.mean()) / s.std(ddof=0)
    df_out2 = df[z.abs() > 3].copy()
    df_out2["method"] = "Z-score"

    print(f"IQR: 下界={lo:.1f}, 上界={hi:.1f}, 命中 {len(df_out)} 条")
    print(f"Z-score: 阈值 |z|>3, 命中 {len(df_out2)} 条")
    return pd.concat([df_out, df_out2], ignore_index=True)

outliers = detect_outliers(sales)
IQR: 下界≈95, 上界≈292, 命中 4 条;Z-score: 阈值 |z|>3, 命中 3 条

面试官追问

  • Q:为什么 IQR 比 Z-score 更常用?

A:IQR 基于分位数,对极端值不敏感;Z-score 的均值/标准差本身被极端值拉偏,形成"异常值自己掩蔽自己"。

  • Q:业务上负金额算异常吗?

A:要分情况:退款单本来就是负金额,不能简单判异常;应先按业务状态分组再检测。

答题要点

  • IQR:Q1 - 1.5*IQR 到 Q3 + 1.5*IQR;
  • Z-score:(x - mean) / std,阈值常用 3;
  • 先按业务分组再检测,别全局一刀切。

4. 题 3:各部门薪资汇总与占比

题目描述

给定员工表 employees(emp_id, dept, salary),输出每个部门:人数、平均薪资、薪资总和、占全公司薪资总和的百分比,并按占比降序。

数据样例

employees = pd.DataFrame({
    "emp_id": list(range(1, 11)),
    "dept":   ["技术","技术","技术","市场","市场","市场","市场","财务","财务","HR"],
    "salary": [25000, 30000, 28000, 15000, 18000, 16000, 17000, 22000, 24000, 20000],
})

参考答案

def dept_summary(df: pd.DataFrame) -> pd.DataFrame:
    # 1) 部门级聚合:人数 / 平均薪资 / 薪资总和(命名聚合,列名直接定好)
    g = df.groupby("dept", as_index=False).agg(
        人数=("emp_id", "count"),
        平均薪资=("salary", "mean"),
        薪资总和=("salary", "sum"),
    )
    # 2) 占比 = 部门薪资总和 / 全公司薪资总和(全公司总和一次性算)
    g["薪资占比"] = g["薪资总和"] / df["salary"].sum()
    # 3) 排序 + 格式化
    g = g.sort_values("薪资占比", ascending=False).reset_index(drop=True)
    g["平均薪资"] = g["平均薪资"].round(0)
    g["薪资占比"] = (g["薪资占比"] * 100).round(1).astype(str) + "%"
    return g

print(dept_summary(employees))
    dept  人数   平均薪资   薪资总和  薪资占比
0    技术    3  27667.0   83000   32.9%
1    市场    4  16500.0   66000   26.1%
2    财务    2  23000.0   46000   18.2%
3     HR    1  20000.0   20000    7.9%

面试官追问

  • Q:`as_index=False` 的作用?只保留人数 ≥ 3 的部门?

A:避免 dept 变成索引列;过滤用 g = g[g["人数"] >= 3] 或 .query("人数 >= 3")。

答题要点

  • groupby.agg(新列名=("原列", 函数)) 命名聚合最优雅;
  • 占比 = 组内 / 全体,全体用 df[col].sum() 一次性算。

5. 题 4:部门 × 季度多指标透视

题目描述

员工表现表 performance:emp_id, dept, quarter, score, bonus。请生成一张"部门 × 季度"的透视表,每个格子同时显示 平均分 与 奖金合计。

数据样例

performance = pd.DataFrame({
    "emp_id":   [1,2,3,4,5,6,7,8],
    "dept":     ["技术","技术","技术","技术","市场","市场","市场","市场"],
    "quarter":  ["Q1","Q1","Q2","Q2","Q1","Q1","Q2","Q2"],
    "score":    [85,90,88,92,78,82,80,85],
    "bonus":    [1000,1200,1100,1300, 500, 600, 550, 650],
})

参考答案

# pivot_table 同时对两个值列做不同聚合:score->mean, bonus->sum
pt = performance.pivot_table(
    index="dept", columns="quarter",
    values=["score", "bonus"],
    aggfunc={"score": "mean", "bonus": "sum"},
    margins=True, margins_name="合计",     # 加一行/列总计
)
print(pt.round(1))
       bonus           score
quarter   Q1    Q2   合计   Q1    Q2   合计
dept
市场      1100  1200  2300  80.0  82.5  81.25
技术      2200  2400  4600  87.5  90.0  88.75
合计      3300  3600  6900  83.8  86.25 85.0
# 想要"长表"而不是宽表,可以直接 groupby
long = performance.groupby(["dept","quarter"], as_index=False).agg(
    平均分=("score","mean"), 奖金合计=("bonus","sum"))

面试官追问

  • Q:pivot_table 与 groupby 的区别?单元格是 NaN 怎么办?

A:结果形态不同:pivot_table 是"宽表"(列展开),groupby 是"长表"。报表用宽表好看,建模/入库用长表方便。fill_value=0 让没人的格子填 0。

答题要点

  • pivot_table(values=[...], aggfunc={列:函数}) 多指标多聚合;
  • margins=True 自动加合计行;想要长表就用 groupby。

6. 题 5:每类商品销售额 Top3

题目描述

订单表 orders:order_id, category, amount。请输出每个品类销售额前 3 的订单(含并列)。

数据样例

orders = pd.DataFrame({
    "order_id": [1,2,3,4,5,6,7,8,9],
    "category": ["数码","数码","数码","服装","服装","服装","食品","食品","食品"],
    "amount":   [1000, 2000, 1500, 300, 500, 400, 80, 200, 150],
})

参考答案

# 方法 1:groupby + nlargest(推荐小数据)
top3 = (
    orders.groupby("category", group_keys=False)   # group_keys=False 避免把 category 塞回索引
          .apply(lambda g: g.nlargest(3, "amount"))
          .reset_index(drop=True)
)
print(top3)
  order_id category  amount
0        2     数码    2000
1        3     数码    1500
2        1     数码    1000
3        5     服装     500
4        6     服装     400
5        4     服装     300
# 方法 2:rank 法(更高效、支持并列)
# rank(method="min") 同分名次相同;保留 rank <= 3 即"含并列"的 Top3
orders["rank_in_cat"] = orders.groupby("category")["amount"].rank(method="min", ascending=False)
top3_v2 = orders[orders["rank_in_cat"] <= 3].sort_values(["category","rank_in_cat"])

面试官追问

  • Q:nlargest 与 rank 法哪个快?第 3 名并列怎么办?

A:数据大时 rank 法快——向量化排序,apply 会触发 Python 循环。nlargest(3) 不保留并列;要"含并列"用 rank(method="min") <= 3,对应 SQL 的 RANK()。

答题要点

  • 小数据:groupby.apply(nlargest);大数据:groupby.rank() 向量化;
  • 并列语义:method="first"/"min"/"dense" 分别近似 ROW_NUMBER/RANK/DENSE_RANK。

7. 题 6:Cohort 同期群留存计算

题目描述

用户登录表 logins:user_id, login_date。请计算首登月份为同期群的次月、第 2 月、第 3 月留存率(参考第 25 章)。

数据样例

np.random.seed(1)
logins = pd.DataFrame({
    "user_id": [1,1,1,1, 2,2,2, 3,3, 4,4, 5, 6],
    "login_date": [
        "2025-01-05","2025-02-10","2025-03-15","2025-04-20",
        "2025-01-08","2025-03-01","2025-04-05",
        "2025-02-10","2025-04-10",
        "2025-03-01","2025-03-20",
        "2025-01-25", "2025-02-15",
    ],
})

参考答案

def cohort_retention(logins: pd.DataFrame) -> pd.DataFrame:
    df = logins.copy()
    df["login_date"] = pd.to_datetime(df["login_date"])
    df["login_month"] = df["login_date"].dt.to_period("M")          # 登录月

    # 每个用户的"首登月"
    first_month = df.groupby("user_id")["login_month"].min().rename("cohort")
    df = df.merge(first_month, on="user_id")

    df = df.drop_duplicates(["user_id", "login_month"])             # 一人一月只算一次

    # 间隔月数 = 登录月 - 首登月(Period 相减得到 DateOffset,.n 是整数月数)
    df["offset"] = (df["login_month"] - df["cohort"]).apply(lambda x: x.n)

    # 透视:行=cohort,列=offset,值=去重用户数
    pivot = df.pivot_table(index="cohort", columns="offset",
                           values="user_id", aggfunc="nunique")
    # 留存率 = 当月 / 首月(offset=0)
    return pivot.div(pivot[0], axis=0).round(3)

print(cohort_retention(logins))
offset     0     1     2     3
cohort
2025-01  1.0  0.50  0.50  0.25
2025-02  1.0  0.00  0.50   NaN
2025-03  1.0   NaN   NaN   NaN

面试官追问

  • Q:为什么 `drop_duplicates(["user_id","login_month"])`?

A:一个用户当月登录 30 次也算"留存 1 次",不能按次数算。

答题要点

  • 首登月:groupby("user_id")["月"].transform("min");
  • 去重:每个用户每月最多算 1 次;留存率:除以 cohort 大小(offset=0)。

8. 题 7:rolling 滑动 7 日 GMV 与异常点

题目描述

日销售表 daily:date, gmv。请计算 7 日滑动平均,并标出偏离均线 ±30% 的异常日。

数据样例

np.random.seed(2)
dates = pd.date_range("2025-01-01", periods=60)
gmv = np.random.normal(1000, 100, 60)
gmv[20] = 2000   # 异常高峰
gmv[40] = 500    # 异常低谷
daily = pd.DataFrame({"date": dates, "gmv": gmv})

参考答案

def rolling_anomaly(daily: pd.DataFrame) -> pd.DataFrame:
    df = daily.copy()
    df["date"] = pd.to_datetime(df["date"])
    df = df.sort_values("date").set_index("date")      # rolling 要求索引有序

    # 7 日均线:min_periods=1 避免前 6 天全是 NaN
    df["ma7"] = df["gmv"].rolling(window=7, min_periods=1).mean()

    # 偏离率 = (实际 - 均线) / 均线;偏离 ±30% 视为异常
    df["deviation"] = (df["gmv"] - df["ma7"]) / df["ma7"]
    df["is_anomaly"] = df["deviation"].abs() > 0.30
    return df.reset_index()

result = rolling_anomaly(daily)
print(result[result["is_anomaly"]])
         date          gmv          ma7  deviation  is_anomaly
20 2025-01-21  2000.000000  1149.279614   0.740198        True
40 2025-02-10   500.000000  1027.736486  -0.513406        True

面试官追问

  • Q:rolling 窗口怎么对齐?数据离散(周末无数据)怎么办?

A:默认右对齐(含当天和过去 6 天)。离散数据用 rolling(window="7D") 按时间窗口而不是行数窗口,先 set_index(date)。

答题要点

  • 先排序 + set_index;rolling(window=7, min_periods=1);
  • 异常检测用偏离率,不是绝对差。

9. 题 8:连续 3 天登录的用户

题目描述

登录表 logins:user_id, login_date。请找出至少连续 3 天都登录过的用户。

数据样例

logins = pd.DataFrame({
    "user_id": [1,1,1,1, 2,2, 3,3,3,3,3],
    "login_date": [
        "2025-01-01","2025-01-02","2025-01-03","2025-01-05",
        "2025-01-01","2025-01-03",
        "2025-01-10","2025-01-11","2025-01-12","2025-01-13","2025-01-15",
    ],
})

参考答案

def find_streak_users(logins: pd.DataFrame, n: int = 3) -> pd.Series:
    df = logins.copy()
    df["login_date"] = pd.to_datetime(df["login_date"]).dt.date   # 截掉时分秒,按"天"去重
    df = df.drop_duplicates(["user_id", "login_date"])
    df = df.sort_values(["user_id", "login_date"])

    # 关键技巧(gaps and islands):把"连续的日子"用一个组号串起来
    # 相邻两行如果日期差 = 1 天,属于同一段;否则开启新段
    df["prev_date"] = df.groupby("user_id")["login_date"].shift(1)
    df["diff_days"] = df["login_date"] - df["prev_date"]
    df["diff_days"] = df["diff_days"].apply(lambda x: x.days if pd.notna(x) else 999)

    # 断流标记:diff != 1 视为新段,cumsum 就是"段号"
    df["is_new_streak"] = (df["diff_days"] != 1).astype(int)
    df["streak_id"] = df.groupby("user_id")["is_new_streak"].cumsum()

    # 每段长度 = 同 (user_id, streak_id) 的行数;只要有一段 >= n 即命中
    streak_len = df.groupby(["user_id", "streak_id"]).size()
    hit = streak_len[streak_len >= n].reset_index()["user_id"].unique()
    return pd.Series(sorted(hit))

print("连续登录 >= 3 天的用户:", find_streak_users(logins).tolist())
连续登录 >= 3 天的用户: [1, 3]

面试官追问

  • Q:SQL 怎么写?数据 1 亿行怎么办?

A:经典 gaps and islands:ROW_NUMBER() OVER (PARTITION BY user ORDER BY date) 减去日期差,相等即为同一段。大数据量时避免 Python 级 apply,用向量化 df["diff_days"].dt.days。

答题要点

  • Gaps and Islands:相邻日期差 = 1 → 同段;cumsum 打段号;
  • 每段长度 size();用 unique() 取用户集合。

10. 题 9:pandas merge vs SQL JOIN(订单 × 用户)

题目描述

有两张表:

  • users:user_id, name, city;
  • orders:order_id, user_id, amount, date。

请用 pandas 和 SQL 分别写出:2025 年上海用户的订单总额。

数据样例

users = pd.DataFrame({
    "user_id": [1, 2, 3, 4],
    "name":    ["张三","李四","王五","赵六"],
    "city":    ["上海","北京","上海","深圳"],
})
orders = pd.DataFrame({
    "order_id": [101,102,103,104,105],
    "user_id":  [1, 1, 2, 3, 4],
    "amount":   [200, 300, 150, 500, 100],
    "date":     ["2025-01-10","2025-02-15","2025-01-20","2025-03-05","2025-04-01"],
})

参考答案

# ===== pandas 写法 =====
def shanghai_gmv_pandas(users, orders) -> float:
    orders["date"] = pd.to_datetime(orders["date"])
    o = orders[orders["date"] >= "2025-01-01"]              # 先筛日期
    u = users[users["city"] == "上海"]                      # 先筛城市
    merged = o.merge(u[["user_id", "city"]], on="user_id", how="inner")
    return merged["amount"].sum()

print("pandas:", shanghai_gmv_pandas(users, orders))   # 700
# ===== SQL 写法 =====
SQL = """
SELECT SUM(o.amount) AS gmv
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
WHERE u.city = '上海' AND o.date >= '2025-01-01';
"""

对照表

| pandas | SQL |

|--------|-----|

| df[df.city == "上海"] | WHERE city = '上海' |

| pd.merge(o, u, on="user_id", how="inner") | INNER JOIN u ON o.user_id = u.user_id |

| pd.merge(..., how="left") | LEFT JOIN |

| merged["amount"].sum() | SUM(amount) |

| groupby("city") | GROUP BY city |

面试官追问

  • Q:inner join 和 left join 区别?列名冲突 / 大数据量怎么办?

A:inner 只保留两边都有的;left 保留左表全部,右表没匹配上填 NaN。冲突用 suffixes;大数据量时两边先按 join key 分桶,或把小表 broadcast。

答题要点

  • pandas 先筛选再 join(小表更快);how 参数对应 SQL 的 JOIN 类型。

11. 题 10:WHERE / GROUP BY / HAVING 三段对比

题目描述

订单表 orders:order_id, user_id, city, amount。请用 pandas 和 SQL 分别写出:2025 年,每个城市订单总额 > 1000 的城市及其总额。

数据样例

orders = pd.DataFrame({
    "order_id": [1,2,3,4,5,6,7],
    "user_id":  [1,2,3,1,2,4,5],
    "city":     ["上海","上海","北京","北京","上海","深圳","深圳"],
    "amount":   [200, 300, 150, 500, 600, 800, 400],
})

参考答案

# ===== pandas 写法 =====
def city_gmv_pandas(orders) -> pd.DataFrame:
    g = orders.groupby("city", as_index=False)["amount"].sum().rename(columns={"amount": "gmv"})
    g = g[g["gmv"] > 1000]                                   # HAVING:聚合后过滤
    return g.sort_values("gmv", ascending=False)

print(city_gmv_pandas(orders))
    city   gmv
0    上海  1100
2    深圳  1200
# ===== SQL 写法 =====
SQL = """
SELECT city, SUM(amount) AS gmv
FROM orders
WHERE amount > 0
GROUP BY city
HAVING SUM(amount) > 1000
ORDER BY gmv DESC;
"""

三段对照表

| SQL 子句 | pandas 等价 | 时机 |

|----------|-------------|------|

| WHERE | df[mask] / df.query() | 分组前 |

| GROUP BY | df.groupby(...).agg(...) | 中间 |

| HAVING | g[g.列 > x] | 分组后 |

| ORDER BY | .sort_values(...) | 最后 |

| LIMIT 10 | .head(10) | 最后 |

| DISTINCT | df.drop_duplicates() | 任意时机 |

面试官追问

  • Q:WHERE 和 HAVING 的本质区别?`groupby.filter()` 与 HAVING 一样吗?

A:WHERE 过滤原始行,HAVING 过滤聚合后的组。groupby.filter 返回原表明细(哪些行属于被保留的组),而 SQL HAVING 返回聚合后的行——不一样!要后者就先 groupby.agg 再 g[mask]。

答题要点

  • 三段式:WHERE → GROUP BY → HAVING;
  • pandas 对应:df[mask] → groupby.agg → g[mask]。

12. 本章小结

12.1 十大高频场景速查

| 场景 | 关键 API |

|------|----------|

| 清洗一条龙 | drop_duplicates + fillna + to_datetime + groupby.transform |

| 异常值检测 | IQR(quantile)/ Z-score((x-mean)/std) |

| 部门汇总 | groupby.agg(命名聚合) |

| 多指标透视 | pivot_table(values=[...], aggfunc={...}) |

| 每类 Top3 | groupby.apply(nlargest) 或 groupby.rank() |

| Cohort 留存 | 首登月 + offset + pivot_table.div |

| 滑动统计 | rolling(window=7, min_periods=1).mean() |

| 连续 N 天 | gaps and islands:cumsum 打段号 |

| JOIN 对比 | pd.merge(how=inner/left) |

| WHERE/GROUP/HAVING | df[mask] → groupby.agg → g[mask] |

12.2 面试通用建议

1. 先复述题目,确认字段含义和边界条件;画思路再写代码,让考官知道你脑子里有框架;

2. 主动说 SQL 版本——这是区分"会 pandas"和"懂数据"的关键;

3. 性能扩展 + 结果校验:数据大时说"分块读 + Dask/Spark";写完用 print(df.head())、df.shape 自证。


下一篇预告:第 33 章 pandas 与 MySQL 实战:读写与 SQL 对比 —— 把 pandas 真正接到生产数据库上,掌握 read_sql / to_sql / 防注入 / chunksize。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 32 章 pandas 面试题精讲:实战场景篇》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。