简介基于Django框架构建的城市PM2.5空气质量数据可视化分析项目源码面向需要完成期末大作业或课程设计的高校学生也适合想入门Python Web开发与数据分析的开发者提供了一套可直接运行、便于二次扩展的完整示例。压缩包共64个文件、约12.38MB其中包含24个CSV格式的PM2.5数据文件、17个Python源码文件以及HTML模板、XML配置、SQL数据库脚本等数据文件涵盖北京、上海、广州、成都、沈阳五城市六年PM2.5记录并拆分为月变化、时段、季度、温度、湿度、风向、气压等多维分析表。目前已有328人学习下载作为个人98分大作业项目代码完整、注释清晰下载后即可在Django环境中启动运行。项目结构清晰含数据导入通用脚本、Django后台逻辑、登录注册页面与可视化展示界面附带MySQL数据库和requirements.txt依赖清单可在本地快速部署对同类数据分析项目能提供整体架构与局部代码的参考。1. 一套能拿 95 分的 Django 城市 PM2.5 可视化大作业源码到底“完整”在哪城市 PM2.5 空气质量数据可视化分析是 Django 课程设计里最高频也最容易翻车的题目数据明明在 CSV 里图却只存在于截图里Django 只负责登录注册评审一问“数据流怎么串起来的”就答不上来。这套基于 Django 城市 PM2.5 空气质量数据可视化分析源码把北上广成沈五城市六年数据、MySQL 数据库、九张维度 CSV 和 ECharts 图表完整串成一条链路从app01的 models/views 到前端index.html每个环节都有对应文件。它适合期末大作业想冲 90 分以上、或者想照着源码复现一版完整可视化课程设计的人新手能顺着步骤跑通熟手能直接改造成自己的课题。2. 项目拆解Django 三层结构、九张关系 CSV 与一张 PM2.5 数据表2.1 目录先分清untitled、app01、data、get_data 各自管什么拿到压缩包先别急着runserver第一件事是把目录认全。项目名虽然叫untitled但里面结构一点不随意典型的 Django 单应用结构加两个业务目录untitled/ ├── manage.py ├── untitled/ # 项目配置目录 │ ├── settings.py │ ├── urls.py │ ├── wsgi.py / asgi.py ├── app01/ # 业务应用 │ ├── models.py / views.py / admin.py │ ├── migrations/ │ └── templates/ │ ├── login.html / reg.html / index.html ├── data/ # 处理好的九张维度 CSV ├── get_data/ # 原始数据与导入脚本 │ ├── 数据导入.py │ ├── 通用脚本.py │ └── 北京.csv / 上海.csv / 广州.csv / 成都.csv / 沈阳.csv ├── db129.sql # MySQL 数据库备份 ├── requirements.txt └── README.mduntitled/是 Django 项目壳管配置app01是唯一业务应用登录注册、数据查询、页面渲染都在这data/里是别人已经清洗好的“结果型” CSV适合直接喂给 EChartsget_data/里是五城市原始 CSV 和导入脚本适合自己重跑一遍数据链路。这个分层最大的价值在于如果作业要求里写了“必须包含数据预处理”你可以理直气壮地指get_data/如果要求写“可视化展示”你指data/加index.html如果要求写“前后端交互”你指app01的视图函数。评分点全覆盖这也是它能拿高分的原因。2.2 八张“关系型” CSV 对应八类图表以及它们的组织逻辑data/目录里的 CSV 命名非常直白基本就是图表标题我把它们和推荐图表类型对照了一下CSV 文件推荐图表说明一年中各个月份变化.csv柱状图 / 折线图12 个月的 PM2.5 均值一天中不同时段.csv折线图24 小时浓度变化曲线不同季度逐年数据.csv分组柱状图按季度聚合的逐年对比时间序列逐年数据.csv面积图 dataZoom六年连续曲线pm2.5与温度之间的关系.csv散点图温度与浓度的相关性pm2.5与相对湿度之间的关系.csv散点图湿度与浓度相关性pm2.5与露点之间的关系.csv散点图露点与浓度相关性pm2.5与大气压之间的关系.csv散点图气压与浓度相关性pm2.5与风向之间的关系.csv雷达图 / 极坐标风向频率分布用这套命名组织项目的好处是代码里不用写一堆注释文件名本身就是文档。你写图表标题时直接取 CSV 文件名后半段评审一眼就知道你做了几个维度。九张图对应九个分析角度覆盖“时间规律 气象因子关系”两类必考方向这也是典型的 95 分作业配置。注意一个容易被忽略的点get_data/里还有一份“北上广成沈五城市六年PM2.5数据汇总 1.csv”文件名里带了空格和数字后缀这是某些脚本自动导出时留下的痕迹。写批量处理脚本时空格会导致shell命令解析出错我的建议是重命名成five_cities_pm25.csv这种干净命名后续读文件省掉一堆转义麻烦。2.3 django models.py 与 db129.sql表结构对齐是复现的第一道门槛db129.sql是 MySQL 备份文件而app01/models.py里定义了 ORM 模型。复现项目时最常见的问题就是两者表名不一致导致migrate建出来的表和你从 SQL 导入的表不是同一张。它们的对应关系一般是这样from django.db import models class CityPM25(models.Model): city models.CharField(max_length20, verbose_name城市) record_time models.DateTimeField(verbose_name观测时间) pm25 models.FloatField(verbose_namePM2.5浓度) temperature models.FloatField(verbose_name温度) humidity models.FloatField(verbose_name相对湿度) pressure models.FloatField(verbose_name大气压) wind_direction models.CharField(max_length20, nullTrue, blankTrue, verbose_name风向) class Meta: db_table pm25_data verbose_name PM2.5 记录关键在Meta类的db_table字段。Django 默认会用app01_citypm25这种“应用名_类名小写”的命名规则建表而db129.sql里导出的表名大概率是pm25_data这种短名。如果两边不匹配runserver能起来但一到查询就报Table pm25_data doesnt exist。我一般拿到手是先看models.py的db_table再打开db129.sql搜CREATE TABLE关键词确认两边表名一致再开始下一步。这里就一个原则以db129.sql里的实际表名为准然后让models.py里的db_table和它对齐千万别反过来改 SQL 文件那会牵连视图层的一堆查询逻辑。3. 从空环境到跑起来Python 虚拟环境、MySQL 建库和 Django 迁移3.1 创建虚拟环境并安装依赖pandas/numpy 和 MySQL 客户端这个项目的运行链路是 Django 加 pandas 加 MySQL所以 Python 环境别直接用系统全局的否则以后装别的包版本冲突哭都来不及。我在全新机器上复现的第一步永远是建虚拟环境cd untitled python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txtrequirements.txt里一般会锁住 Django、pymysql、pandas、numpy、sqlalchemy 这几个核心依赖。如果你发现版本没锁全或者本地已经装了别的版本的 Django建议单独指定一次pip install django4.2 pymysql pandas numpy sqlalchemy这里的版本选择逻辑是Django 4.2 是长期支持版对 MySQL 和模板引擎支持稳定pandas 和 numpy 负责 CSV 预处理sqlalchemy 用于DataFrame.to_sql直连数据库。如果你只是为了让网页跑起来Django 一个包就够但你要重跑get_data/里的导入脚本pandas 和 numpy 是跑不掉的。3.2 settings.py 的核心配置数据库、时区、静态文件打开untitled/settings.py最需要改的是DATABASES这一段。原始代码里的 MySQL 连接信息多半是作者本机的不改你连不上DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: pm25, # 你要建的数据库名 USER: root, PASSWORD: 改成你自己的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }然后是时区配置。这个项目涉及“一天中不同时段”“一年中各个月份”这类时间聚合时区错了数据会差 8 个小时TIME_ZONE Asia/Shanghai USE_TZ False这里USE_TZ False是我特别强调的一点。Django 3 之后默认开启时区支持如果你开着USE_TZ True写入 MySQL 的datetime会转成 UTC而 CSV 里的时间字符串是东八区本地时间查出来一看每一行都偏移 8 小时图表全乱。课程设计项目没有多时区协作需求直接关掉最省心。另外检查INSTALLED_APPS里有没有app01静态文件设置里有没有指向模板目录。如果 ECharts 的 JS 是放在本地static目录还要确认STATIC_URL和STATICFILES_DIRS配置存在否则前端拿不到 JS 文件页面渲染不出来。更省事的做法是直接用 CDN 加载 ECharts但课程设计经常要求“本地可离线演示”所以本地静态文件路径必须能通。3.3 建库导库三步走runserver 前必须完成的迁移确认 settings 改完后顺序非常重要先建数据库再导入 SQL 备份最后跑 Django 迁移。反过来容易出问题——比如migrate会自动建一张django_migrations表你再导入db129.sql如果 SQL 文件里也带这张表的记录就直接冲突了。mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS pm25 DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p pm25 db129.sql python manage.py makemigrations app01 python manage.py migrate这里解释一下为什么makemigrations app01要指定应用名get_data/里那些脚本不是 Django app不需要迁移只对app01做迁移可以避免生成一堆没用的迁移文件。migrate完成后你可以顺手验证一下表是否存在mysql -uroot -p -e USE pm25; SHOW TABLES;看到pm25_data或对应的模型表名才算真正落地。如果db129.sql里的表结构本身和models.py不一致比如少了wind_direction字段后续视图查询就会爆Unknown column错误这时候要回到 2.3 节的方法以 SQL 文件为准改模型而不是硬跑迁移。完成这三步后执行python manage.py runserver 0.0.0.0:8000访问http://127.0.0.1:8000/login/应该能看到登录页。如果db129.sql里已经带了测试账号数据直接登录如果没带就用项目自己的reg.html注册一个新账号。这套登录注册不是摆设它是 Django session 机制的实际演练下一章讲可视化时会用到这个身份状态。4. 数据导入与可视化渲染把 CSV 变成 ECharts 上会动的图4.1 用“数据导入.py”把五城市数据落进 MySQLget_data/里已经放了处理好的五城市 CSV 和导入脚本但我不建议直接一键执行。先看一眼脚本确认它的目标表名是不是和models.py的db_table一致。常见做法是用 pandas 读 CSV再通过 sqlalchemy 写入 MySQLimport pandas as pd from sqlalchemy import create_engine # 连接 MySQLcharset 必须和数据库保持一致 engine create_engine( mysqlpymysql://root:你的密码127.0.0.1:3306/pm25?charsetutf8mb4 ) df pd.read_csv(get_data/北京.csv, encodingutf-8-sig) df.columns [col.strip() for col in df.columns] # 清洗列名首尾空格 df[city] 北京 # 写入城市标识便于五城合并查询 # 追加写入避免重复跑脚本时插两遍数据 df.to_sql(pm25_data, engine, if_existsappend, indexFalse)这段代码有三个参数值得注意。第一个是encodingutf-8-sigExcel 或 Windows 下导出的 CSV 经常带 BOM 头直接按utf-8读会把\ufeff带进第一列列名后期查询匹配全部失败utf-8-sig会帮你剥掉 BOM。第二个是to_sql的if_exists参数append是追加replace是删表重建。我一般用append但前提是每次手动确认数据没重复如果你希望脚本可重复执行且幂等改用replace更保险。第三个是columns清洗五份 CSV 来源不统一列名里可能带空格或全角字符在执行to_sql前先print(df.columns.tolist())看一眼别跳步骤。如果你在运行脚本时报Unknown column xxx in field list多半是 CSV 表头里有脏字符脚本里的字段名映射没对上。不要硬改 CSV改映射字典就行保留原始数据永远是第一原则。4.2 视图层聚合查询按月、按时段、按季度组织 JSON数据进 MySQL 后app01/views.py的任务就是把表里的原始记录按时间维度聚合输出成 ECharts 能直接消费的 JSON。这里最忌讳的做法是在 Python 里for循环一遍objects.all()做手工聚合数据量大时页面会卡到评审怀疑人生。正确姿势是用 ORM 的annotate加数据库聚合import json from django.http import JsonResponse from django.db.models import Avg from django.db.models.functions import ExtractMonth, ExtractHour from .models import CityPM25 def month_data(request): # 按月份提取计算 PM2.5 平均值 rows (CityPM25.objects .annotate(monthExtractMonth(record_time)) .values(month) .annotate(avg_pm25Avg(pm25)) .order_by(month)) data [ {month: int(r[month]), value: round(r[avg_pm25], 2)} for r in rows ] return JsonResponse(data, safeFalse) def hour_data(request): # 按时段提取对应“一天中不同时段.csv” rows (CityPM25.objects .annotate(hourExtractHour(record_time)) .values(hour) .annotate(avg_pm25Avg(pm25)) .order_by(hour)) data [ {hour: int(r[hour]), value: round(r[avg_pm25], 2)} for r in rows ] return JsonResponse(data, safeFalse)ExtractMonth和ExtractHour是 Django 内置的数据库函数它们会把record_time里的月份、小时字段直接转成 SQL 的EXTRACT查询比在 Python 里切片字符串快一个数量级。values(month)配合Avg(pm25)是分组聚合的固定写法生成的就是SELECT MONTH(record_time), AVG(pm25) ... GROUP BY MONTH(record_time)。safeFalse是因为返回的是列表而不是字典对象不加这个参数 Django 会拦你。这里我通常会在视图里顺带做一次“查不到数据”的兜底if not rows: return JsonResponse({error: no data}, status404)原因是五城市数据导入可能存在某一时段缺数据前端拿到空列表时 ECharts 会画一张空白图而这很难排查。返回明确的错误状态码前端能弹提示定位问题快得多。4.3 index.html 与 ECharts折线图、散点图、雷达图的前端渲染前端templates/index.html是整套可视化的脸面。核心思路是页面加载时用fetch请求上一步的 JSON 接口拿到的数据直接喂给 ECharts 的setOption。不需要用 Django 模板标签硬渲染图表数据那样刷新页面反而容易踩转义坑。以“一年中各个月份变化”的折线图为例div idmonthChart styleheight: 360px;/div script src{% static js/echarts.min.js %}/script script fetch(/month-data/) .then(res res.json()) .then(rows { const chart echarts.init(document.getElementById(monthChart)); chart.setOption({ tooltip: { trigger: axis }, grid: { left: 60, right: 30, top: 40, bottom: 40 }, xAxis: { type: category, data: rows.map(r r.month 月) }, yAxis: { type: value, name: PM2.5 }, series: [{ name: PM2.5, type: line, smooth: true, data: rows.map(r r.value) }] }); }); /script这里有一个很多新手会翻车的地方ECharts 的data数组里如果混进了null或NaN折线图会在那个点断掉看起来像数据缺失但实际是 JSON 序列化时把 Python 的None变成了null。所以在 4.2 节的视图代码里我建议对数值字段做一层校验将None或非法值统一转成0或者过滤掉别让它进入图表。散点图比如 pm2.5 与温度关系的配置也类似只是把series.type换成scatter并且data要变成二维数组series: [{ type: scatter, data: rows.map(r [r.temperature, r.pm25]) }]雷达图用于“风向”需要先把数据按方向盘算成 8 个或 16 个方向的角度值再映射到indicator数组。这块如果嫌麻烦可以先用柱状图代替评分不会因为图表类型少而扣分反而会因为你把散点图的趋势线regression插件做出来而加分。5. 复现避坑手册CSV 乱码、MySQL 认证、cookie 与图表空白5.1 读 CSV 中文变乱码图表标题出现“锟斤拷”现象跑数据导入.py或直接pd.read_csv(data/一年中各个月份变化.csv)打印出来的列名全是乱码页面图表标题也出现“锟斤拷”“锘挎椂闂淬”这类字符。原因文件编码是 GBK 或 GB18030但 pandas 默认按utf-8解析还有一类情况是 Windows 的 Excel 导出的 CSV 带 UTF-8 BOM 头utf-8模式会把 BOM 当成列名的一部分。解决先用file命令或文本编辑器看一眼编码。读取时优先尝试encodingutf-8-sig不行再换encodinggbk还不行就encodinggb18030。我自己的习惯是写一个容错读取函数把三种编码都试一遍def read_csv_auto(path): for enc in (utf-8-sig, gbk, gb18030): try: return pd.read_csv(path, encodingenc) except UnicodeDecodeError: continue raise UnicodeDecodeError(f无法识别编码: {path})这个函数会帮你省掉大量“打开文件改编码”的重复劳动。记住一个原则永远不要用 Excel 双击打开后再“另存为 UTF-8”那样会把原始数据的数字精度和日期格式改掉直接在 pandas 层解决编码识别才可控。5.2 MySQL 8 认证报错与“Unknown database”现象执行python manage.py migrate或跑数据导入脚本时报错django.db.utils.OperationalError: (2059, NULL)或者Authentication plugin caching_sha2_password cannot be loaded。另一种是Unknown database pm25。原因MySQL 8.0 默认认证插件是caching_sha2_password而 PyMySQL 老版本只认识mysql_native_passwordUnknown database则是因为你还没创建数据库或者 settings 里的NAME和实际库名不一致。解决Unknown database最简单执行三条命令mysql -uroot -p CREATE DATABASE IF NOT EXISTS pm25 DEFAULT CHARACTER SET utf8mb4;认证问题则二选一升级 PyMySQL 到较新版本pip install -U pymysql或者把 MySQL 用户的认证方式改回去ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;如果你用的是 MySQL 8.0 以上我建议直接升级 PyMySQL 而不是改认证插件因为改了mysql_native_password会影响服务器整体安全性作业演示完你还要改回来麻烦。5.3 登录跳转失效别在 cookie 里手写 token现象注册成功、登录成功跳转到index.html后立刻弹回登录页或者刷新页面后登录状态丢失。原因Django 默认通过SessionMiddleware把会话 ID 写在 cookie 里。如果你同时用了127.0.0.1和localhost两个地址访问浏览器会认为这是两个不同的站点session cookie 不共享另一种更隐蔽的情况是登录视图里自己拼了一个 token 塞进 cookie根本没调用 Django 的login()函数。解决访问地址固定成唯一入口我习惯全程用http://127.0.0.1:8000不用localhost。登录视图里确保走了 Django 的标准流程from django.contrib.auth import authenticate, login user authenticate(usernameusername, passwordpassword) if user: login(request, user)如果你的需求里明确要在 cookie 里设置 token 保持登录态正确做法是把 token 放进 session 而不是裸写在 cookie 上request.session[token] token_stringDjango 会帮你签名、设置过期时间、处理 HttpOnly。不要自己拼一段裸 token 用set_cookie写那样既没有签名校验又容易被 XSS 拿走属于典型的“能用但带你进坑”的写法。5.4 接口有数据但 ECharts 空白NaN 与横坐标密度问题现象浏览器 Network 面板里能看到/month-data/返回了完整 JSON列表里也有数值但 ECharts 图区一片空白控制台也不报错。原因JSON 里的数值包含NaN。Python 的float(nan)转成 JSON 后是NaN而不是nullfetch能把这个字符串解析成 JavaScript 的NaN但 ECharts 遇到NaN会直接放弃绘制该数据点如果NaN占多数整条折线就消失了。解决视图层返回前统一清洗数值字段from math import isfinite def clean_value(v): if v is None: return None return v if isfinite(v) else None然后data构造时包一层clean_value。另一个常被忽略的问题是横坐标太密集时间序列逐年数据.csv 如果按天甚至按小时展示6 年就是 2000 多个点X 轴标签会全部叠在一起看起来像一条黑带。正确解法是两层视图层按月份聚合降低数据量前端加dataZoom组件让用户滑动查看局部dataZoom: [ { type: inside, start: 0, end: 100 }, { type: slider, height: 20, bottom: 10 } ]加上之后时间轴可以拖动缩放既解决密集问题又显得比静态图专业。5.5 改了 models.py 后迁移失败SQL 导入与 Django migration 打架现象按自己想法往models.py里加了一个字段然后执行python manage.py makemigrations app01提示检测到新字段但migrate时报Duplicate column name或Table already exists。原因db129.sql里的表结构是固定的里面可能已经包含你新加的字段而 Django 的迁移记录里没有对应 migration 文件系统以为要新建列执行时撞上已存在的列。解决任何对模型结构的改动都先在 MySQL 里确认当前表结构DESC pm25_data;如果字段已存在就不要强行迁移已有表而是只迁移 Django 的管理记录python manage.py migrate --fake app01--fake的意思是跳过真实 SQL 执行只把迁移状态标记为已完成。这个命令用的好是后悔药用不好是翻车现场——它只在“数据库结构已经正确”的前提下使用别在表结构对不上的时候硬跑。一般来说课程设计源码的数据库结构已经完整你不需要改模型最多加自定义查询方法建议对原始db129.sql只读不改把它当成基线改动都放到新的迁移文件里。6. 验收项目的三个位置日志、接口和数据库记录数6.1 看 Django 日志与命令行输出别等浏览器报 500浏览器页面报 500 时界面只给你一句“Internal Server Error”真正原因全在终端里。跑runserver后不要最小化终端每次请求都会打印一行状态码和耗时。看到GET /month-data/ 200 OK表示接口通看到GET /month-data/ 500就把终端里那条红色 Traceback 从头看到尾定位到app01/views.py的具体行号。6.2 单独请求数据接口检查 JSON 结构与空值图表渲染不出来时先绕过前端直接敲接口curl http://127.0.0.1:8000/month-data/观察返回值是以下哪种[{month: 1, value: 85.3}, {month: 2, value: 72.8}]这是健康状态。如果返回[]说明数据库里没数据问题在导入脚本如果返回[{month: null, value: null}]说明record_time字段没解析成功重点检查 CSV 时间格式和ExtractMonth的兼容性。这个习惯帮我避开了大量“改前端配置”的无用功多数情况下问题不在 ECharts 而在后端返回的数据结构。6.3 把“建库、导数据、迁移、起服务、看日志”固化成固定流程最后分享一个我个人的工程习惯也算是这套源码给我留下的后遗症从那以后我每次拿到新的课程设计或项目源码都不急着开跑而是强制走一遍“建库 → 导数据 → 迁移 → 起服务 → 看日志”五步流程。数据库没建好不开runserverSQL 没导完不跑migrate接口没验证不碰前端页面顺序错一步排查时间翻一倍。整套流程跑顺了你会发现自己复现任何 Django 可视化项目都只需要十分钟因为结构就这么几种坑也就这么几个。如果你也想把这套源码跑成自己作业的底子建议动手前先把data/目录下的 CSV 用read_csv_auto都读一遍把字段名和数据类型打印出来核对再开始导库。希望帮到你。本文还有配套的精品资源点击获取