做城市研究的人都知道,房价数据最让人头疼的就是空间颗粒度。平时能拿到的,基本是市、区一级的平均水平,再细一点可以到板块或街道。可一旦想研究街道内部、甚至街区尺度上的房价差异,这些数据完全不够用。我最近在折腾一套“1990–2026年中国城市房价栅格数据”,100米分辨率、全国覆盖、年度连续,恰好把这个问题从底层解决了。这篇文章我会从头拆一遍这套数据的结构、坐标、单位口径,以及我实际跑分析时踩过的坑和总结出的操作流程,给同样在做房价时空分析的朋友一个可以直接照搬的参考。
这套数据的核心卖点其实就三个词:栅格、百米、年度连续。栅格意味着每个像元都是固定100米乘100米的正方形,不随行政区边界变动;百米意味着一个像元覆盖1公顷面积,大概一个普通小区的尺度,既能拉开街区之间的差异,又不会因为粒度过细产生太多噪声;年度连续则直接决定了一件事——像元级时间序列是天然对齐的,只要投影一致,1990年的像元位置和2020年的像元位置是同一个地方,可以把三十多年压缩成一叠切片来做时空分析。
如果你正在做城市内部空间分异、房价梯度、轨道交通影响评估、或者十几年尺度的城市蔓延研究,这套数据应该是你目前能看到的最顺手的基础图层之一。我来聊聊怎么把它用好。
1. 为什么需要100米分辨率的房价栅格
1.1 传统统计数据的颗粒度困局
先说最要命的问题:行政区划边界会变。市辖区撤并、街道调整、开发区独立统计,十年前后行政区划对不上的情况太常见了。很多研究者在处理长时序房价数据时,花了大量精力去重新匹配边界:要把过去某个区的房价,按现在的地理范围重新加权计算,中间只要有一个街道的归属发生变化,整条序列就断了。而固定网格不存在这个问题——东西南北位置不变,行政区划再怎么动,栅格像元本身还是那个位置。
另一个问题是“均质化”陷阱。一个行政区内可能同时包含极限学区房、老破小、次新改善盘和远郊保障房,全区平均房价把这些信息全部抹掉了。做成100米栅格之后,至少能看出板块内部的价格起伏。我拿数据复盘过几个一二线城市的内部结构,有些老城区表面上均价很高,但放大到百米尺度,高价点和洼地犬牙交错,这种微观格局,任何区级统计数据都表现不出来。
所以说,栅格数据解决的不只是“可视化好看”的问题,它直接改变了可做的研究问题。比如你想测一座城市从中心到边缘的房价梯度曲线,用街道均值只能得到20个点左右,拟合出来特别粗糙;换成100米栅格,城市建成区里是几万个有效像元,沿着城市中心画一条剖面线,出来的梯度曲线平滑又真实。
1.2 栅格化带来的分析能力跃迁
为什么不用小区边界矢量图或者地块面数据,非要转成栅格?我的体会是,栅格带来的最大红利是“计算友好”。
矢量叠加分析要不断做面与面的相交计算,两个图层叠一次还行,叠五层就开始卡了。而栅格数据只要坐标系和分辨率统一,叠加就成了矩阵逐像元运算,速度和质量都不是一个量级。尤其做空间计量、空间滞后、局部聚类(比如LISA、Getis-Ord Gi)这类分析时,栅格数据几乎是首选结构。
另一个好处是跨源数据融合。夜间灯光影像、NDVI、土地覆盖、PM2.5浓度、DEM高程,这些基础地理数据全是栅格格式。房价栅格和它们天然对齐,可以直接做像元级相关分析。你不需要再做矢量转栅格,也不用担心不同源数据的边界误差累积。
1.3 年度连续数据的“时间纵深”价值
1990年这个起点非常讲究。那一年中国住房制度刚从福利分房转向市场化改革,接下来三十多年的房价演变,几乎映射了整个快速城市化进程。这套数据从1990到2026年逐年版铺下来,等于给了你一张能够“回放”城市空间重构的底片。
有了这个时间纵深,很多经典问题就可以被重新回答。比如:中心城区的高房价同心圈是在持续扩大还是被多中心结构替代?新城开发到底是形成了新的价格高地,还是只制造了一个房价洼地?城市轨道交通开通前后,站点周边多少米范围内的房价发生了变化?这些问题用区级面板数据回答不了,因为空间细节不够;用个案的微观样本又缺乏全国可比性。百米栅格加年度连续,恰好补上了这个中间层。
我实际用来做过一个很简单但很有说服力的分析:选取某一座城市,计算每个栅格到城市几何中心欧氏距离,再按年份分组统计房价梯度。结果能明显看到2015年前后梯度曲线斜率变陡,说明中心与外围的价差在扩大,而这种“扩大”在不同时间点的空间形态是不一样的——只有连续切片才能看到这种过程性变化。
2. 数据结构与核心技术点解读
2.1 影像的基本属性:分辨率、坐标系与像元值含义
拿到手的数据如果是一套GeoTIFF,第一件事永远是查看元数据。按我通常的工作流程,会直接写几行Python读取文件头:
import rasterio tif_path = "China_HousePrice_1990.tif" with rasterio.open(tif_path) assrc: print("宽高:", src.width, src.height) print("波段数:", src.count) print("坐标系:", src.crs) print("变换参数:", src.transform) print("数据范围:", (src.bounds.left, src.bounds.bottom, src.bounds.right, src.bounds.top)) print("nodata值:", src.nodatavals) print("像元值类型:", src.dtypes[0])建议先把这几行跑通再谈分析。正常情况下你会看到:平面坐标系(常用的是CGCS2000 / 高斯-克吕格投影,分带可能因城市而异),像元尺寸是100米×100米,数据范围覆盖全国城市建成区及其周边一定缓冲带。注意还有一部分是与省级行政边界拟合过的版本,具体以你拿到的数据为准。
像元值单位这块很容易踩坑。有的数据源以“元/平方米”存储,有的以“万元/平方米”存储,还有的是对数变换后的值。我的经验是,看到数值最高只有三位数的时候,先别激动,检查一下是不是万元单位。以一套2020年北京的数据为例,如果是“元/平方米”计价,城区核心地段的像元值应该到七万到十几万之间;如果发现像元值只有7到15,那基本可以确定是按“万元/平方米”记录的。用错了单位,后面所有统计都得重来。
2.2 年度连续背后的价格口径问题
这是我认为整套数据里最值得关注的技术细节。房价数据天然存在三种“口径差”:名义价与实际价、新房与二手房、年平均值与年末时点值。一套数据要做到1990到2026年连续,必须把口径统一到底。数据处理方通常的做法是:以二手房成交价格为主基线,因为二手房更能反映真实市场供需;新房受预售限价影响太大,政策干扰强。
哪怕都是二手房,跨年份对比时还要考虑通胀因素。1990年的1000元/平米和2020年的5万元/平米,之间不仅有市场上涨,还有整体物价水平的上升。这一点数据处理方一般会用某一基准年份的指数进行平减,把历年价格都折算到基准年份的购买力水平。如果你拿到的数据没做这一步,请务必在分析前自行处理:自行平减最稳妥的办法是使用分省的CPI或专门编制的房地产价格指数,逐年折算之后再做时间序列对比。
还有一个隐藏口径是“空间覆盖范围”。中国城市建成区一直在扩大,1990年主城范围很小,2020年已经延伸到了远郊。数据方通常会以2026年的城市开发边界为基准框定全部图层范围,1990年尚未开发的区域像元标为NoData或者0值。这个设计是合理的,因为它保证了同一位置各年份之间可比较。但使用时要特别小心:统计城市平均房价时,如果把前沿扩张区当作无数据剔除,历年有效像元数量是递增的,直接算简单平均会导致“分母变化”带来的假性下降。这个问题我在后面第4章还会展开讲。
2.3 什么时空分析方法适配这套数据
年度连续、百米栅格、全国覆盖——这三个特征决定了它最适合三类分析方法。
第一类是像元级时间序列分析。把每个有效像元沿时间轴抽取出来,形成一条长度为37的序列(1990–2026)。然后可以计算局部趋势斜率、Mann-Kendall检验、突变点检测,识别哪些区域的房价在持续上升、哪些区域出现拐点。这类分析在城市内部尺度上完成的话,能够精准定位到具体的板块和街区。
第二类是时空立方体分析。沿着“每年一个栅格图层”的思路,将数据组装成时空体(space-time cube),再进行新兴时空热点分析(Emerging Hot Spot Analysis)。传统热点分析只能告诉你在某个时间点哪里贵,而新兴时空热点分析能告诉你哪些地方是持续升温型、哪些是时冷时热型、哪些已经冷却。这种区分对理解城市内部的动态分异非常有帮助。
第三类是事件冲击评估。带上年度信息后,你可以构造准自然实验:轨道交通开通、重大基础设施落地、名校挂牌,事件发生在某一年,前后各截取几年像元数据做倍差法或者合成控制分析。栅格数据天然提供了一组巨大的控制组——相邻街道未受事件影响的像元就是很好的对照组。
3. 实操过程:从入库到第一张分析图
3.1 准备软件与数据整理清单
建议准备的内容包括:Python环境、QGIS桌面软件,以及下面的代码片段。数据方面,你需要把每年的GeoTIFF文件统一放到同一个目录下,文件名格式最好是“China_HousePrice_YYYY.tif”,这样后处理代码可以自动遍历。
如果你还没安装Python地理数据处理生态,可以直接用Anaconda创建新环境:
conda create -n geospatial python=3.10 -y conda activate geospatial conda install -c conda-forge rasterio geopandas shapely matplotlib xarray netcdf4 -y这套环境的通用性很强,后面做栅格处理、矢量叠置、出图都靠得住。QGIS主要是用来做可视化检查和人工目检,Python则负责批量统计和建模。两个工具搭配,效率最高。
3.2 用Python读取栅格和基础统计
第一步写一个遍历函数,把历年数据读进列表,同时输出每个文件的基本信息:
import os import rasterio import numpy as np import pandas as pd data_dir = "path/to/your/rasters" year_list = list(range(1990, 2027)) results = [] foryear in year_list: fpath = os.path.join(data_dir, f"China_HousePrice_{year}.tif") if not os.path.exists(fpath): continue with rasterio.open(fpath) as src: arr = src.read(1) valid = arr[arr > 0] # 假设0或负数为无效 results.append({ "year": year, "mean": np.mean(valid), "median": np.median(valid), "p5": np.percentile(valid, 5), "p95": np.percentile(valid, 95), "valid_count": len(valid) }) df = pd.DataFrame(results) print(df.head())这段代码输出的是全国层面的年度汇总。放在其他分析之前跑一遍,能够快速检验数据是否连续、没有某些年份缺漏,也能发现价格水平是整体抬升还是突然跳变。
如果需要按城市统计,就把矢量边界读进来做掩膜提取。
import geopandas as gpd city_boundary = gpd.read_file("cities.gpkg", layer="city") city_boundary = city_boundary.to_crs("EPSG:3857") # 与栅格投影一致 with rasterio.open("China_HousePrice_2020.tif") as src: raster_crs = src.crs arr = src.read(1) profile = src.profile # 栅格化边界 from rasterio.features import rasterize city_bounds_gdf = city_boundary.geometry mask = rasterize( shapes=city_bounds_gdf, out_shape=arr.shape, transform=profile["transform"], fill=0, default=1, dtype="uint8" ) masked_prices = arr[mask == 1]我一般还会顺手导出一张“逐年有效像元数量变化”的图表。如果发现1990年只有几千个像元,而2020年有几十万,不用惊讶:那是建成区扩张的结果。后面分析一定要设置有最低有效像元数阈值来过滤早期数据。
3.3 三分钟做出城市房价梯度剖面图
选一个城市,提取该市所有像元,计算每个像元到城市主中心点的距离,就能画房价梯度剖面。具体流程是:
- 确定城市主中心坐标(可用市中心地标或老城核心位置)
- 计算每个栅格质心到中心点的欧氏距离
- 按距离分箱,每公里一箱,逐年统计箱内中位数房价
- 按年份画线对比
代码示例:
import numpy as np import xarray as xr import pandas as pd import matplotlib.pyplot asplt center_point = (104.06, 30.67) # 示例城市中心,实际使用要替换 # 读取单年份并提取中心城区像元 with rasterio.open("China_HousePrice_2020.tif") as src: arr = src.read(1) transform = src.transform # 所有像元的行列索引 rows, cols = np.indices(arr.shape) xs, ys = rasterio.transform.xy(transform, rows, cols) xs = np.array(xs) ys = np.array(ys) distances = np.sqrt((xs - center_point[0])**2 + (ys - center_point[1])**2) * 111000 # 粗略度->米 valid_mask = (arr > 0) & (distances < 50000) # 限制50公里内 dist_bands = (distances[valid_mask] / 1000).astype(int) values = arr[valid_mask] profile_df = pd.DataFrame({"distance_km": dist_bands, "price": values}) profile_median = profile_df.groupby("distance_km")["price"].median() plt.plot(profile_median.index, profile_median.values) plt.xlabel("Distance from city center (km)") plt.ylabel("Median house price") plt.title("Urban housing price gradient in 2020") plt.grid(alpha=0.3) plt.show()多城市、多年份的剖面图叠在一起,就能直观看到城市空间结构的演变。我自己做的时候还会用低通滤波或者局部加权回归(LOWESS)做平滑,去掉个别异常像元的毛刺。
4. 常见问题与排查技巧实录
4.1 像元值读出来是“乱码”或者特别巨大
先别急着怀疑数据坏了,按从易到难的顺序排查:检查nodata值是否被误认为有效统计值,检查坐标单位(如果是经纬度,EPSG:4326下的“100米分辨率”会以度为单位表示,读取时的数值含义完全不同),检查像元值的缩放系数。我遇到过一次整个栅格层乘以了0.0001的系数,原本是5万元/平米,读出来是5,需要倒除回去。
读取NoData尤其值得注意。很多GeoTIFF用-9999或者0表示无数据区域,如果你直接对全图做统计,均值会被严重拉低。稳妥做法是:
valid_arr = np.where(arr > 0, arr, np.nan)然后所有后续统计一律使用np.nanmean、np.nanmedian这类函数。这样空值就自动跳过了。
4.2 不同年份对比时数值“跳变”
如果2018年全国均价一万二,2019年变成了八千,先不要急着下“房价崩了”的结论。大概率是口径变化:2019年数据源从“建成区内有效成交像元”扩展到“包含外围新区”,而新区房价较低,把均值拉下去了。或者2019年做了价格指数平减,基准年份变了。
排查方法是同时看两个指标:全局中位数和有效像元数量。如果中位数没变但平均价大跌,基本可以认定是外围低值像元增加导致的。把每年有效像元数量画成柱状图,问题一眼就看得出来。
如果确认是价格指数重新基准,直接按新旧指数的比例把旧年份乘过去即可。升级版本更建议做线性插值,让每个年份都在同一指数基准下可比。
4.3 行政区划调整对统计的影响
这是一个老生常谈但每次都能坑到人的点。你拿到的栅格图层即使完全不随行政区划变动,但在“按城市汇总”时仍然受行政边界影响。比如2016年某县撤县设区,归入某市后,该市的有效像元范围立刻扩大,均价被拉低,房价时序就出现了“断崖”。
解决办法有三条:第一,使用历史上的实际管辖边界来做历年统计,但边界数据极难找全;第二,直接用2026年边界统一历年统计,把扩张区域的早期年份标记为无数据;第三,只取城市核心区的固定范围做分析,比如距市中心30公里以内。我实际最喜欢第三种做法,既简单又避开了边界干扰。
还要提醒一点,城市中心的理解也会变化。有些新城规划之后,实际的多中心结构已经成型,再用几何中心做梯度就会得到双峰曲线。这时候建议用人口热力或者夜间灯光识别出来的“城市主中心”作为基准。
4.4 关于“超分辨率重建”与精度焦虑
这几年图像超分辨率重建这个概念很火,很多人问能不能把100米房价栅格推升到10米甚至更细分辨率。技术上是可行的,比如用GAN或者深度插值模型参考当地街区特征生成更细粒度的房价表面。但我要先泼一盆冷水:超分辨率得到的细节是模型“想象”出来的,并不是真实交易信息。
如果确实需要更细尺度,比较稳妥的是辅助数据融合降尺度:用地块容积率、建筑密度、POI密度、二手房价挂牌点数据建立统计关系,在100米栅格的基础上做空间插值细化到10米。这个方法的精度比纯GAN可信度高很多,因为每一步都有真实物理变量约束。
顺便提一句:热词里提到的分辨率乱象,比如“切换分辨率就黑屏”“不同分辨率找字失败”这些,虽然是别的领域的经验,但放在这里逻辑是一致的——任何分辨率变化都会引入伪信息,关键是变化的逻辑必须直接对应于数据生成机制,不能单纯为了“好看”去做无约束的放大。
5. 应用场景延展:这套数据还能做什么
5.1 房价-轨道交通效应分析
用全样本的百米房价栅格,研究地铁对周边房价的影响非常合适。以某条新开通的地铁线为例,以站点为圆心做500米、1000米、2000米缓冲区,分别提取历年房价栅格像元。再将2000米外、3000米内的像元作为对照组,构造面板数据。因为像元位置固定,只需要逐年提取,非常简单。
我在一个二线城市实验过,结果很典型:地铁从规划到开通,不同阶段影响幅度不一样。规划期站点周边500米内房价涨幅最高,开通后边际效应递减。距离站点2000米以外的像元则几乎没有显著溢价。如果没有这种栅格类型的数据,同样的分析要手工匹配几万个小区,成本高得离谱。
5.2 城市内部空间分异的长周期测度
经典的居住分异指标比如隔离指数,通常基于街区尺度的人口与房价计算。但人口数据很难做到逐年更新,而房价栅格可以。你可以用“首位度指数”“变异系数”“Moran‘s I局部聚类”等方法逐年刻画分异程度。放大到全国层面,还能做城市间的横向对比,看哪些城市的分异在加剧,哪些城市相对均衡。
比如计算每个城市的房价四分位间距/中位数比值(IQR/Median)作为“内部离散程度”指标,然后画出该指标的时间序列。离散程度上升,通常意味着城市内部贵的更贵、便宜的更便宜。这比单纯看平均房价更能说明城市空间结构的质变。
5.3 与多源社会感知数据交叉
栅格房价数据最爽的其实是和别的栅格叠着用。和夜间灯光叠加,可以判断灯光亮度与房价在中小城市是否背离,识别“有新房没人气”的区域;和路网密度叠加,可以检验通勤可达性对房价的边际效应;和空气质量监测数据叠合,可以评估环境溢价。只要保持坐标系和像元大小一致,这些分析在代码层面几乎只是np.corrcoef或者回归问题。
我还试过和社交媒体签到数据结合:把签到密度按同样的100米网格聚合,再做空间相关分析,能看出商业活力与房价的动态匹配程度。这种跨数据交叉对研究城市功能演化特别有用,而你手里的房价栅格就是整个分析的基础坐标系。
我个人做下来的经验是,这套数据最有价值的部分其实在于“静态网格+动态数值”这个组合。只要你不想着去改变它的底层逻辑,老老实实把它当作固定观察窗口,就能省下大量处理行政边界和口径的时间,把精力花在真正的研究问题上。如果后续要做城市间对比,不妨先从单城、单年份、单条剖面线开始,跑通整个流程,再扩大范围。数据是死的人是活的,一百米的格子已经给你搭好了足够扎实的舞台,剩下的就看你想在上面演一出什么戏了。