1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【実践】カリフォルニア住宅価格データセットで学ぶ、EDAとデータクレンジング

1
Last updated at Posted at 2025-09-12

はじめに

この記事はデータ分析の定石というシリーズの1つになります。シリーズでは実データ分析をいくつかのフェーズに分けてまとめています。今回はそのうちの「探索的データ分析(EDA)」と「データクレンジング」に焦点を当てています。

シリーズ全体の流れはこちらになります。
【実践】カリフォルニア住宅価格予測で学ぶ、データ分析の定石

探索的データ分析とデータクレンジング

現実のデータは、必ずしもきれいに整っているわけではありません。複数のデータセットを共通項目名を起点に結合したり、時間などの情報を揃えたりが必要な場合があります。また、ある数値列に一部文字が入っていたり、特定のデータがごっそり抜け落ちていることだってあります。これらは主にデータベースの仕様や設計に由来するものでしょう。このような形式のデータが混在すると、モデルとしてら異なるデータとしては扱うので、精度に影響します。

また、それ以外でも、ごく一部のデータだけが極端に大きな値(外れ値)を持つこともあります。そしてその要因が、センサーの故障によるものなど分析にとってノイズになる場合があります。これらは主に通常では発生しない稀な環境条件でデータになったことが由来するでしょう。このような稀に発生するデータは統計的に扱うのが難しく、多くの場合モデルの精度を下げる要因になります。

このように、このフェーズでは、データセットを1つに結合する、データの課題を特定し、分析に影響するノイズを取り除く作業を行います。

今回のデータはデータセット1つですので、データ結合より後の作業を進めていきます。

欠測値やデータの型の確認と処理

analysis.ipynb
# データ読み込み
california_housing_data = fetch_california_housing()
rawdata = pd.DataFrame(california_housing_data.data, columns=california_housing_data.feature_names)
rawdata["Price"] = california_housing_data.target
rawdata.head()  # データの先頭5行を表示
analysis.ipynb
# 項目の意味がわかるように日本語にしよう
rawdata.rename(columns={
    "MedInc": "世帯所得",
    "HouseAge": "家の築年数",
    "AveRooms": "部屋の数",
    "AveBedrms": "寝室の数",
    "Population": "居住人数",
    "AveOccup": "世帯人数"
}, inplace=True)

# データの型を確認
display(rawdata.info())
# データの値の範囲
display(rawdata.describe())

実行結果を確認します

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 9 columns):
 #   Column     Non-Null Count  Dtype  
---  ------     --------------  -----  
 0   世帯所得       20640 non-null  float64
 1   家の築年数      20640 non-null  float64
 2   部屋の数       20640 non-null  float64
 3   寝室の数       20640 non-null  float64
 4   居住人数       20640 non-null  float64
 5   世帯人数       20640 non-null  float64
 6   Latitude   20640 non-null  float64
 7   Longitude  20640 non-null  float64
 8   Price      20640 non-null  float64
dtypes: float64(9)
memory usage: 1.4 MB
世帯所得         家の築年数          部屋の数          寝室の数          居住人数  \
count  20640.000000  20640.000000  20640.000000  20640.000000  20640.000000   
mean       3.870671     28.639486      5.429000      1.096675   1425.476744   
std        1.899822     12.585558      2.474173      0.473911   1132.462122   
min        0.499900      1.000000      0.846154      0.333333      3.000000   
25%        2.563400     18.000000      4.440716      1.006079    787.000000   
50%        3.534800     29.000000      5.229129      1.048780   1166.000000   
75%        4.743250     37.000000      6.052381      1.099526   1725.000000   
max       15.000100     52.000000    141.909091     34.066667  35682.000000   

               世帯人数      Latitude     Longitude         Price  
count  20640.000000  20640.000000  20640.000000  20640.000000  
mean       3.070655     35.631861   -119.569704      2.068558  
std       10.386050      2.135952      2.003532      1.153956  
min        0.692308     32.540000   -124.350000      0.149990  
25%        2.429741     33.930000   -121.800000      1.196000  
50%        2.818116     34.260000   -118.490000      1.797000  
75%        3.282261     37.710000   -118.010000      2.647250  
max     1243.333333     41.950000   -114.310000      5.000010

今回は緯度経度情報があるので、地図上の分布が妥当かも確認します

analysis.ipynb
# 場所の可視化(地図データならではの処理)
import plotly.express as px
import pandas as pd

# 散布図の作成(価格で色分け)
fig = px.scatter(
    rawdata,
    x='Longitude',
    y='Latitude',
    color='Price',
    title='Latitude and Longitude vs. Price'
)

# グラフの表示
fig.show()

image.png

分析結果の考察

欠損値: データに欠損値は見られませんでした。
データの型:各列の要素のデータの型は一致していました。

データ範囲の考察: 「部屋の数」「寝室の数」「居住人数」「世帯人数」といった項目は、データの75パーセンタイル値から最大値にかけて値が大きく増加しています。ここで、データの概要を振り返りますと、大部分が一戸建ての物件で構成されている集合住宅のようなデータも含まれていると書いておりました。ですので、その影響が現れている可能性があります。

地理的分布: 緯度経度のデータを確認したところ、カリフォルニア州の地理的特徴と一致していました。特に、沿岸部に高価な物件が多く集中している傾向も見られました。

外れ値の確認と処理

「部屋の数」「寝室の数」「居住人数」「世帯人数」の分布から、外れ値を確認します。
ここで外れ値と目的変数である価格の関係も同時に把握するため、価格帯別の分布も観察できるようにします。

analysis.ipynb
# Priceが連続値なので、離散化して可視化しよう。ヒストグラムで分割方法を検討する。
import matplotlib.pyplot as plt
import pandas as pd

bins_pattern = [5, 7, 10, 15, 20, 25]

# 複数のヒストグラムを重ねて表示
plt.figure(figsize=(10, 6))
for bin in bins_pattern:
    plt.hist(rawdata["Price"], bins=bin, alpha=0.5, label=f'{bin} bins')

plt.title('Price Distribution with Different Bin Counts')
plt.xlabel('Price')
plt.ylabel('Frequency')
plt.grid(True)
plt.legend()
plt.show()

image.png

analysis.ipynb
# メモ:7段階で2つの山(二峰性)が見られるため、7段階に分割する。
# Priceが大きいほどランクも大きくなるようにする。
rawdata['Price_rank'] = pd.qcut(rawdata['Price'], q=7, labels=False)
analysis.ipynb
import seaborn as sns
import matplotlib.pyplot as plt

# 日本語表示のためにフォントを設定
plt.rcParams['font.family'] = 'MS Gothic'

# データのコピーを作成
raw_pair = rawdata.copy()

# 各ランクが明確になるように、視認性の高い色を選んでみました。
custom_palette = ['red', 'green', 'blue', 'orange', 'purple', 'brown', 'gray']

# pairplotを作成し、Price_rankで色分け、カスタムパレットを適用
g = sns.pairplot(raw_pair, corner=True, hue='Price_rank', palette='RdYlGn')#palette=custom_palette)


# グラフ全体のサイズを調整
g.fig.set_size_inches(20, 20)

# グラフを表示
plt.show()

output.png

「部屋の数」「寝室の数」「居住人数」「世帯人数」は外れ値の影響か、ヒストグラムの分布が左端に集中しています。
居住人数の外れ値の様子をヒストグラムで観察します。
(説明が冗長になるので、その他の属性情報の観察は割愛します)

analysis.ipynb
import matplotlib.pyplot as plt

def simply_hist(df, bin_num, name, height):
    """
    指定された列のヒストグラムを生成する関数。
    y軸の範囲を調整して、外れ値も確認できるようにする。
    """
    plt.figure(figsize=(8, 5))
    
    plt.hist(df[name], bins=bin_num, alpha=0.5, label=name)
    plt.title(f'Histogram of {name}')
    plt.xlabel(name)
    plt.ylabel('Frequency')
    plt.grid(True)
    plt.ylim(0, height)
    plt.legend()
    plt.show()

# 代表例として、居住人数のヒストグラムを観察
simply_hist(rawdata, 1000, "居住人数", height=10)

image.png
数件の値が飛び飛びになっていることが確認できました。
そこで、飛び値が1%程度と考え、対象列の99パーセンタイルの値を確認してみます。

analysis.ipynb

columns = ["部屋の数", "寝室の数", "居住人数", "世帯人数"]
df = rawdata[columns].copy()

q99 = df.quantile(0.99)
print("各列の99パーセンタイル値:\n", q99)

df_list = []
for col in columns:
    # 99パーセンタイルを超える行を抽出し、リストに追加
    df_list.append(df[df[col] > q99[col]])

# 抽出したデータフレームを結合し、重複行を削除
filtered_df = pd.concat(df_list).drop_duplicates()

# 外れ値と判断された行の数を表示
print("99パーセンタイルを超えるユニークな行数:", len(filtered_df))
各列の99パーセンタイルの値を確認
各列の99パーセンタイル値:
居住人数    5805.830000
Name: 0.99, dtype: float64
99パーセンタイルを超えるユニークな行数: 640

期待どおり先ほどの飛び値が1%の範囲にあることが確認できました。
他の属性情報についても同様に1%程度外れ値があると考え、外れ値を除外したデータでpairplotを観察します。

analysis.ipynb
# 外れ値を除外したデータでpairplotを観察する
g = sns.pairplot(raw_pair.drop(filtered_df.index), corner=True, hue='Price_rank', palette='RdYlGn')

# グラフのサイズを調整
g.fig.set_size_inches(10, 10)

# グラフを表示
plt.show()

output.png

分析結果の考察

外れ値を取り除くことで、データの分布がより明確になることがわかりました。この外れ値は、1世帯あたりの平均居住者数が大きくなることが原因である可能性があります。

集合住宅と一軒家は、同じ環境条件でも価格評価の仕組みが異なります。本来は住居の種類に関する情報も分析に含めるべきですが、今回のデータセットにはその情報が含まれていません。

そこで、分析の目的を「主に一軒家の価格予測」に絞り込むことにします。この目的から考えると、集合住宅のデータは予測のノイズとなり、集合住宅のデータは「外れ値」の特性を持つと見なして分析から除外します。

まとめと次やること

一軒家の価格予測を課題として、集合住宅のような家屋は分析におけるノイズとして、それが1%あるものとしてを削りました。結果、よりはっきりと分布が確認できるようになりました。

緯度と経度の座標を使い、住宅価格別に色分けしてプロットしたところ、地図として正しいことが確認できました。このプロットから、海岸線に近い地域ほど住宅価格が高い物件が集中している傾向が見られました。

次回はこの外れ値を除いたデータに対して、海岸線という情報に着目しつつEDAを実施します。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?