面对失败,不彷徨;面对抉择,不犹豫;面对挑战,不惧怕。
查看缺失值和重复值
在这里说一下哪些算缺失值。比如某一列明明该有一个数,但是却压根就没有数。这会导致程序运行的时候报错,提示无法转换NaN
(Not a Number)。
天池给的这数据集里缺失值特别多,刚开始跑程序的时候到处都报错NaN。所以查看一下缺失值情况是必要的,下面代码用来查看缺失值和重复值:
missing=data_all.isnull().sum()
missing=missing[missing>0]
print(missing)
print(data_all['bodyType'].value_counts())
print(data_all['fuelType'].value_counts())
print(data_all['gearbox'].value_counts())
解释一下这里的isnull().sum()和value_counts()。
isnull()
:该函数会返回一个布尔类型(即True
或False
)的矩阵,表示了原矩阵对应位置是否为NaN缺失值。
isnull().sum()
:sum()会在isnull()返回的布尔矩阵的基础上做累加。我们都知道在语言底层的实现中,True=1
,False=0
。
因此isnull().sum()输出的是矩阵的每一列有多少个缺失值(因为只要有一个缺失值就为True。假设某一列有Q个缺失值,那sum输出的这一列的数值就是Q*1=Q
)。它直接告诉了我们每列缺失值的数量。
value_counts()
:该函数用于查看表格某列中有多少个不同的值,并计算每个不同的值在该列中有多少个重复值。返回的内容相当于一个列表。
数值特征的数据分析
数值特征,指的是该特征的表现形式必须是具体的数值大小,与分类特征相对应(即分类特征的数值表现的是类别,比如0表示优质,1表示劣质)。除此之外,特征工程还有时间特征、文本特征等。
下面代码中,num_features
是我们选出的数值特征,包括功率(power)、价格(price)以及一些匿名数值特征。对应地,categorical_features
则是分类特征,包括名称(name)、商标(brand)等。
这段代码的作用是打印出每一列的不同值以及唯一值的数量等信息,实现的是对原数据集的观察,这就节省了我们自己打开Excel一行行看的时间,方便我们后续操作:
num_features=[ 'kilometer','power','price', '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']
for num in num_features:
print('{}特征有{}个不同值'.format(num,data_all[num].nunique()))
temp=data_all[num].value_counts()
print(temp)
print(pd.DataFrame(data_all[num]).describe())
上述代码中,describe()
函数计算 数据列的各种数据统计量,在这里可以理解为我们简要观察一下矩阵的基本内容。关于describe()的详细参数可以参考:
https://blog.csdn.net/m0_45210226/article/details/108942526
另外,这部分代码中用到一个新的函数nunique()
。
在英语中,unique是“单独”、“唯一”的意思。因此这里的nunique就是n个unique,即“有n个唯一的值”。
补充一点pandas的知识吧。在pandas中有unique()
和nunique()
两个方法:
(1)unique()
是以数组形式(numpy.ndarray)返回所选列的所有唯一值(特征的所有唯一值)。
(2)nunique()
返回的是唯一值的个数。
分类特征的数据分析
讲完了数值特征,当然接着就是分类特征啦!
这里的categorical_features
是分类特征,包括名称(name)、商标(brand)等。
这段代码和上面数值特征部分的代码差不多,作用是要观察一下分类特征数据的基本情况:
categorical_features=['model','name', 'brand', 'notRepairedDamage','bodyType', 'fuelType', 'gearbox','regionCode']
for cat in categorical_features:
print('{}特征有{}个不同值'.format(cat,data_all[cat].nunique()))
temp=data_all[cat].value_counts()
print(temp)
print(pd.DataFrame(data_all[cat]).describe())
清理异常数值
天池给的数据集实在是太坑,里面不但有NaN,还有“-”这种特殊字符。好端端的数值型矩阵,多了“-”之后就成了字符串型矩阵,这得多闹心啊,数据都分析不了。
我和Q同学一起运行代码的时候在这上面头秃了好久。期间还用Excel暴力清理“-”符号,但Excel的替换效率实在是太慢Orz
这里我们可以先输入一段代码,查看一下数据类型:
print(data_all.info())
输出的结果如下:
大家看到notRepairedDamage
这一列数据有多坑了吧!别人都是float
或者int
数值类型只有它是object对象类型!
机器学习模型运算的时候是以数值型矩阵运算的,所以object对象类型是解析不了的,要把它换掉。
这里我们的办法是,先把“-”全都替换成NaN,后续再按填充NaN的办法全都补上数值就好了。
因此清理异常数值‘-’
的代码如下:
data_all['notRepairedDamage'].replace('-', np.nan, inplace=True)
replace()
:把对象中的第一个参数替换为第二个参数,在这里是把’-'替换为NaN。这个函数有一个参数inplace
,英语的意思是“就地更改”。如果设为false,就是replace的时候不更改原来的对象,而是返回一个修改后的新对象。如果设为True,就直接在原对象上修改了,所以我们要设为True。
剔除数据不明显的特征
根据天池Datawhale上的教程,seller
和offerType
这两列的类别特征严重倾斜,一般不会对预测有什么帮助,所以完全可以删掉这两列。
drop()
方法可以对DataFrame对象删除指定行和列,方法是在第一个参数里罗列出要删掉的列名。另外,axis=1
时删除列,axis=0
时删除行。
具体的数据删除代码如下:
#删掉没用的两列
data_all=data_all.drop(['seller','offerType'],axis=1)
for cat in categorical_features:
data_all[(data_all['type']=='train')][cat].value_counts().plot(kind='box')
plt.show()
for num in num_features:
sns.distplot(data_train[num],kde=False,fit=stats.norm)
plt.show()
这部分代码里,写了两个用matplotlib
画图的循环,其作用是针对数值特征或者分类特征的每一列作箱线图,观察数据分布。
sns.distplot()
集合了matplotlib的hist()
与sns.kdeplot()
功能,作用是绘制直方图 + 核密度曲线,这里咱们了解它的应用就行。
详细内容可以查看:
https://blog.csdn.net/pythonxiaopeng/article/details/109642444
贴两张画出来的图像:
上面这张是brand
商标的数据分布图看起来差距还蛮大的哈。下面这张是我们刚才数据清理过的notRepairedDamage
,看起来还挺友好是不是。
查看特征分布情况
这一部分就是调用sns.kdeplot
查看一下我们主要特征的分布情况。
sns.kdeplot()
函数用来画图比较两个变量的关系,是否符合线性回归。一般用来比较特征变量和标签变量上。他有个专业名词叫核密度估计(Kernel density estimaton)。
所谓sns
,其实是我们import seaborn as sns
的结果。seaborn
是一个类似于matplotlib
的Python图形库,也是用来画图。它其实是在matplotlib
的基础上进行了更高级的API封装,从而使得作图更加容易。
绘制出特征分布的代码如下:
feature=[ 'name', 'model', 'brand', 'bodyType', 'fuelType',
'gearbox', 'power', 'kilometer', 'notRepairedDamage', 'regionCode',
'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']
for i in feature:
g=sns.kdeplot(data=data_all[i][(data_all['type']=='train')],color='Red',shade=True)
g = sns.kdeplot(data=data_all[i][(data_all['type'] == 'test')],ax=g, color='Blue', shade=True)
g.set_xlabel(i)
g.set_ylabel("Frequency") #纵轴即特征变量,具体数值与核密度估计数学实现有关
g = g.legend(["train", "test"])
plt.show()
这里展示一下我画出来的结果,其实就是绘制出训练集和测试集数据的基本分布,看起来一目了然。
上面这张是name(名称)的特征分布,下面这张是brand(商标)的特征分布。这个图像作为辅助观察,大概看一看就好了。
分析特征的相关性,并筛除相关性低的
这一部分代码对应特征筛选,用来计算相关性。
corr()
函数用于检查两个变量之间变化趋势的方向以及程度,值范围-1到+1。0表示两个变量不相关,正值表示正相关,负值表示负相关,值越大相关性越强。
method
参数可选pearson、spearman、kendall,pointbiserialr等相关系数,这里用的是斯皮尔曼相关性系数。如果要优化,至于具体选哪个,这是数学角度要考虑的啦!我们就先做应用。
相关性计算代码如下:
feature=['price','name', 'model', 'brand', 'bodyType', 'fuelType',
'gearbox', 'power', 'kilometer', 'notRepairedDamage', 'regionCode',
'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']
corr=data_all[feature].corr(method='spearman')
corr=pd.DataFrame(corr) #用pandas的DataFrame封装一下相关性矩阵
#这是因为seaborn库是基于pandas的,所以必须用pandas的数据类型嘛
sns.heatmap(corr,fmt='0.2f') #fmt就是数据格式format,代表输出的数据类型
plt.show()
# 删掉相关性比较低的特征
data_all=data_all.drop(['regionCode'],axis=1)
这里程序使用sns绘制了一张heatmap
(热度图)。
至于什么是heatmap
,我把画出来的结果放上大家就明白了:
可以看到一目了然,regionCode
(地区编码)和其它因子的相关度都不够高(所有相关性几乎为0),所以本段代码中,我们用drop
把这一列删掉
查看时间特征
时间特征也是一个很重要的特征,因为我们后续要用两个时间特征的变量相减,计算出汽车的使用寿命。这里我们调用pd.to_datetime()
,该函数能够将Str和Unicode转化为时间格式,因为原数据集里面的时间全都是字符串,比如20210330,而不是我们想要的2021-03-30。pd.to_datetime()
能帮我们实现这种从字符到日期的自动转换!
具体日期转换代码如下:
data_all['regDate']= pd.to_datetime(data_all['regDate'], format='%Y%m%d', errors='coerce')
data_all['creatDate']=pd.to_datetime(data_all['creatDate'], format='%Y%m%d', errors='coerce')
print(data_all['regDate'].isnull().sum())
# 在regDate里,有一些数据的格式出错,如20070009,可是我们没有0月
#所以我们需要 errors='coerce'。errors遇到错误会将该值设置为NaN,方便我们后续补全空值
print(data_all['creatDate'].isnull().sum()) #查看有多少空行
接下来到了比较关键的一步了,这也是特征工程数据处理中比较典型的一种做法:
data_all['used_time']=(data_all['creatDate']-data_all['regDate']).dt.days
data_all=data_all.drop(['SaleID','regDate','creatDate','type'],axis=1)#删掉不需要的列
# 使用时间:data['creatDate'] - data['regDate'],反应汽车使用时间,一般来说价格与使用时间成反比
# 不过要注意,数据里有时间出错的格式,
这里我们新构造了一个used_time
列,顾名思义,它代表汽车的使用时长,它相当于提炼了原creatDate
(上线时间)和regDate
(注册时间)的信息。
查看品牌和价格特征
这一部分内容本代码比天池Datawhale上面写的要简洁,针对brand和model这两列做聚类然后求平均。解释一下代码的意思:
groupby()
函数会将在指定列中值相同的元素合并为一个组,因此它返回的结果是一个按组别分配的矩阵。代码相当于对同一商标(brand)的汽车的价格求平均,并对同一车型(model)的汽车的价格也求平均和中位数。
mean()
函数用来求平均值,默认按竖列求平均,如果想对行求平均,则用mean(1)
。
median()
求的是中位数,用法与mean()
类似。
brand_and_price_mean=data_all.groupby('brand')['price'].mean()
model_and_price_mean=data_all.groupby('model')['price'].mean()
brand_and_price_median=data_all.groupby('brand')['price'].median()
model_and_price_median=data_all.groupby('model')['price'].median()
data_all['brand_and_price_mean']=data_all.loc[:,'brand'].map(brand_and_price_mean)
data_all['model_and_price_mean']=data_all.loc[:,'model'].map(model_and_price_mean).fillna(model_and_price_mean.mean())
data_all['brand_and_price_median']=data_all.loc[:,'brand'].map(brand_and_price_mean)
data_all['model_and_price_median']=data_all.loc[:,'model'].map(model_and_price_mean).fillna(model_and_price_median.mean())
后四行代码使用了pandas一个比较巧妙的函数map(),其作用是将调用对象里面的值,根据map里面的键值对(键值对,即key:value形式,类似于字典)重新赋值(专业一点叫映射)。关于map的基本应用,可以借用一张图来表现:
可以看到,map里面实际上相当于提供了一个字典,调用者根据自己内部的数据按图索骥,就可以得到对应的新值。
因此不难理解,这段代码中,实际上是将相同汽车的brand_and_price_mean
、model_and_price_mean
、brand_and_price_median
和model_and_price_median
这几个新增的属性列都按照汽车的品牌价格赋为同一个值。
我个人觉得这更像是对特征做了一次整合,构造一些新的特征。这也符合特征工程之特征构造的几个构造方向:
- 构造统计量特征,报告计数、求和、比例、标准差等;
- 时间特征,包括相对时间和绝对时间,节假日,双休日等;
- 地理信息,包括分箱,分布编码等方法;
- 非线性变换,包括 log/ 平方/ 根号等;
- 特征组合,特征交叉;
- 仁者见仁,智者见智,根据实际情况构造。