Pandas & NumPy Academy · レッスン

ソート済みインデックスのパフォーマンス上の利点

sort_index()でMultiIndexをソートし、timeitでスライスのパフォーマンスを測定して、is_monotonic_increasingをガードとして使用します。

レッスン 4/413 ステップ

「ソート済みインデックスのパフォーマンス上の利点」はCoddyKit上の無料Pandas & NumPy Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPandas & NumPy Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Pandas & NumPy Academyコースには全4レッスンが含まれています。

インデックスのソートがパフォーマンスに重要な理由

ソート済みのインデックスを使うと、ラベル範囲を検索する際に、全件を線形走査する方法(O(n))ではなく二分探索(O(log n))を Pandas が利用できます。100万行の DataFrame では、二分探索なら約20回の比較で対象範囲を見つけられるのに対し、線形走査では最大100万回の比較が必要です。そのため、ソートされていない MultiIndex と比べて、ソート済みの MultiIndex に対するスライス操作は桁違いに高速になります。数百万行を処理する本番パイプラインでは、この差が非常に重要になります。

import pandas as pd
import numpy as np

np.random.seed(42)
# Create a large DataFrame with a MultiIndex
countries = ['DE', 'UK', 'USA', 'FR', 'JP']
dates = pd.date_range('2020-01-01', periods=200)
mi = pd.MultiIndex.from_product([countries, dates], names=['country', 'date'])
df = pd.DataFrame({'value': np.random.randn(len(mi))}, index=mi)

print(f'DataFrame shape: {df.shape}')
print(f'Index is sorted: {df.index.is_monotonic_increasing}')

インデックスがソート済みか確認する

df.index.is_monotonic_increasing を使用すると、インデックスが昇順にソートされているか確認できます。戻り値はブール値です。MultiIndex の場合、Pandas はすべてのレベルを対象に辞書順でソートされているか確認します。MultiIndex に対して .loc[start:end] を使ったスライス操作を行う前には、必ず確認してください。ソートされていないインデックスでは、Pandas のバージョンによって UnsortedIndexError が発生するか、誤った結果が黙って返される場合があります。

import pandas as pd

# Sorted MultiIndex
tuples_sorted = [('A', 1), ('A', 2), ('B', 1), ('B', 2)]
mi_sorted = pd.MultiIndex.from_tuples(tuples_sorted)
df_sorted = pd.DataFrame({'v': [10, 20, 30, 40]}, index=mi_sorted)

# Unsorted MultiIndex
tuples_unsorted = [('B', 2), ('A', 1), ('B', 1), ('A', 2)]
mi_unsorted = pd.MultiIndex.from_tuples(tuples_unsorted)
df_unsorted = pd.DataFrame({'v': [10, 20, 30, 40]}, index=mi_unsorted)

print('Sorted index is_monotonic_increasing:', df_sorted.index.is_monotonic_increasing)
print('Unsorted index is_monotonic_increasing:', df_unsorted.index.is_monotonic_increasing)

sort_index() によるソート

df.sort_index() は、インデックスラベルの昇順に行をソートした新しい DataFrame を返します。降順にするには ascending=False を使用します。MultiIndex の場合、ソートは辞書順で行われます。まず最外部のレベルでソートし、次に各外部グループ内で内部レベルをソートします。pd.concat、フィルタリング、新しい行の追加など、インデックスの順序が乱れる可能性のある操作の後には、必ずソートしてください。

import pandas as pd
import numpy as np

np.random.seed(0)
countries = ['USA', 'UK', 'DE']
years = [2021, 2022, 2023]
mi = pd.MultiIndex.from_product([countries, years], names=['country', 'year'])
df = pd.DataFrame({'gdp': np.random.randint(3000, 26000, 9)}, index=mi)

print('Before sort_index():')
print(df.head(4))

df_sorted = df.sort_index()
print('\nAfter sort_index():')
print(df_sorted.head(4))
print('Is sorted:', df_sorted.index.is_monotonic_increasing)

timeit による検索時間の測定

Python の timeit モジュールは、文を何度も実行して平均することで、その実行にかかる時間を測定します。ソート済みインデックスと未ソートのインデックスによる検索をベンチマークするために使用できます。IPython/Jupyter では、%timeit マジックを使うと、同じ機能をより見やすい出力で利用できます。パフォーマンス最適化によって実際に改善したことを確認する唯一の信頼できる方法はベンチマークです。測定せずに変更後のほうが速いと判断してはいけません。

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
N = 500000
idx = np.random.choice(['A','B','C','D','E'], N)
df_unsorted = pd.DataFrame({'v': np.random.randn(N)}, index=idx)
df_sorted = df_unsorted.sort_index()

# Time label lookup: sorted vs unsorted
t_unsorted = timeit.timeit(lambda: df_unsorted.loc['C'], number=100)
t_sorted = timeit.timeit(lambda: df_sorted.loc['C'], number=100)

print(f'Unsorted lookup (100 runs): {t_unsorted:.3f}s')
print(f'Sorted lookup  (100 runs): {t_sorted:.3f}s')
print(f'Speedup: {t_unsorted/t_sorted:.1f}x')

未ソートの MultiIndex による PerformanceWarning

辞書順にソートされていない MultiIndex をスライスすると、Pandas は PerformanceWarning を出力します。メッセージは 'indexing past lexsort depth may impact performance' です。この警告は、Pandas が二分探索ではなく線形走査に切り替えたことを意味します。単純なケースでは正しい結果が返されますが、ソートされていない多階層インデックスの内部レベルをスライスすると、誤った結果が返される場合があります。この警告はエラーとして扱い、インデックスをソートして根本原因を解消してください。

import pandas as pd
import warnings

# Create an unsorted MultiIndex and trigger the warning
tuples = [('B', 2), ('A', 1), ('B', 1), ('A', 2)]
mi = pd.MultiIndex.from_tuples(tuples, names=['letter', 'num'])
df = pd.DataFrame({'v': [10, 20, 30, 40]}, index=mi)

print('Index sorted?', df.index.is_monotonic_increasing)

# This may trigger PerformanceWarning in some Pandas versions
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter('always')
    try:
        result = df.loc['A':'B', :]
        print('Result:', result)
        if w:
            print('Warning:', str(w[0].message))
    except Exception as e:
        print('Error (common with newer Pandas):', type(e).__name__)

ソート済みと未ソートの MultiIndex スライスをベンチマークする

ソート済みの MultiIndex によるスライスは、Pandas が外部レベルと内部レベルの配列の両方で二分探索を利用できるため、非常に高速です。ここでは、本番の分析パイプラインでよくある100万行の大きな MultiIndex に対するスライスをベンチマークします。ソート済みのバージョンでは線形走査が不要になるため、スライスの選択性に応じて一貫して5~50倍の高速化が見られます。

import pandas as pd
import numpy as np
import timeit

np.random.seed(42)
countries = ['DE', 'UK', 'USA', 'FR', 'JP']
dates = pd.date_range('2010-01-01', periods=200000)

# Sample a random subset for timing test
sample_countries = np.random.choice(countries, 100000)
sample_dates = np.random.choice(dates, 100000)

df = pd.DataFrame({
    'country': sample_countries,
    'date': sample_dates,
    'value': np.random.randn(100000)
}).set_index(['country', 'date'])

df_sorted = df.sort_index()
print('Dataset size:', len(df))
print('Sorted:', df_sorted.index.is_monotonic_increasing)

t = timeit.timeit(lambda: df_sorted.loc['USA'], number=50)
print(f'Sorted lookup (50 runs): {t:.3f}s')

level パラメーターを指定した sort_index

MultiIndex では、すべてのレベルではなく特定のレベルでソートすることもできます。level パラメーターを使用してください。例: df.sort_index(level='year')。これは、外部レベルのグループ分けを維持しながら、各外部グループ内の行の順序を変更したい場合に便利です。sort_remaining=True 引数(デフォルト)を指定すると、指定したレベルより下にある未ソートのレベルもソートされ、完全な辞書順が保証されます。

import pandas as pd
import numpy as np

countries = ['USA', 'UK']
years = [2023, 2021, 2022]  # deliberately unordered
mi = pd.MultiIndex.from_product([countries, years], names=['country', 'year'])
df = pd.DataFrame({'v': range(6)}, index=mi)

print('Before sorting by year level:')
print(df)

# Sort by the inner level (year) only
df_ysorted = df.sort_index(level='year')
print('\nAfter sort_index(level="year"):')
print(df_ysorted)

パイプラインのガードとしての is_monotonic_increasing

本番パイプラインでは、MultiIndex を持つ DataFrame を受け取り、スライスを実行する関数の冒頭にソートガードを追加してください。インデックスがソートされていなければ自動的にソートし、警告をログに記録します。これにより、上流のコードが DataFrame の順序を変更した場合でも、パフォーマンスの低下や誤った結果が発生するのを防げます。関数の境界でガードするほうが、呼び出し側が常にソート済みデータを渡すと仮定するよりも確実です。

import pandas as pd
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def safe_slice(df, key):
    '''Slice a MultiIndex DataFrame, sorting if necessary.'''
    if not df.index.is_monotonic_increasing:
        logger.warning('Index not sorted — sorting now. This is a performance cost.')
        df = df.sort_index()
    return df.loc[key]

# Test with an unsorted DataFrame
tuples = [('B', 2), ('A', 1), ('B', 1), ('A', 2)]
mi = pd.MultiIndex.from_tuples(tuples, names=['letter', 'num'])
df = pd.DataFrame({'v': [10, 20, 30, 40]}, index=mi)

result = safe_slice(df, 'A')
print('Slice result for A:')
print(result)

単純なインデックスでの二分探索に適したソート済みインデックス

ソートによるパフォーマンス上の利点は、通常の(MultiIndex ではない)インデックスにも当てはまります。時系列分析で使用する DatetimeIndex は、ソートされていると日付範囲のスライスが大幅に高速になります。アルファベット順にソートされた文字列インデックスでは、ラベル検索に二分探索を利用できます。タイムスタンプをインデックスとする大量の株価 Series では、DatetimeIndex をソートすることで、100ミリ秒かかっていたスライスを1ミリ秒未満に短縮できる場合があります。

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
# Random timestamps — unsorted
timestamps = pd.date_range('2020-01-01', periods=500000, freq='min')
shuffled = np.random.permutation(timestamps)

prices = pd.Series(np.random.randn(500000), index=shuffled)
prices_sorted = prices.sort_index()

# Time a date range slice
t_unsorted = timeit.timeit(lambda: prices['2020-06-01':'2020-06-30'], number=20)
t_sorted = timeit.timeit(lambda: prices_sorted['2020-06-01':'2020-06-30'], number=20)

print(f'Unsorted: {t_unsorted:.3f}s')
print(f'Sorted:   {t_sorted:.3f}s')
print(f'Speedup: {t_unsorted/t_sorted:.0f}x')

ソートにかかるメモリコスト

ソートは無料ではありません。sort_index() は、inplace=True を使用してその場で変更する場合を除き、DataFrame の新しいコピーを作成します。非常に大きな DataFrame では、一時的にピーク時のメモリ使用量が2倍になります。現実的な方法は、何度もソートを繰り返すのではなく、読み込み時に一度だけソートし、パイプライン全体でソート済みのバージョンを使い続けることです。メモリに余裕がない場合は、df.sort_index(inplace=True) でその場でソートし、一時的なコピーの作成を避けてください。

import pandas as pd
import numpy as np

np.random.seed(0)
countries = ['DE', 'UK', 'USA']
years = [2021, 2022, 2023]
mi = pd.MultiIndex.from_product([countries, years], names=['country', 'year'])
df = pd.DataFrame({'v': np.random.randn(9)}, index=mi)

# Sort once at load time — best practice
df.sort_index(inplace=True)  # no temporary copy
assert df.index.is_monotonic_increasing, 'Index must be sorted!'
print('Pipeline-ready DataFrame (sorted in place):')
print(df)

ソート済みインデックスのベストプラクティスまとめ

インデックスのパフォーマンスに関する重要なルールは次のとおりです。1) concat、merge、append、filter など、インデックスの順序を乱す可能性がある操作の後には、必ず sort_index() を呼び出します。2) インデックスをスライスする関数では、is_monotonic_increasing をガードとして使用します。3) 繰り返しのソートを避けるため、読み込み時にソートし、パイプライン全体でソート済みの DataFrame を使い続けます。4) MultiIndex の DataFrame では、最外部のレベルだけでなく、すべてのレベルがソートされていることを確認します。5) timeit を使用して、対象のパイプラインでソートによる期待どおりの高速化が実際に得られることを検証します。

クイックチェック

このレッスンで学んだソート済みインデックスのパフォーマンスについて、理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、is_monotonic_increasing がインデックスのソート状態と二分探索の利用可否を確認すること、sort_index() がその場でソートするかソート済みのコピーを返すこと、そして timeit が実際の高速化を測定して効果を検証することを学びました。次は、時系列データや金融データのためのローリング統計と累積統計であるウィンドウ関数について学びます。

無料で開始

AI チューターと学ぶ Python — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
30
レッスン
120

よくある質問

「ソート済みインデックスのパフォーマンス上の利点」レッスンは無料ですか?

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

「ソート済みインデックスのパフォーマンス上の利点」で何を学びますか?

sort_index()でMultiIndexをソートし、timeitでスライスのパフォーマンスを測定して、is_monotonic_increasingをガードとして使用します。 ブラウザで直接実行するハンズオンコードでPandas & NumPy Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Pandas & NumPy Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPandas & NumPy Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「ソート済みインデックスのパフォーマンス上の利点」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPandas & NumPy Academyレッスンでコードを書いて実行できますか?

はい。すべてのPandas & NumPy Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. MultiIndexの作成
  2. MultiIndexからのデータ選択
  3. インデックスの整列と再インデックス
  4. ソート済みインデックスのパフォーマンス上の利点
← Pandas & NumPy Academyに戻る