NumPyで広がるPythonの可能性:機械学習・科学計算の基盤を学ぶ

NumPy(ナムパイ、またはナンパイ)は、Pythonプログラミング言語における数値計算の基盤となる非常に重要なライブラリです。Numerical Pythonの略であり、大規模な多次元配列や行列の効率的な操作、そしてそれらに対する高度な数学関数を提供します。データサイエンス、機械学習、科学技術計算といった分野では、ほぼ標準ライブラリとして扱われています。

Pythonは柔軟性の高い動的型付け言語ですが、その性質上、純粋なPythonコードでの数値計算はC言語やJavaのような静的型付け言語に比べて実行速度が遅くなる傾向があります。NumPyはこの問題を解決するために開発されました。内部実装の多くがC言語やFortranで書かれており、最適化されたアルゴリズムによって高速な計算を実現します。

NumPyの歴史は、1995年にJim Huguninらによって開発されたNumericというライブラリに遡ります。その後、Numarrayという競合ライブラリも登場しましたが、2005年にTravis Oliphantがこれら二つのライブラリの機能を取り込み、大幅な改良を加えてNumPyとして統合しました。NumPy 1.0は2006年にリリースされ、以降、オープンソースプロジェクトとして多くの開発者コミュニティによって活発に開発が続けられています。NumPyは、非営利団体NumFOCUSによって財政的に支援されています。

ポイント:

  • Pythonでの高速な数値計算を実現するライブラリ。
  • 多次元配列オブジェクト `ndarray` が中核。
  • データサイエンス、機械学習の分野で必須のツール。
  • 内部はC言語などで実装されており、非常に高速。
  • Pandas, SciPy, Matplotlib, scikit-learnなど多くのライブラリの基盤となっている。

NumPyの主な特徴

NumPyが広く使われる理由は、その強力な機能群にあります。

1. 高速で効率的な多次元配列: `ndarray`

NumPyの中心的な機能は、`ndarray` (N-dimensional array) と呼ばれる多次元配列オブジェクトです。これは、同じデータ型の要素を持つグリッド状のデータ構造です。Python標準のリストとは異なり、`ndarray`はメモリ上で連続した領域にデータが格納されるため、アクセスや演算が非常に高速です。また、要素のデータ型を固定することで、メモリ使用量も最適化されます。

2. ベクトル化された演算 (Vectorization) とユニバーサル関数 (ufunc)

NumPyでは、配列の要素ごとに対する繰り返し処理(ループ)を明示的に書かなくても、配列全体に対する演算を簡潔に記述できます。これをベクトル化と呼びます。例えば、配列の全要素に特定の値を加算する場合、Pythonのリストではループが必要ですが、NumPyでは配列とスカラー値の加算演算子(`+`)を使うだけで済みます。

このベクトル化を実現しているのが、ユニバーサル関数(ufunc)です。`np.sin`, `np.exp`, `np.add` など、要素ごとの演算を行う関数が多数用意されており、これらは内部的に最適化されたC言語のループで実行されるため、Pythonのループよりも桁違いに高速です。


import numpy as np
import time

# Pythonリストでの要素ごとの加算
list_a = list(range(1000000))
list_b = list(range(1000000))

start_time = time.time()
result_list = [a + b for a, b in zip(list_a, list_b)]
end_time = time.time()
print(f"Pythonリストでの処理時間: {end_time - start_time:.6f} 秒")

# NumPy配列での要素ごとの加算
array_a = np.arange(1000000)
array_b = np.arange(1000000)

start_time = time.time()
result_array = array_a + array_b # ベクトル化された演算
end_time = time.time()
print(f"NumPy配列での処理時間: {end_time - start_time:.6f} 秒")
      

上記のコード例からもわかるように、NumPyのベクトル化演算は非常に高速です。

3. ブロードキャスティング (Broadcasting)

形状(shape)が異なる配列同士でも、特定のルールに従って自動的に形状を揃えて演算を実行する機能です。これにより、明示的な形状変更やループ処理なしに、異なるサイズの配列間で効率的に演算を行えます。例えば、配列の各行に同じベクトルを加算するような場合に便利です。

ブロードキャストのルールは以下の通りです。

  • ルール1: 2つの配列の次元数が異なる場合、次元数が少ない方の配列の形状の先頭に1を追加して次元数を揃える。
  • ルール2: ある次元で2つの配列のサイズが異なる場合、どちらかのサイズが1であれば、その次元に沿ってサイズが大きい方と同じになるように要素がコピー(仮想的に拡張)される。
  • ルール3: 全ての次元でサイズが一致するか、どちらかが1であればブロードキャスト可能。それ以外の場合はエラーとなる。

import numpy as np

# 2x3の配列
a = np.array([[1, 2, 3],
              [4, 5, 6]])
# 1x3の配列 (ベクトル)
b = np.array([10, 20, 30])

# ブロードキャスティングによる加算
# bが自動的に [[10, 20, 30], [10, 20, 30]] のように拡張されて計算される
c = a + b
print(c)
# 出力:
# [[11 22 33]
#  [14 25 36]]
      

4. 豊富な数学関数ライブラリ

線形代数 (`numpy.linalg`)、フーリエ変換 (`numpy.fft`)、乱数生成 (`numpy.random`) など、科学技術計算に必要な高度な数学関数が豊富に用意されています。これらも最適化されており、高速に動作します。

5. 他のライブラリとの連携

NumPyは、Pandas(データ解析)、SciPy(科学技術計算)、Matplotlib(データ可視化)、scikit-learn(機械学習)など、Pythonの主要なデータサイエンス系ライブラリの多くで基盤として利用されています。これらのライブラリはNumPyの配列をデータ構造として受け入れるため、シームレスな連携が可能です。

6. C/C++/Fortranコードとの連携

NumPyは、既存のC、C++、Fortranなどで書かれた高速な数値計算コードをPythonから呼び出すためのインターフェースも提供しており、パフォーマンスが重要な部分を既存のコードで補うことも可能です。

NumPyのインストール

NumPyを利用するには、まずPython環境にNumPyをインストールする必要があります。Python本体がインストールされていることが前提です。

最も一般的な方法は、Pythonのパッケージインストーラである `pip` を使う方法と、データサイエンス環境構築によく使われる `conda` を使う方法です。

pip を使用する場合

ターミナル(コマンドプロンプト)を開き、以下のコマンドを実行します。


pip install numpy
      

特定のバージョンを指定してインストールしたい場合は、以下のようにします。


pip install numpy==1.26.0 # 例: バージョン1.26.0をインストール
      

仮想環境(`venv` など)を作成し、その中でインストールすることが推奨されます。これにより、プロジェクトごとに異なるバージョンのライブラリを管理できます。

conda を使用する場合

Anaconda Distribution や Miniforge を利用している場合は、`conda` コマンドでインストールできます。

まず、専用の環境を作成し、アクティベートします(推奨)。


conda create -n my_numpy_env python=3.11 # 環境を作成 (名前は任意、Pythonバージョンも指定可能)
conda activate my_numpy_env         # 環境をアクティベート
      

そして、NumPyをインストールします。`conda-forge` チャンネルからのインストールが推奨されることもあります。


# デフォルトチャンネルからインストール
conda install numpy

# または conda-forge チャンネルからインストール (推奨されることが多い)
conda install -c conda-forge numpy
      

`conda` はNumPyだけでなく、依存関係にある他の非Pythonライブラリ(例: BLAS/LAPACKなどの線形代数ライブラリ)も一緒に管理してくれる利点があります。

インストールの確認

インストールが成功したか確認するには、Pythonインタプリタを起動し、以下のように入力します。


import numpy as np
print(np.__version__)
      

エラーが出ずに、インストールされたNumPyのバージョン番号が表示されれば成功です。慣習として、`numpy` は `np` という別名でインポートされます。

NumPyの基本的な使い方: `ndarray` の操作

NumPyの核心である `ndarray` の基本的な作成方法と操作について見ていきましょう。

1. 配列の作成

様々な方法で `ndarray` を作成できます。

  • Pythonリストやタプルから作成: `np.array()`
  • 
    import numpy as np
    
    # 1次元配列 (ベクトル)
    list_data = [1, 2, 3, 4, 5]
    arr1d = np.array(list_data)
    print(arr1d) # [1 2 3 4 5]
    
    # 2次元配列 (行列)
    list2d_data = [[1, 2, 3], [4, 5, 6]]
    arr2d = np.array(list2d_data)
    print(arr2d)
    # [[1 2 3]
    #  [4 5 6]]
              
  • 特定の値で埋められた配列を作成: `np.zeros()`, `np.ones()`, `np.full()`
  • 
    # 全て0の配列 (3x4行列)
    zeros_arr = np.zeros((3, 4))
    print(zeros_arr)
    
    # 全て1の配列 (要素数5のベクトル)
    ones_arr = np.ones(5)
    print(ones_arr)
    
    # 指定した値で埋める (2x2行列を7で埋める)
    full_arr = np.full((2, 2), 7)
    print(full_arr)
              
  • 連続する値の配列を作成: `np.arange()`, `np.linspace()`
  • 
    # 0から9までの整数配列 (Pythonのrangeに似ている)
    range_arr = np.arange(10)
    print(range_arr) # [0 1 2 3 4 5 6 7 8 9]
    
    # 2から10まで、ステップ2で増加
    range_step_arr = np.arange(2, 11, 2)
    print(range_step_arr) # [ 2  4  6  8 10]
    
    # 0から1までの範囲を5等分した値の配列
    linspace_arr = np.linspace(0, 1, 5)
    print(linspace_arr) # [0.   0.25 0.5  0.75 1.  ]
              
  • 単位行列を作成: `np.eye()`
  • 
    # 3x3の単位行列
    identity_matrix = np.eye(3)
    print(identity_matrix)
    # [[1. 0. 0.]
    #  [0. 1. 0.]
    #  [0. 0. 1.]]
              
  • 乱数で配列を作成: `np.random.rand()`, `np.random.randn()`, `np.random.randint()` (詳細は後述)

2. 配列の属性

`ndarray` オブジェクトは、自身の情報を持つ属性を持っています。


arr = np.array([[1, 2, 3], [4, 5, 6]])

print(f"次元数 (ndim): {arr.ndim}")     # 2
print(f"形状 (shape): {arr.shape}")     # (2, 3) -> 2行3列
print(f"全要素数 (size): {arr.size}")     # 6
print(f"データ型 (dtype): {arr.dtype}") # int64 (環境による)
      
  • `ndim`: 配列の次元数。ベクトルなら1、行列なら2。
  • `shape`: 各次元の要素数をタプルで表したもの。
  • `size`: 配列に含まれる全要素の数。`shape` の各要素の積に等しい。
  • `dtype`: 配列の要素のデータ型 (`int32`, `int64`, `float64`, `bool_` など)。NumPy配列は基本的に同じ型の要素しか持てない。

データ型は、メモリ使用量や計算精度に影響します。`np.array()` 作成時や `astype()` メソッドで指定・変換できます。


# float型で配列を作成
float_arr = np.array([1, 2, 3], dtype=np.float64)
print(float_arr.dtype) # float64

# 整数型配列をfloat型に変換
int_arr = np.arange(5)
float_arr_converted = int_arr.astype(np.float64)
print(float_arr_converted)      # [0. 1. 2. 3. 4.]
print(float_arr_converted.dtype) # float64
      

3. インデックス参照とスライシング

配列の特定の部分要素や部分配列にアクセスする方法です。

  • インデックス参照 (Indexing): 特定の要素にアクセスします。インデックスは0から始まります。
  • 
    arr1d = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
    print(arr1d[0])   # 0 (最初の要素)
    print(arr1d[5])   # 5 (6番目の要素)
    print(arr1d[-1])  # 9 (最後の要素)
    
    arr2d = np.array([[1, 2, 3], [4, 5, 6]])
    print(arr2d[0, 1]) # 2 (1行目、2列目の要素)
    print(arr2d[1, -1]) # 6 (2行目、最後の列の要素)
    # または arr2d[1][-1] のようにも書ける
              
  • スライシング (Slicing): 配列の一部を切り出して、新しい配列(ビュー)を取得します。`start:stop:step` の形式で指定します。`stop` のインデックスは含まれません。
  • 
    arr1d = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
    print(arr1d[2:5])    # [2 3 4] (インデックス2から4まで)
    print(arr1d[:3])     # [0 1 2] (最初からインデックス2まで)
    print(arr1d[5:])     # [5 6 7 8 9] (インデックス5から最後まで)
    print(arr1d[::2])    # [0 2 4 6 8] (最初から最後まで、ステップ2)
    
    arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    # [[1 2 3]
    #  [4 5 6]
    #  [7 8 9]]
    print(arr2d[:2, 1:]) # 最初の2行、かつ2列目以降
    # [[2 3]
    #  [5 6]]
    print(arr2d[1, :])   # 2行目全体 -> [4 5 6]
    print(arr2d[:, 2])   # 3列目全体 -> [3 6 9]
              

    注意: NumPyのスライシングで得られる配列は、元の配列の「ビュー(view)」であることが多いです。ビューは元の配列とデータを共有しているため、ビューを変更すると元の配列も変更されます。完全に独立したコピーが必要な場合は `.copy()` メソッドを使用します。

    
    arr = np.arange(5) # [0 1 2 3 4]
    arr_slice_view = arr[1:4] # [1 2 3] (ビュー)
    arr_slice_view[0] = 99 # ビューを変更
    print(arr) # [ 0 99  2  3  4] (元の配列も変更されている!)
    
    arr_slice_copy = arr[1:4].copy() # コピーを作成
    arr_slice_copy[0] = 111 # コピーを変更
    print(arr) # [ 0 99  2  3  4] (元の配列は変更されない)
              
  • ブールインデックス参照 (Boolean Indexing): 条件に基づいて要素を選択します。
  • 
    names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
    data = np.random.randn(7, 4) # 7x4の正規乱数配列
    
    # namesが'Bob'と等しいかどうかのブール配列
    is_bob = (names == 'Bob')
    print(is_bob) # [ True False False  True False False False]
    
    # is_bobがTrueに対応するdataの行を選択
    print(data[is_bob])
    
    # namesが'Bob'でない行を選択
    print(data[names != 'Bob'])
    
    # 複数の条件 (OR: | , AND: &)
    mask = (names == 'Bob') | (names == 'Will') # BobまたはWill
    print(data[mask])
    
    # 条件を使って値を変更
    data[data < 0] = 0 # dataの負の値を全て0にする
    print(data)
    
    data[names != 'Joe'] = 7 # namesが'Joe'でない行のdataを全て7にする
    print(data)
              
  • ファンシーインデックス参照 (Fancy Indexing): 整数の配列を使って要素を選択します。コピーが返されます。
  • 
    arr = np.empty((8, 4)) # 8x4の未初期化配列
    for i in range(8):
        arr[i] = i
    
    # 特定の行を選択 (4行目、3行目、0行目、6行目の順)
    print(arr[[4, 3, 0, 6]])
    
    # 負のインデックスも使用可能
    print(arr[[-3, -5, -7]]) # 後ろから3行目、5行目、7行目
    
    # 複数のインデックス配列を渡す (行と列のインデックスを指定)
    arr = np.arange(32).reshape((8, 4))
    # [[ 0  1  2  3]
    #  [ 4  5  6  7]
    #  ...
    #  [28 29 30 31]]
    
    # (1,0), (5,3), (7,1), (2,2) の要素を選択
    print(arr[[1, 5, 7, 2], [0, 3, 1, 2]]) # [ 4 23 29 10]
    
    # 特定の行を特定の列の順序で選択
    # 行インデックス: [1, 5, 7, 2]
    # 列インデックス: [0, 3, 1, 2] -> (1,0), (5,3), (7,1), (2,2) の要素を取得
    print(arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]) # 行を選択してから、その結果の列を選択
    # [[ 4  7  5  6]
    #  [20 23 21 22]
    #  [28 31 29 30]
    #  [ 8 11  9 10]]
              

4. 配列の演算

NumPy配列では、要素ごとの算術演算が簡単に行えます。


arr1 = np.array([[1., 2., 3.], [4., 5., 6.]])
arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])

# 要素ごとの加算
print(arr1 + arr2)
# [[ 1.  6.  4.]
#  [11.  7. 18.]]

# 要素ごとの減算
print(arr1 - arr2)
# [[ 1. -2.  2.]
#  [-3.  3. -6.]]

# 要素ごとの乗算
print(arr1 * arr2)
# [[ 0.  8.  3.]
#  [28. 10. 72.]]

# 要素ごとの除算
print(arr1 / arr2) # 0除算はinf (無限大) や nan (非数) になることがある

# 要素ごとのべき乗
print(arr1 ** 2)
# [[ 1.  4.  9.]
#  [16. 25. 36.]]

# スカラーとの演算 (ブロードキャスト)
print(arr1 * 2)
print(1 / arr1)
print(arr1 ** 0.5) # 平方根
      

配列同士の比較も要素ごとに行われ、ブール配列を返します。


arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([[1, 0], [3, 5]])

print(arr1 == arr2)
# [[ True False]
#  [ True False]]

print(arr1 > arr2)
# [[False  True]
#  [False False]]
      

5. 配列の形状変更

配列の形状を変更する関数も用意されています。

  • `reshape()`: 要素数を変えずに形状を変更します。元の配列のビューを返すことが多いです。
  • 
    arr = np.arange(12) # [ 0  1  2  3  4  5  6  7  8  9 10 11]
    reshaped_arr = arr.reshape((3, 4)) # 3行4列に変更
    print(reshaped_arr)
    # [[ 0  1  2  3]
    #  [ 4  5  6  7]
    #  [ 8  9 10 11]]
              
  • `T` 属性 または `transpose()` メソッド: 配列の転置(行と列を入れ替える)を行います。ビューを返します。
  • 
    arr = np.arange(6).reshape((2, 3))
    # [[0 1 2]
    #  [3 4 5]]
    print(arr.T)
    # [[0 3]
    #  [1 4]
    #  [2 5]]
    print(arr.transpose()) # 同様の結果
              
  • `flatten()` と `ravel()`: 多次元配列を1次元配列に変換します。`flatten()` は常にコピーを返し、`ravel()` は可能な場合はビューを返します。
  • 
    arr2d = np.array([[1, 2], [3, 4]])
    flat_copy = arr2d.flatten()
    flat_view = arr2d.ravel()
    print(flat_copy) # [1 2 3 4]
    print(flat_view) # [1 2 3 4]
              

NumPyの応用機能

基本的な操作に加えて、NumPyはさらに高度な機能を提供します。

1. ユニバーサル関数 (ufunc) 再訪

前述の通り、ufuncは要素ごとの演算を高速に行う関数です。算術演算(`np.add`, `np.subtract`, `np.multiply`, `np.divide`, `np.power`など)、三角関数(`np.sin`, `np.cos`, `np.tan`)、指数・対数関数(`np.exp`, `np.log`, `np.log10`)、平方根(`np.sqrt`)、比較演算(`np.maximum`, `np.minimum`, `np.equal`, `np.greater`など)、丸め処理(`np.round`, `np.floor`, `np.ceil`)など、非常に多くの種類があります。


arr = np.arange(1, 6) # [1 2 3 4 5]

print(np.sqrt(arr))     # 平方根
print(np.exp(arr))      # 指数関数 e^x
print(np.log(arr))      # 自然対数

x = np.random.randn(5) # 正規乱数
y = np.random.randn(5)
print(np.maximum(x, y)) # 要素ごとに大きい方を選択

arr_float = np.random.randn(4) * 5
print(arr_float)
# 小数部分と整数部分に分割
remainder, whole_part = np.modf(arr_float)
print(f"Remainder: {remainder}")
print(f"Whole part: {whole_part}")
      

集約系のufuncもあります。これらは配列全体や特定の軸に沿って計算を行います。

  • `np.sum()`: 合計
  • `np.mean()`: 平均値
  • `np.std()`: 標準偏差
  • `np.var()`: 分散
  • `np.min()`, `np.max()`: 最小値、最大値
  • `np.argmin()`, `np.argmax()`: 最小値、最大値のインデックス
  • `np.cumsum()`: 累積和
  • `np.cumprod()`: 累積積
  • `np.any()`, `np.all()`: ブール配列に対して、少なくとも1つTrueがあるか、全てTrueか

arr = np.random.randn(3, 4) # 3x4の正規乱数配列
print(arr)

print(f"全要素の合計: {np.sum(arr)}")
print(f"列ごとの合計 (axis=0): {np.sum(arr, axis=0)}")
print(f"行ごとの平均 (axis=1): {np.mean(arr, axis=1)}")

print(f"最大値: {np.max(arr)}")
print(f"最大値のインデックス (フラット化した場合): {np.argmax(arr)}")

bool_arr = np.array([False, True, False])
print(f"少なくとも1つTrue?: {np.any(bool_arr)}") # True
print(f"全てTrue?: {np.all(bool_arr)}")       # False
      

2. 線形代数 (`numpy.linalg`)

行列積、逆行列、行列式、固有値問題、特異値分解など、線形代数の基本的な演算を行う関数群です。

  • `np.dot()`: 行列積(またはベクトルの内積)。`@` 演算子も使用可能 (Python 3.5以降)。
  • `np.linalg.inv()`: 逆行列
  • `np.linalg.det()`: 行列式
  • `np.linalg.eig()`: 固有値と固有ベクトル
  • `np.linalg.svd()`: 特異値分解
  • `np.linalg.solve()`: 線形方程式 Ax = b を解く
  • `np.trace()`: 対角成分の和(トレース)

import numpy.linalg as LA

A = np.array([[1., 2.], [3., 4.]])
b = np.array([5., 6.])

# 行列積
print(A @ A) # または np.dot(A, A)

# 逆行列
A_inv = LA.inv(A)
print(A_inv)

# 行列式
print(LA.det(A))

# 固有値、固有ベクトル
eigenvalues, eigenvectors = LA.eig(A)
print(f"Eigenvalues: {eigenvalues}")
print(f"Eigenvectors:\n{eigenvectors}")

# 線形方程式 Ax = x を解く (x = A^-1 b)
x = LA.solve(A, b)
print(f"Solution x: {x}")
# 検算
print(np.allclose(A @ x, b)) # True (ほぼ等しいか確認)
      

3. 乱数生成 (`numpy.random`)

様々な確率分布に従う乱数を生成する機能を提供します。シミュレーションや機械学習の初期化などで広く使われます。

  • `np.random.rand(d0, d1, …, dn)`: 0以上1未満の一様分布に従う乱数を指定された形状で生成。
  • `np.random.randn(d0, d1, …, dn)`: 平均0、標準偏差1の標準正規分布(ガウス分布)に従う乱数を指定された形状で生成。
  • `np.random.randint(low, high=None, size=None, dtype=int)`: 指定された範囲(`low` 以上 `high` 未満)の整数乱数を生成。`high` が省略されると0以上 `low` 未満。
  • `np.random.normal(loc=0.0, scale=1.0, size=None)`: 平均 `loc`、標準偏差 `scale` の正規分布に従う乱数を生成。
  • `np.random.uniform(low=0.0, high=1.0, size=None)`: 指定された範囲(`low` 以上 `high` 未満)の一様分布に従う乱数を生成。
  • `np.random.seed(seed=None)`: 乱数生成器のシード(種)を設定。同じシードを使えば、常に同じ乱数列が生成されるため、結果の再現性を確保したい場合に使う。
  • `np.random.shuffle(x)`: 配列 `x` の要素をランダムに並び替える(インプレース操作)。
  • `np.random.permutation(x)`: 配列 `x` の要素をランダムに並び替えたコピーを返す。
  • `np.random.choice(a, size=None, replace=True, p=None)`: 配列 `a` からランダムに要素を選択。`replace=True` で復元抽出、`p` で各要素の選択確率を指定可能。

# シードを設定して再現性を確保
np.random.seed(12345)

# 2x3の0-1一様乱数
print(np.random.rand(2, 3))

# 3x3の標準正規分布乱数
print(np.random.randn(3, 3))

# 1から10までの整数乱数を5つ
print(np.random.randint(1, 11, size=5))

# 平均10, 標準偏差2の正規分布に従う乱数 10個
print(np.random.normal(loc=10, scale=2, size=10))

# 配列のシャッフル
arr = np.arange(10)
np.random.shuffle(arr)
print(arr)

# 配列から3つランダムに選択 (非復元抽出)
print(np.random.choice(arr, size=3, replace=False))
      

4. ファイル入出力

NumPy配列をファイルに保存したり、ファイルから読み込んだりする機能もあります。

  • `np.save(file, arr)`: 配列をNumPy独自のバイナリ形式(`.npy`)で保存。
  • `np.load(file)`: `.npy` または `.npz` ファイルから配列を読み込む。
  • `np.savez(file, name1=arr1, name2=arr2, …)`: 複数の配列を圧縮されたアーカイブ形式(`.npz`)で保存。
  • `np.savetxt(fname, X, fmt=’%.18e’, delimiter=’ ‘, …)`: 配列をテキストファイル(CSVなど)として保存。
  • `np.loadtxt(fname, dtype=float, delimiter=None, …)`: テキストファイルから配列を読み込む。

arr1 = np.arange(10)
arr2 = np.random.randn(3, 3)

# .npy形式で保存
np.save('some_array.npy', arr1)
# 読み込み
loaded_arr1 = np.load('some_array.npy')
print(loaded_arr1)

# .npz形式で複数の配列を保存
np.savez('array_archive.npz', a=arr1, b=arr2)
# 読み込み
archive = np.load('array_archive.npz')
print(archive['a'])
print(archive['b'])

# テキストファイル(CSV)として保存
arr_to_save = np.arange(12).reshape(3, 4)
np.savetxt('array.csv', arr_to_save, delimiter=',', fmt='%d') # 整数として保存
# テキストファイルから読み込み
loaded_from_txt = np.loadtxt('array.csv', delimiter=',', dtype=int)
print(loaded_from_txt)
      

NumPyのパフォーマンスとエコシステム

なぜNumPyは速いのか?

NumPyがPythonの標準リストに比べて高速な理由はいくつかあります。

  • 静的型付けと連続メモリ配置: NumPy配列 (`ndarray`) は、全ての要素が同じデータ型(例: `int64`, `float64`)でなければなりません。これにより、各要素がメモリ上で固定サイズとなり、連続したメモリブロックに効率的に配置できます。一方、Pythonのリストは異なる型のオブジェクトへのポインタを格納するため、メモリ配置が不連続になりがちで、要素へのアクセスに追加のオーバーヘッドが発生します。
  • C言語による実装: NumPyのコアな演算(特にufuncや線形代数演算)は、高度に最適化されたC言語(またはFortran)のコードで実装されています。これにより、Pythonインタープリタのオーバーヘッドを回避し、CPUの計算能力を最大限に活用できます。
  • ベクトル化とキャッシュ効率: ufuncなどのベクトル化された操作は、データを小さなチャンクに分割し、CPUのキャッシュメモリを効率的に利用するように設計されています。連続したメモリ領域に対する一括操作は、キャッシュヒット率を高め、メモリアクセス時間を短縮します。
  • BLAS/LAPACKの利用: 線形代数演算(`numpy.linalg`)では、OpenBLAS, MKL, ATLASといった最適化されたBLAS (Basic Linear Algebra Subprograms) や LAPACK (Linear Algebra PACKage) ライブラリを利用できる場合があり、これによりハードウェアレベルでのさらなる高速化が図られています。

簡単に言えば、NumPyは「型を固定し、メモリを整理し、計算の多くを高速なC言語コードに任せる」ことで、Pythonの柔軟性を保ちつつ、数値計算のパフォーマンスを大幅に向上させています。

PythonデータサイエンスエコシステムにおけるNumPyの位置づけ

NumPyは単独で強力なライブラリですが、その真価はPythonの広範なデータサイエンスエコシステムにおける基盤としての役割にあります。

ライブラリ 主な役割 NumPyとの関係
Pandas データ操作・解析、表形式データ処理(DataFrame, Series) DataFrameやSeriesの内部データ構造としてNumPyの`ndarray`を利用。NumPyの関数や操作を多く統合・拡張している。データの前処理や整理に不可欠。
SciPy 科学技術計算(最適化、積分、信号処理、統計など) NumPyを基盤とし、より高度で専門的な科学技術計算アルゴリズムを提供。NumPy配列を入力として受け付ける関数が多い。
Matplotlib / Seaborn データ可視化、グラフ描画 NumPy配列を直接プロットデータとして受け付ける。データの視覚的分析に用いられる。SeabornはMatplotlibをベースに、より美しい統計グラフを作成しやすくする。
scikit-learn 機械学習(分類、回帰、クラスタリング、次元削減、モデル評価など) 機械学習アルゴリズムへの入力データとしてNumPy配列を標準的に使用。データの前処理やモデルの学習・評価にNumPyの機能が広く活用される。
TensorFlow / PyTorch / Keras ディープラーニング(深層学習) これらのライブラリも内部でテンソル(多次元配列)を扱っており、NumPy配列との相互変換が容易。データの前処理や結果の評価などでNumPyと連携することが多い。
OpenCV / Pillow (PIL) 画像処理 画像をNumPy配列として表現し、ピクセル単位の操作や画像変換を行う。NumPyの配列操作機能が画像処理アルゴリズムの実装に役立つ。

このように、NumPyはこれらのライブラリ群の「共通言語」のような役割を果たしており、NumPyを理解することは、Pythonを用いたデータ分析や機械学習プロジェクトを進める上で不可欠なスキルと言えます。データはしばしばPandasで読み込み・整形され、NumPy配列に変換された後、scikit-learnでモデル学習が行われ、Matplotlibで結果が可視化される、といった流れが一般的です。

2024年6月、NumPyは2006年のバージョン1.0リリース以来となるメジャーバージョンアップ、NumPy 2.0 をリリースしました。これはNumPyの進化における大きな節目となるバージョンです。

NumPy 2.0 は、長年のフィードバックと技術的進歩に基づいて開発され、いくつかの重要な変更と新機能が含まれています。主な変更点は以下の通りです。

  • APIの整理と合理化 (NEP 52): Python APIが整理され、公開APIと非公開APIが明確に分離されました。一部の古い関数やエイリアスが削除され、より学習しやすく使いやすいAPIを目指しています。この変更には後方互換性のない部分も含まれます。
  • 型プロモーションルールの改善 (NEP 50): スカラー値や次元数が0の配列などが関わる演算での型決定ルールが見直され、より直感的で一貫性のある挙動になりました。
  • 新しいDType APIとStringDType (NEP 41): ユーザー定義のデータ型を実装するための新しいAPIが導入されました。また、待望されていた可変長文字列をネイティブにサポートする `StringDType` が追加されました。これにより、固定長の `U` (Unicode) 型よりも柔軟な文字列操作が可能になります。
  • Windows互換性の向上: 64ビットWindows環境でのデフォルトの整数型が `int32` から `int64` に変更され、他のOSとの互換性が向上しました。
  • Python Array API標準のサポート: Pythonの配列ライブラリ間での互換性を目指す Array API standard (v2022.12) に完全準拠した最初のリリースとなりました。
  • パフォーマンスの向上: `sort` や `argsort` などの関数でより高速なアルゴリズムが採用されるなど、パフォーマンス改善も行われています。
  • ビルドシステムの変更: ビルドシステムが `distutils` から `Meson` に移行しました。
  • ABI (Application Binary Interface) の変更: NumPy 2.0 はABI互換性がありません。これは、NumPyのC APIを利用している他のライブラリ(SciPy, Pandas, scikit-learnなど)もNumPy 2.0に対応するために再コンパイルが必要になることを意味します。ただし、NumPy 1.25以降では、NumPy 2.xでビルドしたライブラリがNumPy 1.xでも動作するように互換性が考慮されています。

NumPy 2.0への移行に関する注意:

NumPy 2.0には後方互換性のないAPI/ABIの変更が含まれているため、既存のコードや依存ライブラリとの互換性に注意が必要です。特に、NumPyの内部構造や非推奨APIに依存していたコードは修正が必要になる可能性があります。エコシステム全体(Pandas, SciPyなど)がNumPy 2.0に対応するまでには時間がかかる場合があるため、移行は慎重に行う必要があります。

NumPy 2.0は、将来の改善のための基盤を整備し、よりクリーンで高性能なライブラリへと進化するための重要なステップです。

まとめ

NumPyは、Pythonにおける科学技術計算、データ分析、機械学習の分野で中心的な役割を担う不可欠なライブラリです。その高速な多次元配列 `ndarray`、ベクトル化された演算、ブロードキャスティング、豊富な数学関数群は、効率的な数値処理を可能にします。

主なポイントを再確認しましょう。

  • 高速な数値計算: C言語実装とメモリ最適化により、Python標準リストより遥かに高速。
  • `ndarray`: 多次元配列を効率的に扱うためのコアデータ構造。
  • ベクトル化 & ufunc: ループを使わずに配列全体への演算を簡潔かつ高速に実行。
  • ブロードキャスト: 形状の異なる配列間の演算を自動化。
  • 豊富な機能: 線形代数、乱数生成、フーリエ変換など。
  • エコシステムの基盤: Pandas, SciPy, Matplotlib, scikit-learnなど多くの重要ライブラリがNumPyに依存。
  • NumPy 2.0: API整理、型ルール改善、新しいデータ型導入など、大きな進化を遂げたメジャーアップデート(互換性に注意)。

NumPyをマスターすることは、Pythonでデータに関わるあらゆる作業の効率と可能性を飛躍的に高めることに繋がります。基本的な配列操作から始め、徐々に応用的な機能へと学びを進めていくことで、より複雑なデータ処理や分析、アルゴリズム実装に対応できるようになるでしょう。NumPyは、Pythonデータサイエンスの世界への扉を開く鍵となるライブラリです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です