不動産価格のAI予測は、モデルを選ぶところよりデータの入手と評価設計でつまずきます。学習データをどこから取るのか、どの列を特徴量に入れてよいのか、出てきた精度をどこまで信じてよいのか。国土交通省が公開しているAPIの実際の仕様を確認したうえで、前処理のコード、精度を偽装する列の見分け方、分割方法による評価のブレまでを実測値つきで整理します。
まとめ
先に結論を並べます。次の5点を押さえておけば大きな事故は避けられます。
- データ入手:国土交通省「不動産情報ライブラリ」のXIT001が実質的な標準。申請制で、審査結果の通知は申請後5営業日が目安。
- 旧APIは使えない:かつて広く使われた「土地総合情報システム」は令和6年3月末で廃止され、ドメインごと消滅。そのURLのコードは名前解決の段階で失敗する。
- リーク列に注意:レスポンスの坪単価・平方メートル単価は面積と組めば価格が定まる列。特徴量に入れると誤差率が3分の1に見えるが、本番では埋まらない。
- モデル選択の効果は小さい:実測では重回帰とLightGBMの差が0.9ポイント、特徴量を3列から10列に増やした効果は4.5ポイント。手を入れる順序は特徴量が先。
- 評価は時点で分ける:価格水準が上昇する市場ではランダム分割の交差検証が実力を過大評価する。国内のマンション価格指数は直近10年で1.80倍。
以下、根拠と実装を順に見ていきます。
予測対象の切り分け|取引価格・成約価格・分譲価格の違い
最初に決めるべきは、何の価格を当てにいくのかです。ここが曖昧なまま学習を始めると、集めたデータと目的がずれたまま精度だけを追いかけることになります。公開データで扱える価格は大きく2種類あります。XIT001のパラメータ priceClassification がまさにこの区別で、01 が不動産取引価格情報、02 が成約価格情報です。前者は不動産取引価格情報提供制度によるもので、平成18年4月から不動産取引当事者へのアンケート調査をもとに蓄積されており、令和7年3月31日時点で約547万件が提供されています。APIで指定できる期間も両者で違い、取引価格は2005年から(2005年は第3四半期と第4四半期のみ)、成約価格は2021年からです。
アンケートに基づく以上、すべての取引が入っているわけではありません。件数も種類によって偏っており、令和7年3月31日時点の累計は宅地(土地と建物)1,998,331件、宅地(土地)1,873,353件、中古マンション等893,567件に対し、農地466,257件、林地237,408件です。農地や林地のモデルは学習データが1桁少ないところから始まります。
一方で、新築分譲マンションの販売価格そのものを網羅した公開データセットは存在しません。分譲価格は売主が設定する提示価格であり、成約後の取引価格とは性質が違います。新築の値付けを予測したいなら、周辺の中古取引価格や地価から相対的に推定するアプローチになります。
なお、不動産会社の「AI査定」は価格の推定に営業上の判断が混ざった結果です。本記事で扱うのは、取引価格を目的変数に置いた回帰問題に限ります。
学習データの入手先|国土交通省API(XIT001)の仕様と申請
エンドポイントと必須パラメータ
不動産情報ライブラリのAPIは都市計画や防災情報まで含めて30種類以上が公開されており、価格予測で中心になるのは XIT001「不動産価格(取引価格・成約価格)情報取得API」です。
| 項目 | 内容 |
|---|---|
| エンドポイント | https://www.reinfolib.mlit.go.jp/ex-api/external/XIT001 |
| 認証 | HTTPヘッダー Ocp-Apim-Subscription-Key にAPIキー |
| year(必須) | 取引時期(西暦4桁) |
| quarter | 四半期(1〜4) |
| area / city / station | 都道府県コード / 市区町村コード / 駅コード |
| priceClassification | 01=取引価格、02=成約価格、未指定=両方 |
| レスポンス | gzipエンコードされたJSON |
地域を指定する area、city、station は少なくとも1つを必ず指定する決まりです。全国を一度に取ることはできないため、対象エリアの市区町村コードを列挙してループで回す設計になります。駅コードは国土数値情報の駅データのグループコードを使いますが、対応する駅データが無いコードでは400エラーが返るので、失敗したら市区町村指定に切り替えてください。
APIキーは申請制です。利用者種別(法人・法人以外の団体・個人)を選んで利用約款に同意する形式で、審査結果の通知は申請後5営業日が目安とされています。検証の日程から逆算して先に申請しておいてください。
旧「土地総合情報システム」APIの終了
ここが最初の落とし穴です。以前は「土地総合情報システム」の www.land.mlit.go.jp/webland/api/ 配下がAPIキー不要で呼べたため、多くの解説記事やサンプルコードが今もこのURLを載せています。
このシステムは国土交通省が令和6年3月末で廃止したと公式に告知しており、取引価格情報は令和6年4月から不動産情報ライブラリへ移りました。しかもホスト名自体が現在はDNSにレコードを持ちません。パブリックDNS(8.8.8.8、1.1.1.1)に www.land.mlit.go.jp と land.mlit.go.jp のAレコードを問い合わせても応答は空で、同じ mlit.go.jp 配下の www.mlit.go.jp は正常に解決します。サーバーが落ちているのではなくドメインが引き払われた状態なので、旧URLのコードはHTTPエラーですらなく名前解決の失敗で止まります。
現行APIはキーを付けずに叩くと401で missing subscription key、でたらめなキーなら同じく401で invalid subscription key を返します。401が返るならエンドポイントには到達できている、という切り分けに使えます。
取得コードとgzipの扱い
レスポンスはgzipでエンコードされて返ります。公式のコマンド例が curl に --compressed を付けているのはこのためで、付け忘れるとバイナリがそのまま出力されます。Pythonの requests は Content-Encoding: gzip が付いていれば自動で展開するため、通常は追加の指定が要りません。うまく読めない場合は r.headers.get("Content-Encoding") を確認し、必要なら gzip.decompress() を明示的に呼んでください。
import requests
URL = "https://www.reinfolib.mlit.go.jp/ex-api/external/XIT001"
HEADERS = {"Ocp-Apim-Subscription-Key": "<申請して発行されたAPIキー>"}
def fetch(year, quarter, city, price_classification="01"):
params = {"year": year, "quarter": quarter, "city": city,
"priceClassification": price_classification}
r = requests.get(URL, headers=HEADERS, params=params, timeout=30)
r.raise_for_status()
return r.json() # gzip は requests が自動で展開する
# 中央区の 2015年第2四半期・取引価格情報のみ
payload = fetch(2015, 2, "13102")
# 公式の操作説明ページは出力例の欄が空のまま掲載されているため、
# レコード配列がどのキーに入るかは最初の1回を目視して確認する
print(type(payload), str(payload)[:300])
なお、公式のAPI操作説明ページは「<出力例>」の欄が空のまま掲載されており、レスポンスのトップレベルがどういう形なのかは書かれていません。項目名と型は一覧で定義されている一方で、レコードの配列がどのキーに入るかはドキュメントから読み取れないため、キーを取得したら最初の1回はレスポンス全体をそのまま出力して確認してください。
市区町村コードは XIT002(都道府県内市区町村一覧取得API)で取得できます。なお地区コードは更新のたびに変わることがあり、過去に取得した地区コード・地区名との継続性は保証しないと公式に明記されています。地区コードをキーに過去データと結合する設計は避けてください。
前処理|文字列型のレスポンスと有効数字2桁の丸め
XIT001の出力は、取引価格も面積も建蔽率も、仕様上すべて文字列型です。マニュアルのデータ例では坪単価や間取りの欄が空欄のまま掲載されているので、値の無い項目は空文字で返るとみておくのが安全です。そのまま pandas.DataFrame に渡すと数値列がobject型のままになり、学習の直前でエラーになります。建築年は「1972年」、取引時点は「2015年第2四半期」という日本語混じりの表記なので、それぞれ専用のパーサが必要です。
import re
import pandas as pd
# XIT001 の項目はすべて文字列型で返り、値の無い項目は空文字になる。
# そのまま DataFrame に渡すと数値列が object 型のままになり、学習時に落ちる。
LEAKY = ("PricePerUnit", "UnitPrice") # 取引価格から作られた列=説明変数にできない
def to_num(s):
"""'85000000' -> 85000000.0 / '' や非数値 -> None
※「2000以上」のような上限つきの表記は数値だけを拾ってしまうため、
実データを見て上限表記の有無を確認し、必要なら別途フラグを立てる"""
if s is None:
return None
s = re.sub(r"[^\d.]", "", str(s))
return float(s) if s else None
def build_year(s):
"""'1972年' -> 1972 / 変換できない値 -> None"""
m = re.match(r"(\d{4})年", str(s or ""))
return int(m.group(1)) if m else None
def period_index(s):
"""'2015年第2四半期' -> 8061(四半期の通し番号)"""
m = re.match(r"(\d{4})年第(\d)四半期", str(s or ""))
return int(m.group(1)) * 4 + int(m.group(2)) - 1 if m else None
def to_frame(records):
df = pd.DataFrame(records)
df = df.drop(columns=[c for c in LEAKY if c in df.columns])
for col in ("TradePrice", "Area", "TotalFloorArea", "Frontage",
"Breadth", "CoverageRatio", "FloorAreaRatio"):
if col in df.columns:
df[col] = df[col].map(to_num)
df["build_year"] = df["BuildingYear"].map(build_year)
df["t"] = df["Period"].map(period_index) # 時点分割にも使う
df["age"] = df["t"] // 4 - df["build_year"] # 取引時点の築年数
return df.dropna(subset=["TradePrice", "Area"])
実際に動かすと、建築年が空文字のレコードは build_year と age がNaNになり、取引価格と面積が揃っている行だけが残ります。period_index は「2015年第2四半期」を8061という整数に変換します。四半期を通し番号にしておくと、後述する時点での分割にそのまま使えるうえ、時間トレンドを表す特徴量としても単調増加の数値として渡せます。
欠損を落とすか埋めるかは列によって判断が分かれます。建築年の欠損は土地のみの取引で自然に発生するため、機械的に平均で埋めると土地と建物付きが混ざります。取引の種類(Type)で先に分けてから扱うほうが安全です。
もう一つ、目的変数そのものに前提があります。この制度の取引価格は有効数字2桁に丸められていると国土交通省が明記しており、8,500万円は「85」の精度でしか与えられません。影響を測るためAmes Housingの価格を有効数字2桁に丸めて元の値と比べたところ、丸めだけで平均1.36%ずれました。学習も評価も丸めた値で行うとMAPEは10.64%で、丸めない場合の10.39%から0.25ポイントの悪化です。MAPEが10%規模なら支配的ではありませんが、誤差数%を詰める段階では無視できない床になります。
リークする列の見分け方|単価列を入れた場合の実測
XIT001のレスポンスには PricePerUnit(坪単価)と UnitPrice(平方メートル単価)が含まれています。単価は価格に効きそうな情報に見えますが、単価と面積が分かれば取引価格は一意に決まります。予測したい価格が分かっていなければ埋まらない列なので、値が入っている行でこれを特徴量にすると、答えを渡したうえで答えを当てさせることになります。
どのくらい数字が変わるのかを実測しました。OpenMLの house_prices(米国アイオワ州エイムズ市の住宅取引データから学習用に切り出された1,460件。2006〜2010年の売買時点つき)を使い、面積・築年・敷地面積・品質評価など10列を基本セットとして、そこに「価格÷居住面積」で作った単価列を足した場合と足さない場合を比較しています。目的変数は対数変換し、乱数シードを3〜5本振った平均です。
| 特徴量 | ランダム5分割CV | 時点で分割 |
|---|---|---|
| 基本10列(対数変換) | MAPE 10.8% | MAPE 10.4% |
| 基本10列+単価列 | MAPE 3.4% | MAPE 2.7% |
誤差率が3分の1以下に縮んでいます。これを「精度が高いモデルができた」と報告してしまう事故は珍しくありません。本番で新しい物件を予測する場面ではその物件の単価はまだ分からないので、この精度は再現しません。
見分け方はシンプルで、その列は予測時点で手に入るのかを1列ずつ確認することです。目的変数から算術的に作られている列、成約後にしか確定しない列、集計値として目的変数を含んでいる列が典型的な危険パターンです。分割方法や前処理に起因するリークの整理は交差検証とは?分割手法の選び方とデータリーク対策を実装目線で解説【2026年版】にまとめています。
モデル選択より効くもの|手法の差と特徴量の差
重回帰・ランダムフォレスト・LightGBMの誤差差
候補として挙がるのは重回帰、ランダムフォレスト、勾配ブースティング(LightGBM)の3つです。同じAmes Housingで、2010年の売買をテストに回して比較しました。国内の取引データとは市場も通貨も違うため絶対値をそのまま持ち込むことはできませんが、手法間の差の大きさを見る目的では参考になります。実行環境はPython 3.14.6、scikit-learn 1.9.0、LightGBM 4.7.0です。
| モデル | MAPE | MAE |
|---|---|---|
| 重回帰(線形回帰) | 11.3% | 19,838ドル |
| ランダムフォレスト | 10.8% | 18,582ドル |
| LightGBM | 10.4% | 17,873ドル |
LightGBMが最も良いものの、重回帰との差は0.9ポイントです。決定木ベースの手法は非線形な効き方や特徴量同士の交互作用を自動で拾えるぶん有利ですが、劇的な差ではありません。学習時間はランダムフォレストが最も長く、LightGBMはその4分の1程度、重回帰はさらに1桁短く済みました。それぞれの仕組みはLightGBMとは?勾配ブースティングの仕組みと特徴・XGBoostとの違いを実装例つきで解説と決定木分析とは?分類木・回帰木の仕組みと過学習を防ぐ剪定・アンサンブルを実装目線で解説で扱っています。
特徴量3列・10列・36列での誤差変化
同じデータで、モデルをLightGBMに固定したまま特徴量の数だけを変えると、こうなります。
| 特徴量 | 列数 | MAPE |
|---|---|---|
| 面積・築年・敷地のみ | 3 | 14.9% |
| 品質評価や間取りを追加 | 10 | 10.4% |
| 数値列をすべて投入 | 36 | 9.3% |
3列から10列にしただけで4.5ポイント改善しており、モデルを差し替えて得られる0.9ポイントより大きく効いています。手を入れる順序としては、まず入手できる特徴量を増やすほうが費用対効果が高いということです。
XIT001でいえば、用途地域(CityPlanning)、建蔽率、容積率、前面道路の幅員、建物の構造を素通りさせずに使いきる余地があります。所在地は町・大字レベルまで提供されるため、地域の粒度も市区町村コードより細かく取れます。ただし地域を表す列を数値のまま渡すと、コードの大小に意味があるかのように学習されます。LightGBMはpandasのcategory型をカテゴリ特徴量として扱えるので、下の評価コードのように分割前に型を変えてください。特徴量の作り方そのものは特徴量とは?機械学習での意味と作り方・重要度の見方を実装目線で解説【2026年版】を参照してください。
精度検証の設計|分割方法と評価指標の選び方
ランダム分割と時点分割の使い分け
手元の数字が本番でも出るかは分割の仕方で決まります。よく使われるランダムな交差検証は学習データに未来の取引が混ざるため、価格水準が動く市場では甘い評価になります。ただし、これは常に起きるわけではありません。先ほどのAmes Housingで両方を測ると、ランダム5分割CVが10.8%、2010年をテストにした時点分割が10.4%で、時点分割のほうがわずかに良いという結果でした。このデータの年別中央価格は2006年の163,995ドルから2010年の155,000ドルまでほぼ横ばいで、そもそも水準が動いていないからです。
そこで、同じデータに年率の値上がりだけを人工的に与えて、乖離がどう広がるかを確認しました。
| 年率トレンド | ランダムCV | 時点分割 | 乖離 |
|---|---|---|---|
| 0%(元データ) | 10.8% | 10.4% | -0.4pt |
| 5% | 10.9% | 11.6% | +0.7pt |
| 10% | 11.0% | 13.9% | +3.0pt |
| 15% | 11.1% | 16.9% | +5.9pt |
ランダムCVの数字はトレンドが強くなってもほとんど動きません。値上がりの情報を学習データ側から受け取れてしまうためです。一方で時点分割は素直に悪化するので、ランダムCVの値は市場が動いている局面ほど実力から乖離することになります。
では日本の市場はどのくらい動いているのか。国土交通省の不動産価格指数(住宅・全国・原系列、平成22年=2010年平均を100とする指数)でマンション(区分所有)を見ると、2015年の年平均121.0に対して2025年の年平均は217.8で、10年で1.80倍、年率に直すと6.05%です。上の表でいえば年率5%と10%の間に位置します。国内の取引データで学習するなら、時点で分けた検証を標準にすべき水準です。
なお、この不動産価格指数は現在、公表が止まっています。国土交通省は令和8年7月29日付で、算出プログラムの不具合により令和8年4月以降に公表予定だった分(令和8年1月・2月・3月分および令和7年第4四半期分を含む)の公表を延期すると告知しており、2026年8月時点で入手できる最新値は2025年12月分(221.2)です。市場全体の水準を表す指数を特徴量や補正に使っている場合は、更新が止まっている前提でパイプラインを組んでおく必要があります。
import numpy as np
import lightgbm as lgb
from sklearn.metrics import mean_absolute_percentage_error
# df は前処理済み、t は四半期の通し番号(period_index の出力)
CUTOFF = df["t"].max() - 4 # 直近4四半期をテストに回す
df["MunicipalityCode"] = df["MunicipalityCode"].astype("category") # 地域は数値でなくカテゴリ
train, test = df[df["t"] <= CUTOFF], df[df["t"] > CUTOFF]
FEATURES = ["Area", "TotalFloorArea", "age", "CoverageRatio",
"FloorAreaRatio", "MunicipalityCode", "t"]
model = lgb.LGBMRegressor(n_estimators=400, learning_rate=0.05, random_state=0)
model.fit(train[FEATURES], np.log(train["TradePrice"])) # 対数で学習し
pred = np.exp(model.predict(test[FEATURES])) # 予測時に戻す
print(mean_absolute_percentage_error(test["TradePrice"], pred))
MAE・RMSE・MAPEの使い分け|高額物件が指標に与える影響
指標の選び方でも結論は変わります。同じ予測結果に対して、平均絶対誤差(MAE)は17,873ドル、二乗平均平方根誤差(RMSE)は27,985ドル、平均絶対パーセント誤差(MAPE)は10.4%でした。RMSEが大きく出るのは、誤差を二乗するため外れ値の影響を強く受けるからです。
実際、テストデータの価格上位10%(175件中18件)が二乗誤差の総和の48.5%を占めていました。件数では1割の高額物件が、RMSEという指標の半分を決めている計算です。この状態でRMSEを基準にモデルを選ぶと、大多数の物件での精度ではなく少数の高額物件を当てられるかで判断してしまいます。価格帯で分けて測ると、MAPEは下位90%で10.2%、上位10%で11.7%とほぼ変わらないのに対し、MAEは14,735ドルと45,242ドルで3倍以上開きます。
価格の幅が数百万円から数億円まで広がる対象では、価格帯によらず比較できるMAPEを主指標に置き、金額の実感を見る用途でMAEを併記するのが扱いやすい組み合わせです。RMSEは高額帯の外れを特に嫌う要件があるときに限って使ってください。
運用フェーズの設計|再学習頻度・外れ値・出典表示
再学習の頻度は、扱う物件の種類で要求が変わります。先ほどの年率6.05%はマンション(区分所有)の数字で、同じ指数でも住宅総合は3.28%、住宅地は1.97%、戸建住宅は1.86%です。マンションを扱うなら年1回の再学習では追いつきませんが、戸建や土地であればもう少し緩くても保ちます。XIT001が取引時期を四半期単位で区切っていることを踏まえると、四半期ごとに直近データを足して学習し直し、そのたびに直近の四半期をテストにした誤差を記録しておくのが実務的です。誤差が悪化し始めたら、市場の変化か特徴量の劣化かを切り分けます。
外れ値の扱いも無視できません。実際の取引には通常の市場取引とは言えない事情を抱えたものが混ざります。XIT001の Remarks(取引の事情等)が空でないレコードを除外するか残すかは、方針として決めておいてください。目的変数の対数変換は極端な高額物件の影響を抑えるため、上の実測でも前提にしています。
予測根拠の説明も、金額が大きく相手への説明が要る領域では避けて通れません。どの特徴量が予測をどう押し上げたのかを個別に示せる手法はモデル解釈性とは?説明可能性との違いとSHAP・LIMEの選び方を実装目線で解説で扱っています。
最後に出典の表示です。成果物を外部に出す場合は、利用約款とコンテンツの出典に関する規定を確認したうえで出典を明記してください。社内検証と外部公開で条件が変わる点は、着手前に押さえておくところです。
よくある質問
不動産価格の予測に必要なデータは無料で手に入りますか?
国土交通省の不動産情報ライブラリのAPIは利用料がかからず、申請して発行されるAPIキーで利用できます。ただし利用申請が必要で、審査結果の通知は申請後5営業日が目安です。無条件・即時に使えるわけではないため、検証スケジュールには申請期間を織り込んでください。
どのくらいの誤差率なら実用的といえますか?
用途によって基準が変わります。本記事で使った米国の公開データでの実測では、素直な特徴量セットでMAPEが10%前後でした。相場観を示す用途には足りますが、そのまま提示価格を決めるには足りません。目標値は既存の査定業務が持つ誤差と比べて決めるのが現実的です。なお数字が異様に良いときはリークを、悪いときは分割方法と特徴量の欠落を先に疑ってください。
ディープラーニングを使えば精度は上がりますか?
表形式のデータでは、勾配ブースティングに対する優位性はあまり期待できません。上の比較のとおり、手法の差より特徴量の差のほうが大きく出ます。物件写真や間取り図といった非構造化データを取り込む段階になって、はじめて検討する価値が出てきます。
時系列予測のライブラリを使ったほうがよいのでしょうか?
個別物件の価格を当てる問題は、時点を特徴量の一つとして持つ回帰問題として扱うのが基本です。市場全体の価格水準そのものを将来に向けて外挿したい場合は時系列の手法が向いており、手法の選び分けは需要予測アルゴリズムの選び方と実装手順|時系列・機械学習・Pythonで整理しています。