據(jù)全解析:從解壓、字段對(duì)齊到跨期對(duì)比)
簡介壓縮包內(nèi)含第一次至第七次全國人口普查的區(qū)縣級(jí)人口數(shù)據(jù)以Shapefile矢量格式存儲(chǔ)面向需要借助GIS工具開展人口時(shí)空分析的科研人員、城市規(guī)劃者及社會(huì)科學(xué)研究者。壓縮包共7個(gè)文件涵蓋shp幾何邊界、dbf屬性表、prj坐標(biāo)系統(tǒng)、sbn/sbx空間索引、shx索引及shp.xml元數(shù)據(jù)總大小46.62MB解壓后可直接在ArcGIS等平臺(tái)讀取分析。已有1174人學(xué)習(xí)下載數(shù)據(jù)覆蓋歷次普查關(guān)鍵指標(biāo)可用于人口趨勢分析、區(qū)域差異比較、城市規(guī)劃參考及社會(huì)經(jīng)濟(jì)評(píng)估等多維場景。借助配套的行政區(qū)劃矢量邊界和規(guī)范化屬性字段研究者能快速定位區(qū)縣單元節(jié)省數(shù)據(jù)整理時(shí)間聚焦于人口遷移、老齡化等實(shí)質(zhì)性問題的探索。1. 一普到七普shp人口數(shù)據(jù)包這份zip到底能解決什么做歷史人口數(shù)據(jù)可視化的人大概率都遭遇過這種折磨七普數(shù)據(jù)網(wǎng)上有干凈整潔的 Excel可一普、二普的紙質(zhì)掃描數(shù)據(jù)連個(gè)帶坐標(biāo)的電子版都難找好不容易拿到一份帶行政區(qū)劃的人口統(tǒng)計(jì)表還得自己想辦法把它和矢量邊界做連接。而“一普到七普shp格式人口數(shù)據(jù).zip”恰好把 1953 年到 2020 年七次全國人口普查的人口統(tǒng)計(jì)指標(biāo)直接掛在對(duì)應(yīng)年代的行政區(qū)劃邊界上——省、市、縣三級(jí)的幾何圖形與人口字段一起以 shp 組織解壓后就能在 QGIS、ArcGIS 或 GeoPandas 里直接讀取。這篇文章我從文件結(jié)構(gòu)、字段口徑、空間掛接和常見坑四個(gè)層面講清楚怎么把這份數(shù)據(jù)用穩(wěn)。適合 GIS 數(shù)據(jù)處理和社科研究的初級(jí)到中級(jí)讀者也適合被 Excel 統(tǒng)計(jì)表反復(fù)折磨、想換一條更直接路徑的人。2. Shapefile 文件家族與 zip 包的組織方式讀數(shù)據(jù)前先搞懂這幾件事2.1 一個(gè) SHP 不是單個(gè)文件shp、shx、dbf、prj、cpg 各管什么拿到“一普到七普shp格式人口數(shù)據(jù)”這類包第一反應(yīng)是雙擊 .shp 文件直接打開結(jié)果發(fā)現(xiàn)什么也打不開或者在 QGIS 里加載后屬性表全是亂碼。這通常不是數(shù)據(jù)壞了而是文件家族沒帶齊或缺失了配置文件。Shapefile 是一個(gè)文件集合。最基本的三個(gè)文件缺一不可.shp主文件存儲(chǔ)幾何圖形面、線、點(diǎn)。在人口普查場景里通常是一級(jí)行政邊界的面要素。.shx形狀索引文件負(fù)責(zé)快速定位幾何記錄沒有它讀取速度會(huì)變得極慢。.dbf屬性表文件是 dBase III 格式的數(shù)據(jù)庫人口統(tǒng)計(jì)字段都放在這里這也是亂碼高發(fā)區(qū)。以上三個(gè)文件缺少任何一個(gè)shp 都無法正常打開。此外還常配套兩個(gè)輔助文件。.prj 文件定義坐標(biāo)系沒有它讀數(shù)據(jù)的軟件會(huì)把坐標(biāo)當(dāng)作本機(jī)默認(rèn)坐標(biāo)系導(dǎo)致人口數(shù)據(jù)“飛”到錯(cuò)誤的位置.cpg 文件聲明字符集內(nèi)容通常是 UTF-8 或 GBK。很多亂碼問題的根源不在 .dbf 本身而在當(dāng)前軟件默認(rèn)用 UTF-8 去解了 GBK 編碼的字符串。在解壓之前你完全不知道 prj 和 cpg 是否存在這一點(diǎn)讓 shp 數(shù)據(jù)像個(gè)黑匣子。經(jīng)常遇到的情況是壓縮包里只有 .shp、.shx、.dbf 三個(gè)文件人口統(tǒng)計(jì)字段還在可位置和文字都不可信了。我的建議是拿到壓縮包后不要急著雙擊加載先解壓再快速瀏覽文件清單把缺失項(xiàng)記下來再動(dòng)手。補(bǔ)充說明一點(diǎn)為什么人口普查數(shù)據(jù)喜歡用 shp 而不用 GeoJSON 或 KML。七次普查的時(shí)間跨度從 1953 年到 2020 年大量歷史數(shù)據(jù)是在 GIS 技術(shù)成熟之前以紙質(zhì)表格形式留存的早期入庫時(shí)最通用的矢量格式就是 shapefile。dbf 屬性表又與關(guān)系型數(shù)據(jù)庫的概念天然吻合一個(gè)縣一行記錄、一個(gè)字段一個(gè)指標(biāo)處理起來順手。GeoJSON 雖然便于網(wǎng)絡(luò)傳輸?shù)昵暗目h級(jí)普查數(shù)據(jù)處理流程里還沒有它的位置shp 由此成了歷史人口空間數(shù)據(jù)的默認(rèn)承載格式。2.2 為什么用 zip 多期打包解壓前的目錄規(guī)劃與編碼預(yù)判“一普到七普shp格式人口數(shù)據(jù).zip”用 zip 打包是很自然的選擇因?yàn)?shp 是多文件集合正好以目錄為單位壓縮成一個(gè)包。但 zip 不是為中文環(huán)境設(shè)計(jì)的有三件事可以提前預(yù)判。第一是壓縮包內(nèi)部目錄結(jié)構(gòu)。常見結(jié)構(gòu)有兩種一種是按普查年份分目錄比如 1953、1964、1982、1990、2000、2010、2020 各一個(gè)文件夾每個(gè)文件夾里放當(dāng)年省界、市界、縣界的同名文件另一種是全部平鋪所有年份的 shp 混在一起。如果是后者解壓時(shí)一定要小心同名覆蓋問題——不同年份的縣界文件可能都叫 county.shp全部解壓到同一目錄后解壓的文件會(huì)把前面的覆蓋掉。這是最隱蔽的翻車現(xiàn)場也是解壓這類歷史數(shù)據(jù)包時(shí)我最警惕的事。第二是文件名編碼。zip 的早期規(guī)范只支持 ASCII后來雖然支持 UTF-8但國內(nèi)大量打包工具用的是 GBK 編碼寫文件名尤其是 Windows 上的老壓縮軟件。如果你解壓后發(fā)現(xiàn)目錄和 shp 文件名全是亂碼不要先懷疑包損壞先試著用支持指定編碼的工具重新解壓。Python 的 zipfile 模塊處理這種歷史編碼也有自己的坑實(shí)際命令行操作見第三章。第三是嵌套 zip。常見做法是很多制作團(tuán)隊(duì)會(huì)把每個(gè)普查年份再單獨(dú)壓一層形成雙層 zip 結(jié)構(gòu)外層解壓后會(huì)看到一堆二級(jí)壓縮包。如果只解了外層你會(huì)發(fā)現(xiàn)真正的 .shp 還淹沒在小包里。正式動(dòng)手前先看總目錄結(jié)構(gòu)有沒有 .zip 后綴的嵌套項(xiàng)可以省很多事。我一般會(huì)在解壓前先建一個(gè)干凈的根目錄比如 census_shp然后按年份建子目錄把所有文件落位到對(duì)應(yīng)年份。這樣既避免了同名覆蓋又為后面按年份遍歷讀取打好了基礎(chǔ)。這一步做到位后面加載屬性表、做空間連接都會(huì)順暢很多如果貪方便一次性解壓遇到同名覆蓋就只能從頭再來屬于典型的沒有后悔藥吃。3. 用 GeoPandas 跑通最小讀取流程從 zip 解壓到屬性表檢查3.1 解壓與體檢unzip 命令、中文名亂碼與文件完整性拿到 zip 之后我一般先在命令行做三步看目錄、解壓、核對(duì)文件清單。在 Linux 或 Git Bash 環(huán)境下最順手的命令如下# 先列出壓縮包內(nèi)部結(jié)構(gòu)不急著解壓 unzip -l 一普到七普shp格式人口數(shù)據(jù).zip | head -50 # 如果終端里文件名顯示為亂碼說明打包時(shí)用了GBK編碼文件名 # 用 -O 參數(shù)強(qiáng)制指定編碼再解壓 unzip -O GBK 一普到七普shp格式人口數(shù)據(jù).zip -d ./census_shp/ # 如果本機(jī) unzip 不支持 -O比如 macOS 自帶版本用 7-Zip 代替 7z x 一普到七普shp格式人口數(shù)據(jù).zip -ocensus_shp這里用 unzip -l 而不是直接解壓是為了先看清目錄結(jié)構(gòu)和文件層級(jí)判斷是否存在嵌套 zip 和同名覆蓋風(fēng)險(xiǎn)。如果存在二級(jí) zip我會(huì)把二級(jí)包單獨(dú)再解一層。指定 -O GBK 可以解決文件名亂碼但只在部分平臺(tái)生效macOS 的 unzip 通常不支持 -O改用 7-Zip 是比較穩(wěn)的方案。解壓之后還需要做一次文件完整性核對(duì)。shp 文件集合中同一個(gè)邏輯數(shù)據(jù)集的 .shp、.shx、.dbf 必須放在同一目錄、使用相同主名。我會(huì)用 find 命令把文件找出來逐個(gè)目錄確認(rèn)伴生文件是否齊全# 統(tǒng)計(jì)每個(gè)目錄下的 shp 數(shù)量以及同名 dbf 是否有缺失 find ./census_shp -name *.shp | wc -l find ./census_shp -name *.dbf | wc -l ls ./census_shp/1953/county/這個(gè)動(dòng)作能查出不少問題。常見的是某個(gè)目錄只有 .shp 和 .shx缺了 .dbf屬性表完全讀不出來或者是 .dbf 存在、.prj 缺失位置信息不可信。體檢之后把缺失清單記下來再?zèng)Q定是接受現(xiàn)狀還是換一份數(shù)據(jù)源。把這一步做在前面能省掉后面讀一個(gè)壞一個(gè)、反復(fù)解壓的重復(fù)勞動(dòng)。我對(duì)這類歷史人口數(shù)據(jù)包的習(xí)慣是先保證文件家族完整再做后續(xù)任何空間分析。3.2 打開 shp 并檢查字段GeoPandas 的兩個(gè)關(guān)鍵參數(shù)體檢做完用 Python 讀 shp 是最常見路徑。我推薦 GeoPandas 搭配 pyogrio 引擎讀歷史 shp 時(shí)的兼容性和速度都優(yōu)于默認(rèn)的 fiona。最小讀取代碼如下import geopandas as gpd # 讀取縣界數(shù)據(jù)用 pyogrio 引擎顯式指定編碼為 UTF-8 gdf gpd.read_file( ./census_shp/2000/county.shp, enginepyogrio, encodingutf-8, ) print(gdf.shape) # 行數(shù)、列數(shù) print(gdf.columns) # 列名清單 print(gdf.head(3)) # 前三條記錄引擎參數(shù)和編碼參數(shù)是這里最關(guān)鍵的兩點(diǎn)。pyogrio 是近幾年讀取速度最快的矢量引擎dbf 這種老格式fiona 可能要花十幾秒pyogrio 一兩秒就完成。但 pyogrio 對(duì)中文字段的默認(rèn)處理有自己的邏輯如果不顯式指定 encoding它可能按系統(tǒng)默認(rèn)的 UTF-8 去讀遇到 GBK 寫入的數(shù)據(jù)就會(huì)變成亂碼。所以當(dāng)你發(fā)現(xiàn)字段值“人口數(shù)”變成“浜哄彛鏁”這類亂碼時(shí)先嘗試把 encoding 改為 gbk 再讀一次。另一個(gè)容易忽略的是幾何列的有效性。歷史數(shù)據(jù)偶爾會(huì)出現(xiàn)幾何類型錯(cuò)誤同一個(gè) shp 里混合了面要素和多邊形要素或者坐標(biāo)范圍明顯異常。我習(xí)慣用兩行代碼快速檢查# 檢查幾何類型 print(gdf.geom_type.unique()) # 檢查坐標(biāo)范圍是否合理以經(jīng)緯度或國家平面坐標(biāo)的量級(jí)做初判 print(gdf.total_bounds)幾何類型檢查能在加載后第一時(shí)間發(fā)現(xiàn)文件是否有損壞或?qū)懭氘惓otal_bounds 能發(fā)現(xiàn) .prj 缺失導(dǎo)致的位置漂移。把這兩步做完數(shù)據(jù)能不能用、要不要做清洗心里大概就有數(shù)了。不要跳過這一步直接去算人口增長率數(shù)據(jù)源本身的問題會(huì)在后面每一步被無限放大這是處理任何 shp 數(shù)據(jù)的通用原則。3.3 橫向檢查數(shù)據(jù)覆蓋度哪些年份缺縣、哪些字段為空拿到 shp 并成功讀取只是第一關(guān)接下來要回答一個(gè)更重要的問題這份“一普到七普”的 shp 數(shù)據(jù)覆蓋度到底怎么樣。七個(gè)普查年份不見得每個(gè)年份都有完整的三級(jí)數(shù)據(jù)有些年份可能只有省級(jí)有些年份可能缺少個(gè)別縣。這一步不做后面做跨期對(duì)比時(shí)口徑不一致結(jié)果直接作廢。我一般會(huì)構(gòu)建一個(gè)最簡覆蓋度索引表用兩段代碼完成。第一段是按年份遍歷所有目錄下的縣界 shp統(tǒng)計(jì)每個(gè)文件的行數(shù)import os import geopandas as gpd years [1953, 1964, 1982, 1990, 2000, 2010, 2020] for year in years: fp f./census_shp/{year}/county.shp if not os.path.exists(fp): print(year, 缺失) continue gdf gpd.read_file(fp, enginepyogrio, encodingutf-8) print(year, gdf.shape[0], 縣) # 檢查關(guān)鍵人口字段的空值比例 if total_pop in gdf.columns: print(空值率:, gdf[total_pop].isna().mean())這段代碼把每個(gè)年份的行數(shù)、關(guān)鍵字段空值率打印出來一眼就能看出哪個(gè)時(shí)期普查數(shù)據(jù)質(zhì)量最差。實(shí)際操作中早期年份缺縣的概率明顯更高部分邊疆地區(qū)的縣級(jí)單元缺失或合并。遇到缺失先記錄后面在第 4 章的跨期對(duì)比里再想對(duì)齊策略不要直接丟棄那些縣級(jí)記錄不完整的年份。還有一類問題需要警惕字段類型。dbf 沒有嚴(yán)格的數(shù)字類型一切數(shù)值都以字符或數(shù)字位寬形式存儲(chǔ)。GeoPandas 讀取時(shí)會(huì)做類型推斷但“總?cè)丝凇边@類字段在不同年份里有時(shí)是整數(shù)、有時(shí)是浮點(diǎn)數(shù)有時(shí)甚至是帶逗號(hào)的字符串。在進(jìn)入跨期計(jì)算之前把人口字段統(tǒng)一轉(zhuǎn)成 float64 是最穩(wěn)的后面第 4 章的字段映射會(huì)一并處理。覆蓋度檢查和編碼檢查合在一起構(gòu)成我面對(duì)任何 shp 人口數(shù)據(jù)的標(biāo)準(zhǔn)開場。寧可在這里多花十分鐘不要在得出結(jié)果之后再回來說數(shù)據(jù)有問題。查漏補(bǔ)短的前置動(dòng)作是這個(gè)方向最值得養(yǎng)成的習(xí)慣。4. 跨普查年份的字段口徑對(duì)齊從字段名到行政區(qū)劃代碼的逐層處理4.1 一普到七普字段存在哪些真實(shí)差異等你把七個(gè)年份的數(shù)據(jù)都讀進(jìn)來真正的硬骨頭才開始顯露七次普查的字段名、字段含義、行政區(qū)劃編碼規(guī)則差異很大。把這些差異逐個(gè)說清楚才好繼續(xù)動(dòng)手。第一層差異是字段命名。一普、二普時(shí)期的數(shù)據(jù)是后來錄入的屬性表字段名往往沿用當(dāng)年紙質(zhì)表格的習(xí)慣比如“總?cè)丝凇笨赡鼙挥洖?total、totpop、renkou_count或者干脆是拼音首字母縮寫到五普、六普之后字段名逐漸規(guī)范但也沒有統(tǒng)一國家標(biāo)準(zhǔn)很多數(shù)據(jù)提供商按自己的業(yè)務(wù)習(xí)慣命名。直接跨年份取同名字段大概率會(huì)得到大面積 NaN。第二層差異是統(tǒng)計(jì)口徑。各次普查對(duì)人口的統(tǒng)計(jì)范圍有差別有的按當(dāng)年常住人口有的包含了外出人口有的只統(tǒng)計(jì)有戶籍的人口還有在普查年份里對(duì)流動(dòng)人口做了特殊處理。僅僅是“總?cè)丝凇边@幾個(gè)字在不同普查年份背后對(duì)應(yīng)的可能并非同一群人。做縱向?qū)Ρ葧r(shí)如果不先弄清這個(gè)差異1990 年與 2000 年的數(shù)字疊在一起可能出現(xiàn)不合理的突增或驟降。第三層差異是行政區(qū)劃代碼。1953 年還沒有標(biāo)準(zhǔn)統(tǒng)計(jì)用區(qū)劃代碼縣級(jí)單元的劃分邏輯也與現(xiàn)在大不相同到五普以后12 位統(tǒng)計(jì)用區(qū)劃代碼才逐漸成為標(biāo)準(zhǔn)。如果你直接按縣名做跨期關(guān)聯(lián)同名不同地、同地不同名的情況比比皆是。比如某些縣在 1980 年代拆分成了兩個(gè)新縣有的縣被劃歸別的地級(jí)市還有縣改區(qū)、縣改市的變化。比較穩(wěn)妥的處理方式是找到每個(gè)年份對(duì)應(yīng)的 shp 邊界基于幾何與屬性雙重信息匹配而不是只靠一個(gè)縣名字段。下表列出各普查年份常見的屬性形態(tài)差異普查年份屬性字段形態(tài)區(qū)劃代碼情況典型注意點(diǎn)一普1953字段少常見拼音或英文縮寫無統(tǒng)一標(biāo)準(zhǔn)部分省級(jí)數(shù)據(jù)缺失縣名與今差異大二普1964同上無統(tǒng)一標(biāo)準(zhǔn)部分縣為空值三普1982中文或拼音混合開始有地區(qū)代碼注意縣改市、新設(shè)縣四普1990字段漸規(guī)范有代碼但未統(tǒng)一12位流動(dòng)人口口徑特殊五普2000中英文字段混合12位代碼逐漸普及常住人口口徑變化六普2010較規(guī)范12位代碼與七普差異較小七普2020規(guī)范12位代碼縣級(jí)空缺較少表格里是常見特征具體到你的包要以實(shí)際讀出的 columns 和 .cpg 文件為準(zhǔn)。我處理這類數(shù)據(jù)的第一步永遠(yuǎn)是打印每個(gè)年份的列清單再對(duì)照表格里的典型形態(tài)判斷應(yīng)該映射到哪個(gè)標(biāo)準(zhǔn)字段名。4.2 用字段映射表和 12 位區(qū)劃代碼做對(duì)齊在正式分析之前第一步永遠(yuǎn)是建立字段映射表。實(shí)際操作我不會(huì)直接改原始數(shù)據(jù)而是為每個(gè)年份構(gòu)建一份標(biāo)準(zhǔn)化字段映射把所有人口字段統(tǒng)一到一套標(biāo)準(zhǔn)命名上。以下代碼用來做映射對(duì)齊核心是維護(hù)一個(gè)年份到標(biāo)準(zhǔn)字段名的映射字典再逐年份讀取并重命名import geopandas as gpd # 每個(gè)年份的原始字段名對(duì)應(yīng)標(biāo)準(zhǔn)字段名 FIELD_MAP { 1953: {county: county_name, population: total_pop}, 1964: {縣名: county_name, 人口數(shù): total_pop}, 1982: {NAME: county_name, POP: total_pop}, 1990: {NAME: county_name, TOTAL_P: total_pop}, 2000: {區(qū)縣名稱: county_name, 總?cè)丝? total_pop}, 2010: {CNAME: county_name, TOTPOP: total_pop}, 2020: {county: county_name, pop: total_pop}, } def read_standard(year: str): gdf gpd.read_file(f./census_shp/{year}/county.shp, enginepyogrio, encodingutf-8) rename_map FIELD_MAP[year] gdf gdf.rename(columnsrename_map) keep_columns list(rename_map.values()) [gdf.geometry.name] gdf gdf[keep_columns] if total_pop in gdf.columns: gdf[total_pop] gdf[total_pop].astype(float) gdf[year] int(year) return gdf這段代碼的關(guān)鍵在 rename_map它是每個(gè)年份原始字段與標(biāo)準(zhǔn)字段的對(duì)照。實(shí)際項(xiàng)目中你可能需要先打印每個(gè)年份的 gdf.columns再逐個(gè)把對(duì)應(yīng)關(guān)系填進(jìn)去這個(gè)動(dòng)作可以理解成建映射表。注意保留 year 這一列目的是把不同年份的數(shù)據(jù)縱向堆疊成面板數(shù)據(jù)對(duì)后面時(shí)間對(duì)比非常重要。字段類型轉(zhuǎn)換也值得留意dbf 里的人口數(shù)字可能以字符串形式存儲(chǔ)在 astype(float) 這一步之前需要先手動(dòng)去掉空格和逗號(hào)。某些年份總?cè)丝谧侄稳绻烊肓藵h字注釋比如“未統(tǒng)計(jì)”astype 會(huì)直接報(bào)錯(cuò)此時(shí)需要用 pd.to_numeric 的 errorscoerce 做軟轉(zhuǎn)換把不合法值變成 NaN后面再做填充或剔除。如果目錄里的屬性字段名與我給的示例完全不同不要慌對(duì)照 4.1 的表格和實(shí)際列名重新填映射即可。這條路徑的核心思想是統(tǒng)一字段語義之后才能進(jìn)入跨期計(jì)算否則后續(xù)所有 groupby 和 merge 都在比較不存在可比性的數(shù)據(jù)。4.3 行政區(qū)劃漂移變化讓跨期可比性成為一道必答題字段名統(tǒng)一了區(qū)劃漂移的問題依然存在。1953 年的縣界和 2020 年的縣界不可能完全相同用同一套邊界去承載不同年份的人口會(huì)造成系統(tǒng)性誤差。這里要做一個(gè)選擇是保留各自年份的原始邊界做分年制圖還是統(tǒng)一到某個(gè)參考年份的邊界做跨期計(jì)算。前者展示歷史狀態(tài)最真實(shí)但不便于縱向?qū)Ρ群笳弑阌谧鲈鲩L率計(jì)算和圖表比較卻需要你額外準(zhǔn)備一份統(tǒng)一的參考矢量邊界并且對(duì)人口數(shù)按參考邊界做空間重分配。做跨期對(duì)比時(shí)常見做法是選擇 2020 年七普的縣界作為參考坐標(biāo)系然后把更早年份的縣級(jí)人口數(shù)據(jù)掛接到七普縣界上。掛接邏輯可以按幾何重疊面積加權(quán)分配也可以按已落位的縣級(jí)單元一對(duì)一匹配。如果某個(gè)早期縣能完整對(duì)應(yīng)到七普的一個(gè)縣直接映射即可如果它拆成了兩個(gè)縣就按面積比例把人拆開。用代碼表達(dá)就是先做空間連接再按面積加權(quán)計(jì)算人口import geopandas as gpd # 早期人口 shp 和目標(biāo)年份縣界 shp old read_standard(1953) target gpd.read_file(./census_shp/2020/county.shp, enginepyogrio, encodingutf-8) # 空間求交保留人口字段與面積 inter gpd.overlay(old, target, howintersection) inter[area] inter.geometry.area # 按目標(biāo)縣歸并人口按面積比例估算 gdf_result inter.groupby(target_id).apply( lambda df: (df[total_pop].fillna(0) * (df[area] / df[area].sum())).sum() ).reset_index(namepop_est)注意代碼里的 target_id 是目標(biāo)縣界中每個(gè)縣的唯一標(biāo)識(shí)字段實(shí)際使用時(shí)你要把 target 里的縣代碼字段名填空進(jìn)去??臻g重分配得到的人口是近似值只在分縣面積與人口均勻分布時(shí)比較合理如果只做省級(jí)分析誤差可以接受下探到縣級(jí)甚至鄉(xiāng)鎮(zhèn)級(jí)時(shí)結(jié)果只能當(dāng)作近似。提示面積加權(quán)法建立在人口均勻分布在縣域內(nèi)的假設(shè)上用于省級(jí)尺度可信用于鄉(xiāng)鎮(zhèn)級(jí)精度明顯下降。業(yè)務(wù)上更穩(wěn)的方案是先檢查有多少縣可以一對(duì)一匹配。如果一對(duì)一匹配比例超過 90%直接走 ID 映射剩下的少數(shù)縣再做空間分配這樣能顯著減少人為誤差??臻g掛接是一個(gè)很必要的技能但不要在不理解其假設(shè)的前提下盲用。字段映射、參考邊界、匹配策略這三步做完才能放心地進(jìn)入增長率計(jì)算和可視化階段。5. 避坑排查最容易翻車的五個(gè)細(xì)節(jié)編碼、邊界、字段截?cái)嗯c解壓陷阱5.1 DBF 中文亂碼現(xiàn)象、原因與三行命令解決現(xiàn)象在 QGIS 或 GeoPandas 里打開某個(gè)年份的縣界 shp屬性表里省名、縣名變成“娌沖寳鐪”“宀佺墰宀佸競”一類完全不可讀的字符。原因dbf 文件里存的是 GBK 編碼的中文但讀取方?jīng)]有按 GBK 解碼。QGIS 默認(rèn)假設(shè) dbf 是 UTF-8GeoPandas 在未指定 encoding 時(shí)也走系統(tǒng)默認(rèn)路徑錯(cuò)位解碼中文全部變成亂碼。這與數(shù)據(jù)本身損壞無關(guān)。解決先確認(rèn) .cpg 文件是否存在存在則打開看它寫的字符集然后用 read_file 時(shí)顯式指定 encoding。命令行方案更徹底用 ogr2ogr 轉(zhuǎn)一層編碼再繼續(xù)處理ogr2ogr -lco ENCODINGUTF-8 ./county_utf8.shp ./county.shp -nlt PROMOTE_TO_MULTI如果確認(rèn)原數(shù)據(jù)是 GBK把上面的 ENCODING 參數(shù)換成 GBK 重新另存之后再用 GeoPandas 讀取就不會(huì)亂碼。這個(gè)坑幾乎每個(gè)做 shp 中文數(shù)據(jù)的人都會(huì)踩一次知道原理后三分鐘就能解決。5.2 同一縣名在不同年份有不同區(qū)域邊界漂移的排查法現(xiàn)象2000 年某縣“A 縣”的面積是 3000 平方公里到了 2020 年同名縣只剩 2000 平方公里兩期數(shù)據(jù)畫出的人口密度圖看起來像同一地名的完全不同圖形。原因真實(shí)行政區(qū)劃發(fā)生了變化。A 縣在兩次普查之間經(jīng)歷了撤縣設(shè)區(qū)、部分鄉(xiāng)鎮(zhèn)劃歸鄰縣或者某個(gè)鄉(xiāng)鎮(zhèn)并入而 shp 文件里的幾何邊界忠實(shí)記錄了這些變化。如果只認(rèn)縣名做連接肯定會(huì)算出錯(cuò)誤的增長率。解決不要用“縣名”作為唯一匹配鍵。首先用 total_bounds 比較同一縣名的面積與邊界范圍看出入是否顯著其次用 2020 年七普縣界做參考做 overlay 求交看交疊面積占各自面積的比例。若交疊比例低于 95%打上“邊界漂移”標(biāo)記再對(duì)這部分縣做專門處理。這段篩選邏輯可以用幾十行 Python 完成但核心動(dòng)作就是兩件事對(duì)比面積、計(jì)算交疊比例。5.3 形狀字段名被截?cái)嗍?DBF 老格式的鍋現(xiàn)象讀取某個(gè)早期普查的 shp發(fā)現(xiàn)字段名只有十幾個(gè)字符例如“population_1953”變成了“populatio”數(shù)據(jù)內(nèi)容看起來也不對(duì)。原因dbf 格式對(duì)字段名長度有歷史限制。老式 dBase III 字段名最長 10 個(gè)字節(jié)部分寫入工具做了自動(dòng)截?cái)鄬懭牒髮?shí)際存儲(chǔ)的字段名與原始表頭不一致讀取時(shí)又按截?cái)嗪蟮亩堂ピL問于是數(shù)據(jù)被擠到相鄰字段位置。解決這種問題沒有完全自動(dòng)修復(fù)的辦法。比較有效的方案是先查看該年份屬性表的完整字段列表與說明文檔在讀取時(shí)把截?cái)嘧侄蚊成浠卣_的原名再重新導(dǎo)出。示例代碼如下gdf gpd.read_file(./census_shp/1953/county.shp, enginepyogrio, encodinggbk) rename_map { populatio: total_pop, # 截?cái)嗪蟮淖侄蚊?- 真實(shí)名 hou: household_num, } gdf gdf.rename(columnsrename_map)這種做法依賴文檔或數(shù)據(jù)附帶的 readme。見到截?cái)鄷r(shí)不要強(qiáng)行猜先找原始說明文本或者找同一年份其他層級(jí)的 shp 做交叉參考。需要明確一點(diǎn)這種截?cái)嗍菍懭攵说母袷较拗撇皇亲x取端的問題不要試圖用改配置去解決。5.4 嵌套 zip 與深層目錄解壓后文件“失蹤”的排查流程現(xiàn)象解壓完成后某個(gè)年份的 county.shp 怎么都找不到或者在 QGIS 里加載一個(gè) shp地圖縮放到很遠(yuǎn)的位置看不到任何要素。原因常見原因是解壓時(shí)丟失了文件路徑層級(jí)或嵌套 zip 沒有一并展開。還有一些包把數(shù)據(jù)放在深層目錄下某些解壓工具遇到路徑過長或包含特殊字符時(shí)會(huì)跳過文件日志里有幾行 warning多數(shù)人不會(huì)注意到。加載后位置不對(duì)則是 .prj 缺失或目錄中殘留了另一份同主名的 .prj。解決用 unzip -l 重新看內(nèi)層結(jié)構(gòu)對(duì)比已解壓清單與包里清單找到遺漏的嵌套包后單獨(dú)解檢查 .prj 是否存在缺失就考慮用其它年份同層級(jí)的 .prj 復(fù)制過來試讀但必須明確這是臨時(shí)處理。位置不對(duì)的排查要打開 .prj 文件確認(rèn)坐標(biāo)系信息與原始數(shù)據(jù)一致。做到這一步文件“失蹤”問題基本能定位。5.5 導(dǎo)出 CSV 后人口數(shù)變成科學(xué)計(jì)數(shù)法Excel 帶來的假象現(xiàn)象把 shp 屬性表導(dǎo)出為 CSV用 Excel 打開后人口數(shù)顯示為 1.003E08 這類科學(xué)計(jì)數(shù)法部分?jǐn)?shù)值看起來像被四舍五入了。原因這是 Excel 對(duì)長整型的默認(rèn)展示方式不是 shp 數(shù)據(jù)本身的問題。dbf 中存儲(chǔ)的數(shù)值是精確的只是導(dǎo)出到 CSV 之后 Excel 以默認(rèn)格式顯示數(shù)據(jù)本身并沒有被修改。但如果你把這列導(dǎo)出后拿去做文本處理格式混亂的字符串就會(huì)污染后續(xù)計(jì)算。解決導(dǎo)出 CSV 時(shí)用 pandas 把人口字段保持為整數(shù)類型用 Excel 打開后選中整列將單元格格式改為數(shù)字小數(shù)位設(shè)為 0。如果要導(dǎo)入數(shù)據(jù)庫直接用 pandas 的 dtype 指定列類型為 int64避免中間環(huán)節(jié)被 Excel 二次格式化gdf[[total_pop, male, female]].to_csv( census_2000.csv, indexFalse ) import pandas as pd df pd.read_csv(census_2000.csv, dtype{total_pop: int64})這一步處理不大但能避免很多人掉進(jìn)“Excel 顯示科學(xué)計(jì)數(shù)法”的坑。尤其當(dāng)你把數(shù)據(jù)交接給同事時(shí)一份干凈的 CSV 比一份被 Excel 改來改去的 CSV 省心得多。6. 進(jìn)階把七期數(shù)據(jù)轉(zhuǎn)成時(shí)空長表并導(dǎo)出 WKT方便做跨期對(duì)比6.1 堆疊成一個(gè)長表多個(gè)年份的 shp 讀進(jìn)來之后最終要做成一張“長表”才方便縱向?qū)Ρ?。長表的意思是每一行代表某一年份的某一個(gè)縣包含 year 列、縣名列、人口列和幾何列。用第 4 章的 read_standard 函數(shù)把七個(gè)年份分別讀入然后用 concat 縱向拼起來import pandas as pd frames [read_standard(y) for y in years] panel pd.concat(frames, ignore_indexTrue) panel panel.sort_values([county_name, year])拼接后就能像操作普通數(shù)據(jù)表一樣做跨期計(jì)算比如用 groupby 算五年人口增長率或按年份做小倍數(shù)地圖。這一步的核心價(jià)值是讓不同年份的數(shù)據(jù)在同一個(gè)結(jié)構(gòu)下可被直接索引省去后續(xù)反復(fù) read_file 的重復(fù)動(dòng)作也是做“人口變化地圖”這類可視化最順手的輸入形態(tài)。6.2 導(dǎo)出 GeoJSON 和 WKT便于與其它系統(tǒng)對(duì)接很多下游系統(tǒng)不接受 shp卻接受 GeoJSON 或 WKTWell-Known Text。把這份“一普到七普shp格式人口數(shù)據(jù)”轉(zhuǎn)換成帶邊界文本的表格能極大方便跨團(tuán)隊(duì)協(xié)作。把人口字段和幾何列導(dǎo)出為 WKT 文本列的常見做法如下panel[geometry_wkt] panel.geometry.apply(lambda g: g.wkt if g is not None else ) out panel.drop(columnsgeometry) out.to_csv(census_panel_wkt.csv, indexFalse)這里 apply 調(diào)用的 g.wkt 就是 Shapely 的矢量幾何導(dǎo)出方法結(jié)果是一個(gè)純文本字符串任何程序都能讀。注意如果你的數(shù)據(jù)是面要素導(dǎo)出的 WKT 會(huì)以 POLYGON 或 MULTIPOLYGON 開頭要導(dǎo)入數(shù)據(jù)庫時(shí)直接用數(shù)據(jù)庫自帶的幾何解析函數(shù)讀這一列一般都能識(shí)別。除了 WKTGeoPandas 的 to_file 可以一行導(dǎo)出 GeoJSON 文件行數(shù)多時(shí)建議分層導(dǎo)出避免單文件過大拖慢下游讀取。最后說一個(gè)我個(gè)人的血淚習(xí)慣再好的 shp 數(shù)據(jù)包到手的第一件事永遠(yuǎn)是做一份“數(shù)據(jù)體檢報(bào)告”列出年份、層級(jí)、字段清單、坐標(biāo)系、編碼、空值率這六項(xiàng)指標(biāo)。以前我拿到數(shù)據(jù)后直接上空間分析以為省了時(shí)間結(jié)果半年后從合作方那邊反饋回一個(gè)數(shù)字對(duì)不上溯源才發(fā)現(xiàn)是最早讀數(shù)據(jù)時(shí)字段名截?cái)嘣斐傻摹,F(xiàn)在寧可先花半個(gè)小時(shí)檢查也不想再因?yàn)榛A(chǔ)字段問題返工。這套處理流程我重復(fù)用過很多次“一普到七普shp格式人口數(shù)據(jù).zip”這類包里編碼、區(qū)劃漂移和嵌套壓縮是三個(gè)最容易反復(fù)出問題的點(diǎn)把本章的方案和坑位記牢整份數(shù)據(jù)的價(jià)值才能穩(wěn)定釋放。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取