理论只有付诸实践才能真正掌握。本节我们将通过代码示例,一步步完成一个机器学习任务:根据历史职位信息预测职位投递量。这也是用户提供的脚本所要解决的问题。在动手之前,我们先明确问题和数据:
- 背景:假设我们有一份包含历史职位发布信息的数据集,每条记录包括职位的各种属性(如职位类型、薪资范围、工作经验要求、公司规模等)以及该职位在一段时间内收到的求职者投递数量(投递量)。我们的目标是训练一个模型,输入职位的属性,输出预测的投递量。这可以帮助招聘网站或HR预估某职位的受欢迎程度,以便做推荐或调整策略。
- 问题类型:投递量是一个非负整数,属于回归问题(输出连续值)。不过投递量的分布通常是右偏的(大部分职位投递量较低,少数特别热门职位投递量很高),有时我们会对投递量取对数或进行分类分段,但这里我们直接回归原值。
- 评价指标:可以使用MAE、MSE、RMSE、R²、MAPE等多种指标来衡量预测误差和效果。业务上可能关心平均误差和整体趋势,所以MAE(平均绝对误差)和MAPE(平均绝对百分比误差)比较直观:MAE表示预测值与真实值差值的平均绝对值,MAPE表示平均相对误差的百分比。我们后续也会计算这些指标来评估模型。
数据说明:由于此处我们不直接使用用户的私有数据集,我们选用一个公开的类似结构的数据或模拟数据进行演示。例如,为了方便,我们使用scikit-learn自带的回归数据集(如波士顿房价、糖尿病数据集等)或者构造一个模拟的数据集来走流程。这样可以重点关注代码和方法本身。
下面,我们将演示整个过程,包括:数据准备与预处理、模型训练与调参、模型评估。请注意,代码中的注释会用中文解释每一步骤的作用。
案例一步:数据加载与预处理
首先,我们需要获取数据。这里我们以加州房价数据集为例进行演练(虽然这是房价预测,但过程与投递量预测类似)。加州房价房价数据是一个经典的小型数据集,每条记录有8个特征(如房间数、人均收入、是否靠近河流等),标签是房屋的中位价格。我们用它来模拟投递量预测的过程。
import pandas as pd
from sklearn.datasets import fetch_california_housing
# 加载数据集(加州房价),并构建DataFrame
california = fetch_california_housing()
X = pd.DataFrame(california.data, columns=california.feature_names)
y = pd.Series(california.target, name='MedHouseVal') # MedHouseVal: 住宅中位价,十万美元
print("特征列:", X.columns.tolist())
print("样本数:", X.shape[0])
print(X.head(3)) # 查看前3条样本特征
print(y.head(3)) # 对应的标签(房价)
提示: 在实际的职位数据中,我们可能需要用pd.read_csv()
读取CSV文件到DataFrame。本例中直接使用sklearn的数据集。输出会展示特征列名和样本的一些值,帮助我们了解数据格式。
接下来,我们进行简单的数据预处理。一般包括:处理缺失值、类别编码、特征缩放等。对于加州房价数据,特征都是数值型且没有缺失,故预处理主要是特征缩放。对数值特征做标准化(减均值除标准差)可以加速模型训练收敛、让模型对不同尺度特征更公平。对于树模型来说不敏感缩放,可以跳过标准化;但对神经网络或线性模型,标准化很重要。我们这里演示缩放,以便稍后也能方便地尝试神经网络。
[!note]
为什么需要特征缩放?想象一个购物场景
假设你是一个购物助手,要帮顾客评估商品的整体价值。你看到两个信息:
- 商品重量(以克为单位):500克
- 商品价格(以元为单位):50元
如果直接比较这两个数字,你会觉得重量(500)比价格(50)更重要,因为500比50大得多!但这显然是不对的,我们不能直接比较克和元,它们的单位和尺度完全不同。
标准化:把所有东西变成"相对大小"
想象你是小学老师,要评价学生的身高和体重:
- 小明:身高150厘米,体重45公斤
- 班级平均:身高140厘米,体重35公斤
- 标准差:身高10厘米,体重5公斤
与其说"小明比平均高10厘米,重10公斤",不如说:
- 身高:比平均高1个标准差 (150-140)/10 = 1
- 体重:比平均重2个标准差 (45-35)/5 = 2
这样就能公平比较了!我们把所有测量都变成了"离平均值多少个标准差",这就是标准化。
为什么机器学习需要标准化?
想象你是跑步教练,要根据两个指标预测运动员能否完成马拉松:
- 每天训练时间(小时):1-3之间
- 心率(次/分):60-180之间
如果不做标准化,模型会认为心率比训练时间重要得多,因为心率的数值范围大得多!这就像用米和毫米测量距离,同样是1,但意义完全不同。
什么时候需要标准化?
想象三种不同的裁判:
树形裁判(决策树):
- "如果身高超过170,给高分"
- "如果体重超过60,给低分"
- 他只关心"大于小于",不在意具体数值,所以不需要标准化
距离裁判(如K近邻、神经网络):
- 需要计算"这个运动员和那个运动员有多像"
- 如果不标准化,身高差1米和体重差1克会被当作一样重要!
- 所以必须标准化
线性裁判(如线性回归、逻辑回归):
- 要给每个指标分配权重
- 如果不标准化,大尺度特征会"抢走"所有重要性
- 所以必须标准化
实际操作很简单
就像把所有成绩都换算成百分制:
- 减去平均分(让数据居中)
- 除以标准差(让波动范围相似)
这样所有特征都变成了"标准分数",可以公平竞争了!
记住:标准化不改变数据的本质关系,只是让不同尺度的特征可以公平比较,就像把所有人的身高都换算成"高于平均多少",而不是有的用米有的用尺。
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 将数据集拆分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print("训练集样本数:", X_train.shape[0], "测试集样本数:", X_test.shape[0])
# 数值特征标准化:对训练集拟合Scaler并应用于训练集和测试集
# 这里的关键点是:
# 1. 我们只在训练集上"学习"如何标准化( fit_transform )
# 2. 然后用相同的标准化参数处理测试集( transform )
# 这就像考试时,你不能根据考试题来调整学习方法,必须用之前学到的知识去应对。如果用测试集来"fit",就相当于提前看到了考试题,这会导致"数据泄露",使模型评估结果不可靠。
# 标准化的好处是让不同量级的特征在模型训练中获得公平对待,特别是对于像线性回归、SVM这样对特征尺度敏感的算法非常重要。
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 注意:对训练集用fit_transform
X_test_scaled = scaler.transform(X_test) # 对测试集用transform(使用在训练集fit的参数)
[!note]
数据集拆分
想象你有一本习题集,如果你把所有习题都用来练习,然后考试时遇到完全相同的题目,你肯定会做得很好。但这并不能真正测试你的学习能力,对吧?
数据集拆分就是这个道理:
- 训练集 就像是你用来学习的习题,模型通过这些数据学习规律
- 测试集 就像是考试题,用来检验模型是否真正学会了,而不是简单记忆
特征标准化
想象你和朋友比赛,但是用不同的计分标准:你的分数范围是0-10分,而朋友的是0-100分。直接比较这些分数是不公平的,对吧?
特征标准化就是把所有特征转换到相同的尺度上:
- 比如房价数据中,"收入"可能是几万美元,而"房间数"可能只有个位数
- 标准化后,所有特征都被转换为均值为0、标准差为1的分布
通过train_test_split
按80/20划分数据,这里设置random_state=42
保证可重复。然后用StandardScaler
对特征进行标准化处理。对于真实职位数据,如果有类别型特征,需要在建模前将其编码:可以用One-Hot编码(每个类别转为一个独热向量列)或Target编码(类别替换为平均目标值)等方法。用户提供的代码中对类别型特征就做了这两种编码,我们稍后详细解释。当前例子中无需类别编码,因为数据中没有类别特征。
[!note]
类别特征编码:把文字变成数字的艺术
想象你是一个房产中介,需要把房子的信息输入电脑做分析。其中有个特征是"房子朝向":
- 南北通透
- 朝南
- 朝北
- 朝东
但计算机只懂数字,不懂文字,我们该怎么办呢?这就需要"编码"了!
方法一:One-Hot编码(独热编码)
想象你在填写调查问卷,遇到这样的问题:
"你的房子朝向是?(请在对应选项打√)"
- [ ] 南北通透
- [ ] 朝南
- [ ] 朝北
- [ ] 朝东
One-Hot编码就是这个意思:
- 南北通透 → [1, 0, 0, 0]
- 朝南 → [0, 1, 0, 0]
- 朝北 → [0, 0, 1, 0]
- 朝东 → [0, 0, 0, 1]
就像投票表决一样,每个选项只能选一个,其他都是0。这样的好处是:
- 不同类别之间没有大小关系
- 模型可以分别学习每个类别的影响
- 特别适合决策树这样的模型
缺点是:如果类别太多(比如中国的城市),会产生很多列,增加计算量。
方法二:Target编码(目标编码)
想象你是房产经纪人,发现:
- 南北通透的房子平均售价是100万
- 朝南的房子平均售价是80万
- 朝北的房子平均售价是60万
- 朝东的房子平均售价是70万
Target编码就是用这个平均值来替代类别:
- 南北通透 → 100
- 朝南 → 80
- 朝北 → 60
- 朝东 → 70
这就像是用"实际效果"来代替类别,好处是:
- 不会产生太多新列
- 包含了类别与目标之间的关系
- 特别适合线性模型
但要小心:
- 可能带来数据泄露(使用了未来的信息)
- 需要处理新出现的类别
- 可能过度拟合
实际应用举例
假设我们预测职位的投递量:
职位类型是类别特征:
- Java工程师
- 产品经理
- 销售代表
- 人力资源
One-Hot编码就像是给每个职位一个专属档案:
Java工程师 → [1, 0, 0, 0] # 第一个位置是1 产品经理 → [0, 1, 0, 0] # 第二个位置是1 销售代表 → [0, 0, 1, 0] # 第三个位置是1 人力资源 → [0, 0, 0, 1] # 第四个位置是1
Target编码则看历史数据中各职位的平均投递量:
Java工程师 → 50 # 平均收到50份简历 产品经理 → 40 # 平均收到40份简历 销售代表 → 30 # 平均收到30份简历 人力资源 → 20 # 平均收到20份简历
就像把每个职位类型都转换成了一个"受欢迎度分数"。
小结
类别编码就像是翻译官:
- One-Hot编码是"逐字翻译":保留所有信息,但可能很啰嗦
- Target编码是"意译":简洁但可能损失一些细节
选择哪种方法要看具体情况:
- 类别少,要保留所有信息 → One-Hot编码
- 类别多,关注整体趋势 → Target编码
- 有时两种方法都试试,看哪个效果好
记住:编码不是目的,而是让模型能更好地理解和学习数据的手段!
案例二步:模型训练与调参
现在我们准备训练模型。我们可以先从一个相对简单的模型开始,如随机森林回归,然后观察效果再做改进。随后也可以尝试XGBoost和神经网络来比较。
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
# 初始化随机森林回归模型
# 训练过程就像是这100位专家各自学习如何根据房屋特征预测价格。预测时,每位专家给出自己的估计,然后取平均值作为最终预测。
rf = RandomForestRegressor(n_estimators=100, random_state=42)
# 在训练集上训练模型
rf.fit(X_train_scaled, y_train)
# 在测试集上预测
y_pred = rf.predict(X_test_scaled)
# 计算评估指标
# 1. MAE (平均绝对误差) :预测值与实际值差的平均绝对值。比如,如果预测房价平均偏差5万美元,MAE就是0.5(因为数据集中是以10万美元为单位)。
# 2. RMSE (均方根误差) :先计算平方误差,再取均值,最后开方。它比MAE更重视大错误,因为平方会放大大的误差。
# 3. R² (决定系数) :衡量模型解释数据变异性的比例,范围通常在0到1之间:
# - R²=1:完美预测
# - R²=0:模型不比简单地预测平均值好
# - R²越接近1,模型越好
# 这些指标就像是给专家团队打分:MAE和RMSE告诉你预测偏离真实值多远(越小越好),R²告诉你模型捕捉到了多少房价变化的规律(越接近1越好)。
mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = mse ** 0.5
r2 = r2_score(y_test, y_pred)
print(f"随机森林回归模型 - 测试集评估: MAE={mae:.2f}, RMSE={rmse:.2f}, R^2={r2:.2f}")
在以上代码中,我们训练了一个包含100棵树的随机森林。然后对测试集进行了预测,并计算了MAE、RMSE和R²指标。
指标解释:
- MAE(Mean Absolute Error)表示平均绝对误差,其单位与原始目标相同。MAE=5 表示预测值与真实值平均相差5个单位。在投递量场景下,如果MAE=5,意味着平均每个职位的预测投递量与实际投递量相差5次投递。
- MSE(Mean Squared Error)是均方误差,由于平方项,对较大误差更敏感。RMSE是均方误差的平方根,恢复了原始单位,便于直观解释。MSE/RMSE对于评价模型更苛刻,因为大的错误会被平方放大。RMSE一般 >= MAE,二者接近时说明误差比较稳定,没有大离谱误差;RMSE远大于MAE时说明存在少数较大误差。
- R²(判定系数)表示模型对目标变量方差的解释比例,取值范围0到1。R²=1表示完美预测,R²=0表示模型的预测跟用均值猜测一样差。例如R²=0.8表示模型解释了80%的方差。R²有时也会出现负值(表示模型甚至不如基准均值模型)。
除了上述指标,有时还会用MAPE(Mean Absolute Percentage Error),即平均百分比误差,它告诉我们预测值与真实值相差多少百分比。例如MAPE=10%表示预测值平均偏离真实值10%。但MAPE要注意实际值为0时无法计算,或值很小时会放大百分比,所以要小心使用。用户代码中也计算了MAPE,我们稍后解释。
[!note]
评分指标:如何判断模型预测得好不好?
想象你是一个射箭教练,要评价学员的射箭水平。你会关注什么?
1. MAE(平均绝对误差)- "平均差多远"
想象你在教射箭:
- 小明射了10箭,每箭距靶心的距离是:3厘米、2厘米、4厘米…
- MAE就是把这些距离加起来除以10
- 如果MAE=5厘米,就是说平均每箭偏离靶心5厘米
在预测投递量时:
- MAE=5意味着平均每个职位的预测值与实际值相差5份简历
- 简单直观,容易理解
- 就像测量"平均每次射箭偏离多远"
2. MSE/RMSE(均方误差/均方根误差)- "特别讨厌大错误"
继续射箭的例子:
- 小明:10箭都偏离5厘米
- 小红:9箭都偏离1厘米,但有1箭偏离20厘米
用MAE来看:
- 小明:平均偏离5厘米
- 小红:平均偏离约3厘米((9×1 + 1×20)/10)
- 看起来小红更好?
但用MSE(先平方再平均):
- 小明:5² = 25(每箭都一样)
- 小红:(9×1² + 1×20²)/10 = 40(那一箭严重失误被放大了!)
RMSE就是把MSE开平方,变回原来的单位。
这就像:
- MAE是"一视同仁"的老师,关注平均表现
- MSE/RMSE是"特别讨厌差生"的老师,一次大错误比多次小错误更糟糕
3. R²(判定系数)- "比全班平均水平好多少"
想象班级考试:
- 全班平均60分,标准答案是100分
- 小明考了80分
- 那么R²就是看小明比"猜平均分"强多少
具体来说:
- R²=0:跟猜平均分一样准
- R²=1:完全预测对
- R²=0.8:比猜平均分好80%
- R²为负:还不如猜平均分
就像:
- R²=0:闭着眼射箭
- R²=1:百发百中
- R²=0.8:比随机射箭准很多,但还不完美
4. MAPE(平均绝对百分比误差)- "差多少比例"
想象预测股票价格:
- 预测100元,实际90元:差10%
- 预测10元,实际9元:也差10%
MAPE看相对误差:
- 不在乎绝对数值大小
- 关注偏差占实际值的比例
- MAPE=10%意味着平均预测偏差是实际值的10%
但要小心:
- 实际值接近0时,百分比会变得很大
- 实际值是0时,无法计算百分比
- 就像用"偏离靶心的距离÷靶心到边缘的距离",当靶心在边缘时这个比例会变得很奇怪
总结
这些指标就像不同的裁判:
- MAE是公平裁判:每个错误一视同仁
- RMSE是严格裁判:特别惩罚大错误
- R²是相对裁判:跟基准比较
- MAPE是比例裁判:看相对差距
选择哪个指标?要看你更关心什么:
- 在意平均表现 → 用MAE
- 不能容忍大错误 → 用RMSE
- 要和基准比较 → 用R²
- 关注相对误差 → 用MAPE
就像射箭比赛可能同时需要多个裁判,实际应用中我们常常也会看多个指标,全面评估模型性能。
当我们运行上面的代码时,会得到随机森林的性能输出。假设输出为:随机森林回归模型 - 测试集评估: MAE=2.45, RMSE=3.35, R^2=0.85
这仅是示例数据的结果,意思是平均误差2.45千美元,R²=0.85表明模型解释了85%的房价波动,这对于房价预测来说相当不错。在投递量预测中,我们希望误差尽量小,R²越接近1越好。当然,实际业务中投递量的可预测性可能没有房价这么高,R²能达到0.6-0.7就已经很有价值了,需要具体分析领域。
案例三步:保存模型与加载预测
训练好的模型往往需要持久化保存,以便在真实系统中随时调用进行预测,而无需每次都重新训练。Python提供了多种序列化模型的方式。用户代码中使用了joblib库,这是sklearn推荐用于保存大模型(尤其是包含numpy数组的对象)的工具,比pickle更高效。
例如,我们可以将上面训练的随机森林模型保存到文件,然后在任何需要预测的脚本里。
# 添加模型保存功能
import joblib
# 创建模型保存目录
import os
model_dir = '/Users/lubingyang/Desktop/python-demo/算法demo/models'
if not os.path.exists(model_dir):
os.makedirs(model_dir)
# 保存随机森林模型和标准化器
joblib.dump(best_model, f"{model_dir}/rf_model.joblib")
joblib.dump(scaler, f"{model_dir}/scaler.joblib")
print("模型已保存到", model_dir)
# 演示如何加载模型并进行预测
# 加载保存的模型和标准化器
loaded_rf = joblib.load(f"{model_dir}/rf_model.joblib")
loaded_scaler = joblib.load(f"{model_dir}/scaler.joblib")
# 模拟新数据(这里使用测试集的前3条记录作为示例)
new_data = X_test.iloc[:3]
# 使用加载的标准化器转换新数据
new_data_scaled = loaded_scaler.transform(new_data)
# 使用加载的模型进行预测
rf_predictions = loaded_rf.predict(new_data_scaled)
# 打印预测结果
print("\n预测示例(使用测试集前3条记录):")
print("实际值:", y_test.iloc[:3].values)
print("随机森林预测值:", rf_predictions)
即可载入模型并对新的特征数据进行预测。这样部署非常方便。对于同时需要保存数据预处理步骤(如StandardScaler)或者编码器的,也可以分别保存它们,然后加载后对新数据做相同转换。用户的代码中就保存了目标编码器和One-Hot编码器模型,以确保对新数据做一致的编码转换,这一点非常关键——训练和预测时的数据预处理步骤必须相同。
以上,我们通过一个模拟案例展示了从数据到模型再到预测的全流程。这为我们理解下一节的真实代码做了铺垫。
接下来我们将进入重点:详细解读用户提供的投递量预测脚本。这段代码综合运用了我们之前讨论的许多概念,包括数据清洗、类别编码、随机森林训练、模型评估和保存等。通过对代码逐步拆解讲解,您将看到前面讲的知识如何应用于实际项目,并学到一些额外的实战技巧和可能的优化方向。
投递量预测
import warnings
warnings.filterwarnings(action="ignore")
import os
import sys
import datetime
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import joblib
import matplotlib.pyplot as plt
from category_encoders import TargetEncoder
from sklearn.preprocessing import OneHotEncoder
class DeliveryPredictor:
"""
投递量预测器:就像一个预测快递包裹数量的专家系统
主要完成三个任务:
1. 数据预处理:清洗和转换原始数据,让机器能够理解
2. 模型训练:学习历史数据中的规律
3. 预测新数据:使用学到的规律预测新情况
"""
def __init__(self, target_col, number_cols, type_cols, onehot_cols, model_tag, model_path="/Users/lubingyang/Desktop/python-demo/算法demo/models/"):
"""
初始化预测器,就像准备一个工具箱:
- target_col: 要预测的目标(投递量)
- number_cols: 数值特征(如薪资、职位数等)
- type_cols: 类别特征(如城市、职位类型等)
- onehot_cols: 需要独热编码的特征(如是否加急等)
- model_tag: 模型标签,用于区分不同版本
- model_path: 模型存储路径
"""
self.target_col = target_col
self.number_cols = number_cols
self.type_cols = type_cols
self.onehot_cols = onehot_cols
self.model_tag = model_tag
self.model_path = model_path
# target_encoded_cols
self.target_encoded_cols = [f"{i}_encoded" for i in self.type_cols]
# model save path
if not os.path.exists(self.model_path):
os.makedirs(self.model_path)
# model path
self.target_encoder_path = self.model_path + f"target_encoder-{self.model_tag}.model"
self.onehot_encoder_path = self.model_path + f"onehot_encoder-{self.model_tag}.model"
def model_select(self, model_tag="rf"):
"""
选择预测模型,就像选择预测专家:
- rf: 随机森林,像是100个不同的专家集体决策
- n_estimators=100: 召集100位专家
- min_samples_split=20: 每个决策至少要基于20个样本
- n_jobs=6: 6个专家同时工作(并行计算)
"""
if model_tag == "rf":
# model
self.model = RandomForestRegressor(
n_estimators=100,
random_state=42,
min_samples_split=20,
criterion="squared_error", # 将 "mse" 改为 "squared_error"
n_jobs=6
)
# model name
self.main_model_path = self.model_path + f"apply_count_predict-{model_tag}-{self.model_tag}.model"
def data_preprocess(self, df, is_train=False):
"""
数据预处理流水线,包含:
1. 填充缺失值
2. 目标编码
3. 独热编码
4. 特征整合
"""
# 1. 填充缺失值
df[self.number_cols] = df[self.number_cols].fillna(0).astype(int)
df[self.type_cols] = df[self.type_cols].fillna(0).astype(int)
df[self.onehot_cols] = df[self.onehot_cols].fillna(0).astype(int)
# 2. 目标编码
if is_train:
self.target_encoder = TargetEncoder(cols=self.type_cols, smoothing=1.0)
encode_df = self.target_encoder.fit_transform(df[self.type_cols], df[self.target_col])
joblib.dump(self.target_encoder, self.target_encoder_path)
else:
encode_df = self.target_encoder.transform(df[self.type_cols])
df[self.target_encoded_cols] = encode_df
# 3. 独热编码
if is_train:
self.onehot_encoder = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
encoded_data = self.onehot_encoder.fit_transform(df[self.type_cols])
joblib.dump(self.onehot_encoder, self.onehot_encoder_path)
else:
encoded_data = self.onehot_encoder.transform(df[self.type_cols])
self.onehot_encoded_cols = self.onehot_encoder.get_feature_names_out(self.type_cols).tolist()
encoded_df = pd.DataFrame(encoded_data, columns=self.onehot_encoded_cols, index=df.index)
df = pd.concat([df, encoded_df], axis=1)
# 4. 特征整合
self.features = self.number_cols + self.target_encoded_cols + self.onehot_encoded_cols
print("features num:", len(self.features))
if is_train:
return df[self.features], df[self.target_col]
return df[self.features], None
def train(self, X, y, test_size=0.2):
"""模型训练和评估"""
# 初始化随机森林模型
self.model = RandomForestRegressor(
n_estimators=100,
random_state=42,
min_samples_split=20,
criterion="squared_error", # 更新为新版本参数
n_jobs=6
)
self.main_model_path = self.model_path + f"apply_count_predict-rf-{self.model_tag}.model"
# 划分数据集并训练
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, shuffle=True)
print(f"{datetime.datetime.now()} 开始训练模型...")
self.model.fit(X_train, y_train)
joblib.dump(self.model, self.main_model_path)
# 评估模型
predictions = self.model.predict(X_test)
self._evaluate(y_test, predictions)
return X_train, X_test, y_train, y_test
def load_models(self):
# 预测模型
self.model = joblib.load(self.main_model_path)
# 目标编码
self.target_encoder = joblib.load(self.target_encoder_path)
# onehot编码
self.onehot_encoder = joblib.load(self.onehot_encoder_path)
print("模型加载完成!")
def predict(self, X):
"""
使用训练好的模型进行预测
"""
return self.model.predict(X)
def _evaluate(self, y_true, y_pred):
"""
评估模型效果,就像给预测打分:
- MAE:平均预测偏差有多大
- RMSE:是否有预测严重偏离的情况
- R²:模型能解释多少变化
- MAPE:预测偏差的百分比
"""
print("y_true:", type(y_true), type(y_pred), y_true[:3], y_pred[:3])
epsilon = 1 # 避免除以 0
mape = np.mean(np.abs((y_true - y_pred) / np.maximum(y_true, epsilon))) * 100
print("Model Evaluation:")
print(f"MAE: {mean_absolute_error(y_true, y_pred):.2f}")
print(f"MSE: {mean_squared_error(y_true, y_pred):.2f}")
print(f"RMSE: {np.sqrt(mean_squared_error(y_true, y_pred)):.2f}")
print(f"R²: {r2_score(y_true, y_pred):.2f}")
print(f"MAPE: {mape:.2f}%") # 输出 MAPE 指标
# # 绘制预测结果
# plt.figure(figsize=(12, 6))
# plt.plot(y_true, label='Actual')
# plt.plot(y_pred, label='Predicted')
# plt.title("Delivery Volume Prediction")
# plt.xlabel("Samples")
# plt.ylabel("Deliveries")
# plt.legend()
# plt.show()
def features_importance(self, top_n=30, plot=True):
# 获取特征重要性
feature_importances = self.model.feature_importances_
# 打印特征重要性
feature_importance_df = pd.DataFrame({
'Feature': self.features,
'Importance': feature_importances
}).sort_values(by='Importance', ascending=False)
feature_importance_df.to_csv(self.model_path + f"feature_importance-{self.model_tag}.csv", index=False, encoding="utf-8")
feature_importance_df = feature_importance_df.iloc[:top_n]
if plot:
# 绘制平行柱状图
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.barh(feature_importance_df['Feature'], feature_importance_df['Importance'], color='skyblue')
# 设置标题和标签
plt.xlabel('Feature Importance')
plt.title('Feature Importance Ranking')
plt.show()
if __name__ == "__main__":
# 1. 定义特征
label = "apply_count" # 预测目标:投递量
number_cols = ['valid_days', 'numofposition', 'entwork_max_salary',
'entwork_min_salary', 'entwork_grades']
type_cols = ['month_n', 'work_location_city', 'work_location_prov',
'work_position', 'work_type', 'entwork_grades',
'entwork_posit', 'entwork_jobyear', 'ent_level',
'ent_type', 'ent_scope', 'jobtype']
onehot_cols = ["month_n", 'is_urgent', 'is_high_level_work',
'name_high_level_work', 'is_new_work', 'modify_tag']
# 2. 创建预测器
predictor = DeliveryPredictor(
target_col=label,
number_cols=number_cols,
type_cols=type_cols,
onehot_cols=onehot_cols,
model_tag="0213"
)
# 3. 训练模型
# 加载和预处理数据
df = pd.read_csv('/Users/lubingyang/Desktop/python-demo/算法demo/投递量预测简版数据-20250212.csv')
df = df[df["incline_days"] == 0] # 去除异常数据
print("数据量:", len(df))
# 训练模型
X, y = predictor.data_preprocess(df, is_train=True)
predictor.model_select()
predictor.train(X, y)
predictor.features_importance(plot=False)
# 4. 预测示例
print("\n预测示例 >>>>>>")
test_df = df.sample(n=100) # 随机抽取100条数据测试
predictor.load_models()
X_test, _ = predictor.data_preprocess(test_df, is_train=False)
predictions = predictor.predict(X_test)
print(f"预测结果示例:", predictions[:3].tolist())
print("end!", datetime.datetime.now())