Pandasでよくやる処理まとめ2 （データ参照、編集操作）

[前回](https://qiita.com/yasuaki9973/items/916428272b24c64ed132)に引き続き、今回はデータの参照、編集をする際によく使う処理をまとめてみたいと思います。
※動作検証には、Pandasのバージョン0.25.1を使用しています。

最初に、下記コードを実行してデータフレームのサンプルを作成します。

```python
import pandas as pd

df = pd.DataFrame({
    'name' : ['userA', 'userB', 'userC', 'userD', 'userE'],
    'age' : [20, 25, 41, 33, 22],
    'sex' : ['male', 'female', 'female', 'male', 'male'],
    'price' : 10000
})
```

|   |name  |age |sex    |price |
|---|---   |--- |---    |---   |
|0  |userA |20  |male   |10000 |
|1  |userB |25  |female |10000 |
|2  |userC |41  |female |10000 |
|3  |userD |33  |male   |10000 |
|4  |userE |22  |male   |10000 |


### 特定のカラムを取得する場合

カラム名(列名)で指定すると、対応するカラムのデータが
Seriesの形式で取得することが出来ます。

```python
df['name']
```

|   |name  |
|---|---   |
|0  |userA |
|1  |userB |
|2  |userC |
|3  |userD |
|4  |userE |

### 複数のカラムを取得する場合

カラム名(列名)のリストで指定すると、対応するカラム(複数)のデータが
DataFrameの形式で取得することが出来ます。

```python
target_columns_list = ['name', 'age']
df[target_columns_list]
```

|   |name  |age |
|---|---   |--- |
|0  |userA |20  |
|1  |userB |25  |
|2  |userC |41  |
|3  |userD |33  |
|4  |userE |22  |

### 新規カラムを追加する場合

新規のカラム名を指定して代入操作を行うと、カラムが追加されます。
※既存のカラム名を指定すると、そのカラムが上書きされます。

```python
df['new'] = df['age'] * 10
```

|   |name  |age |sex    |price |new |
|---|---   |--- |---    |---   |--- |
|0  |userA |20  |male   |10000 |200 |
|1  |userB |25  |female |10000 |250 |
|2  |userC |41  |female |10000 |410 |
|3  |userD |33  |male   |10000 |330 |
|4  |userE |22  |male   |10000 |220 |

### 特定の行データのみ取得する場合

特定の行データのみ取得したい場合は、locプロパティにインデックスを指定すると
指定した行のデータをSeriesの形式で取得することが出来ます。


```python
df.loc[0]
```

|   |name  |age |sex    |price |
|---|---   |--- |---    |---   |
|0  |userA |20  |male   |10000 |

### 複数行のデータを取得する場合

複数行のデータを取得したい場合は、locプロパティにインデックスのリストを指定すると
指定した行のデータをまとめてDataFrameの形式で取得することが出来ます。

```python
target_index_list = [0, 1, 2]
df.loc[target_index_list]
```

|   |name  |age |sex    |price |
|---|---   |--- |---    |---   |
|0  |userA |20  |male   |10000 |
|1  |userB |25  |female |10000 |
|2  |userC |41  |female |10000 |

### 行とカラムを指定してデータを取得する場合

あまり頻繁に使う操作ではありませんが、locプロパティでインデックスとカラム名を指定すると
指定した要素のみを取得することが出来ます。

```python

df.loc[1, 'sex']
# female

# 上記で取得した要素に対して代入を行うと、更新される
df.loc[1, 'sex'] = 'updated'
```

|   |name  |age |sex    |price |
|---|---   |--- |---    |---   |
|0  |userA |20  |male   |10000 |
|1  |userB |25  |updated |10000 |
|2  |userC |41  |female |10000 |
|3  |userD |33  |male   |10000 |
|4  |userE |22  |male   |10000 |

### 条件を指定して行を抽出する場合

```python
# 例えばageが25以上の行を抽出する場合
df[(df['age'] >= 25)]

# 先頭に~をつけるとNOTになる
df[~(df['age'] >= 25)]

# ANDで行を抽出する場合
df[(df['age'] >= 25) & (df['sex'] == 'female')]

# ORで行を抽出する場合
df[(df['age'] < 25) | (df['age'] > 40)]
```
以下にANDで行を抽出する場合を例示

|   |name  |age |sex    |price |
|---|---   |--- |---    |---   |
|1  |userB |25  |female |10000 |
|2  |userC |41  |female |10000 |

### groupbyで統計量を計算する場合

```python
# カテゴリー毎に統計量を計算した結果を元のデータフレームに追加したい場合などに便利
# 以下は性別毎に年齢の平均を計算している
# transformに指定する引数を変更すれば、他の統計量も計算出来る
df['average_age_by_sex'] = df.groupby('sex')['age'].transform('mean')

# jupyter notebook などで平均を見たいだけの場合は以下でOK
# df.groupby('sex')['age'].mean()
```
|   |name  |age |sex    |price |new |average_age_by_sex |
|---|---   |--- |---    |---   |--- |---                |
|0  |userA |20  |male   |10000 |200 |25                 |
|1  |userB |25  |female |10000 |250 |33                 |
|2  |userC |41  |female |10000 |410 |33                 |
|3  |userD |33  |male   |10000 |330 |25                 |
|4  |userE |22  |male   |10000 |220 |25                 |

### Pandasで一番単純なループ処理を実装する場合

iterrowsを利用すると、1行ずつインデックスとその行のデータがSeriesの形式
で取得することが出来ます。ただし、データフレームの行数が多くなると処理が遅くなる為、データフレームが100行ぐらいで、ちょっとサボりたいときに使うくらいが限度だと思います。

```python
for index, row in df.iterrows():
    print(index)
    print(type(row))
```

### 特定のカラムを参照して新規のカラムを作成する場合

表題のようなケースで、かつif文を使いたくなるような時は、iterrowsを利用して1行ずつ処理をしたくなると思いますが、大抵の場合は処理速度の観点から止めた方が良いです。
その場合は、以下のようにapplyメソッドを使うと処理を高速化出来ます。

```python
df['age_boundary'] = df['age'].apply(lambda x: '25才以上' if x >=25 else '25才未満')

# 関数を定義してapplyメソッドに渡しても結果は同じ
# def get_age_boundary(age):
#     if age >= 25:
#         return '25才以上'
#     else:
#         return '25才未満'

# df['age_boundary'] = df['age'].apply(get_age_boundary)

```
|   |name  |age |sex    |price |age_boundary |
|---|---   |--- |---    |---   |---          |
|0  |userA |20  |male   |10000 |25才未満      |
|1  |userB |25  |female |10000 |25才以上      |
|2  |userC |41  |female |10000 |25才以上      |
|3  |userD |33  |male   |10000 |25才以上      |
|4  |userE |22  |male   |10000 |25才未満      |

### 複数のカラムを参照して新規のカラムを作成する場合

新しいカラムを作成する為に、複数のカラムを参照する必要があるケースもあると思います。
その場合は、applyメソッドの引数axis=1を指定することで、行毎に各カラムの値を参照することが出来ます。
※この書き方も処理速度的な意味で言うと出来るだけ避けた方が良いです。

```python
def get_price_with_discount_rate(row):

    age = row['age']
    price = row['price']
    
    discount_rate = 1.0
    
    if age >= 40:
        discount_rate = 0.5
            
    return int(price * discount_rate)

df['price_with_discount_rate'] = df.apply(get_price_with_discount_rate, axis=1)

```
|   |name  |age |sex    |price |price_with_discount_rate |
|---|---   |--- |---    |---   |---                      |
|0  |userA |20  |male   |10000 |10000                    |
|1  |userB |25  |female |10000 |10000                    |
|2  |userC |41  |female |10000 |5000                     |
|3  |userD |33  |male   |10000 |10000                    |
|4  |userE |22  |male   |10000 |10000                    |

### np.vectorizeを利用する場合

上記の、複数のカラムを参照して新規のカラムを作成するケースでは、applyメソッドの引数axis=1を指定する方法を取りましたが、このやり方は処理速度の面で問題があります。
上記の代案として、私は実務で利用した事が無いのですが、Numpyのvectorizeメソッドの利用も検討して見ると良いかもしれません。

```python
import numpy as np

def get_price_with_discount_rate(age, price):
        
    discount_rate = 1.0
    
    if age >= 40:
        discount_rate = 0.5
            
    return int(price * discount_rate)

vectorized_func=np.vectorize(get_price_with_discount_rate)
df['price_with_discount_rate'] = vectorized_func(df['age'], df['price'])

```
|   |name  |age |sex    |price |price_with_discount_rate |
|---|---   |--- |---    |---   |---                      |
|0  |userA |20  |male   |10000 |10000                    |
|1  |userB |25  |female |10000 |10000                    |
|2  |userC |41  |female |10000 |5000                     |
|3  |userD |33  |male   |10000 |10000                    |
|4  |userE |22  |male   |10000 |10000                    |

### Series、ndarray、listの相互変換

PandasのSeries、NumPyのndarray、Python標準のlistの相互変換のやり方も
よく忘れるところなので、ここでまとめておきます。
※これ以外のパターンも組み合わせで実現可能です。

```python

import numpy as np

# Series → ndarray
df['name'].values

# ndarray → list
df['name'].values.tolist()

# list → Series
pd.Series([1, 2, 3, 4, 5])

# list → ndarray
np.array([1, 2, 3, 4, 5])

```
---
本記事を執筆するにあたり、以下のサイトを参考にさせて頂きました。
http://sinhrks.hatenablog.com/entry/2015/07/11/223124
https://qiita.com/3x8tacorice/items/3cc5399e18a7e3f9db86
https://note.nkmk.me/python-pandas-numpy-conversion/