☰
民宿价格预测Baseline:数据清洗、特征工程与RMSLE评估全流程
2026/10/19 1:49:13 网站建设 项目流程

先说清楚我在做什么

几个月前我从某个开放数据渠道拿到了一份民宿挂牌数据,记录量大概在二十万条左右,覆盖了十几个城市的房源信息。字段有标题、描述、房型、房间数、可住人数、经纬度、所在城区、评论数、评分、上线时间、历史预订情况,以及我们要预测的目标变量——每晚挂牌价格。

我当时的任务很直接:基于这些字段,训练一个能预测民宿每晚价格的模型。第一次拿到这类数据的人可能会被标题和描述这类文本字段吓到,觉得必须上NLP、上深度学习。但我在做的第一件事,不是搭复杂模型,而是先花时间把数据洗干净、把评估指标定清楚,然后跑一个尽量简单的baseline。

这篇文章就是把整个baseline从零搭建的过程完整写一遍,包括数据清洗、特征工程、模型选择、验证方案,以及几个我实际踩过、回来复盘才发现问题出在哪里的坑。对于刚接触价格预测类题目的同学,这套流程可以直接照着跑;对于想搭自己的baseline的人,我尽量把每个决策的“为什么”也讲清楚,而不是只丢一段代码。

1. 先把问题定义清楚:价格预测的输入输出和评估方式

1.1 目标变量不是“价格”,是“对数价格”

民宿挂牌价格和很多人想的不一样,它不是正态分布的。绝大多数房源集中在某个区间,比如200到800元每晚,但会拖出很长的右尾,那些高端民宿、别墅、整租豪宅,单晚能到三四千甚至更高。直接拿原始价格做回归,模型会被这些高价位样本带走,导致中低价位的房源预测严重偏高。

我用的是最常规的处理方式:对价格取自然对数,用对数价格作为回归目标。等到评估和提交预测结果时,再做指数还原。这么做的好处有两个。

一是缓解右偏分布。取对数后,价格分布会明显更接近正态,对线性模型和树模型都更友好。

二是误差衡量更合理。对同样的绝对误差,比如50元,挂在300元的房源上,和挂在3000元的房源上,业务含义完全不同。对数空间下的误差,天然表示“相对误差”,更贴近定价场景的直觉。

提示:如果你不想手动取对数,也可以直接选择RMSLE这类评估指标,它的数学形式和取对数回归是等价的。但我在实践中还是习惯先把目标变换写清楚,这样后面分析残差时更直观。

1.2 评估指标为什么选RMSLE,而不是MAE或RMSE

评估指标这个事,我在第一次做的时候没有太在意,随手报了RMSE,结果模型调了半天,线下指标很漂亮,换成MAE看却惨不忍睹。原因在于RMSE对大误差的惩罚是平方级的,几个高价位房源的预测偏差会把整体数值拉得很难看。

我这次用的主指标是RMSLE。它的计算方式也很直白:

RMSLE = sqrt(mean( (log(1 + y_pred) - log(1 + y_true))^2 ))

这个指标对高价位样本的惩罚相对温和,对低价位样本的误差更敏感。换成业务语言,就是“同样预测错20%,300元的房子比3000元的房子需要更警惕”,因为低价位房源的容错空间更小。

我同时保留了MAE作为辅助指标。MAE直接看均价偏差是多少元,方便向非技术角色解释模型效果。RMSE我也保留,但它只是作为诊断用——如果RMSLE和RMSE差距很大,通常意味着有少量高价位样本被严重预测错误。

1.3 建模样本的切分方式

时间类数据有个容易犯的错:直接随机切分训练集和测试集。民宿挂牌数据是典型的横截面加时间混合数据,房源的上线时间有先有后,如果随机切分,某些“时序泄漏”问题会被藏在数据里,最后模型上线时表现大幅缩水。

我采用的方案是把数据集按上线时间排序,用较早的作为训练集,较晚的作为验证集,接近末尾的一小段作为最终测试集。这样模拟的是“我用历史房源去预测将来上线的房源”这个真实业务场景。

2. 数据清洗:价格字段和地理坐标里的脏东西

2.1 价格字段的预处理:货币符号、千分位、HTML残留

这块我觉得值得单独写,因为很多人拿到数据的第一件事就是跑模型,结果在数据清洗阶段就埋下了雷。民宿数据的字段一般不会像比赛里整理得那么干净,我这份数据里的价格字段就有三种格式混杂:

$180 1,280 350 USD

有的字符串里还带着HTML标签残留,类似<span class="price">350</span>这种。我的清洗逻辑分几步走:

  1. 先用正则把所有非数字、非小数点的字符都剔除。
  2. 再处理千分位逗号,否则1,280会被解析成1和280。
  3. 最后统一转成浮点数,超出合理范围的值先标记出来,不急着删。
import re import numpy as np import pandas as pd def clean_price(value): if pd.isnull(value): return np.nan text = str(value).replace(",", "") text = re.sub(r"[^0-9.]", "", text) try: price = float(text) except ValueError: return np.nan return price df["price"] = df["price"].apply(clean_price)

清洗完以后,价格的最小值、最大值、分位数都要看一遍。我这份数据里出现了标价1元的房源,也有标价几万元的,这些大概率是数据抓取错误或者测试房源,直接保留会让模型去拟合错误样本。

我的处理原则是:低于10元或高于该城市价格99.5%分位数的样本,先单独拎出来人工判断。如果只是极少数个案,直接在训练集里剔除;如果成规模出现,就要回头检查是不是抓取逻辑出了问题。

2.2 经纬度数据里的坐标漂移

经纬度是民宿价格预测里一个非常关键的特征,因为位置几乎直接决定价格底线。但这份数据的经纬度字段同样有坑。

我清洗时做了一件事:把所有经纬度画在散点图上,叠加城市边界信息,结果发现有一批房源的坐标落在了完全不合逻辑的位置。比如标注在某一线城市市中心的房源,经纬度对应的实际地理位置却在另一个省份。这种漂移通常来源于地图POI解析错误、人工录入偏差、或者部分平台为了保护隐私做的坐标偏移。

我的处理方式是计算每个房源到所在城市中心点的距离,超出合理范围的数据点单独标记成“坐标异常”特征,而不是直接删除。原因很简单:坐标漂移本身可能也是某种数据质量信号,直接删除可能损失样本量,标记出来反而能让模型学习到“这种房源的特征组合”是否有特殊定价。

2.3 缺失值处理:不要无脑填充

缺失值的处理方式,价格预测任务里有一个容易被忽略的点:缺什么、为什么缺,本身就是信息。

比如描述文本缺失,可能意味着该房源是房东草草上架的,那么它的定价逻辑和其他精心维护的房源就不同。这个时候,用“均值填充”或者“众数填充”反倒会抹掉这个信号。我用的方案是:

是否缺失描述 -> 单独建一个布尔特征 描述长度 -> 填充为0 评论数缺失 -> 填充为0,同时记一个是否缺失特征 房间数缺失 -> 按该房型的众数填充

缺失值处理完之后,我会跑一遍特征和价格的相关性,看看缺失标记本身有没有预测力。我这份数据里,描述缺失的房源平均价格比完整描述的房源低了大概20%,这个信号如果不单独建特征,就会被完全忽略。

3. 特征工程:从原始字段里榨出有效信号

3.1 文本类特征的轻量处理

标题和描述是民宿数据里文本信息量最大的两个字段。直接上BERT这类模型不是不行,但对一个baseline来说成本和复杂度都不划算。我用的方案是先跑TF-IDF,把标题向量化,再从中抽几个手工特征。

手工特征包括:标题长度、是否包含“地铁”“市中心”“步行”这类地段词、是否包含“独栋”“别墅”“loft”这类房型词、是否包含“ins风”“网红”“设计师”这类风格词、描述文本的情感极性得分。

这一步骤看似简单,但对价格预测的提升非常明显。以我这边数据为例,标题中包含“地铁”的房源,均价普遍比同城区不含该词的房源高15%到25%,因为通勤便利直接反映在定价里。把这些词转成特征后,数量化信号会比单纯的TF-IDF维度更直接。

TF-IDF本身的维度我控制在5000维以内,用稀疏矩阵形式,后续直接拼接进特征矩阵。这个做法虽然土,但确实有用,尤其对线性模型来说,文本信号是拉开价格差距的重要维度。

3.2 时间维度的信号:不只是“上线天数”那么简单

民宿价格的时序信号主要有两个来源:

一个是房源的“年龄”。新上线的房源为了让排名靠前、积累评论,通常会设置比较低的“促销价格”;上线时间越久、评论积累越多的房源,定价往往越高。我用上线日期和数据集生成日期做差,得到上线天数,同时统计评论数密度,也就是日均评论数。

另一个是“未来预测日”的时间属性。如果在做预测时,目标日期是已知的,那么:

  • 是工作日还是周末
  • 是否处于节假日
  • 是旺季还是淡季
  • 距离入住日期还有多少天

这些在真实的业务预测场景里有强解释力。周末房源价格普遍高于工作日,节假日更是有明显的波峰。我这次没有直接做“给定入住日期预测价格”的版本,而是把上线时间相关特征先纳入baseline。如果你手头的数据带了入住日期,那时间特征是无论如何都不能漏掉的。

3.3 地理特征的加工:从经纬度到区域聚簇

地理特征的加工,我做了三个层次:

第一层是直接用经纬度作为特征。树模型可以从中切分出一些粗略的空间分区,但精度有限。

第二层是计算房源到城市中心点的距离。这一步很关键,因为绝大部分城市的民宿价格都遵循“离中心越近越贵”的规律,虽然大城市会有多个副中心,但单中心城市规律在绝大多数二线城市依然明显。

第三层是做密度聚类。我用经纬度的KMeans聚类,把每个城市的房源聚成20到30个簇,然后把“属于哪个簇”作为类别特征。这么做的好处是,模型能捕捉到“某一小片区域”的整体定价水平,而不是只能用距离这种粗糙的连续变量。

我在实践中发现,第三层特征对模型的贡献最大。原因是民宿的定价受片区影响极大,同一个城市里隔了两公里,价格可能差出50%。聚类特征本质上把“片区均价”这种信息编码进去了。

3.4 类别特征的编码:频数编码还是目标编码

我这份数据里有几个高基数的类别特征,比如城市、城区、房东ID。高基数类别特征如果直接用独热编码,会产生上万维稀疏矩阵,对线性模型还好,对树模型则是灾难。

我的处理方式分两种:

城市和城区这些基数适中、业务意义明确的特征,直接做目标编码。目标编码就是用历史样本里该类别对应的平均价格来替换原始类别值。这个方案有个风险,就是容易过拟合,所以我会用交叉验证里的fold内统计,避免用全量数据算均值造成数据泄漏。

房东ID这种超高基数特征,我改用频数编码。一个房东名下有多少套房源,这个数量本身就能反映其经营规模。专业运营的房东通常有更成熟的定价策略,而只有一两套房源的个体房东定价会更随意,波动更大。

这里有一个非常重要的细节:目标编码中的均值统计必须在每一折交叉验证的“训练折内”计算,否则验证集的信息会泄漏进训练集。这一点我放到后面专门展开讲。

4. Baseline搭建:从线性回归到LightGBM的取舍

4.1 为什么先跑线性模型

我一直认为,baseline的价值不是拿到一个最低的分数,而是建立一个可以解释的判断基准。线性回归在这种任务里虽然精度不如树模型,但它有不可替代的作用:

第一,线性模型对特征的解释性强。哪些特征是正相关、哪些是负相关,系数一目了然,能帮你快速发现特征工程里的逻辑错误。

第二,它能帮你做数据质量验证。如果特征工程做得没问题,线性模型的RMSLE应该能稳定在一个合理范围。如果某个特征加入后指标反而变差,说明这个特征不是有问题,就是编码方式错了。

我的baseline配方是:清洗后的字段特征 + TF-IDF文本特征 + 目标编码特征 + 距离和聚类特征,模型用岭回归,正则化参数稍微调大一点,防止高维稀疏特征过拟合。

4.2 线性baseline的实现细节

岭回归的好处是可以通过L2正则控制系数幅度,对稀疏高维特征很友好。我用的是scikit-learn自带实现,配合StandardScaler做特征标准化。

from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline model = Pipeline([ ("scaler", StandardScaler()), ("ridge", Ridge(alpha=100.0)) ]) model.fit(X_train, y_train_log)

这里的alpha取值很关键。我一开始设的是默认的1.0,结果训练集和验证集的RMSLE差距很大,典型的过拟合。把alpha调到100后,验证集指标明显回升,高维文本特征对模型的影响被压到了合理的范围。

调alpha的过程本身也很有价值:如果你发现alpha怎么调都压不住过拟合,那大概率不是正则化的问题,而是特征工程里引入了泄漏,比如用了全量数据的目标编码。

4.3 验证策略:分组交叉验证

我最终的验证方案不是简单K折,而是按“房源上线时间段”做分组交叉验证。这里的逻辑是,同一时间段上线的房源,其抓取渠道、平台策略、竞争环境都更相似,如果随机打乱,模型会利用这些非业务信号。

分组交叉验证的代码框架是:

from sklearn.model_selection import GroupKFold groups = df["listing_week"] # 按上线周分组 gkf = GroupKFold(n_splits=5) for train_idx, valid_idx in gkf.split(X, y, groups=groups): X_train, X_valid = X.iloc[train_idx], X.iloc[valid_idx] y_train, y_valid = y.iloc[train_idx], y.iloc[valid_idx] ...

用这种方式验证出来的指标,比随机K折更接近模型在真实场景中的表现。我在实际操作中发现,随机K折的RMSLE通常会比分组交叉验证低10%左右,这个差距就是隐藏的时间泄漏造成的。

4.4 线性模型的结果与误差分析

岭回归跑下来,我这份数据上的RMSLE大约是0.48左右,换算回价格,平均偏差在30%上下。这个成绩谈不上好,但对于一个baseline来说足够了,更重要的是看误差分布。

我把预测残差按价格区间拆开看,发现低价房源被系统性高估、高价房源被系统性低估。原因是低价房源数量极大,模型拟合时会把整体预测偏向均值。这个偏差在后续用树模型时会被明显缓解。

同时我看了残差和地理位置的关系:部分票价异常偏高的区域,模型系统性地预测偏低。这就说明区域特征还没被充分编码,把KMeans聚类数从10调到30之后,这部分误差明显下降了。

5. 踩坑记录:时间泄漏、对数变换和异常价格

5.1 目标编码里的时间泄漏

我在第一节就提到目标编码要注意泄漏,这里详细说一下我实际踩坑的过程。

第一版特征工程里,我用全量数据的groupby(城区).mean()计算了每个城区的平均价格,然后填充到特征里。训练时指标非常好看,RMSLE降到0.4以下,我差点以为特征工程已经做到位了。

后面换成按时间分组交叉验证时,指标瞬间变差。排查了很久才发现,问题出在目标编码上——全量均值包含了验证集未来的价格信息。这就好比考试前把答案夹在书里,做题的时候翻到了,以为自己都会,上考场才发现什么都不记得。

正确的做法是在每一折训练中,只用训练集的样本计算目标编码的均值。如果是线上推理场景,则用截至当前时间的所有历史数据来计算。

5.2 对数变换的边界情况:价格为零怎么办

对数变换有个边界问题:如果价格恰好为0,log(0)是负无穷。虽然民宿价格不太可能为0,但我的数据里确实出现了0元价格的房源,大部分是抓取错误。

我的处理方式是做log1p变换:

y_log = np.log1p(df["price"].values)

这样即使价格为0,变换结果也是0,不会产生无穷大。预测时用np.expm1还原。如果你的数据里有比0更小的异常值,那就要先清洗掉,不能直接进模型。

5.3 异常低价房源对模型的扰动

我清洗时保留了一部分价格在10到50元之间的房源,理由是它们可能真的是某种特殊场景的定价,比如青旅床位、秒杀活动价。但模型训练时,这部分样本对残差的贡献很大,因为它们价格低,对数空间下的微小误差都会被放大。

我最终的处理方案是:不是直接删掉这些样本,而是给它们单独建一个“是否低价”的标识特征,让模型能区分“这类房源就是一个特殊品类”和“这类房源是正常房源但被错误标价”。加了标识特征后,低价区间的残差明显平稳了很多。

这里我想特别强调一下:异常值和离群点的处理,尽量不要一删了事。删除之前先确认它是错误还是真实存在,如果是真实的特殊品类,保留并标记,往往能帮助模型学到更有价值的信息。

5.4 坐标异常值不一定要删除

前面提到的坐标漂移,我最初的做法是直接删除。后来发现删除后某些城区的样本量明显减少,导致这些城区的价格预测变得更加不稳定。

调整后的方案是保留这些样本,但把“坐标是否异常”作为一个特征。模型后来的表现显示,坐标异常特征的系数显著不为零,说明坐标异常样本在定价逻辑上确实存在系统性的差异。

6. 从baseline出发:后续迭代的具体方向

6.1 用LightGBM替换线性模型

baseline的价值在于给了你一个起点,而不是终点。我在岭回归之后,下一轮迭代就换成了LightGBM。树模型在这个问题上天然有优势:能自动捕捉特征交互、对异常值更鲁棒、不要求特征标准化,而且能直接处理类别特征。

LightGBM的代码并不复杂:

import lightgbm as lgb params = { "objective": "regression", "metric": "rmse", "learning_rate": 0.05, "num_leaves": 63, "min_child_samples": 20, }

我当时的实验结果是:LightGBM比岭回归的RMSLE降低了大约20%到30%,提升非常明显。但注意,这是在特征工程和验证方案都稳定之后才有的提升,如果你在基线阶段就直接上树模型,遇到问题你会分不清是特征问题还是模型问题。

6.2 从“预测价格”到“价格区间”的改造

实际业务里,有时候我们并不需要预测一个精确的价格,而是需要预测一个价格区间,比如“这个房源应该定在300到350元之间”。这种情况下,可以把回归问题改成多分类问题,把价格离散成若干个梯度区间。

这种改造有个额外的好处:分类模型往往比回归模型更稳,特别是在数据量不足时,回归模型容易在极端价位上产生离谱的预测,分类模型则天然受限在有限的类别里。

如果你用的是这个思路,可以做两阶段的预测:先用分类模型预测价格区间,再在区间内用回归模型预测精准价格。这样既保留了粒度,又降低了模型的方差。

6.3 补充外部数据:交通、商圈与竞争格局

民宿价格的本质是供需关系,单靠房源自身的特征,能捕捉到的只是“这个房源客观价值”的上限。真实的成交价格还会受到周边竞争房源、短期供需波动、交通便利度、商圈热度的影响。

后续迭代可以考虑补充的外部数据包括:

  • 房源周边一公里内其他民宿的均价和供给量
  • 到最近地铁站的距离和时间
  • 周边餐饮、景点、商场的POI密度
  • 目标日期前后的同城大型活动日历

外部数据补充是价格预测模型从“能跑”走向“好用”的关键一步。但在补充之前,建议先把baseline的验证框架固定下来,否则外部数据带来的指标提升也难以判断真假。

最后说两句

跑完这一轮baseline,我最大的体会是:一个项目能不能做好,很多时候不是取决于模型有多先进,而是取决于你对数据的理解有多深。baseline是帮你建立这个理解的最短路径。

如果你现在也在做一个类似的预测任务,我的建议是,先别急着上BERT、上AutoML。花时间搞定评估指标、数据清洗、验证方案,用最简单的模型跑通全流程,然后再逐步替换模型、增加特征。你会发现,每替换一块,提升都是清晰可见的。我自己跑完这个baseline之后,对民宿定价逻辑的理解,比一开始凭空想模型的时候清晰得多。

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

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

立即咨询