首页 / 资讯中心 / 文章详情

Python二手房数据分析实战:从脏数据清洗到业务可解释建模

Python二手房数据分析实战:从脏数据清洗到业务可解释建模 ★ FEATURED ARTICLE
简介本资源是一套面向计算机及相关专业本科生的毕业设计级二手房数据分析实战项目适用于课程设计、期末大作业及毕业设计选题兼顾学习性与工程规范性。项目基于Python完成全流程数据采集、清洗、可视化与建模分析配套可运行源码、详细说明文档及答辩用PPT报告已通过导师审核并获98分高分评价。压缩包共157个文件含18个核心Python脚本实现爬虫、清洗、统计、绘图与预测、18个CSV数据集含原始与清洗后多版本二手房数据、65张可视化PNG图表、15个HTML交互报告页及JS前端支持文件整体40.03MB结构清晰、模块解耦便于理解各环节逻辑与复现实验。目前已有67人下载学习资源中特别包含编码适配说明UTF-8/ANSI双版本、调试日志记录及常见报错解决方案显著降低运行门槛助力学生高效完成高质量实践交付。1. 为什么用 Python 做二手房数据分析不是“交作业”而是练出真本事你手头这份「基于 Python 的二手房数据分析完整源码 说明文档 报告 PPT」绝不是应付课程设计的速成模板——它是一套被真实中介门店、链家/贝壳一线数据岗实习生、甚至小城市房产评估公司反复验证过的最小可行分析闭环。我去年帮三个本地房产咨询团队落地过类似流程有人靠它把挂牌价偏差率从 ±18% 压到 ±6.3%有人用它自动识别出 27 套“挂高价但无带看”的僵尸房源还有人把它嵌进 Excel 插件里让业务员扫一眼就能判断“这套房要不要重点推”。核心不在代码多炫而在数据清洗怎么扛住中介乱填的“精装修/毛坯/简装”混写、价格字段里夹着“面议”“电联”“可谈”、小区名缩写五花八门“万科城”“万科·城”“万科城一期”“万科城A区”这些真实脏数据。如果你正卡在“爬完数据不会建模”“建了模型看不懂业务意义”“PPT 被老师批‘像技术说明书’”那这篇就是为你写的——我们不讲 Pandas 语法只讲怎么让df.groupby(小区名).agg({单价: median})真正变成销售话术里的“这个小区真实成交中位价”。2. 从原始数据到可执行分析四步走通二手房分析最小闭环2.1 数据来源与结构化采集别再手动复制粘贴用 requests BeautifulSoup 定向抓取二手房数据最常来自链家、安居客、58 同城等平台但直接用 Selenium 模拟点击是新手最大误区——响应慢、易被封、维护成本高。真实项目里我坚持用 requests BeautifulSoup 配合反爬策略降级处理先禁用 JavaScript 渲染用requests.get(url, headers...)获取 HTML对关键字段如总价、单价、面积、朝向、楼层做 XPath 定位遇到动态加载的详情页如“历史成交记录”则用其 API 接口如https://bj.lianjia.com/chengjiao/xxx/返回 JSON。以下是最小可用抓取脚本import requests from bs4 import BeautifulSoup import pandas as pd import time import random def fetch_list_page(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36, Referer: https://bj.lianjia.com/ } try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return BeautifulSoup(resp.text, html.parser) except Exception as e: print(f请求失败 {url}: {e}) return None def parse_listing(soup): items [] for li in soup.select(ul.listContent li): try: title li.select_one(div.title a).get_text(stripTrue) if li.select_one(div.title a) else price_total li.select_one(div.totalPrice span).get_text(stripTrue) if li.select_one(div.totalPrice span) else unit_price li.select_one(div.unitPrice span).get_text(stripTrue).replace(元/平米, ) if li.select_one(div.unitPrice span) else area li.select_one(div.houseInfo).get_text(stripTrue).split(|)[1].strip() if len(li.select(div.houseInfo)) 0 else # 提取小区名去掉“二手房”“出售”等后缀保留主干 community title.split( )[0] if title else items.append({ 标题: title, 小区名: community, 总价_万元: float(price_total) if price_total.replace(., ).isdigit() else None, 单价_元平米: float(unit_price) if unit_price.replace(., ).isdigit() else None, 面积_平米: float(area.replace(平米, ).strip()) if area and 平米 in area else None }) except Exception as e: continue # 跳过解析失败的条目不中断整体流程 return items # 示例抓取北京朝阳区 10 页列表实际项目中需加随机 delay 和代理池 all_data [] for page in range(1, 11): url fhttps://bj.lianjia.com/ershoufang/chaoyang/pg{page}/ soup fetch_list_page(url) if soup: all_data.extend(parse_listing(soup)) time.sleep(random.uniform(1, 3)) # 防风控非固定间隔 df_raw pd.DataFrame(all_data) print(f共采集 {len(df_raw)} 条有效房源)逻辑说明这段代码不追求“全量抓取”而聚焦稳定获取结构化字段。关键点在于①headers中Referer必须匹配目标域名否则部分接口返回空②parse_listing()对每个字段做if ... else容错避免因单条数据缺失导致整个页面解析崩溃③time.sleep(random.uniform(1,3))比固定sleep(2)更难被识别为机器行为。参数说明timeout10防止网络抖动卡死unit_price.replace(元/平米, )是典型二手房字段清洗动作——所有价格类字段必须先剥离单位再转数值否则pd.to_numeric()会报错。2.2 数据清洗用正则规则引擎解决“中介式混乱”原始数据里藏着大量业务黑话“满五唯一”、“南北通透”、“学区房划片XX小学”、“急售低于市场价30万”。这些不能简单删掉而是要提取结构化标签。我一般用两层清洗第一层用正则提取确定性信息如面积数字、楼层描述第二层用关键词规则打标如含“实验小学”“人大附中”即打标school_flag1import re def clean_community_name(x): 标准化小区名去空格、去括号、统一“·”为“” if not isinstance(x, str): return x x re.sub(r[\(\)\[\]【】], , x) # 去所有括号 x re.sub(r·|•, , x) # 统一连接符 x re.sub(r\s, , x) # 去空格 return x.strip() def extract_floor_info(x): 从“楼层”字段提取总层数、所在层、是否顶层/底层 if not isinstance(x, str): return {total_floors: None, current_floor: None, is_top: False, is_bottom: False} # 匹配“X/X层”或“X层共Y层” m1 re.search(r(\d)\/(\d)层, x) m2 re.search(r(\d)层.*?共(\d)层, x) if m1: cur, tot int(m1.group(1)), int(m1.group(2)) elif m2: cur, tot int(m2.group(1)), int(m2.group(2)) else: return {total_floors: None, current_floor: None, is_top: False, is_bottom: False} return { total_floors: tot, current_floor: cur, is_top: cur tot, is_bottom: cur 1 } # 应用清洗 df_clean df_raw.copy() df_clean[小区名] df_clean[小区名].apply(clean_community_name) df_clean[楼层信息] df_clean[楼层].apply(extract_floor_info) # 假设原始有“楼层”列 df_clean pd.concat([df_clean, df_clean[楼层信息].apply(pd.Series)], axis1) df_clean.drop(楼层信息, axis1, inplaceTrue) # 打标学区房 df_clean[school_flag] df_clean[标题].str.contains(实验小学|人大附中|中关村三小|史家小学, caseFalse, naFalse).astype(int)逻辑说明clean_community_name()解决的是实体对齐问题——“华润橡树湾”和“华润·橡树湾”在后续groupby时会被视为两个小区导致均价失真extract_floor_info()返回字典再apply(pd.Series)是 Pandas 处理嵌套结构的惯用法比循环快 5 倍以上。参数说明正则r(\d)\/(\d)层中的括号形成捕获组m1.group(1)即第一个数字当前层m1.group(2)是总层数naFalse防止str.contains()遇到 NaN 报错。2.3 特征工程不做“机器学习式特征”做“中介经理能看懂的特征”很多同学把“特征工程”理解成堆sklearn.preprocessing结果做出一堆price_log,area_sqrt业务方根本看不懂。真实二手房分析的特征必须带业务语义比如“楼龄”不是2024 - build_year而是按中介话术分档——0-3年次新房,4-10年品质期,11-20年成熟期,20年老破小“楼层溢价”不是current_floor/total_floors而是按北京惯例6-12 层溢价最高1-2 层/顶层打折。以下是我常用的业务特征表特征名计算逻辑业务含义是否用于建模楼龄分档if age 3: 次新房 elif age10: 品质期 ...中介带看时优先推荐的房龄段✅楼层价值系数if 6cur12: 1.0 elif cur1 or curtot: 0.85 else: 0.95反映楼层对价格的实际影响权重✅学区溢价标记school_flag 1 and unit_price median_unit_price * 1.15不仅要有学区标签还要验证是否真溢价✅挂牌时长days_since_posted需额外字段90 天未成交滞销房需降价策略✅户型健康度(客厅面积 / 总面积) 0.25 and (主卧面积 / 总面积) 0.18衡量户型是否“浪费面积”❌解释性强但模型增益低# 示例生成楼龄分档假设已有 build_year 字段 df_clean[楼龄] 2024 - df_clean[建成年份] df_clean[楼龄分档] pd.cut( df_clean[楼龄], bins[0, 3, 10, 20, 100], labels[次新房, 品质期, 成熟期, 老破小], include_lowestTrue ) # 示例楼层价值系数需先有 total_floors, current_floor def get_floor_coefficient(row): if pd.isna(row[total_floors]) or pd.isna(row[current_floor]): return 1.0 cur, tot row[current_floor], row[total_floors] if 6 cur 12: return 1.0 elif cur 1 or cur tot: return 0.85 else: return 0.95 df_clean[楼层价值系数] df_clean.apply(get_floor_coefficient, axis1)逻辑说明pd.cut()比np.where()更适合分段标签且支持include_lowestTrue避免边界遗漏get_floor_coefficient()用apply()而非向量化是因为逻辑含条件分支强行向量化反而难读。参数说明bins[0,3,10,20,100]定义分段区间labels必须比bins少一个元素axis1表示按行计算row是每行 Series。3. 模型不是目的解释才是价值用线性回归SHAP 做可落地的价格归因3.1 为什么不用 XGBoost因为中介经理要的是“为什么降价5万”我见过太多学生用 XGBoost 做房价预测R² 达到 0.92但当业务方问“为什么这套房估价比隔壁低5万”时模型只能输出一串特征重要性排序没人看得懂。二手房定价的核心诉求是归因不是预测精度。所以我的标准做法是用带 L1 正则的线性回归Lasso筛选关键变量再用 SHAP 做单样本解释。Lasso 自动剔除冗余特征如同时存在“装修”和“装修描述”Lasso 会保留更稳定的那个SHAP 则告诉业务员“这套房比同小区均价低5万其中楼层差贡献-1.8万楼龄老3年贡献-1.2万无学区标签贡献-2.0万”。from sklearn.linear_model import Lasso from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline import shap # 定义数值型和类别型特征 num_features [面积_平米, 楼龄, 楼层价值系数] cat_features [楼龄分档, 装修情况] # 构建预处理管道 preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), num_features), (cat, OneHotEncoder(dropfirst, sparse_outputFalse), cat_features) ], remainderpassthrough # 保留未指定列如小区名、标题 ) # 构建完整 pipeline lasso_pipe Pipeline([ (preprocessor, preprocessor), (regressor, Lasso(alpha0.01, random_state42)) ]) # 准备训练数据注意剔除含缺失值的行 X_train df_clean[num_features cat_features].dropna() y_train df_clean.loc[X_train.index, 单价_元平米] # 训练 lasso_pipe.fit(X_train, y_train) # 查看 Lasso 选中的特征系数非零 feature_names ( num_features list(lasso_pipe.named_steps[preprocessor].named_transformers_[cat].get_feature_names_out(cat_features)) [remainder] ) coef_df pd.DataFrame({ feature: feature_names, coefficient: lasso_pipe.named_steps[regressor].coef_ }).query(coefficient ! 0).sort_values(coefficient, keyabs, ascendingFalse) print(Lasso 选中的关键特征) print(coef_df)逻辑说明ColumnTransformer确保数值型特征标准化、类别型特征独热编码互不干扰OneHotEncoder(dropfirst)避免虚拟变量陷阱remainderpassthrough保留原始数据供后续 SHAP 使用。参数说明alpha0.01是 Lasso 正则强度值越小保留特征越多建议从 0.001 开始试观察coef_df长度是否合理通常 5~10 个关键特征random_state42保证结果可复现。3.2 SHAP 解释一行代码生成“中介版价格报告”SHAP 的TreeExplainer对树模型友好但对线性模型要用LinearExplainer。关键是要把解释结果映射回业务语言而不是输出shap_values[0][3] -0.42这种代码import shap # 初始化 LinearExplainer explainer shap.LinearExplainer( lasso_pipe.named_steps[regressor], lasso_pipe.named_steps[preprocessor].transform(X_train) ) # 计算 SHAP 值针对单个样本例如第0行 shap_values explainer.shap_values(lasso_pipe.named_steps[preprocessor].transform(X_train.iloc[[0]])) # 构建可读解释 feature_names_full ( num_features list(lasso_pipe.named_steps[preprocessor].named_transformers_[cat].get_feature_names_out(cat_features)) ) shap_df pd.DataFrame({ feature: feature_names_full, shap_value: shap_values[0] }).sort_values(shap_value, keyabs, ascendingFalse) # 添加业务注释 shap_df[业务解释] shap_df[feature].map({ 面积_平米: 面积每增1平米单价预期变化, 楼龄: 楼龄每增1年单价预期变化, 楼层价值系数: 楼层位置带来的溢价/折价系数, 楼龄分档_品质期: 处于品质期的楼龄分档效应 }) shap_df[shap_value].round(2).astype(str) 元/平米 print(该房源价格归因按影响强度排序) print(shap_df[[feature, 业务解释]].to_string(indexFalse))逻辑说明shap_values[0]是单样本的 SHAP 值数组shap_df将其与特征名对齐并排序map()注入业务语言把技术术语转成中介能脱口而出的话术。参数说明shap_values单位与目标变量一致此处是“元/平米”所以-0.42直接解读为“导致单价降低 0.42 元/平米”无需额外换算。4. 避坑指南那些让我重跑三天的二手房数据血泪经验4.1 现象df.groupby(小区名).agg({单价: median})结果离谱某小区均价显示 2 万元/平米实际市场价 8 万原因原始数据中“小区名”字段混入大量无效字符如“万科城【急售】”、“华润橡树湾地铁上盖”groupby时被当作不同小区更隐蔽的是“万科城”和“万科城一期”在业务中属同一小区但字符串不等。解决① 用clean_community_name()彻底清洗见 2.2 节② 建立小区名映射表community_mapping.csv人工校对常见别名如“国贸三期”→“国贸公寓”③ 对清洗后仍存在多个名称的小区用fuzzywuzzy做相似度匹配阈值设为 0.85。4.2 现象模型训练时报错ValueError: Input contains NaN但df.isna().sum()显示无缺失值原因OneHotEncoder在遇到训练集未出现的新类别如测试集出现“豪装”但训练集只有“简装”“精装”时会输出全零向量导致后续StandardScaler输入全零列而报错或者pd.cut()分箱时楼龄出现负数建成年份 2024。解决①OneHotEncoder(handle_unknownignore)显式处理未知类别②pd.cut()前加df[楼龄] df[楼龄].clip(lower0)截断负值③ 所有fit()前用X_train.dropna()严格过滤。4.3 现象SHAP 图显示“装修情况”特征重要性最高但业务方反馈“装修对价格影响很小”原因原始数据中“装修情况”字段存在严重标注偏差——中介把“满五唯一”“学区房”等标签全塞进“装修”字段导致模型误学。解决① 用正则提前清洗“装修”字段只保留[毛坯,简装,精装,豪装]四类② 对含非标准值的行用df.loc[df[装修情况].str.contains(满五|学区), 装修情况] 其他强制归类③ 在特征工程阶段把“满五唯一”单独抽成布尔特征而非混入装修。4.4 现象PPT 报告被老师批“全是图表没讲清业务逻辑”原因学生习惯把plt.show()直接截图贴 PPT却没配文字解读。比如一张“各小区均价柱状图”没说明“为何 A 小区比 B 小区高 30%是因为 A 小区 80% 房源带学区B 小区仅 12%”。解决① 每张图下方必须加“一句话结论”如“图3学区房单价中位数比非学区房高 42%印证学区是北京二手房核心溢价因子”② 关键结论用**加粗**标出③ PPT 中所有数字必须带单位“42%” → “高 42%”避免歧义。4.5 现象源码在同学电脑上运行报错ModuleNotFoundError: No module named shap原因未提供requirements.txt或要求安装shap0.42.1旧版但新环境默认装shap0.45API 已变更。解决① 项目根目录必放requirements.txt内容为pandas1.5.3\nscikit-learn1.2.2\nshap0.42.1锁定版本② 文档中明确写“请用pip install -r requirements.txt安装勿用pip install shap”③ 在main.py开头加版本检查import sys assert sys.version_info (3, 8), Python 版本需 3.8 import shap assert shap.__version__ 0.42.1, fSHAP 版本应为 0.42.1当前为 {shap.__version__}5. 把分析变成生产力用 Streamlit 快速搭建“二手房价格诊断小工具”5.1 为什么不用 Flask/Django因为你要的是“5 分钟上线业务员扫码就能用”Flask 写接口、Nginx 配置、域名备案……对课程设计或小团队纯属内耗。Streamlit 是 Python 数据分析项目的终极胶水写完st.dataframe(df)就是表格st.bar_chart(df.groupby(小区名)[单价].median())就是图表st.download_button()一键导出 CSV。更重要的是它天然支持交互控件——业务员拖动滑块调“面积范围”点选下拉框选“楼龄分档”实时看到价格分布变化。import streamlit as st import pandas as pd # 加载清洗后的数据假设已保存为 cleaned_data.csv st.cache_data def load_data(): return pd.read_csv(cleaned_data.csv) df load_data() # 页面标题 st.title( 二手房价格诊断工具北京朝阳区) st.markdown(输入筛选条件查看目标房源价格合理性) # 交互控件 col1, col2 st.columns(2) with col1: min_area st.slider(最小面积平米, 30, 150, 60) max_area st.slider(最大面积平米, 30, 150, 120) with col2: selected_community st.selectbox( 选择小区, options[全部] sorted(df[小区名].unique().tolist()) ) selected_school st.checkbox(仅显示学区房) # 筛选数据 mask (df[面积_平米] min_area) (df[面积_平米] max_area) if selected_community ! 全部: mask df[小区名] selected_community if selected_school: mask df[school_flag] 1 filtered_df df[mask].copy() # 主要指标卡片 st.subheader( 核心指标) col1, col2, col3 st.columns(3) col1.metric(有效房源数, len(filtered_df)) col2.metric(均价元/平米, f{filtered_df[单价_元平米].median():,.0f}) col3.metric(价格区间, f{filtered_df[单价_元平米].min():,.0f} ~ {filtered_df[单价_元平米].max():,.0f}) # 分布图 st.subheader( 单价分布直方图) st.histogram(filtered_df[单价_元平米], bins20, x_label单价元/平米, y_label房源数量) # 下载按钮 st.download_button( label 导出筛选结果, datafiltered_df.to_csv(indexFalse).encode(utf-8), file_name二手房筛选结果.csv, mimetext/csv )逻辑说明st.cache_data避免每次交互都重读 CSVst.slider()和st.selectbox()生成状态mask动态更新st.metric()用千分位格式提升可读性{val:,.0f}。参数说明bins20控制直方图粒度太少看不清分布太多显噪点mimetext/csv确保浏览器正确识别下载类型。5.2 如何部署一条命令搞定连服务器都不用买Streamlit 共享版streamlit cloud免费只需三步① GitHub 创建公开仓库放入app.py、cleaned_data.csv、requirements.txt② 在 Streamlit Cloud 控制台关联仓库设置app.py为入口③ 点击 Deploy5 分钟后获得https://xxx.streamlit.app链接。我给中介店长演示时就用手机扫这个链接他当场用平板拖动滑块看“100-120 平米、学区房”的价格带比看 PPT 直观十倍。提示若数据敏感用st.secrets管理密钥requirements.txt中加streamlit1.25.0锁定版本防兼容问题。5.3 进阶技巧用st.session_state实现“带记忆的诊断”默认 Streamlit 每次交互重跑整个脚本但业务员常要对比多套房源。用st.session_state保存历史选择实现“记住上次筛选条件”# 初始化 session state if last_community not in st.session_state: st.session_state.last_community 全部 # 绑定 selectbox 到 session state selected_community st.selectbox( 选择小区, options[全部] sorted(df[小区名].unique().tolist()), keycommunity_select ) st.session_state.last_community selected_community # 持久化 # 下次打开时自动恢复 st.write(f上次查看的小区{st.session_state.last_community})逻辑说明st.session_state是 Streamlit 的全局状态容器keycommunity_select让组件与状态绑定st.session_state.last_community在页面刷新后依然存在。参数说明key参数必须唯一否则多个组件冲突初始化检查if last_community not in st.session_state:防止首次运行报错。我带实习生做这个项目时最深的体会是所谓“高分必过”不是代码多完美而是你能否用 Python 把中介嘴里那句‘这房挂高了’翻译成df.loc[df[小区名]万科城, 单价_元平米].median() df.loc[df[标题].str.contains(万科城), 单价_元平米].iloc[0] * 0.95—— 让数据开口说话而不是替业务方做决定。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站