ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Pandas数据清洗与整形实战:Airbnb房源数据完整处理指南

2026/9/1 20:39:41 拓冰建站 浏览量
Pandas数据清洗与整形实战:Airbnb房源数据完整处理指南 之前处理 Airbnb 房源数据时最耗时的往往不是建模而是怎么把一份真实且凌乱的 listing 表弄干净。价格字段带着$和千分位逗号、last_review大量缺失、room_type出现Room/room/Private room多种写法这些问题不提前处理后面任何统计、可视化和建模都会跑偏。网上关于 pandas 数据清洗的教程很多但多数只讲单个知识点很难直接套到一份完整数据集上。本文将围绕 Cleaning and Shaping the Airbnb Listings 这一主题整理一套从读取、探查、清洗、整形到导出的完整流程并补充几个真实项目中常见的报错排查思路。无论你刚入门数据分析还是准备用 Airbnb 公开数据做课程项目或 Kaggle 练习都可以直接按这篇文章的步骤走一遍。1. 背景与核心概念1.1 什么是 Airbnb Listings 数据Airbnb Listings 数据简单理解就是房源列表信息。一份常见的房源表会包含房源编号、房源标题、房东信息、所在区域、房型、每晚价格、最少入住晚数、评论数量、最近评论日期、可订天数、经纬度等字段。如果从 Inside Airbnb 这类第三方公开数据平台下载通常可以得到单个城市的listings.csv文件大小从几 MB 到几十 MB 不等行数从几千到几万都有可能。这类数据最大的特点是“看起来能直接用实际上一用就报错”。比如价格列是字符串$120.00评论数有时是数字有时是字符串日期列有的写成2023-06-15有的写成2022/11/03甚至还有空值。所以拿到数据后的第一步不是画图不是建模而是先做 data cleaning 和 data shaping。1.2 数据清洗和数据整形有什么区别在动手写代码之前先厘清两个概念。数据清洗Data Cleaning处理缺失值、重复值、异常值、错误格式目标是让数据“可用”。数据整形Data Shaping调整数据的列名、数据类型、结构、派生字段目标是让数据“好用”。清洗解决的是“脏不脏”的问题整形解决的是“顺不顺手”的问题。实际项目中两者经常连续出现清洗过程中会顺带做整形整形过程中也可能发现新的清洗需求。比如把price从字符串转成浮点数本质上既是清洗也是整形把last_review统一成datetime类型同样跨越了两个环节。1.3 为什么这两步如此重要真实数据分析项目中数据清洗和整形通常要占整个项目 50% 到 70% 的时间。如果这一步做得不扎实模型训练效果会非常差甚至出现“垃圾进、垃圾出”的情况。反过来说如果能把这一步标准化、函数化以后每次拿到新的城市数据只需要改路径和个别字段名就能快速产出一份干净的分析底表这也是本篇文章想帮你达到的目的。2. 环境准备与版本说明在开始代码实战前先确认运行环境。操作系统Windows / macOS / Linux 都可以以下命令与平台无关。Python建议 3.9 或更高版本3.7 以上大部分代码也能运行。pandas建议 1.5 以上或 2.x 版本。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路和清洗逻辑。IDE推荐 Jupyter Notebook 或 VS Code便于逐步观察中间结果。在终端中安装依赖pip install pandas numpy如果你使用的 Anaconda 环境pandas 通常已经内置可以直接跳过这一步。为了不污染全局环境也可以先创建虚拟环境python -m venv airbnb_env source airbnb_env/bin/activate # Windows 下使用 airbnb_env\Scripts\activate pip install pandas numpy建议的项目目录结构如下airbnb_analysis/ ├── data/ │ └── listings.csv ├── notebooks/ │ └── cleaning_shaping.ipynb ├── output/ └── scripts/ └── clean_listings.pydata目录放原始数据output目录放清洗后的文件脚本统一放在scripts下。养成数据目录和代码目录分离的习惯可以避免把原始数据改乱。3. 数据读取与初步探查3.1 读取 CSV 文件先用 pandas 把数据读进来。假设listings.csv位于data目录下import pandas as pd DATA_PATH data/listings.csv df pd.read_csv(DATA_PATH)这里有一个常用参数如果你需要预览前几行可以直接看head()如果文件很大可以指定nrows1000先加载前 1000 行做快速验证避免每次读取全量数据等待时间太长。df_sample pd.read_csv(DATA_PATH, nrows1000)3.2 查看数据概览读取之后先用几个基础命令建立对数据的整体印象。print(数据形状:, df.shape) print(列名:, df.columns.tolist()) df.head()一个典型的输出可能是数据形状: (28732, 16) 列名: [id, name, host_name, neighbourhood_group, room_type, price, minimum_nights, number_of_reviews, last_review, availability_365, latitude, longitude, ...]从shape可以知道有多少行、多少列。从列名能大概看出这份表包含哪些信息。此时建议继续用info()查看每一列的非空数量和数据类型df.info()info()输出里如果某列显示object通常意味着它被 pandas 当成了文本下一步要做类型转换。如果某列的非空数量明显小于总行数说明存在缺失值。3.3 缺失值与类型概览快速查看每列缺失值数量missing df.isna().sum() missing_percent (df.isna().sum() / len(df) * 100).round(2) missing_df pd.DataFrame({缺失数量: missing, 缺失占比: missing_percent}) missing_df输出样式如下字段缺失数量缺失占比host_name120.04%last_review362012.60%price00.00%通过这步可以快速圈出需要重点处理的列。一般来说缺失占比小于 1% 的列可以直接填充或删除少量行缺失占比超过 20% 的列需要认真思考业务含义如果某列缺失 90% 以上基本可以考虑整列丢弃。4. 数据清洗4.1 删除重复记录Airbnb 数据在收集或合并过程中可能出现重复行。先用duplicated()检查print(完全重复行数:, df.duplicated().sum())如果重复行数很多用drop_duplicates()删除df df.drop_duplicates().copy()但在真实数据里“完全重复”其实不算常见更常见的是部分字段重复例如同一个id出现了两行但其中一行的name或价格字段有出入。此时应该根据业务主键去重df df.drop_duplicates(subset[id], keepfirst).copy()keepfirst表示保留第一条keeplast表示保留最后一条。这里需要说明的是按id去重会丢失部分信息所以执行前一定要先确认id是应该唯一的字段备份原始数据并记录去重数量以便后续核对。4.2 处理缺失值处理缺失值没有“万能药”关键是理解业务含义。price缺失严重影响价格分析或建模建议直接删除对应行或者用同区域、同房型的中位数填充。host_name缺失对分析影响较小可以用Unknown填充。last_review缺失如果对应number_of_reviews为 0缺失是正常的表示该房源没有评论可以保留缺失或填充NaT如果评论数大于 0 但日期缺失则建议删除该行或结合其他字段补全。name缺失通常很少但缺失时会导致文本分析失败可以删除或填充No title。一个比较稳妥的清洗逻辑# 删除关键字段缺失的行 df df.dropna(subset[id, price, room_type]).copy() # 分类字段填充 df[host_name] df[host_name].fillna(Unknown) # 日期字段保持缺失后续用 pd.to_datetime 统一处理 df[last_review] pd.to_datetime(df[last_review], errorscoerce)以上代码中dropna(subset[...])是“有取有舍”只删除对业务影响最大的脏数据。4.3 类型转换与价格标准化Airbnb 数据里最常见的格式问题就是price。原始数据中它可能是$120.00、$95、$1,200.00甚至$0.00如果不处理就无法做数值统计。先看一下所有价格样本print(df[price].unique()[:20])如果确认是字符串清洗思路是去掉$和逗号再转成数值def parse_price(value): if pd.isna(value): return pd.NA s str(value).replace($, ).replace(,, ).strip() return pd.to_numeric(s, errorscoerce) df[price] df[price].apply(parse_price)执行后再次检查print(df[price].dtype) print(df[price].describe())如果price已经是float64说明转换成功。此时可以观察最大最小值判断是否存在明显异常值。4.4 异常值处理价格字段最常见的异常值是小于等于 0 的价格和极端高价格。小于等于 0 的价格通常是数据录入错误或测试房源可以直接删除。极端高价需要结合业务判断比如有些豪宅每晚几万美元也是合理数据。一个实用的做法是先看分位数再结合业务阈值处理。# 删除非正价格 df df[df[price] 0].copy() # 查看 99% 分位数 print(df[price].quantile(0.99))如果 99% 分位数是 1000 美元那超过 5000 美元的数据大概率是有问题的可以进一步排查。但我不建议直接粗暴剔除因为 Airbnb 里确实存在高端房源。更推荐的做法是把可疑记录单独保存到output/suspicious_prices.csv人工确认后再决定是否保留。异常值处理之后可以用describe()再次确认分布df[price].describe()5. 数据整形清洗完“脏”的部分接下来把数据结构整理成更便于分析的样子。如果说清洗是“去病”整形就是“塑形”。5.1 列名规范化Airbnb 原始 CSV 的列名有时包含空格或大小写混杂建议统一成小写蛇形命名便于后续写代码时不用频繁查列名。df.columns [col.strip().lower().replace( , _) for col in df.columns]执行后Number Of Reviews会被改成number_of_reviews。这里要注意原始列名可能包含其他特殊字符比如连字符或括号建议按实际输出再调整一次替换规则。5.2 文本字段清理room_type这类分类字段常见问题是大小写和前后空格不一致。例如Entire home/aptPrivate roomprivate roomHotel room如果不统一value_counts()会把同一个类别拆成多个。清理方法df[room_type] df[room_type].str.lower().str.strip()处理后再看类别分布print(df[room_type].value_counts())可以看到所有类别被压缩成少数几个标准值。同样的方法也可以用于name、neighbourhood_group等文本列。5.3 日期字段整形日期字段的目标是统一成datetime类型这样后续才能计算“距今天数”“按月份聚合”等特征。date_columns [host_since, last_review] for col in date_columns: if col in df.columns: df[col] pd.to_datetime(df[col], errorscoerce)errorscoerce的作用是无法解析的日期自动变成NaT不会让程序中断。不过也要注意pd.to_datetime在遇到不同日期格式混用时会弹出警告。如果 pandas 版本支持formatmixed可以显式指定如果版本较旧建议先查看不合规样本再单独处理。5.4 派生特征整形阶段很适合做特征工程。基于已有字段可以顺手生成几个对分析和建模都有用的新特征from datetime import datetime # 计算房源在平台上的天数 if host_since in df.columns: df[listing_age_days] (datetime.now() - df[host_since]).dt.days # 是否拥有评论 df[has_reviews] df[number_of_reviews].fillna(0).map(lambda x: 1 if x 0 else 0) # 价格区间分箱方便做分布分析 price_bins [0, 100, 200, 500, 10000] price_labels [0-100, 100-200, 200-500, 500] df[price_range] pd.cut(df[price], binsprice_bins, labelsprice_labels, rightFalse)需要提醒的是listing_age_days的计算结果会随运行时间变化。如果希望分析可复现更推荐把“数据截止日期”作为参数传进来比如固定使用asof_date datetime(2024, 1, 1)而不是用datetime.now()。这一点在做时间敏感性分析时尤其重要。5.5 区域坐标过滤与字段筛选Airbnb 数据里经纬度字段偶尔会有明显错误比如经度或纬度越界。可以用一个简单的布尔条件过滤# 示例仅保留特定城市的合理坐标范围具体阈值需根据城市调整 valid_coords ( df[latitude].between(40.5, 41.0) df[longitude].between(-74.3, -73.7) ) df df[valid_coords].copy()此外整形阶段也是决定“最终保留哪些列”的时机。如果目标是价格预测可能只需要room_type、price、availability_365、neighbourhood_group、派生特征等如果目标是文本分析那name就是核心列。用下面的方式保留指定字段keep_columns [ id, name, host_name, neighbourhood_group, room_type, price, price_range, minimum_nights, number_of_reviews, has_reviews, last_review, availability_365, listing_age_days ] df_clean df[keep_columns].copy()6. 完整实战案例前面几节把流程拆成了一个个步骤这一节用一个模拟数据集把整套流程串起来。示例数据是构造的字段结构尽量对齐真实 Airbnb Listings方便你替换成自己的 CSV。6.1 构造一份模拟数据import pandas as pd raw_rows [ {id: 1, name: Cozy Room in Brooklyn, host_name: Alice, neighbourhood_group: Brooklyn, room_type: Entire home/apt, price: $125.00, minimum_nights: 3, number_of_reviews: 45, last_review: 2023-06-15, availability_365: 280, latitude: 40.678, longitude: -73.944}, {id: 2, name: Manhattan Studio, host_name: Bob, neighbourhood_group: Manhattan, room_type: Private room, price: $95, minimum_nights: 1, number_of_reviews: 5, last_review: 2022/11/03, availability_365: 0, latitude: 40.753, longitude: -73.983}, {id: 3, name: Shared Room Near Park, host_name: None, neighbourhood_group: Queens, room_type: shared room , price: $1,200.00, minimum_nights: 7, number_of_reviews: 100, last_review: 2023-02-10, availability_365: 365, latitude: 40.714, longitude: -73.831}, {id: 4, name: Luxury Apartment, host_name: Cathy, neighbourhood_group: Manhattan, room_type: Entire home/apt, price: $0.00, minimum_nights: 2, number_of_reviews: 12, last_review: None, availability_365: 60, latitude: 40.760, longitude: -73.984}, {id: 5, name: Budget Room, host_name: David, neighbourhood_group: Bronx, room_type: Private Room, price: $99.00, minimum_nights: 1, number_of_reviews: 0, last_review: None, availability_365: 30, latitude: 40.845, longitude: -73.886}, {id: 1, name: Cozy Room in Brooklyn, host_name: Alice, neighbourhood_group: Brooklyn, room_type: Entire home/apt, price: $125.00, minimum_nights: 3, number_of_reviews: 45, last_review: 2023-06-15, availability_365: 280, latitude: 40.678, longitude: -73.944}, {id: 6, name: Invalid Coord House, host_name: Eve, neighbourhood_group: Manhattan, room_type: Hotel room, price: $300.00, minimum_nights: 2, number_of_reviews: 20, last_review: 2023-01-01, availability_365: 10, latitude: 100.00, longitude: -73.984}, ] df_raw pd.DataFrame(raw_rows) df_raw.to_csv(listings_demo.csv, indexFalse) print(df_raw)上面的模拟数据涵盖了重复行、缺失值、价格格式混乱、日期格式混杂、经纬度异常等常见脏数据问题。6.2 编写完整清洗整形函数下面把前面的步骤封装成一个函数后续处理任意 CSV 时只需把路径传入from datetime import datetime CITY_LAT_RANGE (40.5, 41.0) CITY_LON_RANGE (-74.3, -73.7) def parse_price(value): if pd.isna(value): return pd.NA s str(value).replace($, ).replace(,, ).strip() return pd.to_numeric(s, errorscoerce) def clean_and_shape_listings(df, asof_dateNone): # 避免修改原始数据 data df.copy() # 1. 列名规范化 data.columns [col.strip().lower().replace( , _) for col in data.columns] # 2. 按 id 去重 before len(data) data data.drop_duplicates(subset[id], keepfirst) print(f去重前行数: {before}, 去重后行数: {len(data)}) # 3. 英文列名中如果存在删除关键字段缺失的行 data data.dropna(subset[id, price, room_type]).copy() # 4. 价格解析 data[price] data[price].apply(parse_price) # 5. 删除非正价格 data data[data[price] 0].copy() # 6. 文本字段规范化 data[room_type] data[room_type].astype(str).str.lower().str.strip() data[name] data[name].fillna(No title).astype(str).str.strip() data[host_name] data[host_name].fillna(Unknown).astype(str).str.strip() # 7. 日期全量化 for col in [host_since, last_review]: if col in data.columns: data[col] pd.to_datetime(data[col], errorscoerce) # 8. 数值字段统一 for col in [minimum_nights, number_of_reviews, availability_365]: if col in data.columns: data[col] pd.to_numeric(data[col], errorscoerce).fillna(0) # 9. 删除明显越界的坐标 data data[data[latitude].between(*CITY_LAT_RANGE)] data data[data[longitude].between(*CITY_LON_RANGE)] # 10. 派生特征 if asof_date is None: asof_date datetime.now() data[listing_age_days] (pd.Timestamp(asof_date) - data[host_since]).dt.days data[has_reviews] (data[number_of_reviews] 0).astype(int) data[price_range] pd.cut( data[price], bins[0, 100, 200, 500, 100000], labels[0-100, 100-200, 200-500, 500], rightFalse ) # 11. 保留目标字段 keep_columns [ id, name, host_name, neighbourhood_group, room_type, price, price_range, minimum_nights, number_of_reviews, has_reviews, last_review, availability_365, listing_age_days ] keep_columns [col for col in keep_columns if col in data.columns] data data[keep_columns].copy() return data使用方式df_clean clean_and_shape_listings(df_raw, asof_datedatetime(2024, 1, 1)) print(df_clean)6.3 运行与验证运行代码后预期输出大致是去重前行数: 7, 去重后行数: 6 清洗后行数: 4最终清洗后的数据集应该只剩 4 行因为重复行、缺失价格的行、非正价格、错误坐标分别被删除。price变成float64last_review变成datetime64[ns]room_type变成统一的小写格式。6.4 导出结果清洗完成后导出到output目录import os os.makedirs(output, exist_okTrue) df_clean.to_csv(output/listings_clean.csv, indexFalse)也可以顺手保存一个带data_version的日期后缀文件方便区分版本df_clean.to_csv(output/listings_clean_20240101.csv, indexFalse)这里使用os.makedirs(output, exist_okTrue)能保证目录不存在时自动创建。后面常见问题里会提到不做这一步就可能碰到目录创建失败。7. 常见问题与排查思路处理 Airbnb Listings 数据时下面几个问题出现频率很高问题现象常见原因解决思路price无法转成数字字符串中存在$、逗号、空格或其他符号先str.replace()清理再用pd.to_numeric(errorscoerce)日期解析报错或出现大量NaT日期格式混杂如2023-06-15和2022/11/03共存用pd.to_datetime(..., errorscoerce)然后单独排查转为NaT的样本去重后数据量骤减误把不该当作主键的列放入subset先确认业务主键比如id是否确实唯一room_type类别非常多大小写、空格、拼写不统一统一转小写、去空格再用value_counts()检查导出时目录不存在使用了相对路径且目标目录未创建使用os.makedirs(output, exist_okTrue)或Path.mkdir(parentsTrue, exist_okTrue)下面单独展开两个比较典型的问题。7.1 价格字段解析失败如果parse_price之后出现大量NaN先检查原始字符串到底长什么样bad_price df[df[price].isna()] print(bad_price[price].head(20))常见情况是价格字段里混入了不规则字符例如价格面议、$1,200.00/月等。对于这类数据建议保留原始列并把解析结果单独存为新列避免把原始信息丢失。7.2 重复值删除过度drop_duplicates(subset[id])如果删掉的行数量异常可能是同一个id下实际有不同的房源信息例如共享房源中不同房型共用同一id。遇到这种情况可以改为按id room_type price去重或者在前一步先通过sort_values()把质量更高的行排到前面再去重。7.3 创建输出目录时报 cannot mkdir structure meed cleaning如果你在自动化处理脚本里遇到类似cannot mkdir structure meed cleaning的提示不要只盯着目录名本身。这个报错的意思是脚本尝试创建目录但目录结构没有准备好需要先清理或调整。常见的触发原因有三个。第一目标路径上已经存在一个同名普通文件。例如你有一个文件叫output脚本又想创建同名的output/目录系统就会拒绝。解决方法是先检查并重命名或删除冲突文件。import os from pathlib import Path output_dir Path(output) if output_dir.exists() and not output_dir.is_dir(): output_dir.rename(output_conflict_backup) output_dir.mkdir(parentsTrue, exist_okTrue)第二父目录不存在且脚本没有递归创建目录。在 Python 中只用os.mkdir(data/processed)时如果data或processed都不存在会直接报错。推荐使用from pathlib import Path Path(output/listings_clean).mkdir(parentsTrue, exist_okTrue)第三目录里残留旧的中间文件导致脚本状态混乱。这里的structure meed cleaning其实是在提醒你当前目录结构已经和脚本预期不一致建议把旧的临时目录清理掉重新初始化一个干净的输出目录。在数据工程脚本中建议在脚本开头固定一段“初始化目录”的逻辑把错误消灭在第一步import shutil from pathlib import Path output_dir Path(output) if output_dir.exists(): shutil.rmtree(output_dir) output_dir.mkdir(parentsTrue, exist_okTrue)这个方案适合完全由脚本生成的临时目录但如果目录里有重要手工文件删除前一定要先备份或加确认条件。8. 最佳实践与工程建议如果只是做一次性的探索性分析前面几节已经够用。但如果这个清洗流程要反复使用或者要交给团队其他成员使用下面这些工程习惯值得注意。8.1 保留原始数据副本清洗前先备份原始文件。最简单的方法是把原始 CSV 复制为带raw后缀的文件或者把原始DataFrame保留在内存中不覆盖。数据清洗是不可逆操作一旦在原文件上直接修改后面想恢复就非常麻烦。df_raw_backup df_raw.copy()8.2 把清洗流程封装成函数不要把所有步骤一股脑写在 Jupyter Notebook 的某个单元格里建议封装成clean_and_shape_listings()这样的函数。这样做的优势在于可以批量处理多个城市的 Airbnb 数据可以接入测试框架可以用相同的清洗规则处理训练集和预测集避免数据泄漏或规则不一致。8.3 记录清洗日志在函数里用print()或日志库记录每一步删除的行数和关键统计量。比如print(f删除缺失价格行: {len(df) - len(df.dropna(subset[price]))})清洗结束后可以自动生成一份包含原始数据行数、清洗后行数、删除原因统计的日志文件便于复盘。8.4 控制输出字段范围如果清洗后的数据要共享给同事或用于外部展示建议移除包含个人隐私的字段。Airbnb Listings 原始数据中通常包含房东姓名、精确经纬度等敏感信息不必要的字段不要导出分析时只保留分析需要的列即可这也是数据处理中的最小权限原则。8.5 使用配置文件管理路径如果脚本要在不同机器上运行不要把文件路径硬编码在每个脚本里。可以写一个简单的配置文件config.yaml或config.json统一管理数据路径、城市经纬度范围、价格阈值等参数。这样换城市或换服务器时只需要修改配置不用动代码。{ raw_data_path: data/listings.csv, output_dir: output, city_lat_range: [40.5, 41.0], city_lon_range: [-74.3, -73.7] }8.6 大数据量下的读取策略如果listings.csv有几个 GB一次读入内存会比较吃力。此时可以用chunksize分块读取先清洗每一块再把清洗后的结果累计到最终表中chunks pd.read_csv(data/listings.csv, chunksize100000) df_clean_list [] for chunk in chunks: df_clean_list.append(clean_and_shape_listings(chunk)) df_all pd.concat(df_clean_list, ignore_indexTrue)不过分块处理时要注意重复值去重和全局统计量比如分位数、缺失率不能简单地按块计算需要先在全局层面确认规则然后再分块清洗。8.7 用 pandas 内置方法减少代码量很多清洗操作其实有更简洁的写法。例如判空可以用df[price].str.contains(\\$)数值规范化可以用df[price].str.replace([$,], , regexTrue)。但代码简洁的前提是可读性建议在团队协作时优先选择注释完整、逻辑清晰的写法。结语Airbnb Listings 数据的清洗和整形并不难难的是把每一步想清楚哪个字段是业务主键缺失值应该删除还是填充异常价格是该剔除还是单独保留。建议你拿到一份新数据后不要急着写模型先花一小时把本文的流程完整跑一遍做出第一版干净数据表。后续做可视化、特征工程和模型训练时你会明显感觉到数据顺手很多。最后说一个很实用的习惯每次跑清洗脚本前先复制一份原始 CSV命名为listings_raw_backup.csv。数据清洗是不可逆操作很多翻车案例都是因为在原文件上直接修改后再也恢复不了。把这条记在项目的 README 里能省掉不少麻烦。