零基礎數據挖掘入門系列(三) - 數據清洗和轉換技巧

思維導圖:零基礎入門數據挖掘的學習路徑

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轉換,一般是用於連續的變量不滿足正態的時候,在做線性迴歸的過程中,一般線性模型假定Y=Xβ+εY=Xβ + ε, 其中ε滿足正態分佈,但是利用實際數據建立迴歸模型時,個別變量的係數通不過。例如往往不可觀測的誤差 ε 可能是和預測變量相關的,不服從正態分佈,於是給線性迴歸的最小二乘估計係數的結果帶來誤差,爲了使模型滿足線性性、獨立性、方差齊性以及正態性,需改變數據形式,故應用box-cox轉換。具體詳情這裏不做過多介紹,當然還有很多轉換非正態數據分佈的方式:

  • 對數轉換: yi=ln(xi)y_i = ln(x_i)
  • 平方根轉換: yi=xiy_i = \sqrt {x_i}
  • 倒數轉換: yi=1xiy_i = \frac{1}{x_i}
  • 平方根後取倒數:yi=1xiy_i = \frac{1}{\sqrt{x_i}}
  • 平方根後再取反正弦:yi=Arcsin(xi)y_i = Arcsin(\sqrt{x_i})
  • 冪轉換:yi=xiλ1x~λ+1y_i = \frac{x^\lambda_i-1}{\tilde{x}^{\lambda+1}}, 其中x~=(i=1nxi)1n\tilde{x}=\left(\prod_{i=1}^{n} x_{i}\right)^{\frac{1}{n}}, 參數λ[1.5,1]\lambda \in[-1.5,1]

在一些情況下(P值<0.003)上述方法很難實現正態化處理,所以優先使用Box-Cox轉換,但是當P值>0.003時兩種方法均可,優先考慮普通的平方變換。

Box-Cox的變換公式:
y(λ)={(y+c)λ1λ, if λ0log(y+c), if λ=0y^{(\lambda)}=\left\{\begin{array}{c} \frac{(y+c)^{\lambda}-1}{\lambda}, \text { if } \lambda \neq 0 \\ \log (y+c), \text { if } \lambda=0 \end{array}\right.

具體實現:

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

當然,也給出一個使用案例:使用scipy.stats.boxcox完成BoxCox變換

好了,BOX-COX就介紹這些吧, 因爲這裏處理數據先不涉及這個變換,我們回到這個比賽中來,通過這次的數據介紹一下箱線圖篩選異常並進行截尾:
從上面的探索中發現,某些數值型字段有異常點,可以看一下power這個字段:

# power屬性是有異常點的
num_data.boxplot(['power'])

結果如下:
在這裏插入圖片描述
所以,我們下面用箱線圖去捕獲異常,然後進行截尾, 這裏不想用刪除,一個原因是我已經合併了訓練集和測試集,如果刪除的話肯定會刪除測試集的數據,這個是不行的, 另一個原因就是刪除有時候會改變數據的分佈等,所以這裏考慮使用截尾的方式:

"""這裏包裝了一個異常值處理的代碼,可以隨便調用"""
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')

我們再看一下數據:
在這裏插入圖片描述
是不是比上面的效果好多了?當然,如果想刪除這些異常點,這裏是來自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這個特徵的分佈:
在這裏插入圖片描述
也不錯了,所以就沒再進一步處理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)

# 還可以填充模型預測的值, 這一個在我正在寫的數據競賽修煉筆記的第三篇裏面可以看到,並且超級精彩,還在寫

再回到這個比賽中,我們在數據探索中已經看到了缺失值的情況:
在這裏插入圖片描述
上圖可以看到缺失情況, 都是類別特徵的缺失,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))

結果如下:

在這裏插入圖片描述

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)
    
  2. 類別特徵獨熱一下

    """類別特徵某些需要獨熱編碼一下"""
    hot_features = ['bodyType', 'fuelType', 'gearbox', 'notRepairedDamage']
    cat_data_hot = pd.get_dummies(cat_data, columns=hot_features)
    
  3. 關於高勢集特徵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'])
    

    效果如下:
    在這裏插入圖片描述
    但是我發現KMeans聚類不好,可以嘗試層次聚類試試,並且這個聚類數量啥的應該也會有影響,這裏只是提供一個思路,我覺得這個特徵做的並不是太好,還需改進。

數據清洗和轉換的技巧就描述到這裏,但是不能說是結束,因爲這一塊的知識沒有什麼固定的套路,我們得學會發散思維,然後不斷的試錯探索,這裏只整理的部分方法。

trick3: 通過上面的方式處理完了數據之後,我們要記得保存一份數據到文件,這樣下次再用的時候,就不用再花功夫處理,直接導入清洗後的數據,進行後面的特徵工程部分即可。一定要養成保存數據到文件的習慣。

6. 總結

今天主要是整理一些數據清洗和轉換的技巧,包括異常處理,缺失處理,數據分箱和數據轉換操作, 這些技巧也同樣不僅適用於這個比賽,還可以做遷移。依然是利用思維導圖把知識進行串聯:

在這裏插入圖片描述

關於經驗的話,數據清洗和轉換這一塊只能整理一些方法,然後需要針對具體的數據不斷的嘗試, 只有親自嘗試才能獲得更多的成長,沒有什麼固定的套路或者說方式,沒有什麼循規蹈矩的規定,當然也希望不要把思維限定在上面的這些方法中,因爲畢竟目前我也是小白,這些只是我目前接觸到的一些,所以肯定不會包括所有的方式,希望有大佬繼續補充和交流。

另外,我覺得分享本身就是一種成長,因爲分享知識在幫助自己加深記憶的同時,也是和別人進行思維碰撞的機會,這個過程中會遇到很多志同道合的人一起努力,一起進步,這樣比一個人要好的多。 一個人或許會走的很快,但是一羣人才能走的更遠,所以希望這個系列能幫助更多的夥伴,也希望學習路上可以遇到更多的夥伴, 你看,天上太陽正晴,我們一起吧 😉

對了,上面的數據清洗過程,再做幾個特徵,可以讓誤差降低60多,也算是有點用吧,不過不是太理想,還需要進一步探索這塊, 希望和更多的小夥伴一起試錯,一起交流,然後一起成長。

參考

PS: 本次數據挖掘學習,專題知識將在天池分享,詳情可關注公衆號Datawhale。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章