简介:这份数据包提供百度迁徙指数数据,覆盖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.worksheet+xml,而/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>元素,属性t="s"表示该单元格是共享字符串,t="str"表示公式字符串,无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属性提取列字母,从而得到{列名: 值}的字典。逻辑说明:t="s"时,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(how="all") 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", errors="coerce") # 数值列转float,强制错误为NaN df["in_index"] = pd.to_numeric(df["in_index"], errors="coerce") df["out_index"] = pd.to_numeric(df["out_index"], errors="coerce") df = df.dropna(subset=["date", "city"]) # 城市名去除空格和罕见字符 df["city"] = df["city"].str.replace(r"\s+", "", regex=True) print(df.head())这里的逻辑是:先用col_map把字母列映射成含义明确的字段名;date列用正则匹配筛选出看起来像日期的行,避免表头或多级标题混入;pd.to_datetime统一日期格式,errors="coerce"把无法解析的时间变成NaT;pd.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, distance=4, prominence=0.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}")distance=4表示两个峰值之间至少间隔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(f"2022年迁入指数均值同比变化: {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(index="from_city", columns="to_city", values="value").fillna(0) # 保存到CSV pivot.to_csv("city_od_matrix.csv", encoding="utf-8-sig")groupby对每个城市对求和,pivot将行变成出发城市、列变成目的城市,fillna(0)是为了让后续矩阵运算避开NaN。注意:pivot要求每个行列组合唯一,如果存在重复项需要提前聚合。这个矩阵的规模是N x N,N是城市数量,常见分析包括计算净流量排名、识别强连接对等。如果你手里只有单城市指数,也可以按“迁入指数高、迁出指数高”的城市来推测其核心网络地位。
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", ascending=False).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_exists="replace", index=False) # 创建索引 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_exists="replace"会删除旧表重写,适合一次性全量导入;index=False避免把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_exists="append", index=False)这里用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, x="date", 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,这样即使以后源站改了字段格式,你依然有干净的基底做回溯对比。
本文还有配套的精品资源,点击获取