
1 研究背景与目标二手房挂牌价格同时受到区位、面积、户型、楼龄、楼层、朝向、装修与配套等因素影响。结构化数据分析可以帮助识别市场中主要价格梯度并建立可重复的基准估价流程。本案例不追求复杂算法而是用清晰的数据清洗、可解释特征和常见回归模型展示完整分析链路。1.1 研究目标构建可直接在 Jupyter Notebook 中运行的上海二手房分析流程。将房源描述解析为户型、面积、楼层、朝向及文本卖点标签。比较简单回归模型并从多项指标评价预测质量。逐图解释市场现象、可能原因及对建模的影响。识别目标泄漏、样本偏差和挂牌价与成交价差异等局限。2 数据来源与字段说明交付数据整理自公开的上海链家二手房挂牌数据仓库。案例主题和分析结构参考 Kaggle 上的上海二手房价格数据集与常见房价预测流程。原始字段包含房源标题、区县、板块、小区、户型描述、挂牌总价、单位面积价格和建成年份等。为便于本地复现最终 CSV 已统一为 UTF-8 编码并加入从文本中解析的结构化字段。数据来源说明挂牌信息反映采集时点的卖方报价不代表最终成交价采集时间、覆盖范围和挂牌重复更新可能影响样本代表性。报告中的价格分析仅描述本数据集不外推为当前上海全市场价格水平。字段类别主要字段用途位置字段区县、板块、小区分析空间差异小区用于分组划分不直接进入模型房屋结构室、厅、面积、总楼层、所在楼层、朝向刻画房源物理属性时间属性建成年份、房龄刻画楼龄及其非线性影响价格字段总价、单价总价为目标单价只用于探索禁止进入模型文本标签地铁、南北通透、精装修、满五、采光等从标题提取可解释的0/1卖点特征3 数据清洗与特征工程原始数据共73,316条、12个字段。清洗后保留70,613条记录删除或排除2,703条明显异常或重复样本。主要规则包括总价限定为50万至5000万元、面积限定为15至500平方米、总楼层限定为1至80层、单位面积价格限定为5000至250000元/平方米并对关键结构字段进行正则解析。3.1 目标泄漏控制单位面积价格通常满足“单价≈总价÷面积”。若在预测总价时输入单价模型几乎可以反推出目标得到看似优秀但无法用于真实估价的成绩。因此单价仅用于描述性分析绝不进入训练特征。3.2 文本标签对房源标题进行规则化关键词提取形成地铁、南北通透、精装修、毛坯、满五、满二、采光、阳台、低楼层和景观等二元变量。该方法采用透明的关键词规则容易解释、容易复现也能保留部分非结构化信息。4 探索性数据分析4.1 清洗后字段缺失率miss(df.isna().mean()*100).sort_values(ascendingFalse); showmiss[miss0] plt.figure(figsize(9,5.4)); plt.barh(show.index,show.values,colorCOLORS[0]); plt.xlabel(缺失比例%); plt.ylabel(字段); plt.title(清洗后字段缺失率); plt.grid(axisx,alpha.2); plt.tight_layout(); plt.show()、清洗后的结构化数据仅“建成年份”和由其计算的“房龄”存在缺失共2,514条占3.6%。其余建模字段均已完整解析。建模时对年份类字段采用训练集内中位数填补避免直接删除样本导致区域结构发生偏移。4.2 上海二手房挂牌总价分布4.3 各区县挂牌房源数量浦东挂牌量最高共16,947条占样本的24.0%闵行和宝山也具有较高样本量。金山、崇明和上海周边样本较少因此这些区域的均价和模型误差更容易受个别房源影响解读时不能只比较点估计。4.4 房源建筑面积分布房源面积中位数为81.0平方米样本主体集中在中小户型区间右侧仍有少量大面积改善型与豪宅房源。面积是总价最直接的数量基础但面积与价格并非严格线性位置和单价水平会造成相同面积房源的显著价差。4.5 主要户型挂牌数量4.6 主要朝向的挂牌单价中位数朝向类别之间存在一定单价差异但差异幅度明显小于区县和板块差异。朝向可以作为辅助特征却不应被单独理解为决定性因素其影响还可能与户型、楼层和采光标签共同发生。4.7 文本标签与挂牌单价中位数差异tag_cols[地铁标签,南北通透标签,精装修标签,毛坯标签,满五标签,满二标签,采光标签,阳台标签,低楼层标签,景观标签]; rows[] for t in tag_cols: m1df.loc[df[t]1,单价_元每平].median(); m0df.loc[df[t]0,单价_元每平].median(); rows.append((t.replace(标签,),(m1/m0-1)*100)) spd.DataFrame(rows,columns[标签,差异]).sort_values(差异); plt.figure(figsize(9,6)); plt.barh(s[标签],s[差异],color[COLORS[3] if x0 else COLORS[1] for x in s[差异]]); plt.axvline(0,color#555555); plt.xlabel(有标签房源相对单价差异%); plt.ylabel(文本标签); plt.title(文本标签与挂牌单价中位数差异); plt.grid(axisx,alpha.2); plt.tight_layout(); plt.show()不同标签房源的单价中位数存在正负差异但这些差异并非严格因果效应。例如“低楼层”标签可能更多出现在老旧小区“景观”标签可能集中在高价板块。标签适合用于提升预测而不宜直接解释为标签本身带来的价格溢价。4.8 主要数值特征相关性热力图总价与面积呈较强正相关单价与总价也相关但单价由总价和面积计算得到因此被明确排除出预测特征。建成年份与房龄几乎完全负相关二者同时保留主要为了展示和树模型非线性切分线性模型中标准化与正则化可缓解共线性影响。5 建模方案5.1 数据划分采用 GroupShuffleSplit 按小区分组划分数据训练集56,574条测试集14,039条训练集和测试集的小区交集为0。与普通随机划分相比该方式更严格能够检验模型对未在训练集中出现过的小区的泛化能力。5.2 模型设置模型作用主要设置中位数基线提供最低可接受参照所有测试样本预测为训练集总价中位数岭回归可解释的线性基准类别独热编码数值标准化L2正则化决策树学习非线性和特征交互最大深度16叶节点最少8条样本随机森林降低单棵树方差80棵树最大深度18叶节点最少3条样本5.3 评价指标MAE反映平均绝对万元偏差RMSE对大误差更敏感R²衡量解释的方差比例RMSLE更关注相对误差并降低高价样本支配MAPE便于以百分比解释但在低总价样本上更容易放大。6 模型结果与误差分析num_features[室,厅,面积_平米,总楼层,建成年份,房龄_年,地铁标签,南北通透标签,精装修标签,毛坯标签,满五标签,满二标签,采光标签,阳台标签,低楼层标签,景观标签] cat_features[区县,板块,所在楼层,朝向] Xdf[num_featurescat_features]; ydf[总价_万元]; groupsdf[小区] splitGroupShuffleSplit(n_splits1,test_size0.2,random_state42) train_idx,test_idxnext(split.split(X,y,groups)) X_train,X_testX.iloc[train_idx],X.iloc[test_idx] y_train,y_testy.iloc[train_idx],y.iloc[test_idx] y_train_lognp.log1p(y_train) print(f训练集{len(train_idx):,} 条测试集{len(test_idx):,} 条) print(训练/测试小区交集,len(set(groups.iloc[train_idx]) set(groups.iloc[test_idx]))) def evaluate(name,pred,rows,preds): prednp.maximum(np.asarray(pred),0) rows.append({模型:name,MAE:mean_absolute_error(y_test,pred),RMSE:mean_squared_error(y_test,pred)**0.5,R²:r2_score(y_test,pred),RMSLE:mean_squared_log_error(y_test,pred)**0.5,MAPE:np.mean(np.abs((y_test-pred)/y_test))}) preds[name]pred rows[]; preds{} evaluate(中位数基线,np.repeat(y_train.median(),len(y_test)),rows,preds) num_pipePipeline([(填补,SimpleImputer(strategymedian)),(标准化,StandardScaler())]) cat_pipePipeline([(填补,SimpleImputer(strategymost_frequent)),(编码,OneHotEncoder(handle_unknownignore,min_frequency20))]) pre_linearColumnTransformer([(数值,num_pipe,num_features),(类别,cat_pipe,cat_features)]) ridgePipeline([(预处理,pre_linear),(模型,Ridge(alpha10.0,solverlsqr))]) ridge.fit(X_train,y_train_log); evaluate(岭回归,np.expm1(ridge.predict(X_test)),rows,preds) num_treePipeline([(填补,SimpleImputer(strategymedian))]) cat_treePipeline([(填补,SimpleImputer(strategymost_frequent)),(编码,OrdinalEncoder(handle_unknownuse_encoded_value,unknown_value-1))]) pre_treeColumnTransformer([(数值,num_tree,num_features),(类别,cat_tree,cat_features)]) treePipeline([(预处理,pre_tree),(模型,DecisionTreeRegressor(max_depth16,min_samples_leaf8,random_state42))]) tree.fit(X_train,y_train_log); evaluate(决策树,np.expm1(tree.predict(X_test)),rows,preds) rfPipeline([(预处理,pre_tree),(模型,RandomForestRegressor(n_estimators80,max_depth18,min_samples_leaf3,max_features0.75,random_state42,n_jobs-1))]) rf.fit(X_train,y_train_log); evaluate(随机森林,np.expm1(rf.predict(X_test)),rows,preds) results_dfpd.DataFrame(rows).sort_values(RMSE).reset_index(dropTrue) display(results_df.style.format({MAE:{:.2f},RMSE:{:.2f},R²:{:.3f},RMSLE:{:.3f},MAPE:{:.2%}})) best_nameresults_df.iloc[0][模型]; best_predpreds[best_name]模型MAERMSER²RMSLEMAPE随机森林72.8139.20.8750.18013.0%决策树86.4165.00.8240.21615.7%岭回归76.5236.40.6390.17012.5%中位数基线232.6413.8-0.1060.57241.3%6.1 不同模型的测试集误差对比6.2 随机森林预测残差分布6.3 随机森林主要特征重要性面积、板块和区县等结构与位置变量通常位于重要性前列符合二手房总价由“面积×区位价格水平”共同决定的市场逻辑。特征重要性表示模型分裂时的贡献不等同于因果影响特别是区县与板块存在层级关联重要性会在相关变量之间分摊。7 主要结论清洗后数据包含70,613条房源浦东、闵行和宝山挂牌样本较多区域样本量差异明显。.。。。。。。。。。。。。。。。。。8 局限性与改进方向挂牌价不等于成交价卖方报价受到议价预期影响不能直接代表真实交易价格。数据时间与覆盖范围有限报告只描述交付数据集不用于判断当前上海市场水平。具体细节见原文创造不易谢谢各位多多点赞收藏