PolarsでPythonデータ分析を劇的効率化

IT・プログラミング

PolarsでPythonデータ分析を劇的効率化

データ分析の現状と課題

データ分析の世界は、日々進化を遂げていますが、データ量の爆発的な増加や複雑化する分析ニーズは、データ分析者にとって大きな課題です。例えば、日々の業務でPandasを利用している方は、大規模なデータセットを扱う際に処理速度の遅延やメモリ不足に悩まされた経験があるのではないでしょうか。

Pandasは、Pythonにおけるデータ分析の標準的なライブラリとして広く利用されていますが、シングルスレッドでの処理が中心であるため、CPUリソースを十分に活用できず、大規模データセットの処理には限界があります。また、メモリ効率も課題であり、データ量が増加するにつれて、パフォーマンスが著しく低下する場合があります。

具体的な例として、1GBのCSVファイルをPandasで読み込むのに数分かかる場合があります。また、数十GBのデータセットを処理しようとすると、メモリ不足で処理が中断されることも珍しくありません。

そこで登場するのがPolarsです。Polarsは、Rustで開発された高速なDataFrameライブラリであり、マルチスレッド処理やメモリ効率に優れています。Pandasと比較して、大規模データセットの処理速度を大幅に向上させることが可能です。あるベンチマークテストでは、PolarsがPandasよりも数十倍高速に動作したという結果も報告されています。

Polarsは、Lazy Evaluation(遅延評価)という機能も備えています。これは、処理の実行を可能な限り遅らせることで、不要な計算を省き、メモリ使用量を削減する技術です。これにより、大規模なデータセットに対しても、効率的な分析が可能になります。

このブログでは、Polarsの基本的な使い方から、高度なデータ処理、パフォーマンス最適化までを解説し、Polarsがデータ分析の効率をどのように向上させるかを具体的にご紹介します。Pandasでの課題を克服し、より快適なデータ分析ライフを実現するために、ぜひPolarsを試してみてください。

Polarsの基本操作

このセクションでは、Polarsの基本的な使い方を解説します。PolarsのDataFrameを作成し、データを読み込み、基本的なデータ操作を体験することで、PolarsのAPIとPandasとの違いを理解していきましょう。

DataFrameの作成

PolarsでDataFrameを作成する方法はいくつかあります。最も簡単なのは、Pythonのリストや辞書から作成する方法です。PandasのDataFrameと似た方法で作成できます。

import polars as pl

# リストからDataFrameを作成
data = {
    "名前": ["Alice", "Bob", "Charlie"],
    "年齢": [25, 30, 28],
    "都市": ["東京", "大阪", "京都"]
}

df = pl.DataFrame(data)
print(df)

このコードを実行すると、以下のようなDataFrameが出力されます。

shape: (3, 3)
┌─────────┬─────┬─────────┐
│ 名前    ┆ 年齢  ┆ 都市    │
│ ---     ┆ --- ┆ ---     │
│ str     ┆ i64 ┆ str     │
╞═════════╪═════╪═════════╡
│ Alice   ┆ 25  ┆ 東京    │
│ Bob     ┆ 30  ┆ 大阪    │
│ Charlie ┆ 28  ┆ 京都    │
└─────────┴─────┴─────────┘

PandasのDataFrameとよく似ていますが、PolarsのDataFrameは、より効率的なデータ構造を使用しており、大規模なデータセットの処理に適しています。

データの読み込み

Polarsでは、CSVファイルやParquetファイルなど、様々な形式のデータを読み込むことができます。

import polars as pl
import pandas as pd

# サンプルCSVファイルを作成
data = {'col1': [1, 2], 'col2': [3, 4]}
df_pandas = pd.DataFrame(data)
df_pandas.to_csv("data.csv", index=False)


# サンプルParquetファイルを作成
df_pandas.to_parquet("data.parquet", index=False)


# CSVファイルを読み込む
df = pl.read_csv("data.csv")
print("CSV読み込み:")
print(df)

# Parquetファイルを読み込む
df = pl.read_parquet("data.parquet")
print("Parquet読み込み:")
print(df)

pl.read_csv()関数やpl.read_parquet()関数を使用することで、簡単にデータを読み込むことができます。また、scan_csvscan_parquetを使用すると、Lazy evaluationによる効率的な読み込みが可能です。これは、ファイル全体をメモリに読み込むのではなく、必要な部分だけを読み込むため、メモリ使用量を削減できます。

基本的なデータ操作

DataFrameを作成し、データを読み込んだら、次は基本的なデータ操作を体験してみましょう。

列の選択

.select()メソッドを使用すると、DataFrameから特定の列を選択できます。Pandasのdf[['column1', 'column2']]に相当します。

import polars as pl

# リストからDataFrameを作成
data = {
    "名前": ["Alice", "Bob", "Charlie"],
    "年齢": [25, 30, 28],
    "都市": ["東京", "大阪", "京都"]
}

df = pl.DataFrame(data)

# 名前と年齢の列を選択
df_selected = df.select(["名前", "年齢"])
print(df_selected)

行のフィルタリング

.filter()メソッドを使用すると、特定の条件を満たす行を抽出できます。Pandasではdf[df['年齢'] >= 28]のように書くところを、Polarsではdf.filter(pl.col("年齢") >= 28)と記述します。

import polars as pl

# リストからDataFrameを作成
data = {
    "名前": ["Alice", "Bob", "Charlie"],
    "年齢": [25, 30, 28],
    "都市": ["東京", "大阪", "京都"]
}

df = pl.DataFrame(data)


# 年齢が28歳以上の行を抽出
df_filtered = df.filter(pl.col("年齢") >= 28)
print(df_filtered)

Polarsの高度なデータ処理

このセクションでは、Polarsの持つ高度なデータ処理機能に焦点を当て、データ分析をさらに効率化する方法を解説します。具体的には、データのグループ化、集約、結合といった操作を通じて、Polarsの強力なデータ処理能力を体験していただきます。

グループ化:データを意味のある塊に

データ分析において、特定のカテゴリや属性に基づいてデータをグループ化することは非常に重要です。Polarsでは、group_by()メソッドを使用することで、DataFrameを簡単にグループ化できます。例えば、顧客の購買データを地域ごとにグループ化し、地域ごとの売上傾向を分析するといったことが可能です。

import polars as pl

df = pl.DataFrame({
    "地域": ["東京", "東京", "大阪", "大阪", "名古屋", "名古屋"],
    "売上": [100, 150, 80, 120, 90, 110]
})

grouped_df = df.group_by("地域").sum()
print(grouped_df)

上記の例では、group_by("地域")によってDataFrameが「東京」「大阪」「名古屋」の各地域でグループ化され、その後にsum()メソッドで各地域の売上合計が計算されています。複数列でのグループ化も可能で、より複雑な分析にも対応できます。

集約:グループ化されたデータの要約

グループ化されたデータに対して、平均、合計、最大値、最小値などの集約関数を適用することで、データの特性をより深く理解できます。Polarsでは、agg()メソッドを使用することで、さまざまな集約関数を適用できます。複数の集約を同時に実行することも可能です。

import polars as pl

df = pl.DataFrame({
    "カテゴリ": ["A", "A", "B", "B", "C", "C"],
    "数値": [10, 20, 15, 25, 12, 18]
})

result = df.group_by("カテゴリ").agg([
    pl.col("数値").mean().alias("平均値"),
    pl.col("数値").max().alias("最大値")
])

print(result)

この例では、agg()メソッドに平均値と最大値を計算する集約関数をリスト形式で指定しています。alias()メソッドを使用することで、集約結果の列名を指定できます。これにより、結果がより分かりやすくなります。

結合:複数のデータを統合

複数のDataFrameを結合することで、異なるデータソースからの情報を統合し、より包括的な分析を行うことができます。Polarsでは、join()メソッドを使用することで、SQLのような結合操作を実行できます。内部結合、左外部結合、右外部結合、完全外部結合など、さまざまな結合タイプがサポートされています。

import polars as pl

df1 = pl.DataFrame({
    "ID": [1, 2, 3, 4],
    "名前": ["Alice", "Bob", "Charlie", "David"]
})

df2 = pl.DataFrame({
    "ID": [1, 2, 5],
    "年齢": [25, 30, 28]
})

joined_df = df1.join(df2, on="ID", how="left")
print(joined_df)

上記の例では、join()メソッドのon引数に結合キーとなる列名(”ID”)を指定し、how引数に結合タイプ(”left”:左外部結合)を指定しています。左外部結合では、左側のDataFrame(df1)のすべての行が結果に含まれ、右側のDataFrame(df2)に一致する行がない場合は、右側の列にnull値が挿入されます。

ウィンドウ関数:グループ内での相対的な計算

ウィンドウ関数を使用すると、グループ内の特定の範囲の行に対して計算を行うことができます。これは、移動平均や累積和などの計算に非常に役立ちます。Polarsは、over()句と組み合わせることで、強力なウィンドウ関数を提供します。

import polars as pl

df = pl.DataFrame({
    "日付": ["2023-01-01", "2023-01-02", "2023-01-03", "2023-01-04", "2023-01-05"],
    "売上": [10, 12, 15, 13, 16]
})

df = df.with_columns(pl.col("日付").str.strptime(pl.Date, "%Y-%m-%d").cast(pl.Int64))

windowed_df = df.with_columns(
    pl.col("売上").rolling_mean(window_size=3, min_periods=1).over("日付").alias("3日移動平均")
)

print(windowed_df)

この例では、rolling_mean(window_size=3)によって3日間の移動平均を計算し、over("日付")によって日付順に計算しています。ウィンドウ関数を使うことで、時系列データ分析などが容易になります。

これらの高度なデータ処理機能を活用することで、Polarsはデータ分析の可能性を大きく広げます。ぜひ、これらの機能を活用して、データ分析スキルを向上させてください。

Polarsのパフォーマンス最適化

Polarsの真価は、その驚異的なパフォーマンスにあります。Pandasでは手に負えなかった大規模データも、Polarsならストレスなく扱える可能性があります。ここでは、Polarsのパフォーマンスを最大限に引き出すための最適化テクニックを解説します。メモリ効率、並列処理、そして遅延評価という3つの柱を中心に、具体的な方法を見ていきましょう。

1. メモリ効率の追求

データ分析において、メモリは貴重な資源です。Polarsは、カラム指向のデータ構造やデータ型の最適化により、メモリ使用量を大幅に削減します。

  • データ型の明示的な指定: pl.read_csvなどでデータを読み込む際、dtypes引数を使って各カラムのデータ型を明示的に指定しましょう。例えば、整数型はpl.Int32pl.Int64、浮動小数点型はpl.Float32pl.Float64など、必要な範囲で最も小さい型を選ぶことで、メモリ消費を抑えられます。

    import polars as pl
    import pandas as pd
    
    # サンプルCSVファイルを作成
    data = {'id': [1, 2], 'value': [3.0, 4.0]}
    df_pandas = pd.DataFrame(data)
    df_pandas.to_csv("data.csv", index=False)
    
    # 'data.csv'の'id'カラムをInt32型、'value'カラムをFloat32型として読み込む
    df = pl.read_csv('data.csv', dtypes={'id': pl.Int32, 'value': pl.Float32})
    print(df)
    
  • カテゴリ型の活用: 文字列型のカラムで、値の種類が少ない場合は、pl.Categorical型への変換を検討しましょう。Polarsは内部的に整数でカテゴリを管理するため、メモリ効率が向上します。

    import polars as pl
    import pandas as pd
    
    # サンプルCSVファイルを作成
    data = {'id': [1, 2], 'value': [3.0, 4.0], 'category': ['A', 'B']}
    df_pandas = pd.DataFrame(data)
    df_pandas.to_csv("data.csv", index=False)
    
    # 'data.csv'の'id'カラムをInt32型、'value'カラムをFloat32型として読み込む
    df = pl.read_csv('data.csv', dtypes={'id': pl.Int32, 'value': pl.Float32, 'category': pl.Utf8})
    
    
    df = df.with_columns(pl.col('category').cast(pl.Categorical))
    
    print(df)
    
  • 不要なカラムの削除: 分析に不要なカラムは、早めに削除しましょう。df.selectで必要なカラムのみを選択することで、メモリ使用量を削減できます。

    import polars as pl
    import pandas as pd
    
    # サンプルCSVファイルを作成
    data = {'id': [1, 2], 'value': [3.0, 4.0], 'category': ['A', 'B']}
    df_pandas = pd.DataFrame(data)
    df_pandas.to_csv("data.csv", index=False)
    
    # 'data.csv'の'id'カラムをInt32型、'value'カラムをFloat32型として読み込む
    df = pl.read_csv('data.csv', dtypes={'id': pl.Int32, 'value': pl.Float32, 'category': pl.Utf8})
    
    
    
    df = df.with_columns(pl.col('category').cast(pl.Categorical))
    
    
    # 'id'と'value'カラムのみを選択
    df = df.select(['id', 'value'])
    print(df)
    

2. 並列処理の活用

Polarsは、デフォルトで利用可能なCPUコアを最大限に活用し、データ処理を並列化します。特に、グループ化や集約などの処理でその効果を発揮します。

  • 環境変数の設定: Polarsが使用するスレッド数を制御するには、POLARS_MAX_THREADS環境変数を設定します。システムのコア数に合わせて調整することで、最適なパフォーマンスが得られます。

    export POLARS_MAX_THREADS=8 # 8スレッドを使用する場合
    
  • Chunkサイズ調整: 大規模なデータセットを処理する場合、scan_csvなどの関数でchunk_sizeパラメータを調整することで、並列処理の効率を最適化できます。適切なChunkサイズは、データセットの特性やシステム環境によって異なります。色々な値を試して、最適な値を見つけましょう。

3. 遅延評価(Lazy Evaluation)の徹底活用

Polarsの遅延評価は、クエリの実行を可能な限り遅らせ、最適化された実行計画を立てることで、パフォーマンスを向上させる強力な機能です。

  • scan_csvcollect: CSVファイルを読み込む際は、pl.read_csvの代わりにpl.scan_csvを使用し、必要な処理をすべて記述した後で、df.collect()を実行しましょう。これにより、Polarsはクエリ全体を分析し、最適な実行順序を決定できます。

    import polars as pl
    import pandas as pd
    
    # サンプルCSVファイルを作成
    data = {'id': [1, 2, 3], 'value': [3.0, 200.0, 4.0], 'category': ['A', 'A', 'B']}
    df_pandas = pd.DataFrame(data)
    df_pandas.to_csv("data.csv", index=False)
    
    # 遅延評価でCSVファイルを読み込み、フィルタリングと集約を行う
    df = pl.scan_csv('data.csv') \
           .filter(pl.col('value') > 100) \
           .group_by('category') \
           .agg(pl.sum('value')) \
           .collect()
    
    print(df)
    
  • Predicate Pushdown: 遅延評価により、filter処理を可能な限り早い段階で実行できます。これにより、不要なデータを読み込む前に除外できるため、メモリ使用量と処理時間を大幅に削減できます。
  • Projection Pushdown: select処理も同様に、可能な限り早い段階で実行されます。これにより、不要なカラムの読み込みを避けることができます。

パフォーマンス最適化のベストプラクティス

  • できる限りPolarsの組み込み関数を使用する: applyなどのユーザー定義関数は、パフォーマンスが低下する可能性があります。Polarsの組み込み関数は、最適化されているため、できる限り利用しましょう。
  • クエリの実行順序を最適化する: フィルタリングや射影を早い段階で行うことで、処理対象のデータ量を削減できます。
  • 定期的なプロファイリング: perfなどのツールを使用して、ボトルネックを特定し、最適化の余地を探しましょう。

これらの最適化テクニックを駆使することで、Polarsのパフォーマンスを最大限に引き出し、データ分析を劇的に効率化できます。ぜひ、あなたのデータ分析パイプラインに取り入れてみてください。

実践!Polarsによるデータ分析

このセクションでは、Polarsを実際のデータ分析タスクでどのように活用できるのかを解説します。具体的な事例を通して、Polarsがデータ分析の効率をどのように向上させるかを体感していきましょう。

事例1:大規模ECサイトの売上分析

ある大規模ECサイトの売上データ(数百万行)を分析するケースを考えてみましょう。データには、注文ID、顧客ID、商品ID、注文日時、金額などの情報が含まれています。このデータを使って、時間帯別の売上傾向、人気商品、顧客セグメントごとの購買行動などを分析します。

Pandasでの課題

Pandasでも同様の分析は可能ですが、データ量が大きくなると処理速度が低下し、メモリ不足に陥る可能性があります。特に、グループ化や集約処理は時間がかかり、分析のボトルネックとなることがあります。

Polarsでの解決策

Polarsを使用すると、これらの課題を解決できます。例えば、pl.scan_csv()を使ってデータをLazyFrameとして読み込み、必要な処理のみを実行することで、メモリ使用量を抑えられます。また、.group_by().agg()を組み合わせることで、高速なグループ化と集約処理を実現できます。

import polars as pl
import pandas as pd
import datetime

# サンプルCSVファイルを作成
dates = [datetime.datetime(2023, 1, 1, i, 0, 0) for i in range(5)]
data = {'order_datetime': dates, 'amount': [100, 150, 80, 120, 90]}
df_pandas = pd.DataFrame(data)
df_pandas['order_datetime'] = df_pandas['order_datetime'].astype(str)  # polarsがdatetimeを直接書き込めないのでstrで書き込む
df_pandas.to_csv("sales_data.csv", index=False)

# データの読み込み(LazyFrame)
df = pl.scan_csv("sales_data.csv")

# 時間帯別の売上集計
result = (
    df.with_columns(pl.col("order_datetime").str.strptime(pl.Datetime, "%Y-%m-%d %H:%M:%S").dt.hour().alias("hour"))
    .group_by("hour")
    .agg(pl.col("amount").sum())
    .sort("hour")
    .collect()
)

print(result)

上記のコードは、注文日時から時間帯を抽出し、時間帯ごとに売上金額を合計する処理をPolarsで行う例です。.collect()を最後に実行することで、LazyFrameからDataFrameに変換し、結果を表示できます。Pandasと比較して、処理速度が大幅に向上することが期待できます。

事例2:ソーシャルメディアのテキスト分析

ソーシャルメディアの投稿データ(数百万件)を分析し、特定のキーワードに対する感情分析を行うケースを考えてみましょう。データには、投稿ID、ユーザーID、投稿日時、テキストなどの情報が含まれています。

Pandasでの課題

テキストデータの処理は、文字列操作や正規表現などが多いため、Pandasでは処理速度が遅くなる傾向があります。また、大規模なテキストデータをメモリにロードすると、メモリ不足が発生する可能性があります。

Polarsでの解決策

Polarsは、文字列操作に最適化された関数を多数提供しており、高速なテキスト処理を実現できます。例えば、.str.contains()を使って特定のキーワードを含む投稿を抽出したり、.str.replace_all()を使ってテキストをクリーニングしたりできます。また、LazyFrameを使うことで、メモリ使用量を抑えながら大規模なテキストデータを処理できます。

import polars as pl
import pandas as pd

# サンプルCSVファイルを作成
data = {'text': ["新商品が出ました!", "これは良くない", "新商品は最高!", "全く興味なし"],
        'sentiment': ["positive", "negative", "positive", "negative"]}
df_pandas = pd.DataFrame(data)
df_pandas.to_csv("social_media_data.csv", index=False)

# データの読み込み(LazyFrame)
df = pl.scan_csv("social_media_data.csv")

# 特定のキーワードを含む投稿の抽出
keyword = "新商品"
filtered_df = df.filter(pl.col("text").str.contains(keyword))

# ポジティブな感情の投稿数を集計
positive_count = filtered_df.filter(pl.col("sentiment") == "positive").count()

print(f"{keyword}に関するポジティブな投稿数: {positive_count}")

上記のコードは、「新商品」というキーワードを含む投稿を抽出し、その中でポジティブな感情を持つ投稿数を集計する例です。.str.contains().filter()を組み合わせることで、簡潔かつ高速なテキストフィルタリングを実現できます。

まとめ:Polarsでデータ分析を効率化しよう

これらの事例からわかるように、Polarsは大規模なデータセットや複雑なデータ処理において、Pandasと比較して高いパフォーマンスを発揮します。Polarsを活用することで、データ分析の処理速度を向上させ、より多くの時間を分析自体に費やすことができます。ぜひ、Polarsを導入して、データ分析の効率を劇的に向上させてください。

コメント

タイトルとURLをコピーしました