I wrote this after seeing the responses to my last question.
From the responses I can tell my problem was not understood.
What this simulation does is play a rudimentary “game” between pairs of two teams once per “week” and record the player results and league standings. Since the outcome is stochastic this is repeated multiple times. In actuality, the ratings/parameters of the players would change each simulation step and the output would be compared to other stats, but I left that out for now as it isn’t important to storing the results.
It only requires:
using DataFrames, Random
Then these utility functions to generate players/teams/schedule, the “game engine”, etc:
Summary
# Generate random player names
function gen_pnames(nplayers)
return [randstring('A':'Z', 3) for _ in 1:nplayers]
end
# Generate random team names
function gen_tnames(nteams)
return string.(collect('A':'Z')[1:nteams])
end
# Make schedule where teams plays each other once
function makeschedule(teamnames)
nteams = length(teamnames)
if isodd(nteams)
teamnames = [teamnames; "Bye"]
nteams = nteams + 1
end
df = DataFrame(reshape(teamnames, Int(nteams/2), 2), :auto)
tmp = deepcopy(df)
nw = nteams - 1
nr = size(df, 1)
sched = Vector{DataFrame}(undef, nw)
sched[1] = df
# Rotate df clockwise around [1, 1]
for w in 2:nw
tmp[1, 1] = df[1, 1]
tmp[1, 2] = df[2, 1]
tmp[nr, 1] = df[nr, 2]
if(nr > 2)
for i in reverse(3:nr)
tmp[i - 1, 1] = df[i, 1]
end
end
for i in 2:nr
tmp[i, 2] = df[i - 1, 2]
end
sched[w] = df = deepcopy(tmp)
end
return sched
end
# Generate rosters for each team
function gen_rosters(teamnames, nplayers)
nteams = length(teamnames)
rosters = Vector{DataFrame}(undef, nteams)
for i in 1:nteams
rosters[i] = DataFrame(pname = gen_pnames(nplayers),
skill = rand(1:10, nplayers))
end
rosters = [teamnames, rosters]
return rosters
end
# Play a game between two teams (simply subtract each player skill)
function playgame(team1, team2, teamnames, rosters)
idx1 = findfirst(teamnames .== team1)
idx2 = findfirst(teamnames .== team2)
gameresult = rosters[2][idx1].skill - rosters[2][idx2].skill
gameresult = gameresult + rand(-3:3, length(gameresult))
if sum(gameresult) == 0
teamresult = [0, 0]
else
teamresult = ifelse(sum(gameresult) > 0, [1, 0], [0, 1])
end
teams = DataFrame(teams = [team1, team2], winner = teamresult)
stats1 = DataFrame(pname = rosters[2][idx1].pname,
pts = gameresult)
stats2 = DataFrame(pname = rosters[2][idx2].pname,
pts = -gameresult)
return (teams = teams, stats1 = stats1, stats2 = stats2)
end
# Accumulates wins/losses/etc in the standings
function update_lgstats(lgstats, gamesres)
for i in eachindex(gamesres)
idx1 = findfirst(lgstats.Team .== gamesres[i][1].teams[1])
idx2 = findfirst(lgstats.Team .== gamesres[i][1].teams[2])
win = gamesres[i][1].winner
lgstats.Games[[idx1, idx2]] += [1, 1]
if sum(win) == 0
lgstats.Draw[[idx1, idx2]] += [1, 1]
end
lgstats.Win[[idx1, idx2]] += win
lgstats.Loss[[idx1, idx2]] += reverse(win)
end
return lgstats
end
# Reset standings to zeros
function reset_lgstats(teamnames)
nteams = length(teamnames)
lgstats = DataFrame(Team = teamnames,
Games = zeros(Int32, nteams),
Win = zeros(Int32, nteams),
Loss = zeros(Int32, nteams),
Draw = zeros(Int32, nteams))
return lgstats
end
# Placeholder that returns a random number
function calcmetrics(simres)
return DataFrame(rmse = rand(1:100))
end
And here would be what is in the main simulation function:
nteams = 4
nplayers = 2
nsim = 1
nrep = 3
teamnames = gen_tnames(nteams)
rosters = gen_rosters(teamnames, nplayers)
sched = makeschedule(teamnames)
nweeks = nteams - 1
allres = Vector{}(undef, nsim)
for s in 1:nsim
# params = genparams()
simres = Vector{}(undef, nrep)
# Sim same season nrep times
for r in 1:nrep
lgstats = reset_lgstats(teamnames)
seasonres = Vector{}(undef, nweeks)
# Play Season
for w in 1:nweeks
games = deepcopy(sched[w])
chkbye = Matrix(games) .== "Bye"
if sum(chkbye) == 1
idx_bye = findall(chkbye)[1][1]
deleteat!(games, idx_bye)
end
ngames = nrow(games)
gamesres = Vector{NamedTuple}(undef, ngames)
# Play games for week w
for g in 1:ngames
team1 = games[g, 1]
team2 = games[g, 2]
gamesres[g] = playgame(team1, team2, teamnames, rosters)
end
lgstats = update_lgstats(lgstats, gamesres)
seasonres[w] = [gamesres, deepcopy(lgstats)]
end
simres[r] = seasonres
end
metrics = calcmetrics(simres)
allres[s] = [simres, metrics]
end
What I want to know is how to implement a better way to structure allres
than this vector of vectors of dataframes and tuples I have going on. Also, anything else weird I am doing.
At first I figured that was too much for here, but maybe someone will take a look so why not see. My impression from reading around on this forum and stackexchange, etc is that this is a common issue not really addressed by the MWEs we normally see. So perhaps it could be generally useful.
Thanks to anyone who takes a look!