简介:这是一套面向房地产研究者、数据分析学习者与购房决策人群的西安房价分析工具源码,采用Python与MATLAB混合编程实现。Python负责网络爬虫与数据预处理,MATLAB承担数值计算、可视化与预测建模,覆盖新房、二手房、租赁三类房源,可按区域、户型、建筑年代、装修状态、朝向等多维度展开分析,并借助随机森林、GBR、SVR等模型完成房价预测。资源包共49个文件,约49.41MB,包含29个CSV数据文件、7个Python脚本、6个MATLAB脚本、3张PNG图表及字典与说明文档,目录按爬取、处理、预测、模型结果分层组织,结构清晰便于二次开发。目前已有293人学习下载。读者可据此掌握从数据采集、清洗到建模预测的完整流程,理解两种语言优势互补的工程实践,并参考现成脚本快速复现分析结果,适合作为课程设计、毕业项目或数据分析练手案例。
1. 西安房价数据怎么抓:一份 Python+MATLAB 混合分析源码能跑出什么
西安的房价数据,散在贝壳、安居客、链家这些平台上,单看一个小区、一个月的挂牌价,说明不了任何问题。真正做分析的人需要的是:把新房、二手房、租赁三类房源按区域、户型、朝向、装修、楼层、建筑年代、挂牌时间等维度拆开,再拿去做回归和预测。这份house_crawler源码包,就是围绕这个需求搭起来的一套完整流程——Python 负责爬取和清洗,MATLAB 负责建模和出图,中间用 CSV 文件做数据交换。
它适合三类人:一是想学 Python 爬虫但不想只爬豆瓣 Top250 的,二是做房地产研究或中介后台分析、需要一套可改可扩的数据管线的,三是正在学 MATLAB 统计与机器学习工具箱、想找个真实数据集练手的。源码包一共 49 个文件,其中 29 个 CSV 数据文件、7 个 Python 脚本、6 个 MATLAB 脚本、3 张 PNG 图表和 1 个 readme。下面按「数据怎么来 → 怎么清洗 → 怎么建模 → 坑在哪」的顺序拆开讲。
2. 爬虫脚本拆解:rented_house 与 old_house 两条抓取链路
2.1 三类房源对应三个爬虫入口
源码包里 Python 脚本的命名很直白,rented_house_info_crawler.py抓租赁,old_house_info_crawler.py抓二手房,new_house_info_crawler.py抓新房。三个脚本结构相似,核心逻辑都是「构造列表页 URL → 解析详情页字段 → 写入 CSV」。以二手房为例,抓取字段包括区域、面积、户型、朝向、装修状态、楼层、建筑年代、挂牌中介、挂牌时间。这些字段不是随便定的,它们直接对应后面 MATLAB 建模时的自变量。
我一般会先跑租赁那条链路,因为租赁数据字段少、页面结构简单,适合验证解析逻辑是否跑通。跑通之后再改二手房和新房,避免一上来就被反爬拦住、分不清是代码问题还是网络问题。
2.2 列表页翻页与详情页字段提取
爬虫的主体逻辑分两层:列表页负责拿到详情页链接和翻页,详情页负责提取具体字段。下面这段是二手房爬虫的骨架,实际脚本里字段名和选择器会因目标站点不同而调整,但结构一致:
import requests from bs4 import BeautifulSoup import csv import time BASE_URL = "https://example.com/ershoufang/xian/pg{page}/" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" } def parse_list_page(page): """解析列表页,返回当前页所有详情页链接""" url = BASE_URL.format(page=page) resp = requests.get(url, headers=HEADERS, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") links = [] for item in soup.select(".sellListContent li .title a"): href = item.get("href") if href: links.append(href) return links def parse_detail_page(url): """解析详情页,提取单个房源字段""" resp = requests.get(url, headers=HEADERS, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") record = {} record["title"] = soup.select_one(".sellDetailHeader .main").get_text(strip=True) record["total_price"] = soup.select_one(".price .total").get_text(strip=True) record["unit_price"] = soup.select_one(".unitPrice span").get_text(strip=True) # 区域、户型、朝向、装修、楼层、年代等字段按实际页面结构补充 return record def crawl(start_page, end_page, out_csv): """主流程:翻页抓取并写入 CSV""" all_records = [] for page in range(start_page, end_page + 1): links = parse_list_page(page) for link in links: try: all_records.append(parse_detail_page(link)) except Exception as e: print(f"解析失败 {link}: {e}") time.sleep(1) # 控制请求频率,避免触发风控 time.sleep(2) with open(out_csv, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=all_records[0].keys()) writer.writeheader() writer.writerows(all_records) if __name__ == "__main__": crawl(1, 50, "old_house_info_by_area.csv")这段代码里几个参数值得说清楚。time.sleep(1)是详情页之间的间隔,time.sleep(2)是翻页间隔,这两个值不要调太小,否则很容易被限流。encoding="utf-8-sig"是为了让 Excel 打开 CSV 时不乱码,Windows 上做数据分析的人应该都踩过这个坑。fieldnames取自第一条记录,所以如果某条记录缺字段,写出来的 CSV 列会对不齐——稳妥做法是预先定义好字段列表。
2.3 抓下来的 CSV 长什么样
源码包里 CSV 文件的命名本身就是一份数据字典。old_house_info_by_area.csv是按区域聚合的二手房数据,old_house_info_by_face_direction.csv是按朝向,old_house_info_by_decoration_status.csv是按装修状态,old_house_info_by_build_time.csv是按建筑年代,old_house_info_by_property_agency.csv是按中介机构,old_house_info_by_storey.csv是按楼层。租赁那边有rented_house_info_by_rent.csv和rented_house_info_by_house_type.csv。新房有house_info_by_building_type.csv、house_info_by_sale_status.csv、house_info_by_open_time.csv等。
这种「一个维度一个文件」的组织方式,好处是 MATLAB 那边读起来直接对应分析目标,坏处是文件多、容易搞混。我一般会在data_processing.py里统一做一次合并,生成house_info_data_final.csv作为建模输入。
3. 数据清洗与预处理:data_processing.py 里该做什么
3.1 缺失值、异常值与单位统一
爬下来的原始数据几乎不可能直接进模型。常见问题有三类:价格字段带「万」「元/平米」等单位,需要剥离成数值;面积字段可能写成「89.5平米」或「89.5㎡」,要统一;楼层、年代这类字段经常出现「暂无数据」或空字符串。data_processing.py的职责就是把这些处理干净。
处理策略上,价格和面积用正则提取数字,缺失值按字段重要性分别处理——建模关键字段(单价、面积、区域)缺失的直接丢弃,非关键字段(装修、朝向)缺失的填「未知」。下面是一个可复用的清洗片段:
import pandas as pd import re def clean_price(val): """把 '125万' '89元/平米' 这类字符串转成浮点数""" if pd.isna(val): return None num = re.findall(r"[\d.]+", str(val)) return float(num[0]) if num else None def clean_area(val): """统一面积单位,去掉 '平米' '㎡' 等后缀""" if pd.isna(val): return None num = re.findall(r"[\d.]+", str(val)) return float(num[0]) if num else None def preprocess(input_csv, output_csv): df = pd.read_csv(input_csv, encoding="utf-8-sig") df["total_price"] = df["total_price"].apply(clean_price) df["area"] = df["area"].apply(clean_area) # 丢弃关键字段缺失的行 df = df.dropna(subset=["total_price", "area"]) # 非关键字段填默认值 df["decoration"] = df["decoration"].fillna("未知") df["face_direction"] = df["face_direction"].fillna("未知") # 计算单价,作为后续建模的目标变量之一 df["unit_price"] = df["total_price"] * 10000 / df["area"] df.to_csv(output_csv, index=False, encoding="utf-8-sig") return df if __name__ == "__main__": preprocess("old_house_info_by_area.csv", "house_info_data_final.csv")clean_price和clean_area都用正则提取第一个数字,这对大多数中文房源标题和字段是够用的。unit_price的计算假设total_price单位是万元、area单位是平方米,如果你的数据源单位不同,这个系数要改。dropna的 subset 只保留建模必需字段,避免因为一个无关字段缺失丢掉整条记录。
3.2 类别字段编码与建模输入准备
MATLAB 的回归模型对类别变量支持不如 Python 的 sklearn 直接,所以类别字段(区域、朝向、装修、楼层区间)在 Python 阶段就要做编码。常见做法是 one-hot 编码,或者按目标变量均值做 target encoding。源码包里house_price_prediction_by_factors.py和area_house_price_prediction.py就是干这个的。
我一般会保留一份原始类别列和一份编码后的数值列,前者用于出图时做标签,后者用于喂模型。这样在 MATLAB 里画「各区域均价对比」时不用再回头查映射表。
提示:CSV 文件用
utf-8-sig编码写出,MATLAB 的readtable默认按系统编码读,Windows 上可能乱码。稳妥做法是在 MATLAB 里显式指定'Encoding', 'UTF-8'。
4. MATLAB 建模与预测:随机森林、GBR、SVR 三条路线怎么选
4.1 三种模型的适用场景与参数入口
源码包里house_price_prediction目录下有random_forest_result、gbr_result、svr_result三组结果,对应随机森林、梯度提升回归、支持向量回归三种模型。models目录存放训练好的模型文件,prediction_results存放预测输出,images存放 3 张 PNG 图表。
选型上,随机森林对特征尺度不敏感、能输出特征重要性,适合做第一版基线;GBR 在中小数据集上通常精度更高,但对异常值敏感;SVR 适合样本量不大、特征维度适中的场景,核函数和惩罚系数 C 需要调。源码包把三条路线都跑了一遍,方便对比。
MATLAB 里读数据、划分训练测试集、训练随机森林的骨架大致如下:
% 读取清洗后的建模数据 opts = detectImportOptions('house_info_data_final.csv', 'Encoding', 'UTF-8'); opts.VariableNamingRule = 'preserve'; data = readtable('house_info_data_final.csv', opts); % 选取特征列和目标列 features = data(:, {'area', 'room_num', 'hall_num', 'build_year', 'unit_price'}); target = data.total_price; % 划分训练集和测试集,固定随机种子保证可复现 rng(42); cv = cvpartition(height(data), 'HoldOut', 0.2); X_train = features(training(cv), :); y_train = target(training(cv), :); X_test = features(test(cv), :); y_test = target(test(cv), :); % 训练随机森林回归模型 rf_model = fitrensemble(X_train, y_train, 'Method', 'Bag', ... 'NumLearningCycles', 200, 'Learners', 'Tree'); % 预测并评估 y_pred = predict(rf_model, X_test); rmse = sqrt(mean((y_test - y_pred).^2)); fprintf('随机森林 RMSE: %.2f\n', rmse); % 保存模型 save('models/random_forest_model.mat', 'rf_model');rng(42)固定随机种子,保证每次划分一致,否则调参时结果波动会让你怀疑人生。NumLearningCycles是树的数量,200 是常见起点,数据量大可以加到 500。Method设为'Bag'是 bagging 集成,对应随机森林;如果换成'LSBoost'就是 GBR 路线。Learners设为'Tree'表示基学习器是决策树。
4.2 按区域和按因素两条预测链路
源码包里预测脚本分两条:area_house_price_prediction.py按区域做预测,house_price_prediction_by_factors.py按多因素做预测。前者适合回答「曲江新区下季度均价大概多少」,后者适合回答「同样 90 平米、朝南、精装,在雁塔和高新差多少」。
按区域预测时,通常先把数据按district分组,每组单独训练一个模型,或者把区域作为类别特征加入全局模型。前者样本量小、容易过拟合,后者需要足够的区域样本。我一般先看各区域样本量,少于 100 条的区域合并成「其他」,再统一建模。
2020_ground_truth.csv和ground_truth.csv是留出来做验证的真实数据,用预测结果和它对一下,能直观看出模型在高价区域是否系统性偏低。这个对比步骤不要省,否则模型上线后偏差会很难解释。
4.3 结果文件与图表怎么读
house_price_of_different_districts.csv是各区域房价汇总,prediction_results目录下是模型预测输出,images里 3 张 PNG 分别是趋势图、对比图和特征重要性图。读这些结果时重点看两件事:一是高价区域(高新、曲江)的预测误差是否明显大于低价区域,二是特征重要性里area和build_year的排序是否符合常识。如果build_year重要性异常高,可能是数据里年代字段和价格存在伪相关,需要检查样本分布。
5. 避坑与常见问题:爬虫、编码、模型三条线上的翻车记录
5.1 爬虫被限流,返回空列表或验证码页
现象:跑了几十页之后,parse_list_page返回空列表,或者resp.text里出现验证码页面。原因:请求频率过高,触发了目标站点的风控。解决:把time.sleep间隔调大到 3~5 秒,加随机抖动,必要时轮换 User-Agent。更稳妥的做法是分时段跑,不要一次性抓几千页。
5.2 CSV 用 Excel 打开乱码
现象:Python 写出的 CSV 在 Excel 里中文显示为乱码。原因:Excel 默认按 GBK 解码,而文件是 UTF-8。解决:写出时用encoding="utf-8-sig",BOM 头会让 Excel 正确识别编码。MATLAB 读取时显式指定'Encoding', 'UTF-8'。
5.3 MATLAB 读取 CSV 后数值列变成 cell 数组
现象:readtable读进来后,价格列是 cell 而不是 double,无法直接参与运算。原因:CSV 里该列混有非数值字符(如「暂无」),MATLAB 自动降级为 cell。解决:在 Python 清洗阶段就把非数值行处理掉,或者 MATLAB 里用str2double逐列转换。根本办法是保证house_info_data_final.csv里建模字段全是纯数值。
5.4 随机森林 RMSE 忽高忽低
现象:每次跑模型 RMSE 差很多。原因:没有固定随机种子,训练测试集划分和树的自助采样都在变。解决:rng(42)固定种子,cvpartition也受种子控制。调参对比时尤其要固定,否则无法判断是参数起作用还是随机波动。
5.5 预测结果在高价区域系统性偏低
现象:模型对高新、曲江的预测价普遍低于实际。原因:高价样本少,模型被低价样本主导,回归向均值收缩。解决:对目标变量取对数后再建模,或者按区域分层建模,也可以给高价样本加权。验证时用ground_truth.csv对照,别只看整体 RMSE。
6. 进阶用法:把 2020_ground_truth.csv 用成回归验证的标尺
2020_ground_truth.csv这份文件容易被忽略,但它其实是整个项目里最有价值的验证工具。做法是:用爬取的历史数据训练模型,预测 2020 年的价格,再和 ground truth 逐区域对比,算 MAPE(平均绝对百分比误差)。这比单纯看 RMSE 更能说明模型在真实场景下的可用性。
具体操作上,我会在 MATLAB 里加一段对比脚本:
% 读取预测结果和真实值 pred = readtable('prediction_results/area_prediction.csv', opts); truth = readtable('2020_ground_truth.csv', opts); % 按区域对齐后计算 MAPE joined = innerjoin(pred, truth, 'Keys', 'district'); mape = mean(abs(joined.predicted_price - joined.actual_price) ./ joined.actual_price) * 100; fprintf('各区域 MAPE: %.2f%%\n', mape); % 找出偏差最大的三个区域 [~, idx] = sort(abs(joined.predicted_price - joined.actual_price) ./ joined.actual_price, 'descend'); worst = joined(idx(1:3), :); disp(worst(:, {'district', 'predicted_price', 'actual_price'}));innerjoin按区域对齐两份数据,mape算的是百分比误差,比 RMSE 更直观。sort那几行是找出偏差最大的区域,通常会是样本量少或者价格波动大的区。找到之后回头检查该区域的样本量和特征分布,往往能发现数据问题。
从那以后我每次做完预测,都强制走一遍 ground truth 对比,不看 MAPE 不写结论。这份源码包把验证数据单独留出来,说明作者是认真跑过流程的,不是只堆脚本。希望帮到你。
本文还有配套的精品资源,点击获取