思维导图:零基础入门数据挖掘的学习路径

1. 写在前面

零基础入门数据挖掘是记录自己在Datawhale举办的数据挖掘专题学习中的所学和所想, 该系列笔记使用理论结合实践的方式,整理数据挖掘相关知识,提升在实际场景中的数据分析、数据清洗,特征工程、建模调参和模型融合等技能。所以这个系列笔记共五篇重点内容, 也分别从上面五方面进行整理学习, 既是希望能对知识从实战的角度串联回忆,加强动手能力的锻炼,也希望这五篇笔记能够帮助到更多喜欢数据挖掘的小伙伴,我们一起学习,一起交流吧。

既然是理论结合实践的方式,那么我们是从天池的一个二手车交易价格预测比赛出发进行学习,既可以学习到知识,又可以学习如何入门一个数据竞赛, 下面我们开始吧。

今天是本系列的第三篇文章数据的清晰和转换技巧,依然是围绕着上面的比赛进行展开。数据清洗和转换在数据挖掘中也非常重要,毕竟数据探索我们发现了问题之后,下一步就是要解决问题,通过数据清洗和转换,可以让数据变得更加整洁和干净,才能进一步帮助我们做特征工程,也有利于模型更好的完成任务。如果看了零基础数据挖掘入门系列(二) - 数据的探索性(EDA)分析, 就会发现数据存在下面的一些问题(当然可能不全,欢迎补充交流)

  1. 有异常值, 可以用箱线图锁定异常, 但是有时候不建议直接把样本删除,截尾的方式可能会更好一些
  2. 有缺失值,尤其是类别的那些特征(bodyType, gearbox, fullType, notRepaired的那个)
  3. 类别倾斜的现象(seller, offtype), 考虑删除
  4. 类别型数据需要编码
  5. 数值型数据或许可以尝试归一化和标准化的操作
  6. 预测值需要对数转换
  7. power高度偏斜,这个处理异常之后对数转换试试
  8. 隐匿特征的相关性
  9. 存在高势集model, 及类别特征取值非常多, 可以考虑使用聚类的方式,然后在独热编码

所以今天的重点是在处理上面的问题上,整理数据清洗和转换的技巧,以方便日后查阅迁移。首先是处理异常数据,通过箱线图捕获异常点,然后截尾处理, 然后是整理一些处理缺失的技巧, 然后是数据分桶和数据转换的一些技巧, 下面我们开始。

大纲如下:

  • 异常值处理(箱线图分析删除,截尾,box-cox转换技术)
  • 缺失值处理(不处理, 删除,插值补全, 分箱)
  • 数据分桶(等频, 等距, Best-KS分桶,卡方分桶)
  • 数据转换(归一化标准化, 对数变换,转换数据类型,编码等)
  • 知识总结

Ok, let’s go!

准备工作:数据清洗的时候,我这里先把数据训练集和测试集放在一块进行处理,因为我后面的操作不做删除样本的处理, 如果后面有删除样本的处理,可别这么做。 数据合并处理也是一个trick, 一般是在特征构造的时候合并起来,而我发现这个问题中,数据清洗里面训练集和测试集的操作也基本一致,所以在这里先合起来, 然后分成数值型、类别型还有时间型数据,然后分别清洗。

"""把train_data的price先保存好"""
train_target = train_data['price']
del train_data['price']

"""数据合并"""
data = pd.concat([train_data, test_data], axis=0)
data.set_index('SaleID', inplace=True)

"""把数据分成数值型和类别型还有时间型,然后分开处理"""

"""人为设定"""
numeric_features = ['power', 'kilometer']
numeric_features.extend(['v_'+str(i) for i in range(15)]) 

categorical_features = ['name', 'model', 'brand', 'bodyType', 'fuelType', 'gearbox', 
                        'notRepairedDamage','regionCode', 'seller', 'offerType']

time_features = ['regDate', 'creatDate']

num_data = data[numeric_features]
cat_data = data[categorical_features]
time_data = data[time_features]

trick1: 就是如果发现处理数据集的时候,需要训练集和测试集进行同样的处理,不放将它们合并到一块处理。

trick2: 如果发现特征字段中有数值型,类别型,时间型的数据等,也不妨试试将它们分开,因为数值型,类别型,时间型数据不管是在数据清洗还是后面的特征工程上, 都是会有不同的处理方式, 所以这里将训练集合测试集合并起来之后,根据特征类型把数据分开, 等做完特征工程之后再进行统一的整合(set_index把它们的索引弄成一样的,整合的时候就非常简单了)。

2. 异常值处理

常用的异常值处理操作包括箱线图分析删除异常值, BOX-COX转换(处理有偏分布), 长尾截断的方式, 当然这些操作一般都是处理数值型的数据。

关于box-cox转换,一般是用于连续的变量不满足正态的时候,在做线性回归的过程中,一般线性模型假定MapReduce综合应用案例  电信数据清洗心得_数据挖掘, 其中ε满足正态分布,但是利用实际数据建立回归模型时,个别变量的系数通不过。例如往往不可观测的误差 ε 可能是和预测变量相关的,不服从正态分布,于是给线性回归的最小二乘估计系数的结果带来误差,为了使模型满足线性性、独立性、方差齐性以及正态性,需改变数据形式,故应用box-cox转换。具体详情这里不做过多介绍,当然还有很多转换非正态数据分布的方式:

  • 对数转换: MapReduce综合应用案例  电信数据清洗心得_数据_02
  • 平方根转换: MapReduce综合应用案例  电信数据清洗心得_数据挖掘_03
  • 倒数转换: MapReduce综合应用案例  电信数据清洗心得_数据挖掘_04
  • 平方根后取倒数:MapReduce综合应用案例  电信数据清洗心得_数据清洗_05
  • 平方根后再取反正弦:MapReduce综合应用案例  电信数据清洗心得_数据清洗_06
  • 幂转换:MapReduce综合应用案例  电信数据清洗心得_数据_07, 其中MapReduce综合应用案例  电信数据清洗心得_数据清洗_08, 参数MapReduce综合应用案例  电信数据清洗心得_数据清洗_09

在一些情况下(P值<0.003)上述方法很难实现正态化处理,所以优先使用Box-Cox转换,但是当P值>0.003时两种方法均可,优先考虑普通的平方变换。

Box-Cox的变换公式:
MapReduce综合应用案例  电信数据清洗心得_数据挖掘_10

具体实现:

from scipy.stats import boxcox
boxcox_transformed_data = boxcox(original_data)

当然,也给出一个使用案例:使用scipy.stats.boxcox完成BoxCox变换

好了,BOX-COX就介绍这些吧, 因为这里处理数据先不涉及这个变换,我们回到这个比赛中来,通过这次的数据介绍一下箱线图筛选异常并进行截尾:
从上面的探索中发现,某些数值型字段有异常点,可以看一下power这个字段:

# power属性是有异常点的
num_data.boxplot(['power'])

结果如下:

MapReduce综合应用案例  电信数据清洗心得_数据挖掘_11


所以,我们下面用箱线图去捕获异常,然后进行截尾, 这里不想用删除,一个原因是我已经合并了训练集和测试集,如果删除的话肯定会删除测试集的数据,这个是不行的, 另一个原因就是删除有时候会改变数据的分布等,所以这里考虑使用截尾的方式:

"""这里包装了一个异常值处理的代码,可以随便调用"""
def outliers_proc(data, col_name, scale=3):
    """
        用于截尾异常值, 默认用box_plot(scale=3)进行清洗
        param:
            data: 接收pandas数据格式
            col_name: pandas列名
            scale: 尺度
    """
    data_col = data[col_name]
    Q1 = data_col.quantile(0.25) # 0.25分位数
    Q3 = data_col.quantile(0.75)  # 0,75分位数
    IQR = Q3 - Q1
    
    data_col[data_col < Q1 - (scale * IQR)] = Q1 - (scale * IQR)
    data_col[data_col > Q3 + (scale * IQR)] = Q3 + (scale * IQR)

    return data[col_name]
 
num_data['power'] = outliers_proc(num_data, 'power')

我们再看一下数据:

MapReduce综合应用案例  电信数据清洗心得_数据挖掘_12


是不是比上面的效果好多了?当然,如果想删除这些异常点,这里是来自Datawhale团队的分享代码,后面会给出链接,也是一个模板:

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

这个代码是直接删除数据,这个如果要使用,不要对测试集用哈。下面看看power这个特征的分布:

MapReduce综合应用案例  电信数据清洗心得_数据清洗_13


也不错了,所以就没再进一步处理power,至于其他的数值型是不是需要截尾,这个看自己吧。

3. 缺失值处理

关于缺失值处理的方式,这里也是先上方法, 有几种情况:不处理(这是针对xgboost等树模型), 有些模型有处理缺失的机制,所以可以不处理,如果缺失的太多,可以考虑删除该列, 另外还有插值补全(均值,中位数,众数,建模预测,多重插补等), 还可以分箱处理,缺失值一个箱。

下面整理几种填充值的方式:

# 删除重复值
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)

# 还可以填充模型预测的值, 这一个在我正在写的数据竞赛修炼笔记的第三篇里面可以看到,并且超级精彩,还在写

再回到这个比赛中,我们在数据探索中已经看到了缺失值的情况:

MapReduce综合应用案例  电信数据清洗心得_数据_14


上图可以看到缺失情况, 都是类别特征的缺失,notRepaired这个特征的缺失比较严重, 可以尝试填充, 但目前关于类别缺失,感觉上面的方式都不太好,所以这个也是一个比较困难的地方,感觉用模型预测填充比较不错,后期再说吧,因为后面的树模型可以自行处理缺失。 当然OneHot的时候,会把空值处理成全0的一种表示,类似于一种新类型了。

4. 数据分桶

连续值经常离散化或者分离成“箱子”进行分析, 为什么要做数据分桶呢?

  1. 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
  2. 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
  3. LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
  4. 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;
  5. 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化

当然还有很多原因,LightGBM 在改进 XGBoost 时就增加了数据分桶,增强了模型的泛化性

数据分桶的方式:

  • 等频分桶
  • 等距分桶
  • Best-KS分桶(类似利用基尼指数进行二分类)
  • 卡方分桶

最好是数据分桶的特征作为新一列的特征,不要把原来的数据给替换掉, 所以在这里通过分桶的方式做一个特征出来看看,以power为例

"""下面以power为例进行分桶, 当然构造一列新特征了"""
bin = [i*10 for i in range(31)]
num_data['power_bin'] = pd.cut(num_data['power'], bin, labels=False)

当然这里的新特征会有缺失。

这里也放一个数据分桶的其他例子(迁移之用)

# 连续值经常离散化或者分离成“箱子”进行分析。 
# 假设某项研究中一组人群的数据,想将他们进行分组,放入离散的年龄框中
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
# 如果按年龄分成18-25, 26-35, 36-60, 61以上的若干组,可以使用pandas中的cut
bins = [18, 25, 35, 60, 100]         # 定义箱子的边
cats = pd.cut(ages, bins)
print(cats)   # 这是个categories对象    通过bin分成了四个区间, 然后返回每个年龄属于哪个区间
# codes属性
print(cats.codes)    #  这里返回一个数组,指明每一个年龄属于哪个区间
print(cats.categories)
print(pd.value_counts(cats))   # 返回结果是每个区间年龄的个数

# 与区间的数学符号一致, 小括号表示开放,中括号表示封闭, 可以通过right参数改变
print(pd.cut(ages, bins, right=False))

# 可以通过labels自定义箱名或者区间名
group_names = ['Youth', 'YonngAdult', 'MiddleAged', 'Senior']
data = pd.cut(ages, bins, labels=group_names)
print(data)
print(pd.value_counts(data))

# 如果将箱子的边替代为箱子的个数,pandas将根据数据中的最小值和最大值计算出等长的箱子
data2 = np.random.rand(20)
print(pd.cut(data2, 4, precision=2))   # precision=2 将十进制精度限制在2位

# qcut是另一个分箱相关的函数, 基于样本分位数进行分箱。 取决于数据的分布,使用cut不会使每个箱子具有相同数据数量的数据点,而qcut,使用
# 样本的分位数,可以获得等长的箱
data3 = np.random.randn(1000)   # 正太分布
cats = pd.qcut(data3, 4)
print(pd.value_counts(cats))

结果如下:


MapReduce综合应用案例  电信数据清洗心得_数据清洗_15

5. 数据转换

数据转换的方式, 数据归一化(MinMaxScaler), 标准化(StandardScaler), 对数变换(log1p), 转换数据类型(astype), 独热编码(OneHotEncoder),标签编码(LabelEncoder), 修复偏斜特征(boxcox1p)等。关于这里面的一些操作,我有几篇博客已经描述过了,后面会给出链接, 这里只针对这个问题进行阐述:

  1. 数值特征这里归一化一下, 因为我发现数值的取值范围相差很大
minmax = MinMaxScaler()
num_data_minmax = minmax.fit_transform(num_data)
num_data_minmax = pd.DataFrame(num_data_minmax, columns=num_data.columns, index=num_data.index)
  1. 类别特征独热一下
"""类别特征某些需要独热编码一下"""
hot_features = ['bodyType', 'fuelType', 'gearbox', 'notRepairedDamage']
cat_data_hot = pd.get_dummies(cat_data, columns=hot_features)
  1. 关于高势集特征model,也就是类别中取值个数非常多的, 一般可以使用聚类的方式,然后独热,这里就采用了这种方式:
from scipy.cluster.hierarchy import linkage, dendrogram
#from sklearn.cluster import AgglomerativeClustering
from sklearn.cluster import KMeans

ac = KMeans(n_clusters=3)
ac.fit(model_price_data)

model_fea = ac.predict(model_price_data)
plt.scatter(model_price_data[:,0], model_price_data[:,1], c=model_fea)

cat_data_hot['model_fea'] = model_fea
cat_data_hot = pd.get_dummies(cat_data_hot, columns=['model_fea'])

效果如下:

MapReduce综合应用案例  电信数据清洗心得_数据挖掘_16


但是我发现KMeans聚类不好,可以尝试层次聚类试试,并且这个聚类数量啥的应该也会有影响,这里只是提供一个思路,我觉得这个特征做的并不是太好,还需改进。

数据清洗和转换的技巧就描述到这里,但是不能说是结束,因为这一块的知识没有什么固定的套路,我们得学会发散思维,然后不断的试错探索,这里只整理的部分方法。

trick3: 通过上面的方式处理完了数据之后,我们要记得保存一份数据到文件,这样下次再用的时候,就不用再花功夫处理,直接导入清洗后的数据,进行后面的特征工程部分即可。一定要养成保存数据到文件的习惯。

6. 总结

今天主要是整理一些数据清洗和转换的技巧,包括异常处理,缺失处理,数据分箱和数据转换操作, 这些技巧也同样不仅适用于这个比赛,还可以做迁移。依然是利用思维导图把知识进行串联:

MapReduce综合应用案例  电信数据清洗心得_数据_17

关于经验的话,数据清洗和转换这一块只能整理一些方法,然后需要针对具体的数据不断的尝试, 只有亲自尝试才能获得更多的成长,没有什么固定的套路或者说方式,没有什么循规蹈矩的规定,当然也希望不要把思维限定在上面的这些方法中,因为毕竟目前我也是小白,这些只是我目前接触到的一些,所以肯定不会包括所有的方式,希望有大佬继续补充和交流。

另外,我觉得分享本身就是一种成长,因为分享知识在帮助自己加深记忆的同时,也是和别人进行思维碰撞的机会,这个过程中会遇到很多志同道合的人一起努力,一起进步,这样比一个人要好的多。 一个人或许会走的很快,但是一群人才能走的更远,所以希望这个系列能帮助更多的伙伴,也希望学习路上可以遇到更多的伙伴, 你看,天上太阳正晴,我们一起吧 😉

对了,上面的数据清洗过程,再做几个特征,可以让误差降低60多,也算是有点用吧,不过不是太理想,还需要进一步探索这块, 希望和更多的小伙伴一起试错,一起交流,然后一起成长。

参考:

  • Datawhale 零基础入门数据挖掘-Task3 特征工程
  • 离散数据编码方式总结(OneHotEncoder、LabelEncoder、OrdinalEncoder、get_dummies、DictVectorizer、to_categorical的区别?)
  • 真的明白sklearn.preprocessing中的scale和StandardScaler两种标准化方式的区别吗
  • 打AI比赛的模板整理
  • 类别型数据的预处理方法
  • 缺失值填充的几种方法
  • 机器学习中的异常值检测和处理
  • 特征预处理和特征生成 (三)缺失值的处理
  • 机器学习sklearn(13)层次聚类
  • 数据清洗与准备