How to retrieve minimum unique values from list?

Viewed 888

I have a list of dictionary. I wish to have only one result for each unique api and the result need to show according to priority: 0, 1, 2. May I know how should I work on it?

Data:

[
{'api':'test1', 'result': 0},
{'api':'test2', 'result': 1},
{'api':'test3', 'result': 2},
{'api':'test3', 'result': 0},
{'api':'test3', 'result': 1},
]

Expected output:

[
{'api':'test1', 'result': 0},
{'api':'test2', 'result': 1},
{'api':'test3', 'result': 0},
]
9 Answers

Assuming input data you can do classic sql-ish groupby:

from itertools import groupby

# in case your data is sorted already by api skip the below line
data = sorted(data, key=lambda x: x['api'])

res = [
    {'api': g, 'result': min(v, key=lambda x: x['result'])['result']} 
    for g, v in groupby(data, lambda x: x['api'])
]

Outputs:

[{'api': 'test1', 'result': 0}, {'api': 'test2', 'result': 1}, {'api': 'test3', 'result': 0}]

You can pass through the list once and preserve the best ones you see for each group. This is time and space efficient.

def get_min_unique(items, id_key, value_key):
  lowest = {}
  for item in items:
    key = item[id_key]
    if key not in lowest or lowest[key][value_key] > item[value_key]:
        lowest[key] = item
  return list(lowest.values())

For example with your own data:

data = [
  {'api':'test1', 'result': 0},
  {'api':'test2', 'result': 1},
  {'api':'test3', 'result': 2},
  {'api':'test3', 'result': 0},
  {'api':'test3', 'result': 1},
]

assert get_min_unique(data, 'api', 'result') == [
  {'api': 'test1', 'result': 0},
  {'api': 'test2', 'result': 1},
  {'api': 'test3', 'result': 0},
]
data = [
    {'api': 'test1', 'result': 0},
    {'api': 'test3', 'result': 2},
    {'api': 'test2', 'result': 1},
    {'api': 'test3', 'result': 1},
    {'api': 'test3', 'result': 0}
]

def find(data):
    step1 = sorted(data, key=lambda k: k['result'])
    print('step1', step1)

    step2 = {}
    for each in step1:
        if each['api'] not in step2:
            step2[each['api']] = each
    print('step2', step2)

    step3 = list(step2.values())
    print('step3', step3)
    print('\n')
    return step3

find(data)

Try this, it will give you

step1 [{'api': 'test1', 'result': 0}, {'api': 'test3', 'result': 0}, {'api': 'test2', 'result': 1}, {'api': 'test3', 'result': 1}, {'api': 'test3', 'result': 2}]
step2 {'test1': {'api': 'test1', 'result': 0}, 'test3': {'api': 'test3', 'result': 0}, 'test2': {'api': 'test2', 'result': 1}}
step3 [{'api': 'test1', 'result': 0}, {'api': 'test3', 'result': 0}, {'api': 'test2', 'result': 1}]

Sort all first, then find first for each "api", and there goes your result.

Indulging in code golf:

from itertools import groupby
dut = [
    {'api':'test1', 'result': 0},
    {'api':'test2', 'result': 1},
    {'api':'test3', 'result': 2},
    {'api':'test3', 'result': 0},
    {'api':'test3', 'result': 1},
]

res = [
    next(g)
    for _,g in groupby(
        sorted(dut, key=lambda d: tuple(d.values())),
        key=lambda i: i['api']
    )
]

result:

Out[45]:
[{'api': 'test1', 'result': 0},
 {'api': 'test2', 'result': 1},
 {'api': 'test3', 'result': 0}]

Using the itertools.groupby utility, the iterable fed as the first argument is sorted in ascending order using sorted by api and result and grouped by result only.

groupby returns back an iterable of the key, and iterable of items in this group, as seen here:

In [56]: list(groupby(sorted(dut, key=lambda i: tuple(i.values())), key=lambda i: i['api']))
Out[56]:
[('test1', <itertools._grouper at 0x10af4c550>),
 ('test2', <itertools._grouper at 0x10af4c400>),
 ('test3', <itertools._grouper at 0x10af4cc88>)]

Using a list comprehension, since the group is already sorted, next is used to fetch the first item in the group and the group key is discarded.

The existing answers are fine if you have a need to store every api at every priority and only periodically filter it to highest priority. If you're only ever going to need the highest priority of each api, however, I'd argue you're using the wrong data structure.

>>> from collections import UserDict
>>> 
>>> class DataContainer(UserDict):
...     def __setitem__(self, key, value):
...         cur = self.get(key)
...         if cur is None or value < cur:
...             super().__setitem__(key, value)
...     def __str__(self):
...         return '\n'.join(("'api': {}, 'result': {}".format(k, v) for k, v in self.items()))
... 
>>> data = DataContainer()
>>> data['test1'] = 0
>>> data['test2'] = 1
>>> data['test3'] = 2
>>> data['test3'] = 0
>>> data['test3'] = 1
>>> print(data)
'api': test1, 'result': 0
'api': test2, 'result': 1
'api': test3, 'result': 0

This container will only ever contain the highest priority for each api. Advantages include:

  • Clearly expresses what you're doing
  • No need for code golf
  • Keeps memory footprint to minimum
  • Faster than periodically sorting, grouping, and filtering

not so clean solution like others, but i think step wise, easy to understand one

l = [
{'api':'test1', 'result': 0},
{'api':'test2', 'result': 1},
{'api':'test3', 'result': 2},
{'api':'test3', 'result': 0},
{'api':'test3', 'result': 1},
]

j = {'api':[], 'result':[]}
for i in l:
    if i['api'] not in j['api']:
        j['api'].append(i['api'])
        j['result'].append(i['result']) 
    else:    
        index = j['api'].index(i['api'])
        
        
        if j['result'][index]>i['result']:
            j['result'][index] = i['result']
        
result = []

for i in range(len(j['api'])):
        result.append({'api':j['api'][i],'result':j['result'][i]})
    
print(result)

output

[{'api': 'test1', 'result': 0},
 {'api': 'test2', 'result': 1},
 {'api': 'test3', 'result': 0}]

You could pick another, more efficient data structure: a dict of Counters.

You retain the distribution of results for each api, and the code is relatively straightforward:

data = [
{'api':'test1', 'result': 0},
{'api':'test2', 'result': 1},
{'api':'test3', 'result': 2},
{'api':'test3', 'result': 0},
{'api':'test3', 'result': 1},
]

from collections import Counter

results = {}
for d in data:
    counter = results.setdefault(d['api'], Counter())
    counter[d['result']] += 1

results
# {'test1': Counter({0: 1}),
#  'test2': Counter({1: 1}),
#  'test3': Counter({2: 1, 0: 1, 1: 1})}

[{'api': api, 'result':min(v.keys())} for api, v in results.items()]
# [{'api': 'test1', 'result': 0},
#  {'api': 'test2', 'result': 1},
#  {'api': 'test3', 'result': 0}]

Should you want to get the maximum or the count of results, you'd just need to change the last line.

Here's the cleanest solution (if you are willing to use external libraries):

import pandas as pd
df = pd.DataFrame(data)
dfMin = df.groupby(by='api').min()

dfMin is a Pandas DataFrame with indices api and result the minimum value for each API.

yet another solution ..

result = {}

for d in data: result[ d['api']] = min(result.get(d['api'], d['result']), d['result'])

new_data = [ {'api' : k, 'result': v} for k, v in result.items() ]

print (new_data)

prints

#[{'api': 'test1', 'result': 0}, {'api': 'test2', 'result': 1}, {'api': 'test3', 'result': 0}]
Related