前言

在你通往 AI 大模型开发的道路上,除了掌握复杂的神经网络算法,扎实的数据处理能力才是地基。大模型的训练本质上是海量数据的清洗、特征工程与价值挖掘。

今天我们要做的这个RFM 会员价值打分项目,虽然不涉及深度学习,但它完整演练了数据分析的核心流程:数据加载 -> 清洗 -> 特征提取 -> 量化评分 -> 可视化决策。这是每一位 AI 工程师在处理业务数据时必须具备的“基本功”。

本文将细致地,一步一步地拆解所有操作,不仅告诉你代码怎么写,更告诉你为什么要这么写,以及背后的业务逻辑是什么。


🛠️ 第一步:环境准备与工具导入

任何数据项目的起点,都是准备好我们的“工具箱”。我们需要三个核心库:

  • numpy:数值计算的基础,处理数组高效。
  • pandas绝对主角。用于读取 Excel、清洗数据、分组聚合(GroupBy)。
  • matplotlib:绘图库,将枯燥的数据变成直观的图表。

1.1 代码实操与详解

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

# 【关键设置】解决中文乱码
# Matplotlib 默认不支持中文,直接画图会显示方框。
# 这行代码指定使用 'SimHei' (黑体) 来渲染中文字符。
plt.rcParams['font.sans-serif'] = ['SimHei']

# 【额外提示】
# 读取 Excel 文件需要 openpyxl 引擎支持。
# 如果运行报错 "Missing optional dependency 'openpyxl'",
# 请在终端执行:pip install openpyxl

📝 操作要点:

  • 养成好习惯,在代码开头就处理好编码和字体问题,否则后续画图时出现乱码会非常影响调试效率。
  • 导入 os 模块是为了后续能智能地处理文件路径,避免硬编码路径导致代码换台电脑就无法运行。

📂 第二步:智能加载数据

数据通常不会刚好放在脚本同级目录。为了代码的可移植性,我们需要动态获取当前脚本的位置,并据此拼接数据文件的路径。

2.1 代码实操与详解

# 1. 获取当前脚本所在的绝对目录
# __file__ 代表当前文件的路径,abspath 转为绝对路径,dirname 获取文件夹路径
script_dir = os.path.dirname(os.path.abspath(__file__))

# 2. 拼接数据文件的路径
# 假设数据文件位于上一级目录的 Data 文件夹下:../Data/sales_test.xlsx
data_path = os.path.join(script_dir, "../Data/sales_test.xlsx")

# 3. 读取 Excel 数据
# index_col='USERID':这是一个关键参数。
# 它直接将 'USERID' 列设为 DataFrame 的索引(Index)。
# 这样做的好处是,后续我们可以直接按用户 ID 进行分组操作,无需额外指定列名。
df = pd.read_excel(data_path, index_col='USERID')

# 4. 初步检查数据
print(f"原始数据形状 (行, 列): {df.shape}")
# print(df.head()) # 实际开发中建议取消注释,查看前 5 行数据长什么样

📝 操作要点:

  • 目录image-20260312103603331
  • 检查数据image-20260312103638573
  • 路径拼接:使用 os.path.join 而不是手动写 /\,可以兼容 Windows 和 Mac/Linux 系统。
  • 索引设定index_col 参数非常重要。将用户 ID 设为索引后,数据天然就是“以用户为核心”的结构,这对后续的 groupby 操作至关重要。

🧹 第三步:数据预处理(清洗)

真实世界的数据往往是“脏”的:可能有空行、缺失值。如果不处理,计算时会报错或导致结果偏差。

3.1 代码实操与详解

# dropna(how='all'):只有当一行中【所有】列都为空时,才删除该行。
# 区别:
# how='any':只要有一个数据缺失就删除(太暴力,可能误删有效数据)。
# how='all':整行都没数据才删(比较安全,仅去除无效空行)。
sale_data = df.dropna(how='all')

print(f"清洗后数据形状: {sale_data.shape}")
print(f"列名信息: {sale_data.columns}")

📝 操作要点:

  • 清洗后并输出列名信息image-20260312103909133
  • 对比变化:每次清洗后打印 shape,确认数据量变化是否符合预期。如果行数锐减,说明原数据质量很差,需要进一步排查。
  • 策略选择:对于部分缺失的数据(例如有订单号但缺金额),在实际项目中可能需要填充平均值或删除该特定列,这里我们仅做最基础的全空行清理。

🧠 第四步:核心分析 - 计算 R、F、M 指标

这是项目的灵魂。我们需要把原始的“流水账”转换成每个用户的“三维画像”。

  • R (Recency):最近一次消费时间(越越好——来得多)
  • F (Frequency):消费频率(越越好——花得频繁)
  • M (Monetary):消费金额(越越好——花得多)

4.1 代码实操与详解

# groupby(sale_data.index):按用户 ID(索引)进行分组
# 因为我们在读取时设了 index_col='USERID'(第29行),所以这里直接按索引分组即可

# 1. 计算 R (Recency):找到每个用户最近一次购买时间
# 对 'ORDERDATE' 列取最大值 (max)。
# 注意:日期越大,代表时间越新(例如 2023-01-01 比 2022-01-01 大,也更近)。
R_data = sale_data.groupby(sale_data.index)['ORDERDATE'].max()

# 2. 计算 F (Frequency):统计每个用户下了多少单
# 对 'ORDERID' 列进行计数 (count)
F_data = sale_data.groupby(sale_data.index)['ORDERID'].count()

# 3. 计算 M (Monetary):统计每个用户花了多少钱
# 对 'AMOUNTINFO' 列进行求和 (sum)
M_data = sale_data.groupby(sale_data.index)['AMOUNTINFO'].sum()

print("=== 原始指标计算完成 ===")
# print(R_data.head()) # 查看前几个用户的最近购买时间

📝 操作要点:

  • 查看前几个用户的最近购买时间image-20260312104424109
  • GroupBy 逻辑:记住口诀**“先分组,后聚合”**。Pandas 会自动把同一个用户的所有订单归为一组,然后分别执行 count、sum、max 操作。
  • R 的计算细节:这里取 max() 是因为日期对象本身可比大小,最大的日期即代表“最近”。

📊 第五步:指标打分(离散化)

原始的 R/F/M 数值差异巨大(比如金额有 100 元也有 10000 元,天数有 1 天也有 300 天),直接相加没有意义。我们需要将它们标准化为 1-5 分。

5.1 核心工具:pd.cut

pd.cut 可以将连续数值切成几个区间(桶),并打上标签。

5.2 代码实操与详解

# 1. F 和 M 的打分:直接分为 5 等份,分数 1-5
# labels=[1, 2, 3, 4, 5] 表示:
# 数值最小的区间给 1 分,数值最大的区间给 5 分。
# 对于 F 和 M,数值越大越好,所以逻辑是匹配的。
F_score = pd.cut(F_data, 5, labels=[1, 2, 3, 4, 5])
M_score = pd.cut(M_data, 5, labels=[1, 2, 3, 4, 5])

# 2. R 的特殊处理 (关键逻辑修正)
# R 代表“最近一次时间”,业务逻辑是:时间越近(天数差越小),分数应该越高。
# 但 pd.cut 默认是按数值从小到大分箱的。
# 如果我们计算的是“距离基准日的天数”(R_days):
#   - 天数小 (刚买过) -> 落入第 1 个箱子
#   - 天数大 (很久没买) -> 落入第 5 个箱子
# 如果直接用 labels=[1, 2, 3, 4, 5],会导致“很久没买”的人得 5 分,“刚买过”的人得 1 分,这与业务逻辑相反!

# 设定一个基准时间(通常是数据集中最新的日期或当前日期)
base_date = pd.to_datetime("2022-05-14") 

# 计算相差天数
R_days = (R_data - base_date).dt.days   

# 【修正方案】:
# 为了让“天数小”的人得“高分”,我们需要将标签顺序倒过来:
# 第 1 个箱子 (天数最少) -> 给 5 分
# 第 5 个箱子 (天数最多) -> 给 1 分
R_score = pd.cut(R_days, 5, labels=[5, 4, 3, 2, 1]) 

print("=== 打分完成 ===")

⚠️ 重点逻辑解析:
很多初学者容易在这里犯错。请务必理解:

  • F 和 M:数值越大越好 -> 分箱标签 [1, 2, 3, 4, 5] (从小到大对应低分到高分)。
  • R (基于天数):数值越小越好 -> 分箱标签 [5, 4, 3, 2, 1] (从小到大对应高分到低分)。
  • 如果不做这个反转,最终的 RFM 模型将完全失效,甚至得出“流失用户是高价值用户”的错误结论。

🔗 第六步:合并数据与加权计算

现在我们有三个独立的 Series(R_score, F_score, M_score),需要将它们拼成一张宽表,并根据业务重要性计算总分。

6.1 代码实操与详解

# 1. 准备数据列表
rfm_list = [R_score, F_score, M_score]
rfm_cols = ['r_score', 'f_score', 'm_score']

# 2. 转换与合并
# np.array(rfm_list) 会创建一个 (3, 用户数) 的数组
# .transpose() 将其转置为 (用户数, 3),符合 DataFrame 的行列表结构
# 最后放入 DataFrame,指定列名和索引 (保持用户 ID 不变)
rfm_df = pd.DataFrame(
    np.array(rfm_list).transpose(), 
    dtype=np.int32, 
    columns=rfm_cols, 
    index=R_data.index
)

# 3. 加权得分
# 业务场景模拟:
# 老板认为“消费金额 (M)"最重要,权重给 60%;
# “最近购买时间 (R)"和“频率 (F)"各占 20%。
# 权重可以根据实际业务目标调整(如拉新期可能更看重 F)。
weights = {'r': 0.2, 'f': 0.2, 'm': 0.6}

rfm_df['rfm_w_score'] = (
    rfm_df['r_score'] * weights['r'] + 
    rfm_df['f_score'] * weights['f'] + 
    rfm_df['m_score'] * weights['m']
)

print("=== 加权得分表 (前 5 行) ===")
print(rfm_df.head())

📝 操作要点:

  • 查看加权得分表image-20260312105222143
  • 数据对齐:Pandas 在创建 DataFrame 时会自动根据 index 对齐数据,确保每个用户的 R、F、M 分数不会错位。
  • 权重的意义:这是将数学模型转化为业务决策的关键一步。不同的权重组合代表了不同的运营战略。

🎨 第七步:用户分层与可视化

有了总分,我们怎么划分等级?通常使用**分箱(Binning)**技术,将连续的分數划分为“高、中、低”三个层级。

7.1 定义规则与绘图

# 1. 用户分层规则
# bins=[0, 2, 3, 5] 表示:
# (0, 2] 分 -> '低价值'
# (2, 3] 分 -> '中价值'
# (3, 5] 分 -> '高价值'
bins = [0, 2, 3, 5]
labels = ['低价值', '中价值', '高价值'] 

# 应用分层
rfm_df['客户分类'] = pd.cut(rfm_df['rfm_w_score'], bins=bins, labels=labels)

# 2. 可视化展示
plt.figure(figsize=(8, 6)) # 设置画布大小,单位英寸

# 统计各等级人数
# value_counts() 默认按数量降序排列
counts = rfm_df['客户分类'].value_counts()

# 为了图表顺序符合逻辑(低->中->高),我们手动重排索引
# fill_value=0 防止某个类别没人导致报错
counts = counts.reindex(['低价值', '中价值', '高价值'], fill_value=0) 

# 绘制柱状图
counts.plot(
    kind='bar', 
    color=['#FF6B6B', '#4ECDC4', '#FFE66D'], # 自定义颜色:红、绿、黄
    edgecolor='black', # 柱子边框黑色
    width=0.6
)

plt.title('RFM 客户价值分群统计', fontsize=16, pad=20)
plt.xlabel('会员等级', fontsize=12)
plt.ylabel('会员数量 (人)', fontsize=12)
plt.grid(axis='y', linestyle='--', alpha=0.7) # 只显示 Y 轴虚线网格

# 【进阶技巧】在柱子上方显示具体数值
for i, v in enumerate(counts):
    plt.text(i, v + 0.1, str(int(v)), ha='center', fontsize=12, fontweight='bold')

plt.tight_layout() # 自动调整布局,防止标签被截断
plt.show()

# 3. 保存结果到本地
# encoding='utf-8-sig' 是关键,它能防止 Excel 打开 CSV 时中文乱码
output_path = os.path.join(script_dir, "../Data/rfm_score_result.csv")
rfm_df.to_csv(output_path, encoding='utf-8-sig')
print(f"分析结果已保存至: {output_path}")

📝 操作要点:

  • 分析结果image-20260312105506873
  • 同时,也可以把结果保存到本地:image-20260312105607212
  • 重排索引value_counts 默认按数量排序,这可能导致图表中“高价值”出现在最左边,“低价值”在最右边,不符合阅读习惯。使用 reindex 强制按业务逻辑排序。
  • 编码格式:保存 CSV 给非技术人员(如运营同事)查看时,务必使用 utf-8-sig,这是 Excel 友好型编码。
  • 图表美化:加上数值标签、清晰的标题和网格线,能让报告看起来更专业。

💡 总结与进阶思考

恭喜你!你已经独立完成了一个完整的数据闭环项目:

  1. 数据获取 (Load)
  2. 数据清洗 (Clean)
  3. 特征工程 (Feature Engineering - 计算 RFM)
  4. 建模评分 (Scoring)
  5. 业务洞察 (Visualization & Output)

这个项目虽然代码量不大,但它涵盖了 AI 数据工程中最核心的思维模式:将模糊的业务问题(谁是好客户?)转化为可计算的数学指标(RFM 分数),再通过数据驱动决策。

🚀 进阶挑战(留到下一篇博客讲)

如果你想进一步提升,可以尝试以下优化:

  1. 动态阈值:目前的 bins=[0, 2, 3, 5] 是写死的。如果所有用户分数都很高怎么办?尝试改用百分位数(如前 20% 定义为高价值)来动态划分阈值。
  2. 精细化人群策略
    • 不要只看总分,尝试结合 R、F、M 的具体组合。
    • 例如:R 低 F 低 M 高(新用户但买了一大单)vs R 高 F 高 M 高(老土豪最近不来了)。针对这两类人,你的营销策略应该完全不同。你能写出代码自动识别这 8 类细分人群吗?
  3. 自动化报告:尝试将绘图和文字分析整合成一个 HTML 报告,定时自动发送给运营团队。

希望这篇教程能帮你打通从“代码”到“业务”的任督二脉。在 AI 大模型的浪潮中,这种扎实的数据处理能力将是你最宝贵的财富。继续加油!

完整代码如下:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

# 【关键设置】解决中文乱码
# Matplotlib 默认不支持中文,直接画图会显示方框。
# 这行代码指定使用 'SimHei' (黑体) 来渲染中文字符。
plt.rcParams['font.sans-serif'] = ['SimHei']

# 【额外提示】
# 读取 Excel 文件需要 openpyxl 引擎支持。
# 如果运行报错 "Missing optional dependency 'openpyxl'",
# 请在终端执行:pip install openpyxl


# 1. 获取当前脚本所在的绝对目录
# __file__ 代表当前文件的路径,abspath 转为绝对路径,dirname 获取文件夹路径
script_dir = os.path.dirname(os.path.abspath(__file__))

# 2. 拼接数据文件的路径
# 假设数据文件位于上一级目录的 Data 文件夹下:../Data/sales_test.xlsx
data_path = os.path.join(script_dir, "../Data/sales_test.xlsx")

# 3. 读取 Excel 数据
# index_col='USERID':这是一个关键参数。
# 它直接将 'USERID' 列设为 DataFrame 的索引(Index)。
# 这样做的好处是,后续我们可以直接按用户 ID 进行分组操作,无需额外指定列名。
df = pd.read_excel(data_path, index_col='USERID')

# 4. 初步检查数据
# print(f"原始数据形状 (行, 列): {df.shape}")
# print(df.head()) # 实际开发中建议取消注释,查看前 5 行数据长什么样


# dropna(how='all'):只有当一行中【所有】列都为空时,才删除该行。
# 区别:
# how='any':只要有一个数据缺失就删除(太暴力,可能误删有效数据)。
# how='all':整行都没数据才删(比较安全,仅去除无效空行)。
sale_data = df.dropna(how='all')

# print(f"清洗后数据形状: {sale_data.shape}")
# print(f"列名信息: {sale_data.columns}")


# groupby(sale_data.index):按用户 ID(索引)进行分组
# 因为我们在读取时设了 index_col='USERID'(第29行),所以这里直接按索引分组即可

# 1. 计算 R (Recency):找到每个用户最近一次购买时间
# 对 'ORDERDATE' 列取最大值 (max)。
# 注意:日期越大,代表时间越新(例如 2023-01-01 比 2022-01-01 大,也更近)。
R_data = sale_data.groupby(sale_data.index)['ORDERDATE'].max()

# 2. 计算 F (Frequency):统计每个用户下了多少单
# 对 'ORDERID' 列进行计数 (count)
F_data = sale_data.groupby(sale_data.index)['ORDERID'].count()

# 3. 计算 M (Monetary):统计每个用户花了多少钱
# 对 'AMOUNTINFO' 列进行求和 (sum)
M_data = sale_data.groupby(sale_data.index)['AMOUNTINFO'].sum()

# print("=== 原始指标计算完成 ===")
# print(R_data.head()) # 查看前几个用户的最近购买时间


# 1. F 和 M 的打分:直接分为 5 等份,分数 1-5
# labels=[1, 2, 3, 4, 5] 表示:
# 数值最小的区间给 1 分,数值最大的区间给 5 分。
# 对于 F 和 M,数值越大越好,所以逻辑是匹配的。
F_score = pd.cut(F_data, 5, labels=[1, 2, 3, 4, 5])
M_score = pd.cut(M_data, 5, labels=[1, 2, 3, 4, 5])

# 2. R 的特殊处理 (关键逻辑修正)
# R 代表“最近一次时间”,业务逻辑是:时间越近(天数差越小),分数应该越高。
# 但 pd.cut 默认是按数值从小到大分箱的。
# 如果我们计算的是“距离基准日的天数”(R_days):
#   - 天数小 (刚买过) -> 落入第 1 个箱子
#   - 天数大 (很久没买) -> 落入第 5 个箱子
# 如果直接用 labels=[1, 2, 3, 4, 5],会导致“很久没买”的人得 5 分,“刚买过”的人得 1 分,这与业务逻辑相反!

# 设定一个基准时间(通常是数据集中最新的日期或当前日期)
base_date = pd.to_datetime("2022-05-14")

# 计算相差天数
R_days = (R_data - base_date).dt.days

# 【修正方案】:
# 为了让“天数小”的人得“高分”,我们需要将标签顺序倒过来:
# 第 1 个箱子 (天数最少) -> 给 5 分
# 第 5 个箱子 (天数最多) -> 给 1 分
R_score = pd.cut(R_days, 5, labels=[5, 4, 3, 2, 1])

# print("=== 打分完成 ===")


# 1. 准备数据列表
rfm_list = [R_score, F_score, M_score]
rfm_cols = ['r_score', 'f_score', 'm_score']

# 2. 转换与合并
# np.array(rfm_list) 会创建一个 (3, 用户数) 的数组
# .transpose() 将其转置为 (用户数, 3),符合 DataFrame 的行列表结构
# 最后放入 DataFrame,指定列名和索引 (保持用户 ID 不变)
rfm_df = pd.DataFrame(
    np.array(rfm_list).transpose(),
    dtype=np.int32,
    columns=rfm_cols,
    index=R_data.index
)

# 3. 加权得分
# 业务场景模拟:
# 老板认为“消费金额 (M)"最重要,权重给 60%;
# “最近购买时间 (R)"和“频率 (F)"各占 20%。
# 权重可以根据实际业务目标调整(如拉新期可能更看重 F)。
weights = {'r': 0.2, 'f': 0.2, 'm': 0.6}

rfm_df['rfm_w_score'] = (
    rfm_df['r_score'] * weights['r'] +
    rfm_df['f_score'] * weights['f'] +
    rfm_df['m_score'] * weights['m']
)

print("=== 加权得分表 (前 5 行) ===")
print(rfm_df.head())


# 1. 用户分层规则
# bins=[0, 2, 3, 5] 表示:
# (0, 2] 分 -> '低价值'
# (2, 3] 分 -> '中价值'
# (3, 5] 分 -> '高价值'
bins = [0, 2, 3, 5]
labels = ['低价值', '中价值', '高价值']

# 应用分层
rfm_df['客户分类'] = pd.cut(rfm_df['rfm_w_score'], bins=bins, labels=labels)

# 2. 可视化展示
plt.figure(figsize=(8, 6)) # 设置画布大小,单位英寸

# 统计各等级人数
# value_counts() 默认按数量降序排列
counts = rfm_df['客户分类'].value_counts()

# 为了图表顺序符合逻辑(低->中->高),我们手动重排索引
# fill_value=0 防止某个类别没人导致报错
counts = counts.reindex(['低价值', '中价值', '高价值'], fill_value=0)

# 绘制柱状图
counts.plot(
    kind='bar',
    color=['#FF6B6B', '#4ECDC4', '#FFE66D'], # 自定义颜色:红、绿、黄
    edgecolor='black', # 柱子边框黑色
    width=0.6
)

plt.title('RFM 客户价值分群统计', fontsize=16, pad=20)
plt.xlabel('会员等级', fontsize=12)
plt.ylabel('会员数量 (人)', fontsize=12)
plt.grid(axis='y', linestyle='--', alpha=0.7) # 只显示 Y 轴虚线网格

# 【进阶技巧】在柱子上方显示具体数值
for i, v in enumerate(counts):
    plt.text(i, v + 0.1, str(int(v)), ha='center', fontsize=12, fontweight='bold')

plt.tight_layout() # 自动调整布局,防止标签被截断
plt.show()

# 3. 保存结果到本地
# encoding='utf-8-sig' 是关键,它能防止 Excel 打开 CSV 时中文乱码
output_path = os.path.join(script_dir, "../Data/rfm_score_result.csv")
rfm_df.to_csv(output_path, encoding='utf-8-sig')
print(f"分析结果已保存至: {output_path}")
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐