Convert multiple level list of nested dictionaries into single list of dictionaries

Viewed 72

I want to convert multiple level list of nested dictionaries into a single list of dictionary

input:

list_ = [
 {'Name': 'Paras Jain',
  'Student': [{'Exam': 90,
               'Grade': 'a',
               'class': [{'age': 10, 'subject': 'hindi'},
                         {'age': 11, 'subject': 'maths'}]},
              {'Exam': 99,
               'Grade': 'b',
               'class': [{'age': 14, 'subject': 'evs'},
                         {'age': 15, 'subject': 'science'}]},
              {'Exam': 97,
               'Grade': 'c',
               'class': [{'age': 10, 'subject': 'history'}]}]},
 {'Name': 'Chunky Pandey',
  'Student': [{'Exam': 89,
               'Grade': 'a',
               'class': [{'age': 9, 'subject': 'no'}]},
              {'Exam': 80, 'Grade': 'b', 'class': []}]},
 {'Name': 'abc', 'Student':[]}
]

Required output:

[{'Exam': 90, 'Grade': 'a', 'Name': 'Paras Jain', 'age': 10, 'subject': 'hindi'},
 {'Exam': 90, 'Grade': 'a', 'Name': 'Paras Jain', 'age': 11, 'subject': 'maths'},
 {'Exam': 90, 'Grade': 'b', 'Name': 'Paras Jain', 'age': 14, 'subject': 'evs'},
 {'Exam': 90, 'Grade': 'b', 'Name': 'Paras Jain', 'age': 15, 'subject': 'science'},
 {'Exam': 97, 'Grade': 'c', 'Name': 'Paras Jain', 'age': 10, 'subject': 'history'},
 {'Exam': 89, 'Grade': 'a', 'Name': 'Chunky Pandey', 'age': 9, 'subject': 'no'},
 {'Exam': 89, 'Grade': 'a', 'Name': 'Chunky Pandey', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'abc', 'age': 'NA', 'subject': 'NA'}]
3 Answers

Let's try

for d in list_:
    if not d.get('Student', []):
        if 'Student' in d:
            d['Student'].append({})
        else:
            d['Student'] = [{}]


df = pd.json_normalize(list_, record_path='Student', meta='Name').explode('class', ignore_index=True)
out = df.join(df.pop('class').apply(pd.Series)).drop(columns=0).to_dict(orient='records')
print(df)

   Exam Grade                              class           Name
0  90.0     a    {'age': 10, 'subject': 'hindi'}     Paras Jain
1  90.0     a    {'age': 11, 'subject': 'maths'}     Paras Jain
2  99.0     b      {'age': 14, 'subject': 'evs'}     Paras Jain
3  99.0     b  {'age': 15, 'subject': 'science'}     Paras Jain
4  97.0     c  {'age': 10, 'subject': 'history'}     Paras Jain
5  89.0     a        {'age': 9, 'subject': 'no'}  Chunky Pandey
6  80.0     b                                NaN  Chunky Pandey
7   NaN   NaN                                NaN            abc

pprint(out, width=150)

[{'Exam': 90.0, 'Grade': 'a', 'Name': 'Paras Jain', 'age': 10.0, 'subject': 'hindi'},
 {'Exam': 90.0, 'Grade': 'a', 'Name': 'Paras Jain', 'age': 11.0, 'subject': 'maths'},
 {'Exam': 99.0, 'Grade': 'b', 'Name': 'Paras Jain', 'age': 14.0, 'subject': 'evs'},
 {'Exam': 99.0, 'Grade': 'b', 'Name': 'Paras Jain', 'age': 15.0, 'subject': 'science'},
 {'Exam': 97.0, 'Grade': 'c', 'Name': 'Paras Jain', 'age': 10.0, 'subject': 'history'},
 {'Exam': 89.0, 'Grade': 'a', 'Name': 'Chunky Pandey', 'age': 9.0, 'subject': 'no'},
 {'Exam': 80.0, 'Grade': 'b', 'Name': 'Chunky Pandey', 'age': nan, 'subject': nan},
 {'Exam': nan, 'Grade': nan, 'Name': 'abc', 'age': nan, 'subject': nan}]

Optimized, Tested with 72 million records produced in 41 seconds

For your completely blank [{}] * 900000 example, it finished in 0.42 seconds:

from itertools import chain
from pprint import pprint
from time import time

def make_record(name: str, 
                exam: str='NA', 
                grade: str='NA', 
                age: str='NA', 
                subject: str='NA') -> dict:
    return {'Name': name, 
            'Exam': exam,
            'Grade': grade, 
            'age': age, 
            'subject': subject}

def fix_record(data: dict) -> list:
    new_data = []
    name = data.get('Name', 'NA')
    students = data.get('Student', [])
    if students:
        for student in students:
            exam = student.get('Exam', 'NA')
            grade = student.get('Grade', 'NA')
            classes = student.get('class', [])
            if classes:
                for c in classes:
                    age = c.get('age', 'NA')
                    subject = c.get('subject', 'NA')
                    new_data.append(make_record(name, exam, grade, age, subject))
            else:
                new_data.append(make_record(name, exam, grade)) 
    else:
        new_data.append(make_record(name))
    return new_data

list_ = [
 {'Name': 'Paras Jain',
  'Student': [{'Exam': 90,
               'Grade': 'a',
               'class': [{'age': 10, 'subject': 'hindi'},
                         {'age': 11, 'subject': 'maths'}]},
              {'Exam': 99,
               'Grade': 'b',
               'class': [{'age': 14, 'subject': 'evs'},
                         {'age': 15, 'subject': 'science'}]},
              {'Exam': 97,
               'Grade': 'c',
               'class': [{'age': 10, 'subject': 'history'}]}]},
 {'Name': 'Chunky Pandey',
  'Student': [{'Exam': 89,
               'Grade': 'a',
               'class': [{'age': 9, 'subject': 'no'}]},
              {'Exam': 80, 'Grade': 'b', 'class': []}]},
 {'Name': 'abc', 'Student':[]}
]
list_ *= 9e6

if __name__ == '__main__':
    start = time()

    list_ = list(chain.from_iterable(map(fix_record, list_)))

    print(f'{time()-start:.2f} seconds')
    print('Total Records: ', len(list_))
    print('First 10 Records:')
    pprint(list_[:10], width=150)

    list2 =  [{}] * 9e6
    start = time()
    list2 = list(chain.from_iterable(map(fix_record, list2)))
    print(f'{time()-start:.2f} seconds')
    pprint(list2[:10], width=150)

Output:

41.02 seconds
Total Records:  72000000
First 10 Records:
[{'Exam': 90, 'Grade': 'a', 'Name': 'Paras Jain', 'age': 10, 'subject': 'hindi'},
 {'Exam': 90, 'Grade': 'a', 'Name': 'Paras Jain', 'age': 11, 'subject': 'maths'},
 {'Exam': 99, 'Grade': 'b', 'Name': 'Paras Jain', 'age': 14, 'subject': 'evs'},
 {'Exam': 99, 'Grade': 'b', 'Name': 'Paras Jain', 'age': 15, 'subject': 'science'},
 {'Exam': 97, 'Grade': 'c', 'Name': 'Paras Jain', 'age': 10, 'subject': 'history'},
 {'Exam': 89, 'Grade': 'a', 'Name': 'Chunky Pandey', 'age': 9, 'subject': 'no'},
 {'Exam': 80, 'Grade': 'b', 'Name': 'Chunky Pandey', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'abc', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 90, 'Grade': 'a', 'Name': 'Paras Jain', 'age': 10, 'subject': 'hindi'},
 {'Exam': 90, 'Grade': 'a', 'Name': 'Paras Jain', 'age': 11, 'subject': 'maths'}]

0.42 seconds
[{'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'},
 {'Exam': 'NA', 'Grade': 'NA', 'Name': 'NA', 'age': 'NA', 'subject': 'NA'}]

I tried below two ways but looking for a more optimized way: How I tested for 2700000 records. Just multiple [{}] * 900000

1st way

# tested with 2700000 records
import time
start_time = time.time()
rows = []
for data in list_:
    if data['Student']:
        for row in data['Student']:
            row['Exam'] = row['Exam']
            row['Grade'] = row['Grade']
            row['class'] = row['class']
            row['Name'] = data['Name']
            rows.append(row)
    else:
        rows.append({
            'Exam': '',
            'Grade': '',
            'class': [],
            'Name': data['Name']
        })

res = []

for data in rows:
    if data["class"]:
        for in_row in data['class']:
            in_row['Exam'] = data['Exam']
            in_row['Grade'] = data['Grade']
            in_row['age'] = in_row['age']
            in_row['subject'] = in_row['subject']
            in_row['Name'] = data['Name']
            res.append(in_row)
    else:
        res.append({
            'Exam': data['Exam'],
            'Grade': data['Grade'],
            'age': '',
            'subject': '',
            'Name': data['Name']
        })


end_time = time.time()
print(end_time-start_time)
# 15.956519842147827

2nd way

# First, fix your empty class list, so pd.json_normalize doesn't ignore it:
import time
import pandas as pd
start_time = time.perf_counter()
for i, x in enumerate(list_1):
    if not x['Student']:
        list_1[i]['Student'] = [{'Exam': 'NA', 'Grade': 'NA', "class": [{}]}]
    else:
        for j, y in enumerate(x['Student']):
            if not y['class']:
                list_1[i]['Student'][j]['class'] = [{}]


# https://towardsdatascience.com/
#all-pandas-json-normalize-you-should-know-for-flattening-json-13eae1dfb7dd
df = pd.json_normalize(list_1, record_path=['Student', 'class'], meta=['Name', ['Student', 'Exam'], ['Student', 'Grade']])
# df.columns = df.columns.str.replace('Student.', '', regex=False)
# df.columns = df.columns.str.replace('age', 'Student.class.age', regex=False)
# df.columns = df.columns.str.replace('subject', 'Student.class.subject', regex=False)

dict_ = {
    'age': 'Student.class.age',
    'subject': 'Student.class.subject'
}
# inplace = true because we just modify the working data frame if we pass #inplace = false then it returns a new data frame.
df.rename(columns=dict_, inplace=True)
df = df.fillna('NA')

out = df.to_dict('records')
end_time = time.perf_counter()
print(end_time-start_time)
# 54.662456035614014
Related