Pythonで欠損値の分析をしたい(missingnoを使う)

データの中には、欠損の発生自体が、意味を持っていることがあります。
特にアンケートデータでは人の心理やアンケート手段の影響、センサーでは環境の要因の影響をうけるためです。

欠損はあんまり考えず、埋めたり取り除くことが多いかもしれませんが、上記のような何かの意味がないか、確認するための分析をします。

欠損値の可視化

欠損値の可視化にはmissingnoを使うと便利です。

import missingno as msno

#欠損値の棒グラフ
msno.bar(df)
msno.matrix(df)
#欠損の相関
msno.heatmap(df)
msno.dendrogram(df)

使ってみる

データを作ってみる

欠損の発生は、単純なランダム発生だけでなく、他の値と連動して生まれるものもあります。
その辺りも含めて発生をモデリングしてみます。

  1. 時期的に欠損が発生している(取得者やシステム障害の影響)
  2. ある値の時に発生しやすくなる(深い回答がナイーブで収集しやすくなるなど)
  3. ある値が欠損になると連動する(端末そのものが壊れて、各センサーが同時に欠損するなど)
import random
class MissingData:
  def __init__(self,id):
    self.id = id
    #あるidの間は欠損しやすくする
    if id >= 25 and id <=45:
      self.a = 1 if random.random() >= 0.8 else None
    else:
      self.a = 1 if random.random() >= 0.05 else None

    #v1の時は欠損しやすくする
    self.b = random.choice(['v1','v2','v3'])
    if self.b == 'v2':
      self.c = 1 if random.random() >= 0.6 else None
    else:
      self.c = 1 if random.random() >= 0.25 else None
  
    #nullに相関を持たせる
    self.d = 1 if random.random() >= 0.5 else None
    if self.d is None:
      self.e =1 if random.random() >= 0.5 else None
    else:
      self.e = 1 if random.random() >= 0.1 else None
    
    if self.d is None or self.e is None:
      self.f = 1 if random.random() >= 0.7 else None
    else:
      self.f = 1 if random.random() >= 0.1 else None
    
  def values(self):
    return [
            self.id,
            self.a,
            self.b,
            self.c,
            self.d,
            self.e,
            self.f
    ] 

data = [MissingData(id = i) for i in range(100)]

import pandas as pd
df = pd.DataFrame([md.values() for md in data],columns = 'id,a,b,c,d,e,f'.split(','))

可視化してみる

この前に、まずはざっくりした値や欠損値の確認をすることが多いです。 esu-ko.hatenablog.com

import missingno as msno
msno.bar(df)

msno.dendrogram(df)

msno.heatmap(df)

msno.matrix(df.sort_values(['id']))

その後、大きいところから順にみていきます。

f:id:esu-ko:20200724203229p:plainf:id:esu-ko:20200724203240p:plainf:id:esu-ko:20200724203253p:plainf:id:esu-ko:20200724203302p:plain
  • 棒グラフ: 欠損値のあるカラムをざっくり把握します
  • デンドログラム : 欠損値の出現が似たグループを探します
  • 欠損の相関関係:デンドログラムだと単に欠損がないのか、や関係の強さがわからないので、相関を確認します
  • 細かいデータ:一つ一つのデータを一気に並べることで、周期性や時期の影響がないかみます

ここまでで、最初のモデルで設定した、idの期間による影響と、d,e,f通しが欠損が関係していることを特定できました。

なお、欠損の相関関係は欠損ありを1,なしを0として、その相関係数をとるようです。

関係性を探す

種類の多いデータ(idや時系列)のパターンは上の細かいデータのところでみれました。
数が少ないカテゴリカラムや数値と関係していないかを確認します。
今回は決定木を使ってみます。

▼決定木自体の記事はこちら esu-ko.hatenablog.com

from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import LabelEncoder

#目的変数をエンコード
le = LabelEncoder()
target = le.fit_transform(df['b'])

#欠損値をうめた説明変数(今回は0がない & 欠損以外が1だとわかっているので0で埋めた)
X = df[['a','c','d','e','f']].fillna(0)

#パラメータは今回は手打ち
dtc = DecisionTreeClassifier(min_samples_leaf=20)
dtc.fit(y=target,X=X)

#欠損値
dtc.feature_importances_
#array([0.        , 0.85942077, 0.        , 0.        , 0.14057923])

#木の構造を確認
from sklearn.tree import export_text
print(export_text(dtc,feature_names=list(X.columns)))

結果は下記になります。
こういう時の決定木はクロス集計を複数やって、一番うまくいく分け方を探す代わりなので、小さめにしておく方が見やすいです。
結構決め打ち気味ですが、target=bとcカラムの欠損の関係性を把握できました。

|--- c <= 0.50
|   |--- class: 1
|--- c >  0.50
|   |--- f <= 0.50
|   |   |--- class: 2
|   |--- f >  0.50
|   |   |--- class: 0