ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Pandas类型转换与缺失值处理:数据清洗实战全指南

Pandas类型转换与缺失值处理:数据清洗实战全指南 1. 从会写到不慌为什么第三篇我选择先讲类型转换如果你已经跟着前两篇把DataFrame的创建、索引、筛选、布尔判断这些基础操作过了一遍那你大概正处于一个好像会了又好像不太会的阶段。这种感觉很正常因为pandas真正的门槛从来不是API调用而是当你面对一份真实数据时那些藏在暗处的类型问题、缺失值、编码错误才是逼你熬夜的元凶。这一篇我打算换个思路不按官方文档的目录顺序走而是把实际分析中最容易翻车的几个环节串起来类型转换、字符串处理、缺失值清理、读写文件的边界情况。它们看起来零散但本质上是同一条技能链——让DataFrame里的数据变得听话。你只有先把数据掰顺了后面画图、统计、建模才不会半路炸锅。我遇到过太多初学者DataFrame能建出来、loc和iloc也能用但一执行astype就报错一读CSV就是乱码groupby一跑就NaN满天飞。不是说他们操作错而是缺了一套先诊断、再下手的条件反射。这篇就把我踩过的坑、用顺手的套路以及为什么那样选型的逻辑一次说清。2. 类型转换的底层逻辑dtype比你想的更记仇2.1 一张表看清Pandas常见类型在聊astype之前你至少得先知道pandas里到底有哪些类型它们跟Python原生类型有什么区别。我直接给你一张我桌面上常驻的对照表Pandas dtypePython等价类型常见出现场景坑点备注int64 / int32int计数、编号含NaN时自动变float64float64float金额、比例整数列一旦混入缺失值就升级为floatobjectstr / 混合文本列、混入多种类型最容易被误判成字符串boolbool标志位、开关和0/1互换需要小心datetime64[ns]datetime.datetime日期时间列一旦是字符串排序就全乱timedelta64[ns]datetime.timedelta时长列相减才能得到不是默认类型category无枚举、分类能省内存、能排序但运算受限stringpandas 1.0str纯文本和object不一样扩展类型这张表你不需要背但要用得熟。我个人的经验是拿到一张新表的第一件事不是看前五行而是跑一次df.dtypes。这一行输出能帮你提前预判百分之六十的坑比什么数据审查工具都管用。2.2 astype最常用但不是万能药astype是大多数人最早接触的类型转换函数它的用法相当简单df[年龄].astype(int64) df[单价].astype(float) df[是否有效].astype(bool)但这里有个隐藏陷阱astype是强转它不会帮你做合理性校验。比如你把一个含有未知两个字的字符串列转成float系统会直接抛ValueError或者在你把float列转int时小数点后面的值会被默默砍掉而不是四舍五入。import pandas as pd df pd.DataFrame({金额: [12.6, 8.3, 15.9]}) df[金额_截断] df[金额].astype(int) # 结果是 [12, 8, 15] df[金额_四舍五入] df[金额].round().astype(int) # 结果是 [13, 8, 16]这是我踩过最难受的坑之一报表里金额12.6被打印成12领导一眼就看出不对。所以后来我给自己定了一条规矩——凡是数值列要降精度比如float转int先round()再astype不直接硬转。2.3 to_numeric遇到脏数据时的第一选择如果你的列是混合类型比如读进来的CSV里1,200和1.5k这种带格式的文本astype(float)会直接报错。这种时候应该用pd.to_numeric它的优势在于能配合errors参数做分段处理s pd.Series([12, 14.5, N/A, 8, unknown]) res pd.to_numeric(s, errorscoerce) # 结果为 12.0, 14.5, NaN, 8.0, NaNerrors有三个选项errorsraise默认遇到解析失败就抛异常适合数据质量要求极高的场景。errorscoerce解析失败的变成NaN适合快速清洗但要注意后续填充。errorsignore整个列保持原样适合先看一眼再处理。我常用的套路是先用coerce转一遍再统计isna()的数量然后根据情况决定是填充还是删除。这个方法在金融数据、爬虫数据里几乎天天用。注意to_numeric一次只能处理一列。如果你有二十列都要转我的做法是先用df.select_dtypes(include[object])把疑似文本列筛出来再循环调用to_numeric。具体代码后面章节会写。2.4 datetime转换日期列不乱排序的基石日期时间转换是另一个高频场景也是最容易被人忽略的雷区。很多人读进数据后看到日期那一列长这样2024/1/3 2024-01-05 14:23:00 2024年1月7日如果是object类型你按字符串排序得到的结果根本不对。这时候用pd.to_datetimedf[日期] pd.to_datetime(df[日期], formatmixed)formatmixed是pandas 2.0以后才有的参数可以在同一列里混合多种日期格式时自动识别。但更推荐的做法是把format显式写好速度会快很多df[日期] pd.to_datetime(df[日期], format%Y/%m/%d)转换之后你就能用.dt访问器拿到.year、.month、.dayofweek这些属性也可以直接在groupby里按月份聚合。这一步做没做对直接影响后续所有时间序列分析。3. 字符串列处理的正确姿势别再硬拆了3.1 为什么不能用Python原生字符串方法我见过不少同学拿到DataFrame里的文本列很自然地写出# 错误示范 df[姓名].split(·)然后报错AttributeError: Series object has no attribute split。原因很简单Series的整体不是字符串而是字符串的集合。你要处理的是里面每个元素所以得用pandas的向量化字符串接口——.str访问器。df[姓名].str.split(·) df[姓名].str.strip() df[姓名].str.replace(先生, ) df[姓名].str.contains(张).str访问器几乎集成了Python字符串的所有常用方法而且它天生支持缺失值——当遇到NaN时返回NaN不会像原生方法那样抛错。这是我在清洗爬虫数据时最喜欢它的原因不用每行先判空再处理。3.2 split expand一键拆出多列字符串处理里最实用的一个组合是split配合expandTrue。比如有一列地址是广东省-深圳市-南山区你想拆成省、市、区三列df[[省, 市, 区]] df[地址].str.split(-, expandTrue)如果拆分后的段数不一致expandTrue会补NaN不会报错。这个用法在处理姓名、电话号码、版本号、IP地址时都极其高效。3.3 批量清洗的小套路先替换再拆实战中我常遇到的是半结构化文本比如商品A|数量:3|价格:12.5。这种字符串不规律需要先做统一替换再按分隔符拆分# 1. 先把冒号替换为竖线这样分隔符就统一了 df[信息] df[信息].str.replace(数量:, ).str.replace(价格:, ) # 2. 按竖线拆分成三列 df[[商品, 数量, 价格]] df[信息].str.split(|, expandTrue) # 3. 数量、价格列转数值 df[数量] pd.to_numeric(df[数量], errorscoerce) df[价格] pd.to_numeric(df[价格], errorscoerce)这套替换→拆分→转类型的思路我在不同项目里反复用了至少几十次。它不涉及任何高深API但非常耐打——脏文本只要大体有结构就能顺着这条链路洗成规整的表格。3.4 正则与extract当文本没有固定分隔符时总有那些不好好存数据的场景比如备注列里混合了3个、5件、12双这样的描述你想提取数字。用str.extract配合正则表达式就可以df[数量_提取] df[备注].str.extract(r(\d))这里(\d)会把第一段连续数字抓出来生成的是字符串类型后面记得再to_numeric转一下。正则表达式对新手可能有点劝退但你只需要掌握\d数字、[A-Za-z]字母、.*?任意字符非贪婪这几个就能解决一大半实际问题。4. 缺失值处理的艺术删得干净补得有理4.1 缺失值是怎么混进来的在真实项目里缺失值来源比你想的多用户没填、接口返回空字符串、导出表格里写了暂无、数据清洗时to_numeric把解析失败的变成NaN。你要做的第一件事不是着急删除而是搞清楚缺失值长什么样。# 统计每一列的缺失数量 df.isna().sum() # 查看缺失占比 df.isna().mean()这里有个很容易被忽略的细节pandas默认只把None和np.nan识别为缺失值空字符串不识别。如果你的数据里有大量空字符串isna()根本统计不到。解决办法是先做一个全局替换df df.replace(, pd.NA) df df.replace(暂无, pd.NA) df df.replace(N/A, pd.NA)我做过一次数据审查原本isna统计只报了3个缺失做完上述清洗后变成了200多个。差别不是数据变了而是之前那些缺失值一直藏在非标准缺失表示里。4.2 dropna什么时候该狠心删除dropna是最直观的缺失值清理方式# 丢弃含任何缺失值的行 df.dropna() # 只丢弃整行全为缺失的行 df.dropna(howall) # 指定列范围内有缺失才丢弃 df.dropna(subset[金额, 日期]) # 保留至少有2个非空值的行 df.dropna(thresh2)但我的经验是不要一上来就dropna。删除行等于删除信息除非你确认这些行里的其他列也都没用。比如一张订单表如果你的关键列是金额和时间那么subset只针对这两列做缺失判断其他列有没有缺失不影响行的去留这才是合理姿势。4.3 fillna按业务逻辑而不是按直觉填充填充缺失值比删除更讲理。常用策略包括数值列填充0适合缺失即无的业务比如退款金额、优惠券抵扣没有就是0。数值列填充均值/中位数适合指标型数据比如评分、温度用中位数更抗极端值。前向填充适合时间序列比如库存快照某天没记录用昨天的值。后向填充适合周期性结算比如月底汇总某个值缺失用下个月的回填。# 用中位数填充 df[评分] df[评分].fillna(df[评分].median()) # 按组填充不同类目用不同均值 df[金额] df.groupby(类目)[金额].transform(lambda x: x.fillna(x.mean())) # 时间序列的前向填充 df df.sort_values(日期) df[库存] df[库存].fillna(methodffill)按组填充是我特别推荐的一种做法。比如不同商品类目的单价差异巨大全表填一个均值会失真按类目分组后各自填均值保留了组间差异结果会合理很多。methodffill在pandas 2.2之后要写成df[库存].ffill()老写法有警告过渡期建议直接改用新写法。提示fillna要特别注意inplace参数的问题。我见过太多人写df df.fillna(...)之后又忘了赋值给新变量结果原数据没变。pandas里凡是返回新对象的操作要么重新赋值要么用inplaceTrue两者选一个别混着用。5. 读写文件的边界情况中文路径、编码和类型推断5.1 读CSV时最容易翻车的那一行数据清洗的上游是读文件。这块我积累的血泪教训主要围绕三个词编码、路径、类型推断。先说编码。read_csv默认编码是utf-8但Windows上很多Excel导出的CSV是gbk或gb18030。如果你不指定编码打开直接就UnicodeDecodeError。我的处理习惯是先用兼容性好的utf-8-sig不行就换gbktry: df pd.read_csv(data.csv, encodingutf-8-sig) except UnicodeDecodeError: df pd.read_csv(data.csv, encodinggbk)utf-8-sig比utf-8多了BOM头处理能力读Excel导出的文件更稳。另外中文路径在Windows上经常引发找不到文件的问题建议要么代码里用绝对路径要么用pathlib来处理from pathlib import Path file_path Path(我的数据) / 订单表.csv df pd.read_csv(file_path)5.2 read_csv的dtype参数提前锁定类型这是我想让每个读者都养成习惯的一点在read_csv时就指定每列类型别等到后面再修。因为pandas会自动推断类型但推断三个字就意味着它会猜错。常见的猜错案例订单编号000123被读成整数123前面的0全部丢失。手机号13800138000被读成float64精度丢失后面显示成1.3800138e10。日期2024/1/3被读成字符串排序乱套。解决办法就是提前锁列df pd.read_csv( 订单表.csv, dtype{ 订单编号: str, 手机号: str, 金额: float, 日期: str, # 之后再用to_datetime统一转 }, encodingutf-8-sig )特别是在处理ID列、手机号、身份证号时凡是看着像数字但其实没有算术意义的列一律用str读取这个习惯能省掉后面大量的返工。5.3 写出文件时的编码与索引陷阱有读就有写写文件同样坑多。最常见的问题是写入CSV后Excel里打开中文乱码解决方法是df.to_csv(输出.csv, indexFalse, encodingutf-8-sig)indexFalse是为了不把行号写进去encodingutf-8-sig是为了方便Excel识别。这两个参数我建议每次写CSV默认带齐。如果你要写Excel需要openpyxl库pandas本身不内置df.to_excel(输出.xlsx, indexFalse, sheet_name汇总)另一个写文件时的大坑是DataFrame里混合了不同精度的小数写出后Excel里显示不一致。我的建议是在to_csv之前先对数值列做统一round()比如金额保留两位别把原始float直接丢给Excel。5.4 手机Python环境装pandas很多人不知道的捷径顺带说一个很多人私信问过的问题手机上的Python到底能不能装pandas。答案是能但别用自带的应用商店版本。推荐用Termux这类终端模拟器装好Python后执行pkg install python pip install pandas如果下载慢原因往往出在PyPI默认源上可以换成国内清华源pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple很多人在PC上装pandas也遇到过ERROR: No matching distribution found for pandas这种提示八成是pip源没配置好或者网络环境连不上PyPI。把源切到清华源之后我还没见过解决不了的。PyCharm里装pandas更简单File - Settings - Project - Python Interpreter点加号搜索pandas直接安装即可。如果遇到下载慢也是在Manage Repositories里添加清华源地址。6. 分组与聚合的初体验groupby的先拆后合逻辑6.1 groupby不是分组视图是拆分-应用-合并三步很多基础教程把groupby讲得过于神秘其实它的本质就是三步拆分split—应用apply—合并combine。pandas把数据按某列的值拆成若干小组然后对每组执行一个操作求和、均值、计数最后把结果合并成一张新表。df.groupby(类目)[销售额].sum()这句代码的意思是按类目分组对销售额列求和。得到的结果是一个Series索引是类目值是各组求和。你要是想一次性聚合多个统计量可以用aggdf.groupby(类目)[销售额].agg([sum, mean, count, max])这会返回一个DataFrame每一行是一个类目每一列是一种统计口径。做周报月报的时候这个用法比逐个写sum、mean高效太多。6.2 groupby后为什么会有NaN新手经常在groupby之后发现结果里有NaN然后很困惑。我告诉你最常见的三个原因分组列本身有缺失值。pandas默认把NaN单独分成一组这一组往往不是你想要的。被聚合的列里有NaN。sum()不受影响但mean()的结果会把NaN忽略后求平均不会报错但结果可能跟你预期不同。使用了as_indexFalse之前和之后的索引行为差异导致某些取数操作返回NaN。解决方案很简单# 去除分组列中的缺失值 df df.dropna(subset[类目]) # 聚合时显式指定参数 df.groupby(类目, as_indexFalse)[销售额].mean()as_indexFalse会让分组列留在列里而不是变成索引这在后续继续处理数据时方便很多是我个人的默认选择。6.3 分组里的transform保留行数的隐式广播agg返回的结果是每组一行行数变少。但有些时候你想保留原表的每一行比如计算每个商品价格相对自己类目平均价格的偏差这时就要用transformdf[类目均价] df.groupby(类目)[价格].transform(mean) df[价格偏差] df[价格] - df[类目均价]transform的结果会和原DataFrame保持相同行数相当于把每个组的计算结果广播回组内的每一行。这个操作在做特征工程时极其常用可以说是我从入门到熟练的分水岭之一。如果你已经能熟练使用transform你再看标准化归一化同环比这些概念思路会立刻打开。7. 查缺补漏那些基础教程不会明说的顺手习惯7.1 数据预览别只盯着head()我见过太多人拿到了DataFramedf.head()瞄一眼直接开始处理。这很容易漏掉两个关键信息一是df.dtypes没看二是df.shape没看。我给自己的固定流程是print(df.shape) print(df.dtypes) print(df.head()) print(df.describe())四行代码两分钟内对一张表的结构、规模、字段类型、数值分布有个整体印象。等形成这个条件反射你处理陌生数据的速度会提升一个档次。7.2 善用info()和memory_usage()df.info()会显示每列的非空数量、列类型和内存占用。当你拿到一个几百列的大表时这一步能帮你快速找出看起来是数字但实际上是object的字段以及空值特别多但你以为很少的字段。内存优化也是容易被忽略的。如果一个CSV有上千万行在不做任何处理时pandas会全部加载进内存。这时候把那些取值很少的文本列转成category类型能节省大量内存for col in df.select_dtypes(includeobject).columns: if df[col].nunique() 50: df[col] df[col].astype(category)这是我处理用户画像、类目标签这类数据时常用的手段内存有时候能降一半以上。7.3 判断条件别写在方括号里用query()从一个大数据框里筛满足多个条件的行很多教材教的是df[(df[年龄] 30) (df[城市] 北京)]这种写法能用但括号一多就晕。相比之下query方法更接近自然语言写起来干净很多df.query(年龄 30 and 城市 北京)字符串变量也能传进去甚至支持符号引用外部变量min_age 30 df.query(年龄 min_age and 城市 北京)从可读性、维护性上讲query都更胜一筹。我后来把老代码里的复杂条件筛选都改成了这种写法别的不说至少自己三个月后再看也不会挠头。7.4 轴的方向axis0是行axis1是列这是一个最基础也最容易混的概念。drop、fillna、apply里面都有axis参数我的记忆方式是axis0是往下走跨行操作axis1是往右走跨列操作。例如# 按列方向填充缺失即填充每列自己的中位数 df df.fillna(df.median()) # 按行方向求每一行的合计 df[行合计] df[[价格, 数量]].apply(lambda x: x[价格] * x[数量], axis1)这个记忆方法虽然简单但实际用起来非常稳我从没在轴方向上犯过方向性错误。7.5 使用管道方法链从步骤式到管道式当你操作步骤变多整天写df df.xxx可以直接尝试pandas的管道写法用.pipe()把多个步骤串起来。比如def clean(df): return (df .replace(, pd.NA) .dropna(subset[金额]) .assign(金额lambda x: pd.to_numeric(x[金额], errorscoerce)) .groupby(类目, as_indexFalse)[金额] .mean()) result df.pipe(clean)assign可以一次性新增或覆盖多列而pipe让多个步骤显得像流水线。刚开始看不习惯但代码一旦变长这种链式写法在可读性上优势非常明显。个人实操里我最真切的体会是pandas的每个函数单独看都不难难的是在真实数据场景里把它们连起来用。类型转换、字符串处理、缺失值清洗、读写文件、分组聚合——这是数据分析最趁手的一条主链路三个月中我把这套组合拳反复打了很多遍从几十行的小数据到几百万行的业务表流程通都能通区别只在你会不会先诊断再下手。最后分享一个小技巧在Jupyter里写pandas代码时每做完一步就观察一下输出结果的dtypes和index别急着往下走。数据框的形状和类型是你在处理过程中最可靠的路标比任何报错信息都早一步告诉你问题在哪。这个习惯一旦养成你后面学任何数据分析工具都会比别人少走很多弯路。
返回列表