ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Django外卖配送分析系统实战:从数据建模到可视化

Django外卖配送分析系统实战:从数据建模到可视化 我一直在用Python做数据分析类的项目最近一段时间把整套外卖配送分析流程搬到了Django上从订单数据清洗、指标统计到图表可视化全部在一个Web项目里闭环完成。这个系统说到底解决的是一个很常见的尴尬运营手里攒着大量订单数据、配送记录和商家信息但每次想看某个时段的配送情况、哪个区域单量爆发、哪位骑手效率下降都得让技术临时跑SQL或手动拉Excel数据口径换一个就要重新算一遍。把分析固化成一个Django系统之后日常运营打开浏览器就能看到所有关键指标这比我以前用脚本处理数据再生成报告要直观得多。适合谁看两个方面一是刚学完Django基础、想找一个完整体验数据建模、接口开发、图表渲染全流程的新手二是需要用Web方式做数据分析可视化的开发人员比如餐饮、即时配送、本地生活这类业务场景。如果你只是想把CSV文件画个图那用Jupyter Notebook就够没必要上系统但如果你希望最终交付的是“一个能给别人用的产品”这篇文章的经验应该对你有价值。项目代号里的“35k9z86f”不用纠结就是当时内部仓库的一个标识编号跟技术方案没有关系。1. 系统整体设计与技术选型思路1.1 外卖配送分析系统到底要解决什么问题外卖配送的本质是“订单-商家-配送员-时间-位置”五要素的协同。单看一张订单表没有任何意义把订单、配送员、商家、时间、位置联合起来才能回答以下问题门店的订单集中在哪些时段午高峰和夜宵时段是不是同样需要运力外卖平均配送时长是多少哪个环节拖后腿是接单慢、到店慢还是送达慢哪些商圈或配送区域单量密集需不需要调整配送运力每个骑手每天承接多少单平均配送用时多少有没有异常超时单哪些商家贡献了主要销售额客单价水平又如何这些问题的共同点是它们都不是单表查询可以解决的必须把多张表关联起来做分组聚合。所以数据建模时就要按“业务流程”去建模而不是按“报表需求”去建。一开始如果只想着“我要一个订单表”最后做出来的系统一定会在统计时处处碰壁比如想分析骑手效率时发现订单表里根本没有骑手ID。先把业务链路理清楚再设计表结构这套思路放到任何数据分析项目里都通用。1.2 为什么选择Django而非Flask或Node我经常被问到一个问题做数据分析可视化为什么不用Flask说实话Flask确实也能做但我更看重Django的自带能力。下面这个对比表可以直观看出来能力DjangoFlaskNode/ExpressORM内置迁移机制完善需要自己选配SQLAlchemy需要单独选型Admin后台自带可直接录入数据需要扩展包需要额外开发用户认证内置User/Permission自己集成自己集成模板渲染自带模板引擎Jinja2可选自己选适合场景完整业务系统轻量API/原型高并发实时系统Django是水电齐全的精装房Flask是毛坯房。做外卖配送分析系统时我们需要的东西——用户管理、数据库迁移、模型关系、后台录入、模板渲染——Django全都提供了我只需要把业务逻辑填进去。这也是我在众多方案里选Django的核心原因。而且对新手来说Django的“约定优于配置”特性会减少很多决策成本对团队来说Django的项目结构规范性对后期维护帮助很大。1.3 可视化方案ECharts还是Chart.js还是AntV数据可视化分两层后端负责把统计结果算出来前端负责把统计结果画出来。选型时我主要考虑三点图表类型是否覆盖需求需要折线图、柱状图、饼图、热力图、地图ECharts覆盖最全。中文资料与社区成熟度ECharts文档很完善遇到问题可以快速找到解决方案。部署与定制成本ECharts支持按需引入构建也支持直接用静态js文件很适合企业内部系统。Chart.js能画基础图表但热力、地图类较弱AntV功能强但学习成本高。最终选择ECharts核心原因是它的“开箱即用”程度最高。另外一个实际经验是ECharts的官方示例库非常丰富很多需求直接改官方示例代码就能完成这对不擅长前端样式的后端开发者尤其友好。2. 环境准备与项目骨架搭建2.1 开发环境与版本选择指南写代码首先是环境。我的建议是Python版本3.10、3.11、3.12都行。Django 4.2 LTS官方支持到2026年Django 5.x支持到2029年生产环境优先选LTS版本本地开发可以用较新版本。数据库本地演示用SQLite部署用MySQL 8.0或PostgreSQL。如果是面向正式运营的系统不建议用SQLite因为并发写入能力有限多个后台用户同时录单时容易锁库。代码编辑VSCode搭配Python插件或PyCharm选哪种都行只要把虚拟环境和Django环境跑通。安装步骤python -m venv venv source venv/bin/activate pip install django4.2.16 pip install pillowPillow是后面处理图片和地图素材时需要用到的基础库。Windows下如果使用MySQL安装mysqlclient容易编译失败有两个解决路径安装whl包或者改用pymysql在项目的__init__.py中加入import pymysql; pymysql.install_as_MySQLdb()。我个人的习惯是项目早期统一用SQLite让团队先跑通功能等数据量上来或者并发需求出现再迁移到MySQL。Django迁移工具在两种数据库之间切换成本很低这也是选Django的一个明显好处。2.2 创建Django项目和App项目名叫takeout_analysis下面建两个业务apporders和analysis。命令如下django-admin startproject takeout_analysis . python manage.py startapp orders python manage.py startapp analysis有人会问“为什么把orders和analysis分开用一个app不行吗”这里有一个模块划分的考虑。orders只负责数据实体Store、Courier、Order、DeliveryRecordanalysis负责统计视图、API接口和图表页面。后续如果需要把orders换成其他业务系统analysis能独立复用。数据、分析两个模块的解耦比一个app里所有代码堆在一起好维护得多。2.3 settings配置要点打开settings.py重点改三块。第一块是INSTALLED_APPSINSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, orders, analysis, ]第二块是数据库。本地先用SQLiteDATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } }第三块是静态文件。ECharts的js文件放到static目录STATIC_URL static/ STATICFILES_DIRS [ BASE_DIR / static, ]把这三块配置写在项目早期能避免后面开发到一半再去补基础配置。这里特别注意一定要提前配好时区TIME_ZONE Asia/Shanghai USE_TZ False统计折线图按时段聚合时如果时区不对你会很痛苦地发现数据全部偏移了几个小时。第5章我会专门说这个坑。3. 数据模型设计与业务逻辑实现3.1 外卖配送分析需要哪些核心数据表数据模型设计是整个系统的地基。我建议首版先建下面五张表表名主要字段说明Shop 店铺表name、category、address、longitude、latitude、rating保存商家基础信息供订单关联和区域分析Courier 配送员表name、phone、status、region配送员信息关联配送记录Order 订单表order_no、shop、customer_phone、created_at、amount、delivery_fee、status订单主体分析业务量、销售额DeliveryRecord 配送记录表order、courier、accepted_time、arrived_shop_time、delivered_time、distance_km配送链路的时间戳用于计算各环节耗时Region 区域表city、district、name、latitude、longitude、radius商圈/区域定义用于热力分析与运力规划这里有一个设计经验不要把所有东西都塞在订单表里尤其是配送时间戳。原因在于订单的“交易属性”和“配送履约属性”是两类数据交易属性相对固定配送履约属性变更频繁如果都放一张表一个骑手改派订单会导致记录不断被更新历史报表追踪会很麻烦。拆分之后订单表只管订单本身配送记录可以保留多条历史轨迹甚至能给同一条订单保留改派前后两条配送记录分析时取最新一条即可。ORM示例from django.db import models class Shop(models.Model): name models.CharField(max_length100) category models.CharField(max_length50, blankTrue) longitude models.FloatField() latitude models.FloatField() rating models.FloatField(default0) class Order(models.Model): STATUS_CHOICES [ (pending, 待接单), (delivering, 配送中), (completed, 已完成), (cancelled, 已取消), ] order_no models.CharField(max_length32, uniqueTrue) shop models.ForeignKey(Shop, on_deletemodels.CASCADE, related_nameorders) created_at models.DateTimeField(db_indexTrue) amount models.DecimalField(max_digits10, decimal_places2) delivery_fee models.DecimalField(max_digits8, decimal_places2) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending)在订单的created_at上加db_indexTrue因为后面所有趋势分析都会按这个字段分组聚合有索引和没索引的区别非常明显。3.2 用Django ORM完成日常查询、删除与聚合热搜词里有“django执行查询-删除对象”这里展开说。Django ORM最常用的几个操作。新增一条订单shop Shop.objects.get(id1) Order.objects.create( order_no202501010001, shopshop, created_at2025-01-01 12:00:00, amount35.50, delivery_fee5.00, statuscompleted, )查询已完成订单数量completed_count Order.objects.filter(statuscompleted).count()删除对象# 删除单条 order Order.objects.get(order_no202501010001) order.delete() # 批量删除delete() 会返回(coll_count, {model_name: count}) Order.objects.filter(statuscancelled).delete()删除时需要注意外键行为。在on_deletemodels.CASCADE下删除店铺会连带删除该店铺的订单如果只想保留历史订单选models.PROTECT或SET_NULL更安全。聚合统计是分析系统的核心用aggregate和annotatefrom django.db.models import Count, Sum, Avg total_amount Order.objects.filter( statuscompleted ).aggregate(totalSum(amount), avgSum(amount) / Count(id))按店铺统计订单数result ( Order.objects.filter(statuscompleted) .values(shop__name) .annotate(order_countCount(id)) .order_by(-order_count) )看到shop__name这种写法代表跨表关联查询。Django ORM可以用双下划线把关系链串起来这是分析系统里最常用的写法。我经常跟新手强调数据量不大的时候先在Django shell里验证SQL的结果对不对再写进视图不要直接在浏览器里刷页面调试。3.3 Admin后台快速录入与CSV批量导入Django自带Admin是这套方案的一个隐性优势。orders/admin.pyfrom django.contrib import admin from .models import Shop, Order, Courier, DeliveryRecord admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (order_no, shop, created_at, amount, status) list_filter (status, created_at)这样运营人员就能直接用后台录单、查单。但是手动录入大量历史数据不现实最好做一个CSV导入接口。最简单的版本import csv from django.http import HttpResponse from django.db import transaction def import_orders(request): ... with transaction.atomic(): for row in reader: shop Shop.objects.get(idrow[shop_id]) Order.objects.create( order_norow[order_no], shopshop, created_atrow[created_at], amountrow[amount], delivery_feerow[delivery_fee], statusrow[status], )用transaction.atomic()包住整个导入过程万一中间有一行数据异常要么全部回滚要么全部写入不会出现“导了一半数据库里脏数据”的情况。另外CSV导入前建议先做字段完整性校验比如订单号是否重复、店铺ID是否存在减少入库后才发现问题的概率。4. 数据分析指标与可视化实现4.1 外卖配送分析的指标怎么定义才不跑偏在写聚合查询之前先把指标口径统一否则后面返工特别痛苦。我常用的口径定义订单量趋势按天或按小时统计“已完成”订单数取消单不计入业务量。平均配送时长delivered_time - created_at只统计已完成订单。高峰时段按小时分桶统计订单量找出top10时段往往午高峰和夜宵时段一起出现。骑手效率统计每位骑手的日均完成单量、平均配送时长、超时率。区域热度把所有完成订单的店铺经纬度聚合到区域网格用热力图表示。商家贡献按店铺聚合销售额、订单量、客单价。这里我特别建议加一个“取消率”指标某一时段取消率突然升高很可能代表配送压力过大或恶劣天气影响比单纯看订单量更早发现问题。比如周五晚高峰突降暴雨订单量可能没变但取消率翻倍这时候运营就应该提前安排增援运力而不是等到订单积压才反应。4.2 后端API如何返回可视化需要的数据把统计逻辑放在视图函数里返回JSON给前端。一个订单量趋势接口的完整写法from django.http import JsonResponse from django.db.models import Count from django.db.models.functions import TruncHour from .models import Order def order_trend_api(request): trend_data ( Order.objects.filter(statuscompleted) .annotate(hourTruncHour(created_at)) .values(hour) .annotate(order_countCount(id)) .order_by(hour) ) items [{ hour: item[hour].strftime(%Y-%m-%d %H:00), count: item[order_count], } for item in trend_data] return JsonResponse({items: items})注意几个细节TruncHour是Django 2.2之后内置的数据库函数可以在数据库层面按小时截断时间比Python里循环处理快很多。不能用item[hour]直接丢给JsonResponsedatetime对象不是JSON可序列化类型所以要先用strftime格式化。如果列表里中文乱码在JsonResponse里加上json_dumps_params{ensure_ascii: False}即可。如果要做骑手效率排行接口可以仿照这种写法from django.db.models import Count, Avg, F def courier_stats_api(request): stats ( DeliveryRecord.objects .values(courier__name) .annotate( order_countCount(id), avg_minutesAvg(F(delivered_time) - F(accepted_time)) ) .order_by(-order_count)[:20] ) items [{ name: item[courier__name], count: item[order_count], avg_minutes: round(item[avg_minutes].total_seconds() / 60, 1) if item[avg_minutes] else 0, } for item in stats] return JsonResponse({items: items})F表达式用于在数据库层面对同一行的多个字段做运算不需要把整行加载到Python内存中性能上更优。这也是热词里“django执行查询”被频繁搜索的原因——ORM的查询能力直接决定了分析系统的开发效率。4.3 前端渲染让图表动起来前端需要两张核心图表订单趋势折线图和店铺排行柱状图。模板页面templates/analysis/dashboard.htmldiv idtrendChart stylewidth:100%; height:400px;/div div idshopChart stylewidth:100%; height:400px;/div script src{% static echarts.min.js %}/script script function renderTrend(items) { var chart echarts.init(document.getElementById(trendChart)); chart.setOption({ title: { text: 订单量趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: items.map(item item.hour) }, yAxis: { type: value }, series: [{ type: line, smooth: true, areaStyle: {}, data: items.map(item item.count) }] }); } fetch(/analysis/order-trend/) .then(res res.json()) .then(data renderTrend(data.items)); /script前端拿到数据后只用负责把数组映射到坐标轴数据计算逻辑全在后端这个分离结构对后期维护友好得多。改图表类型、加第二个Y轴都不需要动后端代码。不建议在服务端动态拼接大段JS调试起来非常痛苦。模板里的console和onerror在开发阶段可以保留等稳定了再清理掉。4.4 大屏模式的扩展思路做演示的时候会发现单张卡片式图表不够直观。我的做法是在dashboard页面基础上再做一个大屏页面三行多列顶部放核心数字卡片今日订单量、平均配送时长、订单完成率中部放订单趋势折线图和商家饼图底部放热力图和时间维度切换按钮。ECharts的grid布局可以自由控制每张图的位置。如果需要大屏自动刷新用setInterval(() { refreshData(); }, 30000)每30秒重新请求一次接口。这样业务团队把大屏投到会议室电视上不需要手动刷新就能实时掌握运力情况。5. 常见问题与排查技巧实录5.1 时区问题直接导致统计折线图偏移8个小时这个是新手最容易崩溃的问题。本地测试的时候数据正常部署到服务器后图表全部比实际晚8小时或早8小时。原因就是Django的USE_TZ默认是True数据库保存的是UTC时间你看到的本地时间其实是模板渲染时转换的但分组统计TruncHour按UTC截断后算出来的自然不是北京时间。我的处理方式是把项目定位为“本地业务系统”直接在settings里设TIME_ZONE Asia/Shanghai、USE_TZ False所有时间戳在录入时先统一转成本地时间再入库。如果以后要展示给海外用户再把USE_TZ改成True并在模板里调用localtime过滤器。用USE_TZFalse牺牲了一点国际化弹性但换来的是统计口径简单直接尤其在按小时聚合这种场景下能避免大量误判。5.2 N1查询列表页面卡成PPT当我在店铺列表页面展示所有店铺及其订单量时一开始直接写循环shops Shop.objects.all() for shop in shops: shop.order_count shop.orders.count()表面上看没问题实际上每循环一次就执行一条SQL100个店铺就是101条查询。优化方法是在查询时用annotate一次性搞定from django.db.models import Count shops Shop.objects.annotate(order_countCount(orders))使用Django调试工具django-debug-toolbar可以在页面底部看到SQL查询数量和耗时。我通常把这个工具当作性能检查的第一道关卡。如果发现查询次数激增优先检查循环内部有没有调用ORM查询然后把外键关联的取值改成select_related或prefetch_related比如orders Order.objects.select_related(shop, deliveryrecord).all()这样关联表会通过JOIN一次取回避免循环中触发额外SQL。5.3 图表不显示的几种原因我遇到过几种情况ECharts的容器高度是0。div设置了宽度但没设置高度或者高度百分比失效图表直接不显示。记得给图表容器写固定高度。js文件路径404。模板里写了{% static %}但settings没配STATICFILES_DIRS或者没执行collectstatic。本地开发时用python manage.py runserver会自动找静态目录生产环境必须执行collectstatic并把文件复制到STATIC_ROOT。数据为空时数组为空图表坐标轴没有分类数据看起来像“白屏”。后端保证返回的数组不为空前端可以增加空数据提示。script放在head里DOM还未渲染完就初始化图表也会导致不显示。我习惯把图表相关script放到body的最后或者用window.onload包一层。5.4 接口中文乱码与跨域JSON返回的数据如果含中文需要return JsonResponse(data, json_dumps_params{ensure_ascii: False})如果不指定默认会把中文转成\uXXXX格式浏览器里显示为“乱码”。这个其实不影响功能但排障时很难读。后来考虑把前端独立做成Vue或React应用就需要CORS跨域支持。用pip安装django-cors-headerssettings里加CORS_ALLOW_ALL_ORIGINS True开发环境生产时改成白名单。这步提前配好不亏。5.5 数据库索引统计查询慢的元凶订单表数据量起来后Order.objects.filter(statuscompleted, created_at__range[start, end])这类查询会变慢。解决方式很简单class Order(models.Model): ... created_at models.DateTimeField(db_indexTrue) status models.CharField(max_length20, db_indexTrue)对于经常做group by shop_id的场景可以添加联合索引class Meta: indexes [ models.Index(fields[status, created_at]), ]用联合索引可以让“按状态时间”的过滤和聚合都更快。我在数据达到几十万行时做过对比加索引前后单次统计从秒级下降到了毫秒级这个优化成本很低收益却很明显。6. 部署上线与后续扩展思路6.1 使用Gunicorn和Nginx把系统跑起来开发环境runserver只适合调试正式上线用Gunicorn。流程pip install gunicorn gunicorn takeout_analysis.wsgi:application --bind 0.0.0.0:8000 --workers 3workers数量一般按CPU核心数*21设置。然后配置Nginx反向代理server { listen 80; server_name your_domain; location /static/ { alias /path/to/takeout_analysis/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }静态文件交给Nginx托管动态请求转发给Gunicorn。如果服务器是Linux系统提前安装Python环境和相关依赖操作流程与本地安装类似。Docker部署也是常见的路线把项目写成Dockerfile创建镜像后运行容器环境一致性问题能大幅减少。6.2 后续可以扩展哪些能力让系统更完整第一个是实时推送。如果业务希望大屏页面能实时更新订单数据可以在Django基础上引入Channels用WebSocket把新订单推送前端。Django和WebSocket的整合并不复杂核心是配置ASGI应用、定义消费者前端用WebSocket对象接收消息再刷新图表数据。第二个是权限管理。热搜词里提到的“django rabc”本质上是基于角色的访问控制。Django自带User、Group和Permission可以在管理员后台分配权限比如运营只能看报表但不能修改历史数据管理员才能管理店铺和配送员。这个模块做好后系统就不再只是开发者的工具可以放心交给业务部门。第三个是定时任务。每天凌晨跑一次数据汇总把前一日的订单量、配送时效、取消率写入一个汇总表页面只读汇总表而不是实时聚合并表响应速度会更快。实现方式可以用Celery的beat任务或者简单点用crontab调用管理命令。第四个是地理围栏。给店铺配置半径判断用户下单地址是否在配送范围内超范围自动提示这个对实际运营有直接价值。整体来看这套“Django ECharts 分析指标”的组合做外卖配送分析系统绰绰有余而且很容易扩展成其他业务场景的可视化分析平台。写到这里我想分享一个真实的体会这类分析可视化项目技术栈框架并不是最难的真正决定成败的是数据指标口径是否清晰、数据模型设计是否合理。我一开始拿到“外卖配送分析”需求时第一版系统什么都想展示结果做了几个图表后发现口径互相矛盾——有的按北京时间统计有的按UTC统计有的把取消订单也计算进销售额返工的时间远超预期。后来我把所有指标口径写进项目README每一个接口注释里写清楚计算逻辑项目才真正稳定下来。所以如果你准备上手做这样一个系统先花两天时间整理清楚指标定义和字段含义绝对比急着写代码有价值得多。另外再分享一个小技巧一定要把系统里所有查询放在数据库层面聚合用Django ORM的annotate和aggregate不要在Python端循环求和。我踩过的坑是刚开始图省事把数据拉到Python里算结果订单只有几万条还能忍受到了几十万条接口直接卡到十几秒。优化到数据库层面之后同一套统计逻辑稳定在几百毫秒。这是我个人在这个项目里收获最大的一点希望对你也有帮助。
返回列表