PandasのDataFrameで行の中に格納されているリストを縦に展開する

# バージョン情報

* Python 3.6.3
* NumPy 1.13.3
* Pandas 0.23.4


# import

```python
import numpy as np
import pandas as pd
```


# やりたいこと

```python
df = pd.DataFrame(
    columns=['type', 'items'],
    data=[
        ['type-001', ['apple', 'orange', 'melon']],
        ['type-002', ['apple', 'google', 'microsoft', 'ibm']],
        ['type-003', ['foo', 'bar']],
    ]
)
```
![1.png](https://qiita-image-store.s3.amazonaws.com/0/70225/0516d73b-7437-4598-fcb5-a9f8513d9e0e.png)


こんな感じで中にリストを持っているDataFrameを,

```python
pd.DataFrame(
    columns=['type', 'items'],
    data=[
        ['type-001', 'apple'],
        ['type-001', 'orange'],
        ['type-001', 'melon'],
        ['type-002', 'apple', ],
        ['type-002', 'google'],
        ['type-002', 'microsoft'],
        ['type-002',  'ibm'],
        ['type-003', 'foo'],
        ['type-003', 'bar'],
    ]
)
```
![2.png](https://qiita-image-store.s3.amazonaws.com/0/70225/532a7555-e065-10b3-c635-59bddf9fecdd.png)

これにしたい.


# やりかた

結構よくあるシチュエーションのような気がするが, 超お手軽にやる方法は無さそうな感じだった. この記事も私の自己流になります.

まず次のような関数を作っておく. これは `DataFrame` の `apply` 関数に渡して1行ずつ処理させることを意識しています.

```python
def flatten(row):
    df1 = pd.Series(row['type']).to_frame(name='type')
    df2 = pd.Series(row['items']).to_frame(name='item')
    df1['key'] = df2['key'] = 0
    return df1.merge(df2, how='outer').drop(columns='key')
```

一瞬だけ `key` という名前のカラムを作っているのはcross joinを行いたかったためです. 参考: [cartesian product in pandas - Stack Overflow](https://stackoverflow.com/questions/13269890/cartesian-product-in-pandas)

これを実際に `apply` するコードは下記のようになる:

```python
pd.concat(df.apply(flatten, axis=1).values)
```
![2.png](https://qiita-image-store.s3.amazonaws.com/0/70225/532a7555-e065-10b3-c635-59bddf9fecdd.png)

`df.apply(flatten, axis=1)` の時点では複数の `DataFrame` が入った `Series` が返っているので, それを `pd.concat` で全部くっつけている. 結構力技だと思う.

ちなみに, わざわざ cross join を使っているので `type` の値にリストが入っているようなシチュエーションでもそのまま動く.

```python
df = pd.DataFrame(
    columns=['type', 'items'],
    data=[
        [['type-001', 'type-A'], ['apple', 'orange', 'melon']],
        [['type-002'], ['apple', 'google', 'microsoft', 'ibm']],
        [['type-003'], ['foo', 'bar']],
    ]
)
```

```python
pd.concat(df.apply(flatten, axis=1).values)
```
![3.png](https://qiita-image-store.s3.amazonaws.com/0/70225/dc4c0bfe-2aa0-eeed-42ad-e19e02ef7535.png)


# もうちょっと複雑なとき

現実的なケースを考えると `type` だけでなく `type` と同じ行にいる他の列も一緒に連れてくる必要があるケースがほとんどだろう.

次のようなデータを考えよう:

```python
df = pd.DataFrame(
    columns=['type', 'items', 'bra', 'yo'],
    data=[
        [['type-001', 'type-A'], ['apple', 'orange', 'melon'], 'brabrabrabra', 'Yo'],
        [['type-002'], ['apple', 'google', 'microsoft', 'ibm'], 'brabrabrabrabrabrabrabra', 'YoYo'],
        [['type-003'], ['foo', 'bar'], 'brabrabrabra', 'YoYoYoYo'],
    ]
)
```
![5.png](https://qiita-image-store.s3.amazonaws.com/0/70225/bc732a8f-7178-53b1-92df-6e1e34d004f4.png)

もしも `type` の中身がリストでなければ, 前のコードを

```python
    df1 = pd.DataFrame({'type':row['type'], 'bra':row['bra'], 'yo':row['yo']})
```

と書き換えるだけで対応できるが, 今回は `type` にリストが来るという制約を自ら課してしまったので, もう少し面倒な書き方をする必要がある. `apply` に渡すための関数はこんな感じになる:

```python
def flatten(row):
    df1 = pd.Series(row['type']).to_frame(name='type')
    df2 = pd.Series(row['items']).to_frame(name='item')
    df3 = pd.DataFrame([{'bra': row['bra'], 'yo': row['yo']}])
    df1['key'] = df2['key'] = df3['key'] = 0
    return df1.merge(df3, how='outer').merge(df2, how='outer').drop(columns='key')
```

展開に関係しないカラムはまとめて `df3` に入れておき, 2回 cross join を走らせている. `apply` する側のコードは変わらず

```
pd.concat(df.apply(flatten, axis=1).values).loc[:, ['type', 'item', 'bra', 'yo']]
```
![4.png](https://qiita-image-store.s3.amazonaws.com/0/70225/2ea27be9-017c-7e34-c4c5-5fe8c5ec3fa0.png)

ただし, カラムの順番が気持ち悪くなるので `.loc` で直している.