做数据分析这行,pandas基本上是躲不开的。哪怕你用的是Spark、Flink这类分布式框架,底层思路和数据处理的习惯,很多还是从pandas这套来的。最近整理电脑,翻出来一个之前帮朋友做的电商订单分析项目,算是一个比较完整的“取数-清洗-分析-可视化”全流程案例。正好拿这个项目当引子,把pandas在真实业务里的那些操作串一遍,从数据加载、清洗、透视聚合到可视化输出,每一步的代码、思路和坑都会讲到。
这个案例适合谁看?两种人。一种是刚学完pandas基础语法、想在真实数据集上练手但不知道从哪下手的初学者;另一种是已经工作一阵子、平时用Excel做报表想转到Python、需要一套能直接照着改的分析模板的从业者。我会尽量少讲枯燥的理论,多放可以直接跑的代码和实际业务里的判断逻辑。毕竟数据分析这活儿,最花时间的从来不是写代码,而是想清楚每一步要干什么,以及数据的坑都在哪儿。
这个项目的原始数据模拟了一家中小型电商公司某年全年的订单流水,大概不到两万条记录。数据量不大,但脏数据该有的都有了:缺失、重复、格式错乱、异常值、时间字段类型不对。这种数据就是日常工作中最真实的样子——不是Kaggle上那种给你收拾得干干净净的比赛数据集。
1. 项目背景与分析目标设定
做任何数据分析项目,第一步都不是写代码,而是把业务问题和分析目标定清楚。这步如果省了,后面做的再花哨也是无米之炊。
1.1 业务场景梳理与分析目标拆解
朋友这家公司做的是家居日用品的线上销售,覆盖全国主要城市,订单来自自有小程序和几个第三方平台。他找到我的时候,手头就一个Excel导出的订单明细表,大概有19000多行、十几个字段。他想搞清楚几个问题:这个店一年到底卖了多少、利润大概什么水平;每个月的销售走势如何,有没有明显的淡旺季;哪些品类的商品贡献了主要收入;不同城市或者地区的消费能力差异大不大;以及最重要的——能不能从数据里看出哪些客户是真正值得运营的高价值用户。
这些问题看着不复杂,但落到数据上就变成了一个具体的技术任务清单:数据加载与初步探查、数据清洗与格式统一、维度透视与指标聚合、可视化输出。每个环节都有对应的pandas操作。这个思路在任何数据分析项目里都是通用的,换任何行业都一样:先有业务问题,才有技术方案。
1.2 数据字典与字段设计
收到的原始文件是个CSV,名字叫orders.csv。我习惯拿到数据后先不急着看内容,而是先写一个字段清单,对照业务含义检查。这份订单表的字段结构大致如下:
| 字段名 | 类型 | 业务含义 | 说明 |
|---|---|---|---|
| order_id | object | 订单编号 | 平台生成,理论上唯一 |
| user_id | object | 用户ID | 标识下单用户 |
| order_date | object | 下单日期 | 原始为字符串,需要转日期类型 |
| province | object | 收货省份 | 可能含空值 |
| city | object | 收货城市 | 用于地区分析 |
| category | object | 商品类目 | 大类,如厨房、收纳、清洁 |
| product_name | object | 商品名称 | 文本较长,一般不做聚合 |
| quantity | float | 购买数量 | 应为正整数 |
| unit_price | float | 单价(元) | 商品单价 |
| total_amount | float | 订单金额(元) | 数量乘以单价,但可能被手工修改过 |
| payment_method | object | 支付方式 | 微信/支付宝/银行卡 |
| order_status | object | 订单状态 | 已完成/已取消/退货等 |
这里有个典型的数据认知问题:你以为的total_amount是quantity * unit_price计算出来的,但实际数据里它并不严格遵守这个关系,因为可能有优惠、改价、运费分摊等原因。所以做清洗的时候,不能想当然用公式去校验或重构字段,要先看数据的真实分布再决定怎么处理。
1.3 环境准备与数据加载
个人习惯用Anaconda环境,直接装好pandas、matplotlib、jupyter这些。如果你是用原生Python,安装pandas也就一条命令的事情。国内网络环境如果直接pip安装慢,可以用靠谱的镜像源提升速度。
# 如果还没有pandas,先安装,注意指定镜像源加速 pip install pandas matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple实际项目里我还会顺手把numpy一起装好,虽然这个案例里用得不多,但做数值计算时还是经常需要它。装完之后,开始加载数据。
import pandas as pd import numpy as np import matplotlib.pyplot as plt # 让matplotlib正常显示中文,这步必须放在前面 plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False df = pd.read_csv('orders.csv', encoding='utf-8') print(df.shape) print(df.info())这里要注意:read_csv的encoding参数很关键。别人给你的CSV,可能是utf-8、gbk或者gb18030编码,直接读会报错或者出现乱码。我一般遇到中文乱码,会依次尝试utf-8、gbk、gb18030这三种编码。gb18030是最全的中文编码,兼容性最好,基本能兜底。
先看shape确认行列数,再用info()看字段类型和缺失情况。这一步能快速定位“类型有问题”“空值太多”之类的明显毛病。
2. 数据清洗实操:对脏数据动手的艺术
数据清洗是整个项目里最耗时间、也最体现经验的环节。网上很多教程把清洗讲得很简单——dropna()、fillna()、drop_duplicates()一把梭。但实际业务里,每一步操作都要先想清楚“为什么这么做”,否则很容易把有用的信息也一起删掉了。
2.1 缺失值处理:先判断是“真空”还是“假空”
用df.isnull().sum()扫了一眼,主要字段的缺失情况是这样:
missing_report = df.isnull().sum() missing_report = missing_report[missing_report > 0].sort_values(ascending=False) print(missing_report)输出显示city缺了143个,payment_method缺了89个,province缺了12个,其他字段基本没有缺失。很多人看到缺失第一反应就是删掉或者填个“未知”,其实要先理解业务含义。
province缺失只有12条,总量占比很低,而且和city同时缺失的记录也不多。我查了一下这些记录,发现大多是第三方平台接口推送的订单,天南地北什么城市都有,没有明显的聚集规律,所以可以直接把province和city都缺失的记录删掉,因为地区分析用不到它们。但city缺失而province有值的记录怎么办?这143条里,大多数有province,只是城市没传过来。对地区分析来说,省级维度够用了,所以我把city的空值统一填成'未知城市',保留省级维度。
payment_method缺失的89条,我查了它们的order_status,发现基本都是“已取消”的订单。对取消订单来说,支付方式本身没有分析意义,所以直接用'未知'填充,而不是删除记录。
这里要记住一个原则:先看清楚缺失值长什么样、和哪些字段有关联,再决定处理方式。批量替换前先分组统计。否则很容易把有价值的关联信息一起丢掉。
2.2 去重处理:看似重复的数据未必真的重复
用df.duplicated().sum()检查完全重复的行,发现只有7条。这种完全重复一般是平台数据同步时重复推送造成的,直接删掉没问题:
df = df.drop_duplicates()但真正的坑在于“看起来重复其实不重复”的情况。比如同一个order_id出现了两次,但其他字段不同。我先按order_id检查了一下重复情况:
dup_order = df[df.duplicated('order_id', keep=False)].sort_values('order_id') print(dup_order.shape)发现有160多条记录存在order_id重复。仔细看,发现一个订单号下有多件不同商品是很常见的事情,比如一个订单同时买了“收纳箱”和“清洁剂”,订单号重复但product_name不同。这种就是正常的母子订单结构,不能删。
真正需要处理的是“order_id相同、product_name也相同、金额数量也相同”这种,大概率是重复同步了。我加了一层条件去重:
df = df.drop_duplicates(subset=['order_id', 'product_name', 'quantity', 'unit_price'], keep='first')去重和缺失处理一样,核心是先搞清楚数据生成的逻辑。order_id是天然的业务主键,但它不是数据表里的唯一键,因为一个订单可以包含多行商品明细。认清这个结构,才不会误删正常数据。
2.3 数据类型转换:字符串到数字和日期的必修课
用df.dtypes看了一圈,quantity和unit_price显示是float64,这倒没问题。但order_date是object类型,这个必须转成datetime64,否则后面按月份、星期聚合都没法做。
# 转日期类型,errors='coerce' 让无法解析的值变成NaT df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce') # 检查有多少转换失败的 bad_date_count = df['order_date'].isna().sum() print(f'日期解析失败: {bad_date_count} 条')这里用errors='coerce'而不是默认的raise,是因为真实数据里总会有几条日期格式不规范的,比如“2023/1/5”和“2023-01-05”混着来,甚至还有“2023.1.5”。pd.to_datetime对常见的分隔符有不错的兼容性,但遇到纯数字的20230105这种也是能解析的,errors='coerce'就是让那些真正解析不了的变成NaT,而不是让脚本直接崩掉。
日期转完之后,顺手把quantity和unit_price的异常值处理掉。检查发现有些记录的quantity是0甚至负数,这显然不正常。业务上,数量为0或者负数可能是退款冲正之类的记录。但因为这是订单表,没有单独的退款表,我决定把这些记录过滤掉。金额方面,total_amount缺失的记录直接用quantity * unit_price重算填上,因为商品零售场景下没有复杂的优惠分摊,这个公式基本成立。
# 清洗数量字段 df = df[(df['quantity'] > 0) & (df['quantity'] < 1000)] # 重算确实的总金额 df['total_amount'] = df['total_amount'].fillna(df['quantity'] * df['unit_price'])pandas类型转换时还有个容易被忽略的细节:如果一列里有混合类型(比如一部分是字符串数字,一部分是字符串文本),用astype(float)会直接报错。这时候用pd.to_numeric()加上errors='coerce'更稳妥。
2.4 派生字段:把时间维度拆开才有得玩
清洗干净之后做特征工程——说是特征工程,其实就是从原始字段里提取更多可以分析的维度。对于时间字段,最常用的操作就是拆出“年、月、日、星期、季度、是否工作日”。
df['year'] = df['order_date'].dt.year df['month'] = df['order_date'].dt.month df['day_of_week'] = df['order_date'].dt.dayofweek # 0是周一,6是周日 df['is_weekend'] = df['day_of_week'].apply(lambda x: 1 if x >= 5 else 0)另外我把“客单价”和“商品单价”分开看。客单价是每笔订单的总金额,但同一个order_id有多行明细,所以要先按order_id分组求金额总和才是客单价。商品单价是行级别的。
还有一个高频操作用得很多——分箱。把订单金额划分成不同档位,方便看客单价分布。
bins = [0, 50, 100, 200, 500, 10000] labels = ['0-50', '50-100', '100-200', '200-500', '500+'] df['amount_bin'] = pd.cut(df['total_amount'], bins=bins, labels=labels)到这一步,数据基础就准备好了。这个过程中,我反复提醒自己的是:清洗不是技术活,是业务判断题。每一步操作,都要能说清楚为什么。说不清楚为什么的操作,就别做。
3. 核心分析:从维度聚合到价值洞察
数据准备好了,就可以开始真正回答业务问题了。分析思路不是一个个孤立地跑代码,而是沿着“销售大盘 → 时间趋势 → 品类结构 → 地区分布 → 用户价值”这条线一层层往下钻。
3.1 整体销售指标计算:groupby与agg组合拳
先看整个店的情况。有几个指标要算清楚:总订单数、总销售额、总销量、客单价、订单平均金额。
# 总销售额 total_sales = df['total_amount'].sum() # 总订单数(按order_id去重) unique_orders = df['order_id'].nunique() # 总销量 total_quantity = df['quantity'].sum() # 客单价 = 总销售额 / 总订单数 avg_order_value = total_sales / unique_orders print(f'总销售额: {total_sales:,.0f} 元') print(f'总订单数: {unique_orders} 单') print(f'总销量: {total_quantity:,.0f} 件') print(f'客单价: {avg_order_value:.2f} 元')这里要提醒一下:计算客单价时分子是“所有订单行的金额总和”,分母是“去重后的订单数”。如果直接用df['total_amount'].mean(),那是“每行流水平均金额”,不是客单价。很多新人会在这个地方栽跟头,因为这两者有时候看着像,但业务含义完全不同。
3.2 时间维度分析:月度趋势和星期规律
月度趋势是销售分析里最常规的拆法。用groupby加上agg一步到位:
monthly_sales = df.groupby(['year', 'month']).agg( 销售额=('total_amount', 'sum'), 订单量=('order_id', 'nunique'), 销量=('quantity', 'sum') ).reset_index() monthly_sales['月份'] = monthly_sales['year'].astype(str) + '-' + monthly_sales['month'].astype(str).str.zfill(2) print(monthly_sales)groupby之后一定要接reset_index(),不然索引还是MultiIndex,后续画图和数据筛选都费劲。agg里的语法是新列名=(原始列名, 聚合方式),这种方式比groupby().sum()更灵活,因为你可以在同一次分组里对不同字段用不同聚合方式。
从结果看,这家店10月和11月销售额明显冲高,这是“双11”大促的效应;2月份因为春节物流停运,是全年最低谷。这种结论看着简单,但它意味着后续库存备货和营销投放都要跟着这个节奏走。
星期维度也很有意思:
weekday_sales = df.groupby('day_of_week').agg( 销售额=('total_amount', 'sum'), 订单量=('order_id', 'nunique') ).reset_index() weekday_sales['星期'] = weekday_sales['day_of_week'].map({0:'周一',1:'周二',2:'周三',3:'周四',4:'周五',5:'周六',6:'周日'}) print(weekday_sales)数据分析里有个专业术语叫“数据洞察”,说白了就是看出别人看不出的规律。星期维度的统计显示,周末(周六、周日)的订单量明显高于工作日,这符合家居日用品的目标人群画像——上班族只有周末才有时间逛家居店。
3.3 品类与地区维度:贡献度与排名分析
品类结构是零售分析的核心。这里可以用pivot_table做一个“类目×月份”的交叉表,看每个类目每个月的销售情况:
category_month = pd.pivot_table( df, values='total_amount', index='category', columns='month', aggfunc='sum', fill_value=0 ) print(category_month)pivot_table是pandas里最像Excel数据透视表的功能。它的好处就是一行代码能构建出“行是类目、列是月份”的矩阵,非常方便观察哪个类目在什么月份发力。
从品类排名来看,“收纳整理”和“厨房用品”贡献了超过55%的销售额,是绝对的支柱品类。再看地区维度:
province_sales = df.groupby('province').agg( 销售额=('total_amount', 'sum'), 订单量=('order_id', 'nunique') ).reset_index().sort_values('销售额', ascending=False) # 计算各省销售额占比 province_sales['占比'] = province_sales['销售额'] / province_sales['销售额'].sum() print(province_sales.head(10))占比计算用/而不是apply,因为Series之间的除法本身就是按索引对齐的。这个数据展示地区分布更细,排名前五的省份贡献了七成销售额,其中广东、江苏、浙江三个沿海省份就占了四成多。这个信息对物流仓储布局很有价值。
3.4 用户维度:RFM模型的简化实现
用户价值分析是数据分析里最能体现水平的部分。完整的RFM模型分三个维度:最近一次消费时间(Recency)、消费频率(Frequency)、消费金额(Monetary)。在这个数据集里,我有完整的订单明细,可以做一套简化版RFM。
# 设定分析基准日期(数据集中最后一天) max_date = df['order_date'].max() analysis_date = max_date + pd.Timedelta(days=1) # 按用户聚合RFM指标 rfm = df.groupby('user_id').agg( 最近消费天数=('order_date', lambda x: (analysis_date - x.max()).days), 消费次数=('order_id', 'nunique'), 消费总金额=('total_amount', 'sum') ).reset_index() # 用rank或qcut把用户分群 rfm['R等级'] = pd.qcut(rfm['最近消费天数'], 3, labels=['高', '中', '低']) rfm['F等级'] = pd.qcut(rfm['消费次数'], 3, labels=['高', '中', '低']) rfm['M等级'] = pd.qcut(rfm['消费总金额'], 3, labels=['高', '中', '低']) print(rfm.head())这里用pd.qcut而不是pd.cut,是因为qcut是按分位数切分,保证每个区间样本量大致相等。在做用户分群时,用qcut更合理,因为消费数据往往呈长尾分布,直接按绝对值切会导致大部分用户被分到最低档。
看结果,高价值用户(高R高F高M)只有大约700人,却贡献了近28%的销售额。这就是典型二八法则在数据里的体现。这部分用户值得专门的运营维护,比如更多的新品试用、专属折扣、免邮权益。
4. 可视化输出:pandas自带绘图与matplotlib精修
数据分析做完了,结果还得画出来给人看。pandas的plot()方法底层封装了matplotlib,日常分析阶段出图足够快;到了做汇报材料的时候,再用matplotlib精细调整。
4.1 快速出图:一行代码看趋势
刚才算好的monthly_sales,我最常画的是双轴组合图——柱状图看销售额,折线图看订单量。
fig, ax1 = plt.subplots(figsize=(12, 6)) ax1.bar(monthly_sales['月份'], monthly_sales['销售额'], color='#5B9BD5', alpha=0.7, label='销售额') ax1.set_ylabel('销售额(元)') ax1.set_xlabel('月份') ax2 = ax1.twinx() ax2.plot(monthly_sales['月份'], monthly_sales['订单量'], color='#FF7F0E', marker='o', label='订单量') ax2.set_ylabel('订单量(单)') fig.suptitle('月度销售额与订单量趋势', fontsize=14) fig.autofmt_xdate(rotation=45) plt.show()用twinx()创建共享x轴的双y轴图,好处是销售额和订单量两个量纲完全不同的指标能放在同一张图上对比趋势。但要注意,双y轴图容易带来视觉误导,所以两张图最好用不同颜色,并且确保量级差异不会造成误读。
4.2 结构占比与品类排名可视化
看品类构成的时候,饼图和水平条形图比较直观。但饼图有一个常见坑:品类太多时,小份额的扇区挤在一起根本看不清。我建议,占比小于5%的类目在饼图上统一标为“其他”。
# 品类销售占比 category_sales = df.groupby('category')['total_amount'].sum().sort_values(ascending=False) other_threshold = category_sales.sum() * 0.05 small_categories = category_sales[category_sales < other_threshold] category_sales_display = category_sales[category_sales >= other_threshold].copy() category_sales_display['其他'] = small_categories.sum() plt.figure(figsize=(8, 8)) category_sales_display.plot.pie(autopct='%.1f%%', startangle=90, counterclock=False) plt.title('品类销售额占比') plt.ylabel('') plt.show()这里列了一个逻辑:先计算阈值(总额的5%),再把这些小额类目合并成“其他”。数据可视化有个原则叫“信息有效传递”,当一张图里的信息多到人眼无法识别,这张图就是在浪费读者的注意力。
4.3 从数据到结论:推动业务决策的分析话术
可视化不是终点,结论才是。我做完这些分析后,会整理成一页精简的分析备忘,主题句都是结论型的,不带模棱两可的话:
- 销售高度依赖大促节点:10-11月贡献全年约32%的销售额,日常月份的销售平平,说明店铺缺乏非大促期间的刺激手段。
- 收纳整理和厨房用品是核心品类,贡献过半收入。但也要警惕对单品类过度依赖,如果供应链出问题,整体营收会受影响。
- 高价值用户数量少但贡献大,建议客服团队单独建群维护,定期推送新品,分析这批用户的复购周期,在合适节点做召回。
- 周末订单量高于工作日,和家居类目消费者的空闲时间偏好一致,周末定向投放广告的ROI大概率高于工作日。
分析报告最重要的是“给判断,不给数据汇编”。老板要的是“接下来怎么办”,不是一张张报表。
5. 实战中容易踩的坑:pandas使用避坑清单
这部分内容是我平时工作里最常被问到、也最常翻车的点。每一项都是真实操作中碰过的,按频率排序写出来。
5.1 安装pandas失败或者找不到匹配版本
pip install pandas偶尔会报错,内容类似“could not find a version that satisfies the requirement”。多数情况下不是包不存在,而是网络源连接出了问题。解决办法就是换镜像源,用国内的开源镜像站加速。清华源是我最常用的,速度和稳定性都有保障。
pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple如果还没装pip或者用的是Anaconda,也可以用conda安装:
conda install pandas另外有一种情况是Python版本太低,最新版pandas不再支持。比如pandas 2.x要求Python 3.8以上,如果你的环境还是3.6,就会卡在依赖解析上。解决方案是装一个兼容旧版Python的pandas版本,比如pip install pandas==1.5.3。先看python --version再装,能省不少时间。
5.2 SettingWithCopyWarning:pandas里最经典的警告
操作DataFrame时经常弹出这个警告:
SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
很多人看到警告就慌,其实它只是告诉你:你是在一个“副本”上做修改,这个修改不会写回原DataFrame。这种问题的根源是“链式索引”df[df['a']>1]['b']=2,pandas没法确定你在操作原数据还是副本。
正确做法有两种:要么用df.loc[df['a'] > 1, 'b'] = 2显式指定行列位置,要么在筛选之后用.copy()显式生成一个独立副本。我个人的习惯是:只要我对筛选结果要做改动,第一时间就加.copy(),养成习惯后基本不会再触发这个警告。
# 推荐写法 filtered = df[df['quantity'] > 0].copy() filtered.loc[:, 'new_col'] = filtered['total_amount'] * 2 # 可视化前拷贝一份,避免后续误操作 df_plot = df.copy()5.3 中文显示成方块和读写乱码
matplotlib画图时中文经常显示成小方块,这是字体问题,不是代码逻辑问题。在脚本开头设置字体和负号显示就行:
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False如果设置了还没有效果,说明当前环境没有对应字体。Linux服务器上常见这种情况,用fc-list看系统安装了哪些中文字体,没有就装一个:
# Linux(Debian/Ubuntu) apt-get install fonts-wqy-zenhei fonts-wqy-microheiCSV读写乱码则是编码问题。读入时用encoding='utf-8',如果报错或乱码就换成gbk或gb18030。写出时,如果文件要给别人在Windows上用Excel打开,我一般用encoding='utf-8-sig',这个编码会在文件头加上BOM标记,Excel双击打开才不会乱码。
5.4 inplace参数和链式操作:性能和可读性的取舍
pandas很多方法都带inplace参数,比如df.dropna(inplace=True)。网上对这个参数争论很多,我的使用习惯是:能不用就不用,统一用赋值方式:
# 更推荐 df = df.dropna() # 不推荐 df.dropna(inplace=True)原因很简单:inplace=True在某些pandas版本里对某些方法的表现并不一致,而且它没法用在链式操作中间,出了问题也不容易排查。用赋值方式,每一步都产生新对象,虽然会多用一点内存,但对一般规模的数据分析任务来说毫无影响。分析代码的可读性和可调试性远比这点内存重要。
再补充一个处理大批量数据时的小技巧:如果数据量到了几个G,read_csv会跑得比较慢,可以指定dtype参数减少内存占用,还可以用usecols只加载需要的列。这两招对内存不够的情况特别管用。
6. 分析与自动化扩展:把代码变成可复用的脚本
这个案例做完了,但如果数据每个月都更新一遍,每次都手动跑一遍Jupyter Notebook就太傻了。所以到项目后期,我习惯把整个分析流程封装成函数,用一份脚本接收新数据文件,直接输出最新的月度分析表和图表。
def analyze_orders(filepath): # 加载和清洗 df = pd.read_csv(filepath, encoding='utf-8') df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce') df = df.drop_duplicates().dropna(subset=['order_date']) df = df[df['quantity'] > 0] # 特征工程 df['month'] = df['order_date'].dt.month df['day_of_week'] = df['order_date'].dt.dayofweek # 月度聚合 monthly = df.groupby(['month']).agg( 销售额=('total_amount', 'sum'), 订单量=('order_id', 'nunique') ).reset_index() return monthly result = analyze_orders('orders_new.csv') print(result)把逻辑封装成函数,有几个明显好处:一是新数据来了直接调用,不用从头改代码;二是函数有输入输出,方便别人复用和维护;三是不容易因为手动操作遗漏清洗步骤。做分析项目的时候,我一般会先写一个临时版把结论验证出来,等逻辑稳定了,再整理成这种可复用的脚本。
到这里,一个完整的数据分析综合案例就算跑通了。从原始CSV读入,到清洗、转换、聚合、透视、可视化,再到最终形成固定脚本输出,这是pandas在真实业务里最常见的完整链路。我个人在实际操作中的体会是:pandas本身并不难,真正决定项目成败的是你对业务数据的理解程度,以及每一步操作背后是否想清楚了“为什么”。如果这篇能帮你少走几个坑,那就值得了。后续如果有机会,我打算把RFM分群的部分再往后拓展一层,加入用户生命周期分析和流失预警,那又是另一篇可以讲透的内容了。