Search code examples
pythonoptimizationpulp

Python PULP optimization - Limiting total number of one variable chosen


This is a follow-up to this question: Python PULP optimization - LpVariable across THREE factors?

I've altered the code setup (for clarity) as follows. The goal is to maximize the number of points scored across 3 games. Each of the listed players has a different points projection for each of the 3 games, at each of 3 different positions.

import pulp

# SETS

players = {'Jordan', 'Abdul Jabbar', 'Johnson', 'James', 'Bird', 'Bryant', 'Embiid', 'Curry', 'Wade', 'Bosh', 'Duncan'}
positions = {'FWD', 'CTR', 'GRD'}
games = {4,5,6}

# the full x-set
player_pos_game = {(player, pos, game)
                    for player in players
                    for pos in positions
                    for game in games}

# Maximize the number of total points across the 3 games
prob = pulp.LpProblem("game_assignments", sense=pulp.LpMaximize)

# Binary variable of player/position/game combos
assign = pulp.LpVariable.dicts("assign", player_pos_game, cat=pulp.LpBinary)

The code currently creates a 3-variable X-Set across a handful of basketball players, 3 different positions, and 3 different games. For every player/position/game combination, there is a different projected points total (not listed in the code snippet) that is being maximized.

What I am now trying to do is to create a limit of 5 total players that can be chosen across the 3 games. We can only choose 5 players from the players list. Each of the three games can have different combinations of those 5 players to fill each of the 3 positions, but only 5 players can be chosen total.

Any guidance on this last step would be very much appreciated. From looking at other questions on StackOverflow this feels vaguely like a Big M constraint sort of issue, but I am far from an optimization expert. Thanks!


Solution

  • You'll need another variable to enforce the consistency across the games. As is now, if you sum up across the positions or teams or both, you could get differing players.

    Anyhow, you can introduce another variable that just captures the players selected on the team and link that to the other...

    The "effectiveness" below is similar to what you want, but not indexed by game. That should be an easy mod.

    Code

    import pulp
    from random import random
    
    
    # SETS
    
    players = {'Jordan', 'Abdul Jabar', 'Johnson', 'Manny', 'Moe', 'Jack'}
    positions = {'FWD', 'CTR', 'GRD'}
    games = {4,5,6}
    M_games = len(games)
    
    # the full x-set
    player_pos_game = {(player, pos, game)
                        for player in players
                        for pos in positions
                        for game in games}
    
    # example of a convenience set you'll likely need for constraints...
    # you'll likely want other to make your constraints
    player_pos = {(player, pos) for player in players for pos in positions}
    
    # some fake performance data to make the model work
    effectiveness = {(player, pos): random() for player, pos in player_pos}
    
    
    prob = pulp.LpProblem("game_assignments", sense=pulp.LpMaximize)
    
    # VARS
    # assign player to position in game
    assign = pulp.LpVariable.dicts("assign", player_pos_game, cat=pulp.LpBinary)
    
    # put player on team
    team = pulp.LpVariable.dicts("team", players, cat=pulp.LpBinary)
    
    # CONSTRAINTS
    # set team size
    prob += pulp.lpSum(team) == 3
    
    # link assignment to team membership *for each player*.
    # the sum of all assignments is limited by number of games, or zero if not on team...
    for player in players:
        prob += pulp.lpSum(assign[player, pos, game] 
            for pos in positions for game in games) <= team[player] * M_games
    
    
    # now we need to prevent 1 player for playing multiple positions in each game...
    for player in players:
        for game in games:
            prob += pulp.lpSum(assign[player, pos, game]
                for pos in positions) <= 1
    
    # and we need to ensure that each position is only filled once in each game
    # so this is a "for each" position and game...
    for pos in positions:
        for game in games:
            prob += pulp.lpSum(assign[player, pos, game]
                for player in players) <= 1
    
    # OBJ:  maximize total effectiveness
    prob += pulp.lpSum(effectiveness[player, pos] * assign[player, pos, game]
                for player, pos, game in player_pos_game)
    
    prob.solve()
    
    for key in sorted(player_pos_game, key=lambda x: (x[2], x[0], x[1])):
        if (assign[key].value()): print(f'assign {key[0]} to {key[1]} in game {key[2]}')
    

    Output

    Result - Optimal solution found
    
    Objective value:                7.59249721
    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
    
    assign Abdul Jabar to CTR in game 4
    assign Jordan to GRD in game 4
    assign Manny to FWD in game 4
    assign Abdul Jabar to CTR in game 5
    assign Jordan to GRD in game 5
    assign Manny to FWD in game 5
    assign Abdul Jabar to CTR in game 6
    assign Jordan to GRD in game 6
    assign Manny to FWD in game 6