本文是在做项目过程中对特征工程部分的整理,特征工程更依赖经验知识,所以对于我这种入门小白来说需要一步步探索,本文写的浅显易懂,欢迎拍砖~!
特征工程入门
传送门:特征工程入门
特征工程项目
目录
EDA EDA数据探索分析入门及实战项目(1)
基于EDA探索分析部分我们了解训练集的大致情况:
- 训练集train有15w行,31列;测试集test有5w行,30列(没有预测值price);
- model车型编码、bodytype车身类型、fueltype燃油类型、gearbox变速箱均有缺失值,所有字段都是数值型,只有notRepairedDamage是object类型。
- notRepairedDamage缺失较严重,offerType、seller两个数据严重倾斜,对预测无帮助,power也有异常值
- 预测值是长尾分布,离群较严重
- 数字特征numeric_feature = ['power','kilometer', 'v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12', 'v_13','v_14']
- 分类特征categorical_features = ['name','model','brand','bodyType','fuelType','gearbox','notRepairedDamage','regionCode']
- 数字特征与预测值相关性、数字特征本身分布、数字特征之间的相关性
- 分类特征的箱型图、小提琴图、柱形图(均值)、频数可视化
1. 数据浏览
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
import os
import warnings
warnings.filterwarnings('ignore')
os.chdir('/Users/xy/Desktop/专业知识/阿里天池/天池-二手车交易价格预测')
train = pd.read_csv('used_car_train_20200313.csv',sep = ' ')
test = pd.read_csv('used_car_testA_20200313.csv',sep = ' ')
plt.rcParams['font.sans-serif'] = ['SimHei']#避免中文乱码
plt.rcParams['axes.unicode_minus'] = False#避免中文乱码
pd.set_option('display.max_columns', None)#展示全部列
train.head().append(train.tail())
2. 数据预处理&3. 特征构造
2.1 将数据分为日期特征,类别特征,数值特征
categories_features = ['name', 'model', 'brand', 'bodyType', 'fuelType', 'gearbox', 'notRepairedDamage', 'regionCode', 'seller', 'offerType']
numerical_features = ['power', 'kilometer'] + ['v_{}'.format(i) for i in range(15)]
date_features = ['regDate', 'creatDate']
2.2 单一数值特征处理
2.21 power
- 统计量
sta(train,'power')
sta(test,'power')


- 分布
plt.figure(1),plt.title('train中power分布')
sns.distplot(train['power'],kde=False)
plt.figure(2),plt.title('test中power分布')
sns.distplot(test['power'],kde=False)

可见train和test中power分布一致
方法1:可以利用箱型图剔除异常值
方法2:已知功率[0,600],按照600截断
方法3:按照2500截断
方法4:符合长尾分布,先取log再归一化
#方法1:可以利用箱型图剔除异常值
def outliers_proc(data,col_name,scale):
def box_plot_outliers(data_ser,box_scale):
iqr = box_scale*(data_ser.quantile(0.75)-data_ser.quantile(0.25))
val_up = data_ser.quantile(0.75)+iqr
val_low = data_ser.quantile(0.25)-iqr
rule_up = data_ser>val_up
rule_low = data_ser<val_low
return (rule_up,rule_low),(val_up,val_low)
data_n = data.copy()
data_ser = data_n[col_name]
rule,val = box_plot_outliers(data_ser,box_scale=scale)
index = np.arange(data_ser.shape[0])[rule[0] | rule[1]]
print('删除列数:',len(index))
data_n = data_n.drop(index)
data_n.reset_index(drop=True, inplace=True)
print('现在列数:',len(data_n))
index_up = np.arange(data_ser.shape[0])[rule[0]]
outliers = data_ser.iloc[index_up]
print('上异常值描述统计量是:')
print(pd.Series(outliers).describe())
index_low = np.arange(data_ser.shape[0])[rule[1]]
outliers = data_ser.iloc[index_low]
print('下异常值描述统计量是:')
print(pd.Series(outliers).describe())
plt.figure(1),plt.title('train的power异常值处理前箱线图')
sns.boxplot(data[col_name],data=data,orient='v')
plt.figure(2),plt.title('train的power异常值处理后箱线图')
sns.boxplot(data_n[col_name],data=data,orient='v')


但是剔除异常值后仍是长尾分布
#方法2:已知功率[0,600],按照600截断
train_600 = train
train_600.loc[train_600['power']>600,'power'] = 600
train_600['power'].hist()
columns = ['power','price']
sns.pairplot(data=train_600[columns], kind='scatter',diag_kind='kde')


price与power无明显规律。
按照600截断350以后可以忽略,所以可以试一下按照350截断。
#方法3:按照350截断
train_350.loc[train_350['power']>350,'power'] = 350


按照350截断,随着power增加price最大值也逐渐增大。***这里power=0是需要处理的点。
#方法4:符合长尾分布,先取log再归一化
train_log = train
train_log['power'] = np.log(train_log['power']+1)
train_log['power'] = (train_log['power']-np.min(train_log['power']))/(np.max(train_log['power'])-np.min(train_log['power']))
train_log['power'].hist()
columns = ['power','price']
sns.pairplot(data=train_log[columns], kind='scatter',diag_kind='kde')


***得到的数值比较符合正态分布,与price的关系也符合正态分布。
同理,test与train的power运行结果相似
2.22 kilometer
- 统计量&分布
sta(train,'kilometer')
sta(test,'kilometer')
print(train['kilometer'].value_counts())
print(test['kilometer'].value_counts())
plt.figure(1),plt.title('train的kilometer直方图')
train['kilometer'].hist()
plt.figure(2),plt.title('test的kilometer直方图')
test['kilometer'].hist()


train与test的kilometer分布相似,
可见kilometer已经分过桶,且kilometer越大销量越多,与常识相符毕竟开的时间越长卖车的人越多
- 与price相关性
columns = ['kilometer','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

***kilometer与price无分布规律,所以kilometer不需要做太多处理
2.3 日期特征处理
2.31 regDate
- 统计量
print(train['regDate'].value_counts())
print(test['regDate'].value_counts())
sta(train,'regDate')
sta(test,'regDate')

发现train和test的注册日期中存在异常数据,需要处理。统计量均相似
- 分布
plt.figure(1),plt.title('train的regDate直方图')
train['regDate'].hist()
plt.figure(2),plt.title('test的regDate直方图')
test['regDate'].hist()

train和test的注册日期分布规律相同
#与price相关性
columns = ['regDate','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

***模糊的看出注册日期与price成正相关性,但是由于存在异常值原因,不明显,可以处理后再看下这个结论是否正确。
- 日期处理
#处理成年月日
from tqdm import tqdm
date_cols = ['regDate'] #, 'creatDate']
def date_proc(x):
m = int(x[4:6])
if m == 0:
m = 1
return x[:4] + '-' + str(m) + '-' + x[6:]
for f in tqdm(date_cols):
train[f] = pd.to_datetime(train[f].astype('str').apply(date_proc))
train[f + '_year'] = train[f].dt.year
train[f + '_month'] = train[f].dt.month
train[f + '_day'] = train[f].dt.day
train[f + '_dayofweek'] = train[f].dt.dayofweek
plt.figure()
plt.figure(figsize=(16, 6))
i = 1
for f in date_cols:
for col in ['year', 'month', 'day', 'dayofweek']:
plt.subplot(2, 4, i)
i += 1
v = train[f + '_' + col].value_counts()
fig = sns.barplot(x=v.index, y=v.values)
for item in fig.get_xticklabels():
item.set_rotation(90)
plt.title(f + '_' + col)
plt.tight_layout()
plt.show()

columns = ['regDate_year','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

columns = ['regDate_month','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

columns = ['regDate_day','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

columns = ['regDate_dayofweek','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

其中regdate只有1-12日
***由上可知,车辆注册年份成正态分布,与price有些正相关性;注册月份1月较多,与price无分布规律;注册天数和周几与price无分布规律,自身也没有规律可言。
2.32 creatDate
- 统计量
sta(train,'creatDate')
sta(test,'creatDate')
- 分布
plt.figure(1),plt.title('train的creatDate直方图')
train['creatDate'].hist()
plt.figure(2),plt.title('test的creatDate直方图')
test['creatDate'].hist()

train与test的creatDate分布相似
- 处理年月日
date_cols = ['creatDate'] #, 'creatDate']
def date_proc(x):
m = int(x[4:6])
if m == 0:
m = 1
return x[:4] + '-' + str(m) + '-' + x[6:]
for f in tqdm(date_cols):
train[f] = pd.to_datetime(train[f].astype('str').apply(date_proc))
train[f + '_year'] = train[f].dt.year
train[f + '_month'] = train[f].dt.month
train[f + '_day'] = train[f].dt.day
train[f + '_dayofweek'] = train[f].dt.dayofweek
plt.figure()
plt.figure(figsize=(16, 6))
i = 1
for f in date_cols:
for col in ['year', 'month', 'day', 'dayofweek']:
plt.subplot(2, 4, i)
i += 1
v = train[f + '_' + col].value_counts()
fig = sns.barplot(x=v.index, y=v.values)
for item in fig.get_xticklabels():
item.set_rotation(90)
plt.title(f + '_' + col)
plt.tight_layout()
plt.show()

columns = ['creatDate_year','price']
sns.pairplot(data=train[columns], kind='scatter')

columns = ['creatDate_month','price']
sns.pairplot(data=train[columns], kind='scatter')

columns = ['creatDate_day','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

columns = ['creatDate_dayofweek','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

***可见2016年销量远高于2015年,且16年price明显偏高;上线月份主要在3、4月且与price高度相关,因此构建上线年份和月份很有意义;而上线天数和周几与price没有什么相关性,但是周5至周日的销量比其他时间略高些。
2.33 汽车使用年限
将creatDate和regDate合起来得到汽车使用年限
train['used_time_day'] = (pd.to_datetime(train['creatDate'],format='%Y%m%d',errors='coerce')-
pd.to_datetime(train['regDate'],format='%Y%m%d',errors='coerce')).dt.days
train['used_time_month'] = round(train['used_time_day']/30)
train['used_time_year'] = round(train['used_time_day']/365)
train['used_time_year'].hist()
columns = ['used_time_year','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')


train['used_time_month'].hist()
columns = ['used_time_month','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')


train['used_time_day'].hist()
columns = ['used_time_day','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')


***由上可知,使用年限与price成反比。而且这三个字段应该相关性较高,在后续特征选择时可以只保留一个。
2.3 分类特征处理
2.31 name
- 统计量
print(train['name'].value_counts())
print(test['name'].value_counts())
sta(train,'name')
sta(test,'name')
- 分布
plt.figure(1),plt.title('train的name直方图')
train['name'].hist()
plt.figure(2),plt.title('test的name直方图')
test['name'].hist()
columns = ['name','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')

***train与test的name统计量相似,与price没有分布规律,不用处理扔进模型如果有效果就留下。
2.32 model
****构造新特征统计量,评价与price之间的相关性,保留相关性较高的新特征。
# 构造新特征销售统计量
# 构造新特征销售统计量
def new_features(data,col_name):
data_gb = data.groupby(col_name)
all_info={}
for kind,kind_data in data_gb:
info = {}
kind_data = kind_data[kind_data['price']>0]
info[col_name+'_amount'] = len(kind_data)
info[col_name+'_price_max'] = kind_data.price.max()
info[col_name+'_price_min'] = kind_data.price.min()
info[col_name+'_price_median'] = kind_data.price.median()
info[col_name+'_price_sum'] = kind_data.price.sum()
info[col_name+'_price_std'] = kind_data.price.std()
info[col_name+'_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": col_name})
data = data.merge(brand_fe, how='left', on=col_name)
return data
- 统计量
print(train['model'].value_counts())
print(test['model'].value_counts())
sta(train,'model')
sta(test,'model')
- 分布
plt.figure(1),plt.title('train的model直方图')
train['model'].hist()
plt.figure(2),plt.title('test的model直方图')
test['model'].hist()
columns = ['model','price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')



根据常识,车型编码model对于价格来说十分重要,必须保留。train中有一个缺失值,可以用众数填充,树模型不用处理。
2.33 brand
方法与model相同,这里不展示代码了,只展示结果截图。
为了方便后续分析,这里构建一个分类特征的处理函数:
def sta_category(data,col_name,predict_col='price'):
print(data[col_name].value_counts())
print(sta(data,col_name))
plt.figure(1),plt.title(col_name+'直方图')
data[col_name].hist()
if 'price' in data.columns:
columns = [col_name,'price']
sns.pairplot(data=train[columns], kind='scatter',diag_kind='kde')
else:
None


***汽车品牌毫无疑问也是重要特征;直接可以考虑将车型和品牌结合起来处理。
train和test的分布也一致。一共有40个品牌,也不存在缺失值,所以暂时不做处理,在之后的特征交互阶段在处理。
由相关性可视化可以看出,brand有个很明显的规律,就是中间部分的brand 12-20的样子,price明显偏低。之后看情况可以尝试聚类一下。
2.34 bodyType


***bodyType缺失值需要处理,因为要用他创建新特征,选择向上填充法
由分析得知,编码可能按照销量顺序,因此缺失值按照向下填充
train['bodyType'].fillna(method='ffill',inplace=True)
train['bodyType'].isnull().sum()
***train与test的bodyType分布相似,其中1、2、3、7价格偏低
model、brand、bodyType都有点按照销量编码的意思
2.35 fuelType
train[train['fuelType'].isnull()]


***train和test中的fuelType分不相同,fuelType中2之后数据量较少而且对应的price较低,因此可以合并为一类。
2.36 gearbox

gearbox与price无相关性,且trainhetest的gearbox分布相似,无需处理直接放进模型中。
2.37 notRepairedDamage

***根据常识,无损坏价格高于有损坏,但图中表示无损坏反倒价格较低,推测是已知信息0与1的意义给反了
train['notRepairedDamage'].replace('-',np.nan,inplace=True)
***缺失值直接填补为空值,树模型会处理。
2.38 regionCode
根据常识,不同地区二手车价位略微不同。
regionCode1-4位的情况。


***可见邮编也有种根据销量编号的意思,price与邮编无分布规律。
继续,可以根据邮编拿到省份信息。(德国邮政编码由5个数字组成,其中前两个数字代表省份或州别,后三个数字代表城市地区。)
-
多变量构建特征
下面看下city和brand与价格之间的关系
#不同地区的品牌不同价格有所差异
brand_fe = train.groupby(['city','brand'])['price'].median()
df = pd.DataFrame(brand_fe).rename(columns={'price':'citybrand_price'})
train = train.merge(df, how='left', on=['city','brand'])
***但是不知道怎么分析,哭...等我查查资料
2.39 seller和offerType
seller和offerType只是0和1的信息,对于预测值无价值,删除。
2.40 匿名特征
v_features = ['price']+['v_{}'.format(i) for i in range(15)]
sns.pairplot(train[v_features])

- 找异常值
***从图中其实是可以很明显的看出一些异常值的。
比如,v_14中那突出的一个点;v_5里面那个孤立的一个点。
将这些点找出来,我们可以直接删掉。
注意:删之前,是需要先去test集中验证一下,是否也存在类似的异常,如果不存在,果断删除;如果存在类似的分布,那就不能删除。 - 找关系
这些图中其实也可以看出两个变量是否相关,比如v1和v6 - 找规律
主要找聚类模式。
一是看数据是否被分成明显的几组数据;
二是看各组的数据量是否足够。
比如v5和v14,虽然数据很明显被分成两组,但是左边那个明显数据量过少,很难说聚类后能带来多大的帮助。
一个好的例子是v12和v0/v8和v2,界限清晰,数据量较为平衡
3. 特征构造
二手车估值模型有:
根据使用年限折旧:
- 将二手车分为10年计算,前三年每年折旧15%,中间四年折旧10%,最后三年折旧5%。
- 15%残值不动,前三年每年11%,后四年10%,最后三年9%
根据行驶公里数折旧:
假定一辆车有效寿命为30万公里,将其分为5段,每段6万公里,每段价值依序为新车价的5/15、4/15、3/15、2/15、1/15。
6万公里以内 每公里折旧1/9
6-12万公里 每公里折旧1/15
12-18万公里 每公里折旧1/30
18-24万公里 每公里折旧1/(15*15)
24-30万公里 每公里折旧
# 使用年限折旧
def depreciation_year1(year):
if year <= 3:
return 1 - year * 0.15
elif year > 3 and year <= 7:
return 0.55 - (year-3) * 0.1
elif year > 7 and year <= 10:
return 0.25 - (year-7) * 0.05
else:
return 0
train['depreciation_year1'] = train['used_time_year'].apply(lambda x: depreciation_year1(x))
def depreciation_year2(year):
if year <= 3:
return 1 - 0.85 * year * 0.11
elif year > 3 and year <= 7:
return 0.7195 - 0.85 * (year-3) * 0.1
elif year > 7 and year <= 10:
return 0.3795 - 0.85 * (year-7) * 0.09
else:
return 0.15
train['depreciation_year2'] = train['used_time_year'].apply(lambda x: depreciation_year2(x))


#行驶里程
def depreciation_kilometer(kilo):
if kilo <= 6:
return 1 - kilo * 5 / 90
elif kilo > 6 and kilo <= 12:
return 0.66667 - (kilo-6) * 4 / 90
elif kilo > 12 and kilo <= 18:
return 0.4 - (kilo-12) * 3 / 90
elif kilo > 18 and kilo <= 24:
return 0.2 - (kilo-18) * 2 / 90
elif kilo > 24 and kilo <= 30:
return 0.06667 - (kilo-24) * 1 / 90
train['depreciation_kilo'] = train['used_time_year'].apply(lambda x: depreciation_year1(x))
4. 特征选择
思路:自变量和目标变量之间的关联。
相关性分析: 连续变量 VS 连续变量
卡方检验: 分类变量 VS 分类变量
单因素方差分析: 分类变量 VS 连续变量
需要注意的一点是:
不同的模型,对于相关分析结果的处理是不一样的;
LR模型的话,应当把自变量之间相关性过高的特征,做一个筛选排除;
树模型的话,可以保留;
pd.set_option('display.max_rows', None)#展示全部列
corrs[corrs>0.5]
depreciation_year1,price-0.658
depreciation_year2,price-0.652
depreciation_kilo,price-0.658
model_price_average,price-0.58
model_price_median,price-0.56
regDate_year,0.61
v0,price-0.628
v8,price-0.685
v12,price-0.69
v10,name-0.57
v0,v5-0.72
v0,v8-0.51
v0,regDate_year-0.52
v0,model_price_average-0.54
v1,v6-0.99
v2,v7-0.97
v2,v11-0.8
v2,v12-0.5
v3, used_time_day-0.798
v3, used_time_month-0.798
v3, used_time_year-0.796
v4,v9-0.96
v4,v13-0.93
(1)独立高度相关的,可以只保留一个, V4 V9 V13,可以只保留V4;v3,used_time_day,used_time_month,used_time_year只保留V3;v2,v7,v11只保留v2;
(2)跟price高度相关的特征,比如depreciation_kilo或v8, 我们可以把这个特征跟其他自变量结合起来构造新的特征。
5. 降维
未完待续
总结
终于写完了,总觉得特征工程虽然入门简单但是想要精准get到有效特征很难!可能很多时候都是无用功~😿
参考:
https://tianchi.aliyun.com/notebook-ai/detail?spm=5176.12586969.1002.27.1cd8593anJSF4X&postId=95276
https://tianchi.aliyun.com/notebook-ai/detail?postId=100091
网友评论