根据条件的同等性在数据框中找到匹配的人对或人链

时间:2019-01-17 22:54:17

标签: python python-3.x dataframe

我是Python的初学者-首先,我想为我相当长的问题以及为解决“问题”而编写的可能非常丑陋的程序道歉。

问题出在以下方面:想象一下为度假而交换房屋。人们可以互相交换房子休假。来自“ A”的人员1想要转到“ B”,来自“ B”的人员2想要转到“ A”。然后达到匹配或易货交易,并且两者不再可用于进一步的匹配。另外,应该覆盖这种情况,即人1要从“ A”转到“ B”,人2要从“ B”转到“ C”,因此不可能直接匹配。但是,想要从“ C”转到“ A”的人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到e和5从e到a)?

2 个答案:

答案 0 :(得分:0)

这可以表示为directed graph,其中节点表示人,边缘表示一个人是否可以搬到另一个人的家中。通常,您可以使用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

  

找到有向图的简单循环(基本电路)。

     

simple cycleelementary 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 GiB。在这里,您还可以通过逐行遍历数据帧来逐步构建图形。尽管如此,结果图可能是如此复杂,以至于例如计算所有周期仍然不可行。

将其缩放到更大数据集的另一种方法是通过将数据集随机分成子集而放弃保证的最优性,每个子集都足够小,算法无法工作。然后,获得每个子集的结果并将其合并为全局结果。通过执行多次这样的随机分割并比较各个结果,仍然可以改善解决方案。

代替拆分数据集,人们还可以采样子集,求解子集,然后将那些尚未匹配的人员放回池中。同样,在拆分方法中,您可以在拆分一轮后将所有不匹配的人员聚集在一起,然后反复进行。

答案 1 :(得分:0)

| 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. “ From”和“ Until”列是第一个条件,必须重叠,如果“ From”至“ Until”的时间段重叠并且“匹配伙伴的“多长时间”列必须相等。 我通过在我的最初问题中的代码块内将其添加为另一个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个人?