
本文详解如何使用 pandas 对两个时间点的客户数据进行对比,按 zone/region/district 三级分组,精准计算客户流入、流出、新增、流失数量,并完整列出对应客户姓名清单。
在客户运营或区域管理场景中,常需对比不同时期的客户分布变化,尤其当客户归属存在跨区迁移(如从 A2b 迁至 A2a)、新增入驻或完全退出时,仅统计数量远远不够——业务人员更需要知道“谁来了”“谁走了”“谁转到了哪里”。本文提供一套完整、可复用的 pandas 实现方案,基于 df1(期初快照)和 df2(期末快照),输出包含 13 列的结构化变动报告,其中关键难点在于 将客户姓名聚合为列表并按三级地理维度对齐。
核心思路:分四类客户分别处理,再统一合并
我们把客户变动划分为四类逻辑明确的群体:
- Transfer In(转入):同一客户在 df2 中出现在新区域(与 df1 的 Zone/Region/District 不同);
- Transfer Out(转出):同一客户在 df1 中的原属区域(即其“离开地”);
- Leaver(流失客户):存在于 df1 但完全不在 df2 中的客户;
- New Customer(新增客户):存在于 df2 但完全不在 df1 中的客户。
⚠️ 注意:merge(on='cust_name') 是关键前提——它要求 cust_name 具有唯一性且能稳定标识同一客户。若实际数据中存在重名风险,建议改用 cust_id 作为主键(修改 on='cust_id' 并同步调整列引用)。
完整实现代码(含注释与健壮性增强)
import pandas as pd
import numpy as np
# 构建示例数据(与问题一致)
df1 = pd.DataFrame({
'cust_name': ['cxa', 'cxb', 'cxc', 'cxd', 'cxe', 'cxf'],
'cust_id': ['c1001', 'c1002', 'c1003', 'c1004', 'c1006', 'c1007'],
'town_id': ['t001', 't002', 't001', 't003', 't002', 't002'],
'Zone': ['A', 'A', 'A', 'B', 'A', 'A'],
'Region': ['A1', 'A2', 'A1', 'B1', 'A2', 'A2'],
'District': ['A1a', 'A2a', 'A1a', 'B1a', 'A2b', 'A2b']
})
df2 = pd.DataFrame({
'cust_name': ['cxb', 'cxc', 'cxd', 'cxe', 'cxf'],
'cust_id': ['c1002', 'c1003', 'c1004', 'c1006', 'c1007'],
'town_id': ['t002', 't001', 't003', 't002', 't002'],
'Zone': ['A', 'A', 'A', 'A', 'C'],
'Region': ['A2', 'A1', 'A1', 'A2', 'C1'],
'District': ['A2a', 'A1a', 'A1a', 'A2a', 'C1a']
})
# 步骤1:识别迁移客户(同一客户,区域变化)
merged = df1.merge(df2, on='cust_name', suffixes=('_df1', '_df2'), how='inner')
# 判断是否发生跨区变动(任一地理层级不同即视为迁移)
merged['is_moved'] = (
(merged['Zone_df1'] != merged['Zone_df2']) |
(merged['Region_df1'] != merged['Region_df2']) |
(merged['District_df1'] != merged['District_df2'])
)
moved = merged[merged['is_moved']].copy()
# 步骤2:提取转入 & 转出名单(按目标/源区域分组)
transfer_in = moved[['Zone_df2', 'Region_df2', 'District_df2', 'cust_name']].rename(
columns={'Zone_df2': 'Zone', 'Region_df2': 'Region', 'District_df2': 'District', 'cust_name': 'NamesTransferIn'}
)
transfer_out = moved[['Zone_df1', 'Region_df1', 'District_df1', 'cust_name']].rename(
columns={'Zone_df1': 'Zone', 'Region_df1': 'Region', 'District_df1': 'District', 'cust_name': 'NamTransferOut'}
)
# 步骤3:按三级分组聚合姓名列表(自动去重,保留顺序)
def collect_names(series):
return list(series) # 若需去重:list(series.unique())
in_agg = transfer_in.groupby(['Zone', 'Region', 'District'])['NamesTransferIn'].apply(collect_names).reset_index()
out_agg = transfer_out.groupby(['Zone', 'Region', 'District'])['NamTransferOut'].apply(collect_names).reset_index()
# 步骤4:合并转入/转出(outer join 确保所有变动区域不遗漏)
result = pd.merge(in_agg, out_agg, on=['Zone', 'Region', 'District'], how='outer').fillna('')
# 步骤5:添加流失客户(df1有、df2无)
leavers = df1[~df1['cust_name'].isin(df2['cust_name'])][['cust_name', 'Zone', 'Region', 'District']]
leavers_agg = leavers.groupby(['Zone', 'Region', 'District'])['cust_name'].apply(collect_names).reset_index().rename(columns={'cust_name': 'NamLeaver'})
result = pd.merge(result, leavers_agg, on=['Zone', 'Region', 'District'], how='outer').fillna('')
# 步骤6:添加新增客户(df2有、df1无)
new_customers = df2[~df2['cust_name'].isin(df1['cust_name'])][['cust_name', 'Zone', 'Region', 'District']]
new_agg = new_customers.groupby(['Zone', 'Region', 'District'])['cust_name'].apply(collect_names).reset_index().rename(columns={'cust_name': 'NamNewCustomer'})
result = pd.merge(result, new_agg, on=['Zone', 'Region', 'District'], how='outer').fillna('')
# ✅ 最终结果:已包含全部13列中的姓名字段(其余数值列可基于此表用 groupby.size() 补全)
print(result[
['Zone', 'Region', 'District',
'NamesTransferIn', 'NamTransferOut', 'NamLeaver', 'NamNewCustomer']
])关键注意事项与优化建议
- 空值处理:使用 .fillna('') 将 NaN 替换为空字符串,避免后续 JSON 序列化或 Excel 导出报错;若需保留 None,可改用 fillna(pd.NA)。
- 姓名去重:若同一客户因数据质量问题重复出现,可在 collect_names() 中加入 series.unique()。
- 扩展数值列:本教程聚焦姓名字段,但 Initial Count / Final Count 等可通过 df1.groupby(['Zone','Region','District']).size() 和 df2.groupby(...).size() 快速生成,再 merge 进 result。
- 性能提示:对百万级数据,避免 apply(lambda x: ...),优先使用向量化操作;merge 前确保 cust_name 列已设为索引或启用 sort=False。
- 输出增强:可调用 result.to_excel("customer_movement_report.xlsx", index=False) 直接导出带格式报表。
通过该方案,你不仅获得结构清晰的变动摘要,更掌握了以客户实体为中心、支持业务溯源的精细化分析能力——让每一条数据变动都“有据可查、有人可溯”。









