【机器学习(十二)】机器学习回归案例之二手汽车价格预测—XGBoost回归算法—Sentosa_DSML社区版

时间:2024-09-30 19:55:50

文章目录

  • 一、算法和背景介绍
  • 二、Python代码和Sentosa_DSML社区版算法实现对比
    • (一) 数据读入与统计分析
    • (二) 数据处理
    • (三) 特征选择与相关性分析
    • (四) 样本分区与模型训练
    • (五) 模型评估和模型可视化
  • 三、总结

一、算法和背景介绍

  关于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,