做数据分析时,统计需求经常不是简单的求和或计数,而是"有多少个不重复的值"。比如一张订单表里,同一客户可能下了多笔订单,你想按地区和月份统计"活跃客户数",这本质上就是一个基于多列的数据透视加唯一值计数问题。pandas里实现这个需求的核心组合是pivot_table配合nunique,但实际用起来有不少细节需要留意,下面结合具体例子把整套思路讲清楚。

一、pivot_table配合nunique的基础用法
先构造一份模拟订单数据,包含地区、月份、客户ID和订单金额四个字段。目标是按地区和月份两个维度,统计每个分组下不重复客户的数量。核心代码如下:
import pandas as pd
df = pd.DataFrame({
'region': ['华东', '华东', '华北', '华北', '华东', '华北'],
'month': ['1月', '1月', '1月', '1月', '2月', '2月'],
'customer': ['A', 'B', 'A', 'C', 'A', 'C'],
'amount': [100, 200, 150, 300, 120, 80]
})
result = pd.pivot_table(
df,
index='region',
columns='month',
values='customer',
aggfunc='nunique'
)
print(result)这里的关键点在aggfunc='nunique'。如果不指定聚合方式,pivot_table默认会对数值列求均值,对非数值列直接报错,所以很多人第一次用values指定了客户ID这种字符串列时会碰到DataError。把聚合函数换成nunique后,pandas会对每个分组的customer列做去重再计数,得到的正是每个地区每个月的活跃客户数。
还有一个容易忽略的参数是dropna。pivot_table的dropna参数控制的是"是否丢弃整行全为NaN的分组结果",而nunique在计数时默认不会把NaN算进去。如果你的数据里customer列存在缺失值,想要明确排除或保留,建议在透视前先用df['customer'].isna()检查一下,必要时用fillna或者dropna做预处理,避免统计口径和预期不一致。
二、多列维度与多层索引的处理
实际业务中维度往往不止两个。比如按地区、渠道、月份三个维度透视,可以把index和columns写成列表形式,结果会自动生成多层索引:
result = pd.pivot_table(
df,
index=['region', 'channel'],
columns='month',
values='customer',
aggfunc='nunique',
margins=True, # 追加汇总行/列
margins_name='总计'
)
print(result)
# 把多层索引拉平,方便导出到Excel
flat = result.reset_index()
flat.columns = ['_'.join(col).strip('_') if isinstance(col, tuple) else col
for col in flat.columns]
print(flat)当index传入列表时,结果是多层行索引;如果columns也传列表,列方向也会变成多层列索引。这种结构在分析时很有用,但导出或与其他表合并时经常需要拉平。上面的reset_index加列名拼接是比较通用的处理方式。
margins=True可以自动追加行列汇总,但要注意汇总单元格的口径:margin列的唯一值计数是跨整个行方向去重的,而不是把各行数字简单相加。比如华东1月有客户A、B共2人,2月有A共1人,总计列算出来是2而不是3,因为A在两个月都出现了。这个行为在业务上是正确的,但如果拿透视结果直接做加减校验,很容易误以为算错了。
三、groupby加nunique:更灵活的替代方案
如果只需要长表形式的结果,不一定要做成宽表透视,用groupby往往更直接:
result = (df
.groupby(['region', 'month'])['customer']
.nunique()
.reset_index(name='active_users'))
print(result)groupby写法的好处是链式操作流畅,后续做筛选、排序、可视化都很方便,而且性能通常比pivot_table略好一些,因为省去了从长表转宽表的重排过程。两者的适用场景可以这样区分:需要直接看交叉表或导出报表时用pivot_table,需要把统计结果继续喂给下游分析流程时用groupby。
如果想同时统计多个指标,比如既要唯一客户数又要订单总金额,可以给agg传一个字典或命名聚合:
result = df.groupby(['region', 'month']).agg(
active_users=('customer', 'nunique'),
total_amount=('amount', 'sum')
).reset_index()
print(result)四、常见坑与性能优化
第一个常见的坑是统计结果偏大。原因多半是数据里存在重复记录或者维度字段本身有空格、大小写不一致,比如"客户a"和"客户A"被当成两个人。建议透视前先做一次df['customer'] = df['customer'].str.strip()这类清洗,必要时统一大小写。
第二个坑是大数据量下的性能。nunique需要为每个分组维护一个哈希集合,当分组数达到百万级、基数又很高时,内存和耗时都会明显上升。这种情况下可以先做显式去重再计数,把去重和透视拆成两步:
dedup = df[['region', 'month', 'customer']].drop_duplicates()
result = pd.pivot_table(
dedup,
index='region',
columns='month',
values='customer',
aggfunc='count' # 去重后count等价于nunique,且更快
)
print(result)先drop_duplicates再count的方式,计算量集中在一次去重上,后续的count只是简单计数,整体速度通常比直接nunique快不少。另外如果数据规模特别大,也可以考虑先对customer做类别编码(astype('category')),减少哈希计算时的字符串比较开销。
总结一下,基于多列做唯一值计数,pivot_table加nunique是最直接的方案,groupby加nunique更灵活适合长表输出,大数据量时用先去重再计数的思路优化性能。掌握这几种写法后,面对各种活跃用户、去重商品数之类的统计需求基本都能从容应对。
Python数据透视唯一值计数pandas pivot_table修改时间:2026-09-03 17:34:58