☰
滴滴出行大数据分析与可视化大屏系统实战
2026/10/12 8:20:42 网站建设 项目流程

最近有不少同学来问我,期末或者毕业设计想做一个“大数据分析 + 可视化大屏”方向的项目,选来选去都绕不开hadoop + spark + django + python这个经典组合。我把自己做的一个滴滴出行大数据分析与可视化系统完整复盘一篇,从 Hadoop 伪分布式搭建、Spark 数据清洗、Django 后端接口到 ECharts 大屏展示,结合源码和文档调试过程,把能直接抄作业的步骤、参数和踩过的坑都列出来。如果你正好被这个题目命中,或者在找一套能边做边讲清楚原理的大数据项目,这篇应该能帮你省不少时间。

这个项目的本质不复杂:用 HDFS 存放原始订单数据,用 Spark 做分布式清洗和聚合统计,把统计结果写到 MySQL,然后用 Django 提供 JSON 接口,最后在浏览器里呈现一个可自动刷新的可视化大屏。整个链路就是离线数仓里最典型的“采集 - 存储 - 计算 - 展示”流程,但把每一步都落到工程实现后,你已经能清楚讲出 Hadoop、Spark、Django 各自负责什么,这也是答辩时最容易被问到的地方。

1. 项目整体设计与技术选型

1.1 这个滴滴出行分析系统到底做了什么

我们先别急着写代码,先把题目拆开看。滴滴出行数据分析,核心是处理网约车订单数据。一个订单通常包含订单号、城市编号、上车点经纬度、下车点经纬度、开始时间、结束时间、金额、里程、业务类型、司机 ID 等字段。我的做法是拿一批脱敏后的模拟订单(也可以理解成公开的网约车订单样例)作为数据源,规模上做了几万到几十万条,目的是让 Spark 的分布式算力“有得发挥”,又不至于让你在本地机跑不动。

系统要回答的问题很实际:一天里面什么时段订单最多?哪些城区订单量最大?平均客单价是多少?凌晨和早晚高峰的车辆利用率有何差异?等等。最终通过大屏把这些统计结果展示出来,图表包括订单量时段趋势折线图、区域订单分布柱状图、城市热力图、核心指标数字滚动卡片等。

用这个题目做课程设计也好,毕业设计也好,最大的优势是它“麻雀虽小,五脏俱全”:你会接触到大数据环境安装、分布式文件存储、Spark 统计分析、Web 后端接口、前端 ECharts 可视化,每层都是速成项目里最能出效果的部分。

1.2 为什么偏偏是 Hadoop + Spark + Django + Python

先把技术栈的“为什么”说清楚,不然答辩时容易被一句话噎住。

  • Hadoop:负责分布式存储。原始 CSV 数据放在 HDFS 上,体现的是“大数据先落到分布式文件系统”的常识。哪怕你的实际数据量只有几十万条,也要故意走一遍 HDFS,让评委看到你理解分布式存储的意义。
  • Spark:负责分布式计算。用 PySpark 读取 HDFS 上的数据,做数据清洗、去重、过滤、聚合。为什么用 Spark 而不是 Pandas?因为 Pandas 是单机内存计算,而这个项目的展示重点就是“大数据技术栈”,用 Spark 才能把分布式计算落到实际代码里。
  • Django:负责 Web 后端和 API 接口。它自带 ORM、Admin、模板系统,写起来比 Flask 结构清晰,而且很多同学在课程作业里学过 Django,上手阻力小。
  • Python:把整个链路串起来的胶水语言。数据清洗用 PySpark,后端开发也用 Python,前后端语言统一,维护成本低。
  • 可视化大屏:本质上是前端展示层,用 ECharts 绘制图表。选它不是因为 ECharts 有多高深,而是图表类型丰富、文档齐全、效果炫酷,特别适合“汇报场景”。

可以对照下面这个分层表格看系统结构:

层次组件作用
数据存储层HDFS / MySQL原始订单存 HDFS,聚合统计结果存 MySQL
数据计算层Spark(PySpark)分布式清洗、过滤、聚合
后端应用层Django提供 REST API,读取 MySQL 统计数据
前端展示层ECharts + HTML/JS大屏布局、图表渲染、定时刷新

有人会问,为什么实时性不强还要套这么复杂架构?其实这里做的是离线批次分析,不是实时流处理。你用 Spark 跑一次统计可以只跑几分钟,然后 Django 从 MySQL 读取结果展示。如果非要做成实时刷新,只需要在大屏上用 setInterval 定时请求后端接口重新拉数据就行,这就是本项目的“动态”含义。

2. 环境搭建与数据预处理实战

2.1 从零搭建 Hadoop 伪分布式与 Spark 环境

这个环节是很多人的第一个泥坑。我建议用虚拟机和原生安装方式,尽量别用 Docker,因为课程设计答辩时老师经常问“你的 Hadoop 是怎么装的”,你要是没手工配过,会露怯。下面给一条我验证过的通路。

前置条件:一台 Linux 机器或虚拟机,内存至少 4G,推荐 8G。装好 JDK 1.8,配置环境变量:

export JAVA_HOME=/usr/local/jdk1.8 export PATH=$PATH:$JAVA_HOME/bin

然后是 Hadoop 伪分布式安装。以 Hadoop 3.3.x 为例,把 hadoop 解压到/usr/local/hadoop后,修改这些配置文件:

core-site.xml里指定 NameNode 地址和临时目录:

<property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/usr/local/hadoop/tmp</value> </property>

hdfs-site.xml里设置副本数为 1:

<property> <name>dfs.replication</name> <value>1</value> </property>

然后配置 SSH 免密登录,格式化 NameNode,再启动:

ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys /usr/local/hadoop/bin/hdfs namenode -format /usr/local/hadoop/sbin/start-dfs.sh /usr/local/hadoop/sbin/start-yarn.sh

启动后用jps命令应该能看到 NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager 这五个进程。如果缺进程,先看/usr/local/hadoop/logs下的日志,九成是 JAVA_HOME 没配对或者端口被占用。

Spark 的安装相对轻松。下载带 Hadoop 版本的 Spark 包,比如spark-3.3.2-bin-hadoop3,解压以后配置:

export SPARK_HOME=/usr/local/spark export PATH=$PATH:$SPARK_HOME/bin

单机学习模式可以直接用local[*]来跑,不用启动 Standalone 集群。但为了讲清楚 Spark 集群概念,建议也把 Master 和 Worker 启起来:

$SPARK_HOME/sbin/start-master.sh $SPARK_HOME/sbin/start-worker.sh spark://localhost:7077

启动后浏览器访问http://localhost:8080,就能看到 Spark 的 Web UI,里面会显示 Worker 资源信息,这也是答辩时能截图展示的实打实的证据。

如果你要做 Hadoop 高可用集群,那就得加上 ZooKeeper,常规做法是配置 ZK ensemble 和 JournalNode。不过课程设计阶段伪分布式已经够用,不要为了凑内容硬上 HA 集群,会把整个开发周期拖长一倍。

2.2 手把手用 Spark 清洗滴滴出行数据

数据清洗是整个项目里最体现“大数据分析”内核的部分。我的数据源是一份 CSV 订单文件,字段类似这样:

order_id, city_id, vehicle_type, start_time, end_time, start_lng, start_lat, dest_lng, dest_lat, price, distance, pay_type

先用 PySpark 建一个 SparkSession,从 HDFS 上读取 CSV:

from pyspark.sql import SparkSession from pyspark.sql.functions import col, hour, count, sum as _sum, round spark = SparkSession.builder \ .appName("DidiOrderAnalysis") \ .master("local[*]") \ .getOrCreate() df = spark.read \ .option("header", True) \ .option("inferSchema", True) \ .csv("hdfs://localhost:9000/user/didi/data/didi_orders.csv")

读进来以后先看结构和数量:

df.printSchema() df.show(5) print("total raw rows:", df.count())

清洗有几步不能省:

  • 删除完全重复的行,用order_id做dropDuplicates
  • 过滤掉经纬度异常的记录,比如经度超出 70~140、纬度超出 15~55
  • 过滤掉价格为负或者里程为 0 的脏数据
  • 把start_time转为时间戳,并提取小时字段,方便后面按时段聚合

示例代码:

df_clean = df.dropDuplicates(["order_id"]) \ .filter(col("start_lng").between(70, 140)) \ .filter(col("start_lat").between(15, 55)) \ .filter(col("price") > 0) \ .filter(col("distance") > 0) \ .withColumn("hour_of_day", hour(col("start_time"))) df_clean.cache() print("clean rows:", df_clean.count())

洗完之后开始做几个核心统计:

按小时订单量:

hourly_stat = df_clean.groupBy("hour_of_day") \ .agg(count("order_id").alias("order_count"), round(_sum("price"), 2).alias("total_amount")) \ .orderBy("hour_of_day")

按城市 ID 的订单量:

city_stat = df_clean.groupBy("city_id") \ .agg(count("order_id").alias("order_count"), round(_sum("price"), 2).alias("total_amount")) \ .orderBy(col("order_count").desc())

把结果写回 MySQL,后面 Django 直接查表:

city_stat.write.mode("overwrite") \ .format("jdbc") \ .option("url", "jdbc:mysql://localhost:3306/didi_db?useUnicode=true&characterEncoding=utf8") \ .option("dbtable", "city_order_stat") \ .option("user", "root") \ .option("password", "123456") \ .option("driver", "com.mysql.cj.jdbc.Driver") \ .save()

写 MySQL 前一定要先把对应的表建好,或者用 Spark 自动建表。我这里更推荐先在 MySQL 里手动建表,指定好字段类型和字符集,不然经常出现中文乱码或者字段类型长得太随意的情况。

3. Django 后端与接口开发

3.1 Django 项目初始化和数据模型设计

后端这块我用 Django 4.x + Python 3.9,数据库直接连 MySQL。先用虚拟环境把 Django 装好:

python -m venv venv source venv/bin/activate pip install django pymysql cryptography

创建项目和应用:

django-admin startproject didi_web cd didi_web python manage.py startapp api

在settings.py里注册api应用,并配置 MySQL:

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'api', ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'didi_db', 'USER': 'root', 'PASSWORD': '123456', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } }

别忘了在api/__init__.py或项目配置里让 PyMySQL 冒充 MySQLdb:

import pymysql pymysql.install_as_MySQLdb()

数据模型和 Spark 写出来的统计表一一对应。我在api/models.py里写了两个模型类,一个按小时统计,一个按城市统计:

from django.db import models class OrderHourStat(models.Model): hour_of_day = models.IntegerField(verbose_name="小时") order_count = models.BigIntegerField(verbose_name="订单数") total_amount = models.FloatField(verbose_name="总金额") class Meta: db_table = "hour_order_stat" verbose_name = "小时订单统计" class CityOrderStat(models.Model): city_id = models.IntegerField(verbose_name="城市ID") order_count = models.BigIntegerField(verbose_name="订单数") total_amount = models.FloatField(verbose_name="总金额") class Meta: db_table = "city_order_stat" verbose_name = "城市订单统计"

这里有个小技巧:db_table直接指定成 Spark 写入 MySQL 时用的那张表名,Django 就不需要再用 migrate 生成新表,只要确保 MySQL 里表已经存在,Django 直接读就行。这种方式适合“数据由 Spark 计算,Web 只做展示”的项目结构。

3.2 编写查询接口并避开 ORM 的删除坑

接口设计要围绕大屏需要的数据来,我做了三个主要接口:

  • /api/summary/:返回总订单量、总金额、平均客单价、覆盖城市数
  • /api/trend/:返回 0~23 时订单量和金额趋势
  • /api/city/:返回各城市订单量和金额排名

在api/views.py里写一个通用查询函数:

from django.http import JsonResponse from django.db.models import Sum, Count from .models import OrderHourStat, CityOrderStat def trend(request): rows = list(OrderHourStat.objects.all().order_by("hour_of_day")) data = [ { "hour": r.hour_of_day, "order_count": r.order_count, "total_amount": r.total_amount, } for r in rows ] return JsonResponse({"data": data}) def city(request): rows = list(CityOrderStat.objects.all().order_by("-order_count")[:20]) data = [ { "city_id": r.city_id, "order_count": r.order_count, "total_amount": r.total_amount, } for r in rows ] return JsonResponse({"data": data})

api/urls.py里配置路由:

from django.urls import path from . import views urlpatterns = [ path('summary/', views.summary), path('trend/', views.trend), path('city/', views.city), ]

这里特别提醒一个 Django ORM 的坑,也是很多新手翻车的地方:查询并删除对象时,不要无条件使用filter().delete()。比如你想删掉某个城市 ID 的统计记录,操作前一定要确认过滤条件:

# 危险写法 CityOrderStat.objects.filter(city_id=100).delete() # 安全写法 rows = CityOrderStat.objects.filter(city_id=100) print(rows.count()) # 先看数量,是不是预期值 if rows.exists(): rows.delete()

如果删除的是单条记录,用city_stat = CityOrderStat.objects.get(pk=1),但要注意get()查不到对象会抛 DoesNotExist,更稳妥的是first():

city_stat = CityOrderStat.objects.filter(pk=1).first() if city_stat: city_stat.delete()

之所以强调这个坑,是因为删除操作一旦条件写宽,会把整张表的数据抹掉。我在调试时手滑过一次,五分钟的前端页面数据全空了,最后只能重新跑 Spark 任务,白等十几分钟。

如果你要更实时一点的效果,可以往 Django Channels + WebSocket 方向扩展,后端有数据变化时主动推送前端。但我个人建议项目初期用前端轮询就够了,一个setInterval30 秒请求一次接口,简单稳定,演示也够用。

4. 可视化大屏设计与实现

4.1 大屏布局与核心图表选择

大屏我采用 1920×1080 的深色底版,背景用深蓝渐变,避免花哨,突出数据本身。整体分成五个区域:

  • 顶部居中:系统标题“滴滴出行大数据分析平台”
  • 顶部左侧:核心指标卡片(总订单量、总成交金额、平均客单价、订单量环比)
  • 顶部右侧:时段高峰提醒或指标排名
  • 中间主体:城市订单热力地图,配合全国地图散点
  • 底部左侧:24 小时订单趋势折线图
  • 底部右侧:城市订单量 TOP10 柱状图

前端不用框架,直接一个 HTML 页面,引入 ECharts,用 CSS grid 或 flex 做栅格布局。为了响应式,我给 root 容器设置了百分比宽度,在大屏和普通屏幕上都能自动撑开。

核心部分是用 ECharts 初始化图表。比如底部左侧的折线图:

var trendChart = echarts.init(document.getElementById('trendChart')); function loadTrend() { fetch('/api/trend/') .then(response => response.json()) .then(json => { var data = json.data; trendChart.setOption({ tooltip: { trigger: 'axis' }, grid: { left: '10%', right: '10%', top: '30%', bottom: '20%' }, xAxis: { type: 'category', data: data.map(d => d.hour + '时') }, yAxis: { type: 'value', name: '订单量' }, series: [{ name: '订单量', type: 'line', smooth: true, areaStyle: { opacity: 0.3 }, data: data.map(d => d.order_count) }] }); }); }

底部右侧柱状图同理,注意加载时如果数据为空,要先chart.clear(),否则旧图形会闪烁。

4.2 大屏数据刷新与地图热力联动

可视化大屏不能做一次请求就完事,要模拟“动态监控”效果。我用两个setInterval分别刷新指标和图表:

function loadAll() { loadSummary(); loadTrend(); loadCity(); } setInterval(loadAll, 30000); loadAll();

还要处理窗口尺寸变化:

window.addEventListener('resize', function () { if (trendChart) trendChart.resize(); if (cityChart) cityChart.resize(); if (mapChart) mapChart.resize(); });

地图热力图部分,需要用到 ECharts 的地图组件。城市 ID 映射到具体的城市名时,我在后端做了一层解码,把 city_id 替换为中文名称。如果没有地理坐标数据,也可以用最简单的散点图按经纬度标记订单热点,只需要把数据格式转成[lng, lat, value]数组。

var mapChart = echarts.init(document.getElementById('mapChart')); function loadMap() { fetch('/api/heatmap/') .then(response => response.json()) .then(json => { mapChart.setOption({ geo: { map: 'china', roam: true, itemStyle: { areaColor: '#0f1b35', borderColor: '#3f6fd1' } }, series: [{ type: 'scatter', coordinateSystem: 'geo', data: json.data.map(d => [d.lng, d.lat, d.value]), symbolSize: function (val) { return Math.min(30, val / 100); }, itemStyle: { color: '#ffd24d', shadowBlur: 10, shadowColor: '#ffd24d' } }] }); }); }

如果你用的城市名字分散在全国各地,需要确保后端返回的 map 数据里包含name, value字段,ECharts map 组件会自动匹配 geo 坐标。

整个大屏做下来最花时间的并不是图表配置,而是数据格式的对接。Django 返回 JSON 的字段名,JavaScript 里怎么取,必须在写接口前就定清楚。我建议在后端直接统一返回{"data": [...]}这样的结构,前端 JS 只用取一次json.data,后面不管图表多复杂都不会乱。

5. 常见问题与踩坑记录

5.1 Hadoop 和 Spark 环境里最容易翻的五个车

  • Namenode 格式化后重复启动报错:不要反复执行hdfs namenode -format。每次格式化会生成新的 clusterId,和 DataNode 不一致时,DataNode 就起不来。正确做法是第一次格式化后,如果后续要重新开始,删除 Hadoop 临时目录/usr/local/hadoop/tmp再格式化,或者直接改 hdfs-site.xml 里的dfs.namenode.name.dir和dfs.datanode.data.dir。
  • Spark 内存不足:如果你的数据量稍微大一点,默认 1G 内存很容易 OOM。启动时加参数:
spark-submit --driver-memory 2g --executor-memory 2g your_script.py

如果只是本地调试,直接在 SparkSession 里配置:

spark = SparkSession.builder \ .appName("DidiOrderAnalysis") \ .master("local[*]") \ .config("spark.driver.memory", "2g") \ .getOrCreate()
  • Hadoop Web UI 访问不了 50070/9870:先检查防火墙,再确认core-site.xml里fs.defaultFS用的是 localhost 还是局域网 IP。考试机器上如果端口没起来,八成是 ssh 免密配置有问题,用ssh localhost测一下。
  • HDFS 文件名乱码或中文路径问题:HDFS 支持中文路径,但很多终端编码不统一导致显示乱码,建议所有目录名用英文,别拿中文给文件起名。
  • 如果做 Hadoop 高可用与 ZooKeeper 整合:最佳实践是三台节点都装 ZooKeeper,先启动 ZK,再启动 JournalNode,然后格式化 NameNode。很多教程让你只配一台 ZK,启动后 ZK 单点无意义,还容易让你误以为 HA 成功。课程设计不做 HA 就不用碰这些,避免浪费时间。

5.2 Django 和前端静态资源显示不出来的终极解决方案

很多人在 VSCode 里写 HTML 图片标签时,明明放了图片,Django 就是显示不了。先说原因:Django 开发环境对静态文件有特殊处理,不能直接<img src="/static/img/logo.png">,必须用{% static %}模板标签。

在settings.py里配置:

STATIC_URL = '/static/' STATICFILES_DIRS = [ BASE_DIR / 'static', ]

然后在模板里:

{% load static %} <img src="{% static 'img/logo.png' %}" alt="logo">

还需要确认static目录下有img子目录。调试时可以先打开浏览器的 Network 面板,看图片请求返回的是 404 还是 200。404 一般就是路径问题,200 但图片显示不了,可能是缓存,Ctrl+F5 强制刷新即可。

另外,Django 的 Debug 模式下,如果 URL 路由写成了path('', views.index),模板能正常加载;但如果你用了django.contrib.staticfiles还需要在urls.py最后加:

from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])

5.3 统计指标对不上或查询极慢怎么办

如果你发现大屏上的折线图数据和自己用 Excel 算出来的不一样,先从源头排查。我的习惯是:Spark 每做一步清洗,都用count()打印一次行数,聚合结果先show(10)查看,再写入 MySQL。宁可多打几次日志,也不要等到大屏挂了再回来找。

查询慢的问题,大多是 Django ORM 对统计表查询时没用索引。比如OrderHourStat按小时查询,给字段加索引很有效:

class OrderHourStat(models.Model): hour_of_day = models.IntegerField(db_index=True, verbose_name="小时")

如果是几十万行数据,走 SQL 直接EXPLAIN看看扫描行数,通常加一个普通索引就够了。

还有一个小问题:Spark 的时间戳和 Django 的时区不一致,容易导致按小时统计偏 8 小时。解决方法是清洗时统一设置时区:

spark.conf.set("spark.sql.session.timeZone", "Asia/Shanghai")

别看这个配置不起眼,我一开始没设置,统计出来的白天高峰期全变成了凌晨,最后核对半天才发现是 UTC 和北京时间差了一个时区。

最后说一个我自己的体会:这种大而全的项目,最忌讳一上来就想要“完美架构”。很多人卡在 Hadoop 环境三四个星期,还没跑到 Django,心态就崩了。我的建议是先把后端用本地 CSV 跑通,再用 Flask 或者 Django 做一版单机演示,最后才接 HDFS 和 Spark。环境装上之后,数据流越顺,你的逻辑才越清楚。等这套流程跑完,你会发现“Hadoop 负责存、Spark 负责算、Django 负责给数据、大屏负责好看”这句话,比任何教程都刻在脑子里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询