2026/8/26 2:13:55

Airbnb房源数据清洗与整形实战:用pandas从脏数据到可用视图

Airbnb房源数据清洗与整形实战:用pandas从脏数据到可用视图 拿到 AirBnB 房源数据的那一刻大多数人会先做两件事用head()看前五行然后直接画价格分布图。结果往往是一张没法看的图——价格列里混着$符号和千分位逗号被 Python 当成了字符串price里有 0 元、有 9999 元还有几个明显是手误的超大数last_review一大半是空值reviews_per_month也跟着缺。这个时候才意识到真正拦住你的不是建模不是可视化而是 Data Cleaning 和 Data Shaping 这件听起来不性感、做起来却极其磨人的事。Airbnb 列表数据是一个特别适合用来练习这两项能力的数据集。它不像经典的 Iris 数据那样干净整齐也没有金融风控数据那么高的处理门槛而是恰到好处地混合了数值、分类、文本、日期和地理位置信息。这些字段天然带有真实数据常见的毛病缺失、重复、类型混乱、格式不统一、异常值扎堆。把这份数据从原始状态整理成一套能用于分析的结构化视图等于走完了一遍真实项目中 60% 以上的数据准备工作。这篇文章会以 Airbnb listings 数据为例完整梳理 Data Cleaning 和 Data Shaping 的核心方法先清楚脏数据藏在哪里再讲每个清洗规则背后的业务判断然后用 pandas 把一套最小可复用的流程代码写出来最后聊聊这类工作的适用边界和长期价值。1. 先别急着画图Airbnb 房源数据里最常见的问题1.1 拿到数据后的第一件事不是清洗而是“体检”很多教程上来就教你怎么删缺失值、怎么去重、怎么转换类型但如果你还不知道数据里到底有什么问题这些操作就是盲人摸象。更稳妥的做法是先做一次系统性的数据体检把全貌摸清楚再决定清洗策略。在 pandas 里我通常会按顺序执行这样几条检查import pandas as pd df pd.read_csv(listings.csv) # 1. 看整体规模 print(数据规模:, df.shape) # 2. 看列名和数据类型 print(\n列信息:) print(df.dtypes) # 3. 看缺失值分布 print(\n缺失值数量:) print(df.isnull().sum().sort_values(ascendingFalse)) # 4. 看数值字段的统计概览 print(\n数值统计:) print(df.describe())第一步不是处理任何数据而是花三分钟确认三件最关键的事数据有多少行、多少列每一列被解析成了什么类型哪些字段存在缺失。这几个信息会直接影响后续所有清洗决策。以常见的 Airbnb listings.csv 为例你大概率会发现这样一个格局id和host_id是整数price却因为混入了货币符号被识别成 objectlatitude和longitude是 float但取值范围是不是真的落在某个城市范围内肉眼看不出来last_review被读成字符串而不是 datetime因为里面有大量空值或者格式不统一name和host_name这类文本字段看起来没问题但很可能前后带着空格大小写也不一致。这一轮体检做完你才真正知道自己面对的是什么类型的“脏”。不同的脏数据处理方式完全不同。1.2 几个高频脏数据样本在真实数据集里Airbnb 房源数据的脏数据形态通常集中在以下几类我列成一张表方便对照检查问题类型典型字段表现影响类型错误price包含$、逗号解析为字符串无法直接做数值计算缺失值host_name、last_review、reviews_per_month大量 NaN统计和计算时被跳过或报错异常值price、minimum_nights价格为 0 或极高night 为 999拉偏统计结果和建模效果重复记录id、name、host_id完全相同行或 id 相同但其他字段不同重复计算统计虚高文本不规范name、host_name、neighbourhood空格、大小写不统一、同义词混用分组聚合时把同一类切成多组日期格式不统一last_review部分为空、格式混杂无法直接做时间序列分析这六类问题并不是孤立存在的。一个字段可能同时存在多种问题比如price既有格式问题又有异常值问题last_review既有缺失又有格式问题。所以清洗规则一定要设计成“按列处理、按类判断”而不是用一两个通用函数打天下。很多人一看到缺失值就dropna()一看到重复就drop_duplicates()结果数据行数瞬间少了一半最后连自己删了什么都不知道。这就是没有先做体检的代价。2. 清洗不是删数据而是理解每条规则背后的业务含义2.1 缺失值不能一个 fillna 走天下缺失值是 Data Cleaning 里最容易被误解的问题。表面上看缺失就是空白处理方式无非是删除、填充、保留三种。但真正决定用哪种方式的不是统计指标而是业务含义。在 Airbnb 房源数据里last_review和reviews_per_month往往是同时缺失的。这大概率不是数据采集出了问题而是这套房源根本没有收到过任何评论。此时把它们填充成 0 或者某个平均值反而会掩盖一个真实的业务特征这是一批新上线或几乎没有订单的房源。再看host_name如果缺失说明房源主人没有填写昵称或者录入时漏填。对这类字段填充一个“Unknown”比直接删行更利于保留整体样本量。我的建议是按字段类型建立三套策略合理缺失如无评论导致last_review缺失保留“从未评论”这一信号或显式标记为 0。信息缺失但有代表性如host_name缺失用统一占位符填充避免丢掉整条房源样本。缺失占比极高且分析中不必要如某列缺失超过 70%且与分析目标无关优先考虑删除该列而不是删行。# 示例按业务含义处理缺失 df[last_review] pd.to_datetime(df[last_review], errorscoerce) # 从未被评论与缺失是有业务含义的 df[reviews_per_month] df[reviews_per_month].fillna(0) # 文本字段缺失用占位符 df[host_name] df[host_name].fillna(Unknown)判断规则就一条填充动作不应引入错误的业务信号。你要清楚自己是在处理“数据没采到”还是在处理“业务上本来就该为空”这两件事完全不一样。2.2 异常值怎样判断价格 1000 是正常还是异常异常值的处理比缺失值更依赖业务常识。同样是一个价格 1000 的房源在纽约可能只是普通民宿的周末价在一个小城市可能已经是最贵的豪宅。只看统计分布就删除很可能会把真实的高端市场信号当成噪声处理掉。处理价格字段的钱景一般是这样的# 清理货币符号和千分位逗号 df[price_clean] ( df[price] .astype(str) .str.replace($, , regexFalse) .str.replace(,, , regexFalse) .str.strip() ) df[price_clean] pd.to_numeric(df[price_clean], errorscoerce)转换成数值之后再去看分布。一个更稳妥的异常值判断方法是先用分位数划定一个阈值范围而不是直接写死“价格大于 1000 就删”# 先看四分位数 print(df[price_clean].describe()) # 用 IQR 识别极端值但不要直接删先标出来看 Q1 df[price_clean].quantile(0.25) Q3 df[price_clean].quantile(0.75) IQR Q3 - Q1 outliers df[ (df[price_clean] Q1 - 1.5 * IQR) | (df[price_clean] Q3 1.5 * IQR) ] print(f疑似异常值数量: {len(outliers)})标出来之后更重要的是人工判断价格为 0 的多半是测试数据或录入错误可以删除价格极高但room_type是整套别墅、minimum_nights很短可能是真实存在的高端房源要结合业务目标决定是否排除。Data Cleaning 不是机械执行统计规则而是用统计规则辅助做业务决策。2.3 重复值为什么简单 drop_duplicates 可能误删对重复值的处理也存在类似误区。直接调用drop_duplicates()会按整行内容判断这能去掉完全相同的副本但处理不了“同一个房源出现两行但某些字段有差别”的情况。Airbnb 数据里更常见的重复问题是同一个id可能出现多次但name、price或last_review有细微差异。这可能是数据采集时在不同时间点抓到了更新的版本也可能是同一房源被多个渠道重复录入。# 完全重复行可以安全删除 df_cleaned df.drop_duplicates().copy() # 基于 id 去重保留最新一条 # 假设存在 updated_at 一类的时间列如果没有则要谨慎 df_dedup ( df_cleaned .sort_values(last_review, ascendingFalse) .drop_duplicates(subset[id], keepfirst) )需要提醒的是sort_values依赖一个能表达“先后顺序”的字段。如果原始数据没有这样的字段就不要轻易用“保持某一条”的策略而是输出重复 id 列表人工判断哪一条更完整。去重的真正难点不是去掉重复行而是定义“什么算重复”。这背后其实是在回答一个业务问题这个数据集里唯一的分析单元是什么。对 AirBnB 房源数据来说唯一分析单元通常是一个房源的id那么去重就应以id为准如果要分析房东维度就可能需要按host_id做更多的合并处理。3. 用 pandas 把 Airbnb 列表整形成可用结构3.1 最小清洗流程类型修复与格式统一当数据体检完成、字段层面的处理策略确定之后就可以进入具有实际操作意义的清洗环节。下面这段代码可以看作一套针对 Airbnb listings 数据的最小清洗流程适合先跑通再扩展def clean_airbnb(df): 一个针对 Airbnb listings 数据的通用清洗骨架。 具体字段名请以实际数据为准。 df df.copy() # 1. 修复类型 df[price] ( df[price] .astype(str) .str.replace($, , regexFalse) .str.replace(,, , regexFalse) .str.strip() ) df[price] pd.to_numeric(df[price], errorscoerce) df[last_review] pd.to_datetime(df[last_review], errorscoerce) # 2. 缺失值处理 df[reviews_per_month] df[reviews_per_month].fillna(0) df[host_name] df[host_name].fillna(Unknown) # 3. 文本字段格式统一 text_cols [name, host_name, neighbourhood] for col in text_cols: if col in df.columns: df[col] df[col].str.strip().str.title() # 4. 类别字段统一 if room_type in df.columns: df[room_type] df[room_type].str.strip().str.lower() # 5. 去除完全一致的重复行 df df.drop_duplicates().copy() return df df_clean clean_airbnb(df)这套函数核心就一句话把清洗从“临时处理”变成“可重复执行”。当你每次拿到新增数据时不需要重写一遍逻辑只需要调用同一个函数。这也是从“在 Notebook 里到处乱改”走向“工程化数据预处理”的第一步。需要注意errorscoerce会把无法解析的值转成 NaN这既是一种保护也是一种风险。保护在于不会被异常格式打断风险在于如果price列里混入大量非数值文本清洗后会出现一批空值所以清洗后一定要复查缺失值数量而不是直接拿结果去做分析。3.2 数据整形从“房源表”到“分析视图”Data Cleaning 负责把数据弄干净Data Shaping 负责把数据变成分析友好的形状。Clean 处理的是单元格里的问题Shape 处理的是行和列的组织方式。Airbnb 房源数据最常见的整形需求有两类。第一类是分组聚合比如想知道不同区域、不同类型房源的均价差异# 计算每个 neighborhood 的平均价格和平均评分 neighborhood_stats ( df_clean .groupby(neighbourhood) .agg( avg_price(price, mean), avg_review_score(review_scores_rating, mean), listing_count(id, count), ) .reset_index() ) print(neighborhood_stats.head())第二类是长表转宽表比如想对比不同街区不同房型的房源数量。此时pivot_table是更合适的工具# 不同街区、不同房型的数量矩阵 room_type_pivot pd.pivot_table( df_clean, valuesid, indexneighbourhood, columnsroom_type, aggfunccount, fill_value0, ) print(room_type_pivot.head())整形还有一个常见用途是分箱。把连续变量切成有序分类变量在分析价格区间时很实用# 把价格切成区间方便做分布分析 df_clean[price_bucket] pd.cut( df_clean[price], bins[0, 150, 300, 600, float(inf)], labels[经济型, 舒适型, 高端型, 奢华型], ) print(df_clean[price_bucket].value_counts())Data Shaping 的本质是让数据的形状匹配分析目的。同样的房源表按区域聚合时是一张区域汇总表按房型透视时是一张矩阵表按价格分箱后又是一张分布表。没有哪一种形状是绝对正确的只有适不适合当前问题。3.3 清理后必须做一次“结果验证”清洗和整形之后不能让数据直接投入分析而是要先做一次结果验证。这一步承担的是质检角色。我会从四个角度复查# 1. 行数是否合理变化 print(原始行数:, len(df)) print(清洗后行数:, len(df_clean)) # 2. 列类型是否都正确 print(\n类型检查:) print(df_clean.dtypes) # 3. 缺失值是否还在可接受范围 print(\n清洗后缺失值:) print(df_clean.isnull().sum().sort_values(ascendingFalse)) # 4. 关键字段的取值范围是否符合业务常识 print(\n价格范围:, df_clean[price].min(), -, df_clean[price].max())只要结果里出现类似“清洗后数据量只剩原来的一半”“价格最小值还是负数”“价格仍包含 0 元”这类信号就不要继续往下分析。先回到前面的步骤找出是哪条规则把数据洗过头了还是哪类问题没有被覆盖到。4. 清洗流程怎么从“跑一次”变成“可复用”4.1 顺序比参数更重要很多初学者会陷入一个误区把清洗函数写得特别复杂想一步覆盖所有情况。实际上真正可复用的清洗流程靠的不是复杂参数而是固定的执行顺序。我建议的顺序是读取原始数据别用 Excel 手动改先让人工步骤降到最低。数据体检记录原始 shape、dtypes、缺失值分布。类型修复先把日期、数值、字符串解析对。缺失值处理按业务含义决定填充、删除还是保留。异常值处理用统计工具识别结合业务判断。去重先定义“什么算重复”再执行删除。格式统一处理文本大小写、空格、类别词。数据整形分组、透视、分箱生成分析视图。结果验证重新检查规模、类型、缺失范围和关键字段域。这个顺序不是随便拍的。类型修复必须排在缺失值处理之前因为字符串类型的price无法参与数值缺失判断去重应该排在异常值之后因为价格为 0 的记录和重复记录可能是两码事分开处理更容易定位问题。4.2 关键验证步骤回到原始分析目标清洗流程不能只验证“数据变干净了”还要验证“是不是为分析目标做好了准备”。换句话说你要在清洗前先问自己一个问题这份数据最后供谁使用是画一张散点图还是训练一个价格预测模型还是做一份区域分析报告不同的使用目标对清洗的容忍度完全不同。如果只是做探索性可视化价格里的少量极端值可以保留因为它能暴露分布的长尾如果要做线性回归模型极端值就必须处理否则会严重拉偏系数如果要做“各区域平均租金”的业务报告就需要先决定是否剔除那些长期没有评论、几乎不活跃的房源。建议在清洗函数旁边保留一份cleaning_notes.md记录每条规则的理由和参数选择。这个过程相当于给后续的自己留下决策依据。隔一个月再回来看你就不需要靠记忆去猜当初为什么把reviews_per_month填充成了 0。4.3 新手最容易忽略的三个坑第一个坑是在原数据上直接做赋值修改。pandas 的很多操作默认返回新对象但在某些情况下inplace修改会直接影响原始读入的数据一旦改错就得重新读取文件。保险做法是从第一步就df df.copy()让清洗在一个副本上进行。第二个坑是清洗规则没有留痕。很多人拿着 Notebook 一边跑一边改等到交付时别人问你“这列为什么删了 500 行”你完全答不上来。可复用的流程不是跑完就结束而是要让每一步操作都对后来者透明。第三个坑是忽略环境依赖。同样的 pandas 代码在 pandas 1.x 和 2.x 上可能出现不同的警告甚至行为差异。项目里最好固定使用的 Python 版本和 pandas 版本。这里并不要求你换一堆工具但用requirements.txt或pyproject.toml把依赖记录清楚是让流程在三个月后还能跑起来的最小成本。4.4 排查链路结果不对先查哪一层即使流程规范清洗阶段还是会出现各种“看似对但结果不对”的情况。我的排查建议是走逐层链路不要一上来就怀疑数据集本身。先看输入原始文件是否读取完整编码是不是 UTF-8路径是否正确。再看类型dtype是否与预期一致price是 int 还是 float 还是 object。再看缺失关键字段的缺失值是否在合理范围内errorscoerce是否引入了新的 NaN。再看边界分组聚合是否因为文本大小写不一而产生了重复类别透视表是否填充了不该填充的 0。再看输出结果表的行数与原始表对不上时判断是不是去重规则写得太宽泛。最后看工具限制当前 pandas 版本的函数行为是否有变化比如fillna和drop_duplicates的默认参数在版本演进中的行为差异。走完这层链路大多数问题都能定位到某个具体环节而不是停留在“代码报错了”这个模糊层面。5. 这事儿的边界哪些清洗该做哪些别过度5.1 为什么 Airbnb 房源数据是练习 Data Cleaning 的好样本Airbnb 列表数据的魅力在于它提供了一个复杂度恰到好处的练习场。数据量级一般在一两万到十几万行不会让观察结果变得不可控字段类型覆盖数值、文本、日期、类别能让人在一份数据集里接触到几乎所有常见清洗场景业务语义足够清晰房源的定价、位置、评分、评论数、经营活跃度都是普通人能凭直觉理解的概念。这种“能理解业务含义”的练习数据比一堆抽象指标构成的金融数据更容易建立直觉。当你看到reviews_per_month缺失时你能快速联想到“这个房源可能没有获得评论”而不是在一堆不知所云的字段编号里找规律。5.2 生产环境里的清洗不能这样简单需要提醒的是本文讨论的清洗方式非常适合原型验证、个人分析和教学练习但如果你的场景是生产系统里的实时数据管道、推荐系统特征工程或者自动化报表就不能直接照搬。生产环境要额外考虑更多因素清洗规则必须有版本管理和回归测试不能靠人肉确认结果。数据质量监控需要自动执行比如字段缺失率超过阈值时触发告警。异常值的处理策略往往需要 A/B 验证不能凭业务常识一刀切。删除任何原始数据都要慎重原始数据报错后应该留存或者在任务结束前保留快照用于审计和回溯。如果你只是单次分析项目手写一个函数就够了如果要进入流水线就要把清洗逻辑写成可测试、可回滚、可监控的小模块。这不是过度设计而是生产环境的基本要求。5.3 过度清洗同样危险最后说一个经常被忽略的反方向问题过度清洗。Data Cleaning 不是把数据洗得越“干净”越好而是要保留真实世界里的有效波动。比如一个房源的价格比周边高 30%这在统计上可能是离群值但在业务上可能是该房源带有特殊设施或处于热门地段。如果只凭 IQR 规则把它删掉分析结果确实“更稳定”但也损失了一部分真实的市场信号。更稳妥的做法是把异常值标出来而不是直接抹掉。比如增加一列is_outlier保留原始值同时让后续分析自行决定是否排除。这样既不污染判断也不丢失信息。针对 Airbnb 列表这类数据分析练习真正值得长期沉淀的不是某段具体代码而是一套稳定的处理框架。我可以把它浓缩成五步输入理解搞清楚字段含义、业务背景和唯一分析单元。列级规则逐列确认类型、缺失、异常值并定义处理策略。行级处理基于整个记录判断是否需要删除或合并。结构整形按分析目标做分组、透视、分箱和特征衍生。结果验证用业务常识复查行数、类型、缺失和取值范围。这套框架同样适用于商品订单、用户日志、内容平台数据等其他业务类型。你把 Airbnb 数据跑熟之后真正收获的是迁移到其他数据上的方法论而不只是记住几个 pandas 函数。数据分析里有一句话模型决定上限数据决定下限。对于空气质量这类列表数据认真做一轮 Data Cleaning 和 Data Shaping远比急着调参、急着跑模型更重要。毕竟一份连价格都算不准的数据喂给任何算法都只会得到精致的垃圾输出。如果你手边正好有一份这样的数据建议这一晚先把清洗流程完整跑通体检、修类型、处理缺失、过滤异常、去重、整形、验证。等这套流程能稳定复现的时候你再去研究那个房价预测模型会发现起点完全不同。