包装的异常值处理的代码,可以随便调用。
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:
"""
# 判断标准:四分位距 * scale !!!
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
# 下界过滤一次,生成bool索引
rule_low = (data_ser < val_low)
# 上界过滤一次,生成bool索引
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]
# 删除条件取并集,满足任何一个异常值删除标准都 ok
rule, value = box_plot_outliers(data_series, box_scale=scale)
# 根据 bool 索引找出被删除数据的 index
index = np.arange(data_series.shape[0])[rule[0] | rule[1]]
print("被删除的异常值数量为: {}".format(len(index)))
# 根据 index 删除对应的数据
data_n = data_n.drop(index)
data_n.reset_index(drop=True, inplace=True)
print("删除异常特征值后,数据总量为: {}".format(data_n.shape[0]))
# data_series 未被改动,对异常值进行统计描述
index_low = np.arange(data_series.shape[0])[rule[0]]
outliers = data_series.iloc[index_low]
print("对小于异常值下界的异常特征值进行统计描述:")
print(pd.Series(outliers).describe())
index_up = np.arange(data_series.shape[0])[rule[1]]
outliers = data_series.iloc[index_up]
print("对大于异常值上界的异常特征值进行统计描述:")
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
可以删掉一些异常数据,以 power 为例,但是最终删不删自行判断
但是要注意只能删除训练集的数据, 测试集的数据不能删(掩耳盗铃)!!!
# 被删除的异常值数量为: 963
Train_data = outliers_proc(Train_data, 'power', scale=3)
测试集的 price 特征为 nan
Train_data['train'] = 1
Test_data['train'] = 0
# 默认 axis=0
data = pd.concat([Train_data, Test_data], ignore_index=True)
# (199037, 32)
print(data.shape)
反应汽车使用时间,一般来说价格与使用时间成反比,公式为:data[‘creatDate’] - data[‘regDate’]
# 首先介绍日期格式于python的格式转化:
1/17/07 has the format "%m/%d/%y"
17-1-2007 has the format "%d-%m-%Y"
# 通过以上的格式,可以将DataFrame中的时间格式转换为以下等python格式:
0 2007-03-02
1 2007-03-22
2 2007-04-06
3 2007-04-14
4 2007-04-15
Name: date_parsed, dtype: datetime64[ns]
由于原数据有问题(部分时间并不是统一的格式),直接计算会报错,提醒年月日不匹配
# 报错:ValueError: time data '20070009' does not match format '%Y%m%d' (match)
data['used_time'] = (pd.to_datetime(data['creatDate'], format='%Y%m%d') - pd.to_datetime(data['regDate'], format='%Y%m%d')).dt.days
检索出报错的数据
# 有不少,几十个,除此之外,还有其他的错误格式
data[data.regDate == 20070009]
针对日期格式不一致,正确的构造方式
# 加入参数errors,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
查看某个日期数据错误处理后的结果
data[14:15].used_time
> 14 NaN
> Name: used_time, dtype: float64
type(data[14:15].used_time.values[0])
> <class 'numpy.float64'>
不可行的尝试
data[14:15].used_time.values[0] == np.nan
> False
data[14:15].used_time.values[0] == nan
> NameError: name 'nan' is not defined
data[14:15].used_time.values[0] == 'NaN'
> False
data[14:15].used_time.values[0] == 'nan'
> False
data[14:15].used_time.values[0] == float('nan')
> False
可行的方法1
from math import isnan
isnan(data[14:15].used_time.values[0])
> True
# 但是该函数不可以直接用于整个series
data[ isnan(data['used_time']) ]
> TypeError: cannot convert the series to <class 'float'>
# 但是可以作用于Series的每个元素,生成一个bool类型的Series(dytpe=bool),然后series进行过滤
data[ data['used_time'].apply(isnan) ] # 15072 rows × 33 columns
可行的方法2
# isnull() 可以直接找出所有的缺失值,不需要知道缺失值的数据类型
data['used_time'].isnull().sum() # 15072
数据中有 15072 个样本的时间是有问题的,我们可以选择删除,也可以选择放着。
但是这里不建议删除,因为删除缺失数据占总样本量过大,为7.5%。可以先放着,因为XGBoost 之类的决策树本身就能处理缺失值,所以可以不用管。
其中第四个数据处理后不是缺失值(nan),而是空的字符串
data['regionCode'][:5]
data['regionCode'][:5].apply(lambda x : str(x)[:-3])
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])
data.city.isnull().sum()
> 0
data.city[123808:123809].values[0] == ''
> True
针对训练集,也可以计算其他特征的统计量
# 分组结果不可直接查看,但可以迭代查看
Train_groups = Train_data.groupby("brand")
all_info = {}
# 对于每个商标种类和其对应的分组数据
for kind, kind_data in Train_groups:
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()
# Series 自带的不包括求均值,round 四舍五入保留两位小数
info['brand_price_average'] = round(kind_data.price.sum() / (len(kind_data) + 1), 2)
all_info[kind] = info
# 数据展示: index是每个指标的名称,columns是商标名称(0-39)
pd.DataFrame(all_info)
a = data.brand.unique()
# sort方法不返回任何参数
a.sort()
a
> array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
34, 35, 36, 37, 38, 39], dtype=int64)
brand_stats = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"})
# 基于 brand 特征(on='brand')将 brand_stats 的数据合并到 data(how='left')
data = data.merge(brand_stats, how='left', on='brand')
商标种类对应的数字本来就是0到39,重置 index后仍然不变(本来的index就是0到39,多余的一步)
用来把一组数据分割成离散的区间,并打上标签
# 部分默认参数如下:
pd.cut(x, bins, labels=None)
部分参数的含义
x
:被切分的类数组(array-like)数据,必须是1维的(不能用DataFrame);
bins
:bins是被切割后的区间(或叫“桶”),有3种形式:一个int型的标量、标量序列(数组)或者pandas.IntervalIndex 。
labels
:给分割后的bins打标签,但长度必须和划分后的区间长度相等,比如把年龄x分割成2个年龄段bins后,可以给年龄段打上诸如青年、中年的标签。如果指定labels=False,则返回x中的数据在第几个bin中(从0开始)。
data.power.isnull().sum()
> 0
bin = [i*10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels=False)
data[['power_bin', 'power']].head(3).append(data[['power_bin', 'power']].tail(3))
可以看到,有的特征被分到了NaN桶,包括小的特征值0和大的特征值334,而且缺失值也进桶了!!!
做数据分桶的原因有很多(也有很多其他原因):
- 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
- 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
- LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
- 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量变成 M*N 个变量,进一步引入非线形,提升了表达能力;
- 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
- 增强模型的泛化性能,如:LightGBM 在改进 XGBoost 时就增加了数据分桶,增强了模型的泛化性(与第二条的区别??)
drop函数
# 几个默认参数
DataFrame.drop(self, labels=None, axis=0, index=None, columns=None, inplace=False)
labels
:single label or list-likeaxis
:, default 0columns
:single label or list-likeinplace
:default Falsedata = data.drop(['creatDate', 'regDate', 'regionCode'], axis=1)
data.shape
> (199037, 39)
目前的数据其实已经可以给树模型使用了,将文件压缩保存,相比于csv格式,会剩一半内存(81M—32M)
# index=0,不保存行索引
data.to_csv('data_for_tree.gz', index=0)
index
(是否保存行索引): default Trueheader
(是否保留列名 ): default Truepd.read_csv('data_for_tree.gz')
分开构造是因为不同模型对数据集的要求不同,LR、NN需要特征归一化和分类特征独热编码??
此外,归一化(Normalization)、标准化(Standardization)和 中心化/零均值化(Zero-centered)是不同的
# pandas作图,分布图是hist,而不是distplot
data['power'].plot.hist()
前面已经对 train 数据集进行异常值处理了,现在分布极其不均的原因是 test 数据集中的 power 异常值没被处理。所以刚刚 train 中的 power 异常值还是不删为好,可以用长尾分布截断来代替
# 不能直接加载Train_data,需要进行异常值处理 ,否则也是分布极其不均
Train_data['power'].plot.hist()
对该特征取 log(原因??),再做归一化,原始的 power 特征不再保留
对特征取 log
data['power'] = np.log(data['power'] + 1)
注:np.log 的结果仍是Series,而不是 array
特征的归一化——方法一,基于 numpy 的实现
data['power'] = ((data['power'] - np.min(data['power'])) / (np.max(data['power']) - np.min(data['power'])))
data['power'].plot.hist()
注:np.min(data[‘power’]) 的结果是个标量,所以会和 data[‘power’] 进行广播
特征的归一化——方法二,基于 sklearn 的实现
# fit 或者 transform 的数据必须是二维的,单独的一个特征列或元素需要reshape为(-1,1)或(1,)
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit( data['power'].values.reshape(-1,1) )
# 序列要求数据是一维的
data['power'] = scaler.transform(data['power'].values.reshape(-1,1)).reshape(-1,)
data['power'].plot.hist()
注:sklearn 的 MinMaxScaler 方法处理的是二维数据,而特征取 log 后仍是Series,必须加 .values 转换为 numpy 后才有 reshape方法(一维转化为二维)
该特征分布比较正常,应该是已经做过分桶了
data['kilometer'].plot.hist()
该特征可以直接做归一化,分布图的形状没变化
data['kilometer'] = ( (data['kilometer'] - np.min(data['kilometer']) ) /
( np.max(data['kilometer']) - np.min(data['kilometer'])) )
data['kilometer'].plot.hist()
def max_min_Normalization(x):
"""minmax归一化,极差变换,直接作用于series,而不是series的元素"""
return (x - np.min(x)) / (np.max(x) - np.min(x))
stats_cols = ['brand_amount', 'brand_price_average', 'brand_price_max', 'brand_price_median',
'brand_price_min', 'brand_price_std', 'brand_price_sum']
for col in stats_cols:
data[col] = max_min_Normalization(data[col])
注:series 的 map 是元素级函数,DataFrame中对应的是applymap()函数,当然 DataFrame 的 apply() 函数也是元素级函数
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',
'gearbox', 'notRepairedDamage', 'power_bin'])
data.shape
> (199037, 370)
由于在取对数及归一化那里进行了两种方法,多生成了两组特征,尾缀分别是 _1 和 _2
方法一:特征挨个删除
for col in stats_cols:
data.drop(axis=1, columns=[col + '_2'], inplace=True)
data.drop(axis=1, columns=[col + '_1'], inplace=True)
方法二:特征整组删除,速度更快
# 对列表中的每个字符串元素进行相同的操作
stats_cols1 = [col + '_1' for col in stats_cols]
stats_cols2 = [col + '_2' for col in stats_cols]
data.drop(columns=stats_cols1, axis=1, inplace=True)
data.drop(columns=stats_cols2, axis=1, inplace=True)
删除单个特征
del data['power_log']
数据保存和再读取
data.to_csv('data_for_lr.gz', index=0)
data = pd.read_csv('data_for_lr.gz')
每个数值特征与预测目标相关性分析
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'))
注意:上面是两个 series之间求相关矩阵
绘制 df 的相关矩阵热力图
data_numeric = data[['power', 'kilometer', 'brand_amount', 'brand_price_average',
'brand_price_max', 'brand_price_median', 'price']]
correlation = data_numeric.corr()
# plt 设置画板,sns 作图
f, ax = plt.subplots(figsize = (7, 7))
plt.title('Correlation of Numeric Features with Price', y=1, size=16)
sns.heatmap(correlation, square = True, vmax=0.8, annot=True, fmt='.3f')
原始方法
# pip 下载 mlxtend 库,速度很快
from mlxtend.feature_selection import SequentialFeatureSelector as SFS
from sklearn.linear_model import LinearRegression
sfs = SFS(LinearRegression(),
k_features=10, # 选择10个特征,怎么选的?
forward=True,
floating=False,
scoring = 'r2',
cv = 0)
# 去除预测目标和填充缺失值,并未改变 data 数据
x = data.drop(['price'], axis=1)
x = x.fillna(0)
y = data['price']
sfs.fit(x, y)
sfs.k_feature_names_
存在问题:只是填充了特征中的缺失值,但是没有填充预测目标的缺失值,所以报错:ValueError: Input contains NaN, infinity or a value too large for dtype(‘float64’).
新的方法
# 前面都一致,不同点在后面
x = data.drop(['price'], axis=1)
x = x.fillna(0)
y = data['price'].fillna(0)
sfs.fit(x, y)
sfs.k_feature_names_
将特征选择结果绘制出来,可以看到边际效益
from mlxtend.plotting import plot_sequential_feature_selection as plot_sfs
import matplotlib.pyplot as plt
fig1 = plot_sfs(sfs.get_metric_dict(), kind='std_dev')
plt.grid()
plt.show()
排查NaN
a = data.fillna(0).isnull().sum()
a[a > 0]
b = np.isnan(data.fillna(0)).sum()
b[b > 0]
> Series([], dtype: int64) # 结果一致,都没有
排查inf
# False:不包含,所以加总和应该为样本数量,判断的标准应该是0
c = np.isfinite(data.fillna(0)).sum()
c[c == 0]
# True:包含,判断的标准应该大于0
d = np.isinf(data.fillna(0)).sum()
d[d > 0]
> Series([], dtype: int64) # 结果一致,都没有
类似的函数还有
# 正无穷(np.inf)和负无穷(-np.inf)
np.isneginf
np.isposinf
刚开始忘记找预测目标里的缺失值,导致一直找不到任何缺失值和无穷大值,所以对报错很不解!!
下一章介绍,Lasso 回归和决策树可以完成嵌入式特征选择
参考:
[1] https://tianchi.aliyun.com/notebook-ai/detail?spm=5176.12586969.1002.3.1cd8593aK2i841&postId=95501