今天给大家介绍如何使用 Python 基于 LandScan 全球人口栅格数据,按照商玉萍(2022)论文中的方法,同时测算城市多中心结构的 5 大指标 ,包括中心数量、帕累托指数、多中心指数(含距离)以及去中心化指标。
一、指标来源与计算原理 1.1 数据来源 本方法使用的人口数据为美国能源部橡树岭国家实验室提供的 LandScan 全球人口密度栅格数据 ,空间分辨率约为 1 km × 1 km(坐标参考系转换后约 820 m)。
1.2 文献来源 5 大指标均来自以下论文:
商玉萍. 中国城市多中心空间战略的创新绩效研究——基于集聚经济与舒适度的视角 [J]. 经济学(季刊), 2022.
该论文从集聚经济与舒适度双重视角,考察城市多中心空间战略对创新绩效的影响,所使用的多中心测量指标体系是目前文献中最为系统的之一。
1.3 五大指标定义
变量名
中文名
含义
center
城市中心数量
识别出的有效城市中心(高密度聚集区)数量
pareto
帕累托指数
各中心人口规模的秩-规模幂律回归系数(绝对值),越小表示多中心越均衡
poly
多中心指数
纳入距离的人口规模标准差指数,越小表示分布越均衡
sub3
去中心化指标(3 km)
CBD 3 千米以外的人口占城市总人口的比例
sub5
去中心化指标(5 km)
CBD 5 千米以外的人口占城市总人口的比例
1.4 各指标计算方法
1.5 计算流程总览 整个计算分为以下步骤:
坐标系转换:将 LandScan 栅格数据投影到等面积坐标系(Albers 投影),确保距离计算准确;
裁剪与掩膜:按城市行政边界裁剪栅格,获取该城市范围内的人口格点;
栅格转点:将栅格像元转为空间点数据;
构建点邻接:使用 1000 m 距离阈值构建邻接关系,生成空间权重矩阵;
局部 Moran’s I 检验:使用 R spdep::localmoran(conditional=TRUE) 的条件正态近似法(Sokal 1998)计算每个格点的局部莫兰指数和 p 值,识别统计显著的 HH 聚集区格点;
聚类成中心:将 HH 格点按空间邻接分组,形成连续的”高密度区块”;
筛选有效中心:要求每个区块格点数 ≥ 3、总人口 ≥ 100,000;
计算 5 大指标:center、pareto、poly、sub3、sub5。
1.6 参数设定说明 ANALYSIS_PARAMS = { "sig_level": 0.05, # 局部莫兰指数显著性阈值 "min_cells": 3, # 中心最小格点数(过滤噪点) "min_pop": 100000, # 中心最小人口(单位:人),论文基准为 10 万 "dist_nb": 1000 # 邻接距离阈值(米) } for k, v in ANALYSIS_PARAMS.items(): print(f" {k}: {v}")
关于 dist_nb = 1000 的设定 :LandScan 数据经 Albers 投影后分辨率约为 820 m。相邻格点(上下左右)的距离约为 820 m,对角线方向约为 820 × √2 ≈ 1159 m。将阈值设为 1000 m,可精确选取边邻接格点,不会错误地把对角线邻居算进来。
二、使用 reticulate 创建与管理 Python 虚拟环境 在 R 中通过 reticulate 包来调用 Python,最好的实践是为项目创建一个专属的 Python 虚拟环境,将所需依赖隔离到独立空间,避免与系统 Python(如 Anaconda)发生版本冲突。
重要说明(避免”已初始化”报错) :reticulate 在 R 会话中只能绑定一次 Python ——一旦某个 {python} 代码块运行,Python 解释器就被锁定,之后再调用 use_virtualenv() 会报错:
ERROR: The requested version of Python cannot be used, as another version has already been initialized.
因此,虚拟环境的激活必须在所有 {python} 代码块之前完成 。本文档的解决方案是在 setup chunk 中通过 Sys.setenv(RETICULATE_PYTHON = ...) 提前锁定 Python 路径,这是 reticulate 选取 Python 的最高优先级入口。
2.1 安装 reticulate(仅首次) # 设置 CRAN 镜像(knit 时 R 处于非交互模式,不会自动选择镜像) options(repos = c(CRAN = "https://mirrors.tuna.tsinghua.edu.cn/CRAN/")) # 仅在尚未安装时才安装,避免每次 knit 都重装 if (!requireNamespace("reticulate", quietly = TRUE)) { install.packages("reticulate") message("reticulate 安装完成!") } else { message("reticulate 已安装,版本:", packageVersion("reticulate")) }
2.2 虚拟环境初始化原理(已在 setup chunk 中完成) 本文档的 setup chunk(隐藏运行)包含如下逻辑:
library( reticulate) .venv_name <- ".venv" .venv_python <- virtualenv_python( .venv_name) if ( ! file.exists( .venv_python) ) { virtualenv_create( .venv_name) .venv_python <- virtualenv_python( .venv_name) } Sys.setenv( RETICULATE_PYTHON = .venv_python) use_virtualenv( .venv_name, required = TRUE )
这样做的关键在于:knitr 在处理第一个 {python} chunk 时,reticulate 已经通过 RETICULATE_PYTHON 环境变量知道要使用 .venv,不会再去碰 Anaconda。
2.3 在虚拟环境中安装 Python 包(仅首次) py_pkgs <- c ( "numpy" , "pandas" , "geopandas" , "rasterio" , "scipy" , "scikit-learn" , "libpysal" , "pyreadstat" ) installed <- py_list_packages( ".venv" ) $ package need_install <- setdiff( py_pkgs, installed) if ( length ( need_install) > 0 ) { virtualenv_install( ".venv" , packages = need_install) message( "已安装缺失的包:" , paste( need_install, collapse = ", " ) ) } else { message( "所有 Python 包已就绪,无需安装" ) }
2.4 验证激活状态 # 验证当前绑定的 Python 路径(应指向 .venv 目录) py_config()
2.5 查看已安装的包 pkgs <- py_list_packages( ".venv" ) key_pkgs <- c ( "numpy" , "pandas" , "geopandas" , "rasterio" , "libpysal" , "scipy" , "scikit-learn" , "pyreadstat" ) pkgs[ pkgs$ package %in% key_pkgs, c ( "package" , "version" ) ]
2.6 虚拟环境管理常用命令 # 查看所有已创建的虚拟环境 virtualenv_list() # 删除虚拟环境(当不再需要时) # virtualenv_remove(".venv") # 升级某个包 # virtualenv_install(".venv", packages = "geopandas", ignore_installed = TRUE)
三、全局路径与参数配置 3.1 导入依赖包与参数设定 import numpy as np import pandas as pd import geopandas as gpd import rasterio from rasterio.mask import mask from scipy.stats import norm from sklearn.linear_model import LinearRegression from libpysal.weights import DistanceBand, lag_spatial import os import re from pathlib import Path # 等面积投影(Albers,适合中国范围计算) MYCRS = "+proj=aea +lat_0=0 +lon_0=105 +lat_1=25 +lat_2=47 +x_0=0 +y_0=0 +ellps=krass +units=m +no_defs" PATH = { "pop_tif" : "pop-tif2" , # 已重投影到 mycrs 的 tif 文件夹 "city_shp" : "2021行政区划/市.shp" # 2021 年市级行政区划 } ANALYSIS_PARAMS = { "sig_level" : 0.05, "min_cells" : 3, "min_pop" : 100000, "dist_nb" : 1000 }
数据预处理提示 :如果你的 LandScan tif 文件还是 WGS84 坐标系,需要先转换到 MYCRS:
import rasterio from rasterio.warp import calculate_default_transform, reproject, Resampling def reproject_raster(input_path, output_path, dst_crs): with rasterio.open (input_path) as src: transform, width, height = calculate_default_transform( src.crs, dst_crs, src.width, src.height, *src.bounds ) kwargs = src.meta .copy () kwargs.update ({ 'crs': dst_crs, 'transform': transform, 'width': width, 'height': height }) with rasterio.open (output_path, 'w', **kwargs) as dst: for i in range (1, src.count + 1): reproject( source=rasterio.band(src, i), destination=rasterio.band(dst, i), src_transform=src.transform, src_crs=src.crs, dst_transform=transform, dst_crs=dst_crs, resampling=Resampling.nearest )
四、单城市演示计算(北京市,2020 年) 4.1 读取城市边界与人口栅格 # 读取并转换投影坐标系 city_all = gpd.read_file(PATH['city_shp']) city_all = city_all.to_crs(MYCRS) city_all['city_code'] = city_all['市代码'] city_all['city_name'] = city_all['市'] # 筛选示例城市(北京、天津、石家庄) demo_cities = [110000, 120000, 130100] city_demo = city_all[city_all['city_code'].isin(demo_cities)] # 筛选北京市 city_beijing = city_demo[city_demo['city_code'] == 110000] print(city_beijing)
4.2 栅格转点函数 def raster_to_points( raster_path, city_geom) : "" "读取栅格数据,按城市边界裁剪,并转换为点数据 Notes ----- 使用 all_touched=True 保留所有与边界多边形接触的像元, 与 R terra::mask(touches=TRUE) 的意图一致。 int32 栅格的 nodata 值无法被 np.isnan() 检测, 需先转为 float64 再替换 nodata 为 NaN。 " "" with rasterio.open( raster_path) as src: nodata_val = src.nodata out_image, out_transform = mask( src, [ city_geom] , crop= True, all_touched= True) data = out_image[ 0 ] .astype( np.float64) if nodata_val is not None: data[ data == nodata_val] = np.nan rows, cols = np.where( ~ np.isnan( data) ) values = data[ rows, cols] xs, ys = rasterio.transform.xy( out_transform, rows, cols) gdf = gpd.GeoDataFrame( { 'pop' : values} , geometry= gpd.points_from_xy( xs, ys) , crs= MYCRS ) gdf = gdf.dropna( subset= [ 'pop' ] ) .reset_index( drop= True) return gdf pop_file = os.path.join( PATH[ 'pop_tif' ] , "2020.tif" ) pts = raster_to_points( pop_file, city_beijing.geometry.iloc[ 0 ] ) print( pts.head( ) )
4.3 构建空间权重矩阵 def build_spatial_weights (coords, threshold=1000 ): """构建距离阈值空间权重矩阵""" w = DistanceBand.from_array(coords, threshold=threshold, silence_warnings=True ) w.transform = 'r' return w coords = np.column_stack([pts.geometry.x, pts.geometry.y]) w = build_spatial_weights(coords, ANALYSIS_PARAMS['dist_nb' ])
参数说明
transform = ‘r’:行标准化权重。若一个格点有 4 个邻居,则每个邻居权重 = 1/4;有 2 个邻居则权重 = 1/2。
4.4 局部 Moran’s I 检验 局部 Moran’s I 的核心作用:找出人口高密度且周围也是高密度的区域(HH 聚集区),这正是城市中心的候选位置。
def _local_moran_conditional_p( y, w) : "" " 使用 R spdep::localmoran(conditional=TRUE) 的条件正态近似法计算 LISA p 值。 完全复现 R spdep::localmoran 源码中 conditional=TRUE 的计算逻辑: Ii = (zi / m2) * lag(zi) 其中 zi = y - mean(y), m2 = sum(z^2)/n E[Ii] = -(zi^2 * Wi) / ((n-1) * m2) 条件期望(与 unconditional 的 -Wi/(n-1) 不同) Var[Ii] = (zi/m2)^2 * (n/(n-2)) * (Wi2 - Wi^2/(n-1)) * (m2 - zi^2/(n-1)) z_score = (Ii - E[Ii]) / sqrt(Var[Ii]) p_i = 2 * (1 - Φ(|z_score|)) " "" y = np.asarray( y, dtype= np.float64) n = len( y) z = y - y.mean( ) m2 = np.sum( z * * 2 ) / n z_lag = lag_spatial( w, z) Ii = ( z / m2) * z_lag Wi = np.zeros( n) Wi2 = np.zeros( n) for i in range ( n) : if len( w.neighbors[ i] ) > 0 : weights_i = np.array( w.weights[ i] ) Wi[ i] = np.sum( weights_i) Wi2[ i] = np.sum( weights_i * * 2 ) E_Ii = - ( z * * 2 * Wi) / ( ( n - 1 ) * m2) zi_over_m2 = z / m2 var_Ii = ( zi_over_m2 * * 2 ) * ( n / ( n - 2 ) ) * ( Wi2 - Wi * * 2 / ( n - 1 ) ) * ( m2 - z * * 2 / ( n - 1 ) ) var_Ii = np.maximum( var_Ii, 0.0 ) z_score = np.where( var_Ii > 0 , ( Ii - E_Ii) / np.sqrt( var_Ii) , 0.0 ) p_val = 2.0 * norm.sf( np.abs( z_score) ) return p_val, Ii def local_moran_lisa( pop_values, w, sig_level= 0.05 ) : "" "计算局部 Moran's I 并进行 LISA 分类(条件正态近似法)" "" p_values, Ii = _local_moran_conditional_p( pop_values, w) lag_val = lag_spatial( w, pop_values) med = np.median( pop_values) self_hl = np.where( pop_values > med, 'H' , 'L' ) nbr_hl = np.where( lag_val > med, 'H' , 'L' ) sig = p_values < sig_level types = np.where( ( sig) & ( self_hl == 'H' ) & ( nbr_hl == 'H' ) , 'HH' , 'other' ) return p_values, lag_val, types p_values, lag_val, types = local_moran_lisa( pts[ 'pop' ] .values, w) pts[ 'p_val' ] = p_values pts[ 'sig' ] = p_values < ANALYSIS_PARAMS[ 'sig_level' ] pts[ 'type' ] = types
局部莫兰指数结果包含:
列名
含义
Ii
局部莫兰指数值
p_val
条件正态近似双侧 p 值
type
LISA 类型(HH 或 other)
4.5 提取 HH 格点并聚类成中心 hh = pts[pts['type' ] == 'HH' ].copy() print (f"HH 格点数量: {len (hh)} " )def cluster_centers (hh_gdf, dist_nb=1000 ): """对 HH 格点进行空间聚类,识别城市中心""" if len (hh_gdf) == 0 : return pd.DataFrame() coords = np.column_stack([hh_gdf.geometry.x, hh_gdf.geometry.y]) w_hh = DistanceBand.from_array(coords, threshold=dist_nb, silence_warnings=True ) components = w_hh.component_labels hh_gdf = hh_gdf.copy() hh_gdf['cluster' ] = components centers = hh_gdf.groupby('cluster' ).agg({ 'pop' : 'sum' , 'geometry' : lambda x: (np.mean(x.x), np.mean(x.y)) }).reset_index() centers['n' ] = hh_gdf.groupby('cluster' ).size().values centers['x' ] = centers['geometry' ].apply(lambda p: p[0 ]) centers['y' ] = centers['geometry' ].apply(lambda p: p[1 ]) centers = centers.drop('geometry' , axis=1 ) return centers centers = cluster_centers(hh, ANALYSIS_PARAMS['dist_nb' ]) centers = centers[(centers['n' ] >= ANALYSIS_PARAMS['min_cells' ]) & (centers['pop' ] >= ANALYSIS_PARAMS['min_pop' ])] print (centers)
4.6 计算 5 大指标 此处代码需下载讲义材料查看~
五、批量串行计算(全国所有城市 × 所有年份) 5.1 封装核心计算函数 将以上步骤封装为函数,接收城市代码和人口 tif 文件路径,返回该城市该年份的 5 大指标:
def calc_paper_indicators (city_code, pop_file, city_all, lw=None , params=None ): """计算单个城市单年份的 5 大指标""" if params is None : params = ANALYSIS_PARAMS try : city = city_all[city_all['city_code' ] == city_code] if len (city) == 0 : return pd.DataFrame() city_geom = city.geometry.iloc[0 ] city_name = city['city_name' ].iloc[0 ] pts = raster_to_points(pop_file, city_geom) if len (pts) == 0 : return pd.DataFrame() coords = np.column_stack([pts.geometry.x, pts.geometry.y]) if lw is None : w = build_spatial_weights(coords, params['dist_nb' ]) else : w = lw p_values, lag_val, types = local_moran_lisa(pts['pop' ].values, w, params['sig_level' ]) pts['type' ] = types hh = pts[pts['type' ] == 'HH' ].copy() if len (hh) < params['min_cells' ]: return pd.DataFrame() centers = cluster_centers(hh, params['dist_nb' ]) centers = centers[(centers['n' ] >= params['min_cells' ]) & (centers['pop' ] >= params['min_pop' ])] if len (centers) == 0 : return pd.DataFrame() center_n = len (centers) pareto = calc_pareto_index(centers) poly = calc_polycentric_index(centers) cbd_idx = pts['pop' ].idxmax() cbd_geom = pts.loc[cbd_idx, 'geometry' ] sub_results = calc_decentralization(pts, cbd_geom) year_match = re.search(r'(\d{4})' , os.path.basename(pop_file)) year = int (year_match.group(1 )) if year_match else None return pd.DataFrame({ 'year' : [year], 'city_code' : [city_code], 'city_name' : [city_name], 'center' : [center_n], 'pareto' : [round (pareto, 4 )], 'poly' : [round (poly, 4 )], 'sub3' : [round (sub_results['sub3' ], 4 )], 'sub5' : [round (sub_results['sub5' ], 4 )], 'total_pop' : [pts['pop' ].sum ()] }) except Exception as e: print (f"计算失败: {city_code} , {pop_file} , 错误: {e} " ) return pd.DataFrame() result = calc_paper_indicators(110000 , pop_file, city_demo) print (result)
5.2 加载全部城市与年份文件 # 加载城市数据 city_all = gpd.read_file(PATH['city_shp']) city_all = city_all.to_crs(MYCRS) city_all['city_code'] = city_all['市代码'] city_all['city_name'] = city_all['市'] # 索引所有年份 tif 文件 pop_files = sorted(Path(PATH['pop_tif']).glob("*.tif")) print(f"找到 {len(pop_files)} 个年份文件")
5.3 串行计算所有城市 此处代码需下载讲义材料查看~
六、改进算法:预计算空间权重矩阵 6.1 为什么可以改进? 在上面的批量计算中,对同一个城市,每个年份都重新计算了一次空间权重矩阵 。但实际上,空间权重矩阵只取决于城市边界的形状——它与年份无关,只要城市行政边界不变(本项目使用 2021 年固定边界),同一城市所有年份的权重矩阵完全相同。
因此,可以先把所有城市的权重矩阵计算并保存好 ,然后在计算各年数据时直接读取,显著减少重复计算。
对于一个有 N 个城市、T 个年份的数据集:
方法
权重矩阵计算次数
原始方法
N×T
改进方法
N(预计算一次)
当 T=25(2000~2024 年)时,改进方法可将权重矩阵的计算量缩减为原来的 1/25 。
6.2 预计算并保存所有城市的权重矩阵 import pickle def compute_lw(city_code, city_all, sample_pop_file, output_dir="lwres" ): "" "预计算单个城市的空间权重矩阵" "" try: city = city_all[city_all['city_code'] == city_code] if len(city) == 0: return None city_geom = city.geometry.iloc[0] # 使用样本人口文件确定格点位置 pts = raster_to_points(sample_pop_file, city_geom) if len(pts) == 0: return None coords = np.column_stack([pts.geometry.x, pts.geometry.y]) w = build_spatial_weights(coords, ANALYSIS_PARAMS['dist_nb']) # 保存权重矩阵 os.makedirs(output_dir, exist_ok=True) output_path = os.path.join(output_dir, f"{city_code}.pkl" ) with open (output_path, 'wb') as f: pickle.dump(w, f) return w except Exception as e : print (f"权重矩阵计算失败: {city_code}, 错误: {e}" ) return None # 串行计算所有城市的权重矩阵 # (joblib.Parallel 在 reticulate 环境下无法序列化 __main__ 中定义的函数, # 因此 Rmd 中使用串行循环;如需并行请使用独立的 batch_calculate.py 脚本) sample_pop_file = os.path.join(PATH['pop_tif'], "2020.tif" ) for code in city_demo['city_code'].unique(): compute_lw(code, city_demo, sample_pop_file) print ("======== 所有城市权重矩阵计算完成,保存在 lwres/ ========" )
6.3 使用预计算权重矩阵的改进版计算函数 改进版函数从外部接收 lw 参数,不在函数内部重新计算权重矩阵:
def load_lw (city_code, lw_dir="lwres" ): """加载预计算的空间权重矩阵""" lw_path = os.path.join(lw_dir, f"{city_code} .pkl" ) if os.path.exists(lw_path): with open (lw_path, 'rb' ) as f: return pickle.load(f) return None def calc_paper_indicators2 (current_city, pop_file, lw, params=None ): """改进版计算函数(接受外部 lw)""" if params is None : params = ANALYSIS_PARAMS try : city_geom = current_city.geometry.iloc[0 ] city_code = current_city['city_code' ].iloc[0 ] city_name = current_city['city_name' ].iloc[0 ] pts = raster_to_points(pop_file, city_geom) if len (pts) == 0 : return pd.DataFrame() p_values, lag_val, types = local_moran_lisa(pts['pop' ].values, lw, params['sig_level' ]) pts['type' ] = types hh = pts[pts['type' ] == 'HH' ].copy() if len (hh) < params['min_cells' ]: return pd.DataFrame() centers = cluster_centers(hh, params['dist_nb' ]) centers = centers[(centers['n' ] >= params['min_cells' ]) & (centers['pop' ] >= params['min_pop' ])] if len (centers) == 0 : return pd.DataFrame() center_n = len (centers) pareto = calc_pareto_index(centers) poly = calc_polycentric_index(centers) cbd_idx = pts['pop' ].idxmax() cbd_geom = pts.loc[cbd_idx, 'geometry' ] sub_results = calc_decentralization(pts, cbd_geom) year_match = re.search(r'(\d{4})' , os.path.basename(pop_file)) year = int (year_match.group(1 )) if year_match else None return pd.DataFrame({ 'year' : [year], 'city_code' : [city_code], 'city_name' : [city_name], 'center' : [center_n], 'pareto' : [round (pareto, 4 )], 'poly' : [round (poly, 4 )], 'sub3' : [round (sub_results['sub3' ], 4 )], 'sub5' : [round (sub_results['sub5' ], 4 )], 'total_pop' : [pts['pop' ].sum ()] }) except Exception as e: return pd.DataFrame() city_demo_single = city_demo[city_demo['city_code' ] == 110000 ] lw_demo = load_lw(110000 ) result = calc_paper_indicators2(city_demo_single, pop_file, lw=lw_demo) print (result)
6.4 使用改进算法批量计算全部数据 此处代码需下载讲义材料查看~
6.5 合并所有结果并保存 def merge_results( input_dir= "resb" , output_file= "results.dta" ) : "" "合并所有结果文件" "" all_files = list ( Path( input_dir) .glob( "*.csv" ) ) if len( all_files) == 0 : print( "未找到结果文件" ) return pd.DataFrame( ) dfs = [ pd.read_csv( f) for f in all_files] result = pd.concat( dfs, ignore_index= True) try: import pyreadstat pyreadstat.write_dta( result, output_file) print( f"结果已保存为: {output_file}" ) except ImportError: result.to_csv( output_file.replace( '.dta' , '.csv' ) , index= False) print( f"结果已保存为 CSV 格式" ) return result final_result = merge_results( "resb" , "2000~2024年各城市多中心指标(商玉萍版本).dta" ) print( final_result.head( ) )
最终结果数据集包含如下变量:
变量名
说明
year
年份(2000~2024)
city_code
行政区划代码(2021 年版)
city_name
城市名称
center
有效城市中心数量
pareto
帕累托指数,越小越均衡
poly
含距离的多中心指数,越小越均衡
sub3
CBD 3 千米以外的人口占比
sub5
CBD 5 千米以外的人口占比
total_pop
城市总人口
通常,pareto、poly 值越小,代表各中心的人口分布越均衡(均等);sub3、sub5 越大,代表城市人口越去中心化。
七、稳健性检验:放宽中心人口门槛至 1 万人 商玉萍(2022)论文中提供了一项稳健性检验:将”总人口在 10 万人以上”的条件改为”总人口在 1 万人以上 “,重新确定每个城市的中心数量(2center)作为稳健性指标。
这一操作的实现方式非常简单,只需将 ANALYSIS_PARAMS['min_pop'] 改为 10000,其余代码完全不变:
此处代码需下载讲义材料查看~
点击这里跳转到 RStata 短书平台获取附件:名师讲堂|使用 Python 基于 LandScan 数据测算城市多中心指标(商玉萍版本)
评论