Get a decision tree in a dictionary

Viewed 744

I am looking for a way in python to make a dictionary of dictionaries based on the desired structure dynamically.

I have the data bellow:

{'weather': ['windy', 'calm'], 'season': ['summer', 'winter', 'spring', 'autumn'],  'lateness': ['ontime', 'delayed']} 

I give the structure I want them to be like:

['weather', 'season', 'lateness']

and finally get the data in this format:

{'calm': {'autumn': {'delayed': 0, 'ontime': 0},
          'spring': {'delayed': 0, 'ontime': 0},
          'summer': {'delayed': 0, 'ontime': 0},
          'winter': {'delayed': 0, 'ontime': 0}},
 'windy': {'autumn': {'delayed': 0, 'ontime': 0},
           'spring': {'delayed': 0, 'ontime': 0},
           'summer': {'delayed': 0, 'ontime': 0},
           'winter': {'delayed': 0, 'ontime': 0}}}

This is the manual way that I thought for achieving this:

dtree = {}
for cat1 in category_cases['weather']:
    dtree.setdefault(cat1, {})
    for cat2 in category_cases['season']:
        dtree[cat1].setdefault(cat2, {})
        for cat3 in category_cases['lateness']:
            dtree[cat1][cat2].setdefault(cat3, 0)

Can you think of a way to be able to just change the structure I wrote and having the desired result? Keep in mind that the structure might not be the same size every time.

Also if you think of another way except dictionaries that I can access the result, it will also work for me.

5 Answers

If you're not avert to using external packages, pandas.DataFrame might be a viable candidate since it looks like you'll be using a table:

import pandas as pd
df = pd.DataFrame(
       index=pd.MultiIndex.from_product([d['weather'], d['season']]),
       columns=d['lateness'], data=0
     )

Result:

              ontime  delayed
windy summer       0        0
      winter       0        0
      spring       0        0
      autumn       0        0
calm  summer       0        0
      winter       0        0
      spring       0        0
      autumn       0        0

And you can easily make changes with indexing:

df.loc[('windy', 'summer'), 'ontime'] = 1
df.loc['calm', 'autumn']['delayed'] = 2

# Result:
              ontime  delayed
windy summer       1        0
      winter       0        0
      spring       0        0
      autumn       0        0
calm  summer       0        0
      winter       0        0
      spring       0        0
      autumn       0        2

The table can be constructed dynamically if you will always use the last key for columns, assuming your keys are in the desired insertion order:

df = pd.DataFrame(
       index=pd.MultiIndex.from_product(list(d.values())[:-1]), 
       columns=list(d.values())[-1], data=0
     )

Since you're interested in pandas, given your structure, I would also recommend giving a good read over on MultiIndex and Advance Indexing, just to get some idea on how to play around with your data. Here are some examples:

# Gets the sum of 'delayed' items in all of 'calm'
# Filters all the 'delayed' data in 'calm'    
df.loc['calm', 'delayed']

# summer    5
# winter    0
# spring    0
# autumn    2
# Name: delayed, dtype: int64

# Apply a sum:
df.loc['calm', 'delayed'].sum()

# 7

# Gets the mean of all 'summer' (notice the `slice(None)` is required to return all of the 'calm' and 'windy' group)
df.loc[(slice(None), 'summer'), :].mean()

# ontime     0.5
# delayed    2.5
# dtype: float64

It definitely is very handy and versatile, but before you get too deep into it you might will definitely want to read up first, the framework might take some getting used to.


Otherwise, if you still prefer dict, there's nothing wrong with that. Here's a recursive function to generate based on the given keys (assuming your keys are in the desired insertion order):

def gen_dict(d, level=0):
    if level >= len(d):
        return 0
    key = tuple(d.keys())[level]
    return {val: gen_dict(d, level+1) for val in d.get(key)}

gen_dict(d)

Result:

{'calm': {'autumn': {'delayed': 0, 'ontime': 0},
          'spring': {'delayed': 0, 'ontime': 0},
          'summer': {'delayed': 0, 'ontime': 0},
          'winter': {'delayed': 0, 'ontime': 0}},
 'windy': {'autumn': {'delayed': 0, 'ontime': 0},
           'spring': {'delayed': 0, 'ontime': 0},
           'summer': {'delayed': 0, 'ontime': 0},
           'winter': {'delayed': 0, 'ontime': 0}}}

I think this might work for you.

def get_output(category, order, i=0):
         output = {}
         for key in order[i:i+1]:
             for value in category[key]:
                 output[value] = get_output(category, order, i+1)
         if output == {}:
            return 0
         return output

You could use itertools.product to get the cartesian product between the dictionary values(assuming you want the same key order). Then we can iterate every key except the last, insert/update dictionaries with setdefault. Then we can set the inner most key with a count of 0.

from itertools import product
from pprint import pprint

d = {
    "weather": ["windy", "calm"],
    "season": ["summer", "winter", "spring", "autumn"],
    "lateness": ["ontime", "delayed"],
}

result = {}
for comb in product(*d.values()):
    current = result
    for key in comb[:-1]:
        current = current.setdefault(key, {})
    current[comb[-1]] = 0

pprint(result)

Output:

{'calm': {'autumn': {'delayed': 0, 'ontime': 0},
          'spring': {'delayed': 0, 'ontime': 0},
          'summer': {'delayed': 0, 'ontime': 0},
          'winter': {'delayed': 0, 'ontime': 0}},
 'windy': {'autumn': {'delayed': 0, 'ontime': 0},
           'spring': {'delayed': 0, 'ontime': 0},
           'summer': {'delayed': 0, 'ontime': 0},
           'winter': {'delayed': 0, 'ontime': 0}}}

If we want a custom order(might be useful for older python versions with no guaranteed key order), we can pass a list to product():

order = ['weather', 'season', 'lateness']

result = {}
for comb in product(*map(d.get, order)):
    current = result
    for key in comb[:-1]:
        current = current.setdefault(key, {})
    current[comb[-1]] = 0

Here's a recursive solution that is slightly different from the one provided by r.ook in the excellent accepted answer:

category_cases = {'weather': ['windy', 'calm'],
                  'season': ['summer', 'winter', 'spring', 'autumn'],
                  'lateness': ['ontime', 'delayed']}
order = ['weather', 'season', 'lateness']

def gen_tree(category_cases, order):
    if len(order) == 0:
        return 0
    return {x:gen_tree(category_cases, order[1:]) for x in category_cases[order[0]]}

It does not assume that the dictionary preserves the order of the keys, so it should be more backwards compatible.

Yes, you can achieve this using the following code:

import copy

structure = ['weather', 'season', 'lateness']
data = {'weather': ['windy', 'calm'], 'season': ['summer', 'winter', 'spring', 'autumn'],
        'lateness': ['ontime', 'delayed'], }

d_tree = dict()
n = len(structure)  # length of the structure list
prev_val = 0  # the innermost value
while n > 0:
    n -= 1
    keys = data.get(structure[n]) or list()  # get the list of values from data
    # Idea here is to start with inner most dict and keep moving outer
    d_tree.clear()
    for key in keys:
        d_tree[key] = copy.copy(prev_val)
    prev_val = copy.copy(d_tree)  # Copy the d_tree to put as value to outer dict
print(d_tree)

Hope this helps!!

Related