Forecast of Giancarlo Stanton’s Expected Surplus Value

Summary

In most analyses, I would normally lead with the conclusion—here, surplus value. But, as the perceived goals of analysis are evaluating, well, the analysis, I’ll start with my methods. First, I estimate Stanton’s performance in terms of WAR, tying Stanton’s performance to team wins. Then I connect his wins over replacement to dollars. Finally, I subtract out the contract cost and report surplus as net present value.

Estimate player past performance as Wins Over Replacement

It is common to estimate “Wins Above Replacement” to estimate a players’ total performance value to a team by translating it to a contribution to team wins. The estimation of WAR, unlike other common metrics, is not only tied to discrete events, and the community is divided into how a WAR should be estimated and difference sources report sometimes wildly differing values for a given player/season. Should we be using season averages? Play by play? For purposes here, I’m using WAR as estimated using an R package called openWAR for several reasons, while mentioning its limitations, all important when understanding what the valuation here represents. Open WAR, unlike that reported by popular media, is completely transparent about its calculations. Secondly, unlike with other WARs, openWAR provides an estimate of uncertainty in its estimation (using a technique called bootstrapping). I should note, the openWAR method of estimation suffers its own drawbacks, which I would like to improve at some point, including separately modeling events. A better approach would be to model all aspects of the game as a single, generative model of baseball. That said, this off-the-shelf (and free!) software offers significant improvements over the more opaque, publicly available point-estimates.

Using the openWAR package, I scraped MLB data for seasons 2010 through 2017 (openWAR::getData), keeping regular season games. That data provides the information needed to estimate WAR as outlined in the paper on openWAR (using openWAR::makeWAR). WARs were generated for all players in all seasons. As WAR is properly an estimate and—importantly, includes uncertainty—I ran 1000 bootstrap simulations (openWAR::shakeWAR) and obtain distributions of WAR for each player in each season.

Forecasting WAR with a hierarchical model in Stan

Data from the population of players provides a baseline for Stanton’s performance, and we shift from that baseline based on what we learn from his data. In the hierarchical model used here, as ratio of variance shifts from the population to Stanton, or the other way around, we lean more heavily where the information is more reliable. This makes sense: If one knew nothing about a player, other than he is in the MLB, one should consider him average, and as we learn more we adjust for what we learn.

The model regresses WAR as a (quadratic) function of age (by age, here, instead of strictly applying MLB rules, I just subtracted birth year reported in the Register from the season. A more detailed analysis would not just refine the calculation, but would account for the uncertainty of age inflation of players from certain nationalities), making adjustments from the population’s aging curve for each player. But not all players provide the same amount of useful information. Generally, players in some fielding positions “age” differently than in others. Stanton protects left and right field. Thus, this simplified model limits the data to players who have played a significant amount in those positions, even if that is not their sole position. Simplifying, using plate appearances in those positions, I’ve kept players logging the top 75 percent of plate appearances at either position, dropping the lowest 25 percent.

Further simplifying the data for this analysis, I’ve calculated the mean of each retained player/season’s WAR and total plate appearances. Theoretically, I could have fed the entire distributions into the model—which would more ideally propagate the uncertainty—but I suspect the shear volume of this data would make convergence too slow here. Instead, I attempted to propagate uncertainty using plate appearances. The more plate appearances we observe, the more reliable we should feel in the forecasted distributions of WAR.

We should also be clear in interpreting the information as conditioned upon players who remain in the league. Given time, we should also code the probability that a player, given his WAR in one year, will not play in the following year. Without this, the results here should be read as upwardly biased.

I’ve enclosed the Stan model at the end of this analysis.

The forecasted posterior estimates of Stanton’s WAR—as interpreted with the above simplifying assumptions—are as follows,

We can see that the observed values fit within the middle 50 percent of estimates about half the time. I would expect, from past work, that his expected WAR values would drop further than they show here, and the culprit is likely survival bias in the data. As mentioned, jointly modeling the probability of selection next season based using WAR would help solve this issue. With the above framework for estimating Stanton’s WAR, we now value that war and compare annual value to future contractually obligated payouts.

Dollars Per WAR on the Free-Agent Market

It is fairly standard, and sensical, to estimate the marginal cost of WAR as the cost of procuring WAR on the free-agent market. If Stanton was unavailable, the quickest (but most expensive) way to replace him would be with a free agent.

Economist Matt Swartz has written extensively on estimating the average cost of WAR on the free-agent market, and projecting its growth. To estimate average cost and growth reasonably requires various data sets on free agent contract values, and linking that information to the above estimates of WAR. The time to wrangle an analysis that considers at least the factors described by Matt goes beyond the time frame needed to offer an answer. Thus, for purposes of this exercise, I will apply Swartz’s recent forecast of WAR values (Swartz, M., The Recent History of Free-Agent Pricing (fangraphs.com, July 11, 2017), https://www.fangraphs.com/blogs/the-recent-history-of-free-agent-pricing/) going forward, as follows. He estimates 2017 WAR on the free agent market at $10.5MM/WAR and the growth of that cost exceeding GDP by 2.1 percent. Assuming a 3.8 percent growth in GDP, then, he estimates growth on the free agent market at 5.9 percent. Thus, the cost in millions for WAR on the free agent market from 2018 through 2028 could be:

Season Free agent dollars per WAR (MM)
2018 11.1
2019 11.8
2020 12.5
2021 13.2
2022 14.0
2023 14.8
2024 15.7
2025 16.6
2026 17.6
2027 18.6
2028 19.7

The dollar values here relate to whatever WAR Swartz used, not the specific WAR distributions generated here and, as is well reported, WAR (and, thus, Swartz’s dollar per WAR estimates) may not accurately price our specific WARs. Further, other WAR point estimates rely upon a different definition of a replacement player than does openWAR. In further developing the analysis, we should gather all free agent contracts assigned to the players and match their AAV to our WAR distributions. For purposes of the sketch here, we will just assume Swartz’s estimates are directly applicable.

Thus, Stanton’s unadjusted, annual value contributions are estimated in millions as,

season $MM/WAR (10 percent) $MM/WAR (50 percent) $MM/WAR (90 percent)
2018 13 34 54
2019 4 25 47
2020 18 40 63
2021 16 39 63
2022 26 50 75
2023 25 52 79
2024 25 52 81
2025 23 52 81
2026 20 52 83
2027 16 50 84
2028 5 41 77

Unadjusted surplus value

Unadjusted surplus value simply sums the annual difference between value generated and contract cost. Per Spotrac (http://www.spotrac.com/mlb/miami-marlins/giancarlo-stanton-6864/), Stanton’s remaining contract payouts are as follows:

year aav
2018 25
2019 26
2020 26
2021 29
2022 29
2023 32
2024 32
2025 32
2026 29
2027 25
2028 25

Subtracting the payout to Stanton each season from his value generated each season, we get the following unadjusted range of expected surplus,

10 percent 50 percent 90 percent
-119 177 477

We adjust that value to net present value using Swartz’s estimate of GDP and fee agent inflation above the estimate. Assuming the discount rate equals GDP (3.8 percent), his range of expected net present values in millions are,

10 percnet 50 percent 90 percent
-97 137 372

We can also understand the full range of his possible net present values,

Certainly the model and method can be improved, but I hope this sketch has been a useful starting point for further analysis.

Appendix: Stan Model for Forcasting WAR

data {
  // population level information
  int<lower=1> N;
  vector<lower=0>[N] age;
  vector[N] war;
  vector<lower=0>[N] tpa;
  
  // group level information
  int<lower=1> n_groups;
  int<lower=1,upper=n_groups> group_id[N];
  
  // new data
  int<lower=1> N_nd;
  vector<lower=0>[N_nd] age_nd;
  int<lower=1> n_groups_nd;
  int<lower=1,upper=n_groups_nd> group_id_nd[N_nd];
}

transformed data {
  vector<lower=0>[N] age_sq = age .* age;
}

parameters {
  // population parameters for tpa
  real beta_age;
  real beta_age_sq;
  // real beta_tpa;
  
  // population parameters for war
  real theta_age;
  real theta_age_sq;

  // parameters for sigmas
  real theta_tpa;
  // real alpha_sigma_war;
  real<lower=0> sigma_tpa;
  // vector<lower=0>[N] sigma_war;
  real<lower=0> sigma_war;
  
  // group level parameters
  real mu_tpa;
  real gamma_tpa[n_groups];
  real mu_war;
  real gamma_war[n_groups];
}

model {
  
  // setup group level parameters 
  vector[N] mu_gamma_tpa;
  vector[N] mu_gamma_war;
  for(i in 1:N) {
    mu_gamma_tpa[i] = mu_tpa + gamma_tpa[group_id[i]];
    mu_gamma_war[i] = mu_war + gamma_war[group_id[i]];
  }

  // model plate appearances as function of age and player
  target += normal_lpdf(tpa | beta_age * age + 
                              beta_age_sq * age_sq + 
                              mu_gamma_tpa, 
                              sigma_tpa);
  
  // model uncertainty of WAR as function of plate appearances
  // target += exponential_lpdf(sigma_war | exp(alpha_sigma_war + beta_tpa * tpa));
  
  // model war as function of plate appearances, age, and player
  target += normal_lpdf(war | theta_age * age + 
                              theta_age_sq * age_sq + 
                              theta_tpa * tpa + 
                              mu_gamma_war, 
                              sigma_war);
  
  
  // priors
    
    beta_age ~ normal(0, 100);
    beta_age_sq ~ normal(0, 10);
    // beta_tpa ~ normal(0, 100);
    
    theta_age ~ normal(0, 100);
    theta_age_sq ~ normal(0, 10);
    theta_tpa ~ normal(0, 100);
    
    mu_tpa ~ normal(0, 100);
    mu_war ~ normal(0, 100);
    gamma_tpa ~ normal(0,100);
    gamma_war ~ normal(0,100);
    
    // alpha_sigma_war ~ cauchy(0, 2.5);
    sigma_tpa ~ exponential_lpdf(10);
    sigma_war ~ exponential_lpdf(1);
}

generated quantities {
  
  vector[N] war_pred;
  vector[N] tpa_pred;
  // vector<lower=0>[N] sigma_war_pred;
  vector[N] mu_gamma_tpa;
  vector[N] mu_gamma_war;


  // for new observations
  vector[N_nd] war_pred_nd;
  vector[N_nd] tpa_pred_nd;
  // vector<lower=0>[N_nd] sigma_war_pred_nd;
  vector[N_nd] mu_gamma_tpa_nd;
  vector[N_nd] mu_gamma_war_nd;


  for(i in 1:N) {
    mu_gamma_tpa[i] = mu_tpa + gamma_tpa[group_id[i]];  
    mu_gamma_war[i] = mu_war + gamma_war[group_id[i]];  
  }
  
  
  for (n in 1:N) {
    tpa_pred[n] = normal_rng(beta_age * age[n] + 
                             beta_age_sq * age_sq[n] + 
                             mu_gamma_tpa[n], 
                             sigma_tpa);
    
    // sigma_war_pred[n] = exponential_rng(exp(alpha_sigma_war + beta_tpa * tpa_pred[n]));
    
    war_pred[n] = normal_rng(theta_age * age[n] + 
                             theta_age_sq * age_sq[n] + theta_tpa * tpa_pred[n] + 
                             mu_gamma_war[n],
                             sigma_war);
  }
  
    for(j in 1:N_nd) {
    mu_gamma_tpa_nd[j] = mu_tpa + gamma_tpa[group_id_nd[j]];  
    mu_gamma_war_nd[j] = mu_war + gamma_war[group_id_nd[j]];  
  }
  
  for(nd in 1:N_nd) {
    tpa_pred_nd[nd] = normal_rng(beta_age * age_nd[nd] + 
                                 beta_age_sq * age_nd[nd] * age_nd[nd] + 
                                 mu_gamma_tpa_nd[nd], 
                                 sigma_tpa);
    // sigma_war_pred_nd[nd] = exponential_rng(exp(alpha_sigma_war + beta_tpa * tpa_pred_nd[nd]));
    war_pred_nd[nd] = normal_rng(theta_age * age_nd[nd] + 
                                 theta_age_sq * age_nd[nd] * age_nd[nd] + 
                                 theta_tpa * tpa_pred[nd] +
                                 mu_gamma_war_nd[nd], 
                                 sigma_war);
  }
}