Goals:

  • Estimate subgroup effects

  • Estimate best linear predictor of heterogeneity

  • Estimate nonparametric heterogeneity with kernel and spline regression


Introducing the data

The Application Notebook builds on the dataset that is kindly provided in the hdm package. The data was used in Chernozhukov and Hansen (2004). Their paper investigates the effect of participation in the employer-sponsored 401(k) retirement savings plan (p401) on net assets (net_tfa). Since then the data was used to showcase many new methods. It is not the most comprehensive dataset with basically ten covariates/regressors/predictors:

  • age: age

  • db: defined benefit pension

  • educ: education (in years)

  • fsize: family size

  • hown: home owner

  • inc: income (in US $)

  • male: male

  • marr: married

  • pira: participation in individual retirement account (IRA)

  • twoearn: two earners

However, it is publicly available and the relatively few covariates ensure that the programs do not run too long.

# To install the causalDML package uncomment the following two lines
# library(devtools)
# install_github(repo="MCKnaus/causalDML")

# Load the packages required for later
library(hdm)
library(tidyverse)
library(causalDML)
library(grf)
library(estimatr)


set.seed(1234) # for replicability
options(scipen = 10) # Switch off scientific notation

data(pension)
# Outcome
Y = pension$net_tfa
# Treatment
W = pension$p401
# Create main effects matrix
X = model.matrix(~ 0 + age + db + educ + fsize + hown + inc + male + marr + pira + twoearn, data = pension)



Double ML for AIPW with causalDML package

Like previous notebooks, we create the pseudo-outcome \[\tilde{Y}_{ATE} = \underbrace{\hat{m}(1,X) - \hat{m}(0,X)}_{\text{outcome predictions}} + \underbrace{\frac{W (Y - \hat{m}(1,X))}{\hat{e}(X)} - \frac{(1-W) (Y - \hat{m}(0,X))}{1-\hat{e}(X)}}_{\text{weighted residuals}}\]

by running the DML_aipw function:

# 5-fold cross-fitting with causalDML package
aipw = DML_aipw(Y,W,X)
summary(aipw$ATE)
          ATE      SE      t         p    
1 - 0 11288.4  1166.9 9.6736 < 2.2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
# If you have more time, tune the forest
# forest = create_method("forest_grf",args=list(tune.parameters = "all"))
# aipw = DML_aipw(Y,W,X,ml_w=list(forest),ml_y=list(forest),cf=5)



GATE estimation

The pseudo-outcome can now be used to estimate different heterogeneous effects. We use standard regression models but by using the pseudo-outcome instead of a real outcome we model effect size and not outcome level.

Subgroup effect

First, let’s check a classic. Gender differences. This would usually be implemented by splitting the sample by gender and rerunning the whole analysis in the subsamples separately.

With the pseudo-outcome stored in aipw$ATE$delta this boils down to running an OLS regression with the male indicator as single regressor.

male = X[,7]
blp_male = lm_robust(aipw$ATE$delta ~ male)
summary(blp_male)

Call:
lm_robust(formula = aipw$ATE$delta ~ male)

Standard error type:  HC2 

Coefficients:
            Estimate Std. Error t value  Pr(>|t|) CI Lower CI Upper   DF
(Intercept)    10691       1303  8.2076 2.537e-16     8138    13245 9913
male            2899       2929  0.9895 3.224e-01    -2843     8641 9913

Multiple R-squared:  0.0001018 ,    Adjusted R-squared:  8.978e-07 
F-statistic: 0.9791 on 1 and 9913 DF,  p-value: 0.3224

We can interpret this outcome as we are used to, only that we model now effect size. This means the intercept gives us the average of the reference group (women) and the coefficient tells us how much higher the effect is for men. In this case, we find no significant gender differences in the effect of 401(k) participation on net wealth.

If you are interested in the gender specific effect instead of differences between groups, just run an OLS regression without constant and all group indicators:

female = 1-male
blp_male1 = lm_robust(aipw$ATE$delta ~ 0 + female + male)
summary(blp_male1)

Call:
lm_robust(formula = aipw$ATE$delta ~ 0 + female + male)

Standard error type:  HC2 

Coefficients:
       Estimate Std. Error t value  Pr(>|t|) CI Lower CI Upper   DF
female    10691       1303   8.208 2.537e-16     8138    13245 9913
male      13590       2624   5.180 2.267e-07     8447    18733 9913

Multiple R-squared:  0.009451 , Adjusted R-squared:  0.009251 
F-statistic:  47.1 on 2 and 9913 DF,  p-value: < 2.2e-16

You see that we can transfer all the strategies that we know about modeling outcomes with OLS for modelling causal effects.


Best linear prediction

Maybe we do not want to focus on subgroup analyses but to model the effect using all main effects at our disposal. In standard OLS this would mean to include a lot of interaction effects while completely relying on correct specification of the outcome model.

Using the pseudo-outcome allows us to be completely agnostic about the outcome model and to receive a nice summary of the underlying effect heterogeneity in a familiar format, an OLS output:

blp = lm_robust(aipw$ATE$delta ~ X)
summary(blp)

Call:
lm_robust(formula = aipw$ATE$delta ~ X)

Standard error type:  HC2 

Coefficients:
                Estimate Std. Error t value Pr(>|t|)    CI Lower   CI Upper   DF
(Intercept) -16173.07643  8508.1126 -1.9009 0.057344 -32850.7088   504.5559 9904
Xage           302.55404   105.7100  2.8621 0.004217     95.3409   509.7672 9904
Xdb           5655.01827  2873.3302  1.9681 0.049084     22.7063 11287.3302 9904
Xeduc          438.08990   614.0359  0.7135 0.475578   -765.5455  1641.7253 9904
Xfsize         121.76803   803.8349  0.1515 0.879597  -1453.9119  1697.4480 9904
Xhown         5737.88595  2162.7697  2.6530 0.007990   1498.4171  9977.3548 9904
Xinc             0.04715     0.1977  0.2385 0.811532     -0.3404     0.4347 9904
Xmale         5347.95842  3097.7852  1.7264 0.084310   -724.3310 11420.2479 9904
Xmarr         1277.05730  3572.4932  0.3575 0.720748  -5725.7565  8279.8711 9904
Xpira        -1316.68641  3773.1158 -0.3490 0.727123  -8712.7613  6079.3885 9904
Xtwoearn      1097.38011  4751.4033  0.2310 0.817351  -8216.3374 10411.0976 9904

Multiple R-squared:  0.003032 , Adjusted R-squared:  0.002025 
F-statistic: 5.272 on 10 and 9904 DF,  p-value: 0.00000008826

For example, we see that, all other regressors held constant, being one year older increases the effect of 401(k) participation of wealth on average by $ 303.

Again we realize that everything we learned about OLS for modelling outcomes directly translates to modeling effect sizes.


Non-parametric heterogeneity

The imho coolest thing about having the pseudo-outcome is that we can also estimate heterogeneous effects with nonparametric regressions. This means we are not only agnostic about the outcome and propensity score models but also about the functional of effect heterogeneity.

This is especially useful if we have some continuous variable like age for which we want to understand effect heterogeneity.


Spline regression

The spline_cate function implements spline regression reusing the pseudo-outcome:

age = X[,1]
sr_age = spline_cate(aipw$ATE$delta,age)
plot(sr_age,z_label = "Age")


Kernel regression

The kr_cate function implements kernel regression reusing the pseudo-outcome:

kr_age = kr_cate(aipw$ATE$delta,age)
plot(kr_age,z_label = "Age")

Both nonparametric approaches document the same pattern. The effect of 401(k) participation on net wealth increases more or less linearly until the age of 50 and then slightly drops.

For me this is an incredible new option for our policy evaluation toolbox.



LS0tDQp0aXRsZTogIkNhdXNhbCBNTDogRG91YmxlIE1MIGZvciBncm91cCBhdmVyYWdlIHRyZWF0bWVudCBlZmZlY3RzIg0Kc3VidGl0bGU6ICJBcHBsaWNhdGlvbiBub3RlYm9vayINCmF1dGhvcjogIk1pY2hhZWwgS25hdXMiDQpkYXRlOiAiYHIgZm9ybWF0KFN5cy50aW1lKCksICclbS8leScpYCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6DQogICAgdG9jOiB0cnVlDQogICAgdG9jX2Zsb2F0OiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBzaG93DQotLS0NCg0KDQpHb2FsczoNCg0KLSBFc3RpbWF0ZSBzdWJncm91cCBlZmZlY3RzDQoNCi0gRXN0aW1hdGUgYmVzdCBsaW5lYXIgcHJlZGljdG9yIG9mIGhldGVyb2dlbmVpdHkgDQoNCi0gRXN0aW1hdGUgbm9ucGFyYW1ldHJpYyBoZXRlcm9nZW5laXR5IHdpdGgga2VybmVsIGFuZCBzcGxpbmUgcmVncmVzc2lvbg0KDQo8YnI+DQoNCiMgSW50cm9kdWNpbmcgdGhlIGRhdGENCg0KVGhlIEFwcGxpY2F0aW9uIE5vdGVib29rIGJ1aWxkcyBvbiB0aGUgZGF0YXNldCB0aGF0IGlzIGtpbmRseSBwcm92aWRlZCBpbiB0aGUgYGhkbWAgcGFja2FnZS4gVGhlIGRhdGEgd2FzIHVzZWQgaW4gW0NoZXJub3podWtvdiBhbmQgSGFuc2VuICgyMDA0KV0oaHR0cHM6Ly9kaXJlY3QubWl0LmVkdS9yZXN0L2FydGljbGUvODYvMy83MzUvNTc1ODYvVGhlLUVmZmVjdHMtb2YtNDAxLUstUGFydGljaXBhdGlvbi1vbi10aGUtV2VhbHRoKS4gVGhlaXIgcGFwZXIgaW52ZXN0aWdhdGVzIHRoZSBlZmZlY3Qgb2YgcGFydGljaXBhdGlvbiBpbiB0aGUgZW1wbG95ZXItc3BvbnNvcmVkIDQwMShrKSByZXRpcmVtZW50IHNhdmluZ3MgcGxhbiAoYHA0MDFgKSBvbiBuZXQgYXNzZXRzIChgbmV0X3RmYWApLiBTaW5jZSB0aGVuIHRoZSBkYXRhIHdhcyB1c2VkIHRvIHNob3djYXNlIG1hbnkgbmV3IG1ldGhvZHMuIEl0IGlzIG5vdCB0aGUgbW9zdCBjb21wcmVoZW5zaXZlIGRhdGFzZXQgd2l0aCBiYXNpY2FsbHkgdGVuIGNvdmFyaWF0ZXMvcmVncmVzc29ycy9wcmVkaWN0b3JzOg0KDQotICphZ2UqOiBhZ2UNCg0KLSAqZGIqOiBkZWZpbmVkIGJlbmVmaXQgcGVuc2lvbg0KDQotICplZHVjKjogZWR1Y2F0aW9uIChpbiB5ZWFycykNCg0KLSAqZnNpemUqOiBmYW1pbHkgc2l6ZQ0KDQotICpob3duKjogaG9tZSBvd25lcg0KDQotICppbmMqOiBpbmNvbWUgKGluIFVTICQpDQoNCi0gKm1hbGUqOiBtYWxlDQoNCi0gKm1hcnIqOiBtYXJyaWVkDQoNCi0gKnBpcmEqOiBwYXJ0aWNpcGF0aW9uIGluIGluZGl2aWR1YWwgcmV0aXJlbWVudCBhY2NvdW50IChJUkEpDQoNCi0gKnR3b2Vhcm4qOiB0d28gZWFybmVycw0KDQpIb3dldmVyLCBpdCBpcyBwdWJsaWNseSBhdmFpbGFibGUgYW5kIHRoZSByZWxhdGl2ZWx5IGZldyBjb3ZhcmlhdGVzIGVuc3VyZSB0aGF0IHRoZSBwcm9ncmFtcyBkbyBub3QgcnVuIHRvbyBsb25nLg0KDQpgYGB7ciwgd2FybmluZyA9IEYsIG1lc3NhZ2UgPSBGfQ0KIyBUbyBpbnN0YWxsIHRoZSBjYXVzYWxETUwgcGFja2FnZSB1bmNvbW1lbnQgdGhlIGZvbGxvd2luZyB0d28gbGluZXMNCiMgbGlicmFyeShkZXZ0b29scykNCiMgaW5zdGFsbF9naXRodWIocmVwbz0iTUNLbmF1cy9jYXVzYWxETUwiKQ0KDQojIExvYWQgdGhlIHBhY2thZ2VzIHJlcXVpcmVkIGZvciBsYXRlcg0KbGlicmFyeShoZG0pDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoY2F1c2FsRE1MKQ0KbGlicmFyeShncmYpDQpsaWJyYXJ5KGVzdGltYXRyKQ0KDQoNCnNldC5zZWVkKDEyMzQpICMgZm9yIHJlcGxpY2FiaWxpdHkNCm9wdGlvbnMoc2NpcGVuID0gMTApICMgU3dpdGNoIG9mZiBzY2llbnRpZmljIG5vdGF0aW9uDQoNCmRhdGEocGVuc2lvbikNCiMgT3V0Y29tZQ0KWSA9IHBlbnNpb24kbmV0X3RmYQ0KIyBUcmVhdG1lbnQNClcgPSBwZW5zaW9uJHA0MDENCiMgQ3JlYXRlIG1haW4gZWZmZWN0cyBtYXRyaXgNClggPSBtb2RlbC5tYXRyaXgofiAwICsgYWdlICsgZGIgKyBlZHVjICsgZnNpemUgKyBob3duICsgaW5jICsgbWFsZSArIG1hcnIgKyBwaXJhICsgdHdvZWFybiwgZGF0YSA9IHBlbnNpb24pDQpgYGANCg0KPGJyPg0KPGJyPg0KDQojIERvdWJsZSBNTCBmb3IgQUlQVyB3aXRoIGBjYXVzYWxETUxgIHBhY2thZ2UNCg0KTGlrZSBwcmV2aW91cyBub3RlYm9va3MsIHdlIGNyZWF0ZSB0aGUgcHNldWRvLW91dGNvbWUNCiQkXHRpbGRle1l9X3tBVEV9ID0gXHVuZGVyYnJhY2V7XGhhdHttfSgxLFgpIC0gXGhhdHttfSgwLFgpfV97XHRleHR7b3V0Y29tZSBwcmVkaWN0aW9uc319ICsgXHVuZGVyYnJhY2V7XGZyYWN7VyAoWSAtIFxoYXR7bX0oMSxYKSl9e1xoYXR7ZX0oWCl9IC0gXGZyYWN7KDEtVykgKFkgLSBcaGF0e219KDAsWCkpfXsxLVxoYXR7ZX0oWCl9fV97XHRleHR7d2VpZ2h0ZWQgcmVzaWR1YWxzfX0kJA0KDQpieSBydW5uaW5nIHRoZSBgRE1MX2FpcHdgIGZ1bmN0aW9uOg0KDQpgYGB7cn0NCiMgNS1mb2xkIGNyb3NzLWZpdHRpbmcgd2l0aCBjYXVzYWxETUwgcGFja2FnZQ0KYWlwdyA9IERNTF9haXB3KFksVyxYKQ0Kc3VtbWFyeShhaXB3JEFURSkNCg0KIyBJZiB5b3UgaGF2ZSBtb3JlIHRpbWUsIHR1bmUgdGhlIGZvcmVzdA0KIyBmb3Jlc3QgPSBjcmVhdGVfbWV0aG9kKCJmb3Jlc3RfZ3JmIixhcmdzPWxpc3QodHVuZS5wYXJhbWV0ZXJzID0gImFsbCIpKQ0KIyBhaXB3ID0gRE1MX2FpcHcoWSxXLFgsbWxfdz1saXN0KGZvcmVzdCksbWxfeT1saXN0KGZvcmVzdCksY2Y9NSkNCmBgYA0KDQo8YnI+DQo8YnI+DQoNCg0KIyBHQVRFIGVzdGltYXRpb24NCg0KVGhlIHBzZXVkby1vdXRjb21lIGNhbiBub3cgYmUgdXNlZCB0byBlc3RpbWF0ZSBkaWZmZXJlbnQgaGV0ZXJvZ2VuZW91cyBlZmZlY3RzLiBXZSB1c2Ugc3RhbmRhcmQgcmVncmVzc2lvbiBtb2RlbHMgYnV0IGJ5IHVzaW5nIHRoZSBwc2V1ZG8tb3V0Y29tZSBpbnN0ZWFkIG9mIGEgcmVhbCBvdXRjb21lIHdlIG1vZGVsIGVmZmVjdCBzaXplIGFuZCBub3Qgb3V0Y29tZSBsZXZlbC4NCg0KIyMgU3ViZ3JvdXAgZWZmZWN0DQoNCkZpcnN0LCBsZXQncyBjaGVjayBhIGNsYXNzaWMuIEdlbmRlciBkaWZmZXJlbmNlcy4gVGhpcyB3b3VsZCB1c3VhbGx5IGJlIGltcGxlbWVudGVkIGJ5IHNwbGl0dGluZyB0aGUgc2FtcGxlIGJ5IGdlbmRlciBhbmQgcmVydW5uaW5nIHRoZSB3aG9sZSBhbmFseXNpcyBpbiB0aGUgc3Vic2FtcGxlcyBzZXBhcmF0ZWx5Lg0KDQpXaXRoIHRoZSBwc2V1ZG8tb3V0Y29tZSBzdG9yZWQgaW4gYGFpcHckQVRFJGRlbHRhYCB0aGlzIGJvaWxzIGRvd24gdG8gcnVubmluZyBhbiBPTFMgcmVncmVzc2lvbiB3aXRoIHRoZSBgbWFsZWAgaW5kaWNhdG9yIGFzIHNpbmdsZSByZWdyZXNzb3IuDQoNCmBgYHtyfQ0KbWFsZSA9IFhbLDddDQpibHBfbWFsZSA9IGxtX3JvYnVzdChhaXB3JEFURSRkZWx0YSB+IG1hbGUpDQpzdW1tYXJ5KGJscF9tYWxlKQ0KYGBgDQoNCldlIGNhbiBpbnRlcnByZXQgdGhpcyBvdXRjb21lIGFzIHdlIGFyZSB1c2VkIHRvLCBvbmx5IHRoYXQgd2UgbW9kZWwgbm93IGVmZmVjdCBzaXplLiBUaGlzIG1lYW5zIHRoZSBpbnRlcmNlcHQgZ2l2ZXMgdXMgdGhlIGF2ZXJhZ2Ugb2YgdGhlIHJlZmVyZW5jZSBncm91cCAod29tZW4pIGFuZCB0aGUgY29lZmZpY2llbnQgdGVsbHMgdXMgaG93IG11Y2ggaGlnaGVyIHRoZSBlZmZlY3QgaXMgZm9yIG1lbi4gSW4gdGhpcyBjYXNlLCB3ZSBmaW5kIG5vIHNpZ25pZmljYW50IGdlbmRlciBkaWZmZXJlbmNlcyBpbiB0aGUgZWZmZWN0IG9mIDQwMShrKSBwYXJ0aWNpcGF0aW9uIG9uIG5ldCB3ZWFsdGguDQoNCklmIHlvdSBhcmUgaW50ZXJlc3RlZCBpbiB0aGUgZ2VuZGVyIHNwZWNpZmljIGVmZmVjdCBpbnN0ZWFkIG9mIGRpZmZlcmVuY2VzIGJldHdlZW4gZ3JvdXBzLCBqdXN0IHJ1biBhbiBPTFMgcmVncmVzc2lvbiB3aXRob3V0IGNvbnN0YW50IGFuZCBhbGwgZ3JvdXAgaW5kaWNhdG9yczoNCg0KYGBge3J9DQpmZW1hbGUgPSAxLW1hbGUNCmJscF9tYWxlMSA9IGxtX3JvYnVzdChhaXB3JEFURSRkZWx0YSB+IDAgKyBmZW1hbGUgKyBtYWxlKQ0Kc3VtbWFyeShibHBfbWFsZTEpDQpgYGANCg0KWW91IHNlZSB0aGF0IHdlIGNhbiB0cmFuc2ZlciBhbGwgdGhlIHN0cmF0ZWdpZXMgdGhhdCB3ZSBrbm93IGFib3V0IG1vZGVsaW5nIG91dGNvbWVzIHdpdGggT0xTIGZvciBtb2RlbGxpbmcgY2F1c2FsIGVmZmVjdHMuDQoNCjxicj4NCg0KIyMgQmVzdCBsaW5lYXIgcHJlZGljdGlvbg0KDQpNYXliZSB3ZSBkbyBub3Qgd2FudCB0byBmb2N1cyBvbiBzdWJncm91cCBhbmFseXNlcyBidXQgdG8gbW9kZWwgdGhlIGVmZmVjdCB1c2luZyBhbGwgbWFpbiBlZmZlY3RzIGF0IG91ciBkaXNwb3NhbC4gSW4gc3RhbmRhcmQgT0xTIHRoaXMgd291bGQgbWVhbiB0byBpbmNsdWRlIGEgbG90IG9mIGludGVyYWN0aW9uIGVmZmVjdHMgd2hpbGUgY29tcGxldGVseSByZWx5aW5nIG9uIGNvcnJlY3Qgc3BlY2lmaWNhdGlvbiBvZiB0aGUgb3V0Y29tZSBtb2RlbC4NCg0KVXNpbmcgdGhlIHBzZXVkby1vdXRjb21lIGFsbG93cyB1cyB0byBiZSBjb21wbGV0ZWx5IGFnbm9zdGljIGFib3V0IHRoZSBvdXRjb21lIG1vZGVsIGFuZCB0byByZWNlaXZlIGEgbmljZSBzdW1tYXJ5IG9mIHRoZSB1bmRlcmx5aW5nIGVmZmVjdCBoZXRlcm9nZW5laXR5IGluIGEgZmFtaWxpYXIgZm9ybWF0LCBhbiBPTFMgb3V0cHV0Og0KDQpgYGB7cn0NCmJscCA9IGxtX3JvYnVzdChhaXB3JEFURSRkZWx0YSB+IFgpDQpzdW1tYXJ5KGJscCkNCmBgYA0KDQpGb3IgZXhhbXBsZSwgd2Ugc2VlIHRoYXQsIGFsbCBvdGhlciByZWdyZXNzb3JzIGhlbGQgY29uc3RhbnQsIGJlaW5nIG9uZSB5ZWFyIG9sZGVyIGluY3JlYXNlcyB0aGUgZWZmZWN0IG9mIDQwMShrKSBwYXJ0aWNpcGF0aW9uIG9mIHdlYWx0aCBvbiBhdmVyYWdlIGJ5ICQgYHIgcm91bmQoYmxwJGNvZWZmaWNpZW50c1syXSlgLg0KDQpBZ2FpbiB3ZSByZWFsaXplIHRoYXQgZXZlcnl0aGluZyB3ZSBsZWFybmVkIGFib3V0IE9MUyBmb3IgbW9kZWxsaW5nIG91dGNvbWVzIGRpcmVjdGx5IHRyYW5zbGF0ZXMgdG8gbW9kZWxpbmcgZWZmZWN0IHNpemVzLg0KDQoNCjxicj4NCg0KDQojIyBOb24tcGFyYW1ldHJpYyBoZXRlcm9nZW5laXR5DQoNClRoZSBpbWhvIGNvb2xlc3QgdGhpbmcgYWJvdXQgaGF2aW5nIHRoZSBwc2V1ZG8tb3V0Y29tZSBpcyB0aGF0IHdlIGNhbiBhbHNvIGVzdGltYXRlIGhldGVyb2dlbmVvdXMgZWZmZWN0cyB3aXRoIG5vbnBhcmFtZXRyaWMgcmVncmVzc2lvbnMuIFRoaXMgbWVhbnMgd2UgYXJlIG5vdCBvbmx5IGFnbm9zdGljIGFib3V0IHRoZSBvdXRjb21lIGFuZCBwcm9wZW5zaXR5IHNjb3JlIG1vZGVscyBidXQgYWxzbyBhYm91dCB0aGUgZnVuY3Rpb25hbCBvZiBlZmZlY3QgaGV0ZXJvZ2VuZWl0eS4gDQoNClRoaXMgaXMgZXNwZWNpYWxseSB1c2VmdWwgaWYgd2UgaGF2ZSBzb21lIGNvbnRpbnVvdXMgdmFyaWFibGUgbGlrZSBhZ2UgZm9yIHdoaWNoIHdlIHdhbnQgdG8gdW5kZXJzdGFuZCBlZmZlY3QgaGV0ZXJvZ2VuZWl0eS4NCg0KPGJyPg0KDQojIyMgU3BsaW5lIHJlZ3Jlc3Npb24NCg0KVGhlIGBzcGxpbmVfY2F0ZWAgZnVuY3Rpb24gaW1wbGVtZW50cyBzcGxpbmUgcmVncmVzc2lvbiByZXVzaW5nIHRoZSBwc2V1ZG8tb3V0Y29tZToNCg0KYGBge3IsIHJlc3VsdHM9J2hpZGUnfQ0KYWdlID0gWFssMV0NCnNyX2FnZSA9IHNwbGluZV9jYXRlKGFpcHckQVRFJGRlbHRhLGFnZSkNCmBgYA0KDQpgYGB7cn0NCnBsb3Qoc3JfYWdlLHpfbGFiZWwgPSAiQWdlIikNCmBgYA0KDQo8YnI+DQoNCiMjIyBLZXJuZWwgcmVncmVzc2lvbg0KDQpUaGUgYGtyX2NhdGVgIGZ1bmN0aW9uIGltcGxlbWVudHMga2VybmVsIHJlZ3Jlc3Npb24gcmV1c2luZyB0aGUgcHNldWRvLW91dGNvbWU6DQoNCmBgYHtyLCByZXN1bHRzPSdoaWRlJ30NCmtyX2FnZSA9IGtyX2NhdGUoYWlwdyRBVEUkZGVsdGEsYWdlKQ0KYGBgDQoNCmBgYHtyfQ0KcGxvdChrcl9hZ2Usel9sYWJlbCA9ICJBZ2UiKQ0KYGBgDQoNCkJvdGggbm9ucGFyYW1ldHJpYyBhcHByb2FjaGVzIGRvY3VtZW50IHRoZSBzYW1lIHBhdHRlcm4uIFRoZSBlZmZlY3Qgb2YgNDAxKGspIHBhcnRpY2lwYXRpb24gb24gbmV0IHdlYWx0aCBpbmNyZWFzZXMgbW9yZSBvciBsZXNzIGxpbmVhcmx5IHVudGlsIHRoZSBhZ2Ugb2YgNTAgYW5kIHRoZW4gc2xpZ2h0bHkgZHJvcHMuDQoNCkZvciBtZSB0aGlzIGlzIGFuIGluY3JlZGlibGUgbmV3IG9wdGlvbiBmb3Igb3VyIHBvbGljeSBldmFsdWF0aW9uIHRvb2xib3guDQoNCjxicj4NCjxicj4=