Итерация по фрейму данных Pandas, содержащему вложенные json-дикты с массивами - PullRequest
0 голосов
/ 19 декабря 2018

У меня есть список более или менее однородных json-диктов, которые я загрузил в фрейм данных Pandas.Любой данный дикт может содержать произвольное число уровней, составленных только из других диктов или массивов, например:

[ {"id": [0], "options": [{"name": "dhl", "price": 10}]}, {"id": [0, 1], "options": [{"name": "dhl", "price": 50}, {"name": "fedex", "price": "100"}]}, ]

Теперь я хотел бы иметь возможность эффективно проверять определенные поля - с помощью регулярного выражения, сравнивать весь столбец между двумя фреймами данных и т. д. id, options.name, options.price поля в этом примере.

Я нашел один способ сделатьон должен сгладить фрейм данных один раз, что позволяет нам использовать векторизованные операции, такие как str.contains.

Вот мое рекурсивное решение.

def flatten_df(df, i=0, columns_map=None):
    if not columns_map:
        columns_map = {}

    for c in df.columns[i:]:
        flattened_columns = expand_column(df, c)
        if flattened_columns.empty:
            i += 1
            continue

        def name_column(x):
            new_name = f"{c}_{x}"
            if new_name in df.columns:
                new_name = f"{c}_{uuid.uuid1().hex[:5]}"

            if c in columns_map:
                columns_map[new_name] = columns_map[c]
            else:
                columns_map[new_name] = c
            return new_name

        flattened_columns = flattened_columns.rename(columns=name_column)
        df = pd.concat([df[:], flattened_columns[:]], axis=1).drop(c, axis=1)
        columns_map.pop(c, None)
        return flatten_df(df, i, columns_map)
    return df, columns_map

def expand_column(df, column):
    mask = df[column].map(lambda x: (isinstance(x, list) or isinstance(x, dict)))
    collection_column = df[mask][column]
    return collection_column.apply(pd.Series)

А вотвывод:

id_0 id_1 options_0_name options_0_price options_1_name options_1_price 0 0.0 NaN dhl 10 NaN NaN 1 0.0 1.0 dhl 50 fedex 100

Теперь я могу выполнять векторизованные методы и при необходимости отображать развернутые столбцы в исходные.

Однако, поскольку размерсписок может быть огромным - до миллионов диктов, это решение значительно снижает производительность значительно с увеличением количества вложенных полей (т. е. с увеличением числа рекурсий).

Я использовал pandas.io.json.json_normalize раньше, но расширяетсятолько дикты.

Есть ли другие эффективные способы?Данные могут различаться, однако количество операций с ними ограничено.

Обновление со статистикой производительности:

Это %prun числа для массива из 200 тыс. Элементовс относительно небольшим количеством вложенных полей:

         101001482 function calls (100789761 primitive calls) in 79.717 seconds

   Ordered by: internal time
   List reduced from 478 to 20 due to restriction <20>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 22800000   10.062    0.000   16.327    0.000 <ipython-input-8-786bcc78e0b9>:56(<lambda>)
 53689789    9.168    0.000   10.769    0.000 {built-in method builtins.isinstance}
      139    6.827    0.049   44.534    0.320 {pandas._libs.lib.map_infer}
       25    4.134    0.165    6.469    0.259 internals.py:5074(_merge_blocks)
     26/1    3.525    0.136   79.574   79.574 <ipython-input-8-786bcc78e0b9>:1(flatten_df)
       28    2.958    0.106    2.958    0.106 {pandas._libs.algos.take_2d_axis0_object_object}
      217    2.416    0.011    2.416    0.011 {method 'copy' of 'numpy.ndarray' objects}
      100    2.355    0.024    2.355    0.024 {built-in method numpy.core.multiarray.concatenate}
   102236    2.223    0.000    2.784    0.000 generic.py:4378(__setattr__)
    66259    2.022    0.000    2.022    0.000 {pandas._libs.lib.maybe_convert_objects}
    66261    1.606    0.000    2.670    0.000 {method 'get_indexer' of 'pandas._libs.index.IndexEngine' objects}
    66510    1.413    0.000    3.235    0.000 cast.py:971(maybe_cast_to_datetime)
   133454    1.257    0.000    1.257    0.000 {built-in method numpy.core.multiarray.empty}
69329/34771    1.232    0.000    5.796    0.000 base.py:255(__new__)
101377/66756    1.178    0.000   21.435    0.000 series.py:166(__init__)
   468050    1.102    0.000    4.105    0.000 common.py:1688(is_extension_array_dtype)
  1850890    1.089    0.000    1.089    0.000 {built-in method builtins.hasattr}
   872564    1.044    0.000    2.070    0.000 <frozen importlib._bootstrap>:1009(_handle_fromlist)
    66400    1.005    0.000    8.859    0.000 algorithms.py:1548(take_nd)
464282/464168    0.940    0.000    0.942    0.000 {built-in method numpy.core.multiarray.array}

Я вижу, что значительное время затрачивается на проверку типа данных.

1 Ответ

0 голосов
/ 19 декабря 2018

Я сделал свое векторизованное решение для этого, используя apply(pd.Series), хотя мне пришлось написать некоторый дополнительный код, чтобы он работал как положено.

flatten_list_cols - для столбцов со списком примитивных элементов в нем
flatten_list_of_dict_cols - для столбцов со списком словаря элементов в нем

Вот решение :

import pandas as pd

df = pd.DataFrame([
    {"id": [0], "options": [{"name": "dhl", "price": 10}]},
    {"id": [0, 1], "options": [{"name": "dhl", "price": 50}, {"name": "fedex", "price": 100}]},
])

def flatten_list_cols(df, columns):
    for col in columns:
        # Flatten list of elements into individual columns (e.g. id: [0, 1] to columns id_0 and id_1)
        df = pd.concat([df, df[col].apply(pd.Series).add_prefix(f'{col}_')], axis=1)
        df = df.drop(col, axis=1)

    return df

def flatten_list_of_dict_cols(df, columns):
    for col in columns:
        # Flatten list of elements into individual columns (e.g. id: [0, 1] to columns id_0 and id_1)
        df = pd.concat([df, df[col].apply(pd.Series).add_prefix(f'{col}_')], axis=1)
        # Drop initial columns
        df = df.drop(col, axis=1)

        # Flatten all resulted "dict" columns
        cols_to_flatten = df.filter(regex=f'{col}').columns
        for i in cols_to_flatten:
            df = pd.concat([df, df[i].apply(pd.Series).add_prefix(f'{i}_')], axis=1)

            # Drop redundant columns
            if f'{i}_0' in df.columns:
                df = df.drop(f'{i}_0', axis=1)

        # Drop already flattened columns (with individual dicts, e.g. "options_0", "options_1" e.t.c
        for i in range(0, len(cols_to_flatten)):
            df = df.drop(f'{col}_{i}', axis=1)

    return df

df = flatten_list_cols(df, ['id'])
df = flatten_list_of_dict_cols(df, ['options'])

Результат:

df
    id_0    id_1    options_0_name  options_0_price options_1_name  options_1_price
0   0.0     NaN     dhl             10              NaN             NaN
1   0.0     1.0     dhl             50              fedex           100.0
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...