hello,本次分享kaggle playground最新竞赛,预测贴纸数量。
目标:此挑战的目标是预测不同国家/地区的贴纸销量评估:使用平均绝对百分比误差 (MAPE)评估提交的内容。
数据描述
对于此挑战,你将预测来自不同(真实!)国家/地区的不同虚拟商店的各种 Kaggle 品牌贴纸的多年销售额。该数据集是完全合成的,但包含您在现实世界数据中看到的许多影响,例如周末和假期影响、季节性等。
文件
- train.csv - 训练集,其中包括每个日期-国家/地区-商店-商品组合的销售数据。
- test.csv - 测试集;您的任务是预测num_sold每个日期-国家/地区-商店-商品组合的相应商品销售额 ( )。
- Sample_submission.csv - 正确格式的示例提交文件。
评估指标说明
Mean Absolute Percentage Error (MAPE) 是回归任务中常用的评估指标之一,它衡量了预测值与真实值之间的相对误差,公式如下:
MAPE = 1 n ∑ i = 1 n ∣ y i − y ^ i y i ∣ × 100 \text{MAPE} = \frac{1}{n} \sum_{i=1}^{n} \left| \frac{y_i - \hat{y}_i}{y_i} \right| \times 100 MAPE=n1i=1∑n yiyi−y^i ×100
其中,𝑦𝑖 是实际值,𝑦^𝑖是预测值,n 是样本数量。对于回归问题,MAPE 提供了一个直观的百分比误差度量,通常用于评估模型在实际应用中的性能。
源码
python
import numpy as np
import pandas as pd
train_data=pd.read_csv('train.csv')
test_data=pd.read_csv('test.csv')
train_data.info()
python
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 230130 entries, 0 to 230129
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 230130 non-null int64
1 date 230130 non-null object
2 country 230130 non-null object
3 store 230130 non-null object
4 product 230130 non-null object
5 num_sold 221259 non-null float64
dtypes: float64(1), int64(1), object(4)
memory usage: 10.5+ MB
python
train_data.head()
python
id date country store product num_sold
0 0 2010-01-01 Canada Discount Stickers Holographic Goose NaN
1 1 2010-01-01 Canada Discount Stickers Kaggle 973.0
2 2 2010-01-01 Canada Discount Stickers Kaggle Tiers 906.0
3 3 2010-01-01 Canada Discount Stickers Kerneler 423.0
4 4 2010-01-01 Canada Discount Stickers Kerneler Dark Mode 491.0
可以看到数据除了时间变量,和目标变量其余变量都为分类变量。并且发现目标变量num_sold有少量缺失值。
python
train_data1 = train_data.dropna()
# 遍历所有object类型的字段,查看这些字段的unique()值
for column in train_data1.select_dtypes(include=['object']).columns:
unique_values = train_data1[column].unique()
print(f"Unique values in '{column}': {unique_values}")
python
Unique values in 'country': ['Canada' 'Finland' 'Italy' 'Kenya' 'Norway' 'Singapore']
Unique values in 'store': ['Discount Stickers' 'Stickers for Less' 'Premium Sticker Mart']
Unique values in 'product': ['Kaggle' 'Kaggle Tiers' 'Kerneler' 'Kerneler Dark Mode'
'Holographic Goose']
根据分类变量的值,可以对其使用独热编码。
python
# 划分好目标变量和特征
train_data2=train_data1.drop(['id','num_sold'], axis=1)
test_data2=test_data.drop(['id'], axis=1)
label=train_data1['num_sold']
train_data2.shape, test_data2.shape
python
from sklearn.preprocessing import OneHotEncoder
def encode_categorical_features(train_df, test_df, categorical_columns):
"""
对指定的类别型特征进行 One-Hot 编码,并对训练集和测试集进行特征对齐。
参数:
- train_df: 训练集 DataFrame
- test_df: 测试集 DataFrame
- categorical_columns: 需要编码的类别型特征列名列表
"""
encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore') # 创建 OneHotEncoder
train_encoded = train_data1.copy() # 使用传入的训练集数据
test_encoded = test_data.copy() # 使用传入的测试集数据
for column in categorical_columns:
# 对训练集进行 fit_transform
train_encoded_array = encoder.fit_transform(train_df[[column]])
# 对测试集使用训练集的规则进行 transform
test_encoded_array = encoder.transform(test_df[[column]])
# 将编码后的数据转换为 DataFrame
train_encoded_df = pd.DataFrame(train_encoded_array,
columns=encoder.get_feature_names_out([column]),
index=train_df.index)
test_encoded_df = pd.DataFrame(test_encoded_array,
columns=encoder.get_feature_names_out([column]),
index=test_df.index)
# 合并编码后的数据到原始 DataFrame 中
train_encoded = pd.concat([train_encoded, train_encoded_df], axis=1)
test_encoded = pd.concat([test_encoded, test_encoded_df], axis=1)
# 删除原始的类别型列
train_encoded.drop(column, axis=1, inplace=True)
test_encoded.drop(column, axis=1, inplace=True)
# 确保训练集和测试集的列顺序一致
test_encoded = test_encoded.reindex(columns=train_encoded.columns, fill_value=0)
return train_encoded, test_encoded
# 需要编码的列名
encoder_columns = ['country', 'store', 'product']
# 调用函数进行编码,传入指定的类别列
train_data_encoded, test_data_encoded = encode_categorical_features(train_data2, test_data2, encoder_columns)
# 查看结果维度
print("训练集维度: ", train_data_encoded.shape)
print("测试集维度: ", test_data_encoded.shape)
python
训练集维度: (221259, 17)
测试集维度: (98550, 17)
编码做完,剩下就是时间变量了
python
def generate_date_features(df):
"""为传入的 DataFrame 生成日期相关特征,并删除原始日期列"""
# 将 'date' 列转换为日期类型
df['date'] = pd.to_datetime(df['date'])
# 提取年、月、日、星期等特征
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['day_of_week'] = df['date'].dt.dayofweek # 星期几 (0=Monday, 6=Sunday)
df['week_of_year'] = df['date'].dt.isocalendar().week # 一年中的周数
# 周期性特征 (月和日的正弦和余弦转换)
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
df['day_sin'] = np.sin(2 * np.pi * df['day'] / 31) # 以31天为周期
df['day_cos'] = np.cos(2 * np.pi * df['day'] / 31)
# 进一步提取更细粒度的特征
df['quarter'] = df['date'].dt.quarter # 季度 (1, 2, 3, 4)
df['is_leap_year'] = df['date'].dt.is_leap_year # 是否闰年
df['days_in_month'] = df['date'].dt.days_in_month # 本月天数
df['is_weekend'] = (df['day_of_week'] >= 5).astype(int) # 是否周末 (1=周末, 0=工作日)
# 创建特征:日期周数与月份相关
df['month_day_of_week'] = (df['day_of_week'] + 1) % 7 # 可以加1做一个周期性的变换
# 删除原始 'date' 列
df = df.drop(columns=['date'])
return df
# 对 train_data1 和 test_data 分别调用 generate_date_features 函数
train_data_feature = generate_date_features(train_data_encoded)
test_data_feature = generate_date_features(test_data_encoded)
train_data_feature.shape, test_data_feature.shape
python
((221259, 30), (98550, 30))
这一步操作的目的是从日期列中提取出多个有用的特征,以便模型能够更好地捕捉时间上的规律。具体来说:
- 年、月、日、星期几、周数:帮助模型识别时间中的周期性变化,如年度、月份、星期几等。
- 周期性特征(正弦和余弦转换):将月和日转换为正弦和余弦值,帮助模型识别日期的周期性规律。
- 季度、闰年、每月天数、是否周末:提供更细粒度的信息,帮助模型理解季节变化、闰年影响以及工作日和周末的区别。
- 日期周数与月份的相关特征:通过对星期几进行变换,帮助模型理解每个月的工作日与周末的差异。
- 最后,删除原始的 date 列,因为这些新特征已经包含了日期信息,避免冗余。
python
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_percentage_error
import lightgbm as lgb
import optuna
x = train_data_feature
y = label
# 切分数据集
X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)
# 定义目标函数
def objective(trial):
params = {
'objective': 'regression',
'boosting_type': 'gbdt',
'max_depth': trial.suggest_int('max_depth', 3, 10),
'num_leaves': trial.suggest_int('num_leaves', 20, 150),
'min_child_samples': trial.suggest_int('min_child_samples', 10, 100),
'min_child_weight': trial.suggest_float('min_child_weight', 1e-3, 10.0),
'subsample': trial.suggest_float('subsample', 0.6, 1.0),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
'learning_rate': trial.suggest_float('learning_rate', 1e-4, 0.1),
'reg_lambda': trial.suggest_float('reg_lambda', 1e-3, 10.0),
'reg_alpha': trial.suggest_float('reg_alpha', 1e-3, 10.0),
}
model = lgb.LGBMRegressor(**params, random_state=42)
model.fit(X_train, y_train) # 使用原始的训练数据
y_pred = model.predict(X_test) # 使用原始的测试数据
mape = mean_absolute_percentage_error(y_test, y_pred) # 使用 MAPE 作为评估指标
return mape
# 启动优化
study = optuna.create_study(direction='minimize') # MAPE 越小越好,所以方向是 minimize
study.optimize(objective, n_trials=50)
# 使用最佳参数训练模型
best_params = study.best_trial.params
best_model = lgb.LGBMRegressor(**best_params, random_state=42)
best_model.fit(X_train, y_train)
# 评估
y_pred = best_model.predict(X_test)
mape = mean_absolute_percentage_error(y_test, y_pred)
# 输出结果
print("最佳参数: ", best_params)
print("MAPE: {:.5f}".format(mape))
python
最佳参数: {'max_depth': 7, 'num_leaves': 147, 'min_child_samples': 35, 'min_child_weight': 0.16523950085204664, 'subsample': 0.85660019823679, 'colsample_bytree': 0.9903873300498585, 'learning_rate': 0.09608040411129001, 'reg_lambda': 0.2377823209800927, 'reg_alpha': 2.660750861644525}
MAPE: 0.00558
通过训练可得MAPE为0.00558。
已经是一个较高的水准了。
注意:
标准化有助于加速许多机器学习算法的收敛,尤其是像梯度下降这样的优化算法。然而,LightGBM 等树模型不太依赖标准化,因此不进行标准化操作,模型的表现可能不会有显著变化,甚至有可能在某些情况下有所提高。
若使用其他模型(例如线性回归、支持向量机等),是否进行标准化可能会遇到不同的效果。
本次分享就结束了,希望大家点赞关注。