Pythonでの数値計算：NumPyを使った配列操作と演算

## はじめに
NumPyは、Pythonでの数値計算やデータ処理を行う際に使います。特に、多次元配列や行列の操作、科学計算、データ分析、画像処理など、色々な場面で活用することができます。では、NumPyの使い方を書いていきます！

### ・配列の作成
NumPyの基本的なデータ構造はndarray（N次元配列）です。配列はnp.array()を使って作成します。

#### 1次元配列の作成
NumPyで1次元配列を作成するには、np.array()を使用します。1次元配列は、リストのように並んだ一連の数値データを表します。
```python
import numpy as np

arr = np.array([1, 2, 3, 4, 5])
print(arr)
# 出力: [1 2 3 4 5]
```
ここで、np.array()関数にリスト形式のデータを渡すことで、NumPyの1次元配列を作成しています。arrは配列オブジェクトで、数値データが格納されています。リストと異なり、NumPyの配列は同じ型の要素が格納されており、数値計算に特化しています。



#### 2次元配列の作成
```python
arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(arr_2d)
# 出力:
# [[1 2 3]
#  [4 5 6]
#  [7 8 9]]
```

#### 特殊な配列の作成
NumPyには、特定の条件に基づいて配列を作成するための関数がいくつかあります。

**np.zeros()**
np.zeros()を使うと、指定した形状の全ての要素が0の配列を簡単に作成できます。

```python
zeros_array = np.zeros((3, 4))
print(zeros_array)
# 出力: 
# [[0. 0. 0. 0.]
#  [0. 0. 0. 0.]
#  [0. 0. 0. 0.]]
```

**np.ones()**
np.ones()は、全ての要素が1の配列を作成します。

```python
ones_array = np.ones((2, 3))
print(ones_array)
# 出力: 
# [[1. 1. 1.]
#  [1. 1. 1.]]
```

**np.arange()**
np.arange()は、指定した範囲内で等間隔の数値を持つ配列を作成します。

```python
range_array = np.arange(0, 10, 2)
print(range_array)
# 出力: [0 2 4 6 8]
```

**np.linspace()**
np.linspace()は、指定した範囲を等間隔に分割した数値の配列を作成します。np.arange()と異なり、開始点と終了点を含めて等間隔で分割します。

```python
linspace_array = np.linspace(0, 1, 5)
print(linspace_array)
# 出力: [0.   0.25 0.5  0.75 1.  ]
```

**np.random.rand()**
np.random.rand()は、0から1の範囲のランダムな浮動小数点数を持つ配列を作成します。

```python
random_array = np.random.rand(3, 3)
print(random_array)
# 出力: 
# [[0.6389612  0.17896009 0.29596171]
#  [0.85380865 0.27858462 0.60119412]
#  [0.91632559 0.92601163 0.1574946 ]]
```

### ・配列のインデックスとスライシング
配列のインデックスとスライシングは、NumPyの配列（ndarray）から特定の要素や部分を取り出すことができます。これにより、配列全体ではなく、配列の一部を効率的に操作したり、データを抽出することができます。


#### 1次元配列のインデックス指定
1次元配列の場合、インデックスを指定して特定の要素にアクセスします。
```python
import numpy as np

arr = np.array([10, 20, 30, 40, 50])
print(arr[0])  # 最初の要素
# 出力: 10

print(arr[2])  # 3番目の要素
# 出力: 30

print(arr[-1])  # 最後の要素（負のインデックス）
# 出力: 50
```
正のインデックス: 配列の先頭から順に番号を振り、0が最初の要素になります。
負のインデックス: 配列の末尾から番号を振り、-1が最後の要素を指します。

#### 2次元配列のインデックス指定
2次元配列の場合、インデックスは行と列で指定します。[行, 列]の形式で、特定の要素にアクセスします。
```python
arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print(arr_2d[0, 1])  # 1行目の2列目の要素
# 出力: 2

print(arr_2d[2, 2])  # 3行目の3列目の要素
# 出力: 9

print(arr_2d[-1, -1])  # 最後の行の最後の列の要素
# 出力: 9
```

#### スライシング
スライシングは、配列の一部を抽出することができます。スライスでは、範囲を指定して複数の要素にアクセスします。start:stop:stepという形式でスライスを指定し、配列の部分を取得します。

#### 1次元配列のスライシング
```python
arr = np.array([10, 20, 30, 40, 50])

# 2番目から4番目の要素を取得（stopは含まない）
print(arr[1:4])
# 出力: [20 30 40]

# 最初の3つの要素を取得
print(arr[:3])
# 出力: [10 20 30]

# 3番目から最後まで取得
print(arr[2:])
# 出力: [30 40 50]

# ステップを指定して取得（2つおき）
print(arr[::2])
# 出力: [10 30 50]
```
start: 範囲の開始インデックス（省略すると最初から）。
stop: 範囲の終了インデックス（stopは含まれない）。
step: スライスの間隔（省略すると1）。

#### 2次元配列のスライシング
2次元配列では、行と列に対してスライシングを行います。
```python
arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

# 1行目から2行目、2列目から3列目の要素を取得
print(arr_2d[0:2, 1:3])
# 出力: 
# [[2 3]
#  [5 6]]

# すべての行、2列目以降を取得
print(arr_2d[:, 1:])
# 出力: 
# [[2 3]
#  [5 6]
#  [8 9]]

# 1行目のみ取得
print(arr_2d[0, :])
# 出力: [1 2 3]

# すべての行、1列目のみ取得
print(arr_2d[:, 0])
# 出力: [1 4 7]
```
#### スライシングのステップ
スライシングにおいて、ステップ（step）を指定することで、指定した間隔で要素を取り出すことができます。
```python
arr = np.array([10, 20, 30, 40, 50, 60])

# 2つおきに要素を取得
print(arr[::2])
# 出力: [10 30 50]

# 配列を逆順に取得
print(arr[::-1])
# 出力: [60 50 40 30 20 10]
```

#### 2次元配列のスライシングとステップ
2次元配列でも、行や列に対してステップを指定できます。
```python
arr_2d = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])

# 1行おき、2列おきに要素を取得
print(arr_2d[::2, ::2])
# 出力: 
# [[ 1  3]
#  [ 9 11]]

# 行を逆順に取得
print(arr_2d[::-1, :])
# 出力: 
# [[ 9 10 11 12]
#  [ 5  6  7  8]
#  [ 1  2  3  4]]
```


### ・配列の形状変更
NumPyでは、配列の形状を変更する機能が提供されています。これは、多次元配列を新しい形に再構成するために使われます。例えば、1次元配列を2次元に変更したり、3次元配列にするなど、配列の形状を柔軟に操作できます。
#### reshape()
reshape()を使用すると、配列の要素数を変更せずに形状を変更できます。指定する形状は元の配列の要素数と一致していなければなりません。
```python
import numpy as np

arr = np.arange(12)  # 1次元配列
print(arr)
# 出力: [ 0  1  2  3  4  5  6  7  8  9 10 11]

reshaped_arr = arr.reshape((3, 4))  # 3行4列の2次元配列に形状変更
print(reshaped_arr)
# 出力:
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]
```

#### flatten()
flatten()は、多次元配列を1次元配列に変換するメソッドです。元の形状は無視され、すべての要素が1次元にまとめられます。
```python
arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
flattened_arr = arr_2d.flatten()
print(flattened_arr)
# 出力: [1 2 3 4 5 6 7 8 9]
```
#### ravel()
ravel()も配列を1次元に変換する点でflatten()と似ていますが、ravel()は元の配列と同じメモリを共有するため、変更を加えると元の配列にも影響を与えます。

```python
raveled_arr = arr_2d.ravel()
raveled_arr[0] = 99
print(arr_2d)
# 出力:
# [[99  2  3]
#  [ 4  5  6]
#  [ 7  8  9]]
```

#### transpose()
transpose()を使うと、行列を転置することができます。つまり、行と列を入れ替えます。
```python
transposed_arr = arr_2d.transpose()
print(transposed_arr)
# 出力:
# [[1 4 7]
#  [2 5 8]
#  [3 6 9]]
```

#### resize()
resize()は、reshape()と似ていますが、元の配列自体を変更します。新しい形状に対応する要素が足りない場合は0で埋められます。

```python
arr = np.array([1, 2, 3, 4, 5, 6])
arr.resize((2, 4))
print(arr)
# 出力:
# [[1 2 3 4]
#  [5 6 0 0]]
```


### ・配列の演算
NumPy配列では、要素ごとの演算を効率的に行えますに。これにより、ループを使わずに配列全体に対して加減乗除などの演算を行うことができます。また、配列同士の演算やスカラーとの演算もサポートされています。

#### 配列同士の演算
配列同士の演算は、要素ごとに計算されます。配列の形状が一致している必要があります。
```python
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

# 要素ごとの加算
print(arr1 + arr2)
# 出力: [5 7 9]

# 要素ごとの乗算
print(arr1 * arr2)
# 出力: [ 4 10 18]

# 要素ごとの減算
print(arr1 - arr2)
# 出力: [-3 -3 -3]
```

#### スカラーとの演算
NumPyでは、スカラー（単一の数値）との演算も、各要素に対して一括で実行できます。
```python
arr = np.array([1, 2, 3])

# 各要素に2を掛ける
print(arr * 2)
# 出力: [2 4 6]

# 各要素に3を足す
print(arr + 3)
# 出力: [4 5 6]
```

#### ブロードキャスティング
ブロードキャスティングは、異なる形状の配列間での演算を可能にする機能です。NumPyは自動的に形状を調整して、要素ごとの演算を行います。


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

# スカラーとの加算（各要素に10を足す）
print(arr + scalar)
# 出力:
# [[11 12 13]
#  [14 15 16]]

# 1次元配列と2次元配列の加算
arr2 = np.array([1, 2, 3])
print(arr + arr2)
# 出力:
# [[ 2  4  6]
#  [ 5  7  9]]
```

### ・統計関数
NumPyは、データ解析や機械学習の前処理で重要な統計量を計算するための関数を用意しています。これにより、平均値、中央値、分散、標準偏差などの統計量を簡単に計算することができます。

#### 平均値
np.mean()は、配列全体や指定した軸に沿った平均値を計算します。
```python
arr = np.array([1, 2, 3, 4, 5])

# 配列全体の平均
print(np.mean(arr))
# 出力: 3.0
```

#### 中央値
np.median()は、配列の中央値（真ん中の値）を計算します。

```python
arr = np.array([1, 3, 5, 7, 9])

# 配列の中央値
print(np.median(arr))
# 出力: 5.0
```

#### 分散
np.var()は、配列の分散（データの散らばり具合）を計算します。
```python
arr = np.array([1, 2, 3, 4, 5])

# 配列の分散
print(np.var(arr))
# 出力: 2.0
```

#### 標準偏差
np.std()は、分散の平方根である標準偏差を計算します。
```python
arr = np.array([1, 2, 3, 4, 5])

# 配列の標準偏差
print(np.std(arr))
# 出力: 1.4142135623730951
```

#### 最小値と最大値
np.min()およびnp.max()は、配列の最小値と最大値を計算します。
```python
arr = np.array([1, 2, 3, 4, 5])

# 最小値
print(np.min(arr))
# 出力: 1

# 最大値
print(np.max(arr))
# 出力: 5
```

#### 和と積
np.sum()は配列の要素の合計を、np.prod()は要素の積を計算します。
```python
arr = np.array([1, 2, 3, 4, 5])

# 合計
print(np.sum(arr))
# 出力: 15

# 積
print(np.prod(arr))
# 出力: 120
```