Below I provide code and step-by-step explanations to produce a heatmap for log odd-ratios from log-linear models. I exemplify the implementation in R
using data on intergenerational class mobility for England, France and Sweden.
Steps:
- Load the following packages for data manipulation (tidyverse,modelr,reshape2), log-linear model estimation (vcdExtra,logmult) and ploting (cowplot). Install previously if you do not have them.
library("tidyverse")
library("modelr")
library("reshape2")
library("vcdExtra")
library("logmult")
library("cowplot")
- Input the contingency table an turn it into a data frame. I use the dataset
erikson
from the package gnm
, a dependency of package logmult
. This is a cross-classification of subject’s occupational status (destination) and his father’s occupational status (origin) across 3 countries.
# Inpute data and create contingency table as data.frame()
data(erikson)
table <- ftable(erikson)
mydata <- as.data.frame(table)
levels(mydata$country) <- c("England-Wales","France","Sweden")
# Save levels variables. To be used later.
levels.origin <- levels(mydata$origin)
levels.destination <- levels(mydata$destination)
levels.country <- levels(mydata$country)
This is what the data looks like:
- Next, I set the values to be used as reference categories.
# Set reference categories
mydata$origin <- relevel(mydata$origin, ref = "V/VI")
mydata$destination <- relevel(mydata$destination, ref = "V/VI")
mydata$country <- relevel(mydata$country, ref = "England-Wales")
mydata$Freq <- mydata$Freq + 1 # add small constant to avoid problems with empty cells.
- Fit different model specifications. Some of these modes are log-linear and other are log-multiplicative.
# Fit models
# independence
indep <- gnm(Freq ~ (origin + destination)*country, family = poisson, data = mydata)
# quasi-perfect mobility
qpm <- gnm(Freq ~ (origin + destination)*country + Diag(origin, destination)*country, family = poisson, data = mydata)
# row-column association 1
rc1 <- gnm(Freq ~ (origin + destination)*country + Mult(origin, destination) + Diag(origin, destination)*country, family = poisson, data = mydata)
Initialising
Running start-up iterations..
Running main iterations.........................................................................................................
Done
# quasi-symmetry
qsymm <- gnm(Freq ~ (origin + destination)*country + Symm(origin, destination)*country, family = poisson, data = mydata)
# unidiff or log-multiplicative layers
unidiff <- gnm(Freq ~ (origin + destination)*country + Mult(Exp(country), origin:destination), family = poisson, data = mydata)
Initialising
Running start-up iterations..
Running main iterations........
Done
# saturated
sat <- gnm(Freq ~ origin*destination*country, family = poisson, data = mydata)
# Compare models via godness of fit statistics
models <- glmlist(indep,qpm,rc1,qsymm,unidiff,sat)
LRstats(models)
Likelihood summary table:
AIC BIC LR Chisq Df Pr(>Chisq)
indep 6498.4 6676.5 5152.6 192 < 2.2e-16 ***
qpm 3130.9 3403.4 1731.1 165 < 2.2e-16 ***
rc1 1973.4 2298.3 543.7 150 < 2.2e-16 ***
qsymm 1689.1 2244.5 127.3 84 0.001605 **
unidiff 1649.0 2057.7 171.2 126 0.004580 **
sat 1729.8 2578.6 0.0 0 1.000000
---
Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
- Goodness of fit statistics suggest that the
unidiff
models is the one that better fits the data. At this point I compute prediction from this model and from them, log odd ratios.
# Create a synthetic dataset with all possible combinations of values
dummy.model <- lm(Freq ~ origin + destination + country, data=mydata)
new_x <- mydata %>% data_grid(origin,destination,country,.model=dummy.model)
# Compute predictions from different models. In this case: unidiff, quasi-symmetry and saturated model.
for ( m in c("unidiff","qsymm","sat")) {
chosen_model <- eval(parse(text = m ))
# Predicted counts
predictions <- cbind(mydata%>% data_grid(origin,destination,country,.model=dummy.model), pred = predict(chosen_model, newdata=new_x)) %>%
as_tibble()
# Intercept
intercept <- predictions %>% filter(origin=="V/VI", destination=="V/VI", country=="England-Wales") %>% dplyr::summarise(pred) %>% as.numeric()
# Log odd ratios for marginal distributions
predictions <- predictions %>% mutate(pred = pred - intercept) # remove intercept
predictions_country <- predictions %>% filter(origin=="V/VI", destination=="V/VI") %>% rename(margin_country=pred) %>% select(country,margin_country)
predictions_origin <- predictions %>% filter(country=="England-Wales", destination=="V/VI") %>% rename(margin_origin=pred) %>% select(origin,margin_origin)
predictions_destination <- predictions %>% filter(country=="England-Wales", origin=="V/VI") %>% rename(margin_destination=pred) %>% select(destination,margin_destination)
# match
predictions <- predictions %>% left_join(predictions_country, by="country")
predictions <- predictions %>% left_join(predictions_origin, by="origin")
predictions <- predictions %>% left_join(predictions_destination, by="destination")
# Log odd ratios for marginal distributions origin and destination by country
predictions_country_origin <- predictions %>% filter(origin!="V/VI",country!="England-Wales",destination=="V/VI") %>%
rename(margin_country_origin=pred) %>% mutate(margin_country_origin = margin_country_origin - (margin_country + margin_origin )) %>%
select(country,origin,margin_country_origin)
predictions_country_destination <- predictions %>% filter(origin=="V/VI",country!="England-Wales",destination!="V/VI") %>%
rename(margin_country_destination=pred) %>% mutate(margin_country_destination = margin_country_destination - (margin_country + margin_destination )) %>%
select(country,destination,margin_country_destination)
predictions <- predictions %>% left_join(predictions_country_origin, by=c("country","origin")) %>% replace_na(list(margin_country_origin = 0))
predictions <- predictions %>% left_join(predictions_country_destination, by=c("country","destination")) %>% replace_na(list(margin_country_destination = 0))
# Margin-free log-odd ratios (LORs)
predictions <- predictions %>%
mutate(`log-OR` = pred - (margin_country + margin_origin + margin_destination + margin_country_origin + margin_country_destination) )
# Save predictions
assign(paste0("predictions_",m),predictions)
}
- Finally, for each model I visualize the estimated log odd ratios capturing margin-free association between origin and destination across countries . Of course, other quantities can also be visualized in the same way.
# Combine models
predictions_unidiff <- predictions_unidiff %>% mutate(model = "Unidiff")
predictions_qsymm <- predictions_qsymm %>% mutate(model = "Quasi-symmetry")
predictions_sat <- predictions_sat %>% mutate(model = "Saturated")
predictions <- bind_rows(predictions_unidiff,predictions_qsymm,predictions_sat) %>%
mutate(model = factor(model, levels=c("Unidiff","Quasi-symmetry","Saturated")))
# Plot
plot <- predictions %>%
ggplot(aes(y=factor(origin, levels = rev(levels.origin)),
x=factor(destination, levels = levels.destination))) + facet_grid(model ~ country) + geom_raster(aes(fill= `log-OR`)) +
scale_fill_gradientn(limits=c(-6,6), colours=c("red","white","blue")) +
labs(y="Father's occupation", x= "Children's occupation", colour="") +
theme_bw() + theme(axis.text.x = element_text(size=9, angle=45, vjust=-1, hjust=0),
axis.text.y = element_text(size=9, angle=0),
plot.title= element_text(size=11)) +
scale_x_discrete(position="top")
# Add labels
plot <- plot %>% add_sub(.,"I+II: Service class, III: Routine non-manual employees, IVa+b:Petty bourgeoisie, IVc: Farmers, V/VI: Skilled working class, VIIa: Semi and unskilled working class, VIIb: Agricultural workers", size= 9) %>% ggdraw()
print(plot)
LS0tCnRpdGxlOiAiSGVhdG1hcCBmb3IgcGF0dGVybnMgb2YgYXNzb2NpYXRpb24gaW4gbG9nLWxpbmVhciBtb2RlbHMiCm91dHB1dDoKICBodG1sX25vdGVib29rOiBkZWZhdWx0Ci0tLQoKCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIGZpZy53aWR0aCA9IDEyLCBmaWcuaGVpZ2h0ID0gMTIpCmBgYAoKCkJlbG93IEkgcHJvdmlkZSBjb2RlIGFuZCBzdGVwLWJ5LXN0ZXAgZXhwbGFuYXRpb25zIHRvIHByb2R1Y2UgYSBoZWF0bWFwIGZvciBsb2cgb2RkLXJhdGlvcyBmcm9tIGxvZy1saW5lYXIgbW9kZWxzLiBJIGV4ZW1wbGlmeSB0aGUgaW1wbGVtZW50YXRpb24gaW4gYGBgUmBgYCB1c2luZyBkYXRhIG9uIGludGVyZ2VuZXJhdGlvbmFsIGNsYXNzIG1vYmlsaXR5IGZvciBFbmdsYW5kLCBGcmFuY2UgYW5kIFN3ZWRlbi4KCiMjIyMgU3RlcHM6CgoxLiAgTG9hZCB0aGUgZm9sbG93aW5nIHBhY2thZ2VzIGZvciBkYXRhIG1hbmlwdWxhdGlvbiAodGlkeXZlcnNlLG1vZGVscixyZXNoYXBlMiksIGxvZy1saW5lYXIgbW9kZWwgZXN0aW1hdGlvbiAodmNkRXh0cmEsbG9nbXVsdCkgYW5kIHBsb3RpbmcgKGNvd3Bsb3QpLiBJbnN0YWxsIHByZXZpb3VzbHkgaWYgeW91IGRvIG5vdCBoYXZlIHRoZW0uCgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KCiAgICBsaWJyYXJ5KCJ0aWR5dmVyc2UiKQogICAgbGlicmFyeSgibW9kZWxyIikKICAgIGxpYnJhcnkoInJlc2hhcGUyIikKICAgIGxpYnJhcnkoInZjZEV4dHJhIikKICAgIGxpYnJhcnkoImxvZ211bHQiKQogICAgbGlicmFyeSgiY293cGxvdCIpCgpgYGAKCjxicj4KCjIuIElucHV0IHRoZSBjb250aW5nZW5jeSB0YWJsZSBhbiB0dXJuIGl0IGludG8gYSBkYXRhIGZyYW1lLiBJIHVzZSB0aGUgZGF0YXNldCBgYGBlcmlrc29uYGBgIGZyb20gdGhlIHBhY2thZ2UgYGBgZ25tYGBgLCBhIGRlcGVuZGVuY3kgb2YgcGFja2FnZSBgYGBsb2dtdWx0YGBgLiBUaGlzIGlzIGEgY3Jvc3MtY2xhc3NpZmljYXRpb24gb2Ygc3ViamVjdCdzIG9jY3VwYXRpb25hbCBzdGF0dXMgKGRlc3RpbmF0aW9uKSBhbmQgaGlzIGZhdGhlcidzIG9jY3VwYXRpb25hbCBzdGF0dXMgKG9yaWdpbikgYWNyb3NzIDMgY291bnRyaWVzLiAKCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQogICAgCiAgICAjIElucHV0ZSBkYXRhIGFuZCBjcmVhdGUgY29udGluZ2VuY3kgdGFibGUgYXMgZGF0YS5mcmFtZSgpCiAgICBkYXRhKGVyaWtzb24pCiAgICB0YWJsZSA8LSBmdGFibGUoZXJpa3NvbikKICAgIG15ZGF0YSA8LSBhcy5kYXRhLmZyYW1lKHRhYmxlKQogICAgbGV2ZWxzKG15ZGF0YSRjb3VudHJ5KSA8LSBjKCJFbmdsYW5kLVdhbGVzIiwiRnJhbmNlIiwiU3dlZGVuIikKICAgIAogICAgIyBTYXZlIGxldmVscyB2YXJpYWJsZXMuIFRvIGJlIHVzZWQgbGF0ZXIuCiAgICBsZXZlbHMub3JpZ2luICAgICAgPC0gbGV2ZWxzKG15ZGF0YSRvcmlnaW4pCiAgICBsZXZlbHMuZGVzdGluYXRpb24gPC0gbGV2ZWxzKG15ZGF0YSRkZXN0aW5hdGlvbikKICAgIGxldmVscy5jb3VudHJ5ICAgICA8LSBsZXZlbHMobXlkYXRhJGNvdW50cnkpCgpgYGAKClRoaXMgaXMgd2hhdCB0aGUgZGF0YSBsb29rcyBsaWtlOgoKYGBge3IsIGVjaG89RkFMU0V9CiAgICBwcmludChteWRhdGEgJT4lIGFzX3RpYmJsZSgpKQpgYGAKCjxicj4KCjMuIE5leHQsIEkgc2V0IHRoZSB2YWx1ZXMgdG8gYmUgdXNlZCBhcyByZWZlcmVuY2UgY2F0ZWdvcmllcy4gCgpgYGB7cn0KCiAgICAjIFNldCByZWZlcmVuY2UgY2F0ZWdvcmllcwogICAgbXlkYXRhJG9yaWdpbiAgICAgIDwtIHJlbGV2ZWwobXlkYXRhJG9yaWdpbiwgcmVmID0gIlYvVkkiKQogICAgbXlkYXRhJGRlc3RpbmF0aW9uIDwtIHJlbGV2ZWwobXlkYXRhJGRlc3RpbmF0aW9uLCByZWYgPSAiVi9WSSIpCiAgICBteWRhdGEkY291bnRyeSAgICAgPC0gcmVsZXZlbChteWRhdGEkY291bnRyeSwgcmVmID0gIkVuZ2xhbmQtV2FsZXMiKQogICAgbXlkYXRhJEZyZXEgICAgICAgIDwtIG15ZGF0YSRGcmVxICsgMSAjIGFkZCBzbWFsbCBjb25zdGFudCB0byBhdm9pZCBwcm9ibGVtcyB3aXRoIGVtcHR5IGNlbGxzLiAKCmBgYAoKPGJyPgoKCjQuIEZpdCBkaWZmZXJlbnQgbW9kZWwgc3BlY2lmaWNhdGlvbnMuIFNvbWUgb2YgdGhlc2UgbW9kZXMgYXJlIGxvZy1saW5lYXIgYW5kIG90aGVyIGFyZSBsb2ctbXVsdGlwbGljYXRpdmUuCgogCmBgYHtyfQoKIyBGaXQgbW9kZWxzIAoKIyBpbmRlcGVuZGVuY2UgCmluZGVwIDwtIGdubShGcmVxIH4gKG9yaWdpbiArIGRlc3RpbmF0aW9uKSpjb3VudHJ5LCBmYW1pbHkgPSBwb2lzc29uLCBkYXRhID0gbXlkYXRhKQoKIyBxdWFzaS1wZXJmZWN0IG1vYmlsaXR5IApxcG0gIDwtIGdubShGcmVxIH4gKG9yaWdpbiArIGRlc3RpbmF0aW9uKSpjb3VudHJ5ICsgRGlhZyhvcmlnaW4sIGRlc3RpbmF0aW9uKSpjb3VudHJ5LCBmYW1pbHkgPSBwb2lzc29uLCBkYXRhID0gbXlkYXRhKQoKIyByb3ctY29sdW1uIGFzc29jaWF0aW9uIDEKcmMxIDwtIGdubShGcmVxIH4gKG9yaWdpbiArIGRlc3RpbmF0aW9uKSpjb3VudHJ5ICsgTXVsdChvcmlnaW4sIGRlc3RpbmF0aW9uKSArIERpYWcob3JpZ2luLCBkZXN0aW5hdGlvbikqY291bnRyeSwgZmFtaWx5ID0gcG9pc3NvbiwgZGF0YSA9IG15ZGF0YSkKCiMgcXVhc2ktc3ltbWV0cnkKcXN5bW0gPC0gZ25tKEZyZXEgfiAob3JpZ2luICsgZGVzdGluYXRpb24pKmNvdW50cnkgKyBTeW1tKG9yaWdpbiwgZGVzdGluYXRpb24pKmNvdW50cnksIGZhbWlseSA9IHBvaXNzb24sIGRhdGEgPSBteWRhdGEpCgojIHVuaWRpZmYgb3IgbG9nLW11bHRpcGxpY2F0aXZlIGxheWVycwp1bmlkaWZmIDwtIGdubShGcmVxIH4gKG9yaWdpbiArIGRlc3RpbmF0aW9uKSpjb3VudHJ5ICsgTXVsdChFeHAoY291bnRyeSksIG9yaWdpbjpkZXN0aW5hdGlvbiksIGZhbWlseSA9IHBvaXNzb24sIGRhdGEgPSBteWRhdGEpCgojIHNhdHVyYXRlZApzYXQgPC0gZ25tKEZyZXEgfiBvcmlnaW4qZGVzdGluYXRpb24qY291bnRyeSwgZmFtaWx5ID0gcG9pc3NvbiwgZGF0YSA9IG15ZGF0YSkKCgojIENvbXBhcmUgbW9kZWxzIHZpYSBnb2RuZXNzIG9mIGZpdCBzdGF0aXN0aWNzCgptb2RlbHMgPC0gZ2xtbGlzdChpbmRlcCxxcG0scmMxLHFzeW1tLHVuaWRpZmYsc2F0KQpMUnN0YXRzKG1vZGVscykKCmBgYAoKPGJyPgoKNS4gR29vZG5lc3Mgb2YgZml0IHN0YXRpc3RpY3Mgc3VnZ2VzdCB0aGF0IHRoZSBgYHVuaWRpZmZgYCBtb2RlbHMgaXMgdGhlIG9uZSB0aGF0IGJldHRlciBmaXRzIHRoZSBkYXRhLiBBdCB0aGlzIHBvaW50IEkgY29tcHV0ZSBwcmVkaWN0aW9uIGZyb20gdGhpcyBtb2RlbCBhbmQgZnJvbSB0aGVtLCBsb2cgb2RkIHJhdGlvcy4gCgoKYGBge3IsIGluY2x1ZGU9VFJVRSwgZWNobz1UUlVFfQoKIyBDcmVhdGUgYSBzeW50aGV0aWMgZGF0YXNldCB3aXRoIGFsbCBwb3NzaWJsZSBjb21iaW5hdGlvbnMgb2YgdmFsdWVzCgpkdW1teS5tb2RlbCA8LSBsbShGcmVxIH4gb3JpZ2luICsgZGVzdGluYXRpb24gKyBjb3VudHJ5LCBkYXRhPW15ZGF0YSkKbmV3X3ggPC0gbXlkYXRhICU+JSBkYXRhX2dyaWQob3JpZ2luLGRlc3RpbmF0aW9uLGNvdW50cnksLm1vZGVsPWR1bW15Lm1vZGVsKSAKCiMgQ29tcHV0ZSBwcmVkaWN0aW9ucyBmcm9tIGRpZmZlcmVudCBtb2RlbHMuIEluIHRoaXMgY2FzZTogdW5pZGlmZiwgcXVhc2ktc3ltbWV0cnkgYW5kIHNhdHVyYXRlZCBtb2RlbC4KCmZvciAoIG0gaW4gYygidW5pZGlmZiIsInFzeW1tIiwic2F0IikpIHsKICAKICBjaG9zZW5fbW9kZWwgPC0gZXZhbChwYXJzZSh0ZXh0ID0gbSApKSAgCgogICMgUHJlZGljdGVkIGNvdW50cwogIHByZWRpY3Rpb25zIDwtIGNiaW5kKG15ZGF0YSU+JSBkYXRhX2dyaWQob3JpZ2luLGRlc3RpbmF0aW9uLGNvdW50cnksLm1vZGVsPWR1bW15Lm1vZGVsKSwgcHJlZCA9IHByZWRpY3QoY2hvc2VuX21vZGVsLCBuZXdkYXRhPW5ld194KSkgJT4lCiAgICBhc190aWJibGUoKSAgCgoKICAjIEludGVyY2VwdAogIGludGVyY2VwdCA8LSBwcmVkaWN0aW9ucyAlPiUgZmlsdGVyKG9yaWdpbj09IlYvVkkiLCBkZXN0aW5hdGlvbj09IlYvVkkiLCBjb3VudHJ5PT0iRW5nbGFuZC1XYWxlcyIpICU+JSBkcGx5cjo6c3VtbWFyaXNlKHByZWQpICU+JSBhcy5udW1lcmljKCkKCgogICMgTG9nIG9kZCByYXRpb3MgZm9yIG1hcmdpbmFsIGRpc3RyaWJ1dGlvbnMKICBwcmVkaWN0aW9ucyA8LSBwcmVkaWN0aW9ucyAlPiUgbXV0YXRlKHByZWQgPSBwcmVkIC0gaW50ZXJjZXB0KSAjIHJlbW92ZSBpbnRlcmNlcHQKCiAgcHJlZGljdGlvbnNfY291bnRyeSAgICAgPC0gcHJlZGljdGlvbnMgJT4lIGZpbHRlcihvcmlnaW49PSJWL1ZJIiwgZGVzdGluYXRpb249PSJWL1ZJIikgICU+JSByZW5hbWUobWFyZ2luX2NvdW50cnk9cHJlZCkgJT4lIHNlbGVjdChjb3VudHJ5LG1hcmdpbl9jb3VudHJ5KQogIHByZWRpY3Rpb25zX29yaWdpbiAgICAgIDwtIHByZWRpY3Rpb25zICU+JSBmaWx0ZXIoY291bnRyeT09IkVuZ2xhbmQtV2FsZXMiLCBkZXN0aW5hdGlvbj09IlYvVkkiKSAlPiUgcmVuYW1lKG1hcmdpbl9vcmlnaW49cHJlZCkgJT4lIHNlbGVjdChvcmlnaW4sbWFyZ2luX29yaWdpbikKICBwcmVkaWN0aW9uc19kZXN0aW5hdGlvbiA8LSBwcmVkaWN0aW9ucyAlPiUgZmlsdGVyKGNvdW50cnk9PSJFbmdsYW5kLVdhbGVzIiwgb3JpZ2luPT0iVi9WSSIpICU+JSByZW5hbWUobWFyZ2luX2Rlc3RpbmF0aW9uPXByZWQpICU+JSBzZWxlY3QoZGVzdGluYXRpb24sbWFyZ2luX2Rlc3RpbmF0aW9uKQoKCiAgIyBtYXRjaAogIHByZWRpY3Rpb25zIDwtIHByZWRpY3Rpb25zICU+JSBsZWZ0X2pvaW4ocHJlZGljdGlvbnNfY291bnRyeSwgYnk9ImNvdW50cnkiKQogIHByZWRpY3Rpb25zIDwtIHByZWRpY3Rpb25zICU+JSBsZWZ0X2pvaW4ocHJlZGljdGlvbnNfb3JpZ2luLCBieT0ib3JpZ2luIikKICBwcmVkaWN0aW9ucyA8LSBwcmVkaWN0aW9ucyAlPiUgbGVmdF9qb2luKHByZWRpY3Rpb25zX2Rlc3RpbmF0aW9uLCBieT0iZGVzdGluYXRpb24iKQoKCiAgIyBMb2cgb2RkIHJhdGlvcyBmb3IgbWFyZ2luYWwgZGlzdHJpYnV0aW9ucyBvcmlnaW4gYW5kIGRlc3RpbmF0aW9uIGJ5IGNvdW50cnkKCiAgcHJlZGljdGlvbnNfY291bnRyeV9vcmlnaW4gPC0gcHJlZGljdGlvbnMgJT4lIGZpbHRlcihvcmlnaW4hPSJWL1ZJIixjb3VudHJ5IT0iRW5nbGFuZC1XYWxlcyIsZGVzdGluYXRpb249PSJWL1ZJIikgJT4lCiAgICByZW5hbWUobWFyZ2luX2NvdW50cnlfb3JpZ2luPXByZWQpICU+JSBtdXRhdGUobWFyZ2luX2NvdW50cnlfb3JpZ2luID0gbWFyZ2luX2NvdW50cnlfb3JpZ2luIC0gKG1hcmdpbl9jb3VudHJ5ICsgbWFyZ2luX29yaWdpbiApKSAlPiUKICAgIHNlbGVjdChjb3VudHJ5LG9yaWdpbixtYXJnaW5fY291bnRyeV9vcmlnaW4pCgogIHByZWRpY3Rpb25zX2NvdW50cnlfZGVzdGluYXRpb24gPC0gcHJlZGljdGlvbnMgJT4lIGZpbHRlcihvcmlnaW49PSJWL1ZJIixjb3VudHJ5IT0iRW5nbGFuZC1XYWxlcyIsZGVzdGluYXRpb24hPSJWL1ZJIikgJT4lCiAgICByZW5hbWUobWFyZ2luX2NvdW50cnlfZGVzdGluYXRpb249cHJlZCkgJT4lIG11dGF0ZShtYXJnaW5fY291bnRyeV9kZXN0aW5hdGlvbiA9IG1hcmdpbl9jb3VudHJ5X2Rlc3RpbmF0aW9uIC0gKG1hcmdpbl9jb3VudHJ5ICsgbWFyZ2luX2Rlc3RpbmF0aW9uICkpICU+JSAKICAgIHNlbGVjdChjb3VudHJ5LGRlc3RpbmF0aW9uLG1hcmdpbl9jb3VudHJ5X2Rlc3RpbmF0aW9uKQoKICBwcmVkaWN0aW9ucyA8LSBwcmVkaWN0aW9ucyAlPiUgbGVmdF9qb2luKHByZWRpY3Rpb25zX2NvdW50cnlfb3JpZ2luLCBieT1jKCJjb3VudHJ5Iiwib3JpZ2luIikpICU+JSAgcmVwbGFjZV9uYShsaXN0KG1hcmdpbl9jb3VudHJ5X29yaWdpbiA9IDApKQogIHByZWRpY3Rpb25zIDwtIHByZWRpY3Rpb25zICU+JSBsZWZ0X2pvaW4ocHJlZGljdGlvbnNfY291bnRyeV9kZXN0aW5hdGlvbiwgYnk9YygiY291bnRyeSIsImRlc3RpbmF0aW9uIikpICU+JSAgcmVwbGFjZV9uYShsaXN0KG1hcmdpbl9jb3VudHJ5X2Rlc3RpbmF0aW9uID0gMCkpCgoKICAjIE1hcmdpbi1mcmVlIGxvZy1vZGQgcmF0aW9zIChMT1JzKQogIHByZWRpY3Rpb25zIDwtIHByZWRpY3Rpb25zICU+JSAKICAgIG11dGF0ZShgbG9nLU9SYCA9IHByZWQgLSAobWFyZ2luX2NvdW50cnkgKyBtYXJnaW5fb3JpZ2luICsgbWFyZ2luX2Rlc3RpbmF0aW9uICsgbWFyZ2luX2NvdW50cnlfb3JpZ2luICsgbWFyZ2luX2NvdW50cnlfZGVzdGluYXRpb24pICkgCgoKICAjIFNhdmUgcHJlZGljdGlvbnMgCiAgYXNzaWduKHBhc3RlMCgicHJlZGljdGlvbnNfIixtKSxwcmVkaWN0aW9ucykKICAgIAp9CgoKYGBgCgo8YnI+CgoKNi4gRmluYWxseSwgZm9yIGVhY2ggbW9kZWwgSSB2aXN1YWxpemUgdGhlIGVzdGltYXRlZCBsb2cgb2RkIHJhdGlvcyBjYXB0dXJpbmcgbWFyZ2luLWZyZWUgYXNzb2NpYXRpb24gYmV0d2VlbiBvcmlnaW4gYW5kIGRlc3RpbmF0aW9uIGFjcm9zcyBjb3VudHJpZXMgLiBPZiBjb3Vyc2UsIG90aGVyIHF1YW50aXRpZXMgY2FuIGFsc28gYmUgdmlzdWFsaXplZCBpbiB0aGUgc2FtZSB3YXkuCgoKCmBgYHtyfQoKIyBDb21iaW5lIG1vZGVscwoKcHJlZGljdGlvbnNfdW5pZGlmZiA8LSBwcmVkaWN0aW9uc191bmlkaWZmICU+JSBtdXRhdGUobW9kZWwgPSAiVW5pZGlmZiIpCnByZWRpY3Rpb25zX3FzeW1tIDwtIHByZWRpY3Rpb25zX3FzeW1tICU+JSBtdXRhdGUobW9kZWwgPSAiUXVhc2ktc3ltbWV0cnkiKQpwcmVkaWN0aW9uc19zYXQgPC0gcHJlZGljdGlvbnNfc2F0ICU+JSBtdXRhdGUobW9kZWwgPSAiU2F0dXJhdGVkIikKCnByZWRpY3Rpb25zIDwtIGJpbmRfcm93cyhwcmVkaWN0aW9uc191bmlkaWZmLHByZWRpY3Rpb25zX3FzeW1tLHByZWRpY3Rpb25zX3NhdCkgJT4lCiAgbXV0YXRlKG1vZGVsID0gZmFjdG9yKG1vZGVsLCBsZXZlbHM9YygiVW5pZGlmZiIsIlF1YXNpLXN5bW1ldHJ5IiwiU2F0dXJhdGVkIikpKQoKCiMgUGxvdAoKcGxvdCA8LSBwcmVkaWN0aW9ucyAlPiUgCiAgZ2dwbG90KGFlcyh5PWZhY3RvcihvcmlnaW4sIGxldmVscyA9IHJldihsZXZlbHMub3JpZ2luKSksCiAgICAgICAgICAgICB4PWZhY3RvcihkZXN0aW5hdGlvbiwgbGV2ZWxzID0gbGV2ZWxzLmRlc3RpbmF0aW9uKSkpICsgZmFjZXRfZ3JpZChtb2RlbCB+IGNvdW50cnkpICsgZ2VvbV9yYXN0ZXIoYWVzKGZpbGw9IGBsb2ctT1JgKSkgKwogIHNjYWxlX2ZpbGxfZ3JhZGllbnRuKGxpbWl0cz1jKC02LDYpLCBjb2xvdXJzPWMoInJlZCIsIndoaXRlIiwiYmx1ZSIpKSArCiAgbGFicyh5PSJGYXRoZXIncyBvY2N1cGF0aW9uIiwgeD0gIkNoaWxkcmVuJ3Mgb2NjdXBhdGlvbiIsIGNvbG91cj0iIikgKwogIHRoZW1lX2J3KCkgKyB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChzaXplPTksIGFuZ2xlPTQ1LCB2anVzdD0tMSwgaGp1c3Q9MCksCiAgICAgICAgICAgICAgICAgICAgIGF4aXMudGV4dC55ID0gZWxlbWVudF90ZXh0KHNpemU9OSwgYW5nbGU9MCksCiAgICAgICAgICAgICAgICAgICAgIHBsb3QudGl0bGU9IGVsZW1lbnRfdGV4dChzaXplPTExKSkgKwogIHNjYWxlX3hfZGlzY3JldGUocG9zaXRpb249InRvcCIpIAoKCiMgQWRkIGxhYmVscyAKcGxvdCA8LSBwbG90ICU+JSBhZGRfc3ViKC4sIkkrSUk6IFNlcnZpY2UgY2xhc3MsIElJSTogUm91dGluZSBub24tbWFudWFsIGVtcGxveWVlcywgSVZhK2I6UGV0dHkgYm91cmdlb2lzaWUsIElWYzogRmFybWVycywgVi9WSTogU2tpbGxlZCB3b3JraW5nIGNsYXNzLCBWSUlhOiBTZW1pIGFuZCB1bnNraWxsZWQgd29ya2luZyBjbGFzcywgVklJYjogQWdyaWN1bHR1cmFsIHdvcmtlcnMiLCBzaXplPSA5KSAlPiUgIGdnZHJhdygpCgpwcmludChwbG90KQoKCmBgYAoKCgo=