jsonlの読み込み

まとめ

  • jsonlファイルで中身がバカでかいやつは一気に読み込むとメモリエラーとなるのでchunkを使用するとよい。
  • jsonの中身は辞書型みたいな感じでデータ格納されてるっぽい。
  • バカでかいファイルの読み込みは一度parquet形式で保存して次回からそれを読み込むと速い。

本文

jsonもあんまりよくわかってませんが、jsonlが出てきたのでメモ。

pandasでJSON文字列・ファイルを読み込み(read_json) | note.nkmk.me

まずは上記サイト上の「JSON Lines(.jsonl)を読み込み」を参考にファイルを読み込んでみましょう。

%%time
test = pd.read_json(DATA / "test.jsonl", orient= "record", lines= True)
print(type(test))
display(test.head())

dataframe形式で返ってきました。ただこれは読み込む先のjsonlファイル上の問題ですが、events列が以下の通りlistとなっていてさらにその中身がdictとなっていて扱いずらそうな。

print(test.iloc[0,1])
print(type(test.iloc[0,1]))
print(type(test.iloc[0,1][0]))

そこで、他の人がやっていたchunkを使用して読み込むパターンをやってみましょう。ドキュメントを見る感じchuncksizeはjsonlを読み込むときのみに使用できるみたい。ただ何を表しているのかいまいちわからなかったので、手を動かしながらやってみます。

pandas.read_json — pandas 1.5.1 documentation

%%time
cnt = 0
CHUNKSIZE = 10000
test = pd.read_json(DATA / "test.jsonl", lines= True, chunksize= CHUNKSIZE)
print(type(test))
for chunk in test:
    cnt += 1
    if cnt > 1:
        continue
    display(chunk)
    print(type(chunk))
    
print(cnt)

うーん、まず返してくるのはドキュメントの通りJsonReaderという形式です。for文とかで一行ずつ取り出してみるとchuncksizeを使用しなかったときのようなdataframe形式で取得できました。1chunkあたりのデータ数がちょうどCHUNKSIZEと一致しているので、chucksizeは1回?あたりに読み込む行数みたいなイメージでしょうか。また、今回の場合はfor文168回回しているので、全データ数は10000*168=1,680,000行くらいでしょうか。大きいですね。

結局なぜこのchuncksizeを使用してJsonReader形式のデータを返しているのかとう話ですが、おそらく返ってくる値がdictとかでめんどくさいのを読み込みの段階でうまく扱えるからですかね?読み込む早さはchuncksize使用している方が読み込んだ後にfor文回しているので遅かったです(ちなむと読み込み自体は以下の通りJsonReader形式で読み込んだ方が速かったです。)そもそもjsonオブジェクトって項目名と値を:で結んだりして表現するみたいなので、辞書型みたいで複雑になるのはあるあるなのでしょうか。また、それらをそのままの形式で一気に抽出してからいじるより、サクッとjsonreader形式で抽出してからfor文で辞書型の中身をいじった方が早かったりするんですかね?

%%time
CHUNKSIZE = 10000
test = pd.read_json(DATA / "test.jsonl", lines= True, chunksize= CHUNKSIZE)

jsonreader + for文でaid項目のみ抽出したとき(30s)

%%time
CHUNKSIZE = 10000
test = pd.read_json(DATA / "test.jsonl", lines= True, chunksize= CHUNKSIZE)
print(type(test))
for chunk in test:
    for session_id, events in chunk.values:
        last_aid_list.append([session_id, events[-1]["aid"]])    

lines=Trueで抽出してからapplyで加工したとき(23s)

%%time
test = pd.read_json(DATA / "test.jsonl", orient= "record", lines= True)
test["aid"] = test["events"].apply(lambda x: x[-1]["aid"])

時間測ってみたもののむしろapplyで加工した方が速かった。。うーん、jsonreaderで読み込むことにどういうメリットがあるんだろう。。

上記までもそれなりのデータ数がでしたが、さらに大きいデータになると一気にloadしようとする方はメモリエラー出た一方でchunckは読み込めました。メモリ削減の効果はありそうです。ただ、これだけのデータ数になると読み込みにとにかく時間がかかる。。8分読み込んでもまだ。。

%%time

def read_data(path, is_train):
    '''
    data読み込み
    '''
    
# #     lines 読み込み
# #     データ数大きいとメモリ不足
#     df = pd.read_json(DATA / path, orient= "record", lines= True)
#     df["aid"] = df["events"].apply(lambda x: x[-1]["aid"])
#     df["ts"] = df["events"].apply(lambda x: x[-1]["ts"])
#     df["type"] = df["events"].apply(lambda x: x[-1]["type"])
    
    # jsonreader 読み込み
    last_aid_list = []
    CHUNKSIZE = 10000
    json_reader = pd.read_json(DATA / path, lines= True, chunksize= CHUNKSIZE)
    for chunk in json_reader:
        for session_id, events in chunk.values:
            last_aid_list.append([session_id, events[-1]["aid"]])    
#         del json_reader,chunk
        del chunk
        gc.collect()
    
    df = pd.DataFrame(last_aid_list)
    del last_aid_list
    gc.collect()
    
    return df

train = read_data("train.jsonl", is_train= True)

ということで、parquet formatで早く読み込むというnotebookがあったのでこちらを使わせて頂く。parquetに一度保存して次回からはparquetを読み込むという方法。確かにこれなら読み込みにかかる時間がだいぶ減った。にしても5 / 124ファイルのデータ数が22,547,356行とは。。ひいい。

%%time

def read_data(path, is_train, is_save= True):
    '''
    data読み込み
    '''
    
    # ===================
    # read existing parquet files
    files = sorted(glob('../input/otto-chunk-data-inparquet-format/train_parquet/*'))[0:5]
#     files = sorted(glob('../input/otto-chunk-data-inparquet-format/train_parquet/*'))
    dfs = []
    for path in tqdm(files):
        dfs.append(pd.read_parquet(path))
    dfs = pd.concat(dfs).reset_index(drop=True)
    return dfs

train = read_data("train.jsonl", is_train= True)

おしまい。