A demonstration of the Central Limit Theorem (CLT) using an earthquake dataset from the USGS via Kaggle.
Setup
Load packages used in the analysis.
library(tidyverse)
library(lubridate)
Import the data.
earthquakes <- read.csv('earthquakes.csv') %>%
mutate(
n = 1
, Date = as.character(Date) %>% str_sub(1, 10)
, date = Date %>% mdy(quiet = TRUE)
)
earthquakes[is.na(earthquakes$date) %>% which(), 'date'] <-
earthquakes[is.na(earthquakes$date) %>% which(), 'Date'] %>% ymd()
Create a summary function.
summary_stats <- function(data_frame, var_name) {
data.frame(
count = length(data_frame[, var_name] %>% unlist())
, min = min(data_frame[, var_name] %>% unlist())
, median = median(data_frame[, var_name] %>% unlist())
, max = max(data_frame[, var_name] %>% unlist())
, mean = mean(data_frame[, var_name] %>% unlist())
, sd = sd(data_frame[, var_name] %>% unlist())
)
}
The normal distribution mistake
eq.summary <- summary_stats(earthquakes, 'Magnitude')
print(eq.summary)
Naturally, just because a mean and standard deviation can be computed for a dataset doesn’t mean anything sensible can be said about the mechanism underlying the data generating process. Say we made the mistake of applying a normal (Gaussian) distribution to the data and computing the implied counts of earthquakes above a certain magnitude. To illustrate, here’s a quick plot of the 23412 earthquake magnitudes with the distribution N(5.88, 0.422) superimposed.
bw <- .1
earthquakes %>%
ggplot(aes(x = Magnitude)) +
geom_histogram(binwidth = bw, alpha = .5) +
labs(title = 'Count of 5.5+ magnitude earthquakes, 1965-2016') +
stat_function(
fun = function(x) {(dnorm(x, eq.summary$mean, eq.summary$sd)) * eq.summary$count * bw},
color = "blue", size = 1, alpha = .5
) +
scale_y_continuous(minor_breaks = seq(0, 1E5, by = 250)) +
scale_x_continuous(minor_breaks = seq(0, 9.2, by = bw))

Under this bad normal model, how many 7.0+ earthquakes would be expected over the time period?
pnorm(7, mean = eq.summary$mean, sd = eq.summary$sd, lower.tail = FALSE) * eq.summary$count
[1] 96.66064
But in actuality, 738 were observed. As expected, a normal distribution is an exceptionally poor approximation to earthquake magnitudes. Besides the truncation issue where no earthquakes below 5.5 were included in the data, there is also an obvious long right tail.
Computing averages
Even though earthquake magnitudes clearly do not follow the Gaussian distribution, suitably normalized averages will eventually do so as a consequence of the CLT. Let’s look at the average magnitudes of all 5.5+ earthquakes by month.
eq.by.month <- earthquakes %>%
mutate(
TruncDate = date %>% floor_date('month')
)
# Format data
eq.by.month.agg <- eq.by.month %>%
group_by(TruncDate) %>%
summarise(
AvgMagnitude = mean(Magnitude)
, n = length(Magnitude)
)
eq.mo.summary <- summary_stats(eq.by.month.agg, 'AvgMagnitude')
print(eq.mo.summary)
bw <- .025
eq.by.month.agg %>%
ggplot(aes(x = AvgMagnitude)) +
geom_histogram(binwidth = bw, alpha = .5) +
labs(title = 'Average magnitude of 5.5+ earthquakes per month, 1965-2016', x = 'Avg. Monthly Magnitude') +
stat_function(
fun = function(x) {(dnorm(x, eq.mo.summary$mean, eq.mo.summary$sd)) * eq.mo.summary$count * bw},
color = "blue", size = 1, alpha = .5
) +
scale_y_continuous(minor_breaks = seq(0, 1E5, by = 250)) +
scale_x_continuous(breaks = seq(0, 9.2, by = .1), minor_breaks = seq(0, 9.2, by = bw))

This is already much closer to a normal distribution. Under this model, how many months would we expect to see an average earthquake magnitude of 6.1+?
pnorm(6.1, mean = eq.mo.summary$mean, sd = eq.mo.summary$sd, lower.tail = FALSE) * eq.mo.summary$count
[1] 8.622397
In actuality, 22 months were observed. While the model is still not a great fit, we see that simply aggregating the data led to a distribution that is better approximated by the normal.
Bootstrapping
Finally, consider a single bootstrapped earthquake magnitudes dataset. What would those average monthly magnitudes look like?
# Set seed for reproducible results
set.seed(123)
# Generate a single dataset
eq.boot.first <- sample_frac(eq.by.month, replace = TRUE) %>%
group_by(TruncDate) %>%
summarise(
AvgMagnitude = mean(Magnitude)
, n = length(Magnitude)
)
eq.boot.summary <- summary_stats(eq.boot.first, 'AvgMagnitude')
print(eq.boot.summary)
bw <- .025
eq.boot.first %>%
ggplot(aes(x = AvgMagnitude)) +
geom_histogram(binwidth = bw, alpha = .5) +
labs(title = 'Average magnitude of 5.5+ earthquakes per month,\n bootstrapped data', x = 'Avg. Monthly Magnitude') +
stat_function(
fun = function(x) {(dnorm(x, eq.boot.summary$mean, eq.boot.summary$sd)) * eq.boot.summary$count * bw},
color = "blue", size = 1, alpha = .5
) +
scale_y_continuous(minor_breaks = seq(0, 1E5, by = 250)) +
scale_x_continuous(breaks = seq(0, 9.2, by = .1), minor_breaks = seq(0, 9.2, by = bw))

pnorm(6.1, mean = eq.boot.summary$mean, sd = eq.boot.summary$sd, lower.tail = FALSE) * eq.boot.summary$count
[1] 23.62118
In actuality, 27 months were observed in this resampled dataset. So the bootstrapped estimate already provides better results than a model using the actual historical data! Is this a feature or coincidence? Let’s create 1,000 bootstrapped datasets to check.
set.seed(123)
boot.results <- data.frame()
# Bootstrapping loop
for (i in 1:1000) {
eq.boot <- sample_frac(eq.by.month, replace = TRUE) %>%
group_by(TruncDate) %>%
summarise(
AvgMagnitude = mean(Magnitude)
, n = length(Magnitude)
)
eq.boot.summary <- summary_stats(eq.boot, 'AvgMagnitude')
boot.results <- rbind(
boot.results
, data.frame(
estimated = pnorm(6.1, mean = eq.boot.summary$mean, sd = eq.boot.summary$sd, lower.tail = FALSE) * eq.boot.summary$count
, observed = eq.boot %>% filter(AvgMagnitude >= 6.1) %>% nrow()
)
)
}
Here’s a plot of the estimates of the number of months with an average earthquake magnitude of 6.1+.
boot.preds <- summary_stats(boot.results, 'estimated')
print(boot.preds)
bw <- 1
boot.results %>%
ggplot(aes(x = estimated)) +
geom_histogram(binwidth = bw, alpha = .5) +
labs(title = 'Bootstrapped predictions of count of months experiencing a 6.1+\n average magnitude when averaging across 5.5+ earthquakes\n over a 52-year period', x = 'Prediction') +
stat_function(
fun = function(x) {(dnorm(x, boot.preds$mean, boot.preds$sd)) * boot.preds$count * bw},
color = "blue", size = 1, alpha = .5
) +
scale_x_continuous(minor_breaks = 1:100)

We’ve finally managed to create a normal distribution out of earthquake data. Note that the observed mean across all simulations, 23.61, falls very close to the true count observed in the historical data set, 22.
How can this be? This last plot is a plot of averages: each observation is a count of months across the entire 52-year period, not a count of individual months anymore. We know that within each 52-year history earthquakes are not independent; however, the unit of analysis is now the set of “alternate 52-year histories” which are indeed independent from one another. With 1,000 of these alternate histories the normal approximation is very good.
After all that, here are my takeaways.
- Bootstrapping does not require the usual modeling assumptions on a dataset. If all you’re trying to do is estimate a quantity like mean or standard deviation, it doesn’t matter if the data follows a typical distribution, if individual observations are i.i.d., if there is time dependence, etc.
- Plotting bootstrapped estimates tends to result in a normal distribution even when the underlying phenomenon is not normal, if there’s time dependence, etc.
LS0tDQp0aXRsZTogIlNpZ25pZmljYW50IGVhcnRocXVha2VzIg0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOg0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDoNCiAgICAgIGNvbGxhcHNlZDogRkFMU0UNCi0tLQ0KDQpBIGRlbW9uc3RyYXRpb24gb2YgdGhlIENlbnRyYWwgTGltaXQgVGhlb3JlbSAoQ0xUKSB1c2luZyBhbiBbZWFydGhxdWFrZSBkYXRhc2V0XShodHRwczovL3d3dy5rYWdnbGUuY29tL3VzZ3MvZWFydGhxdWFrZS1kYXRhYmFzZSkgZnJvbSB0aGUgVVNHUyB2aWEgS2FnZ2xlLg0KDQojIFNldHVwDQoNCkxvYWQgcGFja2FnZXMgdXNlZCBpbiB0aGUgYW5hbHlzaXMuDQoNCmBgYHtyIHNldHVwLCBtZXNzYWdlID0gRkFMU0UsIHdhcm5pbmcgPSBGQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShsdWJyaWRhdGUpDQpgYGANCg0KSW1wb3J0IHRoZSBkYXRhLg0KDQpgYGB7cn0NCmVhcnRocXVha2VzIDwtIHJlYWQuY3N2KCdlYXJ0aHF1YWtlcy5jc3YnKSAlPiUNCiAgbXV0YXRlKA0KICAgIG4gPSAxDQogICAgLCBEYXRlID0gYXMuY2hhcmFjdGVyKERhdGUpICU+JSBzdHJfc3ViKDEsIDEwKQ0KICAgICwgZGF0ZSA9IERhdGUgJT4lIG1keShxdWlldCA9IFRSVUUpDQogICkNCmVhcnRocXVha2VzW2lzLm5hKGVhcnRocXVha2VzJGRhdGUpICU+JSB3aGljaCgpLCAnZGF0ZSddIDwtDQogIGVhcnRocXVha2VzW2lzLm5hKGVhcnRocXVha2VzJGRhdGUpICU+JSB3aGljaCgpLCAnRGF0ZSddICU+JSB5bWQoKQ0KYGBgDQoNCkNyZWF0ZSBhIHN1bW1hcnkgZnVuY3Rpb24uDQoNCmBgYHtyfQ0Kc3VtbWFyeV9zdGF0cyA8LSBmdW5jdGlvbihkYXRhX2ZyYW1lLCB2YXJfbmFtZSkgew0KICBkYXRhLmZyYW1lKA0KICAgIGNvdW50ID0gbGVuZ3RoKGRhdGFfZnJhbWVbLCB2YXJfbmFtZV0gJT4lIHVubGlzdCgpKQ0KICAgICwgbWluID0gbWluKGRhdGFfZnJhbWVbLCB2YXJfbmFtZV0gJT4lIHVubGlzdCgpKQ0KICAgICwgbWVkaWFuID0gbWVkaWFuKGRhdGFfZnJhbWVbLCB2YXJfbmFtZV0gJT4lIHVubGlzdCgpKQ0KICAgICwgbWF4ID0gbWF4KGRhdGFfZnJhbWVbLCB2YXJfbmFtZV0gJT4lIHVubGlzdCgpKQ0KICAgICwgbWVhbiA9IG1lYW4oZGF0YV9mcmFtZVssIHZhcl9uYW1lXSAlPiUgdW5saXN0KCkpDQogICAgLCBzZCA9IHNkKGRhdGFfZnJhbWVbLCB2YXJfbmFtZV0gJT4lIHVubGlzdCgpKQ0KICAgICkNCn0NCmBgYA0KDQoNCiMgVGhlIG5vcm1hbCBkaXN0cmlidXRpb24gbWlzdGFrZQ0KDQpgYGB7cn0NCmVxLnN1bW1hcnkgPC0gc3VtbWFyeV9zdGF0cyhlYXJ0aHF1YWtlcywgJ01hZ25pdHVkZScpDQpwcmludChlcS5zdW1tYXJ5KQ0KYGBgDQoNCk5hdHVyYWxseSwganVzdCBiZWNhdXNlIGEgbWVhbiBhbmQgc3RhbmRhcmQgZGV2aWF0aW9uIGNhbiBiZSBjb21wdXRlZCBmb3IgYSBkYXRhc2V0IGRvZXNuJ3QgbWVhbiBhbnl0aGluZyBzZW5zaWJsZSBjYW4gYmUgc2FpZCBhYm91dCB0aGUgbWVjaGFuaXNtIHVuZGVybHlpbmcgdGhlIGRhdGEgZ2VuZXJhdGluZyBwcm9jZXNzLiBTYXkgd2UgbWFkZSB0aGUgbWlzdGFrZSBvZiBhcHBseWluZyBhIG5vcm1hbCAoR2F1c3NpYW4pIGRpc3RyaWJ1dGlvbiB0byB0aGUgZGF0YSBhbmQgY29tcHV0aW5nIHRoZSBpbXBsaWVkIGNvdW50cyBvZiBlYXJ0aHF1YWtlcyBhYm92ZSBhIGNlcnRhaW4gbWFnbml0dWRlLiBUbyBpbGx1c3RyYXRlLCBoZXJlJ3MgYSBxdWljayBwbG90IG9mIHRoZSBgciBlcS5zdW1tYXJ5JGNvdW50ICU+JSBJKClgIGVhcnRocXVha2UgbWFnbml0dWRlcyB3aXRoIHRoZSBkaXN0cmlidXRpb24gTihgciBlcS5zdW1tYXJ5JG1lYW4gJT4lIHJvdW5kKDIpICU+JSBJKClgLCBgciBlcS5zdW1tYXJ5JHNkICU+JSByb3VuZCgyKSAlPiUgSSgpYDxzdXA+Mjwvc3VwPikgc3VwZXJpbXBvc2VkLg0KDQpgYGB7ciwgZmlnLndpZHRoID0gNiwgZmlnLmhlaWdodCA9IDN9DQpidyA8LSAuMQ0KZWFydGhxdWFrZXMgJT4lDQogIGdncGxvdChhZXMoeCA9IE1hZ25pdHVkZSkpICsNCiAgZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSBidywgYWxwaGEgPSAuNSkgKw0KICBsYWJzKHRpdGxlID0gJ0NvdW50IG9mIDUuNSsgbWFnbml0dWRlIGVhcnRocXVha2VzLCAxOTY1LTIwMTYnKSArDQogIHN0YXRfZnVuY3Rpb24oDQogICAgZnVuID0gZnVuY3Rpb24oeCkgeyhkbm9ybSh4LCBlcS5zdW1tYXJ5JG1lYW4sIGVxLnN1bW1hcnkkc2QpKSAqIGVxLnN1bW1hcnkkY291bnQgKiBid30sDQogICAgY29sb3IgPSAiYmx1ZSIsIHNpemUgPSAxLCBhbHBoYSA9IC41DQogICkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobWlub3JfYnJlYWtzID0gc2VxKDAsIDFFNSwgYnkgPSAyNTApKSArDQogIHNjYWxlX3hfY29udGludW91cyhtaW5vcl9icmVha3MgPSBzZXEoMCwgOS4yLCBieSA9IGJ3KSkNCmBgYA0KDQpVbmRlciB0aGlzIGJhZCBub3JtYWwgbW9kZWwsIGhvdyBtYW55IDcuMCsgZWFydGhxdWFrZXMgd291bGQgYmUgZXhwZWN0ZWQgb3ZlciB0aGUgdGltZSBwZXJpb2Q/DQoNCmBgYHtyfQ0KcG5vcm0oNywgbWVhbiA9IGVxLnN1bW1hcnkkbWVhbiwgc2QgPSBlcS5zdW1tYXJ5JHNkLCBsb3dlci50YWlsID0gRkFMU0UpICogZXEuc3VtbWFyeSRjb3VudA0KYGBgDQoNCkJ1dCBpbiBhY3R1YWxpdHksIGByIGVhcnRocXVha2VzICU+JSBmaWx0ZXIoTWFnbml0dWRlID49IDcpICU+JSBucm93KCkgJT4lIEkoKWAgd2VyZSBvYnNlcnZlZC4gQXMgZXhwZWN0ZWQsIGEgbm9ybWFsIGRpc3RyaWJ1dGlvbiBpcyBhbiBleGNlcHRpb25hbGx5IHBvb3IgYXBwcm94aW1hdGlvbiB0byBlYXJ0aHF1YWtlIG1hZ25pdHVkZXMuIEJlc2lkZXMgdGhlIHRydW5jYXRpb24gaXNzdWUgd2hlcmUgbm8gZWFydGhxdWFrZXMgYmVsb3cgNS41IHdlcmUgaW5jbHVkZWQgaW4gdGhlIGRhdGEsIHRoZXJlIGlzIGFsc28gYW4gb2J2aW91cyBsb25nIHJpZ2h0IHRhaWwuDQoNCiMgQ29tcHV0aW5nIGF2ZXJhZ2VzDQoNCkV2ZW4gdGhvdWdoIGVhcnRocXVha2UgbWFnbml0dWRlcyBjbGVhcmx5IGRvIG5vdCBmb2xsb3cgdGhlIEdhdXNzaWFuIGRpc3RyaWJ1dGlvbiwgc3VpdGFibHkgbm9ybWFsaXplZCBhdmVyYWdlcyB3aWxsIGV2ZW50dWFsbHkgZG8gc28gYXMgYSBjb25zZXF1ZW5jZSBvZiB0aGUgQ0xULiBMZXQncyBsb29rIGF0IHRoZSBhdmVyYWdlIG1hZ25pdHVkZXMgb2YgYWxsIDUuNSsgZWFydGhxdWFrZXMgYnkgbW9udGguDQoNCmBgYHtyfQ0KZXEuYnkubW9udGggPC0gZWFydGhxdWFrZXMgJT4lDQogIG11dGF0ZSgNCiAgICBUcnVuY0RhdGUgPSBkYXRlICU+JSBmbG9vcl9kYXRlKCdtb250aCcpDQogICkNCiMgRm9ybWF0IGRhdGENCmVxLmJ5Lm1vbnRoLmFnZyA8LSBlcS5ieS5tb250aCAlPiUNCiAgZ3JvdXBfYnkoVHJ1bmNEYXRlKSAlPiUNCiAgc3VtbWFyaXNlKA0KICAgIEF2Z01hZ25pdHVkZSA9IG1lYW4oTWFnbml0dWRlKQ0KICAgICwgbiA9IGxlbmd0aChNYWduaXR1ZGUpDQogICkNCmBgYA0KDQpgYGB7cn0NCmVxLm1vLnN1bW1hcnkgPC0gc3VtbWFyeV9zdGF0cyhlcS5ieS5tb250aC5hZ2csICdBdmdNYWduaXR1ZGUnKQ0KcHJpbnQoZXEubW8uc3VtbWFyeSkNCmBgYA0KDQpgYGB7ciwgZmlnLndpZHRoID0gNiwgZmlnLmhlaWdodCA9IDN9DQpidyA8LSAuMDI1DQplcS5ieS5tb250aC5hZ2cgJT4lDQogIGdncGxvdChhZXMoeCA9IEF2Z01hZ25pdHVkZSkpICsNCiAgZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSBidywgYWxwaGEgPSAuNSkgKw0KICBsYWJzKHRpdGxlID0gJ0F2ZXJhZ2UgbWFnbml0dWRlIG9mIDUuNSsgZWFydGhxdWFrZXMgcGVyIG1vbnRoLCAxOTY1LTIwMTYnLCB4ID0gJ0F2Zy4gTW9udGhseSBNYWduaXR1ZGUnKSArDQogIHN0YXRfZnVuY3Rpb24oDQogICAgZnVuID0gZnVuY3Rpb24oeCkgeyhkbm9ybSh4LCBlcS5tby5zdW1tYXJ5JG1lYW4sIGVxLm1vLnN1bW1hcnkkc2QpKSAqIGVxLm1vLnN1bW1hcnkkY291bnQgKiBid30sDQogICAgY29sb3IgPSAiYmx1ZSIsIHNpemUgPSAxLCBhbHBoYSA9IC41DQogICkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobWlub3JfYnJlYWtzID0gc2VxKDAsIDFFNSwgYnkgPSAyNTApKSArDQogIHNjYWxlX3hfY29udGludW91cyhicmVha3MgPSBzZXEoMCwgOS4yLCBieSA9IC4xKSwgbWlub3JfYnJlYWtzID0gc2VxKDAsIDkuMiwgYnkgPSBidykpDQpgYGANCg0KVGhpcyBpcyBhbHJlYWR5IG11Y2ggY2xvc2VyIHRvIGEgbm9ybWFsIGRpc3RyaWJ1dGlvbi4gVW5kZXIgdGhpcyBtb2RlbCwgaG93IG1hbnkgbW9udGhzIHdvdWxkIHdlIGV4cGVjdCB0byBzZWUgYW4gYXZlcmFnZSBlYXJ0aHF1YWtlIG1hZ25pdHVkZSBvZiA2LjErPw0KDQpgYGB7cn0NCnBub3JtKDYuMSwgbWVhbiA9IGVxLm1vLnN1bW1hcnkkbWVhbiwgc2QgPSBlcS5tby5zdW1tYXJ5JHNkLCBsb3dlci50YWlsID0gRkFMU0UpICogZXEubW8uc3VtbWFyeSRjb3VudA0KYGBgDQoNCkluIGFjdHVhbGl0eSwgYHIgZXEuYnkubW9udGguYWdnICAlPiUgZmlsdGVyKEF2Z01hZ25pdHVkZSA+PSA2LjEpICU+JSBucm93KCkgJT4lIEkoKWAgbW9udGhzIHdlcmUgb2JzZXJ2ZWQuIFdoaWxlIHRoZSBtb2RlbCBpcyBzdGlsbCBub3QgYSBncmVhdCBmaXQsIHdlIHNlZSB0aGF0IHNpbXBseSBhZ2dyZWdhdGluZyB0aGUgZGF0YSBsZWQgdG8gYSBkaXN0cmlidXRpb24gdGhhdCBpcyBiZXR0ZXIgYXBwcm94aW1hdGVkIGJ5IHRoZSBub3JtYWwuDQoNCiMgQm9vdHN0cmFwcGluZw0KDQpGaW5hbGx5LCBjb25zaWRlciBhIHNpbmdsZSBib290c3RyYXBwZWQgZWFydGhxdWFrZSBtYWduaXR1ZGVzIGRhdGFzZXQuIFdoYXQgd291bGQgdGhvc2UgYXZlcmFnZSBtb250aGx5IG1hZ25pdHVkZXMgbG9vayBsaWtlPw0KDQpgYGB7ciwgZmlnLndpZHRoID0gNiwgZmlnLmhlaWdodCA9IDN9DQojIFNldCBzZWVkIGZvciByZXByb2R1Y2libGUgcmVzdWx0cw0Kc2V0LnNlZWQoMTIzKQ0KIyBHZW5lcmF0ZSBhIHNpbmdsZSBkYXRhc2V0DQplcS5ib290LmZpcnN0IDwtIHNhbXBsZV9mcmFjKGVxLmJ5Lm1vbnRoLCByZXBsYWNlID0gVFJVRSkgJT4lDQogIGdyb3VwX2J5KFRydW5jRGF0ZSkgJT4lDQogIHN1bW1hcmlzZSgNCiAgICBBdmdNYWduaXR1ZGUgPSBtZWFuKE1hZ25pdHVkZSkNCiAgICAsIG4gPSBsZW5ndGgoTWFnbml0dWRlKQ0KICApDQpgYGANCg0KYGBge3J9DQplcS5ib290LnN1bW1hcnkgPC0gc3VtbWFyeV9zdGF0cyhlcS5ib290LmZpcnN0LCAnQXZnTWFnbml0dWRlJykNCnByaW50KGVxLmJvb3Quc3VtbWFyeSkNCmBgYA0KDQpgYGB7ciwgZmlnLndpZHRoID0gNiwgZmlnLmhlaWdodCA9IDN9DQpidyA8LSAuMDI1DQplcS5ib290LmZpcnN0ICU+JQ0KICBnZ3Bsb3QoYWVzKHggPSBBdmdNYWduaXR1ZGUpKSArDQogIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gYncsIGFscGhhID0gLjUpICsNCiAgbGFicyh0aXRsZSA9ICdBdmVyYWdlIG1hZ25pdHVkZSBvZiA1LjUrIGVhcnRocXVha2VzIHBlciBtb250aCxcbiBib290c3RyYXBwZWQgZGF0YScsIHggPSAnQXZnLiBNb250aGx5IE1hZ25pdHVkZScpICsNCiAgc3RhdF9mdW5jdGlvbigNCiAgICBmdW4gPSBmdW5jdGlvbih4KSB7KGRub3JtKHgsIGVxLmJvb3Quc3VtbWFyeSRtZWFuLCBlcS5ib290LnN1bW1hcnkkc2QpKSAqIGVxLmJvb3Quc3VtbWFyeSRjb3VudCAqIGJ3fSwNCiAgICBjb2xvciA9ICJibHVlIiwgc2l6ZSA9IDEsIGFscGhhID0gLjUNCiAgKSArDQogIHNjYWxlX3lfY29udGludW91cyhtaW5vcl9icmVha3MgPSBzZXEoMCwgMUU1LCBieSA9IDI1MCkpICsNCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLCA5LjIsIGJ5ID0gLjEpLCBtaW5vcl9icmVha3MgPSBzZXEoMCwgOS4yLCBieSA9IGJ3KSkNCmBgYA0KDQpgYGB7cn0NCnBub3JtKDYuMSwgbWVhbiA9IGVxLmJvb3Quc3VtbWFyeSRtZWFuLCBzZCA9IGVxLmJvb3Quc3VtbWFyeSRzZCwgbG93ZXIudGFpbCA9IEZBTFNFKSAqIGVxLmJvb3Quc3VtbWFyeSRjb3VudA0KYGBgDQoNCkluIGFjdHVhbGl0eSwgYHIgZXEuYm9vdC5maXJzdCAlPiUgZmlsdGVyKEF2Z01hZ25pdHVkZSA+PSA2LjEpICU+JSBucm93KCkgJT4lIEkoKWAgbW9udGhzIHdlcmUgb2JzZXJ2ZWQgaW4gdGhpcyByZXNhbXBsZWQgZGF0YXNldC4gU28gdGhlIGJvb3RzdHJhcHBlZCBlc3RpbWF0ZSBhbHJlYWR5IHByb3ZpZGVzIGJldHRlciByZXN1bHRzIHRoYW4gYSBtb2RlbCB1c2luZyB0aGUgYWN0dWFsIGhpc3RvcmljYWwgZGF0YSEgSXMgdGhpcyBhIGZlYXR1cmUgb3IgY29pbmNpZGVuY2U/IExldCdzIGNyZWF0ZSAxLDAwMCBib290c3RyYXBwZWQgZGF0YXNldHMgdG8gY2hlY2suDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMTIzKQ0KYm9vdC5yZXN1bHRzIDwtIGRhdGEuZnJhbWUoKQ0KIyBCb290c3RyYXBwaW5nIGxvb3ANCmZvciAoaSBpbiAxOjEwMDApIHsNCiAgZXEuYm9vdCA8LSBzYW1wbGVfZnJhYyhlcS5ieS5tb250aCwgcmVwbGFjZSA9IFRSVUUpICU+JQ0KICBncm91cF9ieShUcnVuY0RhdGUpICU+JQ0KICBzdW1tYXJpc2UoDQogICAgQXZnTWFnbml0dWRlID0gbWVhbihNYWduaXR1ZGUpDQogICAgLCBuID0gbGVuZ3RoKE1hZ25pdHVkZSkNCiAgKQ0KICBlcS5ib290LnN1bW1hcnkgPC0gc3VtbWFyeV9zdGF0cyhlcS5ib290LCAnQXZnTWFnbml0dWRlJykNCiAgYm9vdC5yZXN1bHRzIDwtIHJiaW5kKA0KICAgIGJvb3QucmVzdWx0cw0KICAgICwgZGF0YS5mcmFtZSgNCiAgICAgIGVzdGltYXRlZCA9IHBub3JtKDYuMSwgbWVhbiA9IGVxLmJvb3Quc3VtbWFyeSRtZWFuLCBzZCA9IGVxLmJvb3Quc3VtbWFyeSRzZCwgbG93ZXIudGFpbCA9IEZBTFNFKSAqIGVxLmJvb3Quc3VtbWFyeSRjb3VudA0KICAgICAgLCBvYnNlcnZlZCA9IGVxLmJvb3QgICU+JSBmaWx0ZXIoQXZnTWFnbml0dWRlID49IDYuMSkgJT4lIG5yb3coKQ0KICAgICkNCiAgKQ0KfQ0KYGBgDQoNCkhlcmUncyBhIHBsb3Qgb2YgdGhlIGVzdGltYXRlcyBvZiB0aGUgbnVtYmVyIG9mIG1vbnRocyB3aXRoIGFuIGF2ZXJhZ2UgZWFydGhxdWFrZSBtYWduaXR1ZGUgb2YgNi4xKy4NCg0KYGBge3J9DQpib290LnByZWRzIDwtIHN1bW1hcnlfc3RhdHMoYm9vdC5yZXN1bHRzLCAnZXN0aW1hdGVkJykNCnByaW50KGJvb3QucHJlZHMpDQpgYGANCg0KDQpgYGB7ciwgZmlnLndpZHRoID0gNiwgZmlnLmhlaWdodCA9IDN9DQpidyA8LSAxDQpib290LnJlc3VsdHMgJT4lDQogIGdncGxvdChhZXMoeCA9IGVzdGltYXRlZCkpICsNCiAgZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSBidywgYWxwaGEgPSAuNSkgKw0KICBsYWJzKHRpdGxlID0gJ0Jvb3RzdHJhcHBlZCBwcmVkaWN0aW9ucyBvZiBjb3VudCBvZiBtb250aHMgZXhwZXJpZW5jaW5nIGEgNi4xK1xuIGF2ZXJhZ2UgbWFnbml0dWRlIHdoZW4gYXZlcmFnaW5nIGFjcm9zcyA1LjUrIGVhcnRocXVha2VzXG4gb3ZlciBhIDUyLXllYXIgcGVyaW9kJywgeCA9ICdQcmVkaWN0aW9uJykgKw0KICBzdGF0X2Z1bmN0aW9uKA0KICAgIGZ1biA9IGZ1bmN0aW9uKHgpIHsoZG5vcm0oeCwgYm9vdC5wcmVkcyRtZWFuLCBib290LnByZWRzJHNkKSkgKiBib290LnByZWRzJGNvdW50ICogYnd9LA0KICAgIGNvbG9yID0gImJsdWUiLCBzaXplID0gMSwgYWxwaGEgPSAuNQ0KICApICsNCiAgc2NhbGVfeF9jb250aW51b3VzKG1pbm9yX2JyZWFrcyA9IDE6MTAwKQ0KYGBgDQoNCldlJ3ZlIGZpbmFsbHkgbWFuYWdlZCB0byBjcmVhdGUgYSBub3JtYWwgZGlzdHJpYnV0aW9uIG91dCBvZiBlYXJ0aHF1YWtlIGRhdGEuIE5vdGUgdGhhdCB0aGUgb2JzZXJ2ZWQgbWVhbiBhY3Jvc3MgYWxsIHNpbXVsYXRpb25zLCBgciBib290LnByZWRzJG1lYW4gJT4lIHJvdW5kKDIpICU+JSBJKClgLCBmYWxscyB2ZXJ5IGNsb3NlIHRvIHRoZSB0cnVlIGNvdW50IG9ic2VydmVkIGluIHRoZSBoaXN0b3JpY2FsIGRhdGEgc2V0LCBgciBlcS5ieS5tb250aC5hZ2cgICU+JSBmaWx0ZXIoQXZnTWFnbml0dWRlID49IDYuMSkgJT4lIG5yb3coKSAlPiUgSSgpYC4NCg0KSG93IGNhbiB0aGlzIGJlPyBUaGlzIGxhc3QgcGxvdCBpcyBhIHBsb3Qgb2YgYXZlcmFnZXM6IGVhY2ggb2JzZXJ2YXRpb24gaXMgYSBjb3VudCBvZiBtb250aHMgYWNyb3NzIHRoZSBlbnRpcmUgNTIteWVhciBwZXJpb2QsIG5vdCBhIGNvdW50IG9mIGluZGl2aWR1YWwgbW9udGhzIGFueW1vcmUuIFdlIGtub3cgdGhhdCB3aXRoaW4gZWFjaCA1Mi15ZWFyIGhpc3RvcnkgZWFydGhxdWFrZXMgYXJlIG5vdCBpbmRlcGVuZGVudDsgaG93ZXZlciwgdGhlIHVuaXQgb2YgYW5hbHlzaXMgaXMgbm93IHRoZSBzZXQgb2YgImFsdGVybmF0ZSA1Mi15ZWFyIGhpc3RvcmllcyIgd2hpY2ggYXJlIGluZGVlZCBpbmRlcGVuZGVudCBmcm9tIG9uZSBhbm90aGVyLiBXaXRoIDEsMDAwIG9mIHRoZXNlIGFsdGVybmF0ZSBoaXN0b3JpZXMgdGhlIG5vcm1hbCBhcHByb3hpbWF0aW9uIGlzIHZlcnkgZ29vZC4NCg0KQWZ0ZXIgYWxsIHRoYXQsIGhlcmUgYXJlIG15IHRha2Vhd2F5cy4NCg0KMSkgQm9vdHN0cmFwcGluZyBkb2VzIG5vdCByZXF1aXJlIHRoZSB1c3VhbCBtb2RlbGluZyBhc3N1bXB0aW9ucyBvbiBhIGRhdGFzZXQuIElmIGFsbCB5b3UncmUgdHJ5aW5nIHRvIGRvIGlzIGVzdGltYXRlIGEgcXVhbnRpdHkgbGlrZSBtZWFuIG9yIHN0YW5kYXJkIGRldmlhdGlvbiwgaXQgZG9lc24ndCBtYXR0ZXIgaWYgdGhlIGRhdGEgZm9sbG93cyBhIHR5cGljYWwgZGlzdHJpYnV0aW9uLCBpZiBpbmRpdmlkdWFsIG9ic2VydmF0aW9ucyBhcmUgaS5pLmQuLCBpZiB0aGVyZSBpcyB0aW1lIGRlcGVuZGVuY2UsIGV0Yy4NCjIpIFBsb3R0aW5nIGJvb3RzdHJhcHBlZCBlc3RpbWF0ZXMgdGVuZHMgdG8gcmVzdWx0IGluIGEgbm9ybWFsIGRpc3RyaWJ1dGlvbiBldmVuIHdoZW4gdGhlIHVuZGVybHlpbmcgcGhlbm9tZW5vbiBpcyBub3Qgbm9ybWFsLCBpZiB0aGVyZSdzIHRpbWUgZGVwZW5kZW5jZSwgZXRjLg==