标题:Pandas 数据对比分析:按区域层级统计客户变动并汇总明细名单

发布时间 - 2026-01-13 00:00:00    点击率:

本文详解如何使用 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  # js  # json  # app  # pandas  # count  # sort  # 字符串  # Lambda  # 转出  # 四类  # 若需  # 走了  # 设为  # 转到  # 可在  # 报错  # 可通过  # 谁来 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 谷歌Google入口永久地址_Google搜索引擎官网首页永久入口  Win11怎么查看显卡温度 Win11任务管理器查看GPU温度【技巧】  如何有效防御Web建站篡改攻击?  Laravel如何配置和使用缓存?(Redis代码示例)  如何在HTML表单中获取用户输入并用JavaScript动态控制复利计算循环  如何在服务器上配置二级域名建站?  Laravel如何优雅地处理服务层_在Laravel中使用Service层和Repository层  HTML 中如何正确使用模板变量为元素的 name 属性赋值  如何自定义safari浏览器工具栏?个性化设置safari浏览器界面教程【技巧】  如何用PHP快速搭建CMS系统?  Laravel如何实现全文搜索_Laravel Scout集成Algolia或Meilisearch教程  如何快速生成高效建站系统源代码?  如何注册花生壳免费域名并搭建个人网站?  Laravel辅助函数有哪些_Laravel Helpers常用助手函数大全  轻松掌握MySQL函数中的last_insert_id()  如何在沈阳梯子盘古建站优化SEO排名与功能模块?  香港服务器建站指南:免备案优势与SEO优化技巧全解析  Python自动化办公教程_ExcelWordPDF批量处理案例  Java解压缩zip - 解压缩多个文件或文件夹实例  Laravel如何记录自定义日志?(Log频道配置)  JavaScript实现Fly Bird小游戏  Win11搜索栏无法输入_解决Win11开始菜单搜索没反应问题【技巧】  详解一款开源免费的.NET文档操作组件DocX(.NET组件介绍之一)  安克发布新款氮化镓充电宝:体积缩小 30%,支持 200W 输出  Android仿QQ列表左滑删除操作  网站页面设计需要考虑到这些问题  如何为不同团队 ID 动态生成多个“认领值班”按钮  谷歌浏览器下载文件时中断怎么办 Google Chrome下载管理修复  高性能网站服务器部署指南:稳定运行与安全配置优化方案  Swift中循环语句中的转移语句 break 和 continue  如何快速使用云服务器搭建个人网站?  JavaScript 输出显示内容(document.write、alert、innerHTML、console.log)  历史网站制作软件,华为如何找回被删除的网站?  高端网站建设与定制开发一站式解决方案 中企动力  Win11怎么更改系统语言为中文_Windows11安装语言包并设为显示语言  深圳网站制作的公司有哪些,dido官方网站?  悟空识字怎么关闭自动续费_悟空识字取消会员自动扣费步骤  Laravel如何使用Service Container和依赖注入?(代码示例)  Python文件异常处理策略_健壮性说明【指导】  利用 Google AI 进行 YouTube 视频 SEO 描述优化  JavaScript如何实现路由_前端路由原理是什么  Laravel API路由如何设计_Laravel构建RESTful API的路由最佳实践  html如何与html链接_实现多个HTML页面互相链接【互相】  Laravel如何配置Horizon来管理队列?(安装和使用)  动图在线制作网站有哪些,滑动动图图集怎么做?  python中快速进行多个字符替换的方法小结  常州企业网站制作公司,全国继续教育网怎么登录?  Win11应用商店下载慢怎么办 Win11更改DNS提速下载【修复】  Laravel怎么实现模型属性转换Casting_Laravel自动将JSON字段转为数组【技巧】  Laravel如何实现模型的全局作用域?(Global Scope示例)