1. 程式人生 > 實用技巧 >【12月DW打卡】joyful-pandas - 07 - pandas缺失資料 (缺失值的統計刪除、填充插值、KNN的簡單使用) + 腦圖大綱

【12月DW打卡】joyful-pandas - 07 - pandas缺失資料 (缺失值的統計刪除、填充插值、KNN的簡單使用) + 腦圖大綱

缺失資料

腦圖大綱

小結

import numpy as np
import pandas as pd

【7.2.1 練一練】

對一個序列以如下規則填充缺失值:如果單獨出現的缺失值,就用前後均值填充,如果連續出現的缺失值就不填充,即序列[1, NaN, 3, NaN, NaN]填充後為[1, 2, 3, NaN, NaN],請利用 fillna 函式實現。(提示:利用 limit 引數)

s = pd.Series([1, np.nan, 3, np.nan, np.nan],
              list('abcde'))
# s.fillna(method='both', limit=1) 不支援
ValueError: Invalid fill method. Expecting pad (ffill) or backfill (bfill). Got both
(s.fillna(method='ffill', limit=1) + s.fillna(method='bfill', limit=1))/2

a    1.0
b    2.0
c    3.0
d    NaN
e    NaN
dtype: float64
# 類似的interpolate方法,向後也是不可以的!
s.interpolate(limit_direction='both', limit=1)

a    1.0
b    2.0
c    3.0
d    3.0
e    NaN
dtype: float64

Ex1:缺失值與類別的相關性檢驗

在資料處理中,含有過多缺失值的列往往會被刪除,除非缺失情況與標籤強相關。下面有一份關於二分類問題的資料集,其中 X_1, X_2 為特徵變數, y 為二分類標籤。

df = pd.read_csv('E:\\PycharmProjects\\DatawhaleChina\\joyful-pandas\\data\\missing_chi.csv')
df.head()
X_1 X_2 y
0 NaN NaN 0
1 NaN NaN 0
2 NaN NaN 0
3 43.0 NaN 0
4 NaN NaN 0
df.isna().mean()
X_1    0.855
X_2    0.894
y      0.000
dtype: float64
df.y.value_counts(normalize=True)

0    918
1     82
Name: y, dtype: int64

事實上,有時缺失值出現或者不出現本身就是一種特徵,並且在一些場合下可能與標籤的正負是相關的。關於缺失出現與否和標籤的正負性,在統計學中可以利用卡方檢驗來斷言它們是否存在相關性。按照特徵缺失的正例、特徵缺失的負例、特徵不缺失的正例、特徵不缺失的負例,可以分為四種情況,設它們分別對應的樣例數為\(n_{11}, n_{10}, n_{01}, n_{00}\)。假若它們是不相關的,那麼特徵缺失中正例的理論值,就應該接近於特徵缺失總數\(\times\)總體正例的比例,即:

\[E_{11} = n_{11} \approx (n_{11}+n_{10})\times\frac{n_{11}+n_{01}}{n_{11}+n_{10}+n_{01}+n_{00}} = F_{11} \]

其他的三種情況同理。現將實際值和理論值分別記作\(E_{ij}, F_{ij}\),那麼希望下面的統計量越小越好,即代表實際值接近不相關情況的理論值:

\[S = \sum_{i\in \{0,1\}}\sum_{j\in \{0,1\}} \frac{(E_{ij}-F_{ij})^2}{F_{ij}} \]

可以證明上面的統計量近似服從自由度為\(1\)的卡方分佈,即\(S\overset{\cdot}{\sim} \chi^2(1)\)。因此,可通過計算\(P(\chi^2(1)>S)\)的概率來進行相關性的判別,一般認為當此概率小於\(0.05\)時缺失情況與標籤正負存在相關關係,即不相關條件下的理論值與實際值相差較大。

上面所說的概率即為統計學上關於\(2\times2\)列聯表檢驗問題的\(p\)值, 它可以通過scipy.stats.chi2(S, 1)得到。請根據上面的材料,分別對X_1, X_2列進行檢驗。

"""
2×2 列聯表檢驗問題的 p 值:
特徵缺失的正例  1
特徵缺失的負例  0
特徵不缺失的正例 1
特徵不缺失的負例 0
"""
df = pd.read_csv('E:\\PycharmProjects\\DatawhaleChina\\joyful-pandas\\data\\missing_chi.csv')
# where 和 mask ,這兩個函式是完全對稱的: where 函式在傳入條件為 False 的對應行進行替換,而 mask 在傳入條件為 True 的對應行進行替換,當不指定替換值時,替換為缺失值。"
cat_1 = df.X_1.fillna('NaN').mask(df.X_1.notna(), "NotNaN")
cat_2 = df.X_2.fillna('NaN').mask(df.X_2.notna(), "NotNaN")
# pd.crosstab計算兩個(或多個)因子的簡單交叉表。預設情況下,除非傳遞值陣列和聚合函式,否則將計算因子的頻率表。
df_1 = pd.crosstab(cat_1, df.y, margins=True)
df_2 = pd.crosstab(cat_2, df.y, margins=True)
# 計算大S值
def compute_S(my_df):
    S = []
    for i in range(2):
        for j in range(2):
            E = my_df.iat[i, j]
            F = my_df.iat[i, 2]*my_df.iat[2, j]/my_df.iat[2,2]
            S.append((E-F)**2/F)
    return sum(S)
# 分別計算
res1 = compute_S(df_1)
res2 = compute_S(df_2)
0.9712760884395901
from scipy.stats import chi2
chi2.sf(res1, 1) # X_1檢驗的p值 # 不能認為相關,剔除
0.9712760884395901
chi2.sf(res2, 1) # X_2檢驗的p值 # 認為相關,保留(為當此概率小於0.05即符合條件)
7.459641265637543e-166

結果與scipy.stats.chi2_contingency在不使用\(Yates\)修正的情況下完全一致:

# 列聯表中變數獨立性的卡方檢驗。
from scipy.stats import chi2_contingency
chi2_contingency(pd.crosstab(cat_1, df.y), correction=False)[1]
0.9712760884395901
chi2_contingency(pd.crosstab(cat_2, df.y), correction=False)[1]


7.459641265637543e-166

Ex2:用迴歸模型解決分類問題

KNN是一種監督式學習模型,既可以解決迴歸問題,又可以解決分類問題。對於分類變數,利用KNN分類模型可以實現其缺失值的插補,思路是度量缺失樣本的特徵與所有其他樣本特徵的距離,當給定了模型引數n_neighbors=n時,計算離該樣本距離最近的\(n\)個樣本點中最多的那個類別,並把這個類別作為該樣本的缺失預測類別,具體如下圖所示,未知的類別被預測為黃色:

上面有色點的特徵資料提供如下:

df = pd.read_excel('E:\\PycharmProjects\\DatawhaleChina\\joyful-pandas\\data\\color.xlsx')
df.head(3)
X1 X2 Color
0 -2.5 2.8 Blue
1 -1.5 1.8 Blue
2 -0.8 2.8 Blue

已知待預測的樣本點為\(X_1=0.8, X_2=-0.2\),那麼預測類別可以如下寫出:

from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier(n_neighbors=6)
clf.fit(df.iloc[:,:2], df.Color)
clf.predict([[0.8, -0.2]])
array(['Yellow'], dtype=object)
  1. 對於迴歸問題而言,需要得到的是一個具體的數值,因此預測值由最近的\(n\)個樣本對應的平均值獲得。請把上面的這個分類問題轉化為迴歸問題,僅使用KNeighborsRegressor來完成上述的KNeighborsClassifier功能。

翻到原始碼的Examples

    --------
    >>> X = [[0], [1], [2], [3]]
    >>> y = [0, 0, 1, 1]
    >>> from sklearn.neighbors import KNeighborsRegressor
    >>> neigh = KNeighborsRegressor(n_neighbors=2)
    >>> neigh.fit(X, y)
    KNeighborsRegressor(...)
    >>> print(neigh.predict([[1.5]]))
    [0.5]

照葫蘆畫瓢~~~

df_dummies = pd.get_dummies(df.Color)
df_dummies.head()

from sklearn.neighbors import KNeighborsRegressor
X = df[['X1', 'X2']]
# y = [0, 0, 1, 1]
stack_list = []
for col in df_dummies.columns:
    print(col)
    neigh = KNeighborsRegressor(n_neighbors=6)
    neigh.fit(X, df_dummies[col])
    # 轉為單列資料
    res = neigh.predict([[0.8, -0.2]]).reshape(-1,1)
    print(neigh.predict([[0.8, -0.2]]))
    stack_list.append(res)
stack_list
Blue
[0.16666667]
Green
[0.33333333]
Yellow
[0.5]





[array([[0.16666667]]), array([[0.33333333]]), array([[0.5]])]

由上可知: 預測成藍/綠/黃三個顏色的概率分別是0.16666667,33.3%和50%。
取最大的概率作為最終預測結果,該位置最終預測為黃色。

# np.hstack : 水平(按列)順序堆疊陣列。 [這裡返回一個單列陣列]
# argmax : Return indices of the maximum values along the given axis.
color_res = pd.Series(np.hstack(stack_list).argmax(axis=1))
df_dummies.columns[color_res[0]]
# 輸出為'Yellow'
'Yellow'
  1. 請根據第1問中的方法,對audit資料集中的Employment變數進行缺失值插補。
df = pd.read_csv('E:\\PycharmProjects\\DatawhaleChina\\joyful-pandas\\data\\audit.csv')
df.head(3)

from sklearn.neighbors import KNeighborsRegressor

# df = pd.read_csv('data/audit.csv')

res_df = df.copy()

#將婚姻狀況、性別變成one-hot向量,和年齡、收入、時間、僱用拼接在一起
# 年齡、收入、時間這三個屬性還做了標準化處理
df = pd.concat([pd.get_dummies(df[['Marital', 'Gender']]),
        df[['Age','Income','Hours']].apply(
            lambda x:(x-x.min())/(x.max()-x.min())), df.Employment],1)


X_train = df[df.Employment.notna()]

X_test = df[df.Employment.isna()]



df_dummies = pd.get_dummies(X_train.Employment)

stack_list = []

for col in df_dummies.columns:
        clf = KNeighborsRegressor(n_neighbors=6)
        clf.fit(X_train.iloc[:,:-1], df_dummies[col])
        res = clf.predict(X_test.iloc[:,:-1]).reshape(-1,1)
        stack_list.append(res)

code_res = pd.Series(np.hstack(stack_list).argmax(1))
code_res

0     2
1     0
2     4
3     4
4     4
     ..
95    4
96    4
97    4
98    4
99    4
Length: 100, dtype: int64
cat_res = code_res.replace(
    dict(
    zip(list(range(df_dummies.shape[0])), df_dummies.columns))
)

res_df.loc[res_df.Employment.isna(), 'Employment'] = cat_res.values

res_df.isna().sum()
ID            0
Age           0
Employment    0
Marital       0
Income        0
Gender        0
Hours         0
dtype: int64