Introduction
A few weeks ago, I came across this video from the Hudl Statsbomb 2024 conference, I encourage you to watch it as I found it fascinating. Pablo Galaz Cares (who works for Club Universidad de Chile) details a quantitative approach to multi-season squad planning, balancing competitive success and financial sustainability.
This is done by creating mathematical functions to represent player quality (using OBV modelling) and transfer value estimation, constraints are then applied for budget, league regulation, squad size, among others. The ‘problem’ is then solved to maximise player quality in the team whilst staying within the boundaries of the constraints.
This optimization approach shares clear parallels with Fantasy Football solver methods, where similar mathematical techniques are used to maximize team value within strict budget and squad composition rules. This similarity suggests that Football Manager 2024 could serve as an ideal testing ground for implementing these squad planning strategies. The game’s player attribute ratings are a much easier way to estimate player quality rather than trying to use real data to establish metrics, the game also has very realistic financial systems.
This post showcases my project building a squad-planning optimisation solver, using the Football Manager 2024 game as a sandbox. I chose to use Crystal Palace as my team, I think they represent the only team in the Premier League who are truly midtable.
Data Pre-processing
The first step was getting data from Football Manager by creating a view that showed all player attributes, player features like age, and contract information.
There were two main challenges to sort out: player positions and transfer values. For positions, I initially tried using simple if statements with regex to code positions from the string column, but this didn't work well since FM lists defensive positions first and attacking ones last in the text (for example, a player could have D/WB/M(R), AM(RC) as a position, then the if statements recognise him just as a right-sided defender and ignore the other positions). Instead, I created a dictionary for each player that stores all their possible positions (so, going back to previous example, they would have a dictionary showing they could be a right-back, right-wingback, right-midfielder and so on).
Transfer values were tricky too. Most players had a scouting range for their value (e.g. £20M-£40M) - I used a value in the higher end of the range for transfer targets and the lower end for my own players. For players listed as "Not for Sale," I had to estimate their value using a basic linear regression based on their wage. While I could have made this more complex by including features like league and club reputation, I kept it simple for this first version.
Another small thing to note in my approach is that I used a csv for my own players rather than the standard html export from FM. This is because I wanted to assign my own players a fixed position rather than using the dictionary approach from before, therefore I pasted the html into a csv file and added a column to hardcode positions.
The Optimisation System
The optimization system combines player quality scores with constraints. For quality scoring, I created position-specific weightings for player attributes. For example, wingbacks were scored heavily on Stamina, Work Rate, and Acceleration, with less weight on Passing and Balance. Whilst strikers had higher weightings for Anticipation, Composure, and Finishing, while attributes like Balance and Teamwork carried less weight.
These position-based scores were then combined with a club "DNA" score that reflected Palace's pressing style under Oliver Glasner. This “DNA” score prioritized attributes like Teamwork, Work Rate and Anticipation, and made up 20% of the final score, with the position-specific score accounting for the other 80%.
I added two types of multipliers to adjust these base scores. Age multipliers pushed the solver toward younger talent - players over 32 had their scores cut by 25%, while under-21s received a 7% boost. Players in between that range had less drastic multipliers with no multiplier at ages between 25 and 26, the values for the percentage increase/decrease were arbitrary and adjusted via trial and error. There were also performance multipliers that were based on average ratings: every 0.1 above 6.8 added 1% to the player's score (capped at 7.5), while ratings below 6.8 were penalized more heavily with a 5% reduction per 0.1 decrease.
Then came the various constraints for the problem, with financial restrictions being the most critical. I manually input the available transfer and wage budgets to set hard spending limits for the solver. This meant any proposed squad changes had to keep both transfer fees and total wages within these boundaries.
Squad composition rules were also key. The solver needed to maintain a total squad size of 25 players while meeting specific positional quotas - like having exactly 3 goalkeepers. This led to an interesting challenge with the third-choice keeper position. In real football, this player rarely sees game time and their quality has minimal impact on team performance - they're often a homegrown player or someone content with a backup role. However, the solver, working purely on player quality scores would always try to replace this keeper with a higher-rated option since it couldn't understand the practical realities of the role. To address this, I had to add a specific constraint to "protect" the third-choice keeper position, by locking the worst quality goalkeeper, preventing the solver from suggesting unnecessary upgrades that wouldn't actually benefit the team.
I also added a constraint that allowed me to “lock” and “ban” players, this was useful if the solver was suggesting I sell a player that had recently joined the club, or was suggesting buying a player who was on loan or had no interest in joining.
Algorithm in Action
The optimization approach utilizes linear programming to make squad planning decisions, specifically implemented using the PuLP library. At its core, the algorithm operates through two key components:
Decision Variables The solver uses binary (0 or 1) decision variables for each possible transfer action:
Buy variables: For each potential transfer target and each position they can play. This position-specific approach allows the solver to consider players who can effectively cover multiple roles, reflecting the flexibility needed in modern football.
Sell variables: For each current squad player and each position they can play.
Objective Function The solver aims to maximize total squad quality through a function that combines:
Base squad score (sum of each player's position-specific quality score)
Minus the quality lost from sold players
Plus the quality gained from purchased players
Below, is an example of the solver output using the Palace squad from the start of a save in the Summer of 2023 (using an updated database to reflect the season of 24/25) - the budget was £50,000,000 and £100,000 extra in wages and a max of 5 transfers were set.
The solver has managed to reduce costs in the squad greatly with a net spend of -£15M.
Results - the Crystal Palace Case Study
The above solver output was not realistic, though, as we would never buy 5 players after a full Summer of action already! Instead, I created a simple 3-4-2-1 pressing tactic and simulated to January for some transfer business.
Before starting, a few players wanted to leave in Mateta, Sarr, and Schlupp, so these players were loaned/sold and then the solver was put to action with a transfer budget of £10M and £50k wages. Below are the targets suggested.
Sergio Carreira was the main target with the aim of improving quality on the right flank where Nathaniel Clyne and Daniel Munoz currently resided. You can see he looks like a very decent player who is young and reasonably priced, the scouts also like him a lot.
Fredrik Bjorkan would be signed as a backup to Tyrick Mitchell, so I was pleased to see that the solver suggested someone that could and would take a role as a squad option, he is older than Carreira and also much more attacking.
The solver needed a striker after Mateta’s departure to meet the positional constraints. However, with Eddie Nketiah already in the fold, it clearly felt the budget was better utilised in other areas. It suggested an Atalanta youth player in Tommaso De Nipoti, whilst his profile does not necessarily look Prem-ready, he does intrigue me as a good youth player with the potential to improve. I wonder if he may be loaned out next transfer window.
So, that rounded up the transfer window, with each signing being made rather easily.
I then simulated to the end of the season and reviewed each signing’s contribution. Sergio Carreira made 11 appearances and came in with an average rating of 6.97. He actually managed to get 2 goals and 1 assist in a game against West Ham! That would be one hell of a haul in FPL.
Bjorkan, as expected, largely played rotational minutes, coming on in 9 games and only starting 1. His performance in that limited game time was good though, averaging 7.1.
Tommaso De Nipoti did not really feature, coming on once. This was expected too and I think that a loan move in the Summer would suit a player of this quality rather than keeping him.
Overall, the signings made a decent impact given the small budget used to acquire them. Carreira seemed to establish himself as a regular for the team, with Bjorkan deputising Mitchell admirably. De Nipoti is hopefully going to be one for the future, and if not, he only cost £1M anyway.
Now into the Summer window, we ran the solver again after making some player sales early on and letting the expiring contracts of players like Clyne and Ward run out. The budget was £50M.
The solver suggests making our first major sale in Jefferson Lerma and then selling some of the fringe players in Ahamada and Ebiowei. Below are the profiles of the suggested purchases.
Ignacio Miramon looks to be an exciting, young, and aggressive ball winner in the centre of the park.
Samu is the most expensive option, and surprisingly doesn’t actually look to be much better than Miramon. But, he is another good ball winner who has some more experience - he seems to be a bit of a work horse.
Lorenzo Pirola looks to be a great find, a young and intelligent centre back who is very aggressive. You can really see the club DNA scoring coming out in the wash here.
Ex-United player Facundo Pellistri was also suggested, he has great physicals and is a hard-working, tricky winger.
Niklas Hedl was not available when I went in for him so we had to go down a different route and sign Seimen, who is a 19-year old keeper also suggested by the solver:
After making a few more sales including Rob Holding, the solver suggested we buy a few more players.
Tomas Handel looked to be a cheap rotational option in midfield who can fill the need of the squad whilst we wait for Miramon to join (doesn’t join until next Jan):
Lorenzo Lucca looks to be a great option up front, giving us a different dimension to Nketiah in a player that can hold the ball up well.
Finally, we had Raul Torrente, Holding’s replacement. He is a big guy at 6’4” and looks to be fantastic on the ball with a great eye for a pass.
Overall, the solver suggested we make some very shrewd signings with the total incomings coming in at £80M, whilst we managed to recoup near to £35M in sales, meaning a £45M net spend. The only challenges I can foresee is that we have too many “good” players in the squad, and not enough rotational options. I feel that many players may get unhappy with game time throughout the next season.
After simulating the entire next season, it is time to review the signing’s performances. As there are so many this time, I won’t go through them one by one, instead picking up on the highlights.
Raul Torrente accumulated the most minutes out of any signing with 2,698 with 29 starts and 9 sub appearances. He averaged a rating of 6.87 and managed to score a goal against a promoted Milwall side. He has improved dramatically and is now worth over £30M (signed for £7M) and is wanted by Al-Ittihad.
Sergio Carreira comes in next for most minutes with 1,886 and 23 starts. It looks like he shared the role with Chris Richards and Daniel Munoz, and only managed a 6.66 average rating.
Samu and Miramon largely played rotational roles in the midfield only managing to make 17 starts combined. They both put up average ratings close to 6.8 so were pretty average.
Fredrik Bjorkan continued to deputise for Tyrick Mitchell and came with 1,000 minutes and an average rating of 7.33 which is the highest in the squad by quite a distance. He managed to get 4 goals and 6 assists in that time which is very impressive given his limited minutes. He is now worth over £15M and is wanted by fellow Premier League club, Leeds.
Facundo Pellistri was rather disappointing, making 14 starts but coming in with an average rating of 6.63. I had high hopes for him, but with only 1 assist in those minutes, he certainly underwhelmed.
Seimen made 9 appearances and has largely increased his value, now being worth £30M.
The other players failed to make any sort of impact, which is a shame, I had expected Pirola to be a success and it’s interesting that the assistant manager preferred Raul Torrente to him. Lucca didn’t get enough minutes, but he did score 5 Premier League goals, which is something. Tomas Handel made the least impact with only 146 minutes.
Overall, I think, whilst a lot of the signings did not make too much of an impact on the pitch, the approach feels like a success. Bar Pellistri and Tomas Handel, all players’ value has increased by a substantial amount. All the players bought had great attribute profiles and I subjectively thought they seemed like great finds. I think if I had managed the season rather than simulating, then the end results could have been much better.
Future improvements
The first thing that stands out to me as a potential improvement would be to balance the needs of optimising player quality but also ensuring the “depth” talent is not good enough that they expect too many minutes. I am not too ensure how I would implement this initially, but I am sure it would be using some form of manual input (similar to hardcoding positions of my players), or using the playing time value (i.e. key player or emergency backup etc.) and editing the score needed for each of those roles.
Another improvement would be to better estimate the wage demands of incoming players. Whilst it had no impact on my run with Palace, I can anticipate it being an issue down the line. As a Premier League club, there should be some sort of multiplier added to the wages to players from other leagues, this should also be dynamic based on the average wage of said league.
Conclusion
This was an enjoyable project to work through and it has given me some ideas for new projects, perhaps focussing more on the transfer scouting side rather than a strict in/out solver system. I can imagine this would also be more effective as a lower league side with stricter constraints. I may end up doing an FM save where I use this solver but actually play through the games and see how successful it can be.
If you have any thoughts or ideas, let me know! You can see the code behind the solver here at my GitHub!
Wow just finished reading this. Really enjoyed the piece and the examples you use as I like Football Manager too. Nice idea and approach you used, looking forward to any follow up post if you choose to write one.