TL;DR

  • NumbaはPythonの関数をJIT(Just-In-Time)コンパイルし、数値計算処理をC言語並みの速度に高速化するライブラリです
  • @njitデコレータを関数に付けるだけで、NumPy配列を使った数値ループ処理が数十〜数百倍高速化できます
  • ただし文字列処理や複雑なPythonオブジェクト操作は苦手なため、適用範囲を見極めることが重要です

概要

Numbaは、LLVM(Low Level Virtual Machine)をバックエンドに使い、Pythonの関数をネイティブマシンコードにコンパイルするJITコンパイラです。特徴的なのは、NumPyの配列操作や数値演算を多用するコードに対して非常に強力な最適化を効きやすいという点です。

通常、Pythonでfor文を使ってNumPy配列を要素ごとに処理すると、インタプリタのオーバーヘッドによって著しく遅くなります。これを回避するために、これまではNumPyのベクトル化演算(ブロードキャストなど)を駆使したり、CythonやC拡張を書いたりする必要がありました。

Numbaを使えば、素のPythonループをほぼそのまま書きながら、デコレータを1つ追加するだけでコンパイル済みコードと同等の実行速度を得られます。特に以下のようなケースで威力を発揮します。

  • 画像処理やシグナル処理などのピクセル単位・サンプル単位のループ
  • モンテカルロシミュレーションなど、逐次的な数値計算
  • カスタムの数式評価やベクトル演算がNumPyの標準関数だけでは表現しづらい場合
  • 金融工学のバックテストなど、時系列に沿った逐次計算

一方で、NumbaはPythonの全機能をサポートしているわけではありません。対応しているのは主に数値型(int、float、complex)、NumPy配列、一部のPython標準関数・制御構文に限られます。辞書や文字列処理、pandasのDataFrameを直接扱うような処理には基本的に対応していない(または制限が大きい)点に注意が必要です。

インストール

Numbaはpipまたはcondaでインストールできます。LLVMのバイナリを含む依存関係があるため、conda(特にconda-forgeチャンネル)の利用が推奨されることが多いですが、pipでも問題なくインストールできます。

# pipの場合
pip install numba

# condaの場合(推奨)
conda install -c conda-forge numba

インストール後、以下のコマンドでバージョンやシステム情報を確認できます。

python -c "import numba; print(numba.__version__)"

また、NumbaはNumPyのバージョンに強く依存するため、両者の組み合わせに互換性があるかを事前に確認しておくことが重要です。バージョンの対応表は公式ドキュメントに記載されているので、環境構築時に一度目を通しておくと安心です。

python -c "import numba; numba.parfors" 2>/dev/null && echo "parfors OK"

基本サンプル

1. 最小構成:@njitで関数を高速化する

まずは最も基本的な使い方です。@njitnopython=True@jitと同義)を付けるだけで、関数がコンパイル対象になります。

import numpy as np
from numba import njit
import time

@njit
def sum_squares(arr):
    total = 0.0
    for i in range(arr.shape[0]):
        total += arr[i] ** 2
    return total

# 動作確認
data = np.random.rand(10_000_000)

# 初回呼び出しでコンパイルが走る(ウォームアップ)
start = time.perf_counter()
result = sum_squares(data)
print(f"1回目(コンパイル込み): {time.perf_counter() - start:.4f}秒")

# 2回目以降はコンパイル済みなので高速
start = time.perf_counter()
result = sum_squares(data)
print(f"2回目(実行のみ): {time.perf_counter() - start:.4f}秒")

ポイントは、初回呼び出し時にコンパイルが発生するということです。2回目以降の呼び出しはキャッシュされたマシンコードが使われるため、純粋な実行時間だけを計測できます。ベンチマークを取る際はこの点を必ず考慮してください。

2. NumPyの素朴なループとの速度比較

Numbaの効果を体感するために、素のPythonループとの比較を行います。

import numpy as np
from numba import njit
import time

def sum_squares_python(arr):
    total = 0.0
    for i in range(arr.shape[0]):
        total += arr[i] ** 2
    return total

@njit
def sum_squares_numba(arr):
    total = 0.0
    for i in range(arr.shape[0]):
        total += arr[i] ** 2
    return total

data = np.random.rand(5_000_000)

start = time.perf_counter()
sum_squares_python(data)
print(f"素のPython: {time.perf_counter() - start:.4f}秒")

sum_squares_numba(data)  # ウォームアップ(コンパイル)
start = time.perf_counter()
sum_squares_numba(data)
print(f"Numba: {time.perf_counter() - start:.4f}秒")

環境にもよりますが、素のPythonループが数秒かかる処理が、Numbaでは数ミリ秒〜十数ミリ秒程度まで短縮されることが多く、数十〜100倍程度の高速化が典型的です。

3. parallel=Trueで並列化する

Numbaはprange(parallel range)を使うことで、マルチコアCPUを活用した並列処理も簡単に書けます。

import numpy as np
from numba import njit, prange

@njit(parallel=True)
def parallel_sum(arr):
    total = 0.0
    for i in prange(arr.shape[0]):
        total += arr[i] ** 2
    return total

data = np.random.rand(50_000_000)
result = parallel_sum(data)  # 自動的に複数スレッドで実行される

prangeはforループの範囲がお互いに独立している(データ競合がない)場合にのみ安全に使えます。累積和のように依存関係がある処理には使えないので注意してください。

4. 実務例:移動平均の計算

金融データや時系列データの分析で頻出する移動平均の計算をNumbaで実装する例です。pandasのrolling().mean()より高速に処理できるケースがあります。

import numpy as np
from numba import njit

@njit
def moving_average(arr, window):
    n = arr.shape[0]
    result = np.empty(n - window + 1)
    cumsum = 0.0
    for i in range(window):
        cumsum += arr[i]
    result[0] = cumsum / window

    for i in range(window, n):
        cumsum += arr[i] - arr[i - window]
        result[i - window + 1] = cumsum / window

    return result

prices = np.random.rand(1_000_000) * 100 + 1000
ma20 = moving_average(prices, 20)
print(ma20[:5])

このように、逐次的な差分計算(累積和のスライド)を使うアルゴリズムは、Pythonループのままだと非常に遅くなりがちですが、Numbaと組み合わせることでベクトル化なしでも高速なコードが書けます。

5. @vectorizeでユニバーサル関数(ufunc)を作る

NumPyのnp.sinnp.expのような、要素ごとに適用されるユニバーサル関数を自作したい場合は@vectorizeが便利です。

import numpy as np
from numba import vectorize, float64

@vectorize([float64(float64, float64)])
def custom_activation(x, alpha):
    if x > 0:
        return x
    else:
        return alpha * (np.exp(x) - 1)  # ELU活性化関数風の処理

x = np.linspace(-5, 5, 1000)
result = custom_activation(x, 1.0)

@vectorizeで作った関数はNumPy配列に対してブロードキャストが自動的に適用されるため、機械学習の活性化関数や独自の数式評価などに便利です。

注意点

Numbaを実務で使う際に押さえておくべき落とし穴をまとめます。

1. 対応していないPython機能がある

nopythonモード(@njit)では、Pythonの全機能が使えるわけではありません。特に以下は非対応、または制限があります。

  • 文字列の複雑な操作(フォーマットや正規表現など)
  • 辞書やリストなど、型が混在するコレクション(typed.Dicttyped.Listを使えば一部対応可能)
  • pandasのDataFrameを直接引数に渡すこと(NumPy配列に変換してから渡す必要がある)
  • 独自クラス(@jitclassを使えば対応可能だが制約が多い)

コンパイルエラーが出た場合は、まずどの行が非対応の処理を含んでいるかをエラーメッセージから特定するのが基本の対処法です。

2. 初回コンパイルのオーバーヘッドを忘れない

前述の通り、@njitを付けた関数は初回呼び出し時にコンパイルが走ります。小さいデータに対して一度だけ呼び出すような処理では、コンパイル時間の方が実行時間より長くなり、逆に遅くなることがあります。バッチ処理や繰り返し呼び出される関数にこそ効果を発揮します。

3. cache=Trueでコンパイル結果を永続化する

スクリプトを毎回起動するたびにコンパイルが走るのを避けたい場合は、cache=Trueオプションを使うとコンパイル結果をディスクにキャッシュできます。

@njit(cache=True)
def compute(arr):
    return arr.sum()

4. 型の推論に失敗するとフォールバックが起きる(または例外になる)

nopython=Trueを明示していない@jitデコレータは、型推論に失敗した場合Pythonのオブジェクトモードにフォールバックし、Numbaの恩恵がほとんど得られなくなります。実務では必ず@njitnopython=Trueと同義)を使い、意図せずフォールバックしていないかを確認する習慣をつけましょう。

5. parallel=Trueは常に速くなるわけではない

並列化にはスレッド生成のオーバーヘッドがあるため、データサイズが小さい場合や、ループ内の処理が軽い場合はむしろ遅くなることがあります。実際のデータサイズでベンチマークを取ってから採用を判断してください。

6. デバッグがしづらい

コンパイルされたコードは通常のPythonデバッガ(pdbなど)でステップ実行できません。開発時は@njitを外した状態(あるいはNUMBA_DISABLE_JIT=1環境変数を設定した状態)でロジックを検証し、動作確認が取れてからNumba用に最適化するというワークフローが実務的です。

NUMBA_DISABLE_JIT=1 python your_script.py

FAQ

Q1. NumbaとCythonはどちらを使うべきですか?

用途によります。Numbaは既存のNumPyベースのPythonコードに対してデコレータを追加するだけで高速化できる手軽さが強みで、プロトタイピングや数値計算中心のコードに向いています。一方Cythonは型宣言をより細かく制御でき、Pythonオブジェクトとの相互運用やC/C++ライブラリとの連携に強みがあります。文字列処理やクラスベースの複雑なロジックを含む場合はCython、NumPy配列を使った純粋な数値計算が中心の場合はNumbaを検討するとよいでしょう。

Q2. NumbaはGPUでも使えますか?

はい。numba.cudaモジュールを使うことで、NVIDIA GPU向けにCUDAカーネルをPythonの構文で記述できます。ただし別途CUDA Toolkitのセットアップが必要で、GPU向けのメモリ管理(ホスト・デバイス間のデータ転送など)を意識したコーディングが求められるため、CPU版の@njitよりも学習コストは高くなります。大量データのバッチ処理で明確な高速化ニーズがある場合に検討する価値があります。

Q3. pandasのDataFrameをそのままNumbaの関数に渡せますか?

基本的には推奨されません。nopythonモードはpandasのDataFrameオブジェクトを直接サポートしていないため、df["column"].to_numpy()などでNumPy配列に変換してからNumba関数に渡す設計にするのが定石です。どうしてもDataFrameに対する処理を高速化したい場合は、pandasのapplyにNumba関数を組み合わせる、あるいはpandas.evalやDaskなど他のツールとの比較検討も選択肢に入れるとよいでしょう。


Numbaは「NumPy配列を使った数値計算ループ」という限定された領域において、非常に低コストで高いリターンが得られるツールです。まずは処理時間がボトルネックになっている関数を1つ選び、@njitを付けてみるところから試してみてください。