---
title: "fwildclusterboot"
author: Alexander Fischer
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{fwildclusterboot}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
library(fwildclusterboot)
```

The `fwildclusterboot` package implements the "fast" wild cluster bootstrap algorithm developed in Roodman et al (2019) for regression objects in R. The "fast" algorithm makes it feasible to calculate test statistics based on a large number of bootstrap draws even for large samples - as long as the number of bootstrapping clusters is not too large.

For linear regression models, `fwildclusterboot` supports almost all features of Stata's `boottest` package. This means that a set of different bootstrap distributions, regression weights, fixed effects, and both restricted (WCR) and unrestricted (WCU) bootstrap inference are supported. The main difference is that it currently only supports univariate hypothesis tests of regression paramters of the form $H_{0}: R\beta = r$ vs $H_{1}: R\beta \neq r$, where r is scalar.

Further, `fwildclusterboot` serves as an R port to [WildBootTests.jl](https://github.com/droodman/WildBootTests.jl), which implements the fast wild cluster bootstrap in Julia at extreme speed. Beyond being significantly faster than `fwildclusterboot's` native R implementation of the wild cluster bootstrap for OLS (in particular for more demanding problems), `WildBootTests.jl` offers support for the WRE bootstrap for IV models ([(Davidson & MacKinnon, 2010)](https://www.tandfonline.com/doi/abs/10.1198/jbes.2009.07221)) and functionality for tests of multiple hypothesis.

A description of the "fast" algorithm is beyond the scope of this vignette. It is very clearly presented in [Roodman et al. (2019)](https://econpapers.repec.org/paper/qedwpaper/1406.htm).
For technical details of the implementation in `fwildclusterboot`, have a look at the `technical vignette` (tba).

# The `boottest()` function

The `fwildclusterboot` package consists of one key function, `boottest()`. It implements the fast wild bootstrap and works with regression objects of type `lm`, `felm` and `fixest` from base R and the `lfe` and `fixest` packages. 

To start, we create a random data set with two cluster variables (group_id1 \& group_id2), two fixed effects and a set of covariates. The `icc_` arguments control the cluster variable's intra-cluster correlation.

```{r, error = FALSE, warning = FALSE, message = FALSE}
# set seed via dqset.seed for engine = "R" & Rademacher, Webb & Normal weights
dqrng::dqset.seed(2352342)
# set 'familiar' seed for all other algorithms and weight types 
set.seed(23325)

# load data set voters included in fwildclusterboot
data(voters)

# estimate the regression model via lm
lm_fit <- lm(
  proposition_vote ~ treatment + ideology1 + log_income + Q1_immigration , 
  data = voters
)

# model with interaction
lm_fit_interact <- lm(
  proposition_vote ~ treatment + ideology1 + log_income:Q1_immigration , 
  data = voters
)

```

The `boottest()` function has 4 required and several optional arguments. The required objects are

+ object: a regression object of type `lm`, `fixest` or `felm`
+ clustid: a character vector that defines the clustering variables
+ param: a character vector of length one - the model parameter to be tested
+ B: the number of bootstrap iterations

```{r}
# boottest on an object of type lm
boot_lm <- boottest(
  lm_fit, 
  clustid = "group_id1",
  param = "treatment",
  B = 9999
)
```

To tests for an interaction, it is important to use the coefficient names that are internally created by the modeling function.

```{r}
#names(coef(lm_fit_interact))
boot_lm_interact <- boottest(
  lm_fit_interact,
  clustid = "group_id1",
  param = "log_income:Q1_immigration1",
  B = 9999
)
```

`boottest()` further allows for multivariable tests. Suppose we're interested in 
testing the null hypothesis $0.6*treatment + 0.2*ideology1 = 0.02$. To test such a hypothesis, one would have to specify the hypothesis via the `param`, `R` and `r` arguments: 

```{r}
boot_multi <- boottest(
  lm_fit, 
  clustid = "group_id1",
  param = c("treatment", "ideology1"),
  R = c(0.6, 0.2), 
  r = 0.02, 
  B = 9999
)
```


To access the estimation results, `boottest()` comes with `summary()`, `tidy()` and `glance()` methods. The `tidy()` method returns the estimation results in a data.frame. `summary()` returns  additional information on top of the test statistics reported by `tidy()`. The`glance()` method enables the use of output formatting tools from the `modelsummary` package.

```{r}
# fwildclusterboot's internal summary() method
summary(boot_lm)
summary(boot_multi)

if(requireNamespace("modelsummary")){
  # summary via the modelsummary package
  library(modelsummary)
  msummary(list(boot_lm, boot_lm_interact), 
            estimate = "{estimate} ({p.value})", 
           statistic = "[{conf.low}, {conf.high}]")  
}


```

A `plot()` method allows the user to inspect the bootstrap t-statistics: 

```{r}
plot(boot_lm)
```

<!-- A note on the output in `tidy()` and `summary()`: -->

## Multiway Clustering 

The `boottest()` function supports clustering of any dimension. E.g. for two-way clustering, one simply needs to specify the names of the cluster variables in a character vector.

```{r}
boot_lm <- boottest(
  lm_fit,
  clustid = c("group_id1", "group_id2"), 
  param = "treatment",
  B = 9999
)
summary(boot_lm)
```

## The Heteroskedastic Bootstrap

If you drop the `clustid` argument, `boottest()` will run a heteroskedasticity robust wild bootstrap via the 'R-lean' algorithm. At the moment, the null hypothesis is always imposed, only Rademacher and Webb weights are supported, and no confidence intervals are computed. Further, no regression weights are supported. As all algorithms in `fwildclusterboot`, p-values are calculated based on pivotal t-statistics.

```{r}
boot_lm <- boottest(
  lm_fit,
  param = "treatment",
  B = 9999
)
summary(boot_lm)
boot_lm$engine
```


## Choice of Bootstrap Weights

Furthermore, the user can choose among four different weighting distribution via the `type` argument: Rademacher, Mammen, Normal and Webb. By default, `boottest()` uses the Rademacher distribution.


```{r}
boot_lm_rade <- boottest(
  lm_fit, 
  clustid = c("group_id1", "group_id2"), 
  param = "treatment", 
  B = 999,
  type = "rademacher")
boot_lm_webb <- boottest(
  lm_fit, 
  clustid = c("group_id1", "group_id2"), 
  param = "treatment", 
  B = 999,
  type = "webb"
)

if(requireNamespace("modelsummary")){
  library(modelsummary)
  msummary(list(boot_lm_rade, boot_lm_webb), 
          estimate = "{estimate} ({p.value})", 
         statistic = "[{conf.low}, {conf.high}]")
}
```

## Other function arguments 

Via the function argument `sign_level`, the user can control the significance level of the test. The default value is sign_level = 0.05, which corresponds to a 95\% confindence interval.

```{r}
boot_lm_5 <- boottest(
  lm_fit, 
  clustid = c("group_id1"),
  param = "treatment", B = 9999, 
  sign_level = 0.05
)
boot_lm_10 <- boottest(
  lm_fit, 
  clustid = c("group_id1"),
  param = "treatment", B = 9999, 
  sign_level = 0.10
)

if(requireNamespace("modelsummary")){
  library(modelsummary)
  msummary(list(boot_lm_5, boot_lm_10), 
          estimate = "{estimate} ({p.value})", 
         statistic = "[{conf.low}, {conf.high}]")
}
```

In the case of multiway clustering, the user might want to specify the bootstrap clustering level. By default, boottest chooses the clustering level with the highest number of clusters as `bootcluster = "max"`. Other choices are the minimum cluster, or independent clustering variables.  

```{r}
boot_lm1 <- boottest(
  lm_fit, 
  clustid = c("group_id1", "group_id2"), 
  param = "treatment",
  B = 9999, 
  bootcluster = "min"
)

boot_lm2 <- boottest(
  lm_fit, 
  clustid = c("group_id1", "group_id2"), 
  param = "treatment",
  B = 9999, 
  bootcluster = "group_id1"
)

if(requireNamespace("modelsummary")){
  library(modelsummary)
  msummary(list(boot_lm1, boot_lm2), 
          estimate = "{estimate} ({p.value})", 
         statistic = "[{conf.low}, {conf.high}]")
}
```

## Fixed Effects

Last, `boottest()` supports out-projection of fixed effects in the estimation stage via `lfe::felm()` and `fixest::feols()`. 
Within the bootstrap, the user can choose to project out *only one* fixed effect, which can be set via the `fe` function argument. All other fixed effects specified in either `felm()` or `feols()` are treated as sets of binary regressors.

```{r}

if(requireNamespace("fixest")){
  # estimate the regression model via feols
  library(fixest)
  feols_fit <- feols(
    proposition_vote ~ treatment + ideology1 + log_income | 
      Q1_immigration , 
    data = voters
  )
  boot_feols <- boottest(
    feols_fit, 
    clustid = "group_id1", 
    param = "treatment", 
    B = 9999, 
    fe = "Q1_immigration"
  )
}

```


## The Subcluster Bootstrap 

In the case of few treated clusters, [MacKinnon and Webb (2018)](https://academic.oup.com/ectj/article-abstract/21/2/114/5078969)  suggest to use subclusters to form the bootstrap distribution. `boottest()` allows the user to specify subclusters via the `bootcluster` argument. 

```{r}
boot_min <- boottest(
  lm_fit,
  clustid = c("group_id1", "group_id2"), 
  param = "treatment", 
  B = 9999, 
  bootcluster = "min"
)
boot_var <- boottest(
  lm_fit,
  clustid = c("group_id1", "group_id2"), 
  param = "treatment", 
  B = 9999, 
  bootcluster = "group_id1"
)
boot_2var <- boottest(
  lm_fit,
  clustid = c("group_id1", "group_id2"), 
  param = "treatment", 
  B = 9999, 
  bootcluster = c("group_id1", "Q1_immigration")
)

if(requireNamespace("modelsummary")){
  library(modelsummary)
  msummary(model = list(boot_min, boot_2var), 
         estimate = "{estimate} ({p.value})", 
         statistic = "[{conf.low}, {conf.high}]")
}


```


## Regression Weights / Weighted Least Squares (WLS)

If regression weights are specified in the estimation stage via `lm()`, `feols()` or `felm()`, `boottest()` incorporates the weights into the bootstrap inference: 

```{r}
# regression with weights / WLS
lm_w_fit <- lm(
  proposition_vote ~ treatment + ideology1 + log_income,
  weights = voters$weights,
  data = voters
)

boot_w_lm <- boottest(
  lm_w_fit, 
  clustid = "group_id1", 
  param = "treatment", 
  B = 9999
)

```

## Parallel Execution 

A major bottleneck for the performance of `boottest()` is a large matrix multiplication, which includes the bootstrap weights matrix on the right. In order to speed up the computation, this multiplication calls the c++ Eigen library, which allows for parallelization of dense matrix products. By default, `boottest()` uses one thread. Note that there is a cost of parallelization due to communication overhead. As a rule of thumb, if `boottest()` takes more than 10 seconds per execution, using a second thread might speed up the bootstrap. 

The number of threads can be specified via the `nthreads` argument of `boottest()`:

```{r, eval = FALSE}
boot_lm <- boottest(
  lm_fit, 
  clustid = "group_id1", 
  param = "treatment", 
  B = 9999, 
  nthreads = 2
)
```

# Running the wild cluster bootstrap via WildBootTests.jl 

`fwildclusterboot` serves as an R port to the [WildBootTests.jl](https://github.com/droodman/WildBootTests.jl) package. 

For guidance on how to install `Julia` and `WildBootTests.jl` and how to connect R and Julia, please take a look at the [running WildBootTests.jl through fwildclusterboot](https://s3alfisc.github.io/fwildclusterboot/articles/WildBootTests.html) vignette. 

You can tell `boottest()` to run `WildBootTests.jl` by using the `engine` function argument: 

```{r, eval = FALSE, message = FALSE, warning = FALSE}
boot_lm <- boottest(
  lm_fit, 
  clustid = "group_id1", 
  param = "treatment", 
  B = 9999, 
  engine = "WildBootTests.jl"
)
tidy(boot_lm)
#             term   estimate statistic    p.value   conf.low conf.high
#1 1*treatment = 0 0.07290769  3.709435 0.00060006 0.03326969 0.1134117
```
The seed used within Julia is linked to R's global seed, which you can set through the familiar `set.seed()` function.

If you decide to run all your bootstraps through `WildBootTests.jl`, you can set a global variable via 

```{r, eval = FALSE, message = FALSE, warning = FALSE}
setBoottest_engine("WildBootTests.jl")
```

Calling `boottest()` without specifying `engine = "WildBootTests.jl"` will now automatically run the bootstrap through `WildBootTests.jl`.

## The WRE bootstrap for IV models 

Through `WildBootTests.jl`, `fwildclusterboot` supports the WRE bootstrap by [Davidson & MacKinnon, 2010](https://www.tandfonline.com/doi/abs/10.1198/jbes.2009.07221) for IV (instrumental variables) models for objects of type `ivreg` via the `boottest()` function: 

```{r, eval = FALSE, message = FALSE, warning = FALSE}
library(ivreg)

data("SchoolingReturns", package = "ivreg")
      
# drop all NA values from SchoolingReturns
SchoolingReturns <- na.omit(SchoolingReturns)
ivreg_fit <- ivreg(
  log(wage) ~ education + age + ethnicity + smsa + south + parents14 |
              nearcollege + age  + ethnicity + smsa + south + parents14, 
  data = SchoolingReturns)

      
boot_ivreg <- boottest(
  object = ivreg_fit,
  B = 999,
  param = "education",
  clustid = "kww",
  type = "mammen",
  impose_null = TRUE
)
tidy(boot_ivreg)
#              term  estimate statistic   p.value    conf.low conf.high
# 1 1*education = 0 0.0638822  1.043969 0.2482482 -0.03152655 0.2128746
```

## Tests of multiple joint hypotheses (q > 1)

Through `WildBootTests.jl`, you can also test multiple joint hypotheses via the `mboottest()` function. 
With minor differences, `mboottest()'s` syntax largely mirrors `boottest()`.

To jointly test the null hypothesis $H_0: treatment = 0 \text{ and } ideology1 = 0$ vs $H_0: treatment \neq 0 \text{ and } ideology1 \neq 0$ via a wild cluster bootstrap, you can run

```{r, eval = FALSE, message = FALSE, warning = FALSE}
library(clubSandwich)
R <- clubSandwich::constrain_zero(2:3, coef(lm_fit))
wboottest <- 
mboottest(
  object = lm_fit, 
  clustid = "group_id1", 
  B = 999, 
  R = R
)
tidy(wboottest)
#   teststat p_val
# 1 8.469086     0
```

# Miscellanea


<!-- ## Why does `boottest()` not compute standard errors? -->

<!-- ## A note on performance -->

<!-- ## Inversion of p-values -->

<!-- ### How it works: make use of linearity of both the numerator and J -->

<!-- The starting point of this section are equations (56, 58 and 62) in Roodman et al. They show that once can write the numerator as (eq. 56) -->

<!-- $$ -->
<!--   R(\hat{\beta}^{*} - \tilde{\beta}) = \{S_{c_{*}} [WDXAR' :* \tilde{u}] \}' v^{*}. -->
<!-- $$ -->
<!-- The denominator can be written as (eq. 60) -->

<!-- $$ -->

<!-- $$ -->

<!-- where the object $K$ can be computed as  -->

<!-- $$ -->

<!-- $$ -->

<!-- For univariate hypotheses, both numerator and J are linear in $r$.  -->
<!-- Note that several objects used in the computation of both numerator and J can be written as $\ü :* A$ or $\ü * A$, where $A$ is and $ü$ is .... All derivations are shown for the first case, while the main conclusions apply to the second as well.  -->
<!-- First, for the WCU, it holds that  -->

<!-- \begin{align} -->
<!--     \ü :* A &= (Y - X'\beta) :* A \\ -->
<!--       &= Y :* A - X'\beta :* A \\ -->
<!--       &= Y :* A - X'(X'X)^{-1}X Y :* A \\ -->
<!--       &=  ((1 :- P_{x}) Y) :* A  -->
<!-- \end{align} -->

<!-- For the WCR, the analogous derivation are slightly more complicated:  -->

<!-- \begin{align} -->
<!--   \tilde{u} :* A &= (Y_{r} - X_{r}'\beta_{r}) :* A \\ -->
<!--           &= Y_{r} :* A - X_{r}'\beta_{r} :* A \\ -->
<!--           &= Y_{r} :* A - X_{r}'(X_{r}'X_{r})^{-1}X_{r} Y_{r} :* A \\ -->
<!--           &= Y_{r} :* A - P_{xr} Y_{r} :* A \\ -->
<!-- \end{align} -->

<!-- Now, note that $Y_r = Y - X_0 r$, it follows that  -->

<!-- \begin{align} -->
<!--   \tilde{u} :* A &= Y_{r} :* A - P_{xr} Y_{r} :* A \\  -->
<!--             &= (Y - X_{0} r) :* A - P_{xr} (Y - X_{0} r) :* A \\ -->
<!--             &= Y :* A - X_{0} r :* A - (P_{xr} Y) :* A +  P_{xr}  X_{0} r :*A \\  -->
<!--             &=  Y :* A  - (P_{xr} Y) :* A - P_{xr}  X_{0} r :* A + X_{0} r :* A \\ -->
<!--             &= (Y - P_{xr} Y) :* A + (X_{0} r -P_{xr}  X_{0} r) :* A \\  -->
<!--             &= (1 :- P_{xr})Y :* A + ((1 :- P_{xr})  X_{0}) :* A * r \\ -->
<!--             &= P + Q r -->
<!-- \end{align} -->

<!-- where  -->

<!-- $$ -->
<!--   P = (1 :- P_{xr})Y -->
<!-- $$ -->
<!-- and  -->

<!-- $$ -->
<!--   Q = ((1 :- P_{xr})  X_{0}). -->
<!-- $$ -->
<!-- Both quantities, $P$ and $Q$, are explicitely computed as such in the engine2 function. Q summarizes all terms that vary in $r$, while P summarizes constant terms.  -->

<!-- Note that in the case of the WCU, $X_r = X$, $P_{xr} = P_{x}$, and $X_0$ is a vector of length N, containing zeros.  -->

<!-- Therefore, the following quantities need to be computed:  -->

<!-- + $X_r$ -->
<!-- + $P_{xr}$ -->
<!-- + $X_0$ -->
<!-- + several different object for $A$ -->

<!-- Based on these objects, one can e.g. compute -->

<!-- \begin{align} -->
<!-- S_{c, c*}[\tilde{u} :* A] &= S_{c, c*}[(1 :- P_{xr})Y :* A] + S_{c, c*}[((1 :- P_{xr})  X_{0}) :* A]  r \\ -->
<!-- &= P_{1} + Q_{1} r -->
<!-- \end{align} -->

<!-- For K, one can then write an expression of the form  -->

<!-- \begin{align} -->
<!--   K &= (P_1 + P_2 + P_3) + (Q_1 + Q_2 +   Q_3) r  -->
<!-- \end{align} -->

<!-- For $J$, it follows that  -->

<!-- \begin{align} -->
<!--   J = K v & = (P_1 + P_2 + P_3) v + (Q_1 + Q_2 +   Q_3) v \times r \\ -->
<!--   &= C + D \times r -->
<!-- \end{align} -->



<!-- For $J :* J$, it hence follows  -->

<!-- $$ -->
<!--   J :* J = C :* C  + 2 C :* D \times r + D :* D \times r^{2} -->
<!-- $$ -->

<!-- So, in short: one can simply compute C, D, CC, CD and DD once and then invert the p-value by searching over r.  -->

<!-- Again, note that for for the WCU, $X_0 = 0$, which implies that in the equations above, all elements in $Q$, $Q1$, $Q2$, $Q3$, $D$, $CD$ and $DD$ are zero. In consequence, for the WCU, the numerator can be computed as  -->

<!-- $$ -->


<!-- $$ -->
<!-- and the denominator is  -->

<!-- $$ -->

<!-- $$ -->

<!-- For the WCU, both numerator and denominator do not depend on $r$. -->

<!-- + why might confidence intervals differ for estimates based on `lm()`, `feols()` and `felm()`? -->

## A sanity check if `fwildclusterboot::boottest()` works as intended

A sanity check to see if `fwildclusterboot::boottest()` works as intended is to look at its `t_stat` return value. For both the WCR and WCU bootstrap, `boottest()` re-calculates the "original" - hence non-bootstrapped - t-statistic from its input regression model. The t-stat computed in `boottest()` and the t-stats reported by either `lm()`, `feols()` and `lfe()` under the same error clustering structure and small-sample adjustments should be *identical*. If you find that they differ, please report a bug [on github](https://github.com/s3alfisc/fwildclusterboot/issues). Note that `fwildclusterboot` explicitly tests for t-stat equality against `fixest::feols()` [here](https://github.com/s3alfisc/fwildclusterboot/blob/master/tests/testthat/test-tstat_equivalence.R).

```{r}
data <- 
fwildclusterboot:::create_data(
  N = 1000, 
  N_G1 = 20, 
  icc1 = 0.81,
  N_G2 = 10,
  icc2 = 0.01, 
  numb_fe1 = 10,
  numb_fe2 = 10, 
  seed = 8769
)

# oneway clustering 
feols_fit <- fixest::feols(
  proposition_vote ~ treatment + ideology1 + log_income,
  data = data, 
  cluster = ~group_id1, 
  ssc = fixest::ssc(adj = TRUE, 
                    cluster.adj = TRUE, 
                    cluster.df = 'conventional')
  )
        
feols_tstats <- fixest::coeftable(
  feols_fit
  )[c("treatment", "log_income", "ideology1"), 3]

boot_tstats <- 
lapply(c("treatment", "log_income", "ideology1"), function(x){
  boot1 <- fwildclusterboot::boottest(
    feols_fit, 
    clustid = c("group_id1"),
    B = 999, 
    param = x, 
    ssc = fwildclusterboot::boot_ssc(
      adj = TRUE, 
      cluster.adj = TRUE, 
      cluster.df = 'conventional'),  
      impose_null = TRUE)$t_stat
})        

df <- cbind(feols_tstats, unlist(boot_tstats))
colnames(df) <- c("feols tstat", "boottest tstat")
df

```


## Small Sample Corrections

`boottest()` offers several options for small sample adjustments via the `ssc` function argument which need to be specified via the `boot_ssc()` function. `boot_ssc()` has 4 arguments and is intentionally designed to mimic `fixest's` `ssc()` function. For more information on the default choices and alternative options, see `?fwildclusterboot::boot_ssc()`.

## Memory & the 'lean' implementation of the wild cluster bootstrap

Because the R-implementation of the fast algorithm is memory-intensive, `fwildclusterboot` further supports a Rcpp-based 'lean' implementation of the wild cluster bootstrap in case that memory demands get prohibitively large. In general, the 'lean' algorithm is much slower: its main feature is that it requires *much* less memory. The algorithm is equivalent to the 'wild2' algorithm in the "Fast & Wild" paper by Roodman et al. Note that the implementation in `WildBootTests.jl` is, in general,  very memory-efficient.

```{r, eval = FALSE}
library(bench)

dt <- fwildclusterboot:::create_data(
  N = 10000, 
  N_G1 = 250, 
  icc1 = 0.01, 
  N_G2 = 10, 
  icc2 = 0.01, 
  numb_fe1 = 10, 
  numb_fe2 = 10, 
  seed = 7645
)

lm_fit <- lm(
  proposition_vote ~ treatment + ideology1 + log_income + Q1_immigration ,
  data = dt
)

res <- 
bench::mark(
  "R" = boottest(lm_fit, 
           clustid = "group_id1", 
           param = "treatment", 
           B = 9999, 
           engine = "R", 
           nthreads = 4), 
  "R-lean" = boottest(lm_fit, 
           clustid = "group_id1", 
           param = "treatment", 
           B = 9999, 
           engine = "R-lean", 
           nthreads = 4),
  "WildBootTests.jl" = 
    boottest(lm_fit, 
           clustid = "group_id1", 
           param = "treatment", 
           B = 9999, 
           engine = "WildBootTests.jl"), 
  iterations = 1, 
  check = FALSE
)

res

```

## Seeds

To guarantee reproducibility, you need to 
set a global random seed via

 + `set.seed()` when using
    1) the lean algorithm (via `engine = "R-lean"`),
    2) the heteroskedastic wild bootstrap
    3) the wild cluster bootstrap via `engine = "R"` with Mammen weights or
    4) `engine = "WildBootTests.jl"`
 + `dqrng::dqset.seed()` when using `engine = "R"` for Rademacher, Webb or Normal weights

## Treatment of Invalid Test Statistics for multiway clustering

In case of multi-way clustering, it is not guaranteed that the covariance matrix is positive definite, in which case the resulting bootstrap test statistics are invalid. `boottest()` follows the implementation in STATA and deletes invalid tests statistics, and informs the user with a note.



