Найти оптимальную комбинацию значений словаря списков (возможно, с помощью pandas)

#python #pandas #dataframe #dictionary #feature-extraction

#python #pandas #фрейм данных #словарь #функция-извлечение

Вопрос:

Следующая проблема скорее связана с алгоритмом, чем с проблемой кода.

Представьте, что у меня есть структура данных следующим образом:

 cities = {'price'   : ['malaga','berlin'],
          'food'    : ['milano','barcelona'],
          'shopping': ['milano','barcelona'],
          'weather' : ['barcelona','paris','lisabon','milano'],
          'museums' : ['malaga','berlin','lisabon'],
          'cafes'   : ['paris','roma','lisabon'],
          'kids'    : ['milano','barcelona','paris','roma']}
  

Есть куча характеристик, которые можно найти в разных городах.
Каково минимальное количество городов, которое охватывает все эти характеристики. Т.Е. минимальное количество городов, которые я должен посетить, чтобы получить все преимущества.

До сих пор я начал использовать счетчик

 totals=[]
for key in cities.keys():
    totals.append(cities[key])
totals_together = [city for cities in totals for city in cities]
totals_together
myCounter = Counter(totals_together)
print(myCounter.most_common())
  

РЕЗУЛЬТАТ пока:

 [('milano', 4), ('barcelona', 4), ('paris', 3), ('lisabon', 3), ('malaga', 2), ('berlin', 2), ('roma', 2)]
  

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

Должен быть лучший способ.

Я даже думаю о pandas, но не вижу, что pandas принесет для этой проблемы. Мне это кажется довольно распространенной проблемой.

ПРИМЕЧАНИЕ: я даже не ищу код как таковой, просто идеи о том, как решить проблему, более чем приветствуются.

ПРИМЕЧАНИЕ 2: имейте в виду, что может быть один или несколько городов со всеми характеристиками, но может быть случай (обычно), когда нет ни одного города со всеми характеристиками.

ИТАК, РЕЗУЛЬТАТ, КОТОРЫЙ Я ИЩУ: [‘milano’,’lisabon’] предполагая, что эта комбинация охватывает все характеристики.

Комментарии:

1. (если я правильно понял проблему) Как насчет создания таблицы из этой структуры данных с такими характеристиками, как индекс и города, в виде столбцов, заполненных 1 и 0, а затем взять сумму столбцов и отсортировать эти столбцы на основе суммы? Я думаю, что это включает 1 во время обхода и сортировки в конце

2. Мне кажется, что это даст тот же результат, что и с Counter . Вы получаете список с лучшими городами для посещения, упорядоченный по «характеристикам», но все же я не вижу методов pandas, которые позволили бы мне лучше получить результат, создав (я думаю) так называемую таблицу «истинности». т. е. Таблицу с индексными характеристиками, столбцами cities и True /False, если характеристика присутствует.

Ответ №1:

Один из способов продвижения вперед — создать все комбинации (используя itertools), а затем просмотреть их и подсчитать действия, которые дают вам эти комбинации. Вы можете остановиться, как только найдете комбинацию, которая дает вам все действия.

Использование pandas дает вам простой способ рассчитать количество возможных действий в каждом городе. Я уверен, что вы также могли бы обойтись без.

 import pandas as pd
import itertools

travel = {'price':['malaga','berlin'],
          'food':['milano','barcelona'],
          'shopping':['milano','barcelona'],
          'weather':['barcelona','paris','lisabon','milano'],
          'museums':['malaga','berlin','lisabon'],
          'cafes':['paris','roma','lisabon'],
         'kids':['milano','barcelona','paris','roma']}

# very ugly way to convert the travel into a data frame
# first we create a list of all cities
c = []
for activity in travel.keys():
    for city in travel[activity]:
        c.append(city)
c = set(c)    
a = list(travel.keys())
df = pd.DataFrame(index=pd.Index(c, name='city'), 
                  columns=pd.Index(a, name='activity'))

# then we set all city/activity crosspoints to True
for activity in travel.keys():
    for city in travel[activity]:
        df.loc[city, activity] = True
# and fill the rest with False
df = df.fillna(False)

# how many activities do we want to do?
all_activities = len(df.columns)

# let's store the results in a dictionary

results = {}
for combo_len in range(1, len(df.index)):
    combos = list(itertools.combinations(df.index, combo_len))
    for c in combos:
        # print(f"Combo: {c}")
        activity_count = df.query(f"city in {c}").any().sum()
        results[c] = activity_count
        if activity_count == all_activities:
            print(f"{c}: {max_activities}")
            break
    else:
        continue
    break
  

Код либо остановится, когда будут опробованы все комбинации, либо когда он найдет комбинацию, которая включает в себя все действия.

Первая возможная комбинация, которую он предлагает, это:

 ('barcelona', 'paris', 'berlin'): 7
  

Ответ №2:

На самом деле это похоже на задачу о сумме комбинаций, но вместо целевой суммы вам нужна длина объединения множеств.

Сначала измените свой cities :

 from collections import defaultdict

d = defaultdict(set)

for k, v in cities.items():
    for city in v:
        d[city].add(k)

print (d)

#defaultdict(<class 'set'>, {'malaga': {'price', 'museums'}, 'berlin': {'price', 'museums'}, ...})
  

Теперь вы можете применить логику суммы комбинаций с уникальными значениями, но len вместо этого использовать:

 def find_cities(candidates, target):
    ans = set()
    def dfs(cur, start):
        if cur:
            num = len(set.union(*(d[i] for i in cur)))
        else:
            num = 0
        if num == target:
            ans.add(tuple(sorted(cur)))
            return
        for i in range(start, len(candidates)):
            cur.append(candidates[i])
            dfs(cur, i 1)
            cur.pop()
    dfs([], 0)
    return sorted(ans, key=len)

res = find_cities(list(d.keys()), len(cities))

print (res)

#[('malaga', 'milano', 'paris'), ('barcelona', 'malaga', 'paris'),
# ('barcelona', 'berlin', 'paris'), ('barcelona', 'berlin', 'lisabon'),
# ('berlin', 'milano', 'paris'), ('berlin', 'milano', 'roma'),
# ('barcelona', 'berlin', 'roma'), ('lisabon', 'malaga', 'milano'),
# ('barcelona', 'malaga', 'roma'), ('malaga', 'milano', 'roma'),
# ('barcelona', 'lisabon', 'malaga'), ('berlin', 'lisabon', 'milano'),
# ('barcelona', 'malaga', 'milano', 'paris'), ('berlin', 'malaga', 'milano', 'roma'),
# ('berlin', 'malaga', 'milano', 'paris'), ('barcelona', 'lisabon', 'malaga', 'milano'),
# ('berlin', 'lisabon', 'malaga', 'milano'), ('barcelona', 'berlin', 'lisabon', 'milano'),
# ('barcelona', 'berlin', 'malaga', 'roma'), ('barcelona', 'malaga', 'milano', 'roma'),
# ('barcelona', 'berlin', 'milano', 'roma'), ('barcelona', 'berlin', 'malaga', 'paris'),
# ('barcelona', 'berlin', 'milano', 'paris'), ('barcelona', 'berlin', 'lisabon', 'malaga'),
# ('barcelona', 'berlin', 'malaga', 'milano', 'roma'),
# ('barcelona', 'berlin', 'lisabon', 'malaga', 'milano'),
# ('barcelona', 'berlin', 'malaga', 'milano', 'paris')]