首页/新闻资讯/正文详情

泰迪杯车辆驾驶行为分析:GPS轨迹清洗与聚类建模全流程

发布时间:2026/9/26 7:24:14 来源:云帆数科 栏目:资讯中心
泰迪杯车辆驾驶行为分析:GPS轨迹清洗与聚类建模全流程
简介第七届泰迪杯数据挖掘竞赛“车辆驾驶行为分析”完整项目含源码、文档说明与比赛总结面向数据挖掘学习者、竞赛选手及车辆网联相关毕设学生。项目在常规驾驶行为分析基础上引入省、市、县级温度、天气、湿度等环境数据建立环境评测模型探究环境因素对行驶节能的影响并将环境维度融入驾驶行为评测体系。资源共10个文件压缩包约1.82MB。核心为5个Jupyter分析笔记覆盖数据预处理、经纬度与路程速度探索、450辆车特征整合、聚类及汽车评价体系打分等关键环节另有2个Python脚本用于异常值剔除和绘图2个Markdown文档与1个Word文档提供说明和比赛总结便于理顺全流程。已有167人学习。下载后可按笔记顺序复现赛题解法借鉴环境融合思路与建模流程也可作为课程设计或项目演示基础。作者表示运行遇到问题可私聊远程教学适合需要完整参考实现的学习者。1. 泰迪杯车辆驾驶行为分析一套能直接复现的比赛源码与建模全流程450辆车的GPS轨迹放进KMeans之前真正决定比赛名次的是数据清洗。这套第七届泰迪杯车辆驾驶行为分析资源最值钱的不是最后的聚类代码而是把“经纬度→路程→速度→驾驶行为画像→环境模型”整条链路拆成了五个可运行的ipynb、一个drop_outlier.py和一份比赛总结。它解决的是数据挖掘比赛里最现实的问题拿到轨迹、天气、地区数据后怎么快速做出有业务解释的驾驶行为评分再把温度、湿度这些环境因素量化到节能分析里。适合参加数据挖掘比赛、做课设毕设以及想入行车联网数据分析的从业者。2. 数据预处理与路程速度特征把450辆车的GPS轨迹洗成可用特征宽表2.1 异常值剔除drop_outlier.py 剔的是哪些“怪点”车辆轨迹数据里最常见的问题不是缺数据而是“多出来的假数据”。停车场里GPS信号来回跳经过高架桥下卫星丢失急刹车瞬间速度先冲到120又跌回0这些点如果不处理平均速度会被拉高加速度特征会失真。整车数据挖掘项目里清洗所花的时间通常占一半以上。drop_outlier.py这种脚本一般是用MAD中位数绝对偏差而不是3σ来做离群点剔除。因为速度序列不是正态分布有大量怠速0值和高速巡航值平均值本身就不稳用均值加几个标准差去卡阈值很容易把正常的城市快速路数据也剔掉。MAD只依赖中位数对尖峰和重尾分布稳健得多。参考实现可以写成一个独立函数import numpy as np import pandas as pd def drop_outlier(df, colspeed, k5.2): # 用MAD中位数绝对偏差识别速度离群点比3σ更抗干扰 median df[col].median() mad (df[col] - median).abs().median() # 1.4826 是把MAD换算成正态分布标准差尺度的常数 lower median - k * 1.4826 * mad upper median k * 1.4826 * mad return df[(df[col] lower) (df[col] upper)]这段代码的关键在k的取值。k取5.2是我常用的起点相当于给GPS轨迹留出足够的正常波动空间只把“物理上几乎不可能”的速度点删掉。如果你想更激进k取3到4会把更多急加速峰值也抹掉适合做平稳型驾驶行为分析如果只是想清掉明显的漂移点k取5到6更安全。运行后要看剔除比例控制在1%~3%是健康的超过5%就要怀疑是速度单位问题或者数据本身有重复采样。2.2 经纬度与路程Haversine 公式和速度列怎么算出来的资源里的经纬度.ipynb解决的是一个很基础但绕不开的问题两个GPS点之间的距离不能直接用经纬度差当公里数算。纬度1度约111公里经度1度在赤道约111公里但在北纬40度只有85公里左右必须用球面距离公式。Haversine公式是车联网轨迹处理里最通用的方案import math def haversine(lon1, lat1, lon2, lat2): # 输入两组经纬度输出球面距离单位公里 R 6371.0 lon1, lat1, lon2, lat2 map(math.radians, [lon1, lat1, lon2, lat2]) dlon lon2 - lon1 dlat lat2 - lat1 a math.sin(dlat / 2) ** 2 math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 return 2 * R * math.asin(math.sqrt(a))注意传入的是经纬度的小数形式比如114.30、30.59而不是度分秒。R取6371公里是地球平均半径实际在极地和赤道差几十公里对驾驶行为分析这个精度完全够用。两个采样点之间的瞬时速度用距离除以时间差再乘3.6换算成km/h即可。常见做法是先把所有点按车辆和时间排序再对每个相邻点对算距离最后把距离序列除以时间差序列。2.3 特征窗口3 秒平滑、30 秒统计为什么是这个参数原始速度序列是1Hz甚至更低频率的直接拿来算加速度会非常抖。我一般会先做3秒滑动平均把GPS本身的噪声滤掉再对平滑后的速度做差分得到加速度。加速度阈值方面急加速一般取大于1.5 m/s²急减速取小于-1.5 m/s²这两个阈值来自《驾驶行为评分标准》这类行业参考也和资源里汽车评价体系得分的口径接近。# 3秒平滑后再算加速度避免GPS跳变造成假急加速 df[speed_smooth] df[speed].rolling(window3, min_periods1).mean() df[acc] df[speed_smooth].diff() / 3.6 / 3 # 急加速和急减速标记 df[is_rapid_acc] (df[acc] 1.5).astype(int) df[is_rapid_dec] (df[acc] -1.5).astype(int)这里的除以3.6是把km/h换成m/s再除以3是因为diff是3秒的差分。窗口选3秒而不是1秒是为了滤掉GPS定位噪声带来的虚假加速度30秒统计窗口则是对应城市道路的一个完整驾驶片段短于30秒会有太多空窗口长于5分钟又会把加减速细节平均掉。统计内容一般是平均速度、速度标准差、急加速次数、急减速次数、怠速时长占比。2.4 450 辆车的预处理流水线一个 for 循环串起全流程资源里450辆车预处理.ipynb的组织方式就是典型的逐车清洗再合并。每辆车单独读取、单独做离群点剔除和路程速度计算最后拼成一张以“车辆ID”为行的特征宽表。这一步之所以要逐车做是因为每辆车的采样频率和传感器精度可能不同异常值比例也不一样统一跑一个全局阈值反而会误伤。import pandas as pd all_features [] for car_id in range(1, 451): # 按车循环清洗、算路程速度、滑窗统计 df pd.read_csv(fdata/car_{car_id}.csv) df drop_outlier(df, colspeed, k5.2) df calc_distance_speed(df) # 内部调用haversine feat window_features(df) # 30秒滑窗统计 feat[car_id] car_id all_features.append(feat) feature_table pd.concat(all_features, ignore_indexTrue) feature_table.to_csv(features_450.csv, indexFalse)这样处理完的特征表才是后续KMeans聚类的输入每一行是一辆车列是这辆车在30秒窗口上的聚合值。这里有个经验多辆车合并阶段不要直接concat原始轨迹而是concat特征否则内存压力大且难以定位是哪辆车引入的脏数据。3. 驾驶行为画像KMeans聚类与汽车评价体系得分3.1 聚类特征别把原始速度直接喂进KMeansKMeans对特征尺度极其敏感这是老生常谈但依然频繁翻车的地方。如果把平均速度、速度标准差、急加速次数直接拼在一起跑平均速度动辄几十急加速次数只有个位数距离计算基本被速度主导聚出来的簇本质上是“跑得快还是跑得慢”而不是驾驶行为差异。所以特征选择这一步要刻意加入行为维度急加速频次、急减速频次、夜间行驶占比、怠速时长占比这些才能真正区分激进型和稳健型驾驶风格。from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans features [avg_speed, speed_std, rapid_acc_cnt, rapid_dec_cnt, night_ratio, idle_ratio] X feature_table[features].values X_scaled StandardScaler().fit_transform(X)StandardScaler对每个特征减去均值除以标准差让所有特征在同一尺度下参与距离计算。注意必须用fit_transform在训练集上学习均值和标准差之后对测试集只能用同一个scaler的transform方法不能重新fit。很多人在这个位置少写一行导致训练和预测用了两套尺度聚类中心完全错位。3.2 聚类数 K轮廓系数和业务解释哪个优先聚类数K是驾驶行为分析里最玄学的一个参数。技术层面可以用轮廓系数帮忙筛选它会同时评估簇内紧密度和簇间分离度取值越接近1说明聚类效果越好。但跑出来的轮廓系数会随着K增大单调变化不能只看最高分。from sklearn.metrics import silhouette_score sil_scores [] K_range range(2, 9) for k in K_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) sil_scores.append(silhouette_score(X_scaled, labels))在实际项目里KMeans的n_init参数建议从默认的10提高到20甚至50避免随机初始化带来的不稳定性。K的选择我的习惯是轮廓系数曲线先找一个明显的拐点再回来看每个簇的车均特征是否讲得通。比如K4时有一个簇平均速度28km/h、急加速6.2次/百公里可以解释为拥堵路况下的激进驾驶K5时这个簇又被劈成两半且特征几乎重叠那就说明K取大了。数值指标只是参考业务解释才是最终标准。3.3 评价体系得分从簇标签到百分制的映射聚类的产出是簇标签但比赛和业务方要的是“这辆车能得多少分”。资源里汽车评价体系得分.ipynb做的事就是把每个簇按特征均值排序后映射成百分制得分。常见做法是用簇中心的平均速度排序速度均值最低、急加减速最少的经济型簇给高分平均速度和急加减速都高的激进型簇给低分。centers pd.DataFrame(km.cluster_centers_, columnsfeatures) # 按平均速度升序排列簇中心越省油平稳的簇得分越高 order centers[avg_speed].sort_values().index score_map {order[0]: 90, order[1]: 78, order[2]: 65, order[3]: 55} feature_table[behavior_score] feature_table[cluster].map(score_map)这里要留一个心眼直接固定打分很容易让所有得分集中在几个离散值上导致最终分布没法区分同簇内个体差异。更好的做法是在簇得分基础上再按该车在簇内的急加速次数排名做加减分修正。比如同样是经济型簇急加速次数在簇内排名前20%的扣3分排名后20%的加2分。这样得分连续后续做环境模型时也更好用。4. 环境评测模型把温度、天气、湿度接进驾驶行为得分4.1 环境数据对齐城市字段和时间粒度是主要阻力赛题要求结合省、市、县级地区的温度、天气、湿度等环境特征。这类数据通常来自气象站或公开天气API格式是“某城市某日”或者“某城市某小时”而车辆轨迹是经纬度加时间戳。对齐的第一步是把轨迹数据映射到城市或区县这一步资源里一般已经做了但如果要自己复现可以用逆地理编码库把经纬度翻译成城市名或者按轨迹起点所属城市近似代替。# 天气表里的城市可能带“市”字轨迹表里的城市没有先清洗再合并 trip[city] trip[city].str.replace(市, , regexFalse) weather[city] weather[city].str.replace(市, , regexFalse) # 以城市和日期为键做左连接保留全部行程记录 merged pd.merge(trip, weather, on[city, date], howleft)时间粒度的选择会影响模型效果。如果轨迹数据有小时级时间戳建议把天气也按小时对齐因为温度和湿度的日内波动很大早晚温差对能耗的影响比日平均温度更显著。天气API一般一小时一条记录直接按城市加小时merge即可。如果只有日粒度天气那就退而求其次用日平均温度但要接受信息损失。4.2 环境评测模型线性回归还是树模型环境评测模型的核心目标是量化“温度、天气、湿度对车辆行驶节能的影响”这时候线性回归比随机森林更好用。因为树模型的输出是特征重要性只能告诉你哪个变量重要不能告诉你温度每升高1度能耗变化多少线性回归的系数可以直接读成边际影响。比赛总结里如果要写“低温环境下能耗显著上升”线性回归系数的说服力最强。from sklearn.linear_model import LinearRegression # 节能指标用百公里等效能耗若原始表无能耗字段可用行为得分做代理 merged[energy_per_100km] merged.get( energy_per_100km, merged[behavior_score] * -1 ) X_env merged[[temp, humidity, is_rain, is_snow]] y merged[energy_per_100km] model LinearRegression().fit(X_env, y)is_rain和is_snow需要提前从天气现象字段里拆出来用one-hot或二值标记都行。线性回归的系数解释要注意单位temp的单位是摄氏度humidity是百分比若湿度系数是0.05就意味着湿度每增加10个百分点百公里能耗上升0.5这个量级是合理且可汇报的。4.3 节能影响量化不同温度区间的能耗差异回归系数给出的是全局平均趋势但实际驾驶行为受温度影响往往不是线性的低温到常温区间下降很快常温以上基本平稳。比赛评审更愿意看到这种分段差异而不是一个孤立系数。分组聚合是最直接的方法# 把温度切成5个区间看每个区间下百公里能耗均值 import pandas as pd bins [-20, 0, 10, 20, 30, 40] labels [-20, 0~10, 10~20, 20~30, 30] merged[temp_bin] pd.cut(merged[temp], binsbins, labelslabels) impact merged.groupby(temp_bin, observedFalse)[energy_per_100km].mean()这段代码跑完一般能看到低温段能耗比常温段高出10%~20%的结果。要注意观察每个温度区间的样本量如果0度以下的行程只有几十条均值很容易被少数极端天气驾驶行为带偏汇报时要补一个样本量说明。这是环境模型里最容易忽略的统计陷阱。5. 避坑指南跑这套驾驶行为源码最容易翻车的五个位置5.1 GPS 漂移点把速度推到 180清洗阈值怎么定现象某辆车的瞬时速度列里出现181 km/h但看它的轨迹只是在市区绕圈位置点来回跳跃。原因GPS在立交桥下或高楼密集区丢失卫星信号接收机输出错误坐标相邻两点距离被算成几公里速度自然爆表。如果不处理平均速度直接失真聚类时会被分到“高速型”而实际上司机根本没开那么快。解决不要只设一个绝对速度上限而是用2.1节里的MAD方法按车剔除。如果单是几个尖峰先删除速度大于120km/h且持续时间小于5秒的点段如果是整段信号丢失还要看时间间隔超过10秒的相邻点直接断开不计算这段路程。5.2 时间格式不统一merge 直接少一半数据现象轨迹表里时间列是“2017/1/1 8:30”天气表里是“2017-01-01 08:30:00”直接用字符串merge原本能匹配上的记录全变成NaN后面模型样本量直接砍半。原因Excel导出、API返回、原始CSV三个来源各用各的格式字符串匹配严格按字符比对斜杠和横杠不相等。解决在任何merge操作前先统一时间列格式然后单独提取date字段作为关联键。处理完必须检查merge后的非空比例低于90%就要回头查是格式问题还是城市字段问题。常见做法是把所有时间字段强制解析成datetime类型再格式化。5.3 聚类结果无法解释只按速度分组不是驾驶行为现象KMeans跑出来三个簇簇1全是平均速度80以上的簇2全是40~60的簇3全是20以下的。各簇急加速次数没有明显差别报告写出来像“路况分类”而不是“驾驶行为分类”。原因特征列里速度相关变量占比过高行为特征太少或量纲太小被速度特征淹没。这是3.1节提到的尺度问题再加一个隐患如果只用平均速度和行驶时长KMeans聚的就是出行场景而非人的行为。解决检查特征相关性矩阵速度均值和速度标准差相关度高于0.6时保留一个强制加入急加速频次、夜间占比、怠速占比等行为特征并确认标准化之后它们在距离计算中的实际贡献。特征数量控制在5到7个太多反而让业务解释困难。5.4 天气数据 join 不上城市字段差一个字现象天气表里城市是“武汉市”轨迹表里是“武汉”天气表用省份地级市两列轨迹表只有一列“湖北武汉”。merge之后大部分行没有天气数据。原因行政区划的字段在清洗时没做归一来源系统不一样有的带行政后缀有的不带。解决字符清洗是第一步把“市”“省”“自治区”等后缀统一去掉如果还匹配不上就改用轨迹起点经纬度去匹配最近气象站每个气象站有经纬度和城市属性按Haversine距离取最近的一个。这个方法省事且能兼容到县级数据也符合赛题“省、市、县级”的要求。5.5 训练集和测试集分布不一致模型要固定不能重训现象比赛给了一部分车辆的预处理结果你在训练集上聚出4个簇测试集来了之后又对所有数据重新跑了一遍KMeans结果簇的中心和业务含义全变了评分结果对不上。原因KMeans是无监督算法重新fit就会得到新簇中心。很多人习惯性地对新数据再跑一次fit这是无监督模型落地时最典型的错误。解决训练集上fit好KMeans之后保存模型测试集只用transform或predict把样本映射到最近的簇绝不要重新fit。同样的原则也适用于StandardScaler。比赛提交前把保存模型的pickle文件和评分脚本一起打包能省掉很多麻烦。6. 验证技巧与复盘写法让驾驶行为分析结果能经得起问模型做完只是第一步答辩和比赛总结里评审一定会问“你这个驾驶行为评分有什么实际意义怎么证明它是准的”下面三个验证技巧是我的习惯都写进了资源里的比赛总结文档。第一个技巧是同路段对比验证。找同一个司机在同一个路段、同一时段的早晚高峰行程提取平均速度、急加速次数。如果评分模型合理拥堵晚高峰的急加速次数应该比畅通早高峰少而急减速次数应该更多。这个验证不用额外数据一张分组统计表就能说明问题也最容易让外行听懂。第二个技巧是密度分布图代替均值柱状图。聚类结果对比时不要只画各簇的平均速度柱状图那是把个体差异抹平了。用seaborn的kdeplot画出不同簇的速度分布曲线如果一个簇的分布明显呈双峰说明这个簇里混入了两种驾驶风格K取值可能偏小如果两个簇的分布大面积重叠只有均值略有不同说明聚类边界不清晰。这个可视化在答辩时很加分因为一眼能看出问题在哪里。第三个技巧是把回归系数和特征重要性写进总结文档而不是只写结论。环境模型里温度系数是0.12意味着温度每下降10度百公里能耗上升1.2左右这个数字比“温度影响显著”有说服力得多。还要配上样本量和置信区间防止被质疑数据量太小。从那次比赛之后我每次拿到GPS轨迹数据不管目标是什么都强制先跑一遍离群点剔除和时间格式统一再谈后面的聚类和建模。这套流程看起来不起眼但至少帮我避开了之后很多项目里80%的返工。希望这套源码和复盘文档也能帮到你。本文还有配套的精品资源点击获取

相关推荐

Neo4j知识图谱实战:从本体建模到Cypher查询与数据导入
Neo4j知识图谱实战:从本体建模到Cypher查询与数据导入

简介:一套基于Neo4j图数据库开发的知识图谱项目,可作为毕业设计、课程设计或项目实践的完整参考。项目围绕知识图谱的构建与应用展开,整合了后端Java控制器、前端JavaScript与HTML页面、CSS样式布局,以及Neo4j数据库的db、neostor… · 2026/9/26 7:24:08

指纹浏览器原理拆解:沙箱隔离与指纹仿真如何实现防关联
指纹浏览器原理拆解:沙箱隔离与指纹仿真如何实现防关联

2026 年,我依然经常被人问到同一个问题:指纹浏览器到底是不是“换个浏览器”那么简单?如果你做过跨境电商多店铺运营,或者在 RPA 自动化里需要同时管理多个平台账号,肯定有过这种体验:同一个浏览器开两个窗… · 2026/9/26 7:24:08

VMware安装卡在虚拟网络驱动?彻底解决与排查指南
VMware安装卡在虚拟网络驱动?彻底解决与排查指南

1. 卡在“正在安装虚拟网络驱动程序”到底卡在了哪装 VMware Workstation 这件事,说简单也简单,一路下一步就完事;说坑也真坑,很多人第一次装就栽在同一个地方——进度条走到“正在安装虚拟网络驱动程序”这一步,然后就… · 2026/9/26 7:24:08

Graspness:面向真实场景的可微分抓取置信度建模与6D位姿生成
Graspness:面向真实场景的可微分抓取置信度建模与6D位姿生成

简介:本资源是一套基于Graspness评分机制的机械臂视觉6自由度抓取完整实现方案,面向计算机、人工智能、机器人及电子信息等专业的本科生与研究生,适用于课程设计、毕业设计及机器人感知-操作一体化技术学习。项目采用Python为主开发语言&… · 2026/9/26 7:57:54

ctf-wiki ELF 符号表(.symtab / Elf32_Sym)深度解析:从结构定义到符号解析与定位
ctf-wiki ELF 符号表(.symtab / Elf32_Sym)深度解析:从结构定义到符号解析与定位

文档网络安全教程 【免费下载链接】ctf-wiki Come and join us, we need you! 项目地址: https://gitcode.com/gh_mirrors/ct/ctf-wiki 点击查看 免费下载 导读:本文基于 ctf-wiki 仓库 ELF 文件结构 符号表 一文展开,系统讲解 Linux ELF 目… · 2026/9/26 7:57:54

Coder云开发平台实战:统一开发环境与AI编码代理接入
Coder云开发平台实战:统一开发环境与AI编码代理接入

我接触 Coder 这个项目,是因为团队里一直在吵一个问题:开发环境到底放哪。有人习惯在本地笔记本跑,有人非要申请一台云主机,还有人把代码放到容器里写一半就忘了镜像怎么构建。直到我们把 Coder 部署起来,整个流程才顺… · 2026/9/26 7:57:54

四开关Buck-Boost拓扑详解:宽压输入电源设计实战指南
四开关Buck-Boost拓扑详解:宽压输入电源设计实战指南

1. 四开关Buck-Boost到底是个什么东西1.1 从一个尴尬的电压问题说起做过电源的朋友大概率遇到过这种场景:输入电压标称12V,但实际可能在9V到18V之间晃荡,而你的负载偏偏需要稳定在12V。用Buck吧,输入掉到9V的时候它只能干瞪眼——… · 2026/9/26 7:57:54

Dango-Translator:基于PaddleOCR的本地化OCR翻译操作系统
Dango-Translator:基于PaddleOCR的本地化OCR翻译操作系统

1. 项目概述:这不是一个普通翻译工具,而是一套可嵌入工作流的OCR翻译操作系统 Dango-Translator不是另一个“点一下就出结果”的翻译小工具。我用它三年,从最初在PDF论文里手动框选公式旁的注释,到后来批量处理扫描版古籍、工程图… · 2026/9/26 7:57:54

Python函数入门:从def到return、嵌套与拆包,一文拆解核心概念
Python函数入门:从def到return、嵌套与拆包,一文拆解核心概念

这个系列写到第三篇,前两篇我们把环境折腾明白,也把变量、数据类型、流程控制这些地基打了一遍。到了函数这一篇,很多人的学习节奏会第一次慢下来——不是它有多难,而是它太不像前面那些"看见就能懂"的语法了&#xff1… · 2026/9/26 7:57:48

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码