
简介基于Python/Django的人口普查可视化系统毕业设计源码包面向软件工程、计科、人工智能等计算机相关专业学生与开发者适用于毕业设计、课程设计、项目初期演示也可供数据可视化与Web开发入门参考。压缩包内共215个文件包含112个人口数据Excel表、后台源码、前端页面与样式脚本、数据库脚本及部署文档另有字体、图片等界面资源整体仅5.35MB结构清晰便于快速部署与二次修改。项目已通过Windows/macOS/Linux多平台运行验证获导师认可并达到95分答辩评审成绩目前已有240人学习下载。除可运行系统外还可借助附带数据资料与源码理解人口普查数据的清洗、存储和可视化流程并可在现有模块基础上扩展新功能同时附带的部署文档可帮助快速搭建环境适合课设、毕设或项目立项演示。1. 人口普查可视化系统在做什么一个毕设题目背后的完整工程链路“人口普查可视化系统”这个题目每年都有一批毕业生在做——数据量看起来大、页面看起来要有图表但真正动起手来卡住人的往往不是画图而是数据从哪来、Django怎么把聚合统计吐给前端。这套基于 Python Django 的方案核心就三件事把普查原始数据建好模型用 ORM 写出按区县、年龄、性别分组的统计接口再让 ECharts 把接口数据画成图。它适合想做完整全栈毕设、又不想在数据清洗和部署上耗太久的人。你要的不是炫技而是一条能复现、能答辩、能部署的落地路径下面这条链路就是照着这个目标走的。2. 数据模型与数据准备人口普查表怎么设计才不返工2.1 从统计口径倒推模型字段很多人一上来就急着建模型结果做到可视化阶段发现缺字段、缺索引、维度对不上只能回头改表。我一般会先把统计图想的指标列出来——按区县分组、按年龄分段、按性别对比、按户口性质看城乡结构。这些指标反推回去就是模型里必须有的字段区县、年龄、性别、户口性质、文化程度再加一个唯一记录编号用于去重。from django.db import models class CensusRecord(models.Model): GENDER_CHOICES ( (M, 男), (F, 女), ) HOUSEHOLD_CHOICES ( (urban, 城镇), (rural, 乡村), ) record_id models.CharField(记录编号, max_length32, uniqueTrue) district models.CharField(区县, max_length50, db_indexTrue) town models.CharField(乡镇/街道, max_length50, blankTrue, db_indexTrue) community models.CharField(村/社区, max_length80, blankTrue) age models.IntegerField(年龄, db_indexTrue) gender models.CharField(性别, max_length1, choicesGENDER_CHOICES) household_type models.CharField(户口性质, max_length10, choicesHOUSEHOLD_CHOICES) education models.CharField(文化程度, max_length20, blankTrue) class Meta: db_table census_record indexes [ models.Index(fields[district, gender, age], nameidx_district_gender_age), ]这里有几个选型上的考虑。record_id 设成 unique是为了导入 CSV 时做幂等去重同一份文件重复导入不会产生脏数据。district、age、gender 都加了 db_index因为后面可视化的聚合查询基本都围绕这三个字段分组筛选索引有没有查询速度可能差几十倍。gender 和 household_type 用 choices 限定合法值比后端写 if 判断更省事也从源头挡住脏数据。2.2 创建项目与 App把 Django 骨架先立起来动手写模型之前先把 Django 项目建好。常见做法是虚拟环境隔离依赖然后依次执行创建命令python -m pip install django pandas django-admin startproject census_project cd census_project python manage.py startapp census创建好 app 之后别忘了两件事一是把 census 加进 settings.py 的 INSTALLED_APPS二是在 census/models.py 里把模型定义好然后执行迁移。很多新手在这里翻车迁移时报Table census_censusrecord already exists多半是之前手动建过同名的表或者跑过migrate但模型后来改过字段。这时候不要急着删库先python manage.py makemigrations census生成新的迁移文件再migrate应用一次就好。2.3 批量导入 CSVpandas 读取加 bulk_create数据资料里最常见的就是 CSV 或 Excel 格式的普查表。直接用 Django ORM 逐行 create 在数据量到几万行以后会非常慢所以我一般写一个自定义管理命令用 pandas 读取再用 bulk_create 批量写入。在 census/management/commands/ 目录下新建 import_census.pyimport pandas as pd from django.core.management.base import BaseCommand from census.models import CensusRecord class Command(BaseCommand): help 导入人口普查CSV数据 def add_arguments(self, parser): parser.add_argument(--csv, requiredTrue, helpCSV文件路径) def handle(self, *args, **opts): df pd.read_csv( opts[csv], encodingutf-8-sig, dtype{age: int}, usecols[record_id, district, town, community, age, gender, household_type, education] ) df df.dropna(subset[record_id, district, age]) objs [ CensusRecord( record_idrow[record_id], districtrow[district], townrow.get(town, ), communityrow.get(community, ), ageint(row[age]), genderrow[gender], household_typerow[household_type], educationrow.get(education, ), ) for _, row in df.iterrows() ] CensusRecord.objects.bulk_create(objs, batch_size2000, ignore_conflictsTrue) self.stdout.write(self.style.SUCCESS(f导入完成: {len(objs)} 条))这段命令需要放到 census 项目的 management/commands 目录下运行python manage.py import_census --csv data.csv即可。read_csv 里 encoding 写成 utf-8-sig 是为了兼容带 BOM 的文件如果你拿到的是 GBK 编码的 CSV把 encoding 改成 gbk 就行这一步很关键不然导入后全是乱码。dropna 会直接丢掉 record_id、district、age 为空的行减少后续统计时出现None分组的概率。ignore_conflictsTrue 配合 record_id 的唯一约束重复导入时只会跳过已存在的行不会整个报错。3. Django ORM 统计查询把聚合接口写成可视化的数据出口3.1 values 加 annotate按区县分组的两种写法可视化的核心不是把原始数据全量吐给前端而是让后端先算好聚合结果。Django ORM 里最常用的组合是values()加annotate()values 指定按哪个字段分组annotate 生成统计列。from django.db.models import Count from census.models import CensusRecord # 按区县统计人口总数 district_stats ( CensusRecord.objects .values(district) .annotate(totalCount(id)) .order_by(-total) ) # 结果示例: [{district: 云台区, total: 35890}, {district: 临江区, total: 31244}, ...]这段查询翻译成 SQL 就是SELECT district, COUNT(id) FROM census_record GROUP BY district ORDER BY COUNT(id) DESC。要注意Count(id)和Count(*)的细微区别如果你有外键字段可能为 NULLCount 指定具体字段会跳过 NULL 行Count(*) 则全算。普查记录里 record_id 和 id 都不会为空两者差别不大但如果后面你要统计有文化程度字段的人数Count(education)出来的数字可能比总人数小这是需要心里有数的事情。3.2 按年龄段分桶Case/When 的区间聚合按年龄分段是人口普查可视化的经典需求。年龄是连续整数不能直接按 18、30、45 一个个值分组得先分桶。我第一次做时直接在 Python 里遍历每条记录做区间判断几十万条数据跑了一分多钟后来改成在数据库层面分桶快了几个量级。from django.db.models import Case, When, Value, CharField, Count from census.models import CensusRecord age_group Case( When(age__lt18, thenValue(0-17)), When(age__lt30, thenValue(18-29)), When(age__lt45, thenValue(30-44)), When(age__lt60, thenValue(45-59)), defaultValue(60), output_fieldCharField(), ) age_gender_stats ( CensusRecord.objects .annotate(age_groupage_group) .values(age_group, gender) .annotate(totalCount(id)) .order_by(age_group, gender) )这段代码的灵魂在 Case/When 的顺序。age__lt18放在最前面接着是age__lt30数据库按条件从上往下匹配匹配到就停止。如果你把age__lt60写在前面那 25 岁的人也会被归进45-59因为这条件提前被满足了。写这种区间分桶的 Case 时条件必须从小到大排列或者从大到小配合age__gte写否则统计出来的数据会完全失真。这也是我在实际项目里最容易踩的坑没有之一。3.3 避免 N1 查询select_related 的正确姿势当项目需要把普查记录和乡镇、职业等维度表关联起来展示时就轮到 select_related 上场了。比如你在系统里维护了一张职业表 Vocation人口信息表 PersonInfo 通过外键指向它from django.db import models class Vocation(models.Model): name models.CharField(max_length50) class PersonInfo(models.Model): name models.CharField(max_length100) vocation models.ForeignKey(Vocation, on_deletemodels.PROTECT, related_namepersons)如果做一个“每个职业对应多少人”的统计新手最容易写成循环里每次取外键对象# 不要这样做: 会产生 N1 查询 persons PersonInfo.objects.all() for p in persons: print(p.vocation.name)这行代码在数据库层面会先查一次 PersonInfo再对每条记录查一次 Vocation。假设有 5 万条 PersonInfo就是 1 加 5 万次查询页面不卡才怪。正确做法是persons PersonInfo.objects.select_related(vocation).all() for p in persons: print(p.vocation.name)select_related 会生成一条带 LEFT JOIN 的 SQL一次性把 Vocation 的数据查出来缓存。它只适用于 ForeignKey 和 OneToOne 这类单值关系如果是多对多关系要改用 prefetch_related。做人口普查可视化时只要模型里外键多于一个我建议对查询条件里出现的所有外键都加上 select_related这是让聚合接口不翻车的基本功。4. 可视化接口与图表实现ECharts 与 pyecharts 怎么落地4.1 视图层返回 JSONJsonResponse 与序列化图表要画在浏览器里前端就要拿到结构化数据。Django 里直接返回 JsonResponse 就行不需要走 Django REST Framework 那么重的链路毕设系统的图表接口用 DRF 反而增加学习成本。from django.http import JsonResponse from django.db.models import Count from census.models import CensusRecord def district_gender_api(request): rows ( CensusRecord.objects .values(district, gender) .annotate(totalCount(id)) .order_by(district, gender) ) data [ {district: r[district], gender: r[gender], total: r[total]} for r in rows ] return JsonResponse({code: 0, data: data})这段接口把分组和聚合全部放在数据库层完成Python 这边只做一次轻量的列表推导返回给前端的结构是平铺的 JSON 数组。前端拿到后自己按 district 归类或者后端直接做成嵌套结构都可以。我一般返回平铺结构因为前端用 ECharts 时经常需要同时按 district 和 gender 两条维度做 series平铺结构反而更灵活。4.2 服务端生成图表pyecharts 快速出图如果不想写前端代码pyecharts 是性价比最高的选择。它把 ECharts 的配置封装成了 Python 类服务端直接生成 HTML 片段返回给模板渲染。from pyecharts.charts import Bar from pyecharts import options as opts from django.http import HttpResponse from census.models import CensusRecord from django.db.models import Count def district_bar_chart(request): rows ( CensusRecord.objects .values(district) .annotate(totalCount(id)) .order_by(-total) ) bar ( Bar() .add_xaxis([r[district] for r in rows]) .add_yaxis(人口数, [r[total] for r in rows]) .set_global_opts(title_optsopts.TitleOpts(title各区县人口分布)) ) return HttpResponse(bar.render_embed())这里用的是 render_embed 而不是 render差别在于 render 会生成一个完整的独立 HTML 文件render_embed 只返回图表容器和初始化脚本可以直接嵌到你的 base.html 模板里。pyecharts 适合时间紧、要求“有图能看能答辩”的场景但它的交互定制空间有限如果导师要求联动筛选、钻取还是得走前端 ECharts 方案。4.3 前端异步加载ECharts 的 Ajax 接入前后端分离是更通用、也更像真实项目的做法。Django 只负责提供 JSON 接口前端用 ECharts 拉数据画图。模板页面里放一个容器 div然后写一小段脚本div idmain stylewidth: 100%; height: 480px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script fetch(/api/district-gender/) .then(r r.json()) .then(res { if (res.code ! 0) return; const chart echarts.init(document.getElementById(main)); const districts [...new Set(res.data.map(d d.district))]; const males districts.map(d res.data.find(x x.district d x.gender M)?.total || 0 ); const females districts.map(d res.data.find(x x.district d x.gender F)?.total || 0 ); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [男, 女] }, xAxis: { type: category, data: districts }, yAxis: { type: value }, series: [ { name: 男, type: bar, data: males }, { name: 女, type: bar, data: females } ] }); }); /script这段脚本里最坑的地方是性别字段的匹配。如果数据库 choices 里存的是M和F接口返回的也是这两个字母就必须和前端比对一致。有的项目在模型里把 choices 设成男/女前端也按中文比但导出的 CSV 里写的是male/female导入时没做转换最终接口数据和图表就全对不上。我会在写模型时统一用短代码存库、显示名称用 get_gender_display 去拿避免前后端各翻译一遍。5. 部署避坑指南静态文件、数据库与 uWSGI 的五个常见问题5.1 DEBUGFalse 后静态文件突然 404现象本地runserver一切正常一关 DEBUG、跑完collectstatic再启动服务页面只剩 HTMLCSS、JS 全部 404图表区域空白。原因Django 开发服务器在 DEBUGTrue 时会自动托管静态文件关掉 DEBUG 之后这个行为就被禁用了。项目没有配置 STATIC_ROOT 也没有用静态文件中间件collectstatic 只是把各 app 的静态文件复制到了一个目录并没有一个服务去处理/static/请求。解决安装 whitenoise在 settings.py 里把它加进 MIDDLEWARE并设置 STATIC_ROOTMIDDLEWARE [ django.middleware.security.SecurityMiddleware, whitenoise.middleware.WhiteNoiseMiddleware, # 其余中间件... ] STATIC_ROOT BASE_DIR / staticfiles然后执行python manage.py collectstatic --noinput。白噪声中间件会直接托管 STATIC_ROOT 下的文件不需要额外配 Nginx 的 alias对毕设级别的并发量完全够用。如果部署在 Nginx 后面也可以让 Nginx 直接 alias 到 staticfiles 目录但这需要改两处配置不如 whitenoise 省事。5.2 CSV 导入中文乱码与数据库字符集现象用 pandas 读 CSV 导入后页面上显示的中文全是乱码区县名变成一堆问号或者在 Django admin 里看到的是乱码但在本地 IDE 里跑同样的代码没问题。原因有两层。第一层是 CSV 文件编码不统一有的是 GBK有的是 UTF-8 带 BOM第二层是 MySQL 表或连接字符集不是 utf8mb4中文写入时被截断或转码失败。解决导入时先用chardet检测文件编码或者干脆写一个自动尝试逻辑import pandas as pd def read_csv_auto(path): for enc in [utf-8-sig, gbk, gb18030]: try: return pd.read_csv(path, encodingenc) except UnicodeDecodeError: continue raise ValueError(无法识别的文件编码)数据库层面创建库时显式指定字符集CREATE DATABASE census_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;settings.py 里也要注意别让连接串覆盖掉字符集。建议把 OPTIONS 写进 DATABASES 配置确保连接使用的字符集和表一致。这种乱码问题最玄学因为同样的命令在一台机器上正常、到另一台就乱多半就是文件编码和数据库字符集各管各的。5.3 部署后访问返回 403 Bad Request现象runserver正常部署到服务器后用 IP 访问浏览器直接显示403 Forbidden日志里写着Invalid HTTP_HOST header。原因这是 Django 自身的安全校验。DEBUGFalse 时ALLOWED_HOSTS 默认不接受 IP 和任意域名访问HTTP 请求头里的 Host 不匹配就直接拒掉。解决在 settings.py 里把服务器地址加进去ALLOWED_HOSTS [127.0.0.1, localhost, 192.168.1.100]如果部署环境不固定也可以写成ALLOWED_HOSTS [*]但这就意味着任意 Host 都能访问你的站点不建议公开项目这么做。我一般按实际部署的域名和 IP 逐个填省心也安全。5.4 数据量大时聚合查询爬行索引与预统计表现象几十万条普查记录按区县分组查询需要两三秒前端图表加载时一直转圈。内存看起来也没爆就是慢。原因分组字段没有索引或者索引没有覆盖到查询的组合。Mysql 在做 GROUP BY 时如果字段无索引就需要 filesort 和临时表数据量一上来就是慢查询。解决先查看 Django 生成的 SQL 是否符合预期最简单的方式是打印查询集qs CensusRecord.objects.values(district, gender).annotate(totalCount(id)) print(qs.query)把打印出来的 SQL 放到数据库里EXPLAIN一下看有没有用到索引。如果你的模型里 district 和 gender 都单列建了索引但组合查询仍慢那就按本章前面给的例子建一个联合索引models.Index(fields[district, gender, age])。如果慢查询还是存在说明聚合结果本身适合做预统计表——写一个定时任务每天把分组统计结果算好存到一张统计表里页面接口直接查统计表。5.5 uWSGI 启动后进程内存高进程数与线程配置现象用 uWSGI 跑 Django启动后还没几个人访问内存就吃掉了好几个 G偶尔直接 OOM 把进程杀了。原因uWSGI 默认会根据 CPU 核数生成进程每个进程都预加载一份 Django 代码和数据库连接池。如果机器只有 2G 内存却按 4 核生成 4 个进程每个进程吃 600M 内存很快就爆。解决按内存而非核数配置进程数并发不高时小内存机器用单进程多线程就够了[uwsgi] http :8000 chdir /home/user/census_project module census_project.wsgi:application master true processes 2 threads 2 harakiri 60 max-requests 2000 vacuum true进程数 2 线程数 2 的意思是每个进程开 2 个线程总共能处理 4 个并发请求对毕设演示完全足够。max-requests 设成 2000 是让每个进程处理够 2000 个请求后自动回收防止内存泄漏积累。真空模式会在退出时清理 socket 和 pid 文件跑过uwsgi --stop后不会残留僵尸文件。6. 最后的进阶技巧给聚合查询加一层缓存把响应从秒级降到毫秒级统计接口写好后页面加载还是慢不一定是查询本身慢而是每次刷新都重复执行一遍同样的 GROUP BY。人口普查数据不是高频写入一小时内可能都没有新数据进来完全可以把统计结果缓存住。Django 自带了一个缓存框架默认的 LocMemCache 就能用不需要上 Redis 也能体感明显。from django.core.cache import cache from django.http import JsonResponse from django.db.models import Count from census.models import CensusRecord def district_gender_api(request): cache_key stat:district_gender data cache.get(cache_key) if data is None: rows ( CensusRecord.objects .values(district, gender) .annotate(totalCount(id)) .order_by(district, gender) ) data [ {district: r[district], gender: r[gender], total: r[total]} for r in rows ] cache.set(cache_key, data, 60 * 10) return JsonResponse({code: 0, data: data})这个写法里第一次请求会走完整查询并缓存 10 分钟之后的所有请求都直接从缓存取数据。对于毕设演示场景你甚至可以缓存一天。要注意的是缓存键的粒度如果你按区县、按年龄段、按性别各写一个接口每个接口都得用不同前缀的键不能图省事用一个固定键否则两个接口会互相覆盖数据。另一个技巧是把数据更新时间写进键里比如stat:district_gender:20250612数据重新导入时改一下日期参数缓存自然失效。这样数据更新后刷新页面就能看到新统计不需要重启服务。我现在写聚合接口都会顺手把缓存加上同时把查询逻辑单独抽成函数方便以后换 Redis 后端或者做预统计任务时直接复用。缓存不是银弹它是把重复计算变成读内存但组合上联合索引和预统计表这一套组合拳打下来秒级响应基本能压到几十毫秒。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。