浏览了官方eda过程,觉得操作有点多,一时消化不掉
学习了天才儿童大佬的EDA
首先把训练集读进来简单看看各列的情况,主要看一下预测目标price的情况,发现均值在5900左右,标准差在7500左右,然而最大值居然有99999,可以看出事情不简单,回归题最怕存在离群点…
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max_columns', None)
train_df = pd.read_csv('G:/tianchi/dataMining_cars/used_car_train_20200313.csv', sep=' ')
print(train_df.shape)
train_df.describe()
直接画图看一下,发现跟正态分布相差有一点远,远处的离群点看起来还不少,训练起来误差估计会很大,这些离群点没办法准确预测,训练的时候可以考虑去掉,但如果测试集也有类似的点,那就没办法了,回归场景里面一个离群点带来的误差就能拖垮整个数据集上的指标分数。
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure()
sns.distplot(train_df['price'])
plt.figure()
train_df['price'].plot.box()
plt.show()
把测试集读进来,看看全数据集的情况。
import gc
test_df = pd.read_csv('datalab/231784/used_car_testA_20200313.csv', sep=' ')
print(test_df.shape)
df = pd.concat([train_df, test_df], axis=0, ignore_index=True)
del train_df, test_df
gc.collect()
df.head()
把特征分成三部分,分别是日期特征、类别特征、数值特征。然后看看每一维特征的缺失率、n unique等信息,可以发现seller、offerType这两个特征可以删掉了,所有样本就一个取值,没什么用。从这里还可以发现匿名特征里面的v_0到v_4、v_10到v_14感觉长的有点像,貌似有很多相似的地方
date_cols = ['regDate', 'creatDate']
cate_cols = ['name', 'model', 'brand', 'bodyType', 'fuelType', 'gearbox', 'notRepairedDamage', 'regionCode', 'seller', 'offerType']
num_cols = ['power', 'kilometer'] + ['v_{}'.format(i) for i in range(15)]
cols = date_cols + cate_cols + num_cols
tmp = pd.DataFrame()
tmp['count'] = df[cols].count().values
tmp['missing_rate'] = (df.shape[0] - tmp['count']) / df.shape[0]
tmp['nunique'] = df[cols].nunique().values
tmp['max_value_counts'] = [df[f].value_counts().values[0] for f in cols]
tmp['max_value_counts_prop'] = tmp['max_value_counts'] / df.shape[0]
tmp['max_value_counts_value'] = [df[f].value_counts().index[0] for f in cols]
tmp.index = cols
tmp
把日期列处理一下,提取年、月、日、星期等信息。这里有些日期异常的样本,月份出现了0,因此需要开个函数单独处理一下
from tqdm import tqdm
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):
df[f] = pd.to_datetime(df[f].astype('str').apply(date_proc))
df[f + '_year'] = df[f].dt.year
df[f + '_month'] = df[f].dt.month
df[f + '_day'] = df[f].dt.day
df[f + '_dayofweek'] = df[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 = df[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()
来看一下各个数值特征跟price的相关性。
跟price相关性比较高的有汽车注册年份(regDate_year),应该可以理解为车越新,价格越高;汽车已行驶公里数(kilometer)也还行,应该可以理解为跑的路程越多,车就越旧,价格就越低;匿名特征里面的v_0、v_3、v_8、v_12看起来跟price的相关性很高,原因就不知道了。除了跟price的相关性,还可以发现有些特征跟特征之间的相关性也很高,比如v_1跟v_6、v_2跟v_7、v_3跟v_8、v_4跟v_9等,这些特征之间可能存在冗余现象,训练的时候可以依据效果尝试去掉一部分,或者拆分成两部分,做模型融合
corr1 = abs(df[~df['price'].isnull()][['price'] + date_cols + num_cols].corr())
plt.figure(figsize=(10, 10))
sns.heatmap(corr1, linewidths=0.1, cmap=sns.cm.rocket_r)
接下来看看15维匿名特征分别在训练集和测试集上的分布,如果发现分布不一致的,可以尝试处理。但是貌似这15维特征在训练集和测试集上的分布基本上都挺一致的,无论多奇怪的分布,两个数据集上都挺一致
plt.figure()
plt.figure(figsize=(15, 15))
i = 1
for f in num_cols[2:]:
plt.subplot(5, 3, i)
i += 1
sns.distplot(df[~df['price'].isnull()][f], label='train', color='y', hist=False)
sns.distplot(df[df['price'].isnull()][f], label='test', color='g', hist=False)
plt.tight_layout()
plt.show()
简单看几个n unique比较小的特征上面的price的均值的分布。可以发现regDate_year和kilometer的趋势变化很明显,这也对应了上面的相关性分布,这两个特征跟price的相关性都挺高。kilometer应该是被离散化过的,只保留了整数
plt.figure()
plt.figure(figsize=(20, 18))
i = 1
for f in cate_cols + date_cols + num_cols:
if df[f].nunique() <= 50:
plt.subplot(5, 3, i)
i += 1
v = df[~df['price'].isnull()].groupby(f)['price'].agg({f + '_price_mean': 'mean'}).reset_index()
fig = sns.barplot(x=f, y=f + '_price_mean', data=v)
for item in fig.get_xticklabels():
item.set_rotation(90)
plt.tight_layout()
plt.show()
下面简单做一做特征工程,然后开始训练模型,其实也就是实践了几个常用的套路
df['notRepairedDamage'] = df['notRepairedDamage'].astype('str').apply(lambda x: x if x != '-' else None).astype('float16')
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])
])
from sklearn.model_selection import KFold
train_df = df[~df['price'].isnull()].reset_index(drop=True)
test_df = df[df['price'].isnull()].reset_index(drop=True)
### target encoding目标编码,回归场景相对来说做目标编码的选择更多,不仅可以做均值编码,还可以做标准差编码、中位数编码等
enc_cols = []
stats_default_dict = {
'max': train_df['price'].max(),
'min': train_df['price'].min(),
'median': train_df['price'].median(),
'mean': train_df['price'].mean(),
'sum': train_df['price'].sum(),
'std': train_df['price'].std(),
'skew': train_df['price'].skew(),
'kurt': train_df['price'].kurt(),
'mad': train_df['price'].mad()
}
### 暂且选择这三种编码
enc_stats = ['mean', 'std', 'mad']
skf = KFold(n_splits=5, shuffle=True, random_state=2020)
for f in tqdm(['model', 'brand', 'regionCode']):
enc_dict = {}
for stat in enc_stats:
enc_dict['{}_target_{}'.format(f, stat)] = stat
train_df['{}_target_{}'.format(f, stat)] = 0
test_df['{}_target_{}'.format(f, stat)] = 0
enc_cols.append('{}_target_{}'.format(f, stat))
for i, (trn_idx, val_idx) in enumerate(skf.split(train_df, train_df['price'])):
trn_x, val_x = train_df.iloc[trn_idx].reset_index(drop=True), train_df.iloc[val_idx].reset_index(drop=True)
enc_df = trn_x.groupby(f, as_index=False)['price'].agg(enc_dict)
val_x = val_x[[f]].merge(enc_df, on=f, how='left')
test_x = test_df[[f]].merge(enc_df, on=f, how='left')
for stat in enc_stats:
val_x['{}_target_{}'.format(f, stat)] = val_x['{}_target_{}'.format(f, stat)].fillna(stats_default_dict[stat])
test_x['{}_target_{}'.format(f, stat)] = test_x['{}_target_{}'.format(f, stat)].fillna(stats_default_dict[stat])
train_df.loc[val_idx, '{}_target_{}'.format(f, stat)] = val_x['{}_target_{}'.format(f, stat)].values
test_df['{}_target_{}'.format(f, stat)] += test_x['{}_target_{}'.format(f, stat)].values / skf.n_splits
cols = cate_cols + date_cols + num_cols + feat_cols + enc_cols
sub = test_df[['SaleID']].copy()
test_df = test_df[cols]
labels = train_df['price'].values
train_df = train_df[cols]
print(train_df.shape)
train_df.head()
from sklearn.model_selection import KFold
from lightgbm.sklearn import LGBMRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error
import time
oof = np.zeros(train_df.shape[0])
sub['price'] = 0
feat_imp_df = pd.DataFrame({'feat': cols, 'imp': 0})
skf = KFold(n_splits=5, shuffle=True, random_state=2020)
### 参数可以重新设置一下,天池这里面太慢了所以随便跑跑
clf = LGBMRegressor(
learning_rate=0.1,
n_estimators=1000,
num_leaves=31,
subsample=0.8,
colsample_bytree=0.8,
random_state=2020,
metric=None
)
for i, (trn_idx, val_idx) in enumerate(skf.split(train_df, labels)):
print('--------------------- {} fold ---------------------'.format(i))
t = time.time()
trn_x, trn_y = train_df.iloc[trn_idx].reset_index(drop=True), labels[trn_idx]
val_x, val_y = train_df.iloc[val_idx].reset_index(drop=True), labels[val_idx]
clf.fit(
trn_x, trn_y,
eval_set=[(val_x, val_y)],
categorical_feature=cate_cols,
eval_metric='mae',
early_stopping_rounds=100,
verbose=200
)
feat_imp_df['imp'] += clf.feature_importances_ / skf.n_splits
oof[val_idx] = clf.predict(val_x)
sub['price'] += clf.predict(test_df) / skf.n_splits
print('val mse:', mean_squared_error(val_y, oof[val_idx]))
print('runtime: {}\n'.format(time.time() - t))
mae = mean_absolute_error(labels, oof)
mse = mean_squared_error(labels, oof)
print('cv mae:', mae)
print('cv mse:', mse)
print('sub mean:', sub['price'].mean())
# sub.to_csv('sub_{}_{}_{}.csv'.format(mae, mse, sub['price'].mean()), index=False)
sns.distplot(labels, label='train', color='y', hist=False)
sns.distplot(oof, label='oof', color='g', hist=False)
sns.distplot(sub['price'], label='test', color='r', hist=False)
plt.figure(figsize=(15, 30))
feat_imp_df = feat_imp_df.sort_values('imp').reset_index(drop=True)
sns.barplot(x='imp', y='feat', data=feat_imp_df)
该notebook只是提供一个简单的EDA过程和baseline,不保证分数有多高,特征工程也不保证一定有效,只是教给新人一些常用套路。还有很多东西需要继续做,比如一些跟price相关性并不大的特征为什么被lgb跑出来变得很重要、price的分布太奇怪是否可以尝试做一下处理(去掉离群点、做个转换再训练等)、有些匿名特征为什么长得那么像、相关性很高的特征冗余现象是否需要处理一下等等
我理解的EDA过程,是对数据集整体性地一些数据分析,用概率统计的角度,通过专有的数据分析库进行操作,探索各属性之间的关系以及相关性,在对赛题理解,背景理解的基础之上,对数据集的“模样”有了更深层次的了解。