#!/usr/bin/env python3
"""
Generate trend analysis HTML from JD法拍 CSV files.
Reads all *_法拍房源*.csv files in output/法拍/, merges by paimaiId,
computes monthly trends (上架量/流拍量), and generates an interactive HTML chart.
"""
import csv
import json
import os
import glob
from collections import Counter
CSV_DIR = "output/法拍"
OUTPUT_HTML = os.path.join(CSV_DIR, "东莞法拍房趋势分析.html")
def read_all_csvs(csv_dir):
all_items = {}
csv_files = glob.glob(os.path.join(csv_dir, "*_法拍房源*.csv"))
for csv_path in csv_files:
try:
with open(csv_path, encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
for row in reader:
pid = row.get("拍卖ID", "")
if not pid:
continue
if pid not in all_items:
all_items[pid] = row
else:
existing = all_items[pid]
if row.get("结束时间") and not existing.get("结束时间"):
all_items[pid] = row
except Exception as e:
print(f"Warning: failed to read {csv_path}: {e}")
return list(all_items.values())
def compute_monthly_trends(items, region_filter=None):
if region_filter:
items = [r for r in items if region_filter in r.get("标题", "")]
listed_by_month = Counter()
failed_by_month = Counter()
ended_by_month = Counter()
for r in items:
start = r.get("开始时间", "")[:7]
end = r.get("结束时间", "")[:7]
status = r.get("状态", "")
bid_count = r.get("出价次数", "")
if start:
listed_by_month[start] += 1
if end and status == "已结束":
ended_by_month[end] += 1
if bid_count in ("0", ""):
failed_by_month[end] += 1
month_set = (
set(listed_by_month.keys())
| set(ended_by_month.keys())
| set(failed_by_month.keys())
)
if not month_set:
return []
all_months = sorted(month_set)
start_m = all_months[0]
end_m = all_months[-1]
full_months = []
y, m = int(start_m[:4]), int(start_m[5:7])
ey, em = int(end_m[:4]), int(end_m[5:7])
while (y, m) <= (ey, em):
full_months.append(f"{y:04d}-{m:02d}")
m += 1
if m > 12:
m = 1
y += 1
results = []
for month in full_months:
listed = listed_by_month.get(month, 0)
failed = failed_by_month.get(month, 0)
ended = ended_by_month.get(month, 0)
rate = round(failed / ended * 100, 1) if ended > 0 else None
results.append(
{
"month": month,
"listed": listed,
"failed": failed,
"ended": ended,
"rate": rate,
}
)
return results
def generate_svg(data, panel_idx):
n = len(data)
if n == 0:
return ""
W, H = 1120, 240
padL, padR, padT, padB = 52, 24, 16, 40
plotW = W - padL - padR
plotH = H - padT - padB
max_val = max((max(d["listed"], d["failed"]) for d in data), default=1)
max_val = max(max_val, 5)
y_ticks = 5
y_step_val = max_val / y_ticks if max_val > 0 else 1
def y_pos(val):
if max_val == 0:
return padT + plotH
return padT + plotH - (val / max_val) * plotH
def x_pos(i):
if n == 1:
return padL + plotW / 2
return padL + (i / (n - 1)) * plotW
svg_parts = []
# Y-axis grid lines and labels
for t in range(y_ticks + 1):
val = t * y_step_val
y = y_pos(val)
svg_parts.append(
f'
{date_range} · 按拍卖开始时间月度汇总 · 流拍 = 已结束且出价次数为0
注:上架量按「开始时间」归入对应月份,流拍量按「结束时间」归入对应月份。流拍率 = 流拍量 / 已结束量。近期月份(最近2-3个月)的已结束量和流拍量可能不完整(部分拍卖尚未结束)。
| 区域 | 月份 | 上架量 | 流拍量 | 已结束 | 流拍率 |
|---|