上周三帮朋友看他店里的外卖订单,CSV一共1,043,817行、18列,文件236MB,他MacBook Air M1 8GB内存,pandas读到一半直接MemoryError。我第一反应是数据太大,后来把dtypes打出来才发现两列手机号被识别成object,内存从大概400MB涨到1.6GB,问题不在行数,在类型。 我用的环境是Python 3.11.8、pandas 2.2.2、macOS 14.5,不是云服务器,就是普通笔记本。这个测试我做了三次取中位数,峰值内存用psutil和memory_profiler粗看,不是严谨benchmark,别拿去写论文。 如果你也卡在这,先别急着换库,第一步是修dtype:pd.read_csv('orders.csv', usecols=['order_id','city','amount','phone','created_at'], dtype={'city':'category','amount':'float32','phone':'string'}, chunksize=200000)。我只读6列后,pandas耗时8.7秒,峰值内存约310MB;不指定dtype、全量读是21.4秒,峰值1.6GB。 还有一个坑:category列做groupby要加observed=True,不然空组合会冒出来,结果行数对不上。
第二步我试了DuckDB,版本0.10.2,直接在Python里跑SQL:SELECT city, count(*), avg(amount) FROM read_csv_auto('orders.csv') GROUP BY city ORDER BY 2 DESC LIMIT 20。读CSV加聚合2.9秒,峰值内存约540MB,代码没超过5行。 中文列名和日期它也会猜错,我加了header=true、sample_size=-1、dateformat='%Y-%m-%d'才稳。Polars版本0.20.31,用pl.scan_csv('orders.csv', infer_schema_length=10000).filter(pl.col('amount')>0).group_by('city').agg(pl.len(),pl.col('amount').mean()).collect(),耗时3.6秒,峰值约620MB。 Polars默认infer_schema_length是100行,前100行有空值就会把数值列猜成字符串,我第一次就被一个null列卡了40分钟,挺烦的。SQLite 3.45.1是先导入再建索引,导入后文件412MB,聚合7.8秒,建city索引后3.1秒,峰值约210MB。 下面这张表是同一文件、同一台M1 8GB、三次中位数的结果:pandas 2.2.2优化dtype后9.4秒/310MB/约100MB安装体积;DuckDB 0.10.2是2.9秒/540MB/约40MB;Polars 0.20.31是3.6秒/620MB/约70MB;SQLite 3.45.1建索引后3.1秒/210MB/约1.5MB。
我的独立观点可能跟很多推荐帖不一样:大多数人第一步不该装Polars,而是先上DuckDB。原因不是跑分,是往返次数。你改一个过滤条件,DuckDB只改SQL,不用重跑整个notebook;Polars写lazy很优雅,但API迁移和空值处理会让你在晚上十点怀疑人生。 DuckDB也不是没毛病,read_csv_auto对中文日期和手机号长数字会自作聪明,最好显式写columns或先转Parquet。SQLite适合多人共享小数据集,比如门店维度表,1.5MB安装体积、单文件拷来拷去,比让同事装Python环境靠谱。pandas不是慢,是默认dtype和object列太能吃内存,数据低于500万行、内存16GB以上,修完类型还能用。
选型我一般按这个步骤走:先看CSV大小和内存,小于200MB且内存大于16GB,先pandas修dtype;大于500MB或要反复SQL查,直接DuckDB;生产ETL要流式、多核,再考虑Polars;需要多人共享、离线小表,SQLite或Parquet加DuckDB。 检查脚本很简单:import psutil; print(psutil.Process().memory_info().rss/1024/1024),跑前跑后各打一次。别只看耗时,峰值内存爆了,快3秒也没用。我现在默认先开DuckDB,再决定要不要写pandas,这个顺序帮我少加了很多班。