0Pricing
Pandas & NumPy Academy · 课时

用于减少内存的高效数据类型

将数值列降级转换为 int32/float32,并将字符串列转换为 Categorical,使 DataFrame 内存最多减少 70%。

用于减少内存的高效数据类型 是 CoddyKit 上的免费 Pandas & NumPy Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Pandas & NumPy Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Pandas & NumPy Academy 课程共包含 4 节课。

数据类型为何会影响性能

在 Pandas 中,每一列都有一个 dtype(数据类型),它决定值在内存中的存储方式以及操作的运行速度。Pandas 在加载数据时默认使用较宽的数据类型:int64(每个值占 8 字节)、float64(8 字节)和 object(大小可变,字符串通常每个占 50 到 200 字节)。对于数百万行数据,选择更小的数据类型可以将内存占用降低 50% 到 80%,并且由于能够更好地利用缓存,使操作速度提升 2 到 5 倍。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'id': np.arange(1000000, dtype='int64'),
    'score': np.random.uniform(0, 100, 1000000).astype('float64'),
    'category': np.random.choice(['A','B','C','D'], 1000000)
})

mem = df.memory_usage(deep=True)
print('Memory per column:')
print(mem)
print(f'Total: {mem.sum() / 1e6:.1f} MB')

降低整数列的数据类型位宽

如果某列中的整数值适合更小的取值范围,可以将其从 int64 转换为更小的整数类型。pd.to_numeric(series, downcast='integer') 会自动选择能够容纳所有值的最小整数类型:int8(–128 到 127,1 字节)、int16(–32768 到 32767,2 字节)、int32(±20 亿,4 字节);如果有需要,则保持为 int64。int8 列的内存占用比 int64 少 8 倍。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'age': np.random.randint(18, 90, 500000).astype('int64'),
    'score': np.random.randint(0, 100, 500000).astype('int64'),
    'large_id': np.random.randint(0, 2**31, 500000).astype('int64')
})

# Downcast integers
for col in df.select_dtypes('int64').columns:
    df[col] = pd.to_numeric(df[col], downcast='integer')

print('Dtypes after downcasting:')
print(df.dtypes)
print(f'\nMemory: {df.memory_usage(deep=True).sum()/1e6:.2f} MB')

降低浮点列的数据类型位宽

pd.to_numeric(series, downcast='float') 会在精度允许的情况下将 float64 转换为 float32(每个值占 4 字节,而不是 8 字节)。float32 的精度约为 7 位小数,而 float64 为 15 位。对于大多数分析任务(百分比、价格、归一化特征),float32 的精度已经足够。对于需要高精度的科学计算,则应继续使用 float64。将浮点精度降低一半,可以使内存占用减半并改善缓存性能。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'lat': np.random.uniform(-90, 90, 1000000),
                   'lon': np.random.uniform(-180, 180, 1000000),
                   'temp': np.random.uniform(-40, 50, 1000000)})

print('Before:', df.dtypes.unique())
print(f'Memory: {df.memory_usage(deep=True).sum()/1e6:.1f} MB')

for col in df.select_dtypes('float64').columns:
    df[col] = pd.to_numeric(df[col], downcast='float')

print('After:', df.dtypes.unique())
print(f'Memory: {df.memory_usage(deep=True).sum()/1e6:.1f} MB')

Categorical 数据类型

对于包含大量重复值的字符串列,Categorical dtype 是最能节省内存的数据类型。Pandas 不再将同一个字符串(例如 'Electronics')存储数千次,而是存储一个包含唯一值的字典,以及每一行对应的紧凑整数代码。包含 100 万行且只有 50 个唯一类别的列,内存占用可从约 50 MB(object dtype)降至约 1 MB(Categorical),减少 50 倍。

import pandas as pd
import numpy as np

np.random.seed(0)
categories = ['Electronics', 'Clothing', 'Books', 'Food', 'Furniture',
              'Sports', 'Toys', 'Health', 'Garden', 'Automotive']

df = pd.DataFrame({'product_cat': np.random.choice(categories, 1000000)})

mem_before = df.memory_usage(deep=True).sum()
df['product_cat'] = df['product_cat'].astype('category')
mem_after = df.memory_usage(deep=True).sum()

print(f'Object dtype: {mem_before/1e6:.1f} MB')
print(f'Categorical:  {mem_after/1e6:.2f} MB')
print(f'Reduction: {mem_before/mem_after:.0f}x')

Categorical 何时可以节省内存

只有当一列的唯一值远少于总行数时,Categorical dtype 才能节省内存。当唯一值与总行数的比率(基数)超过约 50% 时,就接近内存使用的平衡点:如果每一行都是唯一字符串,Categorical 实际上会比 object dtype 占用更多内存,因为它同时存储代码数组和类别数组。转换前务必检查 df['col'].nunique() / len(df)——比率低于 0.5(理想情况下低于 0.05)意味着使用 Categorical 会有所帮助。

import pandas as pd
import numpy as np

def memory_gain(df, col):
    n = len(df)
    n_unique = df[col].nunique()
    ratio = n_unique / n
    mem_obj = df[col].memory_usage(deep=True)
    df2 = df.copy()
    df2[col] = df2[col].astype('category')
    mem_cat = df2[col].memory_usage(deep=True)
    print(f'{col}: {n_unique} unique / {n} total (ratio={ratio:.3f})')
    print(f'  Object: {mem_obj/1e6:.2f} MB → Categorical: {mem_cat/1e6:.2f} MB')
    print(f'  {"Saves" if mem_cat < mem_obj else "Wastes"} {abs(mem_obj-mem_cat)/1e6:.2f} MB')

np.random.seed(0)
df = pd.DataFrame({
    'low_card': np.random.choice(['A','B','C'], 500000),   # low cardinality
    'high_card': [f'id_{i}' for i in range(500000)]         # high cardinality
})
memory_gain(df, 'low_card')
memory_gain(df, 'high_card')

Categorical 可提升 GroupBy 性能

除了节省内存之外,Categorical dtype 还可以加快 groupby 操作,因为 Pandas 可以直接使用整数代码,而不必通过计算字符串哈希来查找组边界。对于包含数百万行、并按低基数字符串列(例如地区、产品类别或状态)分组的 DataFrame,在 groupby 之前将这些列转换为 Categorical,速度可能提升 2 到 5 倍。

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'region': np.random.choice(['North','South','East','West'], 1000000),
    'sales': np.random.randn(1000000)
})

# Object dtype groupby
t1 = timeit.timeit(lambda: df.groupby('region')['sales'].mean(), number=50)

# Categorical dtype groupby
df2 = df.copy()
df2['region'] = df2['region'].astype('category')
t2 = timeit.timeit(lambda: df2.groupby('region')['sales'].mean(), number=50)

print(f'Object dtype groupby: {t1/50*1000:.2f} ms')
print(f'Categorical groupby:  {t2/50*1000:.2f} ms')
print(f'Speedup: {t1/t2:.1f}x')

对标记列使用 boolean dtype

二值列(True/False、0/1、yes/no)通常存储为 object 或 int64。将它们转换为 bool dtype 后,每个值只占 1 字节(而 int64 占 8 字节,字符串 'yes'/'no' 占 50 多字节)。确认列中只包含 0/1 或 True/False 值后,请使用 astype(bool)。布尔列还可以实现更快的筛选,因为 Pandas 能够在内部使用按位操作。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'is_premium': np.random.choice([0, 1], 1000000).astype('int64'),
    'has_discount': np.random.choice(['yes', 'no'], 1000000)
})

print('Before:')
print(df.dtypes)
print(f'Memory: {df.memory_usage(deep=True).sum()/1e6:.1f} MB')

df['is_premium'] = df['is_premium'].astype(bool)
df['has_discount'] = df['has_discount'].map({'yes': True, 'no': False}).astype(bool)

print('\nAfter:')
print(df.dtypes)
print(f'Memory: {df.memory_usage(deep=True).sum()/1e6:.2f} MB')

自动化类型优化

编写一个可复用的 optimise_dtypes(df) 函数,自动应用所有优化:将整数向下转换、将浮点数向下转换、将低基数对象转换为 Categorical,以及将 0/1 整数转换为布尔值。在加载数据时运行此函数,从一开始就尽量减少内存占用。这样,团队中每位加载同一数据集的成员都会获得优化后的版本,无需记住手动应用每个步骤。

import pandas as pd
import numpy as np

def optimise_dtypes(df, cat_threshold=0.5):
    '''Reduce DataFrame memory by choosing smaller dtypes.'''
    for col in df.columns:
        col_type = df[col].dtype
        if col_type == 'int64':
            df[col] = pd.to_numeric(df[col], downcast='integer')
        elif col_type == 'float64':
            df[col] = pd.to_numeric(df[col], downcast='float')
        elif col_type == 'object':
            cardinality = df[col].nunique() / len(df)
            if cardinality < cat_threshold:
                df[col] = df[col].astype('category')
    return df

# Test
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randint(0,100,500000),
                   'b': np.random.randn(500000),
                   'c': np.random.choice(['X','Y','Z'],500000)})

before = df.memory_usage(deep=True).sum()
df = optimise_dtypes(df)
after = df.memory_usage(deep=True).sum()
print(f'Before: {before/1e6:.2f} MB → After: {after/1e6:.2f} MB ({before/after:.1f}x reduction)')

非负数据的无符号整数类型

当某一列只包含非负整数(例如 ID、计数或数量)时,请使用无符号整数类型:uint8(0–255)、uint16(0–65535)、uint32(0–40 亿)或 uint64。无符号类型与对应的有符号类型占用相同的字节数,但在正数范围内最多可以存储两倍大的值。例如,范围为 0 到 60,000 的产品 ID 可以使用 uint16(2 字节),而不是 int32(4 字节)。确认该列没有负值后,请使用 astype('uint16')。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'product_id': np.random.randint(0, 50000, 1000000).astype('int64'),
    'quantity': np.random.randint(0, 255, 1000000).astype('int64')
})

print('Before (int64):', df.memory_usage(deep=True).sum() / 1e6, 'MB')

# product_id fits in uint16 (0-65535)
df['product_id'] = df['product_id'].astype('uint16')
# quantity fits in uint8 (0-255)
df['quantity'] = df['quantity'].astype('uint8')

print('After (uint16+uint8):', df.memory_usage(deep=True).sum() / 1e6, 'MB')
print('\nDtypes:', df.dtypes.to_dict())

检查每一步优化后的内存占用

一次应用一个优化,并在每一步之后检查内存占用。这样可以准确了解每种技术的贡献。一个典型的大型 DataFrame 可能会从 2 GB(全部使用默认数据类型)降至 600 MB(整数向下转换),再降至 300 MB(浮点数向下转换),最后降至 200 MB(字符串列使用 Categorical)。记录这一分解过程,有助于向那些在代码库中看到不熟悉数据类型的团队成员说明增加这些复杂性的理由。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'user_id': np.random.randint(0, 99999, 1000000).astype('int64'),
    'age': np.random.randint(18, 80, 1000000).astype('int64'),
    'revenue': np.random.uniform(0, 5000, 1000000).astype('float64'),
    'country': np.random.choice(['US','UK','DE','FR','JP'], 1000000),
    'tier': np.random.choice(['free','basic','pro','enterprise'], 1000000)
})

def mem_mb(df):
    return df.memory_usage(deep=True).sum() / 1e6

print(f'Start: {mem_mb(df):.1f} MB')

for c in ['user_id','age']:
    df[c] = pd.to_numeric(df[c], downcast='integer')
print(f'After int downcast: {mem_mb(df):.1f} MB')

df['revenue'] = pd.to_numeric(df['revenue'], downcast='float')
print(f'After float downcast: {mem_mb(df):.1f} MB')

for c in ['country','tier']:
    df[c] = df[c].astype('category')
print(f'After Categorical: {mem_mb(df):.1f} MB')

保存并重新加载优化后的类型

如果保存为 CSV,优化后的数据类型会丢失(因为所有内容都会被转换回文本)。若要保留数据类型,请使用 Parquet 格式,并调用 df.to_parquet('file.parquet') —— Parquet 可以保留 int32、float32 和 Categorical 类型。使用 pd.read_parquet 重新加载时,DataFrame 仍然具有相同的高效内存类型,无需再次运行优化函数。对于大型文件,Parquet 的加载速度也明显快于 CSV。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'age': np.random.randint(18, 80, 100000).astype('int8'),   # already optimised
    'score': np.random.randn(100000).astype('float32'),
    'tier': pd.Categorical(np.random.choice(['free','pro'], 100000))
})
print('Dtypes:', df.dtypes.to_dict())

# Save to Parquet (preserves dtypes)
df.to_parquet('/tmp/optimised.parquet', index=False)

# Reload — dtypes preserved!
df_loaded = pd.read_parquet('/tmp/optimised.parquet')
print('\nLoaded dtypes:', df_loaded.dtypes.to_dict())
print(f'Memory: {df_loaded.memory_usage(deep=True).sum()/1e6:.2f} MB')

快速检查

测试您对本课所学高效内存数据类型的理解。

课程回顾

在本课中,您学到了:pd.to_numeric(downcast='integer') 可将整数列的存储空间从 8 字节降至 1–4 字节;pd.to_numeric(downcast='float') 可将浮点数的内存占用减半;Categorical dtype 可将低基数字符串列的占用减少最多 50 倍,同时加快 groupby;Parquet format 可在文件保存和加载过程中保留优化后的数据类型。接下来,我们将探索分块读取,即处理超出可用 RAM 的文件。

常见问题解答

「用于减少内存的高效数据类型」课时是免费的吗?

是的 — 「用于减少内存的高效数据类型」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Pandas & NumPy Academy 课程的其余内容,请升级到 CoddyKit PRO。 Pandas & NumPy Academy 课程共包含 4 节课。

「用于减少内存的高效数据类型」这节课中我会学到什么?

将数值列降级转换为 int32/float32,并将字符串列转换为 Categorical,使 DataFrame 内存最多减少 70%。 你通过在浏览器中直接运行的动手代码来练习 Pandas & NumPy Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Pandas & NumPy Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Pandas & NumPy Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「用于减少内存的高效数据类型」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Pandas & NumPy Academy 课中编写并运行代码吗?

能。每节 Pandas & NumPy Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 timeit 和 memory_profiler 进行性能分析
  2. 避免使用 iterrows 和 Python 循环
  3. 用于减少内存的高效数据类型
  4. 大文件的分块读取
← 返回 Pandas & NumPy Academy