Python解析百度迁徙指数数据:从Excel XML到人口流动分析
简介这份数据包提供百度迁徙指数数据覆盖2022年1月1日至5月13日并含2021年全年及2019、2020年部分时段可用于人口流动态势、节假日出行、疫情防控效果等研究场景适合研究人员、政策分析者及数据分析学习者。压缩包为RAR格式、大小3.07MB共10个文件以8个XML文件和2个关系文件为主XML中分别承载工作簿、工作表、样式及共享字符串等数据定义便于结构化读取和二次分析。已有238人学习使用。数据以表格形式记录日期、城市、迁入/迁出指数等字段可使用Excel、Python Pandas或R语言进行清洗、汇总与可视化计算各城市迁徙强度及时间序列趋势或结合节假日、疫情政策等外部因素挖掘人口流动规律为城市吸引力评估、交通规划与公共资源配置提供量化依据。1. 迁徙指数数据到底怎么读——一份Excel背后的Open XML结构如果你拿到的是.rar压缩包而不是数据库第一反应别急着双击打开Excel。这个名为“迁徙指数数据截止2022.5.13”的资源本质上是一个标准的Open XML格式工作簿里面藏着百度迁徙平台qianxi.baidu.com的日度人口流动指数覆盖2022年1月1日到5月13日、2021年全年以及2019和2020年的部分区间。解压后你会看到docProps、xl、_rels这些目录xl/worksheets/sheet1.xml才是真正存数据的载体。直接打开Excel看没问题但如果你想做时间序列分析、城市网络建模或者把多个工作表合并成一张宽表就必须理解这个文件的物理结构否则“读取数据”这一步就会卡住。下面从解压结构开始带你把它拆成可复用的数据集。2. 从RAR到DataFrame迁徙指数数据的解析与清洗2.1 认识Open XML文件内部角色的职责[Content_Types].xml是整个包的“类型声明表”它告诉Excel解析器每个扩展名对应的内容类型。比如/xl/worksheets/sheet1.xml被声明为application/vnd.openxmlformats-officedocument.spreadsheetml.worksheetxml而/xl/styles.xml对应样式定义。_rels/.rels是包级关系文件记录工作簿与文档属性、核心组件之间的引用xl/_rels/workbook.xml.rels则把workbook.xml和各个sheet关联起来。docProps/core.xml保存作者、创建时间等元数据docProps/app.xml记录应用程序版本和工作表数量。这些文件不是摆设当你在Pandas里用read_excel读取时底层库会先解析这些关系定位到sheet1.xml再按sharedStrings.xml中的字符串表去还原单元格文本。如果迁移指数数据里的城市名被存成共享字符串直接解析XML时就会遇到“只有数字索引没有具体名字”的问题。xl/workbook.xml定义了工作簿里的sheet列表、名称和顺序。这个迁移指数包通常有多个sheet每个可能对应不同年份或不同指标。xl/worksheets/sheet1.xml的结构是sheetData下按行、列组织的c元素属性ts表示该单元格是共享字符串tstr表示公式字符串无t属性则默认是数字。xl/theme/theme1.xml只影响颜色字体对数据无意义。理解这些之后你可以完全不依赖Excel直接提取数据这对自动化分析尤为重要。2.2 用Python解析原始XML提取迁徙数据既然拿到了原始文件我一般会先解压.rar再用xml.etree.ElementTree直接解析sheet1.xml而不调用Pandas的read_excel。原因有二第一源数据里可能存在合并单元格、空行或自定义格式read_excel会把空值变成NaN但你可能需要区分“真实缺失”和“无数据”第二直接解析XML能够保留单元格的数据类型避免index被自动转成int64或object。下面是一段提取思路的代码import zipfile import xml.etree.ElementTree as ET XLSX_PATH 迁徙指数数据.xlsx SHEET_XML xl/worksheets/sheet1.xml # 先读取共享字符串表用于映射字符串索引 shared [] with zipfile.ZipFile(XLSX_PATH) as zf: if xl/sharedStrings.xml in zf.namelist(): root ET.fromstring(zf.read(xl/sharedStrings.xml)) ns {m: http://schemas.openxmlformats.org/spreadsheetml/2006/main} for si in root.findall(m:si, ns): # 处理单个文本或多个runs合并的情况 text .join(t.text or for t in si.iter({http://schemas.openxmlformats.org/spreadsheetml/2006/main}t)) shared.append(text) # 解析sheet1.xml root ET.fromstring(zf.read(SHEET_XML)) ns {m: http://schemas.openxmlformats.org/spreadsheetml/2006/main} rows [] for row in root.findall(.//m:sheetData/m:row, ns): row_data {} for cell in row.findall(m:c, ns): ref cell.get(r) # 如 A1, B2 col .join(ch for ch in ref if ch.isalpha()) v cell.find(m:v, ns) value v.text if v is not None else if cell.get(t) s: value shared[int(value)] if value else row_data[col] value rows.append(row_data) print(rows[:5])这段代码先读取sharedStrings.xml把字符串索引映射成真实城市名再遍历sheetData下的每一行按单元格的r属性提取列字母从而得到{列名: 值}的字典。逻辑说明ts时v标签里的数字是共享字符串的下标用shared[int(value)]还原文本普通数字单元格直接取文本值。参数说明ns是Open XML的命名空间必须带上row.findall中.//m:sheetData/m:row是路径表达式匹配所有行。如果你想跳过空白行可以判断row_data是否全为空。这种做法比read_excel更可控尤其适合需要抽取多个sheet或跨表合并的场景。2.3 数据清洗与字段标准化原始sheet里的列名可能是中文比如“城市”“迁入指数”“迁出指数”也可能有“日期”列但日期格式可能是2022-01-01、2022/1/1或Excel序列号。我建议统一改成英文小写字段名方便后续用Pandas操作。下面的代码演示了从解析结果转换到DataFrame并处理常见脏数据import pandas as pd # 假设rows就是上面解析出来的列表每个字典有A/B/C等列键 # 通过第一行映射列名0-date, 1-city, 2-in_index, 3-out_index col_map {A: date, B: city, C: in_index, D: out_index} df pd.DataFrame([{col_map[k]: v for k, v in row.items() if k in col_map} for row in rows]) # 去掉没有数据的行 df df.dropna(howall) df df[df[date].str.match(r\d{4}[-/.]\d{1,2}[-/.]\d{1,2})] # 日期标准化 df[date] pd.to_datetime(df[date], format%Y-%m-%d, errorscoerce) # 数值列转float强制错误为NaN df[in_index] pd.to_numeric(df[in_index], errorscoerce) df[out_index] pd.to_numeric(df[out_index], errorscoerce) df df.dropna(subset[date, city]) # 城市名去除空格和罕见字符 df[city] df[city].str.replace(r\s, , regexTrue) print(df.head())这里的逻辑是先用col_map把字母列映射成含义明确的字段名date列用正则匹配筛选出看起来像日期的行避免表头或多级标题混入pd.to_datetime统一日期格式errorscoerce把无法解析的时间变成NaTpd.to_numeric做同样处理。参数说明format%Y-%m-%d要求输入必须是这种格式如果遇到2022/1/1这类斜杠格式可以改成format%Y/%m/%d或直接去掉format让Pandas自动推断。清洗完成后一个干净、可复用的长表就出来了接下来才能谈分析。3. 迁徙指数的时间序列分析从日粒度看人口流动3.1 数据聚合与重采样百度迁徙指数是日粒度数据每个城市每天有一个迁入指数和一个迁出指数。要观察全国整体趋势我通常先按日期聚合所有城市的总迁入/总迁出。用Pandas的groupby加resample非常直接daily df.groupby(date)[[in_index, out_index]].sum() # 重采样到周粒度取均值消除周末波动 weekly daily.resample(W).mean() # 取出2022年春节前后各14天的数据看峰值 import datetime spring_2022 daily.loc[2022-01-31:2022-02-15] print(spring_2022.head())聚合逻辑把同一天的多个城市指数相加得到全国总迁徙强度resample(W)按周默认以周日结束聚合mean()计算平均值。注意groupby后直接sum()会把空日期跳过如果你想保留所有自然日应该先df.set_index(date).resample(D).sum()重建完整日历。参数说明resample(W)的W是周频率可以使用W-MON指定以周一结束。这样做的意义在于消除工作日与周末出行的结构性差异让趋势线更平滑。春节前后的数据最有看点。2022年春运从1月17日到2月25日百度迁徙指数会在除夕前达到峰值然后在大年初一断崖式下跌。你可以自己验证这个日期区间发现迁入指数峰值一般出现在腊月二十八到除夕而迁出指数在放假前一周达到高点。对比2021年同期会发现2022年峰值明显低于2021年这直接反映了局部疫情对人口流动的抑制。3.2 迁徙强度的峰值识别与节假日效应找峰值不能只靠肉眼我习惯用scipy.signal.find_peaks来识别局部极大值。对于日度迁徙指数序列设置distance参数避免在同一个节假日周期内找到多个相邻点。from scipy.signal import find_peaks import numpy as np values weekly[in_index].values peaks, properties find_peaks(values, distance4, prominence0.1*values.std()) peak_dates weekly.index[peaks] peak_values values[peaks] for d, v in zip(peak_dates, peak_values): print(f峰值日期: {d:%Y-%m-%d}, 迁入指数均值: {v:.1f})distance4表示两个峰值之间至少间隔4个样本这里因为我们用的是周数据所以4周内不重复计数prominence用于筛选显著峰值防止把微小波动当峰。这个方法的参数需要根据数据量调整——如果按日粒度找distance可以设成7避免一周内重复找峰。输出结果里你应该能看到2021年清明节、五一、国庆等典型峰值以及2022年元旦和春节前的峰值。用同样的参数去跑2020年数据你会发现2月份出现一个极低谷这是当年防控措施生效的直接证据。3.3 对比2021与2022年同期迁徙指数“同期对比”是分析政策影响最常用的手段。我的做法是把两年的数据切到相同日期范围然后画在同一个坐标轴里。这里不需要绘制复杂图像但计算差异指标是有用的# 只看1月1日到5月13日 key_ranges {2021: 2021-01-01~2021-05-13, 2022: 2022-01-01~2022-05-13} segments {year: daily.loc[start:end] for year, (start, end) in ...} # 更简单直接筛选 d2021 daily.loc[2021-01-01:2021-05-13] d2022 daily.loc[2022-01-01:2022-05-13] # 计算日均差异和最大萎缩幅度 avg_diff d2022[in_index].mean() - d2021[in_index].mean() ratio d2022[in_index].mean() / d2021[in_index].mean() - 1 # 找出差距最大的10天 gap d2021[in_index] - d2022[in_index] worst10 gap.nlargest(10) print(f2022年迁入指数均值同比变化: {ratio:.1%}) print(worst10)这段代码的要点是先用布尔切片提取两个年份的同期数据然后直接计算均值的差和变化率。gap d2021[in_index] - d2022[in_index]会按日期索引对齐如果两个序列的日期不完全一致Pandas会自动取并集并产生NaN这时需要fillna或者用inner连接。nlargest(10)返回差距最大的10个日期通常这些日期正好对应2022年3月上海、吉林等地的全员核酸或静态管理节点。需要注意的是百度迁徙指数本身是无量纲的相对值不能直接解读为“人数”但比较同一城市、同一季节的相对变化是有意义的。4. 城市级迁徙网络的构建与分析4.1 迁入迁出矩阵的构造每个城市每天的迁入指数和迁出指数是汇总值但百度迁徙平台还提供了城市对之间的“城市迁徙OD”数据通常是一个城市到另一个城市的指数。如果你的sheet1.xml里包含类似“来源城市”“目的地城市”的字段那么可以构造一个有向矩阵。即使没有OD只有单城市的流入流出也可以通过构造宽表来做城市间的相似度分析。假设我们有一个OD长表字段为date, from_city, to_city, value那么按月聚合的矩阵如下od_matrix od_df.groupby([from_city, to_city])[value].sum().reset_index() # 生成 城市x城市 矩阵缺失值填0 pivot od_matrix.pivot(indexfrom_city, columnsto_city, valuesvalue).fillna(0) # 保存到CSV pivot.to_csv(city_od_matrix.csv, encodingutf-8-sig)groupby对每个城市对求和pivot将行变成出发城市、列变成目的城市fillna(0)是为了让后续矩阵运算避开NaN。注意pivot要求每个行列组合唯一如果存在重复项需要提前聚合。这个矩阵的规模是N x NN是城市数量常见分析包括计算净流量排名、识别强连接对等。如果你手里只有单城市指数也可以按“迁入指数高、迁出指数高”的城市来推测其核心网络地位。4.2 城市净迁徙指数计算净迁徙指数 迁入指数 - 迁出指数。这个指标直观反映一个城市在特定时段是人口净流入还是净流出。对于春节这样的节日一二线城市在节前表现为净流出迁出指数远大于迁入而节后则反过来。计算逻辑很简单但要分城市分时段看才能真正说明问题city_daily df.set_index(date).groupby(city)[[in_index, out_index]] net city_daily.apply(lambda x: x[in_index] - x[out_index]).reset_index() net.columns [city, date, net_index] # 按城市看全年累计净迁徙 annual_net net.groupby(city)[net_index].sum().sort_values() print(annual_net.head(10)) # 净流出最大的城市 print(annual_net.tail(10)) # 净流入最大的城市这里用了apply逐小时计算差值注意groupby后的apply返回的是Series我们把它reset_index转成DataFrame。其实更高效的方式是直接df[net] df[in_index] - df[out_index]再按城市求和。净指数的绝对值没有实际人口数含义但排序结果往往和城市经济活跃度一致北上广深在春节期间净流出明显三四线城市净流入。如果你想观察同一城市不同月份的净迁徙变化可以再按月分组计算均值会看到流向随假期和工作机会波动。4.3 基于迁徙指数做城市吸引力排序迁徙指数本质上是个“相对引力”指标可以用来做城市吸引力排序。我习惯把一年内每一天的城市迁入指数累加然后除以该城市的迁出指数得到“流入/流出比”比值大于1说明该城市整体吸引力强于辐射力。但要注意百度指数的口径是“城际流动”省内春运返乡大潮会严重拉低一线城市的比值所以更稳妥的是只取2月-4月的工作日做排序剔除节假日干扰。# 选择工作日数据周一~周五 workday_mask df[date].dt.dayofweek 5 df_workday df[workday_mask] # 按城市计算平均迁入指数与平均迁出指数 attraction df_workday.groupby(city)[[in_index, out_index]].mean() attraction[ratio] attraction[in_index] / attraction[out_index] # 只看人口规模前50城市避免小城市失真 top50 attraction.nlargest(50, in_index) print(top50.sort_values(ratio, ascendingFalse).head(10))代码中dayofweek 5过滤掉周六日这样能减少周末旅游流的大量干扰。nlargest(50, in_index)先选出迁入指数规模最大的50个城市然后按比值排序。参数说明如果数据是2022年1-5月的那么只用这5个月的工作日可能包含春节放假那几天应该再把法定节假日排除。更严谨的做法是把df_workday再剔除中国法定假期但这需要额外的节假日表。这个排序结果可以作为商业选址、区域经济研究的一个辅助维度但不能单独作为结论——百度迁徙指数覆盖的是使用移动服务的用户样本存在一定的偏向性。5. 数据落库与自动化更新把迁徙指数用起来5.1 将清洗结果写入SQLite分析完之后把数据存入本地数据库能极大方便后续查询和增量更新。SQLite不需要单独部署服务适合做单机数据管理。我用Pandas的to_sql把清洗后的长表写进去并建立索引加快按日期和城市过滤的速度import sqlite3 conn sqlite3.connect(migration_index.db) df.to_sql(migration_index, conn, if_existsreplace, indexFalse) # 创建索引 conn.execute(CREATE INDEX idx_date ON migration_index(date)) conn.execute(CREATE INDEX idx_city ON migration_index(city)) conn.commit() # 验证 query SELECT * FROM migration_index WHERE city 北京 AND date 2022-03-01 LIMIT 5 sample pd.read_sql_query(query, conn) print(sample) conn.close()to_sql的if_existsreplace会删除旧表重写适合一次性全量导入indexFalse避免把DataFrame的索引写入数据库。索引建立后按日期范围或城市查询的速度会提升很多尤其在数据量达到几十万行时。参数说明SQLite的日期字段最好存储为TEXT格式因为Pandas的datetime64会被转成字符串而这个字符串在SQLite里按字典序比较仍然符合时间顺序所以date 2022-03-01能正确过滤。5.2 增量更新的思考百度迁徙平台每天发布前一天的指数数据所以定期抓取和更新是很有可能的。常见做法是每天从HTTP端点拉取最新数据然后追加到SQLite。关键在于去重因为网络波动可能导致重复拉取所以应在表中增加唯一约束比如(date, city)的唯一索引CREATE UNIQUE INDEX idx_unique_date_city ON migration_index(date, city);但如果你已经用to_sql写入再添加唯一索引可能会因为已存在脏数据而失败。我一般会在写入前先SELECT检查该日期的城市数据是否已存在new_data pd.DataFrame({date: [2022-05-14], city: [广州], in_index: [12.3], out_index: [9.8]}) # 检查是否已存在 exists pd.read_sql_query(SELECT COUNT(*) as cnt FROM migration_index WHERE date ? AND city ?, conn, params(2022-05-14, 广州)).iloc[0][cnt] if exists 0: new_data.to_sql(migration_index, conn, if_existsappend, indexFalse)这里用params绑定参数防止SQL注入虽然数据是本地可信的但养成习惯没坏处。如果每天跑这个脚本建议搭配一个last_update表记录当前拉取到的最大日期避免每次扫描全表。5.3 可视化仪表板的快速实现不用上重型BI工具用plotly做一个交互式的日度曲线和城市排名图然后输出成HTML文件就能直接分享。import plotly.express as px # 读取全国日度总迁徙指数 daily_all pd.read_sql_query( SELECT date, SUM(in_index) as total_in, SUM(out_index) as total_out FROM migration_index GROUP BY date ORDER BY date , conn) fig px.line(daily_all, xdate, y[total_in, total_out], title全国迁徙指数日变化) fig.update_layout(yaxis_title指数值, legend_title指标) fig.write_html(migration_dashboard.html) # 同时生成一个城市近30天排名表 city_last30 pd.read_sql_query( SELECT city, AVG(in_index) as avg_in FROM migration_index WHERE date date(2022-04-14) GROUP BY city ORDER BY avg_in DESC LIMIT 20 , conn) print(city_last30)这段代码用SQL直接完成聚合避免把全量数据载入内存。plotly.express.line会自动把宽格式的total_in和total_out转换为两条线write_html输出一个自带交互工具的HTML。参数说明SQL里的date(2022-04-14)是SQLite的日期函数如果表中日期字段是TEXT格式date 比较的是字符串你必须保证日期格式统一为YYYY-MM-DD否则排序会出错。最后一个提醒如果你计划长期维护这份数据建议在写入SQLite之前就把原始Excel按年份拆分为多个文件备份因为百度迁徙平台对历史数据的展示有窗口期有些特定日期的数据可能下了线就再难找回。使用zipfile直接读取xlsx并归档原始XML也是一种值得保留的日常习惯——我通常会把每年原始归档放在raw/2019.parquet这样即使以后源站改了字段格式你依然有干净的基底做回溯对比。本文还有配套的精品资源点击获取