オブジェクトを含む配列を1次元化(flatten)する

# 概要

こんな感じのデータがあるとします。

```python
data = [
    {
        'id': 1,
        'list': [
            {
                'name': 'test1',
                'sub_list': [
                    {'type': 'a'},
                    {'type': 'b'},
                    {'type': 'c'},
                ]
            },
            {
                'name': 'test2',
                'sub_list': [
                    {'type': 'd'},
                    {'type': 'e'},
                    {'type': 'f'},
                ]
            },
            {
                'name': 'test3',
                'sub_list': [
                    {'type': 'g'},
                    {'type': 'h'},
                    {'type': 'i'},
                ]
            },
        ],
    },
    {
        'id': 2,
        'list': [
            {
                'name': 'test4',
                'sub_list': [
                    {'type': 'j'},
                    {'type': 'k'},
                    {'type': 'l'},
                ]
            },
            {
                'name': 'test5',
                'sub_list': [
                    {'type': 'm'},
                    {'type': 'n'},
                    {'type': 'o'},
                ]
            },
            {
                'name': 'test6',
                'sub_list': [
                    {'type': 'p'},
                    {'type': 'q'},
                    {'type': 'r'},
                ]
            },
        ],
    },
]
```

これを一次元の配列にflattenしたい時、ありませんか？（僕はありました）

どうやるかを調べてみました。

# pandas.explode

色々調べてみると、pandasにexplodeなんて関数があるではないですか！

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.explode.html

https://stackoverflow.com/questions/24027723/flatten-a-series-in-pandas-i-e-a-series-whose-elements-are-lists

サンプルコードを見てもらうと分かりますが、flattenしたい時にピッタリの関数です。

では、上記のデータをflattenしてみましょう。

```python
df = ps.DataFrame(data) # データ配列を読み込み

df = df.explode('list') # list列をexplode!!!!
df['name'] = df['list'].apply(lambda x: x['name']) # Seriesに変換
df['sub_list'] = df['list'].apply(lambda x: x['sub_list']) # もう一個のリストもSeriesに変換
df = df.explode('sub_list') # こっちもexplode!!!!
df['type'] = df['sub_list'].apply(lambda x: x['type']) # Seriesに変換

df = df.drop(columns=['list', 'sub_list']) # 不要な列を削除
```

結果、このdfはどうなるかと言うと…

```
    id   name type
0    1  test1    a
1    1  test1    b
2    1  test1    c
3    1  test2    d
4    1  test2    e
5    1  test2    f
6    1  test3    g
7    1  test3    h
8    1  test3    i
9    2  test4    j
10   2  test4    k
11   2  test4    l
12   2  test5    m
13   2  test5    n
14   2  test5    o
15   2  test6    p
16   2  test6    q
17   2  test6    r
```

きれいにflattenされていますね。

# pythonのlist.append

今度はforで回して空のlistにappendしてみます。

```python
df_list = []
for d in data:
    for l in d['list']:
        for s in l['sub_list']:
            df_list.append({
                'id': d['id'],
                'name': l['name'],
                'type': s['type'],
            })

df = ps.DataFrame(df_list)
```

結果は上記のexplodeを使用したものと同じになります。

# pandas.json_normalize

なななんと、json_normalizeという「まさにこれ」な関数がありました。(コメントくださった方ありがとうございます！)

https://pandas.pydata.org/docs/reference/api/pandas.json_normalize.html

こちらは以下のように実装します。

```python
df = ps.json_normalize(data, ['list', ['sub_list']], ['id', ['list', 'name']])
df = df.rename(columns={'list.name': 'name'})
```

結果を合わせるためにrenameしていますが、そこが無ければ**実質ワンライナー**ですからね。

これはかなり嬉しいです。

# 内包表記による変換

先程はlist.appendを使用していましたが、内包表記によって一発で全部変換した場合は以下のようになります。（コメントくださった方ありがとうございます！！）

```python
df_list = [
    {
        'id': d['id'],
        'name': l['name'],
        'type': s['type'],
    }
    for d in data
    for l in d['list']
    for s in l['sub_list']
]
df = ps.DataFrame(df_list)
```

これも結果は他の方法と同じになります。

# それぞれの性能について

４つの処理時間を比較すると以下のとおりです。

```
list.append    : 0.0006937980651855469s
explode        : 0.005505084991455078s
Json Normalize : 0.001180887222290039s
内包表記        : 0.00029468536376953125s
```

比較すると明らかですが、**圧倒的に内包表記が早いです。**

ちなみに、これ以外にも「DataFrameのappendを使う」という方法もありますが、遅いのとlist.appendとほぼ書き方が同じなためここには書きません。

https://qiita.com/XPT60/items/c247b2c666e9d1126ffa

データが大量だった場合はまた結果変わるかな？
でも検証が面倒なので今回はこのくらいにしておきます。

# 今回使用したコード

```python
import pandas as ps
import numpy as np
import itertools
import time


data = [
    {
        'id': 1,
        'list': [
            {
                'name': 'test1',
                'sub_list': [
                    {'type': 'a'},
                    {'type': 'b'},
                    {'type': 'c'},
                ]
            },
            {
                'name': 'test2',
                'sub_list': [
                    {'type': 'd'},
                    {'type': 'e'},
                    {'type': 'f'},
                ]
            },
            {
                'name': 'test3',
                'sub_list': [
                    {'type': 'g'},
                    {'type': 'h'},
                    {'type': 'i'},
                ]
            },
        ],
    },
    {
        'id': 2,
        'list': [
            {
                'name': 'test4',
                'sub_list': [
                    {'type': 'j'},
                    {'type': 'k'},
                    {'type': 'l'},
                ]
            },
            {
                'name': 'test5',
                'sub_list': [
                    {'type': 'm'},
                    {'type': 'n'},
                    {'type': 'o'},
                ]
            },
            {
                'name': 'test6',
                'sub_list': [
                    {'type': 'p'},
                    {'type': 'q'},
                    {'type': 'r'},
                ]
            },
        ],
    },
]

start = time.time()
df_list = []
for d in data:
    for l in d['list']:
        for s in l['sub_list']:
            df_list.append({
                'id': d['id'],
                'name': l['name'],
                'type': s['type'],
            })

df = ps.DataFrame(df_list)
print(f'list.append    : {time.time() - start}s')

start = time.time()
df = ps.DataFrame(data)

df = df.explode('list')
df['name'] = df['list'].apply(lambda x: x['name'])
df['sub_list'] = df['list'].apply(lambda x: x['sub_list'])
df = df.explode('sub_list')
df['type'] = df['sub_list'].apply(lambda x: x['type'])

df = df.drop(columns=['list', 'sub_list'])
print(f'explode        : {time.time() - start}s')

start = time.time()
df = ps.json_normalize(data, ['list', ['sub_list']], ['id', ['list', 'name']])
df = df.rename(columns={'list.name': 'name'})
print(f'Json Normalize : {time.time() - start}s')

start = time.time()
df_list = [
    {
        'id': d['id'],
        'name': l['name'],
        'type': s['type'],
    }
    for d in data
    for l in d['list']
    for s in l['sub_list']
]
df = ps.DataFrame(df_list)
print(f'内包表記        : {time.time() - start}s')
```

ここで実行してみてね

https://paiza.io/projects/WH5Xx7jfilczjSP_IfxX2Q

# 変更履歴

- 2022/3/1 コメントを受けて一部のコードと記載を変更しました。