☰
巴西地理数据层级解析:IBGE编码、清洗与配送范围计算实战
2026/10/11 14:03:36 网站建设 项目流程

简介:这份资源是面向巴西地理信息开发者的数据集合,基于巴西地理与统计研究所(IBGE)公开数据整理,适合需要构建地区、州、城市、社区等地理层级应用的 JavaScript 开发者,也可用于数据分析、地图可视化或后端服务的数据支撑。压缩包共 88 个文件,约 1.19MB,以 71 个 JSON 数据文件为核心,覆盖城市、州、地区、中观与微观区域、都市区、海滩、原住民土地、法定亚马逊、半干旱联邦单位、水体、街区等主题;另有 9 个 JavaScript 模块负责数据加载与处理,辅以 TypeScript 声明、README 文档及工程配置文件,便于直接引入项目。目前已有 371 人学习下载。资源采用 MIT 许可,目录按 raw 与 data 分层组织,数据文件命名规整,读者可快速获取结构化的巴西地理边界与统计单元,用于填充下拉选项、绘制区域地图或搭建地理编码服务,省去自行采集与清洗的成本。

1. 巴西地理数据到底包含什么:从州到社区的层级拆解

做跨境物流、本地化运营或者区域数据分析时,绕不开的一个基础问题就是:巴西的行政区划到底怎么分层?很多人第一次接触brazilian-geographic-data这类数据集,以为它就是一个 CSV 文件,里面列着“州”和“城市”两列。实际打开才发现,巴西的地理数据层级比想象中复杂得多——从大区(Região)到州(Estado),从市镇(Município)到区(Distrito),再到社区(Bairro),每一层都有自己的编码规则和边界逻辑。

这个数据集解决的核心问题是:让你在代码里用一套统一的标识符,把巴西任意一个社区关联到它所属的市镇、州和地理坐标。适合做地址标准化、配送范围计算、区域销售分析、地图可视化的人。如果你只需要“圣保罗”和“里约”两个词,那用不上它;但如果你要处理几万个巴西地址,或者要按社区级别做聚合统计,这套数据的价值就出来了。常见做法是把它导入 PostgreSQL 或 SQLite,用 IBGE 编码做关联键,再叠加自己的业务数据。

2. 巴西行政区划的编码体系与数据模型

2.1 IBGE 编码:7 位数字背后的层级逻辑

巴西地理与统计机构(IBGE)给每个市镇分配了一个 7 位编码,这个编码不是随机生成的,而是按层级嵌套的。前 2 位代表州(Unidade da Federação),中间 4 位代表市镇,最后 1 位是校验位。比如3550308对应圣保罗市:35是圣保罗州,5030是市镇序号,8是校验位。

社区(Bairro)没有独立的 IBGE 编码,通常用市镇编码加一个本地序号来标识。州(Estado)有 2 位编码,从11到53,共 27 个联邦单位(26 州加 1 个联邦区)。大区(Região)有 5 个:北部、东北部、中西部、东南部、南部,用 1 位数字表示。

理解这套编码体系的意义在于:你不需要存储完整的层级路径,只需要存最底层的编码,就能反推出所有上层归属。这在数据量大的时候能省很多存储和关联成本。

2.2 数据表结构:州、市镇、社区三张核心表怎么关联

一个典型的brazilian-geographic-data数据集会包含三张核心表:states、cities、neighborhoods。它们通过外键关联,形成树状结构。

-- 州表:27 条记录 CREATE TABLE states ( id SERIAL PRIMARY KEY, ibge_code CHAR(2) UNIQUE NOT NULL, -- 州编码,如 '35' name VARCHAR(100) NOT NULL, -- 州名,如 'São Paulo' abbreviation CHAR(2) NOT NULL, -- 缩写,如 'SP' region VARCHAR(20) NOT NULL -- 所属大区,如 'Sudeste' ); -- 市镇表:约 5570 条记录 CREATE TABLE cities ( id SERIAL PRIMARY KEY, ibge_code CHAR(7) UNIQUE NOT NULL, -- 7 位市镇编码 name VARCHAR(150) NOT NULL, state_id INT NOT NULL REFERENCES states(id), latitude DECIMAL(10, 7), -- 纬度 longitude DECIMAL(10, 7) -- 经度 ); -- 社区表:数量因市镇而异,大城市可达上千条 CREATE TABLE neighborhoods ( id SERIAL PRIMARY KEY, name VARCHAR(150) NOT NULL, city_id INT NOT NULL REFERENCES cities(id), zone VARCHAR(20), -- 区域类型:Norte/Sul/Leste/Oeste/Centro latitude DECIMAL(10, 7), longitude DECIMAL(10, 7) );

这三张表的关联逻辑很直接:neighborhoods.city_id指向cities.id,cities.state_id指向states.id。查询一个社区所属的州,只需要两次 JOIN。

参数说明:ibge_code用CHAR而不是VARCHAR,因为长度固定,查询时不用额外计算长度。经纬度用DECIMAL(10,7)而不是FLOAT,避免浮点精度问题——巴西国土跨度大,小数点后 7 位大约精确到 1 厘米,足够用。

2.3 用 Python 加载数据并做层级查询

实际使用时,我一般先用 Python 把数据加载进来,做一轮清洗和验证,再写入数据库。

import pandas as pd import sqlite3 # 加载 CSV 数据 states = pd.read_csv('states.csv', dtype={'ibge_code': str}) cities = pd.read_csv('cities.csv', dtype={'ibge_code': str}) neighborhoods = pd.read_csv('neighborhoods.csv') # 验证州编码唯一性 assert states['ibge_code'].is_unique, '州编码存在重复' assert len(states) == 27, f'州数量异常: {len(states)}' # 验证市镇编码前两位是否与州编码匹配 cities['state_code'] = cities['ibge_code'].str[:2] valid_state_codes = set(states['ibge_code']) invalid = cities[~cities['state_code'].isin(valid_state_codes)] if not invalid.empty: print(f'发现 {len(invalid)} 条市镇记录的州编码无效') print(invalid[['ibge_code', 'name', 'state_code']].head()) # 写入 SQLite conn = sqlite3.connect('brazil_geo.db') states.to_sql('states', conn, if_exists='replace', index=False) cities.to_sql('cities', conn, if_exists='replace', index=False) neighborhoods.to_sql('neighborhoods', conn, if_exists='replace', index=False) # 层级查询:给定社区名,查所属市镇和州 query = """ SELECT n.name AS neighborhood, c.name AS city, s.name AS state, s.abbreviation FROM neighborhoods n JOIN cities c ON n.city_id = c.id JOIN states s ON c.state_id = s.id WHERE n.name LIKE ? AND c.name LIKE ? LIMIT 10 """ result = pd.read_sql_query(query, conn, params=('%Jardim%', '%São Paulo%')) print(result) conn.close()

逻辑说明:先做数据质量检查,确认州编码唯一且数量正确,再验证市镇编码的前两位是否都能在州表里找到对应记录。这一步能提前发现数据集的完整性问题。写入 SQLite 后用三表 JOIN 做层级查询,LIKE用于模糊匹配社区名,实际生产环境建议加全文索引。

参数说明:dtype={'ibge_code': str}很关键,因为编码有前导零(如11、12),如果让 pandas 自动推断会变成整数,丢失前导零。assert语句用于快速失败,数据有问题时立刻报错而不是继续跑。

3. 从原始数据到可用地址库:清洗与标准化流程

3.1 处理缺失经纬度和重复社区名

原始数据里最常见的问题是经纬度缺失和社区名重复。巴西有 5570 个市镇,但很多小市镇的社区数据并不完整,经纬度字段可能是空的。另外,同一个市镇内可能有多个同名社区,比如“Centro”几乎每个城市都有。

处理策略分三步:第一,对经纬度缺失的记录,用所属市镇的经纬度填充,精度虽然差一些,但至少能定位到城市级别;第二,对同名社区,保留全部记录,但在查询时用city_id做区分;第三,对完全重复的记录(名称、市镇、经纬度都相同),去重保留一条。

# 用市镇经纬度填充社区缺失的经纬度 neighborhoods = neighborhoods.merge( cities[['id', 'latitude', 'longitude']], left_on='city_id', right_on='id', suffixes=('', '_city') ) neighborhoods['latitude'] = neighborhoods['latitude'].fillna(neighborhoods['latitude_city']) neighborhoods['longitude'] = neighborhoods['longitude'].fillna(neighborhoods['longitude_city']) neighborhoods.drop(columns=['id_city', 'latitude_city', 'longitude_city'], inplace=True) # 去重:完全相同的记录只保留一条 before = len(neighborhoods) neighborhoods.drop_duplicates(subset=['name', 'city_id', 'latitude', 'longitude'], inplace=True) after = len(neighborhoods) print(f'去重前 {before} 条,去重后 {after} 条,删除 {before - after} 条')

逻辑说明:merge时用suffixes区分同名列,避免列名冲突。填充后再删除临时列。去重时用subset指定判断重复的列,不传的话会对比所有列,可能漏掉一些实际重复的记录。

参数说明:fillna只填充NaN,不会覆盖已有值。如果市镇经纬度也是空的,填充后仍为NaN,需要在后续步骤中标记为“坐标未知”。

3.2 用正则统一社区名格式

巴西社区名里常见缩写和变体,比如“Jd.”代表“Jardim”,“St.”代表“Santo”,“Vl.”代表“Vila”。如果不做标准化,同一个社区会因为写法不同被当成两个。

import re def normalize_neighborhood(name): if pd.isna(name): return name # 转小写,去除首尾空格 name = name.strip().lower() # 常见缩写替换 replacements = { r'\bjd\.?\b': 'jardim', r'\bvl\.?\b': 'vila', r'\bst\.?\b': 'santo', r'\bsra\.?\b': 'senhora', r'\bdr\.?\b': 'doutor', } for pattern, replacement in replacements.items(): name = re.sub(pattern, replacement, name) # 去除多余空格 name = re.sub(r'\s+', ' ', name) return name neighborhoods['name_normalized'] = neighborhoods['name'].apply(normalize_neighborhood) # 验证:查看标准化前后的对比 sample = neighborhoods[['name', 'name_normalized']].drop_duplicates().head(20) print(sample.to_string(index=False))

逻辑说明:normalize_neighborhood函数先做基础清洗(去空格、转小写),再用正则替换常见缩写。\b是单词边界,确保只替换独立的缩写词,不会误伤“Jd”出现在其他单词中间的情况。最后用\s+把连续空格压缩成一个。

参数说明:re.sub的pattern用原始字符串(r''),避免反斜杠被转义。替换顺序有影响,先替换长模式再替换短模式,避免“Jd.”被部分匹配。

3.3 建立市镇与社区的模糊匹配索引

实际业务中,用户输入的地址往往不规范,比如“Sao Paulo”写成“São Paulo”或“Sao Paulo”,“Jardim Paulista”写成“Jd Paulista”。需要建立模糊匹配索引,让查询能容忍这些差异。

from difflib import SequenceMatcher def fuzzy_match_city(input_name, cities_df, threshold=0.8): """在 cities_df 中查找与 input_name 最匹配的市镇""" input_lower = input_name.strip().lower() best_match = None best_score = 0 for _, row in cities_df.iterrows(): city_name = row['name'].strip().lower() score = SequenceMatcher(None, input_lower, city_name).ratio() if score > best_score: best_score = score best_match = row if best_score >= threshold: return best_match, best_score return None, best_score # 测试 test_inputs = ['Sao Paulo', 'Rio de Janeiro', 'Brasilia', 'Salvador'] for inp in test_inputs: match, score = fuzzy_match_city(inp, cities) if match is not None: print(f'{inp} -> {match["name"]} (相似度: {score:.2f})') else: print(f'{inp} -> 未找到匹配 (最高相似度: {score:.2f})')

逻辑说明:SequenceMatcher计算两个字符串的相似度,返回 0 到 1 之间的值。遍历所有市镇,找到相似度最高的那个。如果最高相似度低于阈值(默认 0.8),认为没有匹配。

参数说明:threshold设 0.8 是一个经验值,太低会误匹配,太高会漏匹配。对于巴西市镇名,0.8 能容忍拼写错误和重音符号差异。如果数据量大,遍历所有市镇会很慢,建议先用首字母或拼音做粗筛,再对候选集做精细匹配。

4. 避坑与排查:巴西地理数据实操中的五个血泪教训

4.1 编码前导零丢失导致关联失败

现象:用 pandas 读取 CSV 后,州编码11变成了11,但01变成了1,和数据库里的CHAR(2)字段关联时匹配不上。

原因:pandas 默认把数字列推断为int64,前导零被丢弃。

解决:读取时显式指定dtype={'ibge_code': str},或者在read_csv时加converters={'ibge_code': lambda x: str(x).zfill(2)}。写入数据库前再检查一遍,确保所有编码长度一致。

4.2 重音符号导致查询结果为空

现象:用户输入“São Paulo”,数据库里存的是“Sao Paulo”(无重音),WHERE name = 'São Paulo'查不到任何记录。

原因:数据源不同,有的带重音,有的不带。巴西葡萄牙语里重音符号很常见,但很多系统在录入时会自动去掉。

解决:在查询时用unaccent函数(PostgreSQL 需要安装unaccent扩展),或者在应用层做归一化。我一般会在数据表里额外存一列name_unaccented,查询时对比这一列。

-- PostgreSQL 安装扩展 CREATE EXTENSION IF NOT EXISTS unaccent; -- 查询时忽略重音 SELECT * FROM cities WHERE unaccent(name) ILIKE unaccent('%São Paulo%');

4.3 社区层级缺失导致聚合结果偏差

现象:按社区做销售统计时,发现某些城市的社区数据只有几十条,而实际有上百个社区,导致统计结果偏低。

原因:brazilian-geographic-data的社区数据覆盖不完整,大城市数据较全,小城市可能只有部分社区。

解决:先检查每个城市的社区数量分布,对数据明显偏少的城市,在聚合时降级到市镇级别,或者用人口加权估算。不要假设社区数据是完整的。

# 检查每个城市的社区数量 city_neighborhood_counts = neighborhoods.groupby('city_id').size().reset_index(name='count') city_neighborhood_counts = city_neighborhood_counts.merge( cities[['id', 'name']], left_on='city_id', right_on='id' ) # 找出社区数量少于 10 的城市 sparse_cities = city_neighborhood_counts[city_neighborhood_counts['count'] < 10] print(f'社区数据稀疏的城市数量: {len(sparse_cities)}') print(sparse_cities[['name', 'count']].head(10))

4.4 经纬度坐标系不统一

现象:把社区经纬度叠加到地图上时,位置偏移了几百米。

原因:不同数据源的经纬度可能基于不同的坐标系,常见的有 WGS84 和 SAD69。巴西历史数据里 SAD69 很常见,和 WGS84 有几十到几百米的差异。

解决:统一转成 WGS84。如果数据里没有标注坐标系,用已知地标做校准。常见做法是用pyproj做转换。

from pyproj import Transformer # SAD69 转 WGS84(巴西常用) transformer = Transformer.from_crs("EPSG:4618", "EPSG:4326", always_xy=True) lon, lat = transformer.transform(-46.6333, -23.5505) # 圣保罗某点 print(f'转换后: {lon:.6f}, {lat:.6f}')

4.5 市镇编码变更导致历史数据对不上

现象:用两年前的数据和今年的数据做对比,发现某些市镇的编码变了,关联不上。

原因:巴西的市镇会合并、拆分或改名,IBGE 编码随之调整。新市镇从老市镇拆分出来时,会分配新编码。

解决:维护一个编码变更映射表,记录哪些老编码对应哪些新编码。如果没有映射表,至少要在数据表里加valid_from和valid_to字段,标记编码的有效期。

5. 进阶用法:用地理数据做配送范围计算与可视化

5.1 基于社区质心的配送半径估算

有了社区经纬度,可以快速估算配送范围。假设你有一个配送中心,想知道哪些社区在 10 公里范围内。

import math def haversine(lat1, lon1, lat2, lon2): """计算两个经纬度点之间的距离(公里)""" R = 6371 # 地球半径,公里 phi1, phi2 = math.radians(lat1), math.radians(lat2) dphi = math.radians(lat2 - lat1) dlambda = math.radians(lon2 - lon1) a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2 return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1-a)) # 配送中心坐标(示例:圣保罗某点) center_lat, center_lon = -23.5505, -46.6333 radius_km = 10 # 筛选在半径内的社区 nearby = [] for _, row in neighborhoods.iterrows(): if pd.isna(row['latitude']) or pd.isna(row['longitude']): continue dist = haversine(center_lat, center_lon, row['latitude'], row['longitude']) if dist <= radius_km: nearby.append({ 'name': row['name'], 'city_id': row['city_id'], 'distance_km': round(dist, 2) }) nearby_df = pd.DataFrame(nearby).sort_values('distance_km') print(f'半径 {radius_km} 公里内共有 {len(nearby_df)} 个社区') print(nearby_df.head(10).to_string(index=False))

逻辑说明:haversine公式计算球面上两点间的最短距离,适合做配送半径估算。遍历所有社区,计算到配送中心的距离,筛选出在半径内的。结果按距离排序,方便查看最近的社区。

参数说明:R = 6371是地球平均半径,单位公里。如果需要更精确的结果,可以用椭球体模型(如 Vincenty 公式),但 haversine 在 10 公里范围内的误差可以忽略。

5.2 用 Folium 做社区级热力图

把社区数据可视化,能直观看到数据分布和密度。

import folium from folium.plugins import HeatMap # 取圣保罗市的社区 sp_city_id = cities[cities['name'] == 'São Paulo']['id'].values[0] sp_neighborhoods = neighborhoods[neighborhoods['city_id'] == sp_city_id].dropna(subset=['latitude', 'longitude']) # 创建地图 m = folium.Map(location=[-23.5505, -46.6333], zoom_start=12) # 添加热力图 heat_data = sp_neighborhoods[['latitude', 'longitude']].values.tolist() HeatMap(heat_data, radius=15, blur=10).add_to(m) # 添加标记 for _, row in sp_neighborhoods.head(50).iterrows(): folium.CircleMarker( location=[row['latitude'], row['longitude']], radius=3, popup=row['name'], color='blue', fill=True ).add_to(m) m.save('sp_neighborhoods_heatmap.html') print(f'已生成热力图,包含 {len(sp_neighborhoods)} 个社区')

逻辑说明:先用city_id筛选出圣保罗市的社区,去掉经纬度缺失的记录。创建 Folium 地图,中心设在圣保罗市中心。HeatMap插件根据点的密度生成热力图,CircleMarker标注具体社区位置。

参数说明:radius=15控制热力图每个点的影响半径,blur=10控制模糊程度。数值越大,热力图越平滑。zoom_start=12适合查看城市级别的分布。

5.3 用 PostGIS 做空间查询

如果数据量大,建议用 PostGIS 做空间查询,比在 Python 里遍历快得多。

-- 启用 PostGIS CREATE EXTENSION IF NOT EXISTS postgis; -- 添加几何列 ALTER TABLE neighborhoods ADD COLUMN geom GEOMETRY(Point, 4326); UPDATE neighborhoods SET geom = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326); -- 创建空间索引 CREATE INDEX idx_neighborhoods_geom ON neighborhoods USING GIST(geom); -- 查询距离某点 10 公里内的社区 SELECT name, city_id, ST_Distance(geom::geography, ST_SetSRID(ST_MakePoint(-46.6333, -23.5505), 4326)::geography) / 1000 AS distance_km FROM neighborhoods WHERE ST_DWithin(geom::geography, ST_SetSRID(ST_MakePoint(-46.6333, -23.5505), 4326)::geography, 10000) ORDER BY distance_km;

逻辑说明:ST_MakePoint创建点几何,ST_SetSRID设置坐标系为 WGS84。ST_DWithin用空间索引快速筛选,ST_Distance计算精确距离。::geography把几何转成地理类型,距离单位变成米。

参数说明:4326是 WGS84 的 EPSG 编码。10000是 10 公里的米数。空间索引GIST能大幅提升查询速度,百万级数据也能毫秒级返回。

我自己的习惯是:拿到任何地理数据,先做一轮编码校验和坐标系确认,再入库。这两步不做,后面全是坑。巴西的数据尤其要注意重音符号和编码前导零,这两个问题我踩过不止一次。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询