做数据分析最怕什么?不是不会调库,而是拿到一堆真实数据后不知道从哪下手。我建议第一个完整项目就从某短租平台的开放数据开始:这是某国际都市的房源数据,字段覆盖价格、房型、位置、评论时间、可订天数,既有连续数值又有分类文本,还有缺失值和异常值。跑完“数据加载→清洗→探索性分析→建模→可视化→结论”这套全流程,你对数据分析的理解会比刷十遍教程都深。这篇文章就是我做完该项目后整理的完整记录,适合刚入门数据分析的读者,也适合想把自己作品集做成全流程项目的同学参考。
1. 项目整体设计与思路拆解
1.1 为什么要拿“某国际都市短租开放数据”当练手项目
我见过很多初学者,学完 pandas 和 matplotlib 之后依然不会做项目,主要原因就是练习数据太“干净”了。教程里的数据往往已经处理得整整齐齐,而真实开放数据带给你的是最原始的状态:价格字段带着货币符号和逗号,日期列有缺失,经纬度偶尔跑偏,甚至同一个区域的写法都有好几种。这份短租数据完美满足了“真实数据”这个条件。
选择这份数据还有几个现实理由:
- 免费公开且规模适中,大约有几万条房源记录,单机内存完全能跑,不需要上分布式。
- 字段类型丰富,包括数值、类别、日期、文本、地理坐标,覆盖主流分析场景。
- 业务故事好讲,价格、房型、区域、评论量可以直接转化为“什么因素影响房价”这类人人都关心的问题。
- 数据有明显缺陷,缺失值、异常值、重复值都真实存在,这对练习数据清洗非常有价值。
动手之前我给这个项目定了一个明确目标:不只做描述性统计,还要完成一个价格预测模型,并且把结论讲给非技术同事听。目标清楚了,后面每一步才不会跑偏。
1.2 全流程分几步走,目标怎么定
整个项目我划分成六个阶段,每个阶段有输入、输出和落地的检查点,下面是我实际执行时用的表:
| 阶段 | 核心动作 | 关键输出 | 检查点 |
|---|---|---|---|
| 数据获取 | 下载开放CSV文件,确认字段含义 | 原始数据表 | 字段个数、行数是否合理 |
| 数据清洗 | 处理缺失值、异常值、类型转换 | 干净的DataFrame | 缺失率为0或已标记 |
| 特征工程 | 构造距离、价格区间、活跃度等特征 | 扩展特征后的数据表 | 新特征是否有业务含义 |
| 探索性分析 | 分布、对比、相关性分析 | 图表 + 初步结论 | 能否支撑后续建模方向 |
| 建模预测 | 训练线性回归、随机森林 | 模型与评估指标 | RMSE、R²是否可比 |
| 结果呈现 | 可视化 + 一页纸结论 | 可交付报告 | 非技术听众能否看懂 |
在这个框架里,建模不是终点,能解释结论才是。所以我在探索性分析阶段就会记录一些关键问题,比如:是不是核心区房源更贵、整套房是不是比单间有明显溢价、评论多的房源价格会不会更高。这些问题之后都会成为建模时的验证项。
1.3 分析口径先定好,后面少返工
真实项目里最容易返工的地方就是口径不清。这个项目里我把口径提前定了三件事:
- 价格口径:只看每晚价格,不处理清洁费和服务费,避免口径混乱。
- 区域粒度:用“区域组”这一层做对比分析,不细分到社区,因为社区级别太碎,样本量不均。
- 房源类型:按数据集里原有的三类房型处理,不额外合并。
这些口径看起来是小事,但如果你做了一半才发现“低价房源其实是共享房间”,回头重跑所有分析的代价会非常大。先花十分钟把口径固化下来,是给后面的自己省时间。
2. 数据获取与环境准备:先把地基打牢
2.1 数据从哪来,字段长什么样
这份开放数据通常以 CSV 格式提供,文件名为listings.csv,下载解压后直接读入即可。核心字段虽然在不同版本里略有差异,但以下几个是必有的:
| 字段名 | 中文含义 | 类型 | 使用场景 |
|---|---|---|---|
| price | 每晚价格 | 字符串,需清洗 | 分析目标 |
| room_type | 房源类型 | 类别 | 分组对比 |
| neighbourhood_group | 所属区域组 | 类别 | 区域分析 |
| latitude / longitude | 经纬度 | 浮点数 | 地理可视化 |
| minimum_nights | 最少入住晚数 | 整数 | 异常值检测 |
| number_of_reviews | 评论总数 | 整数 | 活跃度指标 |
| reviews_per_month | 月均评论数 | 浮点数 | 活跃度指标 |
| last_review | 最近评论日期 | 字符串/日期 | 时间分析 |
| availability_365 | 全年可订天数 | 整数 | 供应分析 |
| calculated_host_listings_count | 房东拥有房源数 | 整数 | 房东规模 |
这些字段足够支撑一套完整分析。我会重点利用price、room_type、neighbourhood_group做业务分组,再用latitude、longitude做空间分布展示。
2.2 环境安装:一个命令装齐所需库
我的建议是直接用 Anaconda 创建一个隔离环境。这里演示我用的命令:
conda create -n data_analysis python=3.10 -y conda activate data_analysis pip install pandas numpy matplotlib seaborn scikit-learn openpyxl注意版本问题:scikit-learn新版本里mean_squared_error增加了squared=False参数可以直接返回 RMSE,旧版本没有。如果你用的还是 0.x 版本的老环境,建议升级到 1.x 或直接用np.sqrt(mean_squared_error(...))手动开方。
2.3 第一眼体检:加载数据后的5分钟内做什么
拿到 CSV 之后,不要急着清洗,先做三件事:看维度、看字段类型、看数值分布。
import pandas as pd df = pd.read_csv("listings.csv") print("数据规模:", df.shape) df.head() print(df.dtypes) print(df.describe())这一步的产出不是多么漂亮的图表,而是对数据质量形成一个整体判断。我当时跑完describe()后立刻发现两个问题:price列的 min 居然是 0,且有些价格高到离谱;reviews_per_month存在大量缺失。这些问题会直接影响后续分析和建模,所以记录到自己的问题清单里,接下来逐一处理。
3. 数据清洗与特征工程:把脏数据变成能用的表
3.1 缺失值处理:先看业务含义再决定填还是删
缺失值处理不能机械套用“均值填充”或“直接删除”。处理之前,先弄清楚缺失为什么发生、缺失代表什么业务含义。当时列了缺失检查代码:
missing = df.isna().sum() missing = missing[missing > 0] missing_percent = (missing / len(df) * 100).round(2) print(pd.DataFrame({"缺失数量": missing, "缺失占比%": missing_percent}))这份数据里比较容易出现缺失的字段集中在评论相关列:last_review和reviews_per_month缺失的房源,通常是上线后从来没有收到过评论。这类房源要么是太新,要么入住率低、没人评价。缺失本身就是一个信号:这个房源没有获得过用户反馈。
我的处理方法是:把reviews_per_month缺失值填 0,表示“当前没有评论活跃度”;把last_review缺失单独创建一个布尔特征has_review,避免用一个假日期去填充干扰时间分析。这样既保留了缺失背后的业务信息,又不影响后续建模。
注意:
last_review不要随便填一个很早的日期,否则后面做“距最近评论天数”时会造出大量异常值。
3.2 异常值与类型修正:价格里的“$”和超低价房源
这个数据集的price列是字符串格式,比如$150或者$1,200,直接astype(float)会报错。洗价格是第一步,也是后面所有分析的前提:
df["price"] = df["price"].replace({r"\$": "", ",": ""}, regex=True).astype(float)清洗之后立刻检查异常值。我当时用两种方式并行判断:先看分布,再看业务合理性。价格最小值出现了 0,这明显是异常值;价格上限用 99% 分位作为截断参考,比写死一个“1000美元”更合理,因为不同城市价格带差异很大,固定阈值很容易误杀正常房源。
valid_price = df["price"].between(10, df["price"].quantile(0.99)) df = df[valid_price]同时,我也检查了minimum_nights,把超过 365 天的极端值视为登记错误或非正常房源,直接剔除。坐标异常值则通过过滤经纬度明显越界的记录处理。
3.3 特征工程:新增4个对价格分析有用的特征
做完清洗,我只做少量特征,保证每一步都能讲清楚业务含义。新增了四个特征:
distance_to_center:房源到全市房源几何中心点的距离,用来量化位置对价格的影响。距离计算用球面距离公式,避免直接用经纬度平面距离在大城市尺度下失真。has_review:是否有评论,0/1 布尔特征。days_since_last_review:最近评论距离数据发布日期的天数,衡量房源近期活跃度。host_listing_count:直接复用原有字段,但把大于 10 的截断为 10,防止极端值影响模型。
距离计算的核心函数如下:
import numpy as np def haversine_np(lat1, lon1, lat2, lon2): R = 6371.0 dlat = np.radians(lat2 - lat1) dlon = np.radians(lon2 - lon1) a = np.sin(dlat / 2) ** 2 + np.cos(np.radians(lat1)) * np.cos(np.radians(lat2)) * np.sin(dlon / 2) ** 2 return R * 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) df["distance_to_center"] = haversine_np( df["latitude"], df["longitude"], df["latitude"].median(), df["longitude"].median() )选取中位数坐标作为中心参照,避免被个别极端坐标带偏。
4. 探索性数据分析(EDA):先看图再建模
4.1 价格分布:右偏数据别硬建模
价格分布是这个项目里最重要的第一张图。直接画直方图会发现一个典型特征:长尾向右拖得很远,少量高端房源把均值拉得很高,中位数却远低于均值。这时如果用原始价格做线性回归,模型会被长尾样本带偏。我把价格做了自然对数变换,让分布更接近正态,后续模型的误差评估也更平稳。
df["log_price"] = np.log1p(df["price"])处理之后你会看到分布明显对称许多。这个变换不是硬凑数学,而是给模型一个更合理的优化空间。后面预测结果要还原成原始价格时,做一次np.expm1(pred)就能转回来。
4.2 区域与房型:哪个区最“金贵”
按区域组和房型做分组统计,能立刻看出市场结构。我用的分析思路是:先看每个区域的房源数量占比,再看该区域的均价中位数。
group_stats = df.groupby(["neighbourhood_group", "room_type"]).agg( 房源数=("price", "count"), 价格中位数=("price", "median"), 价格均值=("price", "mean") ).round(2)结果呈现的很明显:核心区房源数量最多,价格中位数也最高;外围区域房源数量少,价格中位数明显下降。房型上,整套房源的中位数价格最高,共享房间最低,这和直觉一致。不过要再往深看一层:核心区里整套房源和独立房间的价差,明显大于外围区域的同类价差。这种“位置溢价”在后续建楼时会体现为区域特征和距离特征的显著贡献。
4.3 时间相关特征:从评论看供需活跃度
last_review、reviews_per_month、number_of_reviews是理解房源活跃度的三件套。我画了按月份统计的“最近评论数量”折线图,发现整体评论活跃度有明显的季节波动:旅游旺季评论量集中,淡季明显下滑。这说明短租市场供需受季节影响大,价格模型里加入“月份”或“是否旺季”特征会更贴近真实规律。
这部分分析不复杂,但能让你的结论更有业务洞察。很多人只盯着价格分布和区域箱线图,忽略了时间维度,结果模型少了季节性这个解释变量。
4.4 相关性矩阵:哪些特征和价格最相关
建模型之前,先算一组相关性,避免把高度共线的特征直接塞进模型。我挑选了价格、坐标、评论数、可订天数、房源数、距离这些数值特征做相关性矩阵,并用 seaborn 热力图展示。
import seaborn as sns import matplotlib.pyplot as plt corr = df[numeric_cols].corr() sns.heatmap(corr, annot=True, cmap="coolwarm", fmt=".2f") plt.show()结论通常很稳定:距离与价格显著负相关,越靠近中心越贵;房源评分等字段若存在也与价格正相关;可订天数与价格的相关性较弱。这一步给建模提供了特征筛选方向。
5. 建模预测价格:从线性回归到随机森林
5.1 建模前处理:怎么编码、选哪些特征
建模前把数据集划分成特征和标签。标签选log_price而不是原始价格,理由已经讲过。特征方面我保留五类:
- 数值特征:
latitude、longitude、distance_to_center、minimum_nights、availability_365、number_of_reviews、reviews_per_month、days_since_last_review、host_listing_count - 类别特征:
room_type、neighbourhood_group
类别特征用 One-Hot 编码。实际操作时我直接用pd.get_dummies(df, columns=[...]),并在训练测试划分后调用,避免数据泄露。
5.2 基线模型:线性回归和两个核心指标
建模先跑一个线性回归作为基线,目的是给后续复杂模型一个对比基准。划分训练集和测试集时,随机种子固定为 42,保证结果可复现:
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) lr = LinearRegression() lr.fit(X_train, y_train) y_pred = lr.predict(X_test) rmse = mean_squared_error(y_test, y_pred, squared=False) r2 = r2_score(y_test, y_pred)指标怎么解读?RMSE代表预测值和实际值的平均偏差,单位与标签一致。因为标签是log_price,RMSE 越小说明在对数尺度下预测越准确。R²是模型解释的方差比例,0.4 左右在真实房价数据里已经算可用,别期待像教程数据那样动不动 0.9。我当时线性回归的 R² 只有 0.4 出头,RMSE 在 0.65 左右,说明还有提升空间。
注意:把预测结果的 RMSE 读回业务层面时,先做
np.expm1还原价格,再计算百分比误差,这样更容易跟业务方解释。
5.3 进阶模型:随机森林的参数设置
线性回归无法捕捉房型和位置之间的非线性关系,所以我第二个模型选了随机森林。这个模型对缺失值不敏感、不需要特征标准化,而且能输出特征重要性,解释性也不错。
参数设置上我用的是一组经过尝试后的稳健组合:
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=500, max_depth=15, min_samples_split=5, min_samples_leaf=2, random_state=42, n_jobs=-1, oob_score=True ) rf.fit(X_train, y_train)参数为什么这样设:n_estimators=500保证预测稳定,继续增大到 1000 的边际收益很小;max_depth=15限制树深,防止训练集样本量不大时过拟合;min_samples_split=5和min_samples_leaf=2相当于正则化,让每棵树不那么“钻牛角尖”。oob_score=True可以免费看到一个袋外评分,作为快速验证。
最终随机森林的 R² 提升到 0.55 左右,RMSE 降到 0.55 附近。这个提升说明确实存在非线性关系。特征数量不多,训练耗时可接受。
5.4 结果解读:真正影响价格的因素
随机森林有一个特性,就是训练完成后可以直接看feature_importances_。我当时按重要性排序后发现,排名靠前的特征基本稳定为:distance_to_center、room_type、latitude、longitude、minimum_nights、number_of_reviews。
这个结果的意义不在于“模型能用”,而在于它能回答业务问题:位置仍然是短租定价最核心的因素,距离核心圈越近,价格越高;房型的影响紧随其后,整套房源的溢价明显;评论数反映房源口碑和人气,也会正向影响价格。
线性回归的系数还能告诉你方向,随机森林只能给重要程度,所以我更推荐线性回归和树模型一起跑:一个用来说明方向和强度,一个用来做预测精度。两种视角互补。
6. 可视化与交付:让分析结果能被听懂
6.1 三张必画图:地图、箱线图、特征重要性
交付给非技术听众时,图的数量不是越多越好,而是每张图都要回答一个问题。我最终选了这三张:
第一张是价格空间分布图。用经纬度做散点图,颜色映射到价格,立刻可以看到核心区颜色偏亮、外围偏暗,比任何文字都直观:
plt.scatter(df["longitude"], df["latitude"], c=df["price"], cmap="plasma", s=4, alpha=0.5) plt.colorbar(label="price") plt.show()第二张是区域与房型的箱线图,用来展示“不同区域不同房型”的价格分布,既能看中位数,也能看波动范围。
第三张是特征重要性条形图,直接展示模型认为哪些变量最重要。
importance = pd.Series(rf.feature_importances_, index=X_train.columns).sort_values(ascending=True) importance.plot.barh(figsize=(8, 8)) plt.show()6.2 输出结构化结论:一行结论配一张图
我在项目收尾时按照“结论 → 证据 → 建议”的结构整理了一页纸报告。比如:
- 结论:位置是短租定价的最强影响因素。
- 证据:价格散点图呈现明显的中心向外围递减;距离变量在模型重要性中排第一。
- 建议:如果想提高收益,核心区小户型比外围大户型更有竞争力。
这个方法很适合工作场景。别把 20 页图表全丢给老板,一个核心结论配一张核心图,是最有效的表达方式。
6.3 如何把项目代码改造成可复用模板
做完一版分析后,我把清洗、特征工程、建模、画图分别封装成函数,文件名按流程命名。这样下次拿到另一个城市的开放数据,只需要替换数据源路径,微调字段映射就能复用同一套流程。
我建议你也这么做:把load_data()、clean_data()、add_features()、train_model()独立出来,固定输入输出格式。第一次写可能多花半小时,但后续迭代和换数据集时省下的时间远不止半小时。
7. 常见问题与排查技巧实录
7.1 数据加载失败与乱码怎么处理
我在加载 CSV 时遇到过两种典型问题:一是文件路径含中文或空格导致读取失败,二是文件编码不是默认的 UTF-8。读入时最好显式指定编码:
df = pd.read_csv("listings.csv", encoding="utf-8")如果报错,换成encoding="latin-1"或encoding="gbk"试试。另外,尽量避免在路径中使用中文目录,这是最简单的避坑方式。
7.2 缺失值比例超过一半的列怎么处理
有些字段缺失比例可能超过 60%,比如评论相关列。这时候要区分两种情形:如果缺失集中在某个区域或某个房型,那可能是数据登记问题;如果是随机缺失,需要判断该列的业务价值。
我一般原则是:重要特征缺失率超过 60%,先不要直接删列,而是构造“是否缺失”特征再做模型对比。这样能判断缺失本身是否包含信息。随意删列会丢失这部分信号。
7.3 模型和测试集分不好导致过拟合怎么办
我当时踩过的坑是:先对整个数据集做了 One-Hot 编码,再切分训练测试集,结果特征里包含了测试集的信息,属于“数据泄露”。虽然随机森林的 R² 高达 0.7,但换到新数据立刻掉下来。
正确做法是先切分,再在训练集上做编码和特征计算。更严谨一点,连续特征如果要做标准化,也应该先用训练集的均值方差去转换测试集,而不是各自独立处理。
7.4 关于这个项目的几点个人实操心得
这个项目我前后完整跑了三轮。第一轮只想快点出结果,忽略了不少业务细节;第二轮补了距离特征和日期处理,模型解释性变好很多;第三轮才开始整理函数和报告,也算找到了比较舒适的节奏。
最后再分享一个小技巧:做这类全流程项目时,从一开始就给列名用英文、给代码文件按数字编号,你会发现在最后写报告、复现结果时会顺畅很多。真实项目里没人看你写了多花哨的代码,只看你能不能稳定复现结果、把结论讲清楚。数据分析项目(1)到这里收尾,下一期我会把同样的流程换一份数据再做一遍,对比两次分析里哪些步骤可以完全复用,哪些环节必须重新思考。