0

I'm trying to model selection of 15 players for specific number of fixtures. My LpProblem consists of 2 binary variables player and fixture.

choices = LpVariable.dicts(
            "Choices", (fixtures, constraints["player"]), 0, 1, LpBinary)

I would like to limit the amount of player picked for set of fixtures using this constraint(which is bad - it counts all the pick not a number of players used):

prob += lpSum([choices[f][p] for f in fixtures for p in constraints["player"]]
                      ) <= player_count + len(fixtures) - 1, "Transfers limit"

I also set up a constraint to pick exactly 15 players for each fixture:

for fixture in fixtures:
            prob += lpSum([choices[fixture][p]
                           for p in constraints["player"]]) == player_count, str(fixture) + " Total of " + str(player_count) + " players"

My aim is to pick 15 and small amount of changes form fixture to fixture, but for some reason these constraints produce infeasible problem. For example if I search for fixtures = [0,1,2] the problem becomes feasible when I set transfer limit of 45 (15*3). I'm not sure how to formulate transfer limit constraint to achive my goal.

Example:

players = [1, 2, 3, 4, 5, 6]
fixtures = [1, 2, 3]

prob = LpProblem(
    "Fantasy football selection", LpMaximize)

choices = LpVariable.dicts(
    "Players", (fixtures, players), 0, 1, LpBinary)

# objective function
prob += lpSum([predict_score(f, p) * choices[f][p]
               for p in players for f in fixtures]), "Total predicted score"

# constraints
for f in fixtures:
    # total players for each fixture
    prob += lpSum([choices[f][p] for p in players]) == 2, ""
    if f != fixtures[0]:
        # max of 1 change between fixtures
        prob += lpSum([1 if choices[f-1][p] != choices[f]
                       [p] else 0 for p in players]) <= 2, ""

prob.solve()
print("Status: ", LpStatus[prob.status])
Rolfrider
  • 97
  • 1
  • 8
  • Can you provide a [mcve]? Also, can you provide a clearer explanation on what constraint you are struggling to implement - I read the text but am still not clear on what the missing constraint is intended to do. – kabdulla Dec 02 '19 at 18:42
  • Hey, I added a simple example, the constraint I'm struggling to implement is "max of 1 change between fixtures", in example it produce infeasible solution. I guess it's because variables depends on each other and it may be a non linear equation. – Rolfrider Dec 02 '19 at 19:22
  • You generally can't use `if` statements in the way you have indicated - but there will be a way to formulate the constraint you want... you'll just have to think about what operations/constraints are required on the binary variables. Also what is the `predict_score()` function? – kabdulla Dec 02 '19 at 20:09
  • it returns an integer indicating player score – Rolfrider Dec 02 '19 at 20:11
  • my attempt at answer posted below. Note that for a fully [mcve] you should really do any required imports and also provide a minimal implentation of any such functions. See for example dummy `predict_score()` I've put in my answer. – kabdulla Dec 02 '19 at 21:42

1 Answers1

1

I would recommend introducing additional binary variables which can be used to track whether a change is made between ficture f and fixture f-1. You can then apply constraints on how many changes are allowed.

In the example code below if you comment out the last constraint you will find that a higher objective is achieved, but at the cost of more changes. Note also that I've added a tiny penalty for having non-zero changes variables in the objective function - this is to force them to zero when changes are not made - this small penalty is not required for this method to work but might make it a little easier to see what's going on.

Without last constraint should get an objective value of 118, but with it only value of 109 is achieved.

from pulp import *
import random

players = [1, 2, 3, 4, 5]
fixtures = [1, 2, 3, 4]
random.seed(42)

score_dict ={(f, p):random.randint(0,20) for f in fixtures for p in players}

def predict_score(f,p):
    return score_dict[(f,p)]

prob = LpProblem(
    "Fantasy football selection", LpMaximize)

# Does fixture f include player p
choices = LpVariable.dicts(
    "choices", (fixtures, players), 0, 1, LpBinary)

changes = LpVariable.dicts(
    "changes", (fixtures[1:], players), 0, 1, LpBinary)

# objective function
prob += lpSum([predict_score(f, p) * choices[f][p]
               for p in players for f in fixtures]
              ) - lpSum([[changes[f][p] for f in fixtures[1:]] for p in players])/1.0e15, "Total predicted score"

# constraints
for f in fixtures:
    # Two players for each fixture
    prob += lpSum([choices[f][p] for p in players]) == 2, ""

    if f != fixtures[0]:
        for p in players:
            # Assign change constraints, force to one if player
            # is subbed in or out
            prob += changes[f][p] >= (choices[f][p] - choices[f-1][p])
            prob += changes[f][p] >= (choices[f-1][p] - choices[f][p])

        # Enforce only one sub-in + one sub-out per fixture (i.e. at most one change)
        # prob += lpSum([changes[f][p] for p in players]) <= 2

prob.solve()
print("Status: ", LpStatus[prob.status])

print("Objective Value: ", prob.objective.value())

choices_soln = [[choices[f][p].value() for p in players] for f in fixtures]
print("choices_soln")
print(choices_soln)

changes_soln = [[changes[f][p].value() for p in players] for f in fixtures[1:]]
print("changes_soln")
print(changes_soln)
kabdulla
  • 5,199
  • 3
  • 17
  • 30