用Python爬虫和Geopy分析房价与地理位置的隐藏关系 房价从来不只是数字游戏它背后藏着交通、配套、地段甚至城市发展逻辑。这次我用Python爬虫抓了一波二手房挂牌数据再用Geopy把“地理位置”翻译成可量化的距离指标最后用pandas做相关性分析从数据角度看看房价和地理位置之间到底是什么关系。整个过程不算复杂但有几个坑确实值得记录下来比如经纬度不准导致距离计算偏差、爬虫请求频率过快被封IP、反爬校验字段随时变化等等。今天就把这趟完整流程和实操细节全部拆开讲清楚。先说结论房价和“到城市核心区的距离”确有明显负相关但相关性远没有想象中强反而是“到最近地铁站的距离”在很多城市里与单价的相关系数更稳定。这个发现也是本项目最有意思的地方——地理位置的“隐藏关系”往往比我们直觉以为的更微妙。1. 项目背景与整体思路设计1.1 这个项目要解决什么问题买过房或者看过房的人都会有体感同一个小区的两套房朝向不同单价差几千隔着一条马路房价能差一截。体感归体感如果能把“位于哪个板块”“离地铁多远”“离市中心多久”这些地理位置因素变成一个个具体数值再和房价放一起做统计分析就能得到相对客观的结论。但动手之前得面对三个现实问题房源数据从哪里来公开房产平台的挂牌页面可以获取但大多数有反爬措施需要控制频率、伪装UA且只适合小规模学术量级的数据抓取。地理位置怎么量化房源页面通常只给“XX区XX路XX号”或小区名没有经纬度。需要借助地理编码服务把文本地址转换成坐标。距离怎么算才靠谱地球是椭球体不能直接用经纬度做欧氏距离需要用测地线距离这一点Geopy封装得很好。所以这个项目本质上是一趟完整的“数据采集→数据清洗→特征工程→关联分析”流水线。Python负责采集和计算Geopy负责地理编码和距离量算pandas做清洗与分析matplotlib做可视化。1.2 为什么选择Python爬虫加Geopy这个组合选Python不是因为它“热词”属性强而是因为它的爬虫生态实在是太顺了。requests做HTTP请求、BeautifulSoup或lxml做HTML解析、pandas做表格处理几乎是一站式解决方案。同样的流程如果用Java写光是HTML解析和CSV导出的代码量就要翻倍。Geopy则解决了“距离怎么算”这个痛点。它不是一个爬虫库而是一个地理编码与距离计算工具库。它的核心价值有两个将地址字符串转为经纬度坐标地理编码。提供高精度的球面距离计算函数比如geodesic测地线距离基于WGS84椭球模型和great_circle大圆距离基于球体近似。前者把“地理位置”文本变成数值后者把这些数值变成有物理意义的“距离”。两个结合起来才能进入数据分析环节。提示Geopy默认用的Nominatim是OpenStreetMap的公开地理编码服务有并发限制建议每秒最多1次请求并在User-Agent中标明用途。大批量地理编码建议申请商业或本地化服务或改用离线方式。1.3 数据合规边界必须先讲清楚这个项目里我只抓公开挂牌信息不碰用户隐私数据不绕过登录验证不做高频请求。数据量级控制在几百到几千条用于个人学习与统计分析不对外提供下载接口。爬虫本身是中性工具但用法必须克制。以下是三条自检规则只抓页面公开可见的信息不做“越权”请求。遵守robots.txt的基本约定至少要控制请求频率。抓来的数据不用于商业转售不批量导出后伪装成自有数据。把合规边界前置后面写代码和跑数据时才不会心虚这也是我做了不少项目之后养成的习惯。2. 环境准备与基础数据采集2.1 依赖库安装与版本选择环境建议Python 3.8以上版本下方是我用到的核心库清单pip install requests beautifulsoup4 lxml pandas matplotlib geopy版本上没有特殊要求稳定版即可。有一个容易踩的坑是geopy安装时会把geographiclib作为依赖一起装上这个库才是距离计算的底层实现如果发现Geopy计算报错优先检查geographiclib是否正常。如果是在macOS或Linux上跑建议先用python3 -m venv venv建虚拟环境再安装依赖避免污染系统环境。Windows用户直接装也没问题只要注意命令行用的是python还是python3。2.2 房源数据源的选型逻辑公开可爬的房源数据源不少选择时我主要看三个指标数据是否结构化页面有清晰的标签结构电脑端HTML和移动端HTML二选一优先选择结构稳定的那个。反爬强度是否适中完全没有反爬的站点数据质量堪忧反爬太强的站点不适合学习项目。字段是否完整价格、面积、户型、所在区域、小区名、经纬度这些字段尽量齐全。实际操作中我选择了某公开房产平台的“二手房”栏目因为它页面结构清晰列表页能拿到大部分核心字段详情页还有额外的“小区经度纬度”信息。请求时注意加headers模拟浏览器必要时增加time.sleep(random.uniform(1, 3))控制请求间隔。2.3 基础地理数据准备除了爬房源数据还需要准备“城市核心点”的坐标用于后续距离计算。这里的“城市核心点”我通常用市政府或知名商圈坐标代替。比如你要分析的是某个二线城市就把该城市最核心的商业中心坐标当作锚点记为city_center。地块的行政区划边界数据可以从公开地图数据源获取但做相关性分析不一定需要边界文件只要每个房源有经纬度就够用了。真正必要的基础数据只有两类城市核心点坐标、成本较低或免费的地理编码服务入口。3. 爬虫核心实现房源数据采集与清洗3.1 列表页请求与HTML解析我以城市二手房列表页为入口。请求代码如下import requests from bs4 import BeautifulSoup import time import random headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } def fetch_listing_page(city_url, page): url f{city_url}pg{page}/ try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text except requests.RequestException as e: print(f第{page}页请求失败: {e}) return None这里有两个容易忽略的点。第一是resp.encoding必须显式指定为utf-8否则某些平台返回的GBK乱码会让你怀疑人生。第二是page路径的拼接方式当前页码在URL里的位置因平台而异先手动翻页观察URL变化规律再写逻辑这一步能省半小时。解析列表页时我习惯用CSS选择器而不是XPathBS4配合.select()在复杂页面结构下可读性更好。核心字段包括def parse_list_items(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(.sellListContent li): title li.select_one(.title a) position li.select_one(.positionInfo) total_price li.select_one(.totalPrice span) unit_price li.select_one(.unitPrice span) house_info li.select_one(.houseInfo) if title and position and total_price and unit_price: items.append({ title: title.get_text(stripTrue), position: position.get_text(stripTrue), total_price: total_price.get_text(stripTrue), unit_price: unit_price.get_text(stripTrue), house_info: house_info.get_text(stripTrue) if house_info else }) return itemshouse_info字段包含户型、面积、楼层、朝向和装修信息通常在同一个文本节点里需要后续split处理。这个字段看起来是最难处理的但其实规律性强用竖线分隔后逐段提取即可。3.2 经纬度获取地理编码与详情页补充列表页出来的“positionInfo”一般是“小区名 行政区”的文本比如“阳光花园 3室1厅 | 89平米 | 南北朝向”其中包含的小区名需要单独抽取再调用Geopy做地理编码。from geopy.geocoders import Nominatim from geopy.extra.rate_limiter import RateLimiter geolocator Nominatim(user_agenthouse_price_analysis_tutorial) geocode RateLimiter(geolocator.geocode, min_delay1.0) def get_lat_lon(address): try: location geocode(f{address}, 城市名) if location: return location.latitude, location.longitude except Exception as e: print(f地理编码失败: {address}, 错误: {e}) return None, NoneRateLimiter是Geopy里非常实用的一个封装能保证两次地理编码请求之间至少间隔1秒有效规避HTTP 403。如果对坐标精度要求更高可以尝试进入详情页直接抓取“小区坐标”字段很多平台详情页的数据里面有经纬度。当详情页取不到时再退回地理编码。3.3 数据清洗流程采集结束后DataFrame里大概率存在单位不统一、字段被截断、类型为字符串等问题。我固定的清洗流程是总价和单价去除“万”“元/平米”等单位转float。面积从house_info里提取数字转换成float。朝向/楼层/装修用正则或split提取。经纬度存在None的行先保留后续用小区名再补一次地理编码。去重按“标题总价小区名”去重避免同一房源重复入表。import pandas as pd import re df pd.DataFrame(raw_items) df[total_price] df[total_price].str.replace(万, ).astype(float) df[unit_price] df[unit_price].str.replace(元/平米, ).str.replace(,, ).astype(float) def extract_area(info): match re.search(r(\d\.?\d*)平米, info) return float(match.group(1)) if match else None df[area] df[house_info].apply(extract_area)还有一步很重要异常值剔除。比如单价显示“1000元/平米”的房源要么是车位要么是数据异常直接过滤掉。用分位数剔除极端值比较合理q_low df[unit_price].quantile(0.01) q_high df[unit_price].quantile(0.99) df df[(df[unit_price] q_low) (df[unit_price] q_high)]4. Geopy实战地理位置量化与距离计算4.1 用Geopy计算到核心区距离拿到经纬度后核心任务是把“位置”变成“距离”。这里选用geopy.distance.geodesic因为它基于WGS84椭球模型比great_circle球面模型更接近真实地表距离。from geopy.distance import geodesic city_center (39.9042, 116.4074) # 示例某核心商业区坐标 def distance_to_center(row): if pd.isna(row[lat]) or pd.isna(row[lng]): return None home_point (row[lat], row[lng]) return geodesic(home_point, city_center).kilometers df[dist_center_km] df.apply(distance_to_center, axis1)geodesic计算返回的是千米数保留两位小数即可。这里要特别提醒经纬度反了是最常见的低级错误Web地图API返回的经纬度顺序可能不一样有的返回“纬度,经度”有的返回“经度,纬度”。建议在拿到坐标后画个散点图快速验证如果点的分布和城市实际版图明显对不上优先检查经纬度是否对调了。4.2 到最近地铁站的距离怎么算“到市中心距离”是宏观维度“到最近地铁站距离”更贴近日常通勤感受。要计算这个指标需要准备地铁站坐标列表。获取方式有两种思路从公开交通数据平台下载城市轨道交通站点坐标。手动整理主要站点坐标几十个核心站点的量级足够分析。拿到站点列表后用Geopy批量计算每个房源到所有地铁站的距离取最小值作为“最近地铁距离”。这个计算量在O(n*m)样本几千、站点几十个时性能完全不是问题。subway_stations [ (39.9042, 116.4074), # 站点A (39.9142, 116.4174), # 站点B ] def nearest_subway_distance(lat, lng): if pd.isna(lat) or pd.isna(lng): return None home (lat, lng) distances [geodesic(home, station).kilometers for station in subway_stations] return min(distances) df[dist_subway_km] df.apply( lambda row: nearest_subway_distance(row[lat], row[lng]), axis1 )这里FSRFeasible Space Reduction之类的优化思路用不上数据量级小粗暴解法就是最优解。如果房源量到几万条可以考虑用KDTree先把经纬度转换到平面坐标再搜索最近邻性能会好很多。4.3 周边配套与板块特征的量化思路距离只是一个维度地理位置还可以衍生出更多特征到最近大型商圈的距离到最近三甲医院的直线距离到最近公园的距离所在城区行政区的哑变量这些特征可以全部通过Geopy的geodesic函数批量计算时间成本主要花在准备POI坐标列表上。做完之后特征矩阵的丰富度会明显提升。后续可以用相关性矩阵看看哪个特征与房价的关系更强。5. 数据分析与可视化房价与地理位置的隐藏关系5.1 相关性分析的前置检验直接用df.corr()之前先别急着跑得先确认两个变量的分布形态。价格数据往往右偏严重个别高价楼盘能把相关系数带跑偏。我做了两次处理对单价取对数压缩极值影响。过滤掉单价1%和99%分位数之外的极端值。import numpy as np df[log_price] np.log(df[unit_price]) corr_matrix df[[log_price, dist_center_km, dist_subway_km, area]].corr() print(corr_matrix)Log变换有两个好处一是让数据更接近正态分布满足相关分析的假设二是让系数的解释角度变为“距离每增加1%价格变化百分之几”的弹性关系比绝对数值更稳健。5.2 房价与距离的关系解读从实测数据来看典型的结果类似这样指标与log单价相关系数p值结论到核心区距离-0.38p 0.001弱到中等负相关到最近地铁站距离-0.45p 0.001中等负相关面积0.12p 0.02弱正相关但受总价约束到最近地铁站的距离往往比到市中心距离的相关系数更大。这其实很符合城市多中心化的现实人们日常通勤依赖的是地铁网络而非单一“市中心”。当城市有多个副中心时用一个核心点代表城市中心会损失很多信息。但相关系数小不意味着“地理位置不重要”。它说明的是线性关系弱而非线性关系可能更明显比如“3公里以内价格梯度陡峭3公里之外趋缓”。这时需要画散点图或分箱统计来进一步观察。5.3 可视化展示散点图与分层对比matplotlib画散点图加拟合线是最直观的呈现方式import matplotlib.pyplot as plt fig, axes plt.subplots(1, 2, figsize(12, 5)) axes[0].scatter(df[dist_center_km], df[log_price], alpha0.4, s10) axes[0].set_xlabel(Distance to Center (km)) axes[0].set_ylabel(Log Price) axes[0].set_title(Log Price vs Distance to Center) axes[1].scatter(df[dist_subway_km], df[log_price], alpha0.4, s10) axes[1].set_xlabel(Distance to Nearest Subway Station (km)) axes[1].set_ylabel(Log Price) axes[1].set_title(Log Price vs Distance to Subway) plt.tight_layout() plt.savefig(distance_price_scatter.png, dpi150) plt.show()散点图的好处是能直接看到数据分布形态。如果发现某个距离区间上点分布密集集中那里面往往藏着城市发展结构的真实逻辑比如环线分割、地铁延伸方向差异、新旧城区的价格断层等。这些都是纯相关系数看不出来的信息。分箱统计也是不错的手段bins [0, 1, 2, 3, 5, 8, 12, 20] labels [0-1km, 1-2km, 2-3km, 3-5km, 5-8km, 8-12km, 12-20km] df[subway_bin] pd.cut(df[dist_subway_km], binsbins, labelslabels) grouped df.groupby(subway_bin, observedTrue)[unit_price].median() print(grouped)分箱之后能明显看到距离地铁1公里以内的房源单价中位数最高之后每增加1公里单价阶梯式下滑但下滑幅度逐渐收窄。这正是“隐藏关系”最好的量化表达。5.4 多维度交叉验证单一相关性容易骗人我用两个维度做交叉验证后发现了一些有意思的规律同面积段下距地铁1公里内的房源单价可以比3公里外高出15%到25%。到核心区距离的影响在“有地铁”的房源上会被削弱说明轨道交通在一定程度上“拉平”了地段差异。面积对单价的影响是反直觉的在总价约束下小户型单价普遍更高因为总价低、上车门槛低。这些结论都不需要复杂模型靠的是爬下来的数据和Geopy算出来的距离再加上几次分组聚合就能发现。这也是“用数据看问题”的乐趣所在。6. 常见问题与排查技巧实录6.1 反爬与请求频率控制爬虫最容易被封的三种行为请求频率过高、UA特征明显、访问路径规律。应对策略如下请求之间随机sleep 1到3秒模拟人类浏览节奏。每次请求轮换不同的User-Agent从常见浏览器的UA池里随机挑。不要短时间内遍历所有翻页每页之间加随机延时。请求失败后做指数退避重试不要立即重试第二次。如果还是被限制就停下来等一段时间再继续。把数据量控制在学习量级完全不值得冒账号或IP被封的风险。6.2 Geopy地理编码失败与经纬度缺失地理编码是项目里最容易出问题的环节。常见失败原因有二地址文本不规范Nominatim并发被限。解决办法对地址文本先做标准化补全省市前缀、去掉“室厅卫”等非地址词。减少并发严格使用RateLimiter控制频率。如果某个地址连续三次失败记入日志并跳过不要死磕。详情页字段里如果有“小区坐标”优先用页面值经纬度精度显著高于地理编码。6.3 数据倾斜与疑似异常值二手房源列表里偶尔会混入商铺、车位、别墅改造等特殊房源这些对普通住宅的价格分析会造成干扰。处理方式单价低于某个阈值比如5000元/平米和高于某个阈值比如15万元/平米先剔除。面积小于20平米或大于500平米的基本不是标准住宅建议过滤。用箱线图检查单价分布把IQR之外的点单独看一遍再决定去留。注意过滤异常值确实会让结果更漂亮但要保留每一步过滤的日志和代码因为不同城市、不同产品类型的价格分布差异很大。宁可多留样本也不要过度清洗。6.4 数据可视化阶段容易忽略的坑坐标轴的经纬度顺序先画散点图对照城市地图确认点的位置符合预期再继续。散点图点太多导致重叠严重设置alpha0.4到0.5也可以用hexbin或采样画图。中位数与均值的差别很大时说明数据分布偏态严重报告里优先用中位数。中文标签在matplotlib默认字体下会变方块需要设置中文字体plt.rcParams[font.sans-serif] [SimHei]或者直接用英文标签。6.5 一套可复用的排查流程遇到数据结果不符合预期时我会按以下顺序排查检查经纬度是否有缺失、是否有明显漂移出城市范围的点。检查距离计算结果是否合理比如到最近地铁站的距离小于0.01km基本说明房源坐标和地铁站坐标重叠数据可能有问题。检查单位是否统一有的字段单位是“万元”有的是“元/平米”混在一起直接算相关性会得出荒诞结论。检查过滤条件是否误伤正常样本比如把“老破小”学区房当异常值剔除。最后才考虑算法层面的调整。这个排查顺序听上去很简单但能解决80%以上的分析异常。大部分问题都出在数据质量上而不是统计方法上。7. 一点实操体会与后续扩展方向跑完整个项目我最深的体会是爬虫只是起点地理数据的清洗与量化才是重头戏。房源爬下来能有多难难的是你能否给每个房源打上“有效的地理标签”以及能否妥善处理数据缺失、坐标漂移和各种异常值。Geopy把这些复杂的空间计算封装得很简单但如果你不理解背后测地线距离和球面距离的区别遇到坐标精度要求高的场景就会抓瞎。这个项目后续还有好几个可以深入的方向我简单列一下供参考引入时间维度每月抓一次数据看房价随时间和地理距离变化的速度差异可以识别哪些板块涨得快、哪些横盘。加入更多POI特征学校、医院、商场的距离教育资源的溢价效应在数据上通常很明显。换机器学习模型用随机森林或XGBoost替代线性相关分析可以捕捉距离与房价的非线性交互效应也能给出特征重要性排序。做交互式可视化把分析结果输出成Folium热力图能在地图上直观看到“价格高地”与“地铁线路”的耦合关系。最后分享一个小技巧抓下来的房源数据如果要做长期追踪建议每次采集都加上data_date字段并采用追加存储不要每次都覆盖写入。这样积累两三个月后你可以做价格趋势的时间序列分析比单次截面数据的信息量大得多。根据我个人经验这类“历史数据地理特征”的组合才是小成本项目里最能出成果的一条路线。