Data preparation

The S&P 500 numbers are pre-tax whereas the Berkshire numbers are after-tax. Data source: Warren Buffett’s 2021 annual letter.

year = 1965:2021

berkshire = c(49.5, -3.4, 13.3, 77.8, 19.4, -4.6, 80.5, 8.1, -2.5, -48.7,
              2.5, 129.3, 46.8, 14.5, 102.5, 32.8, 31.8, 38.4, 69, -2.7, 
              93.7, 14.2, 4.6, 59.3, 84.6, -23.1, 35.6, 29.8, 38.9, 25, 57.4, 
              6.2, 34.9, 52.2, -19.9, 26.6, 6.5, -3.8, 15.8, 4.3, 0.8, 24.1, 
              28.7, -31.8, 2.7, 21.4, -4.7, 16.8, 32.7, 27, -12.5, 23.4, 
              21.9, 2.8, 11.0, 2.4, 29.6)
sp500 = c(10, -11.7, 30.9, 11, -8.4, 3.9, 14.6, 18.9, -14.8, -26.4, 37.2, 23.6,
          -7.4, 6.4, 18.2, 32.3, -5, 21.4, 22.4, 6.1, 31.6, 18.6, 5.1, 16.6, 
          31.7, -3.1, 30.5, 7.6, 10.1, 1.3, 37.6, 23, 33.4, 28.6, 21, -9.1, -11.9,
          -22.1, 28.7, 10.9, 4.9, 15.8, 5.5, -37, 26.5, 15.1, 2.1, 16, 32.4, 13.7,
          1.4, 12, 21.8, -4.4, 31.5, 18.4, 28.7)

# performance analytics
performance.func = function(df){
  out = df %>%
    mutate(
      sp500_divd_incl=cumprod(c(1, 1 + lead(df$sp500/100)[1:length(df$sp500)-1])),
      berkshire_per_share_mv=cumprod(c(1, 1 + lead(df$berkshire/100)[1:length(df$berkshire)-1]))
    )
  
  list(
    out,
    c(# Compounded Annual Gain
      SP500=round((out$sp500_divd_incl[n+1] ^ (1/n)-1)*100, 1),
      Berkshire=round((out$berkshire_per_share_mv[n+1] ^ (1/n)-1)*100, 1)
    ),
    c(# Performance_Check
      SP500=all.equal(
        out$sp500[-1],
        100*((out$sp500_divd_incl/lag(out$sp500_divd_incl))^(1/(out$year-lag(out$year))) - 1)[-1]
      ),
      Berkshire=all.equal(
        out$berkshire[-1],
        100*((out$berkshire_per_share_mv/lag(out$berkshire_per_share_mv))^(1/(out$year-lag(out$year))) - 1)[-1]
      )
    )
  )
}

Compounded Annual Gain

2012-2021

df = data.frame(year, sp500, berkshire)
df = df %>% filter(between(year, 2011, 2021))
n = nrow(df)-1
out = performance.func(df)

# data.frame(start=out[[1]]$year[2], end=out[[1]]$year[n+1])
data.frame(CAGR=out[[2]]) %>% 
  knitr::kable()
CAGR
SP500 16.6
Berkshire 14.6
out[[1]] %>% 
  select(year, sp500_divd_incl, berkshire_per_share_mv) %>% 
  tidyr::gather("type", "growth", sp500_divd_incl, berkshire_per_share_mv) %>%
  ggplot(aes(x = year, y = growth)) + 
  geom_line(aes(color = type), size = 1) +
  scale_color_manual(values = c("#00AFBB", "#E7B800")) +
  theme_minimal()

out[[1]] %>% 
  knitr::kable()
year sp500 berkshire sp500_divd_incl berkshire_per_share_mv
2011 2.1 -4.7 1.000000 1.000000
2012 16.0 16.8 1.160000 1.168000
2013 32.4 32.7 1.535840 1.549936
2014 13.7 27.0 1.746250 1.968419
2015 1.4 -12.5 1.770698 1.722366
2016 12.0 23.4 1.983181 2.125400
2017 21.8 21.9 2.415515 2.590863
2018 -4.4 2.8 2.309232 2.663407
2019 31.5 11.0 3.036640 2.956382
2020 18.4 2.4 3.595382 3.027335
2021 28.7 29.6 4.627257 3.923426
data.frame(Performance_Check=out[[3]]) %>% 
  knitr::kable()
Performance_Check
SP500 TRUE
Berkshire TRUE

2002-2021

df = data.frame(year, sp500, berkshire)
df = df %>% filter(between(year, 2001, 2021))
n = nrow(df)-1
out = performance.func(df)

# data.frame(start=out[[1]]$year[2], end=out[[1]]$year[n+1])
data.frame(CAGR=out[[2]]) %>% 
  knitr::kable()
CAGR
SP500 9.5
Berkshire 9.3
out[[1]] %>% 
  select(year, sp500_divd_incl, berkshire_per_share_mv) %>% 
  tidyr::gather("type", "growth", sp500_divd_incl, berkshire_per_share_mv) %>%
  ggplot(aes(x = year, y = growth)) + 
  geom_line(aes(color = type), size = 1) +
  scale_color_manual(values = c("#00AFBB", "#E7B800")) +
  theme_minimal()

out[[1]] %>% 
  knitr::kable()
year sp500 berkshire sp500_divd_incl berkshire_per_share_mv
2001 -11.9 6.5 1.0000000 1.000000
2002 -22.1 -3.8 0.7790000 0.962000
2003 28.7 15.8 1.0025730 1.113996
2004 10.9 4.3 1.1118535 1.161898
2005 4.9 0.8 1.1663343 1.171193
2006 15.8 24.1 1.3506151 1.453450
2007 5.5 28.7 1.4248989 1.870591
2008 -37.0 -31.8 0.8976863 1.275743
2009 26.5 2.7 1.1355732 1.310188
2010 15.1 21.4 1.3070447 1.590568
2011 2.1 -4.7 1.3344927 1.515812
2012 16.0 16.8 1.5480115 1.770468
2013 32.4 32.7 2.0495673 2.349411
2014 13.7 27.0 2.3303580 2.983752
2015 1.4 -12.5 2.3629830 2.610783
2016 12.0 23.4 2.6465409 3.221706
2017 21.8 21.9 3.2234869 3.927260
2018 -4.4 2.8 3.0816534 4.037223
2019 31.5 11.0 4.0523743 4.481317
2020 18.4 2.4 4.7980111 4.588869
2021 28.7 29.6 6.1750403 5.947174
data.frame(Performance_Check=out[[3]]) %>% 
  knitr::kable()
Performance_Check
SP500 TRUE
Berkshire TRUE

1966-2021

df = data.frame(year, sp500, berkshire)
# df = df %>% filter(between(year, 2001, 2021))
n = nrow(df)-1
out = performance.func(df)

# data.frame(start=out[[1]]$year[2], end=out[[1]]$year[n+1])
data.frame(CAGR=out[[2]]) %>% 
  knitr::kable()
CAGR
SP500 10.5
Berkshire 19.8
out[[1]] %>% 
  select(year, sp500_divd_incl, berkshire_per_share_mv) %>% 
  tidyr::gather("type", "growth", sp500_divd_incl, berkshire_per_share_mv) %>%
  ggplot(aes(x = year, y = growth)) + 
  geom_line(aes(color = type), size = 1) +
  scale_color_manual(values = c("#00AFBB", "#E7B800")) +
  theme_minimal()

out[[1]] %>% 
  knitr::kable()
year sp500 berkshire sp500_divd_incl berkshire_per_share_mv
1965 10.0 49.5 1.000000 1.000000
1966 -11.7 -3.4 0.883000 0.966000
1967 30.9 13.3 1.155847 1.094478
1968 11.0 77.8 1.282990 1.945982
1969 -8.4 19.4 1.175219 2.323502
1970 3.9 -4.6 1.221053 2.216621
1971 14.6 80.5 1.399326 4.001001
1972 18.9 8.1 1.663799 4.325082
1973 -14.8 -2.5 1.417557 4.216955
1974 -26.4 -48.7 1.043322 2.163298
1975 37.2 2.5 1.431437 2.217381
1976 23.6 129.3 1.769257 5.084454
1977 -7.4 46.8 1.638332 7.463978
1978 6.4 14.5 1.743185 8.546255
1979 18.2 102.5 2.060444 17.306166
1980 32.3 32.8 2.725968 22.982588
1981 -5.0 31.8 2.589670 30.291052
1982 21.4 38.4 3.143859 41.922815
1983 22.4 69.0 3.848083 70.849558
1984 6.1 -2.7 4.082816 68.936620
1985 31.6 93.7 5.372986 133.530233
1986 18.6 14.2 6.372362 152.491526
1987 5.1 4.6 6.697352 159.506136
1988 16.6 59.3 7.809113 254.093275
1989 31.7 84.6 10.284601 469.056185
1990 -3.1 -23.1 9.965779 360.704206
1991 30.5 35.6 13.005341 489.114904
1992 7.6 29.8 13.993747 634.871146
1993 10.1 38.9 15.407116 881.836021
1994 1.3 25.0 15.607408 1102.295026
1995 37.6 57.4 21.475794 1735.012371
1996 23.0 6.2 26.415226 1842.583138
1997 33.4 34.9 35.237912 2485.644654
1998 28.6 52.2 45.315954 3783.151163
1999 21.0 -19.9 54.832305 3030.304082
2000 -9.1 26.6 49.842565 3836.364967
2001 -11.9 6.5 43.911300 4085.728690
2002 -22.1 -3.8 34.206902 3930.471000
2003 28.7 15.8 44.024284 4551.485418
2004 10.9 4.3 48.822930 4747.199291
2005 4.9 0.8 51.215254 4785.176885
2006 15.8 24.1 59.307264 5938.404515
2007 5.5 28.7 62.569164 7642.726610
2008 -37.0 -31.8 39.418573 5212.339548
2009 26.5 2.7 49.864495 5353.072716
2010 15.1 21.4 57.394034 6498.630277
2011 2.1 -4.7 58.599308 6193.194654
2012 16.0 16.8 67.975198 7233.651356
2013 32.4 32.7 89.999162 9599.055350
2014 13.7 27.0 102.329047 12190.800294
2015 1.4 -12.5 103.761654 10666.950257
2016 12.0 23.4 116.213052 13163.016617
2017 21.8 21.9 141.547498 16045.717257
2018 -4.4 2.8 135.319408 16494.997340
2019 31.5 11.0 177.945021 18309.447047
2020 18.4 2.4 210.686905 18748.873776
2021 28.7 29.6 271.154047 24298.540414
data.frame(Performance_Check=out[[3]]) %>% 
  knitr::kable()
Performance_Check
SP500 TRUE
Berkshire TRUE