Как сгладить фрейм данных pandas с некоторыми столбцами в формате json?

#python #json #pandas #dataframe #flatten

#python #json #pandas #фрейм данных #сгладить

Вопрос:

У меня есть фрейм данных, который загружает данные из базы данных. df Большинство столбцов представляют собой строки json, а некоторые даже представляют собой список json. Например:

 id     name     columnA                               columnB
1     John     {"dist": "600", "time": "0:12.10"}    [{"pos": "1st", "value": "500"},{"pos": "2nd", "value": "300"},{"pos": "3rd", "value": "200"}, {"pos": "total", "value": "1000"}]
2     Mike     {"dist": "600"}                       [{"pos": "1st", "value": "500"},{"pos": "2nd", "value": "300"},{"pos": "total", "value": "800"}]
...
  

Как вы можете видеть, не все строки имеют одинаковое количество элементов в строках json для столбца.

Что мне нужно сделать, это сохранить обычные столбцы такими id , name какие они есть, и сгладить столбцы json следующим образом:

 id    name   columnA.dist   columnA.time   columnB.pos.1st   columnB.pos.2nd   columnB.pos.3rd     columnB.pos.total
1     John   600            0:12.10        500               300               200                 1000 
2     Mark   600            NaN            500               300               Nan                 800 
  

Я пытался использовать json_normalize вот так:

 from pandas.io.json import json_normalize
json_normalize(df)
  

Но, похоже, есть некоторые проблемы keyerror . Каков правильный способ сделать это?

Комментарии:

1. Как насчет значений в столбце B? Вы также хотите сгладить словари?

2. ДА. они также должны быть сглажены. в исходном вопросе была опечатка, в которой я поместил columnA для всего сглаженного столбца, но сейчас исправил ее.

Ответ №1:

Вот решение json_normalize() , которое снова используется с помощью пользовательской функции для получения данных в правильном формате, понятном json_normalize функции.

 import ast
from pandas.io.json import json_normalize

def only_dict(d):
    '''
    Convert json string representation of dictionary to a python dict
    '''
    return ast.literal_eval(d)

def list_of_dicts(ld):
    '''
    Create a mapping of the tuples formed after 
    converting json strings of list to a python list   
    '''
    return dict([(list(d.values())[1], list(d.values())[0]) for d in ast.literal_eval(ld)])

A = json_normalize(df['columnA'].apply(only_dict).tolist()).add_prefix('columnA.')
B = json_normalize(df['columnB'].apply(list_of_dicts).tolist()).add_prefix('columnB.pos.') 
  

Наконец, присоединитесь DFs к общему индексу, чтобы получить:

 df[['id', 'name']].join([A, B])
  

Изображение


РЕДАКТИРОВАТЬ: — Согласно комментарию @MartijnPieters, рекомендуемым способом декодирования строк json было бы использовать json.loads() , что намного быстрее по сравнению с использованием ast.literal_eval() , если вы знаете, что источником данных является JSON.

Комментарии:

1. Большое спасибо за ответ! однако есть одна вещь, возвращаемые списки в list_of_dicts (list(d.values())[0], list(d.values())[1]), а не наоборот? В противном случае это сработало идеально для меня.

2. Поскольку вы, должно быть, знаете, что dictionaries не сохраняйте порядок при выполнении итерации, значения, присутствующие в dict , отображались в порядке, противоположном вашему, и, следовательно, возникла необходимость использовать нотацию нарезки по-другому по сравнению с вашим. Если он отображается в том же порядке, что и вы упомянули, продолжайте, или вы даже можете использовать an Ordered Dict для сохранения порядка, если хотите.

3. Почему (медленно!) ast.literal_eval() вызов, когда вы должны использовать json.loads() ? Последний обрабатывает правильные данные в формате JSON, первый — только синтаксис Python , который существенно отличается , когда дело доходит до логических значений, нулей и данных unicode за пределами BMP.

4. @MartijnPieters: Спасибо за комментарий. Я обновил свой пост.

5. Если ваши данные содержат null, вы можете обновить only_dict метод до: return ast.literal_eval(d) if pd.notnull(d) else {} В противном случае он возвращает ValueError: malformed node or string: nan

Ответ №2:

Самый быстрый, кажется,:

 import pandas as pd
import json

json_struct = json.loads(df.to_json(orient="records"))    
df_flat = pd.io.json.json_normalize(json_struct) #use pd.io.json
  

Комментарии:

1. Это был определенно самый простой метод, который сработал для меня. Единственное предостережение в том, что ваши вложенные объекты будут иметь длинные имена (data.level1.level2.level3 … и т.д.)

2. Это определенно мой выбранный ответ — отлично работает и очень лаконичное решение. Спасибо!

3. Это лучший ответ!

4. мне нужно немного orient="records and pd.io.json.json_normalize . now, чтобы вставить этот материал в ячейки долговременной памяти

Ответ №3:

TL; DR Скопируйте-вставьте следующую функцию и используйте ее следующим образом: flatten_nested_json_df(df)

Это самая общая функция, которую я мог придумать:

 def flatten_nested_json_df(df):

    df = df.reset_index()

    print(f"original shape: {df.shape}")
    print(f"original columns: {df.columns}")


    # search for columns to explode/flatten
    s = (df.applymap(type) == list).all()
    list_columns = s[s].index.tolist()

    s = (df.applymap(type) == dict).all()
    dict_columns = s[s].index.tolist()

    print(f"lists: {list_columns}, dicts: {dict_columns}")
    while len(list_columns) > 0 or len(dict_columns) > 0:
        new_columns = []

        for col in dict_columns:
            print(f"flattening: {col}")
            # explode dictionaries horizontally, adding new columns
            horiz_exploded = pd.json_normalize(df[col]).add_prefix(f'{col}.')
            horiz_exploded.index = df.index
            df = pd.concat([df, horiz_exploded], axis=1).drop(columns=[col])
            new_columns.extend(horiz_exploded.columns) # inplace

        for col in list_columns:
            print(f"exploding: {col}")
            # explode lists vertically, adding new columns
            df = df.drop(columns=[col]).join(df[col].explode().to_frame())
            new_columns.append(col)

        # check if there are still dict o list fields to flatten
        s = (df[new_columns].applymap(type) == list).all()
        list_columns = s[s].index.tolist()

        s = (df[new_columns].applymap(type) == dict).all()
        dict_columns = s[s].index.tolist()

        print(f"lists: {list_columns}, dicts: {dict_columns}")

    print(f"final shape: {df.shape}")
    print(f"final columns: {df.columns}")
    return df
  

Он принимает фрейм данных, который может иметь вложенные списки и / или dicts в своих столбцах, и рекурсивно взрывает / сглаживает эти столбцы.

Он использует pandas pd.json_normalize для разнесения словарей (создания новых столбцов) и pandas explode для разнесения списков (создания новых строк).

Простой в использовании:

 # Test
df = pd.DataFrame(
    columns=['id','name','columnA','columnB'],
    data=[
        [1,'John',{"dist": "600", "time": "0:12.10"},[{"pos": "1st", "value": "500"},{"pos": "2nd", "value": "300"},{"pos": "3rd", "value": "200"}, {"pos": "total", "value": "1000"}]],
        [2,'Mike',{"dist": "600"},[{"pos": "1st", "value": "500"},{"pos": "2nd", "value": "300"},{"pos": "total", "value": "800"}]]
    ])

flatten_nested_json_df(df)
  

Это не самая эффективная вещь на земле, и у нее есть побочный эффект сброса индекса вашего фрейма данных, но она выполняет свою работу. Не стесняйтесь настраивать его.

Комментарии:

1. Это, БЕЗУСЛОВНО, лучшее решение, которое я видел за долгое время! Хорошая работа!

2. Привет, это полезно, но, похоже, не сохраняет новый фрейм данных

3. @CameronStewart сохранить где?

4. Это выдает ошибку msg.format(req_len=len(left.columns), given_len=len(right)) ValueError: Unable to coerce to Series, length must be 44: given 1

5. Безусловно, лучшее решение этой проблемы … пальцы вверх

Ответ №4:

создайте пользовательскую функцию для сглаживания columnB , а затем используйте pd.concat

 def flatten(js):
    return pd.DataFrame(js).set_index('pos').squeeze()

pd.concat([df.drop(['columnA', 'columnB'], axis=1),
           df.columnA.apply(pd.Series),
           df.columnB.apply(flatten)], axis=1)
  

введите описание изображения здесь