Skip to content

Instantly share code, notes, and snippets.

@kevinmilner
Last active February 22, 2021 21:05
Show Gist options
  • Save kevinmilner/ca9728412c63b37c724c85777a7c0d39 to your computer and use it in GitHub Desktop.
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
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