为何pd.DataFrame内存占用低于pd.Series?大数据处理内存疑问
问题场景
开发大数据处理程序时,为降低DataFrame内存占用,测试读取大型sas7bdat文件单列数据的两种方式(构建pd.Series与模拟DataFrame方式加载),发现内存指标出现矛盾。
测试结果
方式1:直接构建Series
以Series加载 0 b'Richard Wilkins' 1 b'Richard Wilkins' 2 b'Richard Wilkins' 3 b'Richard Wilkins' 4 b'Richard Wilkins' ... 16539995 b'Rebecca Davis' 16539996 b'Rebecca Davis' 16539997 b'Rebecca Davis' 16539998 b'Rebecca Davis' 16539999 b'Rebecca Davis' 长度: 16540000, 数据类型: object Series数据内存占用: 264640000 Bytes -> 调用pd.Series.memory_usage() Series对象内存占用: 1024200016 Bytes -> 调用sys.getsizeof(Series) 进程物理内存占用: 2740.828125 MB,耗时: 45.28001594543457秒 -> 调用psutil.Process().memory_info().rss
方式2:通过列表收集后concat(模拟DataFrame方式)
以pd.concat构建 0 b'Richard Wilkins' 1 b'Richard Wilkins' 2 b'Richard Wilkins' 3 b'Richard Wilkins' 4 b'Richard Wilkins' ... 16539995 b'Rebecca Davis' 16539996 b'Rebecca Davis' 16539997 b'Rebecca Davis' 16539998 b'Rebecca Davis' 16539999 b'Rebecca Davis' 名称: name, 长度: 16540000, 数据类型: object 结果数据内存占用: 132320128 Bytes 结果对象内存占用: 891880144 Bytes 进程物理内存占用: 3775.93359375 MB,耗时: 41.85226058959961秒
矛盾点与疑问
测试结果显示:
- Series的数据内存为252MB,而第二种方式的数据内存为126MB(仅前者一半);
- Series对象内存为976MB,第二种方式的对象内存为850MB(后者更低);
- 但进程物理内存显示第二种方式占用更高。
请问这是什么原因?开发中应信任哪种内存指标?
测试代码
sas_read_column_series.py
# sas_read_column_series.py import os import time from sys import getsizeof import pandas as pd import psutil file_path = os.path.dirname(os.path.realpath(__file__)) + os.sep df_iter = pd.read_sas( file_path + "test.sas7bdat", chunksize=1000000) # 列名:idx, name, age, gender, country, birthdate, email ps = psutil.Process() start_time = time.time() result_series = pd.Series() while True: df_block = df_iter.read() print( f"当前内存占用: { ps.memory_info().rss / 1024 / 1024 } MB,耗时: { time.time() - start_time } 秒") if len(df_block) == 0: break result_series = pd.concat([result_series, df_block["name"]]) end_time = time.time() print(f"结果Series: { result_series }") print( f"Series数据内存占用: { result_series.memory_usage() } Bytes") print( f"Series对象内存占用: { getsizeof(result_series) } Bytes") print( f"总内存占用: { ps.memory_info().rss / 1024 / 1024 } MB,总耗时: { end_time - start_time } 秒")
sas_read_column_dataframe.py
# sas_read_column_dataframe.py import os import time from sys import getsizeof import pandas as pd import psutil file_path = os.path.dirname(os.path.realpath(__file__)) + os.sep df_iter = pd.read_sas( file_path + "test.sas7bdat", chunksize=1000000) # 列名:idx, name, age, gender, country, birthdate, email ps = psutil.Process() start_time = time.time() result_df = [] while True: df_block = df_iter.read() print( f"当前内存占用: { ps.memory_info().rss / 1024 / 1024 } MB,耗时: { time.time() - start_time } 秒") if len(df_block) == 0: break result_df.append(df_block["name"]) result_df = pd.concat(result_df, axis=0) end_time = time.time() print(f"结果DF: { result_df }") print( f"DF数据内存占用: { result_df.memory_usage() } Bytes") print( f"DF对象内存占用: { getsizeof(result_df) } Bytes") print( f"总内存占用: { ps.memory_info().rss / 1024 / 1024 } MB,总耗时: { end_time - start_time } 秒")
问题解析与建议
内存指标差异原因
memory_usage()
该方法仅统计pandas对象存储数据的内存。对于Series,默认index=True,会包含索引的内存开销;第二种方式通过列表收集块后一次性concat,索引被统一生成,减少了重复开销,因此数据内存占比更低。另外,对于object类型,默认memory_usage()仅统计指针大小,若要统计实际字符串内存,需添加deep=True参数。sys.getsizeof()
该方法统计Python对象的总内存,包括内部结构、元数据等额外开销。pandas的Series是复杂对象,两种方式的差异源于对象内部结构的优化程度:一次性concat生成的对象结构更紧凑,因此整体内存开销更低。psutil的rss
该指标是进程的常驻物理内存,包含临时变量、缓存、未回收的中间对象、Python解释器内存等所有进程占用的资源。第二种方式循环中所有块的Series都被存储在列表中,直到最后concat才可能被释放,因此进程内存占用更高;而第一种方式每次concat后旧Series会被覆盖,引用计数降低后可被垃圾回收,内存开销更小。
开发中指标选择
- 优化数据存储:优先使用
memory_usage(deep=True),它能准确统计数据本身(包括object类型的实际内容)的内存占用,是优化pandas内存的核心指标。 - 了解对象整体开销:
sys.getsizeof()可用于查看Python对象的总内存,但仅作为辅助参考,不能精准反映数据存储的实际开销。 - 监控程序整体内存:psutil的rss适合监控整个程序的内存消耗,用于排查内存泄漏或评估程序的整体资源占用,但无法单独反映单个pandas对象的内存。
另外需注意:第二个脚本中最终result_df实际是Series而非DataFrame,变量命名可能造成混淆,若要测试真正的单列DataFrame,应将df_block["name"]改为df_block[["name"]](双层方括号)。
内容的提问来源于stack exchange,提问作者Eveheeero

