Свести списки диктов по указанным клавишам - PullRequest
0 голосов
/ 19 декабря 2018

Цель: прочитать данные из таблицы SQL, где столбец содержит JSON (массивы), извлечь определенные ключи / значения из JSON в новые столбцы, а затем записать в новую таблицу.Одна из радостей исходного формата данных заключается в том, что некоторые записи данных являются массивами JSON, а некоторые - не массивами (просто JSON).Таким образом, мы можем начать с :

testcase = [(1, [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}]), 
            (2, {'a': 30, 'b': 40}), 
            (3, {'a': 100, 'b': 200, 'd': 300})]
for x in testcase:
    print(x)
(1, [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}])
(2, {'a': 30, 'b': 40})
(3, {'a': 100, 'b': 200, 'd': 300})

Обратите внимание, что первый элемент каждого кортежа - это идентификатор записи.Первая запись - это массив длины два, вторая и третья записи не являются массивами. Желаемый вывод: (в виде DataFrame):

    a   b   data
1   1   2   '{"c": 3}'
1   11  12  '{"c": 13}'
2   30  40  '{}'
3   100     200     '{"d": 300}'

Здесь вы можете видеть, что я извлек ключи 'a' и 'b' из диктов в новые столбцы, оставляяостальные ключи / значения на месте.Желаемое поведение - пустой dict для id = 2.

Сначала я выделил id и данные в отдельные списки.Я пользуюсь этой возможностью, чтобы превратить диктовку в список диктовок (длиной 1), чтобы типы теперь были согласованными:

id = [x[0] for x in testcase]
data_col = [x[1] if type(x[1]) == list else [x[1]] for x in testcase]
for x in data_col:
    print(x)
[{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}]
[{'a': 30, 'b': 40}]
[{'a': 100, 'b': 200, 'd': 300}]

Это кажется немного неуклюжим дополнительным шагом для извлечения id и data_colкак отдельные списки, хотя, по крайней мере, у нас есть приятное свойство, что мы не копируем данные:

id[0] is testcase[0][0]
True
data_col[0] is testcase[0][1]
True

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

Здесь происходит основная мелочь, где я выполняю понимание диктата в понимании двойного списка для итерации по каждому диктову:

popped = [(id, {key: element.pop(key, None) for key in ['a', 'b']}) \
for id, row in zip(id, data_col) for element in row]
for x in popped:
    print(x)
(1, {'a': 1, 'b': 2})
(1, {'a': 11, 'b': 12})
(2, {'a': 30, 'b': 40})
(3, {'a': 100, 'b': 200})

Мне нужно иметь возможность связать каждую новую строку с ее исходным идентификатором, и вышеизложенное достигает этого, правильно воспроизводя соответствующее значение идентификатора (1, 1, 2, 3).После небольшой уборки я могу выстроить все целевые строки:

import pandas as pd
from psycopg2.extras import Json
id2 = [x[0] for x in popped]
cols = [x[1] for x in popped]
data = [Json(item) for sublist in data_col for item in sublist]
popped_df = pd.DataFrame(cols, index=id2)
popped_df['data'] = data

И это дает мне желаемый DataFrame, как показано выше.Но ... все ли мои шутки со списками необходимы?Я не мог сделать простой json_normalize, потому что я не хочу извлекать все ключи, и он падает с комбинацией массивов и не-массивов.

Он также должен быть настолько быстрым, насколько это возможно, насколько это возможнообрабатывать миллионы строк.По этой причине я на самом деле преобразую DataFrame в список с помощью: list (popped_df.itertuples ()), чтобы затем перейти к execute_values ​​() в psycopg2.extras, так что я пока не буду беспокоиться о создании DataFrame и просто создаю список вывода, нов этом посте я действительно спрашиваю, есть ли более чистый и быстрый способ извлечения этих конкретных ключей из диктовок в новые столбцы и строки, устойчивый к тому, является ли запись массивом или нет, и отслеживание соответствующего идентификатора записи.

Я уклонялся от сквозного подхода к пандам, читая данные с помощью pd.read_sql (), поскольку я считывал, что DataFrame.to_sql () был относительно медленным.

Ответы [ 2 ]

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

Вы можете сделать что-то вроде этого:

import pandas as pd

testcase = [(1, [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}]),
            (2, {'a': 30, 'b': 40}),
            (3, {'a': 100, 'b': 200, 'd': 300})]


def split_dict(d, keys=['a', 'b']):
    """Split the dictionary by keys"""
    preserved = {key: value for key, value in d.items() if key in keys}
    complement = {key: value for key, value in d.items() if key not in keys}
    return preserved, complement


def get_row(val):
    preserved, complement = split_dict(val)
    preserved['data'] = complement
    return preserved


rows = []
index = []
for i, values in testcase:
    if isinstance(values, list):
        for value in values:
            rows.append(get_row(value))
            index.append(i)
    else:
        rows.append(get_row(values))
        index.append(i)


df = pd.DataFrame.from_records(rows, index=index)
print(df)

Вывод

     a    b        data
1    1    2    {'c': 3}
1   11   12   {'c': 13}
2   30   40          {}
3  100  200  {'d': 300}
0 голосов
/ 19 декабря 2018

Ваши данные беспорядочные, поскольку вторым элементом вашего testcase может быть list или a dict.В этом случае вы можете создать список с помощью цикла for, а затем передать его конструктору pd.DataFrame:

testcase = [(1, [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}]), 
            (2, {'a': 30, 'b': 40}), 
            (3, {'a': 100, 'b': 200, 'd': 300})]

L = []
for idx, data in testcase:
    for d in ([data] if isinstance(data, dict) else data):
        # string conversion not strictly necessary below
        others = str({k: v for k, v in d.items() if k not in ('a', 'b')})
        L.append((idx, d['a'], d['b'], others))

df = pd.DataFrame(L, columns=['index', 'a', 'b', 'data']).set_index('index')

print(df)

         a    b        data
index                      
1        1    2    {'c': 3}
1       11   12   {'c': 13}
2       30   40          {}
3      100  200  {'d': 300}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...