Chapter 2

Code Call-out 2.1: Regression, Comparison of Means and Covariates

In this code call-out we will explore some basic elements of regression as a means to estimate treatment effects. First we will, very simply, confirm the equivalence between a regression and difference of means estimator for the pooint estimate of binary treatment receipt on outcomes. And secondly, we will explore effect heterogeneity by a single covariate. We will do this with data from Bari et al. (2024), who experimentally assess the impact of access to large business assets in microenteprises in Pakistan, where a randomly treated sample was offered a larger microfinance route to purchase such large assets.

Equivalence between regression and comparison of means

To understand the equivalence between regression analysis and the comparison of means in a binary regression set-up, we will first open the data from Bari et al. (2024) and run a basic regression such as that documented in Table 4 of their paper:

clear all
set more off
use "data/Bari_et_al_2024.dta", clear

reg biz_ta A if wave==1, robust
scalar coef_treatment = _b[A]

Linear regression                               Number of obs     =        739
                                                F(1, 737)         =      14.28
                                                Prob > F          =     0.0002
                                                R-squared         =     0.0192
                                                Root MSE          =     1712.5

------------------------------------------------------------------------------
             |               Robust
      biz_ta | Coefficient  std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
           A |   507.0368   134.1654     3.78   0.000      243.645    770.4287
       _cons |   1149.011   109.9867    10.45   0.000     933.0865    1364.936
------------------------------------------------------------------------------

Here biz_ta is an outcome measuring total business assets, and this is measured at wave 1 (ie the wave in data immediately after treatment was first provided).

To confirm that this is–by definition–simply equivalent to a difference in means we can consider the quantities which would be observed if we substituted \(A=1\) and \(A=0\) into the above equation. In this case we can see that the regression consists of a constant term: \(E[Y_i|Treat_i=0]\), and a second term which capture the difference between units receiving treatment and those not receiving treatment: \(E[Y_i|Treat_i=1]-E[Y_i|Treat_i=0]\). This latter is just the treatment effect of interest. Below we confirm that the coefficient on \(A\) above is equivalent to the difference in means.

// Calculate mean for the treatment group
summarize biz_ta if A==1 & wave==1
scalar mean_treatment = r(mean)

// Calculate mean for the control group
summarize biz_ta if A==0 & wave==1
scalar mean_control = r(mean)

// Calculate and display the difference in means
scalar diff_means = mean_treatment - mean_control
display "Coefficient for treatment in regression: " coef_treatment
display "Difference in means (Treatment - Control): " diff_means

// Display the comparison
di "The coefficient from the regression should be equal to the difference in means to demonstrate equivalence."

    Variable |        Obs        Mean    Std. dev.       Min        Max
-------------+---------------------------------------------------------
      biz_ta |        491    1656.048    1701.928          0   7666.667

    Variable |        Obs        Mean    Std. dev.       Min        Max
-------------+---------------------------------------------------------
      biz_ta |        248    1149.011    1733.225          0   7666.667
Coefficient for treatment in regression: 507.03681
Difference in means (Treatment - Control): 507.03681
The coefficient from the regression should be equal to the difference in means 
> to demonstrate equivalence.

Here we see that, as expected, our mean for the control group is equivalent to the constant in the regression, and the difference in means is equivalent to the coefficient on the treatment indicator in the regression.

Heterogeneity by a baseline covariate \(X\)

Let’s now consider the incorporation of a covariate into this regression and confirm that we can also quite simply break down (linear) heterogeneity, as discussed in (2.10) of the book. Here we will consider a variable which measures the management capacity of the small businesses in the sample. We will consider this management capacity at baseline, which below we generate, standardise so that it is mean zero, and then interact with treatment:

//Generate baseline value
gen temp = mpall if wave==0
bys id: egen manage = mean(temp)

//Standardise so mean-zero
sum manage
replace manage = manage-r(mean)

//Interact variable with treatment
gen Amanage = A*manage
(3,608 missing values generated)

    Variable |        Obs        Mean    Std. dev.       Min        Max
-------------+---------------------------------------------------------
      manage |      4,365    .0263182    .7326993  -1.050634   2.332034
(4,365 real changes made)

We can now consider whether there are differential treatment effects by a firm’s management capacity. To do so, we estimate the interacted regression below which allows for (a) a differential intercept for treated and untreated groups by the inclusion of the treatment dummy and a constant, and (b) a differential slope by management capacity, by including both a control for management capacity, as well as an interaction with treatment:

reg biz_ta A Amanage manage if wave==1, robust

Linear regression                               Number of obs     =        739
                                                F(3, 735)         =      10.90
                                                Prob > F          =     0.0000
                                                R-squared         =     0.0354
                                                Root MSE          =     1700.6

------------------------------------------------------------------------------
             |               Robust
      biz_ta | Coefficient  std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
           A |   485.2146   134.5099     3.61   0.000     221.1452    749.2839
     Amanage |  -229.2233   192.7532    -1.19   0.235    -607.6357    149.1891
      manage |   442.0014   163.0788     2.71   0.007     121.8456    762.1572
       _cons |    1167.59    110.539    10.56   0.000     950.5801      1384.6
------------------------------------------------------------------------------

Above we eastimate this regression, and observe that while in general business with higher management capacity at baseline have much higher business assets, there is not (at least in wave 1) a statistically significantly different return to management among treated and untreated individuals. There is, however, a quite large negative effect on the interaction, weakly suggestive of smaller treatment effects among firms with higher management capacity. We can visualise this as in Figure 2.1 of the book. There are many ways we can do this, and you may wish to explore alternative ways of setting this up by considering marginal effects, or by confirming that the four quantities estimated in the regression above correspond to the two intercepts and two slopes of the lines plotted below which can be similarly plotted “by hand” based on these values. However, below we can see that it is sufficient to plot the predicted effect among treated and control units across the range of the manage variable. We do this in the code block below, first generating this predicted effect from the regression, and then plotting resulting curves for treated and control units.

predict yhat if e(sample)==1, xb
twoway line yhat manage if A==1, lcolor(red) lwidth(thick) ///
    || line yhat manage if A==0, lcolor(blue) lwidth(thick) lpattern(dash) ///
     legend(order(1 "Treated" 2 "Untreated") pos(6) rows(1)) 
(3,626 missing values generated)

While this is just a simple illustration, it points to the flexibility of regression for capturing treatment effects when treatment is randomly assigned, and assumptions of unconditional unconfoundedness are reasonable.

Code Call-out 2.2: Randomization Inference

Randomization inference is perhaps best-illustrated with practical examples. A particularly illuminating approach to understand how randomisation inference works is visualization through tabular permutation. In this code call-out we will first consider a made-up example based on 3 treated units and 3 control units, before working with a larger number of control and treated units in data from an experiment implemented by Banerjee, Duflo, and Sharma (2021), which we discuss below.

An Exact p-value

It is perhaps useful to see a simple example. Consider the case of 6 units, with 3 observations randomly assigned treatment. Imagine that after having been exposed to treatment, the observed outcomes were, in the treatment group: \((34,27,29)\) and in the control group: \((14,18,24)\). A simple comparison of means estimator suggests that the treatment effect is 11.33. To calculate a p-value, we can permute all the possible combinations, and ask what proportion of these are greater than or equal to this treatment effect. If we consider random orderings of 6 units, this suggests that there are \(6!\) possible combinations, but in reality, as we are randomly choosing 3 units from these 6 to assign a permuted treatment status, the actual value of different combinations is \(6\choose 3\) \(=\frac{6!}{3!*(6-3)!}=20\). We document each of these possible permutations, as well as their permuted treatment effect in the Table below. In this case, we can see that only 1 of the 20 different permutations is greater than or equal to 11.33 (the original treatment assignment). Suggesting an exact p-value of \(1/20=0.05\) if a one-sided test is considered. If, however, we wish to consider a two-sided p-value, there are two values as extreme as 11.33, which is permutation 1, and permutation 20 below, suggesting a two-sided p-value of \(2/10=0.10\).

A Simple Illustration of Randomization Inference
Permutation T1 T2 T3 C1 C2 C3 Estimate
Original (1) 34 27 29 14 18 24 11.33
2 34 27 14 29 18 24 1.33
3 34 27 18 14 29 24 4
4 34 27 24 14 18 29 8
5 34 14 29 27 18 24 2.67
6 34 18 29 14 27 24 5.33
7 34 24 29 14 18 27 9.33
8 14 27 29 34 18 24 -2
9 18 27 29 14 34 24 0.67
10 24 27 29 14 18 34 4.67
11 34 14 18 27 29 24 -4.67
12 34 14 24 27 18 29 -0.67
13 34 18 24 14 27 29 2
14 14 27 18 34 29 24 -9.33
15 14 27 24 34 18 29 -5.33
16 18 27 24 14 34 29 -2.67
17 14 18 29 34 27 24 -8
18 14 24 29 34 18 27 -4
19 18 24 29 14 34 27 -1.33
20 14 18 24 34 27 29 -11.33

While this is so simple that we can set it up by hand, it is also useful to see how we can compute this in Stata. First, we will load our “data”:

clear
input Y W
34 1
27 1
29 1
14 0
18 0
24 0
end

and then calculate a comparison of means estimator of the ATT:

sum Y if W == 1
local mean_Y1 = r(mean)
sum Y if W == 0
local mean_Y0 = r(mean)

// Treatment effect
local tau_hat = `mean_Y1' - `mean_Y0'
dis "Treatment effect is:" `tau_hat'

    Variable |        Obs        Mean    Std. dev.       Min        Max
-------------+---------------------------------------------------------
           Y |          3          30    3.605551         27         34

    Variable |        Obs        Mean    Std. dev.       Min        Max
-------------+---------------------------------------------------------
           Y |          3    18.66667    5.033223         14         24
Treatment effect is:11.333333

Now let’s generate our p-value by permuting all possible treatment combinations. While there are many ways we could consider doing this, one is to note that we could start our permutation by assigning units 1, 2 and 3 to treatment and the last three units to control, and subsequently increase the units assigned to treatment until we are assigning units 4, 5 and 6 to treatment, and the remaining units to control. Below we begin by preparing a new frame of data in which to store our resulting treatment effects:

frame create permutations
cwf permutations
set obs 20
gen permutation = _n
gen effect      = .
cwf default
Number of observations (_N) was 0, now 20.
(20 missing values generated)

This is simply a frame containing 20 lines where permutation increases from 1 to 20, with an empty variable effect that we can fill in with our effects. Now, let’s begin permuting our effect, where we will generate a variable Wperm which is our new treated unit. The below code will iterate through the three possible units receiving treatment (t1, t2 and t3), starting from units 1, 2 and 3, and going until these reach units 3, 4 and 5 respectively. It is worth reading through this code carefully given that this is done “manually”, but in each loop we will print out the units receiving treatment so that we can convince ourselves that we are indeed seeing the 20 treatment combinations mentioned in the table above.

gen Wperm = .
local t1 = 1
local t2 = 2
local t3 = 3

local j=1
while `t1'<=4 {
    while `t2'<=5 {
        while `t3'<= 6 {
            dis "Treated units are `t1', `t2', `t3'"
            qui replace Wperm = 0
            qui replace Wperm = 1 in `t1'
            qui replace Wperm = 1 in `t2'
            qui replace Wperm = 1 in `t3'
            qui reg Y Wperm
            // Store permuted effect in permutation frame
            qui frame permutations: replace effect = _b[Wperm] in `j'

            // Increase to next permutation
            local ++j
            // Increase so unit 3 goes up by one unit
            local ++t3
        }
        // We have now reached unit 6 for the third treated unit, so increase second treated unit and start again
        local ++t2
        local t3=`t2'+1
    }
    // We have now reached unit 5 for 2nd unit and 6 for the third treated unit
    // so increase first treated unit and start again    
    local ++t1
    local t2=`t1'+1
    local t3=`t2'+1
}
(6 missing values generated)
Treated units are 1, 2, 3
Treated units are 1, 2, 4
Treated units are 1, 2, 5
Treated units are 1, 2, 6
Treated units are 1, 3, 4
Treated units are 1, 3, 5
Treated units are 1, 3, 6
Treated units are 1, 4, 5
Treated units are 1, 4, 6
Treated units are 1, 5, 6
Treated units are 2, 3, 4
Treated units are 2, 3, 5
Treated units are 2, 3, 6
Treated units are 2, 4, 5
Treated units are 2, 4, 6
Treated units are 2, 5, 6
Treated units are 3, 4, 5
Treated units are 3, 4, 6
Treated units are 3, 5, 6
Treated units are 4, 5, 6

In the code above, within each loop we have estimated our permuted treatment effect, and placed it in the permutations frame. We can now open up this frame, and compare these permutation-based effects to the original effect (which is the first effect stored in our data), and calculate the one-sided and two-sided p-values. We do this below.

// Change to permutations frame
cwf permutations
list

// Calculate a one sided p-value
count if effect >= effect[1]
local p1side = `r(N)' / 20

// Calculate a two sided p-value
count if abs(effect) >= effect[1]
local p2side = `r(N)' / 20


display "One sided p-value: " `p1side'
display "Two sided p-value: " `p2side'

histogram effect, bins(10) xline(11.333, lcolor(red) lwidth(thick))

     +----------------------+
     | permut~n      effect |
     |----------------------|
  1. |        1    11.33333 |
  2. |        2    1.333333 |
  3. |        3           4 |
  4. |        4           8 |
  5. |        5    2.666667 |
     |----------------------|
  6. |        6    5.333333 |
  7. |        7    9.333333 |
  8. |        8   -4.666667 |
  9. |        9   -.6666667 |
 10. |       10           2 |
     |----------------------|
 11. |       11          -2 |
 12. |       12    .6666667 |
 13. |       13    4.666667 |
 14. |       14   -9.333333 |
 15. |       15   -5.333333 |
     |----------------------|
 16. |       16   -2.666667 |
 17. |       17          -8 |
 18. |       18          -4 |
 19. |       19   -1.333333 |
 20. |       20   -11.33333 |
     +----------------------+
  1
  2
One sided p-value: .05
Two sided p-value: .1
(bin=10, start=-11.333333, width=2.2666666)

Here we can confirm that we find the p-values we calculated by hand above, and view these permutations graphically in the resulting histogram above.

A Real Example

It turns out, that to translate this to a case with real data, we do not need to add many additional elements. In this section, we work with data from a randomized control trial that examines asset transfers to poor households in India, as discussed in the paper by Banerjee, Duflo, and Sharma (2021). Here we will consider the impact of receipt of asset transfers on two outcomes: a financial index ind_fin_el1 and an asset index asset_ind_tot_el1.

Before we explore randomisation inference, let’s open these data and estimate a simple regression, considering the effect of treatment receipt on the asset index:

import delimited "data/Banerjee_et_al_2021.csv", clear
regress asset_ind_tot_el1 treatment, robust
local assetEffect = _b[treatment]
(encoding automatically selected: ISO-8859-1)
(8 vars, 991 obs)

Linear regression                               Number of obs     =        682
                                                F(1, 680)         =      11.94
                                                Prob > F          =     0.0006
                                                R-squared         =     0.0174
                                                Root MSE          =     1.5312

------------------------------------------------------------------------------
             |               Robust
asset_ind_~1 | Coefficient  std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
   treatment |   .4077528   .1179794     3.46   0.001     .1761052    .6394004
       _cons |   -.196104   .0756921    -2.59   0.010    -.3447222   -.0474858
------------------------------------------------------------------------------

From the summary we can note that \(\widehat{\tau}_{ATE} = 0.408\) and its standard error equals 0.118. This suggests a p-value of around 0.0005. We have saved the original estimated effect in a local (assetEffect) which we will use below.

We can similarly consider the financial index:

regress ind_fin_el1 treatment, robust
local finEffect = _b[treatment]

Linear regression                               Number of obs     =        815
                                                F(1, 813)         =       0.05
                                                Prob > F          =     0.8174
                                                R-squared         =     0.0001
                                                Root MSE          =      .5931

------------------------------------------------------------------------------
             |               Robust
 ind_fin_el1 | Coefficient  std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
   treatment |  -.0095893   .0415171    -0.23   0.817    -.0910826     .071904
       _cons |   .1354387   .0295983     4.58   0.000     .0773407    .1935367
------------------------------------------------------------------------------

where we observe a point estimate quite close to zero, and p-value of 0.817.

Let’s now consider how we conduct permutation inference in this particular case where there are too many permutations of treatment to feasibly consider them all. Here, rather than considering all possible permutations, we will consider randomly assigned permutations of treatment which respect the original treatment assignment structure. Namely, if we inspect these data, we can see that there are 991 units, 525 of whom receive treatment. We can thus conduct a single permutation by randomly re-assigning treatment to 525 units, and control to the remaining units. We do this below, estimating the corresponding effect.

tab treatment
set seed 121316
gen epsilon = rnormal()
sort epsilon

generate Wperm = 1 in 1/525
replace  Wperm = 0 in 526/991

regress asset_ind_tot_el1 Wperm, robust
drop Wperm epsilon

  treatment |      Freq.     Percent        Cum.
------------+-----------------------------------
          0 |        466       47.02       47.02
          1 |        525       52.98      100.00
------------+-----------------------------------
      Total |        991      100.00
(466 missing values generated)
(466 real changes made)

Linear regression                               Number of obs     =        682
                                                F(1, 680)         =       0.48
                                                Prob > F          =     0.4878
                                                R-squared         =     0.0007
                                                Root MSE          =     1.5442

------------------------------------------------------------------------------
             |               Robust
asset_ind_~1 | Coefficient  std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
       Wperm |   .0821643   .1183721     0.69   0.488    -.1502545    .3145831
       _cons |  -.0430098   .0854911    -0.50   0.615    -.2108679    .1248484
------------------------------------------------------------------------------

Here we see that, perhaps unsurprisingly, our randomly assigned treatment results in a small estimated “effect”, of 0.082, which is substantially smaller than the original treatment effect itself.

To estimate a permutation-based p-value all we need to do is repeat this procedure a large number of times (below 10,000 times), and count the proportion of permuted effects which are larger than our estimated treatment effect. We set this up below, using the ++ notation to increase our local pval by one any time the condition on line 12 is met:

local Npermut = 10000
local pval = 0
forvalues i=1/`Npermut' {
    qui {
        gen epsilon = rnormal()
        sort epsilon
        generate Wperm = 1 in 1/525
        replace  Wperm = 0 in 526/991
        regress asset_ind_tot_el1 Wperm
    }
    // Increase pval by 1 if permuted effect is larger than true effect
    if abs(_b[Wperm])>abs(`assetEffect') local ++pval
    drop epsilon Wperm
}
dis `pval'
dis `pval'/`Npermut'
8
.0008

Here we see that among all of our 10,000 placebos, only 8 effects were larger than the effect we actually observed in our data, suggesting a p-value of 8/10000, or 0.0008.

We can generalise this idea below (for this specific data set-up), and below we do this defining a program which we call randomisation_inference. This program takes a single argument, which is the outcome we wish to consider, and packages the procedure we have explored above, finally printing the p-value and a number of other pieces of information. While we could further optimise this program, for example to automatically infer how to permute the treatment assignment and to return resulting p-values to users, the below is sufficient for our interests here of estimating a randomisation-based p-value.

program randomisation_inference
    args varname
    qui: reg `varname' treatment
    local obs_effect = _b[treatment]

    // Set number of permutations
    local Npermut = 10000
    local pval = 0

    forvalues i = 1/`Npermut' {
        // Generate a permuted treatment variable
        qui {
            gen epsilon = rnormal()
            sort epsilon
            generate Wperm = 1 in 1/525
            replace  Wperm = 0 in 526/991
            regress `varname' Wperm
        }
        // Increase pval by 1 if permuted effect is larger than true effect
        if abs(_b[Wperm])>abs(`obs_effect') local ++pval
        drop epsilon Wperm
    }

    // Calculate p-value
    local p_value = `pval' / `Npermut'
    display "The observed effect of " "`varname'" " is " `obs_effect' " and its p-value is " `p_value'
end

Finally, we can run this program, simply passing as an argument the two variables we have examined above. We do this below:

randomisation_inference ind_fin_el1
randomisation_inference asset_ind_tot_el1
The observed effect of ind_fin_el1 is -.00958932 and its p-value is .8245
The observed effect of asset_ind_tot_el1 is .40775282 and its p-value is .0004

If we compare these p-values to regression-based estimates, we will see that, perhaps unsurpringly, they are very similar.

Code Call-out 2.3: Bootstrap

For an introduction to the bootstrap, refer to Section 2.4.4 of the book. Here we will examine a computational implementation of a bootstrap standard error and confidence intervals, using data from Chong et al. (2016). First we will consider the main regression \[Y_i = \mu + \tau_{ATE}W_i + \varepsilon_i.\] Here we will work with the data Chong_et_al_2016, and examine the total cognitive score (wii_total) as our outcome measure \(Y_i\). We can load the data and examine selected variables on the first few lines of this dataset, before estimating the treatment effect via regression below.

use "data/Chong_et_al_2016.dta", clear
list student_id hhid treat in 1/5

keep if wii_total != . & treat != .
reg wii_total treat

     +-------------------------+
     | studen~d   hhid   treat |
     |-------------------------|
  1. |   001001      1       0 |
  2. |   001002      1       0 |
  3. |   001003      1       0 |
  4. |   002001      2       0 |
  5. |   002002      2       1 |
     +-------------------------+
(0 observations deleted)

      Source |       SS           df       MS      Number of obs   =       208
-------------+----------------------------------   F(1, 206)       =      5.17
       Model |  154413.413         1  154413.413   Prob > F        =    0.0240
    Residual |  6147604.41       206  29842.7399   R-squared       =    0.0245
-------------+----------------------------------   Adj R-squared   =    0.0198
       Total |  6302017.83       207  30444.5306   Root MSE        =    172.75

------------------------------------------------------------------------------
   wii_total | Coefficient  Std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
       treat |   57.46253   25.26164     2.27   0.024     7.658023     107.267
       _cons |   455.6642   14.75907    30.87   0.000      426.566    484.7624
------------------------------------------------------------------------------

From the summary we can note that \(\widehat{\tau}_{ATE} = 57.46\) and its standard error equals 25.26. This suggests a p-value of 0.024, and 95% confidence intervals of [7.66;107.267]. In this case the standard errors are computed via the standard OLS variance estimator, assuming homoscedastic errors, although heteroscedasticity-robust standard errors can be requested quite simply using the robust option from the command regress This is shown below, resulting in slight variations to the reported standard error (and correspondingly, p-value and resulting confidence intervals).

reg wii_total treat, robust

Linear regression                               Number of obs     =        208
                                                F(1, 206)         =       5.65
                                                Prob > F          =     0.0184
                                                R-squared         =     0.0245
                                                Root MSE          =     172.75

------------------------------------------------------------------------------
             |               Robust
   wii_total | Coefficient  std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
       treat |   57.46253   24.17172     2.38   0.018     9.806859    105.1182
       _cons |   455.6642   15.41476    29.56   0.000     425.2733    486.0551
------------------------------------------------------------------------------

An alternative to these closed-form methods for variance estimation is to use bootstrap resampling methods. There are multiple ways a bootstrap can be implemented, but the simplest here will be to simply conduct a paired bootstrap, which is also robust to heteroscedasticity. The pairs bootstrap consists of, first, sampling with replacement the ordered pairs \(\left\{(y_i,w_i)\right\}_{i=1}^{N} = \left\{(y_1,w_1),\ldots,(y_N,w_N)\right\}\) from original data, obtaining a “new” dataset of \(N\) resampled pairs \(\left\{(y_i^*,w_i^*)\right\}_{i=1}^{N} = \left\{(y_1^*,w_1^*),\ldots,(y_N^*,w_N^*)\right\}\). The new dataset thus simply consists of (potentially repeated) randomly selected rows of the original data. Let’s see what this looks like with a single bootstrap replicate. First, we will generate this new dataset, having a look at the first 5 rows of selected variables in the new dataset.

preserve
set seed 121316
bsample
list student_id hhid treat in 1/5
(Note: Below code run with echo to enable preserve/restore functionality.)





     +-------------------------+
     | studen~d   hhid   treat |
     |-------------------------|
  1. |   011001     11       0 |
  2. |   188001    188       0 |
  3. |   174001    174       0 |
  4. |   148001    148       1 |
  5. |   097001     97       0 |
     +-------------------------+

With this bootstrap sample we estimate again the coefficient of interest with standard OLS procedure:

reg wii_total treat
restore
(Note: Below code run with echo to enable preserve/restore functionality.)


      Source |       SS           df       MS      Number of obs   =       208
-------------+----------------------------------   F(1, 206)       =      0.81
       Model |  25627.5269         1  25627.5269   Prob > F        =    0.3680
    Residual |  6485700.15       206  31483.9813   R-squared       =    0.0039
-------------+----------------------------------   Adj R-squared   =   -0.0009
       Total |  6511327.67       207  31455.6892   Root MSE        =    177.44

------------------------------------------------------------------------------
   wii_total | Coefficient  Std. err.      t    P>|t|     [95% conf. interval]
-------------+----------------------------------------------------------------
       treat |   23.40968   25.94699     0.90   0.368    -27.74602    74.56539
       _cons |    494.365   15.15949    32.61   0.000     464.4773    524.2526
------------------------------------------------------------------------------


With this sample, we have obtained an estimate of \(\widehat{\tau}_{ATE}^* =23.41\); a different value to the original value, in line with variation in the sample used for estimation. If we want some idea of how much estimates vary as the sample changes (ie the sampling variation of our estimate), the next step simply consists of repeating this bootstrap process \(B\) times, resulting in \(B\) estimates of \(\widehat{\tau}_{ATE,(b)}^*\) with \(b\in\{1,\ldots,B\}\). Let’s set \(B=1,000\), and do this:

matrix taus = J(1000, 1, .)
forvalues i = 1/1000 {
  quietly{
    preserve
    bsample
    reg wii_total treat
    matrix define taus[`i',1] = _b[treat]
    restore
  }
}

You may wish to step through each of the lines in this loop to ensure that you can see what is going on, but the end product of this code is a column matrix called taus which has been filled in with \(B\) estimates of \(\tau\), \(\left\{\widehat{\tau}^{(b)}\right\}_{b=1}^{B}\). The next step is to convert this column matrix into a variable in our dataset

clear 
svmat taus, names("taus")
number of observations will be reset to 1000
Press any key to continue, or Break to abort
Number of observations (_N) was 0, now 1,000.

We can have a look at the entire empirical distribution of these estimates as follows:

_pctile taus1, percentiles(2.5 97.5)
local p25 = r(r1)
local p975 = r(r2)
kdensity taus1, scheme(plottig) title("{&tau} Empirical Distribution") xtitle("{&tau}") xlabel(, format(%3.2f)) xline(`p25' `p975', lcolor(red) lpattern(dash)) xline(57.46253, lcolor(gs8) lpattern(solid) lwidth(vthick))

Bootstrap Plot Example

As we can see, this empirical distribution is close to centered on the original estimate (in the limit, they will be exactly the same, which is something you may wish to confirm by using a larger value for \(B\)), while also giving us some idea of the variation of the estimate over alternative resamples. We additionally plot the empirical 2.5th and 97.5th quantiles of the distribution, which provides a 95% confidence interval for \(\widehat\tau\). Clearly, and in line with the regression results observed above, we can reject the null that \(\tau=0\) with some certainty. Using these values \(\left\{\widehat{\tau}^{(b)}\right\}_{b=1}^{B}\), finally we display the estimate’s standard error as the standard deviation of the bootstrap estimates, and the confidence intervals can be generated from empirical quantiles. We illustrate this below:

qui sum taus1
di as text "The variance estimate is: " as result round(r(Var), 0.0001) ///
as text " as such, the standard error estimates is: " as result round(r(sd), 0.0001)
_pctile taus1, percentiles(2.5 97.5)
di as text "The empirical 95% confidence interval is: [" ///
as result round(r(r1), 0.0001) as text "," ///
as result round(r(r2), 0.0001) as text "]."
The variance estimate is: 599.7058 as such, the standard error estimates is: 24
> .4889
The empirical 95% confidence interval is: [10.7098,104.6838].

We can see that the standard error, and hence 95% confidence interval, is very closely alligned to that from regression documented above, given both are valid under broadly similar assumptions.

References

Banerjee, Abhijit, Esther Duflo, and Garima Sharma. 2021. Long-Term Effects of the Targeting the Ultra Poor Program.” American Economic Review: Insights 3 (4): 471–86. https://doi.org/10.1257/aeri.20200667.
Bari, Faisal, Kashif Malik, Muhammad Meki, and Simon Quinn. 2024. “Asset-Based Microfinance for Microenterprises: Evidence from Pakistan.” American Economic Review 114 (2): 534–74. https://doi.org/10.1257/aer.20210169.
Chong, Alberto, Isabelle Cohen, Erica Field, Eduardo Nakasone, and Maximo Torero. 2016. “Iron Deficiency and Schooling Attainment in Peru.” American Economic Journal: Applied Economics 8 (4): 222–55. https://doi.org/10.1257/app.20140494.