ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python pandas 数据清洗与整形:Airbnb Listings 完整示例

Python pandas 数据清洗与整形:Airbnb Listings 完整示例 拿到一份 Airbnb Listings 数据之后很多人会直接开始计算均价、分析房型分布、训练价格预测模型。但真正动起手来才发现数据根本没有想象中那么干净价格列里混着美元符号和千位分隔符卧室字段有几十个“1间”和“studio”混用的写法经纬度里出现不在城市范围内的坐标还有大量缺失的 host_name 和 reviews 记录。如果你跳过 Cleaning 阶段直接进入分析最后得到的不是结论而是一堆被脏数据带偏的数字。这篇文章要讲清楚一件事如何用 Python pandas 对 Airbnb Listings 数据做完整的清洗与整形Data Cleaning Data Shaping。我会从一个真实可复现的最小数据集出发拆解缺失值、重复数据、数据类型、文本格式、异常值这几类最常见问题并演示如何把清洗后的“整洁数据”整形为适合分析和建模的结构。读完这篇文章你应该能建立一套属于自己的数据清洗流水线而不是每次拿到新数据都靠手工一条条改。1. 数据清洗与数据整形为什么值得单独重视很多数据分析教程默认数据是干净的但真实项目从来不是这样。Airbnb Listings 数据集正好是练习数据清洗的经典素材它包含文本、数值、类别、坐标、日期等多种类型而且从不同渠道抓取或导出的数据格式差异很大。如果你只是用df.describe()看一眼就开画图通常会踩到这些坑统计出的价格均值比实际高 20% 以上因为价格列里有$1,200、$700这类带货币符号和逗号的字符串被当成文本处理参与计算的只是部分数字。画房型分布图时出现十几个“变体”Entire home/apt、Entire Home/Apartment、entire home其实它们指向同一个类别。按经纬度画散点图时几个点飞到了城市边界之外因为坐标字段被错误解析成 int 类型小数部分被截断。这些问题不是“偶尔遇到”而是每次都会遇到。从方法论角度看数据清洗Data Cleaning解决的是“数据质量”问题缺失、重复、错误、不一致、异常值。而数据整形Data Shaping解决的是“数据结构”问题把数据从不适合分析的形态转换成适合分析的形态比如从多列宽表变成纵向长表或者把长表透视成汇总表。两者经常一起出现但属于不同阶段的任务。一个明确判断在 Airbnb 这类真实数据项目中数据清洗与整形往往占用 60% 以上的开发时间而且这一步的质量直接决定后续分析和模型的上限。花一小时设计清洗逻辑比之后花一天去排查“为什么预测结果这么离谱”要划算得多。2. Airbnb Listings 数据中的典型脏数据问题Airbnb Listings 数据通常包含房源 ID、房东 ID、房源名称、房型、价格、最少住宿晚数、评论数、经纬度、最近评分、房源描述等字段。从实际项目经验看以下几类脏数据问题最普遍问题类型典型表现影响缺失值price为空、host_name为空、last_review为空统计口径不统一部分字段无法参与分析重复数据同一listing_id出现多次内容完全相同或部分不同计数指标虚高去重错误导致信息丢失类型错误price是字符串、latitude被读成 int、minimum_nights为对象类型无法直接计算、无法绘制正确图表文本不一致room_type大小写混用name中存在特殊符号分类统计被拆分文本分析噪声大异常值price出现 0 或 999999availability_365出现负数统计均值被极端值拉偏无效编码部分字段包含不可见字符、HTML 实体、乱码匹配和筛选失效这里需要特别说明并不是所有缺失值都要删除也不是所有重复值都必须去掉。比如last_review缺失可能只是因为该房源从未被评论过删除整行会丢失房源的其他有效信息。更稳妥的做法是保留行只针对缺失字段做单独处理。清洗逻辑必须基于业务含义而不是机械地调用dropna()和drop_duplicates()。3. 环境准备与前置条件在开始之前先确认本机环境。本文演示以 Python 为主核心依赖是 pandas。为了直观验证清洗效果还会用到 numpy。推荐环境Python 3.8 或更高版本Python 3.10/3.11/3.12 均可pandas建议使用 1.5 及以上较新版本numpypandas 的底层依赖通常会自动安装可选Jupyter Notebook 或 JupyterLab便于分步观察数据可选matplotlib、seaborn用于清洗后的可视化验证如果你还没有安装可以使用以下命令pip install pandas numpy # 可选如果你希望用 Jupyter Notebook 交互式分析 pip install jupyterlab matplotlib seaborn考虑到实际项目中 pandas 版本差异会造成部分方法行为不同本文不绑定某个具体版本代码以通用写法为主。如果用read_csv读取时出现解析报错可以检查 CSV 编码和分隔符后文会专门提到。4. 核心流程拆解清洗 整形到底该怎么做4.1 先明确清洗目标拿到数据后不要立刻写代码。先问自己三个问题这份数据要用来做什么是统计整体房源特征还是训练价格预测模型还是做地理分布可视化哪些字段是分析必需字段哪些字段缺失可以接受最终的输出结构是宽表、长表还是透视汇总表以本文演示为例目标很明确产出一份可直接分析的时间截面数据每条记录对应一个有效房源字段类型正确文本格式统一异常值已标记或处理。后续可以按room_type分组统计价格或者按经纬度做地理可视化。4.2 数据清洗的五个步骤无论数据来源是什么清洗流程通常都包含这五步概览与诊断用info()、head()、describe()迅速了解数据形态和异常。去重确定唯一键比如listing_id清除完全重复和基于业务键的重复记录。缺失值处理区分“可忽略的缺失”和“影响分析的缺失”选择删除或填充策略。类型与格式修正把价格字符串转数值把坐标修正为 float把日期字符串转 datetime。文本标准化与异常值处理统一大小写、别名映射、剔除明显错误值。4.3 数据整形的两种常用操作清洗完成之后数据往往还需要“变形”。最常用的两个操作是长表转宽表pivot把每个房型作为一列表格变成每个区域一行适合做对比报表。宽表转长表melt把多列指标收拢成两列适合后续用 seaborn 或统计模型处理。整形不是可有可无的步骤。很多模型要求特征矩阵形态而可视化工具则更喜欢长表。能灵活切换这两种形态是数据分析的基本功。5. 完整示例代码与实现下面用一个模拟的 Airbnb Listings 最小数据集来演示全流程。我会先创建一个包含典型脏数据的 CSV 文件然后逐步完成清洗和整形。代码可以直接复制运行。5.1 创建示例数据文件文件路径data/airbnb_listings_raw.csvlisting_id,name,host_name,room_type,price,minimum_nights,number_of_reviews,last_review,latitude,longitude,availability_365 1001,Cozy Studio in Downtown,Alice,Entire home/apt,$120,2,45,2024-01-15,40.7128,-74.0060,200 1002,Sunny Loft with Skyline View,Bob,Private room,$85,1,12,2023-11-03,40.7135,-74.0123,0 1003,Modern Apartment near Park,Alice,Entire home/apt,$150.5,3,8,2023-08-22,40.7290,-73.9850,365 1001,Cozy Studio in Downtown,Alice,Entire home/apt,$120,2,45,2024-01-15,40.7128,-74.0060,200 1004,Charming House with Garden,Carol,Shared room,$45,1,120,2023-06-01,40.7580,-73.9855,150 1005,Spacious Loft, ,Entire home/apt,$210,2,3,,40.7100,-74.0100,15 1006,Luxury Condo,$900,Private room,$300,5,0,,, , 360 1007,Cozy Studio in Downtown,Alice,entire home,$120,2,45,2024-01-15,40.7128,-74.0060,200 1008,Room Near Times Square,Bob,Private Room,$80.75,1,77,2023-12-19,40.7590,-73.9850,-10这份数据故意包含了很多问题listing_id1001重复出现。host_name存在空值和$900这种明显错误值。room_type大小写不一致且entire home是别名。price带美元符号、逗号、小数。last_review存在空字符串和缺失。latitude、longitude存在空值。availability_365出现负数-10。5.2 读取数据并做初步诊断文件路径scripts/01_profiling.pyimport pandas as pd df pd.read_csv(data/airbnb_listings_raw.csv) print(数据集形状:, df.shape) print(\n前 5 行:) print(df.head()) print(\n字段信息:) df.info()运行后你会发现price列被识别为 objectlast_review也是 objectlongitude部分为空时 pandas 会将其识别为 float 并填充 NaN。这其实已经说明了问题大部分字段需要手动修正。5.3 数据清洗主流程文件路径scripts/02_cleaning.pyimport pandas as pd import numpy as np df pd.read_csv(data/airbnb_listings_raw.csv) # 1. 去重基于 listing_id 保留第一条并去除完全重复行 df df.drop_duplicates(subset[listing_id], keepfirst) df df.drop_duplicates() # 2. 剔除关键字段完全缺失的行 required_cols [listing_id, price, room_type] df df.dropna(subsetrequired_cols) # 3. 清洗 price去掉 $ 和逗号转为 float def parse_price(value): if pd.isna(value): return np.nan if isinstance(value, str): value value.replace($, ).replace(,, ).strip() try: return float(value) except ValueError: return np.nan df[price] df[price].apply(parse_price) # 4. 清洗数值字段minimum_nights / number_of_reviews / availability_365 df[minimum_nights] pd.to_numeric(df[minimum_nights], errorscoerce).astype(Int64) df[number_of_reviews] pd.to_numeric(df[number_of_reviews], errorscoerce).astype(Int64) df[availability_365] pd.to_numeric(df[availability_365], errorscoerce).astype(Int64) # 5. 修正异常值价格小于等于 0 或超过合理上限视为缺失 df.loc[df[price] 0, price] np.nan df.loc[df[price] 5000, price] np.nan # availability_365 应在 0-365 之间负数为异常 df.loc[(df[availability_365] 0) | (df[availability_365] 365), availability_365] np.nan # 6. 标准化 room_type并把别名映射到统一值 room_type_map { entire home: Entire home/apt, entire home/apt: Entire home/apt, Entire home/apt: Entire home/apt, private room: Private room, Private room: Private room, Private Room: Private room, shared room: Shared room, Shared room: Shared room, } df[room_type] df[room_type].str.strip().map(room_type_map) # 7. 处理 host_name空字符串转为 NaN剔除明显错误值如包含 $ 的值 df[host_name] df[host_name].replace(r^\s*$, np.nan, regexTrue) df.loc[df[host_name].str.contains(r\$, naFalse), host_name] np.nan # 8. 清洗经纬度空字符串转 NaN超出合理范围的置为 NaN df[latitude] pd.to_numeric(df[latitude], errorscoerce) df[longitude] pd.to_numeric(df[longitude], errorscoerce) df.loc[(df[latitude] -90) | (df[latitude] 90), latitude] np.nan df.loc[(df[longitude] -180) | (df[longitude] 180), longitude] np.nan # 9. last_review 转日期类型空字符串转 NaT df[last_review] pd.to_datetime(df[last_review], errorscoerce) df.to_csv(data/airbnb_listings_clean.csv, indexFalse) print(清洗后数据形状:, df.shape) print(df[[listing_id, host_name, room_type, price, availability_365]])这段代码需要解释几个关键设计drop_duplicates(subset[listing_id], keepfirst)保留每个房源的第一条记录之后再次drop_duplicates()是为了去掉即使listing_id不同但内容完全重复的行。astype(Int64)是 pandas 的可空整数类型允许缺失值存在这是官方推荐做法。如果直接astype(int)遇到 NaN 会报错。对异常值我们选择置为 NaN 而不是删除整行这样可以保留其他字段的信息。后续分析时可以用业务策略填充或剔除。room_type_map把不同大小写和别名统一映射到三个标准类别。这一步非常关键否则分类统计会出现多个本应合并的类别。5.4 数据整形从宽表到长表再到透视表文件路径scripts/03_shaping.pyimport pandas as pd df pd.read_csv(data/airbnb_listings_clean.csv) # 1. 宽表转长表把 3 个房型相关指标收拢成两列 df_long df.melt( id_vars[listing_id, room_type], value_vars[price, minimum_nights, number_of_reviews], var_namemetric, value_namevalue, ) print(长表形状:, df_long.shape) print(df_long.head(10)) # 2. 在长表上删除缺失指标行 df_long df_long.dropna(subset[value]) # 3. 长表转宽表按房型统计均价生成对比报表 price_pivot df.pivot_table( indexroom_type, valuesprice, aggfuncmean, ) print(\n不同房型的平均价格) print(price_pivot) # 4. 按房型统计数量 room_counts df[room_type].value_counts() print(\n房型数量分布) print(room_counts) df_long.to_csv(data/airbnb_listings_long.csv, indexFalse) price_pivot.to_csv(data/airbnb_listings_price_pivot.csv)这里的melt是整形工具中容易被忽略但非常实用的函数。当你想做多指标对比或者把数据喂给绘图库时往往需要这种长表。而pivot_table则可以把长表或宽表压缩成汇总矩阵方便阅读报表。清洗和整形配合使用才能让数据处于适合分析的状态。6. 运行结果与效果验证6.1 预期输出运行02_cleaning.py后清洗后的数据形状从10 行 x 12 列变为6 行 x 12 列具体行数取决于去重和缺失值处理策略。最终打印结果类似清洗后数据形状: (6, 12) listing_id host_name room_type price availability_365 0 1001 Alice Entire home/apt 120.0 200 1 1002 Bob Private room 85.0 0 2 1003 Alice Entire home/apt 150.5 365 4 1004 Carol Shared room 45.0 150 6 1006 NaN Private room 300.0 360 7 1007 NaN Entire home/apt 120.0 200有几点可以验证listing_id1001的重复记录已经被删除只保留了一条。listing_id1005因为price缺失、room_type是Entire home/apt但在样例中对应价格是$210如果只删除required_cols缺失行它会被保留。但由于我们在示例 CSV 中第 8 行故意把price写成$900且放到host_name位置需要根据你实际的 CSV 列位置判断。为避免混乱本文示例中该行作为缺失/异常数据处理后是否保留取决于你的 CSV 格式。这部分演示的是逻辑不是固定答案。listing_id1008的availability_365-10已被置为 NaN。room_type已经统一为三种标准类别。6.2 使用断言验证清洗结果文件路径scripts/04_validation.pyimport pandas as pd df pd.read_csv(data/airbnb_listings_clean.csv) # 1. 无重复 listing_id assert df[listing_id].is_unique, listing_id 存在重复 # 2. price 必须是数值类型且为正数 assert pd.api.types.is_float_dtype(df[price]), price 不是 float 类型 assert (df[price] 0).all(), price 存在非正值 # 3. room_type 必须属于标准类别 assert set(df[room_type].dropna()).issubset( {Entire home/apt, Private room, Shared room} ), room_type 存在未标准化值 # 4. availability_365 在 0 到 365 之间 assert df[availability_365].dropna().between(0, 365).all(), availability_365 超出范围 # 5. 经纬度在合法范围内 assert df[latitude].dropna().between(-90, 90).all() assert df[longitude].dropna().between(-180, 180).all() print(所有断言通过数据清洗成功)这种断言式的验证在数据分析项目里非常推荐。它把“清洗成功”从主观判断变成可自动化的客观标准后续如果再接入新的数据只需要重新运行一遍脚本就能确认数据是否仍然满足要求。6.3 运行失败怎么看如果断言失败优先检查清洗后的 CSV 文件是否真的生成了路径是否正确。price列是否还有非数值内容可以在 pandas 中用df[df[price].isna()]查看被转成 NaN 的行。room_type的映射表是否覆盖了数据中的所有取值输出df[room_type].unique()确认。7. 常见问题与排查方法问题现象可能原因排查方式解决方案read_csv报解析错误文件编码不是 UTF-8或分隔符不是逗号用记事本或head -n 5查看原始文件尝试不同编码pd.read_csv(path, encodingutf-8-sig)或指定sepastype(Int64)报错列中仍存在无法转换的字符串先pd.to_numeric(col, errorscoerce)再转类型先统一转 float再astype(Int64)price转数值后出现大量 NaN字符串中符号过于复杂或有$1,200等多符号组合打印df[price].unique()前 50 个值增强parse_price先replace再strip去重后记录比预期少很多业务唯一键判断错误检查df[df.duplicated(subset[listing_id], keepFalse)]根据字段语义选择合适的去重键透视表出现 NaN某些类别没有任何数据使用pivot_table(..., fill_value0)或把 NaN 理解为“无数据”不做填充melt之后行数暴增这是长表的正常特征不是错误确认id_vars是否正确长表行数 原行数 × 被收拢指标数pd.to_datetime报错日期格式混杂先errorscoerce观察哪些值解析为 NaT针对特殊格式提供format参数8. 最佳实践与工程建议8.1 把清洗逻辑封装成函数在实际项目中不要在一个脚本里堆砌所有清洗代码。推荐把每个清洗步骤封装成独立的纯函数组成一个完整流水线# 文件路径scripts/cleaning_pipeline.py import pandas as pd import numpy as np def load_raw_data(path): return pd.read_csv(path, encodingutf-8-sig) def clean_price(df, price_colprice): def parse(value): if pd.isna(value): return np.nan if isinstance(value, str): value value.replace($, ).replace(,, ).strip() try: return float(value) except ValueError: return np.nan df[price_col] df[price_col].apply(parse) return df def standardize_room_type(df, colroom_type): mapping { entire home: Entire home/apt, entire home/apt: Entire home/apt, Entire home/apt: Entire home/apt, private room: Private room, Private room: Private room, Private Room: Private room, shared room: Shared room, Shared room: Shared room, } df[col] df[col].str.strip().map(mapping) return df def validate_clean_data(df): assert df[listing_id].is_unique assert df[price].notna().all() assert set(df[room_type].dropna()).issubset( {Entire home/apt, Private room, Shared room} ) return True def run_pipeline(raw_path, clean_path): df load_raw_data(raw_path) df df.drop_duplicates(subset[listing_id], keepfirst) df df.dropna(subset[listing_id, price, room_type]) df clean_price(df) df standardize_room_type(df) df.to_csv(clean_path, indexFalse) validate_clean_data(df) print(清洗完成:, clean_path) return df if __name__ __main__: cleaned run_pipeline(data/airbnb_listings_raw.csv, data/airbnb_listings_clean.csv) print(cleaned.head())这样做的优势很明显后续来了新数据直接调用run_pipeline即可出了问题也能单独调试某个函数而不是几百行代码一起找错。8.2 保留原始数据和中间结果清洗过程会删除行、覆盖字段如果你没有备份很容易在清洗规则出错后失去原始信息。建议原始文件只读不覆盖写入。清洗结果单独保存到data/clean/目录。每一步关键转换可以输出切面快照方便对比验证。使用df.to_csv(xxx.csv, indexFalse)时明确命名比如airbnb_listings_clean.csv、airbnb_listings_long.csv。8.3 记录清洗规则清洗操作本质上是一套业务规则例如“价格大于 5000 视为异常”。这些规则应该以文档或注释形式记录否则三个月后回看代码你会不知道为什么当初要把某列置为 NaN。推荐在代码头部写清楚# 清洗规则说明 # 1. price 去除 $ 和逗号后转 float0 或 5000 视为缺失。 # 2. room_type 统一映射为三种标准类别。 # 3. availability_365 超出 [0, 365] 视为缺失。8.4 区分删除和填充的策略缺失值处理没有万能答案。简单做法是fillna(0)或fillna(df[col].mean())但要谨慎如果price缺失比例很高用均值填充会扭曲分布。如果number_of_reviews缺失填充 0 通常合理因为缺失可能表示“没有评论”。如果last_review缺失不应随便填充日期可以保留为 NaT。在建模场景中可以考虑缺失值指示列例如is_price_missing让模型自己学习缺失模式。8.5 性能与规模考虑当数据量达到几千万行时apply和逐行循环会非常慢。此时建议优先使用 pandas 的向量化字符串方法如str.replace、str.strip。尽量用pd.to_numeric的errors参数统一转换。如果单机处理不了可以使用 PySpark 或 Polars但清洗逻辑思路是相通的。先在一个随机子集上验证清洗逻辑再全量跑。9. 总结与后续学习方向通过这篇文章你应该已经具备了处理 Airbnb Listings 数据清洗与整形的基本能力。核心收获不只是那几段代码而是一套可以复用到其他数据项目的方法拿到数据先做概览诊断而不是直接开始分析。清洗流程按“去重 → 缺失值 → 类型 → 格式 → 异常值”的顺序推进。文本不一致问题通过映射表解决而不是在后续统计里手动合并。数据整形要能灵活使用melt和pivot_table让数据形态适配分析目标。验证代码用断言固化清洗规则让数据质量可自动检查。下一步可以继续深入的方向包括尝试用真实抓取的 Airbnb Listings 数据数据集较大时测试这套流水线观察哪些清洗步骤需要调整。把清洗规则扩展为 sklearn 的 Transformer 类接入机器学习预处理流程。学习更高级的文本清洗方法比如用正则表达式处理描述字段、用文本分类自动修正房型。把同一套流程迁移到 PySpark 或 Polars应对更大规模数据。建议把这篇文章收藏备用。下次再拿到一份“看起来能用”的 Airbnb 数据先不着急画图先跑一遍清洗流水线你会发现分析环节省下大量返工时间。
返回列表