Найти совпадающие пары или цепочки людей в кадре данных, основываясь на равенстве условий - PullRequest
0 голосов
/ 18 января 2019

Я новичок в Python - сначала я хочу извиниться за мой довольно длинный вопрос и, возможно, очень уродливую программу, которую я написал, чтобы решить мою "проблему".

Проблема в следующем: представьте себе обмен домов на отдых. Люди могут обменять свои дома на отдых между собой. Человек 1 из «А» хочет перейти к «Б», человек 2 из «Б» хочет перейти к «А». Затем достигается матч или бартер, и оба больше не доступны для дальнейших матчей. Кроме того, должен быть рассмотрен случай, когда человек 1 хочет перейти от «A» к «B», человек 2 - из «B» в «C», и поэтому прямое сопоставление невозможно. Тем не менее, человек 3, который хочет перейти от «С» до «А». Тогда сделка в этой цепочке из 3 станет возможной. Люди могут также не указывать конкретное место назначения и, следовательно, могут отправиться в любое место, если кто-то захочет пойти к ним.

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

Набор данных выглядит следующим образом:

name, from, to, matching partner

person1, a, b,
person2, b, a,
person3, a, b,
person4, b, c,
person5, c, a,

После моего алгоритма:

name, from, to, matching partner

person1, a, b,person2
person2, b, a,person1
person3, a, b,person4person5
person4, b, c,person5person3
person5, c, a,person3person4

Вот как я реализовал это с моей программой на Python:

import pandas as pd
import numpy as np
import datetime as dt

data = pd.read_csv("myfile.csv", delimiter=";")

#Fill Missing Data in column "matchingpartner"
data.loc[:,"matchingpartner"] = data.loc[:,"matchingpartner"].fillna("no partner yet")

#Decide which Search Distance should be used (dict_5 for 5miles, dict_10 for 10miles, dict_9000 for 9000miles)
dict_postcode = dict_10

#Matching: Direkt 1:1 or Chain with 3 Persons
for x in range(0,data.shape[0]):
    for y in range(1,data.shape[0]): 
        for z in range(2,data.shape[0]): 
            if (x==y) | (x==z) | (y==z):
                continue
             #1 search for direct matching partners:
            if (    ((str(data.loc[x,"to"]) in dict_postcode[str(data.loc[y,"from"])]) or data.loc[x,"to"] =="dontcare") 
                and ((str(data.loc[y,"to"]) in dict_postcode[str(data.loc[x,"from"])]) or data.loc[y,"to"] =="dontcare")
                #only for persons without existing matching partner
                and (data.loc[x,"matchingpartner"] == "no partner yet") 
                and (data.loc[y,"matchingpartner"] == "no partner yet")):
                    data.loc[x,"matchingpartner"] = data.loc[y,"name"]
                    data.loc[y,"matchingpartner"] = data.loc[x,"name"]

            #2 If pairwise matching from #1 did not work, try to build matching chains of 3 or more
            elif (   str(data.loc[x,"to"]) in dict_postcode[str(data.loc[y,"from"])]  or data.loc[x,"to"] =="dontcare")     
                and (str(data.loc[y,"to"]) in dict_postcode[str(data.loc[z,"from"])]  or data.loc[y,"to"] =="dontcare")    
                and (str(data.loc[z,"to"]) in dict_postcode[str(data.loc[x,"from"])]  or data.loc[z,"to"] =="dontcare")   
                #only for persons without existing matching partner
                and (data.loc[x,"matchingpartner"] == "no partner yet")
                and (data.loc[y,"matchingpartner"] == "no partner yet")
                and (data.loc[z,"matchingpartner"] == "no partner yet")):
                    data.loc[x,"matchingpartner"] = data.loc[y,"name"] + data.loc[z,"name"]
                    data.loc[y,"matchingpartner"] = data.loc[z,"name"] + data.loc[x,"name"] 
                    data.loc[z,"matchingpartner"] = data.loc[x,"name"] +data.loc[y,"name"]

Он работает и выдает желаемый результат, НО он супер-медленный. Вопрос 1: знаете ли вы более элегантный и эффективный способ решить эту проблему? Время выполнения очень долго прямо сейчас. В моем наборе данных содержится около 400 000 наблюдений.

Вопрос 2. Прямо сейчас, из-за медленной скорости алгоритма, я допускаю только цепочки из 3 человек. Знаете ли вы, как я мог бы обобщить эту процедуру без циклов for, возможно, чтобы я мог определить определенный максимальный размер цепи (например, 5 человек, 1 хочет от a до b, 2 от b до c, 3 от c до d, 4 от d к е и 5 от е к а)?

Ответы [ 2 ]

0 голосов
/ 01 февраля 2019
| NAME       | home | destination | From       | Until      | howlong | HAS    | WANTS  |
|------------|------|-------------|------------|------------|---------|--------|--------|
| Carole     | A    | B           | 01.01.2020 | 01.02.2020 | 5days   | 2rooms | 3rooms |
| Irvin      | B    | A           | 01.01.2020 | 01.02.2020 | 5days   | 3rooms | 2rooms |
| Kelvin     | A    | B           | 02.06.2021 | 05.08.2021 | 9days   | 1room  | 2rooms |
| Stanley    | B    | C           | 02.05.2021 | 05.08.2021 | 9days   | 2rooms | 3rooms |
| Lazaro     | C    | A           | 20.05.2021 | 05.08.2021 | 9days   | 3rooms | 1room  |
| Yong       | A    | B           | 01.01.2020 | 03.05.2020 | 20days  | 1room  | 1room  |
| Florentino | B    | A           | 04.05.2020 | 08.08.2020 | 20days  | 1room  | 1room  |

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

  1. столбцы «От» и «До» в качестве первого условия должны перекрываться, лица могут участвовать только в матче, если периоды времени «От» до «До» перекрываются И дополнительно столбец howlong »должен совпадать для соответствующих партнеров. Я реализовал это в своем коде, добавив его в качестве еще одного оператора if в блоке кода в моем первоначальном вопросе:

    #Define function that checks if temporal matching between 2 people is possible     
    #Conditions:         
        # 1 The specified periods derived from start and end date must overlap             
        # ((s1 <= s2 & e2 <= e1) | (s2 <= s1 & e1 <= e2))         
        # 2 Start date of Person2 + desired duration of Person1 must be before the end      date of Person 2         
        # 3 Start date of Person1 + desired duration of Person2 must be before the end    date of Person 1
    def timechecker(start1,end1,duration1,start2,end2,duration2):
    if (((start1<=start2) and (end2<=end1)) or ((start2<=start1) and (end1<=end2)) and (start2+duration1<=end2) and (start1+duration2<=end1)):
    return True
    else:
    return False
    
    #Time Matching
    if...
    ...
            and ((timechecker(data.loc[x,'From'],data.loc[x,'Until'],data.loc[x,'duration'],data.loc[y,'From'],data.loc[y,'Until'],data.loc[y,'duration'])))
    
  2. В качестве второго условия столбцы «HAS» и «WANTS» следуют той же логике, что и столбцы «home» и «destination», поэтому я предполагаю, что код будет выглядеть следующим образом:

    graph.add_edges_from(np.argwhere(np.equal.outer(df['destination'], df['home']) and
      np.equal.outer(df['HAS'], df['WANTS'])))
    
  3. Можно ли установить предел того, насколько большими могут быть циклы? Возможно ли разрешить только циклы, например 3 или 5 человек?

0 голосов
/ 18 января 2019

Это может быть представлено как направленный граф , где узлы представляют людей, а ребра указывают, может ли человек переехать в дом другого человека.В общем случае вы можете использовать np.equal.outer для вычисления кандидатов на хост, а затем использовать networkx для создания соответствующего графика.Например:

           home destination
Carole        A           C
Irvin         A           D
Kelvin        C           A
Stanley       A           D
Lazaro        E           C
Yong          C           A
Florentino    B           E
Jose          C           E
Bettye        B           E
Clinton       A           D
Georgia       A           D
Lon           A           E
Ezra          C           D
Tim           A           E
Mae           A           B

# Create the graph by feeding the edges.
graph = nx.DiGraph()
graph.add_edges_from(np.argwhere(np.equal.outer(df['destination'], df['home'])))

Некоторые атрибуты результирующего графа:

graph.number_of_nodes()  # 15
graph.number_of_edges()  # 31
list(graph.edges())  # [(0, 2), (0, 5), (0, 7), (0, 12), (2, 0), (2, 1), (2, 3), (2, 9), (2, 10), (2, 11), (2, 13), (2, 14), (5, 0), (5, 1), (5, 3), (5, 9), (5, 10), (5, 11), (5, 13), (5, 14), (7, 4), (11, 4), (13, 4), (14, 6), (14, 8), (4, 2), (4, 5), (4, 7), (4, 12), (6, 4), (8, 4)]

Поиск возможных кандидатов

Теперь мы можем получить наборы возможных кандидатов путем поиска циклов вэтот график (цикл означает, что каждый, кто переезжает в другой дом, также сдаст их в аренду);мы можем использовать nx.simple_cycles:

Найти простые циклы (элементарные цепи) ориентированного графа.

A simple cycle или elementary circuit - это замкнутый путь, где нетузел появляется дважды.Две элементарные схемы различны, если они не являются циклическими перестановками друг друга.

list(nx.simple_cycles(graph))  # [[0, 7, 4, 5], [0, 7, 4, 2], [0, 5, 14, 8, 4, 2], [0, 5, 14, 6, 4, 2], [0, 5, 13, 4, 2], [0, 5, 11, 4, 2], [0, 5], [0, 2, 14, 8, 4, 5], [0, 2, 14, 6, 4, 5], [0, 2, 13, 4, 5], [0, 2, 11, 4, 5], [0, 2], [2, 14, 8, 4], [2, 14, 6, 4], [2, 13, 4], [2, 11, 4], [4, 7], [4, 5, 14, 8], [4, 5, 14, 6], [4, 5, 13], [4, 5, 11]]

Поиск наилучшего отображения

Жадный подход

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

# Greedy approach.
cycles = []
nodes = set()
for cycle in nx.simple_cycles(graph):
    if nodes.isdisjoint(cycle):
        cycles.append(cycle)
        nodes.update(cycle)

Это дает cycles == [[0, 7, 4, 5]], что, однако, не является оптимальным решением.

Оптимальное решение

Если это выполнимо с вычислительной точки зрения, оно может быть найдено методом перебораиспользование другого графа, представляющего совместимые (т.е. непересекающиеся) множества;мы добавляем пустой набор, чтобы мы могли использовать величину других наборов (длину соответствующих циклов) в качестве веса ребер:

import itertools as it

all_cycles = list(nx.simple_cycles(graph))
disjoint = np.zeros((len(all_cycles),)*2 , dtype=bool)
disjoint[np.triu_indices(disjoint.shape[0], k=1)] = list(map(
    lambda x: set.isdisjoint(*x),
    it.combinations(map(set, all_cycles), 2)
))

# Add the empty set as a starting point, so we can use cycle length as edge weight.
disjoint = np.concatenate([np.ones((1, disjoint.shape[1]), dtype=bool), disjoint], axis=0)
disjoint = np.concatenate([np.zeros((disjoint.shape[0], 1), dtype=bool), disjoint], axis=1)
lengths = np.fromiter(map(len, [[]] + all_cycles), dtype=int)
indices = np.argwhere(disjoint)

c_graph = nx.DiGraph()
c_graph.add_edges_from(zip(indices[:, 0], indices[:, 1], ({'weight': l} for l in lengths)))
best_combination = nx.dag_longest_path(c_graph, 'weight')

Результат - [0, 7, 13].Обратите внимание, что это включает пустой набор в качестве 0-го индекса, поэтому нам нужно сместить каждый индекс на -1.Следовательно, окончательное решение представляет собой соединение циклов [[0, 5], [2, 14, 8, 4]], которые имеют общую длину 6. Фактически это означает, что

  • Carole должен переключаться с Yong,
  • и Kelvin -> Mae -> Bettye -> Lazaro -> Kelvin.

Масштабируемость

Теперь все это вычислительно непросто, и для вашего примера 400 000 выборок это будет невозможно.Уже np.equal.outer имеет потребление памяти O (N ^ 2) и, следовательно, заканчивается ~ 20 ГиБ.Здесь вы также можете построить график постепенно, перебирая кадр данных строка за строкой.Тем не менее, результирующий график может быть настолько сложным, что все равно невозможно вычислить все циклы, например.

Другой вариант масштабирования этого для больших наборов данных - отказаться от гарантированной оптимальности путем случайного разбиения набора данных на подмножества.каждый из которых достаточно мал, чтобы алгоритм работал.Затем результаты получаются для каждого подмножества и объединяются в глобальный результат.Выполнив несколько таких случайных разбиений и сравнив отдельные результаты, можно еще улучшить решение.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...