ARTICLE DETAIL

资讯详情

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

Pandas实战:Airbnb房源数据清洗与塑形全流程

Pandas实战:Airbnb房源数据清洗与塑形全流程 Cleaning and Shaping the Airbnb Listings 这类任务说人话就是把一份从公开渠道下载回来的 Airbnb 房源 CSV从原始混乱表整理成一张可以直接做分析、画地图、跑模型的干净表。很多新手把注意力都放在最后的可视化上结果价格还是字符串、日期还是文本、重复记录也没去干净图一出来全是错的。这篇文章更适合刚开始接触数据清洗的人也适合需要交付一份“别人能直接复用”的房源数据表的读者。最值得关注的点不是把表清得多漂亮而是每一步清洗都有可验证的标准字段类型、缺失值、重复记录、异常值以及最后导出时的版本和编码。1. 先搞清楚 Airbnb 房源数据到底要“清”什么1.1 原始数据里常见的几类脏数据Airbnb 公开的 listings 数据通常是一张 CSV 表格常见列有 id、name、host_id、neighbourhood_group、latitude、longitude、room_type、price、minimum_nights、number_of_reviews、last_review、reviews_per_month、calculated_host_listings_count、availability_365。这些列名看起来规范但实际数据经常是另一种情况。price 字段经常是字符串取值可能是 $150.00、1,200 或 150直接做平均值运算会报错或者得出一个没有意义的结果。last_review 是日期字段但很多行是 2023-06-15 这样的字符串还有一部分是空值。name 和 description 这类文本字段里混着全角空格、换行符、HTML 标签。重复记录不一定整行完全相同更多是同一个 id 出现多次但其他字段有差异。我把这些统称为“脏数据”。清洗的目标就是把字段类型、缺失值、重复值、异常值、文本格式全部处理到合理状态。注意这里的“合理”不是指把所有空值都填满而是指每个字段都有明确的类型、清晰的口径和可解释的处理方式。1.2 先写清洗目标再动代码一个常见误区是拿到 CSV 就开始写 replace写到哪算哪。我一般会先列三到五条清洗目标price 从字符串转成浮点数单位统一last_review 全部转成 datetime解析不了的按缺失处理删除完全重复行并按 id 去重经纬度超出合法范围的记录单独标记最终导出一份 utf-8 编码的 clean CSV。目标写在前面后面每一步都能拿它来检查。比如删完重复行之后用 id 的去重结果核对价格转换后统计缺失率有没有升高。如果没有目标清洗过程很容易越做越乱。到了塑形阶段才发现某个字段的口径不对再回头改清洗逻辑整体成本会翻倍。1.3 Cleaning 和 Shaping 是两个步骤Cleaning 是把错误的数据改对Shaping 是把对的数据整理成适合分析的形状。比如把连续价格分成区间、新增可用率列、把多表合并成宽表这些都是 shaping。清洗放在前面塑形放在后面。顺序反了会非常麻烦分箱、透视、建模全做完了才发现价格还是字符串返工成本很高。简单的判断标准是如果某一列的值还不能被直接计算、比较或聚合那它还在 cleaning 阶段如果能被直接使用只是想让表的结构更适合某个分析场景那才是 shaping。2. 准备环境并读取数据先摸清表再动手2.1 依赖环境不用太重但版本要确认本地跑这套流程Python 3.9 以上加 pandas、numpy 就够了。清洗和塑形不涉及深度模型没有 GPU、显存之类的需求普通笔记本就行。建议先确认版本python -V pip show pandas numpy这里有一个经验如果当前环境里还有其他项目在跑不要急着升级 pandas。版本变动可能会影响其他脚本更稳妥的方法是建一个独立虚拟环境python -m venv airbnb_env source airbnb_env/bin/activate # Windows 下是 airbnb_env\Scripts\activate pip install pandas numpy虚拟环境的好处是整个清洗流程的依赖是隔离的。就算未来 pandas 升级也不会影响这个脚本的可复现性。2.2 读取 CSV 最容易踩的两个坑读取本身一行代码就能完成但参数写不对后面全是问题。import pandas as pd df pd.read_csv(listings.csv, encodingutf-8, low_memoryFalse)low_memoryFalse 很容易被忽略。列很多的大 CSVpandas 默认分段读取可能同一列前半段推断成字符串、后半段推断成数字最后整列变成 object。设置 low_memoryFalse 可以避免这种不稳定推断。数据量大的时候这个参数会让读取稍慢一点但换来的是类型稳定值得。如果读取时报 UnicodeDecodeError先试 encodingutf-8-sig这个参数能处理带 BOM 的文件不行再试 gbk 或 latin1。不要反复盲试先用记事本或编辑器打开文件确认编码格式。读进来之后先跑三行print(df.shape) print(df.columns.tolist()) print(df.dtypes)行数和预期不符很可能文件用了分号分隔或者文本里含有未被引号保护的逗号列类型全是 object说明类型推断出了问题需要回到读取参数上排查。2.3 缺失值要按字段看不能只看总数missing df.isna().sum() missing_ratio missing / len(df) print(missing_ratio.sort_values(ascendingFalse))我一般重点看缺失率超过 50% 的字段。像 last_review 和 reviews_per_month在原始数据里经常是联动的没有评论的房源这两列同时为空。这是业务逻辑决定的正常缺失不是数据错误不能简单全删。判断标准可以是缺失率低于 1% 的字段删除对应行影响不大缺失率高但不是核心分析字段保留并标记核心字段缺失率超过 20%先回到数据源确认是不是下载版本不全而不是硬着头皮填充。3. 字段级清洗逐列处理不要整体糊弄3.1 价格字段先清理符号再转数值价格字段的处理顺序是转成字符串、去货币符号、去千分位、去空格、转数值。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)errorscoerce 表示解析失败的值变成 NaN而不是抛异常。这样能保留现场方便统计到底有多少条价格异常。如果直接让它抛异常脚本会中断而且你仍然不知道坏数据长什么样。转完马上检查print(df[price_clean].isna().sum()) print(df[price_clean].describe())如果转换后缺失数比转换前明显增加说明有非标准格式常见的是 150 / night 这种带注释的字符串。把这类样本打印出来单独看再决定替换规则。注意替换规则要尽量窄只处理你确认过的格式不要写一个把所有非数字字符都删掉的万能正则那样容易把合法价格也改坏。3.2 日期字段转成 datetime不要留字符串last_review 这类日期列必须转成 datetime否则后续做“近 90 天有评论”“按月份聚合”都做不了。df[last_review_dt] pd.to_datetime(df[last_review], errorscoerce)如果你的 pandas 版本比较老有些人会加 infer_datetime_formatTrue。这里提醒一句pandas 2.x 里这个参数已经废弃新版默认会尝试推断常见格式不需要再传传了反而可能报警告。转换后同样要看缺失比例。如果失败率很高先打印原始值确认是 2023/6/15 还是 06-15-2023再决定是否手动指定 format。不要一上来就固定格式因为大部分情况下 pandas 的默认推断已经足够可靠。3.3 文本字段保留原始列新增清洗列name 字段的常见清洗方式df[name_clean] ( df[name] .astype(str) .str.strip() .str.replace(\n, , regexFalse) .str.replace(r\s, , regexTrue) )description 这类长文本如果只是入库或展示建议保留原始列另存一列清洗后的版本。清洗规则随时可能调整保留原始列才能重新生成不用重新下载数据。这里有一个非常容易踩的坑字段里如果是 NaNastype(str) 会把 NaN 变成字符串 nan。这样后续统计时会出现一个看起来像真实值的 nan很难发现。处理方式是在转换前或转换后把 nan 统一替换成真正的空值或者在 astype 前先用 fillna 占位。3.4 经纬度、区域和房型字段经纬度检查用范围判断invalid_lat df[~df[latitude].between(-90, 90)] invalid_lon df[~df[longitude].between(-180, 180)]注意经纬度等于 0 不一定非法它可能是某个城市的默认占位坐标需要结合区域字段一起判断不能只看这一个条件。多年前的纽约 Airbnb 数据里就出现过一部分经纬度为 0 的记录如果直接当作真实坐标去画地图所有点都会堆在地图左下角整张图直接废掉。room_type 这种分类字段先统计唯一值再统一映射。不要直接按自己的想象替换要先看清原始数据里到底有哪几种写法。df[room_type_normalized] ( df[room_type] .astype(str) .str.strip() .str.lower() .replace({ entire home/apt: entire_home, private room: private_room, shared room: shared_room, hotel room: hotel_room, }) )4. 去重、去异常、统一业务口径4.1 先定主键再判断重复Airbnb 数据一般用 id 作为房源唯一标识。判断重复不要用整行比较而是按 id 看dup_by_id df[df.duplicated(subset[id], keepFalse)].sort_values(id)先观察这些重复 id 是完全一样还是部分字段不同。完全一样直接 drop_duplicates 解决部分不同通常保留“更新”的那条。判断更新标准可以用 last_review也可以用数据抓取日期。如果实在没有时间依据就保留靠后的行并在清洗说明里写清楚这个规则。一个容易犯的错是直接 df.dropna()。如果数据里存在正常业务缺失一整行删掉会带走大量有效信息。缺失处理要先分字段做判断再决定是删除、标记还是填充。重复处理和缺失处理一样都需要留下可追溯的记录否则交付数据时别人问“为什么少了 200 行”你答不上来。4.2 异常值要结合业务看不能只靠统计方法价格等于 0 几乎可以确定是异常价格等于 5000 可能是真实的高端房源。统计方法只能告诉你哪些值偏离不能告诉你它是不是错。我的处理顺序是用 describe 看分布和分位数打印极端值对应的 name、room_type、minimum_nights结合多个字段判断比如 minimum_nights365 的房源价格偏低可能是月租口径明显不合理的记录单独标记不直接删除。minimum_nights 里出现过 1000、9999 这类值看着像异常实际是房东设置的长期出租限制。要不要修正取决于业务需求不能因为数值大就删。同样地number_of_reviews 很大不是异常那是热门房源reviews_per_month 是 NaN 反而是正常的因为它和 last_review 联动。4.3 分类字段要先看唯一值再统一映射区域字段经常混着英文名、本地语言写法、大小写不一致。统一前先执行print(df[neighbourhood_group].value_counts(dropnaFalse))看到全部取值后再写映射规则。这里有个很容易翻车的点统一分类时把两个含义不同的分类合并成了同一个。比如 Private room 和 private room 可以合并但 Entire home/apt 和 Hotel room 绝不能合并。映射表里的一行代码可能就是后续分析口径变化的原因。如果区域字段里有一类是 Manhattan、另一类是 manhattan这种大小写差异直接 lower 后合并即可。但如果出现不同语言写法比如 曼哈顿 和 Manhattan就需要先问清楚业务上要用哪个口径而不是自己拍板合并。5. Shaping把清洗结果整理成能直接分析的形状5.1 选列、重命名、定类型清洗完成后挑出真正需要的列重命名统一风格设置正确类型。这一步是在确定最终交付表的结构。columns_keep [ id, name_clean, host_id, neighbourhood_cleansed, latitude, longitude, room_type_normalized, price_clean, minimum_nights, number_of_reviews, last_review_dt, reviews_per_month, availability_365, ] df_out df[columns_keep].rename(columns{ price_clean: price, last_review_dt: last_review, room_type_normalized: room_type, })字段命名建议统一用小写加下划线比如 price_per_night、review_count。这套风格和 pandas、SQL、各种可视化工具都兼容不会因为字段名大小写不一致踩坑。类型设置要看用途。id 适合 intprice 和 reviews_per_month 适合 floatlast_review 是 datetimeroom_type 和 neighbourhood 可以设为 category。分类字段设成 category 能提升 groupby 和绘图的效率但不是所有字符串都适合转字段基数太高反而浪费。比如 name_clean 就不应该设成 category每个值都不重复category 类型不会带来性能收益。5.2 派生字段和分箱塑形阶段常做的事是增加派生字段常见有price_per_night 单日价格has_reviews 是否有评论review_recency 到参考日期的天数availability_ratio 可用率price_bucket 价格分档。分箱不要用拍脑袋的固定值。先看价格分布再用分位数切或者明确业务上的“低中高”定义。df_out[price_bucket] pd.qcut( df_out[price], q4, duplicatesdrop, )pd.qcut 会按分位数切分。如果数据里大量重复值导致分箱
返回列表