
我最早接触数据分析的时候也是先从 Pandas 和 NumPy 下手的。这两个库一个管“表格”一个管“数值计算”几乎所有和数据打交道的场景都绕不开它们。特别是做电商用户分析这种业务订单表、用户表、行为日志一堆 CSV 导出来动辄几十万行用 Excel 打开直接卡死用纯 Python 循环去算更是慢得离谱。但换成 Pandas 的 DataFrame 和 NumPy 的向量化操作几个核心方法下去几秒钟就能汇总出用户的消费习惯、复购周期、价值分层效率完全不在一个量级。这篇内容适合想系统上手 Pandas 和 NumPy 的人也适合那些已经能跑通简单脚本、但面对真实业务数据时不知道从哪下手的朋友。我会按“思路设计 → 环境准备 → 数据清洗 → 分组聚合 → 实战建模”这条线来讲每一段都会配可运行的代码最后用一个完整的电商用户分析案例把前面的知识点串起来。你跟着敲一遍之后换一批数据套同样的思路也基本能直接上手。1. 整体设计思路为什么你学了很多 Pandas 技巧做项目还是懵很多初学者有个通病就是单独看每个函数都懂groupby会用merge也会用但拿到一张真实的订单表时不知道第一步该干什么。这个问题的根源在于数据分析不是“会函数”就行的而是要先建立一个处理流程的框架感。1.1 数据分析的标准工作流从原始表到业务结论我一般会把数据分析拆成五个环节获取数据 → 数据清洗 → 维度拆解 → 指标计算 → 结果解读。Pandas 和 NumPy 在整个链路中的作用侧重不同。获取数据Pandas 负责把 CSV、Excel、数据库查询结果读成 DataFrame。数据清洗处理缺失值、去重、类型转换、异常值过滤这部分是体力活但决定了后续分析的可信度。维度拆解用groupby、pivot_table按用户、时间、商品类目等维度切分数据。指标计算用agg完成求和、均值、计数、分位数等聚合NumPy 的数学函数在自定义计算中非常高效。结果解读把聚合结果整理为业务洞察比如“高价值用户占多少”、“复购周期集中在几天”。1.2 为什么选 Pandas NumPy而不是其他方案有人会问现在不是有 Spark、Polars、ClickHouse 这些更“大”的工具吗为什么还要先学 Pandas。原因很简单Pandas 是数据分析生态的通用语言很多高级工具都借鉴了它的 DataFrame 设计而且对于 10 万行到几百万行级别的数据Pandas 在单机上的表现足够优秀根本不需要引入分布式组件。核心是先把业务逻辑跑通再去谈扩展和性能优化。NumPy 则是 Pandas 的底座它提供了高效的多维数组对象ndarray和向量化计算能力。Pandas 里的 Series 和 DataFrame底层数据存储就是 NumPy 数组。理解了 NumPy 的广播机制和索引方式你在 Pandas 里做条件筛选、算数运算时就会更通透遇到性能问题也知道该怎么查。核心思路以业务问题为驱动倒推需要哪些数据操作。不要为了用函数而用函数每一步操作都要回答“这一步是为了解决什么问题”。2. 环境准备与数据基础先把 Pandas 和 NumPy 跑起来这一节我会带你从环境搭建开始把 Series、DataFrame 这些基本概念过一遍并用一段真实模拟的电商订单数据做第一次“体检”。2.1 安装与导入一分钟搞定基础环境如果你用的是 AnacondaPandas 和 NumPy 已经自带了直接导入就行。如果是纯 Python 环境打开终端执行pip install pandas numpy然后导入我的习惯是给这两个库设置固定别名import numpy as np import pandas as pd print(pd.__version__) print(np.__version__)别小看这两行版本打印不同版本 API 会有细微差别你把版本写进博客或者笔记里后面排查问题能少走很多弯路。2.2 两个核心数据结构Series 和 DataFrameSeries一维带标签的数组可以理解为“一列数据”就是 Excel 里的一列。DataFrame二维表格可以想象成 Excel 的一个 Sheet它由多个 Series 组成每个 Series 是一列。创建一个 DataFrame 很简单最常用的是从字典构建import pandas as pd df pd.DataFrame({ user_id: [1001, 1002, 1003], order_amount: [299, 459, 129], order_date: [2024-01-01, 2024-01-02, 2024-01-03] }) print(df)你会发现字典的键变成了列名值是整列数据。这种结构非常贴近我们平时看到的订单明细表。2.3 读取真实数据让 Pandas 帮你干活实际工作中数据很少在代码里手工创建更多是从外部文件加载。load 一个电商订单 CSV 的经典写法df pd.read_csv( order_data.csv, encodingutf-8, parse_dates[order_date], dtype{user_id: str} )这里有两个关键参数很多人会忽略。parse_dates会把指定列解析成时间对象这样后续做“按月统计”会非常方便dtype强制指定列类型比如订单编号、手机号这类适合用字符串存避免因为前导零丢失或者被当成数值处理而出现精度问题。读取之后的第一步永远是体检数据print(df.info()) print(df.describe()) print(df.head(10))df.info()可以看到每列的数据类型和缺失情况df.describe()输出数值列的统计摘要包括均值、标准差、最小最大值。这两行执行完之后你对数据质量就有了第一感觉。注意CSV 常见的坑是编码问题尤其是中文数据。读取时报UnicodeDecodeError时把encoding换成gbk或gb18030通常会解决。3. 数据清洗与预处理数据分析里 80% 的时间都花在这很多业务方给你的表都不及格乱码的日期字段、混合类型的手机号、数值列里藏着“无成交”这种文本。数据清洗没有捷径就是用 Pandas 把各种脏数据统一修正成规范化格式。我总结了一套固定流程你自己做项目时直接套用。3.1 缺失值处理先看清“缺多少”再决定“怎么办”拿到数据的第一个动作不是急着填补而是统计缺失情况null_count df.isnull().sum() print(null_count[null_count 0])缺失值的处理有四种常见策略需要按比例和场景选缺失比例极小 1%直接删除df.dropna(subset[关键列])。缺失比例较大的数值列用中位数填充df[amount].fillna(df[amount].median())。用户属性类字段填充“未知”df[city].fillna(未知)。时间列缺失先排序再按用户分组填充df.groupby(user_id)[order_date].ffill()。每种策略背后都有业务逻辑。比如客单价填充中位数比均值更稳妥因为均值容易被少数大额订单拉高而中位数更接近普通用户的消费水平。3.2 重复记录处理不要盲删要区分“完全重复”和“部分重复”用df.duplicated().sum()可以查看重复行数量。删除时我的习惯是保留第一条df df.drop_duplicates(subset[order_id], keepfirst)但要特别提醒电商订单表里同一个用户买多件商品会生成多个子单这些记录在订单号上不同但在用户维度和时间维度上看起来“重复”这种情况下千万不要删。判断重复的关键是业务上什么字段组合才应该唯一就把什么组合传给subset。3.3 NumPy 在清洗中最实用的几个场景Pandas 的清洗经常要配合 NumPy 的条件判断。比如把金额异常的订单打标然后分析而不是直接删掉df[amount_flag] np.where( df[order_amount] 5000, 高额订单, 普通订单 )另一个常用场景是批量替换多种异常值。比如把“无成交”、“NULL”、“null”、“-”统一处理成 NaNimport numpy as np df.replace([无成交, NULL, null, -], np.nan, inplaceTrue)这里inplaceTrue直接修改原数据但官方现在推荐用df df.replace(...)的写法避免引用混乱的问题。我自己的习惯是能不用inplace就不用除非明确要节省内存。3.4 类型转换与时间字段处理把机器的“眼睛”擦亮类型转换主要是为了“让机器正确理解每一列”。比如订单日期是字符串直接比较大小是按字典序排的一旦月份超过 9 就会出错必须转换成时间类型df[order_date] pd.to_datetime(df[order_date])然后就能轻松提取年月df[order_month] df[order_date].dt.to_period(M)to_period(M)的结果是类似2024-01这样的月度周期对象分组统计比单纯用字符串月份更高效排序也更准确。数值列如果混入了中文逗号或者单位可以用 Pandas 的正则替换清理df[amount] df[amount].astype(str).str.replace(,, ).str.replace(元, ).astype(float)这条链式操作值得细看先把非字符串转换为字符串然后依次去掉逗号和单位最后转成浮点数。处理脏数据时链式操作能省下大量中间变量。4. 分组聚合与透视Pandas 真正的核心战场清洗完数据下一步就是“怎么问问题”。比如电商老板往往最关心三个指标总销售额是多少、每个月的趋势怎么样、哪些用户贡献最多。这三个问题本质上都是“分组 聚合”。4.1 groupby 的底层逻辑拆分 → 应用 → 合并Pandas 的聚合操作可以用一句话概括把一个大表拆成多个小组对每个小组做计算再把结果合并回一张表。整个过程对应三个函数groupby拆分agg应用结果自动合并。来看一个非常典型的月度销售统计monthly_sales ( df.groupby(df[order_date].dt.to_period(M))[order_amount] .sum() .reset_index() ) print(monthly_sales)这段代码的意思是先按订单日期的月份分组然后对每一组的order_amount列求和最后reset_index()把月份从索引变成普通列。如果没有reset_index()结果会带一个层级索引后面要用月份去 join 其他表时会比较别扭。4.2 agg 聚合函数一次算多个指标的精髓agg是 Pandas 里最值得记住的方法之一。我们可以在一次分组里同时求总金额、订单数、平均客单价以及价格分位数stats_df ( df.groupby(user_id)[order_amount] .agg([sum, count, mean, median, lambda x: x.quantile(0.75)]) .rename(columns{ sum: total_amount, count: order_count, mean: avg_amount, median: median_amount, lambda: amount_75 }) .reset_index() )这段代码会生成一张每个用户的消费汇总表。你可能会疑惑mean和median结合一起看有什么意义如果平均值远大于中位数说明这个用户偶尔会有一笔大额消费大部分订单金额其实不高如果两者接近说明消费行为比较稳定。这个判断在用户分层时非常有用。如果需要对不同列应用不同的聚合函数你可以传入字典result df.groupby(user_id).agg({ order_amount: sum, order_id: count, coupon_discount: mean })4.3 透视表 pivot_tableExcel 数据透视表的 Python 版本透视表和groupby的区别在于它生成的是一张二维“交叉表”行和列各代表一个维度单元格是聚合值。比如行看用户群体列看月份值看销售额pivot pd.pivot_table( df, valuesorder_amount, indexuser_group, columnsorder_month, aggfuncsum, fill_value0 )fill_value0这个小参数非常有价值默认情况下透视表里有空值做热力图或者导出报表时会出现一块块空白填成 0 之后视觉和逻辑上都更整洁。4.4 向量化运算与 apply性能差距在这里拉开的Pandas 的核心性能优势是向量化。也就是说df[amount] * 0.9这个操作会在底层用 NumPy 对整个列同时计算而不是一个值一个值地循环 Python 对象。能用列间运算解决的问题永远不要用apply逐行处理。下面是典型的场景对比# 推荐直接列间计算 df[calculated_col] df[price] * df[quantity] - df[discount] # 备选apply 逐行 df[calculated_col] df.apply( lambda row: row[price] * row[quantity] - row[discount], axis1 )两段代码逻辑一样但第一段的速度可能是第二段的数十倍。那apply就完全没用了吗也不是当计算逻辑真的很复杂、需要多列条件判断或者要调用一个现成的业务函数时apply的可读性更好。你只需要在写代码时有一个清晰意识能向量化优先向量化确实搞不定再用apply。5. 电商用户分析完整实战从模拟数据到 RFM 模型前面的内容都是零部件这一节我们把它们组装成一台完整机器。我会用一段模拟数据完整演示电商用户分析的核心流程数据模拟 → 清洗 → 消费特征提取 → RFM 用户分层 → 结果解读。代码完整可直接运行。5.1 构造模拟数据不知道业务长啥样就自己先建一个import numpy as np import pandas as pd np.random.seed(42) user_ids np.arange(1001, 1201) n_orders 3000 # 模拟用户ID部分用户高频购买部分用户低频 sample_users np.random.choice(user_ids, sizen_orders, replaceTrue) # 模拟订单金额对数正态分布比较贴近电商客单价分布 order_amounts np.random.lognormal(mean4, sigma0.8, sizen_orders) # 模拟订单日期从2023-01-01到2024-12-31 order_dates pd.date_range(2023-01-01, 2024-12-31, freqD) sample_dates np.random.choice(order_dates, sizen_orders, replaceTrue) df_orders pd.DataFrame({ user_id: sample_users, order_amount: order_amounts.round(2), order_date: sample_dates }) # 模拟用户注册信息表 df_users pd.DataFrame({ user_id: user_ids, gender: np.random.choice([男, 女], sizelen(user_ids), replaceTrue), city: np.random.choice([北京, 上海, 广州, 深圳, 杭州], sizelen(user_ids)), register_date: pd.date_range(2022-01-01, 2023-12-31, freqD)[np.random.randint(0, 730, sizelen(user_ids))] })这段模拟逻辑里有两个值得说明的设计。一是订单金额我用的是lognormal(4, 0.8)中心值大概在e^4 ≈ 54.6附近这符合电商订单“小单多、大单少”的偏态分布。二是日期直接从两年的时间范围里随机抽取天然就是无序的正好用来验证清洗和排序步骤。5.2 数据清洗把模拟数据当成线上脏数据来对待我们先人为制造一些脏数据然后演示逐项处理# 注入一些异常值 df_orders.loc[0, order_amount] np.nan df_orders.loc[1, order_amount] 99999 # 去重 df_orders df_orders.drop_duplicates() # 删除订单金额为空的记录 df_orders df_orders.dropna(subset[order_amount]) # 过滤明显异常的订单金额比如超过10000元 df_orders df_orders[df_orders[order_amount] 10000] # 订单日期转时间格式 df_orders[order_date] pd.to_datetime(df_orders[order_date]) # 排序方便后续计算最近消费时间 df_orders df_orders.sort_values([user_id, order_date])异常值过滤这里我要多说一句直接删除 99999 这类极值是一种偷懒但不够严谨的做法。如果你的目的是做整体销售统计极值可能是真实的大客户贡献删了反而失真。所以更稳妥的方式是先用群体分位数打标比如某个用户消费远超同类用户时单独保留后进行二次核实。但为了示例流程简洁这里先按过滤处理。5.3 用户消费特征汇总每个用户的三个核心数字电商用户分析里最经典的用户特征提取就是对每个用户计算三个维度total_amount总消费金额order_count消费次数last_order_days最后一次消费距今天数from datetime import datetime today datetime(2025, 1, 1) user_features ( df_orders.groupby(user_id) .agg( total_amount(order_amount, sum), order_count(order_amount, count), last_order_date(order_date, max) ) .reset_index() ) user_features[last_order_days] (today - user_features[last_order_date]).dt.days计算last_order_days用的是向量化操作today减日期列得到一个Timedelta列再用.dt.days转成整数天数。这个值直接对应 RFM 模型里的 F 和 M。5.4 合并用户信息表把零散信息 Join 成统一宽表有了用户交易特征我们再合并用户注册信息形成一个包含用户属性的宽表user_analysis pd.merge( user_features, df_users, onuser_id, howinner ) print(user_analysis.head())merge的how参数决定了保留哪些数据。这里inner表示只保留两边都有的用户。实际操作中要注意如果订单表里有用户 ID 但用户注册表里查不到用inner就会把他们的交易丢掉了。这会直接影响后续统计口径所以先howleft保留订单表全部用户再排查未匹配的用户是什么情况是比较稳的做法。5.5 用 NumPy 实现对 RFM 打分不要用 Excel 一个个手动分RFM 是三个维度的简称Recency最近消费时间、Frequency消费频次、Monetary消费金额。经典的分层思路是先按三分位数给每个维度打 1~3 分再组合成用户等级。def rfm_classify(df): r_score pd.qcut(df[last_order_days], 3, labels[3, 2, 1]) f_score pd.qcut(df[order_count].rank(methodfirst), 3, labels[1, 2, 3]) m_score pd.qcut(df[total_amount].rank(methodfirst), 3, labels[1, 2, 3]) return r_score.astype(int), f_score.astype(int), m_score.astype(int) user_analysis[r_score], user_analysis[f_score], user_analysis[m_score] rfm_classify(user_analysis) user_analysis[rfm_label] ( user_analysis[r_score].astype(str) user_analysis[f_score].astype(str) user_analysis[m_score].astype(str) )这里有一个关键函数rank(methodfirst)。因为order_count和total_amount这两个维度本身是离散且存在大量并列值直接用pd.qcut会报“分位数边界不够唯一”的错误。先rank再qcut等于给每个值分配了一个排位号就能稳定分成三等份。这是我踩过很多次坑之后总结出的稳妥做法。5.6 用户分层结果解读对比不同组用户的业务特征分好层之后马上做一件事对每组用户计算平均指标看看不同分层是不是真的有明显业务差异。summary ( user_analysis.groupby(rfm_label) .agg( user_count(user_id, count), avg_total(total_amount, mean), avg_count(order_count, mean), avg_days(last_order_days, mean) ) .sort_values(avg_total, ascendingFalse) ) print(summary)这个输出表就是数据分析的最终交付物。你会看到类似333标签最近买、买得多、买得贵的用户平均消费金额和活跃天数都远高于111标签用户。下次运营活动选召回人群时333直接做成重点维护名单111做流失唤回就有了明确的数据支撑。如果你觉得九层还是太细想更通用地分成“高价值用户、潜力用户、风险用户、流失用户”四类可以写一个简单的映射逻辑def map_to_segment(row): if row[r_score] 3 and row[f_score] 3 and row[m_score] 3: return 高价值用户 elif row[r_score] 2 and row[f_score] 2: return 潜力用户 elif row[r_score] 1: return 流失风险用户 else: return 一般用户 user_analysis[segment] user_analysis.apply(map_to_segment, axis1) print(user_analysis.groupby(segment)[user_id].count())这种硬编码映射的好处是极度直白业务同事一眼就能看懂不会出现“你分组的逻辑是什么”的困惑。代价是边界条件需要你人工思考清楚不能盲目套用别人的阈值。电商业务各有各的节奏有的店铺一周内买两次才算高活跃有的一个月一次就很高频按照你实际数据的分位数来定阈值才是关键。6. 常见错误与排查技巧我踩过的坑希望你跳过Pandas 写多了之后你会慢慢形成一套“错误直觉”看到报错信息基本知道问题出在哪。这一节我整理几个我在电商数据分析里摔过跟头的典型问题每个都配上排查思路。6.1 明明有数据groupby 结果却为空原因多半是分组键里有大量缺失值或者分组键类型不一致。排查方法print(df[user_id].isnull().sum()) print(df[user_id].dtype)如果分组键是 NaNgroup 会默认忽略导致部分用户凭空消失。解决办法是先把缺失值补上或者过滤掉。6.2 qcut 报错 Bin edges must be unique这个错误几乎每个用 RFM 打分的人都会遇到原因就是前面提到的并列值过多。处理方案就两条一是加rank(methodfirst)二是减少分箱数量比如从 5 箱改成 3 箱。6.3 合并表之后行数翻倍merge出来的结果突然多出大量行通常是因为两个表在on字段上存在一对多关系。比如你用了user_id关联但用户表里有重复的user_id这时候结果就是笛卡尔积式的膨胀。排查方法是在合并前先检查两张表的唯一性print(df_users[user_id].duplicated().sum()) print(df_orders[user_id].duplicated().sum())6.4 时间字段类型不匹配有时候日期列从 CSV 读进来变成了字符串你用df[df[order_date] 2024-01-01]也能跑但排序和按月聚合的结果会非常诡异。最稳的排查手段是执行完pd.to_datetime后立刻打印df.dtypes确认目标列已经是datetime64[ns]类型。6.5 内存占用过高导致运行卡顿几百万行数据不是 Pandas 的极限但如果你把每个字段都设成object类型内存会呈指数级膨胀。一个简单的优化手段df[is_vip] df[is_vip].astype(bool) df[gender] df[gender].astype(category)category类型对低基数字段非常友好能显著降低内存占用。类似订单状态、城市、性别这种取值种类少但重复多的列都建议转换成category。总结成一句话的习惯拿到任何数据先info()检查类型再duplicated()查重复再isnull()查缺失全做完之后再进行任何计算。7. 写在最后的个人经验我在实际项目里用这套流程跑过的电商数据反复验证下来有几个体会。第一代码写得快不如思路清晰动手写 Pandas 代码之前先在纸上把“我想得到哪些指标、按哪个维度分组”写下来比乱试函数效率高得多。第二不要害怕apply慢它慢但逻辑直白等性能瓶颈真的出现了再优化也不迟但如果你天生就写成向量化风格未来数据量翻几倍你会感谢自己。第三NumPy 的向量化运算在 Pandas 里无处不在遇到列计算性能堪忧的时候先想想能不能把数据转成numpy.ndarray再算然后重新拼回 DataFrame。最后分享一个小技巧Pandas 有一个.pipe()方法可以把一串数据处理逻辑封装成函数链让代码像流水线一样清晰。def load_and_clean(df): df df.drop_duplicates() df df.dropna(subset[order_amount]) df[order_date] pd.to_datetime(df[order_date]) return df df ( pd.read_csv(orders.csv) .pipe(load_and_clean) .pipe(add_user_features) )这种写法特别适合把清洗逻辑沉淀成固定函数库下次换一批数据调用同一套管道处理思路完全复用。数据分析这件事有思路的人越做越透亮没有任何捷径多写多练多复盘慢慢你也会形成自己的一套处理流程。