欠損値ってなんやねん
自己紹介
どもども、フリーランスエンジニアとして働いている井上弥風(いのうえみふう)です。
最近魚の飼育に凝ってまして、最初はネオンテトラを15匹だけ購入したつもりが、なぜか今55匹になってる今日この頃です。
もうハマりにハマっちゃって水槽の水でミニトマト育て始めちゃってる(クソ情報失礼しました)
はじめに
私について
私は元々バックエンド・インフラメインで仕事をしていて、AI周りの学習は今年の2月に始めました。
とりあえずG検定・AWS AIF周りの試験勉強をして、何となく全体像を知った後、実際にXGBoostという決定木ベースのモデルで開発を始めました。
今もこのモデルで開発をしていて、あわよくば収益化を目指している(マジで可能性あると思っている領域なのでおもろい)のですが、やっぱり我流で進めているので色々と壁にぶつかってる、、、って状況です。
この記事を書いた背景
ぶつかった内容を言語化することで知識をより定着させたいと思い、そのための第一段としての記事が「決定木ってなんやねん」です。
この記事で記載すること・しないこと
- 記載する内容
- 欠損値とは何か
- Pythonで欠損値を確認する方法
- 欠損値の扱い方や対処法
- 記載しない内容
- AIの基本的な前提知識
- LLMやAIエージェントなど、流行の技術トピック
対象読者
- 欠損値ってなんだろう?
- 欠損値ってどう扱えばいいんだろう?
- とか思ってる方
欠損値ってなんやねん
実際にPythonを用いて欠損値の確認や対処方法を解説する前に、前提として「欠損値とは何か?」について説明していきます。
欠損値って何?その基本と種類
まず、欠損値とはその名の通り「欠損している値」ですね。
少し遠回りして説明しますが、前提として機械学習モデルは特徴量というデータを利用して学習し、学習済みモデルを利用して推論や生成をします。
では、例えば収入と勤続年数から住宅ローンを組めるか否かを自動で判断するAIを作成したいとします。
この時、必要な特徴量と正解ラベルは三つとします(実際にはこんなシンプルではないですが)
- 特徴量
- 収入
- 勤続年数
- 正解ラベル
- 住宅ローンの承認可否
その上で、モデルを実運用で利用したい場合、大きく分けて以下三つのステップを踏む必要があります
- データ収集
- モデルの学習
- 実運用
その上で、モデルを学習させるために、下記データを複数用意したとします。
収入 (円) | 勤続年数 | 住宅ローン承認可否 |
---|---|---|
7,000,000 | 12 | 承認 |
4,500,000 | 3 | 不承認 |
5,500,000 | 8 | 承認 |
3,000,000 | 2 | 不承認 |
8,000,000 | 15 | 承認 |
以下省略...(何レコードも続く) |
モデルは、このようなデータを元に「収入が高く勤続年数が長い場合は承認」といったパターンを学習していきます。
では、もし以下のようにデータに欠損があったらどうでしょうか?
収入 (円) | 勤続年数 | 住宅ローン承認可否 |
---|---|---|
null | 12 | 承認 |
4,500,000 | null | 不承認 |
5,500,000 | 8 | 承認 |
null | 2 | 不承認 |
8,000,000 | 15 | 承認 |
以下省略...(何レコードも続く) |
この場合、「どのデータを基準に承認・不承認を判断すればよいのか」が分からないため、モデルの学習が正しく行えなくなってしまう訳ですね。
ここで重要なのは、モデルが正しく学習できるのは「そもそも学習するためのデータがしっかりと存在しているから」という点です。
つまり、人間でも学習教材がなければ学習が出来ないのと同じく、欠損値が存在するとモデルは正しいパターンを学べなくなってしまいます。
このように、データが欠損している部分、つまり「null(実際にはNaNなど表記は様々ある)」と表記される値が欠損値です。
欠損値ってどう紛れ込む?
欠損値の存在は理解を理解した上で、「そもそもどうして欠損値が発生するのか?」という点を解説していきます。
簡単に言うと、欠損値の発生は「データ収集方法」や「処理の仕方」に大きく依存します。
詳しく見ていきましょー
例1: スクレイピングの場合
例えば、Amazonのサイトから各商品のレビュー数、値段、販売企業の設立年数などをスクレイピングして、商品に信頼性があるかどうかをAIで自動判定するシステム作りたいとします。
※Amazonでスクレイピングが可能かどうか、そもそもこういうデータが取得できるかはさておき
この場合、ざっくりと必要そうなデータは以下のようなものですね
- 特徴量
- レビュー数
- 値段
- 会社の設立年数
- 正解ラベル
- 商品に信頼性があるかどうか
では、実際にスクレイピングをする場合、大きく分けて二つの取得方法があるかと思います。
- サイトに直接アクセスし、HTMLからデータを抽出し、CSVやDBに登録する方法
- 一旦ローカルにHTMLを保存し、その後データを抽出してCSVやDBに登録する方法
もし、スクレイピングのプログラムに誤りがあり、特定のデータが正常に取得できなかったり、HTML保存時にファイルが破損してしまった場合、本来あるべきデータが欠落した状態で保存されることになります。結果として、学習データに気づかぬうちに欠損値が紛れ込むリスクが高まります。
こういった作業は基本的にプログラムを通じて行う訳ですが、もしスクレイピングの処理に誤りがあって特定のデータが取得できないままCSV or DBに登録されていたら、、、もしローカルにHTMLを保存する際の処理に不備があってデータが破損していたら、、、データは元のデータを保っていないまま保存されてしまう訳ですね。
そして、そのデータを基にモデルを学習させる場合、気付かぬうちに欠損値が紛れ込む可能性があるわけです。
別の例として、政治に関するアンケート調査を用いてモデルに学習させるケースを考えてみましょう。
例2: アンケート調査の場合
例えば、政治家に関するアンケートで、好きな政治家を入力する欄があるとします。
この時、その質問に回答しなかった場合、その回答欄は空欄となり、結果として欠損値になる訳ですね。
ここではイメージしやすい例を二つほど上げましたが、このように欠損値が紛れ込むシーンはたくさんあるため、モデル学習やデータ解析をしたい場合は注意が必要な訳です。
欠損値は必ずしも悪者じゃない?
前提として、欠損値はモデルの性能に悪影響を及ぼす事が多いです。
収入 (円) | 勤続年数 | 住宅ローン承認可否 |
---|---|---|
null | 12 | 承認 |
4,500,000 | null | 不承認 |
5,500,000 | 8 | 承認 |
null | 2 | 不承認 |
8,000,000 | 15 | 承認 |
以下省略...(何レコードも続く) |
上記のデータを人間が見ても、欠損値のあるデータじゃ予測しずらいというのは何となく分かりますよね。
では、欠損値は必ずしも悪いものなのでしょうか?
結論としては、基本的には欠損値はない方が望ましいですが、状況によっては欠損値自体が重要な情報となるケースもあります。
まずは悪い例から見ていきましょう。例えば、本来あるべきデータが単にデータの破損などで無い場合、これはモデルの推論などに悪影響を与えますね。
しかし、「特定のデータが欠損していること = 特定の状況を表していること」もあり、欠損値があることが推論や解析において重要な場合もあるのです。
収入 (円) | 勤続年数 | 副業収入 (円) | 住宅ローン承認可否 |
---|---|---|---|
7,000,000 | 12 | 1,000,000 | 承認 |
4,500,000 | 3 | Null | 不承認 |
5,500,000 | 8 | 500,000 | 承認 |
3,000,000 | 2 | Null | 不承認 |
8,000,000 | 15 | 2,000,000 | 承認 |
以下省略...(何レコードも続く) |
例えば上記の場合、「副業収入」がnullになっている行は、「本収入はあるが副業を行っていない」または「副業で利益が出ていない」ことを意味していると解釈できますね。
この場合、欠損値そのものが「副業をしていない」という情報となり、モデルにとってマイナスの影響を与えないどころか、状況の違いを反映する指標として役立つ可能性があります(実際には利用するモデルのアルゴリズムなどで性能や精度が変わりますが)。
整理すると、欠損値が悪影響を及ぼすかどうかは、そのデータが何を意味しているかによるわけですね。
データが本来存在すべきなのに欠損している場合は問題と捉えて良いですが、逆に「欠損=特定の状態」を意味している場合は、その情報が有効な場合もあるわけですね。
個人的に感じたのが、データの欠損が良いのかどうかなどはある程度そのドメイン領域、かつそのデータ自体に対する理解が無いと難しいなと思いました。
欠損値の補完について
「でも、欠損値が紛れ込んじゃってる場合って、モデルの精度は下がるけどそのまま学習させるしかないの?」という疑問を明確にしていきます。
結論から言うと、「補完」を行うことが多いです。
例えば、以下のようなアンケート結果をモデルに学習させたいとします。
回答者 | 満足度(1~10) |
---|---|
A | 8 |
B | Null |
C | 9 |
D | Null |
E | 7 |
しかし、このままではBさんやDさんの回答が抜け落ちているため、、モデルが正しく学習できませんね。
そこで、欠損値を「補完」してデータを埋める手法を使う訳です。
代表的な補完方法としては以下のようなものが存在します。
- 平均値補完
- 数値データの場合、全体の平均値で欠損値を埋める方法です。
- ※欠点:外れ値に影響されやすい
- 中央値補完
- 分布が偏っている場合、中央値で補完することで外れ値の影響を軽減できます。
- 最頻値補完
- カテゴリカルデータでは、最も頻出する値(モード)で補完する方法が有効です。
- 線形補完
- 時系列や連続データでは、前後の値から線形的に推定する方法です。
- k近傍法(KNN)
- 近傍のデータを参照して欠損値を推定する方法。データの構造を反映しやすいですが、計算コストがかかる点に注意が必要です。
- 多重代入法
- 欠損値を複数回推定し、その結果を統合することで、統計的な精度を向上させる手法です。
例として、平均値補完の場合はこんな感じになりますね。
回答者 | 満足度(1~10) |
---|---|
A | 8 |
B | 8 |
C | 9 |
D | 8 |
E | 7 |
何万レコードもあるデータからどう欠損値を見つけ出す?
では、扱うデータが膨大な場合、欠損値をどう見つけ出すのでしょうか?
目視には当然限界があるため、何かしら自動化して見つける必要がありますね。
発見方法には色々な手法があるのですが、記事の後半でPythonを利用した欠損値の検出方法について解説していきます。
利用するデータセットについて
では、実際にデータセットとPythonを利用して欠損値の理解を深めていきたいため、まずは利用するデータセットについて紹介します。
データセットについて
今回利用するのは「住宅価格」に関するデータセットです。
具体的には、ボストン近郊の住宅情報と価格に関するデータです。
※ちなみに、この「住宅価格データセット」はカリフォルニア大学アーバイン校が無償で提供してくれているデータセットです。
データセットの内容
データセットの内容は以下です。
変数名 | 詳細 |
---|---|
CRIM | 人口1人あたりの犯罪発生率 |
ZN | 25000平方フィートを超える住居エリアの割合 |
INDUS | 小売以外の商業用地が占める面積の割合 |
CHAS | チャールズ川沿い地域の有無を示すダミー変数(1: 該当、0: 非該当) |
NOX | 大気中のNOx濃度 |
RM | 1940年以前に建てられた建物の割合 |
DIS | 主要な5つの雇用施設までの距離 |
PAD | 環状高速道路へのアクセスのしやすさ |
TAX | 1万ドルあたりの不動産税率の総額 |
PTRATIO | 各町における児童と教師の比率 |
B | 町毎の黒人比率を1000(Bk-0.63)^2で算出 |
LSTAT | 低所得層に従事する住民の割合 |
MEDV | 所有者が占有する住宅の中央値(単位: $1000) |
また、実際に下記URLでブラウザからでもアクセスできます。
↓こんな感じ
データセットを理解するために軽くPythonで動かす
では、実際にPythonでデータセットを取得して、データをログに出してみます。
(カラム名が大文字英語で分かりずらいため日本語に変換しています)
※df.head()
で最初の五行をログに出している
# ライブラリのインポート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams["font.family"] = "Meiryo"
# データセットの読み込み
df = pd.read_csv(
"https://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data",
header=None,
sep="\\s+",
)
df.columns = [
"犯罪率", # CRIM: 町ごとの犯罪発生率
"大規模住宅地率", # ZN: 25,000平方フィート以上の住宅地の割合
"非小売業率", # INDUS: 非小売業が占める土地の割合
"チャールズ川ダミー", # CHAS: チャールズ川沿いなら1、そうでなければ0
"窒素酸化物濃度", # NOX: 窒素酸化物濃度(10ppmあたり)
"部屋数", # RM: 住宅あたりの平均部屋数
"築年数", # AGE: 1940年以前に建てられた持ち家の割合
"距離", # DIS: ボストンの雇用センターまでの加重距離
"幹線道路アクセス", # RAD: 幹線道路へのアクセスのしやすさ
"固定資産税率", # TAX: 1万ドルあたりの固定資産税率
"生徒教師比", # PTRATIO: 町ごとの生徒と教師の比率
"黒人比率指数", # B: 1000(Bk - 0.63)^2, Bkは町の黒人比率
"低所得者率", # LSTAT: 低地位の人口の割合(%)
"住宅価格中央値", # MEDV: 持ち家の中央値(1000ドル単位)
]
print(df.head())
実行結果
犯罪率 大規模住宅地率 非小売業率 チャールズ川ダミー 窒素酸化物濃度 部屋数 築年数 距離 幹線道路アクセス 固定資産税率 生徒教師比 黒人比率指数 低所得者率 住宅価格中央値
0 0.00632 18.0 2.31 0 0.538 6.575 65.2 4.0900 1 296.0 15.3 396.90 4.98 24.0
1 0.02731 0.0 7.07 0 0.469 6.421 78.9 4.9671 2 242.0 17.8 396.90 9.14 21.6
2 0.02729 0.0 7.07 0 0.469 7.185 61.1 4.9671 2 242.0 17.8 392.83 4.03 34.7
3 0.03237 0.0 2.18 0 0.458 6.998 45.8 6.0622 3 222.0 18.7 394.63 2.94 33.4
4 0.06905 0.0 2.18 0 0.458 7.147 54.2 6.0622 3 222.0 18.7 396.90 5.33 36.2
見ずらいのでテーブル形式にしたものが以下です(これも見ずらくて草)
No | 犯罪率 | 大規模住宅地率 | 非小売業率 | チャールズ川ダミー | 窒素酸化物濃度 | 部屋数 | 築年数 | 距離 | 幹線道路アクセス | 固定資産税率 | 生徒教師比 | 黒人比率指数 | 低所得者率 | 住宅価格中央値 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.00632 | 18.0 | 2.31 | 0 | 0.538 | 6.575 | 65.2 | 4.0900 | 1 | 296.0 | 15.3 | 396.90 | 4.98 | 24.0 |
1 | 0.02731 | 0.0 | 7.07 | 0 | 0.469 | 6.421 | 78.9 | 4.9671 | 2 | 242.0 | 17.8 | 396.90 | 9.14 | 21.6 |
2 | 0.02729 | 0.0 | 7.07 | 0 | 0.469 | 7.185 | 61.1 | 4.9671 | 2 | 242.0 | 17.8 | 392.83 | 4.03 | 34.7 |
3 | 0.03237 | 0.0 | 2.18 | 0 | 0.458 | 6.998 | 45.8 | 6.0622 | 3 | 222.0 | 18.7 | 394.63 | 2.94 | 33.4 |
4 | 0.06905 | 0.0 | 2.18 | 0 | 0.458 | 7.147 | 54.2 | 6.0622 | 3 | 222.0 | 18.7 | 396.90 | 5.33 | 36.2 |
今回は各データの詳細は説明しないのですが、簡単に言うとこのデータセットはボストン近郊の506地区ごとのデータを一レコードずつに格納しているわけですね。
まあ各地区の治安とか築年数とかの情報って感じです。
では、次の章で実際に欠損値の確認をしていきましょう。
欠損値をコードで見てみる
実際に先ほどのデータセットを利用して欠損値を確認して行きます。
PythonのライブラリであるPandasで簡単に欠損値をカウントする
今回はPandasというライブラリで欠損値の確認をします。
実行コードが以下です。
※df.isnull().sum()
で欠損値の集計結果を出力できます。
# ライブラリのインポート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams["font.family"] = "Meiryo"
# データセットの読み込み
df = pd.read_csv(
"https://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data",
header=None,
sep="\\s+",
)
df.columns = [
"犯罪率", # CRIM: 町ごとの犯罪発生率
"大規模住宅地率", # ZN: 25,000平方フィート以上の住宅地の割合
"非小売業率", # INDUS: 非小売業が占める土地の割合
"チャールズ川ダミー", # CHAS: チャールズ川沿いなら1、そうでなければ0
"窒素酸化物濃度", # NOX: 窒素酸化物濃度(10ppmあたり)
"部屋数", # RM: 住宅あたりの平均部屋数
"築年数", # AGE: 1940年以前に建てられた持ち家の割合
"距離", # DIS: ボストンの雇用センターまでの加重距離
"幹線道路アクセス", # RAD: 幹線道路へのアクセスのしやすさ
"固定資産税率", # TAX: 1万ドルあたりの固定資産税率
"生徒教師比", # PTRATIO: 町ごとの生徒と教師の比率
"黒人比率指数", # B: 1000(Bk - 0.63)^2, Bkは町の黒人比率
"低所得者率", # LSTAT: 低地位の人口の割合(%)
"住宅価格中央値", # MEDV: 持ち家の中央値(1000ドル単位)
]
# 欠損値の確認
print(df.isnull().sum())
実行結果が以下です。
犯罪率 0
大規模住宅地率 0
非小売業率 0
チャールズ川ダミー 0
窒素酸化物濃度 0
部屋数 0
築年数 0
距離 0
幹線道路アクセス 0
固定資産税率 0
生徒教師比 0
黒人比率指数 0
低所得者率 0
住宅価格中央値 0
dtype: int64
結果を見ると、全て「0」とありますね。
つまり、このデータセットは合計で506レコード(506地区文のデータが存在する)が、どのレコード・どのカラムにおいても欠損値がないことを意味します。
そのため、この学習用データは「欠損値の観点」からは整ったデータセットと言えます。
わざと欠損値を紛れ込ませて見る
といっても、実際に欠損値がある場合にしっかりと検知できるのかを知りたいため、欠損値を紛れ込ませてみましょう。
具体的には、全行数の10%分の行をランダムに選び、選ばれた行の「部屋数」列に欠損値を意味する np.nan
を代入します。
# ライブラリのインポート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams["font.family"] = "Meiryo"
# データセットの読み込み
df = pd.read_csv(
"https://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data",
header=None,
sep="\\s+",
)
df.columns = [
"犯罪率", # CRIM: 町ごとの犯罪発生率
"大規模住宅地率", # ZN: 25,000平方フィート以上の住宅地の割合
"非小売業率", # INDUS: 非小売業が占める土地の割合
"チャールズ川ダミー", # CHAS: チャールズ川沿いなら1、そうでなければ0
"窒素酸化物濃度", # NOX: 窒素酸化物濃度(10ppmあたり)
"部屋数", # RM: 住宅あたりの平均部屋数
"築年数", # AGE: 1940年以前に建てられた持ち家の割合
"距離", # DIS: ボストンの雇用センターまでの加重距離
"幹線道路アクセス", # RAD: 幹線道路へのアクセスのしやすさ
"固定資産税率", # TAX: 1万ドルあたりの固定資産税率
"生徒教師比", # PTRATIO: 町ごとの生徒と教師の比率
"黒人比率指数", # B: 1000(Bk - 0.63)^2, Bkは町の黒人比率
"低所得者率", # LSTAT: 低地位の人口の割合(%)
"住宅価格中央値", # MEDV: 持ち家の中央値(1000ドル単位)
]
# 元のデータの欠損値確認
print("【元のデータの欠損値確認】")
print(df.isnull().sum())
# 元のデータフレームのコピーを作成
df_missing = df.copy()
# 乱数のシードを固定(再現性のため)
np.random.seed(0)
# データの行数を取得し、10%の行をランダムに選択
n_rows = len(df_missing)
missing_indices = np.random.choice(n_rows, size=int(0.1 * n_rows), replace=False)
# 「部屋数」列に欠損値(np.nan)を挿入
df_missing.loc[missing_indices, "部屋数"] = np.nan
# 欠損値を挿入後の確認
print("【欠損値挿入後の欠損値確認】")
print(df_missing.isnull().sum())
実行結果が以下
【元のデータの欠損値確認】
犯罪率 0
大規模住宅地率 0
非小売業率 0
チャールズ川ダミー 0
窒素酸化物濃度 0
部屋数 0
築年数 0
距離 0
幹線道路アクセス 0
固定資産税率 0
生徒教師比 0
黒人比率指数 0
低所得者率 0
住宅価格中央値 0
dtype: int64
【欠損値挿入後の欠損値確認】
犯罪率 0
大規模住宅地率 0
非小売業率 0
チャールズ川ダミー 0
窒素酸化物濃度 0
部屋数 50
築年数 0
距離 0
幹線道路アクセス 0
固定資産税率 0
生徒教師比 0
黒人比率指数 0
低所得者率 0
住宅価格中央値 0
dtype: int64
しっかりと欠損値が検出できますね!
まとめ
今回は欠損値について記載しました
本来は「欠損値の補完~~」とか色々やりたかったんですが、それはまた次回ということで
というのと、個人的に思っているのがAI関連の記事って少ないですよね...?
これからいくつかAI関連の記事を出して行って、反応良さそうだったらガンガン出していこうと思います~
良かったらTwitterフォローしてねっ!
Discussion