はい、承知いたしました。Pythonでの二次元配列の作り方と使い方について、詳細な解説を含む記事を作成します。約5000語を目指し、Python標準機能(リストのリスト)とNumPyの両方について深く掘り下げます。
もう迷わない!Pythonでの二次元配列の作り方と使い方
Pythonプログラミングにおいて、データを効率的に管理・操作することは非常に重要です。特に、表形式のデータ、マトリックス、ゲームの盤面、画像データなど、縦と横の構造を持つデータを扱う際には、「二次元配列」が不可欠な役割を果たします。
しかし、Pythonには他の言語(C++, Javaなど)のような組み込みの「配列型」がありません。そのため、Pythonで二次元配列を扱おうとしたときに、「どうやって作るの?」「どうやって要素にアクセスするの?」「どんなライブラリを使うべきなの?」といった疑問を持つ方が少なくありません。また、いくつかの作成方法が存在するため、それぞれの特徴や注意点を知らないと、予期せぬバグに遭遇することもあります。
この記事では、Pythonにおける二次元配列の概念から、具体的な作成方法、要素へのアクセス、操作、そしてそれぞれの方法の使い分けについて、初心者の方でも理解できるように、コード例を豊富に交えながら徹底的に解説します。Python標準機能であるリストを使った方法と、科学技術計算などで広く使われているNumPyライブラリを使った方法の両方に焦点を当てます。この記事を読めば、Pythonでの二次元配列の扱いに自信を持てるようになるでしょう。
さあ、一緒にPythonでの二次元配列の世界を深く探求していきましょう。
1. 二次元配列とは何か?なぜ必要なのか?
まず、二次元配列とは何か、その基本的な概念を理解することから始めましょう。
一次元配列(リスト)のおさらい
Pythonにおいて、複数の要素をまとめて扱う基本的なデータ構造はリストです。例えば、[1, 2, 3, 4, 5]
のように、一列に並んだデータの集まりを表現できます。このような、要素が一つのシーケンス上に並んだ構造を「一次元配列」と考えることができます。Pythonではこれをリストで表現するのが一般的です。
“`python
一次元配列(リスト)の例
scores = [85, 92, 78, 95, 88]
print(scores)
“`
この一次元配列は、例えば「あるクラスの生徒のテストの点数リスト」のように、単一の属性に関するデータの集まりを表現するのに適しています。
二次元配列の概念
これに対して、二次元配列は、データを縦方向と横方向、すなわち「行」と「列」の二つの方向を持つ格子状の構造で表現します。例えるなら、スプレッドシートや表のようなものです。
列0 | 列1 | 列2 | |
---|---|---|---|
行0 | データ(0,0) | データ(0,1) | データ(0,2) |
行1 | データ(1,0) | データ(1,1) | データ(1,2) |
行2 | データ(2,0) | データ(2,1) | データ(2,2) |
各データ要素は、「何行目の何列目か」という二つのインデックス(添え字)を使って特定されます。通常、インデックスは0から始まります。つまり、一番左上の要素は(0,0)、その右隣は(0,1)、その真下は(1,0)となります。
二次元配列が使われる場面
二次元配列は、現実世界やプログラミングの様々な場面で登場します。
- 表形式データ: データベースから取得したデータ、CSVファイルの内容など、行と列を持つ表形式のデータを扱う際に自然な構造です。
- 行列(マトリックス): 数学や物理、コンピュータグラフィックスなどで登場する行列演算は、二次元配列そのものです。
- ゲームの盤面: オセロ、チェス、マインスイーパーなどのゲームの盤面は、マス目の状態を二次元配列で表現することが多いです。
- 画像処理: モノクロ画像は、各ピクセルの明るさを行と列で並べた二次元配列として表現できます(カラー画像は三次元配列になることが多いです)。
- 地図/グリッド: 地図上の特定のエリアをグリッドに分割し、各グリッドの情報を保持する場合など。
このように、二次元の構造を持つデータを扱う際には、二次元配列が非常に効果的なデータ構造となります。
Pythonにおける二次元配列の実現方法
Pythonには専用の「二次元配列型」は組み込まれていませんが、主に以下の二つの方法で二次元配列を実現し、扱います。
- Python標準機能(リストのリスト): リストの中に別のリストを入れることで、疑似的な二次元配列構造を作ります。これは最も基本的な方法であり、Pythonのリストの柔軟性を活かせます。
- NumPyライブラリ(
ndarray
): 科学技術計算やデータ分析の分野で広く使われるNumPyライブラリは、高性能な多次元配列オブジェクトndarray
を提供しています。数値計算を高速に行うのに非常に適しています。
どちらの方法にも利点と欠点があり、扱うデータの種類や目的に応じて適切な方法を選択することが重要です。この記事では、これら二つの方法について、それぞれの「作り方」と「使い方」を詳細に解説していきます。
2. Python標準機能による二次元配列(リストのリスト)
まずは、特別なライブラリをインストールすることなく、Pythonの標準機能であるリストを使って二次元配列を表現する方法を見ていきましょう。これは「リストのリスト」と呼ばれる構造です。
基本的な考え方:リストの中にリストを入れる
リストのリストは、その名の通り、リストの要素として別のリストを持つ構造です。外側のリストが行を表し、内側のリストが列を表すと考えると、二次元配列のように扱うことができます。
“`python
リストのリストの例
3行4列の二次元配列を表現
matrix = [
[1, 2, 3, 4], # 0行目
[5, 6, 7, 8], # 1行目
[9, 10, 11, 12] # 2行目
]
“`
この matrix
というリストは、3つの要素を持っています。それぞれの要素は [1, 2, 3, 4]
、[5, 6, 7, 8]
、[9, 10, 11, 12]
という別のリストです。これがそれぞれ0行目、1行目、2行目を構成していると考えます。
作成方法
リストのリストによる二次元配列の作成方法にはいくつかのアプローチがあります。
a) 静的な作成(直接記述)
要素の値が最初から決まっている場合や、小規模な二次元配列を作成する場合は、リストリテラルを使って直接記述するのが最もシンプルです。
“`python
3×3の単位行列を作成
identity_matrix = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
]
print(identity_matrix)
“`
b) 動的な作成(ループを使用)
サイズを指定して、初期値を持つ二次元配列を作成する場合や、各要素の値を計算によって決定する場合は、ループを使うのが一般的です。
例えば、3行5列のすべての要素が0の二次元配列を作成する場合:
“`python
rows = 3
cols = 5
zero_matrix = []
for i in range(rows):
row = []
for j in range(cols):
row.append(0) # 各要素を0で初期化
zero_matrix.append(row)
print(zero_matrix)
出力: [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
“`
このコードでは、まず外側のループで行を作成し、内側のループでその行に含まれる列の要素を作成しています。そして、作成した行を zero_matrix
リストに追加しています。
c) リスト内包表記を使用
Pythonのリスト内包表記を使うと、上記のループを使った作成をより簡潔に記述できます。これは非常にPythonらしい書き方であり、よく使われます。
基本的な形は [ [要素の式 for j in range(cols)] for i in range(rows) ]
となります。
例えば、上記の「すべての要素が0の3行5列の二次元配列」は以下のように書けます。
“`python
rows = 3
cols = 5
リスト内包表記を使った作成
zero_matrix_comprehension = [[0 for j in range(cols)] for i in range(rows)]
print(zero_matrix_comprehension)
出力: [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
“`
これは、外側の [ ... for i in range(rows)]
が行を作り、内側の [0 for j in range(cols)]
がそれぞれの行の列要素(ここではすべて0)を作っていることを意味します。
別の例として、要素が (行番号, 列番号)
のタプルであるような4×3の二次元配列を作成する場合:
“`python
rows = 4
cols = 3
coord_matrix = [[(i, j) for j in range(cols)] for i in range(rows)]
print(coord_matrix)
出力: [[(0, 0), (0, 1), (0, 2)], [(1, 0), (1, 1), (1, 2)], [(2, 0), (2, 1), (2, 2)], [(3, 0), (3, 1), (3, 2)]]
“`
このように、リスト内包表記は非常に強力で簡潔な二次元配列の作成方法です。
d) リスト内包表記の注意点:シャローコピーの問題
リスト内包表記で二次元配列を作成する際に、非常によく陥りやすい落とし穴があります。それは、内側のリストが同じオブジェクトを共有してしまう「シャローコピー(浅いコピー)」の問題です。
例えば、「すべての要素が0の3行5列の二次元配列」を以下のように作成しようとすると、問題が発生します。
“`python
rows = 3
cols = 5
間違いやすい書き方!
outer_list = [inner_list] * rows の形に似ている
problem_matrix = [[0] * cols] * rows # これは危険!
print(“初期状態:”)
print(problem_matrix)
要素を変更してみる (1行目, 2列目)
problem_matrix[1][2] = 99
print(“\n要素変更後:”)
print(problem_matrix)
“`
このコードを実行すると、予想外の結果になります。
“`
初期状態:
[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
要素変更後:
[[0, 0, 99, 0, 0], [0, 0, 99, 0, 0], [0, 0, 99, 0, 0]]
“`
problem_matrix[1][2] = 99
と1行目(インデックス1)の2列目(インデックス2)だけを変更したつもりなのに、なぜか0行目と2行目の同じ列(インデックス2)の要素も 99
に変わってしまいました。
これは、[[0] * cols] * rows
という書き方が、[0] * cols
によって作られた 全く同じリストオブジェクトを rows
回繰り返して外側のリストに入れている ためです。つまり、problem_matrix[0]
、problem_matrix[1]
、problem_matrix[2]
は、すべてメモリ上の同じリストオブジェクトを参照しています。そのため、どれか一つの行の要素を変更すると、他の行の同じ位置の要素も同時に変更されてしまうのです。
この問題を避けるためには、各行が独立したリストオブジェクトである ように作成する必要があります。そのために、前述したリスト内包表記 [[0 for j in range(cols)] for i in range(rows)]
の形を使うのが正しい方法です。この書き方では、外側のループが回るたびに [0 for j in range(cols)]
が新しく評価され、新しいリストオブジェクトが作成されるため、各行は独立したものになります。
正しい作成方法の再確認:
シャローコピー問題を避け、各行が独立したリストになるように二次元配列を作成するには、以下の方法を使います。
- ネストしたループ:
python
rows = 3
cols = 5
matrix = []
for i in range(rows):
matrix.append([0] * cols) # 各行で新しいリスト [0, 0, 0, 0, 0] を作成
または
python
rows = 3
cols = 5
matrix = []
for i in range(rows):
row = []
for j in range(cols):
row.append(0) # 新しい行リストに要素を追加
matrix.append(row) - リスト内包表記 (推奨):
python
rows = 3
cols = 5
matrix = [[0 for j in range(cols)] for i in range(rows)] # 外側のループで新しい内側リストを作成
これらの正しい方法で作成した場合、要素の変更は意図した通りに1箇所だけに行われます。
“`python
rows = 3
cols = 5
correct_matrix = [[0 for j in range(cols)] for i in range(rows)]
correct_matrix[1][2] = 99
print(correct_matrix)
出力: [[0, 0, 0, 0, 0], [0, 0, 99, 0, 0], [0, 0, 0, 0, 0]] # 1行目だけが変更されている
“`
リストのリストで二次元配列を作成する際は、このシャローコピーの問題に十分注意してください。特に、[初期値] * 列数
のリストを 行数
回繰り返す [[初期値] * 列数] * 行数
という書き方は、ほとんどの場合で避けるべきです。
要素へのアクセス
リストのリストで表現された二次元配列の要素には、matrix[行インデックス][列インデックス]
の形式でアクセスします。インデックスはどちらも0から始まります。
“`python
matrix = [
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
]
0行目, 0列目の要素にアクセス
print(matrix[0][0]) # 出力: 10
1行目, 2列目の要素にアクセス
print(matrix[1][2]) # 出力: 60
2行目, 1列目の要素にアクセス
print(matrix[2][1]) # 出力: 80
“`
存在しないインデックスにアクセスしようとすると、IndexError
が発生します。
スライスを使ったアクセス
Pythonリストのスライス機能を使って、行全体や部分的な行を取得することもできます。
“`python
matrix = [
[10, 20, 30, 40],
[50, 60, 70, 80],
[90, 100, 110, 120]
]
1行目全体を取得
row_1 = matrix[1]
print(row_1) # 出力: [50, 60, 70, 80]
0行目から2行目(2行目は含まない)までを取得
rows_0_to_1 = matrix[0:2]
print(rows_0_to_1)
出力: [[10, 20, 30, 40], [50, 60, 70, 80]]
2行目の1列目から3列目(3列目は含まない)までを取得
まず2行目を取得し、次にそのリストをスライス
partial_row_2 = matrix[2][1:3]
print(partial_row_2) # 出力: [100, 110]
“`
リストのリストでは、列全体や部分的な列を直接スライスで取得するのは少し手間がかかります。これは、リストは行を要素として持つ構造であり、列方向には直接的なシーケンス構造がないためです。列を取得するには、各行から該当する列の要素を取り出すループ処理などが必要になります。
“`python
2列目全体を取得する例
matrix = [
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
]
column_2 = [row[2] for row in matrix] # リスト内包表記を使用
print(column_2) # 出力: [30, 60, 90]
“`
このように、リストのリストで列を扱う際は、行を扱うよりも少し複雑になります。
要素の変更
特定の要素の値を変更するには、アクセスと同じように matrix[行インデックス][列インデックス] = 新しい値
の形式を使います。
“`python
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
1行目, 1列目の要素を99に変更
matrix[1][1] = 99
print(matrix)
出力: [[1, 2, 3], [4, 99, 6], [7, 8, 9]]
0行目全体を新しいリストに置き換える
matrix[0] = [10, 11, 12]
print(matrix)
出力: [[10, 11, 12], [4, 99, 6], [7, 8, 9]]
“`
行や列の操作
行の追加・削除
行の追加や削除は、外側のリストに対して行うため、比較的簡単です。リストの append()
、insert()
、pop()
、remove()
などのメソッドがそのまま使えます。
“`python
matrix = [
[1, 2],
[3, 4]
]
新しい行を追加 (末尾に)
matrix.append([5, 6])
print(“行追加後 (append):”, matrix)
出力: [[1, 2], [3, 4], [5, 6]]
新しい行を挿入 (指定位置に)
matrix.insert(1, [7, 8]) # 1行目 (インデックス1) の位置に挿入
print(“行挿入後 (insert):”, matrix)
出力: [[1, 2], [7, 8], [3, 4], [5, 6]]
行を削除 (インデックス指定)
deleted_row = matrix.pop(0) # 0行目を削除
print(“行削除後 (pop):”, matrix)
print(“削除された行:”, deleted_row)
出力: 行削除後 (pop): [[7, 8], [3, 4], [5, 6]]
出力: 削除された行: [1, 2]
行を削除 (値指定 – リスト全体と一致する要素を削除)
matrix.remove([3, 4])
print(“行削除後 (remove):”, matrix)
出力: 行削除後 (remove): [[7, 8], [5, 6]]
“`
列の追加・削除
列の追加や削除は、リストのリスト構造では少し手間がかかります。これは、各行リストに独立して操作を行う必要があるためです。
列の追加: 各行のリストに新しい要素を追加します。
“`python
matrix = [
[1, 2],
[3, 4],
[5, 6]
]
各行の末尾に新しい列を追加 (例: 7, 8, 9 を追加)
new_column_values = [7, 8, 9]
if len(matrix) == len(new_column_values):
for i in range(len(matrix)):
matrix[i].append(new_column_values[i])
print(“列追加後 (末尾):”, matrix)
# 出力: 列追加後 (末尾): [[1, 2, 7], [3, 4, 8], [5, 6, 9]]
else:
print(“行数と追加する列の値の数が一致しません。”)
各行の指定位置に新しい要素を挿入 (例: インデックス1の位置に 10, 11, 12 を挿入)
matrix = [
[1, 2, 7],
[3, 4, 8],
[5, 6, 9]
]
insert_column_values = [10, 11, 12]
insert_index = 1 # 挿入したい列のインデックス
if len(matrix) == len(insert_column_values):
for i in range(len(matrix)):
matrix[i].insert(insert_index, insert_column_values[i])
print(f”列挿入後 (インデックス {insert_index}):”, matrix)
# 出力: 列挿入後 (インデックス 1): [[1, 10, 2, 7], [3, 11, 4, 8], [5, 12, 6, 9]]
else:
print(“行数と挿入する列の値の数が一致しません。”)
“`
列の削除: 各行のリストから要素を削除します。
“`python
matrix = [
[1, 10, 2, 7],
[3, 11, 4, 8],
[5, 12, 6, 9]
]
指定位置の列を削除 (例: インデックス 1 の列を削除)
delete_index = 1 # 削除したい列のインデックス
注意: 後ろから削除するか、リスト内包表記などで新しいリストを生成するのが安全
前から削除するとインデックスがずれるため
方法1: リスト内包表記で新しい行リストを生成
new_matrix = []
for row in matrix:
new_row = row[:delete_index] + row[delete_index+1:] # 削除したいインデックスを除いて結合
new_matrix.append(new_row)
matrix = new_matrix
print(f”列削除後 (インデックス {delete_index}, 方法1):”, matrix)
出力: 列削除後 (インデックス 1, 方法1): [[1, 2, 7], [3, 4, 8], [5, 6, 9]]
方法2: 各行リストからpopを使う(後ろから削除する場合や、正確なインデックス管理が必要な場合)
例: 末尾の列 (インデックス len(row) – 1) を削除する場合
matrix = [
[1, 2, 7],
[3, 4, 8],
[5, 6, 9]
]
last_col_index = len(matrix[0]) – 1 # 最後の列のインデックスを仮定 (全行で列数が同じ場合)
for row in matrix:
if len(row) > last_col_index: # 念のため存在確認
row.pop(last_col_index)
print(f”列削除後 (末尾):”, matrix)
出力: 列削除後 (末尾): [[1, 2], [3, 4], [5, 6]]
“`
列の操作は行の操作に比べて直感的ではなく、各行に対して個別に操作を行う必要があることがわかります。
二次元配列の走査(イテレーション)
二次元配列の全要素にアクセスしたり、処理したりする場合、ネストしたループを使うのが最も一般的です。
“`python
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(“要素の走査:”)
for row_index in range(len(matrix)): # 行のループ
for col_index in range(len(matrix[row_index])): # 列のループ
print(f”要素 ({row_index}, {col_index}): {matrix[row_index][col_index]}”)
print(“\n要素の値のみの走査:”)
for row in matrix: # 各行リストを取得
for element in row: # 各行リスト内の要素を取得
print(element, end=” “)
print() # 各行の終わりに改行
“`
出力:
“`
要素の走査:
要素 (0, 0): 1
要素 (0, 1): 2
要素 (0, 2): 3
要素 (1, 0): 4
要素 (1, 1): 5
要素 (1, 2): 6
要素 (2, 0): 7
要素 (2, 1): 8
要素 (2, 2): 9
要素の値のみの走査:
1 2 3
4 5 6
7 8 9
“`
リスト内包表記を使って、既存の二次元配列から新しい二次元配列を生成することもよく行われます。
“`python
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
全要素を2倍にした新しい二次元配列を作成
doubled_matrix = [[element * 2 for element in row] for row in matrix]
print(“要素を2倍にした配列:”, doubled_matrix)
出力: 要素を2倍にした配列: [[2, 4, 6], [8, 10, 12], [14, 16, 18]]
“`
リストのリストの応用例
- 簡単な表データ: 学生名と点数のリストなど、小規模なデータを表現するのに使えます。
python
student_data = [
["Alice", 85, 90],
["Bob", 78, 88],
["Charlie", 92, 76]
]
# "Bob"の1回目のテストの点数にアクセス: student_data[1][1] -> 78 - ゲームの盤面: 3目並べの盤面などを表現できます。
python
board = [
[' ', ' ', ' '],
[' ', 'X', ' '],
[' ', ' ', 'O']
]
# 中央のマスにアクセス: board[1][1] -> 'X'
リストのリストの利点と欠点
利点:
- 標準ライブラリ: Pythonに標準で備わっている機能だけで実現できます。追加のインストールは不要です。
- 手軽さ: シンプルな構造なので、小規模なデータを扱う際には直感的で手軽に扱えます。
- 柔軟性: リストの要素は異なるデータ型を持つことができるため、二次元配列内の要素も型が混在していても問題ありません(例:
[[1, "apple"], [2, "banana"]]
)。また、各行のリストの長さが異なっていても構造上は問題ありません(ただし、これは厳密な意味での「二次元配列」とは少し異なりますが)。
欠点:
- 計算速度: 特に大規模な数値データに対して、要素ごとの演算や行列演算を行う場合、Pythonのループ処理はC言語などで実装された処理に比べて低速です。
- メモリ効率: 各行が個別のリストオブジェクトであることや、リストの内部実装により、大量の同種データを格納する場合にはメモリ効率が悪くなることがあります。
- 操作の複雑さ: 列方向の操作(挿入、削除、特定の列の取得など)が、行方向の操作に比べて複雑になります。また、全体に対する数学的な演算(合計、平均など)を行うための組み込み機能がありません。
- シャローコピーの問題: リストの複製時には注意が必要で、意図しない共有が発生しやすいです。
リストのリストは、小規模なデータや、要素の型が不均一なデータを扱う場合に適しています。しかし、大規模な数値計算や、高速な行列・配列操作が必要な場合は、次に説明するNumPyの使用を強く推奨します。
3. NumPyによる二次元配列(ndarray)
科学技術計算、データ分析、機械学習などの分野では、NumPyライブラリを使った二次元配列の扱いがデファクトスタンダードとなっています。NumPyは、Pythonのリストが持つ欠点(特に速度とメモリ効率)を克服し、強力な数値計算機能を提供します。
なぜNumPyを使うのか?
- 高速性: NumPyの主要なデータ構造である
ndarray
は、C言語で実装されており、要素が同じデータ型であるため、Pythonのリストよりもはるかに高速な数値計算が可能です。特に、大きな配列に対する要素ごとの演算(ベクトル化演算)や行列演算は、Pythonのループで書くよりも劇的に速くなります。 - メモリ効率: 同じデータ型の要素が連続したメモリブロックに格納されるため、Pythonリストよりもメモリ効率が良いです。
- 豊富な機能: 配列の作成、操作、変換、統計処理、線形代数、フーリエ変換など、多岐にわたる数学的・科学技術計算のための関数が提供されています。
- 簡潔な記述: ブロードキャスト機能などを利用することで、複雑な配列操作も少ないコード量で記述できます。
NumPyを使うには、まずインストールが必要です。通常はpipコマンドでインストールできます。
bash
pip install numpy
インストール後、スクリプトの冒頭で import numpy as np
と記述して使用するのが一般的です。np
はNumPyの慣習的なエイリアスです。
NumPy配列(ndarray)の作成方法
NumPyの二次元配列は numpy.ndarray
オブジェクトとして表現されます。いくつかの作成方法があります。
a) Pythonリストからの変換
既存のPythonリスト(リストのリスト)からNumPy配列を作成するのが最も一般的です。
“`python
import numpy as np
python_list_2d = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
np.array() を使用
numpy_array_2d = np.array(python_list_2d)
print(numpy_array_2d)
print(type(numpy_array_2d)) # 出力:
“`
出力:
[[1 2 3]
[4 5 6]
[7 8 9]]
<class 'numpy.ndarray'>
NumPy配列は、リストのようにカンマ区切りでなく、要素が等間隔で表示されることが多いです。
b) 初期化関数
特定の値で埋められた配列を作成するための便利な関数があります。
np.zeros((行数, 列数))
: すべての要素が0の配列を作成します。np.ones((行数, 列数))
: すべての要素が1の配列を作成します。np.full((行数, 列数), 値)
: 指定した値ですべての要素を埋めた配列を作成します。np.empty((行数, 列数))
: 指定した形状の配列を作成しますが、要素の値は初期化されず、その時点のメモリ上のゴミデータが入ります。高速に配列を確保したい場合に利用しますが、初期値は不定です。
“`python
import numpy as np
3×4のゼロ配列
zeros_array = np.zeros((3, 4))
print(“ゼロ配列:\n”, zeros_array)
2×3のイチ配列
ones_array = np.ones((2, 3))
print(“イチ配列:\n”, ones_array)
4×2で要素が7の配列
full_array = np.full((4, 2), 7)
print(“要素が7の配列:\n”, full_array)
5×5の空配列 (値は不定)
empty_array = np.empty((5, 5))
print(“空配列:\n”, empty_array)
“`
出力 (empty_arrayの値は実行ごとに異なります):
ゼロ配列:
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
イチ配列:
[[1. 1. 1.]
[1. 1. 1.]]
要素が7の配列:
[[7 7]
[7 7]
[7 7]
[7 7]]
空配列:
[[0. 0. 0. 0. 0.] # 例。実際は不定
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]
zeros
や ones
はデフォルトで浮動小数点数型 (float) の配列を作成します。整数型など、別のデータ型を指定したい場合は、dtype
引数を使用します。
“`python
要素が整数のゼロ配列
zeros_int = np.zeros((2, 2), dtype=int)
print(“整数ゼロ配列:\n”, zeros_int)
出力:
整数ゼロ配列:
[[0 0]
[0 0]]
“`
c) 数値シーケンスからの作成
特定の範囲や間隔で要素を持つ配列を作成する関数もあります。二次元配列として直接これらの関数を使うことは少ないですが、一次元配列を作成してから形状を変更(reshape)して二次元配列にすることがよくあります。
np.arange(start, stop, step)
: 指定した範囲で等間隔な値を生成します。np.linspace(start, stop, num)
: 指定した範囲を、指定した個数で等分した値を生成します。
“`python
0から99までの整数を持つ一次元配列を作成し、10×10の二次元配列に変換
array_1d = np.arange(100)
array_2d_from_arange = array_1d.reshape((10, 10))
print(“arange + reshape:\n”, array_2d_from_arange)
0から1までの範囲を25個で等分した一次元配列を作成し、5×5の二次元配列に変換
array_1d_linspace = np.linspace(0, 1, 25)
array_2d_from_linspace = array_1d_linspace.reshape((5, 5))
print(“linspace + reshape:\n”, array_2d_from_linspace)
“`
d) ランダムな値を持つ配列の作成
NumPyの random
モジュールを使うと、様々な分布に従うランダムな値を持つ配列を作成できます。
np.random.rand(行数, 列数)
: 0から1までの一様乱数。np.random.randn(行数, 列数)
: 標準正規分布に従う乱数。np.random.randint(low, high, size=(行数, 列数))
: 指定した範囲の整数乱数。
“`python
3×3の0-1様乱数配列
random_uniform = np.random.rand(3, 3)
print(“一様乱数配列:\n”, random_uniform)
2×4の標準正規分布乱数配列
random_normal = np.random.randn(2, 4)
print(“正規分布乱数配列:\n”, random_normal)
4×5の10から20(含まず)までの整数乱数配列
random_int = np.random.randint(10, 20, size=(4, 5))
print(“整数乱数配列:\n”, random_int)
“`
NumPy配列の属性
NumPy配列オブジェクトは、その形状、データ型などの情報を持つ属性を持っています。
“`python
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])
print(“配列:\n”, matrix)
print(“形状 (shape):”, matrix.shape) # 出力: (2, 3) -> 2行3列
print(“次元数 (ndim):”, matrix.ndim) # 出力: 2 -> 二次元
print(“要素数 (size):”, matrix.size) # 出力: 6 -> 2 * 3 = 6
print(“データ型 (dtype):”, matrix.dtype) # 出力: int64 (環境によって異なる場合あり)
print(“各要素のバイト数 (itemsize):”, matrix.itemsize) # 出力: 8 (int64の場合)
print(“配列全体のバイト数 (nbytes):”, matrix.nbytes) # 出力: 48 (6 * 8 = 48)
“`
これらの属性は、配列の情報を確認したり、配列操作の際に非常に役立ちます。特に shape
は配列の構造を理解する上で最も重要な属性です。
要素へのアクセス
NumPy配列の要素へのアクセス方法は、Pythonリストよりも柔軟で効率的です。
a) インデックスを使ったアクセス
特定の要素にアクセスするには、行インデックスと列インデックスを指定します。NumPyでは、array[行インデックス, 列インデックス]
というカンマ区切りの記法が推奨されます。Pythonリストのように array[行インデックス][列インデックス]
も使えますが、カンマ区切りの方が高速な場合が多いです。
“`python
import numpy as np
matrix = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
1行目, 2列目の要素にアクセス (カンマ区切り)
print(matrix[1, 2]) # 出力: 60
0行目, 0列目の要素にアクセス (リスト形式)
print(matrix[0][0]) # 出力: 10
2行目, 1列目の要素にアクセス
print(matrix[2, 1]) # 出力: 80
“`
b) スライスを使ったアクセス
NumPyのスライスは非常に強力です。array[行スライス, 列スライス]
の形式で、行や列の範囲を指定して部分配列を取得できます。スライスの記法はリストと同じく start:stop:step
です。
- 行全体を選択するには
:
. - 特定の行を選択するには 行インデックス.
- 特定の列を選択するには 列インデックス.
“`python
import numpy as np
matrix = np.array([
[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]
])
2行目全体を取得
print(“2行目全体:”, matrix[2, :]) # 出力: [ 9 10 11 12]
1列目全体を取得
print(“1列目全体:”, matrix[:, 1]) # 出力: [ 2 6 10 14]
0行目から2行目(2行目は含まない)、1列目から3列目(3列目は含まない)の部分配列を取得
print(“部分配列 (0:2, 1:3):\n”, matrix[0:2, 1:3])
出力:
[[ 2 3]
[ 6 7]]
最後の行を取得 (-1 インデックス)
print(“最後の行:”, matrix[-1, :]) # 出力: [13 14 15 16]
最初の2行と最後の2列を取得
print(“最初の2行と最後の2列:\n”, matrix[:2, -2:])
出力:
[[ 3 4]
[ 7 8]]
飛び飛びの行や列を取得 (スライスにステップを指定)
0行目から末尾まで2行おきに取得 (0行目, 2行目)
print(“2行おきに行を取得:\n”, matrix[::2, :])
出力:
[[ 1 2 3 4]
[ 9 10 11 12]]
0列目から末尾まで3列おきに取得 (0列目, 3列目)
print(“3列おきに列を取得:\n”, matrix[:, ::3])
出力:
[[ 1 4]
[ 5 8]
[ 9 12]
[13 16]]
“`
スライスによるアクセスは、元の配列の「ビュー(view)」を返します。これは、取得した部分配列を変更すると、元の配列も変更される可能性があるということです。明示的なコピーが必要な場合は、.copy()
メソッドを使用します。
“`python
part = matrix[:2, 1:3]
print(“ビュー:”, part)
ビューの要素を変更
part[0, 0] = 999
print(“ビュー変更後:”, part)
print(“元の配列 (変更されている!):\n”, matrix) # 元のmatrixも変更されている
“`
出力:
ビュー: [[2 3]
[6 7]]
ビュー変更後: [[999 3]
[ 6 7]]
元の配列 (変更されている!):
[[ 1 999 3 4]
[ 5 6 7 8]
[ 9 10 11 12]
[ 13 14 15 16]] # matrix[0,1] が999になっている
c) ブールインデックス参照 (Boolean Indexing)
条件を満たす要素だけを選択的に取り出す非常に強力な機能です。配列と同じ形状を持つブール値の配列(True/False)を作成し、それをインデックスとして指定します。
“`python
import numpy as np
matrix = np.array([
[10, -5, 20],
[-8, 15, -3],
[30, -1, 25]
])
正の値のみを選択
positive_elements = matrix[matrix > 0]
print(“正の値のみ:”, positive_elements) # 出力: [10 20 15 30 25] -> 1次元配列になる
条件を満たす要素を0に置換
matrix[matrix < 0] = 0
print(“負の値を0に置換後:\n”, matrix)
出力:
[[10 0 20]
[ 0 15 0]
[30 0 25]]
特定の条件を満たす要素を含む行を選択
例: 20より大きい要素を1つでも含む行
rows_with_large_values = matrix[np.any(matrix > 20, axis=1), :]
print(“20より大きい要素を含む行:\n”, rows_with_large_values)
出力:
[[10 0 20] # matrix[0,2] == 20 は条件を満たさない… が、ここでは例として > 20 を使う
[30 0 25]] # matrix[2,0]=30, matrix[2,2]=25 が条件を満たす
あ、np.any(matrix > 20, axis=1) は、各行に対して > 20 の要素があるか判定します。
0行目: False (20は>20ではない)
1行目: False
2行目: True (30 > 20, 25 > 20)
なので、出力は [[30 0 25]] になります。訂正!
正しい出力 (matrix > 20 は [[False False False], [False False False], [True False True]]):
20より大きい要素を含む行:
[[30 0 25]]
別の例: 10以上の要素のみで構成された配列 (これも1次元になる)
elements_ge_10 = matrix[matrix >= 10]
print(“10以上の要素のみ:”, elements_ge_10) # 出力: [10 20 15 30 25]
“`
ブールインデックス参照は、データフィルタリングに非常に強力な機能です。条件を満たす要素を取得すると、通常は1次元のNumPy配列が返されます。
d) ファンシーインデックス参照 (Fancy Indexing)
インデックスとして整数のリストや配列を指定することで、特定の行や列をまとめて、かつ任意の順序で取得する機能です。
“`python
import numpy as np
matrix = np.array([
[10, 20, 30, 40],
[50, 60, 70, 80],
[90, 100, 110, 120],
[130, 140, 150, 160]
])
0行目と2行目を取得
print(“0行目と2行目:\n”, matrix[[0, 2], :])
出力:
[[ 10 20 30 40]
[ 90 100 110 120]]
1列目と3列目を取得
print(“1列目と3列目:\n”, matrix[:, [1, 3]])
出力:
[[ 20 40]
[ 60 80]
[100 120]
[140 160]]
複数のインデックス配列を使って、特定の要素の組み合わせを取得
(0,1), (1,3), (2,0) の要素を取得
row_indices = np.array([0, 1, 2])
col_indices = np.array([1, 3, 0])
print(“(0,1), (1,3), (2,0) の要素:”, matrix[row_indices, col_indices]) # 出力: [ 20 80 90]
“`
ファンシーインデックス参照で取得される配列は、元の配列のビューではなく、常に新しい配列(コピー) になります。
要素の変更
インデックス、スライス、ブールインデックス参照、ファンシーインデックス参照を使って、要素を変更することもできます。
“`python
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
単一要素の変更
matrix[1, 1] = 99
print(“単一要素変更後:\n”, matrix)
出力:
[[ 1 2 3]
[ 4 99 6]
[ 7 8 9]]
スライスを使った部分配列への値の一括代入
matrix[0, 0:2] = [10, 11] # 0行目の0列目と1列目を変更
print(“スライスで一括代入後:\n”, matrix)
出力:
[[10 11 3]
[ 4 99 6]
[ 7 8 9]]
ブールインデックス参照を使った条件に合う要素の一括代入
matrix[matrix > 50] = 0 # 50より大きい要素を0にする
print(“> 50 の要素を0に置換後:\n”, matrix)
出力:
[[10 11 3]
[ 4 99 6]
[ 7 8 9]] # 99が50より大きいので0に変わった
ファンシーインデックス参照を使った要素の一括代入
row_indices = np.array([0, 2])
col_indices = np.array([2, 0])
matrix[row_indices, col_indices] = [77, 88] # matrix[0,2]=77, matrix[2,0]=88 に変更
print(“ファンシーインデックス参照で一括代入後:\n”, matrix)
出力:
[[10 11 77]
[ 4 99 6]
[88 8 9]]
“`
NumPyの強力な操作
NumPyの最大の強みは、配列全体や部分配列に対する数学的な演算や操作を高速に行えることです。
a) 要素ごとの演算 (Element-wise operations) とブロードキャスト (Broadcasting)
NumPy配列同士の加算、減算、乗算、除算などは、デフォルトで要素ごとに行われます。
“`python
import numpy as np
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(“a + b:\n”, a + b) # 各要素の合計
print(“a * b:\n”, a * b) # 各要素の積 (アダマール積/要素積)
print(“a / b:\n”, a / b) # 各要素の商
print(“a ** 2:\n”, a ** 2) # 各要素の2乗
print(“np.sin(a):\n”, np.sin(a)) # 各要素に対するサイン関数
“`
また、「ブロードキャスト」という機能により、形状が異なる配列同士でも、一定のルールに基づいて要素ごとの演算が可能になります。例えば、配列とスカラ値の演算は、スカラ値が配列全体に適用されるように拡張(ブロードキャスト)されます。
“`python
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])
配列 + スカラ値
print(“matrix + 10:\n”, matrix + 10) # 全要素に10を加算
出力:
[[11 12 13]
[14 15 16]]
配列 + 一次元配列 (形状ルールを満たす場合)
matrixの形状は (2, 3)
1×3 の一次元配列 [10, 20, 30] を加算
[10, 20, 30] は matrix の各行にブロードキャストされる
row_vector = np.array([10, 20, 30])
print(“matrix + row_vector:\n”, matrix + row_vector)
出力:
[[11 22 33] # [1,2,3] + [10,20,30]
[14 25 36]] # [4,5,6] + [10,20,30]
2×1 の一次元配列 [[10],[20]] を加算
[[10],[20]] は matrix の各列にブロードキャストされる
col_vector = np.array([[10], [20]])
print(“matrix + col_vector:\n”, matrix + col_vector)
出力:
[[11 12 13] # [1,2,3] + [10] (ブロードキャスト)
[24 25 26]] # [4,5,6] + [20] (ブロードキャスト)
“`
ブロードキャストのルールは少し複雑ですが、理解するとNumPyでの記述が非常に効率的になります。基本的なルールは、「末尾の次元から比較し、一致するか、どちらかが1であるか、どちらかが存在しない場合にブロードキャスト可能」というものです。
b) 行列演算
数学的な行列の積(ドット積)もNumPyで簡単に行えます。
“`python
import numpy as np
a = np.array([[1, 2], [3, 4]]) # 2×2 行列
b = np.array([[5, 6], [7, 8]]) # 2×2 行列
行列積 (@ 演算子 または np.dot() 関数)
matrix_product = a @ b
または matrix_product = np.dot(a, b)
print(“行列積 (a @ b):\n”, matrix_product)
出力:
[[15 + 27, 16 + 28] -> [ 5+14, 6+16] -> [19 22]
[35 + 47, 36 + 48]] -> [15+28, 18+32] -> [43 50]
[[19 22]
[43 50]]
``
*
行列積は、要素ごとの積(アダマール積)とは異なるので注意が必要です。演算子は要素ごとの積、
@演算子(または
np.dot`)は行列積です。
c) 集計関数
配列全体の合計、平均、最大値、最小値などを計算する関数が豊富に用意されています。これらの関数は axis
引数を指定することで、行方向または列方向の集計を行うことができます。
np.sum()
np.mean()
np.max()
,np.min()
np.std()
(標準偏差),np.var()
(分散)np.argmax()
,np.argmin()
(最大値・最小値のインデックス)np.all()
,np.any()
(ブール配列に対する論理演算)
“`python
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])
配列全体の合計
print(“全体合計:”, np.sum(matrix)) # 出力: 21 (1+2+3+4+5+6)
行ごとの合計 (axis=0 で列方向に集計 -> 各列の合計)
print(“行ごとの合計 (axis=0):\n”, np.sum(matrix, axis=0)) # 出力: [5 7 9] ([1+4, 2+5, 3+6])
列ごとの合計 (axis=1 で行方向に集計 -> 各行の合計)
print(“列ごとの合計 (axis=1):\n”, np.sum(matrix, axis=1)) # 出力: [ 6 15] ([1+2+3, 4+5+6])
配列全体の平均
print(“全体平均:”, np.mean(matrix)) # 出力: 3.5
列ごとの最大値
print(“列ごとの最大値 (axis=1):\n”, np.max(matrix, axis=1)) # 出力: [3 6]
``
axisの指定は直感と逆になる場合があるので注意が必要です。
axis=0は「0番目の軸(行)」に沿って演算を行う、つまり各列を縦方向に見て集計する、と考えられます。
axis=1` は「1番目の軸(列)」に沿って演算を行う、つまり各行を横方向に見て集計する、と考えられます。
d) 配列の形状操作
配列の形状を変更したり、次元を操作したりするための関数です。
reshape((新しい形状))
: 配列の要素数を変えずに形状を変更します。T
またはtranspose()
: 行と列を入れ替えます(転置行列)。flatten()
: 配列を1次元に平坦化します(常に新しいコピーを返す)。ravel()
: 配列を1次元に平坦化します(可能な場合はビューを返す)。
“`python
import numpy as np
matrix = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8]
]) # 2×4
4×2の形状に変更
reshaped_matrix = matrix.reshape((4, 2))
print(“reshape (4×2):\n”, reshaped_matrix)
出力:
[[1 2]
[3 4]
[5 6]
[7 8]]
転置
transposed_matrix = matrix.T
または transposed_matrix = matrix.transpose()
print(“転置:\n”, transposed_matrix)
出力:
[[1 5]
[2 6]
[3 7]
[4 8]]
1次元に平坦化
flattened_array = matrix.flatten()
print(“flatten:\n”, flattened_array) # 出力: [1 2 3 4 5 6 7 8]
“`
e) 配列の結合と分割
複数の配列を結合したり、一つの配列を分割したりする関数です。
np.concatenate((配列1, 配列2, ...), axis=軸)
: 指定した軸に沿って配列を結合します。np.vstack((配列1, 配列2, ...))
: 配列を垂直方向(行方向、axis=0)に結合します。concatenate
でaxis=0
を指定するのと同じです。np.hstack((配列1, 配列2, ...))
: 配列を水平方向(列方向、axis=1)に結合します。concatenate
でaxis=1
を指定するのと同じです。np.split(配列, 分割数 or 分割位置リスト, axis=軸)
: 配列を指定した数または位置で分割します。np.vsplit(配列, 分割数 or 分割位置リスト)
: 配列を垂直方向(行方向)に分割します。split
でaxis=0
を指定するのと同じです。np.hsplit(配列, 分割数 or 分割位置リスト)
: 配列を水平方向(列方向)に分割します。split
でaxis=1
を指定するのと同じです。
“`python
import numpy as np
a = np.array([[1, 2], [3, 4]]) # 2×2
b = np.array([[5, 6]]) # 1×2
c = np.array([[7], [8]]) # 2×1
垂直方向に結合 (vstack)
aとbを結合する場合、列数が一致する必要がある
vertical_stack = np.vstack((a, b)) # 形状が合わないためエラー
形状を合わせる必要がある
b_reshaped = b.reshape(1, 2)
vertical_stack = np.vstack((a, b_reshaped))
print(“vstack:\n”, vertical_stack)
出力:
[[1 2]
[3 4]
[5 6]]
水平方向に結合 (hstack)
aとcを結合する場合、行数が一致する必要がある
horizontal_stack = np.hstack((a, c)) # 形状が合わないためエラー
形状を合わせる必要がある
c_reshaped = c.reshape(2, 1)
horizontal_stack = np.hstack((a, c_reshaped))
print(“hstack:\n”, horizontal_stack)
出力:
[[1 2 7]
[3 4 8]]
matrix_to_split = np.array([
[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]
]) # 4×4
垂直方向に2分割 (2つの2×4配列になる)
v_split = np.vsplit(matrix_to_split, 2)
print(“vsplit (2分割):\n”, v_split)
出力:
[array([[1, 2, 3, 4], [5, 6, 7, 8]]), array([[ 9, 10, 11, 12], [13, 14, 15, 16]])]
(リストの中に2つのNumPy配列が入っている)
水平方向に [1, 3] の位置で分割 (3つの配列になる)
0列目から1列目の手前、1列目から3列目の手前、3列目から末尾
h_split = np.hsplit(matrix_to_split, [1, 3])
print(“hsplit ([1, 3]で分割):\n”, h_split)
出力:
[array([[ 1], [ 5], [ 9], [13]]), array([[ 2, 3], [ 6, 7], [10, 11], [14, 15]]), array([[ 4], [ 8], [12], [16]])]
(リストの中に3つのNumPy配列が入っている)
“`
結合・分割関数を使う際は、結合・分割する軸に対して配列の形状が一致している必要があることに注意が必要です。
NumPy配列の応用例
- 数値計算・統計処理: 大量のデータに対する合計、平均、標準偏差などの計算。
- 画像処理: 画像をピクセル値のNumPy配列として読み込み、明るさ調整(全要素に定数を加算)、フィルタリング(畳み込みなど)、リサイズなどの処理を行う。
- 機械学習: 特徴量ベクトルや行列、ニューラルネットワークの重みなどはNumPy配列として表現・操作されることがほとんどです。
- 物理シミュレーション: 多数の点の位置や速度などを配列で管理し、時間発展を計算する。
NumPy配列の利点と欠点
利点:
- 高速性: 大規模な数値計算において、Python標準のリストを使った場合と比較して圧倒的に高速です。
- メモリ効率: 特に数値データを大量に扱う際に、メモリ使用量を抑えられます。
- 豊富な機能: 数学、統計、線形代数など、科学技術計算に必要な高度な機能が多数提供されています。
- 簡潔な記述: ベクトル化、ブロードキャストにより、少ないコードで複雑な処理を記述できます。
欠点:
- NumPyライブラリへの依存: NumPyをインストールする必要があります。
- 要素の型: 原則として、配列内のすべての要素は同じデータ型である必要があります(これにより高速化とメモリ効率が実現されています)。異なる型のデータを混在させたい場合は、リストのリストの方が適している場合があります。
- 学習コスト: リストに比べて、NumPy特有の概念(dtype, shape, axis, ブロードキャストなど)を理解する必要があります。
NumPyは、Pythonで数値データを本格的に扱うなら必須と言えるライブラリです。データ分析ライブラリPandasや機械学習ライブラリscikit-learn、深層学習ライブラリTensorFlow/PyTorchなども、内部的にNumPyを利用しています。
4. どちらを使うべきか?リストのリスト vs NumPy ndarray
Python標準のリストを使った二次元配列と、NumPyの ndarray
。どちらを使うべきかは、扱うデータの特性、データの規模、必要な処理によって異なります。
リストのリストが適しているケース:
- 小規模なデータ: データ量が少なく、パフォーマンスがボトルネックにならない場合。
- 非均一なデータ型: 行や列の中で、整数、文字列、浮動小数点数など、異なるデータ型を混在させたい場合。
- 簡単な表形式データ: 厳密な数値計算よりも、データを「表」として表現・管理したい場合。
- NumPyの依存を避けたい場合: 非常にシンプルなスクリプトで、追加ライブラリのインストールを避けたい場合。
- 行単位の操作が主で、列単位の操作や数値計算がほとんどない場合。
例: 設定情報を保持する小さな表、ゲームの盤面(各マスに「空」「石」「駒」など異なる種類の情報が入る場合)、異なる型の情報を1行にまとめたリストの集まり。
NumPyが適しているケース:
- 大規模な数値データ: 数万、数百万といったオーダーのデータを扱う場合。
- 高速な計算が必要な場合: 特に要素ごとの演算、行列演算、統計処理など、大量の数値計算を行う場合。
- 科学技術計算、データ分析、機械学習: これらの分野ではNumPyの機能が不可欠です。
- 画像処理: 画像を数値データとして扱う場合。
- 配列全体や部分配列に対する数学的な操作が多い場合。
例: センサーデータ、財務データ、画像データ、機械学習モデルのパラメータ、物理シミュレーションの結果。
簡単な判断基準:
もし、あなたが扱っているデータが主に数値であり、そのデータに対して何らかの計算(合計、平均、標準偏差、行列積など)を行いたい、あるいはデータ量が大きい、という場合は、迷わずNumPyを使うことを推奨します。NumPyを使うことで、コードが簡潔になり、実行速度も大幅に向上することが期待できます。
一方で、もしデータが小さく、要素の型がばらばらで、主に個々の要素にアクセスしたり、行を追加・削除したりする程度の簡単な操作しか行わないのであれば、リストのリストで十分な場合もあります。
ただし、データ分析や機械学習を学ぶ上で、NumPyは基本的なツールとなるため、数値データを扱う機会が多いのであれば、早めにNumPyの使い方を習得しておくことをお勧めします。多くのライブラリがNumPy配列を前提として設計されています。
5. よくある間違いとトラブルシューティング
二次元配列を扱う上で、特に初心者の方が陥りやすい間違いと、その対処法について見ていきましょう。
a) リスト内包表記でのシャローコピー問題 ([[初期値] * cols] * rows
)
間違い:
“`python
間違い!
rows = 3
cols = 5
matrix = [[0] * cols] * rows
print(matrix)
matrix[1][2] = 99
print(matrix) # 他の行も変わってしまう
“`
理由: [0] * cols
で作成された同じリストオブジェクトが、外側のリストに rows
回参照される形で格納されるためです。
正しい対処法:
各行が独立したリストとして作成されるように、ネストしたリスト内包表記を使用します。
“`python
正しい
rows = 3
cols = 5
matrix = [[0 for j in range(cols)] for i in range(rows)]
print(matrix)
matrix[1][2] = 99
print(matrix) # 1行目だけが変わる
“`
あるいは、NumPyを使用します。
“`python
import numpy as np
NumPy (正しい)
rows = 3
cols = 5
matrix = np.zeros((rows, cols), dtype=int)
print(matrix)
matrix[1, 2] = 99
print(matrix)
“`
NumPyの初期化関数は、常に独立した要素を持つ新しい配列を作成します。
b) NumPyでのブロードキャストの理解不足
間違い:
形状が互換性のないNumPy配列同士で演算を行おうとする。
“`python
import numpy as np
a = np.array([[1, 2], [3, 4]]) # 2×2
b = np.array([1, 2, 3]) # 1×3
形状が異なるためエラーになる
print(a + b) # ValueError: operands could not be broadcast together …
“`
理由: NumPyのブロードキャスト規則を満たさない形状の配列同士では、要素ごとの演算はできません。
対処法:
演算を行う前に、配列の shape
属性を確認し、ブロードキャスト規則を満たすようにどちらか(または両方)の配列の形状を調整します。reshape
や新しい軸の追加 (np.newaxis
) を使います。
“`python
import numpy as np
a = np.array([[1, 2], [3, 4]]) # 2×2
例: 1次元配列を列ベクトルとして扱いたい場合
b = np.array([10, 20]) # 1次元, 形状 (2,)
これを列ベクトル (2, 1) に変換
b_col = b[:, np.newaxis] # または b.reshape((2, 1))
print(“a:\n”, a)
print(“b_col:\n”, b_col)
print(“a + b_col:\n”, a + b_col) # a (2,2) と b_col (2,1) はブロードキャスト可能
出力:
a:
[[1 2]
[3 4]]
b_col:
[[10]
[20]]
a + b_col:
[[11 12]
[23 24]]
``
ValueError: operands could not be broadcast together with shapes (…) (…)` は、ブロードキャストの問題を示しています。エラーメッセージに表示される形状を確認し、どこに不整合があるかを特定することが重要です。
エラーメッセージ
c) NumPyでの軸 (axis
) の指定ミス
間違い:
集計関数などで axis
を指定する際に、期待する方向と逆を指定してしまう。
“`python
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])
行方向の合計を出したいのに axis=0 を指定してしまう
print(np.sum(matrix, axis=0)) # これは列ごとの合計になる
“`
理由: axis=0
は「0番目の軸(行)」を潰して計算する、つまり列方向に計算を進めていくため、結果として「各列の集計値」が得られます。axis=1
は「1番目の軸(列)」を潰して計算するため、行方向に計算を進めていき、「各行の集計値」が得られます。
正しい対処法:
axis=0
は列方向の操作、axis=1
は行方向の操作(二次元の場合)と覚えるか、小さな配列で実際に試して確認します。
axis=0
-> 結果の形状から0番目の次元(行)がなくなる -> 列ごとの計算axis=1
-> 結果の形状から1番目の次元(列)がなくなる -> 行ごとの計算
“`python
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])
列ごとの合計 (axis=0)
print(“列ごとの合計:”, np.sum(matrix, axis=0)) # 出力: [5 7 9]
行ごとの合計 (axis=1)
print(“行ごとの合計:”, np.sum(matrix, axis=1)) # 出力: [ 6 15]
“`
d) インデックス範囲エラー (IndexError
)
間違い:
存在しないインデックスにアクセスしようとする。
“`python
my_list_2d = [[1, 2], [3, 4]]
print(my_list_2d[2][0]) # IndexError: list index out of range
my_numpy_array = np.array([[1, 2], [3, 4]])
print(my_numpy_array[2, 0]) # IndexError: index 2 is out of bounds for axis 0 with size 2
“`
理由: リストや配列のサイズを超えるインデックスを指定しています。
対処法:
アクセスする前に、リストや配列のサイズ(リストの場合は len(list)
や len(list[0])
、NumPyの場合は array.shape
)を確認します。ループ処理の場合は、range(len(list))
や range(array.shape[0])
, range(array.shape[1])
など、適切な範囲でインデックスを生成するようにします。
e) NumPyでの形状不一致エラー (ValueError
)
間違い:
形状を変更する reshape
や、結合する concatenate
/vstack
/hstack
などで、要素数が一致しない形状を指定したり、結合軸に対して形状が合わない配列を渡したりする。
“`python
import numpy as np
arr = np.arange(6) # array([0, 1, 2, 3, 4, 5]) size=6
要素数が6なのに、2×4 (要素数8) の形状を指定
reshaped_arr = arr.reshape((2, 4)) # ValueError: cannot reshape array of size 6 into shape (2,4)
a = np.array([[1, 2], [3, 4]]) # 2×2
b = np.array([[5, 6, 7]]) # 1×3
vstack は列数が一致しないとエラー
np.vstack((a, b)) # ValueError: all the input arrays must have same number of dimensions, and the dimensions along axis 1 must be equal.
“`
理由: reshape
は要素数を変えずに形状だけを変更する操作なので、元の配列の要素数と新しい形状の要素数を掛け合わせたものが一致しないとエラーになります。結合関数では、指定した軸以外の次元のサイズが一致している必要があります。
対処法:
エラーメッセージに表示される形状 (shape
) や要素数 (size
) を確認し、指定した形状や結合する配列の形状が規則を満たしているか確認します。reshape
する際は、新しい形状の要素数の積が元の要素数と同じになるようにします。結合する際は、結合する軸以外の次元のサイズが一致していることを確認します。reshape
の際に -1
を使うと、NumPyに自動で計算させることができます(例: arr.reshape((-1, 2))
は要素数6の配列を ?x2 の形状にする -> 3×2 になる)。
6. より進んだトピック (簡潔に)
a) 高次元配列
NumPyは二次元配列だけでなく、三次元以上の高次元配列も同じ ndarray
オブジェクトとして扱うことができます。三次元配列は「深さ(またはチャンネル)」を持つデータ(例: カラー画像 – 高さx幅x色チャンネル)、四次元配列は「時間」や「バッチサイズ」などの次元を持つデータ(例: 動画、深層学習の入力データ)などに使われます。
“`python
import numpy as np
2x3x4 の三次元配列
array_3d = np.zeros((2, 3, 4))
print(“三次元配列の形状:”, array_3d.shape) # 出力: (2, 3, 4)
print(“三次元配列の次元数:”, array_3d.ndim) # 出力: 3
要素へのアクセス (例: 1番目の層, 0行目, 2列目の要素)
print(array_3d[1, 0, 2])
“`
高次元配列でも、インデックス、スライス、ブロードキャスト、集計関数などの概念は同様に適用されます。
b) Pandasとの連携
データ分析ライブラリであるPandasは、NumPyの上に構築されており、表形式データに特化した DataFrame
という強力なデータ構造を提供します。DataFrame
は、行と列にラベル(インデックス名、列名)を付けることができ、欠損値の扱い、データクリーニング、統計分析、データの読み書きなどが容易に行えます。
Pandasの DataFrame
は、内部的にはNumPy配列を利用しています。NumPy配列とPandas DataFrame
間は簡単に変換できます。
“`python
import pandas as pd
import numpy as np
NumPy配列からPandas DataFrameを作成
np_array = np.array([
[10, 20, 30],
[40, 50, 60]
])
df = pd.DataFrame(np_array, columns=[‘ColA’, ‘ColB’, ‘ColC’], index=[‘RowX’, ‘RowY’])
print(“NumPyからDataFrame:\n”, df)
Pandas DataFrameからNumPy配列を取得
np_from_df = df.values
print(“\nDataFrameからNumPy:\n”, np_from_df)
“`
データ分析の現場では、生データの読み込みや前処理にPandas、数値計算や高度な配列操作にNumPyというように、両者を組み合わせて使うことが非常に多いです。
7. まとめ
この記事では、Pythonで二次元配列を扱うための主要な二つの方法、すなわちPython標準機能のリストを使った方法(リストのリスト)と、NumPyライブラリを使った方法(ndarray)について詳細に解説しました。
リストのリスト は、Pythonに標準で備わっている機能であり、手軽に二次元構造を表現できます。要素の型が混在しても問題なく、小規模なデータを扱う場合には十分な柔軟性を持っています。ただし、シャローコピーの問題には注意が必要であり、大規模データや数値計算には向いていません。列方向の操作がやや煩雑になる点も欠点です。
NumPyのndarray は、科学技術計算やデータ分析においてデファクトスタンダードとなっている強力な多次元配列オブジェクトです。要素の型は均一である必要がありますが、C言語で実装されているため高速であり、メモリ効率も優れています。作成、アクセス、操作、数学関数、形状操作など、配列を扱うための豊富な機能が提供されており、特にベクトル化演算やブロードキャスト機能により、簡潔かつ効率的なコード記述が可能です。
どちらの方法を選ぶかは、あなたの解決したい問題や扱っているデータの特性によって異なります。
- 簡単な表形式のデータ、小規模、非数値データ、型の混在 → リストのリスト
- 数値データ、大規模、高速な計算・操作が必要、科学技術計算・データ分析 → NumPy ndarray
多くの場面で、特に数値データを扱う場合はNumPyが強力な味方となります。データ分析や機械学習に進むのであれば、NumPyの習得は避けて通れません。
二次元配列は、様々な種類のデータを構造化して扱うための基本的なツールです。リストのリストでの基本的な考え方を理解した上で、NumPyを使うことでその真価を発揮する場面が多いことを学びました。
これで、Pythonでの二次元配列の作り方や使い方で迷うことはなくなるはずです。ぜひ、この記事で学んだことを活かして、あなたのPythonプログラミングに役立ててください。
もし、さらに疑問点が出てきたり、特定の応用例について知りたい場合は、遠慮なく質問してください。
Happy Coding!