ARTICLE DETAIL

资讯详情

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

Pandas处理CSV实战:从安装踩坑到分块存储优化

Pandas处理CSV实战:从安装踩坑到分块存储优化 处理CSV这活儿我大概写了得有八九年了。从最早用Excel打开一个200MB的文件卡到无响应到后来换用Pandas几秒钟读完再把结果写回CSV供业务部门复用——这个切换几乎是每个做数据分析的人都会经历的坎。今天这篇不打算讲那种官方文档式的API大全而是把我实际用Pandas处理CSV存储时的完整链路从安装踩坑、读入参数、清洗转换到分块优化、存储选型一整套真实可复现的流程放出来。无论你是刚装好Pandas还没跑通第一个pd.read_csv还是已经在处理百万级数据但总觉得内存紧张这篇应该都有对应能直接抄的东西。1. 安装与版本那些坑先让pandas在环境里完整跑起来1.1 清华源报错could not find a version的完整排查搜索引擎里这个报错出现频率极高ERROR: Could not find a version that satisfies the requirement pandas (from versions: none)后面还跟着一句No matching distribution found。很多人看到这串英文就以为是网络断了其实多数情况不是。我第一次遇到这问题是在一台装了Python 3.12的老电脑上当时的pandas稳定版还停留在2.1.x官方还没放出适配3.12的wheel包。pip install pandas去PyPI上查找时发现没有兼容当前Python版本的编译产物干脆一个版本都不报给你直接提示from versions: none。这就是裸奔的Python版本和尚未跟进构建的pandas之间常见的时间差。排查步骤按顺序做基本能定位到九成的问题先看Python版本python --version确认是3.9、3.10、3.11还是3.12。不同版本对应的pandas构建覆盖面不一样。再升级pippython -m pip install --upgrade pip。旧版pip解析依赖的能力弱有时明明有兼容版本却找不到。确认当前pandas支持的版本范围。pandas 2.x系列在较新的Python上构建及时但如果你卡在Python 3.8或3.7pip只会去匹配旧版pandas容易撞见依赖冲突。指定清华源时也要看报错细节pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple清华源本身是很稳的但如果镜像同步存在延迟刚发布的pandas版本可能还没被同步上去。这时加上--timeout 60或换阿里源、中科大源多试一次往往就通了。真正稳定可复现的解决路径是创建虚拟环境时就把Python版本和对齐的pandas版本锁好。比如我现在的项目里统一用Python 3.10 pandas 2.0.x三个环境装了三四次从没再被这个报错困扰过。建议没有特殊需求的读者直接无脑上Python 3.10或3.11兼容性面最广。检查Python位数python -c import platform; print(platform.architecture())32位Python对大文件操作的内存上限低很多CSV处理快不起来。检查是否装错环境pip -V看pip指向的site-packages路径很多人装了包却调不出来就是环境混了。用pip list | grep pandas确认已安装版本和来源。1.2 手机与不含pip的桌面环境里装pandas的注意点现在不少人会用手机写Python练习或者在一些受限办公电脑上装环境。手机端如果用的是Termux这类Linux终端模拟器直接pip install pandas通常可行但需要先装好build-essential、python-dev等编译工具因为部分依赖在ARM架构上没有预编译wheel需要现场编译耗时可能很长还容易中途失败。更省事的方式是用Pydroid 3这类自带pandas预装包的应用打开就能跑import pandas as pd。还有些正版办公电脑不开放外网源或者连pip都无法使用。这时可以从PyPI的wheel页面手动下载pandas的.whl文件拷贝到目标机器上用pip install ./pandas.whl离线安装。需要留意pandas依赖numpy、python-dateutil、pytz、tzdata这几个包缺哪个补哪个。离线装过一次之后建议用pip download -r requirements.txt -d ./offline_pkgs把常用包全部留存下次就快了。# 离线下载完整依赖 pip download pandas -d ./offline_pkgs -i https://pypi.tuna.tsinghua.edu.cn/simple # 目标机器安装 pip install ./offline_pkgs/*2. 读入CSV前先搞明白编码、分隔符与类型推断2.1 编码问题用utf-8-sig一招解决CSV文件的编码问题是我见过的新手翻车重灾区。明明文件里是中文数据pd.read_csv(数据.csv)一跑出来一堆乱码或者直接报UnicodeDecodeError。原因很直接read_csv默认用UTF-8解码而国内很多系统导出的CSV用的是GBK/GB2312编码Windows下的Excel另存为CSV时尤其常见。真正稳妥的做法是读入时显式指定编码import pandas as pd df pd.read_csv(业务数据.csv, encodingutf-8-sig)如果是Excel导出的文件通常编码是ANSI也就是GBK可以改用encodinggbk。我自己的经验是先用utf-8-sig试不行再换gbk。utf-8-sig和普通utf-8的区别在于它会自动处理文件开头的BOM标记避免读出来后第一列列名变成\ufeff列名这种情况。Excel导出UTF-8 CSV时经常带BOM所以统一用utf-8-sig读最省心。遇到连编码都无法确定的文件不要瞎猜直接用二进制打开看前几十个字节with open(未知.csv, rb) as f: raw f.read(100) print(raw)如果字节里出现大量连续的两个字节表示一个汉字且没有\xef\xbb\xbf开头大概率是GBK。也可以用chardet库自动检测实测准确率尚可但中文短文本时会抽风最终还是靠试。2.2 分隔符不只是逗号从sep到正则表达式CSV里的字母C是Comma但现实世界里的分隔符千奇百怪。欧洲某些系统导出的数据用分号;日志文件用制表符\t还有些老系统用竖线|。最坑的是明明数据里字段值本身就包含逗号比如地址北京市,朝阳区如果不加引号包裹读进来就会多出一列整行数据全乱。read_csv中等号左边的sep参数就是干这个的# 分号分隔 df pd.read_csv(data.csv, sep;) # 制表符 df pd.read_csv(data.tsv, sep\t) # 空格分隔多个连续空格压缩 df pd.read_csv(data.txt, sepr\s)sep还支持正则表达式这是很多老手都在用但新手不知道的细节。比如有的文件里分隔符不规律有时是逗号有时是多个空格用sepr[,;|]就能把连续的逗号分号竖线任意组合都当成一个分隔符处理。另外一个容易被忽略的参数是delimiter它和sep其实是同一个东西写谁都行但注意不要同时写两个会报参数冲突。还有个delim_whitespaceTrue等价于sepr\s是处理空格分隔文件的速记写法在日志类数据上很常用。遇到分隔符极其混乱的文件我习惯先不急着读完整而是nrows5只读前五行看看效果确认列数、列名、字段值都正常了再放开读全量。这一招能少踩很多坑。2.3 用dtype和parse_dates控制读入成本read_csv默认会自己去推断每一列的数据类型这听起来很方便但推断过程有代价对于大文件Pandas需要额外扫描数据才能决定某个列到底是int64还是float64有时还会把该是数字的列推断成object或者把大数字转成科学计数法。两个参数能有效控制这个问题df pd.read_csv(large_data.csv, dtype{ 商户ID: int32, 用户手机号: str, 金额: float32, 交易时间: str, }, parse_dates[交易时间])dtype参数本质上是把类型推断的主导权从Pandas手里拿回来。你在读文件之前就知道哪一列是ID、哪一列是文本、哪一列是金额直接告诉Pandas省掉它的扫描过程。对大文件来说这一项能明显缩短读入时间同时降低内存占用因为float32占用的内存是float64的一半。parse_dates的用处是把日期字符串直接转成datetime64类型。这么做的好处相当大只有真正的datetime类型才能用dt.year、dt.month做时间聚合也才能做时间排序、时间窗口筛选。如果你不在读入时转换后续还得再调用pd.to_datetime浪费一趟全表扫描。还有个实用的读入策略df_preview pd.read_csv(huge.csv, nrows1000) print(df_preview.dtypes)先读1000行看一下类型再正式读取时把手动指定的dtype传进去这是一个非常高效的工作流。我每次拿到陌生的大CSV文件都会这样先探路比盲读全量再回头排查快得多。3. 数据清洗过程中的类型转换与字段规整3.1 astype和pd.to_numeric的现实差异CSV读进来之后最常见的抱怨就是我这列明明是数字怎么算不了平均。打开dtypes一看那一列是object类型。这种情况多发生在文件里带了货币符号、千分位逗号、或者空格。用astype直接硬转经常翻车df[金额].astype(float) # 如果值是 1,234.00直接报错pd.to_numeric带了个errors参数可以让转换对脏数据更容忍df[金额] pd.to_numeric(df[金额].str.replace(, ).str.replace(,, ), errorscoerce)用errorscoerce时无法转换的值会变成NaN而不是中断整个DataFrame的处理。之后再统一填缺失值或剔除异常值即可。这套做法在处理网页爬虫导出的数据、银行账单、财务系统导出数据时几乎必用。再补一招如果列里混入中文数字、百分比的场景先做替换再to_numericdf[增长率] df[增长率].str.replace(%, ).astype(float) / 100一个很常见的错误是链式调用写的爽但改动没有生效——因为某些列本身是字符串类型.str.replace返回新列但你忘了赋值回去。整个过程记住一句话凡是涉及列内容改变的清洗操作都要把结果重新赋给原列df[列名] df[列名].astype(...)这种左边的赋值不能省。3.2 日期时间列的统一格式处理日期格式是CSV里真正让人头秃的问题。同一个文件里可能出现2024/01/01、2024-01-01、20240101、2024年1月1日这几种写法特别是多个系统合并出来的文件。Pandas的to_datetime本身有很强的解析能力大多数标准格式都能识别但碰上混合格式时最好还是先统一成字符串再做解析。df[交易日期] pd.to_datetime(df[交易日期], formatmixed, errorscoerce)其中formatmixed是pandas 2.x才有的参数表示允许Pandas用推断方式处理混用格式底层会自动选择解析策略实测对一列里同时存在斜杠线和横杠线的情况有效。如果确认格式统一也可以指定精确格式来加速解析# 只适配YYYY/MM/DD df[交易日期] pd.to_datetime(df[交易日期], format%Y/%m/%d)errorscoerce在这里同样好用解析失败的日期变成NaT事后可以用df[df[交易日期].isna()]快速定位问题行。我一直建议在清洗阶段就把日期统一转成标准datetime类型因为这个类型做时间区间筛选时表达能力比字符串强太多。# 筛选2024年1月之后的数据 mask df[交易日期] 2024-01-01 df.loc[mask]日期字符串之间的比较容易出各种边界问题datetime类型则永远按时间顺序来不会出现2024-9-1 2024-10-1这类字符串排序错误。3.3 字符串列里的隐形空格与脏值CSV文件里的字符串列尤其是用户填写的字段经常藏着肉眼看不见的首尾空格。这些空格会让groupby时同样的内容被分成两个组北京和北京 看起来差不多但分组统计时就是两个组。我踩过最惨的一次是把一个四万多行的地址表做去重统计跑完发现数量多了一倍排查半天才看到是空格搞的鬼。解决办法是清洗时统一调.str.strip():df[城市] df[城市].str.strip()如果需要同时清理左右空白、统一大小写可以连着写df[用户名] df[用户名].str.strip().str.lower()还有一类脏值是全角空格、不间断空格\xa0这些在网页抓取的数据里很常见。普通的.strip()对付不了需要用正则替换df[备注] df[备注].str.replace(r[\u3000\xa0], , regexTrue)处理完字符串列之后我习惯用.nunique()再确认一遍分组基数是否符合预期print(df[城市].nunique()) print(df[城市].value_counts().head())如果洗的不干净这里一眼就暴露问题。字符串清洗这步没有太多黑魔法核心就是把你能想到的所有空白形式全部干掉。4. 大文件与内存极限分块读取和降内存实战4.1 memory_usage告诉你的内存真相判断Pandas处理CSV时内存占多少不要靠猜用memory_usage看df.info(memory_usagedeep) df.memory_usage(deepTrue)一个容易忽略的事实同样一份CSV磁盘上可能只有80MB读进DataFrame后内存轻松涨到500MB甚至更多。原因是磁盘上的字符串是紧凑存储的而Pandas对object类型的列会把每个字符串作为独立Python对象来管理对象头开销极大。所以优化内存的第一步永远是看看到底哪一列在吃内存。我刚入行时处理过一个约2GB的CSV电脑配置一般第一次pd.read_csv直接把内存吃满整个进程被杀。后来用df.memory_usage(deepTrue)逐列看发现一列是用户详情的自由文本占了一半内存。把这一列单独剔除后剩下字段全部指定dtype内存降到不到原来的四分之一。4.2 chunksize分块处理与增量写回当文件大到一次读入就会撑爆内存的时候正确姿势是分块读取。read_csv支持chunksize参数返回一个TextFileReader迭代器可以逐块处理chunk_iter pd.read_csv(超大文件.csv, chunksize100000) results [] for chunk in chunk_iter: # 对每个块做清洗或聚合 summary chunk.groupby(类别)[金额].sum() results.append(summary) final_result pd.concat(results).groupby(level0).sum() print(final_result)分块的精髓在于块内处理块间合并。每个块独立做变换最后把块的中间结果汇总成一个final_result这样内存里永远只同时存在一个块和一个小得多的中间结果。如果分块处理的目标是输出另一个大CSV可以用to_csv的modea追加写配合每个块写出时去掉表头chunk_iter pd.read_csv(源数据.csv, chunksize100000) first True for chunk in chunk_iter: clean chunk.dropna(subset[关键列]) clean.to_csv(清洗后.csv, modea, indexFalse, headerfirst) first False这里headerfirst意味着只有第一块写出时带列名后面的块只追加数据。这个方法在内存受限的机器上处理几GB的CSV非常顺畅基本靠它救了我和我的老笔记本好几次。4.3 category类型在业务数据里的奇效category类型是pandas 2.x时代降内存效果最立竿见影的工具尤其适合那些只有少数几种取值但出现次数极多的列比如性别、省市区、订单状态、支付渠道。一个字符串列存了一万次待支付用object类型就要存一万个Python字符串对象转成category后底层只维护一个类似枚举的映射表每行数据只存一个整数编码。df[订单状态] df[订单状态].astype(category) df[省份] df[省份].astype(category)转换之后再df.info(memory_usagedeep)看内存往往能看到惊人的下降。我遇到过一个情况是某列从占内存25%降到只占不到2%。category类型还有另一层价值——原来乱序的字符串分组操作转成category之后groupby时的排序会和category的categories顺序一致这在业务排布、图表展示时反而能保持固定顺序不会乱跳。比如柱状图想按待支付、已支付、已退款的顺序展示提前给category指定categories顺序即可。from pandas.api.types import CategoricalDtype order CategoricalDtype(categories[待支付, 已支付, 已退款], orderedTrue) df[订单状态] df[订单状态].astype(order)之后value_counts()、groupby出来的顺序都会严格按这个顺序排列这在后面接可视化时能省去不少手工调序的麻烦。5. 从CSV写入到更优的存储方案5.1 to_csv的参数细节index、encoding与columns写完清洗好的数据输出CSV也一样有门道。to_csv里被我用到频率最高的三个参数是index、encoding和columns。indexFalse几乎是我写CSV时的肌肉记忆。默认情况下Pandas会把索引列也写出去生成一个叫Unnamed: 0的列别人拿到文件后总会问这第一列哪来的。自己用倒无所谓交出去的CSV一定带indexFalse。encodingutf-8-sig是给下游使用者减少困扰的输出设置。如果文件要交给同事用Excel打开UTF-8无BOM会导致Excel直接识别成乱码utf-8-sig因为有BOM标记Excel能正确识别为中文UTF-8打开就不再乱码了。一次性只输出部分列可以用columns参数传一个列名列表既省文件体积又避免把冗余字段交给下游df.to_csv(输出.csv, indexFalse, encodingutf-8-sig, columns[订单号, 金额, 交易日期])另外还有几个细节sep\t可以输出制表符分隔的样式方便一些特殊软件或人工贴到Excel中时自动分列float_format%.2f可以控制浮点数的精度避免写出0.30000000000000004这种难看的尾数。5.2 压缩与切分超过Excel上限怎么处理Excel的CSV打开上限大约是1048576行超过这个行数的数据文件对方用Excel打开时会被截断或者提示文件格式与扩展名不匹配。处理这类大文件基本思路是切分输出。按固定行数切分随手就能写chunk_iter pd.read_csv(源数据.csv, chunksize500000) for i, chunk in enumerate(chunk_iter): chunk.to_csv(f输出_part{i1}.csv, indexFalse, encodingutf-8-sig)或者如果DataFrame已经整体在内存里了直接用iloc按行切分n 500000 for i in range(0, len(df), n): df.iloc[i:in].to_csv(f输出_part{i//n1}.csv, indexFalse, encodingutf-8-sig)压缩方面to_csv支持在文件名后加.gz后缀Pandas会自动用gzip压缩写文件df.to_csv(输出.csv.gz, indexFalse, compressiongzip)压缩后的文件在传输时能省九成空间读取时Pandas也能推断压缩格式直接读pd.read_csv(输出.csv.gz)唯一要注意的是压缩文件无法用Excel直接打开但作为归档、交付给分析师用Pandas处理这个方案又稳又省。5.3 parquet和HDF5在什么场景才值得换CSV是交换格式不是存储格式。它跨软件通用、谁都能打开、Excel也能认这就是它的最大价值。但它也有明显短板没有类型信息所有的类型推断都要靠每次重新扫描文件内容文件体积大重复数据压缩率低不支持随机切片读取。当我需要反复使用同一份中间结果、且性能要求高时我会把数据转存成parquet格式df.to_parquet(处理结果.parquet, indexFalse)再次读取时类型信息完整保留pd.read_parquet的速度比同等体量CSV快好几倍而且文件通常小很多。同样一个五六百万行的数据集CSV可能两百多MBparquet压缩后三四十MB读取耗时也会从十几秒降到两三秒。另一种老牌选择是HDF5格式它的优势是在一个文件里可以存多份数据集、支持按键访问df.to_hdf(存储.h5, keyorder_table, modew) pd.read_hdf(存储.h5, keyorder_table)HDF5适合需要持久化的DataFrame写入读取速度也快但它对字符串支持的效率不如parquet而且依赖PyTables库装起来比pyarrow稍麻烦一点。我自己的选型规律是场景推荐格式原因给外部系统/同事交换数据CSV通用性最好任何人可打开中间结果要反复读parquet类型保留、体积小、读取快同一文件存多份结构化数据HDF5文件内按键访问按需读取长期归档大数据parquet.gz压缩率高且保留类型给Excel重新导入CSV utf-8-sig不会乱码行数低于104W才建议如果是规范CSV文件还可以顺手加上schema校验读进来后用assert检查列是否存在、dtypes是否符合预期。这步能帮我提前发现上游系统悄悄改了字段名或字段类型的问题。6. 实战链路从足球数据到GIS平台的属性表处理6.1 football-data.co.uk的赔率CSV解析思路经常玩数据建模的人可能见过football-data.co.uk这个站点上面有大量足球联赛的赛果、赔率历史数据直接以CSV形式提供下载。这类公开数据集的CSV解析和业务内网的表格逻辑一致但有几个细节非常典型。首先是列名里包含特殊字符比如AvgH、AvgD、AvgA这类缩写毫无可读性。读入之后我一般会重新命名df pd.read_csv(football_data.csv, encodingutf-8-sig) df.columns [比赛日期, 主队, 客队, 主队进球, 客队进球, ...]其次是像这类数据里经常会有部分行的某些字段为空尤其是赛季末尾或者停赛调整的行。处理时不要删整行而是确认哪一列对业务建模最关键再决定。比如建模用历史赔率做输入那么赔率列的空值肯定要剔除掉但如果只是统计胜负分布进球列没有缺失就保留全表。另一个常被忽略的点是日期格式这类网站通常使用DD/MM/YYYY而to_datetime默认按YYYY-MM-DD处理更容易出错所以一定要显式指定format或者先替换df[Date] pd.to_datetime(df[Date], format%d/%m/%Y, errorscoerce)这类公开CSV在Kaggle、GitHub等渠道经常有人二次加工读入前最好都看一眼前几行的列名类型避免走上先读再猜猜错再返工的弯路。6.2 ArcGIS/Pro这类GIS软件里的CSV导入与pandas配合Arcgis以及Arcgis Pro这类GIS软件里导入CSV也是热搜高位问题。很多人以为GIS软件对CSV的导入是一键操作实际上CSV里有经纬度列、坐标参考系不规范、属性列带中文编码问题时导入结果往往出现点位置全错或者属性表乱码根本原因是CSV本身没有携带坐标参考系信息软件只能盲猜。我自己的处理流程是先用Pandas把CSV清洗好再导进GIS软件。清洗的重点包括确认经纬度列名能被GIS识别。ArcGIS通常认X、Y或Longitude、Latitude这类固定字段名如果CSV里写的是经度、纬度可以先重命名。把经纬度列转换为统一的float64类型去掉°符号、方向缩写之类的干扰内容。明确坐标参考系是WGS84还是GCJ-02等这一步直接关系到点会不会偏到海里。Pandas处理不了坐标参考系但能保证进入GIS前的字段干净。ArcGIS Pro本身也内置了Python环境可以安装pandas。在Pro的Python环境中用conda装pandas时注意不要直接替换ArcGIS自带的环境应该新建克隆环境再装否则可能破坏ArcGIS的包依赖。回到CSV本身即便不导入GIS我们也可以用Pandas自带的聚合能力分析CSV中的空间属性比如按城市聚合统计、按经纬度网格分桶。这样处理底表时就能避开GIS软件里大文件拖拽卡顿的问题。6.3 数据可视化联动清洗后的CSV如何喂给matplotlibPandas处理完CSV最终往往要落到图上。pandas与matplotlib练习这个热搜词说明很多人卡在数据处理完却不知道如何画图。其实链路特别顺DataFrame可以直接作为matplotlib的数据源df.plot()是最快捷的入口。因为matplotlib本身不认CSV它认的是数组和DataFrame。所以整个流程天然地分成了Pandas管清洗、matplotlib管呈现两段。import matplotlib.pyplot as plt df[月度销售额] df.groupby(df[交易日期].dt.to_period(M))[金额].sum() df[月度销售额].plot(kindbar) plt.title(月度销售额趋势) plt.show()一个很常见的坑是中文显示matplotlib默认字体不含中文图表上的中文标题会变成方框。解决方式建议先加载支持中文的字体plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] # 黑体或微软雅黑 plt.rcParams[axes.unicode_minus] False # 防止负号显示异常画图之前还有一个值得做的动作确保x轴的类型正确。时间序列聚合的索引最好转成时间格式否则x轴的顺序和刻度标签会乱。用pd.to_datetime统一过一遍再画比直接拿字符串日期画要顺得多。可视化这一步不需要画多复杂柱状图、折线图、直方图基本就能覆盖日常汇报的八成需求。Pandas的绘图本质上是matplotlib的一个封装底层对象可以混用后续要精细化调整直接调用plt的API即可。7. 最后聊几句长期实战里沉淀下来的习惯处理CSV到现在我最深的体会是CSV文件的坑很少来自pandas本身多数来自对文件结构、编码、类型和存储目标的理解不够。一个真正高效的处理流程往往不是写一段漂亮的链式代码而是在读入之前已经确认了编码、分隔符、列类型、目标存储格式这四个关键变量。这也是我写这篇文章的初衷——从搜索引擎的热搜词能看出来大家最困惑的其实就集中在安装源报错、读入乱码、类型转换失败、大文件内存不足、GIS或可视化联动这五个方向。每个方向我都给出了实际跑过的解决方案代码也都在我自己的日常脚本里重复用了很久。读者如果在公司或者自己的项目里遇到相同报错直接照抄对应的段落基本就能解决问题。如果后面有精力我打算在此基础上再写一篇关于Pandas处理千万级CSV时的并行化方案pandas.read_csv配合dask或modin的分片策略和普通分块不太一样。这次先到这里有任何想法欢迎在评论区交流。
返回列表