ai-market-mechanism/plots/generate_plots.py

221 lines
8.2 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""生成试点数据分析图表。
输出:
plots/pilot_run_data.png — 各地市预警数据与接入时间线(多面板)
plots/vendor_effort.png — 供应商人天投入饼图
plots/access_cost.png — 接入链路耗时对比图
"""
import csv
import os
import sys
from datetime import datetime
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import matplotlib.dates as mdates
PLOT_DIR = os.path.dirname(os.path.abspath(__file__))
_CJK_FONT = "Noto Sans CJK SC"
_STAGE_COLORS = {
"稳定运行": "#2E7D32",
"实际产出": "#1565C0",
"推广接入": "#BDBDBD",
}
def load_csv(name):
path = os.path.join(PLOT_DIR, name)
with open(path, encoding="utf-8") as f:
return list(csv.DictReader(f))
def _parse_date(s):
s = (s or "").strip()
if not s:
return None
for fmt in ("%Y-%m-%d", "%Y-%m"):
try:
return datetime.strptime(s, fmt)
except ValueError:
continue
return None
def plot_run_data():
rows = load_csv("pilot_run_data.csv")
plt.rcParams["font.sans-serif"] = [_CJK_FONT]
plt.rcParams["axes.unicode_minus"] = False
cities = [r["city"] for r in rows]
alerts = [int(r["alert_count"]) if r.get("alert_count") else 0 for r in rows]
stages = [r.get("stage", "") for r in rows]
bar_colors = [_STAGE_COLORS.get(s, "#999") for s in stages]
fig, (ax1, ax2) = plt.subplots(
2, 1, figsize=(13, 9), gridspec_kw={"height_ratios": [3, 2]}
)
fig.suptitle(
"图 1 电量突增能力应用运行数据可视化(截至 2026-06-25)",
fontsize=14, fontweight="bold", y=0.98,
)
# ── Panel A: 横向柱状图 ──
y_pos = list(range(len(cities)))
bars = ax1.barh(y_pos, alerts, color=bar_colors, edgecolor="white", linewidth=0.6, height=0.65)
ax1.set_yticks(y_pos)
ax1.set_yticklabels(cities, fontsize=10)
ax1.set_xlabel("累计预警数据(条)", fontsize=10)
ax1.invert_yaxis()
ax1.grid(axis="x", alpha=0.25, linestyle="--")
ax1.spines["top"].set_visible(False)
ax1.spines["right"].set_visible(False)
x_max = max(alerts) if max(alerts) > 0 else 10000
ax1.set_xlim(0, x_max * 1.28)
for i, (val, stage) in enumerate(zip(alerts, stages)):
if val > 0:
ax1.text(val + x_max * 0.01, i, f"{val:,}",
va="center", fontsize=9, fontweight="bold", color="#333")
else:
ax1.text(x_max * 0.01, i, "接入中(尚无产出)",
va="center", fontsize=8, color="#888")
ax1.text(x_max * 1.22, i, stage,
va="center", fontsize=8, color=_STAGE_COLORS.get(stage, "#666"))
legend_elems = [Patch(facecolor=_STAGE_COLORS[s], label=s) for s in ["稳定运行", "实际产出", "推广接入"]]
ax1.legend(handles=legend_elems, loc="lower right", fontsize=9, framealpha=0.9, title="运行阶段")
stats_text = (
"汇总指标(表 2)\n"
"─────────────\n"
"异常命中 144,853 条\n"
"预警推送 476 次\n"
"监测规则 10 条\n"
"覆盖人员 6 名\n"
"推广培训 7 场\n"
"对接地市 8 个"
)
ax1.text(0.98, 0.02, stats_text, transform=ax1.transAxes,
fontsize=8, va="bottom", ha="right", family="Noto Sans Mono CJK SC",
bbox=dict(boxstyle="round,pad=0.5", facecolor="#F5F5F5", edgecolor="#CCC", alpha=0.92))
# ── Panel B: 接入时间线 ──
data_end = datetime(2026, 6, 25)
for i, r in enumerate(rows):
start = _parse_date(r.get("start_date"))
output = _parse_date(r.get("output_date"))
stage = r.get("stage", "")
color = _STAGE_COLORS.get(stage, "#BDBDBD")
if start:
ax2.barh(i, (data_end - start).days, left=mdates.date2num(start),
height=0.5, color=color, edgecolor="white", linewidth=0.5, alpha=0.85)
if output:
ax2.plot(output, i, marker="D", markersize=7, color="#E65100",
markeredgecolor="white", markeredgewidth=0.8, zorder=5)
ax2.annotate(f"产出\n{output.strftime('%m-%d')}",
xy=(output, i), xytext=(8, 0), textcoords="offset points",
fontsize=7, color="#E65100", va="center")
ax2.set_yticks(y_pos)
ax2.set_yticklabels(cities, fontsize=10)
ax2.invert_yaxis()
ax2.set_title("各地市接入时间线与产出里程碑", fontsize=11, pad=8)
ax2.xaxis.set_major_locator(mdates.MonthLocator())
ax2.xaxis.set_minor_locator(mdates.DayLocator(bymonthday=[10, 20]))
ax2.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
ax2.grid(axis="x", alpha=0.2, linestyle="--")
ax2.spines["top"].set_visible(False)
ax2.spines["right"].set_visible(False)
from matplotlib.lines import Line2D
diamond = Line2D([0], [0], marker="D", color="w", markerfacecolor="#E65100",
markersize=7, label="产出起始")
ax2.legend(handles=[diamond], loc="lower right", fontsize=9, framealpha=0.9)
plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.savefig(os.path.join(PLOT_DIR, "pilot_run_data.png"), dpi=150, bbox_inches="tight")
plt.close()
def plot_vendor_effort():
rows = load_csv("vendor_effort.csv")
labels = [r["category"] for r in rows]
sizes = [int(r["person_days"]) for r in rows]
fig, ax = plt.subplots(figsize=(8, 6))
wedges, texts, autotexts = ax.pie(
sizes, labels=labels, autopct="%1.0f%%", startangle=140,
textprops={"fontsize": 9},
)
ax.set_title("供应商人天投入结构(合计 100 人天)")
plt.tight_layout()
plt.savefig(os.path.join(PLOT_DIR, "vendor_effort.png"), dpi=150)
plt.close()
def plot_access_cost():
rows = load_csv("access_cost.csv")
steps = [r["step"] for r in rows if r["step"] != "端到端合计"]
ideal = [int(r["ideal_days"]) for r in rows if r["step"] != "端到端合计"]
actual = [int(r["actual_days"]) for r in rows if r["step"] != "端到端合计"]
x = range(len(steps))
width = 0.35
fig, ax = plt.subplots(figsize=(10, 5))
ax.bar([i - width / 2 for i in x], ideal, width, label="理想耗时", color="#4CAF50")
ax.bar([i + width / 2 for i in x], actual, width, label="实际耗时", color="#FF5722")
ax.set_ylabel("耗时(工作日)")
ax.set_title("单地市接入链路耗时:理想 vs 实际")
ax.set_xticks(x)
ax.set_xticklabels(steps, rotation=20, ha="right")
ax.legend()
plt.tight_layout()
plt.savefig(os.path.join(PLOT_DIR, "access_cost.png"), dpi=150)
plt.close()
def plot_city_comparison():
rows = load_csv("city_comparison.csv")
cities = [r["city"] for r in rows]
alerts = [int(r["alert_count"]) for r in rows]
teams = [r["team"] for r in rows]
colors = {"第一梯队": "#4CAF50", "第二梯队": "#2196F3", "第三梯队": "#BDBDBD"}
bar_colors = [colors.get(t, "#999") for t in teams]
fig, ax = plt.subplots(figsize=(10, 5))
bars = ax.bar(cities, alerts, color=bar_colors)
ax.set_ylabel("累计预警数据(条)")
ax.set_title("各地市接入阶段与产出对比(截至 2026-06-25)")
for bar, val, team in zip(bars, alerts, teams):
if val > 0:
ax.text(bar.get_x() + bar.get_width() / 2, val + 200,
f"{val:,}", ha="center", fontsize=9)
else:
ax.text(bar.get_x() + bar.get_width() / 2, 200,
team, ha="center", fontsize=8, color="#666")
from matplotlib.patches import Patch
legend = [Patch(facecolor=colors[t], label=t) for t in ["第一梯队", "第二梯队", "第三梯队"]]
ax.legend(handles=legend, loc="upper right")
plt.xticks(rotation=30, ha="right")
plt.tight_layout()
plt.savefig(os.path.join(PLOT_DIR, "city_comparison.png"), dpi=150)
plt.close()
def main():
plot_run_data()
plot_vendor_effort()
plot_access_cost()
plot_city_comparison()
print("图表已生成:pilot_run_data.png, vendor_effort.png, access_cost.png, city_comparison.png")
if __name__ == "__main__":
main()