概述

本节汇总 groupby 中的特殊情况:时间重采样分组、分类数据 observed 参数、缺失值 dropna 参数,以及分组后产生多索引结果的处理方式。


9.1 时间重采样

通过 pd.Grouper(freq=...) 可以在 groupby 中直接按时间频率分组,避免先 set_index 再 resample。

import pandas as pd
import numpy as np
 
# 示例:每日交易数据
df = pd.DataFrame({
    "日期": pd.date_range("2024-01-01", periods=30, freq="D"),
    "门店": ["A", "B"] * 15,
    "销售额": np.random.randint(500, 2000, 30),
})
 
# 按周分组求和(freq="W" 默认按周日结束)
df.groupby(pd.Grouper(key="日期", freq="W"))["销售额"].sum()
 
# 按月分组
df.groupby(pd.Grouper(key="日期", freq="M"))["销售额"].sum()
 
# 按季度分组
df.groupby(pd.Grouper(key="日期", freq="Q"))["销售额"].sum()
 
# 门店 + 月度双分组
df.groupby(["门店", pd.Grouper(key="日期", freq="M")])["销售额"].agg(["sum", "mean"])
 
# 使用 level 时间索引分组
df_ts = df.set_index("日期")
df_ts.groupby([pd.Grouper(freq="M"), "门店"])["销售额"].sum()

常用频率字符串: D(天)、W(周)、M(月)、Q(季度)、Y(年)、H(小时)、T/min(分钟)。


9.2 分类分组 observed

当分组键为 Categorical 类型时,observed 决定是否包含未出现的类别:

observed行为
False(默认)结果包含全部预定义类别(即使某类没有数据),可用于保持类别完整性
True仅包含实际观测到的类别,结果更精简
# 准备分类数据
cat_df = pd.DataFrame({
    "等级": pd.Categorical(
        ["高", "中", "高"],
        categories=["低", "中", "高"],   # "低" 从未出现
        ordered=True,
    ),
    "数量": [10, 20, 30],
})
 
# observed=False(默认):包含 "低",计数为 0
cat_df.groupby("等级", observed=False)["数量"].agg(["sum", "count"])
 
# observed=True:只显示实际类别
cat_df.groupby("等级", observed=True)["数量"].agg(["sum", "count"])

pandas 2.x 行为提示

早期版本 observed 默认 False;若希望分组结果更小更快,建议显式指定 observed=True。此外,groupby(..., observed=False) 时,dropna=False 可同时保留缺失类别与 NaN 组。


9.3 缺失值 dropna

groupby(dropna=True/False) 控制分组键为缺失值的组是否保留。

df_nan = pd.DataFrame({
    "部门": ["A", None, "B", None, "C"],
    "销售额": [100, 200, 300, 400, 500],
})
 
# dropna=True(默认):丢弃 NaN 组
df_nan.groupby("部门", dropna=True)["销售额"].sum()
 
# dropna=False:保留 NaN 组(组名显示为 NaN)
df_nan.groupby("部门", dropna=False)["销售额"].sum()

补充说明

  • 多列分组时,只要任一列分组键为 NaN,该行即视为缺失组;
  • dropna=False 在数据审计与缺失值分析中很有用;
  • 对分类数据,dropna=False 时缺失值类别会被单独列出。
# 多列分组 + dropna=False
df_nan2 = pd.DataFrame({
    "部门": ["A", None, "A"],
    "地区": ["北", "南", None],
    "销售额": [1, 2, 3],
})
df_nan2.groupby(["部门", "地区"], dropna=False)["销售额"].sum()

9.4 多索引结果

多列分组后,结果索引会变为 MultiIndex;可使用 reset_index()、.droplevel()、.swaplevel()、.sort_index() 等处理。

df = pd.DataFrame({
    "年份": [2023, 2023, 2024, 2024, 2024],
    "季度": ["Q1", "Q2", "Q1", "Q2", "Q3"],
    "销售额": [100, 150, 200, 250, 300],
})
 
# 多列分组产生 MultiIndex
result = df.groupby(["年份", "季度"])["销售额"].sum()
# 年份  季度
# 2023  Q1    100
#       Q2    150
# 2024  Q1    200
#       Q2    250
#       Q3    300
# Name: 销售额, dtype: int64
 
# 展平为普通 DataFrame
result.reset_index()
 
# 保留分组键为列(as_index=False)
df.groupby(["年份", "季度"], as_index=False)["销售额"].sum()
 
# 删除某一层级
result.droplevel("季度")
 
# 交换层级顺序
result.swaplevel("季度", "年份").sort_index()
 
# 多索引列(多列多重聚合)
df.groupby("年份")["销售额"].agg(["sum", "mean", "max"])
# 输出列也为 MultiIndex

常用处理模式

# 多索引结果 → 整洁 DataFrame
df.groupby(["年份", "季度"]).agg(
    总销售额=("销售额", "sum"),
    平均销售额=("销售额", "mean"),
).reset_index()

相关笔记