数据挖掘学习小组:Task3.特征工程
Task3.特征工程
- 对特征工程的痛苦早有耳闻,今天也来好好学习一下
- 特征工程目标
对于特征进行进一步分析,并对于数据进行处理
深入分析特征工程,并针对数据制作相应的可视化图表或撰写相关总结文档,在完成后进行打卡登记
- 内容介绍
常见的特征工程包括:
异常处理:
- 基于箱线图(亦可采用3σ方法)识别并剔除异常数据点;
- Box-Cox转换用于修正数据偏态;
- 长尾截断处理以减少极端值的影响。
特征归一化/标准化:
- 对数据进行归一化处理以符合统计分析需求;
- 应用范围较广的标准化方法包括将指标转化为 [0,1] 标度;
- 在面对呈幂律分布的数据时可采用如下公式进行变换: log(1 + x₁ + median) log(1 + x₁ + median)
- 数据划分:
- 均匀分布分桶;
- 等宽区间分桶;
- 类似基于基尼指数的二分类方法(类似于Best-KS 分桶);
- χ² 用于分类任务的分桶;
- 缺失值处理:
- 无需处理(适用于类似 XGBoost 这类树模型);
- 移除(当缺失数据过多时);
- 进行插值补全操作:包括使用均值、中位数或众数进行简单插补;基于建模的预测进行复杂插补;采用多重插补法弥补缺失数据;运用压缩感知技术进行插值;以及通过矩阵分解方法恢复缺失信息;
- 实施分箱处理:将连续变量划分为若干区间。
- 特征提取:
- 提取统计量相关特征, 包括计数指标, 累加值, 比例参数以及标准差等多个维度;
- 分析时间属性的多维度信息, 包括时点参考与时长基准, 节假日节点及周末休息日;
- 应用空间分箱策略及分布编码技术来处理地理信息;
- 对数值进行对数转换, 平方处理或开根号运算以实现非线性映射;
- 通过不同维度之间的交互作用融合来构建复杂的特征组合;
- 因人而异地选择最优特征工程策略。
- 特征提取
- 筛选出型(filter):先对原始数据进行特征提取后用于训练学习模型;常见的方法包括 Relief、方差分析、相关性分析、卡方检验以及互信息法等;
- 包裹策略(wrapper):以所选学习模型的性能指标为评价标准;其典型代表是 Las Vegas Wrapper 方法;
- 嵌入机制(embedding):融合筛选型与包裹式的策略,在学习模型构建过程中自然实现了特征提取功能;其中lasso回归是一种典型实现方法;
- 降维
- PCA/ LDA/ ICA;
- 特征选择也是一种降维。
删除异常值
箱线图的原理即显示异常数据的依据 :
在箱线图中,中间是一个箱体区域,并被标注为粉红色部分。左侧、中央和右侧各有一条标记线:左侧代表下四分位数(Q₁),右侧代表上四分位数(Q₃),中央位置则是中位数值(Median)。上下两个四分位数值之间的差即为内距四分位间距(IQR)。通过计算 Q₁ - 1.5 × IQR 确定最低边界线的位置;而 Q₃ + 1.5 × IQR 则标定最高边界线的位置。超出最高边界线的数据被视为极大异常点;低于最低边界线的数据则被认为是极小异常点;因此,在上下两个边界之外的观测数据都被视为异常点。

一旦明确了解异常数据产生的机制之后
首先
在剔除异常数据后生成的箱线图中仍存在异常值。这并不适用于所有情况;有些数据仅经过一次去极值处理就不再含有极值点;然而有些则需要进行多次去极处理才能使生成的箱线图中没有极值显现。举例来说,在本研究的第一部分所述的方法下从原始调查所得的人口收入(即集合A)中去除所有异常点后得到了集合B;随后对集合B绘制新的箱线图时仍然发现了剩余的异常点(如上所示)。然而需要注意的是,在这种情况下集合B实际上是集合A执行了一次去极处理后的结果;因此对于原始集合A而言,我们操作一次其实就已经删除了其中的所有极端数值。换句话说,后续由集合B绘制出的结果中的"极端数值"应当被视为该子集的独特特征而非原始总体的数据属性。因此在一般情况下,通过合理的选择和执行一次去极处理即可去除总体中的大部分潜在影响因素
特征构造
# 训练集和测试集放在一起,方便构造特征
Train_data['train']=1
Test_data['train']=0
data = pd.concat([Train_data, Test_data], ignore_index=True)
print(data['train'].tail())
print(data.shape)
使用时间:
# 使用时间:data['creatDate'] - data['regDate'],反应汽车使用时间,一般来说价格与使用时间成反比
# 不过要注意,数据里有时间出错的格式,所以我们需要 errors='coerce'
data['used_time'] = (pd.to_datetime(data['creatDate'], format='%Y%m%d', errors='coerce') -
pd.to_datetime(data['regDate'], format='%Y%m%d', errors='coerce')).dt.days
传入 errors=‘coerce’ 的情况下,Pandas 遇到无法转换的数据时会将这些数据赋值为 NaN(Not a Number)。
# 看一下空数据,有 15k 个样本的时间是有问题的,我们可以选择删除,也可以选择放着。
# 但是这里不建议删除,因为删除缺失数据占总样本量过大,7.5%
# 我们可以先放着,因为如果我们 XGBoost 之类的决策树,其本身就能处理缺失值,所以可以不用管;
data['used_time'].isnull().sum()
# 从邮编中提取城市信息,相当于加入了先验知识
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])
data = data
data['city']
# 计算某品牌的销售统计量,同学们还可以计算其他特征的统计量
# 这里要以 train 的数据计算统计量
Train_gb = Train_data.groupby("brand")
all_info = {}
for kind, kind_data in Train_gb:
info = {}
kind_data = kind_data[kind_data['price'] > 0]
info['brand_amount'] = len(kind_data)
info['brand_price_max'] = kind_data.price.max()
info['brand_price_median'] = kind_data.price.median()
info['brand_price_min'] = kind_data.price.min()
info['brand_price_sum'] = kind_data.price.sum()
info['brand_price_std'] = kind_data.price.std()
info['brand_price_average'] = round(kind_data.price.sum() / (len(kind_data) + 1), 2)
all_info[kind] = info
brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"})
data = data.merge(brand_fe, how='left', on='brand')
# 数据分桶 以 power 为例
# 这时候我们的缺失值也进桶了,
# 为什么要做数据分桶呢,原因有很多,= =
# 1. 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
# 2. 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
# 3. LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
# 4. 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;
# 5. 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
# 当然还有很多原因,LightGBM 在改进 XGBoost 时就增加了数据分桶,增强了模型的泛化性
bin = [i*10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels=False)
data[['power_bin', 'power']]
# 删除不需要的数据
data = data.drop(['creatDate', 'regDate', 'regionCode'], axis=1)
# 目前的数据其实已经可以给树模型使用了,所以我们导出一下
data.to_csv('/home/tianchi/myspace/data_for_tree.csv', index=0)
# 我们可以再构造一份特征给 LR NN 之类的模型用
# 之所以分开构造是因为,不同模型对数据集的要求不同
# 我们看下数据分布:
data['power'].plot.hist()

# 我们刚刚已经对 train 进行异常值处理了,但是现在还有这么奇怪的分布是因为 test 中的 power 异常值,
# 所以我们其实刚刚 train 中的 power 异常值不删为好,可以用长尾分布截断来代替
Train_data['power'].plot.hist()
# 我们对其取 log,在做归一化
from sklearn import preprocessing
min_max_scaler = preprocessing.MinMaxScaler()
data['power'] = np.log(data['power'] + 1)
data['power'] = ((data['power'] - np.min(data['power'])) / (np.max(data['power']) - np.min(data['power'])))
data['power'].plot.hist()
对kilometer以及其他特征也做归一化
# 所以我们可以直接做归一化
data['kilometer'] = ((data['kilometer'] - np.min(data['kilometer'])) /
(np.max(data['kilometer']) - np.min(data['kilometer'])))
data['kilometer'].plot.hist()
# 除此之外 还有我们刚刚构造的统计量特征:
# 'brand_amount', 'brand_price_average', 'brand_price_max',
# 'brand_price_median', 'brand_price_min', 'brand_price_std',
# 'brand_price_sum'
# 这里不再一一举例分析了,直接做变换,
def max_min(x):
return (x - np.min(x)) / (np.max(x) - np.min(x))
data['brand_amount'] = ((data['brand_amount'] - np.min(data['brand_amount'])) /
(np.max(data['brand_amount']) - np.min(data['brand_amount'])))
data['brand_price_average'] = ((data['brand_price_average'] - np.min(data['brand_price_average'])) /
(np.max(data['brand_price_average']) - np.min(data['brand_price_average'])))
data['brand_price_max'] = ((data['brand_price_max'] - np.min(data['brand_price_max'])) /
(np.max(data['brand_price_max']) - np.min(data['brand_price_max'])))
data['brand_price_median'] = ((data['brand_price_median'] - np.min(data['brand_price_median'])) /
(np.max(data['brand_price_median']) - np.min(data['brand_price_median'])))
data['brand_price_min'] = ((data['brand_price_min'] - np.min(data['brand_price_min'])) /
(np.max(data['brand_price_min']) - np.min(data['brand_price_min'])))
data['brand_price_std'] = ((data['brand_price_std'] - np.min(data['brand_price_std'])) /
(np.max(data['brand_price_std']) - np.min(data['brand_price_std'])))
data['brand_price_sum'] = ((data['brand_price_sum'] - np.min(data['brand_price_sum'])) /
(np.max(data['brand_price_sum']) - np.min(data['brand_price_sum'])))
# 对类别特征进行 OneEncoder
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',
'gearbox', 'notRepairedDamage', 'power_bin'])
# 这份数据可以给 LR 用
data.to_csv('/home/tianchi/myspace/data_for_lr.csv', index=0)
特征筛选
过滤式
# 相关性分析
print(data['power'].corr(data['price'], method='spearman'))
print(data['kilometer'].corr(data['price'], method='spearman'))
print(data['brand_amount'].corr(data['price'], method='spearman'))
print(data['brand_price_average'].corr(data['price'], method='spearman'))
print(data['brand_price_max'].corr(data['price'], method='spearman'))
print(data['brand_price_median'].corr(data['price'], method='spearman'))

包裹式
嵌入式
经验总结
在比赛中占据核心地位的便是特征工程,在这些领域中表现突出的比赛往往具有显著的竞争优势。各具特色的模型在常规比赛中表现往往相差不大,在这种情况下仅通过微调参数所能获得的效果提升幅度非常有限。然而,在训练阶段投入大量资源优化构建出高质量特征的能力则会直接决定最终的比赛结果。
特征工程的主要目的是将数据转换为更好地反映潜在问题的特征,并提高机器学习性能。例如,在图像识别任务中去除了噪声有助于提升模型效果;而对缺失数据进行填充通常基于先验知识或假设以确保数据完整性。
特征构造也属于特征工程的一部分,其目的是为了增强数据的表达。
某些比赛中的某些特性属于匿名性质。
对于那些具有明确特征含义且非匿名的特征工程,在工业类型相关比赛中通常会采用基于信号处理技术的频域中的特征提取方法以及数据丰富度与分布偏态性的分析来构建出更具实际应用价值的特征指标。同样地,在推荐系统领域中也采用类似的方法进行操作。具体而言,在推荐系统中会根据不同类型的点击率进行统计分析,在不同时间段内观察用户的浏览行为,并结合用户属性信息来进行综合评估。这种深入分析的方法不仅能够揭示用户行为背后的驱动因素,还能够帮助挖掘出潜在的价值点
当然地来说,特征工程的本质就是与模型紧密结合。这正说明了为何要在LR神经网络中进行分桶处理以及进行特征归一化。而对于如何评估这些特性的表现及其重要性,则通常需要依赖于模型本身的反馈机制
总的来说,特征工程是一个入门简单,但想精通非常难的一件事。
关于Datawhale:
Datawhale是一家致力于推动数据科学与人工智能领域开放源代码的开源组织。
它汇聚了来自各领域高校以及知名企业的优秀学习者群体。
该组织致力于培养一支具有开放合作精神和创新探索能力的团队。
其倡导真实自我展示、开放包容的态度、互信互助的精神以及勇于尝试、积极担当的态度。
Datawhale秉持开放理念探索开发开源内容、开展开源学习活动以及推广开源方案。
本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale:

