【ITニュース解説】Cómo usar Pandas para analizar 100000 filas en segundos
2026年09月30日に「Dev.to」が公開したITニュース「Cómo usar Pandas para analizar 100000 filas en segundos」について初心者にもわかりやすく解説しています。
ITニュース概要
Pandasで10万行ものデータを秒速で分析するには、適切な技術が必要だ。データ読み込み時のカラム選択や型指定、ループ処理を避けた高速なフィルタリング、groupbyの活用などが鍵となる。これらの最適化を学ぶことで、効率的でクリーンなデータ分析が可能になる。
ITニュース解説
データ分析の分野では、大量の情報を素早く処理する能力が、もはや贅沢品ではなく必須のスキルとなっている。Pythonのデータ操作・分析ライブラリとして最も広く使われているPandasは、何十万ものデータを瞬時に処理できる強力なツールを提供する。しかし、多くのユーザーは処理の遅さ、メモリの過剰な消費、非効率なコードに直面し、せっかくの分析作業が煩わしいものになっている。ここで重要なのは、Pandasの基本的な関数を知っているだけでなく、データロード、フィルタリング、集計、そして最適化といった、適切な技術を習得することだ。これらの技術を使いこなすことで、Pandasの基盤であるNumPyを最大限に活用し、クリーンで再現性があり、拡張性の高いコードで10万行、あるいはそれ以上のデータを数秒で分析できるようになる。
Pandasが大量のデータ分析に理想的なツールとされるのには、明確な理由がある。それは、高水準プログラミング言語であるPythonの柔軟性と、低水準言語で実装されたベクトル化された操作のパフォーマンスを兼ね備えている点にある。Pythonの標準的なループ処理では、一つ一つのデータを個別に処理するため、大きなオーバーヘッドが発生する。これに対し、PandasはNumPyのエンジンを利用して配列全体に操作を適用する、いわゆる「ベクトル化」された処理を行うため、実行時間を劇的に短縮できる。
10万行というデータは、いわゆる「ビッグデータ」とまではいかないものの、非効率な処理方法が顕著に現れる規模である。この程度のデータであれば、現代のコンピュータのメモリ(RAM)に十分に収まる。しかし、もしiterrows()のような行ごとのイテレーションを使ったり、apply()関数を最適化せずに使ったり、ループ内で繰り返しデータ連結(concatenate)を行ったりすると、処理時間は数秒から数分へと大幅に増えてしまう。Pandasは、まさにそうした非効率を避けるために設計されており、データ全体に対してベクトル化操作を行うSeriesやDataFrameといった構造を提供し、メモリを最小限に抑え、速度を最大化するインデックスやグループ化メソッドの豊富なエコシステムも備えている。
さらに、Pandasはブール値によるフィルタリング、SQLのようなgroupbyによる集計、結合(join)、ピボットテーブルといった操作を内部で最適化して統合している。これは、パフォーマンスを向上させるためにPython以外の言語を使う必要がないことを意味する。適切なテクニックを使えば、コードは速いだけでなく、読みやすく、保守しやすいものとなる。
高速な分析の最初のステップは、データの読み込みと準備の段階から始まる。非効率な読み込み方法は、10万行のDataFrameの読み込みに数分を要することもある。特にCSVファイルの場合、データ型の混在、日付形式の解釈、欠損値の扱いで時間がかかることがある。
まず、PandasとNumPyの最新バージョンをインストールしておくことが重要だ。多くのパフォーマンス最適化は新しいバージョンで実装されるため、常に最新の状態を保つのが基本である。CSVファイルを扱う際には、ファイル全体を読み込んでからフィルタリングするのではなく、usecolsパラメータを使って必要な列だけを最初から読み込むと良い。この簡単な変更だけで、Pandasが不要な列を解析する手間が省け、占有メモリ量と読み込み時間の両方を削減できる。
もう一つの重要な点は、データ型を明示的に指定することだ。デフォルトでは、Pandasはデータ型を自動的に推測するが、これにより本来は整数や日付であるべき列が文字列(object型)として扱われることがあり、メモリ消費が増え、数値計算が遅くなる原因となる。ファイルを読み込む際にdtypesパラメータを使ってデータ型を定義する(例えば、int64の代わりにint32、項目数が少ない列にはcategory型を使うなど)ことで、メモリ使用量を半分に減らし、フィルタリング速度を向上させることが可能だ。
Parquet、Feather、HDF5といったバイナリ形式のデータファイルを利用すると、さらに劇的な改善が見られる。これらの形式はデータ型をネイティブに保持し、CSVよりも数倍高速に読み込める。もしデータのパイプラインで可能であれば、データをParquet形式で保存することは、分析速度を向上させる上で最も効果的な方法の一つとなる。
データソースがSQLデータベースの場合、テーブル全体をPandasに読み込んでからフィルタリングするのではなく、SQLクエリでWHERE句、LIMIT句、JOIN句を使って、データがメモリに到達する前にその量を減らすことが推奨される。この原則は常に同じで、フィルタリングと選択はデータソースのできるだけ近い場所で行うべきである。
例として、次のように最適化された読み込みを行うことができる。
1import pandas as pd 2import numpy as np 3 4# 必要な列のみを定義したデータ型で読み込む 5dtypes = { 6 'id': 'int32', 7 'categoria': 'category', 8 'precio': 'float32', 9 'cantidad': 'int16', 10 'ciudad': 'category' 11} 12 13df = pd.read_csv( 14 'ventas_100k.csv', 15 usecols=['id', 'categoria', 'precio', 'cantidad', 'ciudad'], 16 dtype=dtypes, 17 parse_dates=['fecha'] 18) 19 20# メモリ使用量を確認する 21print(df.info(memory_usage='deep'))
このコードでは、特定のデータ型を定義し、関連性のない列を避けることで、メモリ使用量を大幅に削減している。20列を読み込むのと5列を読み込むのとでは、数秒の差が出ることがあり、この影響はスクリプトを繰り返し実行するたびに大きくなる。
DataFrameが読み込まれた後も、数万件のレコードを迅速に処理するための具体的なテクニックがいくつかある。最も重要かつ第一のテクニックは、ベクトル化されたブールフィルタリングを使うことだ。ループでデータを行ごとに巡回する代わりに、ブールマスク(True/Falseの条件式)を構築し、Pandasが内部で最適化された方法でそれを適用する。
例えば、「カテゴリーが'Electrónica'で、かつ価格が1000を超える販売」をフィルタリングする場合、df[(df['categoria'] == 'Electrónica') & (df['precio'] > 1000)]のように記述する。この式は、Pythonインタープリタが各行を呼び出すことなく、C言語レベルで評価される。iterrows()やitertuples()のような関数をフィルタリングに使うことは避けるべきだ。これらの関数は、ベクトル化されたO(n)の操作をPythonのオーバーヘッドを伴うO(n)の操作に変えてしまい、10倍から100倍もの処理速度の低下を招く。
二つ目の本質的なテクニックは、groupby()関数の賢い使い方である。この関数はPandasの中でも最も強力なものの一つだが、しばしば誤解されている。df.groupby('categoria')['ventas'].sum()のように実行すると、PandasはGroupByオブジェクトを作成し、データをグループ化し、その後効率的に集計を適用する。さらに高速化するには、グループ化の対象となる列をset_index()でインデックスとして設定する方法がある。これにより、繰り返し行うグループ化操作が高速になることがある。
三つ目のテクニックは、不要なデータコピーを避けることだ。Pandasは操作によってデータの「ビュー」(元のデータへの参照)を返すこともあれば、「コピー」(元のデータとは別の新しいデータ)を返すこともあり、複数の代入操作を連鎖させるとメモリ内でデータが重複してしまう可能性がある。可能な場合はインプレース操作(既存のデータに直接変更を加える操作)を使用する。最近のバージョンではSettingWithCopyWarningという警告が出るため、この方法は推奨されにくくなっているが、代わりに、df.query('precio > 500').assign(ingresos=lambda x: x['precio'] * x['cantidad']).groupby('categoria')['ingresos'].sum().reset_index()のようにメソッドを流れるように連鎖させるスタイルがクリーンであり、中間変数の数を最小限に抑える。
もう一つの重要なテクニックは、query()とeval()メソッドを使うことだ。query()メソッドは、フィルタリング式を文字列として記述することを可能にし、Pandasはnumexprというライブラリを利用してこれを評価する。numexprは、大規模な一時配列の生成を避けつつ、算術演算や比較演算を高速化する。10万行のデータセットの場合、複数の条件を持つ操作で20%から50%の性能向上が見られることがある。
最後に、繰り返し出現するテキスト列にはカテゴリ型を利用することを検討すべきだ。オブジェクト型(文字列)の列をカテゴリ型に変換することで、メモリ使用量を削減し、groupbyやソート操作を高速化できる。これは、文字列比較の代わりに整数コードでの比較が行われるためだ。
これらのテクニックの力を示すため、10万行のデータセットに対して現実的な分析フローを構築してみよう。シナリオは、オンラインストアがカテゴリ別の売上高、平均注文額、そして最新の四半期で最も売上高の大きかった上位5都市を計算したいというものだ。これを控えめなハードウェアでも1秒未満で実行する。
まず、10万行のシミュレートされたデータセットを生成する。
1import pandas as pd 2import numpy as np 3 4np.random.seed(42) 5n = 100000 6 7df = pd.DataFrame({ 8 'id': np.arange(n), 9 'fecha': pd.date_range('2024-01-01', periods=n, freq='min').normalize(), 10 'categoria': np.random.choice(['Electrónica', 'Ropa', 'Hogar', 'Deportes'], n), 11 'ciudad': np.random.choice(['Madrid', 'Barcelona', 'Valencia', 'Sevilla', 'Bilbao'], n), 12 'precio': np.random.uniform(10, 500, n).round(2), 13 'cantidad': np.random.randint(1, 10, n) 14}) 15 16# データ型を最適化する 17df['categoria'] = df['categoria'].astype('category') 18df['ciudad'] = df['ciudad'].astype('category') 19df['precio'] = df['precio'].astype('float32') 20df['cantidad'] = df['cantidad'].astype('int16')
このデータ生成自体は一瞬で終わる。次に、完全な分析を実行する。
1# 最新の四半期(2024年10月~12月)でフィルタリング 2mask = (df['fecha'] >= '2024-10-01') & (df['fecha'] <= '2024-12-31') 3df_q4 = df.loc[mask].copy() 4 5# 各行の売上高を計算 6df_q4['ingresos'] = df_q4['precio'] * df_q4['cantidad'] 7 8# 集計 9resultados = df_q4.groupby('categoria', observed=True).agg( 10 total_ingresos=('ingresos', 'sum'), 11 total_ventas=('ingresos', 'size'), 12 ticket_promedio=('ingresos', 'mean') 13).reset_index() 14 15# 売上高上位5都市 16top_ciudades = df_q4.groupby('ciudad', observed=True)['ingresos'].sum().nlargest(5).reset_index() 17 18print("カテゴリ別の結果:") 19print(resultados) 20print("\n上位都市:") 21print(top_ciudades)
平均的なコンピュータでは、このスクリプト全体(10万行のデータ生成、フィルタリング、計算、集計を含む)が0.5秒未満で実行される。その鍵は、すべての操作がベクトル化されており、データ型が最初から最適化されていることにある。もしこの同じフローをiterrows()やカテゴリ型を使わないapply()で繰り返した場合、その複雑さに応じて処理時間は数秒、あるいは数分にまで伸びる可能性もある。
基本的なテクニックを習得したら、さらに一歩進んだ最適化でパフォーマンスを向上させることができる。その一つに、DaskやModinを使って自動的に操作を並列化する方法がある。DaskはDataFrameを複数のパーティションに分割し、複数のブロックを並列に処理することで、プロセッサのすべてのコアを活用する。ModinはPandasと互換性のあるAPIを提供するが、内部ではRayやDaskをバックエンドとして利用しており、import pandas as pdをimport modin.pandas as pdに変更するだけで、コードへの変更を最小限に抑えられる。
もしPandasを純粋に使いたいのであれば、独立したグループの処理のように分割できるタスクに対して、multiprocessingモジュールを使った手動での並列処理を実装することも可能だ。しかし、10万行程度のデータであれば、これまでの最適化を適用していれば並列処理は通常不要である。真のボトルネックは、多くの場合、ディスクI/Oであり、処理速度ではない。
もう一つの高度なテクニックは、複数の列を扱う複雑な操作にeval()を使うことだ。例えば、複合的な指標を計算する場合に、df.eval('ingresos = precio * cantidad', inplace=True)のように記述すると、列を段階的に作成するよりも効率的な一時配列が生成される。これはeval()がnumexprを利用して中間配列の生成を削減するためである。
また、不要なソート操作は避けるべきだ。sort_values()は時間とメモリのコストがかかるため、結果がソートされている必要がない場合は省略する。同様に、ループ内でDataFrameを連結する際には、レコードをリストに蓄積し、最後に一度だけpd.concat()を使用すべきだ。繰り返し連結を行うと、各イテレーションでDataFrameの完全なコピーが生成され、パフォーマンスが二次曲線的に低下する。
パフォーマンスのプロファイリングには、Jupyter Notebookの%timeitやPythonのcProfileモジュールなどのツールを使うことで、真のボトルネックを特定できる。多くの場合、処理時間の80%は20%の操作で消費されており、その小さな部分を最適化することで不釣り合いなほどの大きな影響が得られる。
最後に、永続的なストレージ形式も考慮しよう。データをSnappy圧縮を施したParquet形式で保存することで、10万行のデータセットを次回以降の実行でミリ秒単位で読み込むことが可能になり、CSVのテキスト解析にかかるコストを完全に排除できる。Parquet形式での読み込み、最適化されたデータ型、そしてベクトル化された操作の組み合わせは、実際には高速な分析のための「勝利の構成」である。
まとめると、Pandasを使って10万行のデータを数秒で分析する能力は、高性能なコンピュータに依存するのではなく、適切なテクニックを適用することにかかっている。それは、選択的なデータロード、正確なデータ型定義、ベクトル化されたフィルタリング、そしてgroupby()や効率的な集計の賢い利用といった実践だ。これらの習慣は、遅くてメモリを消費しやすいプロセスを、俊敏で再現性があり、拡張性の高い分析フローへと変貌させる。もしあなたのプロジェクトが10万件以上のレコードを扱う規模に成長しても、これらの戦略は、Parquetのようなバイナリ形式やDaskのような並列処理ツールと合わせて、Pythonの柔軟性を犠牲にすることなく、パフォーマンスを維持することを可能にするだろう。