深圳道路交通数据集实战:从数据清洗到随机森林分类全流程
简介深圳道路交通数据集采自深圳市政府开放平台覆盖深圳市各区的道路信息可为机器学习项目提供城市级交通数据支撑。该数据集包含交通流量、车速、事故记录、天气条件等多维字段适合用于交通拥堵预测、流量规律挖掘、路线规划与智能交通系统开发等场景尤其适合数据科学研究者、算法工程师及高校相关专业学生实践使用。包体共2个文件压缩包大小仅2.35MB其中traffic.sql为数据库结构化文件内含建表、索引等SQL定义便于数据查询与管理获取来源.txt则记录了数据采集的时间、地点与更新方式等背景信息有助于理解数据适用范围与局限性。目前已有500人浏览学习数据集体量轻便、字段结构化程度较高上手门槛低可作为交通类机器学习项目的高质量实验样本便于快速导入数据库开展分析与建模。1. 深圳道路交通数据集适合哪些机器学习项目先看标签再谈下载想用深圳道路交通数据集做机器学习项目先别急着下载。这份来自深圳市政府开放平台的数据是道路级的路网信息覆盖深圳各区字段以道路属性为主——名称、等级、长度、起止点、行政区、坐标适合做道路分类、路网聚类等任务。它的优点是官方渠道公开、可复现、更新周期固定局限是粒度到道路级没有实时车流量和路况序列做流量预测得再找时序数据。所以我一般把它当特征底表而不是唯一数据源。适合的人群是交通方向毕设的学生、做路网分析 PoC 的工程师以及机器学习入门练手的数据开发。项目开发的第一步是数据体检下面按取数、清洗、特征、建模、避坑五步展开。2. 从深圳市政府开放平台取数检索道路数据集、识别字段与首轮统计深圳道路交通数据集的下载入口是深圳市政府数据开放平台。平台上的数据集通常以 CSV、JSON 或 Excel 格式发布页面会标注更新频率、数据条数和数据字典。真正决定项目进度的不是下载速度而是你有没有在下载前看懂元数据。我一般先做三项体检更新频率是否在一年内、有没有数据字典、字段名是不是稳定的中文命名。这三项过关数据集才值得进入你的机器学习项目。2.1 在开放平台用道路关键词定位数据集先做元数据体检平台检索框输入道路路网道路信息通常会返回多个相关数据集名称接近但发布部门不同。不要只看名字就下载先打开数据字典核对字段清单和样例值。政府开放数据的字段名会随批次调整比如道路等级和道路级别可能指同一个东西也可能不是。以平台当前发布的数据字典为准下面这张表是我处理这类道路数据时最常见的字段清单用途列直接对应后续机器学习特征的来源常见字段类型说明机器学习用途道路名称字符串道路的中文名称去重和分组道路编码字符串平台内部编码主键、去重道路等级字符串高速/快速/主干/次干/支路分类标签或有序特征所属区字符串福田、南山等行政区类别特征起止点字符串起点和终点路段名可选文本特征长度数值单位通常是米连续特征坐标字符串经纬度或投影坐标空间特征更新时间时间记录最后更新日期增量过滤注意字段名以平台数据字典当前版本为准不同批次可能叫道路等级或道路级别。只看样例文件前几行是不够的样例无法暴露缺失、别名和单位不一致的问题。把数据字典和样例各看一遍确认长度的单位到底是米还是公里确认坐标是经纬度还是投影坐标这些误判到清洗阶段才发现返工成本很高。2.2 把下载好的 CSV 读成 DataFrame编码与读取参数的一次性设置平台下载接口通常需要登录 token自动化脚本容易随接口改版失效所以我一般手动下载 CSV 到本地再用 pandas 读取。读取参数有两个是常年固定的encodingutf-8 和 dtypestr。前者解决中文乱码后者防止长度这类数值字段被中间环节转成科学计数法后精度丢失。import pandas as pd # 手动下载到本地后读取先统一按字符串读入 df pd.read_csv( shenzhen_road.csv, encodingutf-8, dtypestr, keep_default_naFalse, ) print(行数:, df.shape[0]) print(列名:, df.columns.tolist())这段代码的逻辑是把所有字段先读成字符串交给后续清洗脚本统一做类型转换。keep_default_naFalse 的意义在于暂无无这类占位文本不会被 pandas 自动当成空值缺失规则由自己定义而不是被解析器替你做决定。参数说明encoding 只有两种候选utf-8 不行就试 gbk政府平台导出的 CSV 常见这两者dtypestr 会让数值计算报错所以只用于读取原始文件这一步不要用在已清洗的数据上。2.3 首轮统计按行政区核对覆盖度先扫一眼缺失率拿到 DataFrame 后第一件事不是画图也不是建模而是确认覆盖面。深圳的行政区包括福田、罗湖、南山、盐田、宝安、龙岗、龙华、坪山、光明、大鹏正常的数据集应该都能覆盖到。用 value_counts 看行政区分布再对关键字段做缺失率统计。# 行政区覆盖度核对正常应覆盖全部10个区 print(df[所属区].value_counts()) # 关键字段缺失率超过5%建议进入清洗流程 missing df[[道路等级, 长度, 坐标, 更新时间]].isna().mean() print(missing)value_counts 会暴露两个问题一是某个区完全没有记录二是福田区和深圳市福田区这种别名并存导致同一个区被拆成两行。缺失率统计是给清洗脚本定工作量的5% 是个经验阈值——低于 5% 的字段直接删行影响不大高于 5% 就要考虑保留缺失标记做特征而不是粗暴删除。这一步花五分钟能省掉清洗阶段至少一半的返工。3. 清洗深圳道路数据把政府原始表变成机器学习可用的样本政府开放数据不是为机器学习设计的清洗顺序错了后面会很别扭。我一般按类型校正 → 坐标处理 → 别名与去重推进每一步都在上一步的干净结果上操作防止前面造出来的脏数据污染后面的判断。3.1 类型校正与中文列名归一化长度和时间字段不再拖后腿原始 CSV 全部按字符串读入后第一件事就是类型转换。长度字段要转成 float更新时间要转成 datetime这两步不做后续所有数值特征和增量过滤都会出问题。# 长度转数值乱码统一变成 NaN df[长度] pd.to_numeric(df[长度], errorscoerce) # 更新时间转时间类型 df[更新时间] pd.to_datetime(df[更新时间], errorscoerce) # 列名归一成统一风格后续特征代码少踩坑 df df.rename(columns{ 所属区: area, 道路等级: road_level, 长度: length_m, 坐标: coord, 道路编码: road_id, 道路名称: road_name, })errorscoerce 的作用是把无法解析的文本变成 NaN这样整列的 dtype 才能统一成 float。如果不加这个参数一行脏数据会中断整个转换。列名归一化看起来是洁癖实际是为了后面写特征代码时少打几个中文字段名也避免多个 CSV 拼接时字段名不一致。转换完成后建议马上看一眼 dtypes 和各列空值数量确认转换没有把整列变成 NaN。3.2 坐标解析与越界过滤用深圳的经纬度区间做第一道闸坐标字段是这类数据最容易翻车的地方。平台给的格式可能是113.123,22.456也可能是投影坐标先看数据字典确认。如果是经纬度写一个兼容英文逗号和中文逗号的解析函数再做越界过滤。深圳的经纬度大致范围是东经 113.0 到 115.0、北纬 22.0 到 23.5明显越界的记录要么是坐标错位要么是解析失败。import pandas as pd import numpy as np import re def parse_coord(s): # 兼容 113.123,22.456 和 113.12322.456 if not isinstance(s, str): return pd.NA, pd.NA parts re.split(r[,], s.strip()) if len(parts) ! 2: return pd.NA, pd.NA try: lon, lat float(parts[0]), float(parts[1]) except ValueError: return pd.NA, pd.NA return lon, lat df[lon], df[lat] zip(*df[coord].map(parse_coord)) # 深圳经纬度大致范围越界的直接过滤 valid (df[lon].between(113.0, 115.0)) (df[lat].between(22.0, 23.5)) df df[valid] print(坐标有效占比:, round(valid.mean(), 4))解析函数先用正则拆分隔符再转 float任何一步失败都返回空值而不是抛出异常中断整条流水线。范围过滤用的是 between闭区间方便调整边界。注意如果数据字典写明是投影坐标这套逻辑不能用要按平台提供的投影参数转成经纬度或者直接用投影坐标算距离特征。这里最核心的教训是坐标字段的格式判断比解析本身更重要。3.3 去重与行政区别名归一顺序反了会误删有效记录政府平台的数据经常同一条道路分多条记录发布比如按路段拆分。去重前先想清楚主键有道路编码的按编码去重没有的按道路名称所属区长度组合判断。别名归一必须放在去重前面否则福田区和深圳市福田区会被当成两条不同的记录去重后留下哪条都是随机的。# 先做别名归一再去重顺序不能反 alias_map { 深圳市福田区: 福田区, 福田: 福田区, 深圳市南山区: 南山区, 南山: 南山区, } df[area] df[area].replace(alias_map) # 按道路编码去重保留最新更新的一条 df df.sort_values(更新时间).drop_duplicates( subset[road_id], keeplast )replace 传入字典把别名统一到标准区名。drop_duplicates 前先 sort_values保证按时间排序后 keeplast 保留的是最新记录如果不排序保留哪条由文件内部顺序决定结果不可控。做完这步再用前面的 value_counts 核对一遍行政区分布应该能看到一个干净的十区结构。去重后的行数会明显小于原始行数这是正常的不代表数据丢了。4. 特征工程与基线模型用深圳道路数据跑通道路等级分类原始数据没有标签这是这类道路信息表最常见的问题。我的做法是给自己造一个有监督任务用长度、行政区、坐标等属性预测道路等级。道路等级本身就是政府维护的权威标注把它当目标既验证了数据质量也拿到一个能跑通的机器学习模型基线后续真要换任务特征工程成果可以复用。4.1 从道路长度构造连续特征和分箱特征让模型拿到可解释的尺度长度是这份数据里最直接的连续特征但原始数值跨度大从几百米的支路到几十公里的快速路都有直接喂给模型会被长尾带偏。常见做法是同时保留对数变换和分箱特征。import numpy as np # 千米化让数值量级更直观 df[length_km] df[length_m] / 1000.0 # 对数变换压缩长尾分布 df[length_log] np.log1p(df[length_m]) # 分箱特征边界按道路常识设定 df[length_bin] pd.cut( df[length_km], bins[0, 1, 3, 10, 100], labels[短, 中, 长, 超长], )log1p 在长度为 0 时不会报错比 log 更适合含零数据这是它作为默认选择的原因。分箱边界 1/3/10/100 是经验值1 公里以下是支路和小区路常见尺度3 公里是短连接路阈值10 公里以上通常是跨区干道。用 pd.cut 生成的类别特征可以直接进模型也可以再 one-hot。想省事就只用 length_log分箱特征主要是给树模型增加非线性切分点实际提升有限但可解释性好。4.2 类别特征编码道路等级做有序映射行政区走 one-hot道路等级本身有顺序高速公路、快速路、主干路、次干路、支路从高到低。对这种有序类别用字典映射成整数比 one-hot 更合理可以保留顺序信息。行政区没有顺序关系属于名义变量用 one-hot 编码。基线阶段不需要做太多区划交叉特征one-hot 够用。level_order { 高速公路: 4, 快速路: 3, 主干路: 2, 次干路: 1, 支路: 0, } df[level_code] df[road_level].map(level_order) # 把没映射上的未知等级过滤掉避免脏标签进模型 df df[df[level_code].notna()] # 行政区 one-hot area_dummies pd.get_dummies(df[area], prefixarea) df pd.concat([df, area_dummies], axis1)map 返回空值表示未知等级这里选择直接过滤因为等级是你要预测的目标标签不干净模型学到的东西不可信。get_dummies 默认会把所有类别展开如果某个区在测试集出现而训练集没有预测时会列数不一致这就是为什么后续要用字段清单做兼容性检查。等级映射的数字大小只表示相对顺序不表示间距树模型不受影响线性模型要小心。4.3 随机森林基线参数怎么设先看分类报告而不是只盯准确率特征不多、样本量中等的情况下随机森林是比 XGBoost 更稳妥的机器学习模型基线不用调太多参数就能给出合理结果。样本量上了几十万再换 XGBoost 或 LightGBM。切分时用 stratify 按等级比例分层防止测试集里某类等级一个都没有。from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report feature_cols [length_log] [c for c in df.columns if c.startswith(area_)] X df[feature_cols] y df[level_code] # 分层切分保证测试集里每个等级都有样本 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) clf RandomForestClassifier( n_estimators300, max_depth10, min_samples_leaf5, class_weightbalanced, random_state42, ) clf.fit(X_train, y_train) print(classification_report(y_test, clf.predict(X_test)))参数说明n_estimators 从 100 加到 300 收益递减超过 500 只增加训练时间max_depth 限制在 10 以内防止对道路样本过拟合min_samples_leaf5 让每个叶子至少有 5 个样本预测更稳class_weightbalanced 是针对支路占比过高的问题如果不设模型会倾向把所有样本预测成支路准确率虚高但小类全错。评估指标看宏平均宏 F1 或每个等级的分类报告不要只看 accuracy。一个容易忽略的点这里用的是简单随机切分只适合基线验证。同一条道路的多条记录如果同时出现在训练集和测试集会发生数据泄漏测试指标虚高。严格做法是按道路名称分组切分我在第五章的踩坑记录里专门展开。4.4 特征重要性检查判断长度、区划哪个在真正起作用随机森林不是黑匣子训练完把 feature_importances_ 打出来看一眼能判断模型到底在学什么规律也能发现特征构造是否有问题。importances pd.Series( clf.feature_importances_, indexfeature_cols ).sort_values(ascendingFalse) print(importances.head(10))如果 length_log 排第一说明道路规模在决定等级符合常识如果某几个区划特征异军突起说明等级分布有明显地域差异可以往各区道路建设标准不同的方向做业务解读。特征重要性还有一个用途验证特征是否传对了。如果某个你认为很重要的特征重要性为 0先检查是不是全列空值或编码错误而不是怀疑模型。5. 避坑深圳道路交通数据集落地会踩的 5 个实际问题下面五条是我用这类政府道路数据踩过的坑每一条都按现象、原因、解决展开。这些问题在深圳道路交通数据集上出现的概率很高提前看过再看后续代码能少走弯路尤其最后一条数据泄漏属于隐蔽性最强、返工代价最大的一类。5.1 行政区字段出现别名和缺失value_counts 直接少了一区现象首轮统计时发现只出来 9 个区某区记录数莫名少了一半。 原因同一行政区分批录入存在福田区深圳市福田区福田三种写法value_counts 把它们当成三个类别真实分布被拆散。 解决做一张别名映射表统一区名再做一次分布核对。注意别名表要覆盖平台历史版本只覆盖当前版本下次更新数据又会出现新写法。别名映射表建议单独存成 JSON 或放到配置模块便于增量补充。5.2 道路等级是中文文本没编码就塞进模型直接报错现象训练时报错 could not convert string to float或者树模型把每个中文等级当成独立类别效果奇差。 原因pandas 读进来是字符串sklearn 的树模型不接受字符串特征而最容易忘记的就是先做标签编码。 解决按等级有序映射成整数再检查 map 之后是否存在空值。顺序映射表写成模块级常量训练和推理共用同一份避免两处维护两份映射导致不一致。映射表里留一个unknown分支比直接报错更好排查。5.3 用 Excel 打开过 CSV 再另存坐标精度被悄悄截断现象解析坐标后有效过滤掉了四分之一的数据检查发现经纬度小数位从 6 位变成 3 位。 原因Excel 默认显示精度另存为 CSV 时把小数位截断原来能区分两条相邻道路的坐标变成完全相同的两个点。 解决下载后直接用 pandas 读取原始 CSV不要用 Excel 打开再另存。如果必须用 Excel 查看只看不存或者复制到新文件处理不要覆盖原文件。这个坑的隐蔽之处在于文件能正常打开肉眼也看不出精度变化只有统计唯一坐标数量时才会发现。5.4 支路占比过半模型全体预测支路但你还觉得准确率不错现象准确率 0.7 上下看起来不错打开分类报告发现高速公路和快速路的精确率几乎为 0。 原因数据里支路、次干路占比太高模型学到的捷径是把所有样本都预测成支路整体准确率依然好看。 解决训练时加 class_weightbalanced评估指标改用宏平均宏 F1。如果还是不均衡对训练集做分层抽样或者按等级设置采样权重。不要用准确率作为这类数据的最终指标这是机器学习项目里最容易自我误导的地方准确率只配当辅助参考。5.5 同一条道路分了多条分段记录按行切分造成数据泄漏现象验证集效果很好上线换新数据效果大幅下滑。 原因同一条道路按路段拆成多条记录随机切分后同一条道路的记录同时落在训练集和测试集模型其实是在背道路而不是学规律。 解决按道路编码或道路名称分组切分用 GroupShuffleSplit 代替 train_test_split。实现上只差一个参数但决定了这个模型到底有没有泛化能力。from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next( gss.split(X, y, groupsdf[road_id]) ) X_train, X_test X.iloc[train_idx], X.iloc[test_idx] y_train, y_test y.iloc[train_idx], y.iloc[test_idx]GroupShuffleSplit 的 groups 参数传入每条样本所属的道路编码切分时保证同一个道路编码的所有分段记录只出现在同一侧。n_splits1 表示只生成一组切分test_size 保持 0.2 不变。如果平台数据没有道路编码退而求其次用道路名称行政区组合当 group 键效果接近。6. 版本化与增量验证重新取数后如何判断该不该重训政府开放平台会周期性更新道路数据新文件的字段顺序和取值可能变化。我第一次拿新文件直接推理模型输出乱套追查才发现缺了一列特征。之后养成习惯训练结束把字段清单、等级映射、模型参数一起存成 json。# 把训练时的字段清单和等级映射存下来 import json meta { feature_columns: feature_cols, level_map: level_order, } with open(model_meta.json, w, encodingutf-8) as f: json.dump(meta, f, ensure_asciiFalse, indent2)推理脚本先读这份 json对比新数据的列集缺列或列顺序不一致就停下来报警而不是带着错的特征硬跑。ensure_asciiFalse 让中文映射表直接可读indent2 方便 git diff 查看改动。这个习惯成本极低但能挡住大部分换数据后悄悄变差的问题。第二个快检是分布漂移。把新数据的行政区占比和训练时存下来的占比对比如果某个区掉了 10 个百分点以上说明取数口径或道路覆盖变了先查平台更新说明再决定要不要重训。判断逻辑很简单占比差异小沿用旧模型差异大重训并复查清洗脚本。我现在每次重新取数第一件事跑字段体检脚本第二件事看分布对比两项都过了才谈重训。这个习惯帮我挡住好几次线上翻车比任何调参都值。希望帮到你。本文还有配套的精品资源点击获取