Author: Laurent Berge
Date: 2021-03-29

# Exporting estimation tables

#### Laurent Berge

#### 2021-03-29

`fixest` offers a tool, the function `etable`, to view estimation tables in `R` or export them to Latex.

The main advantage of this function is its simplicity: it is completely integrated with other `fixest` functions, making it exceedingly easy to export multiple estimation results with, say, different types of standard-errors. On the other hand, its main limitations are that i) only `fixest` objects can be exported, and ii) only Latex is supported.

It also offers a fair deal of customization, and since you can seamlessly change its default values, you can completely transform the style of your tables without modifying a single line of code.

Note that there exists excellent alternatives to export tables, like for instance modelsummary or texreg; but they are, necessarily, less integrated with `fixest` objects, often necessiting more lines of code to export the same results.

This document does not describe `etable`’s arguments in details (the help page provides many examples). Rather, it illustrates some features that may be hidden at first sight.

This document applies to `fixest` version 0.8.0 or higher.

## Preliminaries

Throughout this document, we will use data from the airquality data base. We also set a dictionary that will be used to rename the variables used in `etable`. This dictionary is set once and for all.

    library(fixest)
    data(airquality)

    # Setting a dictionary
    setFixest_dict(c(Ozone = "Ozone (ppb)", Solar.R = "Solar Radiation (Langleys)",
                     Wind = "Wind Speed (mph)", Temp = "Temperature"))

## Exporting multiple estimations to data.frames

Let’s estimate the following four models and cluster the standard-errors by `Day`:

    # On multiple estimations: see the dedicated vignette
    est = feols(Ozone ~ Solar.R + sw0(Wind + Temp) | csw(Month, Day),
                airquality, cluster = ~Day)

By default, when the argument `file` is missing, the function `etable` returns a `data.frame`. Let’s see the ouput of the previous estimations:

    etable(est)

    #>                                     model 1          model 2         model 3
    #> Dependent Var.:                 Ozone (ppb)      Ozone (ppb)     Ozone (ppb)
    #>
    #> Solar Radiation (Langleys) 0.115*** (0.023)   0.052* (0.020) 0.108** (0.033)
    #> Wind Speed (mph)                            -3.11*** (0.799)
    #> Temperature                                  1.88*** (0.367)
    #> Fixed-Effects:             ---------------- ---------------- ---------------
    #> Month                                   Yes              Yes             Yes
    #> Day                                      No               No             Yes
    #> __________________________ ________________ ________________ _______________
    #> S.E.: Clustered                     by: Day          by: Day         by: Day
    #> Observations                            111              111             111
    #> R2                                  0.31974          0.63686         0.58018
    #> Within R2                           0.12245          0.53154         0.12074
    #>                                     model 4
    #> Dependent Var.:                 Ozone (ppb)
    #>
    #> Solar Radiation (Langleys)   0.051* (0.024)
    #> Wind Speed (mph)           -3.29*** (0.778)
    #> Temperature                 2.05*** (0.242)
    #> Fixed-Effects:             ----------------
    #> Month                                   Yes
    #> Day                                     Yes
    #> __________________________ ________________
    #> S.E.: Clustered                     by: Day
    #> Observations                            111
    #> R2                                  0.81604
    #> Within R2                           0.61471

What can we notice? First, the variables are properly labeled. Second, the fixed-effects section details which fixed-effects is included in which model. Third, the type of standard-error is reminded in a dedicated row.

Starting from this table, two elements are detailed: a) how to change the look of the table with the `style.df` argument, b) how to leverage tools from other packages with the `postprocess.df` argument.

### Changing the look of the data.frame with `style.df`

You can change many elements of the `data.frame` with the argument `style.df` whose input must come from the function `style.df`. The style monitors many elements of the table, in particular the titles of the sections. Let’s have an example:

    etable(est, style.df = style.df(depvar.title = "", fixef.title = "",
                                     fixef.suffix = " fixed effect", yesNo = "yes"))

    #>                                     model 1          model 2         model 3
    #>                                 Ozone (ppb)      Ozone (ppb)     Ozone (ppb)
    #>
    #> Solar Radiation (Langleys) 0.115*** (0.023)   0.052* (0.020) 0.108** (0.033)
    #> Wind Speed (mph)                            -3.11*** (0.799)
    #> Temperature                                  1.88*** (0.367)
    #> Month fixed effect                      yes              yes             yes
    #> Day fixed effect                                                         yes
    #> __________________________ ________________ ________________ _______________
    #> S.E.: Clustered                     by: Day          by: Day         by: Day
    #> Observations                            111              111             111
    #> R2                                  0.31974          0.63686         0.58018
    #> Within R2                           0.12245          0.53154         0.12074
    #>                                     model 4
    #>                                 Ozone (ppb)
    #>
    #> Solar Radiation (Langleys)   0.051* (0.024)
    #> Wind Speed (mph)           -3.29*** (0.778)
    #> Temperature                 2.05*** (0.242)
    #> Month fixed effect                      yes
    #> Day fixed effect                        yes
    #> __________________________ ________________
    #> S.E.: Clustered                     by: Day
    #> Observations                            111
    #> R2                                  0.81604
    #> Within R2                           0.61471

In the previous example, the dependent variable and fixed-effects (FE) headers have been removed, and this is achieved with the (explicit) arguments `depvar.title` and `fixef.title`. Furthermore the suffix `"fixed effect"` is added to each fixed-effect variable, and the indicator of which FE is included in which model is slightly changed. There are more options that are described in the `style.df` documentation.

#### Postprocessing with external functions

Since the output of `etable` is a `data.frame`, any formatting function handling `data.frame`s can be leveraged. It is then very easy to integrate it into `etable`. Let’s have an example with the package `pander`:

    library(pander)

    etable(est, postprocess.df = pandoc.table.return, style = "rmarkdown")

    #>
    #>
    #> |             &nbsp;             |     model 1      |     model 2      |
    #> |:------------------------------:|:----------------:|:----------------:|
    #> |      **Dependent Var.:**       |   Ozone (ppb)    |   Ozone (ppb)    |
    #> |                                |                  |                  |
    #> | **Solar Radiation (Langleys)** | 0.115*** (0.023) |  0.052* (0.020)  |
    #> |      **Wind Speed (mph)**      |                  | -3.11*** (0.799) |
    #> |        **Temperature**         |                  | 1.88*** (0.367)  |
    #> |       **Fixed-Effects:**       | ---------------- | ---------------- |
    #> |           **Month**            |       Yes        |       Yes        |
    #> |            **Day**             |        No        |        No        |
    #> | **__________________________** | ________________ | ________________ |
    #> |      **S.E.: Clustered**       |     by: Day      |     by: Day      |
    #> |        **Observations**        |       111        |       111        |
    #> |             **R2**             |     0.31974      |     0.63686      |
    #> |         **Within R2**          |     0.12245      |     0.53154      |
    #>
    #> Table: Table continues below
    #>
    #>
    #>
    #> |             &nbsp;             |     model 3     |     model 4      |
    #> |:------------------------------:|:---------------:|:----------------:|
    #> |      **Dependent Var.:**       |   Ozone (ppb)   |   Ozone (ppb)    |
    #> |                                |                 |                  |
    #> | **Solar Radiation (Langleys)** | 0.108** (0.033) |  0.051* (0.024)  |
    #> |      **Wind Speed (mph)**      |                 | -3.29*** (0.778) |
    #> |        **Temperature**         |                 | 2.05*** (0.242)  |
    #> |       **Fixed-Effects:**       | --------------- | ---------------- |
    #> |           **Month**            |       Yes       |       Yes        |
    #> |            **Day**             |       Yes       |       Yes        |
    #> | **__________________________** | _______________ | ________________ |
    #> |      **S.E.: Clustered**       |     by: Day     |     by: Day      |
    #> |        **Observations**        |       111       |       111        |
    #> |             **R2**             |     0.58018     |     0.81604      |
    #> |         **Within R2**          |     0.12074     |     0.61471      |

What did it do? First, it called the function `pandoc.table.return` from within `etable`. Second, the argument `style` is not from `etable` but is from `pander`’s function. Indeed, all the arguments to the postprocessing function are caught and passed to it. So far so good. But you could say: why bother using the posprocessing function when we could just use piping? You’re right, but wait a second for the next section.

#### Setting `etable` default values

One important feature of `etable` is that you can set the default values of almost all its arguments. This includes the postprocessing function. Let’s change the default values of `style.df` and `postprocess.df`:

    my_style = style.df(depvar.title = "", fixef.title = "",
                        fixef.suffix = " fixed effect", yesNo = "yes")
    setFixest_etable(style.df = my_style, postprocess.df = pandoc.table.return)

Since now `pandoc.table.return` is the default postprocessing, all its arguments are added to `etable`. So calls like that are valid even though `style` or `caption` are not arguments from `etable`:

    etable(est[rhs = 2], style = "rmarkdown", caption = "New default values")

    #>
    #>
    #> |             &nbsp;             |     model 1      |     model 2      |
    #> |:------------------------------:|:----------------:|:----------------:|
    #> |                                |   Ozone (ppb)    |   Ozone (ppb)    |
    #> |                                |                  |                  |
    #> | **Solar Radiation (Langleys)** |  0.052* (0.020)  |  0.051* (0.024)  |
    #> |      **Wind Speed (mph)**      | -3.11*** (0.799) | -3.29*** (0.778) |
    #> |        **Temperature**         | 1.88*** (0.367)  | 2.05*** (0.242)  |
    #> |     **Month fixed effect**     |       yes        |       yes        |
    #> |      **Day fixed effect**      |                  |       yes        |
    #> | **__________________________** | ________________ | ________________ |
    #> |      **S.E.: Clustered**       |     by: Day      |     by: Day      |
    #> |        **Observations**        |       111        |       111        |
    #> |             **R2**             |     0.63686      |     0.81604      |
    #> |         **Within R2**          |     0.53154      |     0.61471      |
    #>
    #> Table: New default values

## Exporting multiple estimations to Latex

We now illustrate the exports to Latex. First, to include all the sections of a table, let’s add a fifth estimation to the previous example; this new estimation includes variables with varying slopes:

    est_slopes = feols(Ozone ~ Solar.R + Wind | Day + Month[Temp], airquality)

To export to Latex, use the argument `tex = TRUE` (note that this argument is on when the argument `file` is not missing):

    etable(est, est_slopes, tex = TRUE)

The previous code produces the following table:

The style of the table is rather sober, but no worries: most of it can be customized. We now illustrate: a) how to change the look of the table with the argument `style.tex`, and how to include custom features with the argument `postproces.tex`.

### Changing the look of the Latex table with `style.tex`

The argument `style.tex` defines how the table looks. It allows an in-depth customization of the table. The table is split into several components, each allowing some customization. The components of a table and some of its associated keywords are described by the following figure:

The argument `style.tex` only accepts outputs from the function `style.tex`. That function is documented and describes the different components that can be found in the previous illustration.

The function `style.tex` has two starting points (in the argument `main`), either the style of the first table displayed, either a much more compact style named “aer”. Let’s show the same table with the aer style, without stars beside the coefficients, and different fit statistics:

    etable(est, est_slopes, style.tex = style.tex("aer"),
           signifCode = NA, fitstat = ~ r2 + n, tex = TRUE)

Which yields the following table:

### Adding custom features with `postprocess.tex`

When `tex = TRUE` `etable` returns a character vector. It is possible to modify it at will with the argument `postprocess.tex`. When a postprocessing function is detected, two additional tags are added to the character vector identifying the start and end of the table (`"%start:tab\\n"` and `"%end:tab\\n"`).

Assume we want to set the rule widths of the table, we could write the following function:

    set_rules = function(x, heavy, light){
      # x: the character vector returned by etable

      tex2add = ""
      if(!missing(heavy)){
        tex2add = paste0("\\setlength\\heavyrulewidth{", heavy, "}\n")
      }
      if(!missing(light)){
        tex2add = paste0(tex2add, "\\setlength\\lightrulewidth{", light, "}\n")
      }

      if(nchar(tex2add) > 0){
        x[x == "%start:tab\n"] = tex2add
      }

      x
    }

Now we can summon that function from `etable`:

    etable(est, est_slopes, postprocess.tex = set_rules, heavy = "0.14em", tex = TRUE)

Of course it is even more convenient to set `set_rules` as the default postprocessing function.

#### Setting `etable` default values: Latex edition

To set the default values, like for the data.frame output, use `setFixest_etable`:

    setFixest_etable(style.tex = style.tex("aer"), postprocess.tex = set_rules,
                     fitstat = ~ r2 + n, signifCode = NA)

Now we can access directly the arguments of the postprocessing function and the default style is the one of the second table:

    etable(est, est_slopes, heavy = "0.14em", tex = TRUE)

    #> \setlength\heavyrulewidth{0.14em}
    #> \begin{tabular}{lccccc}
    #> \toprule
    #> &\multicolumn{5}{c}{Ozone (ppb)}\\
    #> &(1) & (2) & (3) & (4) & (5)\\
    #> \midrule Solar Radiation (Langleys)&0.115 & 0.052 & 0.108 & 0.051 & 0.056\\
    #>   &(0.023) & (0.020) & (0.033) & (0.024) & (0.023)\\
    #> Wind Speed (mph)&   & -3.11 &    & -3.29 & -3.05\\
    #>   &   & (0.799) &    & (0.778) & (0.822)\\
    #> Temperature&   & 1.88 &    & 2.05 &   \\
    #>   &   & (0.367) &    & (0.242) &   \\
    #>  &  & & & & \\
    #> R$^2$ & 0.31974&0.63686&0.58018&0.81604&0.82896\\
    #> Observations & 111&111&111&111&111\\
    #>  &   &   &   &   &  \\
    #> Month fixed effects & $\checkmark$ & $\checkmark$ & $\checkmark$ & $\checkmark$ & $\checkmark$\\
    #> Day fixed effects &  &  & $\checkmark$ & $\checkmark$ & $\checkmark$\\
    #> Temperature $\times $ Month &  &  &  &  & $\checkmark$\\
    #> \bottomrule
    #> \end{tabular}

## Setting custom fit statistics

It is often useful to include in a table some fit statistics that are not standard, or simply that may not be included in `fixest` built-in fit statistics. While it is possible to include any extra line in the table with the argument `extraline`, this is rather cumbersome and possibly error-prone if this task has to be repeated.

To avoid that kind of issue, `fixest` allows the user to register custom fit statistics. Once they are registered, they can be seamlessly called via the `fitstat` argument in `etable`.

Let’s continue with the previous example using the `airquality` data set, and now let’s display different p-values of statistical significance for the variable `Solar.R`. These p-values will vary depending on how we compute the VCOV matrix.

Here is an example that will shortly be explained:

    fitstat_register(type = "p_s", alias = "pvalue (standard)",
                     fun = function(x) pvalue(x, se = "s")["Solar.R"])

    fitstat_register(type = "p_h", alias = "pvalue (Heterosk.)",
                     fun = function(x) pvalue(x, se = "h")["Solar.R"])

    fitstat_register(type = "p_day", alias = "pvalue (Day)",
                     fun = function(x) pvalue(x, cluster = "Day")["Solar.R"])

    fitstat_register(type = "p_month", alias = "pvalue (Month)",
                     fun = function(x) pvalue(x, cluster = "Month")["Solar.R"])

    # We first reset the default values set in the previous sections
    setFixest_etable(reset = TRUE)
    # Now we display the results with the new fit statistics
    etable(est, fitstat = ~ . + p_s + p_h + p_day + p_month)

    #>                                       model 1            model 2
    #> Dependent Var.:                   Ozone (ppb)        Ozone (ppb)
    #>
    #> Solar Radiation (Langleys) 0.1148*** (0.0234)   0.0522* (0.0202)
    #> Wind Speed (mph)                              -3.109*** (0.7986)
    #> Temperature                                    1.875*** (0.3671)
    #> Fixed-Effects:             ------------------ ------------------
    #> Month                                     Yes                Yes
    #> Day                                        No                 No
    #> __________________________ __________________ __________________
    #> S.E.: Clustered                       by: Day            by: Day
    #> Observations                              111                111
    #> R2                                    0.31974            0.63686
    #> Within R2                             0.12245            0.53154
    #> pvalue (standard)                     0.00022            0.02957
    #> pvalue (Heterosk.)                    6.64e-6            0.02268
    #> pvalue (Day)                             3e-5            0.01468
    #> pvalue (Month)                        0.06066            0.26992
    #>                                      model 3            model 4
    #> Dependent Var.:                  Ozone (ppb)        Ozone (ppb)
    #>
    #> Solar Radiation (Langleys) 0.1078** (0.0329)   0.0509* (0.0236)
    #> Wind Speed (mph)                             -3.289*** (0.7777)
    #> Temperature                                   2.052*** (0.2415)
    #> Fixed-Effects:             ----------------- ------------------
    #> Month                                    Yes                Yes
    #> Day                                      Yes                Yes
    #> __________________________ _________________ __________________
    #> S.E.: Clustered                      by: Day            by: Day
    #> Observations                             111                111
    #> R2                                   0.58018            0.81604
    #> Within R2                            0.12074            0.61471
    #> pvalue (standard)                    0.00196            0.03720
    #> pvalue (Heterosk.)                   0.00103            0.02815
    #> pvalue (Day)                         0.00263            0.03881
    #> pvalue (Month)                       0.02676            0.29967

The function `fitstat_register` is a tool to add fit statistics in the `fitstat` engine. The first argument, `type`, is the code name by which the statistic is to be summoned. The argument `alias` provides the row name of the statistics: how it should look in the table. Finally in the `fun` argument is the function computing the statistic. That function must apply to a `fixest` estimation and must also return a single value.

Once these statistics are registered, they can seamlessly be summoned with the argument `fitstat` and will appear in the order the user provide. Note that the dot in `fitstat = ~ . + p_s + etc` represents the default statistics to be displayed and need not be there.

## Using external functions to export multiple results

Thanks to contributors (namely Karl Dunkle Werner), `fixest` objects are compatible with broom methods. This means that export functions building on `broom` can be leveraged, like for instance the excellent modelsummary.

In case you use external export tools, here are some tips.

Multiple estimations, like the object `est` in the previous examples, are a bit special and `broom` methods can’t apply directly. To export them, you first need to coerce the results into a list: by simply using `as.list(est)`.

By default in `fixest` the estimations are separated from the calculation of the VCOV matrices. That’s not a problem when using `etable` since, after providing the argument `se` or `cluster`, all VCOVs are calculated at once. Using other tools for exportation requires a call to `summary` for each model to compute the appropriate standard-errors. But this process can also be automatised. The function `.l()` can be used to coerce several `fixest` objects to which `summary` can then be applied, for example:

    summary(.l(est, est_slopes), cluster = ~ Month)

The previous code returns a list of the five estimations for which the standard-errors are all clustered at the Month level, and can then be exported with external software.