Django民宿房源数据分析可视化系统:从数据清洗到ECharts大屏的完整实践

发布时间:2026/11/2 10:28:07
Django民宿房源数据分析可视化系统:从数据清洗到ECharts大屏的完整实践 简介这是一份基于 Django 的民宿房源数据分析可视化系统完整源码面向需要完成 Python 毕业设计、课程设计或期末大作业的高校学生。系统聚焦民宿房源数据提供数据清洗、统计分析、图表展示与后台管理等完整功能界面清晰、操作流畅兼具教学演示与实际应用价值。压缩包共807个文件以321个JS、147个CSS、80个HTML、40个Python文件为主涵盖前端页面、Django后端逻辑、可视化样式、字体图标以及少量说明文档与配置文件整体大小11.88MB目录结构清晰便于定位模块和二次开发。项目已经过严格调试可直接运行体验也适合参考其数据可视化方案、功能模块划分和 Django 工程组织方式。目前已有774人学习下载对正在筹备毕设或项目实战的初学者是值得参考的完整示例。1. 民宿房源数据分析可视化系统这个 Django 毕设到底在做什么拿到这个标题对应的源码包时我第一次打开只看了三个文件就明白了models.py、views.py 和一个引了 ECharts 的模板页。说白了它就是一个用 Django 做后端、pandas 做数据分析、ECharts 做前端图表的民宿房源数据分析可视化系统把几十万条订单和房源数据变成区域热力图、价格趋势线和入住率柱状图。真正动手复现后你会发现这套系统最值钱的不是图表画得多好看而是背后那几个业务口径——平均房价怎么算、入住率分母是谁、区域热度用什么加权——这些定清楚了页面上的每个数字才站得住。适合谁做两类人。一类是拿它当毕业设计、想完整走一遍「爬数据→清洗→入库→接口→可视化大屏」链路的在校生另一类是已经能跑通 Django 增删改查、想看看分析类项目怎么组织数据表和聚合查询的从业者。下面前四章按我做这个方向时的习惯拆开讲最后收在我想留给你的一招上。2. 先设计再写代码数据表、清洗口径与分析指标这个项目最容易翻车的顺序是先画页面、后补数据。页面画得再漂亮数据口径对不上答辩老师一句「你这个均价是怎么算出来的」就能把人问住。所以第一步不是建 Django 项目而是把数据模型、清洗逻辑和分析口径钉死。2.1 三张核心表的关系和字段设计民宿房源分析至少要覆盖静态属性、动态流水和状态变化三类数据。我在源码包里看到的分表思路也是这样房源表House存区域、房型、面积、经纬度、挂牌价这些不变或极少变的信息订单表Order存每一笔成交的时间、间夜数、金额还有一张订单明细或房态表用来还原「某一天这个房源有没有被订出去」——算入住率必须靠它光有订单总额算不出来。表关系不复杂House 一对多 OrderOrder 关联到具体房源。真正要注意的是冗余字段。分析接口最终要按区域分组、按月份切片如果每次查询都去 join 城市表和区域表几十万行数据下性能立刻崩。所以导入时就把region_name、city_name直接冗余到订单表里查询时少两次 join代价只是存储空间多一点点。2.2 从 CSV 到 DataFrame清洗时最容易漏的四类脏数据爬虫抓下来或老师给的数据几乎不会有干净的。我处理这个项目时遇到最多的四类问题价格字段带着「元/晚」之类的单位、日期格式不统一有的/有的-、经纬度有极少数空值、房型字段同一个意思多种写法「大床房」「大床间」其实是一回事。这些都必须在下一次loaddata之前清完。我一般单独写一个clean_data.py不混进 Django 的 ORM 操作里import pandas as pd df pd.read_csv(house.csv, encodingutf-8-sig) # 价格统一转数字去掉单位、处理空值 df[price] ( df[price] .astype(str) .str.replace(元/晚, ) .str.replace( , ) ) df[price] pd.to_numeric(df[price], errorscoerce) # 日期统一成 datetime解析不了的置空再删 df[date] pd.to_datetime(df[date], format%Y-%m-%d, errorscoerce) # 经纬度空值直接删行几百条不影响整体 df df.dropna(subset[price, date, lng, lat]) # 房型归一化全部走映射表 room_type_map {大床房: 大床, 大床间: 大床, 标间: 双床, 标准间: 双床} df[room_type] df[room_type].map(room_type_map).fillna(其他) # 按城市区域补齐冗余字段方便后面聚合 df[region_name] df.groupby(city_name)[region_name].transform(first) df.to_csv(house_clean.csv, indexFalse, encodingutf-8-sig)关键参数就两个errorscoerce让解析失败的值变成NaN而不是抛异常打断整批处理format%Y-%m-%d告诉 pandas 日期长什么样避免它拿%Y/%m/%d的格式硬解另一种写法。房型归一化用map而不是replace是为了让没匹配上的值统一落到「其他」不会漏出脏分类。2.3 三个核心指标的口径均价、入住率、区域热度口径是这个项目的灵魂。平均房价就有两种算法挂牌均价是把所有房源挂牌价做算术平均成交均价是总营收除以总间夜数。两个都能算但结论差很多——挂牌价偏高的房源多算术平均会被拉高成交均价才反映真实市场。我做的时候两个都算页面默认展示成交均价挂牌均价放在对比图里答辩时反而成了加分项。入住率的口径更讲究分子是「某时间段内已订间夜数」分母是「可订间夜数」。分母必须排除已经下架或停业的房源否则房源下架前的空置期会被算成没卖出去入住率虚低。区域热度我用的是订单量 60%、成交金额 30%、平均评分 10% 的加权值权重放在配置里而不是写死在代码中方便调。聚合脚本大概是这样的import pandas as pd orders pd.read_csv(orders_clean.csv, encodingutf-8-sig) # 成交均价总营收 / 总间夜 orders[amount_nights] orders[amount] / orders[nights] # 区域识别因为有冗余字段直接 groupby 即可 region_stats ( orders.groupby(region_name) .agg( order_cnt(order_id, count), total_amount(amount, sum), total_nights(nights, sum), avg_rating(rating, mean), ) .reset_index() ) region_stats[avg_price] region_stats[total_amount] / region_stats[total_nights] region_stats[heat] ( region_stats[order_cnt] * 0.6 region_stats[total_amount] * 0.3 region_stats[avg_rating] * 0.1 )这里有个隐藏的坑amount是整单金额除以nights得到每晚均价再除以总间夜数会得到错误结果。正确做法是总金额直接除以总间夜数不要在中间先算每晚均价再求平均——那是算术平均不是加权平均。这个细节稍微不注意页面上的数字就会和老师手动核算的对不上。我在region_stats里把order_cnt、total_amount、total_nights全部聚出来是为了后续能随时切换口径。如果你只算一个数后面换个算法就得重跑整个清洗流程。切记分析类系统把中间结果多保留几个字段永远是对的。3. Django 后端把聚合结果变成前端能直接读的 JSON数据清洗完接下来就是用 Django 把结果暴露成接口。这套系统的接口不复杂基本就是三四个 JSON 接口分别服务区域热力图、价格趋势、入住率柱状图和房源列表。但写法上有个分水岭是在 Python 里循环算还是让数据库把聚合做完再返回。3.1 models.py 怎么建、外键怎么挂models.py 是 Django 项目的骨架。我见过很多新手把 JSONField 当万能药一个字段塞整个字典结果后面每个查询都要在代码里拆。正确做法是老老实实拆字段外键挂清楚。from django.db import models class House(models.Model): house_id models.CharField(max_length32, uniqueTrue, db_indexTrue) city_name models.CharField(max_length32) region_name models.CharField(max_length32) room_type models.CharField(max_length16) price models.DecimalField(max_digits10, decimal_places2) lng models.FloatField() lat models.FloatField() on_sale models.BooleanField(defaultTrue) class Meta: db_table house indexes [models.Index(fields[region_name, on_sale])] class Order(models.Model): order_id models.CharField(max_length64, uniqueTrue) house models.ForeignKey(House, on_deletemodels.CASCADE, related_nameorders) order_date models.DateField(db_indexTrue) checkin_date models.DateField() nights models.PositiveIntegerField() amount models.DecimalField(max_digits10, decimal_places2) rating models.FloatField(nullTrue, blankTrue) # 冗余字段直接冗余区域名避免聚合时跨表 join city_name models.CharField(max_length32) region_name models.CharField(max_length32) class Meta: db_table order indexes [models.Index(fields[region_name, order_date])]两个要记住的参数on_deletemodels.CASCADE表示删房源时级联删订单这是电商类系统最常见的关联策略Django 默认强制你写清楚否则不让迁移db_indexTrue加在order_date和region_name上后面按月份和区域过滤时全靠这两个索引不加的话几十万行数据查询会慢到页面超时。related_nameorders是为了让house.orders.all()能直接取到该房源全部订单写 ORM 时省事不少。3.2 视图层用聚合查询避免 Python 循环计算视图层最容易犯的错是 N1 查询循环每个区域里面再查一次数据库。正确姿势是在 Queryset 层面把聚合做完用annotate一次性把Count、Sum、Avg全部算出来。from django.db.models import Count, Sum, Avg, F, FloatField, ExpressionWrapper def region_stats(request): # annotate 在 SQL 层完成聚合而不是 Python 循环 stats ( Order.objects .filter(order_date__year2024) .values(region_name) .annotate( order_cntCount(order_id), total_amountSum(amount), nights_sumSum(nights), avg_ratingAvg(rating), ) .order_by(-order_cnt) ) result [] for item in stats: avg_price round(item[total_amount] / item[nights_sum], 2) heat round( item[order_cnt] * 0.6 item[total_amount] * 0.3 item[avg_rating] * 0.1, 2, ) result.append({ name: item[region_name], order_cnt: item[order_cnt], total_amount: float(item[total_amount]), avg_price: avg_price, heat: heat, }) return JsonResponse({data: result})这里有个参数值得单独讲filter(order_date__year2024)是 Django 的字段名双下划线语法翻译成 SQL 是WHERE order_date BETWEEN 2024-01-01 AND 2024-12-31。你可以在values(region_name)前面加多个.filter()拼接条件但要注意每个 filter 都是 AND 关系想要「2024 年或 2025 年」得用Q对象包起来不能连写两个 filter。3.3 接口返回结构Date 和 Decimal 怎么安全转 JSONJsonResponse在 Django 内部走的是json.dumps而Decimal和date都不是 JSON 原生类型直接返回会报Object of type Decimal is not JSON serializable。很多新手在这里卡住然后去百度最后用str()强行转数字精度丢了不说前端拿到的还是字符串。import json from django.core.serializers.json import DjangoJSONEncoder from django.http import JsonResponse def price_trend(request): # 按月份算成交均价输出 ECharts 需要的 [[月份, 均价], ...] rows ( Order.objects .filter(order_date__year2024) .annotate(monthF(order_date__month)) .values(month) .annotate(total_amountSum(amount), total_nightsSum(nights)) .order_by(month) ) trend [ [item[month], round(item[total_amount] / item[total_nights], 2)] for item in rows ] # DjangoJSONEncoder 自动处理 Decimal 和 date/datetime return JsonResponse({data: trend}, encoderDjangoJSONEncoder)encoderDjangoJSONEncoder是 Django 3.x 之后推荐的标准做法它能序列化Decimal、date、datetime、UUID和timedelta省掉你手写转换函数。前端拿到的trend是标准的[[1, 568.5], [2, 622.0], ...]数组ECharts 的折线图可以直接吃这个格式不用二次 map。前端和后端约定好接口返回结构很重要我习惯统一返回{data: ...}包一层错误时返回{error: ...}。这样前端 fetch 的时候只需要判断data在不在不用为每个页面单独设计错误结构。4. 常见问题避坑从静态文件 404 到数据差了 8 小时这个项目我前后跑过三轮每一轮都踩了不同的坑。这一章把最耽误时间的五个问题集中写出来每条都是「现象 → 原因 → 解决」的路子照着排查能省一下午。4.1 Django 开发服务器下图表文件 404现象页面能开但 ECharts 的 JS 文件加载不出来控制台一片红色 404。原因Django 开发服务器默认不服务静态文件模板里用{% load static %}引的路径需要STATICFILES_DIRS告诉 Django 去哪找。很多项目模板是直接从静态页面改的路径写的是相对路径js/echarts.min.js而不是模板标签{% static js/echarts.min.js %}自然 404。解决settings.py 里配上目录然后用模板标签替换所有静态资源引用。# settings.py STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]!-- 模板中所有 JS/CSS 引用统一这样写 -- script src{% static js/echarts.min.js %}/script还有一个低频但更隐蔽的情况STATICFILES_DIRS配了但路径指向的文件夹不存在Django 不会报错只会默默 404。所以配置完先确认static/js目录真实存在再刷新页面。4.2 日期区间查询差一天订单少统计一批现象按月统计营收2024 年 12 月的数字比实际少了 8 小时的数据1 月的数据里又混进了 12 月 31 日深夜的单。原因Django 的USE_TZ True开启后所有DateTimeField都按 UTC 存取本地东八区和 UTC 差 8 小时。如果数据库存的是datetime你在代码里写order_date__year2024Django 会翻译成 UTC 时区的时间边界正好把本地 8 点前的订单截掉。解决这个项目用的全是DateField而不是DateTimeField订单只有日期没有时刻直接避免时区问题。如果某些场景必须用DateTimeField查询前先timezone.localtime()把边界算明白from django.utils import timezone import datetime start timezone.make_aware(datetime.datetime(2024, 12, 1, 0, 0, 0)) end timezone.make_aware(datetime.datetime(2025, 1, 1, 0, 0, 0)) orders Order.objects.filter(order_date__gtestart, order_date__ltend)4.3 中文在图表里显示成方块现象ECharts 的标题和图例中文全部变成「□□□」地图上的区域名也不能看。原因ECharts 默认字体走的是sans-serifLinux 服务器上如果没装中文字体或者浏览器字体链里没有可用的中文渲染就会出方块。解决初始化图表时显式指定字体优先级同时确认服务器装了中文字体包。var chart echarts.init(document.getElementById(chart)); chart.setOption({ textStyle: { fontFamily: Microsoft YaHei, PingFang SC, Noto Sans CJK SC, sans-serif } });服务器端用fc-list | grep -i cjk看一眼有没有中文字体没有的话apt install fonts-noto-cjk装上否则即使浏览器指定了字体导出图片时照样是方块。4.4 几十万条数据聚合卡到页面超时现象区域热力图接口响应 8 到 10 秒前端一直转圈。原因视图层在 Python 里循环了每个区域每个循环里又发一次COUNT(*)查询。区域有 30 个就触发 31 条 SQL加上每条查询都扫描全表不慢才怪。解决把循环里的查询并成一个group by也就是前面 3.2 节那种values().annotate()写法。如果数据库本身也慢检查索引有没有建到位——region_name和order_date的联合索引是关键没有的话先migrate后补一条原生 SQL 建索引CREATE INDEX CONCURRENTLY idx_order_region_date ON order (region_name, order_date);4.5 数据库迁移后字段对不上表里多出几百列现象python manage.py migrate执行成功但查询时报column order.xxx does not exist。原因CSV 清洗的时候字段名里带了空格比如region_name pandas 转 DataFrame 后列名没清理直接生成CREATE TABLE时把空格也带进去了。后续代码里写的字段名和数据库实际列名对不上。解决清洗脚本里统一做一次列名规范化再进 ORMdf.columns [c.strip().lower().replace( , _) for c in df.columns]这个步骤一定要放在to_csv之前不然惨案已经写进文件里了。如果不幸已经迁移了就 DROP 表重来——反正是开发环境数据重新导一次比手动 ALTER 几十列快得多。5. 一个值得留的进阶习惯把聚合逻辑抽出来复用最后留一个我自己的编码习惯这招让后面改需求省了大力气不要把聚合逻辑塞在视图函数里。视图函数的职责只是「接收请求、取参数、调服务、返回 JSON」。聚合指标、口径计算、权重配置这些全部抽到一个独立的services.py里。# services.py from django.db.models import Count, Sum, Avg def get_region_stats(year2024, weight(0.6, 0.3, 0.1)): 区域统计订单量、成交均价、热度加权值 order_weight_cnt, amount_weight, rating_weight weight stats ( Order.objects .filter(order_date__yearyear) .values(region_name) .annotate( order_cntCount(order_id), total_amountSum(amount), nights_sumSum(nights), avg_ratingAvg(rating), ) .order_by(-order_cnt) ) result [] for item in stats: result.append({ name: item[region_name], avg_price: round(item[total_amount] / item[nights_sum], 2), heat: round( item[order_cnt] * order_weight_cnt item[total_amount] * amount_weight item[avg_rating] * rating_weight, 2, ), }) return result视图里就干净了# views.py from django.http import JsonResponse from .services import get_region_stats def region_stats(request): year request.GET.get(year, 2024) return JsonResponse({data: get_region_stats(int(year))})这样做的好处是同一个聚合函数可以被接口、导出 Excel、生成 PDF 报告共用改口径只动services.py一处。我第一次做的时候把聚合写死在视图里后来老师要求加一个「只看 2024 下半年」的筛选我改了四个接口其中一个还改漏了数据对不上查了一整晚。之后所有分析类项目我都沿用这个习惯再没翻过车。希望这个拆分习惯和前面几章的踩坑记录能帮到你能让你的民宿房源数据分析可视化系统少走几段弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询