如何提升Python代码内存效率?处理400万行数据时遭遇内存错误
嘿,针对你400万行数据集的内存占用问题,我来分享几个实打实的优化思路,不管是数据加载还是日期拆分环节都能帮你省不少内存~
一、核心日期拆分逻辑的优化:绕开字符串转换的冗余开销
你现在用strftime转成字符串再转类型的操作,其实是绕了大弯——pandas的dt属性本身就提供了直接提取日期组件的方法,不仅更快,还能大幅降低内存占用:
优化后的代码:
# 先定义星期映射(对应你的分类顺序) weekday_map = {0: 'Mon', 1: 'Tue', 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun'} # 处理start_time相关列 trip_df['start_date'] = trip_df['start_time'].dt.floor('D') # 直接截断到日期,比strftime再转datetime高效N倍 trip_df['start_year'] = trip_df['start_time'].dt.year.astype('int16') # 年份范围小,用int16足够 trip_df['start_month'] = trip_df['start_time'].dt.month.astype('uint8') # 月份1-12,uint8完全覆盖 trip_df['start_hour'] = trip_df['start_time'].dt.hour.astype('uint8') # 小时0-23,同样用uint8 trip_df['start_weekday'] = trip_df['start_time'].dt.weekday.map(weekday_map).astype(shrtwkday) # 处理end_time相关列,逻辑完全一致 trip_df['end_date'] = trip_df['end_time'].dt.floor('D') trip_df['end_year'] = trip_df['end_time'].dt.year.astype('int16') trip_df['end_month'] = trip_df['end_time'].dt.month.astype('uint8') trip_df['end_hour'] = trip_df['end_time'].dt.hour.astype('uint8') trip_df['end_weekday'] = trip_df['end_time'].dt.weekday.map(weekday_map).astype(shrtwkday)
为什么这样优化?
- 避免字符串转换:
strftime会把日期转成字符串,再转int的过程不仅慢,还会产生临时字符串对象占用内存,直接用dt.year/dt.month提取数值是原生操作,效率高很多。 - 压缩数值类型:默认的
int是int64(8字节),而年份用int16(2字节)、月份/小时用uint8(1字节)就完全足够,400万行的话,单列就能省6-7倍的内存。 - 直接生成日期列:
dt.floor('D')直接把datetime截断到日期,不用先转字符串再转datetime,一步到位。
二、数据加载阶段的优化:从源头减少内存占用
你的加载代码已经做了usecols和dtypes的设置,这很好,但还有几个可以再抠细节的地方:
优化后的加载代码:
import os import pandas as pd from pandas.api.types import CategoricalDtype shrtwkday = CategoricalDtype(categories=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], ordered=True) use_cols = ['duration_sec','start_time','end_time','start_station_id','start_station_name','start_station_latitude', 'start_station_longitude','end_station_id','end_station_name','end_station_latitude', 'end_station_longitude','bike_id','user_type'] # 进一步压缩dtypes,用最小够用的类型 dtypes = { 'duration_sec': 'int32', # 骑行时长一般不会超过2^31-1,用int32比int64省一半内存 'start_station_id': pd.Int64Dtype(), 'start_station_name': 'str', 'start_station_latitude': 'float32', # 经纬度用float32精度完全足够(误差在厘米级) 'start_station_longitude': 'float32', 'end_station_id': pd.Int64Dtype(), 'end_station_name': 'str', 'end_station_latitude': 'float32', 'end_station_longitude': 'float32', 'bike_id': 'int32', 'user_type': CategoricalDtype() # 把user_type直接设为类别型,比字符串省大量内存 } parse_dates = ['start_time','end_time'] # 用生成器表达式代替列表append,减少中间列表的内存占用 trip_df = pd.concat( (pd.read_csv( os.path.join("./source_files/", file), compression='zip', delimiter=';', usecols=use_cols, dtype=dtypes, header=0, parse_dates=parse_dates, na_values=['', ' '], infer_datetime_format=True # 自动推断日期格式,加速datetime解析 ) for file in os.listdir("./source_files/")), axis=0, ignore_index=True )
关键优化点:
- 压缩浮点类型:经纬度用
float32代替float64,内存直接减半,而且完全不影响实际使用。 - 类别型列提前设置:
user_type这类只有少数类别的列,直接设为CategoricalDtype,比字符串类型省70%以上的内存。 - 生成器代替列表:用
(pd.read_csv(...) for file in files)生成器代替li.append(df),不需要把所有子DataFrame都存在列表里再concat,减少中间内存占用。 - 加速日期解析:加
infer_datetime_format=True让pandas自动推断日期格式,比默认解析快很多。
三、额外的内存小技巧
你提到的用10%样本测试、复用原DataFrame的思路非常棒,再补充两个小技巧:
- 定期清理内存:在处理完大步骤后,可以用
del temp_obj然后调用gc.collect()手动释放内存(尤其是Jupyter环境,内存回收有时候不及时)。 - 分块处理(可选):如果全量加载还是压力大,可以用
pd.read_csv的chunksize参数分块加载,每处理一块就合并到结果中,避免一次性加载全量数据。
内容的提问来源于stack exchange,提问作者J. Brown
相关产品推荐
相关产品推荐

