是否对数据科学竞赛感到陌生?是否在排行榜上难以攀升?曾经也经历过,如果没有一些经过验证的技术,这确实是一个令人畏惧的障碍。而最有效(也最有趣)的步骤就是特征工程!
如果还没有在竞赛中尝试过特征工程,那错过了很多乐趣。经常看到的大多数获胜解决方案和方法,其核心都有特征工程。像SRK和Rohan Rao这样的竞赛专家都证明了特征工程的力量,以及它如何将推向排行榜的上游。
个人非常喜欢在数据科学竞赛中进行特征工程时可以使用的各种技能。解决问题的能力、分析思维和好奇心——所有这些技能都得到了考验。
坚信特征工程是机器学习艺术中被低估的部分。它可能是竞赛中最耗时的步骤,但它激发的创造力正是数据科学的核心!
特征工程也是那些拥有领域专业知识的人可以大放异彩的关键领域之一。将在本文中涵盖所有这些内容,并且将在一个实际的数据科学问题上实现特征工程!
特征工程是数据科学和机器学习中的一门艺术。它指的是从现有特征中创建新特征,从数据集当前拥有的变量列表中提出新的变量。
就像工业加工可以从原始矿石中提取纯金一样,特征工程可以从非常嘈杂的原始数据中提取宝贵的“alpha”。
毕竟,必须挖很多土才能找到金子。🙂
特征工程本质上是一个创造性的过程,不应该受到规则或限制的过度约束。然而,相信在执行特征工程时,应该遵循一些指导方针:
窥视(未来)是特征工程的“原罪”(以及一般预测建模)。它指的是使用关于未来的信息(或者还不知道的信息)来构建数据。这可能是显而易见的,比如使用next_12_months_returns。然而,它通常是相当微妙的,比如使用整个时间段的均值或标准差来归一化数据点(这隐含地将未来信息泄露到特征中)。测试是是否能够在那个时间点计算数据点时得到完全相同的值,而不是今天。
上述的推论是,还需要诚实地知道当时会知道什么,而不仅仅是当时发生了什么。例如,短期借贷数据由交易所报告,有很大的时间滞后。希望在知道它的日期上给特征打上时间戳。
许多机器学习算法期望每个输入特征对每个观测值都有一个值(某种类型的)。如果想象一个电子表格,其中每个特征是一列,每个观测值是一行,那么表格的每个单元格都应该有一个值。通常情况下,表中的一些特征会自然地比其他特征更频繁地更新自己。价格数据几乎持续更新,而短期库存、分析师估计或EBITDA则每几周或几个月更新一次。在这些情况下,将使用像最后观察值向前(LOCF)这样的方案,以确保自然低频列的每个特征都有一个值。当然,会小心避免无意中窥视!
最后,非常重要的是,只有当它有意义时,才以捕获序数的方式表示特征。例如,通常将“一周中的哪一天”表示为整数(1到7)是一个坏主意,因为这隐含地告诉模型将星期五视为与星期四非常相似,但“多一点”。它还会说星期日和星期一是完全不同的(如果星期日=7,星期一=1)。可能会错过数据中的各种有趣模式。
是时候看看特征工程在行动了!让首先了解问题陈述,然后深入Python实现。
零售公司“ABC Private Limited”想要了解客户对不同类别产品的购买金额。他们想要构建一个模型来预测客户对各种产品的购买金额,这将帮助他们为客户创建针对不同产品的个性化优惠。
可以在这里下载数据集。将首先导入所需的Python库:
# 导入Python库
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error, r2_score
接下来是数据理解部分:
# 查看数据集的前几行
df.head()
这是得到的输出:
在这里,Product_ID、Gender、Age、City_Category和Stay_In_Current_City_Years都是分类特征。接下来,将尝试找出它们各自的唯一值数量:
# 查找每个分类特征的唯一值数量
df.nunique()
这表明Gender、City_Category、Age和Stay_In_Current_City_Years有一些唯一的值,而product_ID有很多唯一的值。
将在这里计算缺失值的百分比。如果之前参加过任何竞赛,一定知道这一步:
# 定义一个函数来计算特定列的总缺失值
def calculate_missing_values(column):
return df[column].isnull().sum()
# 创建一个数据框来以表格格式显示结果
missing_values_df = pd.DataFrame(df.apply(calculate_missing_values), columns=['Missing Values'])
missing_values_df = missing_values_df.sort_values('Missing Values', ascending=False)
在这里,定义了一个函数来计算特定列的总缺失值。然后,创建了一个数据框来以表格格式显示结果。结果按%空值的降序显示:
接下来,创建一个数据框以进行竞赛提交:
# 创建提交数据框
df_submission = pd.DataFrame()
让从数据准备开始。将对分类变量进行标签编码。如果想了解什么是分类特征以及独热编码和标签编码之间的区别,可以查看这篇文章:
# 对分类变量进行标签编码
from sklearn.preprocessing import LabelEncoder
# 定义一个函数来对分类变量进行标签编码
def label_encode_features(df, features):
le = LabelEncoder()
for feature in features:
df[feature] = le.fit_transform(df[feature])
# 对分类特征进行标签编码
label_encode_features(df, ['Gender', 'City_Category', 'Age', 'Stay_In_Current_City_Years'])
对于剩下的两个分类变量User_ID和Product_ID,将使用for循环,因为它们的唯一值数量相当大,不能使用字典来定义。
再次,这是一篇关于apply和transform函数的。
移除不是一个解决方案,因为product_category_3有69.7%的值缺失。由于product_category是一个更通用的特征,需要用一个值填充它,以便它不会泛化与同一用户的其他product_category的关系。所以,让用-999来填充值:
# 填充缺失值
df['Product_Category_3'] = df['Product_Category_3'].fillna(-999)
首先,开始创建一个基线决策树模型,即没有特征工程和超参数优化:
# 创建基线决策树模型
X = df.drop('Target_Variable', axis=1)
y = df['Target_Variable']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 创建决策树回归器对象
dt = DecisionTreeRegressor(random_state=42)
dt.fit(X_train, y_train)
# 预测测试集结果
y_pred = dt.predict(X_test)
# 计算并打印RMSE误差和R2分数
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
print(f'RMSE: {rmse}, R2: {r2}')
# 计算特征重要性
feature_importances = pd.Series(dt.feature_importances_, index=X.columns)
feature_importances.nlargest(10).plot(kind='barh')
特征工程部分的RMSE得分相当低!现在轮到特征工程了。这部分需要大量的实践和特征验证。将首先创建User_Id和Product_Id的平均值:
# 创建User_Id和Product_Id的平均值特征
df['mean_User_Id'] = df.groupby('User_ID')['User_ID'].transform('mean')
df['mean_Product_Id'] = df.groupby('Product_ID')['Product_ID'].transform('mean')
使用groupby结合transform来创建特征。这是一种节省时间且代码量少的惊人技术。
现在将再次拟合决策树模型并检查特征重要性:
# 拟合决策树模型并检查特征重要性
dt.fit(X_train, y_train)
y_pred = dt.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
print(f'RMSE: {rmse}, R2: {r2}')
feature_importances = pd.Series(dt.feature_importances_, index=X.columns)
feature_importances.nlargest(10).plot(kind='barh')
这证实了特征不是多余的,确实有助于更好地理解模型关系。已经将这个模型提交到了公共排行榜:
在看到平均值的特征重要性之后,将添加user_id和product_id的最大值和最小值特征:
# 添加user_id和product_id的最大值和最小值特征
df['max_User_Id'] = df.groupby('User_ID')['User_ID'].transform('max')
df['min_User_Id'] = df.groupby('User_ID')['User_ID'].transform('min')
df['max_Product_Id'] = df.groupby('Product_ID')['Product_ID'].transform('max')
df['min_Product_Id'] = df.groupby('Product_ID')['Product_ID'].transform('min')
再次,评估决策树模型并计算特征重要性:
# 评估决策树模型并计算特征重要性
dt.fit(X_train, y_train)
y_pred = dt.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
print(f'RMSE: {rmse}, R2: {r2}')
feature_importances = pd.Series(dt.feature_importances_, index=X.columns)
feature_importances.nlargest(10).plot(kind='barh')
RMSE得分没有太大改善。正如在基线模型中看到的,Product_Category1具有最高特征重要性。所以在这里,将添加max和min特征列。除此之外,将为每个分类特征创建一个计数列,以更好地理解模型与特征的关系:
# 为每个分类特征创建一个计数列
df['count_Gender'] = df.groupby('Gender')['Gender'].transform('count')
df['count_City_Category'] = df.groupby('City_Category')['City_Category'].transform('count')
df['count_Age'] = df.groupby('Age')['Age'].transform('count')
df['count_Stay_In_Current_City_Years'] = df.groupby('Stay_In_Current_City_Years')['Stay_In_Current_City_Years'].transform('count')
所有这些代码行都遵循使用上面定义的transform函数创建特征的类似程序。将在这个数据集上拟合决策树模型并计算RMSE得分:
# 拟合决策树模型并计算RMSE得分
dt.fit(X_train, y_train)
y_pred = dt.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(f'RMSE: {rmse}')
这在RMSE上取得了重大飞跃。这是在公共排行榜上提交代码后得到的结果:
现在是超参数优化的部分。将使用gridsearchcv来计算最优超参数。要深入了解超参数调整,可以查看这篇文章:
# 使用gridsearchcv进行超参数优化
from sklearn.model_selection import GridSearchCV
# 设置超参数网格
param_grid = {
'max_depth': [10, 20, 30],
'min_samples_leaf': [1, 3, 5],
'min_samples_split': [2, 5, 10]
}
# 创建网格搜索对象
grid_search = GridSearchCV(DecisionTreeRegressor(random_state=42), param_grid, cv=5)
grid_search.fit(X_train, y_train)
# 获取最佳超参数
best_params = grid_search.best_params_
print(f'Best Parameters: {best_params}')
# 使用最佳超参数拟合决策树模型
dt_best = DecisionTreeRegressor(**best_params, random_state=42)
dt_best.fit(X_train, y_train)
y_pred = dt_best.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(f'RMSE: {rmse}')