Flask智能租房------详情页 完整知识点详解
一、详情页房源数据展示
1.1 路由与视图函数设计
知识点: Flask路由传参、视图函数返回模板、数据库查询
python
# ==================== app.py ====================
from flask import Flask, render_template, jsonify
from flask_sqlalchemy import SQLAlchemy
# 创建Flask应用实例
app = Flask(__name__)
# 配置数据库连接URI(以MySQL为例)
# 格式:mysql+pymysql://用户名:密码@主机:端口/数据库名
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/rental'
# 关闭SQLAlchemy的修改追踪事件,减少内存开销
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 创建SQLAlchemy数据库实例
db = SQLAlchemy(app)
# ==================== 定义房源数据模型 ====================
class House(db.Model):
"""房源信息表模型类"""
# 指定对应的数据库表名
__tablename__ = 'house'
# 房源ID,主键,自增
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# 房源标题(如"朝阳区精装两居室")
title = db.Column(db.String(200), nullable=False)
# 所在区域(如"朝阳区")
region = db.Column(db.String(50))
# 所在街道/商圈
street = db.Column(db.String(100))
# 小区名称
community = db.Column(db.String(100))
# 户型(如"2室1厅")
house_type = db.Column(db.String(50))
# 面积(平方米)
area = db.Column(db.Float)
# 朝向(如"南北")
orientation = db.Column(db.String(20))
# 楼层信息(如"中楼层/6层")
floor = db.Column(db.String(50))
# 月租金(元)
price = db.Column(db.Float)
# 每平方米单价
unit_price = db.Column(db.Float)
# 发布时间
publish_time = db.Column(db.DateTime)
# 图片链接
image_url = db.Column(db.String(500))
# 详情链接
detail_url = db.Column(db.String(500))
# ==================== 定义配套设施数据模型 ====================
class Facility(db.Model):
"""房源配套设施表模型类"""
__tablename__ = 'facility'
# 配套设施ID,主键
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# 关联的房源ID(外键)
house_id = db.Column(db.Integer, db.ForeignKey('house.id'))
# 是否有暖气
heating = db.Column(db.Boolean, default=False)
# 是否有天然气
gas = db.Column(db.Boolean, default=False)
# 是否有电梯
elevator = db.Column(db.Boolean, default=False)
# 是否有宽带
internet = db.Column(db.Boolean, default=False)
# 是否有空调
air_conditioner = db.Column(db.Boolean, default=False)
# 是否有热水器
water_heater = db.Column(db.Boolean, default=False)
# 是否有洗衣机
washer = db.Column(db.Boolean, default=False)
# 是否有冰箱
fridge = db.Column(db.Boolean, default=False)
# 是否有电视
tv = db.Column(db.Boolean, default=False)
# 是否有床
bed = db.Column(db.Boolean, default=False)
# 是否有衣柜
wardrobe = db.Column(db.Boolean, default=False)
# 建立与House表的关系(一对多关系反向引用)
# backref='facilities'表示House对象可以通过house.facilities访问配套
house = db.relationship('House', backref=db.ref('facilities'))
# ==================== 详情页路由 ====================
@app.route('/detail/<int:house_id>')
def detail(house_id):
"""
详情页视图函数
:param house_id: URL中传入的房源ID(整数类型)
:return: 渲染后的详情页HTML模板
"""
# 根据主键ID查询房源信息
# get_or_404:如果查不到会自动返回404页面
house = House.query.get_or_404(house_id)
# 查询该房源的配套设施信息(一对多,取第一条记录)
facility = Facility.query.filter_by(house_id=house_id).first()
# 将房源信息和配套设施传入模板进行渲染
return render_template('detail.html', house=house, facility=facility)
# ==================== API接口:获取房源详情数据 ====================
@app.route('/api/house/<int:house_id>')
def get_house_detail(house_id):
"""
获取房源详情的JSON接口(供前端AJAX调用)
:param house_id: 房源ID
:return: JSON格式的房源数据
"""
# 查询房源信息
house = House.query.get(house_id)
# 如果房源不存在,返回错误信息和404状态码
if not house:
return jsonify({'code': 404, 'msg': '房源不存在'}), 404
# 构造返回数据字典
data = {
'code': 200, # 状态码
'msg': 'success', # 提示信息
'data': {
'id': house.id,
'title': house.title,
'region': house.region,
'street': house.street,
'community': house.community,
'house_type': house.house_type,
'area': house.area,
'orientation': house.orientation,
'floor': house.floor,
'price': house.price,
'unit_price': house.unit_price,
'publish_time': house.publish_time.strftime('%Y-%m-%d'), # 格式化日期
'image_url': house.image_url
}
}
# 使用jsonify将字典转为JSON响应
return jsonify(data)
# 启动Flask开发服务器
if __name__ == '__main__':
# debug=True开启调试模式,代码修改后自动重启
app.run(debug=True, port=5000)
1.2 详情页模板渲染(Jinja2)
知识点: Jinja2模板语法、模板继承、变量插值、条件判断、过滤器
html
<!-- ==================== templates/base.html(基础模板) ==================== -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<!-- 页面标题,子模板可以覆盖此block -->
<title>{% block title %}智能租房{% endblock %}</title>
<!-- 引入Bootstrap CSS框架 -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css"
rel="stylesheet">
<!-- 自定义样式block,子模板可扩展 -->
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<!-- url_for生成首页链接 -->
<a class="navbar-brand" href="{{ url_for('index') }}">智能租房平台</a>
</div>
</nav>
<!-- 主体内容区域,子模板填充此block -->
<div class="container mt-4">
{% block content %}{% endblock %}
</div>
<!-- 引入Bootstrap JS -->
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js">
</script>
<!-- 子模板可扩展JS -->
{% block extra_js %}{% endblock %}
</body>
</html>
html
<!-- ==================== templates/detail.html(详情页模板) ==================== -->
{% extends 'base.html' %}
<!-- 覆盖title block -->
{% block title %}{{ house.title }} - 智能租房{% endblock %}
{% block content %}
<!-- ==================== 房源基本信息区域 ==================== -->
<div class="row">
<!-- 左侧:房源图片 -->
<div class="col-md-6">
<!-- 使用三元运算符判断图片是否存在,不存在则显示默认图 -->
<img src="{{ house.image_url if house.image_url else '/static/images/default.jpg' }}"
class="img-fluid rounded"
alt="{{ house.title }}">
</div>
<!-- 右侧:房源基本信息 -->
<div class="col-md-6">
<!-- 房源标题 -->
<h2 class="mb-3">{{ house.title }}</h2>
<!-- 价格信息 -->
<!-- 使用Jinja2过滤器format格式化数字 -->
<div class="price-tag mb-3">
<span class="text-danger fs-3 fw-bold">{{ "%.0f"|format(house.price) }}</span>
<span class="text-muted">元/月</span>
</div>
<!-- 基本信息表格 -->
<table class="table table-bordered">
<tbody>
<tr>
<td class="bg-light fw-bold" width="30%">所在区域</td>
<!-- 使用字符串拼接显示区域和街道 -->
<td>{{ house.region }} - {{ house.street }}</td>
</tr>
<tr>
<td class="bg-light fw-bold">小区名称</td>
<td>{{ house.community }}</td>
</tr>
<tr>
<td class="bg-light fw-bold">户型</td>
<td>{{ house.house_type }}</td>
</tr>
<tr>
<td class="bg-light fw-bold">面积</td>
<!-- 使用round过滤器保留1位小数 -->
<td>{{ house.area | round(1) }}㎡</td>
</tr>
<tr>
<td class="bg-light fw-bold">朝向</td>
<td>{{ house.orientation }}</td>
</tr>
<tr>
<td class="bg-light fw-bold">楼层</td>
<td>{{ house.floor }}</td>
</tr>
<tr>
<td class="bg-light fw-bold">单价</td>
<td>{{ "%.2f"|format(house.unit_price) }}元/㎡/月</td>
</tr>
<tr>
<td class="bg-light fw-bold">发布时间</td>
<!-- 使用strftime过滤器格式化日期时间 -->
<td>{{ house.publish_time.strftime('%Y年%m月%d日') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ==================== 房源配套设施展示区域 ==================== -->
<div class="card mt-4 mb-4">
<div class="card-header">
<h4 class="mb-0">配套设施</h4>
</div>
<div class="card-body">
<div class="row">
{# 使用Jinja2的条件判断,检查facility是否存在 #}
{% if facility %}
{# 定义配套设施的显示列表(字段名: 中文名称) #}
{# 使用dict字典构造内联数据 #}
{% set facilities = [
('暖气', facility.heating),
('天然气', facility.gas),
('电梯', facility.elevator),
('宽带', facility.internet),
('空调', facility.air_conditioner),
('热水器', facility.water_heater),
('洗衣机', facility.washer),
('冰箱', facility.fridge),
('电视', facility.tv),
('床', facility.bed),
('衣柜', facility.wardrobe)
] %}
{# 遍历配套设施列表 #}
{% for name, available in facilities %}
<div class="col-md-3 col-sm-4 mb-2">
{# 使用Jinja2的三元表达式判断是否具备该设施 #}
{% if available %}
<span class="badge bg-success">{{ name }} ✓</span>
{% else %}
<span class="badge bg-secondary">{{ name }} ✗</span>
{% endif %}
</div>
{% endfor %}
{% else %}
<p class="text-muted">暂无配套设施信息</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
二、利用ECharts实现数据可视化
2.1 认识ECharts
知识点: ECharts简介、引入方式、基本概念
html
<!-- ==================== ECharts引入方式 ==================== -->
<!-- 方式一:通过CDN引入(推荐,无需下载) -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
<!-- 方式二:通过npm安装(适用于Vue/React项目) -->
<!-- npm install echarts --save -->
<!-- 然后在JS文件中引入:import * as echarts from 'echarts'; -->
<!-- 方式三:下载到本地引入 -->
<!-- 从 https://echarts.apache.org/zh/download.html 下载 -->
<!-- <script src="/static/js/echarts.min.js"></script> -->
2.2 ECharts的基本使用
知识点: 初始化图表、配置项option、setOption方法
html
<!-- ==================== ECharts基本使用完整示例 ==================== -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ECharts基本使用</title>
<!-- 引入ECharts库 -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
</head>
<body>
<!-- 第一步:准备一个具备宽高的DOM容器(ECharts必须渲染在此类容器中) -->
<!-- 注意:必须设置width和height,否则图表无法正常显示 -->
<div id="main" style="width: 800px; height: 500px;"></div>
<script>
// 第二步:基于准备好的DOM,初始化ECharts实例
// echarts.init()接收一个DOM元素作为参数
var myChart = echarts.init(document.getElementById('main'));
// 第三步:指定图表的配置项和数据(option对象)
var option = {
// title:图表标题配置
title: {
text: 'ECharts入门示例', // 主标题文本
subtext: '这是副标题', // 副标题文本
left: 'center', // 标题水平居中
textStyle: {
fontSize: 18, // 主标题字号
fontWeight: 'bold', // 主标题字重
color: '#333' // 主标题颜色
}
},
// tooltip:提示框组件,鼠标悬停时显示
tooltip: {
trigger: 'item' // 触发方式:'item'鼠标悬停在数据项上
},
// legend:图例组件
legend: {
orient: 'vertical', // 图例排列方向:垂直
left: 'left', // 图例靠左
data: ['销量'] // 图例数据(需与series的name匹配)
},
// xAxis:X轴配置
xAxis: {
type: 'category', // 类目轴(离散数据)
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
axisLabel: {
fontSize: 12, // X轴标签字号
color: '#666' // X轴标签颜色
}
},
// yAxis:Y轴配置
yAxis: {
type: 'value', // 数值轴(连续数据)
axisLabel: {
fontSize: 12,
color: '#666'
}
},
// series:系列列表(核心配置,决定图表类型和数据)
series: [{
name: '销量', // 系列名称(与legend对应)
type: 'bar', // 图表类型:bar=柱状图
data: [5, 20, 36, 10, 10, 20], // 系列数据
// itemStyle:数据项样式
itemStyle: {
color: '#5470c6', // 柱状图颜色
borderRadius: [4, 4, 0, 0] // 柱子顶部圆角
},
// label:数据标签
label: {
show: true, // 显示数据标签
position: 'top', // 标签在柱子顶部
fontSize: 12
}
}]
};
// 第四步:使用刚指定的配置项和数据显示图表
myChart.setOption(option);
// 额外:自适应窗口大小变化
// 当浏览器窗口resize时,图表自动重新调整大小
window.addEventListener('resize', function() {
myChart.resize();
});
</script>
</body>
</html>
2.3 ECharts常用配置项详解
知识点: title、tooltip、legend、xAxis、yAxis、series、grid、toolbox
javascript
// ==================== ECharts完整配置项说明 ====================
var option = {
// ==================== 1. 标题组件 ====================
title: {
text: '主标题', // 主标题文本,支持\n换行
subtext: '副标题', // 副标题文本
left: 'center', // 标题水平位置:'left'/'center'/'right'/像素值/百分比
top: 'top', // 标题垂直位置
textAlign: 'center', // 标题文字水平对齐方式
textStyle: {
color: '#333', // 主标题颜色
fontSize: 18, // 主标题字号
fontWeight: 'bold', // 主标题字重
fontFamily: 'Microsoft YaHei' // 主标题字体
},
subtextStyle: {
color: '#aaa', // 副标题颜色
fontSize: 14 // 副标题字号
}
},
// ==================== 2. 图例组件 ====================
legend: {
data: ['系列A', '系列B'], // 图例数据数组,需与series的name对应
left: 'right', // 水平位置
top: 'middle', // 垂直位置
orient: 'vertical', // 排列方向:'horizontal'/'vertical'
itemWidth: 15, // 图例标记的宽度
itemHeight: 10, // 图例标记的高度
textStyle: {
color: '#333',
fontSize: 13
}
},
// ==================== 3. 提示框组件 ====================
tooltip: {
trigger: 'axis', // 触发类型:'item'(数据项)/'axis'(坐标轴)
axisPointer: {
type: 'shadow' // 指示器类型:'line'/'shadow'/'cross'
},
// 自定义提示框内容(函数形式)
formatter: function(params) {
// params是数据点数组
var result = params[0].name + '<br/>'; // 显示类目名称
params.forEach(function(item) {
// 圆点 + 系列名 + 数据值
result += item.marker + item.seriesName + ': ' + item.value + '元<br/>';
});
return result;
}
},
// ==================== 4. 网格组件(控制图表区域大小和位置) ====================
grid: {
left: '3%', // 图表左边距(距容器)
right: '4%', // 图表右边距
bottom: '3%', // 图表底边距
top: '15%', // 图表顶边距
containLabel: true // 包含坐标轴标签(防止标签溢出)
},
// ==================== 5. 工具箱组件 ====================
toolbox: {
feature: {
saveAsImage: {}, // 保存为图片按钮
dataView: {}, // 数据视图按钮
restore: {}, // 还原按钮
dataZoom: {} // 数据区域缩放按钮
}
},
// ==================== 6. X轴配置 ====================
xAxis: {
type: 'category', // 类型:'category'类目轴/'value'数值轴/'time'时间轴
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
name: '日期', // 坐标轴名称
axisLine: { // 坐标轴线样式
lineStyle: {
color: '#999',
width: 1
}
},
axisTick: { // 坐标轴刻度
show: true,
alignWithLabel: true // 刻度与标签对齐
},
axisLabel: { // 坐标轴标签
color: '#666',
fontSize: 12,
rotate: 0 // 标签旋转角度(防止重叠时使用)
},
splitLine: { // 分隔线(X轴一般隐藏)
show: false
}
},
// ==================== 7. Y轴配置 ====================
yAxis: {
type: 'value',
name: '价格(元)',
min: 0, // 最小值
max: 500, // 最大值
interval: 100, // 刻度间隔
axisLabel: {
color: '#666',
fontSize: 12,
// 自定义标签格式(例如加上单位)
formatter: '{value} 元'
},
splitLine: { // 分隔线(Y轴通常显示,便于读数)
show: true,
lineStyle: {
type: 'dashed', // 虚线样式
color: '#eee'
}
}
},
// ==================== 8. 数据系列 ====================
series: [
{
name: '系列A', // 系列名称(与legend对应)
type: 'line', // 图表类型
data: [120, 200, 150, 80, 70, 110, 130],
smooth: true, // 是否平滑曲线
symbol: 'circle', // 标记图形:'circle'/'rect'/'triangle'等
symbolSize: 8, // 标记大小
lineStyle: { // 线条样式
width: 2,
color: '#5470c6'
},
itemStyle: { // 数据项样式
color: '#5470c6'
},
areaStyle: { // 面积填充样式(折线图用)
color: {
type: 'linear', // 线性渐变
x: 0, y: 0, x2: 0, y2: 1, // 从上到下
colorStops: [
{ offset: 0, color: 'rgba(84, 112, 198, 0.5)' }, // 起始色(50%透明)
{ offset: 1, color: 'rgba(84, 112, 198, 0)' } // 结束色(全透明)
]
}
},
// 数据标签
label: {
show: false, // 是否显示标签
position: 'top', // 标签位置
fontSize: 11
}
}
],
// ==================== 9. dataZoom(数据缩放组件) ====================
dataZoom: [
{
type: 'slider', // 滑动条型数据缩放
xAxisIndex: 0, // 控制的X轴索引
start: 0, // 起始位置百分比
end: 100 // 结束位置百分比
}
]
};
三、户型占比可视化(饼图 Pie Chart)
3.1 后端接口设计与实现
知识点: Flask蓝图、分组查询GROUP BY、聚合函数COUNT、JSON接口返回
python
# ==================== blueprints/analysis.py ====================
from flask import Blueprint, jsonify
from sqlalchemy import func
from app import db
from models import House
# 创建蓝图实例(用于模块化管理路由)
# url_prefix='/api/analysis' 表示该蓝图下所有路由都以该前缀开头
analysis_bp = Blueprint('analysis', __name__, url_prefix='/api/analysis')
@analysis_bp.route('/house_type/<street>')
def get_house_type_ratio(street):
"""
获取同街道房源的户型分类数据和数量(用于饼图展示)
思路:
1. 根据街道筛选房源
2. 按户型分组(GROUP BY house_type)
3. 统计每种户型的数量(COUNT)
:param street: 街道名称
:return: JSON数据 {labels: [...], values: [...]}
"""
# 使用SQLAlchemy进行分组聚合查询
# func.count(House.id) 相当于 SQL 中的 COUNT(house.id)
# .label('count') 给聚合结果起别名
results = db.session.query(
House.house_type, # SELECT house_type
func.count(House.id).label('count') # COUNT(id) AS count
).filter(
House.street == street # WHERE street = ?
).group_by(
House.house_type # GROUP BY house_type
).all() # 执行查询,返回所有结果
# 解析查询结果
# results是一个元组列表,如 [('2室1厅', 45), ('3室2厅', 30), ...]
labels = [] # 户型名称列表(用于饼图标签)
values = [] # 数量列表(用于饼图数据)
for house_type, count in results:
labels.append(house_type) # 添加户型名称
values.append(count) # 添加对应数量
# 返回JSON数据
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'labels': labels, # 如:['1室0厅', '2室1厅', '3室2厅']
'values': values # 如:[10, 45, 30]
}
})
python
# ==================== 在app.py中注册蓝图 ====================
from blueprints.analysis import analysis_bp
# 注册分析蓝图
# 注册后,蓝图中的路由会自动加上前缀 /api/analysis
app.register_blueprint(analysis_bp)
3.2 前端饼图实现
知识点: ECharts饼图(pie)、label标签配置、roseType玫瑰图
html
<!-- ==================== templates/analysis.html ==================== -->
{% extends 'base.html' %}
{% block title %}户型占比分析{% endblock %}
{% block content %}
<h3 class="mb-4">同街道户型占比分析</h3>
<!-- 选择街道的下拉框 -->
<div class="mb-3">
<label class="form-label">选择街道:</label>
<select id="streetSelect" class="form-select" style="width: 200px;">
<option value="三里屯">三里屯</option>
<option value="望京">望京</option>
<option value="回龙观">回龙观</option>
</select>
</div>
<!-- ECharts图表容器 -->
<div id="pieChart" style="width: 700px; height: 500px;"></div>
{% endblock %}
{% block extra_js %}
<!-- 引入ECharts -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
<!-- 引入jQuery(用于AJAX请求) -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script>
// 初始化饼图实例
var pieChart = echarts.init(document.getElementById('pieChart'));
// 定义颜色数组(饼图各扇区的颜色)
var colorList = [
'#5470c6', '#91cc75', '#fac858', '#ee6666',
'#73c0de', '#3ba272', '#fc8452', '#9a60b4',
'#ea7ccc', '#48b8d0', '#f5a9a9', '#b8e6c8'
];
// 页面加载时,默认查询第一个街道的数据
loadPieData($('#streetSelect').val());
// 当下拉框选项变化时,重新加载数据
$('#streetSelect').change(function() {
loadPieData($(this).val());
});
/**
* 加载饼图数据的函数
* @param {string} street - 街道名称
*/
function loadPieData(street) {
// 发送AJAX GET请求获取数据
$.ajax({
url: '/api/analysis/house_type/' + street, // 请求地址
type: 'GET', // 请求方法
dataType: 'json', // 期望返回JSON格式
success: function(res) { // 请求成功的回调函数
if (res.code === 200) {
// 将后端返回的labels和values转为饼图需要的格式
// [{value: 45, name: '2室1厅'}, {value: 30, name: '3室2厅'}, ...]
var pieData = [];
for (var i = 0; i < res.data.labels.length; i++) {
pieData.push({
value: res.data.values[i], // 数值(扇区大小)
name: res.data.labels[i] // 名称(显示在标签和图例中)
});
}
// 设置饼图配置项
var option = {
// 标题
title: {
text: street + ' - 户型占比分布', // 动态标题
subtext: '数据来源:租房平台',
left: 'center', // 居中
textStyle: {
fontSize: 18,
color: '#333'
}
},
// 提示框
tooltip: {
trigger: 'item', // 悬停在扇区上触发
// {a}系列名 {b}数据名 {c}数值 {d}百分比
formatter: '{a} <br/>{b}: {c}套 ({d}%)'
},
// 图例
legend: {
orient: 'vertical', // 垂直排列
left: 'left', // 靠左
top: 'middle' // 垂直居中
},
// 系列(饼图)
series: [{
name: '户型占比', // 系列名
type: 'pie', // 图表类型:饼图
radius: ['35%', '65%'], // 内外半径(环形图/空心饼图)
// 如果不写radius或设为'50%',则为实心饼图
// 设置['35%', '65%']形成环形图(甜甜圈效果)
center: ['55%', '55%'], // 饼图中心位置(相对容器)
// 'roseType'设为'area'可变为南丁格尔玫瑰图
// roseType: 'area',
// 是否启用防止标签重叠策略
avoidLabelOverlap: true,
// 标签配置
label: {
show: true, // 显示标签
position: 'outside', // 标签在扇区外侧
// 自定义标签格式:名称 + 百分比
formatter: '{b}\n{d}%',
fontSize: 12,
color: '#555'
},
// 引导线配置(标签与扇区之间的连线)
labelLine: {
show: true, // 显示引导线
length: 15, // 第一段线长度
length2: 20, // 第二段线长度
smooth: true // 平滑引导线
},
// 扇区样式
itemStyle: {
// 使用回调函数为每个扇区分配不同颜色
color: function(params) {
return colorList[params.dataIndex % colorList.length];
},
borderColor: '#fff', // 扇区边框颜色
borderWidth: 2, // 扇区边框宽度
shadowBlur: 10, // 阴影模糊大小
shadowColor: 'rgba(0,0,0,0.1)' // 阴影颜色
},
// 高亮状态样式(鼠标悬停时)
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 20,
shadowOffsetX: 0,
shadowColor: 'rgba(0,0,0,0.3)'
}
},
data: pieData // 饼图数据
}]
};
// 渲染图表
pieChart.setOption(option);
}
},
error: function(err) {
// 请求失败时输出错误信息
console.error('获取户型数据失败:', err);
}
});
}
</script>
{% endblock %}
四、小区房源数量TOP20可视化(柱状图 Bar Chart)
4.1 后端接口设计与实现
知识点: 多表查询、排序ORDER BY、限制查询LIMIT、联合查询
python
# ==================== blueprints/analysis.py(续) ====================
@analysis_bp.route('/community_top20/<street>')
def get_community_top20(street):
"""
获取指定街道下小区房源数量TOP20数据
思路:
1. 按街道筛选
2. 按小区名称分组
3. 统计每个小区的房源数量
4. 按数量降序排列
5. 取前20条
:param street: 街道名称
:return: JSON数据
"""
results = db.session.query(
House.community, # SELECT community
func.count(House.id).label('count') # COUNT(id) AS count
).filter(
House.street == street # WHERE street = ?
).group_by(
House.community # GROUP BY community
).order_by(
func.count(House.id).desc() # ORDER BY COUNT(id) DESC(降序)
).limit(20).all() # LIMIT 20(只取前20条)
# 分离数据
communities = [] # 小区名称列表
counts = [] # 房源数量列表
# 注意:results是从大到小排序的,但柱状图通常从下到上显示
# 所以需要反转顺序,让TOP1在柱状图顶部
for community, count in reversed(results):
communities.append(community)
counts.append(count)
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'communities': communities,
'counts': counts
}
})
4.2 前端柱状图实现
知识点: ECharts横向柱状图、渐变色、grid配置、dataZoom
html
<!-- ==================== 柱状图展示区域 ==================== -->
<div id="barChart" style="width: 800px; height: 600px;"></div>
<script>
// 初始化柱状图实例
var barChart = echarts.init(document.getElementById('barChart'));
/**
* 加载柱状图数据
* @param {string} street - 街道名称
*/
function loadBarData(street) {
$.ajax({
url: '/api/analysis/community_top20/' + street,
type: 'GET',
dataType: 'json',
success: function(res) {
if (res.code === 200) {
var option = {
title: {
text: street + ' - 小区房源数量TOP20',
subtext: '按房源数量从多到少排列',
left: 'center',
textStyle: {
fontSize: 18,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'axis', // 在坐标轴上触发(整个柱子区域)
axisPointer: {
type: 'shadow' // 使用阴影指示器
},
// 自定义提示框格式
formatter: function(params) {
// params[0]获取第一个系列的数据
return params[0].name + '<br/>'
+ '房源数量: <strong>' + params[0].value + '</strong> 套';
}
},
// 网格配置(控制图表在容器中的位置和大小)
grid: {
left: '3%', // 左边距
right: '8%', // 右边距(留出数值标签空间)
bottom: '3%',
top: '12%',
containLabel: true // 包含坐标轴标签
},
// X轴(数值轴,因为是横向柱状图,X轴显示数值)
xAxis: {
type: 'value',
name: '房源数量(套)',
axisLabel: {
fontSize: 12,
color: '#666'
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: '#e8e8e8'
}
}
},
// Y轴(类目轴,显示小区名称)
yAxis: {
type: 'category',
data: res.data.communities, // 小区名称数组
axisLabel: {
fontSize: 11,
color: '#333',
// 如果小区名称过长,截断显示
formatter: function(value) {
if (value.length > 6) {
return value.substring(0, 6) + '...';
}
return value;
}
},
// Y轴线隐藏
axisLine: { show: false },
// Y轴刻度隐藏
axisTick: { show: false }
},
series: [{
name: '房源数量',
type: 'bar', // 柱状图
data: res.data.counts, // 数量数组
barWidth: '60%', // 柱子宽度(百分比)
// 使用渐变色
itemStyle: {
// 创建线性渐变:从左到右,颜色由浅到深
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#83bff6' }, // 起始色(浅蓝)
{ offset: 0.5, color: '#188df0' }, // 中间色(深蓝)
{ offset: 1, color: '#188df0' } // 结束色(深蓝)
]),
borderRadius: [0, 4, 4, 0] // 右侧圆角(横向柱状图)
},
// 高亮状态
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#d4a7f7' },
{ offset: 0.5, color: '#b35cf0' },
{ offset: 1, color: '#b35cf0' }
])
}
},
// 数据标签(在柱子右侧显示数值)
label: {
show: true,
position: 'right', // 标签在柱子右侧
fontSize: 11,
color: '#666',
formatter: '{c} 套' // {c}表示数据值
}
}],
// 数据缩放(当数据多时,可以拖动滑块查看)
dataZoom: [
{
type: 'slider', // 滑动条型
yAxisIndex: 0, // 控制Y轴
right: '0%',
startValue: 0, // 起始索引
endValue: 9, // 结束索引(默认显示10条)
width: 15 // 滑动条宽度
}
]
};
barChart.setOption(option);
}
},
error: function(err) {
console.error('获取TOP20数据失败:', err);
}
});
}
// 初始加载
loadBarData($('#streetSelect').val());
// 窗口大小改变时,重新调整图表大小
window.addEventListener('resize', function() {
barChart.resize();
pieChart.resize();
});
</script>
五、户型价格走势可视化(折线图 Line Chart)
5.1 后端接口设计与实现
知识点: 日期函数处理、时间序列查询、平均值计算AVG
python
# ==================== blueprints/analysis.py(续) ====================
@analysis_bp.route('/price_trend/<street>/<house_type>')
def get_price_trend(street, house_type):
"""
获取指定街道下某户型的平均价格和时间序列数据
思路:
1. 筛选指定街道和户型
2. 按月份分组(使用DATE_FORMAT或strftime提取年月)
3. 计算每月平均价格
4. 按时间排序
:param street: 街道名称
:param house_type: 户型(如'2室1厅')
:return: JSON数据(时间序列 + 平均价格)
"""
# 根据不同数据库选择日期格式化函数
# MySQL: func.date_format(House.publish_time, '%Y-%m')
# SQLite: func.strftime('%Y-%m', House.publish_time)
results = db.session.query(
# 将日期格式化为"年-月"格式(如"2024-01")
func.date_format(House.publish_time, '%Y-%m').label('month'),
# 计算每月平均价格,保留2位小数
func.round(func.avg(House.price), 2).label('avg_price'),
# 统计每月数据条数(用于评估数据可靠性)
func.count(House.id).label('count')
).filter(
House.street == street, # 条件1:指定街道
House.house_type == house_type # 条件2:指定户型
).group_by(
func.date_format(House.publish_time, '%Y-%m') # 按年月分组
).order_by(
'month' # 按月份升序排列
).all()
# 分离时间序列和价格数据
months = [] # 月份列表(X轴数据)
avg_prices = [] # 平均价格列表(Y轴数据)
data_counts = [] # 数据量列表(用于提示框参考)
for month, avg_price, count in results:
months.append(month)
avg_prices.append(float(avg_price)) # 确保转为浮点数
data_counts.append(count)
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'months': months, # 如:['2024-01', '2024-02', ...]
'avg_prices': avg_prices, # 如:[3500, 3650, 3700, ...]
'data_counts': data_counts # 如:[15, 20, 18, ...]
}
})
5.2 前端折线图实现
知识点: ECharts折线图(line)、面积图areaStyle、标记线markLine
html
<!-- ==================== 折线图展示区域 ==================== -->
<div class="mb-3">
<label class="form-label">选择户型:</label>
<select id="houseTypeSelect" class="form-select" style="width: 200px;">
<option value="1室0厅">1室0厅</option>
<option value="1室1厅">1室1厅</option>
<option value="2室1厅" selected>2室1厅</option>
<option value="2室2厅">2室2厅</option>
<option value="3室1厅">3室1厅</option>
<option value="3室2厅">3室2厅</option>
</select>
</div>
<div id="lineChart" style="width: 800px; height: 500px;"></div>
<script>
// 初始化折线图实例
var lineChart = echarts.init(document.getElementById('lineChart'));
/**
* 加载折线图数据
* @param {string} street - 街道名称
* @param {string} houseType - 户型
*/
function loadLineData(street, houseType) {
$.ajax({
url: '/api/analysis/price_trend/' + street + '/' + houseType,
type: 'GET',
dataType: 'json',
success: function(res) {
if (res.code === 200) {
var data = res.data;
// 计算平均值(用于标记线)
var total = 0;
for (var i = 0; i < data.avg_prices.length; i++) {
total += data.avg_prices[i];
}
var overallAvg = (total / data.avg_prices.length).toFixed(2);
var option = {
title: {
text: street + ' - ' + houseType + ' 价格走势',
subtext: '月平均租金变化趋势',
left: 'center'
},
tooltip: {
trigger: 'axis',
// 自定义提示框:显示月份、平均价格、样本量
formatter: function(params) {
var idx = params[0].dataIndex;
return '月份: ' + params[0].name + '<br/>'
+ '平均价格: <strong>' + params[0].value + '</strong> 元/月<br/>'
+ '样本量: ' + data.data_counts[idx] + ' 条';
}
},
legend: {
data: ['月平均租金', '整体均价'],
top: '10%'
},
grid: {
left: '3%',
right: '4%',
bottom: '8%',
top: '20%',
containLabel: true
},
// X轴:时间轴
xAxis: {
type: 'category',
data: data.months,
boundaryGap: false, // 数据点在刻度线上(不留间距)
axisLabel: {
rotate: 45, // 标签旋转45度(防止重叠)
fontSize: 11,
color: '#666'
}
},
// Y轴:数值轴
yAxis: {
type: 'value',
name: '平均租金(元/月)',
axisLabel: {
formatter: '{value}',
fontSize: 11
}
},
series: [
{
name: '月平均租金',
type: 'line', // 折线图
data: data.avg_prices,
smooth: true, // 平滑曲线(贝塞尔插值)
symbol: 'circle', // 数据点形状
symbolSize: 8, // 数据点大小
lineStyle: {
width: 3, // 线条宽度
color: '#5470c6' // 线条颜色
},
itemStyle: {
color: '#5470c6',
borderWidth: 2,
borderColor: '#fff' // 数据点边框白色
},
// 面积填充(形成面积图效果)
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(84, 112, 198, 0.4)' },
{ offset: 1, color: 'rgba(84, 112, 198, 0.05)' }
])
},
// 标记线(在图表中画水平/垂直参考线)
markLine: {
data: [
{
name: '整体均价', // 标记线名称
yAxis: parseFloat(overallAvg), // Y轴位置(整体平均值)
lineStyle: {
color: '#ee6666', // 红色虚线
type: 'dashed',
width: 2
},
label: {
formatter: '均价: ' + overallAvg + '元',
fontSize: 11,
color: '#ee6666'
}
}
]
},
// 标记点(标注最高值和最低值)
markPoint: {
data: [
{
type: 'max', // 最大值
name: '最高价',
label: { formatter: '{c}元' }
},
{
type: 'min', // 最小值
name: '最低价',
label: { formatter: '{c}元' }
}
],
symbolSize: 50
}
}
]
};
lineChart.setOption(option);
}
},
error: function(err) {
console.error('获取价格走势数据失败:', err);
}
});
}
// 初始加载
loadLineData($('#streetSelect').val(), $('#houseTypeSelect').val());
// 监听下拉框变化
$('#streetSelect, #houseTypeSelect').change(function() {
loadLineData($('#streetSelect').val(), $('#houseTypeSelect').val());
});
</script>
六、预测房价走势可视化(散点图 + 线性回归)
6.1 线性回归算法
知识点: 线性回归原理、最小二乘法
python
# ==================== 线性回归算法原理演示 ====================
import numpy as np
# ========== 1. 线性回归数学原理 ==========
# 线性回归模型公式:y = wx + b
# 其中:
# w = 权重(斜率/回归系数)
# b = 偏置(截距)
# x = 自变量(如面积)
# y = 因变量(如价格)
# 最小二乘法求解公式:
# w = Σ((xi - x̄)(yi - ȳ)) / Σ((xi - x̄)²)
# b = ȳ - w * x̄
# ========== 2. 手动实现线性回归 ==========
def linear_regression_manual(x, y):
"""
手动实现简单线性回归(最小二乘法)
:param x: 自变量数组(如房屋面积列表)
:param y: 因变量数组(如房屋价格列表)
:return: (斜率w, 截距b)
"""
n = len(x) # 数据样本数量
# 计算x和y的均值
x_mean = sum(x) / n # x的平均值 x̄
y_mean = sum(y) / n # y的平均值 ȳ
# 计算分子:Σ((xi - x̄)(yi - ȳ))
# 协方差的分子部分
numerator = 0
for i in range(n):
numerator += (x[i] - x_mean) * (y[i] - y_mean)
# 计算分母:Σ((xi - x̄)²)
# x的方差的分子部分
denominator = 0
for i in range(n):
denominator += (x[i] - x_mean) ** 2
# 求解斜率w
w = numerator / denominator
# 求解截距b
b = y_mean - w * x_mean
return w, b
# ========== 3. 使用示例 ==========
# 模拟数据:房屋面积(平方米)
areas = [40, 50, 60, 70, 80, 90, 100, 110, 120, 130]
# 模拟数据:对应月租金(元)
prices = [2500, 3000, 3500, 4200, 4800, 5500, 6200, 6800, 7500, 8200]
# 调用手动实现的线性回归
w, b = linear_regression_manual(areas, prices)
print(f"手动计算结果:斜率w = {w:.4f}, 截距b = {b:.4f}")
print(f"回归方程:price = {w:.2f} * area + {b:.2f}")
# 使用模型预测:面积为95平方米的房屋租金
predicted_price = w * 95 + b
print(f"预测面积95㎡的租金为:{predicted_price:.2f} 元/月")
# ========== 4. 使用NumPy实现(更简洁) ==========
# np.polyfit(x, y, deg) 多项式拟合,deg=1为线性
# 返回 [斜率w, 截距b]
coefficients = np.polyfit(areas, prices, deg=1)
w_np, b_np = coefficients[0], coefficients[1]
print(f"\nNumPy计算结果:斜率w = {w_np:.4f}, 截距b = {b_np:.4f}")
# 预测多个面积值
test_areas = [45, 65, 85, 105, 125]
for area in test_areas:
pred = w_np * area + b_np
print(f" 面积 {area}㎡ → 预测租金 {pred:.0f} 元/月")
6.2 认识scikit-learn库
知识点: scikit-learn安装与导入、LinearRegression模型、fit与predict方法、模型评估
python
# ==================== scikit-learn线性回归完整教程 ====================
# ========== 1. 安装scikit-learn ==========
# pip install scikit-learn
# ========== 2. 导入所需模块 ==========
from sklearn.linear_model import LinearRegression # 线性回归模型
from sklearn.model_selection import train_test_split # 数据集划分
from sklearn.metrics import r2_score, mean_squared_error # 模型评估指标
import numpy as np
# ========== 3. 准备数据 ==========
# 模拟房源数据:面积(平方米)
X = np.array([40, 50, 60, 70, 80, 90, 100, 110, 120, 130,
45, 55, 65, 75, 85, 95, 105, 115, 125, 135])
# 模拟房源数据:月租金(元)
y = np.array([2500, 3000, 3500, 4200, 4800, 5500, 6200, 6800, 7500, 8200,
2700, 3200, 3800, 4500, 5100, 5800, 6500, 7100, 7800, 8500])
# 注意:sklearn要求输入X为二维数组(n_samples, n_features)
# 当前X是一维数组,需要reshape为二维
# reshape(-1, 1)表示自动计算行数,列数为1
X = X.reshape(-1, 1)
print(f"输入数据X的形状: {X.shape}") # (20, 1)
print(f"输出数据y的形状: {y.shape}") # (20,)
# ========== 4. 划分训练集和测试集 ==========
# train_test_split(X, y, test_size, random_state)
# X: 特征数据
# y: 目标数据
# test_size: 测试集占比(0.2表示20%用于测试)
# random_state: 随机种子(确保每次运行结果一致)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2, # 20%的数据作为测试集
random_state=42 # 随机种子,固定为42(任意整数)
)
print(f"训练集大小: {X_train.shape[0]} 条") # 16 条
print(f"测试集大小: {X_test.shape[0]} 条") # 4 条
# ========== 5. 创建并训练模型 ==========
# 创建线性回归模型实例
model = LinearRegression()
# fit()方法:用训练数据拟合(训练)模型
# 内部会自动计算最优的回归系数w和截距b
model.fit(X_train, y_train)
# 查看训练后的模型参数
print(f"\n模型参数:")
print(f" 回归系数(斜率) w = {model.coef_[0]:.4f}") # model.coef_ 是数组
print(f" 截距 b = {model.intercept_:.4f}") # model.intercept_ 是标量
# ========== 6. 使用模型进行预测 ==========
# predict()方法:用测试数据进行预测
y_pred = model.predict(X_test)
# 打印预测结果对比
print(f"\n预测结果对比:")
print(f" {'面积(㎡)':<10} {'实际租金(元)':<15} {'预测租金(元)':<15} {'误差(元)':<10}")
print(f" {'-'*50}")
for i in range(len(X_test)):
error = abs(y_test[i] - y_pred[i]) # 计算绝对误差
print(f" {X_test[i][0]:<10} {y_test[i]:<15.0f} {y_pred[i]:<15.0f} {error:<10.0f}")
# ========== 7. 模型评估 ==========
# R²决定系数(越接近1越好,表示模型解释了多少方差)
r2 = r2_score(y_test, y_pred)
# MSE均方误差(越小越好)
mse = mean_squared_error(y_test, y_pred)
# RMSE均方根误差(MSE开根号,单位与原始数据一致,更直观)
rmse = np.sqrt(mse)
print(f"\n模型评估指标:")
print(f" R²决定系数: {r2:.4f}") # 接近1说明模型拟合好
print(f" 均方误差MSE: {mse:.2f}")
print(f" 均方根误差RMSE: {rmse:.2f}")
# ========== 8. 预测新数据 ==========
# 预测面积为95平方米的房屋月租金
new_area = np.array([[95]]) # 注意:必须是二维数组
predicted_rent = model.predict(new_area)
print(f"\n预测面积95㎡的月租金为:{predicted_rent[0]:.2f} 元")
# 批量预测
new_areas = np.array([[60], [80], [100], [120], [140]])
predicted_rents = model.predict(new_areas)
print(f"\n批量预测结果:")
for area, rent in zip(new_areas, predicted_rents):
print(f" 面积 {area[0]}㎡ → 预测租金 {rent:.0f} 元/月")
6.3 后端预测接口实现
知识点: Flask接口中集成scikit-learn模型、数据预处理、模型持久化
python
# ==================== blueprints/prediction.py ====================
from flask import Blueprint, jsonify, request
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
import numpy as np
from models import House
from app import db
# 创建预测蓝图
prediction_bp = Blueprint('prediction', __name__, url_prefix='/api/prediction')
@prediction_bp.route('/price/<street>/<house_type>')
def predict_price(strend, house_type):
"""
预测指定街道、指定户型的未来房价走势
思路:
1. 从数据库获取该街道、该户型的所有历史数据
2. 提取面积(X)和价格(y)作为训练数据
3. 使用线性回归模型进行训练
4. 生成预测区间数据
5. 返回历史数据 + 预测数据
:param street: 街道名称
:param house_type: 户型
:return: JSON数据(历史数据 + 预测数据)
"""
# ========== 1. 查询历史数据 ==========
houses = House.query.filter_by(
street=street,
house_type=house_type
).all()
# 如果数据量太少(少于5条),无法建立有效模型
if len(houses) < 5:
return jsonify({
'code': 400,
'msg': '数据量不足,无法进行预测分析'
})
# ========== 2. 提取特征和标签 ==========
# 特征(X):房屋面积
# 标签(y):月租金
areas = [] # 面积列表
prices = [] # 价格列表
for house in houses:
# 确保面积和价格都不为空
if house.area and house.price:
areas.append(house.area)
prices.append(house.price)
# 转换为NumPy数组,并reshape为二维(sklearn要求)
X = np.array(areas).reshape(-1, 1) # 形状:(n, 1)
y = np.array(prices) # 形状:(n,)
# ========== 3. 创建并训练线性回归模型 ==========
model = LinearRegression()
model.fit(X, y) # 训练模型
# 获取模型参数
w = model.coef_[0] # 斜率(每增加1平方米,价格增加多少元)
b = model.intercept_ # 截距
# ========== 4. 生成预测数据 ==========
# 确定预测范围:从最小面积-10到最大面积+10
min_area = min(areas) - 10
max_area = max(areas) + 10
# 生成均匀分布的面积点(用于绘制预测线)
# np.linspace(start, stop, num) 生成num个等间距的数
predict_areas = np.linspace(min_area, max_area, 50)
# 使用模型预测价格
predict_prices = model.predict(predict_areas.reshape(-1, 1))
# ========== 5. 计算置信区间(上下界) ==========
# 计算残差标准差(用于估计预测误差范围)
y_train_pred = model.predict(X) # 训练数据的预测值
residuals = y - y_train_pred # 残差 = 实际值 - 预测值
std_residual = np.std(residuals) # 残差的标准差
# 置信区间 = 预测值 ± 2倍标准差(约95%置信度)
upper_bound = predict_prices + 2 * std_residual # 上界
lower_bound = predict_prices - 2 * std_residual # 下界
lower_bound = np.maximum(lower_bound, 0) # 确保下界不为负数
# ========== 6. 构造返回数据 ==========
# 历史散点数据
history_data = []
for i in range(len(areas)):
history_data.append({
'area': areas[i],
'price': prices[i]
})
# 预测线数据
trend_data = []
for i in range(len(predict_areas)):
trend_data.append({
'area': round(float(predict_areas[i]), 1),
'price': round(float(predict_prices[i]), 2),
'upper': round(float(upper_bound[i]), 2),
'lower': round(float(lower_bound[i]), 2)
})
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'history': history_data, # 历史散点数据
'trend': trend_data, # 预测趋势线数据
'model_info': {
'slope': round(float(w), 4), # 斜率
'intercept': round(float(b), 2), # 截距
'equation': f'price = {w:.2f} * area + {b:.2f}', # 回归方程
'r2_score': round(float(model.score(X, y)), 4), # R²决定系数
'data_count': len(areas) # 样本数量
}
}
})
python
# ==================== 在app.py中注册蓝图 ====================
from blueprints.prediction import prediction_bp
app.register_blueprint(prediction_bp)
6.4 前端散点图实现
知识点: ECharts散点图(scatter)、多系列叠加、视觉映射visualMap
html
<!-- ==================== 散点图+预测线展示区域 ==================== -->
<div id="scatterChart" style="width: 800px; height: 550px;"></div>
<script>
// 初始化散点图实例
var scatterChart = echarts.init(document.getElementById('scatterChart'));
/**
* 加载预测数据并绘制散点图+预测线
* @param {string} street - 街道
* @param {string} houseType - 户型
*/
function loadScatterData(street, houseType) {
$.ajax({
url: '/api/prediction/price/' + street + '/' + houseType,
type: 'GET',
dataType: 'json',
success: function(res) {
if (res.code === 200) {
var data = res.data;
// ========== 准备历史散点数据 ==========
// 格式:[[面积1, 价格1], [面积2, 价格2], ...]
var historyScatter = [];
for (var i = 0; i < data.history.length; i++) {
historyScatter.push([
data.history[i].area,
data.history[i].price
]);
}
// ========== 准备预测趋势线数据 ==========
// 格式:[[面积1, 预测价1], [面积2, 预测价2], ...]
var trendLine = [];
for (var i = 0; i < data.trend.length; i++) {
trendLine.push([
data.trend[i].area,
data.trend[i].price
]);
}
// ========== 准备置信区间数据(面积范围数据对) ==========
var upperLine = []; // 上界线
var lowerLine = []; // 下界线
for (var i = 0; i < data.trend.length; i++) {
upperLine.push([
data.trend[i].area,
data.trend[i].upper
]);
lowerLine.push([
data.trend[i].area,
data.trend[i].lower
]);
}
// ========== 构造ECharts配置项 ==========
var option = {
title: {
text: street + ' - ' + houseType + ' 房价预测分析',
subtext: '基于线性回归模型 | R²=' + data.model_info.r2_score,
left: 'center',
textStyle: {
fontSize: 18,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'item',
// 自定义提示框
formatter: function(params) {
if (params.seriesName === '历史数据') {
// 散点的提示信息
return '面积: ' + params.value[0] + '㎡<br/>'
+ '租金: ' + params.value[1] + '元/月';
} else if (params.seriesName === '预测趋势') {
return '面积: ' + params.value[0] + '㎡<br/>'
+ '预测租金: ' + params.value[1].toFixed(0) + '元/月';
}
return params.seriesName + ': ' + params.value[1].toFixed(0);
}
},
legend: {
data: ['历史数据', '预测趋势', '置信上界', '置信下界'],
top: '12%',
left: 'center'
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
top: '22%',
containLabel: true
},
// X轴
xAxis: {
type: 'value',
name: '面积(㎡)',
nameLocation: 'middle',
nameGap: 30,
axisLabel: {
formatter: '{value}',
fontSize: 11
},
splitLine: {
show: true,
lineStyle: { type: 'dashed', color: '#eee' }
}
},
// Y轴
yAxis: {
type: 'value',
name: '月租金(元)',
nameLocation: 'middle',
nameGap: 50,
axisLabel: {
formatter: '{value}',
fontSize: 11
},
splitLine: {
show: true,
lineStyle: { type: 'dashed', color: '#eee' }
}
},
// 工具箱
toolbox: {
feature: {
saveAsImage: {}, // 保存图片
restore: {}, // 还原
dataZoom: {} // 缩放
},
right: '2%'
},
// 数据缩放
dataZoom: [
{
type: 'inside', // 内部型(鼠标滚轮缩放)
xAxisIndex: 0
}
],
series: [
// ========== 系列1:历史数据散点 ==========
{
name: '历史数据',
type: 'scatter', // 散点图类型
data: historyScatter,
symbolSize: 12, // 散点大小
itemStyle: {
color: '#5470c6', // 散点颜色
opacity: 0.7, // 透明度
borderColor: '#fff',
borderWidth: 1.5
},
// 散点的强调样式(鼠标悬停时)
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0,0,0,0.3)',
opacity: 1
}
}
},
// ========== 系列2:预测趋势线 ==========
{
name: '预测趋势',
type: 'line', // 折线图类型
data: trendLine,
smooth: false, // 直线(线性回归结果是直线)
symbol: 'none', // 不显示数据点
lineStyle: {
color: '#ee6666', // 红色
width: 3,
type: 'solid'
}
},
// ========== 系列3:置信区间上界 ==========
{
name: '置信上界',
type: 'line',
data: upperLine,
symbol: 'none', // 不显示数据点
lineStyle: {
color: '#fac858', // 黄色虚线
width: 1.5,
type: 'dashed'
}
},
// ========== 系列4:置信区间下界 ==========
{
name: '置信下界',
type: 'line',
data: lowerLine,
symbol: 'none',
lineStyle: {
color: '#fac858',
width: 1.5,
type: 'dashed'
}
},
// ========== 系列5:置信区间填充(上下界之间的阴影) ==========
{
name: '置信区间',
type: 'line',
data: upperLine, // 以上界数据为基准
symbol: 'none',
lineStyle: { opacity: 0 },
areaStyle: {
// 使用渐变填充
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(250, 200, 88, 0.2)' },
{ offset: 1, color: 'rgba(250, 200, 88, 0.05)' }
])
},
// 通过stack实现上下界之间的面积填充
// 此处使用简单方式,直接以上界线做面积图即可
// 更精确的置信区间填充可以使用自定义系列
z: 0 // 图层顺序(放在最底层)
}
],
// 标注线(在图表上标注关键信息)
graphic: [
{
type: 'text', // 文本标注
left: '10%',
bottom: '15%',
style: {
text: '回归方程: ' + data.model_info.equation,
fontSize: 13,
fontWeight: 'bold',
fill: '#333'
}
}
]
};
scatterChart.setOption(option);
// 更新模型信息显示区域
$('#modelInfo').html(
'<div class="alert alert-info">'
+ '<strong>回归方程:</strong>' + data.model_info.equation + '<br/>'
+ '<strong>R²决定系数:</strong>' + data.model_info.r2_score + '<br/>'
+ '<strong>样本数量:</strong>' + data.model_info.data_count + '条<br/>'
+ '<strong>解释:</strong>面积每增加1㎡,月租金平均增加'
+ data.model_info.slope.toFixed(2) + '元'
+ '</div>'
);
}
},
error: function(err) {
console.error('获取预测数据失败:', err);
}
});
}
// 初始加载
loadScatterData($('#streetSelect').val(), $('#houseTypeSelect').val());
// 监听变化
$('#streetSelect, #houseTypeSelect').change(function() {
loadScatterData($('#streetSelect').val(), $('#houseTypeSelect').val());
});
</script>
七、综合补充知识点
7.1 Flask蓝图(Blueprint)完整使用
知识点: 蓝图创建、注册、模块化项目结构
python
# ==================== 项目目录结构 ====================
# rental_project/
# ├── app.py # 应用入口
# ├── config.py # 配置文件
# ├── models.py # 数据模型
# ├── blueprints/ # 蓝图目录
# │ ├── __init__.py
# │ ├── analysis.py # 数据分析蓝图
# │ ├── prediction.py # 预测蓝图
# │ └── house.py # 房源蓝图
# ├── templates/ # 模板目录
# │ ├── base.html
# │ ├── detail.html
# │ └── analysis.html
# └── static/ # 静态文件目录
# ├── css/
# ├── js/
# └── images/
# ==================== config.py ====================
class Config:
"""Flask应用配置类"""
# 数据库连接URI
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@localhost:3306/rental'
# 关闭修改追踪
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 密钥(用于session、CSRF保护等)
SECRET_KEY = 'my-secret-key-123'
# JSON中文不转义
JSON_AS_ASCII = False
# ==================== app.py ====================
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from config import Config
# 创建数据库实例(不在这里绑定app,在init_app中绑定)
db = SQLAlchemy()
def create_app():
"""
应用工厂函数(用于创建Flask应用实例)
优点:
1. 可以创建多个不同配置的应用实例
2. 延迟绑定,避免循环导入
3. 便于测试
"""
# 创建Flask应用实例
app = Flask(__name__)
# 加载配置
app.config.from_object(Config)
# 初始化数据库扩展
db.init_app(app)
# 注册蓝图
from blueprints.house import house_bp
from blueprints.analysis import analysis_bp
from blueprints.prediction import prediction_bp
app.register_blueprint(house_bp) # 注册房源蓝图
app.register_blueprint(analysis_bp) # 注册分析蓝图
app.register_blueprint(prediction_bp) # 注册预测蓝图
return app
# 启动入口
if __name__ == '__main__':
app = create_app()
app.run(debug=True, port=5000)
python
# ==================== blueprints/house.py ====================
from flask import Blueprint, render_template, jsonify
from app import db
from models import House, Facility
# 创建房源蓝图
# template_folder指定该蓝图专用的模板目录(可选)
house_bp = Blueprint(
'house',
__name__,
url_prefix='/house' # 路由前缀
)
@house_bp.route('/detail/<int:house_id>')
def detail(house_id):
"""详情页视图"""
house = House.query.get_or_404(house_id)
facility = Facility.query.filter_by(house_id=house_id).first()
return render_template('detail.html', house=house, facility=facility)
@house_bp.route('/api/<int:house_id>')
def get_house_api(house_id):
"""获取房源详情API"""
house = House.query.get(house_id)
if not house:
return jsonify({'code': 404, 'msg': '未找到'}), 404
return jsonify({
'code': 200,
'data': {
'id': house.id,
'title': house.title,
'price': house.price,
'area': house.area,
'house_type': house.house_type,
'community': house.community,
'region': house.region,
'street': house.street
}
})
# 自定义错误处理(仅在该蓝图范围内生效)
@house_bp.errorhandler(404)
def house_not_found(error):
"""房源不存在时的处理"""
return jsonify({
'code': 404,
'msg': '房源信息不存在'
}), 404
7.2 AJAX跨域与错误处理
知识点: 前端AJAX封装、错误处理、loading状态
javascript
// ==================== 通用AJAX请求封装 ====================
/**
* 封装通用的AJAX请求函数
* @param {string} url - 请求地址
* @param {string} method - 请求方法(GET/POST)
* @param {object} data - 请求数据(POST时使用)
* @param {function} successCallback - 成功回调函数
* @param {function} errorCallback - 失败回调函数(可选)
*/
function request(url, method, data, successCallback, errorCallback) {
// 显示loading遮罩层
$('#loading').show();
$.ajax({
url: url, // 请求URL
type: method, // 请求方法
data: data ? JSON.stringify(data) : null, // 序列化数据
contentType: 'application/json', // 发送数据的编码类型
dataType: 'json', // 期望接收的数据类型
timeout: 10000, // 超时时间:10秒
// 请求成功回调
success: function(res) {
$('#loading').hide(); // 隐藏loading
if (res.code === 200) {
// 调用成功回调,传入数据
if (typeof successCallback === 'function') {
successCallback(res.data);
}
} else {
// 业务逻辑错误(code不为200)
showError(res.msg || '请求失败');
}
},
// 请求失败回调
error: function(xhr, status, error) {
$('#loading').hide(); // 隐藏loading
// 根据HTTP状态码显示不同错误信息
var errorMsg = '';
switch (xhr.status) {
case 404:
errorMsg = '请求的资源不存在';
break;
case 500:
errorMsg = '服务器内部错误';
break;
case 0:
errorMsg = '网络连接失败,请检查网络';
break;
default:
errorMsg = '请求失败:' + error;
}
showError(errorMsg);
// 如果提供了错误回调,也调用它
if (typeof errorCallback === 'function') {
errorCallback(xhr, status, error);
}
},
// 请求完成(无论成功失败都会执行)
complete: function() {
$('#loading').hide();
}
});
}
/**
* 显示错误提示信息
* @param {string} msg - 错误消息文本
*/
function showError(msg) {
$('#errorToast .toast-body').text(msg); // 设置消息内容
var toast = new bootstrap.Toast($('#errorToast'));
toast.show(); // 显示提示框
}
// ==================== 使用示例 ====================
// 调用封装函数获取户型占比数据
request(
'/api/analysis/house_type/望京', // URL
'GET', // 方法
null, // 无请求体
function(data) { // 成功回调
console.log('获取数据成功:', data);
renderPieChart(data.labels, data.values);
},
function(xhr, status, error) { // 失败回调
console.error('请求失败:', error);
}
);
7.3 ECharts动态更新与图表联动
知识点: setOption合并更新、clear()清空、图表事件监听、多图表联动
javascript
// ==================== ECharts动态更新 ====================
// 初始化图表
var chart = echarts.init(document.getElementById('chart'));
// ========== 1. 初始化一个空option ==========
var option = {
title: { text: '动态数据展示' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: [] },
yAxis: { type: 'value' },
series: [{ type: 'line', data: [] }]
};
chart.setOption(option);
// ========== 2. 动态更新数据(合并模式) ==========
// setOption传入新数据时,默认会与已有option合并(notMerge默认为false)
// 只需传入要更新的部分即可
function updateChart(newXData, newSeriesData) {
chart.setOption({
xAxis: {
data: newXData // 更新X轴数据
},
series: [{
data: newSeriesData // 更新系列数据
}]
});
}
// 调用示例
updateChart(
['1月', '2月', '3月', '4月', '5月'],
[3000, 3200, 3100, 3500, 3800]
);
// ========== 3. 完全替换option(notMerge模式) ==========
// 当需要完全重置图表配置时,设置notMerge为true
var newOption = {
title: { text: '全新图表' },
series: [{ type: 'bar', data: [10, 20, 30] }]
};
chart.setOption(newOption, true); // 第二个参数true = notMerge(完全替换)
// ========== 4. 清空图表 ==========
chart.clear(); // 清除所有数据和组件
// ========== 5. 图表事件监听 ==========
// 点击事件
chart.on('click', function(params) {
// params包含被点击数据项的详细信息
console.log('点击了:', params.name); // 数据名称
console.log('数据值:', params.value); // 数据值
console.log('系列名:', params.seriesName); // 系列名称
console.log('数据索引:', params.dataIndex); // 数据索引
// 示例:点击饼图扇区后,跳转到详情列表
if (params.componentType === 'series') {
window.location.href = '/house/list?type=' + params.name;
}
});
// 鼠标移入事件
chart.on('mouseover', function(params) {
console.log('鼠标移入:', params.name);
});
// ========== 6. 多图表联动(connect方法) ==========
var pieChart = echarts.init(document.getElementById('pieChart'));
var barChart = echarts.init(document.getElementById('barChart'));
var lineChart = echarts.init(document.getElementById('lineChart'));
// 将多个图表进行关联
// 关联后,一个图表的tooltip会同步显示在所有图表中
echarts.connect([pieChart, barChart, lineChart]);
// 或者使用dispatchAction手动触发联动
// 当饼图点击时,更新柱状图数据
pieChart.on('click', function(params) {
var selectedType = params.name;
// 发起AJAX请求获取该户型的详细数据
loadBarDataForType(selectedType);
// 高亮柱状图对应的数据
barChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
name: selectedType
});
});
7.4 scikit-learn模型持久化
知识点: pickle/joblib保存模型、加载模型、避免重复训练
python
# ==================== 模型持久化(保存与加载) ====================
import pickle
import joblib
import os
from sklearn.linear_model import LinearRegression
import numpy as np
# ========== 方法一:使用pickle保存和加载 ==========
def save_model_pickle(model, filepath):
"""
使用pickle将训练好的模型保存到文件
:param model: 已训练的sklearn模型对象
:param filepath: 保存路径(.pkl文件)
"""
# wb = write binary(二进制写入模式)
with open(filepath, 'wb') as f:
pickle.dump(model, f) # 将模型序列化写入文件
print(f"模型已保存至: {filepath}")
def load_model_pickle(filepath):
"""
使用pickle从文件加载模型
:param filepath: 模型文件路径
:return: 加载的模型对象
"""
# rb = read binary(二进制读取模式)
with open(filepath, 'rb') as f:
model = pickle.load(f) # 从文件反序列化加载模型
print(f"模型已加载: {filepath}")
return model
# ========== 方法二:使用joblib保存和加载(推荐用于大模型) ==========
def save_model_joblib(model, filepath):
"""
使用joblib保存模型(对包含大numpy数组的模型更高效)
:param model: 已训练的sklearn模型对象
:param filepath: 保存路径(.joblib文件)
"""
joblib.dump(model, filepath) # 保存模型
print(f"模型已保存至: {filepath}")
def load_model_joblib(filepath):
"""
使用joblib加载模型
:param filepath: 模型文件路径
:return: 加载的模型对象
"""
model = joblib.load(filepath) # 加载模型
print(f"模型已加载: {filepath}")
return model
# ========== 实际使用示例 ==========
# 模拟训练数据
X_train = np.array([[40], [60], [80], [100], [120], [140]])
y_train = np.array([2500, 3500, 4800, 6200, 7500, 8800])
# 训练模型
model = LinearRegression()
model.fit(X_train, y_train)
# 保存模型
model_dir = 'models/saved/'
os.makedirs(model_dir, exist_ok=True) # 创建目录(如不存在)
save_model_pickle(model, model_dir + 'price_model.pkl')
save_model_joblib(model, model_dir + 'price_model.joblib')
# ========== 在Flask接口中使用已保存的模型 ==========
def predict_with_saved_model(street, house_type, area):
"""
使用预训练模型进行预测(避免每次请求都重新训练)
:param street: 街道
:param house_type: 户型
:param area: 要预测的面积
:return: 预测价格
"""
# 构造模型文件路径
model_path = f'models/saved/{street}_{house_type}_model.pkl'
# 检查模型文件是否存在
if os.path.exists(model_path):
# 加载已保存的模型(快速,无需重新训练)
model = load_model_pickle(model_path)
else:
# 模型不存在,需要重新训练
houses = House.query.filter_by(
street=street, house_type=house_type
).all()
X = np.array([[h.area] for h in houses if h.area and h.price])
y = np.array([h.price for h in houses if h.area and h.price])
model = LinearRegression()
model.fit(X, y)
# 训练完成后保存模型,下次直接使用
save_model_pickle(model, model_path)
# 使用模型进行预测
predicted_price = model.predict([[area]])
return float(predicted_price[0])
7.5 Jinja2模板过滤器自定义
知识点: 自定义过滤器、注册到Flask应用
python
# ==================== 自定义Jinja2过滤器 ====================
# 在app.py中定义并注册自定义过滤器
@app.template_filter('price_format')
def price_format(value):
"""
自定义价格格式化过滤器
将数字格式化为带千分位的价格字符串
例如:6500 → "6,500"
"""
if value is None:
return '暂无'
return f'{value:,.0f}' # 使用Python格式化字符串
@app.template_filter('area_format')
def area_format(value):
"""
自定义面积格式化过滤器
将数字格式化为带单位的面积字符串
例如:85.5 → "85.5㎡"
"""
if value is None:
return '暂无'
return f'{value:.1f}㎡'
@app.template_filter('time_ago')
def time_ago(value):
"""
自定义时间格式化过滤器(相对时间)
将datetime转为"X天前"、"X小时前"等格式
"""
if value is None:
return '暂无'
from datetime import datetime
now = datetime.now() # 获取当前时间
diff = now - value # 计算时间差
days = diff.days # 天数差
seconds = diff.seconds # 秒数差(不含天数部分)
if days > 30:
return value.strftime('%Y-%m-%d') # 超过30天显示具体日期
elif days > 0:
return f'{days}天前'
elif seconds >= 3600:
hours = seconds // 3600
return f'{hours}小时前'
elif seconds >= 60:
minutes = seconds // 60
return f'{minutes}分钟前'
else:
return '刚刚'
html
<!-- ==================== 在模板中使用自定义过滤器 ==================== -->
<!-- 使用price_format过滤器 -->
<p>月租金:{{ house.price | price_format }} 元/月</p>
<!-- 输出示例:月租金:6,500 元/月 -->
<!-- 使用area_format过滤器 -->
<p>面积:{{ house.area | area_format }}</p>
<!-- 输出示例:面积:85.5㎡ -->
<!-- 使用time_ago过滤器 -->
<p>发布时间:{{ house.publish_time | time_ago }}</p>
<!-- 输出示例:发布时间:3天前 -->
<!-- 多个过滤器链式调用 -->
<p>{{ house.title | upper | truncate(20) }}</p>
<!-- upper: 转大写 truncate(20): 截断到20个字符 -->
7.6 ECharts多图表组合仪表盘
知识点: 一个页面中同时渲染多个ECharts图表
html
<!-- ==================== 分析仪表盘页面 ==================== -->
{% extends 'base.html' %}
{% block title %}数据分析仪表盘{% endblock %}
{% block content %}
<h2 class="mb-4">智能租房数据分析仪表盘</h2>
<!-- 使用Bootstrap网格布局,一行两列 -->
<div class="row">
<!-- 左上:户型占比饼图 -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">户型占比分布</div>
<div class="card-body">
<div id="pieChart" style="width:100%; height:400px;"></div>
</div>
</div>
</div>
<!-- 右上:小区TOP20柱状图 -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">小区房源数量TOP20</div>
<div class="card-body">
<div id="barChart" style="width:100%; height:400px;"></div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- 左下:价格走势折线图 -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">户型价格走势</div>
<div class="card-body">
<div id="lineChart" style="width:100%; height:400px;"></div>
</div>
</div>
</div>
<!-- 右下:房价预测散点图 -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">房价预测分析</div>
<div class="card-body">
<div id="scatterChart" style="width:100%; height:400px;"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script>
// ========== 初始化所有图表实例 ==========
var pieChart = echarts.init(document.getElementById('pieChart'));
var barChart = echarts.init(document.getElementById('barChart'));
var lineChart = echarts.init(document.getElementById('lineChart'));
var scatterChart = echarts.init(document.getElementById('scatterChart'));
// ========== 图表联动:将所有图表关联 ==========
echarts.connect([pieChart, barChart, lineChart, scatterChart]);
// ========== 窗口自适应 ==========
window.addEventListener('resize', function() {
// 一次性调整所有图表大小
pieChart.resize();
barChart.resize();
lineChart.resize();
scatterChart.resize();
});
// ========== 加载各图表数据(此处省略具体实现,参考前文) ==========
loadPieData('望京');
loadBarData('望京');
loadLineData('望京', '2室1厅');
loadScatterData('望京', '2室1厅');
</script>
{% endblock %}
知识点总结表
| 序号 | 知识点分类 | 核心技术/模块 | 关键API/方法 |
|---|---|---|---|
| 1 | Flask路由与视图 | Flask | @app.route(), render_template(), jsonify() |
| 2 | 数据库查询 | SQLAlchemy | query.filter_by(), group_by(), func.count(), func.avg() |
| 3 | 模板渲染 | Jinja2 | {% extends %}, {% block %}, {``{ variable }}, {% if %}, {% for %} |
| 4 | 蓝图模块化 | Flask Blueprint | Blueprint(), app.register_blueprint() |
| 5 | 饼图可视化 | ECharts pie | type: 'pie', radius, label, roseType |
| 6 | 柱状图可视化 | ECharts bar | type: 'bar', LinearGradient, dataZoom, 横向柱状图 |
| 7 | 折线图可视化 | ECharts line | type: 'line', smooth, areaStyle, markLine, markPoint |
| 8 | 散点图可视化 | ECharts scatter | type: 'scatter', symbolSize, 多系列叠加 |
| 9 | 线性回归 | scikit-learn | LinearRegression(), .fit(), .predict(), .score() |
| 10 | 模型评估 | sklearn.metrics | r2_score(), mean_squared_error() |
| 11 | 模型持久化 | pickle/joblib | pickle.dump(), joblib.dump(), joblib.load() |
| 12 | 前端异步请求 | jQuery AJAX | $.ajax(), success, error |
| 13 | 自定义过滤器 | Jinja2 | @app.template_filter() |
| 14 | 应用工厂模式 | Flask | create_app() 工厂函数 |