拿到一份 Airbnb Listings 数据,很多人的第一反应是直接开始画图、跑模型,结果一做特征分析就发现价格列带着$符号、评分大量缺失、同一套房源重复出现好几遍。本文就围绕 Airbnb 房源列表数据的清洗与整形,整理一套从原始 CSV 到可直接分析的数据集完整流程,包含代码示例、常见报错和工程习惯,适合正在学习 pandas、准备做数据比赛或搭建数据分析项目的开发者。
为了让过程可复现,我会按“先体检、再清洗、后整形、再导出”的顺序讲解。你不需要额外准备数据库,只要能跑 Python 和 pandas 就能跟着操作。
1. 为什么要清洗和整形 Airbnb 房源数据
1.1 什么是 Airbnb Listings 数据
Airbnb Listings 数据,简单说就是 Airbnb 平台上房源列表信息的汇总表。每条记录对应一套房源,字段通常包含房源 ID、房源名称、房东信息、位置坐标、房间类型、可住人数、价格、评分、评论数、可预订天数等。
这类数据在数据分析、城市研究、旅游推荐、价格预测等场景中非常常见。很多人会从公开渠道下载某个城市或区域的listings.csv文件,然后基于它做分析。但公开数据并不等于“干净数据”,恰恰相反,它保留了真实业务数据里最常见的脏乱形态。
1.2 原始数据常见问题
把一份典型的 Airbnb listings 文件打开后,你会遇到下面这些问题:
- 价格列是字符串,例如
$120.00,带货币符号和千分位逗号,不能直接求均值。 - 浴室数量列是
2.5 baths这类混合文本,需要提取数字。 last_review日期列可能是空值,也可能是不同格式的日期字符串。host_is_superhost列是t/f这样的布尔文本,不是 Python 的True/False。- 同一房源 ID 重复出现,可能来自多次抓取或数据合并。
- 大量列存在缺失值,例如
reviews_per_month对于没有评论的房源是空值。 - 部分极端值,例如价格为 0、可住人数为 0,会影响统计结果。
这些问题如果不处理,后面的分组统计、透视表、可视化、机器学习建模都会出现偏差。
1.3 清洗与整形的关系
这里需要区分两个概念:
- 清洗(Cleaning):解决“数据不对”的问题,包括缺失值、重复值、异常值、类型错误、格式不统一。
- 整形(Shaping):解决“数据不方便用”的问题,包括行列调整、字段拆分、类型转换、新增特征、分组汇总、透视转换。
清洗是整形的前提,整形是清洗后的下一步。大多数 pandas 教程会把它们混在一起讲,但实际项目里最好按这个顺序推进:先保证每条记录和每个字段是“对的”,再把它变成“好用的”。本文的实战部分也采用这个流程。
2. 环境准备与数据集说明
2.1 运行环境与依赖
本文代码基于 Python 3 环境,核心依赖是 pandas 和 numpy。你可以用 pip 安装:
pip install pandas numpy版本需要根据你的项目实际情况调整,本文示例以 pandas 1.5+ / 2.x 的常见写法为例,重点演示清洗与整形思路。如果你使用的是更早版本,个别方法名可能有差异,但整体流程一致。
推荐使用 Jupyter Notebook 或 VS Code 的交互式窗口逐步执行,方便分步查看每一阶段的数据形态。
2.2 数据集字段说明
为了演示效果,下面以一份典型的 Airbnblistings.csv为例。真实数据可能列名略有不同,但核心字段通常是这些:
| 字段名 | 含义 | 常见脏数据示例 |
|---|---|---|
| id | 房源唯一 ID | 重复记录 |
| name | 房源名称 | 空白、乱码 |
| host_id | 房东 ID | 无 |
| host_name | 房东名称 | 缺失 |
| neighbourhood_group | 区域组 | 大小写混用 |
| latitude / longitude | 经纬度 | 缺失或越界 |
| room_type | 房间类型 | 空格、大小写不一致 |
| price | 每晚价格 | $120.00 |
| minimum_nights | 最少入住天数 | 异常值 0 |
| number_of_reviews | 评论数 | 缺失 |
| last_review | 最近评论日期 | 字符串日期 |
| reviews_per_month | 每月评论数 | 缺失 |
| availability_365 | 一年可订天数 | 缺失 |
| bathrooms_text | 浴室描述 | 2.5 baths |
| host_is_superhost | 是否超赞房东 | t/f |
2.3 项目目录结构设计
建议先建好目录,把数据文件和脚本分离。这里有一个常见教训:很多人把脚本、原始数据、输出结果全放在同一层目录,开发到后面目录越来越乱,甚至在不同路径之间复制文件时出现奇怪报错。
一个朴素的目录结构如下:
airbnb_clean/ ├── data/ │ └── listings.csv ├── output/ │ └── airbnb_listings_clean.csv ├── scripts/ │ └── clean_listings.py └── notebooks/ └── 01_eda.ipynb在终端里创建目录时,如果父目录不存在或权限不足,会出现类似cannot mkdir的报错。这往往不是命令本身写错,而是目录结构需要先理清:要么提前创建好父目录,要么检查当前用户是否有写权限。在 Python 脚本里更推荐使用pathlib的方式,后面实战代码会给出。
3. 核心方法拆解
3.1 数据体检:info、describe、dtypes
拿到数据第一步不是清洗,而是体检。pandas 提供了三个最基础的方法:
import pandas as pd df = pd.read_csv("data/listings.csv") print(df.shape)df.shape查看行数和列数。df.info()查看每列的非空数量、数据类型,是最快的缺失值扫描方式。df.describe(include="all")查看数值列和类别列的大致分布。
执行完这三步,你基本能判断哪些列需要重点处理。
3.2 缺失值处理
缺失值处理没有银弹,核心思路是分列处理:
- 如果某列缺失比例过高,且业务上不重要,可以直接删除该列。
- 如果缺失比例低,可以选择填充,常用中位数、均值、众数或固定值。
- 如果缺失列是日期、评论等强相关字段,可以结合业务规则填充,例如没有评论的房源
reviews_per_month填 0。
判断缺失比例可以这样写:
missing = df.isna().mean().sort_values(ascending=False) print(missing[missing > 0])isna()返回布尔表,mean()计算每列缺失比例,按降序输出后,一眼就能看出哪些列是“缺失大户”。
3.3 重复值处理
重复值要看业务含义:
- 完全重复的行可以直接删除。
- 更常见的是
id重复,但其他字段略有差异,这时需要指定subset参数按 ID 去重。
df.drop_duplicates(subset=["id"], keep="first", inplace=True)keep="first"表示保留第一次出现的记录,后面的重复记录删除。
3.4 数据类型修正
pandas 读入 CSV 后,所有列默认按内容推断类型。遇到混合类型时,经常出现明明应该是数字的列变成object。修正方式是用pd.to_numeric:
df["price"] = pd.to_numeric(df["price"], errors="coerce")errors="coerce"的意思是:能转成数字就转,不能转的变成NaN。这样既不会因为个别脏数据中断程序,也能在后续检查中看到哪些值有问题。
3.5 字符串清洗
字符串清洗通常处理四类问题:
- 去除首尾空格:
str.strip() - 统一大小写:
str.lower()/str.upper() - 替换或删除字符:
str.replace() - 提取子串:
str.extract()
对于$120.00这样的价格,可以先把货币符号和逗号去掉,再转数值。对于2.5 baths,可以用正则表达式提取小数部分。
3.6 日期处理
日期字段最常见的两个问题:一是空值,二是格式不统一。pandas 的pd.to_datetime能自动解析大部分常用日期格式:
df["last_review"] = pd.to_datetime(df["last_review"], errors="coerce")一旦解析失败的值变成NaT,日期列就是真正的datetime64类型,后续可以做月份提取、天数计算等操作。
4. 完整实战:清洗并整形 Airbnb 房源表
这一节我们把前面拆解的方法串成一个完整流程。建议新建一个脚本文件scripts/clean_listings.py,每一段代码都可以独立运行,也可以通过主函数串起来。
4.1 创建项目目录并读取数据
# 文件路径:scripts/clean_listings.py from pathlib import Path import pandas as pd import numpy as np # 创建输出目录 BASE_DIR = Path(__file__).resolve().parent.parent DATA_DIR = BASE_DIR / "data" OUTPUT_DIR = BASE_DIR / "output" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) print("输出目录:", OUTPUT_DIR)这里用exist_ok=True,即使目录已经存在也不会报错。相比os.makedirs在目录已存在时抛FileExistsError,这种方式更省心,也避免了 shell 里cannot mkdir这类因目录冲突导致的报错。
读取数据:
df = pd.read_csv( DATA_DIR / "listings.csv", na_values=["", "N/A", "NA"], ) print("原始数据形状:", df.shape)na_values可以把常见空字符串统一识别为缺失值,避免后续清洗时漏掉。
4.2 第一步:数据体检
print(df.info())运行后会看到类似下面的输出(列数会根据真实数据变化):
RangeIndex: 5000 entries, 0 to 4999 Data columns (total 18 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 5000 non-null int64 1 name 4930 non-null object 2 price 4970 non-null object 3 room_type 5000 non-null object 4 bathrooms_text 4800 non-null object ...可以明显看到:price是object而不是数值,name、bathrooms_text都有缺失。
接下来查看数值分布和缺失比例:
print(df.describe(include="all")) missing_ratio = df.isna().mean().sort_values(ascending=False) print(missing_ratio[missing_ratio > 0])4.3 第二步:处理缺失值
这一步的策略是先删再填。
先删除缺失严重且业务价值不高的列:
# 这些列如果缺失率过高,直接删除 cols_to_drop = ["license", "neighborhood_overview"] df = df.drop(columns=[c for c in cols_to_drop if c in df.columns])对缺失率较低但有明确业务含义的列做填充:
# 没有评论的房源,每月评论数填 0 df["reviews_per_month"] = df["reviews_per_month"].fillna(0) # 评分缺失用中位数填充,避免极端值影响 df["review_scores_rating"] = ( df["review_scores_rating"].fillna(df["review_scores_rating"].median()) ) # 房间描述缺失可以先填 "Unknown" 或暂时保留 df["name"] = df["name"].fillna("Unknown")对于last_review日期列,如果没有最近评论,实际上不应该随意填一个假日期。更合理的做法是把日期解析后,结合number_of_reviews判断:评论数为 0 的房源,把last_review保留为缺失,在后续分析中单独处理即可。
df["last_review"] = pd.to_datetime(df["last_review"], errors="coerce")4.4 第三步:处理重复值
先统计完全重复的行:
print("完全重复行数:", df.duplicated().sum()) df = df.drop_duplicates()再看id层面的重复:
print("按 id 去重前的行数:", len(df)) df = df.drop_duplicates(subset=["id"], keep="first") print("按 id 去重后的行数:", len(df))很多真实场景里,完全重复的行并不多,但id重复很常见。按id去重后,数据集的唯一性就有了保障。
4.5 第四步:修正字段类型与格式
价格列清洗:
df["price"] = ( df["price"] .astype(str) .str.replace("$", "", regex=False) .str.replace(",", "", regex=False) .str.strip() ) df["price"] = pd.to_numeric(df["price"], errors="coerce") print(df["price"].describe())这里先把价格统一转成字符串再清洗,避免个别行是数字时直接调用str方法报错。regex=False表示把$当作普通字符替换,不做正则匹配。
浴室数量提取:
df["bathrooms"] = ( df["bathrooms_text"] .astype(str) .str.extract(r"([\d.]+)") .astype(float) ) df["bathrooms"] = pd.to_numeric(df["bathrooms"], errors="coerce")str.extract(r"([\d.]+)")会从2.5 baths中提取2.5,从1 bath中提取1。
布尔字段转换:
df["host_is_superhost"] = df["host_is_superhost"].map( {"t": True, "f": False, "T": True, "F": False} )如果转换后仍有NaN,说明原始数据里存在其他值,可以打印检查:
print(df["host_is_superhost"].value_counts(dropna=False))房间类型统一小写并去除空格:
df["room_type"] = ( df["room_type"].astype(str).str.strip().str.lower() ) print(df["room_type"].value_counts())4.6 第五步:整形与特征塑造
当基础字段都干净之后,就可以进入“整形”阶段。常见的整形操作包括筛选、排序、分组、透视和新增特征。
先剔除明显异常的记录:
df = df[ df["price"].between(10, 2000) & df["accommodates"].between(1, 16) ].copy()copy()很重要,它避免后续修改触发SettingWithCopyWarning这类 pandas 警告。
新增“人均价格”特征:
df["price_per_person"] = ( df["price"] / df["accommodates"].replace(0, np.nan) )把accommodates为 0 的情况先替换成NaN,避免除零错误。
新建价格区间列:
bins = [0, 100, 200, 500, 10000] labels = ["经济型", "舒适型", "高端型", "豪华型"] df["price_level"] = pd.cut( df["price"], bins=bins, labels=labels, right=False, )分组统计不同房型的房源数量、均价和评分:
room_summary = ( df.groupby("room_type", as_index=False) .agg( 房源数=("id", "count"), 平均价格=("price", "mean"), 中位价格=("price", "median"), 平均评分=("review_scores_rating", "mean"), ) .sort_values("房源数", ascending=False) ) print(room_summary)如果数据里有neighbourhood_group字段,还可以做房型和区域的交叉透视:
pivot = pd.pivot_table( df, index="neighbourhood_group", columns="room_type", values="price", aggfunc="median", ) print(pivot.head())透视表很适合观察“哪个区域的哪种房型贵”这类问题。
4.7 第六步:导出清洗结果
清洗完成后的数据,导出为新的 CSV 文件:
output_path = OUTPUT_DIR / "airbnb_listings_clean.csv" df.to_csv(output_path, index=False, encoding="utf-8-sig") print("已保存:", output_path)encoding="utf-8-sig"是为了让 Excel 打开 CSV 时不会出现中文乱码,是中文项目里的实用习惯。
5. 常见问题与排查思路
在清洗 Airbnb 数据时,最容易遇到下面这些问题:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
启动脚本提示cannot mkdir | 父目录不存在、权限不足或目录已存在 | 使用Path.mkdir(parents=True, exist_ok=True),或先检查目录与权限 |
pd.read_csv报ParserError | 文件编码不对、CSV 引号不闭合 | 指定encoding="utf-8"或engine="python" |
价格列转数值后出现大量NaN | 清洗规则没覆盖特殊格式,例如$1,200.00的去逗号顺序不对 | 先用str.replace去掉$和,,再pd.to_numeric |
str.extract提取不到数字 | 源数据格式与正则不匹配 | 先value_counts()查看唯一值,再调整正则 |
SettingWithCopyWarning警告 | 对切片后的 DataFrame 直接赋值 | 在切片后调用.copy() |
日期解析后全是NaT | 日期字符串格式特殊或混入文本 | 用errors="coerce"保留可解析值,再单独检查无法解析的样本 |
| 删除缺失列后行列数对不上 | 列名不存在却被drop | 用列表推导式[c for c in cols if c in df.columns]过滤 |
排查思路建议按“数据体检 → 定位影响范围 → 先处理结构问题再处理内容问题 → 每步打印形状确认”的顺序进行。每次清洗操作后都打印一次df.shape,能很快定位是哪一步改变了行数或列数。
6. 最佳实践与工程建议
6.1 保留原始数据,清洗数据单独输出
永远不要把清洗结果覆盖到原始 CSV 上。原始文件是数据资产的基线,清洗脚本应该可以重复执行,输出到独立目录。这样即使清洗规则写错了,重新跑一遍脚本即可。
6.2 清洗脚本要可复现
把清洗过程写成函数,而不是在 Notebook 里一个 Cell 一个 Cell 地敲。推荐拆成:
def load_data(path): ... def clean_missing(df): ... def clean_types(df): ... def shape_features(df): ... def main(): df = load_data(...) df = clean_missing(df) df = clean_types(df) df = shape_features(df) ...每个函数只做一件事,方便单独测试和复用。
6.3 用日志记录每一步
项目规模变大后,清洗步骤可能非常多。建议在关键节点打印或记录日志:
import logging logging.basicConfig(level=logging.INFO) logging.info("after load: %s", df.shape) logging.info("after dedup: %s", df.shape)这样跑完整个脚本,你能清楚看到每一次操作的影响。
6.4 谨慎处理缺失值
填充缺失值不等于凭空造数据。填充前要问自己:这个缺失是“业务上不存在”还是“数据采集失败”?
- 业务上不存在:例如没有评论的房源,
reviews_per_month填 0 合理。 - 数据采集失败:不应该直接填均值,建议先分析缺失原因。
6.5 正则和类型转换要兜底
errors="coerce"不是偷懒,而是让程序在遇到脏数据时继续运行,并在后续检查中暴露问题。关键是转换后要抽样检查NaN,确认没有把有效数据误判成缺失。
6.6 大批量数据时注意内存
Airbnb 城市级别的数据通常只有几万行,内存压力不大。但如果你处理的是全国或全球级别的数据,建议在读取时只选择需要的列:
cols = ["id", "price", "room_type", "accommodates", "number_of_reviews", "review_scores_rating", "last_review"] df = pd.read_csv("data/listings.csv", usecols=cols)7. 总结与下一步
通过本文的流程,你应该已经掌握了 Airbnb Listings 数据清洗与整形的完整方法:先识别数据问题,再处理缺失值、重复值和类型错误,最后通过分组、透视、特征工程把数据变成适合分析和建模的形态。这套流程不仅适用于 Airbnb 数据,换到商品列表、用户画像、订单流水等场景,思路完全一致。
接下来你可以继续学习三块内容:
- 可视化分析:用 matplotlib 或 seaborn 查看不同房型、不同区域的价格分布。
- 异常值检测:用 IQR 或机器学习方法识别价格、评论数中的异常值。
- 价格预测建模:把清洗后的数据应用到线性回归或树模型,预测房源价格。
实际项目中,优先关注缺失值处理和类型一致性,这两步直接影响后续所有分析的准确性。建议你找一份真实的 Airbnb 数据,把本文每个步骤都手动跑一遍,遇到报错就对照常见问题表格排查,很快就能把清洗流程建立成自己的标准模板。
如果本文对你有帮助,可以收藏备用,也欢迎在评论区交流你在清洗 Airbnb 数据时遇到的其他坑。