Last active
February 22, 2021 21:05
-
-
Save kevinmilner/ca9728412c63b37c724c85777a7c0d39 to your computer and use it in GitHub Desktop.
Code for simulating Trevor Bauer on a 4-day pitching rotation with the rest of the 2021 Dodgers on a 5-day rotation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.text.DecimalFormat; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Random; | |
public class RotationCalcs { | |
public enum TieBreaker { | |
RANDOM, | |
SHORTER_REST, | |
LONGER_REST | |
} | |
public static class Pitcher { | |
public final String name; | |
public final int prefRest; | |
public final int minRest; | |
public Pitcher(String name, int prefRest, int minRest) { | |
super(); | |
this.name = name; | |
this.prefRest = prefRest; | |
this.minRest = minRest; | |
} | |
} | |
public static class PitcherSeason implements Comparable<PitcherSeason> { | |
public final Pitcher pitcher; | |
private final TieBreaker tieBreaker; | |
private int curRest; | |
private int gamesStarted = 0; | |
private List<Integer> restPeriods; | |
private int ilStints = 0; | |
private int ilDaysLeft = 0; | |
public PitcherSeason(Pitcher pitcher, TieBreaker tieBreaker) { | |
this.pitcher = pitcher; | |
this.tieBreaker = tieBreaker; | |
this.curRest = pitcher.prefRest < Integer.MAX_VALUE ? -1 : 0; // -1 means ready for first start | |
this.restPeriods = new ArrayList<>(); | |
} | |
public boolean isPreferred() { | |
return curRest == pitcher.prefRest; | |
} | |
public boolean isReady() { | |
return !isOnIL() && (curRest >= pitcher.minRest || curRest == -1); | |
} | |
public int daysAfterPreferred() { | |
if (curRest < pitcher.prefRest) | |
return -Integer.MAX_VALUE; | |
return curRest - pitcher.prefRest; | |
} | |
@Override | |
public int compareTo(PitcherSeason o) { | |
// check IL status | |
if (isOnIL() || o.isOnIL()) { | |
if (isOnIL() && o.isOnIL()) | |
return Integer.compare(ilDaysLeft, o.ilDaysLeft); | |
if (isOnIL()) | |
return -1; | |
return 1; | |
} | |
// if one pitcher is ready but the other isn't promote the one who is ready | |
if (isReady() != o.isReady()) { | |
if (isReady()) | |
return -1; | |
return 1; | |
} | |
// special case for first start | |
if (curRest < 0 || o.curRest < 0) { | |
// awaiting first start | |
if (curRest < 0 && o.curRest < 0) | |
return 0; | |
if (curRest < 0) | |
return -1; | |
return 1; | |
} | |
// this is the meat of the comparison. we sort by how far over of a pitcher's preferred rest they are | |
// choose farthest over their preferred rest | |
int cmp = Integer.compare(o.daysAfterPreferred(), daysAfterPreferred()); | |
if (cmp != 0) | |
return cmp; | |
// if we're there and 2 pitchers are at their preferred rest, use a tie breaker | |
if (isPreferred() && o.isPreferred()) { | |
switch (tieBreaker) { | |
case RANDOM: | |
// choose randomly | |
if (Math.random() < 0.5) | |
return -1; | |
return 1; | |
case LONGER_REST: | |
return Integer.compare(o.curRest, curRest); | |
case SHORTER_REST: | |
return Integer.compare(curRest, o.curRest); | |
default: | |
throw new IllegalStateException("shouldn't get here"); | |
} | |
} | |
// everything is the same, choose the most rested | |
return Integer.compare(o.curRest, curRest); | |
} | |
public void pitch() { | |
if (curRest >= 0) { | |
restPeriods.add(curRest); | |
} | |
curRest = 0; | |
gamesStarted++; | |
} | |
public void rest() { | |
if (isOnIL()) | |
ilDaysLeft--; | |
else if (curRest >= 0) | |
curRest++; | |
} | |
public void scratch() { | |
InjuredList il = pickIL(); | |
int days = il.days; | |
if (days < 0) | |
// miss start, pick random rest period up to preferred rest (missing a few days, not full IL) | |
days = new Random().nextInt(Integer.min(9, pitcher.prefRest))+1; | |
ilDaysLeft = days; | |
ilStints++; | |
} | |
public boolean isOnIL() { | |
return ilDaysLeft > 0; | |
} | |
@Override | |
public String toString() { | |
return pitcher.name+"\tcurRest="+curRest+"\tpref?\t"+isPreferred()+"\tready?\t"+isReady()+"\tIL?\t"+isOnIL(); | |
} | |
public int[] restDistribution(int maxRest) { | |
int[] dist = new int[maxRest]; | |
for (int rest : restPeriods) { | |
if (rest >= maxRest) | |
dist[maxRest-1]++; | |
else | |
dist[rest]++; | |
} | |
return dist; | |
} | |
} | |
public enum InjuredList { | |
MISS_START(0.6, -1), | |
TEN_DAY(0.3, 10), | |
FIFTEEN_DAY(0.08, 15), | |
SIXTY_DAY(0.02, 60); | |
private double prob; | |
private int days; | |
private InjuredList(double prob, int days) { | |
this.prob = prob; | |
this.days = days; | |
} | |
} | |
public static InjuredList pickIL() { | |
double r = Math.random(); | |
double sum = 0d; | |
for (InjuredList il : InjuredList.values()) { | |
sum += il.prob; | |
if (r <= sum) | |
return il; | |
} | |
throw new IllegalStateException("IL probs don't sum to 1? r="+r+", sum="+sum); | |
} | |
public static class SeasonSimulation { | |
private Map<Pitcher, PitcherSeason> pitcherSeasons; | |
private List<PitcherSeason> rotation; | |
private double offDayProb; | |
private double injuryProb; | |
private int bullpenGames = 0; | |
public SeasonSimulation(Pitcher[] pitchers, double offDayProb, double injuryProb, TieBreaker tieBreaker) { | |
this.offDayProb = offDayProb; | |
this.injuryProb = injuryProb; | |
rotation = new ArrayList<>(); | |
pitcherSeasons = new HashMap<>(); | |
for (Pitcher pitcher : pitchers) { | |
PitcherSeason season = new PitcherSeason(pitcher, tieBreaker); | |
pitcherSeasons.put(pitcher, season); | |
rotation.add(season); | |
} | |
} | |
public void simulate(boolean verbose) { | |
int games = 0; | |
int targetNumGames = 162; | |
int day = 1; | |
while (games <= targetNumGames) { | |
// pick the best pitcher | |
boolean offDay = day > 1 && Math.random() < offDayProb; | |
if (offDay) { | |
if (verbose) | |
System.out.println("Day "+day+" OFF DAY"); | |
for (PitcherSeason pitcher : rotation) | |
pitcher.rest(); | |
} else { | |
Collections.sort(rotation); | |
if (verbose) | |
System.out.println("Day "+day+" rotation:"); | |
boolean starterFound = false; | |
for (int i=0; i<rotation.size(); i++) { | |
PitcherSeason pitcher = rotation.get(i); | |
if (pitcher.isOnIL()) { | |
if (verbose) | |
System.out.println("\t"+pitcher+"\tREST (IL)"); | |
pitcher.rest(); | |
continue; | |
} | |
if (!starterFound) { | |
if (Math.random() < injuryProb) { | |
pitcher.scratch(); | |
if (verbose) | |
System.out.println("\t"+pitcher+"\tSCRATCH: "+pitcher.ilDaysLeft+" day IL"); | |
} else if (pitcher.isReady()) { | |
if (verbose) | |
System.out.println("\t"+pitcher+"\tSTARTER"); | |
pitcher.pitch(); | |
starterFound = true; | |
} | |
} else { | |
if (verbose) | |
System.out.println("\t"+pitcher+"\tREST DAY"); | |
pitcher.rest(); | |
} | |
} | |
if (!starterFound) { | |
if (verbose) | |
System.out.println("\tNo pitchers ready, BULLPEN GAME"); | |
bullpenGames++; | |
} | |
games++; | |
} | |
day++; | |
} | |
day -= 2; | |
games--; | |
System.out.println("Played "+games+" games in "+day+" days"); | |
} | |
public PitcherSeason getSeason(Pitcher pitcher) { | |
return pitcherSeasons.get(pitcher); | |
} | |
} | |
public static void main(String[] args) { | |
Pitcher[] pitchers = { | |
new Pitcher("Kershaw", 4, 4), | |
new Pitcher("Buehler", 4, 4), | |
new Pitcher("Bauer", 3, 3), | |
// new Pitcher("Bauer", 4, 4), | |
new Pitcher("Price", 4, 4), | |
new Pitcher("Urias", 4, 4), | |
new Pitcher("Gonsolin", Integer.MAX_VALUE, 4), | |
new Pitcher("May", Integer.MAX_VALUE, 4), | |
}; | |
double offDayProb = 0.1d; | |
double injuryProb = 0.05d; | |
TieBreaker tieBreaker = TieBreaker.RANDOM; | |
// double offDayProb = 0.0d; | |
// double injuryProb = 0.0d; | |
int numSeasons = 1000000; | |
double[][] aveRestDists = new double[pitchers.length][11]; | |
double[] aveStarts = new double[pitchers.length]; | |
double[] aveILs = new double[pitchers.length]; | |
double aveBPs = 0d; | |
int minRest = 3; | |
for (int i=0; i<numSeasons; i++) { | |
System.out.println("Season "+(i+1)); | |
SeasonSimulation sim = new SeasonSimulation(pitchers, offDayProb, injuryProb, tieBreaker); | |
boolean verbose = i == numSeasons-1; | |
sim.simulate(verbose); | |
for (int p=0; p<pitchers.length; p++) { | |
Pitcher pitcher = pitchers[p]; | |
PitcherSeason season = sim.getSeason(pitcher); | |
int[] dist = season.restDistribution(aveRestDists[p].length); | |
// will normalize these later | |
for (int j=0; j<dist.length; j++) { | |
aveRestDists[p][j] += dist[j]; | |
if (dist[j] > 0) | |
minRest = Integer.min(minRest, j); | |
} | |
aveStarts[p] += season.gamesStarted; | |
aveILs[p] += season.ilStints; | |
} | |
aveBPs += sim.bullpenGames; | |
} | |
// normalize | |
for (int p=0; p<pitchers.length; p++) { | |
for (int i=0; i<aveRestDists[p].length; i++) | |
aveRestDists[p][i] /= (double)numSeasons; | |
aveStarts[p] /= (double)numSeasons; | |
aveILs[p] /= (double)numSeasons; | |
} | |
aveBPs /= (double)numSeasons; | |
System.out.println("Averages after "+numSeasons+" season simulations:"); | |
int padding = 0; | |
for (Pitcher p : pitchers) | |
padding = Integer.max(padding, p.name.length()); | |
// header | |
for (int i=0; i<padding; i++) | |
System.out.print(" "); | |
System.out.print(" \tStarts\tRmode\tIL's"); | |
for (int i=minRest; i<aveRestDists[0].length; i++) | |
System.out.print("\tR="+i); | |
System.out.println("+"); | |
for (int p=0; p<pitchers.length; p++) { | |
System.out.print(pitchers[p].name); | |
for (int i=pitchers[p].name.length(); i<padding; i++) | |
System.out.print(" "); | |
System.out.print(" \t"+formatNum(aveStarts[p])); | |
int mode = 0; | |
double[] dist = aveRestDists[p]; | |
for (int i=0; i<dist.length; i++) { | |
if (dist[i] > dist[mode]) | |
mode = i; | |
} | |
System.out.print("\t"+mode+"\t"+formatNum(aveILs[p])); | |
for (int i=minRest; i<dist.length; i++) | |
System.out.print("\t"+formatNum(dist[i])); | |
System.out.println(); | |
} | |
System.out.println("Bullpen Games:\t"+formatNum(aveBPs)); | |
} | |
private static final DecimalFormat df = new DecimalFormat("0.0"); | |
private static String formatNum(double num) { | |
if (num > 0d && num < 0.05) | |
return "<0.1"; | |
return df.format(num); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment