ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

迁徙指数数据获取与分析:从Python抓取到时间序列挖掘

2026/9/11 22:49:02 拓冰建站 浏览量
迁徙指数数据获取与分析:从Python抓取到时间序列挖掘 简介迁徙指数数据包源自百度迁徙平台覆盖2022年1月1日至5月13日并附带2021年全年及2019、2020年部分历史数据。面向研究人口流动、城市网络与疫情防控政策的科研人员、政府机构及商业分析者可用来分析城际迁徙强度、迁入迁出峰值与趋势变化验证城市吸引力及社会经济活动等研究假设。数据包共10个文件主体为8个开放式XML工作表与2个关系定义文件压缩包大小3.07MB工作表内应包含日期、城市、迁入指数、迁出指数等字段可结合Excel/Pandas/R等工具做清洗、折线图绘制与关联分析提取不同时段、区域的迁徙特征。这些数据对理解节假日人口迁移规律与区域管控效果尤其有帮助。目前已有238人浏览学习适合需要获取跨年度人口流动样本的课题或行业应用场景。1. 迁徙指数数据截止2022.5.13真正值钱的是时间边界这个标题看起来像一份静态表格实际上背后是一整套人口流动观测体系。移动互联网时代百度迁徙、腾讯位置大数据、联通智慧足迹这类平台通过位置服务与手机信令把一座城市每天迁入迁出的规模换算成指数对外发布。这个指数的量纲不是人次而是相对于基准日的归一化强度直接拿两个城市或两个年份的数值比较必须先确认口径。截止2022.5.13这个时间点让数据变成冻结快照之后上游平台无论怎么调整算法这份切片都不会再变这对历史回溯和模型回测来说是最理想的性质。下面按对齐口径、抓取数据、清洗落库、分析调参、质量校核的顺序展开适合每天跟迁徙数据打交道的分析师和数据工程师。2. 迁徙指数数据的字段口径与抓取参数2.1 迁徙指数是相对强度不是真实客流迁徙指数的原始数据来自用户授权的位置上报、基站切换和 Wi-Fi 探针平台方把一次完整的跨城市移动识别为一次迁徙事件。但直接暴露真实人次既涉及隐私也会因为各平台覆盖用户比例不同而产生口径差异所以对外发布的统一是归一化指数。常见的做法是选定一个基准日把该日的迁徙事件数定义为 100其他日期按比例换算。举例来说某城市迁入指数为 120意思是当日迁入事件数比基准日多了两成而不是有 120 个人进城。理解了这一点就能避开两个常见错误。第一不同平台的指数不能直接混用因为它们的用户池和基期不同A 平台的 120 和 B 平台的 120 没有可比性。第二指数可以在同一个平台内部做时间趋势比较但不能把一年 365 天的指数直接加总当作年度客流总量这是量纲问题不是计算精度问题。拿到截止2022.5.13的数据集时第一步不是看数字而是确认这份数据的来源平台和基期规则否则后续所有报表都建立在不可比的基础上。2.2 cityrank.jsonp 接口的关键参数与最小抓取代码百度迁徙的cityrank接口是公开可访问的返回格式为 JSONP。用 Python 抓取时先做一层解包把回调函数名剥掉再交给标准库解析。import json import pandas as pd import requests def fetch_city_rank(date_str: str, city_id: str, move_type: str move_in, scope: str nationwide) - pd.DataFrame: 抓取指定日期的城市级迁徙排行。 date_str: 日期字符串格式为 YYYYMMDD例如 20220513 city_id: 目标城市行政区划编码例如 110000 表示北京 move_type: move_in 表示迁入目标城市move_out 表示迁出 scope: 统计范围nationwide 表示全国范围内统计 url https://huiyan.baidu.com/migration/cityrank.jsonp params { dt: city, id: city_id, date: date_str, scope: scope, type: move_type, } resp requests.get(url, paramsparams, timeout15) text resp.text # 返回体形如 b({...})截取第一个左括号到最后一个右括号之间的 JSON payload json.loads(text[text.index(() 1: text.rindex())]) rows payload[data][list] return pd.DataFrame(rows)这段代码把参数集中在params字典里方便后续换成批量参数组合。dtcity表示按城市维度聚合如果不传这个参数部分版本会返回全国总量或按省份聚合字段结构完全不同。type决定方向move_in返回的是从哪些城市迁入到目标城市move_out返回的是目标城市的人迁往了哪些城市。接口返回的list中通常包含城市名、城市编码和指数值城市编码在后续清洗时统一补齐 6 位避免 10000 和 100000 被当成同一座城市。参数示例值作用dtcity聚合粒度city 为城市级id110000目标城市区划编码date20220513查询日期精确到天scopenationwide统计范围固定传 nationwide 即可typemove_in / move_out迁徙方向决定返回结果含义请求频率要控制连续抓取时每两次请求至少间隔 0.5 秒。接口并没有承诺不限流短时间内高并发容易触发验证页返回的内容不再是 JSONP 而是一段 HTML此时json.loads会直接抛异常。把这一层异常捕获放到批量任务里单个日期抓取失败只记录日志不中断整个任务。2.3 截止 2022.5.13 隐含的时间序列特征2022 年 5 月 13 日是星期五往前推两周是五一假期。迁徙指数在假期前后呈现出非常典型的节前集中迁出、节后集中迁入形态而 5 月 13 日已经回到常规通勤节奏周内周期开始主导周一至周五迁出大于迁入周末相反。这个日期切片适合用来研究静态时段特征但不适合用来推断全年规律因为季节性和节假日效应在这份截断数据里只覆盖了冬季到春末这一段。另一个容易忽略的点是截止 2022.5.13 意味着数据不是流式追加的。做预测模型时只能用 5 月 13 日及之前的数据做训练之后的真实值没有出现在数据集中这天然规避了数据泄漏。很多时间序列项目所谓的验证集漂移在这个固定切片上根本不存在这是它做回测实验的天然优势。3. 用 Python 把迁徙指数数据落成本地时间序列3.1 批量抓取时如何对日期循环与异常续跑单日抓取只是验证接口连通性真正做分析要连续抓取至少 90 天。把日期列表传给循环函数每个日期单独调用fetch_city_rank抓到的数据先暂存内存全部完成后统一合并。import time from pathlib import Path def fetch_period(date_list, city_id, move_type, interval: float 0.5, retry: int 2) - pd.DataFrame: frames [] for day in date_list: for attempt in range(retry 1): try: df fetch_city_rank(day, city_id, move_type) df[date] day frames.append(df) break except Exception as exc: print(f{day} 第 {attempt 1} 次抓取失败: {exc}) time.sleep(interval * (attempt 1)) time.sleep(interval) return pd.concat(frames, ignore_indexTrue)这段代码把失败重试和限速合并在一起。retry控制每个日期的最大尝试次数第一次失败后等待 0.5 秒第二次失败等待 1 秒重试仍失败就跳过该日期。设计上要容忍局部缺失因为后续清洗阶段会专门处理空窗日期。抓取结束后立刻把原始数据落盘不要只留在内存里常见的做法是保存成 parquet 文件并带上抓取时间戳避免因进程崩溃而重新抓一遍。3.2 清洗时必须统一日期、城市编码和重复值线上接口返回的城市名存在同名问题比如吉林既是省名也是市名朝阳在辽宁和北京都存在。清洗时不能只按城市名去重必须以城市编码为唯一键。下面这段清洗逻辑覆盖了三个高频问题日期格式转换、城市编码补零、按城市和日期去重。def normalize_migration(df: pd.DataFrame) - pd.DataFrame: df df.copy() # 日期列统一成 datetime 类型 df[date] pd.to_datetime(df[date], format%Y%m%d) # 城市编码补齐 6 位避免 1100 和 110000 被当成不同城市 df[city_code] df[city_code].astype(str).str.zfill(6) # 部分接口版本带 province 字段用它做城市名消歧 if province in df.columns: df[city_name] df[province] · df[city_name] # 同一城市同一天只保留一条记录 df df.drop_duplicates(subset[date, city_code, city_name]) return df.sort_values([city_code, date]).reset_index(dropTrue)清洗的关键不在于代码量而在于排序和去重的键选择。drop_duplicates如果不指定subset会因为接口返回的排名列表中城市名重复而误删有效数据。排序用[city_code, date]保证后面做groupby时每个城市的时间序列是顺序排列的否则算环比时前后颠倒结果没有意义。清洗后要检查每个城市的日期覆盖天数如果目标城市 90 天数据里只有 60 天记录说明中间有抓取失败或接口屏蔽需要补抓。3.3 以宽表还是长表落库分析场景不同表结构完全不同。趋势分析适合长表一行一个城市一天的数值聚类和相关性分析适合宽表每行一个城市、每列一个日期。落库时我一般直接建长表分析时再透视成宽表这样保留最大灵活性。CREATE TABLE migration_index ( date Date, city_code String, city_name String, move_in Float32 COMMENT 迁入指数, move_out Float32 COMMENT 迁出指数, dt String DEFAULT 2022-05-13 COMMENT 数据快照截止时间 ) ENGINE MergeTree() PARTITION BY toYYYYMM(date) ORDER BY (city_code, date);表结构里特意加了dt字段记录数据快照截止时间这是这类固定切片数据集最容易忽略的元数据。同一张表以后可能追加新批次数据没有dt列就无法区分哪些行属于 2022.5.13 之前的旧切片。ORDER BY选(city_code, date)让同一个城市的数据在存储上连续查询单城市时间序列时只需要扫一个分区段不需要全表扫描。PARTITION BY toYYYYMM(date)按月分区删除某个月的数据直接 drop 分区即可。4. 迁徙指数数据的分析玩法与参数调优4.1 用净迁入率替代原始指数做城市吸引力对比原始迁徙指数受城市人口基数影响极大北京和一座三线城市的迁入指数不在同一量级直接对比无法体现相对吸引力。常见做法是把同一城市同一天的迁入和迁出放在一起计算净迁入率消除城市规模带来的偏差。def add_net_ratio(df: pd.DataFrame) - pd.DataFrame: df df.copy() total df[move_in] df[move_out] # 净流入率范围在 [-1, 1] 之间 df[net_ratio] (df[move_in] - df[move_out]) / total.replace(0, pd.NA) return df净迁入率为正说明当天人口净流入为负说明净流出接近 0 说明双向流动均衡。分母用迁入加迁出的总和而不是城市常住人口因为迁徙指数本身是相对值两个相对值做除法时作为基准的比例效应会被抵消。这个指标对数值的高低不敏感更适合跨城市横向比较。4.2 同日同环比参数怎么选才不踩周内周期迁徙指数有很强的星期周期性工作日的迁出指数普遍高于周末高铁和航班时刻表也强化了这种规律。直接拿 5 月 13 日和前一天的 5 月 12 日做环比两个都是工作日结果相对平稳但如果拿 5 月 13 日周五和 5 月 12 日周四比完再拿 5 月 14 日周六和 5 月 13 日比数值会出现剧烈波动这不是突发事件只是周期性。# 日环比反映短期波动但会受周内周期干扰 df[dod] df.sort_values(date)[move_in].pct_change(1) # 周同比消除星期效应适合观察真实趋势变化 df[wow] df.sort_values(date)[move_in].pct_change(7) # 年同比需要至少 1 年数据季节效应被完全消除 df[yoy] df.sort_values(date)[move_in].pct_change(365)对比类型窗口长度适用场景日环比1 天突发事件检测、政策影响评估周同比7 天常规趋势分析推荐首选年同比365 天长期结构性变化需要满一年数据参数选择上pct_change(7)是处理迁徙数据最稳妥的起点。它把每个周五和上周五相比两个同类星期几做差周内周期被剔除。如果拿着截止 2022.5.13 的数据只算日环比会出现大量虚高的波动峰值分析结论很容易做反。4.3 用 KMeans 聚类识别城市流动模式时先做标准化城市迁徙序列的形态比绝对值更有分析价值。有的城市全年迁出平稳只在春节前后出现尖峰有的城市在暑期出现持续两个月的迁出抬升。把这两种城市都丢进聚类算法如果不做标准化平稳城市的数值差异会被尖峰城市的极值淹没聚类结果几乎完全由人口规模决定。from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler def cluster_cities(df: pd.DataFrame, n_clusters: int 4): # 透视成宽表行是城市列是日期 pivot df.pivot_table(indexcity_code, columnsdate, valuesmove_in) # 每个城市的序列独立标准化消除规模差异 scaler StandardScaler() X scaler.fit_transform(pivot.fillna(pivot.median())) model KMeans(n_clustersn_clusters, random_state42, n_init10) labels model.fit_predict(X) return pivot.index, labelsStandardScaler对每一列做标准化但在宽表里每一列是一个日期标准化后每个日期的均值变为 0实际是把所有城市在单日的水平差异抹平。聚类算法由此关注的是城市序列的形态相似性而不是数值高低。n_init10是 KMeans 的常规设定防止单次初始化落入局部最优如果隐式聚类数量不清楚用轮廓系数遍历 2 到 6 选一个曲率拐点比直接拍脑袋定 4 更有说服力。5. 用节假日效应快速校核迁徙指数数据质量拿到外部数据集先别急着建模用已知的节假日效应反向验证数据可靠性成本最低。2022 年 5 月 13 日前最近的大型节假日是五一劳动节节前最后一个工作日 2022.4.29 的迁出指数应当明显高于前后一周的均值迁入大省如广东、浙江还应该在节后 5.4 到 5.5 出现迁入反弹。如果数据集里看不到这两个峰先说数据有问题别先怀疑业务逻辑。def check_holiday_signal(s: pd.Series, holiday: str, pre_days: int 3): 检查指定日期前是否出现迁徙强度尖峰。 s s.sort_index() holiday pd.Timestamp(holiday) pre_window s.loc[holiday - pd.Timedelta(dayspre_days): holiday] threshold s.median() * 1.5 if pre_window.max() threshold: print(f警告: {holiday.date()} 节前未出现超过中位数 1.5 倍的迁徙尖峰) return False return True阈值的选取不用太严格迁徙指数在节假日前通常会上升到日常中位数的 2 倍以上取 1.5 倍已经能过滤大部分误判。这段校验不需要每一天都看每天只取迁出指数的最大值所以速度很快300 天数据十秒钟内能跑完。第二个校核手段是双源交叉验证。如果手里只有百度迁徙的 2022.5.13 截止数据可以抓取同一天的腾讯迁徙指数虽然两者绝对数值不同但同方向的变化趋势应当高度相关。把两个来源按城市对齐后计算 Spearman 相关系数正常城市之间相关系数在 0.8 以上低于 0.5 的城市要么是数据缺失导致对齐错位要么是抓取时日期参数串了。相关系数矩阵不需要做完整版只抽查迁入迁出排名前 20 的城市半小时就能定位全链路中哪一环出了问题。最后一个常用校核手段是周期性自洽检查。固定切片下做不了回测但能验证数据是否满足星期周期同一个城市任意相邻两周的同一星期几迁出指数差值的绝对值不应普遍超过 50%。逐城市计算diff的标准差如果超过这个阈值大概率是混入了其他日期或城市的数据回到第 3 章的drop_duplicates和sort_values重新核对。这一套校核做完数据质量就有底了再往后无论做城市群识别还是节假日客流预测前提都是同一份可信的时间序列。本文还有配套的精品资源点击获取