基于POI与基站轨迹的用户画像标签实战
简介这份PPT聚焦大数据驱动的用户画像构建从大数据概念、特征切入结合“基于POI的文体标签开发”实战案例适合产品、数据分析、推荐系统初学者及从业人员参考。资源共1个pptx文件压缩包7.66MB共25页内容精炼且结构清晰。已有174人学习。内容涵盖大数据起源的“三驾马车”、体量与维度特点以及用户画像的真实性、目标性、应用性与长久性等核心概念。实际案例中详细展示了从爬取高德POI信息、获取中移基站与用户轨迹到多维度数据融合、特征工程和模型结果验证的完整链路并针对数据可靠性问题提出Z值检验、经验分析与异常数据剔除思路结合上海移动真实用户连续五天的标签收敛数据可帮助读者快速掌握用户画像在工业场景中的搭建方法与排错策略。1. 用户画像不是标签堆砌2000万级轨迹数据的POI文体标签实战很多人以为用户画像就是给用户打上“喜欢运动”“科教爱好者”这类标签真正做落地时才发现标签的定义、数据来源、关联逻辑才是决定画像可用性的关键。这个案例来自一个真实项目的复盘基于上海移动2000万用户的位置轨迹数据结合高德地图POI信息为用户建立科教文化、体育休闲两类文体标签。项目的核心不是算法而是如何把POI、基站、人三层数据可靠地串起来并用Z值检验识别场馆活动时间。适合正在做推荐系统前置画像、用户增长策略或位置大数据分析的同学参考。大数据技术原理与用户画像构建在工程上是两码事下面我会按数据获取、融合、验证、上线的顺序拆开讲。2. 数据获取与清洗高德POI爬虫、基站轨迹与坐标系误差矫正决定画像质量的第一步不是模型而是数据源。文体标签需要两个基础信息一是“哪些地方属于文体场馆”二是“用户去过哪些地方”。前者我用高德地图的POI数据后者用运营商基站信令产生的用户轨迹数据。这两个数据源都有各自的坑下面逐个说。先说清楚为什么选这两个再讲采集和清洗的具体做法。2.1 为什么选“高德POI 基站轨迹”而不直接依赖GPS轨迹在真实生产环境里绝大多数用户位置数据不是手机GPS而是基站信令。一个基站覆盖几百米到几公里直接拿用户坐标和场馆坐标做距离匹配结果会非常脏。POI数据能提供场馆的精准经纬度和分类所以把它当作锚点。用户轨迹数据体现的是用户在不同基站之间的切换序列精度有限但胜在覆盖全、采样频率稳定。把POI当作一级点、基站当作二级点、用户当作三级点是工程上比较稳妥的关联方式这个我在第3章会展开。2.2 高德POI爬虫关键词搜索接口与分页拉取高德开放平台的“地点搜索”接口可以根据关键词和类型返回POI列表。我用的参数是types限定为科教文化类和体育休闲类city限定上海citylimit防止返回其他城市结果offset控制每页条数extensionsall拿详细信息。下面是一个可运行的爬虫片段import requests import time import pandas as pd API_KEY YOUR_AMAP_KEY # 替换为你自己的高德开放平台Key base_url https://restapi.amap.com/v3/place/text poi_list [] for page in range(1, 10): params { key: API_KEY, types: 080301|080302, # 示例编码体育休闲/科教文化按高德最新分类表调整 city: 上海, citylimit: true, offset: 25, page: page, extensions: all } resp requests.get(base_url, paramsparams, timeout10) data resp.json() if data[status] ! 1: break pois data[pois] if not pois: break for poi in pois: lon, lat poi[location].split(,) poi_list.append({ name: poi[name], type: poi[type], lon: float(lon), lat: float(lat), address: poi.get(address, ) }) time.sleep(0.2) # 控制QPS避免触发高德限流 df_poi pd.DataFrame(poi_list) df_poi df_poi.drop_duplicates(subset[name, lon, lat]) print(df_poi.shape)这里有两个关键点要解释。第一types不传宽泛的“文体”而是传具体分类编码是为了让高德返回更干净的场馆数据避免混入餐饮和商铺。第二sleep(0.2)在单机爬虫里非常必要高德对个人开发者QPS默认限制很低并发太猛会直接报“USER_DAILY_QUERY_OVER_LIMIT”。实测中我一般把单日请求控制在2000次以内。爬回来的POI并不是直接能用。高德地图的信息收集存在滞后和缺失比如某新建的社区体育馆可能半年后才上线。我的做法是拉取后做一轮清洗先按行政区划和名称去重再对关键大型场馆虹口足球场、上海体育场、上海博物馆等维护一份人工名单用模糊匹配补全缺失项。经验性分析不是可选项而是这个场景的必要补充。2.3 基站轨迹数据的预处理与坐标系误差矫正基站数据来自运营商数据库一般有用户ID、时间戳、位置区码LAC、小区编号CI、经纬度等字段。它最常见的三个问题坐标系不统一、经纬度登记错误、人为录入误差。如果不处理后续融合的时候会出现大量“用户在场馆内但基站测出来在河里”的怪异结果。下面是用Python做粗过滤和坐标修正的示例import pandas as pd import numpy as np # 读取基站轨迹数据uid, time, lac, ci, lon, lat df pd.read_csv(base_station_trace.csv, names[uid, time, lac, ci, lon, lat]) # 1. 粗过滤限定在上海的大致经纬度范围内 lon_min, lon_max 120.85, 122.20 lat_min, lat_max 30.67, 31.88 df df[(df[lon] lon_min) (df[lon] lon_max) (df[lat] lat_min) (df[lat] lat_max)] # 2. 同一个基站lacci的坐标取中位数作为修正值 df[loc_key] df[lac].astype(str) _ df[ci].astype(str) median_coords df.groupby(loc_key)[[lon, lat]].median() df df.merge(median_coords, onloc_key, suffixes(_raw, _fixed)) # 3. 修正坐标与原始坐标偏差过大时用修正值 df[lon_final] np.where(abs(df[lon_raw] - df[lon_fixed]) 0.1, df[lon_fixed], df[lon_raw]) df[lat_final] np.where(abs(df[lat_raw] - df[lat_fixed]) 0.1, df[lat_fixed], df[lat_raw])这里的逻辑是先用城市范围把漂移到海外的点筛掉再按基站编号分组取中位数。取中位数而不是均值是因为均值容易受个别极端值影响中位数更能代表一个基站的“真实位置”。最后一步的偏差阈值0.1度大约11公里超过这个范围就认定原始坐标登记错误。注意这不是坐标转换真正的坐标系转换需要用高德或百度提供的坐标纠偏接口我一般在爬POI时统一用高德坐标系基站原始坐标如果是WGS-84还需要先转成GCJ-02再参与计算。下面这个表整理了我在项目中用到的误差处理方式方便你在别的数据集上复用误差来源实际表现处理方式坐标系不一致点位整体偏移几十米到几百米统一转为GCJ-02坐标系基站经纬度登记错误点位出现在水域、山区或城市外结合城市边界和地形数据过滤人为录入误差同一个基站出现多个相距很远的坐标按lacci分组取坐标中位数POI信息滞后新建场馆搜不到用人工重点场馆名单模糊匹配补全2.4 经验性分析弥补POI数据缺口经验性分析不是拍脑袋而是用低成本规则弥补结构化数据的盲区。比如高德POI分类里“虹口足球场”会被标为体育休闲服务但可能没有具体到“足球场”这个细类。这时候我会结合公开场馆运营名单和百科数据把名称和坐标对齐后再手工补充一个场馆类型字段。这件事看起来很土但能明显提升后续标签细化的准确性。实际项目里我还把重点场馆的关联半径单独维护了一份配置表防止被通用规则误伤。3. 三层数据融合构建POI-基站-人的关联模型数据清洗完成后下一步是把三个层级的数据融成可建模的宽表。这里最忌讳的是拿POI坐标和用户轨迹坐标直接算距离。下面说明为什么我要用三层结构以及每层之间的关联方式。3.1 为什么不直接匹配POI和用户轨迹假设用户正在虹口足球场看演唱会他的手机可能会接入足球场旁边的基站A也可能因为信号拥塞接入几百米外的大悦城基站B。如果你直接把用户轨迹坐标和POI坐标算距离基站B会让他被判成“去过商场”而不是“去过足球场”。引入基站这一层后一个POI会关联一组可能覆盖到它的基站只要用户落在这一组基站里就认为他有到访可能。这种“区域扩展”的方式把定位误差消化在关联关系里而不是苛求每一个坐标都精准。3.2 计算POI与基站的距离Haversine公式与关联半径要把POI和基站关联起来第一步是算球面距离。我一般不用sklearn里的欧式距离因为经纬度的单位长度在不同纬度差异很大。Haversine公式更合适from math import radians, sin, cos, sqrt, asin def haversine(lon1, lat1, lon2, lat2): R 6371.0 # 地球半径单位km dlon radians(lon2 - lon1) dlat radians(lat2 - lat1) a sin(dlat / 2) ** 2 cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon / 2) ** 2 return 2 * R * asin(sqrt(a))调用这个函数把一个POI周围一定半径内的基站找出来。返回值是公里和半径比较时要乘以1000转换成米。关联半径不能一刀切场馆面积越大覆盖基站越多半径也要相应放大。下表是项目里的初始配置POI类型示例关联半径(米)大型体育场虹口足球场800中型体育馆区级体育中心500博物馆/图书馆上海博物馆300小型文化场馆社区文化活动中心150关联半径直接影响结果的召回和精度。半径太小用户在场馆但没连上周边基站会被漏掉半径太大会把旁边购物中心的人也算进来。我一般先用半径扫描一遍计算每个POI关联的基站数量如果某个大型场馆关联不到5个基站就去查是POI坐标错了还是该区域基站密度太低。3.3 从基站序列提取用户到访特征有了POI和基站的关联表接下来要把用户轨迹数据处理成“用户-场馆-时间”的访问记录。以某用户一天的轨迹为例他经过的基站序列里只要连续命中某个POI关联基站集合并且累计停留时间超过一定阈值就算一次候选到访。下面是一段PySpark聚合逻辑from pyspark.sql import functions as F # poi_bs: (poi_id, bs_id) POI与基站关联表 # trace: (uid, bs_id, date, hour_minute, duration_min) 用户访问基站记录 joined trace.alias(t).join( poi_bs.alias(p), F.col(t.bs_id) F.col(p.bs_id) ) feature_df joined.groupBy(t.uid, p.poi_id).agg( F.countDistinct(t.date).alias(visit_days), F.sum(t.duration_min).alias(total_duration), F.avg( F.when(F.col(t.hour_minute).between(18:00, 22:00), 1).otherwise(0) ).alias(night_ratio) ) # 初始到访至少3天、累计1小时 labeled feature_df.withColumn( is_visit, (F.col(visit_days) 3) (F.col(total_duration) 60) )这段代码的逻辑是先通过基站ID把用户轨迹和POI关联再按用户和POI聚合。visit_days用countDistinct而不是count因为用户一天内可能多次经过同一个基站重复计数会让结果失真。total_duration是累计驻留分钟数night_ratio计算的是晚间时段访问占整体访问的比例。is_visit判定条件“至少3天、累计1小时”是我的初始值你需要根据自己的数据分布调整。如果用户是被动路过通常很难满足“3天、1小时”这个条件。3.4 标签细化从“到过”到“爱好”通过上面的代码我们能得到用户和文体场馆的访问关系但这还不能叫“爱好”。如果用户每个月去三次社区文化活动中心但他只是去那里上自习那给他打“科教文化爱好者”标签就有争议。所以在细化标签时我会再做两层处理第一层按POI的一级分类科教文化/体育休闲汇总每个用户的访问次数和时长只有当两类分数差异明显时才给他打对应的一级标签。第二层再根据具体场馆类型给用户打二级标签比如去过多次虹口足球场可以打“足球观赛”去过多次科技馆可以打“科技馆常客”。这里不需要复杂算法用简单的加权打分即可关键是要和业务方确认每个标签的使用场景。4. 活动时间识别用Z值检验替代黑箱模型判断场馆活动在用户画像素描中知道用户去了哪个场馆还要知道那个场馆当时是否有活动。同一个虹口足球场工作日晚上可能只有几百人有演唱会时会有数万人。如果不区分这两种状态标签会变得极不稳定。4.1 为什么选Z值检验而不是机器学习模型判断场馆是否在举办活动最直接的方法是爬票务平台但很多中小型场馆的活动不会上票务平台。另一个思路是训练分类器但活动样本太少正负样本极不均衡模型很容易过拟合。Z值检验的思路来自统计学也在基因表达异常检测里经常使用先计算某个场馆在相同时间段比如周三晚18-21点的历史访问人数均值和标准差再用当前人数减去均值除以标准差得到Z值。Z值越大说明当前相对于历史越异常。这个方法不需要标签数据只要历史访问人数统计足够稳定就能用。相比黑箱模型它更可控、更容易定位问题。4.2 工程实现分组计算Z值我把用户访问记录按时段聚合后对每个POI单独计算Z值而不是混在一起算因为大型体育场和小型文化馆的正常访客量级差别巨大混在一起会抹平异常信号。代码片段如下import pandas as pd import numpy as np # df: (poi_id, date, hour, count) 每个POI每小时访问人数 daily df.groupby([poi_id, date, hour], as_indexFalse)[count].sum() def zscore_for_poi(group): group[avg_count] group[count].transform(mean) group[std_count] group[count].transform(std) # std为0时用1代替避免除零 group[zscore] (group[count] - group[avg_count]) / group[std_count].replace(0, 1) return group daily daily.groupby(poi_id, group_keysFalse).apply(zscore_for_poi) events daily[daily[zscore] 3]这里有个细节一定要先按poi_id分组再计算均值和标准差不能把所有POI放在一起算。因为大型体育场和小型文化馆的正常访客量级差别巨大混在一起会抹平异常信号。阈值3对应约99.7%置信水平实际项目中如果数据量少我会放宽到2.5然后通过人工验证看准确率。4.3 用真实活动验证识别结果识别出活动时段后必须用已知活动做验证。当时我直接用五月天演唱会做校验效果如下日期场馆识别Z值实际活动2019-11-01虹口足球场18.4五月天演唱会2019-11-02虹口足球场21.7五月天演唱会2019-11-03虹口足球场16.9五月天演唱会2019-11-04虹口足球场1.3无活动上面Z值很高是因为演唱会在18点后带来的访问人数是日常的数倍异常信号非常强。验证时除了看活动安排表还要再做用户级抽样抽取活动时段内被识别为“到访”的用户看他们是否在后续时段再次出现在相关场所或者是否匹配到票务特征。多角度验证可以防止把路过人群当活动参与人群。4.4 结果收敛性分析与阈值调整模型上线后需要每天观察输出的标签人数是否稳定。项目里连续五天的累计去重人数如下统计日期当日模型输出累计人数去重新增不重复人数20191110227462227462227462201911112189263578831304212019111230432044928914052201911133006555170567767201911143106335634204636可以看到输出人数每天都在30万左右波动但新增不重复人数快速衰减说明标签库在收敛不会无限膨胀。如果某一天新增不重复人数突然放大我一般先看是否有新的POI被加入关联表或者某个基站的坐标发生了漂移。这个“收敛性检查”比单纯看准确率更能反应工程稳定性。它也是标签系统上线后最值得做的一个监控指标。5. 标签上线前必须做好的四件事从样本回测到推荐模型接入标签不是算出来就能直接上线尤其是在生产环境里一个错误的标签会影响下游推荐、运营触达等一系列动作。最后这四个检查项是这个项目里我认为最值得复用的部分。5.1 用户级抽样回测总量准确不代表用户准确。上线前我会随机抽2000个被标记用户人工核对他们的行为轨迹确认不是被动路过。如果准确率低于85%先调整关联半径和停留时长阈值不要急着上线。5.2 用滑动窗口做增量重算原始项目里Hadoop可调用的计算资源有限无法支持每天全量训练。我用的是“30天滑动窗口”做增量重算每三天跑一次最近30天的数据中间日期沿用上次的标签只在窗口内新增或撤销标签。这样既能保证时效性又能把计算压力控制在一个可接受的范围。5.3 数据源变更监控高德POI会更新运营商的基站坐标也会变。我每天会对比POI数量和基站坐标分布如果某个区域的POI数量在一天内突然增加10%以上大概率不是真实新增而是数据源格式或坐标系出了问题。发现问题后我宁可停更一天也不要把脏数据合入标签库。5.4 把标签作为推荐模型特征时的接入方式用户画像在工业场景里通常是推荐模型的前置。直接把“文体爱好者”这个二值特征丢进模型信息量太薄。我一般会把标签转成multi-hot稀疏特征例如“科教文化-博物馆”“体育休闲-足球”各占一维再叠加Z值作为置信度权重。这样既能保留细分信息又避免了硬规则过滤带来的召回损失。另外上线后每周看一次标签收敛曲线。如果新增不重复人数连续多日放大优先排查关联半径和POI去重两个环节这是这个项目里最容易出问题的地方。不要试图用更复杂的模型掩盖数据问题数据源稳定了标签自然稳定。本文还有配套的精品资源点击获取