はじめに:なぜ今NumPy 2.xを学ぶべきなのか
Pythonでデータサイエンスをやっているなら、NumPyは避けて通れない存在ですよね。pandas、scikit-learn、matplotlib、SciPy、TensorFlow、PyTorch——みんなNumPyの配列演算の上に成り立っています。だからこそ、2024年6月にリリースされたNumPy 2.0は、Python科学計算コミュニティ全体にかなりの衝撃を与えました。
2006年以来、実に18年ぶりのメジャーバージョンアップです。
ABI互換性の破壊、型昇格ルールの根本的な変更、100以上のAPIの削除や名前変更——正直、そのインパクトは計り知れません。そして2025年末にリリースされたNumPy 2.4では、フリースレッドPython対応の強化やユーザー定義dtypeの改善、アノテーションの充実など、進化はまだまだ止まりません。
この記事では、NumPy 2.xシリーズ(2.0~2.4)の重要な変更点を実践コード付きで解説していきます。既存の1.xコードをどう移行するか、新機能でどうパフォーマンスを上げるか、フリースレッドPythonとの連携まで、データサイエンティストやMLエンジニアが「今すぐ使える」知識を一通りカバーします。では早速、始めましょう。
環境構築:NumPy 2.4のインストールと確認
まずは最新のNumPy 2.4をインストールしましょう。Python 3.10以上が必要です。
# NumPy 2.4のインストール
pip install numpy==2.4.1
# バージョン確認
python -c "import numpy as np; print(np.__version__); print(np.show_config())"
conda環境を使っている方はこちら。
conda install numpy=2.4.1 -c conda-forge
ちなみに、NumPy 2.xではビルドシステムがsetuptoolsからMesonに移行しています。ビルド時間が大幅に短縮され、クロスコンパイルのサポートも改善されました。pipやcondaでインストールする分には特に意識する必要はないですが、ソースからビルドする場合はMesonが必要になるので、その点だけ頭の片隅に置いておいてください。
破壊的変更その1:名前空間のクリーンアップ(NEP 52)
NumPy 2.0で最も目に見える変更は、メインのnp名前空間から約100個の項目がバッサリ削除されたことです。NumPy 1.xからアップグレードした直後にAttributeErrorが出たら、まず疑うべきはこの変更ですね。
よくある変更一覧
実務で頻繁に遭遇する名前変更・削除をまとめました。
import numpy as np
# NumPy 2.0で削除されたエイリアスと代替
# 旧 → 新
# np.float_ → np.float64
# np.complex_ → np.complex128
# np.string_ → np.bytes_
# np.unicode_ → np.str_
# np.Inf → np.inf
# np.NaN → np.nan
# np.trapz → np.trapezoid
# np.in1d → np.isin
# np.product → np.prod
# np.cumproduct → np.cumprod
# np.sometrue → np.any
# np.alltrue → np.all
# np.mat → np.asmatrix
# 実際のエラー例と対処
try:
result = np.trapz([1, 2, 3, 4])
except AttributeError:
result = np.trapezoid([1, 2, 3, 4])
print(f"台形積分の結果: {result}") # 7.5
Ruffによる自動修正
手動で全ファイルを修正するのは現実的じゃありません。ありがたいことに、RuffリンターのNPY201ルールを使えば、プロジェクト全体を一括で修正できます。
# Ruffのインストール
pip install ruff
# NumPy 2.0の名前空間変更を自動修正
ruff check --select NPY201 --fix .
# 変更内容のプレビュー(ファイルは変更しない)
ruff check --select NPY201 .
この自動修正ツールはNumPy開発チーム自身が提供しているもので、筆者の経験では中規模コードベースの90%以上の問題を自動的に解決してくれました。残りの10%は、プロジェクト固有のカスタムラッパーや動的なAPI呼び出しに関するもので、こちらは手動での確認が必要です。
破壊的変更その2:型昇格ルールの刷新(NEP 50)
NumPy 2.0で最も「罠にはまりやすい」変更がこれです。NEP 50の導入により、型昇格(type promotion)のルールが根本的に変わりました。
何が変わったのか
NumPy 1.xでは、型昇格が「値に依存」していました。つまり、実際の数値の大きさによって結果のdtypeが変わることがあったんです。NumPy 2.0ではこれが「型にのみ依存」するルールに統一されました。
import numpy as np
# 最も典型的な例:float32 + Pythonのfloat
a = np.float32(3.0)
b = 2.0 # Pythonのfloat(float64相当)
result = a + b
print(result.dtype)
# NumPy 1.x: float64(Pythonのfloatの精度が「勝った」)
# NumPy 2.x: float32(Pythonスカラーは「弱い型」として扱われる)
# NumPyスカラー同士:より広い型が勝つ
c = np.float32(3.0)
d = np.float64(2.0)
result = c + d
print(result.dtype) # float64(np.float64の方が「強い」型)
# 配列演算での影響
arr = np.arange(10, dtype=np.float32)
result = arr + 1.5 # PythonのfloatがNumPyのfloat32に適応
print(result.dtype) # float32(NumPy 2.x)、NumPy 1.xではfloat64だった
機械学習での実務的な影響
この変更、GPUメモリに制約のある機械学習ワークフローではかなり大きいです。NumPy 1.xでは、意図せずfloat32からfloat64に昇格してしまい、メモリ使用量が2倍になるバグが頻発していました(個人的にも何度かハマりました)。NumPy 2.xなら、float32の配列にPythonのfloatを足してもfloat32のまま。メモリ効率が格段に良くなります。
# 機械学習での実践例:特徴量の正規化
features = np.random.randn(10000, 256).astype(np.float32)
# NumPy 2.xでは、この演算はfloat32を維持する
mean = features.mean(axis=0) # float32
std = features.std(axis=0) # float32
normalized = (features - mean) / (std + 1e-8) # float32のまま!
print(f"正規化後のdtype: {normalized.dtype}") # float32
print(f"メモリ使用量: {normalized.nbytes / 1024 / 1024:.1f} MB") # ~9.8 MB
ただし、この変更によって既存コードの計算結果が微妙に変わる可能性があります。特に、数値テストで小数点以下の細かい比較をしている場合は要注意です。
新機能:StringDType — 可変長文字列の新時代
NumPy 2.0で導入され、2.2で大幅に改善されたStringDType。これはNumPyにおける文字列データの扱い方を根本的に変える機能です。
従来の問題点
これまでNumPyで文字列を扱うには、固定長のnumpy.str_(UnicodeのU型)を使うか、Pythonオブジェクトとしてobject型の配列に格納するかの2択でした。固定長文字列は最長の文字列に合わせてメモリを確保するため無駄が多く、object型は型安全性やパフォーマンスに問題がありました。どちらを選んでも何かしら妥協が必要だったんですよね。
StringDTypeの使い方
import numpy as np
from numpy.dtypes import StringDType
# StringDTypeで配列を作成
names = np.array(
["田中太郎", "Tanaka", "これは非常に長い名前のテストです"],
dtype=StringDType()
)
print(names.dtype) # StringDType()
print(names) # ['田中太郎' 'Tanaka' 'これは非常に長い名前のテストです']
# 従来の固定長U型との比較
names_fixed = np.array(
["田中太郎", "Tanaka", "これは非常に長い名前のテストです"],
dtype="U"
)
print(names_fixed.dtype) #
欠損値のサポート
StringDTypeは欠損値(NA/NaN)のネイティブサポートも備えています。これ、従来のNumPy文字列型にはなかった機能で、地味にうれしいポイントです。
# 欠損値対応のStringDType
dt = StringDType(na_object=np.nan)
data = np.array(["東京", np.nan, "大阪", "名古屋", np.nan], dtype=dt)
# np.isnanで欠損値を検出
mask = np.isnan(data)
print(f"欠損値マスク: {mask}") # [False True False False True]
# 欠損値を除外したデータ
valid_data = data[~mask]
print(f"有効なデータ: {valid_data}") # ['東京' '大阪' '名古屋']
numpy.stringsモジュール
NumPy 2.0では、文字列操作のための新しいnumpy.strings名前空間も導入されました。ufuncベースなので、高速な文字列操作が可能です。
# numpy.stringsモジュールによる文字列操作
cities = np.array(["tokyo", "osaka", "kyoto"], dtype=StringDType())
# 大文字変換
upper_cities = np.strings.upper(cities)
print(upper_cities) # ['TOKYO' 'OSAKA' 'KYOTO']
# 文字列の長さ
lengths = np.strings.str_len(cities)
print(f"文字列長: {lengths}") # [5 5 5]
# 文字列の検索
contains_o = np.strings.find(cities, "o")
print(f"'o'の位置: {contains_o}") # [1 0 2]
新機能:matvecとvecmat — より直感的な行列ベクトル演算
NumPy 2.2で導入されたnp.matvecとnp.vecmat。行列とベクトルの積を明示的かつ安全に計算するための新しい関数です。
なぜ新しい関数が必要だったのか
従来のnp.matmul(@演算子)は汎用的で便利なんですが、1次元ベクトルとの演算時に暗黙の次元追加が行われるため、意図しない形状変換が起こることがありました。matvecとvecmatはこの問題をスッキリ解消してくれます。
import numpy as np
# 行列とベクトルの準備
A = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=np.float64)
v = np.array([1, 0, -1], dtype=np.float64)
# 行列×ベクトル(matvec)
result_mv = np.matvec(A, v)
print(f"matvec結果: {result_mv}") # [-2. -2. -2.]
# ベクトル×行列(vecmat)
result_vm = np.vecmat(v, A)
print(f"vecmat結果: {result_vm}") # [-6. -6. -6.]
# 従来のmatmulとの比較
result_matmul = A @ v
print(f"matmul結果: {result_matmul}") # [-2. -2. -2.](この場合は同じ)
バッチ処理での威力
matvecとvecmatの真価はバッチ処理で発揮されます。ブロードキャスティングを活用して、複数の行列×ベクトル演算を一度にまとめて実行できるんです。
# バッチ処理:10個の変換行列を10個のベクトルに適用
batch_size = 10
matrices = np.random.randn(batch_size, 3, 3) # 10個の3x3行列
vectors = np.random.randn(batch_size, 3) # 10個の3次元ベクトル
# バッチmatvec:全ペアを一度に計算
results = np.matvec(matrices, vectors)
print(f"バッチ結果の形状: {results.shape}") # (10, 3)
# 一つの行列を全ベクトルに適用(ブロードキャスティング)
single_matrix = np.random.randn(3, 3)
results_broadcast = np.matvec(single_matrix, vectors)
print(f"ブロードキャスト結果の形状: {results_broadcast.shape}") # (10, 3)
ベクトル化とブロードキャスティング:NumPyパフォーマンスの核心
NumPyの真髄はやっぱり「ベクトル化」と「ブロードキャスティング」にあります。Pythonのforループを排除して、C言語レベルの最適化されたコードで配列演算を行うことで、10倍から100倍のパフォーマンス向上が期待できます。ここ、NumPyを使う上で最も重要なセクションかもしれません。
ベクトル化の基本原則
import numpy as np
import time
# 悪い例:Pythonのforループ
def normalize_loop(data):
result = np.empty_like(data)
for i in range(data.shape[0]):
for j in range(data.shape[1]):
result[i, j] = (data[i, j] - data[:, j].mean()) / data[:, j].std()
return result
# 良い例:ベクトル化
def normalize_vectorized(data):
return (data - data.mean(axis=0)) / data.std(axis=0)
# パフォーマンス比較
data = np.random.randn(1000, 50)
start = time.time()
result_loop = normalize_loop(data)
time_loop = time.time() - start
start = time.time()
result_vec = normalize_vectorized(data)
time_vec = time.time() - start
print(f"forループ: {time_loop:.4f}秒")
print(f"ベクトル化: {time_vec:.6f}秒")
print(f"高速化倍率: {time_loop / time_vec:.0f}倍")
ブロードキャスティングのルール
ブロードキャスティングとは、異なる形状の配列間で演算を行う際に、NumPyが自動的に配列の形状を「拡張」してくれる仕組みです。覚えるべきルールは3つだけ。
- ルール1:次元数が異なる場合、少ない方の形状の左側に1を追加して次元数を揃える
- ルール2:特定の次元でサイズが1の場合、もう一方のサイズに合わせて拡張する
- ルール3:いずれの次元でもサイズが異なり、どちらも1でない場合はエラー
import numpy as np
# ブロードキャスティングの実例
# 例1:スカラーと配列
arr = np.array([1, 2, 3, 4])
result = arr * 2 # スカラー2が[2, 2, 2, 2]に拡張される
print(result) # [2 4 6 8]
# 例2:1次元と2次元
matrix = np.array([[1, 2, 3],
[4, 5, 6]]) # 形状(2, 3)
row_vector = np.array([10, 20, 30]) # 形状(3,) → (1, 3)に拡張
result = matrix + row_vector
print(result)
# [[11 22 33]
# [14 25 36]]
# 例3:列ベクトルと行ベクトルの外積的な演算
col = np.array([[1], [2], [3]]) # 形状(3, 1)
row = np.array([10, 20, 30]) # 形状(3,) → (1, 3)
result = col * row # (3, 1) * (1, 3) → (3, 3)
print(result)
# [[ 10 20 30]
# [ 20 40 60]
# [ 30 60 90]]
実践:Z-Score正規化をブロードキャスティングで
# データサイエンスでの典型的な使用例:Z-Score正規化
np.random.seed(42)
dataset = np.random.randn(1000, 5) * np.array([10, 1, 100, 0.1, 50])
# ブロードキャスティングによる一括正規化
mean = dataset.mean(axis=0) # 形状(5,)
std = dataset.std(axis=0) # 形状(5,)
z_scores = (dataset - mean) / std # (1000, 5) - (5,) → ブロードキャスト
print(f"正規化前 - 平均: {dataset.mean(axis=0).round(2)}")
print(f"正規化後 - 平均: {z_scores.mean(axis=0).round(2)}") # ほぼ0
print(f"正規化後 - 標準偏差: {z_scores.std(axis=0).round(2)}") # ほぼ1
パフォーマンス最適化テクニック
NumPy 2.xでは内部最適化もいくつか施されていますが、コードの書き方次第でさらなるパフォーマンス向上が可能です。ここでは実務で役立つテクニックを紹介します。
ビューとコピーの理解
NumPyのメモリ効率を最大化するには、「ビュー」と「コピー」の違いを理解することが不可欠です。これを知っているかどうかで、メモリ使用量が何倍も変わることがあります。
import numpy as np
# ビュー:元の配列とメモリを共有(高速・省メモリ)
arr = np.arange(1000000)
view = arr[::2] # スライスはビューを返す
print(f"ビューはメモリ共有: {np.shares_memory(arr, view)}") # True
# コピー:独立したメモリ領域に複製(遅い・メモリ消費)
copy = arr[::2].copy()
print(f"コピーはメモリ共有: {np.shares_memory(arr, copy)}") # False
# ファンシーインデックスはコピーを返す
fancy = arr[[0, 1, 2, 3]]
print(f"ファンシーインデックスはメモリ共有: {np.shares_memory(arr, fancy)}") # False
np.whereによる条件分岐のベクトル化
# 条件分岐のベクトル化
data = np.random.randn(1000000)
# 悪い例:リスト内包表記
# result_slow = np.array([x if x > 0 else 0 for x in data])
# 良い例:np.where
result_fast = np.where(data > 0, data, 0)
# さらに良い例:np.clipやnp.maximum
result_best = np.maximum(data, 0) # ReLU関数と同じ
# 複数条件の場合はnp.selectが便利
conditions = [
data < -1,
(data >= -1) & (data <= 1),
data > 1
]
choices = [-1, 0, 1]
result_multi = np.select(conditions, choices)
print(f"3値分類結果のユニーク値: {np.unique(result_multi)}") # [-1 0 1]
np.einsumによる高度な配列演算
アインシュタインの縮約記法を使ったnp.einsumは、複雑な配列演算を簡潔かつ高速に記述できる強力なツールです。慣れるまで少し時間がかかりますが、一度覚えると手放せなくなります。
# einsumの実践例
A = np.random.randn(100, 50)
B = np.random.randn(50, 30)
# 行列積
C = np.einsum('ij,jk->ik', A, B)
# バッチ行列積
batch_A = np.random.randn(10, 100, 50)
batch_B = np.random.randn(10, 50, 30)
batch_C = np.einsum('bij,bjk->bik', batch_A, batch_B)
print(f"バッチ行列積の形状: {batch_C.shape}") # (10, 100, 30)
# トレース(対角要素の和)
M = np.random.randn(5, 5)
trace = np.einsum('ii->', M)
print(f"トレース: {trace:.4f}(np.trace: {np.trace(M):.4f})")
# 要素ごとの積の和(内積)
v1 = np.random.randn(100)
v2 = np.random.randn(100)
dot = np.einsum('i,i->', v1, v2)
print(f"内積: {dot:.4f}(np.dot: {np.dot(v1, v2):.4f})")
NumPy 2.2の改善:reprでの形状表示
地味だけど嬉しい改善。NumPy 2.2では、大きな配列のrepr(表示)に形状が明示的に表示されるようになりました。デバッグ時に「この配列、何行何列だっけ?」とわざわざ.shapeを叩く手間が減ります。
# NumPy 2.2+:大きな配列のrepr改善
large_array = np.random.randn(1000, 500)
print(repr(large_array))
# array([[ 0.123, -0.456, ..., -0.789, 0.012],
# [ 0.345, -0.678, ..., 0.901, -0.234],
# ...,
# [-0.567, 0.890, ..., -0.123, 0.456],
# [ 0.789, -0.012, ..., 0.345, -0.678]],
# shape=(1000, 500)) # ← 形状が表示されるようになった
フリースレッドPythonとNumPy:GIL解放の新時代
Python 3.13で実験的に導入され、Python 3.14で正式サポートとなったフリースレッドPython(GILなしPython)。これはNumPyを含むデータサイエンスエコシステム全体に大きな変革をもたらしています。
GIL(Global Interpreter Lock)とは何だったのか
GILは、CPythonインタープリタ内で一度に一つのスレッドしかPythonバイトコードを実行できないようにするロック機構でした。この制約のせいで、マルチスレッドのPythonコードでも真の並列処理ができず、CPUバウンドなタスクではスレッドを増やしても性能が向上しないという根本的な問題がありました。長年Pythonプログラマーを悩ませてきた存在です。
NumPy 2.3+のフリースレッド対応
NumPy 2.1からフリースレッドPythonの実験的サポートが始まり、2.3以降で本格的な対応が進んでいます。特に重要なのは、ufunc演算がスレッドセーフになったこと。複数スレッドから同じufuncを同時に呼び出しても安全に動作します。
import numpy as np
import threading
import time
def compute_intensive(data, result, index):
"""重い計算をシミュレート"""
# フリースレッドPythonでは、これらのufunc演算が
# 真に並列で実行される
result[index] = np.sum(np.sin(data) ** 2 + np.cos(data) ** 2)
# データの準備
n_threads = 4
chunk_size = 2500000
data_chunks = [np.random.randn(chunk_size) for _ in range(n_threads)]
results = [None] * n_threads
# マルチスレッドで計算
start = time.time()
threads = []
for i in range(n_threads):
t = threading.Thread(
target=compute_intensive,
args=(data_chunks[i], results, i)
)
threads.append(t)
t.start()
for t in threads:
t.join()
elapsed = time.time() - start
print(f"マルチスレッド処理時間: {elapsed:.4f}秒")
print(f"結果: {[f'{r:.0f}' for r in results]}")
注意点とベストプラクティス
フリースレッドPythonはワクワクする技術ですが、使う際にはいくつか押さえておくべきポイントがあります。
- object型配列に注意:
dtype=np.object_の配列はGILによる保護がなくなるため、マルチスレッドからのアクセスでデータ競合が発生する可能性があります - シングルスレッド性能の低下:フリースレッドビルドでは、シングルスレッドの性能が約5~10%低下します。マルチスレッドの恩恵がないならば、通常のPythonビルドの方が速いです
- C拡張の互換性:フリースレッド非対応のC拡張をインポートすると、そのプロセスのGILが再有効化されてしまい、フリースレッドの利点が台無しになります
# フリースレッド対応の確認方法
import sys
import sysconfig
# フリースレッドビルドかどうかを確認
is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
print(f"フリースレッドビルド: {is_free_threaded}")
# GILの状態を確認(Python 3.13+)
if hasattr(sys, '_is_gil_enabled'):
print(f"GIL有効: {sys._is_gil_enabled()}")
実践:NumPy 2.xとpandas・scikit-learnの連携
NumPyは単独で使うだけでなく、pandasやscikit-learnとの連携で真価を発揮します。NumPy 2.xの変更がこれらのライブラリとの統合にどう影響するか、具体的に見ていきましょう。
pandasとの連携
pandas 2.2以降はNumPy 2.xと互換性があります。特にPyArrowバックエンドとNumPy 2.xの組み合わせは、メモリ効率とパフォーマンスの両面で大きな改善をもたらしてくれます。
import numpy as np
import pandas as pd
# NumPy配列からpandas DataFrameへの変換
data = np.random.randn(10000, 4).astype(np.float32)
columns = ['特徴量A', '特徴量B', '特徴量C', '特徴量D']
df = pd.DataFrame(data, columns=columns)
# NumPy 2.xの型昇格がpandasにも影響
print(f"DataFrameのdtype: {df.dtypes.unique()}") # float32を維持
# NumPy配列との相互変換
arr = df.to_numpy() # pandas → NumPy
print(f"NumPy配列のdtype: {arr.dtype}") # float32
# pandasの演算でもNumPy 2.xの型ルールが適用される
df['合計'] = df['特徴量A'] + df['特徴量B']
print(f"合計列のdtype: {df['合計'].dtype}") # float32
scikit-learnとの連携
scikit-learn 1.5以降がNumPy 2.x互換です。前処理パイプラインとNumPyのベクトル化を組み合わせれば、効率的な機械学習ワークフローを構築できます。
import numpy as np
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.model_selection import train_test_split
# サンプルデータの生成
np.random.seed(42)
n_samples = 5000
X = np.column_stack([
np.random.normal(100, 15, n_samples), # 特徴量1:正規分布
np.random.exponential(2, n_samples), # 特徴量2:指数分布
np.random.uniform(0, 1, n_samples), # 特徴量3:一様分布
np.random.poisson(5, n_samples).astype(float), # 特徴量4:ポアソン分布
])
y = (X[:, 0] > 100).astype(int) # 簡単な二値分類
# データ分割
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# NumPyベースの前処理
# 方法1:scikit-learnのScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 方法2:NumPyで直接実装(より高速な場合がある)
mean = X_train.mean(axis=0)
std = X_train.std(axis=0)
X_train_manual = (X_train - mean) / std
X_test_manual = (X_test - mean) / std
# 結果の一致を確認
print(f"差異の最大値: {np.abs(X_train_scaled - X_train_manual).max():.2e}")
print(f"訓練データの形状: {X_train_scaled.shape}")
print(f"テストデータの形状: {X_test_scaled.shape}")
NumPy 2.xへの移行チェックリスト
さて、既存のNumPy 1.xプロジェクトを2.xに移行する際の実践的なチェックリストをまとめました。移行を考えている方は、ぜひ参考にしてください。
移行前の準備
- 依存パッケージの互換性確認:pandas ≥ 2.2.0、SciPy ≥ 1.14.0、scikit-learn ≥ 1.5.0、matplotlib ≥ 3.9.0が必要
- テストスイートの整備:型昇格の変更で数値比較のテストが失敗する可能性があるので、テストカバレッジを事前に確認しておく
- Ruffの準備:
pip install ruffで自動修正ツールをインストール
移行の実行
# ステップ1:Ruffで名前空間の変更を自動修正
ruff check --select NPY201 --fix .
# ステップ2:NumPy 2.xをインストール
pip install "numpy>=2.4"
# ステップ3:テストを実行して問題を特定
pytest --tb=short -q
# ステップ4:型昇格関連の問題を手動で修正
# 典型的な修正パターン:
# - np.float_(x) → np.float64(x)
# - 明示的なdtype指定を追加
# - 数値比較のtolerance調整
互換性を維持するコードの書き方
NumPy 1.xと2.xの両方で動作するコードを書きたい場合は、こんなテクニックがあります。
import numpy as np
# バージョン検出
NUMPY_2 = int(np.__version__.split('.')[0]) >= 2
# 互換性のあるコードパターン
def safe_trapezoid(y, x=None):
"""NumPy 1.xと2.xの両方で動作する台形積分"""
if hasattr(np, 'trapezoid'):
return np.trapezoid(y, x)
return np.trapz(y, x)
# もう一つのアプローチ:try/exceptパターン
try:
from numpy import trapezoid
except ImportError:
from numpy import trapz as trapezoid
result = trapezoid([1, 2, 3, 4])
print(f"台形積分: {result}")
実践プロジェクト:NumPy 2.xで時系列データを分析する
ここまで学んだ知識を統合して、実践的な時系列データ分析の例を見てみましょう。NumPy 2.xの新機能とベクトル化テクニックをフル活用します。
import numpy as np
# 1年間の疑似的な売上データを生成
np.random.seed(42)
days = 365
dates = np.arange(days)
# トレンド + 季節性 + ノイズ
trend = 1000 + dates * 2.5
seasonality = 200 * np.sin(2 * np.pi * dates / 365)
weekly_pattern = 100 * np.sin(2 * np.pi * dates / 7)
noise = np.random.normal(0, 50, days)
sales = trend + seasonality + weekly_pattern + noise
print(f"売上データの形状: {sales.shape}")
print(f"売上データのdtype: {sales.dtype}")
print(f"平均売上: {sales.mean():.0f}円")
# 移動平均の計算(ベクトル化)
def moving_average(data, window):
"""NumPyベクトル化による移動平均の計算"""
cumsum = np.cumsum(data)
cumsum[window:] = cumsum[window:] - cumsum[:-window]
return cumsum[window - 1:] / window
ma_7 = moving_average(sales, 7) # 7日移動平均
ma_30 = moving_average(sales, 30) # 30日移動平均
print(f"7日移動平均(最新5日): {ma_7[-5:].round(0)}")
print(f"30日移動平均(最新5日): {ma_30[-5:].round(0)}")
# 前年同期比の計算(ブロードキャスティング活用)
# 月ごとの集計
month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
monthly_indices = np.cumsum([0] + month_lengths)
monthly_sales = np.array([
sales[monthly_indices[i]:monthly_indices[i+1]].sum()
for i in range(12)
])
print(f"\n月別売上合計:")
months = ['1月', '2月', '3月', '4月', '5月', '6月',
'7月', '8月', '9月', '10月', '11月', '12月']
for m, s in zip(months, monthly_sales):
print(f" {m}: {s:,.0f}円")
# 異常値検出(Z-Scoreベース)
z_scores = (sales - sales.mean()) / sales.std()
anomalies = np.where(np.abs(z_scores) > 2.5)[0]
print(f"\n異常値(Z-Score > 2.5)の日数: {len(anomalies)}日")
print(f"異常値の日: {anomalies}")
# 自己相関の計算(ベクトル化)
def autocorrelation(data, max_lag=30):
"""NumPyベクトル化による自己相関の計算"""
n = len(data)
data_centered = data - data.mean()
variance = np.sum(data_centered ** 2)
acf = np.array([
np.sum(data_centered[:n-lag] * data_centered[lag:]) / variance
for lag in range(max_lag + 1)
])
return acf
acf = autocorrelation(sales, max_lag=14)
print(f"\n自己相関(ラグ0-14):")
for lag, corr in enumerate(acf):
bar = '█' * int(abs(corr) * 20)
sign = '+' if corr >= 0 else '-'
print(f" ラグ{lag:2d}: {sign}{bar} ({corr:.3f})")
NumPy 2.4の追加改善点
2025年12月にリリースされたNumPy 2.4には、ここまで紹介した機能以外にもいくつか注目すべき改善が含まれています。
same_valueキャスティング
新しいキャスティングオプション'same_value'が追加されました。「値が変わらない場合にのみキャストを許可する」という、安全志向のキャスティングモードです。
# same_valueキャスティングの例
import numpy as np
# 値が保持される変換は成功する
arr_int = np.array([1, 2, 3], dtype=np.int32)
arr_float = arr_int.astype(np.float64, casting='same_value')
print(f"int32 → float64: {arr_float}") # [1. 2. 3.]
# 精度が失われる変換はエラーになる
arr_big = np.array([2**53 + 1], dtype=np.int64)
try:
arr_f = arr_big.astype(np.float64, casting='same_value')
except TypeError as e:
print(f"キャスティングエラー: 値の精度が失われるため拒否")
x86 CPUの最適化グループ
NumPy 2.4では、x86 CPUの機能がマイクロアーキテクチャベースのグループに再編成されました。Linuxディストリビューションの標準やGoogle Highwayの要件に沿った最適化が自動的に適用されます。レガシーなAMD機能(XOP、FMA4)やIntel Xeon Phiのサポートも削除され、コードベースがすっきりしています。
アノテーションの改善
型アノテーションも大幅に改善されています。NumPy 2.2で改善されたfloat64とcomplex128の型アノテーションに加え、ランタイムでのシグネチャ内省が強化され、より多くの関数で正確な型情報が得られるようになりました。
# NumPy 2.2+の型アノテーション改善
import numpy as np
# float64がbuilt-in floatのサブタイプとして認識される
x: float = np.float64(6.28) # 静的型チェッカーでエラーにならない
# complex128がbuilt-in complexのサブタイプとして認識される
z: complex = np.complex128(1 + 2j) # 同様にエラーにならない
print(f"x = {x}, type: {type(x)}")
print(f"z = {z}, type: {type(z)}")
print(f"isinstance(x, float): {isinstance(x, float)}") # True
np.zerosとLinuxヒュージページ
NumPy 2.2では、Linuxシステムにおける大きなnp.zerosの割り当てでヒュージページ(hugepages)が使用されるようになりました。大規模なデータ処理を行うLinux環境では、メモリアクセスのパフォーマンスが目に見えて向上します。
# 大きな配列の割り当てパフォーマンス
import numpy as np
import time
# 大きなゼロ配列の割り当て(Linuxではヒュージページが使用される)
start = time.time()
large_zeros = np.zeros((10000, 10000), dtype=np.float64)
elapsed = time.time() - start
print(f"10000x10000のゼロ配列割り当て: {elapsed:.6f}秒")
print(f"メモリ使用量: {large_zeros.nbytes / 1024 / 1024:.1f} MB")
まとめ:NumPy 2.xが切り拓くデータサイエンスの未来
NumPy 2.xは、18年ぶりのメジャーバージョンアップにふさわしい、Pythonデータサイエンスの基盤を大きく進化させるリリースです。最後に、主要なポイントを振り返っておきましょう。
- 名前空間のクリーンアップ(NEP 52):約100項目の削除でAPIがすっきり。Ruffの
NPY201ルールで自動移行が可能 - 型昇格の統一(NEP 50):値ではなく型に基づく一貫した昇格ルールで、特にMLワークフローでのメモリ効率が向上
- StringDType:可変長文字列のネイティブサポートで、テキストデータの扱いが劇的に改善
- matvec / vecmat:より安全で直感的な行列ベクトル演算が可能に
- フリースレッドPython対応:GILのないPythonでの真の並列処理に向けた準備が着実に進行中
- パフォーマンス改善:ヒュージページ、高速な属性ルックアップ、改善されたメモリ管理など、内部最適化も多数
既存プロジェクトの移行は、Ruffの自動修正ツールを活用すれば思ったよりスムーズにいきます。2026年初頭の現在、主要なデータサイエンスパッケージはすべてNumPy 2.x互換になっており、新規プロジェクトでNumPy 2.4を採用しない理由はほとんどないでしょう。
NumPyの進化はPythonデータサイエンスの進化そのものです。ベクトル化、ブロードキャスティング、新しいdtypeシステム——これらの基本を確実に身につけて、より効率的なコードを書いていきましょう。