scikit-learnのボストン住宅価格のデータをインポートすると、以下のメッセージが出力されます。倫理的な問題があり、scikit-learnとしてはカリフォルニア住宅価格データの利用を推奨しています。したがって今後は、カリフォルニア住宅価格データの活用が増える可能性があります。そこで、この記事では、カリフォルニア住宅価格データの前処理について見解を述べたいと思います。
カリフォルニア住宅価格データは、どのようなデータか?
scikit-learnからカリフォルニア住宅価格データを取得し、DESCRキーの情報を確認します。
import pandas as pd
import seaborn as sns
from sklearn.datasets import fetch_california_housing
dataset = fetch_california_housing()
print(dataset.DESCR)
以下のように解釈可能です。
- 1990年のアメリカの国勢調査に基づく以下のデータ
- 一定のエリア(以下ブロックと記載)で集計
- レコード数は20640
- カラムの構成は以下の通り
英 | 日(直訳) |
---|---|
MedInc | ブロックの所得中央値 |
HouseAge | ブロックの家屋年齢の中央値 |
AveRooms | 1世帯あたりの平均部屋数 |
AveBedrms | 1世帯あたりの平均寝室数 |
Population | ブロックの人口 |
AveOccup | 世帯人数の平均値 |
Latitude | 緯度 |
Longitude | 経度 |
Price | 住宅価格 |
実際のデータを確認
DataFrameで実データを確認します。
df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
df['Price'] = dataset.target
df.head()
基本情報を確認
基本情報も確認します。
df.describe()
df.info()
欠損はないことが分かります。
各カラムのデータ分布
各カラムの分布を確認します。
df.hist(bins=30, figsize=(15, 10))
いくつか怪しげな分布があるので、順に確認します。
Medinc(所得中央値)
特に前処理の必要性は見当たりません。
AveRooms(平均部屋数), AveBedrms(平均寝室数)
大きい値が確認しづらいため、散布図で表示します。
sns.scatterplot(data=df, x='AveRooms', y='AveBedrms')
突出したデータが2件あります。抽出して詳細を確認します。
df.query('AveRooms > 100')
最初に確認したDESCRキーの説明で以下の記載がありました。おそらく、これに該当する2件と考えられます。
An household is a group of people residing within a home.
Since the average number of rooms and bedrooms in this dataset
are provided per household,
these columns may take surpinsingly large values
for block groups with few households and many empty houses,
such as vacation resorts.
世帯とは、家庭内に居住する人々の集団である。
このデータセットでは、1世帯あたりの平均的な部屋数と寝室数を示しているため、
別荘地など世帯数が少なく空家が多いブロックでは、
これらの列が突出して大きな値をとることがある。
※DeepL(https://www.deepl.com/translator)による翻訳
緯度・経度からGoogle Mapで確認します。
緯度, 経度 | Google Map |
---|---|
38.91, -120.08 | https://goo.gl/maps/5EtYsaVJXWcT5sYa9 |
38.80, -120.08 | https://goo.gl/maps/JjbTB2Fou1Fq4Vnu5 |
レイクタホの別荘かもしれません。
上記のようなデータが生まれる背景として、AveRooms(平均部屋数), AveBedrms(平均寝室数)は、AllRooms(ブロックの全部屋数), AllBedrms(ブロックの全寝室数), Household(世帯数)という元データがあり、以下の計算によって算出されていると推測できます。
- AllRooms ÷ Household = AveRooms
- AllBedrms ÷ Household = AveBedrms
別のカラムの情報として、Population(ブロックの人口), AveOccup(世帯人数の平均値)があります。同様に、以下の計算によって算出されていると推測できます。
- Population ÷ Household = AveOccup
上記3つの算出ロジックが正しい場合、Population(ブロックの人口)とAveOccup(世帯人数の平均値)から、Household(世帯数)を算出できます。さらに、AveRooms(平均部屋数), AveBedrms(平均寝室数), Household(世帯数)から、AllRooms(ブロックの全部屋数), AllBedrms(ブロックの全寝室数)を算出できます。
一度計算してみます。
df['Household'] = df['Population']/df['AveOccup']
df['AllRooms'] = df['AveRooms']*df['Household']
df['AllBedrms'] = df['AveBedrms']*df['Household']
df.head()
きれいな整数で出力されました。推測の正しさを補強する結果です。新たに生成したHousehold(世帯数), AllRooms(ブロックの全部屋数), AllBedrms(ブロックの全寝室数)の分布を確認します。
df[['Household', 'AllRooms', 'AllBedrms']].hist(bins=30, figsize=(15, 10))
大きな値のデータが確認しづらいので、散布図で確認します。
sns.stripplot(data = df['Household'])
Household(世帯数)について、目立った外れ値は見当たりません。
sns.scatterplot(data=df, x='AllRooms', y='AllBedrms')
AllRooms(ブロックの全部屋数), AllBedrms(ブロックの全寝室数)はきれいに相関しており、目立った外れ値も見当たりません。
したがって、AveRooms(平均部屋数), AveBedrms(平均寝室数)よりも、その元データと推測されるHousehold(世帯数), AllRooms(ブロックの全部屋数), AllBedrms(ブロックの全寝室数)の活用をおすすめします。
Population(ブロックの人口)
大きな値のデータが確認しづらいので、散布図で確認します。
sns.stripplot(data = df['Population'])
突出している2つデータを抽出して確認します。
df.query('Population > 20000')
緯度・経度から、どの場所を指しているか確認します。
緯度, 経度 | Google Map |
---|---|
36.64, -121.79 | https://goo.gl/maps/zfyzHtN5V4ns8S5EA |
33.35, -117.42 | https://goo.gl/maps/EvH6GN6DyDd8n6r77 |
森林・山岳地帯のため、Population(ブロックの人口)の値のエラーが疑われます。一方で、AveOccup(世帯人数の平均値)は通常範囲の値を示しています。AveOccup(世帯人数の平均値)が「Population ÷ Household = AveOccup」で算出されているという前提が正しい場合、Population(ブロックの人口)がエラーとなれば、AveOccup(世帯人数の平均値)の値にも影響がでます。
したがって、Population(ブロックの人口)の値は正常であり、集計単位であるブロックの領域が広いために、Population(ブロックの人口)の値が大きくなっていると考える方が妥当でしょう。
AveOccup(世帯人数の平均値)
大きな値のデータが確認しづらいので、散布図で確認します。
sns.stripplot(data = df['AveOccup'])
上位4つデータを抽出して確認します。
df.query('AveOccup > 200')
緯度・経度から、どの場所を指しているか確認します。
緯度, 経度 | Google Map |
---|---|
40.41, -120.51 | https://goo.gl/maps/JVo1yj2XbrGfdYMj6 |
38.69, -121.15 | https://goo.gl/maps/zJZVsaYLef3h9CZh7 |
35.32, -120.70 | https://goo.gl/maps/PU6JvRJQ7nUh5Q889 |
38.32, -121.98 | https://goo.gl/maps/FNwkDhbgoSif3RGD7 |
それぞれ、近くに刑務所(High Desert State Prison, California State Prison - Sacramento, California Men's Colony, California State Prison Solano)があります。
Population(ブロックの人口)は実際に居住している人数、Household(世帯数)はドキュメント上(例えば、住民票や世帯主数など)の数値であり、その結果、刑務所エリアでAveOccup(世帯人数の平均値)の数値が高まっていると考えられます。
AveOccup(世帯人数の平均値)についても、Household(世帯数)で割ることで大きな値が発生しているため、代わりに元データと推測されるPopulation(ブロックの人口), Household(世帯数)の活用をおすすめします。
Latitude(緯度), Longitude(経度)
とくに目立った外れ値は確認できません。散布図にプロットすると、カリフォルニアの地形をほぼ網羅していることを確認できます。
sns.scatterplot(data=df, x='Longitude', y='Latitude')
引用:Google Map https://goo.gl/maps/McF5JYkYddyY7SWX8
住宅価格の予測において、多くの場合、沿岸部と内陸部という指標は有効に機能します。カリフォルニアの地理から、「南西→北東」方向を「海岸→内陸」方向と近似することが可能です。「南西→北東方向」軸の指標として「緯度と経度の合計値」を活用するなど、新しい列を追加しても良いでしょう。
HouseAge(ブロックの家屋年齢の中央値), Price(住宅価格)
一番右のバーが不自然に突出しています。
HouseAge(ブロックの家屋年齢の中央値), Price(住宅価格)それぞれの最大値は上記のとおり、52と5.00001です。それぞれ最大値となっているデータ数を確認します。
print(df.query('HouseAge == 52').shape)
print(df.query('Price == 5.00001').shape)
最大値のデータが、それぞれ1000データ近くあることを意味しています。多くのデータが一致することは現実的に考えづらく、おそらく国勢調査で、52以上のデータ、5.00001以上のデータとして処理されていると推測されます。したがって、数値として取り扱う必要がある場合は、それぞれ最大値のデータのみ除外することが推奨されます。
df = df[df['HouseAge'] != 52]
df = df[df['Price'] != 5.00001]
まとめ
各カラムに対する見解は以下の通りです。
英 | 日(直訳) | 見解 |
---|---|---|
MedInc | ブロックの所得中央値 | 前処理不要 |
HouseAge | ブロックの家屋年齢の中央値 | 最大値52のデータを削除 |
AveRooms | 1世帯あたりの平均部屋数 | 利用しない |
AveBedrms | 1世帯あたりの平均寝室数 | 利用しない |
Population | ブロックの人口 | 前処理不要 |
AveOccup | 世帯人数の平均値 | 利用しない |
Latitude | 緯度 | 前処理不要 |
Longitude | 経度 | 前処理不要 |
Price | 住宅価格 | 最大値5.00001のデータを削除 |
Household | 世帯数 | PopulationとAveOccupから新規に作成 |
AllRooms | ブロックの全部屋数 | HouseholdとAveRoomsから新規に作成 |
AllBedrms | ブロックの全寝室数 | HouseholdとAveBedrmsから新規に作成 |
前処理は、実際のデータを見る(今回の場合はGoogle Mapを見る)というアクションが重要だと、改めて再認識しました。1日でアメリカの刑務所を4つも調べたのは、初めての体験でした。
前処理の技術的な側面は、以下のコンテンツ(私が制作したもの)がおすすめです。前処理の部分が一部無料で見られます。