0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-14

概要

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

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 コメントを受けて一部のコードと記載を変更しました。
0
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?