0Pricing
Pandas & NumPy Academy · レッスン

メモリ削減のための効率的なデータ型

数値列をint32/float32にダウンキャストし、文字列列をCategoricalに変換して、DataFrameのメモリ使用量を最大70%削減します。

「メモリ削減のための効率的なデータ型」はCoddyKit上の無料Pandas & NumPy Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応の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分の1です。

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に変換します(8バイトから4バイト)。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は、値が繰り返される文字列列に対して、メモリを最も大きく削減できる方法です。同じ文字列(例: 'Electronics')を何千回も保存する代わりに、Pandasは一意な値の辞書と、各行に対応するコンパクトな整数コードを保存します。100万行で一意なカテゴリが50個だけの列では、メモリ使用量が約50 MB(object dtype)から約1 MB(Categorical)になり、50分の1に削減できます。

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) 関数を作成します。整数のダウンキャスト、浮動小数点数のダウンキャスト、カーディナリティの低い object 列の Categorical への変換、0/1 の整数の bool への変換を行います。メモリ使用量を最初から最小限に抑えるため、データの読み込み時にこの関数を実行します。これにより、各手順を手動で適用することを覚えていなくても、同じデータセットを読み込むチームメンバー全員が最適化済みのデータを取得できます。

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 があります。符号なし整数型は符号付き整数型と同じバイト数でありながら、正の範囲では最大で2倍の値を格納できます。たとえば、0~60,000の商品 IDは、4バイトの int32 ではなく、2バイトの uint16 に収まります。列に負の値がないことを確認したうえで、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())

各最適化手順後のメモリ使用量を確認する

最適化を一度に1つずつ適用し、各手順の後にメモリ使用量を確認します。これにより、それぞれの手法がどれだけ効果をもたらしたかを正確に把握できます。一般的な大規模 DataFrame では、すべてデフォルトの dtype で2 GBだったものが、整数のダウンキャストで600 MB、浮動小数点数のダウンキャストで300 MB、文字列列を Categorical にすることで200 MBになることがあります。この内訳を文書化しておくと、コードベース内の見慣れない dtype に戸惑うチームメンバーに、追加の複雑さが必要な理由を説明できます。

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 に保存すると、すべてのデータがテキストに戻されるため、最適化した dtype は失われます。dtype を保持するには、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分の1に削減しながら groupby も高速化できること、そしてParquet 形式によってファイルの保存と読み込みの間でも最適化した dtype を保持できることを学びました。次は、利用可能な RAM を超えるファイルを処理する、チャンク単位の読み込みについて学びます。

よくある質問

「メモリ削減のための効率的なデータ型」レッスンは無料ですか?

はい。「メモリ削減のための効率的なデータ型」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Pandas & NumPy Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Pandas & NumPy Academyコースには全4レッスンが含まれています。

「メモリ削減のための効率的なデータ型」で何を学びますか?

数値列をint32/float32にダウンキャストし、文字列列をCategoricalに変換して、DataFrameのメモリ使用量を最大70%削減します。 ブラウザで直接実行するハンズオンコードでPandas & NumPy Academyを演習し、24時間対応の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に戻る