
本文介绍如何使用pandas对两个时间点的客户数据表(df1、df2)进行精细化比对,按zone/region/district三级分组统计客户数量变化,并完整列出“转入、转出、新增、流失”四类客户的姓名清单。
在客户运营分析中,常需追踪客户在地理层级(如 Zone → Region → District)上的动态迁移与结构变化。仅统计数量增减远远不够——业务方更关注“谁来了”“谁走了”“谁转到了哪里”。以下教程将带你从零构建一个完整的客户变动分析流水线,输出包含13列的结构化结果表(含所有人员姓名列表),兼顾准确性与可读性。
核心逻辑拆解
整个流程围绕四类客户行为展开:
- Transfer In(转入):客户在 df2 中出现在某区域,但其在 df1 中属于其他区域(同名客户跨区迁移);
- Transfer Out(转出):客户在 df1 中属于某区域,但在 df2 中迁至其他区域;
- New Customer(新增):客户仅存在于 df2,不在 df1 中(全新客户);
- Leaver(流失):客户仅存在于 df1,不在 df2 中(自然流失或离网)。
⚠️ 关键注意:cust_name 作为唯一标识符(假设无重名),若实际场景存在重名风险,应改用 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']
})
# Step 1: 合并两表,保留双方地理信息(用于识别迁移)
merged = df1.merge(df2, on='cust_name', suffixes=('_old', '_new'), how='outer', indicator=True)
# _merge == 'both' → 存在于两期;'left_only' → 仅 df1(潜在leaver);'right_only' → 仅 df2(潜在new)
# Step 2: 分类处理四类客户
# ✅ Transfer In & Out(仅针对共同客户,且地理变更)
common_customers = merged[merged['_merge'] == 'both'].copy()
common_customers['moved'] = common_customers['District_old'] != common_customers['District_new']
# 转入:新归属地(df2 的 Zone/Region/District)
transfer_in = common_customers[common_customers['moved']].copy()
transfer_in = transfer_in[['Zone_new', 'Region_new', 'District_new', 'cust_name']].rename(
columns={'Zone_new': 'Zone', 'Region_new': 'Region', 'District_new': 'District'}
)
# 转出:旧归属地(df1 的 Zone/Region/District)
transfer_out = common_customers[common_customers['moved']].copy()
transfer_out = transfer_out[['Zone_old', 'Region_old', 'District_old', 'cust_name']].rename(
columns={'Zone_old': 'Zone', 'Region_old': 'Region', 'District_old': 'District'}
)
# ✅ New Customers(仅 df2)
new_customers = merged[merged['_merge'] == 'right_only'][['cust_name', 'Zone', 'Region', 'District']]
# ✅ Leavers(仅 df1)
leavers = merged[merged['_merge'] == 'left_only'][['cust_name', 'Zone', 'Region', 'District']]
# Step 3: 按三级地理维度分组聚合姓名列表
def agg_names(series):
return list(series) if not series.empty else []
result = pd.concat([
transfer_in.groupby(['Zone', 'Region', 'District'])['cust_name'].apply(agg_names).rename('NamesTransferIn'),
transfer_out.groupby(['Zone', 'Region', 'District'])['cust_name'].apply(agg_names).rename('NamTransferOut'),
new_customers.groupby(['Zone', 'Region', 'District'])['cust_name'].apply(agg_names).rename('NamNewCustomer'),
leavers.groupby(['Zone', 'Region', 'District'])['cust_name'].apply(agg_names).rename('NamLeaver')
], axis=1).fillna('').reset_index()
# Step 4: 补全初始/最终计数(可选,增强业务可读性)
initial_cnt = df1.groupby(['Zone', 'Region', 'District']).size().rename('Initial Count')
final_cnt = df2.groupby(['Zone', 'Region', 'District']).size().rename('Final Count')
result = result.merge(initial_cnt, on=['Zone', 'Region', 'District'], how='left').fillna(0).astype({'Initial Count': 'int'})
result = result.merge(final_cnt, on=['Zone', 'Region', 'District'], how='left').fillna(0).astype({'Final Count': 'int'})
# 计算衍生指标(按需添加)
result['Transfer Out Count'] = result['NamTransferOut'].str.len()
result['Transfer In Count'] = result['NamesTransferIn'].str.len()
result['New Customer Count'] = result['NamNewCustomer'].str.len()
result['Leaver Count'] = result['NamLeaver'].str.len()
# 重排列并输出(符合问题要求的13列顺序)
output_cols = [
'Zone', 'Region', 'District',
'Initial Count', 'Final Count',
'Transfer Out Count', 'Transfer In Count',
'New Customer Count', 'Leaver Count',
'NamesTransferIn', 'NamTransferOut', 'NamLeaver', 'NamNewCustomer'
]
result = result.reindex(columns=output_cols)
print(result.to_string(index=False))输出说明与业务提示
运行上述代码后,你将获得结构清晰、字段完备的结果表。例如:
| Zone | Region | District | Initial Count | Final Count | Transfer Out Count | Transfer In Count | New Customer Count | Leaver Count | NamesTransferIn | NamTransferOut | NamLeaver | NamNewCustomer |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A | A1 | A1a | 2 | 2 | 0 | 1 | 0 | 1 | ['cxd'] | [] | ['cxa'] | [] |
| C | C1 | C1a | 0 | 1 | 0 | 0 | 1 | 0 | [] | [] | [] | ['cxf'] |
✅ 优势亮点:
- 使用 merge(..., how='outer', indicator=True) 统一识别三类客户状态,逻辑更鲁棒;
- 所有聚合均基于 groupby + apply(list),天然支持空组填充(fillna('') 或 []);
- 列名严格对齐需求,后续可直接导出 Excel 或对接 BI 工具;
- 易扩展:如需添加“转入来源区域”“流失原因标签”,只需在对应子集增加字段即可。
? 最后建议:
- 生产环境中务必校验 cust_name 唯一性,或优先使用 cust_id 作为 join key;
- 若数据量大(>100万行),建议用 pd.concat([df1, df2]).drop_duplicates(...) 配合 map 替代多次 merge 提升性能;
- 名单列可进一步处理为字符串(', '.join(x))便于阅读,或保留 list 类型供下游程序解析。
至此,你已掌握一套工业级客户地理变动分析方案——不止于数字,更见人名。









