Last time, we focused
on understanding variation within a single variable. This time, we’re
understanding covariation, which is the relationship between two or more
variables. This is where we start to ask more complex questions, like
“Do employees with higher salaries tend to have more years of
experience?”
library(tidyverse)
attrition_data <- read_csv("https://raw.githubusercontent.com/pplonski/datasets-for-start/refs/heads/master/employee_attrition/HR-Employee-Attrition-All.csv")
#Convert chr columns to factor
attrition_data <- attrition_data %>% mutate(across(where(is.character), as.factor))
Understanding Covariation
Covariation is the tendency for the values of two or more variables
to vary together in a related way. It’s the core of most statistical
analysis. When two variables covary, you can predict one using the
other. For example, if you know an employee’s
MonthlyIncome
, you have a better idea of their
JobLevel
.
Visualizing Covariation: Continuous & Categorical Variables
A very common task is to explore how a continuous variable’s
distribution changes across different categories.
Boxplots (geom_boxplot
)
While frequency polygons (geom_freqpoly
) are good for
comparing densities, boxplots are a fantastic visual shorthand for
comparing continuous distributions across categories. They are compact
and highlight the median, the interquartile range (IQR), and
outliers.
Let’s compare the MonthlyIncome
across different
JobLevels
.
ggplot(data = attrition_data, mapping = aes(x = factor(JobLevel), y = MonthlyIncome)) +
geom_boxplot()

The plot shows a clear positive relationship: as JobLevel increases,
the median MonthlyIncome also increases.
Reordering and Flipping
Sometimes your categorical variable is unordered, and the default
alphabetical order isn’t the most informative. We can use
reorder()
to arrange the categories based on a summary
statistic of the continuous variable, like the median. Let’s compare
MonthlyIncome
across different
EducationFields
.
ggplot(data = attrition_data, mapping = aes(x = reorder(EducationField, MonthlyIncome, FUN = median), y = MonthlyIncome)) +
geom_boxplot() +
coord_flip()

Flipping the coordinates (coord_flip()
) is useful when
the category names are long, making the labels more readable.
Visualizing Covariation: Two Categorical Variables
To visualize the relationship between two categorical variables, we
need to count how many observations fall into each combination of
categories.
geom_count()
This is the simplest way. geom_count() automatically calculates the
counts and represents them with the size of the circles. Let’s look at
the relationship between JobRole and Department.
ggplot(data = attrition_data) +
geom_count(mapping = aes(x = Department, y = JobRole))

Manual Counting with dplyr
and
geom_tile()
For more control, we can manually count the combinations using
dplyr
and then use a heat map (geom_tile()
) to
visualize the results. This is often more flexible.
attrition_data %>%
count(Department, JobRole) %>%
ggplot(mapping = aes(x = Department, y = JobRole)) +
geom_tile(mapping = aes(fill = n))

The darker the tile, the more employees in that
Department
/JobRole
combination. This tells us,
for example, that the Research & Development
department
has a large number of Research Scientist
and
Laboratory Technician
roles, which makes perfect sense.
Visualizing Covariation: Two Continuous Variables
The classic way to visualize the relationship between two continuous
variables is with a scatterplot using geom_point()
. Let’s
explore the relationship between MonthlyIncome
and
TotalWorkingYears
.
ggplot(data = attrition_data) +
geom_point(mapping = aes(x = TotalWorkingYears, y = MonthlyIncome))

The Overplotting Problem
For large datasets, scatterplots can suffer from overplotting, where
many points overlap and create a big black blob, hiding the true
distribution.
Let’s pretend our dataset is much larger. Solutions include:
- Transparency (
alpha
): Make the points
semi-transparent so you can see where they are most dense.
ggplot(data = attrition_data) +
geom_point(mapping = aes(x = TotalWorkingYears, y = MonthlyIncome), alpha = 1/5)

- 2D Binning (
geom_bin2d
or
geom_hex
): Divide the plot into 2D bins and use
color to represent the number of points in each bin. This is like a 2D
histogram.
ggplot(data = attrition_data) +
geom_bin2d(mapping = aes(x = TotalWorkingYears, y = MonthlyIncome))

This reveals that most employees are clustered at lower
TotalWorkingYears
and lower MonthlyIncome
.
Patterns and Models
The patterns we see in our data give us clues. A strong relationship
between TotalWorkingYears
and MonthlyIncome
makes sense—we’d expect more experienced employees to earn more.
Sometimes, a dominant relationship can hide other, more subtle ones.
This is where models can be useful during EDA. We can use a model to
“remove” the effect of a variable and then look at the residuals (what’s
left over).
Let’s model the relationship between MonthlyIncome and JobLevel.
library(modelr)
mod <- lm(MonthlyIncome ~ JobLevel, data = attrition_data)
attrition_data_cleaned <- attrition_data %>%
add_residuals(mod)
ggplot(data = attrition_data_cleaned) +
geom_boxplot(mapping = aes(x = JobRole, y = resid)) +
coord_flip()

By plotting the residuals against JobRole
, we are now
looking at the relative salary differences, after accounting for
JobLevel
. This might reveal which JobRoles
are
over or underpaid relative to their JobLevel
.
ggplot2
Conciseness
As you become more comfortable, you can write more concise code by
omitting argument names like data =
and
mapping =
.
Example: Instead of
ggplot(data = attrition_data, mapping = aes(x = JobRole)) + geom_bar()
,
you can write
ggplot(attrition_data, aes(x = JobRole)) + geom_bar()
.
You’ll also see a lot of pipes (%>%
) leading into a
ggplot2 call.
attrition_data %>%
group_by(Education) %>%
summarise(MedianIncome = median(MonthlyIncome)) %>%
ggplot(aes(x = reorder(Education, MedianIncome), y = MedianIncome)) +
geom_bar(stat = "identity")
This is a very common workflow in R, and it’s a powerful way to
transition from data manipulation to visualization.
Tools
G
LS0tDQp0aXRsZTogIkNvdmFyaWF0aW9uIGFuZCBBZHZhbmNlZCBWaXN1YWxpemF0aW9uIFRlY2huaXF1ZXMiDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOg0KICAgIHRvYzogdHJ1ZQ0KICAgIHRvY19mbG9hdDogdHJ1ZQ0KLS0tDQoNCltMYXN0IHRpbWVdKGh0dHBzOi8vcnB1YnMuY29tL3VreTk5NC8xMzQzNjgyKSwgd2UgZm9jdXNlZCBvbiB1bmRlcnN0YW5kaW5nIHZhcmlhdGlvbiB3aXRoaW4gYSBzaW5nbGUgdmFyaWFibGUuIFRoaXMgdGltZSwgd2UncmUgdW5kZXJzdGFuZGluZyBjb3ZhcmlhdGlvbiwgd2hpY2ggaXMgdGhlIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIHR3byBvciBtb3JlIHZhcmlhYmxlcy4gVGhpcyBpcyB3aGVyZSB3ZSBzdGFydCB0byBhc2sgbW9yZSBjb21wbGV4IHF1ZXN0aW9ucywgbGlrZSAiRG8gZW1wbG95ZWVzIHdpdGggaGlnaGVyIHNhbGFyaWVzIHRlbmQgdG8gaGF2ZSBtb3JlIHllYXJzIG9mIGV4cGVyaWVuY2U/Ig0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KYXR0cml0aW9uX2RhdGEgPC0gcmVhZF9jc3YoImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9wcGxvbnNraS9kYXRhc2V0cy1mb3Itc3RhcnQvcmVmcy9oZWFkcy9tYXN0ZXIvZW1wbG95ZWVfYXR0cml0aW9uL0hSLUVtcGxveWVlLUF0dHJpdGlvbi1BbGwuY3N2IikNCiNDb252ZXJ0IGNociBjb2x1bW5zIHRvIGZhY3Rvcg0KYXR0cml0aW9uX2RhdGEgPC0gYXR0cml0aW9uX2RhdGEgJT4lIG11dGF0ZShhY3Jvc3Mod2hlcmUoaXMuY2hhcmFjdGVyKSwgYXMuZmFjdG9yKSkNCmBgYA0KDQojIyBVbmRlcnN0YW5kaW5nIENvdmFyaWF0aW9uDQpDb3ZhcmlhdGlvbiBpcyB0aGUgdGVuZGVuY3kgZm9yIHRoZSB2YWx1ZXMgb2YgdHdvIG9yIG1vcmUgdmFyaWFibGVzIHRvIHZhcnkgdG9nZXRoZXIgaW4gYSByZWxhdGVkIHdheS4gSXQncyB0aGUgY29yZSBvZiBtb3N0IHN0YXRpc3RpY2FsIGFuYWx5c2lzLiBXaGVuIHR3byB2YXJpYWJsZXMgY292YXJ5LCB5b3UgY2FuIHByZWRpY3Qgb25lIHVzaW5nIHRoZSBvdGhlci4gRm9yIGV4YW1wbGUsIGlmIHlvdSBrbm93IGFuIGVtcGxveWVlJ3MgYE1vbnRobHlJbmNvbWVgLCB5b3UgaGF2ZSBhIGJldHRlciBpZGVhIG9mIHRoZWlyIGBKb2JMZXZlbGAuDQoNCiMjIFZpc3VhbGl6aW5nIENvdmFyaWF0aW9uOiBDb250aW51b3VzICYgQ2F0ZWdvcmljYWwgVmFyaWFibGVzDQpBIHZlcnkgY29tbW9uIHRhc2sgaXMgdG8gZXhwbG9yZSBob3cgYSBjb250aW51b3VzIHZhcmlhYmxlJ3MgZGlzdHJpYnV0aW9uIGNoYW5nZXMgYWNyb3NzIGRpZmZlcmVudCBjYXRlZ29yaWVzLg0KDQojIyMgQm94cGxvdHMgKGBnZW9tX2JveHBsb3RgKQ0KV2hpbGUgZnJlcXVlbmN5IHBvbHlnb25zIChgZ2VvbV9mcmVxcG9seWApIGFyZSBnb29kIGZvciBjb21wYXJpbmcgZGVuc2l0aWVzLCBib3hwbG90cyBhcmUgYSBmYW50YXN0aWMgdmlzdWFsIHNob3J0aGFuZCBmb3IgY29tcGFyaW5nIGNvbnRpbnVvdXMgZGlzdHJpYnV0aW9ucyBhY3Jvc3MgY2F0ZWdvcmllcy4gVGhleSBhcmUgY29tcGFjdCBhbmQgaGlnaGxpZ2h0IHRoZSBtZWRpYW4sIHRoZSBpbnRlcnF1YXJ0aWxlIHJhbmdlIChJUVIpLCBhbmQgb3V0bGllcnMuDQoNCkxldCdzIGNvbXBhcmUgdGhlIGBNb250aGx5SW5jb21lYCBhY3Jvc3MgZGlmZmVyZW50IGBKb2JMZXZlbHNgLg0KDQpgYGB7cn0NCmdncGxvdChkYXRhID0gYXR0cml0aW9uX2RhdGEsIG1hcHBpbmcgPSBhZXMoeCA9IGZhY3RvcihKb2JMZXZlbCksIHkgPSBNb250aGx5SW5jb21lKSkgKw0KICBnZW9tX2JveHBsb3QoKQ0KYGBgDQoNClRoZSBwbG90IHNob3dzIGEgY2xlYXIgcG9zaXRpdmUgcmVsYXRpb25zaGlwOiBhcyBKb2JMZXZlbCBpbmNyZWFzZXMsIHRoZSBtZWRpYW4gTW9udGhseUluY29tZSBhbHNvIGluY3JlYXNlcy4NCg0KIyMjIyBSZW9yZGVyaW5nIGFuZCBGbGlwcGluZw0KU29tZXRpbWVzIHlvdXIgY2F0ZWdvcmljYWwgdmFyaWFibGUgaXMgdW5vcmRlcmVkLCBhbmQgdGhlIGRlZmF1bHQgYWxwaGFiZXRpY2FsIG9yZGVyIGlzbid0IHRoZSBtb3N0IGluZm9ybWF0aXZlLiBXZSBjYW4gdXNlIGByZW9yZGVyKClgIHRvIGFycmFuZ2UgdGhlIGNhdGVnb3JpZXMgYmFzZWQgb24gYSBzdW1tYXJ5IHN0YXRpc3RpYyBvZiB0aGUgY29udGludW91cyB2YXJpYWJsZSwgbGlrZSB0aGUgbWVkaWFuLiBMZXQncyBjb21wYXJlIGBNb250aGx5SW5jb21lYCBhY3Jvc3MgZGlmZmVyZW50IGBFZHVjYXRpb25GaWVsZHNgLg0KDQpgYGB7cn0NCmdncGxvdChkYXRhID0gYXR0cml0aW9uX2RhdGEsIG1hcHBpbmcgPSBhZXMoeCA9IHJlb3JkZXIoRWR1Y2F0aW9uRmllbGQsIE1vbnRobHlJbmNvbWUsIEZVTiA9IG1lZGlhbiksIHkgPSBNb250aGx5SW5jb21lKSkgKw0KICBnZW9tX2JveHBsb3QoKSArDQogIGNvb3JkX2ZsaXAoKQ0KYGBgDQoNCkZsaXBwaW5nIHRoZSBjb29yZGluYXRlcyAoYGNvb3JkX2ZsaXAoKWApIGlzIHVzZWZ1bCB3aGVuIHRoZSBjYXRlZ29yeSBuYW1lcyBhcmUgbG9uZywgbWFraW5nIHRoZSBsYWJlbHMgbW9yZSByZWFkYWJsZS4NCg0KIyMgVmlzdWFsaXppbmcgQ292YXJpYXRpb246IFR3byBDYXRlZ29yaWNhbCBWYXJpYWJsZXMNClRvIHZpc3VhbGl6ZSB0aGUgcmVsYXRpb25zaGlwIGJldHdlZW4gdHdvIGNhdGVnb3JpY2FsIHZhcmlhYmxlcywgd2UgbmVlZCB0byBjb3VudCBob3cgbWFueSBvYnNlcnZhdGlvbnMgZmFsbCBpbnRvIGVhY2ggY29tYmluYXRpb24gb2YgY2F0ZWdvcmllcy4NCg0KIyMjIGBnZW9tX2NvdW50KClgDQpUaGlzIGlzIHRoZSBzaW1wbGVzdCB3YXkuIGdlb21fY291bnQoKSBhdXRvbWF0aWNhbGx5IGNhbGN1bGF0ZXMgdGhlIGNvdW50cyBhbmQgcmVwcmVzZW50cyB0aGVtIHdpdGggdGhlIHNpemUgb2YgdGhlIGNpcmNsZXMuIExldCdzIGxvb2sgYXQgdGhlIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIEpvYlJvbGUgYW5kIERlcGFydG1lbnQuDQoNCmBgYHtyfQ0KZ2dwbG90KGRhdGEgPSBhdHRyaXRpb25fZGF0YSkgKw0KICBnZW9tX2NvdW50KG1hcHBpbmcgPSBhZXMoeCA9IERlcGFydG1lbnQsIHkgPSBKb2JSb2xlKSkNCmBgYA0KDQojIyMgTWFudWFsIENvdW50aW5nIHdpdGggYGRwbHlyYCBhbmQgYGdlb21fdGlsZSgpYA0KRm9yIG1vcmUgY29udHJvbCwgd2UgY2FuIG1hbnVhbGx5IGNvdW50IHRoZSBjb21iaW5hdGlvbnMgdXNpbmcgYGRwbHlyYCBhbmQgdGhlbiB1c2UgYSBoZWF0IG1hcCAoYGdlb21fdGlsZSgpYCkgdG8gdmlzdWFsaXplIHRoZSByZXN1bHRzLiBUaGlzIGlzIG9mdGVuIG1vcmUgZmxleGlibGUuDQoNCmBgYHtyfQ0KYXR0cml0aW9uX2RhdGEgJT4lDQogIGNvdW50KERlcGFydG1lbnQsIEpvYlJvbGUpICU+JQ0KICBnZ3Bsb3QobWFwcGluZyA9IGFlcyh4ID0gRGVwYXJ0bWVudCwgeSA9IEpvYlJvbGUpKSArDQogIGdlb21fdGlsZShtYXBwaW5nID0gYWVzKGZpbGwgPSBuKSkNCmBgYA0KDQpUaGUgZGFya2VyIHRoZSB0aWxlLCB0aGUgbW9yZSBlbXBsb3llZXMgaW4gdGhhdCBgRGVwYXJ0bWVudGAvYEpvYlJvbGVgIGNvbWJpbmF0aW9uLiBUaGlzIHRlbGxzIHVzLCBmb3IgZXhhbXBsZSwgdGhhdCB0aGUgYFJlc2VhcmNoICYgRGV2ZWxvcG1lbnRgIGRlcGFydG1lbnQgaGFzIGEgbGFyZ2UgbnVtYmVyIG9mIGBSZXNlYXJjaCBTY2llbnRpc3RgIGFuZCBgTGFib3JhdG9yeSBUZWNobmljaWFuYCByb2xlcywgd2hpY2ggbWFrZXMgcGVyZmVjdCBzZW5zZS4NCg0KIyMgVmlzdWFsaXppbmcgQ292YXJpYXRpb246IFR3byBDb250aW51b3VzIFZhcmlhYmxlcw0KVGhlIGNsYXNzaWMgd2F5IHRvIHZpc3VhbGl6ZSB0aGUgcmVsYXRpb25zaGlwIGJldHdlZW4gdHdvIGNvbnRpbnVvdXMgdmFyaWFibGVzIGlzIHdpdGggYSBzY2F0dGVycGxvdCB1c2luZyBgZ2VvbV9wb2ludCgpYC4gTGV0J3MgZXhwbG9yZSB0aGUgcmVsYXRpb25zaGlwIGJldHdlZW4gYE1vbnRobHlJbmNvbWVgIGFuZCBgVG90YWxXb3JraW5nWWVhcnNgLg0KDQpgYGB7cn0NCmdncGxvdChkYXRhID0gYXR0cml0aW9uX2RhdGEpICsNCiAgZ2VvbV9wb2ludChtYXBwaW5nID0gYWVzKHggPSBUb3RhbFdvcmtpbmdZZWFycywgeSA9IE1vbnRobHlJbmNvbWUpKQ0KYGBgDQoNCiMjIyMgVGhlIE92ZXJwbG90dGluZyBQcm9ibGVtDQpGb3IgbGFyZ2UgZGF0YXNldHMsIHNjYXR0ZXJwbG90cyBjYW4gc3VmZmVyIGZyb20gb3ZlcnBsb3R0aW5nLCB3aGVyZSBtYW55IHBvaW50cyBvdmVybGFwIGFuZCBjcmVhdGUgYSBiaWcgYmxhY2sgYmxvYiwgaGlkaW5nIHRoZSB0cnVlIGRpc3RyaWJ1dGlvbi4NCg0KTGV0J3MgcHJldGVuZCBvdXIgZGF0YXNldCBpcyBtdWNoIGxhcmdlci4gU29sdXRpb25zIGluY2x1ZGU6DQoNCiAtIF9fVHJhbnNwYXJlbmN5X18gKGBhbHBoYWApOiBNYWtlIHRoZSBwb2ludHMgc2VtaS10cmFuc3BhcmVudCBzbyB5b3UgY2FuIHNlZSB3aGVyZSB0aGV5IGFyZSBtb3N0IGRlbnNlLg0KDQpgYGB7cn0NCmdncGxvdChkYXRhID0gYXR0cml0aW9uX2RhdGEpICsNCiAgZ2VvbV9wb2ludChtYXBwaW5nID0gYWVzKHggPSBUb3RhbFdvcmtpbmdZZWFycywgeSA9IE1vbnRobHlJbmNvbWUpLCBhbHBoYSA9IDEvNSkNCmBgYA0KDQogLSBfXzJEIEJpbm5pbmcgKGBnZW9tX2JpbjJkYCBvciBgZ2VvbV9oZXhgKV9fOiBEaXZpZGUgdGhlIHBsb3QgaW50byAyRCBiaW5zIGFuZCB1c2UgY29sb3IgdG8gcmVwcmVzZW50IHRoZSBudW1iZXIgb2YgcG9pbnRzIGluIGVhY2ggYmluLiBUaGlzIGlzIGxpa2UgYSAyRCBoaXN0b2dyYW0uDQoNCmBgYHtyfQ0KZ2dwbG90KGRhdGEgPSBhdHRyaXRpb25fZGF0YSkgKw0KICBnZW9tX2JpbjJkKG1hcHBpbmcgPSBhZXMoeCA9IFRvdGFsV29ya2luZ1llYXJzLCB5ID0gTW9udGhseUluY29tZSkpDQpgYGANCg0KVGhpcyByZXZlYWxzIHRoYXQgbW9zdCBlbXBsb3llZXMgYXJlIGNsdXN0ZXJlZCBhdCBsb3dlciBgVG90YWxXb3JraW5nWWVhcnNgIGFuZCBsb3dlciBgTW9udGhseUluY29tZWAuDQoNCiMjIFBhdHRlcm5zIGFuZCBNb2RlbHMNClRoZSBwYXR0ZXJucyB3ZSBzZWUgaW4gb3VyIGRhdGEgZ2l2ZSB1cyBjbHVlcy4gQSBzdHJvbmcgcmVsYXRpb25zaGlwIGJldHdlZW4gYFRvdGFsV29ya2luZ1llYXJzYCBhbmQgYE1vbnRobHlJbmNvbWVgIG1ha2VzIHNlbnNl4oCUd2UnZCBleHBlY3QgbW9yZSBleHBlcmllbmNlZCBlbXBsb3llZXMgdG8gZWFybiBtb3JlLg0KDQpTb21ldGltZXMsIGEgZG9taW5hbnQgcmVsYXRpb25zaGlwIGNhbiBoaWRlIG90aGVyLCBtb3JlIHN1YnRsZSBvbmVzLiBUaGlzIGlzIHdoZXJlIG1vZGVscyBjYW4gYmUgdXNlZnVsIGR1cmluZyBFREEuIFdlIGNhbiB1c2UgYSBtb2RlbCB0byAicmVtb3ZlIiB0aGUgZWZmZWN0IG9mIGEgdmFyaWFibGUgYW5kIHRoZW4gbG9vayBhdCB0aGUgcmVzaWR1YWxzICh3aGF0J3MgbGVmdCBvdmVyKS4NCg0KTGV0J3MgbW9kZWwgdGhlIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIE1vbnRobHlJbmNvbWUgYW5kIEpvYkxldmVsLg0KDQpgYGB7cn0NCmxpYnJhcnkobW9kZWxyKQ0KbW9kIDwtIGxtKE1vbnRobHlJbmNvbWUgfiBKb2JMZXZlbCwgZGF0YSA9IGF0dHJpdGlvbl9kYXRhKQ0KDQphdHRyaXRpb25fZGF0YV9jbGVhbmVkIDwtIGF0dHJpdGlvbl9kYXRhICU+JQ0KICBhZGRfcmVzaWR1YWxzKG1vZCkNCg0KZ2dwbG90KGRhdGEgPSBhdHRyaXRpb25fZGF0YV9jbGVhbmVkKSArDQogIGdlb21fYm94cGxvdChtYXBwaW5nID0gYWVzKHggPSBKb2JSb2xlLCB5ID0gcmVzaWQpKSArDQogIGNvb3JkX2ZsaXAoKQ0KYGBgDQoNCkJ5IHBsb3R0aW5nIHRoZSByZXNpZHVhbHMgYWdhaW5zdCBgSm9iUm9sZWAsIHdlIGFyZSBub3cgbG9va2luZyBhdCB0aGUgcmVsYXRpdmUgc2FsYXJ5IGRpZmZlcmVuY2VzLCBhZnRlciBhY2NvdW50aW5nIGZvciBgSm9iTGV2ZWxgLiBUaGlzIG1pZ2h0IHJldmVhbCB3aGljaCBgSm9iUm9sZXNgIGFyZSBvdmVyIG9yIHVuZGVycGFpZCByZWxhdGl2ZSB0byB0aGVpciBgSm9iTGV2ZWxgLg0KDQojIyBgZ2dwbG90MmAgQ29uY2lzZW5lc3MgIA0KQXMgeW91IGJlY29tZSBtb3JlIGNvbWZvcnRhYmxlLCB5b3UgY2FuIHdyaXRlIG1vcmUgY29uY2lzZSBjb2RlIGJ5IG9taXR0aW5nIGFyZ3VtZW50IG5hbWVzIGxpa2UgYGRhdGEgPWAgYW5kIGBtYXBwaW5nID1gLg0KDQpFeGFtcGxlOg0KSW5zdGVhZCBvZiBgZ2dwbG90KGRhdGEgPSBhdHRyaXRpb25fZGF0YSwgbWFwcGluZyA9IGFlcyh4ID0gSm9iUm9sZSkpICsgZ2VvbV9iYXIoKWAsIHlvdSBjYW4gd3JpdGUgYGdncGxvdChhdHRyaXRpb25fZGF0YSwgYWVzKHggPSBKb2JSb2xlKSkgKyBnZW9tX2JhcigpYC4NCg0KWW91J2xsIGFsc28gc2VlIGEgbG90IG9mIHBpcGVzIChgJT4lYCkgbGVhZGluZyBpbnRvIGEgZ2dwbG90MiBjYWxsLg0KDQpgYGB7cn0NCmF0dHJpdGlvbl9kYXRhICU+JQ0KICBncm91cF9ieShFZHVjYXRpb24pICU+JQ0KICBzdW1tYXJpc2UoTWVkaWFuSW5jb21lID0gbWVkaWFuKE1vbnRobHlJbmNvbWUpKSAlPiUNCiAgZ2dwbG90KGFlcyh4ID0gcmVvcmRlcihFZHVjYXRpb24sIE1lZGlhbkluY29tZSksIHkgPSBNZWRpYW5JbmNvbWUpKSArDQogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKQ0KYGBgDQoNClRoaXMgaXMgYSB2ZXJ5IGNvbW1vbiB3b3JrZmxvdyBpbiBSLCBhbmQgaXQncyBhIHBvd2VyZnVsIHdheSB0byB0cmFuc2l0aW9uIGZyb20gZGF0YSBtYW5pcHVsYXRpb24gdG8gdmlzdWFsaXphdGlvbi4NCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KVG9vbHMNCg0KRw==