Search code examples
python-3.xoptimizationlinear-programmingpulp

How to incorporate OR condition into pulp optimization problem?


I am trying to build a fantasy team where the goal is to maximize its total expected points based on expected points of individual players subject to certain constraints.
Here's the relevant part of my code:

players = [player for player in input_dict]
prices = {player: input_dict[player]['price'] for player in players}
player_teams = {player: input_dict[player]['team'] for player in players}
player_positions = {player: input_dict[player]['position'] for player in players}
points = {player: input_dict[player]['expected_points'] for player in players}
league_teams = list(set(team for team in player_teams.values()))

prob = LpProblem('Fantasy_team', LpMaximize)
player_var = LpVariable.dicts('Players', players, 0, 1, cat='Integer')

# maximize points
prob += lpSum([player_var[player] * points[player] for player in players])
# number of players to be selected
prob += lpSum([player_var[player] for player in players]) == 11 
# max budget
prob += lpSum([player_var[player] * prices[player] for player in players]) <= 100
# position constraints
prob += lpSum([player_var[player] for player in players if player_positions[player] == 'goalkeeper']) == 1
prob += lpSum([player_var[player] for player in players if player_positions[player] == 'defender']) >= 2
prob += lpSum([player_var[player] for player in players if player_positions[player] == 'defender']) <= 5
prob += lpSum([player_var[player] for player in players if player_positions[player] == 'midfielder']) >= 2
prob += lpSum([player_var[player] for player in players if player_positions[player] == 'midfielder']) <= 5
prob += lpSum([player_var[player] for player in players if player_positions[player] == 'forward']) >= 0
prob += lpSum([player_var[player] for player in players if player_positions[player] == 'forward']) <= 3

# max players from one team

for league_team in league_teams:
    team_players = []
    for player, lp_var in player_var.items():
        if player_teams[player] == league_team:
            team_players.append(lp_var)
    prob += lpSum(team_players) <= 3

However, what if I want to add one additional constraint: minimum defenders (or any other position) from one team = 2. In other words, at least two defenders have to come from the same team.

It seems to me that another decision variable would be required but I do not understand how this should be done.

EDIT:

I made some progress but I am now stuck with this error. Apparently Pulp is not happy with max function. Is there any way to get around this?

positions = ['defender',]
team_hires = LpVariable.dicts('count', (positions, league_teams), cat='Integer')

for league_team in league_teams:
    player_vars = []
    for player in player_var:
        if player_teams[player] == league_team:
            player_vars.append(player_var[player])
    team_hires['defender'].update({league_team: lpSum(player_vars)})

prob += max([team_hires['defender'][team] for team in league_teams]) >= 2

TypeError: '>' not supported between instances of 'LpAffineExpression' and 'LpAffineExpression'

Solution

  • I needed to re-post this answer as my previous one (deleted) is confusing and I think I led you astray...

    In order to write a constraint that forces at least 1 team to support the minimum number of hires per position, you need to be able to iterate over the teams (so you need a set of teams) and you need to be able to iterate over the positions that have a minimum-hires constraint. The example below does that with a "multiple choice" constraint that is a binary variable which implies a source team is supporting the min hires for a particular position. So if a particular team is "selected" by the solver to support the min hires, that variable is "1" and then the number of hires from that team must be greater than the min requirement (that is the first part of the constraint). The second part is that you must force the sum of that variable over all teams to be at least 1 to ensure that at least one team is forced to support the requirement. That is the second part of the constraint.

    Note the syntax on this could probably be a bit cleaner if you changed your decision variable to be triple-indexed [player, pos, source_team], but this still works fine!

    Data file (data.csv)

    player,price,team,position,expected_points
    bob,24,wolves,defender,2.1
    sam,23,wolves,defender,2.4
    tommy,20,wolves,forward,3.4
    bill,20,wolves,forward,3.6
    eddie,22,wolves,forward,3.1
    tim,23,bears,defender,2.2
    earl,23,bears,defender,1.0
    dorf,24,bears,forward,3.5
    bennie,30,bears,forward,3.6
    

    Model

    from pulp import *
    import pandas as pd
    
    df = pd.read_csv('data.csv')
    df = df.set_index('player')
    input_dict = df.to_dict('index')
    
    
    players = [player for player in input_dict]
    prices = {player: input_dict[player]['price'] for player in players}
    player_teams = {player: input_dict[player]['team'] for player in players}
    player_positions = {player: input_dict[player]['position'] for player in players}
    points = {player: input_dict[player]['expected_points'] for player in players}
    league_teams = list(set(team for team in player_teams.values()))
    
    # min numbers per position
    pos_mins = {'defender':2,
                'forward':2}
    
    # min numbers from single team by position
    team_pos_mins = {'defender':2}   # must hire at least 2 defenders from same source team
    positions = player_positions.values()
    pos_team_combos = {(pos, team) for pos in positions for team in league_teams}
    
    prob = LpProblem('Fantasy_team', LpMaximize)
    hire = LpVariable.dicts('Players', players, cat='Binary')   # this is binary decision...
    support_min_hires = LpVariable.dicts('team hires', pos_team_combos, cat='Binary')  # 1 if that team supports the min for position
    
    
    # maximize points
    prob += lpSum([hire[player] * points[player] for player in players])
    # number of players to be selected
    prob += lpSum([hire[player] for player in players]) == 4
    # max budget
    prob += lpSum([hire[player] * prices[player] for player in players]) <= 100
    # position constraints
    for pos in pos_mins:
        # this pattern could be replicated for all of your max/mins
        prob += lpSum(hire[player] for player in players if player_positions[player] == pos) >= pos_mins[pos]
    
    # hires by team constraint
    for pos in team_pos_mins:
        # the min number hired from team must be greater than the requirement, if that team is selected to support the min...
        for team in league_teams:
            prob += lpSum(hire[player] for player in players 
                    if player_positions[player] == pos
                    and player_teams[player] == team) >= support_min_hires[pos, team] * team_pos_mins[pos]
        # force at least one team to suppoprt the minimum hires
        prob += lpSum(support_min_hires[pos,team] for team in league_teams) >= 1
    
    #print(prob)
    
    status = prob.solve()
    print(status)
    for p in players:
        print(p, hire[p].value())
    
    # for QA...
    for pos in team_pos_mins:
        for team in league_teams:
            print(f'{team} covers min hires for {pos}:  {support_min_hires[pos,team].value()>0}')
    

    Result

    Result - Optimal solution found
    
    Objective value:                11.70000000
    Enumerated nodes:               0
    Total iterations:               0
    Time (CPU seconds):             0.00
    Time (Wallclock seconds):       0.00
    
    Option for printingOptions changed from normal to all
    Total time (CPU seconds):       0.00   (Wallclock seconds):       0.01
    
    1
    bob 1.0
    sam 1.0
    tommy 0.0
    bill 1.0
    eddie 0.0
    tim 0.0
    earl 0.0
    dorf 0.0
    bennie 1.0
    wolves covers min hires for defender:  True
    bears covers min hires for defender:  False
    [Finished in 0.9s]