pandasの`df.index = List[...]`的な代入文が嫌い

# pandasの`df.index = List[...]`的な代入文が嫌い

pandasにおいて、データフレームの行名や列名を設定・更新するとき、`df.index = List[...]`的な代入操作は普遍的に行われている。つまり、

```python
df = pd.DataFrame(np.random.randn(5, 3))
print(df)
#           0         1         2
# 0  0.221652 -0.243116 -0.551044
# 1  0.285863 -0.596317  0.070156
# 2  1.404026  0.362331 -0.237264
# 3  0.320011  1.144342  1.560148
# 4 -1.386724 -0.234210 -0.488311
```

のようなデータフレームがあったときに、

```python
df.columns = ['A', 'B', 'C']
print(df)
#           A         B         C
# 0  0.221652 -0.243116 -0.551044
# 1  0.285863 -0.596317  0.070156
# 2  1.404026  0.362331 -0.237264
# 3  0.320011  1.144342  1.560148
# 4 -1.386724 -0.234210 -0.488311
```

のような操作をすることが、一般的に行われている。

公式ドキュメントにもこっそり

> If you create an index yourself, you can just assign it to the `index` field:
>
> ```python
> data.index = index
> ```
>
> ――[Indexing and selecting data — pandas 1.0.1 documentation](https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#adding-an-ad-hoc-index)

などと書いてある。

僕も、わけもわからずネットのコードを写経していた時代にはよくやっていた。

しかし、このような操作（表記というべきか？）は避けるべきではなかろうか。

```python
df.columns = ['A', 'B', 'C']
```

なにやら奇妙な一行という感じがする。この違和感がどこからくるのかよくわからないが。

オブジェクトの属性に直接リストを突っ込むというところに、なんとも言えぬ気持ち悪さを感じるのかもしれない。

## じゃあどうすればいいのか

`pd.DataFrame.set_axis()`あるいは`pd.DataFrame.rename()`を使おう。

### `set_axis()`の使い方

```python
df = pd.DataFrame(np.random.randn(5, 3))
print(df)
#           0         1         2
# 0  0.221652 -0.243116 -0.551044
# 1  0.285863 -0.596317  0.070156
# 2  1.404026  0.362331 -0.237264
# 3  0.320011  1.144342  1.560148
# 4 -1.386724 -0.234210 -0.488311
```

のようなデータフレームがあったとき、以下のようにして列名を変更する。

```python
# df.columns = ['A', 'B', 'C'] ではなく
df.set_axis(['A', 'B', 'C'],
            axis='columns', inplace=True)
print(df)
#           A         B         C
# 0  0.221652 -0.243116 -0.551044
# 1  0.285863 -0.596317  0.070156
# 2  1.404026  0.362331 -0.237264
# 3  0.320011  1.144342  1.560148
# 4 -1.386724 -0.234210 -0.488311
```

`set_axis()`の第一引数に名前のリストを渡し、続いて`index`（行名）を変更するときは`axis=0`あるいは`axis='index'`、`columns`（列名）を変更するときは`axis=1`あるいは`axis='columns'`とする。`inplace`オプションは他のメソッドと同じ。

`set_axis()`の第一引数には名前のリストだけでなく、`Index`オブジェクトを渡すこともできる。したがって、`pd.MultiIndex`を渡すことで、マルチインデックスにすることができる。

例えば、公式のクックブック『[Creating a MultiIndex from a labeled frame](https://pandas.pydata.org/pandas-docs/stable/user_guide/cookbook.html#multiindexing)』では

```python
df = pd.DataFrame({'One_X': [1.1, 1.1, 1.1],
                   'One_Y': [1.2, 1.2, 1.2],
                   'Two_X': [1.11, 1.11, 1.11],
                   'Two_Y': [1.22, 1.22, 1.22]})
print(df)
#    One_X  One_Y  Two_X  Two_Y
# 0    1.1    1.2   1.11   1.22
# 1    1.1    1.2   1.11   1.22
# 2    1.1    1.2   1.11   1.22
```

この`columns`をマルチインデックスにするのに、以下の操作を行っている。

```python
df.columns = pd.MultiIndex.from_tuples([tuple(c.split('_'))
                                        for c in df.columns])
print(df)
#    One        Two
#      X    Y     X     Y
# 0  1.1  1.2  1.11  1.22
# 1  1.1  1.2  1.11  1.22
# 2  1.1  1.2  1.11  1.22
```

これは以下のようにする。

```python
df.set_axis(pd.MultiIndex.from_tuples([tuple(c.split('_'))
                                       for c in df.columns]),
            axis='columns', inplace=True)
print(df)
#    One        Two
#      X    Y     X     Y
# 0  1.1  1.2  1.11  1.22
# 1  1.1  1.2  1.11  1.22
# 2  1.1  1.2  1.11  1.22
```

結局、要は

```python
df.index = new_idx
df.columns = new_col

# ↑同じ処理（表記が違うだけ）↓

df.set_axis(new_idx, axis='index', inplace=True)
df.set_axis(new_col, axis='columns', inplace=True)
```

ということである。

### `rename()`の使い方

`pd.DataFrame.rename()`は、既存の行明・列名を変換するときに用いる。

まず、辞書で１：１の置換ができる。辞書のキーに存在しない列名があっても、エラーが出るということはない。

```python
df = pd.DataFrame(np.random.randn(5, 3))
print(df)
#           0         1         2
# 0  0.221652 -0.243116 -0.551044
# 1  0.285863 -0.596317  0.070156
# 2  1.404026  0.362331 -0.237264
# 3  0.320011  1.144342  1.560148
# 4 -1.386724 -0.234210 -0.488311

df.rename(columns={0: 'A', 1: 'B'}, inplace=True)
print(df)
#           A         B         2
# 0  0.221652 -0.243116 -0.551044
# 1  0.285863 -0.596317  0.070156
# 2  1.404026  0.362331 -0.237264
# 3  0.320011  1.144342  1.560148
# 4 -1.386724 -0.234210 -0.488311

df.rename(columns={2: 'C', 3: 'D'}, inplace=True)
print(df)
#           A         B         C
# 0  0.221652 -0.243116 -0.551044
# 1  0.285863 -0.596317  0.070156
# 2  1.404026  0.362331 -0.237264
# 3  0.320011  1.144342  1.560148
# 4 -1.386724 -0.234210 -0.488311
```

`index`と`columns`を両方同時に変更できる。

```python
df = pd.DataFrame(np.random.randn(5, 3))
print(df)
#           0         1         2
# 0  0.221652 -0.243116 -0.551044
# 1  0.285863 -0.596317  0.070156
# 2  1.404026  0.362331 -0.237264
# 3  0.320011  1.144342  1.560148
# 4 -1.386724 -0.234210 -0.488311

df.rename(index={k: v for k, v in enumerate('abcde')},
          columns={k: v for k, v in enumerate('ABC')},
          inplace=True)
print(df)
#           A         B         C
# a  0.221652 -0.243116 -0.551044
# b  0.285863 -0.596317  0.070156
# c  1.404026  0.362331 -0.237264
# d  0.320011  1.144342  1.560148
# e -1.386724 -0.234210 -0.488311
```

また、行名・列名に関数を適用して変換することができる。

```python
df = pd.DataFrame(np.random.randn(5, 3),
                  index=list('abcde'), columns=list('ABC'))
print(df)
#           A         B         C
# a  0.221652 -0.243116 -0.551044
# b  0.285863 -0.596317  0.070156
# c  1.404026  0.362331 -0.237264
# d  0.320011  1.144342  1.560148
# e -1.386724 -0.234210 -0.488311

df.rename(index=str.upper,
          columns=lambda x: x*2,
          inplace=True)
print(df)
#          AA        BB        CC
# A  0.221652 -0.243116 -0.551044
# B  0.285863 -0.596317  0.070156
# C  1.404026  0.362331 -0.237264
# D  0.320011  1.144342  1.560148
# E -1.386724 -0.234210 -0.488311
```

以上のように、`rename()`は、`index`あるいは`columns`引数に置換辞書ないし関数を渡して用いる。`set_axis()`のように、以下のような書き方もできるが、おすすめはしない。

```python
df.rename({0: 'A', 1: 'B'}, axis='columns', inplace=True)
```

## 何が良いか

`df.set_axis()`は`df.index = List[...]`的なやつと何が違うのか？

まず、気持ち悪さがない。

次に、`inplace`オプションがあるため、元のデータフレームの行明・列名を変更するのか、行明・列名を変更したデータフレームのコピーを作成するのか選択できる。後者を実行する場合、`df.index = List[...]`的なやつは２行を要する。

```python
df2 = df.copy()
df2.index = new_idx
```

`set_axis()`はより短く書ける。

```python
df2 = df.set_axis(new_idx)
```

また、`set_axis()`は文ではないので、メソッドチェーンに加えたり、リスト内包表記の中で処理することができる。例えば、

```python
df = pd.concat([df_sub.set_axis(['A', 'B', 'C'], axis='columns').drop(columns='B')
                for df_sub in list_of_dfs])
```

みたいなことができる。

また、`rename()`はマルチインデックスの行名・列名を変更するときに便利である。

```python
print(df)
#    One        Two
#      X    Y     X     Y
# 0  1.1  1.2  1.11  1.22
# 1  1.1  1.2  1.11  1.22
# 2  1.1  1.2  1.11  1.22

# 複数のレベルを一度に変更
df.rename(columns={'One': 'one', 'X': 'x', 'Y': 'y'}, inplace=True)
print(df)
#    one        Two
#      x    y     x     y
# 0  1.1  1.2  1.11  1.22
# 1  1.1  1.2  1.11  1.22
# 2  1.1  1.2  1.11  1.22

# 特定レベルのみに関数適用
df.rename(columns=str.upper, level=0, inplace=True)
print(df)
#    ONE        TWO
#      x    y     x     y
# 0  1.1  1.2  1.11  1.22
# 1  1.1  1.2  1.11  1.22
# 2  1.1  1.2  1.11  1.22
```

---

みんなも`df.index = List[...]`からは卒業しよう！