山东大学软件学院项目实训-创新实训-大数据租房推荐智能体-前端部分(6)
前面我们搞定了“专业报告”,让 AI 的推荐有了文字依据。但在复盘时我发现,纯文本和表格虽然严谨,却不够直观——用户很难一眼看出房源在“价格”与“通勤”之间的权衡关系。
为了解决这个问题,也为了满足任务书 4.2 节“图表生成时间 ≤1 秒”的性能指标,这一阶段我引入了交互式可视化模块。目标很明确:用雷达图展示六维能力,用柱状图对比通勤成本,并且在架构上坚持“数据预处理缓存 + 声明式绘图”的原则。
1. 选型抉择:Plotly 还是 Altair?
在动手写代码前,我对比了 Streamlit 生态中最主流的两个可视化库:
Altair:声明式语法极其简洁,适合快速出图,但在复杂的交互(如缩放、悬停高亮)上略显单薄。
Plotly:交互体验极佳,自带丰富的工具栏,且对雷达图等复杂图表的支持非常成熟。
考虑到我们需要展示“多维度评分雷达图”,并且希望用户在查看时能进行悬停查看详细分值,我最终选择了 Plotly。它的 graph_objects 能够完美承载任务书 2.4 要求的六大维度(价格、通勤、楼层、配套、户型、装修)。
2. 核心实现:charts.py 的数据归一化与渲染
这是本阶段的逻辑核心。由于雷达图的各个维度量纲完全不同(例如价格是几万,而装修分是0-10),直接绘图会导致坐标轴失真。因此,我在 frontend/app_lib/charts.py 中封装了数据清洗与归一化的逻辑。
设计亮点:
自动归一化:在 prepare_radar_series 函数中,将原始分数统一映射到 0-100 的展示区间,确保雷达图的正多边形视觉比例协调。
性能缓存:利用 @st.cache_data 装饰器对数据预处理过程进行缓存。只要房源列表和权重向量不变,再次渲染图表的时间几乎可以忽略不计,轻松满足“≤1秒”的性能要求。
双图联动:除了雷达图,我还同步实现了通勤时间的分组柱状图,方便用户直观对比不同房源的通勤成本。
import plotly.graph_objects as go
import streamlit as st
from typing import Sequence, Dict, Any
# 雷达图六大维度
RADAR_DIMENSION_KEYS = [
("price", "价格"),
("commute", "通勤"),
("floor", "楼层"),
("facilities", "配套"),
("layout", "户型"),
("decoration", "装修"),
]
def _score_to_0_100(value: float) -> float:
if value <= 1.0:
return max(0.0, min(100.0, value * 100.0))
if value <= 10.0:
return max(0.0, min(100.0, value * 10.0))
return max(0.0, min(100.0, value))
@st.cache_data(show_spinner=False)
def cached_radar_series(listings_json: str, weights_json: str) -> list[dict]:
import json
listings = json.loads(listings_json)
weights = json.loads(weights_json)
series = []
for li in listings:
raw_scores = {}
bd = li.get("score_breakdown", {})
# 提取并清洗分数
for key, label in RADAR_DIMENSION_KEYS:
# 兼容多种字段名
raw_key = _canonical_dim_key(key) or key
if raw_key in bd:
try:
raw_scores[key] = float(bd[raw_key])
except (TypeError, ValueError):
pass
values = [_score_to_0_100(raw_scores.get(key, 0)) for key, _ in RADAR_DIMENSION_KEYS]
series.append({
"name": li.get("title", "未知房源"),
"values": values
})
return series
def plot_radar(series: Sequence[Dict]) -> go.Figure:
fig = go.Figure()
for item in series:
fig.add_trace(go.Scatterpolar(
r=item["values"],
theta=[label for _, label in RADAR_DIMENSION_KEYS],
name=item["name"],
fill='toself'
))
fig.update_layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
showlegend=True,
title="房源六维能力雷达图"
)
return fig
def plot_commute_bars(rows: list[dict[str, Any]]) -> go.Figure:
fig = go.Figure()
if not rows:
fig.update_layout(title="暂无通勤数据")
return fig
# 自动检测交通方式(地铁/驾车)
modes = sorted({r.get("mode", "综合") for r in rows})
labels = sorted(set(r.get("label") for r in rows))
for mode in modes:
y_data = [next((r["minutes"] for r in rows if r.get("label") == label and r.get("mode") == mode), 0) for label in labels]
fig.add_trace(go.Bar(x=labels, y=y_data, name=mode))
fig.update_layout(
barmode="group",
title="通勤时长对比(分钟)",
yaxis_title="通勤耗时",
xaxis_tickangle=-45
)
return fig
3. UI 布局与交互反馈
为了不让图表抢占首屏地图和列表的空间,我采用了 st.tabs 标签页策略。
实现逻辑:
分层展示:在主界面设置「房源列表」、「分析报告」、「可视化图表」三个 Tab。用户只有在需要深度对比时才会点击进入图表页,保证了首屏加载的轻快。
性能监控:在图表底部实时显示渲染耗时,确保满足 ≤1s 的硬性指标。
空状态处理:当后端尚未返回评分数据时,图表区域会显示友好的占位提示,而不是报错或留白。
# frontend/app.py — 页面布局实现
import streamlit as st
from app_lib.charts import render_charts_panel
with tab_charts:
if not processed_listings:
st.info("暂无房源数据,请先进行筛选或对话推荐。")
else:
st.subheader("📊 房源多维可视化")
# 直接调用封装好的图表面板
render_charts_panel(processed_listings, preferences=user_preferences)
# 内部会显示类似:
# st.caption(f"图表渲染耗时约 {total_time:.0f} ms;目标 ≤1s")
4. 总结与展望
通过引入 Plotly 和数据缓存机制,我们成功补齐了智能体的“可视化拼图”。
直观性提升:雷达图让用户一眼看清房源的长板与短板,柱状图则量化了通勤痛点。
性能达标:借助 st.cache_data,即使面对几十套房源的批量渲染,前端也能保持丝滑流畅,实测渲染时间控制在 200ms - 600ms 之间。
至此,我们的系统已经具备了“记忆(持久化)、表达(报告)、洞察(图表)”三大能力。
更多推荐
所有评论(0)