Convert nested DataFrame with sorted unique values, to a nested Dictionary in Python

Viewed 248

I'm trying to take a nested DataFrame and convert it to a nested Dictionary.

Here is my original DataFrame with the following unique values:

input: df.head(5)

output:

    reviewerName                                  title    reviewerRatings
0        Charles       Harry Potter Book Seven News:...                3.0
1      Katherine       Harry Potter Boxed Set, Books...                5.0
2           Lora       Harry Potter and the Sorcerer...                5.0
3           Cait       Harry Potter and the Half-Blo...                5.0
4          Diane       Harry Potter and the Order of...                5.0

input: len(df['reviewerName'].unique())

output: 66130

Given that there are multiple values in each of the 66130 unqiue values (ie. "Charles" would occur 3 times), I took the 66130 unique "reviewerName" and assign them all as the key in the new nested DataFrame, then assign the value using "title" and "reviewerRatings" as another layer of key:value in the same nested DataFrame.

input: df = df.set_index(['reviewerName', 'title']).sort_index()

output:

                                                       reviewerRatings
    reviewerName                               title
         Charles    Harry Potter Book Seven News:...               3.0
                    Harry Potter and the Half-Blo...               3.5
                    Harry Potter and the Order of...               4.0
       Katherine    Harry Potter Boxed Set, Books...               5.0
                    Harry Potter and the Half-Blo...               2.5
                    Harry Potter and the Order of...               5.0
...
230898 rows x 1 columns

As a follow up to the first question, I tried to convert the nested DataFrame to a nested Dictionary.

The new nested DataFrame column indexing above shows "reviewerRatings" in the 1st row (column 3) and "reviewerName" and "title" in the 2nd row (column 1 and 2), and when I run the df.to_dict() method below, output shows {reviewerRatingsIndexName: {(reviewerName, title): reviewerRatings}}

input: df.to_dict()

output:

{'reviewerRatings': 
 {
  ('Charles', 'Harry Potter Book Seven News:...'): 3.0, 
  ('Charles', 'Harry Potter and the Half-Blo...'): 3.5, 
  ('Charles', 'Harry Potter and the Order of...'): 4.0,   
  ('Katherine', 'Harry Potter Boxed Set, Books...'): 5.0, 
  ('Katherine', 'Harry Potter and the Half-Blo...'): 2.5, 
  ('Katherine', 'Harry Potter and the Order of...'): 5.0,
 ...}
}

But for my desired output below, I'm looking to get my output as {reviewerName: {title: reviewerRating}} which is exactly the way I had sorted in the nested DataFrame.

{'Charles': 
 {'Harry Potter Book Seven News:...': 3.0, 
  'Harry Potter and the Half-Blo...': 3.5, 
  'Harry Potter and the Order of...': 4.0},   
 'Katherine':
 {'Harry Potter Boxed Set, Books...': 5.0, 
  'Harry Potter and the Half-Blo...': 2.5, 
  'Harry Potter and the Order of...': 5.0},
...}

Is there any way to manipulate the nested DataFrame or nested Dictionary so that when I run df.to_dict() method, it would show {reviewerName: {title: reviewerRating}}.

Thanks!

2 Answers

Use groupby with lambda function for dictionaries per reviewerName and then output Series convert by to_dict:

print (df)
  reviewerName                             title  reviewerRatings
0      Charles  Harry Potter Book Seven News:...              3.0
1      Charles  Harry Potter Boxed Set, Books...              5.0
2      Charles  Harry Potter and the Sorcerer...              5.0
3    Katherine  Harry Potter and the Half-Blo...              5.0
4    Katherine   Harry otter and the Order of...              5.0

d = (df.groupby('reviewerName')['title','reviewerRatings']
       .apply(lambda x: dict(x.values))
       .to_dict())
print (d)

{
    'Charles': {
        'Harry Potter Book Seven News:...': 3.0,
        'Harry Potter Boxed Set, Books...': 5.0,
        'Harry Potter and the Sorcerer...': 5.0
    },
    'Katherine': {
        'Harry Potter and the Half-Blo...': 5.0,
        'Harry otter and the Order of...': 5.0
    }
}

There are a couple of approaches. You can use groupby with to_dict, or iterate rows with collections.defaultdict. Notably, the latter isn't necessarily less efficient.

groupby + to_dict

Construct a series from each groupby object and convert it to a dictionary to give a series of dictionary values. Finally, convert this to a dictionary of dictionaries via another to_dict call.

res = df.groupby('reviewerName')\
        .apply(lambda x: x.set_index('title')['reviewerRatings'].to_dict())\
        .to_dict()

collections.defaultdict

Define a defaultdict of dict objects and iterate your dataframe by row.

from collections import defaultdict

res = defaultdict(dict)
for row in df.itertuples(index=False):
    res[row.reviewerName][row.title] = row.reviewerRatings

The resulting defaultdict does not need to be converted back to a regular dict as defaultdict is a subclass of dict.

Performance benchmarking

Benchmarking is set-up and data dependent. You should test with your own data to see what works best.

# Python 3.6.5, Pandas 0.19.2

from collections import defaultdict
from random import sample

# construct sample dataframe
np.random.seed(0)
n = 10**4  # number of rows
names = np.random.choice(['Charles', 'Lora', 'Katherine', 'Matthew',
                          'Mark', 'Luke', 'John'], n)
books = [f'Book_{i}' for i in sample(range(10**5), n)]
ratings = np.random.randint(0, 6, n)

df = pd.DataFrame({'reviewerName': names, 'title': books, 'reviewerRatings': ratings})

def jez(df):
    return df.groupby('reviewerName')['title','reviewerRatings']\
             .apply(lambda x: dict(x.values))\
             .to_dict()

def jpp1(df):
    return df.groupby('reviewerName')\
             .apply(lambda x: x.set_index('title')['reviewerRatings'].to_dict())\
             .to_dict()

def jpp2(df):
    dd = defaultdict(dict)
    for row in df.itertuples(index=False):
        dd[row.reviewerName][row.title] = row.reviewerRatings
    return dd

%timeit jez(df)   # 33.5 ms per loop
%timeit jpp1(df)  # 17 ms per loop
%timeit jpp2(df)  # 21.1 ms per loop
Related