dplyr のアレを Pandas でやる

## 概要

N番煎じですが、R の dplyr でやっていたことを Python の Pandas で実現する方法をまとめました。
私が苦戦したものに絞って、試行錯誤の末に得た自分なりの解法を記載しています。
より良い方法をご存知でしたら教えてください！

### dplyr と Pandas の対比まとめ

| dplyr, tidyr, ... | Pandas (おすすめ) | Pandas (イマイチ) | 備考 |
| :-- | :-- | :-- | :-- |
| `%>%` | `.` or `pipe` |  |  |
| `select` | `.loc[]` | `filter` or `drop` |  |
| `filter` | `[]` | `query` |  |
| `mutate` | `assign` |  |  |
| `group_by` | `groupby` |  |  |
| `summarise` | `assign` + `agg` |  |  |
| `*_join` | `merge` |  | まだ調査中です |
| `gather` | `stack` | `melt` |  |
| `spread` | `unstack` | `pivot` |  |

Pandas と dplyr のより網羅的な対比は、既に記事が出ています。
私が参考にしたものを[この記事の末尾](#参考)で紹介していますので、詳しくはそちらをご覧ください 。

## 前置き｜テーブルのキーの扱い方の違い

Pandas を使い始める前に理解すべきこととして、dplyr と Pandas ではテーブルのキーの扱い方に大きな違いがあります。この違いを認識していなかった私はかなり苦戦しました……

以下の2点を理解して (覚悟して？) おくと、学習コストが多少減るのではないでしょうか。

* Pandas ではテーブルのキーが Index という別オブジェクトで管理されていること
* Pandas ではキーや列名に階層構造を持たせられること

### dplyr におけるキーの扱い

R のデータフレームにはテーブルのキーを表す方法として row names という仕組みが用意されています。しかし、dplyr (が利用している、データフレームを拡張したオブジェクトである tibble) では row names をサポートせず (参考：[Hadley さんの Advanced R](https://adv-r.hadley.nz/vectors-chap.html#rownames))、テーブルのキーも値と同様に列として扱います (Pandas のドキュメントの表現を借りるなら "SQL-style" で扱います)。そのため、普段 dplyr を使っている人はテーブルのキーを値と分けて管理するという感覚がないのではないかと思います。

以下の表は dplyr で iris データセットを集計した例です。キーを表す species も値と同様にひとつの列として扱われており、row names のように別枠で管理されてはいません。

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>species</th>
      <th>sepal_length_min</th>
      <th>sepal_length_max</th>
      <th>sepal_width_min</th>
      <th>sepal_width_max</th>
      <th>petal_length_min</th>
      <th>petal_length_max</th>
      <th>petal_width_min</th>
      <th>petal_width_max</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>setosa</td>
      <td>4.3</td>
      <td>5.8</td>
      <td>2.3</td>
      <td>4.4</td>
      <td>1</td>
      <td>1.9</td>
      <td>0.1</td>
      <td>0.6</td>
    </tr>
    <tr>
      <td>versicolor</td>
      <td>4.9</td>
      <td>7</td>
      <td>2</td>
      <td>3.4</td>
      <td>3</td>
      <td>5.1</td>
      <td>1</td>
      <td>1.8</td>
    </tr>
    <tr>
      <td>virginica</td>
      <td>4.9</td>
      <td>7.9</td>
      <td>2.2</td>
      <td>3.8</td>
      <td>4.5</td>
      <td>6.9</td>
      <td>1.4</td>
      <td>2.5</td>
    </tr>
  </tbody>
</table>

### Pandas におけるキーの扱い

一方 Pandas は真逆で、テーブルのキーを表す方法として Index という仕組みを用意しており、テーブルのキーと値を積極的に分けて管理します。そのため、Pandas のデータフレームに対する操作には以下の2種類があり、dplyr と比べるとかなり複雑です。

* dplyr のように列を指定するメソッド
    * 例) `sort_values` (`dplyr::arrange` に相当), `melt` (`tidyr::gather` に相当)
* Index を指定するメソッド
    * 例) `sort_index` (`dplyr::arrange` に相当), `stack` (`tidyr::gather` に相当)

以下の表は Pandas で iris データセットを集計した例です。species と書かれている列が Index で、値を表す他の列とは別に管理されています。また、列名が2段になっていますが、これは MultiIndex と呼ばれるもので、Pandas ではキーや列名に階層構造を持たせることができます。

<table border="1" class="dataframe">
  <thead>
    <tr>
      <th></th>
      <th colspan="2" halign="left">sepal_length</th>
      <th colspan="2" halign="left">sepal_width</th>
      <th colspan="2" halign="left">petal_length</th>
      <th colspan="2" halign="left">petal_width</th>
    </tr>
    <tr>
      <th></th>
      <th>min</th>
      <th>max</th>
      <th>min</th>
      <th>max</th>
      <th>min</th>
      <th>max</th>
      <th>min</th>
      <th>max</th>
    </tr>
    <tr>
      <th>species</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>setosa</th>
      <td>4.3</td>
      <td>5.8</td>
      <td>2.3</td>
      <td>4.4</td>
      <td>1.0</td>
      <td>1.9</td>
      <td>0.1</td>
      <td>0.6</td>
    </tr>
    <tr>
      <th>versicolor</th>
      <td>4.9</td>
      <td>7.0</td>
      <td>2.0</td>
      <td>3.4</td>
      <td>3.0</td>
      <td>5.1</td>
      <td>1.0</td>
      <td>1.8</td>
    </tr>
    <tr>
      <th>virginica</th>
      <td>4.9</td>
      <td>7.9</td>
      <td>2.2</td>
      <td>3.8</td>
      <td>4.5</td>
      <td>6.9</td>
      <td>1.4</td>
      <td>2.5</td>
    </tr>
  </tbody>
</table>

## 本題｜dplyr のアレを Pandas でやる方法

### サンプルデータの読み込み

Pandas の使い方を説明するためのサンプルデータとして、iris データセットを使います。

```python
import numpy as np
import pandas as pd
from sklearn import datasets

iris = datasets.load_iris()
iris = \
    pd.DataFrame(iris.data, columns = iris.feature_names). \
    rename(columns = {
        "sepal length (cm)": "sepal_length",
        "sepal width (cm)": "sepal_width",
        "petal length (cm)": "petal_length",
        "petal width (cm)": "petal_width"
    }). \
    assign(species = iris.target). \
    assign(species = lambda df: df.species.apply(lambda x: iris.target_names[x])). \
    assign(id = np.arange(0, iris.target.size))
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sepal_length</th>
      <th>sepal_width</th>
      <th>petal_length</th>
      <th>petal_width</th>
      <th>species</th>
      <th>id</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>5.1</td>
      <td>3.5</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>4.9</td>
      <td>3.0</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>1</td>
    </tr>
    <tr>
      <th>2</th>
      <td>4.7</td>
      <td>3.2</td>
      <td>1.3</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>2</td>
    </tr>
    <tr>
      <th>3</th>
      <td>4.6</td>
      <td>3.1</td>
      <td>1.5</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>3</td>
    </tr>
    <tr>
      <th>4</th>
      <td>5.0</td>
      <td>3.6</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>4</td>
    </tr>
  </tbody>
</table>

### パイプ `magrittr::%>%`

#### `.` を使う方法

DataFrame オブジェクトのメソッドは基本的に DataFrame オブジェクトを返すので、ひたすら `.` でメソッドチェーンをつなげていけば `magrittr::%>%` のような書き方ができます。

```python
# iris %>% head(10) %>% tail(5)
iris.head(10).tail(5)
```

#### `pipe` を使う方法

DataFrame オブジェクトが持っていない関数を適用したい場合には、`pipe` メソッドを使います。`pipe` メソッドには DataFrame オブジェクトを受け取って DataFrame オブジェクトを返す関数を渡します。DataFrame オブジェクトが持っているメソッドでは不足することが多々あるので、必然的に `pipe` メソッドを頻繁に使うことになります。この記事の中でも何度も使います。

```python
iris.pipe(lambda df: df.iloc[5:10, :])
```

### 列の選択 `dplyr::select`

#### `.loc[]` + `pipe` を使う方法

DataFrame オブジェクトには `filter` や `drop` (後で紹介します) というメソッドで列の選択を行うことができますが、これらのメソッドはできることに制約があり使える場面が限られています。いろいろ試した結果、結局 `.loc[]` を使うのが一番だという結論に至りました。

基本構文は次の通りです。直接 `iris.loc[:, ...]` とせずに `pipe` メソッドを噛ませているところがポイントです。こうすることで、この後紹介する DataFrame オブジェクトのパラメータを参照した高度な列の絞り方を行う場合でもメソッドチェーンをつなぐことが可能になります。

```python
# iris %>% select(sepal_length, sepal_width)
iris.pipe(lambda df: df.loc[:, '''ここに列の選択条件を書く'''])
```

> [2018/10/10 追記]
> `.loc[]` に関数を指定できることを知りました。次のように書くことで `pipe` メソッドを噛ませなくてもメソッドチェーンをつなぐことができます。
> 
> ```python
> iris.loc[:, lambda df: '''ここに列の選択条件を書く''']
> ```

特定のパターンを含む列だけを取り出したい場合は、DataFrame オブジェクトの列名を表す `columns` パラメータ (Index オブジェクト) と、Index オブジェクトに対する文字列処理メソッド群へアクセスするための `str` エイリアスを使います (参考：[`str` エイリアスでアクセスできる文字列処理メソッドの一覧](http://pandas.pydata.org/pandas-docs/stable/api.html#string-handling))。

```python
# iris %>% select(startswidth("sepal"))
iris.pipe(lambda df: df.loc[:, df.columns.str.startswith("sepal")])

# iris %>% select(endswith("length"))
iris.pipe(lambda df: df.loc[:, df.columns.str.endswith("length")])

# iris %>% select(contains("^.*_.*$"))
iris.pipe(lambda df: df.loc[:, df.columns.str.contains("^.*_.*$")])
```

> [2018/10/10 追記]
> `pipe` メソッドを使わなくても、次のように書けます。
> 
> ```python
> iris.loc[:, lambda df: df.columns.str.startswith("sepal")]
> ```

特定のパターンを含まない列だけを取り出したい場合は、`~` で反転させます (boolean 型の NumPy 配列をビット演算子 `~` で反転させて上手く行く根拠を見つけられていないので、ダメな場合があるかもしれません……[`numpy.invert` のドキュメント](https://docs.scipy.org/doc/numpy/reference/generated/numpy.invert.html) を読むと Examples の一番下に boolean 型は boolean 型として反転させる旨が記載されているので、おそらく `numpy.__invert__` も同様の実装になっているのではないでしょうか)。

```python
# iris %>% select(-startswidth("sepal"))
iris.pipe(lambda df: df.loc[:, ~df.columns.str.startswith("sepal")])

# iris %>% select(-endswith("length"))
iris.pipe(lambda df: df.loc[:, ~df.columns.str.endswith("length")])

# iris %>% select(-contains("^.*_.*$"))
iris.pipe(lambda df: df.loc[:, ~df.columns.str.contains("^.*_.*$")])
```


#### `filter` や `drop` を使う方法

列を選択には `filter` メソッドを使うこともできます。選択する列を列挙できるなら、`filter` メソッドを使うほうが `.loc[]` + `pipe` よりも簡潔に表現できます。

```python
# iris %>% select(sepal_length, sepal_width)
iris.filter(["sepal_length", "sepal_width"])
```

`filter` メソッドは正規表現が使えるので、柔軟な列選択が可能です。

```python
# iris %>% select(starts_with("sepal"))
iris.filter(regex = "^sepal")
```

特定の列を除きたい場合は、`drop` メソッドを使うこともできます。除去する列を列挙できるなら、`drop` メソッドを使うほうが `.loc[]` + `pipe` よりも簡潔に表現できます。`drop` メソッドは行と列の両方に対応したメソッドなので、列を除きたい場合には引数 `column` に列名を列挙します。

```python
# iris %>% select(-petal_length, -petal_width)
iris.drop(columns = ["petal_length", "petal_width"])
```

### 行の選択 `dplyr::filter`

#### `[]` + `pipe` を使う方法

行を選択するには、`[]` を使います。

```python
# iris[iris$sepal_length > 5, ]
iris[iris.sepal_length > 5]
```

ただし、素直に `[]` を使うとメソッドチェーンをつなげなくなってしまう (条件部で自分自身を参照しているので、連続して行選択を行うには一度チェーンを切って代入しなければならない) ので、メソッドチェーンをつなげたい場合には `pipe` メソッドを噛ませます。

```python
# iris %>% filter(sepal_length > 5)
iris.pipe(lambda df: df[df.sepal_length > 5])
```

> [2018/10/10 追記]
> `.loc[]` と同様に、`[]` も 関数を指定することができます。
>
> ```python
> iris[lambda df: df.sepal_length > 5]
> ```

#### `query` メソッドを使う方法

`[]` の他に `query` メソッドを使う方法もありますが、イマイチ使い勝手が良くないです。

```python
iris.query("sepal_length > 5")
```

行を絞る条件を文字列で渡す都合上、いろいろ制約があります。例えばメソッドを呼び出そうとすると、デフォルトの評価エンジン (`numexpr`) ではエラーになります。評価エンジンに `python` を指定すると動くようになりますが、そこまでするなら `[]` + `pipe` でいいかなという感じです。

```python
# Error
iris.query("sepal_length.notnull()")
# OK
iris.query("sepal_length.notnull()", engine = "python")
```

### 新しい列の追加 `dplyr::mutate`

#### `assign` を使う方法

新しい列を追加するには、`assign` メソッドを使います。`assign` メソッドには DataFrame オブジェクトを受け取り Series オブジェクトを返す関数を渡します。

```python
# iris %>% mutate(sepal_area = sepal_length * sepal_width)
iris.assign(sepal_area = lambda df: df.sepal_length * df.sepal_width)
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sepal_length</th>
      <th>sepal_width</th>
      <th>petal_length</th>
      <th>petal_width</th>
      <th>species</th>
      <th>id</th>
      <th>sepal_area</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>5.1</td>
      <td>3.5</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>0</td>
      <td>17.85</td>
    </tr>
    <tr>
      <th>1</th>
      <td>4.9</td>
      <td>3.0</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>1</td>
      <td>14.70</td>
    </tr>
    <tr>
      <th>2</th>
      <td>4.7</td>
      <td>3.2</td>
      <td>1.3</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>2</td>
      <td>15.04</td>
    </tr>
    <tr>
      <th>3</th>
      <td>4.6</td>
      <td>3.1</td>
      <td>1.5</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>3</td>
      <td>14.26</td>
    </tr>
    <tr>
      <th>4</th>
      <td>5.0</td>
      <td>3.6</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>4</td>
      <td>18.00</td>
    </tr>
  </tbody>
</table>

#### `assign` で特定の値を書き換える方法

ある条件を満たす行の値を書き換えるには、`mask` メソッドを使います。`mask` メソッドは条件を満たす場合には指定した値を、満たさない場合には元の値を返します。

```python
# iris %>% mutate(sepal_length = if_else(sepal_length < 5, NA_real_, sepal_length))
iris.assign(sepal_length = lambda df: df.sepal_length.mask(df.sepal_length < 5, np.nan))
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sepal_length</th>
      <th>sepal_width</th>
      <th>petal_length</th>
      <th>petal_width</th>
      <th>species</th>
      <th>id</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>5.1</td>
      <td>3.5</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>NaN</td>
      <td>3.0</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>1</td>
    </tr>
    <tr>
      <th>2</th>
      <td>NaN</td>
      <td>3.2</td>
      <td>1.3</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>2</td>
    </tr>
    <tr>
      <th>3</th>
      <td>NaN</td>
      <td>3.1</td>
      <td>1.5</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>3</td>
    </tr>
    <tr>
      <th>4</th>
      <td>5.0</td>
      <td>3.6</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>4</td>
    </tr>
  </tbody>
</table>

同様に、ある条件を満たさない行の値を書き換えるには、`where` メソッドを使います。`where` メソッドは条件を満たす場合には元の値を、満たさない場合には指定した値を返します。

```python
# iris %>% mutate(sepal_length = if_else(sepal_length < 5, sepal_length, NA_real_))
iris.assign(sepal_length = lambda df: df.sepal_length.where(df.sepal_length < 5, np.nan))
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sepal_length</th>
      <th>sepal_width</th>
      <th>petal_length</th>
      <th>petal_width</th>
      <th>species</th>
      <th>id</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>NaN</td>
      <td>3.5</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>4.9</td>
      <td>3.0</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>1</td>
    </tr>
    <tr>
      <th>2</th>
      <td>4.7</td>
      <td>3.2</td>
      <td>1.3</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>2</td>
    </tr>
    <tr>
      <th>3</th>
      <td>4.6</td>
      <td>3.1</td>
      <td>1.5</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>3</td>
    </tr>
    <tr>
      <th>4</th>
      <td>NaN</td>
      <td>3.6</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>4</td>
    </tr>
  </tbody>
</table>

#### `assign` で既存の列の値に応じて新しい列の値を変える方法

既存の列の値に応じて新しい列の値を変えるには、デフォルト値を代入した新しい列をあらかじめ作っておき、その列に対して `mask` メソッドや `where` メソッドを使って既存の列の値に応じて作った列の値を書き換えます。`dplyr::if_else` (`base::ifelse` でもいいですが) に相当するメソッドがあればより簡潔に書けそうですが、私が探した限りでは見つけられませんでした。

```python
# iris %>% mutate(islong = if_else(sepal_length >= 5 & petal_length >= 1.3, "long", "short"))
iris. \
assign(islong = "short"). \
assign(islong = lambda df: df.islong.mask((df.sepal_length >= 5) & (df.petal_length >= 1.3), "long"))
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sepal_length</th>
      <th>sepal_width</th>
      <th>petal_length</th>
      <th>petal_width</th>
      <th>species</th>
      <th>id</th>
      <th>islong</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>5.1</td>
      <td>3.5</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>0</td>
      <td>long</td>
    </tr>
    <tr>
      <th>1</th>
      <td>4.9</td>
      <td>3.0</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>1</td>
      <td>short</td>
    </tr>
    <tr>
      <th>2</th>
      <td>4.7</td>
      <td>3.2</td>
      <td>1.3</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>2</td>
      <td>short</td>
    </tr>
    <tr>
      <th>3</th>
      <td>4.6</td>
      <td>3.1</td>
      <td>1.5</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>3</td>
      <td>short</td>
    </tr>
    <tr>
      <th>4</th>
      <td>5.0</td>
      <td>3.6</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>setosa</td>
      <td>4</td>
      <td>long</td>
    </tr>
  </tbody>
</table>

### グループ化 `dplyr::group_by` と集約 `dplyr::summarise`

`dplyr::summarise` に該当するメソッドはありません。一応 `agg` がありますが、動作としては `dplyr::summarise_all` や `dplyr::summarise_at` に近いです。

#### `agg` で `dplyr::summarise_all` 相当の操作をやる方法

`agg` の引数に集約関数のリストを指定すると、DataFrame オブジェクトの全列に対して指定した全関数を適用した集計結果が得られます (`dplyr::summarise_all` 相当)。

```python
# iris %>% group_by(species) %>% summarise_all(funs(min, max))
iris. \
groupby("species"). \
agg([min, max])
```

<table border="1" class="dataframe">
  <thead>
    <tr>
      <th></th>
      <th colspan="2" halign="left">sepal_length</th>
      <th colspan="2" halign="left">sepal_width</th>
      <th colspan="2" halign="left">petal_length</th>
      <th colspan="2" halign="left">petal_width</th>
      <th colspan="2" halign="left">id</th>
    </tr>
    <tr>
      <th></th>
      <th>min</th>
      <th>max</th>
      <th>min</th>
      <th>max</th>
      <th>min</th>
      <th>max</th>
      <th>min</th>
      <th>max</th>
      <th>min</th>
      <th>max</th>
    </tr>
    <tr>
      <th>species</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>setosa</th>
      <td>4.3</td>
      <td>5.8</td>
      <td>2.3</td>
      <td>4.4</td>
      <td>1.0</td>
      <td>1.9</td>
      <td>0.1</td>
      <td>0.6</td>
      <td>0</td>
      <td>49</td>
    </tr>
    <tr>
      <th>versicolor</th>
      <td>4.9</td>
      <td>7.0</td>
      <td>2.0</td>
      <td>3.4</td>
      <td>3.0</td>
      <td>5.1</td>
      <td>1.0</td>
      <td>1.8</td>
      <td>50</td>
      <td>99</td>
    </tr>
    <tr>
      <th>virginica</th>
      <td>4.9</td>
      <td>7.9</td>
      <td>2.2</td>
      <td>3.8</td>
      <td>4.5</td>
      <td>6.9</td>
      <td>1.4</td>
      <td>2.5</td>
      <td>100</td>
      <td>149</td>
    </tr>
  </tbody>
</table>

#### `agg` で `dplyr::summarise_at` 相当の操作をやる方法

`agg` の引数に列名と集約関数のリストをペアにした辞書を渡すと、指定した列に対して指定した関数を適用した集計結果が得られます (`dplyr::summarise_at` 相当)。

```python
# iris %>% group_by(Species) %>% summarise_at(vars(Sepal.Length, Sepal.Width), funs(min, max))
iris. \
groupby("species"). \
agg({ "sepal_length": [min, max], "sepal_width": [min, max] })
```

<table border="1" class="dataframe">
  <thead>
    <tr>
      <th></th>
      <th colspan="2" halign="left">sepal_length</th>
      <th colspan="2" halign="left">sepal_width</th>
    </tr>
    <tr>
      <th></th>
      <th>min</th>
      <th>max</th>
      <th>min</th>
      <th>max</th>
    </tr>
    <tr>
      <th>species</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>setosa</th>
      <td>4.3</td>
      <td>5.8</td>
      <td>2.3</td>
      <td>4.4</td>
    </tr>
    <tr>
      <th>versicolor</th>
      <td>4.9</td>
      <td>7.0</td>
      <td>2.0</td>
      <td>3.4</td>
    </tr>
    <tr>
      <th>virginica</th>
      <td>4.9</td>
      <td>7.9</td>
      <td>2.2</td>
      <td>3.8</td>
    </tr>
  </tbody>
</table>

#### `agg` で `dplyr::summarise` 相当の操作をやる方法

`agg` は既存の列それぞれに対して集約関数を適用するメソッドなので、`dplyr::summarise` のように複数の列を組み合わせて集約したり、その結果を新しい列に格納したりすることができません。そういった処理を書きたい場合には、`assign` で事前に列を作り、その列に集約直前の状態まで計算した結果を格納し、その列に対して `agg` で集約関数を適用します。

```python
# iris %>%
# group_by(Species) %>%
# summarise(
#     n = n(),
#     n_sepal_over5 = sum(Sepal.Length >= 5),
#     n_sepal_over6 = sum(Sepal.Length >= 6),
#     n_sepal_over7 = sum(Sepal.Length >= 7)
# )
iris. \
assign(
    n = 1,
    n_sepal_over5 = lambda df: df.sepal_length.apply(lambda x: 1 if x >= 5 else 0),
    n_sepal_over6 = lambda df: df.sepal_length.apply(lambda x: 1 if x >= 6 else 0),
    n_sepal_over7 = lambda df: df.sepal_length.apply(lambda x: 1 if x >= 7 else 0)
). \
filter(regex = "species|^n"). \
groupby("species"). \
agg(sum)
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>n</th>
      <th>n_sepal_over5</th>
      <th>n_sepal_over6</th>
      <th>n_sepal_over7</th>
    </tr>
    <tr>
      <th>species</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>setosa</th>
      <td>50</td>
      <td>30</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>versicolor</th>
      <td>50</td>
      <td>49</td>
      <td>24</td>
      <td>1</td>
    </tr>
    <tr>
      <th>virginica</th>
      <td>50</td>
      <td>49</td>
      <td>43</td>
      <td>12</td>
    </tr>
  </tbody>
</table>

#### [参考] MultiIndex をフラットにする方法

`agg` で集約するとグループ化した列が Index に、列名が 列名×集約関数名の MultiIndex になってしまい、dplyr 勢としては気持ち悪いですね。次のようにすることで tibble 風に変形できます (参考：[How to flatten a hierarchical index in columns - Stack Overflow](https://stackoverflow.com/questions/14507794/python-pandas-how-to-flatten-a-hierarchical-index-in-columns))。

* `pipe(...)` で MultiIndex をフラットにすることができます。
    * `df.columns.tolist()` で `(第一階層の列名, 第二階層の列名)` というタプルのリストを得ます。
    * `[e[0] + "_" + e[1] ...]` で各階層の列名を "_" 区切りで結合し、フラットな列名を生成します。
    * `pd.DataFrame(df.values, index = df.index, column = ...)` で列名を付け替えます。
* `reset_index` で Index を列に戻すことができます。

```python
iris. \
groupby("species"). \
agg([min, max]). \
pipe(lambda df: pd.DataFrame(
    df.values,
    index = df.index,
    columns = [e[0] + "_" + e[1] for e in df.columns.tolist()])
). \
reset_index()
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>species</th>
      <th>sepal_<br>length_<br>min</th>
      <th>sepal_<br>length_<br>max</th>
      <th>sepal_<br>width_<br>min</th>
      <th>sepal_<br>width_<br>max</th>
      <th>petal_<br>length_<br>min</th>
      <th>petal_<br>length_<br>max</th>
      <th>petal_<br>width_<br>min</th>
      <th>petal_<br>width_<br>max</th>
      <th>id_<br>min</th>
      <th>id_<br>max</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>setosa</td>
      <td>4.3</td>
      <td>5.8</td>
      <td>2.3</td>
      <td>4.4</td>
      <td>1.0</td>
      <td>1.9</td>
      <td>0.1</td>
      <td>0.6</td>
      <td>0.0</td>
      <td>49.0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>versicolor</td>
      <td>4.9</td>
      <td>7.0</td>
      <td>2.0</td>
      <td>3.4</td>
      <td>3.0</td>
      <td>5.1</td>
      <td>1.0</td>
      <td>1.8</td>
      <td>50.0</td>
      <td>99.0</td>
    </tr>
    <tr>
      <th>2</th>
      <td>virginica</td>
      <td>4.9</td>
      <td>7.9</td>
      <td>2.2</td>
      <td>3.8</td>
      <td>4.5</td>
      <td>6.9</td>
      <td>1.4</td>
      <td>2.5</td>
      <td>100.0</td>
      <td>149.0</td>
    </tr>
  </tbody>
</table>

Pandas を使うからには Pandas の文化に従うべきだとは思いますが……

### データフレームの結合 `dplyr::*_join`

(調査中です)

### 縦持ち横持ち変換 `tidyr::gather` `tidyr::spread`

Pandas で `tidyr::gather` や `tidyr::spread` をやるためには、冒頭の前置きで触れた Index とまじめに向き合う必要があります。SQL-style を貫き通すことはできません……

#### `stack` で `tidyr::gather` 相当の操作をやる方法

縦持ちにするには、`stack` メソッドを使います。縦持ちしたくない列はすべて `set_index` メソッドであらかじめ Index に移しておく必要があります。

```python
# iris %>% gather(key = NA, value = NA, -id, -species)
iris. \
set_index(["id", "species"]). \
stack()
```

```:実行結果
id   species                
0    setosa     sepal_length    5.1
                sepal_width     3.5
                petal_length    1.4
                petal_width     0.2
1    setosa     sepal_length    4.9
                sepal_width     3.0
                petal_length    1.4
                petal_width     0.2
...
```

`stack` メソッドで縦持ちにすると MultiIndex の Series オブジェクトが返ってきます。普段 dplyr を使っている身としては SQL-style の DataFrame オブジェクトに直したい気持ちでいっぱいですが、MultiIndex を積極的に使っていけば後続の処理も Series オブジェクトのままで特に困りません。例えば、縦持ちした後に `species` 別・変数別の最小値と最大値を集計するには次のようにします。

```python
# iris %>%
# gather(key = key, value = value, -Species) %>%  # 本当は key = NA, value = NA 相当
# group_by(Species, key) %>%
# summarise_all(funs(min, max))
iris. \
set_index(["id", "species"]). \
stack(). \
groupby(level = [1, 2]). \
agg([min, max])
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th></th>
      <th>min</th>
      <th>max</th>
    </tr>
    <tr>
      <th>species</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="4" valign="top">setosa</th>
      <th>sepal_length</th>
      <td>4.3</td>
      <td>5.8</td>
    </tr>
    <tr>
      <th>sepal_width</th>
      <td>2.3</td>
      <td>4.4</td>
    </tr>
    <tr>
      <th>petal_length</th>
      <td>1.0</td>
      <td>1.9</td>
    </tr>
    <tr>
      <th>petal_width</th>
      <td>0.1</td>
      <td>0.6</td>
    </tr>
    <tr>
      <th rowspan="4" valign="top">versicolor</th>
      <th>sepal_length</th>
      <td>4.9</td>
      <td>7.0</td>
    </tr>
    <tr>
      <th>sepal_width</th>
      <td>2.0</td>
      <td>3.4</td>
    </tr>
    <tr>
      <th>petal_length</th>
      <td>3.0</td>
      <td>5.1</td>
    </tr>
    <tr>
      <th>petal_width</th>
      <td>1.0</td>
      <td>1.8</td>
    </tr>
    <tr>
      <th rowspan="4" valign="top">virginica</th>
      <th>sepal_length</th>
      <td>4.9</td>
      <td>7.9</td>
    </tr>
    <tr>
      <th>sepal_width</th>
      <td>2.2</td>
      <td>3.8</td>
    </tr>
    <tr>
      <th>petal_length</th>
      <td>4.5</td>
      <td>6.9</td>
    </tr>
    <tr>
      <th>petal_width</th>
      <td>1.4</td>
      <td>2.5</td>
    </tr>
  </tbody>
</table>

#### `melt` で `tidyr::gather` 相当の操作をやる方法

一応、`melt` メソッドを使えば `tidyr::gather` と同等の操作を実現できます。メソッドの使い勝手も得られる結果もほぼ `tidyr::gather` と同じです。ただし、この後で説明しますが `tidyr::spread` に対応するメソッドが Pandas には存在しないため、縦持ち横持ちの往復を SQL-style で貫き通すことができません。貫けないのであれば stack で縦持ち変換を行うほうがよいと判断しました。

```python
# iris %>% gather(key = key, value = value, -id, -species)
iris.melt(var_name = "key", value_name = "value", id_vars = ["id", "species"])
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>id</th>
      <th>species</th>
      <th>key</th>
      <th>value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>setosa</td>
      <td>sepal_length</td>
      <td>5.1</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>setosa</td>
      <td>sepal_length</td>
      <td>4.9</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>setosa</td>
      <td>sepal_length</td>
      <td>4.7</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>setosa</td>
      <td>sepal_length</td>
      <td>4.6</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>setosa</td>
      <td>sepal_length</td>
      <td>5.0</td>
    </tr>
  </tbody>
</table>

#### `unstack` で `tidyr::spread` に相当する操作をやる方法

横持ちにするには、`unstack` メソッドを使います。`stack` メソッドと同じで、横持ちしたくない列はすべて `set_index` メソッドであらかじめ Index に移しておく必要があります。

```python
# iris_stacked <-iris %>% gather(key = key, value = value, -id, -species)
iris_stacked = iris.melt(var_name = "key", value_name = "value", id_vars = ["id", "species"])

# iris_stacked %>% spread(key = key, value = value, -id, -species)
iris_stacked. \
set_index(["id", "species", "key"]). \
unstack()
```

<table border="1" class="dataframe">
  <thead>
    <tr>
      <th></th>
      <th></th>
      <th colspan="4" halign="left">value</th>
    </tr>
    <tr>
      <th></th>
      <th>key</th>
      <th>petal_length</th>
      <th>petal_width</th>
      <th>sepal_length</th>
      <th>sepal_width</th>
    </tr>
    <tr>
      <th>id</th>
      <th>species</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <th>setosa</th>
      <td>1.4</td>
      <td>0.2</td>
      <td>5.1</td>
      <td>3.5</td>
    </tr>
    <tr>
      <th>1</th>
      <th>setosa</th>
      <td>1.4</td>
      <td>0.2</td>
      <td>4.9</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2</th>
      <th>setosa</th>
      <td>1.3</td>
      <td>0.2</td>
      <td>4.7</td>
      <td>3.2</td>
    </tr>
    <tr>
      <th>3</th>
      <th>setosa</th>
      <td>1.5</td>
      <td>0.2</td>
      <td>4.6</td>
      <td>3.1</td>
    </tr>
    <tr>
      <th>4</th>
      <th>setosa</th>
      <td>1.4</td>
      <td>0.2</td>
      <td>5.0</td>
      <td>3.6</td>
    </tr>
  </tbody>
</table>

DataFrame オブジェクトに対して `unstack` メソッドを呼び出すと複数の列を横持ちにする想定の挙動になるため、得られる結果の列名が MultiIndex になります。横持ちしたい列が一つしかない場合は、`set_index` メソッドで横持ち不要な列を Index に移した後に `[]` で横持ちしたい列だけ取り出して (Series オブジェクトにして) `unstack` メソッドを呼ぶことで得られる結果の列名がフラットになります。合わせて `reset_index` メソッドで Index を列に移すと、SQL-style にできます。

```python
iris_stacked. \
set_index(["id", "species", "key"])["value"]. \
unstack(). \
reset_index()
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>key</th>
      <th>id</th>
      <th>species</th>
      <th>petal_length</th>
      <th>petal_width</th>
      <th>sepal_length</th>
      <th>sepal_width</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>setosa</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>5.1</td>
      <td>3.5</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>setosa</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>4.9</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>setosa</td>
      <td>1.3</td>
      <td>0.2</td>
      <td>4.7</td>
      <td>3.2</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>setosa</td>
      <td>1.5</td>
      <td>0.2</td>
      <td>4.6</td>
      <td>3.1</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>setosa</td>
      <td>1.4</td>
      <td>0.2</td>
      <td>5.0</td>
      <td>3.6</td>
    </tr>
  </tbody>
</table>

#### `pivot` で `tidyr::spread` 相当の操作をやる方法

一応、`pivot` メソッドを使えば `tidyr::spread` と同等の操作を実現できます。メソッドの使い勝手もほぼ `tidyr::spread` と同じです。ただし大きな違いとして、横持ちしない列は一つしか選べない (`pivot(index = ...)` の `...` には列を一つしか指定できない) という制約があります。そのため、複合キーを持つ場合や一部の列のみ横持ちさせたい場合に `pivot` メソッドは使えません。

```python
# iris_stacked %>% spread(key = key, value = value, -id)
iris_stacked.pivot(index = "id", columns = "key", values = "value")
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>key</th>
      <th>petal_length</th>
      <th>petal_width</th>
      <th>sepal_length</th>
      <th>sepal_width</th>
    </tr>
    <tr>
      <th>id</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1.4</td>
      <td>0.2</td>
      <td>5.1</td>
      <td>3.5</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1.4</td>
      <td>0.2</td>
      <td>4.9</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>2</th>
      <td>1.3</td>
      <td>0.2</td>
      <td>4.7</td>
      <td>3.2</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1.5</td>
      <td>0.2</td>
      <td>4.6</td>
      <td>3.1</td>
    </tr>
    <tr>
      <th>4</th>
      <td>1.4</td>
      <td>0.2</td>
      <td>5.0</td>
      <td>3.6</td>
    </tr>
  </tbody>
</table>

## 参考

Pandas と dplyr の対比については、以下のページを参考にしました。

* この記事は以下のページから着想を得て作りました。<br>dplyr と Pandas の考え方の違いにも触れており、わかりやすいです。
    * [Comparison of pandas and dplyr - nbviewer](http://nbviewer.jupyter.org/gist/TomAugspurger/6e052140eaa5fdb6e8c0)
* より網羅的な対比を知りたい方は、以下のページが参考になると思います。
    * [Rユーザー向け Pythonデータ処理入門 - Qiita](https://qiita.com/takaiyuk/items/4cb1708a3f886b3d2043)
    * [RユーザーのためのPython対応表 [tidyr, ggplot2] - Qiita](https://qiita.com/takaiyuk/items/0bf9b1db8b4707e3dae2)

各メソッドの細かい使い方については、以下のページを参考にしました。

* パイプでやりきる方法
    * [Python/pandasのデータ処理で再帰代入撲滅委員会 - Qiita](https://qiita.com/piroyoung/items/dd209801ca60a0b00c11)
* 行や列の選択方法
    * [Indexing and Selecting Data - Pandas documentation](https://pandas.pydata.org/pandas-docs/stable/indexing.html)
* `str` 経由で文字列処理する方法
    * [Working with Text Data - Pandas documentation](https://pandas.pydata.org/pandas-docs/stable/text.html)
* DataFrame オブジェクトの結合
    * [Merge, join, and concatenate - Pandas documentation](https://pandas.pydata.org/pandas-docs/stable/merging.html)
* 縦持ち横持ち変換
    * [Reshaping and Pivot Tables - Pandas documentation](https://pandas.pydata.org/pandas-docs/stable/reshaping.html)