Python Pandas自我加入合并笛卡尔积,产生所有组合和总和

时间:2016-07-28 12:31:30

标签: python python-2.7 pandas linear-programming

我是Python的新手,似乎它具有很大的灵活性,并且比传统的RDBMS系统更快。

开展一个非常简单的过程来创建随机幻想团队。我来自RDBMS背景(Oracle SQL),这似乎不是这种数据处理的最佳选择。

我使用从csv文件读取的pandas创建了一个数据框,现在有一个包含两列的简单数据框 - Player,Salary:

`                    Name  Salary
0              Jason Day   11700
1         Dustin Johnson   11600
2           Rory McIlroy   11400
3          Jordan Spieth   11100
4         Henrik Stenson   10500
5         Phil Mickelson   10200
6            Justin Rose    9800
7             Adam Scott    9600
8          Sergio Garcia    9400
9          Rickie Fowler    9200`

我想通过python(pandas)做的是产生6个玩家的所有组合,其工资在45000-50000之间。

在查找python选项时,我发现itertools组合很有趣,但它会产生大量的组合列表,而不会过滤工资总和。

在传统的SQL中,我会做一个大规模的合并笛卡尔加入与SUM,但后来我让玩家在不同的地方..

如A,B,C则C,B,A ..

我的传统SQL不能很好地运行,如下所示:

` SELECT distinct
ONE.name AS "1", 
  TWO.name AS "2",
    THREE.name AS "3",
      FOUR.name AS "4", 
  FIVE.name AS "5", 
  SIX.name AS "6",
   sum(one.salary + two.salary + three.salary + four.salary + five.salary + six.salary) as salary
  FROM 
  nl.pgachamp2 ONE,nl.pgachamp2 TWO,nl.pgachamp2 THREE, nl.pgachamp2 FOUR,nl.pgachamp2 FIVE,nl.pgachamp2 SIX
 where ONE.name != TWO.name
 and ONE.name != THREE.name
 and one.name != four.name
 and one.name != five.name
 and TWO.name != THREE.name
 and TWO.name != four.name
 and two.name != five.name
 and TWO.name != six.name
 and THREE.name != four.name
 and THREE.name != five.name
 and three.name != six.name
 and five.name != six.name
 and four.name != six.name
 and four.name != five.name
 and one.name != six.name
 group by ONE.name, TWO.name, THREE.name, FOUR.name, FIVE.name, SIX.name`

有没有办法在Pandas / Python中执行此操作?

任何可以指出的文档都会很棒!

3 个答案:

答案 0 :(得分:2)

正如评论中所提到的,这是一个约束满足问题。它有一个组合部分,但由于你没有定义任何目标来最小化或最大化,所以它还不是一个优化问题。您可以通过多种方式解决这些问题:您可以尝试像piRSquared这样的暴力攻击或使用像PM 2Ring这样的启发式算法。我将提出一个0-1线性编程的解决方案,并使用PuLP库来建模并解决问题。

from pulp import *
import pandas as pd
df = df.set_index('Name')
feasible_solutions = []

为了对问题进行建模,首先需要定义决策变量。这里,决策变量将是每个玩家的指示变量:如果选择该玩家则为1,否则为0。以下是您在PuLP中的表现:

players = LpVariable.dicts('player', df.index.tolist(), 0, 1, LpInteger)

接下来,您创建了一个问题:

prob = pulp.LpProblem('Team Selection', pulp.LpMinimize)

正如我前面提到的,你的问题没有说明任何目标。您只想创建所有可能的团队。因此,我们将定义一个任意的目标函数(我将再次回到这个任意函数)。

prob += 0

您主要有两个约束:

1)球队将有5名球员:

prob += lpSum([players[player] for player in players]) == 5

请记住,玩家字典存储我们的决策变量。 players[player]是1(如果该玩家在团队中)或0(否则)。因此,如果对所有这些求和,则结果应等于5.

2)总薪水应在45k到50k之间。

prob += lpSum([players[player] * df.at[player, 'Salary'] 
                                        for player in players]) <= 50000
prob += lpSum([players[player] * df.at[player, 'Salary'] 
                                        for player in players]) >= 45000

这类似于第一个约束。在这里,我们不算数,而是总结工资(当玩家在团队中时,值将为1,因此它将乘以相应的工资。否则,该值将为零,乘法也将为零)

主要建模在这里完成。如果您致电prob.solve(),它会找到满足这些限制条件的 解决方案。通常,在优化问题中,我们提供目标函数并尝试最大化或最小化。例如,假设您有玩家技能的分数。您的预算有限,您无法继续选择前5名球员。因此,在我们陈述prob += 0的部分中,您可以定义目标函数以最大化总技能分数。但那不是你想要的,所以让我们继续。

找到解决方案后,您可以为问题添加另一个约束,说明下一个解决方案应该与此不同,您可以生成所有解决方案。

while prob.solve() == 1:
    current_solution = [player for player in players if value(players[player])]
    feasible_solutions.append(current_solution)
    prob += lpSum([players[player] for player in current_solution]) <= 4

prob.solve()是解决问题的方法。根据结果​​,它返回一个整数。如果找到最优解,则结果为1.对于不可行或无界解,存在不同的代码。因此,只要我们能够找到新的解决方案,我们就会继续循环。

在循环中,我们首先将当前解决方案附加到我们的feasible_solutions列表中。然后,我们添加另一个约束:对于这5个玩家,变量的总和不能超过4(最大值5,如果是5,我们知道这是相同的解决方案)。

如果你运行它,你将获得与piRSquared相同的结果。

enter image description here

那么,这有什么好处呢?

我们使用整数/二进制线性编程的主要原因是组合数量的增长非常快。这称为combinatorial explosion。看看可能的团队数量(没有任何约束):

from scipy.misc import comb
comb(10, 5)
Out: 252.0

comb(20, 5)
Out: 15504.0

comb(50, 5)
Out: 2118760.0

comb(100, 5)
Out: 75287520.0

评估所有组合几乎是不可能的。

当然,当您想要列出满足这些约束条件的所有组合时,您仍然存在这种风险。如果满足约束的组合数量很大,则计算将花费大量时间。你无法避免这种情况。但是,如果该子集很小或者仍然很大,但是您正在评估该集合上的函数,则会更好。

例如,请考虑以下DataFrame:

import numpy as np
np.random.seed(0)
df = pd.DataFrame({'Name': ['player' + str(i).zfill(3) for i in range(100)],
                   'Salary': np.random.randint(0, 9600, 100)})

75287520解决方案中的268个满足薪资限制。我的电脑花了44秒才列出它们。使用暴力破解需要数小时才能找到它们(更新:需要8小时21分钟)。

默认情况下,PuLP使用开源解算器Cbc。您可以使用PuLP的其他开源/商业替代解算器。商业化的通常比预期的更快(虽然它们非常昂贵)。

另一种选择是约束编程,正如我在评论中提到的那样。对于这些问题,您可以找到许多通过约束编程来减少搜索空间的智能方法。我对整数编程感到满意,所以我展示了一个基于该模型的模型,但我应该注意,约束编程对此可能更好。

答案 1 :(得分:1)

我为6的组合运行了这个,发现没有满意的团队。我用了5来代替。

这应该可以帮助你:

from itertools import combinations
import pandas as pd


s = df.set_index('Name').squeeze()
combos = pd.DataFrame([c for c in combinations(s.index, 5)])
combo_salary = combos.apply(lambda x: s.ix[x].sum(), axis=1)
combos[(combo_salary >= 45000) & (combo_salary <= 50000)]

enter image description here

答案 2 :(得分:1)

这是使用简单算法的非熊猫解决方案。它从按工资排序的玩家列表中递归生成组合。这可以让它跳过产生超过工资帽的组合。

正如piRSquared所提到的那样,没有6支球队属于问题中规定的工资限制范围,因此我选择限制来产生少数球队。

#!/usr/bin/env python3

''' Limited combinations

    Generate combinations of players whose combined salaries fall within given limits

    See http://stackoverflow.com/q/38636460/4014959

    Written by PM 2Ring 2016.07.28
'''

data = '''\
0              Jason Day   11700
1         Dustin Johnson   11600
2           Rory McIlroy   11400
3          Jordan Spieth   11100
4         Henrik Stenson   10500
5         Phil Mickelson   10200
6            Justin Rose    9800
7             Adam Scott    9600
8          Sergio Garcia    9400
9          Rickie Fowler    9200
'''

data = [s.split() for s in data.splitlines()]
all_players = [(' '.join(u[1:-1]), int(u[-1])) for u in data]
all_players.sort(key=lambda t: t[1])
for i, row in enumerate(all_players):
    print(i, row)
print('- '*40)

def choose_teams(free, num, team=(), value=0):
    num -= 1
    for i, p in enumerate(free):
        salary = all_players[p][1]
        newvalue = value + salary
        if newvalue <= hi:
            newteam = team + (p,)
            if num == 0:
                if newvalue >= lo:
                    yield newteam, newvalue
            else:
                yield from choose_teams(free[i+1:], num, newteam, newvalue)
        else:
            break

#Salary limits
lo, hi = 55000, 60500

#Indices of players that can be chosen for a team 
free = tuple(range(len(all_players)))

for i, (t, s) in enumerate(choose_teams(free, 6), 1):
    team = [all_players[p] for p in t]
    names, sals = zip(*team)
    assert sum(sals) == s
    print(i, t, names, s)

<强>输出

0 ('Rickie Fowler', 9200)
1 ('Sergio Garcia', 9400)
2 ('Adam Scott', 9600)
3 ('Justin Rose', 9800)
4 ('Phil Mickelson', 10200)
5 ('Henrik Stenson', 10500)
6 ('Jordan Spieth', 11100)
7 ('Rory McIlroy', 11400)
8 ('Dustin Johnson', 11600)
9 ('Jason Day', 11700)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
1 (0, 1, 2, 3, 4, 5) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Phil Mickelson', 'Henrik Stenson') 58700
2 (0, 1, 2, 3, 4, 6) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Phil Mickelson', 'Jordan Spieth') 59300
3 (0, 1, 2, 3, 4, 7) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Phil Mickelson', 'Rory McIlroy') 59600
4 (0, 1, 2, 3, 4, 8) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Phil Mickelson', 'Dustin Johnson') 59800
5 (0, 1, 2, 3, 4, 9) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Phil Mickelson', 'Jason Day') 59900
6 (0, 1, 2, 3, 5, 6) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Henrik Stenson', 'Jordan Spieth') 59600
7 (0, 1, 2, 3, 5, 7) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Henrik Stenson', 'Rory McIlroy') 59900
8 (0, 1, 2, 3, 5, 8) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Henrik Stenson', 'Dustin Johnson') 60100
9 (0, 1, 2, 3, 5, 9) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Henrik Stenson', 'Jason Day') 60200
10 (0, 1, 2, 3, 6, 7) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Justin Rose', 'Jordan Spieth', 'Rory McIlroy') 60500
11 (0, 1, 2, 4, 5, 6) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Phil Mickelson', 'Henrik Stenson', 'Jordan Spieth') 60000
12 (0, 1, 2, 4, 5, 7) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Phil Mickelson', 'Henrik Stenson', 'Rory McIlroy') 60300
13 (0, 1, 2, 4, 5, 8) ('Rickie Fowler', 'Sergio Garcia', 'Adam Scott', 'Phil Mickelson', 'Henrik Stenson', 'Dustin Johnson') 60500
14 (0, 1, 3, 4, 5, 6) ('Rickie Fowler', 'Sergio Garcia', 'Justin Rose', 'Phil Mickelson', 'Henrik Stenson', 'Jordan Spieth') 60200
15 (0, 1, 3, 4, 5, 7) ('Rickie Fowler', 'Sergio Garcia', 'Justin Rose', 'Phil Mickelson', 'Henrik Stenson', 'Rory McIlroy') 60500
16 (0, 2, 3, 4, 5, 6) ('Rickie Fowler', 'Adam Scott', 'Justin Rose', 'Phil Mickelson', 'Henrik Stenson', 'Jordan Spieth') 60400

如果您使用的旧版Python不支持yield from语法,则可以替换

yield from choose_teams(free[i+1:], num, newteam, newvalue)

for t, v in choose_teams(free[i+1:], num, newteam, newvalue):
    yield t, v