Flask入门学习教程,从入门到精通,Flask智能租房——详情页完整知识点详解(8)

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() 工厂函数
相关推荐
吃好睡好便好14 小时前
矩阵的求幂运算
人工智能·学习·线性代数·算法·matlab·矩阵
Irissgwe14 小时前
十、LangGraph能力详解(1)LangGraph介绍及核心概念
python·ai·langchain·ai编程·工作流·langgraph
l1t14 小时前
DeepSeek总结的使用实体-组件-系统和基于存在性处理进行Python编程9-11
开发语言·python
keineahnung234514 小时前
在 Google Colab 中安裝 PyTorch 2.2.0
人工智能·pytorch·python·深度学习
逆光行14 小时前
奖池派对自动化测试方案与实践报告
python·功能测试·postman
AC赳赳老秦14 小时前
OpenClaw多Agent分工协作:按工作模块拆分Agent,实现全流程自动化闭环
java·大数据·数据库·python·自动化·php·openclaw
weixin_4280053014 小时前
C#调用 AI学习从0开始-第2阶段(Function Calling+工具调用智能体)-第8天Function Calling原理
人工智能·学习·c#·functioncalling
十年伴树14 小时前
python --version返回空行
开发语言·python
meilindehuzi_a14 小时前
AI 时代的高效编程:从 Python 切片基础到魔塔社区大模型 Prompt 实战
python·prompt