ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Airbnb数据清洗与特征工程实战:构建可复用数据管道

Airbnb数据清洗与特征工程实战:构建可复用数据管道 这次我们来看一个数据项目Cleaning and Shaping the Airbnb Listings。它要处理的事情很具体——把原始、零散、来自不同渠道的 Airbnb 房源列表数据整理成一份结构清晰、特征完整、能直接用于分析或建模的干净数据集。很多刚接触数据科学的人第一次上手真实项目时往往不是倒在模型上而是倒在数据上价格变成了字符串、缺失值遍布全表、同一座城市的房源计量单位不统一、多批次数据合并后索引混乱。这个项目的核心不在算法而在清洗和整形。这 5 个点是它最值得关注的地方结构化的字段清洗流程价格、日期、评分、经纬度、设施、描述文本都能规范化。可复用的数据管道设计同一套代码可以批量处理多个城市、多个时间段的 Airbnb 房源数据。建模前的特征工程把原始字段变成可训练的特征为后续回归、分类或聚类做准备。低硬件门槛这个项目主要在 CPU 环境下运行不需要昂贵显卡普通笔记本即可完成。可扩展性好清理完的数据可以输出成 CSV、Parquet也可以进一步包装成 API 服务。这篇文章会带你把整个流程跑通先准备环境和数据目录再把原始 CSV 加载进 Pandas接着依次完成字段清理、价格规范化、时间解析、文本特征构造最后输出训练集和验证集并做一次完整的数据质量检查。如果你正在准备数据分析面试或者想第一次体验“真正处理一份脏数据”这篇文章可以直接收藏。下面开始实操。1. 核心能力速览能力项说明项目类型数据清洗 / 特征工程 / ETL 流程数据对象Airbnb 房源列表数据可按城市、年份、月份批量处理主要功能缺失值处理、重复值去重、价格规范化、日期解析、文本清洗、特征构造、数据集划分输入格式CSV、Excel、JSON 均可适配本文以 CSV 为例输出格式CSV、Parquet可按 train/val/test 拆分压缩方式支持 gzip 压缩适合存储大规模房源数据推荐硬件普通 CPU 即可处理千万级样本建议内存 16G 以上显存需求无纯结构化数据处理不需要 GPU是否需要深度学习框架不需要本文只用 Pandas / NumPy / Scikit-learn是否支持批量任务支持可遍历多个城市目录批量清洗是否提供 API默认无但清洗模块可以封装成 FastAPI 服务供外部调用适合场景数据科学入门练习、Airbnb 商业分析、房源价格预测前期准备2. 项目目标与适用场景这个项目的主要目标是把“能看的表格”变成“能建模的表格”。原始 Airbnb 列表数据通常会有很多问题字段类型错乱、价格带货币符号、日期格式不统一、描述文本里夹杂 HTML 标签、同一房源在多个批次中重复出现。如果没有清洗和整形后续任何分析都会产生偏差。从使用场景看它适合四类读者数据分析初学者想学习真实数据清洗过程而不是一直看着干净的 toy dataset。机器学习入门者需要为价格预测、房源推荐模型准备训练集避免出现数据泄露和格式错误。业务分析人员希望快速统计不同城市、不同房型的均价、评分、入住率趋势。数据工程师想在正式建模前搭建一套可重复运行的批量数据处理管道。不适合的场景也要说清楚。如果你需要的是实时抓取 Airbnb 数据那这不是清洗项目而是爬虫和接口对接项目。如果需要处理图片、视频等多媒体内容那又会涉及 GPU 和深度学习模型和本项目的侧重点不同。这里只解决“结构化表格数据”的清洗和整形问题。使用边界方面一定要注意数据来源的合法性和隐私问题。Airbnb 或类似平台的数据如果涉及房东个人信息、非公开评价、私人联系方式就不得在未授权的情况下收集和使用。公开的统计类数据也要先确认许可协议。本文只演示清洗方法不鼓励任何绕过平台条款抓取数据的行为。3. 环境准备与数据前置条件3.1 运行环境这个项目不需要复杂环境Python 3.8 以上版本即可。推荐使用虚拟环境隔离依赖避免和系统其他项目冲突。python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate安装依赖时建议把核心库统一写入requirements.txtpandas1.5 numpy1.23 scikit-learn1.2 matplotlib3.6 seaborn0.12执行安装pip install -r requirements.txt如果你后续要把清洗结果接入 API 服务可以再安装 FastAPI 和 uvicorn但这不是本文必需项。3.2 数据准备建议准备一份 Airbnb listings 标准 CSV。常用字段包括id房源唯一编号name房源名称host_id房东编号neighbourhood街区latitude / longitude经纬度room_type房源类型price价格minimum_nights最低住宿晚数number_of_reviews评价数量last_review最近评价时间reviews_per_month每月评价数calculated_host_listings_count房东拥有房源数availability_365一年内可预订天数如果没有现成数据也可以在公开数据集中寻找。需要注意公开数据集的许可证和使用限制必须提前确认。3.3 检查磁盘空间如果你要处理多个城市的房源数据原始 CSV 可能达到几百 MB 到几个 GB。建议预留至少 5-10 倍于原始数据大小的磁盘空间用于中间结果和最终输出。4. 数据目录设计与启动加载实际工程中很多处理脚本跑不起来问题不在数据处理逻辑而是目录结构缺失。相信不少人见过类似报错FileNotFoundError: [Errno 2] No such file or directory: ./data/processed/2023/new_york/listings_clean.csv在 Linux 或容器环境里命令行可能直接提示mkdir: cannot create directory structure: No such file or directory这就是典型的目录结构设计问题。数据项目第一步不是写清洗函数而是把原始数据、中间数据、输出数据分目录管理。建议统一采用下面的目录结构airbnb_cleaning/ ├── data/ │ ├── raw/ │ │ └── new_york_2023_03.csv │ └── processed/ │ ├── train/ │ ├── val/ │ └── test/ ├── src/ │ ├── clean.py │ ├── features.py │ └── utils.py ├── output/ │ └── quality_report/ └── requirements.txt在代码中可以在启动阶段统一创建所有目标目录import os from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent RAW_DIR BASE_DIR / data / raw PROCESSED_DIR BASE_DIR / data / processed TRAIN_DIR PROCESSED_DIR / train VAL_DIR PROCESSED_DIR / val TEST_DIR PROCESSED_DIR / test QUALITY_DIR BASE_DIR / output / quality_report for folder in [RAW_DIR, PROCESSED_DIR, TRAIN_DIR, VAL_DIR, TEST_DIR, QUALITY_DIR]: folder.mkdir(parentsTrue, exist_okTrue) print(fCreated: {folder})mkdir(parentsTrue, exist_okTrue)是所有路径问题的标准解决方案。它会在创建目标目录时自动把父目录一起创建同时避免重复创建报错。如果你还是看到cannot mkdir优先检查权限和路径中是否包含不存在的上级目录。4.1 加载原始数据数据目录准备好之后就可以读取原始 CSVimport pandas as pd df pd.read_csv(data/raw/new_york_2023_03.csv, low_memoryFalse) print(f数据维度: {df.shape}) print(df.head())如果文件比较大可以指定部分列读取减少内存占用usecols [ id, name, host_id, neighbourhood, latitude, longitude, room_type, price, minimum_nights, number_of_reviews, last_review, reviews_per_month, calculated_host_listings_count, availability_365 ] df pd.read_csv(data/raw/new_york_2023_03.csv, usecolsusecols, low_memoryFalse)加载后首先观察字段类型和缺失情况print(df.dtypes) print(df.isna().sum())这一步能看到大多数问题哪些字段被误读成了字符串哪些字段缺失严重哪些字段类型需要强制转换。5. 数据清洗字段级处理5.1 去除重复值Airbnb 列表数据经常因为多次数据采集而出现重复。重复判断不能只看 id因为 id 可能在不同批次中缺失或改动所以需要结合业务字段联合去重。duplicate_mask df.duplicated(subset[id, name, host_id, latitude, longitude], keepFalse) print(f疑似重复记录数: {duplicate_mask.sum()}) df_clean df.drop_duplicates(subset[id, name, host_id, latitude, longitude], keepfirst)如果数据量很大也可以直接用主键 id 去重df_clean df.sort_values(last_review, na_positionlast).drop_duplicates(subset[id], keeplast)这里使用last_review排序的目的是保留信息最完整的记录。5.2 缺失值处理Airbnb 数据里最常见的缺失字段是last_review和reviews_per_month。这两个字段高度相关如果一个房源从未有过评价那么last_review为空reviews_per_month也为空。处理思路有两种业务规则填充reviews_per_month缺失且number_of_reviews为 0则填充为 0。交互式填充last_review缺失时可以保留为 NaT避免伪造时间。示例代码df_clean[reviews_per_month] df_clean[reviews_per_month].fillna(0) df_clean[last_review] pd.to_datetime(df_clean[last_review], errorscoerce)如果缺失字段比例过高比如超过 50%建议直接删除该列否则对下游特征构造影响较大。5.3 异常值过滤房价数据中最容易出现的异常值是“极端高价”和“0 元”。实际业务中0 元可能是数据错误也可能是平台测试房需要按业务规则过滤。一个通用做法是 filter 掉价格小于等于 0 或大于某一百分位数的记录low_price df_clean[price].quantile(0.01) high_price df_clean[price].quantile(0.99) df_clean df_clean[(df_clean[price] low_price) (df_clean[price] high_price)]注意过滤标准要结合具体城市。不同城市、不同房型的合理价格区间差异很大直接用全局阈值可能误删正常数据。更稳妥的办法是按room_type和neighbourhood分组后再计算分位数。6. 价格数据规范化Airbnb 原始数据中的price字段常常带着$符号和逗号分隔符比如$150.00、1,299.00。如果不处理Pandas 会把这一列识别成字符串无法直接做数值运算。清理步骤df_clean[price] ( df_clean[price] .astype(str) .str.replace(r[$,\s], , regexTrue) .astype(float) )处理完以后可以生成一个新的特征price_per_night含义就是单晚价格很多情况下可以直接用于回归模型。df_clean[price_per_night] df_clean[price] / df_clean[minimum_nights].clip(lower1)如果数据中存在清洗相关的服务费、清洁费那要单独建字段不要把清洁费直接合并到房价里否则会破坏业务含义。同时建议比较清洗前后的分布差异import seaborn as sns import matplotlib.pyplot as plt sns.histplot(df_clean[price], bins50) plt.title(Price Distribution After Cleaning) plt.show()这张图能直观告诉你价格字段是否已经变成数值型、是否仍然存在极端长尾。7. 文本清洗与特征构造7.1 清洗房源名称和描述name和description是最典型的文本字段它们会有大量噪音HTML 标签、特殊符号、多余空格、全角半角混用、URL 链接等。清洗这些文本需要写一个通用函数。import re def clean_text(text): if isinstance(text, float): text text str(text) text re.sub(r[^], , text) # 去除HTML标签 text re.sub(rhttp\S|www\.\S, , text) # 去除URL text re.sub(r[^\w\s\u4e00-\u9fff], , text) # 保留中文和英文单词 text re.sub(r\s, , text) return text.strip().lower() df_clean[name_clean] df_clean[name].apply(clean_text)这里要注意清洗规则必须结合实际数据调整。比如字段中可能出现价格、米制单位、数字这些是否保留要看后续分析目标。7.2 设施属性处理amenities字段通常是一个 JSON 风格的字符串比如[Wifi, Kitchen, Free street parking, Hot water, Essentials]我们可以把它解析成列表并获取数量import json def parse_amenities(x): try: items json.loads(x) if isinstance(x, str) else [] return items except Exception: return [] df_clean[amenities_list] df_clean[amenities].apply(parse_amenities) df_clean[amenities_count] df_clean[amenities_list].apply(len)如果需要更丰富的特征可以单独做多类别二值化。比如“Wifi”、“Kitchen”、“Pool”、“Air conditioning”这几个核心设施可以用MultiLabelBinarizerfrom sklearn.preprocessing import MultiLabelBinarizer mlb MultiLabelBinarizer() amenity_features mlb.fit_transform(df_clean[amenities_list]) amenity_df pd.DataFrame(amenity_features, columnsmlb.classes_[:20])只保留出现频率最高的前 20 个设施避免特征矩阵太稀疏。7.3 时间特征时间字段last_review是建模时非常有价值的特征。把字符串变成 datetime 类型后可以拆出年、月、季度、距离当前时间的天数等特征。df_clean[last_review] pd.to_datetime(df_clean[last_review], errorscoerce) df_clean[review_year] df_clean[last_review].dt.year df_clean[review_month] df_clean[last_review].dt.month df_clean[days_since_last_review] ( pd.Timestamp.today() - df_clean[last_review] ).dt.days如果数据集的截止日期是离今天很远的旧数据days_since_last_review会有时间偏移问题。更合理的做法是用数据集快照日期减去last_review而不是用当前系统时间。8. 数据整形与特征工程8.1 分类变量编码room_type、neighbourhood、host_id、city都是分类变量。其中room_type类别少可以直接独热编码neighbourhood类别多可以用类别频次或目标编码。独热编码room_type_dummies pd.get_dummies(df_clean[room_type], prefixroom) df_clean pd.concat([df_clean, room_type_dummies], axis1)高频类别编码对于 neighbourhood直接用get_dummies会产生大量稀疏列占用大量内存。可以先按频次过滤只保留出现超过 20 次的街区。top_neighbourhood df_clean[neighbourhood].value_counts() top_neighbourhood top_neighbourhood[top_neighbourhood 20].index df_clean[neighbourhood_top] df_clean[neighbourhood].where(df_clean[neighbourhood].isin(top_neighbourhood), Other)8.2 经纬度特征经纬度不能直接作为数值喂给普通线性模型因为距离和位置在地理空间上不是线性关系。常见做法是保留原始经纬度特征。对地理位置做聚类生成“区域聚类”特征。计算到某个中心点的距离。from sklearn.cluster import KMeans coords df_clean[[latitude, longitude]].dropna() kmeans KMeans(n_clusters10, random_state42, n_initauto) coords[loc_cluster] kmeans.fit_predict(coords) df_clean df_clean.merge(coords[[loc_cluster]], left_indexTrue, right_indexTrue, howleft)聚类数 10 只是一个起点实际需要根据业务场景和数据范围调整。如果你是分析单一城市聚类数可以小一点如果是多城市数据聚类数要更大。8.3 派生特征从已有字段中构造一些有业务含义的特征可以明显提升模型效果df_clean[reviews_per_night] df_clean[number_of_reviews] / df_clean[availability_365].clip(lower1) df_clean[host_listing_ratio] df_clean[calculated_host_listings_count] / df_clean[number_of_reviews].clip(lower1) df_clean[minimum_nights_log] np.log1p(df_clean[minimum_nights])构造派生特征时要注意避免数据泄露。比如如果模型要预测价格那么不能把price自身参与构造的特征作为输入除非是在做异常检测或其他非预测任务。9. 数据集划分与批量输出9.1 数据划分清洗和特征构造完成之后需要把数据集切分为训练集、验证集和测试集。切分时要保证不随机打乱时间序列结构如果数据带有日期最好按时间顺序切分先用早期数据训练再用后期数据验证。from sklearn.model_selection import train_test_split X df_clean.drop(columns[price, price_per_night]) y df_clean[price_per_night] X_train, X_temp, y_train, y_temp train_test_split( X, y, test_size0.3, random_state42 ) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, random_state42 )分割完以后要检查特征中是否包含未来信息。比如last_review、days_since_last_review可能会引入时间泄露需要结合业务判断是否保留。9.2 批量处理多个城市如果你的数据目录里有多个城市的文件像下面这样data/raw/ ├── new_york_2023_03.csv ├── san_francisco_2023_03.csv ├── los_angeles_2023_03.csv └── chicago_2023_03.csv可以写一个批处理函数按城市循环清洗并输出from pathlib import Path RAW_DIR Path(data/raw) PROCESSED_DIR Path(data/processed) for filepath in RAW_DIR.glob(*.csv): city filepath.stem.split(_)[0] print(fProcessing: {city}) city_df pd.read_csv(filepath, low_memoryFalse) city_df clean_data(city_df) city_df create_features(city_df) output_dir PROCESSED_DIR / city output_dir.mkdir(parentsTrue, exist_okTrue) city_df.to_parquet(output_dir / listings_clean.parquet, indexFalse)批量处理时建议每个文件单独生成一份质量报告避免一个文件出错导致整个流程中断。可以写try...except捕获异常并记录日志。import logging logging.basicConfig(levellogging.INFO) for filepath in RAW_DIR.glob(*.csv): try: process_city(filepath) except Exception as e: logging.error(f处理 {filepath} 失败: {e}) continue9.3 输出 Parquet 文件CSV 文件通用性强但占用空间大、读取速度慢。如果后续要多次加载建议输出为 Parquet 格式并启用压缩df_clean.to_parquet(data/processed/listings_clean.parquet, indexFalse, compressiongzip)如果仍需要 CSV 给非技术同事查看可以分表输出df_clean.to_csv(data/processed/listings_clean.csv, indexFalse, encodingutf-8-sig)utf-8-sig编码能避免 Excel 打开 CSV 文件时出现中文乱码。10. 数据验证与质量检查清洗完并不代表工作完成还需要验证数据质量是否达标。可以从 4 个角度检查。10.1 字段类型检查expected_types { price: float64, number_of_reviews: int64, latitude: float64, last_review: datetime64[ns] } for column, expected in expected_types.items(): actual str(df_clean[column].dtype) assert actual expected, f{column} 类型为 {actual}, 预期 {expected}10.2 缺失值比例检查missing_rate df_clean.isna().mean().sort_values(ascendingFalse) print(missing_rate[missing_rate 0.05])保留缺失率超过 5% 的字段清单然后逐项确认处理策略。如果缺失率高且无法填充考虑删除。10.3 唯一性和重复性检查duplicate_id df_clean[id].is_unique print(fid 唯一性: {duplicate_id})10.4 分布稳定性检查对数值型特征画出清洗前后的分位数对比print(df_clean[[price, minimum_nights, availability_365]].describe())如果发现某个字段的最小值、最大值存在严重离群需要回到清洗阶段重新处理。11. 常见问题与排查方法问题现象可能原因排查方式解决方案启动时提示cannot mkdir或FileNotFoundError输出目录不存在或路径中包含不存在的上级目录检查Path路径打印结果查看当前工作目录使用parentsTrue, exist_okTrue或统一从项目根目录创建目录加载 CSV 后价格字段无法做数值运算价格列包含$、逗号、空格查看dtype打印前 20 行使用正则替换非数值字符后astype(float)日期字段解析失败日期格式不统一如2023-03-15和03/15/2023混用打印唯一值使用pd.to_datetime(..., errorscoerce)后再统一格式内存占用过高读取了不需要的列或 CSV 文件过大查看df.info()和任务管理器指定usecols分块读取或转换为 Parquet中文文本乱码编码不一致检查文件编码声明使用encodingutf-8-sig读取或输出清洗后数据行数急剧减少去重或异常值过滤阈值过于激进查看shape前后变化打印被过滤样本调整分位数阈值或按城市分组过滤批量任务中一个城市失败导致全部停止没有做异常隔离查看日志失败堆栈使用try...except记录失败并继续处理特征中出现大量 NaN派生特征除数为 0查看新的isna().sum()使用.clip(lower1)或填充默认值训练集和验证集分布差异大随机切分导致时间信息泄露或不平衡对比各特征均值改用时间切分或分层采样12. 最佳实践与使用建议12.1 先小参数验证再全量运行无论是清洗还是特征工程第一次运行都不要直接处理全量数据。选取一个小文件或sample(1000)先跑通流程确认输出结果符合预期再扩大数据范围。df_sample df.sample(1000, random_state42) # 先在小样本上调试12.2 保留最小可运行配置把清洗流程封装成函数并提供一份simple_config.py或config.yaml便于快速修改字段名、过滤阈值、输出目录。这样项目可以反复复用。# config.yaml 示例 input_dir: data/raw output_dir: data/processed drop_duplicate_cols: [id, name, host_id] price_filter: true room_type: all feature_engineer: true在代码中读取配置时可以使用yaml.safe_load。未来换城市、换时间段数据集时只需要修改配置不用动核心逻辑。12.3 目录规范要强制建议所有路径都从项目根目录计算不要依赖当前工作目录。否则你在 Jupyter 里运行正常换成命令行执行时很容易出现cannot mkdir这类路径错误。统一的路径管理代码应该放在项目入口的最前面。12.4 日志和版本管理数据项目要不要做版本管理我的建议是必须做。至少记录原始数据文件名、清洗时间、运行脚本版本、输出文件 md5 值。这样后续别人问起这份数据是怎么产生的你能快速回答。import hashlib def file_md5(path): with open(path, rb) as f: return hashlib.md5(f.read()).hexdigest()如果数据量很大md5 会很慢也可以只记录文件大小和行数。12.5 隐私与合规提醒这是最容易忽略的部分。Airbnb 房源列表包含房东 ID、位置坐标、评论时间等信息。处理这些数据时必须遵守以下原则只使用公开、合法、明确许可的数据。不采集或处理个人信息如房东手机号、邮箱、真实姓名。不公开未经脱敏的坐标数据防止精确到门牌号。涉及商业用途时必须先确认平台条款和数据许可协议。使用公开数据集做教学演示时也要保留数据来源说明。12.6 接口 API 扩展清洗模块本身不依赖 Web 服务但如果你希望别人也能通过接口调用可以将清洗逻辑封装成一个函数然后用 FastAPI 暴露为 HTTP 服务。# app.py 示例 from fastapi import FastAPI, UploadFile import pandas as pd import io app FastAPI() app.post(/clean) async def clean_data(file: UploadFile): content await file.read() df pd.read_csv(io.BytesIO(content)) df_clean run_cleaning_pipeline(df) return df_clean.to_dict(orientrecords)启动服务uvicorn app:app --host 127.0.0.1 --port 8000调接口时上一个文件会返回清洗后的 JSON 数组。这种方式适合快速原型验证但并不适合超大文件上传生产环境应该改为任务异步处理和文件队列。12.7 批量任务设计建议批量清洗多个城市数据时建议做到两点每一步输出都有独立目录中间结果不互相覆盖。每处理一个文件就写一条日志记录成功或失败状态。批次失败后不要立刻重跑全量先根据日志定位失败文件。如果是因为源数据文件字段缺失就补充字段映射如果是因为权限不足就检查目录路径。13. 总结与下一步Cleaning and Shaping the Airbnb Listings 是个非常适合练手的数据项目。它不涉及高深的模型却覆盖了真实数据工作中最常见的问题字段类型混乱、缺失值、异常值、文本噪音、日期解析、特征构造、批量处理和目录管理。把这一套流程走完你就具备独立处理结构化数据集的基础能力。如果第一次尝试建议先去完成三件事一是把原始 CSV 加载进来并打印dtypes和isna().sum()用几分钟观察数据到底有多脏二是用正则表达式把价格字段清洗成数值型并画出分布图三是把清洗函数封装成可复用的模块尝试一次按城市批量处理。最容易踩的坑就是路径和目录结构。记住看到cannot mkdir structure这类错误时先检查父目录是否存在再用parentsTrue, exist_okTrue。环境、路径、日志这些问题比清洗逻辑本身更容易卡住你。下一步可以继续扩展的方向包括把清洗结果接入机器学习模型做房价预测增加更多特征比如评论情感分析、房源描述关键词、地理聚类或者把整个流程改造成定时任务每天自动处理增量数据。数据更新后重新运行管道几分钟内就能得到最新版训练集。如果后面还想继续深入可以把清洗逻辑封装成更完整的 ETL 工具配合 Airflow、DVC 或 Prefect 做任务调度。这个方向足够你继续折腾很久。
返回列表