42
31

More than 1 year has passed since last update.

Pandasでネストした多対多データのjsonファイルを扱う

Last updated at Posted at 2018-12-13

ネストした多対多データのjsonファイルを扱う中で、行の対象を変えることがあります。
Pandasを用いてそれを行う方法を紹介します。

多対多のデータの例として、ファッションアイテムとコーディネートのデータを取り上げます。
WEARのようなコーディネート投稿サイトでは、1つのコーディネートに対し複数のアイテムが使われ、逆に、1つのアイテムが複数のコーデに使われることがあります。
なので、コーディネートとアイテムは多対多の関係にあります。

このようなデータにおいて、行の対象をコーディネートからアイテム、アイテムからコーディネートにする方法を紹介します。

具体的には、以下のような2つの形のDataFrameを行ったり来たりする変形を行います。

以下、行がコーディネートのDataFrame

スクリーンショット 2018-12-10 14.34.19.png

以下、行がアイテムのDataFrame

スクリーンショット 2018-12-10 15.13.05.png

先にコードだけ載せます。

コーデ→アイテム

import pandas as pd
from pandas import json_normalize

df_items = json_normalize(df.to_dict("records"),
               "items", "set_id")
               
df_items.sort_values("item_id")

アイテム→コーデ

df_items.groupby("set_id")\
    [df_items.columns.difference(df.columns)]\
    .apply(lambda df_group: df_group.to_dict("records")) \
    .rename("items")\
    .reset_index()

データの確認

jupyter notebook で実行していきます。
sample.json にデータがあるとし、読み込みます。
初めは、行がコーディネートの形だとします。

import pandas as pd

json_file = "./sample.json"
df = pd.read_json(json_file)

df
スクリーンショット 2018-12-10 14.34.19.png

items カラムの各行には、そのコーデで使っているアイテムのリストが入っています。
アイテムは辞書で表現されているので、辞書のリストが各行に入っています。

具体的に、 items カラムの1行目を見てみます。
このコーデは5つのアイテムを使っていることがわかります。

df["items"].iloc[1]
\[{'index': 1,
  'name': '黒のテーラードジャケット',
  'price': 2735.0,
  'likes': 1864,
  'image': 'http://example.com/06.jpg',
  'categoryid': 3},
 {'index': 2,
  'name': '赤のセーター',
  'price': 8.69,
  'likes': 416,
  'image': 'http://example.com/07.jpg',
  'categoryid': 62},
 {'index': 3,
  'name': '白シャツ',
  'price': 1794.0,
  'likes': 156,
  'image': 'http://example.com/03.jpg',
  'categoryid': 35},
 {'index': 4,
  'name': '黒スキニー',
  'price': -1,
  'likes': 21,
  'image': 'http://example.com/08.jpg',
  'categoryid': 4240},
 {'index': 5,
  'name': '○ld sk○○l',
  'price': 28.0,
  'likes': 417,
  'image': 'http://example.com/08.jpg',
  'categoryid': 3336}\]

行をコーディネートからアイテムにする

上記のような 辞書のリスト のカラムに対し、リストの各要素であるアイテム(辞書)を行とするDataFrameを作るには pandas.io.json.json_normalize を用います。

from pandas.io.json import json_normalize

df_items = json_normalize(df.to_dict("records"),
               "items", "set_id")
               
df_items.sort_values("item_id")

上記のコードを実行すると、以下のように行がアイテムのDataFrameを得られます。

スクリーンショット 2018-12-10 15.13.05.png

上記のコードの詳細を追っていきます。

.to_dict("records")

json_normalize(df.to_dict("records"),
               "items", "set_id")

df.to_dict("records") はDataFrameを辞書(のリスト)にするのですが、方法がいくつかあり、orient という引数で指定します。
orient="records" では以下のように、各行を辞書としたリストが作成されます。各辞書のキーは「カラム名」、バリューは「その行、列での値」になります。

[
    {
        "items": [
            {
                "item_id": 10,
                "name": "モッズコート",
                "price": 135.0,
                "likes": 7,
                "image": "http://example.com/10.jpg",
                "categoryid": 4
            },
            {
                "item_id": 11,
                "name": "黒のタートルネック",
                "price": 845.0,
                "likes": 1996,
                "image": "http://example.com/11.jpg",
                "categoryid": 46
            },
            {
                "item_id": 8,
                "name": "黒スキニー",
                "price": -1,
                "likes": 21,
                "image": "http://example.com/08.jpg",
                "categoryid": 4240
            },
            {
                "item_id": 12,
                "name": "Dr.マーチソ(8ホール)",
                "price": 62.0,
                "likes": 1,
                "image": "http://example.com/12.jpg",
                "categoryid": 106
            }
        ],
        "likes": 408,
        "name": "寒い冬も怖くない、ドラゲナイコーデ",
        "set_id": 202407512,
        "views": 2554
    },
    ...
]

json.json_normalize

json_normalize(df.to_dict("records"),
               "items", "set_id")
  • data=df.to_dict("records"):変形したいDataFrameを辞書のリストにしたもの
  • record_path="items":要素を展開したい 辞書のリスト のカラム名
  • meta="set_id":展開後も残しておきたい元のDataFrameのカラム名

アイテムの辞書に set_id は含まれていません。
もし、 meta 引数がなければ、変形後に set_id は持ち越されません。

また、 持ち越したいカラムが複数あれば、meta 引数をカラム名の リストで指定する こともできます。

上記以外にも引数があり、より複雑な整形ができます。
詳しくは公式ドキュメントで確認できます。

行をアイテムからコーディネートに戻す

今度は逆に、行をアイテムからコーディネートに戻します。

df_new = df_items.groupby("set_id")\
    [df_items.columns.difference(df.columns)]\
    .apply(lambda df_group: df_group.to_dict("records")) \
    .rename("items")\
    .reset_index()

df_new

上記のコードを実行すると、以下のように行がコーディネートのDataFrameに戻すことができます。
ただし、 df_items 作成時に落としたカラムは戻せません。

スクリーンショット 2018-12-10 15.25.33.png

上記のコードの詳細を追っていきます。

.groupby("set_id")

df_items.groupby("set_id")\
    [df_items.columns.difference(df.columns)]\
    .apply(lambda df_group: df_group.to_dict("records")) \
    .rename("items")\
    .reset_index()

groupby を行うと、引数で指定したカラムが同じ値を持つ行どうしをグループとしてまとめます。
結果として、 DataFrameを要素に持つSeriesのようなもの ができます。
各要素のDataFrameはグループを表し、指定したカラムは同じ値で統一されています。

今回は、コーデのidである set_id が同じアイテムのグループが作られます。

[df_items.columns.difference(df.columns)]

df_items.groupby("set_id")\
    [df_items.columns.difference(df.columns)]\
    .apply(lambda df_group: df_group.to_dict("records")) \
    .rename("items")\
    .reset_index()

この記述では、変形後のアイテムの辞書に set_id が入り込まないようにしています。

groupby の後も、DataFrameと同様に [[カラム名]] で使いたいカラムに絞ることができます。

DataFrame.columns には difference という演算が備わっており、 df1.columns.difference(df2.columns) とすることで、 df1 のうち df2 に含まれないカラム名を指定することができます。

今、 df_items のうち、 df と共有しているカラムは含めたくないので、 df_items.columns.difference.(df.columns) としています。
こうすることで、 json_normalizemeta を複数指定した場合にも対応できます。

.apply(lambda df_group: df_group.to_dict("records"))

df_items.groupby("set_id")\
    [df_items.columns.difference(df.columns)]\
    .apply(lambda df_group: df_group.to_dict("records")) \
    .rename("items")\
    .reset_index()

Series や DataFrame 同様、 groupby の結果にも apply を施すことができます。
apply で指定した関数の引数は、 各グループを表すDataFrameになります。

今回の例だと、同じコーデでまとめたアイテムを行とするDataFrameです。
これを最初の形と同じく辞書にして、ほぼ完成です。

ただ、このままだと、コーデのid set_id を index に持つSeriesができるだけです。
元の形にするには、DataFrameにします。

.rename("items").reset_index()

df_items.groupby("set_id")\
    [df_items.columns.difference(df.columns)]\
    .apply(lambda df_group: df_group.to_dict("records")) \
    .rename("items")\
    .reset_index()

reset_index は index を振り直す以外に、Series を DataFrameにする ことにも使えます。
元の値とindexをカラムに持つDataFrameができます。
また、単純に振り直す場合は、 reset_index(drop=True) を使います。

ただ、これだけだと、できあがったDataFrameのカラムが [["set_id", 0]] となってしまいます。
なので、DataFrameにする前にSeriesの名前を rename"items" にしておくことで、 [["set_id", "items"]] とすることができます。

まとめ

Pandasを用いて、ネストした多対多データのjsonファイルを読み込んだDataFrameの行の対象を変える方法を紹介しました。

例としてファッションのデータを使い、行をコーディネート→アイテム→コーディネートと変形するコードを記載しました。

コーディネート→アイテムの場合は json_normalize 、 アイテム→コーディネートの場合は groupbyapply(lambda df:df.to_dict("records")) がキーポイントでした。

それでは皆さん、良いお年を!

42
31
1

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
42
31