零基础入门数据挖掘Task3

Datawhale零基础入门数据挖掘-Task3特征工程

前提回顾

赛题以二手车市场为背景,要求选手预测二手汽车的交易价格,这是一个典型的回归问题。通过这道赛题来引导大家走进AI数据竞赛的世界,主要针对于于竞赛新人进行自我练习、自我提高。

零基础入门数据挖掘 - 二手车交易价格预测”是阿里天池的一个入门比赛,这个比赛问题是一个回归问题,评价标准为平均绝对值误差——MAE(Mean Absolute Error)

题目给出的测试集有15万条二手汽车信息,验证集有5万条。一条二手车信息中包含31项变量信息,其中15项匿名变量,其余16项变量(含价格变量)。实际上划分变量就是30项自变量(包含数字型变量、类别型变量、文本型变量和时间型变量),1项因变量(整数形式的二手车交易价格)。

SaleID(交易ID)是唯一编码,不作为后续有效变量的考虑。
seller(销售方)在测试集中只有一条是“非个体”,其余十四万九千九百九十九都是“个体”。而在验证集中发现只有一种情况。因此选择放弃这项变量。
offerType(报价类型)在测试集中只有一种。而在验证集中发现只有一种情况。因此选择放弃这项变量。
power(发动机功率)范围应该是[ 0, 600 ],而在测试集和验证集中都存在大于600的信息,有待后续处理。

有效变量名描述类型
v系列特征匿名特征,包含v0-14在内15个匿名特征数字
power发动机功率:范围 [ 0, 600 ]数字
kilometer汽车已行驶公里,单位万km数字
regDate汽车注册日期,例如20160101,2016年01月01日时间
creatDate汽车上线时间,即开始售卖时间时间
name汽车交易名称,已脱敏类别
model车型编码,已脱敏类别
brand汽车品牌,已脱敏类别
bodyType车身类型:豪华轿车:0,微型车:1,厢型车:2,大巴车:3,敞篷车:4,双门汽车:5,商务车:6,搅拌车:7类别
fuelType燃油类型:汽油:0,柴油:1,液化石油气:2,天然气:3,混合动力:4,其他:5,电动:6类别
gearbox变速箱:手动:0,自动:1类别
notRepairedDamage汽车有尚未修复的损坏:是:0,否:1类别
regionCode地区编码,已脱敏类别

Task3 特征工程

特征工程是比赛中最至关重要的的一块,特别的传统的比赛,大家的模型可能都差不多,调参带来的效果增幅是非常有限的,但特征工程的好坏往往会决定了最终的排名和成绩。

其实在我看来,特征工程很多部分不能很简单做出划分。就好像数据挖掘、数据分析、特征构造等等。每个人都有自己的习惯,我这里分享的单纯就是我个人用于这一比赛的一个特征工程流程:数据清洗,识别变量,形成特征。

阅在特征构造前

其实做数据挖掘比赛也是一个增长见识、开拓思维的过程,需要对比赛所处领域进行了解、分析。
中国汽车流通协会: 2018年全国二手车市场分析
中国汽车流通协会:2019年11月全国二手车市场详细分析
中国汽车流通协会:2020年1-2月二手车市场分析
看完了中国汽车流通协会的几份二手车市场分析,有几点需要后续处理。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述首先是月交易走势,交易明显与月份/季节有关。其次,二手车交易量与所处地理区域也密切相关,华东地区交易约占全市场的三分之一。而北京、辽宁、浙江等地的交易均价较高,这也说明了地区与二手车交易之间存在相关性。二手车交易车龄分布为3年以内、3-6年、7-10年、(2016年以前考虑的是3-10年)、10年以上,交易最热门的是3-6年车龄的二手车。车型、品牌、使用能源、车损情况等也都是二手车交易中极为重要的影响因素。

动在特征构造前

部分数据的清洗在之前的环节已经进行过了,接下来会有部分重复或者被省略。
异常值会对预测模型造成一定的影响,对异常值妥善处理有时候也会提高一点分数。我主要处理的是power和price。题目限定的power虽然是在0-600区间,但实际上无论测试集和验证集中都有超过600极限的。我的处理方法有两步,第一步就是对超过600的都算作600,第二步就是根据全部的power数据添加标签分级分类。

#power分布情况
print(train['power'].describe())
print(train['power'][train['power']>600].describe())
plt.hist(train['power'][train['power']>600], histtype = 'bar') 
plt.show()
print(test['power'].describe())
print(test['power'][test['power']>600].describe())
plt.hist(test['power'][test['power']>600], histtype = 'bar') 
plt.show()

price的分布不合意愿,最后按照的是上一篇blog所说做log(x+1)处理。

#从参考文献中get的异常值处理函数代码
def outliers_proc(data, col_name, scale=3):
    """
    用于清洗异常值,默认用 box_plot(scale=3)进行清洗
    :param data: 接收 pandas 数据格式
    :param col_name: pandas 列名
    :param scale: 尺度
    :return:
    """

    def box_plot_outliers(data_ser, box_scale):
        """
        利用箱线图去除异常值
        :param data_ser: 接收 pandas.Series 数据格式
        :param box_scale: 箱线图尺度,
        :return:
        """
        iqr = box_scale * (data_ser.quantile(0.75) - data_ser.quantile(0.25))
        val_low = data_ser.quantile(0.25) - iqr
        val_up = data_ser.quantile(0.75) + iqr
        rule_low = (data_ser < val_low)
        rule_up = (data_ser > val_up)
        return (rule_low, rule_up), (val_low, val_up)

    data_n = data.copy()
    data_series = data_n[col_name]
    rule, value = box_plot_outliers(data_series, box_scale=scale)
    index = np.arange(data_series.shape[0])[rule[0] | rule[1]]
    print("Delete number is: {}".format(len(index)))
    data_n = data_n.drop(index)
    data_n.reset_index(drop=True, inplace=True)
    print("Now column number is: {}".format(data_n.shape[0]))
    index_low = np.arange(data_series.shape[0])[rule[0]]
    outliers = data_series.iloc[index_low]
    print("Description of data less than the lower bound is:")
    print(pd.Series(outliers).describe())
    index_up = np.arange(data_series.shape[0])[rule[1]]
    outliers = data_series.iloc[index_up]
    print("Description of data larger than the upper bound is:")
    print(pd.Series(outliers).describe())
    
    fig, ax = plt.subplots(1, 2, figsize=(10, 7))
    sns.boxplot(y=data[col_name], data=data, palette="Set1", ax=ax[0])
    sns.boxplot(y=data_n[col_name], data=data_n, palette="Set1", ax=ax[1])
    return data_n

分别对未作处理和做了log(1+x)处理的price调用上述函数。

Train_data = outliers_proc(train, 'price', scale=3)
Delete number is: 3381
Now column number is: 146619
Description of data less than the lower bound is:
count    0.0
mean     NaN
std      NaN
min      NaN
25%      NaN
50%      NaN
75%      NaN
max      NaN
Name: price, dtype: float64
Description of data larger than the upper bound is:
count     3381.000000
mean     38198.588583
std      12999.814410
min      26911.000000
25%      29500.000000
50%      33800.000000
75%      41500.000000
max      99999.000000
Name: price, dtype: float64

在这里插入图片描述
可以看出有很多较大的price值离群。

train['price'] = np.log1p(train['price'])
Train_data = outliers_proc(train, 'price', scale=3)
Delete number is: 0
Now column number is: 150000
Description of data less than the lower bound is:
count    0.0
mean     NaN
std      NaN
min      NaN
25%      NaN
50%      NaN
75%      NaN
max      NaN
Name: price, dtype: float64
Description of data larger than the upper bound is:
count    0.0
mean     NaN
std      NaN
min      NaN
25%      NaN
50%      NaN
75%      NaN
max      NaN
Name: price, dtype: float64

在这里插入图片描述
做了log(1+x)处理后明显改善了。数据的缺失也是一个很大的问题。事实上,如果NAN存在的数量真的很大会考虑直接删除该特征(因为其作用太小了),如果很小一般选择填充(一般是-999、-1、平均值、中位数balabala)。这里分享一段不错的代码。

# 删除重复值
data.drop_duplicates()
# dropna()可以直接删除缺失样本,但是有点不太好

# 填充固定值
train_data.fillna(0, inplace=True) # 填充 0
data.fillna({0:1000, 1:100, 2:0, 4:5})   # 可以使用字典的形式为不用列设定不同的填充值

train_data.fillna(train_data.mean(),inplace=True) # 填充均值
train_data.fillna(train_data.median(),inplace=True) # 填充中位数
train_data.fillna(train_data.mode(),inplace=True) # 填充众数

train_data.fillna(method='pad', inplace=True) # 填充前一条数据的值,但是前一条也不一定有值
train_data.fillna(method='bfill', inplace=True) # 填充后一条数据的值,但是后一条也不一定有值

"""插值法:用插值法拟合出缺失的数据,然后进行填充。"""
for f in features: 
    train_data[f] = train_data[f].interpolate()
    
train_data.dropna(inplace=True)

"""填充KNN数据:先利用knn计算临近的k个数据,然后填充他们的均值"""
from fancyimpute import KNN
train_data_x = pd.DataFrame(KNN(k=6).fit_transform(train_data_x), columns=features)
————————————————
版权声明:本文为CSDN博主「Miracle8070」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wuzhongqiang/article/details/105012634

前一篇blog中未对缺省值进行处理,是因为我在这里要推荐一下真爱——lgb,它会自动处理缺省值,故并不需要对缺失值进行填充。同时LGBM采用了Many vs many的切分方式,实现了类别特征的最优切分。用Lightgbm可以直接输入类别特征。当然也可以考虑自行构造,将数值型统计量对应到类别。 也就是统计每个类别变量下各个target比例,转化数值,这里列举了三个类型变量"brand"、“regionCode”、“model”。

# 这里要以 train 的数据计算统计量
Train_brand = train.groupby("brand")
all_info = {}
for kind, kind_data in Train_brand:
    info = {}
    kind_data = kind_data[kind_data['price'] > 0]
    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_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')

Train_region = train.groupby("regionCode")
all_info = {}
for kind, kind_data in Train_region:
    info = {}
    kind_data = kind_data[kind_data['price'] > 0]
    info['regionCode_price_max'] = kind_data.price.max()
    info['regionCode_price_median'] = kind_data.price.median()
    info['regionCode_price_min'] = kind_data.price.min()
    info['regionCode_price_std'] = kind_data.price.std()
    info['regionCode_price_average'] = round(kind_data.price.sum() / (len(kind_data) + 1), 2)
    all_info[kind] = info
regionCode_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "regionCode"})
data = data.merge(regionCode_fe, how='left', on='regionCode')

train_model = train[~train['model'].isnull()].reset_index(drop=True)
Train_model = train_model.groupby("model")
all_info = {}
for kind, kind_data in Train_model:
    info = {}
    kind_data = kind_data[kind_data['price'] > 0]
    info['model_price_max'] = kind_data.price.max()
    info['model_price_median'] = kind_data.price.median()
    info['model_price_min'] = kind_data.price.min()
    info['model_price_std'] = kind_data.price.std()
    info['model_price_average'] = round(kind_data.price.sum() / (len(kind_data) + 1), 2)
    all_info[kind] = info
model_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "model"})
data = data.merge(model_fe, how='left', on='model')

关于归一化,lgb是树模型 ,有些反馈说归一化没有什么帮助,对这个数据我没有尝试归一化,有时间可以试一试。
最后还要推荐一个被忽略的数据探索分析神器,pandas_profiling(链接直达pandas-profiling 2.5.0的详细介绍说明)。

import pandas_profiling
import pandas as pd
pd.set_option('display.max_columns',None)
#读取数据
test = pd.read_csv("used_car_testA_20200313.csv", sep=' ')
#先看下pandas的describe()
test.describe(include='all')
#再使用pandas_profiling
test_html = pandas_profiling.ProfileReport(test,minimal=True)#数据集较大时可指定ProfileReport(minimal=True)不进行相关系数等计算
test_html.to_file("test_html.html")

直接在同一目录下打开"test_html.html"即可。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
直截了当告诉咱们,这两个特征无用了哈哈哈,真的方便啊。

做在特征构造时

首先是月交易走势,交易明显与月份/季节有关。然而我们的测试集中大部分交易发生在3、4月份,迷之数据。

#used_time分布情况
data = pd.concat([train, test], ignore_index=True)
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
print(data['used_time'].describe())
#查看几个分位数
print(data['used_time'].quantile(.2),data['used_time'].quantile(.4),data['used_time'].quantile(.6),data['used_time'].quantile(.8))
count    184899.000000
mean       4435.115047
std        1953.763472
min          85.000000
25%        2945.000000
50%        4421.000000
75%        5928.000000
max        9222.000000
Name: used_time, dtype: float64
2592.0 3858.0 5043.0 6208.0

其次,二手车交易车龄分布为3年以内、3-6年、7-10年、(2016年以前考虑的是3-10年)、10年以上。但是实际平均使用天数都到4435天,也就是说平均都超过了十年…真的有点迷,我觉得这个数据可能不是国内的。因为单从交易月份和车龄都和前面参考的中国汽车流通协会的几份二手车市场分析不太一样。

# 从邮编中提取城市信息,相当于加入了先验知识
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])

在这里插入图片描述
参考中有提到先验emmm,还有小伙伴说是个欧洲的编码哈哈哈,但是这个’regionCode’(地区编码)是题目明确提到的“已脱敏”,所以我没有做这样的操作。总之,根据上述一些数据表现,推断出这个数据来源不是大中华地区了(或者数据抽样时有某种神秘规则)。故对于之前的二手车市场分析只能借鉴部分有用信息了,比如某些类别可能是影响价格的因素(基本的方向还是在的),举具体的例子就是地区还是影响着价格的而且影响非常的大(下图是我做完全部流程后的特征重要性分析图,至少在我的特征里的确对价格的影响是最大的)。

plt.figure(figsize=(12,8))
sns.barplot(y='feature', x='importance',data=feature_importance)
plt.title('importance of feature')

在这里插入图片描述

from scipy.stats import entropy


feat_cols = []

### count编码
for f in tqdm([
    'regDate', 'creatDate', 'regDate_year',
    'model', 'brand', 'regionCode'
]):
    df[f + '_count'] = df[f].map(df[f].value_counts())
    feat_cols.append(f + '_count')

### 用数值特征对类别特征做统计刻画,随便挑了几个跟price相关性最高的匿名特征
for f1 in tqdm(['model', 'brand', 'regionCode']):
    g = df.groupby(f1, as_index=False)
    for f2 in tqdm(['v_0', 'v_3', 'v_8', 'v_12']):
        feat = g[f2].agg({
            '{}_{}_max'.format(f1, f2): 'max', '{}_{}_min'.format(f1, f2): 'min',
            '{}_{}_median'.format(f1, f2): 'median', '{}_{}_mean'.format(f1, f2): 'mean',
            '{}_{}_std'.format(f1, f2): 'std', '{}_{}_mad'.format(f1, f2): 'mad'
        })
        df = df.merge(feat, on=f1, how='left')
        feat_list = list(feat)
        feat_list.remove(f1)
        feat_cols.extend(feat_list)

### 类别特征的二阶交叉
for f_pair in tqdm([
    ['model', 'brand'], ['model', 'regionCode'], ['brand', 'regionCode']
]):
    ### 共现次数
    df['_'.join(f_pair) + '_count'] = df.groupby(f_pair)['SaleID'].transform('count')
    ### n unique、熵
    df = df.merge(df.groupby(f_pair[0], as_index=False)[f_pair[1]].agg({
        '{}_{}_nunique'.format(f_pair[0], f_pair[1]): 'nunique',
        '{}_{}_ent'.format(f_pair[0], f_pair[1]): lambda x: entropy(x.value_counts() / x.shape[0])
    }), on=f_pair[0], how='left')
    df = df.merge(df.groupby(f_pair[1], as_index=False)[f_pair[0]].agg({
        '{}_{}_nunique'.format(f_pair[1], f_pair[0]): 'nunique',
        '{}_{}_ent'.format(f_pair[1], f_pair[0]): lambda x: entropy(x.value_counts() / x.shape[0])
    }), on=f_pair[1], how='left')
    ### 比例偏好
    df['{}_in_{}_prop'.format(f_pair[0], f_pair[1])] = df['_'.join(f_pair) + '_count'] / df[f_pair[1] + '_count']
    df['{}_in_{}_prop'.format(f_pair[1], f_pair[0])] = df['_'.join(f_pair) + '_count'] / df[f_pair[0] + '_count']
    
    feat_cols.extend([
        '_'.join(f_pair) + '_count',
        '{}_{}_nunique'.format(f_pair[0], f_pair[1]), '{}_{}_ent'.format(f_pair[0], f_pair[1]),
        '{}_{}_nunique'.format(f_pair[1], f_pair[0]), '{}_{}_ent'.format(f_pair[1], f_pair[0]),
        '{}_in_{}_prop'.format(f_pair[0], f_pair[1]), '{}_in_{}_prop'.format(f_pair[1], f_pair[0])
    ])

对于各类别特征的分析可以先不管三七二十一构造一堆,比如均值、方差、偏度、峰度、分位数balabala。在大佬的分享中,除了构造了这几十种特征外,还提到了“这些特征之间可能存在冗余现象,训练的时候可以依据效果尝试去掉一部分,或者拆分成两部分,做模型融合”,这也是“改在特征构造后”中最后说到的后续“重复整个特征工程”可以操作的部分,在未来的task4或者task5中可能会出现(如果试验成功的话hhh)。

改在特征构造后

把目前能想到的特征都给构造完事后,做一波相关性分析。

# 看看各数值类型特征在训练集上的相关性
numeric_features.append('price')
price_numeric = train_0[numeric_features]
correlation = price_numeric.corr()
correlation
#print(correlation['price'].sort_values(ascending = False),'\n')
plt.figure(figsize=(16, 16))
sns.heatmap(correlation, annot=True, linewidths=0.1, cmap=sns.cm.rocket_r)

在这里插入图片描述
上面代码导出来的热力图是会有正负的,实际观看体验不佳,还是绝对值下更清晰。在这里插入图片描述

在统计学中,统计数据主要可分为四种类型,分别是定类数据,定序数据,定距数据,定比变量。
1.定类数据(Nominal):名义级数据,数据的最低级,表示个体在属性上的特征或类别上的不同变量,仅仅是一种标志,没有序次关系。例如, ”性别“,”男“编码为1,”女“编码为2。定类变量之间的相关系数,只能以变量值的次数来计算,常用λ系数法;
2.定序数据(Ordinal):数据的中间级,用数字表示个体在某个有序状态中所处的位置,不能做四则运算。例如,“受教育程度”,文盲半文盲=1,小学=2,初中=3,高中=4,大学=5,硕士研究生=6,博士及其以上=7。定序变量的相关性测量常用Gamma系数法和Spearman系数法;
3.定距数据(Interval):具有间距特征的变量,有单位,没有绝对零点,可以做加减运算,不能做乘除运算。例如,温度。定距变量的相关性测量常用Pearson系数法;
4.定比变量(Ratio):数据的最高级,既有测量单位,也有绝对零点,例如职工人数,身高。一般来说,数据的等级越高,应用范围越广泛,等级越低,应用范围越受限。不同测度级别的数据,应用范围不同。等级高的数据,可以兼有等级低的数据的功能,而等级低的数据,不能兼有等级高的数据的功能。
————————————————
版权声明:本文为CSDN博主「baity940418」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/baity940418/article/details/78142247

我使用到的除了数值类型变量(按照上面这个分类其实应该属于定距数据(Interval)),就是定类数据(Nominal)、定序数据(Ordinal)。比如gearbox(变速箱)“手动:0,自动:1”就是定类数据。下面这张图看task3直播回放的时候截的。
在这里插入图片描述
定类变量和数值型数据的相关性分析可以使用eta系数,我知道SPSS是可以做这个的,可以移步百度搜索学习一下(参考文献也放了一个“spss等级变量如何进行相关性分析”链接)。目前只做了数值型特征的相关性分析也就是使用了皮尔逊相关系数(Pearson Correlation Coefficient)(这可以移步知乎学习一下)。https://www.linkedin.com/pulse/approaching-almost-any-machine-learning-problem-abhishek-thakur
实际上现在还处于Trained ML Model之前的步骤,不过下一阶段就是Machine Learning了。回到我提到的“特征工程流程:数据清洗,识别变量,形成特征”,特征工程在整个项目中不止一次出现。因为后续会根据分析结果(比如上面的相关性分析结果)与验证集再筛选特征、修改模型重复整个特征工程,一次次修改,才能修改出一个泛化能力不断增强的“好”模型。
在这里插入图片描述

Reference

  1. Datawhale 零基础入门数据挖掘-Task3 工程特征
  2. LGBParameters官方说明
  3. Seaborn简单介绍
  4. Approaching (Almost) Any Machine Learning Problem
  5. spss等级变量如何进行相关性分析
  6. 各种相关系数
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值