这次我们不聊模型,不聊框架,而是聊一个更现实的数据分析问题:月入2.5万的网约车司机,到底有多辛苦?
与其靠感觉和情绪,不如直接拿数据说话。我整理了一套用 Python 分析网约车司机流水数据的思路,从订单流水清洗、成本扣减、有效工时统计,到净收入、时薪和疲劳指数计算,全流程用脚本跑通。这样可以客观拆解“月入2.5万”需要跑多少单、在线多少小时、承担多少成本,以及强度到底处在什么水平。
这篇文章适合三类读者:一是想入行网约车、需要理性评估收入的朋友;二是手头有司机流水数据、想做数据分析或写报告的技术人;三是想练手 pandas 数据清洗和可视化的人。整个项目不需要 GPU,普通电脑就能跑,环境简单,重点在数据思维和分析方法。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 网约车司机收入与疲劳度数据分析脚本 |
| 输入数据 | 订单流水 CSV,包含订单金额、平台抽成、里程、时长、起止时间、油电费等字段 |
| 核心功能 | 计算净收入、有效工时、时薪、高峰期订单占比、长途订单占比、疲劳指数 |
| 运行环境 | Python 3.9+,Windows / macOS / Linux 均可 |
| 主要依赖 | pandas、matplotlib、flask(可选) |
| 启动方式 | 命令行执行脚本 |
| 是否支持 API | 可扩展为 Flask 接口,供其他工具调用 |
| 是否支持批量任务 | 支持批量处理多个月份的流水文件 |
| 硬件要求 | CPU 即可,内存 4G 以上,无需 GPU |
| 适合场景 | 司机收入分析、出行平台数据研究、副业评估、数据分析教学 |
这里要强调一点:文中的示例数据是模拟生成的,目的是验证分析流程。真实场景中请使用司机自己的授权流水数据,并严格保护个人隐私。
2. 适用场景与使用边界
这个分析方案能解决一个核心问题:把“辛苦”变成可量化的指标。
比如,月流水 3 万,平台抽成 25%,油电成本 3000 元,违章和保养折损 1000 元,那么净收入大约是 18500 元。如果在线 400 小时,有效接单时间 220 小时,时薪只有 84 元左右。再叠加连续驾驶时间和深夜订单比例,就能看出这份工作真实的体力消耗程度。
但也要清楚这个方案的边界:
- 它不适合分析平台规则、派单算法这类黑盒问题。我们不讨论平台抽成是否合理,只算司机到手收入。
- 它不能精确计算所有成本,比如车辆折旧、保险、维修因为车型不同差异很大,只能根据输入数据估算。
- 它需要相对干净的流水记录,如果司机只用语音接单、没有导出数据,需要手动整理。
- 数据隐私需要格外注意。订单流水可能包含乘客上下车位置、手机号脱敏信息等,分析时要注意脱敏,分析完成后及时删除或加密保存。
如果涉及劳动关系、平台责任等法律问题,请咨询专业律师,本方案只提供技术分析手段。
3. 环境准备与前置条件
整个项目只需要 Python 环境和三个常用库。推荐使用 Python 3.9 以上版本,太老版本对 pandas 和 matplotlib 的兼容性不好。
3.1 安装 Python
如果电脑上还没有 Python,可以从官网下载安装,安装时记得勾选“Add Python to PATH”。装好后在命令行确认:
python --version能看到版本号说明环境正常。
3.2 创建隔离环境(推荐)
不要直接往系统环境里装一堆包,建议为这个分析项目建一个独立虚拟环境:
mkdir driver-income-analysis cd driver-income-analysis python -m venv venvWindows 激活:
venv\Scripts\activatemacOS / Linux 激活:
source venv/bin/activate激活后终端提示符前面会出现(venv),说明已经进入虚拟环境。
3.3 安装依赖
pip install pandas matplotlib flaskpandas:用于表格数据清洗和聚合。matplotlib:用于生成收入曲线、工时分布等图表。flask:用于扩展 API 服务,如果暂时不需要,可以先不安装。
安装完成后,可以快速检查依赖版本:
import pandas as pd import matplotlib print(pd.__version__) print(matplotlib.__version__)3.4 准备数据文件
在项目目录下创建一个data文件夹,用来存放流水 CSV。建议的字段格式如下:
| 字段名 | 示例值 | 说明 |
|---|---|---|
| order_id | D20250501120001 | 订单编号 |
| start_time | 2025-05-01 08:30:00 | 接单开始时间 |
| end_time | 2025-05-01 09:10:00 | 订单结束时间 |
| order_amount | 86.50 | 乘客支付金额 |
| platform_fee | 20.30 | 平台抽成 / 信息服务费 |
| distance_km | 25.8 | 订单里程 |
| wait_minutes | 12 | 接驾等待+途中等待时间 |
| fuel_cost | 18.00 | 本单油费或电费(可按里程折算) |
| city | 上海 | 所在城市 |
如果你的流水数据字段名不一样,可以在后面的脚本里做列名映射,不必强求一致。
4. 数据准备与模拟数据生成
在没有真实数据之前,我们先写一个脚本生成模拟数据,用来验证分析逻辑。模拟数据按一个月生成,假设司机每天出车 10 小时,中途随机休息,共产生约 600 条订单。
4.1 生成模拟流水
新建generate_sample_data.py:
import csv import random from datetime import datetime, timedelta random.seed(42) output_path = "./data/sample_orders.csv" start_date = datetime(2025, 5, 1, 6, 0, 0) days = 30 hourly_density = 1.8 # 平均每小时订单数量 fields = [ "order_id", "start_time", "end_time", "order_amount", "platform_fee", "distance_km", "wait_minutes", "fuel_cost", "city" ] with open(output_path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() order_seq = 0 for day in range(days): current = start_date + timedelta(days=day, hours=0) # 模拟每天早6点到晚12点开工 day_start = current.replace(hour=6, minute=0, second=0) day_end = current.replace(hour=23, minute=59, second=59) cursor = day_start while cursor < day_end: # 随机生成订单间隔 interval = random.randint(15, 60) cursor += timedelta(minutes=interval) if cursor > day_end: break order_seq += 1 order_id = f"D{day+1:02d}{order_seq:06d}" # 订单金额集中在 15~120 元 order_amount = round(random.uniform(15, 120), 2) # 平台抽成约 20%~30% platform_fee = round(order_amount * random.uniform(0.2, 0.3), 2) # 里程 2~40 公里 distance_km = round(random.uniform(2, 40), 1) # 等待时间 0~20 分钟 wait_minutes = random.randint(0, 20) # 油费按每公里0.5~0.8元估算 fuel_cost = round(distance_km * random.uniform(0.5, 0.8), 2) end_time = cursor + timedelta(minutes=random.randint(15, 60)) writer.writerow({ "order_id": order_id, "start_time": cursor.strftime("%Y-%m-%d %H:%M:%S"), "end_time": end_time.strftime("%Y-%m-%d %H:%M:%S"), "order_amount": order_amount, "platform_fee": platform_fee, "distance_km": distance_km, "wait_minutes": wait_minutes, "fuel_cost": fuel_cost, "city": "上海" }) cursor = end_time # 随机休息 0~30 分钟 cursor += timedelta(minutes=random.randint(0, 30)) print(f"生成完成: {output_path}")这里的关键是生成一段看起来真实的数据,用来验证后续计算逻辑。运行方式:
python generate_sample_data.py运行后会生成data/sample_orders.csv。
4.2 认识数据格式
生成的模拟数据中,order_amount是乘客支付金额,platform_fee是平台抽成,实际到手流水应该是order_amount - platform_fee。fuel_cost是每单的能源成本,还没有算车辆折损和保险。
这个模拟数据刻意把订单集中在白天和晚上,没有生成凌晨 2 点到 5 点的订单,因为很多平台这时候单量少。如果你想分析熬夜强度,后续可以自己往数据里加凌晨订单。
5. 流水清洗与核心指标计算
拿到流水后,第一步是清洗。真实导出数据经常有缺失、重复、格式混乱问题,所以需要做这几件事:
- 解析时间列。
- 计算订单时长。
- 计算实际到手流水。
- 标记高峰时段。
- 按天聚合,统计在线时长、订单数、净收入等。
5.1 编写数据分析脚本
新建analyze_income.py:
import pandas as pd import numpy as np DATA_PATH = "./data/sample_orders.csv" # 读取数据 df = pd.read_csv(DATA_PATH, encoding="utf-8-sig") print("原始数据量:", len(df)) print("\n前5行数据:") print(df.head()) # 清洗:去掉重复订单 df = df.drop_duplicates(subset=["order_id"]) # 清洗:删除关键字段为空的记录 df = df.dropna(subset=["order_amount", "platform_fee", "start_time", "end_time"]) # 时间列转换 df["start_time"] = pd.to_datetime(df["start_time"]) df["end_time"] = pd.to_datetime(df["end_time"]) # 计算订单时长(小时) df["order_hours"] = (df["end_time"] - df["start_time"]).dt.total_seconds() / 3600 # 计算实际到手流水 df["real_income"] = df["order_amount"] - df["platform_fee"] # 标记高峰时段:7-9点 和 17-20点 df["hour"] = df["start_time"].dt.hour df["is_peak"] = df["hour"].isin([7, 8, 9, 17, 18, 19, 20]) # 标记深夜时段:23点-凌晨4点 df["is_late_night"] = df["hour"].isin([23, 0, 1, 2, 3, 4]) # 计算成本:油费/电费 + 车辆折损估算(按每公里0.2元) df["depreciation_cost"] = df["distance_km"] * 0.2 df["total_cost"] = df["fuel_cost"] + df["depreciation_cost"] # 净收入 = 实际到手流水 - 本单直接成本 df["net_income"] = df["real_income"] - df["total_cost"] print("\n清洗后数据量:", len(df)) print("\n字段统计:") print(df[["order_amount", "real_income", "net_income", "distance_km"]].describe())这个脚本会输出基础统计信息。这里用的是模拟数据,因此depreciation_cost只是一个估算值,真实使用时需要根据车型、购车价格和预期行驶里程调整。
5.2 按天聚合收入与工时
光看每单数据不够,需要按天汇总:
# 按天汇总 df["date"] = df["start_time"].dt.date day_stats = df.groupby("date").agg( total_order_amount=("order_amount", "sum"), total_platform_fee=("platform_fee", "sum"), total_real_income=("real_income", "sum"), total_net_income=("net_income", "sum"), total_distance=("distance_km", "sum"), order_count=("order_id", "count"), total_order_hours=("order_hours", "sum"), peak_order_count=("is_peak", "sum"), late_night_order_count=("is_late_night", "sum") ).reset_index() print("\n每日汇总:") print(day_stats.head(10))5.3 计算月均指标
接着计算月均每日收入、时薪、高峰订单占比等:
monthly_summary = { "总自然天数": day_stats["date"].nunique(), "总订单数": day_stats["order_count"].sum(), "月总流水(不含抽成)": round(day_stats["total_order_amount"].sum(), 2), "月平台抽成": round(day_stats["total_platform_fee"].sum(), 2), "月实际到手流水": round(day_stats["total_real_income"].sum(), 2), "月净收入(扣除直接成本)": round(day_stats["total_net_income"].sum(), 2), "月行驶总里程": round(day_stats["total_distance"].sum(), 1), "月有效订单时长": round(day_stats["total_order_hours"].sum(), 2), "每日平均净收入": round(day_stats["total_net_income"].sum() / day_stats["date"].nunique(), 2), "单均净收入": round(day_stats["total_net_income"].sum() / day_stats["order_count"].sum(), 2), } print("\n===== 月度汇总 =====") for k, v in monthly_summary.items(): print(f"{k}: {v}")如果模拟数据显示月净收入接近 25000 元,就可以继续看每日在线时长和疲劳指数,判断这个收入强度是否合理。
5.4 计算辛苦程度指标
辛苦程度不能只看收入,还要看时间投入和身体消耗。我们定义四个可量化指标:
| 指标 | 计算方式 | 含义 |
|---|---|---|
| 有效工时 | 所有订单时长之和 | 真正在开车服务乘客的时间 |
| 在线时长 | 从第一单到最后一单的时间跨度 | 司机“上班”的总时长,包含等单、休息 |
| 时薪 | 净收入 / 有效工时 | 每小时实际产出 |
| 高峰订单占比 | 高峰订单数 / 总订单数 | 高峰时段集中度,越高说明越要赶时间 |
| 深夜订单占比 | 深夜订单数 / 总订单数 | 熬夜程度,影响身体恢复 |
| 连续劳动隐患 | 最长连续接单时间 | 是否超过疲劳驾驶阈值 |
代码:
df_sorted = df.sort_values("start_time") # 按天计算在线时长:从第一天第一单到最后一单的时间跨度 df_sorted["date"] = df_sorted["start_time"].dt.date online_hours = {} for date, group in df_sorted.groupby("date"): duration_hours = (group["end_time"].max() - group["start_time"].min()).total_seconds() / 3600 online_hours[date] = duration_hours day_stats["online_hours"] = day_stats["date"].map(online_hours) # 有效接单率 day_stats["efficiency"] = day_stats["total_order_hours"] / day_stats["online_hours"] # 时薪 = 净收入 / 有效订单时长 day_stats["hourly_income"] = day_stats["total_net_income"] / day_stats["total_order_hours"] print("\n每日效率指标:") print(day_stats[["date", "total_net_income", "online_hours", "total_order_hours", "efficiency", "hourly_income"]].head(10)) # 综合疲劳指数:这里用 有效订单时长占比 + 高峰订单占比 + 深夜订单占比 做一个简单加权 peak_ratio = day_stats["peak_order_count"].sum() / day_stats["order_count"].sum() late_ratio = day_stats["late_night_order_count"].sum() / day_stats["order_count"].sum() efficiency_avg = day_stats["efficiency"].mean() fatigue_index = peak_ratio * 0.4 + late_ratio * 0.3 + (1 - efficiency_avg) * 0.3 print(f"\n高峰订单占比: {peak_ratio:.2%}") print(f"深夜订单占比: {late_ratio:.2%}") print(f"平均有效接单率: {efficiency_avg:.2%}") print(f"疲劳指数(0~1,越高越累): {fatigue_index:.3f}")这里fatigue_index只是一个演示用的加权值,不是标准医学指标。你可以根据实际研究调整权重,把它当作观测趋势的参考。
6. 功能测试与效果验证
分析脚本写完后,要按下面的步骤验证效果,判断结果是否合理。
6.1 测试用例
| 测试项 | 输入 | 预期结果 |
|---|---|---|
| 数据读取 | 模拟 CSV 文件 | 读取成功,打印前 5 行 |
| 重复订单清洗 | 手动加入重复 order_id | 重复行被删除 |
| 时间解析 | 标准时间字符串 | 能按天聚合 |
| 净收入计算 | 已知订单金额、抽成、油费 | 计算结果正确 |
| 在线时长计算 | 某天首单/末单时间 | 跨度大于有效订单时长 |
| 时薪计算 | 净收入/有效工时 | 输出合理数字 |
建议在跑完整分析前,先用一个只有 10 条订单的小文件测试。比如把sample_orders.csv复制成test_orders.csv,只保留前 10 行,修改analyze_income.py中的路径,跑通后再切回完整数据。
6.2 效果验证脚本
可以在分析脚本末尾加入结果校验逻辑:
# 简单校验 assert day_stats["total_net_income"].sum() > 0, "净收入异常" assert day_stats["online_hours"].between(4, 24).all(), "在线时长超出合理范围" print("\n数据校验通过,分析结果可用。")如果断言失败,说明上游数据或计算逻辑有问题,需要回头检查。
6.3 从结果反推辛苦程度
假设运行后得到下面这样的结果(这里用模拟数据演示):
- 月净收入:约 25000 元。
- 月有效订单时长:约 300 小时。
- 月在线时长:约 420 小时。
- 时薪:约 83 元。
- 高峰订单占比:约 35%。
- 深夜订单占比:约 8%。
这意味着平均每天在线 14 小时,有效开车 10 小时,每天净收入 833 元。看起来月入确实可观,但每天 14 小时绑在车上,身体和精神消耗非常大。再结合通勤、吃饭、充电、上厕所的时间,实际出门时间可能接近 15 到 16 小时。这个强度就是很多人说“月入 2.5 万很辛苦”的原因。
如果希望更精确,可以把online_hours替换成司机实际打卡上下线时间,而不是用首末单时间近似。
7. 批量分析与接口 API 扩展
实际使用中,一个司机可能有多个月份的流水,或者你想把分析能力提供给其他工具使用,这时候可以做两件事:批量分析和 API 封装。
7.1 批量分析多个 CSV
将所有流水文件放在data/monthly/目录下,命名为2025-04.csv、2025-05.csv,然后写一个循环:
import os import glob result_rows = [] for path in glob.glob("./data/monthly/*.csv"): month_df = pd.read_csv(path, encoding="utf-8-sig") # 复用前面的清洗和聚合逻辑 month_total_net = month_df["net_income"].sum() if "net_income" in month_df.columns else None result_rows.append({"file": os.path.basename(path), "net_income": month_total_net}) result_df = pd.DataFrame(result_rows) print(result_df)更稳妥的方式是把前面清洗和计算逻辑封装成函数,而不是复制粘贴。
7.2 封装成 Flask API
把分析逻辑封装成 HTTP 接口后,前端页面、小程序或其他服务都能调用。下面是一个最小示例:
from flask import Flask, request, jsonify import pandas as pd import tempfile app = Flask(__name__) def analyze_file(file_path): df = pd.read_csv(file_path, encoding="utf-8-sig") df = df.drop_duplicates(subset=["order_id"]) df["start_time"] = pd.to_datetime(df["start_time"]) df["end_time"] = pd.to_datetime(df["end_time"]) df["real_income"] = df["order_amount"] - df["platform_fee"] df["net_income"] = df["real_income"] - df["fuel_cost"] total_net = round(df["net_income"].sum(), 2) total_orders = len(df) order_hours = round((df["end_time"] - df["start_time"]).dt.total_seconds().sum() / 3600, 2) return { "total_net_income": total_net, "total_orders": total_orders, "total_order_hours": order_hours } @app.route("/api/analyze", methods=["POST"]) def analyze(): file = request.files.get("file") if not file: return jsonify({"error": "no file uploaded"}), 400 tmp_path = tempfile.mktemp(suffix=".csv") file.save(tmp_path) result = analyze_file(tmp_path) os.remove(tmp_path) return jsonify(result) if __name__ == "__main__": app.run(host="127.0.0.1", port=8090)启动服务:
python api_server.py用 curl 测试:
curl -X POST -F "file=@./data/sample_orders.csv" http://127.0.0.1:8090/api/analyze返回 JSON:
{ "total_orders": 612, "total_net_income": 29786.52, "total_order_hours": 312.58 }需要注意:接口启动后只能在本机访问,如果要暴露到局域网,需要设置host="0.0.0.0",同时要考虑访问权限和文件上传大小限制。生产环境建议加 Token 校验和文件类型校验。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 读取 CSV 报编码错误 | 文件不是 UTF-8 编码 | 用文本编辑器查看编码 | 读取时指定encoding="gbk"或encoding="utf-8-sig" |
| 时间列无法解析 | 时间格式不统一,比如有的带毫秒 | 打印原始值 | 用pd.to_datetime(..., errors="coerce"),再检查空值 |
net_income出现负数 | 成本字段大于到手流水 | 检查油费单位、平台抽成是否重复 | 校正字段含义,确认成本口径 |
| 在线时长超过 24 小时 | 一天内首末单跨天或数据有误 | 查看该天的首末单时间 | 对跨天订单做日期归属修正 |
| 按天聚合时日期错误 | start_time被解析成带时间的对象 | 打印dtypes | 使用df["start_time"].dt.date获取日期部分 |
| 运行脚本内存占用高 | 数据量过大 | 查看数据行数 | 改用分块读取pd.read_csv(..., chunksize=10000) |
| API 上传文件失败 | 服务未启动或端口被占用 | 检查日志和端口 | 杀掉占用进程或更换--port |
| 分析结果和平台订单总金额不一致 | 平台抽成字段是否包含优惠券、奖励 | 对比平台账单 | 以平台账单为准,调整字段计算口径 |
9. 资源占用、最佳实践与后续建议
9.1 资源占用观察
这个分析是纯 CPU 计算,典型的 30 天、600 条订单数据在普通笔记本上跑完不到 5 秒,内存占用约 200MB 左右。即使处理上万条订单,pandas 也足够应对。你可以在脚本里这样粗略观察耗时:
import time start = time.time() # 执行分析... print(f"耗时: {time.time() - start:.2f}s")如果数据量达到几十万条,可以考虑用polars或spark,但对个人司机月度分析来说,pandas 完全够用。
9.2 最佳实践建议
- 第一次运行时,先用模拟数据和极少量真实样本验证流程。
- 数据文件统一放在
data/目录,输出结果放在output/目录,保持目录结构清晰。 - 写分析脚本时,把清洗、计算、导出封装成函数,方便复用。
- 批量分析前先备份原始数据,不要直接在原文件上修改。
- 涉及真实乘客位置和手机号等敏感信息,务必先脱敏再保存。
- 如果要用这个分析结果做职业决策,建议结合多个平台的对账单、实际车辆损耗和相关行业数据一起看,不要只依赖单一指标。
- API 服务如果没有公网需求,就只监听
127.0.0.1,避免数据泄露。
9.3 后续可以扩展的方向
你可以把这里的数据分析方案继续升级成一个小型驾驶行为看板:接入司机每日打卡数据,自动计算连续驾驶时长;按周、按月生成收入走势图;结合天气和城市数据看单量波动;甚至用线性回归预测下一个月的收入区间。这样“月入2.5万有多辛苦”就不再是感觉,而是一套可以复用的数据模型。
如果你手里有真实的网约车流水数据,建议先按上面的流程跑一遍,把online_hours、efficiency、hourly_income这几个指标打印出来,然后对照每天的实际作息,你就能对收入与辛苦程度得到更清晰的判断。这个分析脚本也完全可以反过来用于其他自由职业者,比如外卖骑手、独立货运司机,只需要调整字段映射和成本模型。