@
目录一、算法和背景介绍
关于XGBoost的算法原理,已经进行了介绍与总结,相关内容可参考【机器学习(一)】分类和回归任务-XGBoost算法-Sentosa_DSML社区版一文。本文以预测二手车的交易价格为目标,通过Python代码和Sentosa_DSML社区版分别实现构建XGBoost回归预测模型,并对模型进行评估,包括评估指标的选择与分析。最后得出实验结论,确保模型在二手汽车价格回归预测中的有效性和准确性。
数据集介绍
以预测二手车的交易价格为任务,数据来自某交易平台的二手车交易记录,总数据量超过40w,包含31列变量信息,其中15列为匿名变量。数据集概况介绍:
二、Python代码和Sentosa_DSML社区版算法实现对比
(一) 数据读入与统计分析
1、python代码实现
import os
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib import rcParams
from sklearn.preprocessing import MaxAbsScaler
from sklearn.model_selection import train_test_split
from xgboost import XGBRegressor
from xgboost import plot_importance
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
数据读入
file_path = r'.\二手汽车价格.csv'
output_dir = r'.\xgb'
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件未找到: {file_path}")
if not os.path.exists(output_dir):
os.makedirs(output_dir)
df = pd.read_csv(file_path)
print(df.isnull().sum())
print(df.head())
>> SaleID name regDate model ... v_11 v_12 v_13 v_14
0 0 736 20040402 30.0 ... 2.804097 -2.420821 0.795292 0.914763
1 1 2262 20030301 40.0 ... 2.096338 -1.030483 -1.722674 0.245522
2 2 14874 20040403 115.0 ... 1.803559 1.565330 -0.832687 -0.229963
3 3 71865 19960908 109.0 ... 1.285940 -0.501868 -2.438353 -0.478699
4 4 111080 20120103 110.0 ... 0.910783 0.931110 2.834518 1.923482
统计分析
rcParams['font.family'] = 'sans-serif'
rcParams['font.sans-serif'] = ['SimHei']
stats_df = pd.DataFrame(columns=[
'列名', '数据类型', '最大值', '最小值', '平均值', '非空值数量', '空值数量',
'众数', 'True数量', 'False数量', '标准差', '方差', '中位数', '峰度', '偏度',
'极值数量', '异常值数量'
])
def detect_extremes_and_outliers(column, extreme_factor=3, outlier_factor=5):
if not np.issubdtype(column.dtype, np.number):
return None, None
q1 = column.quantile(0.25)
q3 = column.quantile(0.75)
iqr = q3 - q1
lower_extreme = q1 - extreme_factor * iqr
upper_extreme = q3 + extreme_factor * iqr
lower_outlier = q1 - outlier_factor * iqr
upper_outlier = q3 + outlier_factor * iqr
extremes = column[(column < lower_extreme) | (column > upper_extreme)]
outliers = column[(column < lower_outlier) | (column > upper_outlier)]
return len(extremes), len(outliers)
for col in df.columns:
col_data = df[col]
dtype = col_data.dtype
if np.issubdtype(dtype, np.number):
max_value = col_data.max()
min_value = col_data.min()
mean_value = col_data.mean()
std_value = col_data.std()
var_value = col_data.var()
median_value = col_data.median()
kurtosis_value = col_data.kurt()
skew_value = col_data.skew()
extreme_count, outlier_count = detect_extremes_and_outliers(col_data)
else:
max_value = min_value = mean_value = std_value = var_value = median_value = kurtosis_value = skew_value = None
extreme_count = outlier_count = None
non_null_count = col_data.count()
null_count = col_data.isna().sum()
mode_value = col_data.mode().iloc[0] if not col_data.mode().empty else None
true_count = col_data[col_data == True].count() if dtype == 'bool' else None
false_count = col_data[col_data == False].count() if dtype == 'bool' else None
new_row = pd.DataFrame({
'列名': [col],
'数据类型': [dtype],
'最大值': [max_value],
'最小值': [min_value],
'平均值': [mean_value],
'非空值数量': [non_null_count],
'空值数量': [null_count],
'众数': [mode_value],
'True数量': [true_count],
'False数量': [false_count],
'标准差': [std_value],
'方差': [var_value],
'中位数': [median_value],
'峰度': [kurtosis_value],
'偏度': [skew_value],
'极值数量': [extreme_count],
'异常值数量': [outlier_count]
})
stats_df = pd.concat([stats_df, new_row], ignore_index=True)
print(stats_df)
def save_plot(filename):
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
file_path = os.path.join(output_dir, f"{filename}_{timestamp}.png")
plt.savefig(file_path)
plt.close()
for col in df.columns:
plt.figure(figsize=(10, 6))
df[col].dropna().hist(bins=30)
plt.title(f"{col} - 数据分布图")
plt.ylabel("频率")
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
file_name = f"{col}_数据分布图_{timestamp}.png"
file_path = os.path.join(output_dir, file_name)
plt.savefig(file_path)
plt.close()
if 'car_brand' in df.columns and 'price' in df.columns:
grouped_data = df.groupby('car_brand')['price'].count()
plt.figure(figsize=(8, 8))
plt.pie(grouped_data, labels=grouped_data.index, autopct='%1.1f%%', startangle=90, colors=plt.cm.Paired.colors)
plt.title("品牌和价格分布饼状图", fontsize=16)
plt.axis('equal')
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
file_name = f"car_brand_price_distribution_{timestamp}.png"
file_path = os.path.join(output_dir, file_name)
plt.savefig(file_path)
plt.close()
2、Sentosa_DSML社区版实现
首先,进行数据读入,利用文本算子直接对数据进行读取,右侧进行读取属性配置
接着,利用描述算子即可对数据进行统计分析,得到每一列数据的数据分布图、极值、异常值等结果。连接描述算子,右侧设置极值倍数为3,异常值倍数为5。
右击执行,得到数据统计分析结果,可以对数据每一列的数据分布图、存储类型,最大值、最小值、平均值、非空值数量、空值数量、众数、中位数、极值和异常值数量等进行计算并展示,结果如下所示:
描述算子执行结果有助于我们对于数据的理解和后续分析。
(二) 数据处理
1、python代码实现
进行数据处理操作
def handle_power(power, threshold=600, fill_value=600):
return fill_value if power > threshold else power
def handle_not_repaired_damage(damage, missing_value='-', fill_value=0.0):
return fill_value if damage == missing_value else damage
def extract_date_parts(date, part):
if part == 'year':
return str(date)[:4]
elif part == 'month':
return str(date)[4:6]
elif part == 'day':
return str(date)[6:8]
def fix_invalid_month(month, invalid_value='00', default='01'):
return default if month == invalid_value else month
columns_to_fill_with_mode = ['model', 'bodyType', 'fuelType', 'gearbox']
for col in columns_to_fill_with_mode:
mode_value = df[col].mode().iloc[0]
df[col].fillna(mode_value, inplace=True)
df = (
df.fillna({
'model': df['model'].mode()[0],
'bodyType': df['bodyType'].mode()[0],
'fuelType': df['fuelType'].mode()[0],
'gearbox': df['gearbox'].mode()[0]
})
.assign(power=lambda x: x['power'].apply(handle_power).fillna(600))
.assign(notRepairedDamage=lambda x: x['notRepairedDamage'].apply(handle_not_repaired_damage).astype(float))
.assign(
regDate_year=lambda x: x['regDate'].apply(lambda y: str(extract_date_parts(y, 'year'))),
regDate_month=lambda x: x['regDate'].apply(lambda y: str(extract_date_parts(y, 'month'))).apply(
fix_invalid_month),
regDate_day=lambda x: x['regDate'].apply(lambda y: str(extract_date_parts(y, 'day')))
)
.assign(
regDate=lambda x: pd.to_datetime(x['regDate_year'] + x['regDate_month'] + x['regDate_day'],
format='%Y%m%d', errors='coerce'),
creatDate=lambda x: pd.to_datetime(x['creatDate'].astype(str), format='%Y%m%d', errors='coerce')
)
.assign(
car_day=lambda x: (x['creatDate'] - x['regDate']).dt.days,
car_year=lambda x: (x['car_day'] / 365).round(2)
)
.assign(log1p_price=lambda x: np.log1p(x['price']))
)
print(df.head())
>> SaleID name regDate model ... regDate_day car_day car_year log1p_price
0 0 736 2004-04-02 30.0 ... 02 4385 12.01 7.523481
1 1 2262 2003-03-01 40.0 ... 01 4757 13.03 8.188967
2 2 14874 2004-04-03 115.0 ... 03 4382 12.01 8.736007
3 3 71865 1996-09-08 109.0 ... 08 7125 19.52 7.783641
4 4 111080 2012-01-03 110.0 ... 03 1531 4.19 8.556606
print(df.dtypes)
>>SaleID int64
name int64
regDate datetime64[ns]
model float64
brand int64
bodyType float64
fuelType float64
gearbox float64
power int64
kilometer float64
notRepairedDamage float64
regionCode int64
seller int64
offerType int64
creatDate datetime64[ns]
price int64
v_0 float64
v_1 float64
...
2、Sentosa_DSML社区版实现
通过描述算子的执行结果可以观察到,"model","bodyType","fuelType","gearbox","power","notRepairedDamage"列需要进行缺失值和异常值处理,首先,连接异常值缺失值填充算子,点击配置列选择,选择需要进行异常值缺失值处理的列。
然后,对配置列异常值缺失值填充方式进行选择。"model","bodyType","fuelType","gearbox"列选择保留异常值,利用众数填充缺失值,
"power"列选择输入规则处理异常值,指定异常值的检测规则为‘power
>600’,选择按照缺失值方法进行填充,使用固定值600对缺失值进行填充。
"notRepairedDamage"列选择输入规则处理异常值,指定异常值的检测规则为notRepairedDamage
== '-',选择按照缺失值方法进行填充,使用固定值0.0对缺失值进行填充。
然后,利用生成列算子分别提取年,月和日信息,并生成对应的列,生成年,月和日列的表达式分别为:substr(regDate
,1,4)、substr(regDate
,5,2)、substr(regDate
,7,2)。
生成列算子处理完成后的结果如下所示:
为了处理无效或缺失的月份信息,利用填充算子对月份列regDate_月
进行处理,填充条件为regDate_月 == '00'
,使用填充值 '01'对 regDate
列进行填充。
对于有效的regDate列数据,利用concat(regDate_年, regDate_月, regDate_日) 来填充regDate列。通过从有效的年、月、日列填充一个新的regDate,可以修复原始数据中某些部分不完整或异常的情况。
通过格式算子将‘regDate’和‘creatData’列修改为String类型(Intege不能直接修改为Date类型),将‘notRepairedDamage’列修改为Double类型。
将‘regDate’和‘creatData’列修改为Date类型(格式:yyyyMMdd)。
格式修改完成后,利用生成列算子。
1、生成'car day' 列,表达式为DATEDIFF(creatDate
, regDate
),计算汽车注册时间 (regDate) 与上线时间 (creatDate) 之间的日期差。
2、生成'car year’列表示汽车使用的年数。表达式为DATEDIFF(creatDate
, regDate
) / 365,计算使用的年数。
3、生成'log1p_price'列,表达式为log1p(price
),通过计算 price 的自然对数,避免价格为 0 时的错误。
生成列执行结果如下所示:
这些步骤为后续的建模和分析奠定了坚实的数据基础,减少了数据异常的影响,增强了模型对数据的理解能力。
(三) 特征选择与相关性分析
1、python代码实现
直方图和皮尔森相关性系数计算
def plot_log1p_price_distribution(df, column='log1p_price', bins=20,output_dir=None):
"""
绘制指定列的分布直方图及正态分布曲线
参数:
df: pd.DataFrame - 输入数据框
column: str - 要绘制的列名
bins: int - 直方图的组数
output_dir: str - 保存图片的路径
"""
sns.set(style="whitegrid")
plt.figure(figsize=(10, 6))
sns.histplot(df[column], bins=bins, kde=True, stat='density',
color='orange', edgecolor='black', alpha=0.6)
mean = df[column].mean()
std_dev = df[column].std()
x = np.linspace(df[column].min(), df[column].max(), 100)
p = np.exp(-0.5 * ((x - mean) / std_dev) ** 2) / (std_dev * np.sqrt(2 * np.pi))
plt.plot(x, p, 'k', linewidth=2, label='Normal Distribution Curve')
plt.title('Distribution of log1p_price with Normal Distribution Curve', fontsize=16)
plt.xlabel('log1p_price', fontsize=14)
plt.ylabel('Density', fontsize=14)
plt.legend()
plt.tight_layout()
if output_dir:
if not os.path.exists(output_dir):
os.makedirs(output_dir)
save_path = os.path.join(output_dir, 'log1p_price_distribution.png')
plt.savefig(save_path, dpi=300)
plt.show()
plot_log1p_price_distribution(df,output_dir=output_dir)
numeric_df = df.select_dtypes(include=[np.number])
correlation_matrix = numeric_df.corr()
log1p_price_corr = correlation_matrix['log1p_price'].drop('log1p_price')
print(log1p_price_corr)
删除'SaleID', 'name', 'regDate', 'model', 'brand', 'regionCode', 'seller', 'offerType', 'creatDate', 'price', 'v_4', 'v_7', 'v_13', 'regDate_year', 'regDate_month', 'regDate_day'列并进行流式归一化:
columns_to_drop = ['SaleID', 'name', 'regDate', 'model', 'brand', 'regionCode', 'seller', 'offerType',
'creatDate', 'price', 'v_4', 'v_7', 'v_13', 'regDate_year', 'regDate_month', 'regDate_day']
df = df.drop(columns=columns_to_drop)
print(df.head())
>> bodyType fuelType gearbox power ... v_14 car_day car_year log1p_price
0 1.0 0.0 0.0 60 ... 0.914763 4385 12.01 7.523481
1 2.0 0.0 0.0 0 ... 0.245522 4757 13.03 8.188967
2 1.0 0.0 0.0 163 ... -0.229963 4382 12.01 8.736007
3 0.0 0.0 1.0 193 ... -0.478699 7125 19.52 7.783641
4 1.0 0.0 0.0 68 ... 1.923482 1531 4.19 8.556606
columns_to_normalize = df.columns.drop('log1p_price')
scaler = MaxAbsScaler()
df[columns_to_normalize] = scaler.fit_transform(df[columns_to_normalize])
df = df.round(3)
print(df.head())
>> bodyType fuelType gearbox power ... v_14 car_day car_year log1p_price
0 0.143 0.0 0.0 0.100 ... 0.106 0.475 0.475 7.523
1 0.286 0.0 0.0 0.000 ... 0.028 0.516 0.516 8.189
2 0.143 0.0 0.0 0.272 ... -0.027 0.475 0.475 8.736
3 0.000 0.0 1.0 0.322 ... -0.055 0.772 0.772 7.784
4 0.143 0.0 0.0 0.113 ... 0.222 0.166 0.166 8.557
2、Sentosa_DSML社区版实现
选择直方图图表分析算子,设定 log1p_price 列作为统计列,分组方式选择组数为20,然后启用显示正态分布的选项,用于展示 log1p_price列的值在不同区间的分布情况。
得到直方图结果如下所示:
连接皮尔森相关系数算子并计算每一列之间的相关性,右侧设置需要计算皮尔森相关性系数的列,目的是为了分析特征之间的关系,以便为数据建模和特征选择提供依据。
计算得到结果如下所示:
通过进一步处理,删除冗余特征,连接删除和重命名算子,'SalelD','name','regDate','model','brand','regionCode','seller','offerType','creatDate','price','v_4','v_7','v_13','regDate_年','regDate_月','regDate_日'列进行删除。
连接流式归一化算子进行特征处理,右侧选择归一化列,算法类型选择绝对值标准化。
可以右击算子预览特征处理后的结果。
(四) 样本分区与模型训练
1、python代码实现
X = df.drop(columns=['log1p_price'])
y = df['log1p_price']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = XGBRegressor(
n_estimators=100,
learning_rate=1,
max_depth=6,
min_child_weight=1,
subsample=1,
colsample_bytree=0.8,
objective='reg:squarederror',
eval_metric='rmse',
reg_alpha=0,
reg_lambda=1,
scale_pos_weight=1,
)
model.fit(X_train, y_train)
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)
2、Sentosa_DSML社区版实现
连接样本分区算子,对数据处理和特征工程完成后的数据划分训练集和测试集,以用于实现后续的模型训练和验证流程。训练集和测试集样本比例为8:2。
然后,连接类型算子,展示数据的存储类型,测量类型和模型类型,将'log1p_price'列的模型类型设置为Label。
样本分区完成后,连接XGBoost回归算子,可再右侧配置算子属性,评估指标即算法的损失函数,有均方根误差、均方根对数误差、平均绝对误差、伽马回归偏差四种;学习率,树的最大深度,最小叶子节点样本权重和,子采样率,最小分裂损失,每棵树随机采样的列数占比,L1正则化项和L2正则化项都是用来防止算法过拟合。当子节点样本权重和不大于所设的最小叶子节点样本权重和时不对该节点进行进一步划分。添加节点方式、最大箱数、是否单精度,这三个参数是当树构造方法是为hist的时候,才生效。最小分裂损失指定了节点分裂所需的最小损失函数下降值。
在本案例中,回归模型中的属性配置为,迭代次数:100,学习率:1,最小分裂损失:0,数的最大深度:6,最小叶子节点样本权重和:1,子采样率:1,每棵树随机采样的列数占比:0.8,树构造算法:auto,正负样本不均衡调节权重:1,L2正则化项:1,L1正则化项:0,学习目标为reg:squarederror,评估指标为均方根误差,初始预测分数为0.5,并计算特征重要性和残差直方图。
右击执行可以得到XGBoost回归模型。
(五) 模型评估和模型可视化
1、python代码实现
def calculate_metrics(y_true, y_pred):
r2 = r2_score(y_true, y_pred)
mae = mean_absolute_error(y_true, y_pred)
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100
smape = np.mean(2 * np.abs(y_true - y_pred) / (np.abs(y_true) + np.abs(y_pred))) * 100
mse = mean_squared_error(y_true, y_pred)
return {
'R2': r2,
'MAE': mae,
'RMSE': rmse,
'MAPE': mape,
'SMAPE': smape,
'MSE': mse
}
train_metrics = calculate_metrics(y_train, y_train_pred)
test_metrics = calculate_metrics(y_test, y_test_pred)
print("训练集评估结果:")
print(train_metrics)
>>训练集评估结果:
{'R2': 0.9762793552927467, 'MAE': 0.12232836257076264, 'RMSE': 0.18761878906931295, 'MAPE': 1.5998275558939563, 'SMAPE': 1.5950003598874698, 'MSE': 0.035200810011835344}
print("\n测试集评估结果:")
print(test_metrics)
>>测试集评估结果:
{'R2': 0.9465739577985525, 'MAE': 0.16364796002127327, 'RMSE': 0.2815951292200689, 'MAPE': 2.176241755969303, 'SMAPE': 2.1652435034262068, 'MSE': 0.0792958168004673}
画出特征重要行图和残差直方图
plt.figure(figsize=(10, 6))
plot_importance(model, importance_type='weight', max_num_features=10, color='orange')
plt.title('特征重要性图', fontsize=16)
plt.xlabel('重要性', fontsize=14)
plt.ylabel('特征', fontsize=14)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.show()
plt.figure(figsize=(10, 6))
sns.histplot(train_residuals, bins=30, kde=True, color='blue')
plt.title('残差分布', fontsize=16)
plt.xlabel('残差', fontsize=14)
plt.ylabel('技术', fontsize=14)
plt.axvline(x=0, color='red', linestyle='--')
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.show()
test_data = pd.DataFrame(X_test)
test_data['log1p_price'] = y_test
test_data['predicted_log1p_price'] = y_test_pred
test_data_subset = test_data.head(200)
original_values = y_test[:200]
predicted_values = y_test_pred[:200]
x_axis = range(1, 201)
plt.figure(figsize=(12, 6))
plt.plot(x_axis, original_values.values, label='Original Values', color='orange')
plt.plot(x_axis, predicted_values, label='Predicted Values', color='green')
plt.title('Comparison of Original and Predicted Values')
plt.ylabel('log1p_price')
plt.legend()
plt.grid()
plt.show()
2、Sentosa_DSML社区版实现
连接评估算子对模型进行评估。
得到训练集和测试集的评估结果如下所示:
连接过滤算子,对测试集数据进行过滤,表达式为Partition_Column
=='Testing',
再接折线图图表分析算子,选择Lable列预测值列和原始值列,利用折线图进行对比分析。
右击执行可得到测试集预测结果和原始值的对比图。
右击模型可以查看特征重要性图、残差直方图等模型信息。结果如下所示:
通过连接各类算子并配置相关参数,完成了基于 XGBoost 回归算法的二手汽车价格预测建模。从数据的导入和清洗,到特征工程、模型训练,再到最终的预测、性能评估及可视化分析完成全流程建模,通过计算模型的多项评估指标,如均方误差 (MSE) 和 R² 值,全面衡量了模型的性能。结合可视化分析,实际值与预测值的对比展示了模型在二手汽车价格预测任务中的优异表现。通过这一系列的操作,完整体现了 XGBoost 在复杂数据集中的强大表现,为二手汽车价格预测提供了准确而可靠的解决方案。
三、总结
相比传统代码方式,利用Sentosa_DSML社区版完成机器学习算法的流程更加高效和自动化,传统方式需要手动编写大量代码来处理数据清洗、特征工程、模型训练与评估,而在Sentosa_DSML社区版中,这些步骤可以通过可视化界面、预构建模块和自动化流程来简化,有效的降低了技术门槛,非专业开发者也能通过拖拽和配置的方式开发应用,减少了对专业开发人员的依赖。
Sentosa_DSML社区版提供了易于配置的算子流,减少了编写和调试代码的时间,并提升了模型开发和部署的效率,由于应用的结构更清晰,维护和更新变得更加容易,且平台通常会提供版本控制和更新功能,使得应用的持续改进更为便捷。
为了非商业用途的科研学者、研究人员及开发者提供学习、交流及实践机器学习技术,推出了一款轻量化且完全免费的Sentosa_DSML社区版。以轻量化一键安装、平台免费使用、视频教学和社区论坛服务为主要特点,能够与其他数据科学家和机器学习爱好者交流心得,分享经验和解决问题。文章最后附上官网链接,感兴趣工具的可以直接下载使用