Python Pandas 自连接以合并笛卡尔积以生成所有组合和总和

Python Pandas self join for merge cartesian product to produce all combinations and sum

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

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

我使用从 csv 文件读取的 pandas 制作了一个数据框,现在有一个包含两列的简单数据框 -- 球员,薪水:

`                    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) 做的是产生薪水在一定数额 45000 到 50000 之间的 6 名球员的所有组合。

在查找 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有没有办法做到这一点?

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

我运行这是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)]

这是一个使用简单算法的非 Pandas 解决方案。它从按薪水排序的球员列表中递归生成组合。这让它可以跳过生成超过工资帽的组合。

正如 piRSquared 提到的那样,没有 6 人的团队落在问题中规定的工资限制范围内,因此我选择限制来生成少量团队。

#!/usr/bin/env python3

''' Limited combinations

    Generate combinations of players whose combined salaries fall within given limits

    See 

    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

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

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

如评论中所述,这是一个约束满足问题。它有一个组合部分,但由于您没有定义 objectives 来最小化或最大化,所以它不是优化问题(目前)。您可以通过多种方式解决此问题:您可以尝试像 piRSquared 这样的蛮力或使用像 PM 2Ring 这样的启发式算法。我将提出一个 0-1 线性规划的解决方案,并使用 PuLP 库来建模和解决问题。

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

为了对问题建模,首先您需要定义决策变量。在这里,决策变量将是每个玩家的指示变量:如果该玩家 selected,它将为 1,否则为 0。以下是您在 PuLP 中的操作方法:

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

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

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

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

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,乘以相应的薪水,否则数值为0,乘积也为0) .

主要的建模就到这里了。如果您调用 prob.solve(),它将找到 a 满足这些约束的解决方案。通常,在优化问题中,我们提供一个 objective 函数并尝试最大化或最小化它。例如,假设您有玩家技能的分数。您的预算有限,您无法继续 select 前 5 名玩家。因此,在我们声明 prob += 0 的部分,您可以定义一个 objective 函数来最大化总技能分数。但这不是您想要的,所以让我们继续。

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

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()是解决问题的方法。根据结果​​,它 returns 是一个整数。如果找到最优解,则结果为 1。对于不可行或无界的解,有不同的代码。所以只要我们能找到新的解决方案,我们就继续循环。

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

如果你运行这个,你会得到与piRSquared相同的结果。

那么,这样做的好处是什么?

我们使用 integer/binary 线性规划的主要原因是组合的数量增长得非常快。这叫做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。还有其他开放的 source/commercial 替代求解器可以与 PuLP 一起使用。商业的通常比预期的更快(虽然它们非常昂贵)。

另一种选择是我在评论中提到的约束编程。对于这类问题,您可以找到许多聪明的方法来使用约束规划来减少搜索 space。我对整数规划很满意,所以我展示了一个基于它的模型,但我应该注意到约束规划可能对此更好。