ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Jupyter Notebook的Python用户画像构建:RFM实战指南

基于Jupyter Notebook的Python用户画像构建:RFM实战指南 简介这套基于Jupyter Notebook的Python用户画像构建源码面向希望系统性学习用户画像的数据分析师、产品运营及Python开发者可帮助读者从原始用户行为数据出发完成多维度画像标签的快速构建。资源包共20个文件含13个CSV数据文件、4个IPython Notebook分析文档、1个Python源脚本、1个PNG可视化结果图及1个txt说明文件整体压缩后约118.46MB。CSV文件提供了用户属性和行为记录Notebook完整展示数据清洗、特征提取与可视化分析流程独立Python脚本可作为数据预处理模块复用。目前已有125人学习下载附带说明文档与可视化结果适合作为用户画像入门项目的参考蓝本。通过该源码读者能掌握Pandas、Matplotlib等分析工具的实际应用理解如何将原始数据转为支撑商业决策的用户标签具有较强的工程实践价值。1. 用户画像项目为什么偏偏选 Jupyter Notebook 来搭做用户画像这件事很多团队第一反应是上大数据平台、写 Spark 任务或者直接丢给算法工程师跑个聚类模型。但真到落地的时候你会发现80% 的精力根本不在模型上而在数据清洗、特征试探、标签口径反复对齐这些脏活累活上。Jupyter Notebook 在这个场景下几乎是不可替代的——它能让你把「看数据 → 写规则 → 看结果 → 改规则」这个循环压缩到几秒钟而不是每次改一个阈值都要重新提交一次任务等半小时。这不是效率提升的问题是能不能做下去的问题。这篇文章要拆的就是一套基于 Jupyter Notebook 的 Python 用户画像构建源码思路。适合手里有一份用户订单或行为数据、想跑出第一版可用标签但不想一上来就上重型框架的从业者。我会从环境搭建、数据探查、RFM 特征计算、标签映射到可视化输出把每一步的代码和参数都摊开讲最后落在几个最容易让人翻车的坑上。这套方案我用来做过电商、内容社区和工具类产品的画像初版单机跑百万级用户数据没有问题再往上就该换引擎了但思路完全通用。先说一个反直觉的结论用户画像不是「画」出来的是「算」出来的。大多数业务方要的画像本质上是一张「用户 ID → 标签集合」的映射表而不是什么炫酷的可视化大屏。可视化只是最后给人看的。所以整篇的核心会放在「如何用 Python 把原始行为数据变成可落库、可查询、可更新的标签表」Jupyter 只是承载这个过程最顺手的载体。2. Jupyter Notebook 跑 Python 画像的准备工作环境、数据与首个探查代码2.1 本地 Jupyter 环境的三个选型理由与安装方式Jupyter Notebook 网页版界面大家都不陌生但真到自己装的时候还是有不少选择。常见的有三条路直接用 Anaconda 自带的环境、用 pip 单独装、或者用 VS Code 里集成的 Notebook。以我自己的经验如果是纯粹做用户画像这种以 pandas 为核心的活儿Anaconda 是最省心的因为它把 pandas、numpy、matplotlib 这些科学计算基础包都预装好了省去很多 python 安装教程里反复出现的依赖地狱。# 用 conda 创建独立环境避免污染系统 Python conda create -n persona python3.9 -y conda activate persona # 安装核心库pandas 做数据处理matplotlib 做可视化openpyxl 用于导出 Excel pip install pandas matplotlib openpyxl jupyter这里有几个参数值得说清楚。python3.9 是版本参数我选 3.9 而不是 3.12 这类新版是因为部分旧代码或第三方库比如某些金融数据接口在 3.10 以上会有兼容问题用户画像领域的源码大多跑在 3.7-3.9 区间没必要用最新的 Python 去挑战玄学兼容性。conda create -n 后面的 persona 是环境名可以随意改。最后一行 pip install 装了三个包就够了pandas 做数据加工、matplotlib 做画像可视化、openpyxl 是 pandas 写 Excel 的后端引擎。实际上 pandas 的 DataFrame.to_excel 方法需要 openpyxl很多人在这一步翻车导出时报错才发现没装。装完之后运行jupyter notebook启动服务浏览器会自动打开工作台。如果你在服务器上跑需要加--ip0.0.0.0参数才能从外部访问但本地开发不需要。启动以后建议先跑一个最简单的代码验证环境# 验证环境可用 import pandas as pd import matplotlib.pyplot as plt print(pandas version:, pd.__version__) print(matplotlib version:, plt.matplotlib.__version__)这段代码没有业务逻辑但它是所有后续工作的前提。pandas 的version能看到当前版本如果你之前装过其他版本的 pandas这里能帮你确认 conda 环境有没有激活成功。很多人犯的错是 conda activate 之后在终端里 pip list 看着对但 Jupyter 里 import 的还是旧版本——这是 Jupyter 内核和终端环境不一致导致的后面避坑章节会专门讲。2.2 理解源数据结构订单表、行为表、属性表怎么组织用户画像的输入数据没有标准格式但绝大多数业务场景会落到三种表上。第一种是用户属性表也叫用户基础信息表包含 user_id、性别、年龄、注册时间、城市这些相对静态的字段。第二种是订单表或交易表每行是一笔订单包含 user_id、订单金额、下单时间、商品类目。第三种是行为表比如浏览记录、点击日志、搜索关键词。对于第一版画像来说前两张表基本够用行为表可以做更细的兴趣标签但数据量大且噪声高我建议第二期再接。以电商为例订单表的结构通常长这样# 模拟订单表结构实际项目中直接从数据库或 CSV 读入 import pandas as pd df_orders pd.DataFrame({ user_id: [U001, U001, U002, U003, U003, U003], order_id: [A1001, A1002, B2001, C3001, C3002, C3003], order_amount: [299.0, 59.9, 129.0, 799.0, 39.9, 199.0], order_time: [2024-01-03, 2024-02-15, 2024-01-28, 2024-03-01, 2024-03-20, 2024-04-02], category: [数码, 食品, 服饰, 家电, 食品, 数码] }) print(df_orders.head()) # 输出每一列的数据类型 print(df_orders.dtypes)这段代码模拟了一个最小可用的订单表。head() 看数据长什么样dtypes 看每列类型。这里有个很容易被忽略的坑order_time 列在 pandas 里的类型通常是 object字符串必须转成 datetime 类型才能做时间差计算和月度聚合。# 字符串时间转 datetime统一时间格式 df_orders[order_time] pd.to_datetime(df_orders[order_time]) # 设定一个分析基准日一般取数据的最大日期或今天 reference_date pd.Timestamp(2024-12-31) print(df_orders.dtypes)pd.to_datetime 是专门处理时间字符串转时间对象的函数它会自动推断格式但遇到「2024/01/03」和「2024-01-03」混存的情况可能推断出错稳妥的做法是在函数里传 format 参数比如format%Y-%m-%d告诉 pandas 别猜了直接按这个格式解析速度能快不少。reference_date 是画像计算里的关键常量后面算 RFM 的 R最近一次消费距今天数全靠它这个日期选错了整个画像都会偏。2.3 数据探查用 info 和 describe 摸清数据底细拿到真实数据后第一件事永远是探查而不是急着写特征工程。Jupyter 相比脚本文件的好处在这里体现得很彻底——你可以一格一格地跑看到哪一步不对劲马上回头改不必整个脚本重来。# 探查数据基本情况 print(订单总数:, len(df_orders)) print(用户总数:, df_orders[user_id].nunique()) print(缺失值情况:) print(df_orders.isnull().sum()) print(订单金额描述统计:) print(df_orders[order_amount].describe())nunique() 是 pandas 里统计唯一值数量的方法这里用来算有多少个不同用户非常常用。isnull().sum() 统计每列缺失值数量如果 user_id 有缺失说明上游数据有问题要先处理。describe() 对数值列输出 count、mean、std、min、四分位数、max一眼看出订单金额有没有异常值——比如某笔订单金额是负的大概率是退款记录混进来了需要在特征工程前过滤掉。# 检查退款或异常订单金额为负或为零的记录要单独处理 df_orders df_orders[df_orders[order_amount] 0] print(过滤后订单数:, len(df_orders))这行代码用布尔索引过滤掉金额 0 的订单。为什么不直接删因为退款订单可能对画像有业务意义比如标记退款用户但第一版画像里为了口径干净先过滤掉。后面要加退款标签再通过 user_id 关联回原始数据。这是我在实际项目中反复用到的思路不要在一开始追求全量标签先把主链路跑通再往细里做。3. 用户画像的核心构建逻辑从 RFM 特征计算到标签体系映射3.1 RFM 模型的三个维度与阈值设定的两种思路用户画像如果不结合业务模型很容易做成字段搬运工——把原表字段改个名就成了所谓画像业务方根本不买账。在电商和交易类场景里RFM 模型是经过验证的、可以快速产出业务价值的特征框架。R 是 Recency最近一次消费距离今天的天数衡量用户活跃度F 是 Frequency统计周期内消费次数衡量用户粘性M 是 Monetary统计周期内消费总金额衡量用户价值。RFM 的经典之处在于它把用户价值拆成了三个可计算的维度而不是停留在「高价值用户」「低价值用户」这种模糊概念上。但 R、F、M 怎么设定阈值不同项目差异很大。常见做法有两种一种是按业务经验固定阈值比如 R 30 天算活跃另一种是按数据分布切分比如按三分位数把每个维度分成高、中、低三档。我的习惯是先用第二种让数据自己说话未来版本再用业务经验做修正。# 以用户 ID 分组计算 RFM 三个维度的值 import numpy as np # 每个用户最近一次消费时间、消费次数、消费总金额 rfm df_orders.groupby(user_id).agg( recency(order_time, lambda x: (reference_date - x.max()).days), frequency(order_time, count), monetary(order_amount, sum) ).reset_index() print(rfm.head())这里的关键是 agg 方法配合命名聚合。groupby(user_id) 把数据按用户分组agg 接收一个字典key 是新列名value 是元组原列名, 聚合函数。recency 那一列用了 lambda 函数x.max() 取该用户最近的订单时间reference_date 减它再取 .days得到的天数就是最近一次消费距今多久。frequency 直接用 count统计订单行数。monetary 用 sum把该用户所有订单金额加起来。这三行代码就是 RFM 计算的核心七八个用户的数据能跑百万用户也能跑只是后者需要点时间。# 查看 R、F、M 的分布辅助阈值设定 print(rfm[[recency, frequency, monetary]].describe())describe 输出的四分位数非常有用。假设 frequency 的 75% 分位数是 8说明 75% 的用户消费次数不超过 8 次monetary 的 50% 分位数是 560意味着有一半用户消费总额低于 560 元。这些数字是后面定阈值的直接依据不要跳过。3.2 特征离散化pd.cut 与 pd.qcut 的差异有了 R、F、M 三个连续值之后下一步是把它们变成「高/中/低」这样的等级标签。这一步在用户画像里叫离散化或分箱。pandas 提供了两个函数pd.cut 按等距切分pd.qcut 按分位数切分。等距切分就是 0-30 天、30-60 天、60 天以上这样固定间隔分位数切分则是保证每个箱子里用户数量差不多。在用户画像场景里我强烈建议用 qcut因为用户行为数据通常极度偏态——少数用户贡献大部分消费等距切分会把绝大多数用户压到同一个箱子里区分度极差。# 用 qcut 按分位数切分把 RFM 变成 1-3 的等级 rfm[R_score] pd.qcut(rfm[recency], 3, labels[3, 2, 1]) rfm[F_score] pd.qcut(rfm[frequency].rank(methodfirst), 3, labels[1, 2, 3]) rfm[M_score] pd.qcut(rfm[monetary].rank(methodfirst), 3, labels[1, 2, 3]) print(rfm.head())这里有几个细节必须说明。第一R 的 labels 是 [3, 2, 1]因为 R 值越小代表越活跃所以最近消费的用户应该得最高分 3 分。F 和 M 是越大越好所以 labels 是 [1, 2, 3]。第二frequency 和 monetary 可能出现大量相同的值比如很多用户只消费过一次金额都是 99直接 qcut 会报错「Bin edges must be unique」。解决方法是先对列做 rank(methodfirst)给相同值赋予不同排名消除重复然后再切分。这是实际项目里最常踩的坑没有之一。# 如果 qcut 报错重复值用这个方法兜底 try: rfm[R_score] pd.qcut(rfm[recency], 3, labels[3, 2, 1]) except ValueError as e: print(qcut 报错:, e)try-except 不是常态逻辑但在数据探查阶段用来自检是合理的。如果看到 Bin edges must be unique 的错误就说明这一列有太多重复值需要先 rank 或者改用 pd.cut。qcut 是「保证数量平均」的切分思路cut 是「保证区间宽度一致」的思路。用户画像里 F 和 M 用 qcutR 可以用 cut 也可以用 qcut看你对「活跃」的定义是绝对天数还是相对分布。3.3 标签体系设计把分数组合翻译成人话RFM 三个分数都是 1-3 分组合起来一共有 27 种可能。如果每种组合都定义一个标签业务方记不住运营也没法用。实际项目中一般会把 27 种组合映射到更少的业务标签。最常见的映射逻辑分两类第一类是 VIP 分层比如总分 7-9 分是「高价值用户」4-6 分是「成长用户」1-3 分是「沉默用户」第二类是分维度标签比如 R1 打「流失风险」标签F3 打「高频用户」标签。# 计算 RFM 总分用于 VIP 分层 rfm[total_score] rfm[R_score].astype(int) rfm[F_score].astype(int) rfm[M_score].astype(int) # 定义分层函数 def rfm_level(score): if score 8: return 高价值用户 elif score 5: return 成长用户 elif score 3: return 沉默用户 else: return 流失用户 rfm[user_level] rfm[total_score].apply(rfm_level) print(rfm.head())astype(int) 是必要的因为 qcut 出来的 score 列类型是 category分类类型直接相加会报错先转成整数。rfm_level 函数接收总分按阈值返回对应的业务标签。apply 是 pandas 里的逐行应用函数的方法把 rfm_level 作用到 total_score 每一行上。阈值 8 和 5 是经验值也可以按 27 种组合的实际业务解释来定比如 R 和 M 都高但 F 低的人可能是低频大额用户应该单独拉出来打「重要价值用户」标签而不是简单塞进成长用户里。这就是标签体系设计的基本思路先有框架再针对特殊组合做细分。4. 用户画像的工程化落地代码结构分层、可视化与报告导出4.1 按单元格拆分的模块化代码组织方式Jupyter Notebook 最大的坑是写着写着所有代码堆在一个格子里数据量小的时候看不出问题数据量一大、逻辑一复杂改一个变量就要重跑整个单元格时间成本完全不可控。我做用户画像项目时会刻意按「数据加载 → 数据清洗 → 特征计算 → 标签映射 → 结果导出 → 可视化」六个逻辑段拆成六个单元格每个单元格只做一件事前一个单元格的输出是后一个的输入。这个习惯的回报在你调试的时候才会体现。假设你计算完 RFM 之后发现某个用户的 frequency 算错了你不必重跑数据加载和清洗只需要改特征计算那个格子Jupyter 会保留前面单元格的运行状态直接往下执行就行。这就是 Notebook 相对传统脚本的核心优势它有内存状态不是每次从头跑。# 第 1 格参数配置区集中管理所有可调参数 CONFIG { data_path: ./data/orders.csv, reference_date: 2024-12-31, r_threshold: 30, # R 维度活跃天数阈值 f_threshold: 5, # F 维度高频阈值 m_threshold: 1000, # M 维度高金额阈值 output_dir: ./output/ }把所有可调参数集中到第一个格子里是我强烈推荐的做法。你想想如果阈值散落在十几个格子里每次调参需要上下翻找改错一格还不好定位。集中管理之后调参就是改一个字典重跑一次方便得不是一点半点。后面要说的自动化批处理也是建立在这个结构之上的。这就是从「自己写着玩」到「别人能接手维护」的分水岭。# 第 2 格数据读取统一入口 import pandas as pd df_orders pd.read_csv( CONFIG[data_path], parse_dates[order_time], # 加载时直接解析时间列 dtype{user_id: str} # 用户 ID 按字符串读取防止精度丢失 ) print(数据加载完成共, len(df_orders), 条记录)read_csv 的两个参数值得特别讲。parse_dates 可以在加载时直接把指定列转成 datetime 类型省得后面再调一次 pd.to_datetime对大文件来说加载速度更快。dtype 指定列的类型——user_id 用 str 而不是默认的 int64是因为很多用户 ID 是超过 15 位的数字用 int64 读取会出现科学计数法或者精度丢失这个坑非常隐蔽。一旦 user_id 变了后面所有按用户聚合的结果全部作废而且你还很难察觉。4.2 利用 pandas 透视表输出画像明细表RFM 算完之后得到的是每个用户一行、包含 R/F/M 分数和 user_level 的明细表。这个表就是用户画像的核心资产后续无论做运营筛选、个性化推荐还是用户分层分析都是基于这张表来做。实际项目中需要把它输出成不同格式给不同角色用运营要看带标签明细的 Excel管理层要看汇总统计数据团队要的是可以直接入库的 CSV。# 导出画像明细表到 Excel每个 sheet 放一类内容 with pd.ExcelWriter(CONFIG[output_dir] user_persona.xlsx, engineopenpyxl) as writer: rfm.to_excel(writer, sheet_name用户RFM明细, indexFalse) # 各用户层的人数分布 level_dist rfm[user_level].value_counts().reset_index() level_dist.columns [用户层级, 人数] level_dist.to_excel(writer, sheet_name层级分布, indexFalse) print(导出完成文件保存在:, CONFIG[output_dir] user_persona.xlsx)pd.ExcelWriter 是 pandas 写多 sheet Excel 的入口engine 指定 openpyxl。with 语句确保写入完成后文件正常关闭不用手动 save。rfm.to_excel 的第一个参数是写入器对象sheet_name 指定工作表名indexFalse 不把 DataFrame 的索引写入 Excel。value_counts() 统计每个 user_level 的人数是用户画像报告里最常用的一行代码。这个 Excel 导出方案适合几百 MB 以内的数据量再大的数据量建议直接写 CSV 到数据库。# 同时导出 CSV方便数据团队入库 rfm.to_csv(CONFIG[output_dir] user_persona.csv, indexFalse, encodingutf-8-sig)encodingutf-8-sig 是关键参数。直接用默认的 utf-8 导出 CSV再用 Excel 打开会乱码因为 Excel 默认按 GBK 解码。utf-8-sig 会在文件开头写入 BOM 头Excel 就能正确识别编码了。这是我第一次做用户画像导出时踩过的坑当时交付给运营的 CSV 全是乱码非常尴尬。4.3 画像可视化的三种图表层分布、特征占比、Top 用户统计可视化在用户画像里不是用来炫技的而是用来「一眼看懂」数据结构和验证标签合理性的。常见的可视化有三种场景第一用户层级分布饼图或柱状图回答「我们用户结构健不健康」第二高价值用户在城市、年龄段上的分布回答「高价值用户长什么样」第三RFM 三维散点图看用户聚集情况。import matplotlib.pyplot as plt # 设置中文字体避免标签乱码 plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False # 用户层级分布柱状图 level_counts rfm[user_level].value_counts() fig, ax plt.subplots(figsize(8, 5)) level_counts.plot(kindbar, color[#4C72B0, #55A868, #C44E52, #8172B2], axax) ax.set_title(用户层级分布) ax.set_xlabel(用户层级) ax.set_ylabel(人数) plt.tight_layout() plt.show()plt.rcParams 是 matplotlib 的全局配置项font.sans-serif 设置中文字体列表从左到右按顺序读取当前系统有什么字体就用哪个。不设的话图里的中文会显示成方块。subplots 创建画布和坐标轴figsize 是宽高单位英寸。plot(kindbar) 在 pandas 里直接画柱状图color 参数传颜色列表数量和柱子数量对应。tight_layout 自动调整子图参数让标题和坐标轴标签不被截断。show() 在 Jupyter 里会把图内嵌渲染在单元格下方这是 Notebook 体验好的一个直观体现——图和代码在一起不用来回切窗口。# 高价值用户与沉默用户在消费金额上的对比箱线图 import matplotlib.pyplot as plt high_value rfm[rfm[user_level] 高价值用户][monetary] silent_user rfm[rfm[user_level] 沉默用户][monetary] fig, ax plt.subplots(figsize(8, 5)) data_plot [high_value, silent_user] ax.boxplot(data_plot, labels[高价值用户, 沉默用户]) ax.set_title(高价值 vs 沉默用户消费金额分布对比) ax.set_ylabel(累计消费金额元) plt.show()boxplot 是箱线图能同时看数据的分布形态。中位数、四分位距、异常值一目了然。如果你发现高价值用户和沉默用户的箱体高度重合说明 RFM 的 M 维度阈值设定有问题或者高价值用户的定义过于依赖 R 和 F 而 M 区分度不够回去检查 3.1 里的聚合逻辑而不是继续往深处做。可视化在这里是验证手段不只是汇报工具。这就是把数据和业务连接起来的过程折腾几个来回你对用户结构的理解会深很多。5. 用户画像构建的避坑指南六个典型错误与排查路径5.1 时间字段类型错误导致 Recency 计算全乱现象算出来的 recency 有负数或者所有用户都是同一个值明显不合理。原因最常见的情况是 order_time 列没有正确转成 datetime 类型导致 reference_date - x.max() 得到的是字符串减字符串抛异常或产生 NaN。另一种情况是时间字段里混了不同格式比如「2024/1/3」和「2024-01-03」共存pd.to_datetime 默认推断可能把前者当成 1 月 3 日后者当成 1 月 3 日看起来一样但类型不同。解决在读取数据时就指定 parse_dates[order_time]或者读取后统一用 pd.to_datetime 加 format 参数强制格式。我一般在数据清洗单元格第一行就打印 df.dtypes 检查时间列类型确认是 datetime64 再继续。如果 format 不确定先跑 df[order_time].astype(str).str[:10] 看字符串统一截到日期部分再转换能避免很多时间精度带来的奇怪问题。5.2 用户 ID 精度丢失导致画像张冠李戴现象画像结果里用户数比原始数据里少或者两个不同用户被合并成一个。原因user_id 在 CSV 里是 18 位数字pandas 默认按 int64 读取超出 15 位有效数字时精度丢失最后几位变成 0。两个不同 ID 在丢失精度后可能变得相同groupby 就把他们算成同一个人了。这种错误极其隐蔽——不报错、看起来一切正常但结果全错。解决load 数据时显式传 dtype{user_id: str}强制按字符串读。已经读错了再补救的话用 df[user_id] df[user_id].astype(str) 也救不回来因为精度已经丢了。所以必须在入口处防住。这个坑我帮别人排查过整整一下午最后发现是这么低级的原因从那以后我对 ID 列一律按字符串处理。5.3 qcut 掉进唯一值陷阱现象执行 pd.qcut 时抛 ValueError提示 Bin edges must be unique。原因qcut 要求分位点对应的值必须唯一。如果用户购买次数大量集中在 1 次那么 33% 和 66% 的分位点都落在 1 上分箱边界不唯一函数直接拒绝工作。解决先对列做 rank(methodfirst) 破坏重复值代码在 3.2 已经给过。但要记住 rank 之后的分数不再严格代表原始数值大小关系它代表的是排序位置。对 F 和 M 这种偏态分布来说排序位置比绝对数值更有业务意义——第一名和第 100 名的购买次数都是 5 次但在排序上确实有先后。如果业务上必须按绝对数值分箱改用 pd.cut 并自行指定 bins 边界。5.4 Jupyter 内核状态混乱导致代码改了没生效现象改了单元格里的代码重新运行结果还是老样子或者某个变量删了再定义旧值还在影响计算。原因Jupyter 的内存状态是累积的。你运行了定义 user_level 的单元格后来删掉那段代码再运行后续单元格user_level 在内存里仍然存在因为只要内核不重启所有曾经执行过的变量都还在。另一个常见场景是之前定义过 df_orders后来重新赋值但没有成功覆盖新代码用的还是旧数据。解决在 Notebook 最开始加一个「重置环境」格运行%reset -f清空所有变量或者手动重启内核然后 Run All。我的习惯是每次大规模改代码前先 Kernel → Restart Run All保证所有变量都是按当前代码从头算出来的而不是依赖旧内存状态。这也是 Jupyter 项目「源码可复现」的核心要求——如果换个机器重跑没法得到同样结果这个代码就没有交付价值。5.5 matplotlib 中文乱码现象图表里的中文标题和标签显示为方块或者乱码。原因matplotlib 默认字体不支持中文中文字体又因系统而异Windows 是 SimHeimacOS 是 PingFang SCLinux 服务器上可能根本没有中文字体。解决在代码开头用 plt.rcParams 配置字体列表像 4.3 里那样写三个备选。如果服务器上确实没中文字体最省事的方案是通过 pip 安装中文字体包或者在系统里放置一个 ttf 字体文件并指定路径。这个坑不影响数据正确性但影响交付体验——运营看到乱码图表的第一反应就是你做的画像不靠谱技术信任感瞬间崩塌。5.6 Excel 导出失败或打开报错现象to_excel 执行时报 ModuleNotFoundError或者文件生成后 Excel 打不开、提示文件损坏。原因pandas 写 Excel 需要额外安装 openpyxl没有的话报 ModuleNotFoundError。打开报错的原因大多是文件被占用或者写入过程中途出错导致没有正常关闭。解决确保环境里装了 openpyxl通过 pip install openpyxl。文件占用问题的解决方式是把输出路径的文件先删掉再写或者写文件名里带时间戳避免重名。另外 with 语句确保文件正常关闭这是最稳妥的写法。6. 让用户画像跑出复利从手动脚本到自动化更新与参数化配置做到这一步你已经有了完整的画像构建源码、一套踩过的坑的经验、以及能产出报表的 Notebook。但每次都要手动打开 Jupyter、逐格运行、再手动分发结果这件事做一次新鲜做十次就烦了。进阶的方向是让画像跑批自动化这也是从「自己用」到「团队用」的关键一跃。常见的做法是用 nbconvert 或 Papermill 把 Notebook 变成可批量执行的流水线。nbconvert 可以把 .ipynb 转成 .py 脚本加上 --execute 参数可以无界面执行整个 Notebook输出结果文件。Papermill 更进阶一点——它是一个参数化执行 Notebook 的工具允许你为同一个 Notebook 传入不同的参数文件比如不同月份的参考日期、不同的数据源路径而不必修改代码。# 用 Papermill 批量执行 Notebook支持传参 import papermill as pm pm.execute_notebook( input_pathuser_persona.ipynb, output_pathuser_persona_executed.ipynb, parameters{ CONFIG: { data_path: ./data/orders_2024Q4.csv, reference_date: 2024-12-31, output_dir: ./output/2024Q4/ } } )这里有个关键设计你的 Notebook 第一个单元格必须是一个包含所有可调参数的 CONFIG 字典Papermill 会用 parameters 传进来的值覆盖对应变量然后按顺序执行其他所有单元格。这意味着你的画像代码一次写好之后每个月的跑批只需改数据路径和参考日期两个参数不用打开 Notebook 动任何逻辑代码。这是个很大的提升——既保留了 Notebook 的交互性和可视化能力又具备了脚本的可重复性和自动化能力。更彻底的做法是脱离 Notebook把画像逻辑抽成纯 Python 模块。数据加载、清洗、RFM 计算、标签映射、导出分别写成函数然后用一个 main 函数串联。这个时候 Notebook 只留作探索和分析的入口生产跑批完全走脚本。长期来看这是最健康的架构但没必要第一步就这么干——等画像标签真的稳定了、业务方天天在用了再考虑迁移不迟。自动化的调度可以简单到用 Linux 的 cron 定时任务每天凌晨两点跑一次输出当天更新的画像表。Cron 配置很简单一行表达式加一条命令0 2 * * * cd /path/to/project papermill user_persona.ipynb output.ipynb。这样业务方每天上班看到的就是昨天的画像数据而不是等你去跑脚本。这一步做完用户画像从被动响应变成了主动服务价值感完全不同。最后说一个我从这套方案里悟到的习惯。用户画像不是一次性的项目而是需要持续维护的数据资产——业务在变、用户在变、标签口径也在变。所以我现在的做法是每次跑完画像都会顺手记录一下当次的参数配置和结果摘要存成一个 JSON 文件放在 output 目录旁边。下次有人问「这个标签的口径是什么」不用翻代码直接看配置记录就行。这个习惯救了我很多次也让你在接手别人项目或者别人接手你项目的时候少很多痛苦。希望这些经验对你正在做的画像项目有帮助。本文还有配套的精品资源点击获取
返回列表