【NumPyレベルアップ編 第3回】データ表現の進化!構造化配列とnumpy.randomの高度な使い方 💎🎲
「NumPy配列って、全部同じデータ型じゃないとダメなの?Excelの表みたいに列ごとに型を変えられたらいいのに…」
「サイコロを振る以上の、もっと本格的なランダムデータ生成ってどうやるんだろう?」
「正規分布とかポアソン分布とか、統計で聞くような乱数もNumPyで作れるの?」
こんにちは! NumPy探検隊、隊長のPythonistaです! 前回のレベルアップ編第2回では、ベクトル化やメモリレイアウト、適切なデータ型の選択、そしてnp.memmap
といった、NumPyのパフォーマンスを引き出すための重要なテクニックを学びましたね。これで、大きなデータセットにも効率的に対処する力がついたはずです。
シリーズ第3回となる今回は、NumPyのさらに奥深い機能、いわば「隠れた実力者」たちに光を当てていきます。具体的には、
- 1つの配列内に異なるデータ型のフィールド(列)を持つことができる構造化配列 (Structured Arrays)
- そして、より現代的で高機能な乱数生成APIを提供する
numpy.random
モジュールの深掘り(特にGenerator
オブジェクトと様々な確率分布)
の2つのテーマを探求します。これらの機能をマスターすれば、NumPyでのデータ表現の幅が広がり、より現実に即した複雑なデータ構造を扱ったり、高度なシミュレーションや統計モデリングのための基礎を築いたりすることができるようになります!
1. 構造化配列 (Structured Arrays):1つの配列で多様なデータを表現! 📊
これまでのNumPy配列では、全ての要素が同じデータ型(例: 全てint64
、全てfloat64
)であるという前提がありました。しかし、実際のデータでは、Excelの表やデータベースのテーブルのように、各列(フィールド)が異なるデータ型を持つことがよくあります(例: 名前は文字列、年齢は整数、給与は浮動小数点数など)。
構造化配列は、このような複数の異なるデータ型からなる「レコード」を効率的に扱うためのNumPyの機能です。各要素が、C言語の構造体のように、名前付きのフィールドを持つことができます。
1.1. なぜ構造化配列が必要か?
- 異種データの効率的な格納: 関連する異なる種類のデータを、1つのNumPy配列内にまとめて格納できます。
- フィールド名によるアクセス: インデックス番号だけでなく、意味のあるフィールド名を使ってデータにアクセスできるため、コードの可読性が向上します。
- 表形式データの扱い: CSVファイルやデータベースから読み込んだ表形式のデータを、NumPy内部で(Pandasを使わずに)比較的簡単に扱いたい場合に便利です。
- バイナリデータやC言語との連携: 特定のバイナリファイル形式のデータを読み書きしたり、C言語の構造体とデータをやり取りしたりする際にも役立ちます。
1.2. 構造化配列の定義方法 (dtype
の工夫)
構造化配列を定義する鍵は、dtype
引数の指定方法にあります。フィールド名、データ型、そしてオプションで形状(配列フィールドの場合)をタプルのリストとして指定します。
import numpy as np
# 構造化配列のデータ型を定義
# フィールド名、データ型、(オプションで形状) のタプルのリスト
employee_dtype = np.dtype([
('name', 'U10'), # 名前 (最大10文字のUnicode文字列)
('age', np.int32), # 年齢 (32ビット整数)
('department', 'S20'), # 部署 (最大20バイトの文字列)
('salary', np.float64) # 給与 (64ビット浮動小数点数)
])
# データを作成 (タプルのリストとして各レコードを準備)
employee_data = [
('Alice', 30, 'HR', 50000.0),
('Bob', 24, 'Engineering', 60000.0),
('Charlie', 35, 'HR', 55000.0),
('David', 28, 'Sales', 62000.0)
]
# 構造化配列を作成
employees = np.array(employee_data, dtype=employee_dtype)
print(f"\n--- 構造化配列 (社員データ) ---")
print(employees)
print(f"\n配列の形状: {employees.shape}") # (4,) -> 4つのレコードを持つ1次元配列
print(f"最初のレコード: {employees[0]}")
データ型指定の文字列(例: 'U10'
, 'S20'
, 'i4'
(int32), 'f8'
(float64)など)やNumPyの型オブジェクト(例: np.int32
)が使えます。
1.3. フィールド名を使ったデータアクセス
構造化配列の最大の利点は、フィールド名を使って特定の「列」のデータにアクセスできることです。
# employees配列が上記で定義済みとする
print(f"\n--- フィールド名でのアクセス ---")
# 'name'フィールドの全データを取得 (新しい配列が返る)
names = employees['name']
print(f"全ての名前: {names}")
print(f"名前の配列の型: {type(names)}")
print(f"名前の配列のdtype: {names.dtype}") # 'U10'
# 'salary'フィールドの全データを取得
salaries = employees['salary']
print(f"全ての給与: {salaries}")
# 特定のレコードの特定のフィールドにアクセス
print(f"\n最初の社員の名前: {employees[0]['name']}")
print(f"2番目の社員の給与: {employees[1]['salary']}")
# 複数のフィールドを一度に取得 (結果は新しい構造化配列)
name_and_salary = employees[['name', 'salary']]
print(f"\n名前と給与:\n{name_and_salary}")
1.4. 構造化配列のソート
特定のフィールドを基準にして構造化配列をソートすることも可能です。
# employees配列が上記で定義済みとする
# 'age'フィールドで昇順ソート
sorted_by_age = np.sort(employees, order='age')
print(f"\n--- 年齢でソート ---")
print(sorted_by_age)
# 複数のフィールドでソート (例: 'department'で昇順、同じ部署なら'salary'で降順)
# 降順ソートは直接サポートされていないため、一度昇順ソートしてから逆順にするなどの工夫が必要な場合がある
# または、Pandasを使うとより簡単に複雑なソートができます。
sorted_by_dept_then_salary_desc = np.sort(employees, order=['department', 'salary']) # salaryも昇順になる
# salaryを降順にするには少し工夫が必要 (例: salaryの符号を反転させたフィールドでソートするなど)
print(f"\n--- 部署(昇順)、給与(昇順)でソート ---")
print(sorted_by_dept_then_salary_desc)
構造化配列は、CSVファイルのような表形式データをNumPyの配列として効率的に扱いたい場合に、Pandasライブラリの代替となり得る一つの選択肢です(ただし、Pandasほど高機能ではありません)。
2. numpy.random
モジュールの深掘り:より高度な乱数生成 🎰
これまでのNumPy入門やPython標準ライブラリのrandom
モジュールで、基本的な乱数生成には触れてきました。NumPyのnumpy.random
モジュールは、さらに高度で統計的に優れた乱数生成機能を提供します。特に、NumPy 1.17以降で導入された新しいAPIであるGenerator
オブジェクトを使う方法が推奨されています。
2.1. 新しいAPI:Generator
オブジェクト (np.random.default_rng()
)
古いAPI(例: np.random.rand()
, np.random.randint()
など)もまだ使えますが、新しいコードではGenerator
オブジェクト経由で各種乱数生成メソッドを呼び出す方が、再現性や統計的性質の面で優れているとされています。
import numpy as np
# Generatorオブジェクトを作成
rng = np.random.default_rng(seed=42) # seedを指定すると再現性のある乱数系列が得られる
# 0.0以上1.0未満の一様乱数を3つ生成
random_floats = rng.random(3)
print(f"\n--- Generatorオブジェクトによる乱数生成 ---")
print(f"一様乱数 (0-1): {random_floats}")
# 1から10までの整数を一様に5つ生成 (10も含む)
random_integers = rng.integers(low=1, high=11, size=5) # highは含まない(未満)ので+1する
print(f"整数乱数 (1-10): {random_integers}")
# 配列の要素をシャッフル (元の配列は変更されない、コピーが返る)
my_array = np.arange(5)
shuffled_array = rng.permutation(my_array)
print(f"元の配列: {my_array}")
print(f"シャッフルされた配列: {shuffled_array}")
# リストからランダムに要素を選択
my_list = ['A', 'B', 'C', 'D']
chosen_element = rng.choice(my_list)
print(f"リストから選択: {chosen_element}")
np.random.default_rng()
でGenerator
インスタンスを作成し、そのインスタンスのメソッド(例: .random()
, .integers()
, .permutation()
, .choice()
など)を使って乱数を生成します。seed
を指定することで、何度実行しても同じ乱数列を得られる(再現性)のは以前と同様です。
2.2. 様々な確率分布に従う乱数の生成
Generator
オブジェクトは、様々な統計的な確率分布に従う乱数を生成するメソッドを持っています。これにより、より現実に近いシミュレーションや、統計モデリングが可能になります。
- 正規分布 (Normal / Gaussian Distribution): 平均(
loc
)と標準偏差(scale
)を指定します。自然界の多くの現象(身長、誤差など)がこの分布に従うと言われています。rng.normal(loc=0.0, scale=1.0, size=None)
- 二項分布 (Binomial Distribution): 試行回数(
n
)と各試行での成功確率(p
)を指定し、成功回数を返します。コイン投げをN回行ったときの表の出る回数などがこの分布に従います。rng.binomial(n, p, size=None)
- ポアソン分布 (Poisson Distribution): 一定時間または一定空間内で特定のイベントが発生する平均回数(
lam
, ラムダ)を指定し、実際に発生した回数を返します。稀なイベントの発生数(例: 1時間あたりのウェブサイトへのアクセス数、一定面積あたりの特定の植物の数)などがこの分布に従うことがあります。rng.poisson(lam=1.0, size=None)
import numpy as np
import matplotlib.pyplot as plt # 可視化のためにMatplotlibを使用
rng = np.random.default_rng(seed=123)
# 正規分布 (平均0, 標準偏差1, 1000個)
normal_samples = rng.normal(loc=0, scale=1, size=1000)
print(f"\n正規分布サンプルの最初の5つ: {normal_samples[:5]}")
# plt.hist(normal_samples, bins=30, density=True, alpha=0.7, label='Normal Dist.')
# plt.title("Normal Distribution Sample")
# plt.legend()
# plt.show()
print("(正規分布のヒストグラム表示はコメントアウトしています)")
# 二項分布 (試行回数10回, 成功確率0.5, 1000サンプル) - コイン10回投げを1000セット
binomial_samples = rng.binomial(n=10, p=0.5, size=1000)
print(f"\n二項分布サンプルの最初の5つ (10回中何回成功したか): {binomial_samples[:5]}")
# plt.hist(binomial_samples, bins=np.arange(12)-0.5, density=True, alpha=0.7, label='Binomial Dist.')
# plt.title("Binomial Distribution Sample (n=10, p=0.5)")
# plt.xticks(np.arange(11))
# plt.legend()
# plt.show()
print("(二項分布のヒストグラム表示はコメントアウトしています)")
# ポアソン分布 (平均発生回数3回, 1000サンプル)
poisson_samples = rng.poisson(lam=3, size=1000)
print(f"\nポアソン分布サンプルの最初の5つ (発生回数): {poisson_samples[:5]}")
# plt.hist(poisson_samples, bins=np.arange(np.max(poisson_samples)+2)-0.5, density=True, alpha=0.7, label='Poisson Dist.')
# plt.title("Poisson Distribution Sample (lambda=3)")
# plt.xticks(np.arange(np.max(poisson_samples)+1))
# plt.legend()
# plt.show()
print("(ポアソン分布のヒストグラム表示はコメントアウトしています)")
(上記のヒストグラム表示部分は、実行するとグラフが表示されます。ブログ記事では、これらのグラフの画像を貼り付けるとより分かりやすくなります。)
2.3. 簡単なモンテカルロシミュレーションへの応用例:円周率の推定
モンテカルロ法は、乱数を使ったシミュレーションを何度も行い、その結果から近似的な解を得る手法です。非常に簡単な例として、正方形内にランダムに点を打ち、そのうち円の中に入った点の割合から円周率πを推定してみましょう。
import numpy as np
rng = np.random.default_rng()
num_samples = 1000000 # サンプル数 (多いほど精度が上がる)
# 0から1の範囲でx座標とy座標をランダムに生成
x_coords = rng.random(num_samples)
y_coords = rng.random(num_samples)
# 原点からの距離の2乗を計算 (x^2 + y^2)
distances_squared = x_coords**2 + y_coords**2
# 距離の2乗が1以下(つまり半径1の円の内側)の点の数をカウント
points_inside_circle = np.sum(distances_squared <= 1)
# 円周率の推定値 (面積比: (円の面積 / 正方形の面積) = (π*r^2 / (2r)^2) = π/4 )
# なので、π ≈ 4 * (円の中の点の数 / 全ての点の数)
estimated_pi = 4 * points_inside_circle / num_samples
print(f"\n--- モンテカルロ法による円周率の推定 ---")
print(f"サンプル数: {num_samples}")
print(f"円の内側に入った点の数: {points_inside_circle}")
print(f"推定された円周率π: {estimated_pi}")
print(f"実際の円周率π (NumPy定数): {np.pi}")
これはモンテカルロ法の非常に単純な例ですが、numpy.random
がシミュレーションにおいてどのように使われるかの雰囲気を感じていただけたかと思います。
まとめ:NumPyの多様なデータ表現と高度なランダム性で、分析・シミュレーションの扉を開く!
NumPyレベルアップ編の第3回では、普段あまり意識しないかもしれないけれど非常に強力なNumPyの機能に焦点を当てました。
- 構造化配列: 1つの配列内に異なるデータ型のフィールド(列)を持たせることで、Excelの表やデータベースのレコードのような複雑なデータを効率的に表現し、フィールド名でアクセスする方法。
numpy.random
モジュールの深掘り:- 推奨される
Generator
オブジェクト (np.random.default_rng()
) の使い方。 - 正規分布、二項分布、ポアソン分布といった様々な確率分布に従う乱数の生成方法。
- 乱数を使ったモンテカルロシミュレーションの簡単な導入例。
- 推奨される
構造化配列は、特定の種類のデータを扱う際にコードの可読性と効率を向上させ、numpy.random
の高度な機能は、より現実に近いシミュレーションや統計的な分析を行うための強力な基盤となります。これらの「隠れた実力者」たちを使いこなすことで、あなたのNumPyスキルはさらに磨かれ、より専門的な課題にも対応できるようになるでしょう。
NumPyの探求はまだまだ続きます。次回は、NumPyを使ったより実践的な線形代数演算の応用や、科学技術計算ライブラリの最高峰であるSciPyとの連携など、さらにエキサイティングなトピックに触れていく予定です(内容は変更になるかもしれません!)。お楽しみに!
【NumPyレベルアップ編 第4回・オプション】限界突破!NumPyと外部環境連携 - C連携・高速化(Cython/Numba)・SciPyへの扉 🌌
コメント
コメントを投稿