标题:Pandas 数据对比分析:按区域层级统计客户变动并汇总明细名单
技术百科
心靈之曲
发布时间:2026-01-13
浏览: 次 本文详解如何使用 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) 直接导出带格式报表。
通过该方案,你不仅获得结构清晰的变动摘要,更掌握了以客户实体为中心、支持业务溯源的精细化分析能力——让每一条数据变动都“有据可查、有人可溯”。
# excel
# 走了
# 可通过
# 可在
# 若需
# app
# 转出
# 转到
# 设为
# js
# json
# 字符串
# 报错
# count
# Lambda
# sort
# pandas
# 四类
# 谁来
相关栏目:
<?muma
$count = M('archives')->where(['typeid'=>$field['id']])->count();
?>
【
AI推广<?muma echo $count; ?>
】
<?muma
$count = M('archives')->where(['typeid'=>$field['id']])->count();
?>
【
SEO优化<?muma echo $count; ?>
】
<?muma
$count = M('archives')->where(['typeid'=>$field['id']])->count();
?>
【
技术百科<?muma echo $count; ?>
】
<?muma
$count = M('archives')->where(['typeid'=>$field['id']])->count();
?>
【
谷歌推广<?muma echo $count; ?>
】
<?muma
$count = M('archives')->where(['typeid'=>$field['id']])->count();
?>
【
百度推广<?muma echo $count; ?>
】
<?muma
$count = M('archives')->where(['typeid'=>$field['id']])->count();
?>
【
网络营销<?muma echo $count; ?>
】
<?muma
$count = M('archives')->where(['typeid'=>$field['id']])->count();
?>
【
案例网站<?muma echo $count; ?>
】
<?muma
$count = M('archives')->where(['typeid'=>$field['id']])->count();
?>
【
精选文章<?muma echo $count; ?>
】
相关推荐
- 如何使用 Python 合并文件夹内多个 Exce
- php高频调试功能有哪些_php常用调试函数与工具
- c++的位运算怎么用 与、或、异或、移位操作详解【
- Win11怎么更改盘符_Win11磁盘管理修改驱动
- Win11怎么开启剪贴板历史记录_Windows1
- 如何正确访问 Laravel 模型或对象的属性而非
- 如何在Golang中实现基础配置管理功能_Gola
- Drupal 中渲染节点时出现 HTML 标签嵌套
- Win11怎么设置默认PDF阅读器 Win11修改
- Win11用户账户控制怎么关_Win11关闭UAC
- 如何使用Golang实现错误包装与传递_Golan
- Windows10系统怎么查看设备管理器_Win1
- Python解释执行模型_字节码流程说明【指导】
- Win11怎么设置开机密码_Windows11账户
- 如何在 Pandas 中按元素交集合并两列字符串
- c# 在ASP.NET Core中管理和取消后台任
- 如何在Golang中实现文件下载_Golang文件
- 如何在 Go 中创建包含映射(map)的切片(sl
- Windows10系统怎么查看显卡驱动_Win10
- Win11怎么开启游戏工具栏_Windows11
- Win10如何更改电脑休眠时间_Windows10
- c++中的可变参数模板(variadic temp
- Win11怎么更改鼠标指针方案_Windows11
- 如何用正则与预处理结合精准拦截拼接式垃圾域名
- php下载安装包太大怎么下载_分卷压缩下载方法【教
- c# Task.Yield 的作用是什么 它和Ta
- PHP接收参数值为空怎么办_判断和处理空参数方法说
- php本地部署后数据库连接报错_1045acces
- Win11怎么查看局域网电脑_Windows 11
- Python抽象类与接口设计_规范说明【指导】
- Win11怎么设置虚拟内存_Windows 11优
- Windows10系统怎么查看系统版本_Win10
- Python项目回滚策略_发布安全说明【指导】
- Win11触摸板没反应怎么办_开启Win11笔记本
- Win11时间格式怎么改成12小时制 Win11时
- 如何使用Golang指针与接口结合_实现方法调用和
- 如何使用Golang实现聊天室消息存档_存储聊天记
- 如何将竖排文本文件转换为横排字符串
- c++ namespace命名空间用法_c++避免
- 如何在Golang中实现自定义Benchmark_
- Win11怎么清理C盘虚拟内存_Win11清理虚拟
- Win11怎么关闭专注助手 Win11关闭免打扰模
- Windows服务持续崩溃怎样修复_系统服务保护机
- Mac如何创建和管理多个桌面空间_Mac高效多任务
- 手机php怎么转mp4_手机端php文件转mp4a
- c++如何连接Redis c++ hiredis库
- php嵌入式多设备通信怎么实现_php同时管理多个
- Mac怎么查看活动监视器_理解Mac进程和资源占用
- 如何在Golang中解压文件_Golang com
- C#怎么使用委托和事件 C# delegate与e

'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']
])
QQ客服