「勘」ではなくPythonでSUUMOデータ分析し、”データで「お買い得」物件”を見つける
理想は「相場より割安な、良い中古戸建」を見つけること。
これ、私だけではなく誰しもが割安には興味がありますよね!
そのために毎日SUUMOやATHOMEをチェックしたりしますよね
本当に「相場より安い物件」、チェックできてますか?
相場感が付くまでに時間がかかる、着いたとしてもその毎日の作業ちゃんとできてますか?
抜けた出たり、見落としていたりしませんか?
なんとなく「安そう」「お得かも」…
結局、最後は 「勘」や「雰囲気」 で判断なんてダメですよね
- その価格、駅からの距離や建物の条件を踏まえて本当に適正?
- 見た目の安さの裏に、再建築不可のような致命的なリスクが隠れていないか?
- データに基づいて、客観的に「割安度」を判断する方法はないのか?
そんな疑問や不安を感じているなら、この記事がきっと役に立ちます。
これらの課題を解決するために、Pythonを使ってSUUMOの物件データを分析し、「データで割安度を判定する仕組み」 に取りかかりました。
この記事を読めば、あなたの物件探しは「勘」から「データ」へ、きっと変わるはずです。
🎯 この分析で目指したもの:「データが示す割安物件」の発掘
目的はいたってシンプル
**「データに基づき、相場と比較して割安な中古戸建を効率的に見つけ出す」**こと。
具体的には、以下のステップで実現を目指しました。
- SUUMOから中古戸建ての情報を自動収集:まずは「宝の山」を手に入れる
- 駅距離、面積、築年数、周辺相場など、価格に影響する要因を徹底的に数値化
- 機械学習(重回帰分析)で、物件ごとの 「データが予測する適正価格」 を算出
- その予測価格と実際の販売価格を比較し、 「真の割安度・割安率」 を算出
これにより、「なんとなく安い」ではなく、「データ分析の結果、〇〇%割安と判定された」物件をリストアップします。
データ分析で見えてきた驚きの結果、そして現実
実際にデータ分析を実行し、「割安率」で上位になった物件をいくつかピックアップして詳細を確認してみました。
結果は…
✅ データが示す「割安物件」、見つけられました!
モデルが弾き出したランキング上位には、確かに周辺相場と比較して明らかに安く見える物件が数多く含まれていました。
「築年数や広さ、駅からの距離が似ているのに、なぜこの物件だけこんなに安いんだ?」
データは、教えてくれました。
❌ ただし、その多くには「安い理由」があった!
残念ながら、データが示す「割安物件」の多くは、価格が低いなりの構造的な、あるいは法的な要因を抱えていました。
- 再建築不可物件: 建物を一度壊すと、現在の法律では同じ場所に新しく建てられない土地。資産価値は大きく下がります。
- 接道義務を満たさない土地: 建築基準法上の道路に2m以上接していない土地。これも再建築が難しいケースが多く、リスクが高いとされます。
- 旗竿地: 道路から奥まっており、細長い通路を通って敷地にアクセスする形状の土地。日当たりやプライバシー、建築コスト面で敬遠されがちです。
これは、データ分析を進める上で非常に大きな 「学び」 となりました。
💡 データ分析から得た重要な気づき
今回の分析を通じて、以下の点が明確になりました。
✔︎ データは「相場とのズレ」を正確に示す強力なツール
モデルが出した「割安度」は、物件が周辺相場からどれだけ乖離しているかを客観的に教えてくれます。これにより、「勘」に頼らない、データに基づいた相場観を養うことができます。
しかし、「投資対象として適切か」は別の視点が必要
データで「割安」と判定されても、それが 「買い得」 を意味するとは限りません。価格が安い背景にある法的制限や物理的な条件を見抜く目は、データ分析だけでは補えない、不動産投資家として不可欠なスキルになります。
データ分析は強力な「発見ツール」ですが、それに加えて物件ごとの詳細な調査と、不動産特有のリスクを見抜く知識が不可欠です。
分析の全体フローと使用したPythonコード
ここからは、実際にどのような手順でデータ分析を行い、上記の結論に至ったのかを説明していきます。
今回の分析対象は愛知県内の中古戸建てデータです。
1. データの読み込み
2. 住所や価格・面積などの整形
3. 駅スコアと徒歩分数を使って立地スコアを作成
4. 市区町村ごとの平均価格を算出(名古屋市は区単位に)
5. 3σ法で外れ値を除去
6. モデル構築&予測
7. 割安物件のスコアリング&可視化
1️⃣ データ読み込み
まずは、SUUMOから取得したCSVデータ(suumo_results.csv
を想定)を読み込みます。
import pandas as pd
import numpy as np
import re
df = pd.read_csv("suumo_results.csv")
df_score = pd.read_csv("station_score_top100_only_cleaned_final.csv")
2️⃣ 交通情報から「駅名」「徒歩分数」を抽出
def extract_station_and_walk(text):
try:
match = re.search(r'「(.+?)」歩(\d+)分', str(text))
if match:
return pd.Series([match.group(1), int(match.group(2))])
except:
pass
return pd.Series([None, None])
df[["駅名だけ", "徒歩分"]] = df["交通"].apply(extract_station_and_walk)
df["駅名だけ"] = df["駅名だけ"].astype(str) + "駅"
3️⃣ 駅スコアをマッピング
score_map = df_score.set_index("駅名")["駅スコア(1〜10点)"].to_dict()
df["駅スコア"] = df["駅名だけ"].map(score_map).fillna(0.5)
4️⃣ 価格・面積・築年数の数値化
def parse_price(price_str):
if pd.isnull(price_str): return None
price_str = str(price_str)
if "億" in price_str:
match = re.match(r"(\d+)億(\d*)万円?", price_str)
if match:
oku = int(match.group(1)) * 10000
man = int(match.group(2)) if match.group(2) else 0
return oku + man
else:
match = re.match(r"(\d+)万円?", price_str)
if match:
return int(match.group(1))
return None
df["価格(万円)"] = df["価格"].apply(parse_price)
def parse_area(text):
match = re.match(r"([\d.]+)m2", str(text))
return float(match.group(1)) if match else None
df["建物面積"] = df["建物面積"].apply(parse_area)
df["土地面積"] = df["土地面積"].apply(parse_area)
def calc_age(text):
match = re.match(r"(\d{4})年(\d{1,2})月", str(text))
if match:
year, month = int(match.group(1)), int(match.group(2))
return 2025 - year + (5 - month) / 12
return None
df["築年数"] = df["築年月"].apply(calc_age)
5️⃣ 「市区町村+区」単位で平均価格を作る
def extract_detailed_city(text):
text = str(text)
if "名古屋市" in text:
match = re.search(r"名古屋市(.+?区)", text)
if match:
return "名古屋市" + match.group(1)
else:
match = re.search(r"愛知県(.+?[市区町村])", text)
if match:
return match.group(1)
return None
df["市区町村_詳細"] = df["所在地"].apply(extract_detailed_city)
df["市区町村平均価格"] = df.groupby("市区町村_詳細")["価格(万円)"].transform("mean")
6️⃣ 徒歩分 → 徒歩階級 に変換(カテゴリ変数)
徒歩時間は細かすぎるので分類
def walk_category(x):
if pd.isnull(x): return None
if x <= 5: return "~5分"
elif x <= 10: return "6~10分"
elif x <= 15: return "11~15分"
elif x <= 20: return "16~20分"
else: return "21分~"
df["徒歩階級"] = df["徒歩分"].apply(walk_category)
from sklearn.preprocessing import LabelEncoder
df["徒歩階級_code"] = LabelEncoder().fit_transform(df["徒歩階級"].astype(str))
7️⃣ 立地スコアと価格の対数変換
駅スコアと徒歩時間をかけ合わせた「立地価値スコア」も作成します。
また、価格は偏りが大きいので log1p で対数変換します。
df["立地価値スコア"] = df["駅スコア"] / (df["徒歩分"] + 1)
df["価格_log"] = np.log1p(df["価格(万円)"])
df["市区町村_code"] = LabelEncoder().fit_transform(df["市区町村_詳細"].astype(str))
8️⃣ 説明変数と目的変数の準備
features = [
"建物面積", "土地面積", "築年数", "徒歩分", "駅スコア",
"立地価値スコア", "市区町村平均価格", "市区町村_code", "徒歩階級_code"
]
target = "価格_log"
df_model = df[features + [target]].dropna()
X = df_model[features]
y = df_model[target]
9️⃣ 外れ値除去(3σ法)
全体のバランスを壊す異常値を除外します。
mean = X.mean()
std = X.std()
X_filtered = X.copy()
for col in X.columns:
low = mean[col] - 3 * std[col]
high = mean[col] + 3 * std[col]
X_filtered = X_filtered[(X_filtered[col] > low) & (X_filtered[col] < high)]
# yもフィルタに合わせて取り出す
y_filtered = y[X_filtered.index]
🔟 モデル構築(線形回帰)
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
X_train, X_test, y_train, y_test = train_test_split(X_filtered, y_filtered, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = LinearRegression()
model.fit(X_train_scaled, y_train)
✅ モデル評価
from sklearn.metrics import mean_absolute_error
# スコア
print("訓練スコア:", model.score(X_train_scaled, y_train))
print("テストスコア:", model.score(X_test_scaled, y_test))
🧠 重み(係数)の確認
coefs = pd.DataFrame({
"特徴量": features,
"係数": model.coef_
})
print(coefs.sort_values("係数", key=abs, ascending=False))
🏆 割安ランキング出力
# 全体データで再予測
X_all_scaled = scaler.transform(X)
df["予測価格_log"] = model.predict(X_all_scaled)
df["予測価格(万円)"] = np.expm1(df["予測価格_log"])
# 割安度(予測価格と実価格の差)
df["割安度"] = df["予測価格(万円)"] - df["価格(万円)"]
df["割安率(%)"] = df["割安度"] / df["予測価格(万円)"] * 100
# 上位20件を抽出
cheap_df = df.loc[X.index].sort_values("割安率(%)", ascending=False).head(20)
cheap_df.to_csv("割安物件ランキング.csv", index=False)
print("💾 割安物件ランキングを保存しました!")
💡 今回の価格予測に使用した特徴量まとめ
特徴量 | 内容 |
---|---|
建物面積 | m²単位に変換済。広さが価格に与える影響。 |
土地面積 | 同上。土地の広さが価格に与える影響。 |
築年数 | 築年を現在から逆算。古さによる価格の下落傾向。 |
徒歩分 | 数値化した徒歩時間。駅からの距離の影響。 |
駅スコア | 駅の利用者数などを基にした利便性スコア。駅力の影響。 |
立地価値スコア | 駅スコアと徒歩分を組み合わせた複合的な立地指標。 |
市区町村平均価格 | そのエリアの相場観をモデルに伝える。 |
市区町村_code | エリア情報を機械学習が扱える数値ID。 |
徒歩階級_code | 徒歩距離をカテゴリ化した数値ID。 |
🧠 重回帰分析による価格予測
整備した特徴量を使って、中古戸建ての価格を予測するモデルを構築・学習させます。
📌 さらなる高みへ:この分析の改善点と次のステップ
今回の分析は、データで「割安らしき物件」を発見する第一歩です。
しかし、見えてきた課題、特に「安いのには理由がある物件」を自動で判別するためには、さらなる改善が必要だと思います。
検討中の改善アイデアは以下の通りです。
改善アイデア | 内容 |
---|---|
備考欄や物件詳細のテキスト解析 | 「再建築不可」「建築条件付き」「私道負担」などのリスクを示唆するキーワードを自然言語処理で検出。 |
接道情報の数値化 | 前面道路の幅員や接道長をデータとして取得・加工し、モデルに加える。 |
ハザードマップとの連携 | 浸水区域、土砂災害警戒区域などのリスク情報を物件所在地と紐付けて特徴量にする。 |
他のデータソースの活用 | 土地の公示価格・基準地価、周辺施設の情報などを組み合わせる。 |
これらの要素を取り込むことで、 単に「相場より安い」だけでなく、「リスクが少なく、投資対象として検討に値する割安物件」 を絞り込める可能性が高まります。
✅ まとめ:データ分析で不動産投資の新たな視点
今回のPythonを使ったSUUMOデータ分析で、以下の大きな成果と学びを得ることができました。
✔︎ 「勘」ではなく、データで「相場からの乖離」を捉えられるようになった!
物件の適正価格をデータで予測し、実際の価格と比較することで、客観的な「割安度」を算出するロジックを構築できた。これまで属人的な作業と評価から脱却の兆しが見えたと思います。
✔︎ データは強力な「発見ツール」だが、全てではないことを理解!
データが示す「割安」が、そのまま「お買い得」ではない現実を痛感。安さの裏に隠されたリスクを見抜くことの重要性を再認識できました。
不動産投資において、データ分析は万能ではありません。しかし、広大な物件情報の中から可能性のあるものを効率的に見つけ出し、「相場観」を客観的に補強するための、強力な武器となると考えています。
💬 最後に、未来のデータ駆動型投資へ
今回の分析は、あくまで個人的なプロジェクトの途中経過であり、改善の余地は多々あります。
しかし、自分の手でデータを集め、分析し、仮説検証を繰り返す過程で、単に「安い物件」を見つけるだけでなく、「なぜその物件がその価格なのか?」という不動産の本質に、データを通して迫ることができたのではないかと感じています。
これは、不動産投資に必要な「目利き力」を養う上で、非常に貴重な経験でした。
もし今あなたが、「なんとなく」の物件探しに限界を感じているなら。
データ分析という新しい武器を手に入れて、不動産投資の精度を一緒に高めていきませんか?
この分析が、あなたのデータを元にした不動産投資の参考になれば嬉しいです。
「いいね!」やコメントで、あなたの感想やアイデアをぜひお聞かせください!