概要
こんな感じのデータがあるとします。
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なんて関数があるではないですか!
サンプルコードを見てもらうと分かりますが、flattenしたい時にピッタリの関数です。
では、上記のデータをflattenしてみましょう。
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してみます。
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という「まさにこれ」な関数がありました。(コメントくださった方ありがとうございます!)
こちらは以下のように実装します。
df = ps.json_normalize(data, ['list', ['sub_list']], ['id', ['list', 'name']])
df = df.rename(columns={'list.name': 'name'})
結果を合わせるためにrenameしていますが、そこが無ければ実質ワンライナーですからね。
これはかなり嬉しいです。
内包表記による変換
先程はlist.appendを使用していましたが、内包表記によって一発で全部変換した場合は以下のようになります。(コメントくださった方ありがとうございます!!)
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)
これも結果は他の方法と同じになります。
それぞれの性能について
4つの処理時間を比較すると以下のとおりです。
list.append : 0.0006937980651855469s
explode : 0.005505084991455078s
Json Normalize : 0.001180887222290039s
内包表記 : 0.00029468536376953125s
比較すると明らかですが、圧倒的に内包表記が早いです。
ちなみに、これ以外にも「DataFrameのappendを使う」という方法もありますが、遅いのとlist.appendとほぼ書き方が同じなためここには書きません。
データが大量だった場合はまた結果変わるかな?
でも検証が面倒なので今回はこのくらいにしておきます。
今回使用したコード
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')
ここで実行してみてね
変更履歴
- 2022/3/1 コメントを受けて一部のコードと記載を変更しました。