在数据分析工作中,我们经常会遇到这样的原始表:同一机构编号org_id在多条记录里反复出现,而每一行都带着部门名称、负责人、标签等字段。为了让报表更紧凑,需要把相同org_id的记录压成一行,并且把多列里的取值去重后拼成字符串。Pandas提供了groupby机制,但默认聚合不会自动去重,如果直接求和或字符串相加,会出现大量重复文本。理解分组对象的内部结构以及agg函数的传参方式,是写好这段代码的核心。

基础实现:用groupby与agg完成单列去重拼接
最直观的思路是先按org_id分组,然后对目标列应用一个能把多个值变成去重字符串的函数。Python内置的set可以轻松去重,但集合是无序的,若希望结果稳定,可以排序后再拼接。在agg中传入匿名函数或命名函数都可以,Pandas会把每个分组里该列形成的Series传进去。
下面构造一份示例数据,包含org_id、dept和owner两列,其中同一机构存在重复部门与重复负责人。我们用自定义函数先转集合再去重拼接,避免传统写法里出现的abc,abc这类冗余。
import pandas as pd
data = {
'org_id': [1, 1, 1, 2, 2, 3],
'dept': ['研发', '研发', '市场', '财务', '财务', '人事'],
'owner': ['张三', '张三', '李四', '王五', '赵六', '钱七']
}
df = pd.DataFrame(data)
def deduplicate_join(series):
# 转成集合去重,再排序保证结果稳定,最后用逗号拼接
unique_items = sorted(set(series.dropna().astype(str)))
return ','.join(unique_items)
result = df.groupby('org_id').agg({
'dept': deduplicate_join,
'owner': deduplicate_join
}).reset_index()
print(result)
上面的代码会输出三行,org_id为1的机构部门被合并为“研发,市场”,负责人变为“张三,李四”。这种写法逻辑清晰,且能复用在任意文本列上。需要注意的是,如果原始字段含缺失值,dropna可以避免把字符串nan拼进去,否则结果里会出现尴尬的nan字样。
相比先groupby再用加号拼接,自定义去重函数虽然多写几行,但数据质量更好。尤其在标签类字段里,用户往往只关心出现过哪些类别,而不关心出现几次,这时候去重拼接就是刚需而非可选项。
多列差异化聚合:同一分组内混合统计与拼接
真实业务里,往往不是所有列都要去重拼接。比如金额列需要求和,次数需要计数,而名称类字段才需要去重拼接。groupby的agg允许对不同的列传不同的聚合逻辑,我们可以把numpy的sum、size与自己的去重函数混用,一次算出所有指标。
以下示例在前面数据基础上增加金额字段amount,并对org_id分组后同时计算总金额、部门去重拼接和负责人去重拼接。通过字典形式声明每列的处理函数,代码可读性很高,也方便后续维护时增减字段。
import pandas as pd
data = {
'org_id': [1, 1, 1, 2, 2, 3],
'dept': ['研发', '研发', '市场', '财务', '财务', '人事'],
'owner': ['张三', '张三', '李四', '王五', '赵六', '钱七'],
'amount': [100, 200, 150, 300, 50, 80]
}
df = pd.DataFrame(data)
def deduplicate_join(series):
unique_items = sorted(set(series.dropna().astype(str)))
return ','.join(unique_items)
result = df.groupby('org_id').agg(
total_amount=('amount', 'sum'),
dept_list=('dept', deduplicate_join),
owner_list=('owner', deduplicate_join),
record_count=('org_id', 'size')
).reset_index()
print(result)
这种命名聚合(named aggregation)从Pandas 0.25开始支持,它让输出列名直接在agg里定义,不必再写繁琐的rename。对于宽表清洗来说,能在一步之内把数值统计和文本去重拼接全部完成,显著减少中间变量。
如果某些列本身就可能包含逗号,那么用逗号拼接会产生歧义。此时可以改用其他分隔符,比如竖线|,或者干脆返回Python列表对象而不是字符串。返回列表时,后续导出Excel虽不如字符串直观,但在程序内部继续处理反而更方便,避免了再次拆分字符串的开销。
性能与替代方案:apply与agg的取舍
当数据量达到百万行级别,频繁调用Python层自定义函数会让groupby变慢,因为Pandas无法把set操作向量化。一种替代思路是用apply对整个分组做处理,在分组内部用更底层的方式拼装,但apply本质也是逐组Python循环,未必更快。真正提升速度的办法,是尽量先用drop_duplicates缩小数据,再分组拼接。
下面演示先对org_id和需要拼接的列做去重,再轻量聚合,这样进入自定义函数的行数大幅减少,在重复度高的日志型数据上效果明显。
import pandas as pd
data = {
'org_id': [1, 1, 1, 2, 2, 3],
'dept': ['研发', '研发', '市场', '财务', '财务', '人事'],
'owner': ['张三', '张三', '李四', '王五', '赵六', '钱七']
}
df = pd.DataFrame(data)
# 先按分组键和拼接列去重,降低后续聚合体积
dedup = df.drop_duplicates(subset=['org_id', 'dept', 'owner'])
def join_col(series):
return ','.join(sorted(series.astype(str)))
result = dedup.groupby('org_id').agg(
dept_list=('dept', join_col),
owner_list=('owner', join_col)
).reset_index()
print(result)
从工程角度看,如果去重拼接只是报表展示前的最后一步,数据规模又在万行内,直接用前面讲的agg加自定义函数就足够。若嵌入到每日跑批的ETL里且数据膨胀快,就要评估是否该在数据库侧用GROUP_CONCAT配合DISTINCT先处理,把重活交给更擅长的组件。
另外,Pandas的explode方法可以和去重拼接反向配合:当别人给你一份已拼接好的文本列时,你能用split加explode还原成多行,再做其他关联。掌握这对互逆操作,面对机构维度数据进出宽表场景就能游刃有余,不至于被格式转换卡住进度。