← Files tochi-satei-kunARCHIVED FILE
skills/tochi-satei-kun/scripts/main.py
11.2 KB · Oct 2, 2026 · 00:30 UTC
# Copyright 2026 Koichi Matsuda / SignalYield Advisory
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""パイプライン全体のオーケストレータ。Claude が SKILL.md の指示でこれを呼ぶ。
v1.2.9 で、koji/kijun の標準価格計算・公示番号変換等の補助関数群を
`main_helpers.py` に分離し、本ファイルを 20KB 未満に圧縮。Cowork の
プラグイン配布層によるファイル truncate を回避するため。
使い方:
python main.py <property.json> <mlit.csv> <koji.csv> [<kijun.csv>] [--out <dir>] [--asof YYYY-MM-DD]
"""
import argparse
import json
import math as _m
import shutil
import sys
from datetime import date, datetime
from pathlib import Path
import pandas as pd
from load_mlit import load_mlit_csv, load_koji_auto, load_kijun_auto
from scope import scope_dataframe, filter_recent_for_comparison, DEFAULT_COMPARISON_MONTHS
from similarity import compute_similarity, top_k
from time_adjust import annual_rate_for_city, apply_time_adjustment
from hedonic import fit_hedonic, annotate_district_mean, annotate_station_mean, DIR_SCORE
from correction import (apply_correction, correction_breakdown, hijun_correction_for_case,
compute_target_district_mean, compute_target_station_mean)
from hijun_breakdown import hijun_breakdown_detail # v1.2.9: correction.py から分離
from aggregation import assess
from xlsx_writer import write_xlsx
from json_writer import write_json
# v1.2.9: 補助関数を main_helpers に分離
from main_helpers import (
_hedonic_population_predict,
_standard_price_for_city,
_compute_koji_timeseries,
)
def run_pipeline(property_path: str, mlit_path: str, koji_path: str, kijun_path: str = None,
out_dir: str = None, asof: date = None, json_out_path: str = None) -> Path:
# 1. 入力読込
with open(property_path, encoding="utf-8") as f:
target = json.load(f)
if asof is None:
asof_str = target.get("査定時点")
if asof_str:
asof = datetime.strptime(asof_str, "%Y-%m-%d").date()
else:
asof = date.today()
df = load_mlit_csv(mlit_path)
koji = load_koji_auto(koji_path)
kijun = load_kijun_auto(kijun_path) if kijun_path else None
# 2. スコープ
scoped, scope_log = scope_dataframe(df, target, asof)
# 3. 時点修正(地区一致優先+直近1年変動率)
rate_info = annual_rate_for_city(
koji, kijun, target["市区町村名"],
target_district=target.get("地区名"),
asof=asof,
)
adjusted = apply_time_adjustment(scoped, asof, rate_info["rate"])
# 3b. 地区/最寄駅 平均単価をターゲット符号化として df に annotate
adjusted = annotate_district_mean(adjusted)
adjusted = annotate_station_mean(adjusted)
target["_target_district_mean"] = compute_target_district_mean(adjusted, target)
target["_target_station_mean"] = compute_target_station_mean(adjusted, target)
# 4. ヘドニック回帰(MLIT全期間で係数推定、n 最大化)
hed = fit_hedonic(adjusted)
# 5. 類似度 → top 3
recent = filter_recent_for_comparison(adjusted, asof, months=DEFAULT_COMPARISON_MONTHS)
if len(recent) < 3:
recent = adjusted
scope_log["warnings"].append(
f"直近{DEFAULT_COMPARISON_MONTHS}ヶ月の事例が3件未満のため、比準事例選定も全期間から実施"
)
scope_log["comparison_recent_count"] = len(recent)
sim = compute_similarity(recent, target)
top_cases = top_k(sim, k=3)
# 6. 個別格差補正
corrected = apply_correction(top_cases, hed, target)
breakdown = correction_breakdown(corrected, hed)
# 6b. 比準表データ生成(鑑定書様式)
def _kobetsu_pct(beta, tx_val, cx_val):
if beta is None:
return 0.0
contrib = float(beta) * (float(tx_val) - float(cx_val))
return (_m.exp(contrib) - 1.0) * 100
coef = hed.get("coefficients", {}) if hed.get("ok") else {}
target_dir_score = float(DIR_SCORE.get(str(target.get("前面道路:方位", "")).strip(), 0))
target_fusei = 1.0 if target.get("土地の形状") == "不整形" else 0.0
# 角地:MLIT データに角地情報無し → 業者明示入力のみ採用(自動デフォルト無し)
kado_explicit = target.get("角地補正率(%)")
if kado_explicit is None:
target_kado = 0.0
else:
try:
target_kado = float(kado_explicit)
except (TypeError, ValueError):
target_kado = 0.0
hijun_rows = []
hijun_detail_rows = []
for idx, (_, row) in enumerate(corrected.iterrows()):
h = hijun_correction_for_case(row, hed, target)
case_dir_score = float(DIR_SCORE.get(str(row.get("road_dir", "")).strip(), 0))
case_fusei = 1.0 if row.get("shape") == "不整形" else 0.0
h["個別格差_角地"] = target_kado
h["個別格差_方位"] = _kobetsu_pct(
coef.get("dir_score", {}).get("beta") if "dir_score" in coef else None,
target_dir_score, case_dir_score)
h["個別格差_不整形"] = _kobetsu_pct(
coef.get("D_fuseikei", {}).get("beta") if "D_fuseikei" in coef else None,
target_fusei, case_fusei)
# 方位・不整形は正本価格でβ補正済み。個別格差欄は説明表示に留め、
# 価格へ再乗算しない。角地のみ、明示入力時に apply_correction 側で1回適用。
h["個別格差_総和_pct"] = 0.0
h["個別格差_総和_factor"] = 100.0
h["案件査定価格"] = float(row["corrected_unit_price"])
case_no = row.get("case_no")
h["事例番号"] = int(case_no) if pd.notna(case_no) else (idx + 1)
h["順位"] = "規範性の高い事例" if idx == 0 else f"類似事例{['②','③','④','⑤'][idx-1] if idx-1 < 4 else idx+1}"
h["取引価格"] = float(row["unit_price"])
h["地区"] = row.get("district", "")
h["取引時点"] = str(row.get("transaction_date", ""))
h["取引四半期"] = row.get("transaction_quarter_str", "") or str(row.get("transaction_date", ""))
h["面積"] = int(row["area"])
h["最寄駅"] = row.get("station", "")
h["駅距離"] = row.get("walk_min", "")
h["道路種別"] = row.get("road_type", "")
h["道路幅員"] = row.get("road_width", "")
h["方位"] = row.get("road_dir", "")
h["形状"] = row.get("shape", "")
h["用途地域"] = row.get("zoning", row.get("city_planning", ""))
h["容積率_pct"] = row.get("floor_area_ratio", "")
hijun_rows.append(h)
detail = hijun_breakdown_detail(row, hed, target)
detail["記号"] = chr(ord("A") + idx)
detail["事例番号"] = h["事例番号"]
hijun_detail_rows.append(detail)
# 7. ヘドニック母集団予測
hed_pred = _hedonic_population_predict(hed, target)
# 9. 集約:apply_correction が生成した正本価格系列をそのまま使う。
assessment = assess(corrected, target["面積(㎡)"])
# 10. 地域標準価格チェック
standard_check = _standard_price_for_city(
koji, kijun, target["市区町村名"], asof,
target_district=target.get("地区名"),
target=target,
)
# 11. xlsx 出力
out_dir = Path(out_dir) if out_dir else Path(property_path).parent / "output"
fname = f"土地査定_{target.get('物件略号', 'NONAME')}_{asof.strftime('%Y%m%d')}.xlsx"
out_path = out_dir / fname
ctx = {
"target": target,
"asof": asof,
"scope_log": scope_log,
"rate_info": rate_info,
"hedonic": hed,
"cases": corrected,
"breakdown": breakdown,
"assess": assessment,
"refs": {"hedonic_pred": hed_pred},
"standard_check": standard_check,
"hijun_rows": hijun_rows,
"hijun_detail_rows": hijun_detail_rows,
"koji_timeseries": _compute_koji_timeseries(
koji, target["市区町村名"], target.get("地区名"),
selected_ids=[pt["id"] for pt in standard_check.get("selected_points", [])]
),
"adjusted_full": adjusted,
"raw_case_count": len(df),
}
write_xlsx(ctx, out_path)
if json_out_path:
write_json(ctx, json_out_path)
return out_path
def _copy_to_user_desktop(src_path: Path):
"""v1.2.7: 生成された xlsx をユーザーのデスクトップに自動コピー。
Windows MAX_PATH(259 文字)制限回避。コピー成功時は Path、失敗時 None。
"""
home = Path.home()
candidates = [
home / "OneDrive" / "デスクトップ",
home / "OneDrive" / "Desktop",
home / "Desktop",
]
for dest_dir in candidates:
try:
if dest_dir.exists() and dest_dir.is_dir():
dest_path = dest_dir / src_path.name
shutil.copy2(src_path, dest_path)
return dest_path
except (OSError, PermissionError):
continue
return None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("property", help="査定対象物件JSON")
ap.add_argument("mlit", help="MLIT 取引価格情報CSV")
ap.add_argument("koji", help="公示地価CSV")
ap.add_argument("kijun", nargs="?", default=None,
help="基準地価CSV(任意・後方互換用。通常は指定しない)")
ap.add_argument("--out", default=None, help="出力ディレクトリ")
ap.add_argument("--asof", default=None, help="査定時点 YYYY-MM-DD")
ap.add_argument("--json-out", default=None,
help="決定的JSON出力先ファイルパス(任意。Codex/Claude間の査定値突合用)")
args = ap.parse_args()
asof = datetime.strptime(args.asof, "%Y-%m-%d").date() if args.asof else None
out_path = run_pipeline(args.property, args.mlit, args.koji, args.kijun,
out_dir=args.out, asof=asof, json_out_path=args.json_out)
print(f"[OK] 生成完了: {out_path}")
print("[i] 本出力は机上査定(参考値)です。不動産鑑定評価ではありません。")
desktop_copy = _copy_to_user_desktop(out_path)
if desktop_copy:
print(f"[OK] デスクトップにコピー: {desktop_copy}")
else:
print("[!] デスクトップへの自動コピーに失敗しました。手動でコピーしてください。")
if args.json_out:
print(f"[OK] JSON出力: {args.json_out}")
if __name__ == "__main__":
main()
SHA-256: 880f1f3737603c6c68df67053d725bbe9a8669c0d0641e22b6e6d3f7c2cd420e