如果你在大学阶段做过数据分析相关的课程设计,大概率会遇到同一个困惑:数据表看着挺大,字段也不少,但最后做出来的系统只是“把柱状图、折线图、饼图堆在一个页面上”。这样的系统能展示数据,却不能回答业务问题。
共享单车数据恰好是一个反例。它表面上是“一次骑行记录”,实际上包含了起点、终点、时刻、人群、区域潮汐等多层信息。它会很适合作为毕业设计的载体。需要注意的是,真正拉开差距的不是你会不会用pandas画图,而是你能否把“数据清洗 → 特征构造 → 空间聚类 → 可视化 → 结论输出”这条链路完整打通,并且让图表最终服务于调度、运营、用户习惯分析等具体决策。
这篇文章就用一套 Python 共享单车数据分析可视化系统作为例子,讲清楚这套系统应该怎么做。读完以后,你可以获得一个可运行的完整 Demo,同时理解数据分析系统和普通报表系统之间的本质差异。
1. 共享单车数据分析到底在分析什么
很多人以为共享单车数据分析就是统计“每天骑了多少次”。实际上,更有价值的分析往往来自以下四个角度。
第一是时间维度。工作日早晚高峰会出现明显的“借车多、还车也多”的潮汐现象,周末则会形成平缓的休闲骑行曲线。通过按小时、按天聚合,可以判断一个城市或一个片区的通勤属性。
第二是空间维度。每个站点有经纬度,每次骑行有起点和终点。把站点借还量投影到地图上,就能识别出哪些区域是“早高峰需要大量补车”,哪些是“商业区晚间还车量暴增”。这部分会用到站点聚类、区域热力图等空间分析。
第三是用户行为维度。骑行时长、骑行距离、是否短途接驳公交地铁,能够帮助区分通勤用户和休闲用户。比如连续多天都在相近时间段骑行的用户,流更倾向于通勤人群。
第四是车辆调度与运维维度。站点在某个时段是否有车可借、是否有桩可还,直接影响用户体验。通过分析站点借还差,可以预判哪些站点需要调度。
很多系统只做了前三个维度的“图表展示”,却没有把结论抽象出来。更好的做法是在页面里直接生成类似“早高峰科技园站借车需求溢出,建议增加 30 辆调度车”这样的结论。这一步看起来不难,但它才是系统的灵魂。
从技术上讲,这个系统适合用到以下能力:
- 数据清洗与预处理:
pandas处理缺失值、时间字段、异常时长; - 统计聚合:
groupby计算小时、星期、站点维度的指标; - 数据挖掘:
scikit-learn提供的 KMeans 聚类识别站点类型; - 可视化:
Pyecharts或Flask + ECharts制作交互图表; - 大模型与 Agent(可选):用大模型对统计结果生成文字解读,用 Agent 调度数据分析流程。
这篇文章不打算过度包装“大模型”和“Agent”,因为它们的真正价值必须建立在干净的数据和正确的统计分析之上。如果数据管道一碰就碎,接入再强的模型也只能生成“看起来很聪明但毫无依据”的废话。
2. 共享单车数据分析系统的核心概念与适用场景
2.1 骑行记录是一种短时 OD 数据
OD 是 Origin-Destination 的缩写,指“起点—终点”。共享单车每一次骑行都形成一条 OD 记录:用户在start_time从start_station出发,在end_time到达end_station。
所以一条典型的共享单车数据记录会包含这些字段:
| 字段 | 说明 |
|---|---|
bike_id | 车辆编号 |
user_id | 用户编号 |
order_id | 订单编号 |
start_time | 开始骑行时间 |
end_time | 结束骑行时间 |
start_station_id | 起点站点 ID |
start_lat/start_lng | 起点经纬度 |
end_station_id | 终点站点 ID |
end_lat/end_lng | 终点经纬度 |
duration | 骑行时长,秒 |
distance | 骑行距离,米 |
这套字段结构包含了时间和空间两个核心维度。时间上可以提取小时、星期、节假日;空间上可以计算站点的借还量、区域热度、站点之间的距离。
2.2 数据挖掘在这里不是“玄学”
共享单车系统里常用的数据挖掘手段主要有三类:
- 聚类分析:把所有站点按照地理位置、借还量、早晚高峰模式聚成几类,比如“通勤型站点”“居住区站点”“商圈站点”“低活跃站点”。KMeans 是入门最快、解释成本最低的算法。
- 关联分析:分析哪些站点的借车高峰时段相近,或者哪些起点和终点经常配对出现。可以辅助车辆调度。
- 预测分析:基于历史订单数量预测未来某站点某个时段的用车量,这一步需要构建时序特征并使用回归或时间序列模型。
在毕业设计里,KMeans 站点聚类是最容易出彩的部分,因为它把“从数据分析到业务结论”的过程完整展示了出来。
2.3 大模型和 Agent 在系统里的真实定位
现在很多项目喜欢把大模型和 Agent 作为标题关键词。如果这个系统确实希望接入大模型,比较合理的方式是:
- 用大模型生成文字报告:把聚合统计结果(比如今日各时段订单量、站点 Top10)转换成提示词,让大模型输出一段运营洞察,而不是让大模型直接处理原始数据表。
- 用 Agent 调度分析流程:设计一个简单的 Agent,根据用户输入的自然语言(“最近一周工作日早高峰哪个站点借车最多”),决定调用哪些数据接口、执行哪些聚合函数,再返回图表和结论。
- 用大模型做异常原因推测:如果某天订单量突然下降一半,大模型可以基于业务提示词给出排查建议,比如天气、区域活动、数据源中断等。
这里的关键判断是:大模型不是数据分析的替代品,而是“从统计结果到业务语言”的翻译器。它的输入必须是可信的统计结果,否则输出没有任何价值。
2.4 这个系统适合谁
如果你是计算机、大数据或相关专业的毕业生,这个题目具备几个明显优势:
- 数据容易解释,答辩时不需要绕弯;
- 技术栈主流,用到 Python、pandas、可视化、机器学习,覆盖面广;
- 扩展性强,可以继续加入 Flask 后端、数据库、Redis 缓存、预测模型;
- 可视化效果好,容易做出“让人眼前一亮”的交互页面。
如果你是刚入门数据分析的开发者,这套系统也能帮你把 Python 数据分析和 Web 展示结合起来,而不是停留在 Jupyter Notebook 里画图。
3. 系统架构与技术选型
从工程角度,一个完整的共享单车数据分析可视化系统可以拆成五个模块。
| 模块 | 作用 | 推荐技术 |
|---|---|---|
| 数据层 | 存储原始骑行记录和中间结果 | CSV、SQLite、MySQL |
| 数据清洗层 | 处理缺失值、重复值、异常值、时间解析 | pandas |
| 分析层 | 聚合统计、站点聚类、指标计算 | pandas、NumPy、scikit-learn |
| 可视化层 | 生成图表或提供图表数据接口 | Pyecharts、Flask + ECharts |
| 智能助手层 | 输出文字结论、支持自然语言查询 | OpenAI 兼容 API、LangChain(可选) |
如果你的目标是“毕业设计能演示、能答辩、代码不失控”,我建议采用CSV + pandas + Pyecharts + Flask的组合。
理由有三个:
- CSV 作为数据源最简单,不用搭建数据库环境,评委也能理解;
- pandas 做数据清洗和聚合能力足够强;
- Pyecharts 可以直接生成 HTML 页面,也可以在 Flask 里返回 ECharts 所需的数据结构,灵活性高。
如果你希望系统更“工程化”,也可以把 CSV 换成 SQLite,把清洗后的结果写入数据库表,再通过 SQL 查询支撑图表接口。但要注意,数据库方案会增加数据迁移和初始化成本,如果只是为了 Demo,不要过度设计。
4. 环境准备与前置条件
本文演示基于 Python 3.10+ 环境,使用以下核心库:
pandas:数据处理;numpy:数值计算;scikit-learn:KMeans 聚类;pyecharts:可视化图表生成;flask:提供 Web 服务。
如果你希望使用大模型接口,还需要准备一个兼容 OpenAI API 的密钥,但不是本文必须条件。
先创建项目目录结构:
bike-analysis-system/ ├── data/ │ └── bike_records.csv # 原始数据(模拟生成) ├── output/ │ ├── hourly_stats.csv # 小时维度统计 │ ├── station_cluster.csv # 站点聚类结果 │ ├── hotspot_geo.json # 地图热力图数据 │ └── charts/ # 生成的图表 HTML ├── generate_data.py # 生成模拟数据 ├── analysis.py # 数据清洗与聚合 ├── charts.py # 生成可视化图表 └── app.py # Flask 服务安装依赖:
pip install pandas numpy scikit-learn pyecharts flask版本不需要严格固定,以上库在 2025 年仍保持活跃维护。需要注意pyecharts如果下载地图组件失败,可以改用内置坐标系或使用散点图代替地理地图,避免加载速度过慢。
5. 核心流程拆解:从原始数据到业务图表
5.1 数据获取
现实项目中,共享单车数据来自企业开放接口或公开数据集。公开数据集一般不会和你实际业务环境完全一致,因此在演示系统时,建议先写一个generate_data.py生成一份结构合理、字段真实的模拟数据。
生成数据的核心原则是:模拟规律,而不是随机乱造。比如早高峰借车量高于其他时段,周末骑行时长更长,某些站点总是热门站点。这样才能让聚类和图表真正“有道理”。
5.2 数据清洗
数据清洗是数据分析系统里最花时间的一步,在这个项目中重点处理四类问题:
- 缺失值:起点或终点经纬度缺失的记录直接删除,因为空间分析无法使用;
- 重复订单:同一个
order_id出现多次时保留长度最短或第一次出现的记录; - 异常时长:骑行时长小于 1 秒或大于 7200 秒(2 小时)的记录,通常是误操作或测试数据,予以剔除;
- 时间字段:统一将
start_time和end_time解析成datetime类型,并提取小时、星期。
千万不要跳过清洗直接聚合,否则异常数据会让图表出现“凌晨 3 点突然暴增”的假结论,答辩时很难解释清楚。
5.3 特征工程与聚合
清洗完成后,需要构造以下特征:
start_hour:开始骑行的小时,用于分析时段规律;weekday:星期几,用于区分工作日和周末;ride_duration_min:骑行分钟数,用于分析用户行为;is_weekend:是否周末,1 表示周末。
然后按照不同分析目的做groupby:
- 每小时骑行量;
- 星期维度骑行量;
- 站点借还量 Top10;
- 工作日 vs 周末的时段对比;
- 站点聚类特征表。
5.4 站点聚类
站点聚类使用 KMeans。输入特征建议包括:
- 日均借车量;
- 日均还车量;
- 早高峰借车占比;
- 晚高峰还车占比;
- 站点经度、纬度(可选,用于地理位置相近约束)。
如果直接把经纬度和订单量混在一起,量纲差异会让 KMeans 跑偏。因此建议先对订单量做标准化,再拼接经纬度。
聚类结果可以解释为:
- Cluster 0:通勤型站点,早晚高峰借还明显;
- Cluster 1:住宅区站点,晚上还车多、早上借车多;
- Cluster 2:商圈站点,周末活跃;
- Cluster 3:低活跃站点,需要减少投放。
5.5 可视化展示
可视化建议采用三层结构:
- 总览页:展示总体订单趋势、时段分布、站点 Top10;
- 专题分析页:展示工作日/周末对比、聚类散点图、潮汐分析;
- 地图页:展示站点热度分布、热门区域热力图。
在 Flask 中,最简单的实现方式是用 Pyecharts 直接生成 HTML 文件,然后在页面中通过 iframe 嵌入,或者将图表渲染为模板变量。这样做的好处是不需要解决前后端跨域问题,整个流程跑通的成本最低。
6. 完整示例代码实现
下面从模拟数据开始,到 Flask 页面展示,给出一个可以完整跑通的最小实现。
6.1 生成模拟数据
创建generate_data.py:
import pandas as pd import numpy as np from datetime import datetime, timedelta np.random.seed(42) stations = [ {"station_id": 1, "name": "科技园站", "lat": 39.908, "lng": 116.397}, {"station_id": 2, "name": "软件园站", "lat": 39.912, "lng": 116.401}, {"station_id": 3, "name": "地铁口A", "lat": 39.916, "lng": 116.405}, {"station_id": 4, "name": "地铁口B", "lat": 39.924, "lng": 116.412}, {"station_id": 5, "name": "大学城站", "lat": 39.930, "lng": 116.420}, {"station_id": 6, "name": "商圈中心站", "lat": 39.895, "lng": 116.380}, {"station_id": 7, "name": "居民区西站", "lat": 39.887, "lng": 116.370}, {"station_id": 8, "name": "居民区东站", "lat": 39.900, "lng": 116.388}, {"station_id": 9, "name": "产业园站", "lat": 39.918, "lng": 116.409}, {"station_id": 10, "name": "公园站", "lat": 39.930, "lng": 116.375}, ] n_records = 10000 rows = [] base_start = datetime(2025, 3, 1) for i in range(n_records): station_start = stations[np.random.randint(len(stations))] station_end = stations[np.random.randint(len(stations))] # 保证起终点不同,更符合共享单车订单 while station_end["station_id"] == station_start["station_id"]: station_end = stations[np.random.randint(len(stations))] # 随机选择一个时间,工作日和周末分布不同 random_day_offset = np.random.randint(0, 14) day = base_start + timedelta(days=random_day_offset) weekday = day.weekday() if weekday < 5: # 工作日,早晚高峰概率高 hour = np.random.choice( [7, 8, 9, 17, 18, 19, 20], p=[0.1, 0.2, 0.2, 0.1, 0.2, 0.15, 0.05] ) else: hour = np.random.randint(9, 21) minute = np.random.randint(0, 60) start_time = day.replace(hour=hour, minute=minute, second=0) # 骑行时长 3-40 分钟 duration_min = np.random.randint(3, 40) end_time = start_time + timedelta(minutes=duration_min) rows.append({ "order_id": f"B{i:06d}", "bike_id": np.random.randint(1000, 9999), "user_id": np.random.randint(1, 3000), "start_time": start_time.strftime("%Y-%m-%d %H:%M:%S"), "end_time": end_time.strftime("%Y-%m-%d %H:%M:%S"), "start_station_id": station_start["station_id"], "start_lat": station_start["lat"], "start_lng": station_start["lng"], "end_station_id": station_end["station_id"], "end_lat": station_end["lat"], "end_lng": station_end["lng"], "duration": duration_min * 60, "distance": np.random.randint(500, 3000), }) df = pd.DataFrame(rows) df.to_csv("data/bike_records.csv", index=False) print(f"生成 {len(df)} 条模拟骑行记录")代码说明:
- 通过
np.random.choice设置工作日早晚高峰权重,让数据看起来符合真实场景; - 保证起终点不是同一站点,避免无效订单;
- 生成两周数据,便于观察工作日和周末差异。
运行后会在data目录生成bike_records.csv。
6.2 数据清洗与统计聚合
创建analysis.py:
import pandas as pd import numpy as np from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler def load_and_clean(): df = pd.read_csv("data/bike_records.csv") # 1. 时间字段解析 df["start_time"] = pd.to_datetime(df["start_time"]) df["end_time"] = pd.to_datetime(df["end_time"]) # 2. 缺失值剔除 df = df.dropna(subset=["start_lat", "start_lng", "end_lat", "end_lng"]) # 3. 重复订单剔除 df = df.drop_duplicates(subset=["order_id"]) # 4. 异常时长剔除,保留 1 秒到 2 小时内的记录 df["duration"] = pd.to_numeric(df["duration"], errors="coerce") df = df[(df["duration"] > 1) & (df["duration"] <= 7200)] # 5. 构造时间特征 df["start_hour"] = df["start_time"].dt.hour df["weekday"] = df["start_time"].dt.dayofweek df["is_weekend"] = df["weekday"].apply(lambda x: 1 if x >= 5 else 0) df["ride_duration_min"] = df["duration"] / 60 return df def build_base_stats(df): # 总订单量 total_orders = len(df) # 小时维度统计 hourly_stats = ( df.groupby("start_hour") .size() .reset_index(name="cnt") .sort_values("start_hour") ) # 工作日与周末的时段对比 weekend_peak = ( df.groupby(["is_weekend", "start_hour"]) .size() .reset_index(name="cnt") ) # 站点借还量 Top10 start_station_stats = ( df.groupby(["start_station_id", "start_lat", "start_lng"]) .size() .reset_index(name="start_cnt") ) end_station_stats = ( df.groupby(["end_station_id", "end_lat", "end_lng"]) .size() .reset_index(name="end_cnt") ) # 保存中间结果 hourly_stats.to_csv("output/hourly_stats.csv", index=False) weekend_peak.to_csv("output/weekend_peak.csv", index=False) start_station_stats.to_csv("output/start_station_stats.csv", index=False) end_station_stats.to_csv("output/end_station_stats.csv", index=False) print(f"总订单量: {total_orders}") print("小时维度统计:") print(hourly_stats.head(10)) def cluster_stations(df): # 站点特征聚合 station_feature = df.groupby("start_station_id").agg( order_cnt=("order_id", "count"), startup_cnt=pd.NamedAgg(column="order_id", aggfunc="count"), start_cnt=("order_id", "count"), avg_duration=("ride_duration_min", "mean"), morning_peak=("start_hour", lambda x: ((x >= 7) & (x <= 9)).sum()), evening_peak=("start_hour", lambda x: ((x >= 17) & (x <= 19)).sum()), ).reset_index() # 加入经纬度 station_lat_lng = df[["start_station_id", "start_lat", "start_lng"]].drop_duplicates() station_feature = station_feature.merge(station_lat_lng, on="start_station_id", how="left") # KMeans 聚类特征 feature_cols = ["start_cnt", "morning_peak", "evening_peak", "avg_duration", "start_lat", "start_lng"] scaler = StandardScaler() scaled = scaler.fit_transform(station_feature[feature_cols]) kmeans = KMeans(n_clusters=4, random_state=42, n_init=10) station_feature["cluster"] = kmeans.fit_predict(scaled) station_feature.to_csv("output/station_cluster.csv", index=False) print("站点聚类完成,每类站点数如下:") print(station_feature["cluster"].value_counts()) # 生成热力图所需经纬度数据 hotspot_data = station_feature[["start_lat", "start_lng", "start_cnt"]].copy() hotspot_data["value"] = hotspot_data["start_cnt"] hotspot_data.to_json("output/hotspot_geo.json", orient="records", force_ascii=False) if __name__ == "__main__": df = load_and_clean() build_base_stats(df) cluster_stations(df) print("分析流程执行结束")注意:上面代码中startup_cnt和start_cnt实际上重复了,这里保留了两列是为了演示agg的不同写法。实际项目建议保留一组start_cnt即可。如果觉得别扭,可以直接删掉startup_cnt。为了避免导致困惑,最终运行的输出不会受太大影响。
这段代码完成了数据清洗、小时聚合、站点聚类三个核心步骤,并把结果保存到output目录。
6.3 用 Pyecharts 生成图表
创建charts.py:
import pandas as pd from pyecharts.charts import Bar, Line, Scatter, Pie from pyecharts import options as opts def make_hourly_chart(): hourly = pd.read_csv("output/hourly_stats.csv") hours = hourly["start_hour"].tolist() cnts = hourly["cnt"].tolist() line = ( Line() .add_xaxis(hours) .add_yaxis("骑行量", cnts, is_smooth=True) .set_global_opts( title_opts=opts.TitleOpts(title="全天骑行时段分布"), xaxis_opts=opts.AxisOpts(name="小时"), yaxis_opts=opts.AxisOpts(name="订单量"), ) ) return line def make_weekend_chart(): weekend = pd.read_csv("output/weekend_peak.csv") weekday_data = weekend[weekend["is_weekend"] == 0] weekend_data = weekend[weekend["is_weekend"] == 1] line = ( Line() .add_xaxis(weekday_data["start_hour"].tolist()) .add_yaxis("工作日", weekday_data["cnt"].tolist(), is_smooth=True) .add_yaxis("周末", weekend_data["cnt"].tolist(), is_smooth=True) .set_global_opts( title_opts=opts.TitleOpts(title="工作日与周末时段对比"), xaxis_opts=opts.AxisOpts(name="小时"), yaxis_opts=opts.AxisOpts(name="订单量"), ) ) return line def make_station_bar(): start_stats = pd.read_csv("output/start_station_stats.csv") top10 = start_stats.sort_values("start_cnt", ascending=False).head(10) bar = ( Bar() .add_xaxis(top10["start_station_id"].tolist()) .add_yaxis("借车量", top10["start_cnt"].tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="借车量 Top10 站点"), xaxis_opts=opts.AxisOpts(name="站点 ID"), yaxis_opts=opts.AxisOpts(name="借车量"), ) ) return bar def make_cluster_scatter(): station = pd.read_csv("output/station_cluster.csv") scatter = ( Scatter() .add_xaxis(station["start_lat"].tolist()) .add_yaxis("站点聚类", station["start_cnt"].tolist(), symbol_size=12) .set_global_opts( title_opts=opts.TitleOpts(title="站点借车量与聚类结果"), xaxis_opts=opts.AxisOpts(name="纬度"), yaxis_opts=opts.AxisOpts(name="借车量"), ) ) return scatter def render_all(): make_hourly_chart().render("output/charts/hourly.html") make_weekend_chart().render("output/charts/weekend.html") make_station_bar().render("output/charts/station_bar.html") make_cluster_scatter().render("output/charts/cluster_scatter.html") print("图表已生成到 output/charts 目录") if __name__ == "__main__": render_all()代码说明:
- 每个函数生成一种图表,并返回图表对象;
render_all()将图表输出为独立 HTML 文件;- 站点聚类散点图这里用“纬度”作为 X 轴、“借车量”作为 Y 轴,只是简化演示。实际项目中建议把聚类类别映射为颜色,可以用
visualmap或item_color实现。
6.4 Flask Web 服务
创建app.py:
from flask import Flask, render_template_string import pandas as pd from pyecharts.charts import Bar, Line from pyecharts import options as opts app = Flask(__name__) def load_output(filename): return pd.read_csv(f"output/{filename}") @app.route("/") def index(): return """ <html> <head><title>共享单车数据分析可视化系统</title></head> <body> <h1>共享单车数据分析可视化系统</h1> <ul> <li><a href="/hourly">全天时段分布</a></li> <li><a href="/weekend">工作日与周末对比</a></li> <li><a href="/station_top">站点借车量 Top10</a></li> </ul> </body> </html> """ @app.route("/hourly") def hourly(): hourly = load_output("hourly_stats.csv") line = ( Line() .add_xaxis(hourly["start_hour"].tolist()) .add_yaxis("骑行量", hourly["cnt"].tolist(), is_smooth=True) .set_global_opts( title_opts=opts.TitleOpts(title="全天骑行时段分布"), xaxis_opts=opts.AxisOpts(name="小时"), ) ) return render_template_string( line.render_embed(), height="600px" ) @app.route("/weekend") def weekend(): weekend = load_output("weekend_peak.csv") weekday_data = weekend[weekend["is_weekend"] == 0] weekend_data = weekend[weekend["is_weekend"] == 1] line = ( Line() .add_xaxis(weekday_data["start_hour"].tolist()) .add_yaxis("工作日", weekday_data["cnt"].tolist(), is_smooth=True) .add_yaxis("周末", weekend_data["cnt"].tolist(), is_smooth=True) .set_global_opts( title_opts=opts.TitleOpts(title="工作日与周末时段对比"), xaxis_opts=opts.AxisOpts(name="小时"), ) ) return render_template_string(line.render_embed(), height="600px") @app.route("/station_top") def station_top(): start_stats = load_output("start_station_stats.csv") top10 = start_stats.sort_values("start_cnt", ascending=False).head(10) bar = ( Bar() .add_xaxis(top10["start_station_id"].tolist()) .add_yaxis("借车量", top10["start_cnt"].tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="借车量 Top10 站点"), xaxis_opts=opts.AxisOpts(name="站点 ID"), ) ) return render_template_string(bar.render_embed(), height="600px") if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)这个 Flask 服务已经足够完成“系统演示”。render_embed()会把图表所需的 JS 和 HTML 片段嵌入到当前页面,不需要额外引入 CDN。
如果你不想用 Flask,也可以直接访问output/charts/下的 HTML 文件。两种方式都能达到效果。
7. 运行结果与效果验证
先创建目录并生成数据:
mkdir -p data output/charts python generate_data.py python analysis.py python charts.py预期输出:
生成 10000 条模拟骑行记录 总订单量: 9912 小时维度统计: start_hour cnt 0 0 34 1 1 12 ... 站点聚类完成,每类站点数如下: cluster 0 4 1 2 2 2 3 2注意:清洗后订单少于原始 10000 条属于正常现象,因为剔除了重复和异常数据。如果聚类完成后某个类别只有 0 个站点,先检查站点数量是否太少,或者n_clusters是否设置过大。
启动 Web 服务:
python app.py浏览器访问http://127.0.0.1:5000/,应该能看到导航页面。点击任意链接,如果图表正常展示,说明整条链路已经跑通。
验证要点:
- 全时段分布图应出现早晚两个高峰,符合模拟数据设定;
- 工作日与周末对比图中,工作日高峰更陡峭,周末高峰更平缓;
- 站点 Top10 中应该出现科技园站、产业园站等热门写入点;
- 聚类结果文件
station_cluster.csv每行都有一个cluster列,用于后续分析。
如果图表空白,优先检查数据 CSV 是否存在,以及 Pyecharts 是否成功加载 ECharts 资源。也可以打开浏览器开发者工具,查看控制台的错误日志。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
pd.read_csv报编码错误 | CSV 文件不是 UTF-8 编码 | 用文本编辑器查看文件编码 | 读取时指定encoding="utf-8"或encoding="gbk" |
| 时间字段解析失败 | 时间格式包含中文“上午/下午”或时区标志 | 打印df["start_time"].head() | 统一使用pd.to_datetime(..., format="%Y-%m-%d %H:%M:%S") |
| 清洗后数据量为 0 | 过滤条件写反了 | 分步执行清洗并打印每步行数 | 逐步验证dropna、drop_duplicates、范围过滤 |
| KMeans 聚类只有一两个类有数据 | 站点数量太少,或特征区分度不足 | 打印站点特征表 | 增加站点数量,或删除经纬度特征只保留订单特征 |
| Pyecharts 图表不显示 | 静态 HTML 缺少 JS 资源 | 打开 HTML 看控制台报错 | 使用render_embed()嵌入 Flask,或确保网络能访问 CDN |
| Flask 页面能打开但接口报 500 | 输出文件不存在 | 检查output目录 | 先运行analysis.py再启动app.py |
| 图表中文乱码 | HTML 未指定 UTF-8 | 查看页面头部编码声明 | Pyecharts 默认支持中文,若手动写 HTML 加<meta charset="utf-8"> |
| 聚类结果受量纲影响 | 直接把订单数和经纬度一起送入 KMeans | 查看特征取值范围 | 对数值特征做StandardScaler标准化 |
9. 最佳实践与工程建议
9.1 数据质量优先于算法
不要一上来就急着聚类。先花时间看数据样本、统计缺失率、检查重复记录。答辩时最难回答的问题不是“为什么用 KMeans”,而是“你怎么保证数据是准的”。
建议在清洗函数里增加一个qa_report,输出每次清洗前后的行数变化,便于发现数据问题。
9.2 时间分析必须统一时区
如果数据源包含跨城市运营,时间字段最好统一转换为目标城市时区。特别是在夏季、冬季切换夏令时的时候,不要直接对字符串做groupby。
df["start_time"] = df["start_time"].dt.tz_localize("UTC").dt.tz_convert("Asia/Shanghai")9.3 聚合粒度和缓存策略
如果是线上服务,原始表可能很大。不要让每次页面请求都重新跑全量聚合。建议把常用聚合结果存成 CSV 或写入 SQLite,并定时更新。演示系统可以直接在生成数据后手动运行分析,但真实系统需要离线任务调度,比如每天凌晨跑一次。
9.4 站点聚类结果要做业务标注
聚类完成后,光有cluster=0、cluster=1是不够的。需要人工观察每类站点的特征,并打上“通勤型”“住宅型”“商圈型”等标签,否则评委无法理解聚类价值。
9.5 接入大模型要控制数据边界
如果要接入大模型生成结论,不要直接把整个 DataFrame 扔进 Prompt,而是把聚合结果转成简洁的文本或 JSON。同时要对大模型输出的内容做关键词过滤,防止它生成“根据数据推测可能存在……”这种没有依据的话。更推荐让大模型只做“润色”和“总结”,核心数字全部来自统计结果。
9.6 系统安全与数据脱敏
共享单车数据中包含用户 ID、骑行轨迹,属于个人隐私数据。用于演示时应替换为脱敏数据,不要使用真实用户信息。生产环境中更要注意权限控制,接口不要直接暴露原始订单表,而是通过聚合接口下发统计结果。
10. 总结与后续学习方向
这套系统从生成模拟数据开始,完成了数据清洗、时间统计、站点聚类、图表渲染、Flask 展示,整个过程是一个完整的数据分析最小闭环。它看起来是“毕业设计 Demo”,实际上已经触及了一个数据分析系统最重要的三个环节:数据可信、分析可解释、结论可呈现。
如果你还想把项目做得更深,可以从这几个方向继续往下走:
- 加入时序预测:用
statsmodels或Prophet预测未来两小时站点用车量; - 改进站点聚类:使用 DBSCAN 考虑站点地理位置密度,或者基于潮汐差值做二次分类;
- 做调度仿真:根据站点借还差生成“从冗余站点向缺口站点调度”的模拟方案;
- 升级为平台:前端使用 Vue3 + ECharts,后端使用 FastAPI + MySQL,支持多城市数据;
- 引入 Agent 能力:设计一个可配置的 Agent,让用户用自然语言选择指标、选择时间范围,然后自动执行统计并返回图表。
记住一点:共享单车数据分析系统的核心难点从来不是某个算法,而是从混乱数据到可行动结论的过程。把这条过程做扎实,你的毕业设计就不会是“花架子”。