Nothing
#----------------------------------------------#
# Author: Laurent Berge
# Date creation: Thu Jul 09 09:52:31 2020
# ~: etable
#----------------------------------------------#
#' Estimations table (export the results of multiples estimations to a DF or to Latex)
#'
#' Aggregates the results of multiple estimations and displays them in the form of either a Latex
#' table or a `data.frame`. Note that you will need the `booktabs` package for the Latex table to
#' render properly. See [`setFixest_etable`] to set the default values, and [`style.tex`] to customize Latex output.
#'
#' @inheritParams summary.fixest
#' @inheritParams setFixest_nthreads
#'
#' @param ... Used to capture different `fixest` estimation objects (obtained with [`femlm`],
#' [`feols`] or [`feglm`]). Note that any other type of element is discarded. Note that you can
#' give a list of `fixest` objects.
#' @param digits Integer or character scalar. Default is 4 and represents the number of significant
#' digits to be displayed for the coefficients and standard-errors. To apply rounding instead of
#' significance use, e.g., `digits = "r3"` which will round at the first 3 decimals. If character,
#' it must be of the form `"rd"` or `"sd"` with `d` a digit (`r` is for round and `s` is for
#' significance). For the number of digits for the fit statistics, use `digits.stats`. Note that
#' when significance is used it does not exactly display the number of significant digits: see
#' details for its exact meaning.
#' @param digits.stats Integer or character scalar. Default is 5 and represents the number of
#' significant digits to be displayed for the fit statistics. To apply rounding instead of
#' significance use, e.g., `digits = "r3"` which will round at the first 3 decimals. If character,
#' it must be of the form `"rd"` or `"sd"` with `d` a digit (`r` is for round and `s` is for
#' significance). Note that when significance is used it does not exactly display the number of
#' significant digits: see details for its exact meaning.
#' @param tex Logical: whether the results should be a data.frame or a Latex table. By default,
#' this argument is `TRUE` if the argument `file` (used for exportation) is not missing; it is
#' equal to `FALSE` otherwise.
#' @param fitstat A character vector or a one sided formula (both with only lowercase letters). A
#' vector listing which fit statistics to display. The valid types are 'n', 'll', 'aic', 'bic' and
#' r2 types like 'r2', 'pr2', 'war2', etc (see all valid types in [`r2`]). Also accepts valid types
#' from the function [`fitstat`]. The default value depends on the models to display. Example of
#' use: `fitstat=c('n', 'cor2', 'ar2', 'war2')`, or `fitstat=~n+cor2+ar2+war2` using a formula. You
#' can use the dot to refer to default values:` ~ . + ll` would add the log-likelihood to the
#' default fit statistics.
#' @param title (Tex only.) Character scalar. The title of the Latex table.
#' @param float (Tex only.) Logical. By default, if the argument `title` or `label` is provided, it
#' is set to `TRUE`. Otherwise, it is set to `FALSE`.
#' @param se.below Logical or `NULL` (default). Should the standard-errors be displayed below the
#' coefficients? If `NULL`, then this is `TRUE` for Latex and `FALSE` otherwise.
#' @param se.row Logical scalar, default is `NULL`. Whether should be displayed the row with the
#' type of standard-error for each model. When `tex = FALSE`, the default is `TRUE`.
#' When `tex = FALSE`, the row is showed only when there is a table-footer and the types of
#' standard-errors differ across models.
#' @param keep Character vector. This element is used to display only a subset of variables. This
#' should be a vector of regular expressions (see [`base::regex`] help for more info). Each
#' variable satisfying any of the regular expressions will be kept. This argument is applied post
#' aliasing (see argument `dict`). Example: you have the variable `x1` to `x55` and want to display
#' only `x1` to `x9`, then you could use `keep = "x[[:digit:]]$"`. If the first character is an
#' exclamation mark, the effect is reversed (e.g. keep = "!Intercept" means: every variable that
#' does not contain \dQuote{Intercept} is kept). See details.
#' @param drop Character vector. This element is used if some variables are not to be displayed.
#' This should be a vector of regular expressions (see [`base::regex`] help for more info). Each
#' variable satisfying any of the regular expressions will be discarded. This argument is applied
#' post aliasing (see argument `dict`). Example: you have the variable `x1` to `x55` and want to
#' display only `x1` to `x9`, then you could use `drop = "x[[:digit:]]{2}`". If the first character
#' is an exclamation mark, the effect is reversed (e.g. drop = "!Intercept" means: every variable
#' that does not contain \dQuote{Intercept} is dropped). See details.
#' @param order Character vector. This element is used if the user wants the variables to be
#' ordered in a certain way. This should be a vector of regular expressions (see [`base::regex`]
#' help for more info). The variables satisfying the first regular expression will be placed first,
#' then the order follows the sequence of regular expressions. This argument is applied post
#' aliasing (see argument `dict`). Example: you have the following variables: `month1` to `month6`,
#' then `x1` to `x5`, then `year1` to `year6`. If you want to display first the x's, then the
#' years, then the months you could use: `order = c("x", "year")`. If the first character is an
#' exclamation mark, the effect is reversed (e.g. order = "!Intercept" means: every variable that
#' does not contain \dQuote{Intercept} goes first). See details.
#' @param dict A named character vector or a logical scalar. It changes the original variable names
#' to the ones contained in the `dict`ionary. E.g. to change the variables named `a` and `b3` to
#' (resp.) \dQuote{$log(a)$} and to \dQuote{$bonus^3$}, use `dict=c(a="$log(a)$",b3="$bonus^3$")`.
#' By default, it is equal to `getFixest_dict()`, a default dictionary which can be set with
#' [`setFixest_dict`]. You can use `dict = FALSE` to disable it. By default `dict` modifies the
#' entries in the global dictionary, to disable this behavior, use "reset" as the first element
#' (ex: `dict=c("reset", mpg="Miles per gallon")`).
#' @param file A character scalar. If provided, the Latex (or data frame) table will be saved in a
#' file whose path is `file`. If you provide this argument, then a Latex table will be exported, to
#' export a regular `data.frame`, use argument `tex = FALSE`.
#' @param replace Logical, default is `FALSE`. Only used if option `file` is used. Should the
#' exported table be written in a new file that replaces any existing file?
#' @param convergence Logical, default is missing. Should the convergence state of the algorithm be
#' displayed? By default, convergence information is displayed if at least one model did not
#' converge.
#' @param signif.code Named numeric vector, used to provide the significance codes with respect to
#' the p-value of the coefficients. Default is `c("***"=0.01, "**"=0.05, "*"=0.10)` for a Latex
#' table and `c("***"=0.001, "**"=0.01, "*"=0.05, "."=0.10)` for a data.frame (to conform with R's
#' default). To suppress the significance codes, use `signif.code=NA` or `signif.code=NULL`. Can
#' also be equal to `"letters"`, then the default becomes `c("a"=0.01, "b"=0.05, "c"=0.10)`.
#' @param label (Tex only.) Character scalar. The label of the Latex table.
#' @param headers Character vector or list. Adds one or more header lines in the table. A header
#' line can be represented by a character vector or a named list of numbers where the names are the
#' cell values and the numbers are the span. Example: `headers=list("M"=2, "F"=3)` will create a
#' row with 2 times "M" and three time "F" (this is identical to
#' `headers=rep(c("M", "F"), c(2, 3))`). You can stack header lines within a list, in that case the
#' list names will be displayed in the leftmost cell.
#' Example: `headers=list(Gender=list("M"=2, "F"=3), Country="US"` will create two header lines.
#' When `tex = TRUE`, you can add a rule to separate groups by using `":_:"` somewhere in the row
#' name (ex: `headers=list(":_:Gender"=list("M"=2, "F"=3))`. You can monitor the placement by
#' inserting a special character in the row name: "^" means at the top, "-" means in the middle
#' (default) and "_" means at the bottom. Example: `headers=list("_Country"="US")` will add the
#' country row as the very last header row (after the model row). Finally, you can use the special
#' value "auto" to include automatic headers when the data contains split sample estimations. By
#' default it is equal to `list("auto")`. You can use `.()` instead of `list()`.
#' @param fixef_sizes (Tex only.) Logical, default is `FALSE`. If `TRUE` and fixed-effects were
#' used in the models, then the number of "units" per fixed-effect dimension is also displayed.
#' @param fixef_sizes.simplify Logical, default is `TRUE`. Only used if `fixef_sizes = TRUE`. If
#' `TRUE`, the fixed-effects sizes will be displayed in parentheses instead of in a separate line
#' if there is no ambiguity (i.e. if the size is constant across models).
#' @param family Logical, default is missing. Whether to display the families of the models. By
#' default this line is displayed when at least two models are from different families.
#' @param keepFactors Logical, default is `TRUE`. If `FALSE`, then factor variables are displayed
#' as fixed-effects and no coefficient is shown.
#' @param powerBelow (Tex only.) Integer, default is -5. A coefficient whose value is below
#' `10**(powerBelow+1)` is written with a power in Latex. For example `0.0000456` would be written
#' `4.56$\\times 10^{-5}$` by default. Setting `powerBelow = -6` would lead to `0.00004` in Latex.
#' @param interaction.combine Character scalar, defaults to `" $\\times$ "` for Tex and to `" = "`
#' otherwise. When the estimation contains interactions, then the variables names (after aliasing)
#' are combined with this argument. For example: if `dict = c(x1="Wind", x2="Rain")` and you have
#' the following interaction `x1:x2`, then it will be renamed (by default)
#' `Wind $\\times$ Rain` -- using `interaction.combine = "*"` would lead to `Wind*Rain`.
#' @param interaction.order Character vector of regular expressions. Only affects variables that
#' are interacted like x1 and x2 in `feols(y ~ x1*x2, data)`. You can change the order in which the
#' interacted variables are displayed: e.g. `interaction.order = "x2"` would lead to "x1 x x2"
#' instead of "x1 x x2". Please look at the argument 'order' and the dedicated section in the help
#' page for more information.
#' @param i.equal Character scalar, defaults to `" $=$ "` when `tex = TRUE` and `" = "` otherwise.
#' Only affects factor variables created with the function [`i`], tells how the variable should be
#' linked to its value. For example if you have the `Species` factor from the `iris` data set, by
#' default the display of the variable is `Species = Setosa`, etc. If `i.equal = ": "` the display
#' becomes `Species: Setosa`.
#' @param depvar Logical, default is `TRUE`. Whether a first line containing the dependent
#' variables should be shown.
#' @param coefstat One of `"se"` (default), `"tstat"` or `"confint"`. The statistic to report for
#' each coefficient: the standard-error, the t-statistics or the confidence interval. You can
#' adjust the confidence interval with the argument `ci`.
#' @param ci Level of the confidence interval, defaults to `0.95`. Only used if
#' `coefstat = confint`.
#' @param style.tex An object created by the function [`style.tex`]. It represents the style of the
#' Latex table, see the documentation of [`style.tex`].
#' @param style.df An object created by the function [`style.df`] It represents the style of the
#' data frame returned (if `tex = FALSE`), see the documentation of [`style.df`].
#' @param notes (Tex only.) Character vector. If provided, a `"notes"` section will be added at the
#' end right after the end of the table, containing the text of this argument. If it is a vector,
#' it will be collapsed with new lines. If `tpt = TRUE`, the behavior is different: each element of
#' the vector is an item. If the first element of the vector starts with `"@"`, then it will be
#' included verbatim, and in case of `tpt = TRUE`, right before the first item. If that element is
#' provided, it will replace the value defined in `style.tex(notes.intro)` or
#' `style.tex(notes.tpt.intro)`.
#' @param group A list. The list elements should be vectors of regular expressions. For each
#' elements of this list: A new line in the table is created, all variables that are matched by the
#' regular expressions are discarded (same effect as the argument `drop`) and `TRUE` or `FALSE`
#' will appear in the model cell, depending on whether some of the previous variables were found in
#' the model.
#' Example: `group=list("Controls: personal traits"=c("gender", "height", "weight"))`
#' will create an new line with `"Controls: personal traits"` in the leftmost cell, all three
#' variables gender, height and weight are discarded, `TRUE` appearing in each model containing at
#' least one of the three variables (the style of `TRUE`/`FALSE` is governed by the argument
#' `yesNo`). You can control the placement of the new row by using 1 or 2 special characters at the
#' start of the row name.
#' The meaning of these special characters are: 1) `"^"`: coef., `"-"`: fixed-effect,
#' `"_"`: stats, section; 2) `"^"`: 1st, `"_"`: last, row.
#' For example: `group=list("_^Controls"=stuff)` will place the line at the top of the 'stats'
#' section, and using `group=list("^_Controls"=stuff)` will make the row appear at the bottom of
#' the coefficients section. For details, see the dedicated section.
#' @param extralines A vector, a list or a one sided formula. The list elements should be either a
#' vector representing the value of each cell, a list of the form
#' `list("item1" = #item1, "item2" = #item2, etc)`, or a function.
#' This argument can be many things, please have a look at the dedicated help section;
#' a simplified description follows. For each elements of this list: A new line in the table is
#' created, the list name being the row name and the vector being the content of the cells.
#' Example: `extralines=list("Sub-sample"=c("<20 yo", "all", ">50 yo"))` will create an new line
#' with `"Sub-sample"` in the leftmost cell, the vector filling the content of the cells for the
#' three models. You can control the placement of the new row by using 1 or 2 special characters at
#' the start of the row name. The meaning of these special characters are:
#' 1) `"^"`: coef., `"-"`: fixed-effect, `"_"`: stats, section;
#' 2) `"^"`: 1st, `"_"`: last, row.
#' For example: `extralines=list("__Controls"=stuff)` will place the line at the bottom of the
#' stats section, and using `extralines=list("^^Controls"=stuff)` will make the row appear at the
#' top of the 'coefficients' section. For details, see the dedicated section. You can use `.()`
#' instead of `list()`.
#' @param fixef.group Logical scalar or list (default is `NULL`). If equal to `TRUE`, then all
#' fixed-effects always appearing jointly in models will be grouped in one row. If a list, its
#' elements must be character vectors of regular expressions and the list names will be the row
#' names. For ex. `fixef.group=list("Dates fixed-effects"="Month|Day")` will remove the `"Month"`
#' and `"Day"` fixed effects from the display and replace them with a single row named
#' "Dates fixed-effects". You can monitor the placement of the new row with two special characters
#' telling where to place the row within a section: first in which section it should appear:
#' `"^"` (coef.), `"-"` (fixed-effects), or `"_"` (stat.) section;
#' then whether the row should be `"^"` (first), or `"_"` (last). These two special characters must
#' appear first in the row names. Please see the dedicated section
#' @param placement (Tex only.) Character string giving the position of the float in Latex. Default
#' is "htbp". It must consist of only the characters 'h', 't', 'b', 'p', 'H' and '!'.
#' Reminder: h: here; t: top; b: bottom; p: float page; H: definitely here;
#' !: prevents Latex to look for other positions. Note that it can be equal to the empty string
#' (and you'll get the default placement).
#' @param drop.section Character vector which can be of length 0 (i.e. equal to `NULL`). Can
#' contain the values "coef", "fixef", "slopes" or "stats". It would drop, respectively, the
#' coefficients section, fixed-effects section, the variables with varying slopes section or the
#' fit statistics section.
#' @param reset (`setFixest_etable` only.) Logical, default is `FALSE`. If `TRUE`, this will reset
#' all the default values that were already set by the user in previous calls.
#' @param .vcov A function to be used to compute the standard-errors of each fixest object. You can
#' pass extra arguments to this function using the argument `.vcov_args`. See the example.
#' @param .vcov_args A list containing arguments to be passed to the function `.vcov`.
#' @param poly_dict Character vector, default is `c("", " square", " cube")`. When raw polynomials
#' (`x^2`, etc) are used, the variables are automatically renamed and `poly_dict` rules the display
#' of the power. For powers greater than the number of elements of the vector, the value displayed
#' is `$^{pow}$` in Latex and `^ pow` in the R console.
#' @param postprocess.tex A function that will postprocess the character vector defining the latex
#' table. Only when `tex = TRUE`. By default it is equal to `NULL`, meaning that there is no
#' postprocessing. When `tex = FALSE`, see the argument `postprocess.df`. See details.
#' @param postprocess.df A function that will postprocess.tex the resulting data.frame. Only when
#' `tex = FALSE`. By default it is equal to `NULL`, meaning that there is no postprocessing. When
#' `tex = TRUE`, see the argument `postprocess.tex`.
#' @param fit_format Character scalar, default is `"__var__"`. Only used in the presence of IVs. By
#' default the endogenous regressors are named `fit_varname` in the second stage. The format of the
#' endogenous regressor to appear in the table is governed by `fit_format`. For instance, by
#' default, the prefix `"fit_"` is removed, leading to only `varname` to appear.
#' If `fit_format = "$\\\\hat{__var__$"}`, then `"$\\hat{varname$"}` will appear in the table.
#' @param coef.just (DF only.) Either `"."`, `"("`, `"l"`, `"c"` or `"r"`, default is `NULL`.
#' How the coefficients should be justified. If `NULL` then they are right aligned if
#' `se.below = FALSE` and aligned to the dot if `se.below = TRUE`. The keywords stand
#' respectively for dot-, parenthesis-, left-, center- and right-aligned.
#' @param meta (Tex only.) A one-sided formula that shall contain the following elements:
#' date or time, sys, author, comment and call. Default is `NULL`. This argument is a shortcut to
#' controlling the meta information that can be displayed in comments before the table. Typically
#' if the element is in the formula, it means that the argument will be equal to `TRUE`.
#' Example: `meta = ~time+call` is equivalent to `meta.time = TRUE` and `meta.call = TRUE`.
#' The "author" and "comment" elements are a bit special. Using `meta = ~author("Mark")` is
#' equivalent to `meta.author = "Mark"` while `meta=~author` is equiv. to `meta.author = TRUE`.
#' The "comment" must be used with a character string inside:
#' `meta = ~comment("this is a comment")`. The order in the formula controls the order of
#' appearance of the meta elements. It also has precedence over the `meta.XX` arguments.
#' @param meta.time (Tex only.) Either a logical scalar (default is `FALSE`) or "time" or "date".
#' Whether to include the time (if `TRUE` or "time") or the date (if "date") of creation of the
#' table in a comment right before the table.
#' @param meta.sys (Tex only.) A logical scalar, default is `FALSE`. Whether to include system
#' information (from `Sys.info()`) in a comment right before the table.
#' @param meta.author (Tex only.) A logical scalar (default is `FALSE`) or a character vector. If
#' `TRUE` then the identity of the author (deduced from the system user in `Sys.info()`) is
#' inserted in a comment right before the table. If a character vector, then it should contain
#' author names that will be inserted as comments before the table, prefixed with `"Created by:"`.
#' For free-form comments see the argument `meta.comment`.
#' @param meta.comment (Tex only.) A character vector containing free-form comments to be inserted
#' right before the table.
#' @param meta.call (Tex only.) Logical scalar, default is `FALSE`. If `TRUE` then the call to the
#' function is inserted right before the table in a comment.
#' @param view Logical, default is `FALSE`. If `TRUE`, then the table generated in Latex by
#' `etable` and then is displayed in the viewer pane. Note that for this option to work you need
#' i) pdflatex or the R package `tinytex`, ii) imagemagick and ghostscript, or the
#' R package `pdftools`. All three software must be installed and on the path.
#' @param x An object returned by `etable`.
#' @param tpt (Tex only.) Logical scalar, default is FALSE. Whether to use the `threeparttable`
#' environment. If so, the `notes` will be integrated into the `tablenotes` environment.
#' @param arraystretch (Tex only.) A numeric scalar, default is `NULL`. If provided,
#' the command `\\renewcommand*{\\arraystretch{x}}` is inserted, replacing `x` by the value of
#' `arraystretch`. The changes are specific to the current table and do not affect the rest of the
#' document.
#' @param fontsize (Tex only.) A character scalar, default is `NULL`. Can be equal to `tiny`,
#' `scriptsize`, `footnotesize`, `small`, `normalsize`, `large`, or `Large`. The change affect the
#' table only (and not the rest of the document).
#' @param adjustbox (Tex only.) A logical, numeric or character scalar, default is `NULL`. If not
#' `NULL`, the table is inserted within the `adjustbox` environment. By default the options are
#' `width = 1\\textwidth, center` (if `TRUE`). A numeric value changes the value before
#' `\\textwidth`. You can also add a character of the form `"x tw"` or `"x th"` with `x` a number
#' and where tw (th) stands for text-width (text-height). Finally any other character value is
#' passed verbatim as an `adjustbox` option.
#' @param tabular (Tex only.) Character scalar equal to "normal" (default), `"*"` or `"X"`.
#' Represents the type of tabular environment to use: either `tabular`, `tabular*` or `tabularx`.
#' @param export Character scalar giving the path to a PNG file to be created, default is `NULL`.
#' If provided, the Latex table will be converted to PNG and copied to the `export` location. Note
#' that for this option to work you need a working distribution of `pdflatex`, `imagemagick` and
#' `ghostscript`, or the R packages `tinytex` and `pdftools`.
#' @param markdown Character scalar giving the location of a directory, or a logical scalar.
#' Default is `NULL`. This argument only works in Rmarkdown documents, when knitting the document.
#' If provided: two behaviors depending on context. A) if the output document is Latex, the table
#' is exported in Latex. B) if the output document is not Latex, the table will be exported to PNG
#' at the desired location and inserted in the document via a markdown link. If equal to `TRUE`,
#' the default location of the PNGs is a temporary folder for `R > 4.0.0`,
#' or to `"images/etable/"` for earlier versions.
#' @param view.cache Logical, default is `FALSE`. Only used when `view = TRUE`.
#' Whether the PNGs of the tables should be cached.
#' @param type Character scalar equal to 'pdflatex' (default), 'magick', 'dir' or 'tex'.
#' Which log file to report; if 'tex', the full source code of the tex file is returned,
#' if 'dir': the directory of the log files is returned.
#' @param highlight List containing coefficients to highlight.
#' Highlighting is of the form `.("options1" = "coefs1", "options2" = "coefs2", etc)`.
#' The coefficients to be highlighted can be written in three forms: 1) row, eg `"x1"` will
#' highlight the full row of the variable `x1`; 2) cells, use `'@'` after the coefficient name to
#' give the column, it accepts ranges, eg `"x1@2, 4-6, 8"` will highlight only the columns
#' 2, 4, 5, 6, and 8 of the variable `x1`; 3) range, by giving the top-left and
#' bottom-right values separated with a semi-colon, eg `"x1@2 ; x3@5"` will highlight
#' from the column 2 of `x1` to the 5th column of `x3`. Coefficient names are partially
#' matched, use a `'%'` first to refer to the original name (before dictionary) and
#' use `'@'` first to use a regular expression. You can add a vector of row/cell/range.
#' The options are a comma-separated list of items. By default the highlighting is done
#' with a frame (a thick box) around the coefficient, use `'rowcol'` to highlight with a
#' row color instead. Here are the other options: `'se'` to highlight the standard-errors too;
#' `'square'` to have a square box (instead of rounded); `'thick1'` to `'thick6'`
#' to monitor the width of the box; `'sep0'` to `'sep9'` to monitor the inner spacing.
#' Finally the remaining option is the color: simply add an R color (it must be a valid R color!).
#' You can use `"color!alpha"` with "alpha" a number between 0 to 100 to change
#' the alpha channel of the color.
#' @param coef.style Named list containing styles to be applied to the coefficients. It must be of
#' the form `.("style1" = "coefs1", "style2" = "coefs2", etc)`. The style must contain the
#' string `":coef:"` (or `":coef_se:"` to style both the coefficient and its standard-error).
#' The string `:coef:` will be replaced verbatim by the coefficient value. For example use
#' `"\\textbf{:coef:}"` to put the coefficient in bold. Note that markdown markup is enabled
#' so `"**:coef:**"` would also put it in bold. The coefficients to be styled can be written
#' in three forms: 1) row, eg `"x1"` will style the full row of the variable `x1`; 2) cells,
#' use `'@'` after the coefficient name to give the column, it accepts ranges,
#' eg `"x1@2, 4-6, 8"` will style only the columns 2, 4, 5, 6, and 8 of the variable `x1`;
#' 3) range, by giving the top-left and bottom-right values separated with a semi-colon,
#' eg `"x1@2 ; x3@5"` will style from the column 2 of `x1` to the 5th column of `x3`.
#' Coefficient names are partially matched, use a `'%'` first to refer to the original name
#' (before dictionary) and use `'@'` first to use a regular expression. You can add a
#' vector of row/cell/range.
#' @param page.width Character scalar equal to `'fit'` (default), `'a4'` or `'us'`; or a
#' single Latex measure (like `'17cm'`) or a double one (like `"21, 2cm"`). Only used when
#' the Latex table is to be viewed (`view = TRUE`), exported (`export != NULL`) or displayed
#' in Rmarkdown (`markdown != NULL`). It represents the text width of the page in which the
#' Latex table will be inserted. By default, `'fit'`, the page fits exactly the table (i.e.
#' text width = table width). If `'a4'` or `'us'`, two times 2cm is removed from the page
#' width to account for margins. Providing a page width and a margin width, like in
#' `"17in, 1in"`, enables a correct display of the argument `adjustbox`. Note that the
#' margin width represent the width of a single side margin (and hence will be doubled).
#' @param div.class Character scalar, default is `"etable"`. Only used in Rmarkdown documents
#' when `markdown = TRUE`. The table in an image format is embedded in a `<div>` container,
#' and that container is of class `div.class`.
#'
#'
#' @details
#' The function `esttex` is equivalent to the function `etable` with argument `tex = TRUE`.
#'
#' The function `esttable` is equivalent to the function `etable` with argument `tex = FALSE`.
#'
#' To display the table, you will need the Latex package `booktabs` which contains
#' the `\\toprule`, `\\midrule` and `\\bottomrule` commands.
#'
#' You can permanently change the way your table looks in Latex by using `setFixest_etable`.
#' The following vignette gives an example as well as illustrates how to use the `style` and
#' postprocessing functions: [Exporting estimation tables](https://lrberge.github.io/fixest/articles/exporting_tables.html).
#'
#' When the argument `postprocess.tex` is not missing, two additional tags will be included in the
#' character vector returned by `etable`: `"%start:tab\\n"` and `"%end:tab\\n"`. These can be used
#' to identify the start and end of the tabular and are useful to insert code within the `table`
#' environment.
#'
#' @section How does `digits` handle the number of decimals displayed?:
#'
#' The default display of decimals is the outcome of an algorithm. Let's take the example
#' of `digits = 3` which "kind of" requires 3 significant digits to be displayed.
#'
#' For numbers greater than 1 (in absolute terms), their integral part is always displayed and
#' the number of decimals shown is equal to `digits` minus the number of digits in the integral
#' part. This means that `12.345` will be displayed as `12.3`. If the number of decimals should
#' be 0, then a single decimal is displayed to suggest that the number is not whole. This means
#' that `1234.56` will be displayed as `1234.5`. Note that if the number is whole, no decimals
#' are shown.
#'
#' For numbers lower than 1 (in absolute terms), the number of decimals displayed is equal
#' to `digits` except if there are only 0s in which case the first significant digit is shown.
#' This means that `0.01234` will be displayed as `0.012` (first rule), and that 0.000123 will
#' be displayed as `0.0001` (second rule).
#'
#' @section Arguments keep, drop and order:
#' The arguments `keep`, `drop` and `order` use regular expressions. If you are not aware
#' of regular expressions, I urge you to learn it, since it is an extremely powerful way
#' to manipulate character strings (and it exists across most programming languages).
#'
#' For example drop = "Wind" would drop any variable whose name contains "Wind". Note that
#' variables such as "Temp:Wind" or "StrongWind" do contain "Wind", so would be dropped.
#' To drop only the variable named "Wind", you need to use `drop = "^Wind$"` (with "^" meaning
#' beginning, resp. "$" meaning end, of the string => this is the language of regular expressions).
#'
#' Although you can combine several regular expressions in a single character string using pipes,
#' `drop` also accepts a vector of regular expressions.
#'
#' You can use the special character "!" (exclamation mark) to reverse the effect of the regular
#' expression (this feature is specific to this function). For example `drop = "!Wind"` would drop
#' any variable that does not contain "Wind".
#'
#' You can use the special character "%" (percentage) to make reference to the original variable
#' name instead of the aliased name. For example, you have a variable named `"Month6"`, and use a
#' dictionary `dict = c(Month6="June")`. Thus the variable will be displayed as `"June"`. If you
#' want to delete that variable, you can use either `drop="June"`, or `drop="%Month6"` (which makes
#' reference to its original name).
#'
#' The argument `order` takes in a vector of regular expressions, the order will follow the
#' elements of this vector. The vector gives a list of priorities, on the left the elements with
#' highest priority. For example, order = c("Wind", "!Inter", "!Temp") would give highest
#' priorities to the variables containing "Wind" (which would then appear first), second highest
#' priority is the variables not containing "Inter", last, with lowest priority, the variables not
#' containing "Temp". If you had the following variables: (Intercept), Temp:Wind, Wind, Temp you
#' would end up with the following order: Wind, Temp:Wind, Temp, (Intercept).
#'
#' @section The argument `extralines`:
#'
#' The argument `extralines` adds well... extra lines to the table. It accepts either a list, or a
#' one-sided formula.
#'
#' For each line, you can define the values taken by each cell using 4 different ways: a) a vector,
#' b) a list, c) a function, and d) a formula.
#'
#' If a vector, it should represent the values taken by each cell. Note that if the length of the
#' vector is smaller than the number of models, its values are recycled across models, but the
#' length of the vector is required to be a divisor of the number of models.
#'
#' If a list, it should be of the form `list("item1" = #item1, "item2" = #item2, etc)`. For example
#' `list("A"=2, "B"=3)` leads to `c("A", "A", "B", "B", "B")`. Note that if the number of items is
#' 1, you don't need to add `= 1`. For example `list("A"=2, "B")` is valid and leads to
#' `c("A", "A", "B"`. As for the vector the values are recycled if necessary.
#'
#' If a function, it will be applied to each model and should return a scalar (`NA` values
#' returned are accepted).
#'
#' If a formula, it must be one-sided and the elements in the formula must represent either
#' `extralines` macros, either fit statistics (i.e. valid types of the function [`fitstat`]). One
#' new line will be added for each element of the formula. To register `extralines` macros, you
#' must first register them in [`extralines_register`].
#'
#' Finally, you can combine as many lines as wished by nesting them in a list. The names of the
#' nesting list are the row titles (values in the leftmost cell). For example
#' `extralines = list(~r2, Controls = TRUE, Group = list("A"=2, "B"))` will add three lines,
#' the titles of which are "R2", "Controls" and "Group".
#'
#'
#' @section Controlling the placement of extra lines:
#'
#' The arguments `group`, `extralines` and `fixef.group` allow to add customized lines in the
#' table. They can be defined via a list where the list name will be the row name. By default, the
#' placement of the extra line is right after the coefficients (except for `fixef.group`, covered
#' in the last paragraph). For instance, `group = list("Controls" = "x[[:digit:]]")` will create a
#' line right after the coefficients telling which models contain the control variables.
#'
#' But the placement can be customized. The previous example (of the controls) will be used for
#' illustration (the mechanism for `extralines` and `fixef.group` is identical).
#'
#' The row names accept 2 special characters at the very start. The first character tells in which
#' section the line should appear: it can be equal to `"^"`, `"-"`, or `"_"`, meaning respectively
#' the coefficients, the fixed-effects and the statistics section (which typically appear at the
#' top, mid and bottom of the table). The second one governs the placement of the new line within
#' the section: it can be equal to `"^"`, meaning first line, or `"_"`, meaning last line.
#'
#' Let's have some examples. Using the previous example, writing `"_^Controls"` would place the new
#' line at the top of the statistics section. Writing `"-_Controls"` places it as the last row of
#' the fixed-effects section; `"^^Controls"` at the top row of the coefficients section; etc...
#'
#' The second character is optional, the default placement being in the bottom. This means that
#' `"_Controls"` would place it at the bottom of the statistics section.
#'
#' The placement in `fixef.group` is defined similarly, only the default placement is different.
#' Its default placement is at the top of the fixed-effects section.
#'
#' @section Escaping special Latex characters:
#'
#' By default on all instances (with the notable exception of the elements of [`style.tex`])
#' special Latex characters are escaped. This means that `title="Exports in million $."` will be
#' exported as `"Exports in million \\$."`: the dollar sign will be escaped. This is true for the
#' following characters: &, `$`, %, _, ^ and #.
#'
#' Note, importantly, that equations are NOT escaped. This means that
#' `title="Functional form $a_i \\times x^b$, variation in %."` will be displayed as:
#' `"Functional form $a_i \\times x^b$, variation in \\%."`: only the
#' last percentage will be escaped.
#'
#' If for some reason you don't want the escaping to take place, the arguments `headers` and
#' `extralines` are the only ones allowing that. To disable escaping, add the special token
#' ":tex:" in the row names.
#' Example: in `headers=list(":tex:Row title"="weird & & %\\n tex stuff\\\\")`,
#' the elements will be displayed verbatim. Of course, since it can easily ruin your table,
#' it is only recommended to super users.
#'
#' @section Markdown markup:
#'
#' Within anything that is Latex-escaped (see previous section), you can use a markdown-style
#' markup to put the text in italic and/or bold. Use `*text*`, `**text**` or `***text***` to
#' put some text in, respectively, italic (with `\\textit`), bold (with `\\textbf`) and italic-bold.
#'
#' The markup can be escaped by using an backslash first. For example `"***This: \\***, are
#' three stars***"` will leave the three stars in the middle untouched.
#'
#' @return
#' If `tex = TRUE`, the lines composing the Latex table are returned invisibly while the table
#' is directly prompted on the console.
#'
#' If `tex = FALSE`, the data.frame is directly returned. If the argument `file` is not missing,
#' the `data.frame` is printed and returned invisibly.
#'
#' @seealso
#' For styling the table: [`setFixest_etable`], [`style.tex`], [`style.df`].
#'
#' See also the main estimation functions [`femlm`], [`feols`] or [`feglm`].
#' Use [`summary.fixest`]
#' to see the results with the appropriate standard-errors, [`fixef.fixest`] to extract the
#' fixed-effects coefficients.
#'
#' @author
#' Laurent Berge
#'
#' @examples
#'
#'
#' est1 = feols(Ozone ~ i(Month) / Wind + Temp, data = airquality)
#' est2 = feols(Ozone ~ i(Month, Wind) + Temp | Month, data = airquality)
#'
#' # Displaying the two results in a single table
#' etable(est1, est2)
#'
#' # keep/drop: keeping only interactions
#' etable(est1, est2, keep = " x ")
#' # or using drop (see regexp help):
#' etable(est1, est2, drop = "^(Month|Temp|\\()")
#'
#' # keep/drop: dropping interactions
#' etable(est1, est2, drop = " x ")
#' # or using keep ("!" reverses the effect):
#' etable(est1, est2, keep = "! x ")
#'
#' # order: Wind variable first, intercept last (note the "!" to reverse the effect)
#' etable(est1, est2, order = c("Wind", "!Inter"))
#' # Month, then interactions, then the rest
#' etable(est1, est2, order = c("^Month", " x "))
#'
#' #
#' # dict
#' #
#'
#' # You can rename variables with dict = c(var1 = alias1, var2 = alias2, etc)
#' # You can also rename values taken by factors.
#' # Here's a full example:
#' dict = c(Temp = "Temperature", "Month::5"="May", "6"="Jun")
#' etable(est1, est2, dict = dict)
#' # Note the difference of treatment between Jun and May
#'
#' # Assume the following dictionary:
#' dict = c("Month::5"="May", "Month::6"="Jun", "Month::7"="Jul",
#' "Month::8"="Aug", "Month::9"="Sep")
#'
#' # We would like to keep only the Months, but now the names are all changed...
#' # How to do?
#' # We can use the special character '%' to make reference to the original names.
#'
#' etable(est1, est2, dict = dict, keep = "%Month")
#'
#' #
#' # signif.code
#' #
#'
#' etable(est1, est2, signif.code = c(" A"=0.01, " B"=0.05, " C"=0.1, " D"=0.15, " F"=1))
#'
#' #
#' # Using the argument style to customize Latex exports
#' #
#'
#' # If you don't like the default layout of the table, no worries!
#' # You can modify many parameters with the argument style
#'
#' # To drop the headers before each section, use:
#' # Note that a space adds an extra line
#' style_noHeaders = style.tex(var.title = "", fixef.title = "", stats.title = " ")
#' etable(est1, est2, dict = dict, tex = TRUE, style.tex = style_noHeaders)
#'
#' # To change the lines of the table + dropping the table footer
#' style_lines = style.tex(line.top = "\\toprule", line.bottom = "\\bottomrule",
#' tablefoot = FALSE)
#' etable(est1, est2, dict = dict, tex = TRUE, style.tex = style_lines)
#'
#' # Or you have the predefined type "aer"
#' etable(est1, est2, dict = dict, tex = TRUE, style.tex = style.tex("aer"))
#'
#' #
#' # Group and extralines
#' #
#'
#' # Sometimes it's useful to group control variables into a single line
#' # You can achieve that with the group argument
#'
#' setFixest_fml(..ctrl = ~ poly(Wind, 2) + poly(Temp, 2))
#' est_c0 = feols(Ozone ~ Solar.R, data = airquality)
#' est_c1 = feols(Ozone ~ Solar.R + ..ctrl, data = airquality)
#' est_c2 = feols(Ozone ~ Solar.R + Solar.R^2 + ..ctrl, data = airquality)
#'
#' etable(est_c0, est_c1, est_c2, group = list(Controls = "poly"))
#'
#' # 'group' here does the same as drop = "poly", but adds an extra line
#' # with TRUE/FALSE where the variables were found
#'
#' # 'extralines' adds an extra line, where you can add the value for each model
#' est_all = feols(Ozone ~ Solar.R + Temp + Wind, data = airquality)
#' est_sub1 = feols(Ozone ~ Solar.R + Temp + Wind, data = airquality,
#' subset = ~ Month %in% 5:6)
#' est_sub2 = feols(Ozone ~ Solar.R + Temp + Wind, data = airquality,
#' subset = ~ Month %in% 7:8)
#' est_sub3 = feols(Ozone ~ Solar.R + Temp + Wind, data = airquality,
#' subset = ~ Month == 9)
#'
#' etable(est_all, est_sub1, est_sub2, est_sub3,
#' extralines = list("Sub-sample" = c("All", "May-June", "Jul.-Aug.", "Sept.")))
#'
#' # You can monitor the placement of the new lines with two special characters
#' # at the beginning of the row name.
#' # 1) "^", "-" or "_" which mean the coefficients, the fixed-effects or the
#' # statistics section.
#' # 2) "^" or "_" which mean first or last line of the section
#' #
#' # Ex: starting with "_^" will place the line at the top of the stat. section
#' # starting with "-_" will place the line at the bottom of the FEs section
#' # etc.
#' #
#' # You can use a single character which will represent the section,
#' # the line would then appear at the bottom of the section.
#'
#' # Examples
#' etable(est_c0, est_c1, est_c2, group = list("_Controls" = "poly"))
#' etable(est_all, est_sub1, est_sub2, est_sub3,
#' extralines = list("^^Sub-sample" = c("All", "May-June", "Jul.-Aug.", "Sept.")))
#'
#'
#' #
#' # headers
#' #
#'
#'
#' # You can add header lines with 'headers'
#' # These lines will appear at the top of the table
#'
#' # first, 3 estimations
#' est_header = feols(c(Ozone, Solar.R, Wind) ~ poly(Temp, 2), airquality)
#'
#' # header => vector: adds a line w/t title
#' etable(est_header, headers = c("A", "A", "B"))
#'
#' # header => list: identical way to do the previous header
#' # The form is: list(item1 = #item1, item2 = #item2, etc)
#' etable(est_header, headers = list("A" = 2, "B" = 1))
#'
#' # Adding a title +
#' # when an element is to be repeated only once, you can avoid the "= 1":
#' etable(est_header, headers = list(Group = list("A" = 2, "B")))
#'
#' # To change the placement, add as first character:
#' # - "^" => top
#' # - "-" => mid (default)
#' # - "_" => bottom
#' # Note that "mid" and "top" are only distinguished when tex = TRUE
#'
#' # Placing the new header line at the bottom
#' etable(est_header, headers = list("_Group" = c("A", "A", "B"),
#' "^Currency" = list("US $" = 2, "CA $" = 1)))
#'
#'
#' # In Latex, you can add "grouped underlines" (cmidrule from the booktabs package)
#' # by adding ":_:" in the title:
#' etable(est_header, tex = TRUE,
#' headers = list("^:_:Group" = c("A", "A", "B")))
#'
#' #
#' # extralines and headers: .() for list()
#' #
#'
#' # In the two arguments extralines and headers, .() can be used for list()
#' # For example:
#' etable(est_header, headers = .("^Currency" = .("US $" = 2, "CA $" = 1)))
#'
#'
#'
#' #
#' # fixef.group
#' #
#'
#' # You can group the fixed-effects line with fixef.group
#'
#' est_0fe = feols(Ozone ~ Solar.R + Temp + Wind, airquality)
#' est_1fe = feols(Ozone ~ Solar.R + Temp + Wind | Month, airquality)
#' est_2fe = feols(Ozone ~ Solar.R + Temp + Wind | Month + Day, airquality)
#'
#' # A) automatic way => simply use fixef.group = TRUE
#'
#' etable(est_0fe, est_2fe, fixef.group = TRUE)
#'
#' # Note that when grouping would lead to inconsistencies across models,
#' # it is avoided
#'
#' etable(est_0fe, est_1fe, est_2fe, fixef.group = TRUE)
#'
#' # B) customized way => use a list
#'
#' etable(est_0fe, est_2fe, fixef.group = list("Dates" = "Month|Day"))
#'
#' # Note that when a user grouping would lead to inconsistencies,
#' # the term partial replaces yes/no and the fixed-effects are not removed.
#'
#' etable(est_0fe, est_1fe, est_2fe, fixef.group = list("Dates" = "Month|Day"))
#'
#' # Using customized placement => as with 'group' and 'extralines',
#' # the user can control the placement of the new line.
#' # See the previous 'group' examples and the dedicated section in the help.
#'
#' # On top of the coefficients:
#' etable(est_0fe, est_2fe, fixef.group = list("^^Dates" = "Month|Day"))
#'
#' # Last line of the statistics
#' etable(est_0fe, est_2fe, fixef.group = list("_Dates" = "Month|Day"))
#'
#'
#'
#' #
#' # Using custom functions to compute the standard errors
#' #
#'
#' # You can use external functions to compute the VCOVs
#' # by feeding functions in the 'vcov' argument.
#' # Let's use some covariances from the sandwich package
#'
#' etable(est_c0, est_c1, est_c2, vcov = sandwich::vcovHC)
#'
#' # To add extra arguments to vcovHC, you need to write your wrapper:
#' etable(est_c0, est_c1, est_c2, vcov = function(x) sandwich::vcovHC(x, type = "HC0"))
#'
#'
#' #
#' # Customize which fit statistic to display
#' #
#'
#' # You can change the fit statistics with the argument fitstat
#' # and you can rename them with the dictionary
#' etable(est1, est2, fitstat = ~ r2 + n + G)
#'
#' # If you use a formula, '.' means the default:
#' etable(est1, est2, fitstat = ~ ll + .)
#'
#'
#' #
#' # Computing a different SE for each model
#' #
#'
#' est = feols(Ozone ~ Solar.R + Wind + Temp, data = airquality)
#'
#' #
#' # Method 1: use summary
#'
#' s1 = summary(est, "iid")
#' s2 = summary(est, cluster = ~ Month)
#' s3 = summary(est, cluster = ~ Day)
#' s4 = summary(est, cluster = ~ Day + Month)
#'
#' etable(list(s1, s2, s3, s4))
#'
#' #
#' # Method 2: using a list in the argument 'vcov'
#'
#' est_bis = feols(Ozone ~ Solar.R + Wind + Temp | Month, data = airquality)
#' etable(est, est_bis, vcov = list("hetero", ~ Month))
#'
#' # When you have only one model, this model is replicated
#' # along the elements of the vcov list.
#' etable(est, vcov = list("hetero", ~ Month))
#'
#' #
#' # Method 3: Using "each" or "times" in vcov
#'
#' # If the first element of the list in 'vcov' is "each" or "times",
#' # then all models will be replicated and all the VCOVs will be
#' # applied to each model. The order in which they are replicated
#' # are governed by the each/times keywords.
#'
#'
#' # each
#' etable(est, est_bis, vcov = list("each", "iid", ~ Month, ~ Day))
#'
#' # times
#' etable(est, est_bis, vcov = list("times", "iid", ~ Month, ~ Day))
#'
#' #
#' # Notes and markup
#' #
#'
#' # Notes can be also be set in a dictionary
#' # You can use markdown markup to put text into italic/bold
#'
#' dict = c("note 1" = "*Notes:* This data is not really random.",
#' "source 1" = "**Source:** the internet?")
#'
#' est = feols(Ozone ~ csw(Solar.R, Wind, Temp), data = airquality)
#'
#' etable(est, dict = dict, tex = TRUE, notes = c("note 1", "source 1"))
#'
#'
#'
etable = function(..., vcov = NULL, stage = 2, agg = NULL,
se = NULL, ssc = NULL, cluster = NULL,
.vcov = NULL, .vcov_args = NULL, digits = 4, digits.stats = 5, tex,
fitstat = NULL, title = NULL, coefstat = "se", ci = 0.95,
se.row = NULL, se.below = NULL,
keep = NULL, drop = NULL, order = NULL,
dict = TRUE, file = NULL, replace = FALSE, convergence = NULL,
signif.code = NULL, label = NULL, float = NULL,
headers = list("auto"), fixef_sizes = FALSE,
fixef_sizes.simplify = TRUE, keepFactors = TRUE,
family = NULL, powerBelow = -5,
interaction.combine = NULL, interaction.order = NULL,
i.equal = NULL, depvar = TRUE, style.tex = NULL,
style.df = NULL, notes = NULL, group = NULL, extralines = NULL,
fixef.group = NULL, placement = "htbp", drop.section = NULL,
poly_dict = c("", " square", " cube"), postprocess.tex = NULL,
postprocess.df = NULL, tpt = FALSE, arraystretch = NULL, adjustbox = NULL,
fontsize = NULL, fit_format = "__var__", coef.just = NULL,
tabular = "normal", highlight = NULL, coef.style = NULL,
meta = NULL, meta.time = NULL, meta.author = NULL, meta.sys = NULL,
meta.call = NULL, meta.comment = NULL, view = FALSE,
export = NULL, markdown = NULL, page.width = "fit",
div.class = "etable"){
#
# Checking the arguments
#
# Need to check for the presence of the se
useSummary = TRUE
if(missnull(vcov) && missnull(se) && missnull(cluster) && missing(.vcov) && missing(stage) && missnull(agg)){
useSummary = FALSE
}
# The depvar
if(missing(depvar) && !missing(file)){
depvar = TRUE
}
check_arg(tex, view, "logical scalar")
if(missing(tex)){
if(!missing(file)) {
tex = TRUE
} else {
tex = FALSE
}
}
# Float or not
check_arg(float, "NULL logical scalar")
if(missnull(float)){
if(!missing(title) || !missing(label)){
float = TRUE
} else {
float = FALSE
}
} else if(!float && (!missnull(title) || !missnull(label))) {
what = c("title", "label")[c(!missing(title), !missing(label))]
warning("Since float = FALSE, the argument", enumerate_items(what, "s.is"), " ignored.",
immediate. = TRUE, call. = FALSE)
}
check_value(div.class, "character scalar")
# NOTA: now that I allow the use of .(stuff) for headers and extralines
# list(...) will raise an error if subtitle (now deprec) is used with .()
# And there's no clear error message => I think it's a fair limitation
dots = error_sender(list(...), "Some elements in '...' could not be evaluated: ")
if(".up" %in% names(dots)){
# internal call from esttable/esttex
.up = 2
dots[[".up"]] = NULL
} else {
.up = 1
}
if(length(dots) == 0){
stop("You must provide at least one element in '...'.")
}
#
# Deprecated items
#
if("subtitles" %in% names(dots)){
if(is.null(getOption("fixest_etable_arg_subtitles"))){
warning("The argument 'subtitles' is deprecated. Please use 'headers' instead.")
options(fixest_etable_arg_subtitles = TRUE)
}
headers = dots$subtitles
dots$subtitles = NULL
}
if("extraline" %in% names(dots)){
if(is.null(getOption("fixest_etable_arg_extraline"))){
warning("The argument 'extraline' is deprecated. Please use 'extralines' instead (note the last 's'!).")
options(fixest_etable_arg_extraline = TRUE)
}
extralines = dots$extraline
dots$extraline = NULL
}
if("sdBelow" %in% names(dots)){
if(is.null(getOption("fixest_etable_arg_sdBelow"))){
warning("The argument 'sdBelow' is deprecated. Please use 'se.below' instead.")
options(fixest_etable_arg_sdBelow = TRUE)
}
se.below = dots$sdBelow
dots$sdBelow = NULL
}
if("signifCode" %in% names(dots)){
if(is.null(getOption("fixest_etable_arg_signifCode"))){
warning("The argument 'signifCode' is deprecated. Please use 'signif.code' instead.")
options(fixest_etable_arg_signifCode = TRUE)
}
signif.code = dots$signifCode
dots$signifCode = NULL
}
# Getting the model names
if(.up == 2){
# it's pain in the necky
sysOrigin = sys.parent()
mc = match.call(definition = sys.function(sysOrigin), call = sys.call(sysOrigin), expand.dots = FALSE)
dots_call = mc[["..."]]
} else {
mc = match.call(expand.dots = FALSE)
dots_call = mc[["..."]]
}
# vcov
vcov = oldargs_to_vcov(se, cluster, vcov, .vcov)
if(is_function_in_it(vcov) && missnull(.vcov_args)){
vcov_fun = if(is.function(vcov)) vcov else vcov[[1]]
.vcov_args = catch_fun_args(vcov_fun, dots, exclude_args = "vcov", erase_args = TRUE)
for(var in intersect(names(.vcov_args), names(dots))) dots[[var]] = NULL
}
# Arguments that can be set globally
opts = getOption("fixest_etable")
args_global = c("postprocess.tex", "postprocess.df", "view", "markdown", "page.width")
for(arg in setdiff(args_global, names(mc))){
if(arg %in% names(opts)){
assign(arg, opts[[arg]])
}
}
# argument only in setFixest_etable: cache
cache = opts$view.cache
check_arg(markdown, "NULL scalar(logical, character)")
if(is.logical(markdown)){
# R > 4.0.0: we use R_user_dir() to store the files across sessions
# else images/etable/
# The bookkeeping is handled in the dedicated function build_tex_png
if(isFALSE(markdown)) markdown = NULL
}
tex_origin = tex
is_md = !is.null(markdown)
if(is_md){
if(!is_Rmarkdown()){
if("markdown" %in% names(mc)){
warning("The argument 'markdown' only works when knitting Rmarkdown documents. It is currently ignored.")
}
markdown = NULL
is_md = FALSE
} else {
tex = TRUE
export = NULL
view = FALSE
if(knitr::is_latex_output()){
is_md = FALSE
}
}
}
is_export = !is.null(export)
# We can display the table in the R console AND at the same time showing
# the latex table in the viewer
do_df = isFALSE(tex)
do_tex = tex || view || is_export
#
# postprocess.tex
#
# Note that I need to catch the arguments from the two pp functions
# => so that the same call lead to valid evaluations
check_arg(postprocess.tex, "NULL function arg(1,)")
pp_tex = postprocess.tex
is_pp_tex = !is.null(pp_tex)
check_arg(postprocess.df, "NULL function arg(1,)")
pp_df = postprocess.df
is_pp_df = !is.null(pp_df)
# Catching the arguments
pp_tex_args = pp_df_args = list()
if(!is.null(names(dots))){
if(is_pp_tex){
fm = formalArgs(pp_tex)
qui = names(dots) %in% fm
pp_tex_args = dots[qui]
dots = dots[!qui]
}
if(is_pp_df){
fm = formalArgs(pp_df)
qui = names(dots) %in% fm
pp_df_args = dots[qui]
dots = dots[!qui]
}
}
if(length(dots) == 0){
stop("After cleaning the arguments to the postprocessing functions, there is no element left in '...'. Please provide at least one.")
}
for(i in seq_along(dots)){
if(!is_fixest_model(dots[[i]]) && !(is.list(dots[[i]]) && is_fixest_model(dots[[i]][[1]]))){
msg = ""
if(!is.null(names(dots))){
msg = paste0(" (named '", names(dots)[i], "')")
}
stop("The ", n_th(i), " element of '...'", msg, " is not valid: it should be a fixest object or a list of fixest objects, it is neither.")
}
}
# internal argument
caption.number = TRUE
build_etable_list = function(TEX){
results2formattedList(
dots = dots, vcov = vcov, ssc = ssc, fitstat_all = fitstat,
stage = stage, agg = agg,
.vcov_args = .vcov_args, digits = digits, digits.stats = digits.stats,
se.row = se.row, se.below = se.below,
signif.code = signif.code, coefstat = coefstat,
ci = ci, title = title, float = float, headers = headers,
keepFactors = keepFactors, tex = TEX, useSummary = useSummary,
dots_call = dots_call, powerBelow = powerBelow, dict = dict,
interaction.combine = interaction.combine, interaction.order = interaction.order,
i.equal = i.equal, convergence = convergence,
family = family, keep = keep, drop = drop, file = file, order = order,
label = label, fixef_sizes = fixef_sizes,
fixef_sizes.simplify = fixef_sizes.simplify,
depvar = depvar, style.tex = style.tex, style.df = style.df,
replace = replace, notes = notes, group = group, extralines = extralines,
fixef.group = fixef.group, placement = placement,
drop.section = drop.section, poly_dict = poly_dict,
tex_tag = TRUE, fit_format = fit_format,
coef.just = coef.just, meta = meta, meta.time = meta.time,
meta.author = meta.author, meta.sys = meta.sys,
meta.call = meta.call, meta.comment = meta.comment,
tpt = tpt, arraystretch = arraystretch, adjustbox = adjustbox,
fontsize = fontsize, tabular = tabular, highlight = highlight,
coef.style = coef.style, caption.number = caption.number,
mc = mc, .up = .up + 1
)
}
# Checking the requirements for exports to png
if(view || is_md || is_export){
build_ok = check_build_available()
if(!isTRUE(build_ok)){
args = c("view", "export", "markdown")
what = args[args %in% names(mc)]
if(length(what) > 0){
warni("The argument{$s, enum.bq, require?what} {build_ok} to work. So they are currently ignored.")
}
view = is_md = is_export = FALSE
markdown = export = NULL
do_tex = tex = tex_origin
}
}
if(do_tex){
# if we're in markdown, we remove the table number
if(is_md){
caption.number = FALSE
}
info_tex = build_etable_list(TRUE)
res_tex = etable_internal_latex(info_tex)
n_models = attr(res_tex, "n_models")
attr(res_tex, "n_models") = NULL
}
if(do_df){
info_df = build_etable_list(FALSE)
res_df = etable_internal_df(info_df)
}
make_png = function(x) NULL
is_png = view || is_md || is_export
if(is_png){
make_png = function(x) build_tex_png(x, view = view, export = export,
markdown = markdown, cache = cache,
page.width = page.width)
}
# DF requested, but also png, OK user
if(isFALSE(tex) && is_png){
# Some exporting functions only print stuff!
# I need to take care of that! PAIN IN THE NECK!
ok = TRUE
if(is_pp_tex){
pp_tex_args_all = list(res_tex)
if(length(pp_tex_args) > 0){
pp_tex_args_all[names(pp_tex_args)] = pp_tex_args
}
tex_output = capture.output(res_tex <- do.call(pp_tex, pp_tex_args_all))
if(length(tex_output) > 0){
ok = FALSE
args = c("view", "export")[c(view, is_export)]
warni("The argument{$s, enum.bq, don't?args} work with the current postprocessing function for tex. Try to remove it first.")
}
}
if(ok) make_png(res_tex)
}
# Export to file
is_file = !missnull(file)
if(is_file){
error_sender(sink(file = file, append = !replace),
"Argument 'file': error when creating the document in ", file)
on.exit(sink())
}
# Output to the requested format
if(tex){
if(is_pp_tex){
pp_tex_args_all = list(res_tex)
if(length(pp_tex_args) > 0){
pp_tex_args_all[names(pp_tex_args)] = pp_tex_args
}
res_tex = do.call(pp_tex, pp_tex_args_all)
}
if(is.null(res_tex)){
if(is_png){
args = c("view", "export", "markdown")[c(view, is_export, is_md)]
warni("The argument{$s, enum.bq, don't?args} work with the current postprocessing function for tex. Try to remove it first.")
}
return(invisible(NULL))
}
res_tex = res_tex[!res_tex %in% c("%start:tab\n", "%end:tab\n")]
res_tex = tex.nice(res_tex, n_models) # we wait after PP to nicify
path = make_png(res_tex)
if(is_md){
if(!knitr::is_latex_output()){
path = path_to_relative(path)
cat(sma('<div class = "{div.class}"><img src = "{path}"></div>\n'))
return(invisible(NULL))
}
}
if(is_file){
# We add extra whitespaces => otherwise the Latex file is a bit cluttered
cat("\n")
cat(res_tex, sep = "\n")
cat("\n\n")
}
if(identical(class(res_tex), "character")){
class(res_tex) = "etable_tex"
}
if(is_file){
return(invisible(res_tex))
} else {
return(res_tex)
}
} else {
# Some postptocessing functions return nothing (just print)
# other return a character vector => I need to take care of that
if(is_pp_df){
pp_df_args_all = list(res_df)
if(length(pp_df_args) > 0){
pp_df_args_all[names(pp_df_args)] = pp_df_args
}
res_df = do.call(pp_df, pp_df_args_all)
}
if(is.null(res_df)){
return(invisible(NULL))
}
if(is_file){
if(is.data.frame(res_df)){
print(res_df)
} else {
cat(res_df)
}
return(invisible(res_df))
} else {
if(is.data.frame(res_df)){
return(res_df)
} else {
cat(res_df)
return(invisible(res_df))
}
}
}
}
gen_etable_aliases = function(){
# esttex and esttable are the functions that existed when the package was launched
# Now the two have been merged into etable
# I like it much better
# I wanted to deprecate them, but maintainance with that function is very easy
etable_args = formals(etable)
arg_name = names(etable_args)
arg_default = sapply(etable_args, deparse_long)
#
# esttable
#
esttable_def = "esttable = function("
coll = paste0(", \n", strrep(" ", nchar(esttable_def)))
qui_df = !arg_name %in% c("tex", "title", "label", "float", "style.tex",
"notes", "placement", "postprocess.tex",
"meta", "meta.time", "meta.author", "meta.sys",
"meta.call", "meta.comment", "tpt", "arraystretch",
"adjustbox", "fontsize", "view", "tabular", "markdown")
esttable_args = paste0(arg_name[qui_df], " = ", arg_default[qui_df], collapse = coll)
esttable_args = gsub(" = ,", ",", esttable_args)
coll_bis = paste0(", \n", strrep(" ", nchar(" etable(")))
etable_call = paste0(arg_name[qui_df][-1], " = ", arg_name[qui_df][-1], collapse = coll_bis)
esttable_fun = paste0(esttable_def, esttable_args, "){\n\n",
" etable(...", coll_bis, etable_call, coll_bis,
"tex = FALSE", coll_bis, ".up = 2)\n}")
esttable_rox = "#' @describeIn etable Exports the results of multiple `fixest` estimations in a Latex table."
#
# esttex
#
esttex_def = "esttex = function("
coll = paste0(", \n", strrep(" ", nchar(esttex_def)))
qui_tex = !arg_name %in% c("tex", "style.df", "postprocess.df", "coef.just")
esttex_args = paste0(arg_name[qui_tex], " = ", arg_default[qui_tex], collapse = coll)
esttex_args = gsub(" = ,", ",", esttex_args)
etable_call = paste0(arg_name[qui_tex][-1], " = ", arg_name[qui_tex][-1], collapse = coll_bis)
esttex_fun = paste0(esttex_def, esttex_args, "){\n\n",
" etable(...", coll_bis, etable_call, coll_bis,
"tex = TRUE", coll_bis, ".up = 2)\n}")
esttex_rox = "#' @describeIn etable Exports the results of multiple `fixest` estimations in a Latex table."
# Writing the functions
intro = c("# Do not edit by hand\n# => aliases to the function etable")
s = "\n\n\n"
text = c(intro, s, esttable_rox, esttable_fun, s, esttex_rox, esttex_fun, s)
update_file("./R/alias_etable.R", text)
}
results2formattedList = function(dots, vcov = NULL, ssc = getFixest_ssc(), stage = 2,
agg = NULL, .vcov_args = NULL, digits = 4,
digits.stats = 5, fitstat_all, se.row = NULL,
se.below = NULL, dict,
signif.code = c("***"=0.01, "**"=0.05, "*"=0.10),
coefstat = "se", ci = 0.95, label, headers, title,
float = FALSE, replace = FALSE, keepFactors = FALSE,
tex = FALSE, useSummary, dots_call, powerBelow = -5,
interaction.combine, interaction.order, i.equal,
convergence, family, drop, order,
keep, file, fixef_sizes = FALSE, fixef_sizes.simplify = TRUE,
depvar = FALSE, style.tex = NULL, style.df=NULL,
notes = NULL, group = NULL, extralines=NULL,
fixef.group = NULL, placement = "htbp", drop.section = NULL,
poly_dict = c("", " square", " cube"), tex_tag = FALSE,
fit_format = "__var__", coef.just = NULL,
meta = NULL, meta.time = NULL, meta.author = NULL,
meta.call = NULL, meta.sys = NULL, meta.comment = NULL,
tpt = FALSE, arraystretch = NULL, adjustbox = NULL,
fontsize = NULL, tabular = "normal", highlight = NULL,
coef.style = NULL, caption.number = TRUE,
mc = NULL, .up = 1){
# This function is the core of the function etable
set_up(.up)
# Setting the default values (we take extra care for "style")
if(tex){
check_arg(style.tex, "NULL class(fixest_style_tex)")
# The variable style will be changed via the defaults
style_user = style.tex
} else {
check_arg(style.df, "NULL class(fixest_style_df)")
# The variable style will be changed via the defaults
style_user = style.df
}
#
# Setting the default
#
opts = getOption("fixest_etable")
sysOrigin = sys.parent(.up)
if(length(opts) > 0){
args_usr = setdiff(names(mc), c("style.tex", "style.df"))
# We modify only non-user provided arguments
for(v in names(opts)){
if(!v %in% args_usr){
assign(v, opts[[v]])
}
}
}
# Getting the default style values
if(tex){
if(!"style.tex" %in% names(opts)){
style = fixest::style.tex(main = "base")
} else {
style = style.tex
}
} else if(!tex){
if(!"style.df" %in% names(opts)){
style = fixest::style.df(default = TRUE)
} else {
# We rename style.df into style
style = style.df
}
}
# Setting the style defaults
# We modify the default set with setFixest_etable()
if(length(style_user) > 0){
style[names(style_user)] = style_user
}
# Arguments both in style AND in etable
args_dual = c("tpt", "arraystretch", "fontsize", "adjustbox",
"tabular", "signif.code")
for(arg in setdiff(args_dual, names(mc))){
# We set to the default in style (only if NOT user-provided)
if(arg %in% names(style)){
assign(arg, style[[arg]])
}
}
#
# Full control
#
check_arg(title, "NULL character scalar")
check_set_arg(coefstat, "match(se, tstat, confint)")
check_set_arg(notes, "NULL character vector no na")
if(length(notes) > 0) notes = notes[nchar(notes) > 0]
check_arg("logical scalar", replace, fixef_sizes, fixef_sizes.simplify,
keepFactors, tex, depvar)
check_arg("NULL logical scalar", convergence, family, se.below, se.row, tpt)
if(is.null(tpt)) tpt = FALSE
isTex = tex
if(missnull(family)){
show_family = NULL
} else {
show_family = family
}
if(is.null(se.below)){
se.below = isTex
}
# start: coef.just
check_arg(coef.just, "NULL charin", .choices = c(".", "(", "l", "c", "r"))
if(!isTex){
# this parameter is only used in DF
if(is.null(coef.just) && se.below){
coef.just = "."
}
if(coefstat == "confint" && identical(coef.just, ".")){
coef.just = "c"
}
if(identical(coef.just, "r")){
coef.just = NULL
}
}
# end: coef.just
# digits argument + formatting
digits_list = check_set_digits(digits, up = 2)
digits = digits_list$digits
round = digits_list$round
fun_format = function(x) format_number(x, digits = digits, round = round, pow_below = powerBelow, tex = isTex)
digits_list = check_set_digits(digits.stats, up = 2)
digits.stats = digits_list$digits
round.stats = digits_list$round
fun_format_stats = function(x) format_number(x, digits = digits.stats, round = round.stats, pow_below = powerBelow, tex = isTex)
# we rename the argument
if(missing(depvar)){
# Default is set in the end
show_depvar = NULL
} else {
show_depvar = depvar
}
check_arg(keep, drop, order, "character vector no na NULL",
.message = "The arg. '__ARG__' must be a vector of regular expressions (see help(regex)).")
check_arg(file, label, interaction.combine, i.equal, "NULL character scalar")
check_set_arg(tabular, "match(normal, *, X)")
# interaction.combine: Default depends on type
if(is.null(interaction.combine)){
# interaction.combine = if(isTex) " $\\times $ " else " x "
interaction.combine = style$interaction.combine
}
# i.equal
if(is.null(i.equal)){
# i.equal = if(isTex) " $=$ " else " = "
i.equal = style$i.equal
}
check_set_arg(signif.code, "NULL NA | match(letters) | named numeric vector no na GE{0} LE{1}")
if(missnull(signif.code)){
if(isTex){
signif.code = c("***"=0.01, "**"=0.05, "*"=0.10)
} else {
signif.code = c("***"=0.001, "**"=0.01, "*"=0.05, "."=0.10)
}
}
add_signif = TRUE
if(identical(signif.code, "letters")){
signif.code = c("a"=0.01, "b"=0.05, "c"=0.10)
} else if(length(signif.code) == 0 || (length(signif.code) == 1 && is.na(signif.code))){
add_signif = FALSE
} else {
signif.code = sort(signif.code)
}
# eval_dot: headers + extralines + highlight
headers = error_sender(eval_dot(headers, .up + 1), arg_name = "headers", up = .up)
extralines = error_sender(eval_dot(extralines, .up + 1), arg_name = "extralines", up = .up)
highlight = error_sender(eval_dot(highlight, .up + 1), arg_name = "highlight", up = .up)
coef.style = error_sender(eval_dot(coef.style, .up + 1), arg_name = "coef.style", up = .up)
check_arg(headers, "NULL character vector no na | NA | list")
if(is.null(headers)) headers = list()
check_arg(highlight, "NULL character vector no na | list")
if(is.character(highlight)){
# We always want HL to be a list!
# hl = "Petal.L>1-2"
# hl = c("rowcol, lightblue" = "Petal.L>1-2")
if(length(highlight) == 1){
highlight = as.list(highlight)
} else {
# length 2 => can't have names
# hl = c("Petal.L@1", "Sepal.L@3")
highlight = list(highlight)
}
}
check_arg(coef.style, "NULL named list")
if(is.null(coef.style)){
coef.style = list()
}
check_arg(poly_dict, "character vector no na")
check_arg(powerBelow, "integer scalar LE{-1}")
check_arg(ci, "numeric scalar GT{0.5} LT{1}")
check_arg(dict, "NULL logical scalar | named character vector no na")
check_arg(placement, "character scalar")
if(isTex){
if(nchar(placement) > 0){
p_split = strsplit(placement, "")[[1]]
check_value(p_split, "strict multi charin(h, t, b, p, H, !)",
.message = "Argument 'placement' must be a character string containing only the following characters: 'h', 't', 'b', 'p', 'H', and '!'.")
}
}
check_set_arg(drop.section, "NULL multi match(fixef, slopes, stats)")
check_set_arg(group, "NULL{list()} named list l0")
check_set_arg(extralines, "NULL{list()} list l0 | os formula | vector")
# we check it more in depth later
check_arg(fixef.group, "NULL{list()} logical scalar | named list l0")
if(isFALSE(fixef.group)){
fixef.group = list()
}
IS_FIXEF_GROUP = length(fixef.group) > 0
check_arg(fit_format, "character scalar")
if(!grepl("__var__", fit_format, fixed = TRUE)){
stop("The argument 'fit_format' should include the special name '__var__' that will be replaced by the variable name. So far it does not contain it.")
}
check_arg(arraystretch, "NULL numeric scalar GT{0}")
check_set_arg(fontsize, "NULL match(tiny, scriptsize, footnotesize, small, normalsize, large, Large)")
# adjustbox + default
adjustbox = check_set_adjustbox(adjustbox, .up)
#
# meta
#
meta_txt = ""
if(isTex){
check_arg(meta, "NULL os formula")
# Note that meta CANNOT be set globally, so if not null it has **always** precedence!
meta_order = c("time", "author", "sys", "call", "comment")
if(!is.null(meta)){
# meta = ~time + author("hahaha") + sys + comment("this is nuts")
meta_vars = all.vars(meta, functions = TRUE)
meta_vars = meta_vars[grepl("[[:alpha:]]{2,}", meta_vars)]
check_set_value(meta_vars, "multi match(time, date, sys, author, comment, call)",
.message = paste0("The argument 'meta' must be a one-sided formula composed of the following variables: \n",
" time, date, author, sys, call or comment."))
# date/time/call/sys cannot be directly edited
if("date" %in% meta_vars) meta.time = "date"
if("time" %in% meta_vars) meta.time = "time"
if("call" %in% meta_vars) meta.call = TRUE
if("sys" %in% meta_vars) meta.sys = TRUE
meta_vars[meta_vars == "date"] = "time"
meta_order = order_apply(meta_order, paste0("^", meta_vars, "$"))
meta_terms = attr(terms(meta), "term.labels")
if("author" %in% meta_vars) {
if(!"author" %in% all.vars(meta, functions = FALSE)){
# means author is a function
my_term = meta_terms[grepl("^aut.*\\(", meta_terms)]
my_term = gsub("^[^\\(]+\\(", "I(", my_term)
meta.author = eval(str2lang(my_term))
} else {
meta.author = TRUE
}
}
if("comment" %in% meta_vars){
if(!"comment" %in% all.vars(meta, functions = FALSE)){
my_term = meta_terms[grepl("^com.*\\(", meta_terms)]
my_term = gsub("^[^\\(]+\\(", "I(", my_term)
meta.comment = eval(str2lang(my_term))
} else {
# Comments MUST be provided explicitly
# => we rely on the default comment then (if provided)
}
}
}
check_set_arg(meta.time, "NULL match(date, time) | logical scalar")
check_arg(meta.call, meta.sys, "NULL logical scalar")
check_arg(meta.author, "NULL logical scalar | character vector no na")
check_arg(meta.comment, "NULL character vector no na")
for(meta_value in meta_order){
if(meta_value == "time" && !is.null(meta.time)){
if(isTRUE(meta.time)) meta.time = "time"
my_date = switch(meta.time, "date" = Sys.Date(), "time" = Sys.time())
meta_txt = paste0(meta_txt, "% Created: ", my_date, "\n")
} else if(meta_value == "sys" && isTRUE(meta.sys)){
my_sys = Sys.info()[1:5]
meta_txt = paste0(meta_txt, "% System info: ",
paste0(names(my_sys), ": ", my_sys, collapse = ", "), "\n")
} else if(meta_value == "call" && isTRUE(meta.call)){
sc = sys.call(sysOrigin)
my_call = paste0("% ", deparse(sc), collapse = "\n")
meta_txt = paste0(meta_txt, "% Call:\n", my_call, "\n")
} else if(meta_value == "author" && !is.null(meta.author) && !isFALSE(meta.author)){
if(isTRUE(meta.author)){
user_all = Sys.info()[c("login", "user", "effective_user")]
author_txt = "% Created by: "
if(length(unique(user_all)) == 1){
# Windows
author_txt = paste0(author_txt, user_all[1])
} else {
# Unix
user_login = user_all["login"]
user_no_login = user_all[-1]
if(length(unique(user_all)) == 2){
if(user_login %in% user_no_login){
i = which(user_no_login == user_login)
user_login = paste0(user_login, " (login, ", names(user_no_login)[i], ")")
user_rest = paste0(user_no_login[-i], " (", names(user_no_login)[-i], ")")
} else {
user_login = paste0(user_login, " (login)")
user_rest = paste0(user_all[2], "(user, effective_user)")
}
author_txt = paste0(author_txt, user_login, ", ", user_rest)
} else {
author_txt = sma("{author_txt}{user_all[1]} (login), {user_all[2]} (user), ",
"{user_all[3]} (effective_user)")
}
}
} else {
author_txt = meta.author
if(length(author_txt) > 1){
author_txt = paste0(author_txt, collapse = "\n% ")
}
if(!grepl("^%", author_txt)){
author_txt = paste0("% Created by: ", author_txt)
}
}
meta_txt = paste0(meta_txt, author_txt, "\n")
} else if(meta_value == "comment" && !is.null(meta.comment)){
if(length(meta.comment) > 1){
meta.comment = paste0(meta.comment, collapse = "\n% ")
}
if(!grepl("^%", meta.comment)){
meta.comment = paste0("% ", meta.comment)
}
meta_txt = paste0(meta_txt, meta.comment, "\n")
}
}
}
# yesNo => used later when setting the fixed-effect line
yesNo = style$yesNo
# default values for dict
dict_global = getFixest_dict()
if(missing(dict) || isTRUE(dict)) {
dict = dict_global
} else if(isFALSE(dict)) {
dict = NULL
} else {
# dict changes the values set in the global dict
if(dict[1] == "reset"){
dict_global = c()
dict = dict[-1]
}
if(length(dict) > 0){
dict_global[names(dict)] = dict
}
dict = dict_global
}
# headers => must be a list
# We get the automatic headers, if split is used
AUTO_HEADERS = FALSE
i_auto_headers = 1
if(is.list(headers)){
if(length(headers) > 0){
qui = sapply(headers, function(x) identical(x, "auto"))
if(any(qui)){
i_auto_headers = which(qui)[1]
headers = headers[!qui]
AUTO_HEADERS = TRUE
}
# We expand the headers if needed
if(length(headers) > 0){
# ex: headers = list(Gender = list("M"=2, "F"=2))
if(length(headers[[1]]) == 1){
# ex: headers = list("M"=2, "F"=2)
# or list("M", "F" = 2)
if(is.numeric(headers[[1]]) || # case list("M" = 2, "F" = 3)
(!is.null(names(headers)) && nchar(names(headers)[1]) == 0 && is.character(headers[[1]]))){ # case list("M", "F" = 2)
headers = list(headers)
}
}
for(i in seq_along(headers)){
# expand_list_vector: list("A"=2, "B") => c("A", "A", "B")
headers[[i]] = expand_list_vector(headers[[i]])
}
# We ensure headers have names
if(is.null(names(headers))){
names(headers) = character(length(headers))
}
}
}
} else if(anyNA(headers)){
headers = list()
} else {
# It's a character vector
if(identical(headers, "auto")){
headers = list()
AUTO_HEADERS = TRUE
} else {
# we need names
headers = list(headers)
names(headers) = ""
}
}
# formatting the names of the models
dots_names = names(dots_call)
if(!is.null(dots_names)){
for(i in 1:length(dots_call)){
if(dots_names[i] != ""){
dots_call[[i]] = dots_names[i]
} else {
dots_call[[i]] = deparse_long(dots_call[[i]])
}
}
}
n_dots = length(dots)
if(n_dots == 0) stop_up("Not any estimation as argument.")
all_models = list()
model_names = list()
auto_headers = list()
model_id = NULL
k = 1
for(i in 1:n_dots){
di = dots[[i]]
if("fixest" %in% class(di)){
all_models[[k]] = di
if(any(class(dots_call[[i]]) %in% c("call", "name"))){
model_names[[k]] = deparse_long(dots_call[[i]])
} else {
model_names[[k]] = as.character(dots_call[[i]])
}
k = k + 1
} else if(any(c("list", "fixest_list", "fixest_multi") %in% class(di))){
# we get into this list to get the fixest objects
types = sapply(di, function(x) class(x)[1])
qui = which(types %in% c("fixest", "fixest_multi"))
is_multi = inherits(di, "fixest_multi")
for(m in qui){
mod = di[[m]]
# handling names
if(is_multi){
if(any(class(dots_call[[i]]) %in% c("call", "name"))){
mod_name = deparse_long(dots_call[[i]])
} else {
mod_name = as.character(dots_call[[i]])
}
mod_name = paste0(mod_name, ".", m)
} else {
if(n_dots > 1){
if(is.null(names(di)[m]) || names(di)[m] == ""){
mod_name = paste0(dots_call[[i]], "[[", m, "]]")
} else {
mod_name = paste0(dots_call[[i]], "$", names(di)[m])
}
} else {
mod_name = as.character(names(di)[m])
}
}
if(inherits(mod, "fixest_multi")){
for(j in seq_along(mod)){
all_models[[k]] = mod[[j]]
model_names[[k]] = paste0(mod_name, ".", j)
k = k + 1
}
} else {
# regular fixest or from fixest_list
all_models[[k]] = mod
model_names[[k]] = mod_name
id = di[[m]]$model_id
if(!is.null(id)){
model_id[k] = id
}
k = k + 1
}
}
}
}
if(length(all_models) == 0) stop_up("Not any 'fixest' model as argument!")
n_models = length(all_models)
if(AUTO_HEADERS){
# SAMPLE (ie split)
sample_info = lapply(all_models, function(x) x$model_info$sample)
sample_info_ok = which(lengths(sample_info) > 0)
for(i in sample_info_ok){
my_headers = list()
si = sample_info[[i]]
if(tex == FALSE){
# We need to rename to avoid FE problem duplicate row names
my_headers$title = paste0("Sample (", dict_apply(si$var, dict), ")")
} else {
my_headers$title = dict_apply(si$var, dict)
}
my_headers$value = si$value
my_headers$index = i
auto_headers[[length(auto_headers) + 1]] = my_headers
}
}
IS_MULTI_VCOV = FALSE
IS_EACH = FALSE
if(!missnull(vcov) && identical(class(vcov), "list") && length(vcov) > 1){
IS_MULTI_VCOV = TRUE
vcov_1 = vcov[[1]]
is_rep = identical(vcov_1, "times") || identical(vcov_1, "each")
if(is_rep || n_models == 1){
if(is_rep){
vcov[[1]] = NULL
}
n_vcov = length(vcov)
if(n_vcov == 0){
stop_up("The argument 'vcov' is not valid.")
}
n_total = n_models * n_vcov
if(vcov_1 == "times"){
id_mod = rep(1:n_models, n_vcov)
id_vcov = rep(1:n_vcov, each = n_models)
} else {
IS_EACH = TRUE
id_mod = rep(1:n_models, each = n_vcov)
id_vcov = rep(1:n_vcov, n_models)
}
mega_model_names = vector("list", n_total)
mega_models = vector("list", n_total)
mega_vcov = vector("list", n_total)
for(i in 1:n_total){
m_name = model_names[[id_mod[i]]]
if(length(m_name) == 0 || m_name == ""){
mega_model_names[[i]] = paste0("model ", id_mod[i], ".", id_vcov[i])
} else {
mega_model_names[[i]] = m_name
}
mega_models[[i]] = all_models[[id_mod[i]]]
mega_vcov[[i]] = vcov[[id_vcov[i]]]
}
if(length(auto_headers) > 0){
# I need to find out the new indexes... pain in the neck....
if(vcov_1 == "times"){
for(i in seq_along(auto_headers)){
index = auto_headers[[i]]
n_index = length(index)
auto_headers[[i]] = rep(index, n_vcov) + n_models * rep(0:(n_vcov-1), each = n_index)
}
} else {
for(i in seq_along(auto_headers)){
index = auto_headers[[i]]
new_index = (index - 1) * n_vcov + 1
n_index = length(index)
auto_headers[[i]] = rep(new_index, each = n_vcov) + rep(1:n_vcov, n_index)
}
}
}
model_names = mega_model_names
all_models = mega_models
n_models = length(all_models)
vcov = mega_vcov
} else {
check_value(vcov, "list len(value)", .value = n_models,
.message = "If 'vcov' is a list, it must be of the same length as the number of models, or you should add the 'each' or 'times' keyword as the first element of the list.")
}
}
auto_headers_clean = list()
if(length(auto_headers) > 0){
# We reconstruct the headers properly
for(i in seq_along(auto_headers)){
my_sub = auto_headers[[i]]
if(my_sub$title %in% names(auto_headers_clean)){
value = auto_headers_clean[[my_sub$title]]
} else {
value = character(n_models)
}
value[my_sub$index] = my_sub$value
auto_headers_clean[[my_sub$title]] = value
}
}
# Formatting the names (if needed)
alternative_names = paste0("model ", 1:n_models)
who2replace = sapply(model_names, function(x) length(x) == 0 || x == "")
model_names[who2replace] = alternative_names[who2replace]
if(length(model_id) == n_models){
index = rep(0, max(model_id))
sub_index = rep(1, n_models)
for(i in 1:n_models){
id = model_id[i]
index[id] = index[id] + 1
sub_index[i] = index[id]
}
model_names = paste0("model ", model_id, ".", sub_index)
}
#
# ... summary ####
#
check_arg(stage, "integer vector no na len(,2) GE{1} LE{2}")
if(is_function_in_it(vcov)){
# finding the name
sysOrigin = sys.parent(.up)
mc = match.call(definition = sys.function(sysOrigin), call = sys.call(sysOrigin), expand.dots = FALSE)
# must be either in .vcov (backward comp) or in vcov
if(".vcov" %in% names(mc)){
vcov_name = deparse_long(mc$.vcov)
} else {
vcov_name = deparse_long(mc$vcov)
}
if(missnull(.vcov_args)) .vcov_args = list()
check_set_arg(.vcov_args, "list",
.message = "The argument '.vcov_args' must be a list of arguments to be passed to the function in '.vcov'.")
}
# If vcov is provided, we use summary
# if is_mult, we'll have to unroll the results
check_mult = any(qui_iv <- sapply(all_models, function(x) isTRUE(x$iv)))
is_mult = FALSE
if(check_mult){
stage = unique(stage)
# we check if we'll have multiple results
if(length(stage) > 1){
# Multiple for sure
is_mult = TRUE
} else if(1 %in% stage && any(sapply(all_models[qui_iv],
function(x) length(x$iv_endo_names) > 1))){
# Means we have multiple end, so svl first stages
is_mult = TRUE
}
}
# The following two objects are only used if is_mult
all_models_bis = list()
iv_times = c()
# we'll use iv headers only if length(stage) > 1
iv_sub = c()
for(m in 1:n_models){
if(useSummary){
if(is_function_in_it(vcov)){
x = summary(all_models[[m]], vcov = vcov, stage = stage, .vcov_args = .vcov_args, vcov_name = vcov_name, agg = agg)
} else {
if(IS_MULTI_VCOV){
x = summary(all_models[[m]], vcov = vcov[[m]], ssc = ssc, stage = stage, agg = agg)
} else {
x = summary(all_models[[m]], vcov = vcov, ssc = ssc, stage = stage, agg = agg)
}
}
} else {
# What do we do if vcov not provided?
# we apply summary only to the ones that are not summaries
x = all_models[[m]]
if(!isTRUE(x$summary)){
# not a summary => we apply summary to trigger default behavior
x = summary(x, ssc = ssc)
}
}
if(is_mult){
if("fixest_multi" %in% class(x)){
for(i in seq_along(x)){
all_models_bis[[length(all_models_bis) + 1]] = x[[i]]
}
iv_times[m] = length(x)
} else {
all_models_bis[[length(all_models_bis) + 1]] = x
iv_times[m] = 1
}
} else {
all_models[[m]] = x
}
}
if(is_mult){
# We've got multiple estimations from summary => we expand the headers
all_models = all_models_bis
model_names = rep(model_names, iv_times)
i_max = rep(iv_times, iv_times)
suffix = paste0(".", unlist(lapply(iv_times, seq)))
suffix[i_max == 1] = ""
model_names = paste0(model_names, suffix)
if(length(auto_headers) > 0){
# We expand the headers
for(i in seq_along(auto_headers_clean)){
auto_headers_clean[[i]] = rep(auto_headers_clean[[i]], iv_times)
}
}
if(AUTO_HEADERS){
my_stages = sapply(all_models, function(x) ifelse(is.null(x$iv_stage), 3, x$iv_stage))
if(length(unique(my_stages)) > 1){
dict_stage = c("First", "Second", " ")
auto_headers_clean[["IV stages"]] = dict_stage[my_stages]
}
}
n_models = length(all_models)
}
if(length(auto_headers_clean) > 0){
# We place the auto headers in the right location
auto_headers_format = list()
for(i in seq_along(auto_headers_clean)){
my_title = names(auto_headers_clean)[i]
auto_headers_format[[dict_apply(my_title, dict)]] = dict_apply(auto_headers_clean[[i]], dict)
}
headers = insert(headers, auto_headers_format, i_auto_headers)
}
# if there are headers
if(length(headers) == 0){
isHeaders = FALSE
} else {
n_all = lengths(headers)
if(any(n_models %% n_all != 0)){
i_pblm = which(n_models %% n_all != 0)[1]
info = if(length(n_all) == 1) "" else paste0(" (", n_th(i_pblm), " header)")
stop_up("If argument 'headers' is provided, its elements must be of the same length as, or a divisor of, the number of models. Current lengths: ", n_all[i_pblm], " vs ", n_models, " models", info, ".")
}
for(i in which(n_all != n_models)){
if(IS_EACH){
headers[[i]] = rep(headers[[i]], each = n_models/n_all[i])
} else {
headers[[i]] = rep(headers[[i]], n_models/n_all[i])
}
}
isHeaders = TRUE
if(isTex){
h_names = names(headers)
for(i in seq_along(headers)){
h_i = h_names[i]
if(grepl(":tex:", h_i)){
# no escaping
h_i = sub(":tex:", "", h_i, fixed = TRUE)
} else {
h_i = escape_latex(h_i)
h_i = gsub("^\\\\(\\^|\\_)", "\\1", h_i)
h_i = gsub(":\\_:", ":_:", h_i, fixed = TRUE)
headers[[i]] = escape_latex(headers[[i]])
}
h_names[i] = h_i
}
names(headers) = h_names
} else {
# we clean the possible Latex markup
h_names = gsub(":_:|:tex:", "", names(headers))
names(headers) = h_names
}
}
#
# We check the group and extralines arguments
#
if(missing(drop)) drop = NULL
for(i in seq_along(group)){
check_value(group[[i]], "character vector", .message = "The elements of argument 'group' must be character vectors of regular expressions.")
drop = unique(c(drop, group[[i]]))
}
#
# ... extralines ####
#
if(inherits(extralines, "formula")){
# If a formula => to summon registered stats
extralines = extralines_extractor(extralines, tex = isTex)
} else if(!is.list(extralines)){
# => vector
extralines = list(extralines)
} else if(length(extralines) > 0 && all(lengths(extralines) == 1) &&
!inherits(extralines[[1]], "formula") && !is.function(extralines[[1]])){
# list("A" = 2, "B")
extralines = list(extralines)
}
el_new = list() # I need it to cope with list(~f+ivf+macro, "my vars" = TRUE)
# => the first command will create several lines
el_names = names(extralines)
if(is.null(el_names)) el_names = character(length(extralines))
el_names = uniquify_names(el_names)
for(i in seq_along(extralines)){
check_value(extralines[[i]], "vector | function | os formula | list",
.message = sma("The elements of argument 'extralines' must be vectors of ",
"length {n_models}, logical scalars, functions, one-sided formulas,",
" or lists."),
.value = n_models)
el = extralines[[i]]
if("formula" %in% class(el)){
el_tmp = extralines_extractor(el, el_names[i], tex = isTex)
for(k in seq_along(el_tmp)){
el_new[[names(el_tmp)[k]]] = el_tmp[[k]]
}
} else {
if(!is.function(el)){
if(is.list(el)){
el = expand_list_vector(el)
}
if(length(el) < n_models){
# we extend
n_el = length(el)
if(n_models %% n_el != 0){
stop_up("In 'extralines', the number of elements in the {nth ? i} line ",
"is not a divisor of the number of models ({n_el} vs {n_models} models).")
}
if(IS_EACH){
el = rep(el, each = n_models/n_el)
} else {
el = rep(el, n_models/n_el)
}
}
}
el_new[[el_names[i]]] = el
}
}
extralines = el_new
# Now we catch the functions + normalization of the names
el_fun_id = NULL
if(length(extralines) > 0){
el_fun_id = which(sapply(extralines, is.function)) # set of ID such that el[id] is a function
if(length(el_fun_id) > 0){
el_origin = extralines
}
if(length(unique(names(extralines))) != length(extralines)){
new_names = uniquify_names(names(extralines))
names(extralines) = new_names
}
}
# end: extralines
# we keep track of the SEs
se_type_list = list()
check_interaction_reorder = FALSE
var_list = var_reorder_list = coef_list = coef_below = sd_below = list()
depvar_list = obs_list = fitstat_list = list()
r2_list = aic_list = bic_list = loglik_list = convergence_list = list()
sqCor_list = family_list = list()
# To take care of factors
fe_names = c()
is_fe = vector(mode = "list", n_models)
nb_fe = vector(mode = "list", n_models) # the number of items per factor
slope_names = c()
slope_flag_list = vector(mode = "list", n_models)
if(!is.null(dict) && isTex){
dict = escape_latex(dict)
}
#
# ... fitstat ####
#
if("fitstat" %in% names(opts)){
fitstat_all = opts$fitstat
}
if(missnull(fitstat_all)){
# => do default
fitstat_all = "."
} else if(isFALSE(fitstat_all) || (length(fitstat_all) == 1 &&
(is.na(fitstat_all) || fitstat_all == ""))){
fitstat_all = NULL
drop.section = c(drop.section, "stats")
} else if("formula" %in% class(fitstat_all)){
check_arg(fitstat_all, "os formula",
.message = "Argument 'fitstat' must be a one sided formula (or a character vector) containing valid types from the function fitstat (see details in ?fitstat).")
fitstat_all = .xpd(fitstat_all, frame = parent.frame(.up))
fitstat_all = gsub(" ", "", strsplit(deparse_long(fitstat_all[[2]]), "+", fixed = TRUE)[[1]])
fitstat_all = unique(fitstat_all)
} else {
check_arg(fitstat_all, "character vector no na",
.message = "Argument 'fitstat' must be a one sided formula (or a character vector) containing valid types from the function fitstat (see details in ?fitstat).")
}
if("." %in% fitstat_all){
# Default values:
# - if all OLS: typical R2
# - if any non-OLS: pseudo R2 + squared cor.
is_ols = sapply(all_models, function(x) x$method_type == "feols")
if(all(is_ols)){
if(any(sapply(all_models, function(x) "fixef_vars" %in% names(x)))){
# means any FE model
fitstat_default = c("r2", "wr2")
} else {
fitstat_default = c("r2", "ar2")
}
} else {
fitstat_default = c("cor2", "pr2", "bic")
}
fitstat_default = c("n", fitstat_default)
if(any(sapply(all_models, function(x) !is.null(x$theta)))){
fitstat_default = c(fitstat_default, "theta")
}
fitstat_default = setdiff(fitstat_default, fitstat_all)
if(length(fitstat_default) > 0){
i = which(fitstat_all == ".")[1]
if(i == length(fitstat_all)){
fitstat_all = c(fitstat_all[0:(i-1)], fitstat_default)
} else {
fitstat_all = c(fitstat_all[0:(i-1)], fitstat_default, fitstat_all[(i+1):length(fitstat_all)])
}
}
}
fitstat_all = tolower(fitstat_all)
# checking the types
fitstat_fun_types = fitstat(give_types = TRUE)
fitstat_type_allowed = fitstat_fun_types$types
fitstat_all = unique(fitstat_all)
type_alias = fitstat_fun_types$type_alias
if(any(fitstat_all %in% names(type_alias))){
i = intersect(fitstat_all, names(type_alias))
fitstat_all[fitstat_all %in% i] = type_alias[fitstat_all[fitstat_all %in% i]]
}
pblm = setdiff(fitstat_all, fitstat_type_allowed)
if(length(pblm) > 0){
stop_up("Argument `fitstat` must be a one sided formula (or a character vector) ",
"containing valid types from the function fitstat ",
"(see details in `?fitstat` or use `fitstat(show_types = TRUE)`). ",
"The type{$s, enum.q, is ? pblm} not valid.")
}
if(isTex){
fitstat_dict = fitstat_fun_types$tex_alias
} else {
fitstat_dict = fitstat_fun_types$R_alias
}
# Renaming the stats with user-provided aliases
if(length(dict) > 0){
user_stat_name = intersect(names(fitstat_dict), names(dict))
if(length(user_stat_name) > 0){
fitstat_dict[user_stat_name] = dict[user_stat_name]
}
# Now with the aliases
user_stat_name = intersect(names(type_alias), names(dict))
if(length(user_stat_name) > 0){
fitstat_dict[type_alias[user_stat_name]] = dict[user_stat_name]
}
}
# end: fitstat
#
# ... Loop ####
#
reformat_fitstat = FALSE
for(m in 1:n_models){
x = all_models[[m]]
if(isTRUE(x$onlyFixef)){
se_type_list[[m]] = "NONE_FIXEF_ONLY"
} else {
se_type_list[[m]] = attr(x$se, "type")
}
# family
family = x$family
if(x$method %in% c("femlm", "feNmlm")){
fam = switch(family, poisson = "Poisson", negbin = "Neg. Bin.",
gaussian = "Gaussian", logit = "Logit")
} else if(x$method %in% c("feglm", "feglm.fit")){
if(family$family == "poisson" && family$link == "log"){
fam = "Poisson"
} else if(family$family == "binomial" && family$link == "logit"){
fam = "Logit"
} else if(family$family == "binomial" && family$link == "probit"){
fam = "Probit"
} else {
# we try to give the greatest details ()
fam = paste0(family$family, '("', family$link, '")')
}
} else if(x$method %in% c("feols", "feols.fit")){
fam = "OLS"
} else if(x$method == "fepois"){
fam = "Poisson"
} else if(x$method == "fenegbin"){
fam = "Neg. Bin."
}
family_list[[m]] = fam
# variable dependante:
depvar = gsub(" ", "", as.character(x$fml)[[2]])
a = x$coeftable
if(is.null(a)){
a = data.frame(coef = NA_real_, se = NA_real_, tstat = NA_real_,
pvalue = NA_real_, row.names = "TO_BE_REMOVED")
} else if(!is.data.frame(a)){
class(a) = NULL
a = as.data.frame(a)
}
# We drop the .theta coefficient
if(x$method == "fenegbin" || (x$method %in% c("femlm", "feNmlm") && family == "negbin")){
quiTheta = rownames(a) == ".theta"
a = a[!quiTheta, ]
}
#
# START: Formatting of the factors / FEs / Slopes
#
# on enleve les facteurs des variables a garder
if(!keepFactors){
fact = rownames(a)
qui_drop = grepl("factor(", fact, fixed = TRUE)
a = a[!qui_drop, , FALSE]
b = fact[qui_drop]
c = sapply(b, function(x) strsplit(x, "factor(", fixed=TRUE)[[1]][2])
d = sapply(c, function(x) strsplit(x, ")", fixed=TRUE)[[1]][1])
factor_var = unique(d)
# Now the number of items per factor
if(length(factor_var) == 0){
nbItems = character(0)
} else {
nbItems = addCommas(sapply(factor_var, function(x) 1+sum(grepl(x, b))))
}
} else {
factor_var = c()
nbItems = character(0)
}
#
# Fixed-effects
#
if(!is.null(x$fixef_terms)){
terms_full = extract_fe_slope(x$fixef_terms)
fixef_vars = terms_full$fixef_vars
factor_var = c(factor_var, fixef_vars, recursive=TRUE)
new_items = addCommas(as.vector(x$fixef_sizes[fixef_vars]))
names(new_items) = fixef_vars
nbItems = c(nbItems, new_items)
} else if(!is.null(x$fixef_vars)){
factor_var = c(factor_var, x$fixef_vars, recursive=TRUE)
new_items = addCommas(as.vector(x$fixef_sizes))
names(new_items) = names(x$fixef_sizes)
nbItems = c(nbItems, new_items)
}
nb_fe[[m]] = nbItems
# Formatting
lFactor = rep(yesNo[1], length(factor_var))
names(lFactor) = factor_var
is_fe[[m]] = lFactor
fe_names = unique(c(fe_names, factor_var, recursive=TRUE))
#
# SLOPES
#
if(!is.null(x$fixef_terms)){
terms_full = extract_fe_slope(x$fixef_terms)
slope_fe = terms_full$slope_fe
slope_vars = terms_full$slope_vars
# we change the names right away
slope_fe_name = slope_fe
slope_vars_name = slope_vars
if(!is.null(dict)){
qui = which(slope_fe %in% names(dict))
if(length(qui) > 0){
slope_fe_name[qui] = dict[slope_fe[qui]]
}
qui = which(slope_vars %in% names(dict))
if(length(qui) > 0){
slope_vars_name[qui] = dict[slope_vars[qui]]
}
}
if(nchar(style$slopes.format) > 0){
slope_format = style$slopes.format
slope_var_full = c()
for(i in seq_along(slope_vars_name)){
slope_var_full[i] = gsub("__slope__", slope_fe_name[i],
gsub("__var__", slope_vars_name[i],
slope_format, fixed = TRUE),
fixed = TRUE)
}
} else {
slope_var_full = paste0(slope_vars_name, " (", slope_fe_name, ")")
}
} else {
slope_var_full = c()
}
slope_flag = rep(yesNo[1], length(slope_var_full))
names(slope_flag) = slope_var_full
slope_flag_list[[m]] = slope_flag
slope_names = unique(c(slope_names, slope_var_full, recursive = TRUE))
#
# END: FE/slope formatting
#
#
# Now we rename the variables
#
# on enleve les espaces dans les noms de variables
var = var_origin = c(gsub(" ", "", row.names(a)))
# renaming
if(TRUE){
# Now I clean white spaces in dict_apply
qui = gsub(" ", "", var, fixed = TRUE) %in% gsub(" ", "", names(dict), fixed = TRUE)
var = dict_apply(var, dict)
tv = table(var)
if(any(tv > 1)){
value_pblm = names(tv)[tv > 1][1]
var_pblm = c(gsub(" ", "", row.names(a)))[var == value_pblm]
stop_up("Problematic value for argument 'dict': The variables {enum.q ? var_pblm}",
" have all the same alias ({q?value_pblm}) in the same estimation. ",
"This is not supported, please provide a separate alias for each.")
}
# if there are still interactions, we rename them
new_var = var
var_left = var[!qui]
if(length(var_left) > 0 && any(grepl(":|\\*", var_left))){
check_interaction_reorder = TRUE
qui_inter = grepl("(?<=[^:\\*])(::|:|\\*)(?=[^:\\*])", var_left, perl = TRUE)
inter = strsplit(var_left[qui_inter], "(?<=[^:\\*])(:|\\*)(?=[^:\\*])", perl = TRUE)
fun_rename = function(x){
# We put the factors on the right
qui_factor = grepl("::", x)
if(any(qui_factor)){
res = x
x_split = strsplit(res, "::")
# There may be svl factors
for(i in which(qui_factor)){
if(res[i] %in% names(dict)){
res[i] = dict[res[i]]
} else {
value_split = dict_apply(x_split[[i]], dict)
res[i] = paste(value_split, collapse = i.equal)
}
}
# We put the factors on the right
res = res[base::order(qui_factor)]
} else {
res = x
}
res = dict_apply(res, dict)
res = order_apply(res, interaction.order)
res = paste0(res, collapse = interaction.combine)
return(res)
}
inter_named = sapply(inter, fun_rename)
new_inter = sapply(inter, function(x) fun_rename(sort(x)))
var[!qui][qui_inter] = inter_named
new_var[!qui][qui_inter] = new_inter
}
# IV: fit_
if(any(qui_iv <- grepl("^fit_", var_left))){
# IVs
iv_vars = gsub("^fit_", "", var_left[qui_iv])
if(fit_format == "__var__"){
iv_vars = dict_apply(iv_vars, dict)
} else {
for(i in seq_along(iv_vars)){
iv_vars = gsub("__var__", dict_apply(iv_vars[i], dict), fit_format)
}
}
var[!qui][qui_iv] = new_var[!qui][qui_iv] = iv_vars
}
# We take care of poly
# DEPRECATED: this should not be done!!!!!!!!!!!
# Poly is no regular polynomial but orthogonal polynomial!!!!!
# This will imply difference in estimates with raw powers, we should not conflate them
# We take care of raw powers
qui_pow = grepl("^I\\([^\\^]+\\^[[:digit:]]+\\)$", var_left)
if(any(qui_pow)){
# We clean only I(var^d)
pow_var = gsub("^I\\(|\\^.+$", "", var_left[qui_pow])
pow_digit = as.numeric(gsub(".+\\^|\\)", "", var_left[qui_pow]))
pow_digit_clean = poly_dict[pow_digit]
if(isTex){
pow_digit_clean[is.na(pow_digit_clean)] = paste0("$^{", pow_digit[is.na(pow_digit_clean)], "}")
} else {
pow_digit_clean[is.na(pow_digit_clean)] = paste0(" ^ ", pow_digit[is.na(pow_digit_clean)])
}
pow_named = paste0(dict_apply(pow_var, dict), pow_digit_clean)
var[!qui][qui_pow] = new_var[!qui][qui_pow] = pow_named
}
# poly(xx, d)p => poly(xx)p
if(any(grepl("^poly\\(", var_left))){
qui_poly = grepl("^poly\\([^,]+,[[:digit:]]\\)", var_left)
if(any(qui_poly)){
poly_new = gsub("^(poly\\([^,]+),[[:digit:]]\\)", "\\1)", var_left[qui_poly])
var[!qui][qui_poly] = new_var[!qui][qui_poly] = poly_new
}
# poly(var) => poly(var)1
qui_poly_clean = grepl("^poly\\([^,]+\\)$", var[!qui])
if(any(qui_poly_clean)){
poly_new = gsub("^(poly\\([^,]+\\))$", "\\11", var[!qui][qui_poly_clean])
var[!qui][qui_poly_clean] = new_var[!qui][qui_poly_clean] = poly_new
}
}
names(new_var) = names(var) = var_origin
var_reorder_list[[m]] = new_var
} else {
# We reorder the interaction terms alphabetically
new_var = var
qui = grepl("(?<=[^:]):(?=[^:])", new_var, perl = TRUE)
if(any(qui)){
check_interaction_reorder = TRUE
inter = strsplit(new_var[qui], "(?<=[^:]):(?=[^:])", perl = TRUE)
new_inter = sapply(inter, function(x) paste0(sort(x), collapse = ":"))
new_var[qui] = new_inter
}
names(new_var) = names(var) = var_origin
var_reorder_list[[m]] = new_var
}
coef = fun_format(a[, 1])
if(coefstat == "se"){
se_value = fun_format(a[, 2])
} else if(coefstat == "tstat"){
se_value = fun_format(a[, 3])
} else if(coefstat == "confint"){
se_value = apply(confint(x, level = ci), 1, function(z) paste0("[", fun_format(z[1]), "; ", fun_format(z[2]), "]"))
} else {
stop_up("Wrong value for the argument 'coefstat': ", coefstat, " is not supported.")
}
if(add_signif){
if(isTex){
pval = cut(a[, 4], breaks = c(-1, signif.code, 100), labels = c(tex_star(names(signif.code)), ""))
} else {
pval = cut(a[, 4], breaks = c(-1, signif.code, 100), labels = c(names(signif.code), ""))
}
pval[is.na(pval)] = ""
} else {
pval = rep("", nrow(a))
}
# If the coefficient is bounded, we suppress the 'stars'
isBounded = grepl("bounded", se_value)
if(any(isBounded)){
pval[isBounded] = ""
}
if(coefstat != "confint"){
structured_coef = c(paste0(coef, pval, " (", se_value, ")"))
} else {
structured_coef = c(paste0(coef, pval, " ", se_value))
}
# saving the infos
var_list[[m]] = var
names(structured_coef) = var
coef_list[[m]] = structured_coef
if(se.below){
cb = c(paste0(coef, pval))
if(coefstat != "confint"){
sb = c(paste0("(", se_value, ")"))
} else {
sb = se_value
}
names(cb) = names(sb) = var
coef_below[[m]] = cb
sd_below[[m]] = sb
}
# La depvar
depvar_list[[m]] = depvar
#
# Fit statistics
#
n = nobs(x)
obs_list[[m]] = n
convergence_list[[m]] = ifelse(is.null(x$convStatus), TRUE, x$convStatus)
K = x$nparams
if(length(fitstat_all) == 0){
fitstat_list[[m]] = NA
} else {
my_stats = lapply(fitstat(x, fitstat_all, etable = TRUE, verbose = FALSE), fun_format_stats)
if(any(grepl("::", names(my_stats), fixed = TRUE))){
reformat_fitstat = TRUE
}
fitstat_list[[m]] = my_stats
}
#
# extralines, when function
#
for(i in el_fun_id){
f = el_origin[[i]]
if(m == 1){
extralines[[i]] = f(x)
} else {
extralines[[i]] = c(extralines[[i]], f(x))
}
}
}
if(check_interaction_reorder){
if(length(unique(unlist(var_reorder_list))) < length(unique(unlist(var_list)))){
var_list = var_reorder_list
for(m in 1:length(var_list)){
names(coef_list[[m]]) = var_list[[m]]
if(se.below){
names(coef_below[[m]]) = var_list[[m]]
names(sd_below[[m]]) = var_list[[m]]
}
}
}
}
#
# Fitstat reformating
#
if(length(fitstat_all) > 0){
if(reformat_fitstat){
# PAIN IN THE NECK!
all_names = unique(unlist(lapply(fitstat_list, names)))
all_names_new = c()
for(v in fitstat_all){
all_names_new = c(all_names_new, all_names[gsub("::.+", "", all_names) == v])
}
fitstat_list_new = list()
for(i in seq_along(fitstat_list)){
my_list = fitstat_list[[i]]
fitstat_list_new[[i]] = lapply(all_names_new, function(x) ifelse(is.null(my_list[[x]]), NA, my_list[[x]]))
}
# Now the aliases
fitstat_dict_new = c()
fun_rename_stats = function(x, isTex) {
if(!grepl("::", x, fixed = TRUE)) return(fitstat_dict[x])
xx = strsplit(x, "::")[[1]]
var = dict_apply(xx[2], dict)
if(isTex){
var = escape_latex(var)
}
res = paste0(fitstat_dict[xx[1]], ", ", var)
res
}
for(v in all_names_new){
fitstat_dict_new[v] = fun_rename_stats(v, isTex)
}
fitstat_list = fitstat_list_new
attr(fitstat_list, "format_names") = fitstat_dict_new
} else {
attr(fitstat_list, "format_names") = fitstat_dict[fitstat_all]
}
}
if(isTex){
if(missnull(title)){
title = "no title"
} else {
title = escape_latex(title, makecell = FALSE)
}
} else {
if(missnull(title)){
title = NULL
}
}
if((missnull(convergence) && any(convergence_list == FALSE)) || (!missnull(convergence) && convergence)){
convergence = TRUE
} else {
convergence = FALSE
}
if(isTRUE(show_family) || (is.null(show_family) && length(unique(family_list)) > 1)){
family = TRUE
} else {
family = FALSE
}
if((is.null(show_depvar) && length(unique(unlist(depvar_list))) > 1) || isTRUE(show_depvar)){
depvar = TRUE
} else {
depvar = FALSE
}
if(IS_FIXEF_GROUP && length(fe_names) > 0){
# Table of presence of fixed-effects
is_fe_mat = c()
for(i in 1:length(is_fe)){
# The order of the FEs needs not be the same since is_FE is
# sequentially constructed (hence the following construct)
is_fe_mat = cbind(is_fe_mat, 1 * (!is.na(is_fe[[i]][fe_names])))
}
# row => FEs
# column => models
if(isTRUE(fixef.group) && length(fe_names) > 1){
fe_groups = list()
n_fe = nrow(is_fe_mat)
g = 1 # => group
i_left = 1:n_fe
while(length(i_left) > 1){
i = i_left[1]
i_left = i_left[-1]
any_done = FALSE
for(j in i_left){
# we check if same pattern
if(all(is_fe_mat[i, ] == is_fe_mat[j, ])){
any_done = TRUE
if(length(fe_groups) < g){
# we initialize it
fe_groups[[g]] = c(i, j)
} else {
# we add j
fe_groups[[g]] = c(fe_groups[[g]], j)
}
i_left = setdiff(i_left, j)
}
}
if(any_done){
g = g + 1
}
}
# We simply group the fixed-effects + we apply dict
fe_names_origin = fe_names
for(my_group in fe_groups){
# We drop the old
group_fe = fe_names_origin[my_group]
fe_names = setdiff(fe_names, group_fe)
# and add the new
group_fe_new = rename_fe(group_fe, dict)
new_fe = enumerate_items(group_fe_new)
fe_names = c(new_fe, fe_names)
# Now we update is_fe
for(i in seq_along(is_fe)){
current_fe = is_fe[[i]]
if(all(group_fe %in% names(current_fe))){
current_fe[new_fe] = yesNo[1]
is_fe[[i]] = current_fe
}
}
}
} else {
# Custom grouping from the user
# Behavior:
# The values in the row:
# - if all the row's FEs are in the model => TRUE
# - if some of the row's FEs are in the model => partial
# - if none is in the model => FALSE
#
# The regular fixed-effects:
# - FEs of a group are removed if they are either always fully present or fully absent
# - if an FE from a group is partially present at least once
# * it will stay as regular FE
# * this avoids messing up from the user side
#
fe_names_full = rename_fe(fe_names, dict)
names(fe_names_full) = fe_names
is_inconsistent = rep(FALSE, length(fixef.group))
fixef.extralines = list()
fe2remove = c()
for(i in seq_along(fixef.group)){
my_group = keep_apply(fe_names_full, fixef.group[[i]])
if(length(my_group) == 0){
# not any FE found, even partial => we skip the row
# we skip the row
next
}
my_group_origin = names(my_group)
is_there = c()
for(m in 1:length(is_fe)){
# We check full and partial presence
fe_model = names(is_fe[[m]])
if(all(my_group_origin %in% fe_model)){
is_there[m] = TRUE
} else if(any(my_group_origin %in% fe_model)){
# partial presence
is_there[m] = NA
is_inconsistent[i] = TRUE
} else {
is_there[m] = FALSE
}
}
ok_removal = !any(is.na(is_there))
if(ok_removal){
fe2remove = c(fe2remove, my_group_origin)
}
is_there = yesNo[2 - is_there]
is_there[is.na(is_there)] = "partial"
# Now we create the extra line
el_name = names(fixef.group)[i]
if(grepl("^(\\^|_|-)", el_name)){
# => the user specifies the placement => replaces the default
} else {
# we add the default placement
el_name = paste0("-^", el_name)
}
fixef.extralines[[el_name]] = is_there
}
if(length(fixef.extralines) > 0){
# We add it to extra lines
extralines[names(fixef.extralines)] = fixef.extralines
}
if(length(fe2remove) > 0){
fe_names = setdiff(fe_names, fe2remove)
}
# Warning for inconsistencies
if(any(is_inconsistent)){
i = which(is_inconsistent)[1]
if(length(is_inconsistent) == 1){
msg = "the group leads to an inconsistent row (defined by "
} else if(all(is_inconsistent)){
msg = "all groups lead to inconsistent rows (e.g. the one defined by "
} else {
msg = sma("some groups lead to inconsistent rows (the {nth?i} one defined by ")
}
warn_up("In 'fixef.group', ", msg, deparse_long(fixef.group[[i]]),
").\nTo create inconsistent rows: use drop.section = 'fixef' combined with the arghument 'extralines'.")
}
}
}
res = list(se_type_list = se_type_list, var_list = var_list, coef_list = coef_list,
coef_below = coef_below, se.row = se.row, sd_below = sd_below,
depvar_list = depvar_list,
obs_list = obs_list, convergence_list = convergence_list, fe_names = fe_names,
is_fe = is_fe, nb_fe = nb_fe, slope_flag_list = slope_flag_list,
slope_names = slope_names, useSummary = useSummary, model_names = model_names,
family_list = family_list, fitstat_list = fitstat_list, headers = headers,
isHeaders = isHeaders, title = title, convergence = convergence, family = family,
keep = keep, drop = drop, order = order, file = file, label = label,
se.below = se.below,
signif.code = signif.code, fixef_sizes = fixef_sizes,
fixef_sizes.simplify = fixef_sizes.simplify,
depvar = depvar, useSummary = useSummary, dict = dict, yesNo = yesNo,
add_signif = add_signif,
float = float, coefstat = coefstat, ci = ci, style = style,
notes = notes, group = group,
extralines = extralines, placement = placement, drop.section = drop.section,
tex_tag = tex_tag, fun_format = fun_format, coef.just = coef.just, meta = meta_txt,
tpt = tpt, arraystretch = arraystretch, adjustbox = adjustbox, fontsize = fontsize,
tabular = tabular, highlight = highlight, coef.style = coef.style,
caption.number = caption.number)
return(res)
}
etable_internal_latex = function(info){
# Internal function to display the latex table
n_models = length(info$depvar_list)
# Getting the information
se_type_list = info$se_type_list
var_list = info$var_list
coef_list = info$coef_list
coef_below = info$coef_below
sd_below = info$sd_below
depvar_list = info$depvar_list
depvar = info$depvar # flag for including the depvar
obs_list = info$obs_list
convergence_list = info$convergence_list
fe_names = info$fe_names
is_fe = info$is_fe
nb_fe = info$nb_fe
slope_names = info$slope_names
slope_flag_list = info$slope_flag_list
family_list = info$family_list
fitstat_list = info$fitstat_list
headers = info$headers
isHeaders = info$isHeaders
title = info$title
label = info$label
keep = info$keep
drop = info$drop
order = info$order
file = info$file
family = info$family
convergence = info$convergence
se.below = info$se.below
signif.code = info$signif.code
fixef_sizes = info$fixef_sizes
fixef_sizes.simplify = info$fixef_sizes.simplify
dict = info$dict
yesNo = info$yesNo
add_signif = info$add_signif
float = info$float
coefstat = info$coefstat
ci = info$ci
style = info$style
notes = info$notes
group = info$group
extralines = info$extralines
placement = info$placement
drop.section = info$drop.section
tex_tag = info$tex_tag
fun_format = info$fun_format
meta = info$meta
se.row = info$se.row
tpt = info$tpt
arraystretch = info$arraystretch
adjustbox = info$adjustbox
fontsize = info$fontsize
tabular = info$tabular
highlight = info$highlight
coef.style = info$coef.style
caption.number = info$caption.number
# top line
style$line.top = switch(style$line.top,
"simple" = "\\toprule",
"double" = "\\tabularnewline \\midrule \\midrule",
style$line.top)
if(nchar(style$line.top) > 1){
style$line.top = paste0(style$line.top, "\n")
}
# bottom line
if(style$tablefoot){
style$line.bottom = switch(style$line.bottom,
"simple" = "\\midrule",
"double" = "\\midrule \\midrule",
style$line.bottom)
} else {
style$line.bottom = switch(style$line.bottom,
"simple" = "\\bottomrule",
"double" = "\\midrule \\midrule & \\tabularnewline",
style$line.bottom)
}
if(nchar(style$line.bottom) > 1){
style$line.bottom = paste0(style$line.bottom, "\n")
}
#
# prompting the infos gathered
#
# Starting the table
myTitle = title
if(!is.null(label)){
myTitle = paste0("\\label{", label, "} ", myTitle)
}
caption = ""
info_center = "\\centering\n"
if(float){
if(nchar(placement) > 0){
placement = paste0("[", placement, "]")
}
table_begin = paste0("\\begin{table}", placement, "\n")
caption = paste0("\\caption{", myTitle, "}\n")
if(nchar(style$caption.after) > 0){
caption = paste0(caption, style$caption.after, "\n")
}
if(!caption.number){
table_begin = paste0("\\renewcommand{\\thetable}{}\n", table_begin)
}
table_end = "\\end{table}"
} else {
table_begin = "\\begingroup\n"
table_end = "\\par\\endgroup\n"
}
info_width = ""
if(!is.null(style$rules_width) && !all(is.na(style$rules_width))){
w = style$rules_width
info_width = c()
if(!is.na(w[1])){
info_width = paste0("\\setlength\\heavyrulewidth{", w[1], "}\n")
}
if(!is.na(w[2])){
info_width = c(info_width, paste0("\\setlength\\lightrulewidth{", w[2], "}\n"))
}
}
space = if(style$no_border) "@{}" else ""
if(tabular == "normal"){
tabular_begin = paste0("\\begin{tabular}{", space, "l",
strrep("c", n_models),
space, "}\n", style$line.top)
tabular_end = "\\end{tabular}\n"
} else if(tabular == "*"){
tabular_begin = paste0("\\begin{tabular*}{\\textwidth}{@{\\extracolsep{\\fill}}",
space, "l", strrep("c", n_models),
space, "}\n", style$line.top)
tabular_end = "\\end{tabular*}\n"
} else if(tabular == "X"){
all_cols = .dsb("l *.[n_models]{>{\\centering\\arraybackslash}X}")
tabular_begin = paste0("\\begin{tabularx}{\\textwidth}{",
space, all_cols, space, "}\n", style$line.top)
tabular_end = "\\end{tabularx}\n"
}
# 1st lines => dep vars
depvar_list = dict_apply(c(depvar_list, recursive = TRUE), dict)
depvar_list = escape_latex(depvar_list)
# We write the dependent variables properly, with multicolumn when necessary
# to do that, we count the number of occurrences of each variable (& we respect the order provided by the user)
nb_multi = 1
names_multi = depvar_list[1]
if(n_models > 1){
k = 1
old_dep = depvar_list[1]
for(i in 2:length(depvar_list)){
if(depvar_list[i] == old_dep){
nb_multi[k] = nb_multi[k] + 1
} else {
k = k + 1
nb_multi[k] = 1
names_multi[k] = old_dep = depvar_list[i]
}
}
}
if(style$depvar.style != ""){
if(style$depvar.style == "*"){
names_multi = paste0("\\textit{", names_multi, "}")
} else if(style$depvar.style == "**"){
names_multi = paste0("\\textbf{", names_multi, "}")
} else {
names_multi = paste0("\\textbf{\\textit{", names_multi, "}}")
}
}
# now the proper format
first_line = style$depvar.title
if(length(nb_multi) == 1){
first_line = gsub("(s)", "", first_line, fixed = TRUE)
} else {
first_line = gsub("(s)", "s", first_line, fixed = TRUE)
}
for(i in 1:length(nb_multi)){
if(nb_multi[i] == 1){
# no multi column
first_line = paste0(first_line, " & ", names_multi[i])
} else {
first_line = paste0(first_line, " & \\multicolumn{", nb_multi[i], "}{c}{", names_multi[i], "}")
}
}
first_line = paste0(first_line, "\\\\\n")
if(!depvar) first_line = NULL
# Model line
if(nchar(style$model.format) > 0){
m = style$model.format
if(grepl("1", m, fixed = TRUE)){
info_split = strsplit(m, "1", fixed = TRUE)[[1]]
m_info = 1:n_models
} else if(grepl("i", m, fixed = TRUE)){
info_split = strsplit(m, "i", fixed = TRUE)[[1]]
unit = "i.ii.iii.iv.v.vi.vii.viii.ix.x.xi.xii.xiii.xiv.xv.xvi.xvii.xviii.xix.xx"
m_info = strsplit(unit, ".", fixed = TRUE)[[1]][1:n_models]
} else if(grepl("I", m, fixed = TRUE)){
info_split = strsplit(m, "I", fixed = TRUE)[[1]]
unit = "I.II.III.IV.V.VI.VII.VIII.IX.X.XI.XII.XIII.XIV.XV.XVI.XVII.XVIII.XIX.XX"
m_info = strsplit(unit, ".", fixed = TRUE)[[1]][1:n_models]
} else if(grepl("a", m, fixed = TRUE)){
info_split = strsplit(m, "a", fixed = TRUE)[[1]]
m_info = letters[1:n_models]
} else if(grepl("A", m, fixed = TRUE)){
info_split = strsplit(m, "A", fixed = TRUE)[[1]]
m_info = LETTERS[1:n_models]
}
right = ifelse(length(info_split) > 1, info_split[2], "")
model_format = paste0(info_split[1], m_info, right)
} else {
model_format = paste0("(", 1:n_models, ")")
}
model_line = paste0(style$model.title, " & ", paste0(model_format, collapse = " & "), "\\\\\n")
#
# the coefficients
#
# we need to loop not to lose names
all_vars = c()
for(vars in var_list){
# setdiff drops names
all_vars = c(all_vars, vars[!vars %in% all_vars])
}
# dealing with the special case of only-fixef estimations
all_vars = all_vars[all_vars != "TO_BE_REMOVED"]
if(length(all_vars) == 0){
drop.section = c(drop.section, "coef")
}
for(i in seq_along(group)){
gi = group[[i]]
present = sapply(var_list, function(x) any(keep_apply(x, gi, TRUE)))
group[[i]] = present
}
# keeping some coefs
all_vars = keep_apply(all_vars, keep)
# dropping some coefs
all_vars = drop_apply(all_vars, drop)
# ordering the coefs
all_vars = order_apply(all_vars, order)
if(length(all_vars) == 0){
if((!is.null(keep) || !is.null(drop)) && length(group) == 0){
if(!is.null(keep) && !any(grepl("^%", keep))){
msg = paste0(" In particular, to 'keep' variables using their original names ",
"(before dict is applied), use the special character '%' first. ",
"E.g. keep = \"%", keep[1], "\"")
} else if(!is.null(drop) && !any(grepl("^%", drop))){
msg = paste0(" In particular, to 'drop' variables using their original names ",
"(before dict is applied), use the special character '%' first. ",
"E.g. drop = \"%", drop[1], "\"")
} else {
msg = ""
}
stop_up("Not any variable was selected, please reframe your keep/drop arguments.",
msg, "\n To drop the coefficients section, use the argument drop.section = 'coef'.")
}
# => we drop the coef section
drop.section = c(drop.section, "coef")
}
if("coef" %in% drop.section){
# => we drop the coef section
coef_title = NULL
coef_lines = NULL
info_tikz = NULL
} else {
# a simple line with only "variables" written in the first cell
if(nchar(style$var.title) == 0){
coef_title = ""
} else if(style$var.title == "\\midrule"){
coef_title = "\\midrule "
} else {
coef_title = paste0(style$var.title, "\\\\\n")
}
# we have coefficients to display
# The names are set in results2formattedList
coef_names = escape_latex(all_vars)
names(coef_names) = all_vars
if(se.below){
coef_mat = c()
for(v in all_vars){
myCoef = mySd = myLine = c()
for(m in 1:n_models){
myCoef = c(myCoef, coef_below[[m]][v])
mySd = c(mySd, sd_below[[m]][v])
}
myCoef[is.na(myCoef)] = " "
mySd[is.na(mySd)] = " "
coef_mat = rbind(coef_mat, c(coef_names[v], myCoef), c(" ", mySd))
}
} else {
coef_mat = all_vars
for(m in 1:n_models){
coef_mat = cbind(coef_mat, coef_list[[m]][all_vars])
}
coef_mat[is.na(coef_mat)] = " "
coef_mat[, 1] = coef_names
}
coef_mat = style_apply(coef.style, coef_mat, all_vars)
info_mat = highlight_apply(highlight, coef_mat, all_vars)
coef_mat = info_mat$coef_mat
info_tikz = info_mat$preamble
coef_lines = paste0(apply(coef_mat, 1, paste, collapse = " & "), "\\\\ \n")
coef_lines = paste0(coef_lines, collapse = "")
}
#
# Fixed-effects (if needed)
#
nb_FE_lines = ""
if(length(fe_names) > 0 && !"fixef" %in% drop.section){
if(nchar(style$fixef.title) == 0){
fixef_title = ""
} else if(style$fixef.title == "\\midrule"){
fixef_title = "\\midrule "
} else {
fixef_title = paste0(style$fixef.title, "\\\\\n")
}
# The number of FEs
for(m in 1:n_models) {
quoi = is_fe[[m]][fe_names]
quoi[is.na(quoi)] = yesNo[2]
is_fe[[m]] = quoi
# We do the same for the number of items
quoi = nb_fe[[m]][fe_names]
quoi[is.na(quoi)] = "--"
nb_fe[[m]] = quoi
}
all_fe = matrix(c(is_fe, recursive = TRUE), nrow = length(fe_names))
# We change the names of the FEs
fe_names = rename_fe(fe_names, dict)
fe_names_raw = escape_latex(fe_names)
fe_names = paste0(style$fixef.prefix, fe_names_raw, style$fixef.suffix)
# The fixed-effects sizes
if(fixef_sizes){
# For the number of items
all_nb_FEs = matrix(c(nb_fe, recursive=TRUE), nrow = length(fe_names))
is_complex = rep(TRUE, nrow(all_nb_FEs))
if(fixef_sizes.simplify){
check_simplified = function(x) length(unique(x[grepl("[[:digit:]]", x)])) == 1
is_simple = apply(all_nb_FEs, 1, check_simplified)
for(i in which(is_simple)){
x = all_nb_FEs[i, ]
nb = x[grepl("[[:digit:]]", x)][1]
fe_names[i] = paste0(fe_names[i], " (", nb, ")")
}
is_complex = !is_simple
}
if(any(is_complex)){
fe_names_nbItems = paste0(style$fixef_sizes.prefix, fe_names_raw[is_complex],
style$fixef_sizes.suffix)
all_nb_FEs = cbind(fe_names_nbItems, all_nb_FEs[is_complex, , drop = FALSE])
nb_FE_lines = paste0(paste0(apply(all_nb_FEs, 1, paste0, collapse = " & "),
collapse="\\\\\n"),
"\\\\\n")
}
}
all_fe = cbind(fe_names, all_fe)
FE_lines = paste0(paste0(apply(all_fe, 1, paste0, collapse = " & "),
collapse="\\\\\n"),
"\\\\\n")
} else {
FE_lines = NULL
fixef_title = NULL
}
#
# Slopes (if needed)
#
if(length(slope_names) > 0 && !"slopes" %in% drop.section){
if(nchar(style$slopes.title) == 0){
slope_intro = ""
} else if(style$slopes.title == "\\midrule"){
slope_intro = "\\midrule "
} else {
slope_intro = paste0(style$slopes.title, "\\\\\n")
}
# reformat the yes/no slope
for(m in 1:n_models) {
quoi = slope_flag_list[[m]][slope_names]
quoi[is.na(quoi)] = yesNo[2]
slope_flag_list[[m]] = quoi
}
# Changing the slope names
qui = slope_names %in% names(dict)
who = slope_names[qui]
slope_names[qui] = dict[who]
slope_names = escape_latex(slope_names)
# Matrix with yes/no information
all_slopes = matrix(c(slope_flag_list, recursive = TRUE), nrow = length(slope_names))
all_slopes = cbind(slope_names, all_slopes)
slope_lines = paste0(paste0(apply(all_slopes, 1, paste0, collapse = " & "),
collapse = "\\\\\n"),
"\\\\\n")
} else {
slope_intro = NULL
slope_lines = NULL
}
# Headers
if(isHeaders){
n_head = length(headers)
h_names = names(headers)
headers_top = c()
headers_mid = c()
headers_bottom = c()
for(i in 1:n_head){
h_i = h_names[i]
add_rule = FALSE
if(grepl(":_:", h_i)){
add_rule = TRUE
h_i = gsub(":_:", "", h_i, fixed = TRUE)
}
h_placement = substr(h_i, 1, 1)
if(h_placement %in% c("^", "-", "_")){
h_i = substr(h_i, 2, nchar(h_i))
} else {
h_placement = "-"
}
h_value = paste0(h_i, " & ", tex_multicol(headers[[i]], add_rule = add_rule), "\n")
if(h_placement == "^") {
headers_top[length(headers_top) + 1] = h_value
} else if(h_placement == "-"){
headers_mid[length(headers_mid) + 1] = h_value
} else {
headers_bottom[length(headers_bottom) + 1] = h_value
}
}
headers_top = paste(headers_top, collapse = "")
headers_mid = paste(headers_mid, collapse = "")
headers_bottom = paste(headers_bottom, collapse = "")
} else {
headers_top = ""
headers_mid = ""
headers_bottom = ""
}
# Convergence information
info_convergence = ""
if(convergence){
info_convergence = paste0("Convergence &", paste(convergence_list, collapse = " & "), "\\\\\n")
}
# information on family
if(family){
info_family = paste0(" & ", paste(family_list, collapse = " & "), "\\\\\n")
} else {
info_family = ""
}
# The standard errors => if tablefoot = TRUE
# We go through this in order to get the multi se if needed
isUniqueSD = length(unique(unlist(se_type_list))) == 1
nb_col = length(obs_list) + 1
foot_intro = paste0("\\multicolumn{", nb_col, "}{l}{\\emph{")
if(is.null(se.row)){
# we show the SE row if:
# - NO FOOTER & MULTIPLE SEs
se.row = isFALSE(style$tablefoot) && !isUniqueSD
}
# Automatic footer information
info_SE_footer = ""
if(style$tablefoot){
bottom_line = style$line.bottom
style$line.bottom = ""
if("coef" %in% drop.section){
info_SE_footer = paste0(bottom_line, " \\\\\n")
} else if(identical(style$tablefoot.value, "default")){
if(isUniqueSD){
my_se = unique(unlist(se_type_list)) # it comes from summary
# every model has the same type of SE
# Now we modify the names of the clusters if needed
my_se = format_se_type_latex(my_se, dict)
if(coefstat == "se"){
coefstat_sentence = " standard-errors in parentheses"
} else if(coefstat == "tstat"){
coefstat_sentence = " co-variance matrix, t-stats in parentheses"
} else {
coefstat_sentence = paste0(" co-variance matrix, ", round(ci*100),
"\\% confidence intervals in brackets")
}
info_SE_footer = paste0(bottom_line, foot_intro, my_se,
coefstat_sentence, "}}\\\\\n")
if(add_signif){
info_SE_footer = paste0(info_SE_footer, foot_intro, "Signif. Codes: ",
paste(names(signif.code), signif.code, sep=": ",
collapse = ", "),
"}}\\\\\n")
}
} else {
# Multiple types of SEs: we're more general
all_se_type = sapply(se_type_list, format_se_type_latex, dict = dict, inline = TRUE)
if(coefstat == "se"){
coefstat_sentence = "Standard-Errors"
} else {
coefstat_sentence = "Co-variance"
}
info_muli_se = paste0(coefstat_sentence, " & ",
paste(all_se_type, collapse = " & "), "\\\\\n")
if(add_signif){
info_SE_footer = paste0(bottom_line, foot_intro, "Signif. Codes: ",
paste(names(signif.code), signif.code,
sep = ": ", collapse = ", "),
"}}\\\\\n")
} else {
myAmpLine = paste0(paste0(rep(" ", length(depvar_list) + 1),
collapse = " & "), "\\tabularnewline\n")
info_SE_footer = paste0(bottom_line, myAmpLine, "\\\\\n")
}
}
} else {
value = style$tablefoot.value
if(isUniqueSD){
my_se = unique(unlist(se_type_list))
my_se = format_se_type_latex(my_se, dict)
value = gsub("__se_type__", my_se, value)
} else {
# not super elegant, but can't to much more // else: enumerate the SEs?
value = gsub("__se_type__", "", value)
}
info_SE_footer = paste0(bottom_line, paste(foot_intro, value, "}}\\\\\n", collapse = ""))
}
}
info_muli_se = ""
if(se.row){
all_se_type = sapply(se_type_list, format_se_type_latex, dict = dict, inline = TRUE)
if(coefstat == "se"){
coefstat_sentence = "Standard-Errors"
} else {
coefstat_sentence = "Co-variance"
}
info_muli_se = paste0(coefstat_sentence, " & ", tex_multicol(all_se_type), "\n")
}
#
# Fit statistics
#
if(!"stats" %in% drop.section){
if(nchar(style$stats.title) == 0){
stat_title = ""
} else if(style$stats.title == "\\midrule"){
stat_title = "\\midrule "
} else {
stat_title = paste0(style$stats.title, "\\\\\n")
}
stat_lines = paste0(nb_FE_lines, info_convergence, info_muli_se)
if(!all(sapply(fitstat_list, function(x) all(is.na(x))))){
fit_names = attr(fitstat_list, "format_names")
nb = length(fit_names)
for(fit_id in 1:nb){
fit = sapply(fitstat_list, function(x) x[[fit_id]])
if(all(is.na(fit))) next
fit[is.na(fit)] = ""
stat_lines = paste0(stat_lines, fit_names[fit_id], " & ",
paste0(fit, collapse = " & "), "\\\\\n")
}
}
} else {
stat_title = stat_lines = ""
}
# Notes
info_notes = ""
if(length(notes) > 0){
notes_intro = if(tpt) style$notes.tpt.intro else style$notes.intro
if(grepl("^@", notes[1])){
notes_intro = paste0(gsub("^@", "", notes[1]), " ")
notes = notes[-1]
}
if(length(notes) > 0){
notes = dict_apply(notes, dict)
notes = escape_latex(notes, makecell = FALSE)
if(tpt){
if(nchar(trimws(notes_intro)) > 0){
notes_intro = paste0(trimws(notes_intro), "\n")
}
notes = paste0("\\item ", notes, collapse = "\n")
# The note intro is placed right after the } so that you can pass options
# like [flushleft]
info_notes = paste0("\n\\begin{tablenotes}", notes_intro, notes,
"\n\\end{tablenotes}\n")
} else {
info_notes = paste0("\n", notes_intro, paste0(notes, collapse = "\\\\\n"), "\n")
}
}
}
#
# Group
#
create.fixef_title = FALSE
for(i in seq_along(group)){
gi = group[[i]]
gi_format = yesNo[2 - gi]
gi_name = names(group)[i]
gi_full = ""
gi_where = "coef"
gi_top = FALSE
if(grepl("^(\\^|_|-)", gi_name)){
# first character: section: ^|-|_
# second character: the location: ^|_
# sec: section
sec = substr(gi_name, 1, 1)
gi_name = substr(gi_name, 2, nchar(gi_name))
if(grepl("^(\\^|_)", gi_name)){
row = substr(gi_name, 1, 1)
gi_name = substr(gi_name, 2, nchar(gi_name))
gi_top = row == "^"
}
gi_where = switch(sec, "^" = "coef", "-" = "fixef", "_" = "stat")
}
gi_full = paste0(gi_full, gi_name, " & ", paste0(gi_format, collapse = " & "), "\\\\\n")
if(gi_top){
if(gi_where == "coef"){
coef_lines = c(gi_full, coef_lines)
} else if(gi_where == "fixef"){
create.fixef_title = TRUE
FE_lines = c(gi_full, FE_lines)
} else {
stat_lines = c(gi_full, stat_lines)
}
} else {
if(gi_where == "coef"){
coef_lines = c(coef_lines, gi_full)
} else if(gi_where == "fixef"){
create.fixef_title = TRUE
FE_lines = c(FE_lines, gi_full)
} else {
stat_lines = c(stat_lines, gi_full)
}
}
}
#
# Extra lines
#
for(i in seq_along(extralines)){
el = extralines[[i]]
# The format depends on the type
if(is.logical(el)){
if(length(el) == 1){
el_format = rep(yesNo[2 - el], n_models)
} else {
el_format = yesNo[2 - el]
}
} else if(is.numeric(el)){
el_format = fun_format(el)
} else {
el_format = el
}
el_format[is.na(el_format)] = ""
el_name = names(extralines)[i]
el_full = ""
el_where = "coef"
el_top = FALSE
if(grepl("^(\\^|_|-)", el_name)){
# first character: section: ^|-|_
# second character: the location: ^|_
# sec: section
sec = substr(el_name, 1, 1)
el_name = substr(el_name, 2, nchar(el_name))
if(grepl("^(\\^|_)", el_name)){
row = substr(el_name, 1, 1)
el_name = substr(el_name, 2, nchar(el_name))
el_top = row == "^"
}
el_where = switch(sec, "^" = "coef", "-" = "fixef", "_" = "stat")
}
if(grepl(":tex:", el_name, fixed = TRUE)){
# No escaping
el_name = gsub(":tex:", "", el_name, fixed = TRUE)
} else {
# escaping
el_name = escape_latex(el_name)
el_format = escape_latex(el_format)
}
el_full = paste0(el_full, el_name, " & ", paste0(el_format, collapse = " & "), "\\\\\n")
if(el_top){
if(el_where == "coef"){
coef_lines = c(el_full, coef_lines)
} else if(el_where == "fixef"){
create.fixef_title = TRUE
FE_lines = c(el_full, FE_lines)
} else {
stat_lines = c(el_full, stat_lines)
}
} else {
if(el_where == "coef"){
coef_lines = c(coef_lines, el_full)
} else if(el_where == "fixef"){
create.fixef_title = TRUE
FE_lines = c(FE_lines, el_full)
} else {
stat_lines = c(stat_lines, el_full)
}
}
}
if(create.fixef_title && is.null(fixef_title) && !"fixef" %in% drop.section){
if(nchar(style$fixef.title) == 0){
fixef_title = ""
} else if(style$fixef.title == "\\midrule"){
fixef_title = "\\midrule "
} else {
fixef_title = paste0(style$fixef.title, "\\\\\n")
}
}
# Stacking var and stat
coef_stack = c(coef_title, coef_lines)
stat_stack = c(stat_title, stat_lines)
fixef_stack = c(fixef_title, FE_lines, slope_intro, slope_lines)
# Now we place the fixed-effects
if(style$fixef.where == "var"){
coef_stack = c(coef_stack, fixef_stack)
} else {
stat_stack = c(stat_stack, fixef_stack)
}
if(tex_tag){
tag_tabular_before = "%start:tab\n"
tag_tabular_end = "%end:tab\n"
} else {
tag_tabular_before = tag_tabular_end = ""
}
#
# Other info:
#
tpt_begin = tpt_end = tpt_caption = ""
if(tpt){
tpt_begin = "\\begin{threeparttable}[b]\n"
tpt_end = "\\end{threeparttable}\n"
tpt_caption = caption
caption = ""
}
info_stretch = ""
if(!is.null(arraystretch)){
info_stretch = paste0("\\renewcommand*{\\arraystretch}{", arraystretch, "}")
}
adj_box_begin = adj_box_end = ""
if(!is.null(adjustbox)){
adj_box_begin = paste0("\\begin{adjustbox}{", adjustbox, "}\n")
adj_box_end = "\\end{adjustbox}\n"
}
info_font = ""
if(!is.null(fontsize)){
info_font = paste0("\\", fontsize, "\n")
}
# meta information: has been computed in results2formattedList
res = c(meta, table_begin, info_tikz, caption, info_center, info_width, info_font, adj_box_begin,
tpt_begin, tpt_caption, info_stretch,
tag_tabular_before, tabular_begin,
headers_top, first_line, headers_mid, model_line, info_family, headers_bottom,
coef_stack, stat_stack, info_SE_footer, style$line.bottom,
tabular_end, tag_tabular_end,
info_notes, tpt_end, adj_box_end, table_end)
res = res[nchar(res) > 0]
attr(res, "n_models") = n_models
return(res)
}
etable_internal_df = function(info){
n_models = length(info$depvar_list)
# Getting the information
se_type_list = info$se_type_list
var_list = info$var_list
coef_list = info$coef_list
coef_below = info$coef_below
sd_below = info$sd_below
depvar_list = info$depvar_list
convergence_list = info$convergence_list
fe_names = info$fe_names
is_fe = info$is_fe
nb_fe = info$nb_fe
slope_names = info$slope_names
slope_flag_list = info$slope_flag_list
family_list = info$family_list
fitstat_list = info$fitstat_list
title = info$title
label = info$label
keep = info$keep
drop = info$drop
order = info$order
file = info$file
family = info$family
convergence = info$convergence
se.below = info$se.below
signif.code = info$signif.code
add_signif = info$add_signif
fixef_sizes = info$fixef_sizes
depvar = info$depvar
useSummary = info$useSummary
model_names = info$model_names
coefstat = info$coefstat
dict = info$dict
group = info$group
extralines = info$extralines
style = info$style
fun_format = info$fun_format
drop.section = info$drop.section
coef.just = info$coef.just
se.row = info$se.row
# naming differences
headers = info$headers
isHeaders = info$isHeaders
# The coefficients
depvar_list = dict_apply(unlist(depvar_list), dict)
# we need to loop not to lose names
all_vars = c()
for(vars in var_list){
# setdiff drops names
all_vars = c(all_vars, vars[!vars %in% all_vars])
}
# dealing with the special case of only-fixef estimations
all_vars = all_vars[all_vars != "TO_BE_REMOVED"]
if(length(all_vars) == 0){
drop.section = c(drop.section, "coef")
}
for(i in seq_along(group)){
gi = group[[i]]
present = sapply(var_list, function(x) any(keep_apply(x, gi, TRUE)))
group[[i]] = present
}
# keeping some coefs
all_vars = keep_apply(all_vars, keep)
# dropping some coefs
all_vars = drop_apply(all_vars, drop)
# ordering the coefs
all_vars = order_apply(all_vars, order)
if(length(all_vars) == 0){
if((!is.null(keep) || !is.null(drop)) && length(group) == 0){
if(!is.null(keep) && !any(grepl("^%", keep))){
msg = sma(" In particular, to 'keep' variables using their original names ",
"(before dict is applied), use the special character '%' first. ",
"E.g. keep = \"%", keep[1], "\"")
} else if(!is.null(drop) && !any(grepl("^%", drop))){
msg = sma(" In particular, to 'drop' variables using their original names ",
"(before dict is applied), use the special character '%' first. ",
"E.g. drop = \"%", drop[1], "\"")
} else {
msg = ""
}
stop_up("Not any variable was selected, please reframe your keep/drop arguments.", msg,
"\n To drop the coefficients section, use the argument drop.section = 'coef'.")
}
# => we drop the coef section
drop.section = c(drop.section, "coef")
}
if("coef" %in% drop.section){
res = NULL
} else {
se.below = info$se.below
if(se.below){
coef_below = info$coef_below
sd_below = info$sd_below
coef_se_mat = c()
for(v in all_vars){
myCoef = mySd= myLine = c()
for(m in 1:n_models){
myCoef = c(myCoef, coef_below[[m]][v])
mySd = c(mySd, sd_below[[m]][v])
}
myCoef[is.na(myCoef)] = " "
mySd[is.na(mySd)] = " "
coef_se_mat = rbind(coef_se_mat, myCoef, mySd)
}
n_vars = length(all_vars)
my_names = character(2 * n_vars)
my_names[1 + 2 * 0:(n_vars - 1)] = all_vars
coef_mat = cbind(my_names, coef_se_mat)
} else {
coef_mat = all_vars
for(m in 1:n_models) coef_mat = cbind(coef_mat, coef_list[[m]][all_vars])
coef_mat[is.na(coef_mat)] = " "
}
if(!is.null(coef.just)){
if(coef.just == "."){
align_fun = function(x) sfill(sfill(x, anchor = "."), right = TRUE)
} else if(coef.just == "("){
align_fun = function(x) sfill(sfill(x, anchor = "("), right = TRUE)
} else if(coef.just == "c"){
align_fun = function(x) format(x, justify = "centre")
} else if(coef.just == "l"){
align_fun = function(x) sfill(x, anchor = "left")
}
new_mat = list(coef_mat[, 1])
for(i in 2:ncol(coef_mat)){
new_mat[[i]] = align_fun(coef_mat[, i])
}
coef_mat = do.call("cbind", new_mat)
}
res = coef_mat
}
#
# Group
#
before_stat = after_stat = c()
before_fixef = after_fixef = c()
for(i in seq_along(group)){
gi = group[[i]]
gi_format = style$yesNo[2 - gi]
gi_name = names(group)[i]
gi_where = "coef"
gi_top = FALSE
if(grepl("^(\\^|_|-)", gi_name)){
# first character: section: ^|-|_
# second character: the location: ^|_
# sec: section
sec = substr(gi_name, 1, 1)
gi_name = substr(gi_name, 2, nchar(gi_name))
if(grepl("^(\\^|_)", gi_name)){
row = substr(gi_name, 1, 1)
gi_name = substr(gi_name, 2, nchar(gi_name))
gi_top = row == "^"
}
gi_where = switch(sec, "^" = "coef", "-" = "fixef", "_" = "stat")
}
my_line = c(gi_name, gi_format)
if(gi_top){
if(gi_where == "coef"){
res = rbind(my_line, res)
} else if(gi_where == "fixef"){
before_fixef = rbind(before_fixef, my_line)
} else {
before_stat = rbind(before_stat, my_line)
}
} else {
if(gi_where == "coef"){
res = rbind(res, my_line)
} else if(gi_where == "fixef"){
after_fixef = rbind(after_fixef, my_line)
} else {
after_stat = rbind(after_stat, my_line)
}
}
}
#
# Extra lines
#
for(i in seq_along(extralines)){
el = extralines[[i]]
yesNo = style$yesNo
# The format depends on the type
if(is.logical(el)){
if(length(el) == 1){
el_format = rep(yesNo[2 - el], n_models)
} else {
el_format = yesNo[2 - el]
}
} else if(is.numeric(el)){
el_format = fun_format(el)
} else {
el_format = el
}
el_format[is.na(el_format)] = ""
el_where = "coef"
el_name = names(extralines)[i]
el_top = FALSE
if(grepl("^(\\^|_|-)", el_name)){
# first character: section: ^|-|_
# second character: the location: ^|_
# sec: section
sec = substr(el_name, 1, 1)
el_name = substr(el_name, 2, nchar(el_name))
if(grepl("^(\\^|_)", el_name)){
row = substr(el_name, 1, 1)
el_name = substr(el_name, 2, nchar(el_name))
el_top = row == "^"
}
el_where = switch(sec, "^" = "coef", "-" = "fixef", "_" = "stat")
}
# we clean possible tex markup
el_name = gsub(":tex:", "", el_name, fixed = TRUE)
my_line = c(el_name, el_format)
if(el_top){
if(el_where == "coef"){
res = rbind(my_line, res)
} else if(el_where == "fixef"){
before_fixef = rbind(before_fixef, my_line)
} else {
before_stat = rbind(before_stat, my_line)
}
} else {
if(el_where == "coef"){
res = rbind(res, my_line)
} else if(el_where == "fixef"){
after_fixef = rbind(after_fixef, my_line)
} else {
after_stat = rbind(after_stat, my_line)
}
}
}
#
# Depvar
#
# The line with the dependent variable => defined here to get the width
preamble = c()
dep_width = 0
if(depvar){
preamble = rbind(c(style$depvar.title, depvar_list), preamble)
dep_width = nchar(as.vector(preamble))
}
# the headers
if(isHeaders){
# we need to provide unique names... sigh...
n_head = length(headers)
h_names = names(headers)
headers_top = c()
headers_bottom = c()
for(i in 1:n_head){
h_i = h_names[i]
h_placement = substr(h_i, 1, 1)
if(h_placement %in% c("^", "-", "_")){
h_i = substr(h_i, 2, nchar(h_i))
} else {
h_placement = "-"
}
h_value = c(sfill(h_i, i), headers[[i]])
if(h_placement %in% c("^", "-")){
headers_top = rbind(headers_top, h_value)
} else {
headers_bottom = rbind(headers_bottom, h_value)
}
}
preamble = rbind(headers_top, preamble, headers_bottom)
}
# Used to draw lines
if(is.null(res)){
longueur = pmax(dep_width, 8)
} else {
longueur = apply(res, 2, function(x) max(nchar(as.character(x))))
longueur = pmax(dep_width, longueur)
longueur = pmax(longueur, 8)
}
#
# fixed-effects
#
if(length(fe_names) > 0 && !"fixef" %in% drop.section){
for(m in 1:n_models) {
quoi = is_fe[[m]][fe_names]
quoi[is.na(quoi)] = style$yesNo[2]
is_fe[[m]] = quoi
}
fe_names = rename_fe(fe_names, dict)
if(nchar(style$fixef.prefix) > 0){
fe_names = paste0(style$fixef.prefix, fe_names)
}
if(nchar(style$fixef.suffix) > 0){
fe_names = paste0(fe_names, style$fixef.suffix)
}
all_fe = matrix(c(is_fe, recursive=TRUE), nrow = length(fe_names))
all_fe = cbind(fe_names, all_fe)
if(nchar(style$fixef.title) > 0){
myLine = paste(rep(style$fixef.line, 30), collapse = "")
res = rbind(res, c(style$fixef.title, sprintf("%.*s", longueur[-1], myLine)))
}
if(length(before_fixef) > 0){
res = rbind(res, before_fixef)
}
res = rbind(res, all_fe)
} else {
if(length(before_fixef) > 0 || length(after_fixef) > 0){
# We create the fixed-effects title if needed
if(!"fixef" %in% drop.section && nchar(style$fixef.title) > 0){
myLine = paste(rep(style$fixef.line, 30), collapse = "")
res = rbind(res, c(style$fixef.title, sprintf("%.*s", longueur[-1], myLine)))
}
}
if(length(before_fixef) > 0){
res = rbind(res, before_fixef)
}
}
if(length(after_fixef) > 0){
res = rbind(res, after_fixef)
}
#
# The slopes
#
if(length(slope_names) > 0 && !"slopes" %in% drop.section){
# reformatting the yes/no
for(m in 1:n_models) {
quoi = slope_flag_list[[m]][slope_names]
quoi[is.na(quoi)] = style$yesNo[2]
slope_flag_list[[m]] = quoi
}
all_slopes = matrix(c(slope_flag_list, recursive=TRUE), nrow = length(slope_names))
all_slopes = cbind(slope_names, all_slopes)
if(nchar(style$slopes.title) > 0){
myLine = paste(rep(style$slopes.line, 30), collapse = "")
res = rbind(res, c(style$slopes.title, sprintf("%.*s", longueur[-1], myLine)))
}
res = rbind(res, all_slopes)
}
# preamble created before because used to set the width
if(length(preamble) > 0){
if(style$headers.sep){
preamble = rbind(preamble, rep(" ", length(longueur)))
}
res = rbind(preamble, res)
}
if(nchar(style$stats.title) > 0){
if(nchar(style$stats.title) == 1){
n = max(nchar(res[, 1]), 12)
if(all(grepl("(", unlist(se_type_list), fixed = TRUE))){
n = max(n, 15)
}
fit_names = attr(fitstat_list, "format_names")
if(length(fit_names) > 0){
n = max(n, max(nchar(fit_names)))
}
myLine = paste(rep(style$stats.title, 40), collapse = "")
stats_title = sprintf("%.*s", n, myLine)
} else {
stats_title = style$stats.title
}
myLine = paste(rep(style$stats.line, 30), collapse = "")
res = rbind(res, c(stats_title, sprintf("%.*s", longueur[-1], myLine)))
}
# the line with the families
if(family){
res = rbind(res, c("Family", unlist(family_list)))
}
if(!isFALSE(se.row) && !"coef" %in% drop.section){
# default is to always show the SE row
if(coefstat == "se"){
coefstat_sentence = "S.E. type"
} else {
coefstat_sentence = "VCOV type"
}
se_type_format = c()
for(m in 1:n_models) se_type_format[m] = format_se_type(se_type_list[[m]], longueur[[1+m]], by = TRUE, dict = dict)
main_type = ""
if(all(grepl("Clustered", unlist(se_type_list), fixed = TRUE))){
main_type = ": Clustered"
coefstat_sentence = gsub(" type", "", coefstat_sentence)
}
res = rbind(res, c(paste0(coefstat_sentence, main_type), c(se_type_format, recursive = TRUE)))
}
# convergence status
if(convergence){
res = rbind(res, c("Convergence", c(convergence_list, recursive = TRUE)))
}
#
# Fit statistics
#
if(length(before_stat) > 0){
res = rbind(res, before_stat)
}
if(!"stats" %in% drop.section && !all(sapply(fitstat_list, function(x) all(is.na(x))))){
fit_names = attr(fitstat_list, "format_names")
nb = length(fit_names)
for(fit_id in 1:nb){
fit = sapply(fitstat_list, function(x) x[[fit_id]])
if(all(is.na(fit))) next
fit[is.na(fit)] = "--"
res = rbind(res, c(fit_names[fit_id], fit))
}
}
if(length(after_stat) > 0){
res = rbind(res, after_stat)
}
# if titles
modelNames = model_names
# we shorten the model names to fit the width
for(m in 1:n_models){
modelNames[m] = charShorten(modelNames[[m]], longueur[[1+m]], keep.digits = TRUE)
}
# I need to do that otherwise as.data.frame goes wild
res_list = list()
for(i in 1:ncol(res)){
res_list[[i]] = unlist(res[, i])
}
names(res_list) = paste0("x", 1:length(res_list))
res = as.data.frame(res_list)
names(res) = c("variables", modelNames)
tvar = table(res$variables)
if(any(tvar > 1)){
qui = which(res$variables %in% names(tvar)[tvar > 1])
add_space = c("", " ")
if(length(qui) > 2){
for(i in 3:length(qui)){
add_space[i] = paste(rep(" ", i), collapse = "")
}
}
res$variables = as.character(res$variables)
res$variables[qui] = paste0(res$variables[qui], add_space)
}
names(res)[1] = ""
# We rename theta when NB is used
quiTheta = which(row.names(res) == ".theta")
row.names(res)[quiTheta] = "Over-dispersion"
class(res) = c("etable_df", "data.frame")
if(add_signif){
attr(res, "signif") = signif.code
}
return(res)
}
####
#### User-level ####
####
#' @rdname etable
setFixest_etable = function(digits = 4, digits.stats = 5, fitstat,
coefstat = c("se", "tstat", "confint"),
ci = 0.95, se.below = TRUE, keep, drop, order, dict,
float,
fixef_sizes = FALSE, fixef_sizes.simplify = TRUE,
family, powerBelow = -5,
interaction.order = NULL, depvar, style.tex = NULL,
style.df = NULL, notes = NULL, group = NULL, extralines = NULL,
fixef.group = NULL, placement = "htbp", drop.section = NULL,
view = FALSE, markdown = NULL, view.cache = FALSE,
page.width = "fit",
postprocess.tex = NULL, postprocess.df = NULL,
fit_format = "__var__", meta.time = NULL,
meta.author = NULL, meta.sys = NULL,
meta.call = NULL, meta.comment = NULL,
reset = FALSE, save = FALSE){
#
# Argument checking => strong since these will become default values
#
check_set_digits(digits)
check_set_digits(digits.stats)
# fitstat (chiant) => controle reporte a fitstat_validate
if(!missing(fitstat)){
fitstat = fitstat_validate(fitstat)
}
check_set_arg(coefstat, "match")
check_arg(ci, "numeric scalar GT{0.5} LT{1}")
check_arg("logical scalar",
se.below, fixef_sizes, fixef_sizes.simplify,
float, family, depvar, reset, view)
check_arg(keep, drop, order, "character vector no na NULL",
.message = "The arg. '__ARG__' must be a vector of regular expressions (see help(regex)).")
check_arg(interaction.order, "NULL character scalar")
check_arg(notes, "character vector no na")
check_arg(powerBelow, "integer scalar LE{-1}")
check_arg(dict, "NULL logical scalar | named character vector no na")
check_set_arg(group, extralines, "NULL{list()} named list l0")
check_set_arg(fixef.group, "NULL{list()} logical scalar | named list l0")
check_arg(placement, "character scalar")
if(!missing(placement)){
if(nchar(placement) == 0) stop("Argument 'placement' cannot be the empty string.")
p_split = strsplit(placement, "")[[1]]
check_value(p_split, "strict multi charin(h, t, b, p, H, !)",
.message = "Argument 'placement' must be a character string containing only the following characters: 'h', 't', 'b', 'p', 'H', and '!'.")
}
check_set_arg(drop.section, "NULL multi match(fixef, slopes, stats)")
check_arg(style.tex, "NULL class(fixest_style_tex)")
check_arg(postprocess.tex, postprocess.df, "NULL function arg(1,)")
check_arg(fit_format, "character scalar")
if(!grepl("__var__", fit_format, fixed = TRUE)){
stop("The argument 'fit_format' should include the special name '__var__' that will be replaced by the variable name. So far it does not contain it.")
}
# meta
check_set_arg(meta.time, "NULL match(date, time) | logical scalar")
check_arg(meta.call, meta.sys, "NULL logical scalar")
check_arg(meta.author, "NULL logical scalar | character vector no na")
check_arg(meta.comment, "NULL character vector no na")
check_arg(view, view.cache, "logical scalar")
check_arg(markdown, "NULL scalar(logical, character)")
page.width = check_set_page_width(page.width)
#
# Setting the defaults
#
# Getting the existing defaults
opts = getOption("fixest_etable")
if(is.null(opts)){
# We first look at the "root" default
root_default = renvir_get("fixest_etable")
if(is.null(root_default)){
opts = list()
} else {
opts = root_default
}
} else if(!is.list(opts)){
warning("Wrong formatting of option 'fixest_etable', all options are reset.")
opts = list()
} else if(reset){
opts = list()
}
# Style setting
if(length(style.tex) > 0){
# We ensure we always have ALL components provided
if(length(opts$style.tex) == 0){
basic_style = fixest::style.tex(main = "base")
} else {
basic_style = opts$style.tex
}
basic_style[names(style.tex)] = style.tex
style.tex = basic_style
}
check_arg(style.df, "NULL class(fixest_style_df)")
if(length(style.df) > 0){
# We ensure we always have ALL components provided
if(length(opts$style.df) == 0){
basic_style = fixest::style.df(default = TRUE)
} else {
basic_style = opts$style.df
}
basic_style[names(style.df)] = style.df
style.df = basic_style
}
# Saving the default values
mc = match.call()
args_default = setdiff(names(mc)[-1], c("reset", "save"))
# NOTA: we don't allow delayed evaluation => all arguments must have hard values
for(v in args_default){
opts[[v]] = eval(as.name(v))
}
options(fixest_etable = opts)
# Saving at the project level if needed
check_set_arg(save, "logical scalar | match(reset)")
if(isTRUE(save)){
renvir_update("fixest_etable", opts)
} else if(identical(save, "reset")){
renvir_update("fixest_etable", NULL)
}
}
#' @rdname etable
getFixest_etable = function(){
opts = getOption("fixest_etable")
if(!is.list(opts)){
warning("Wrong formatting of option 'fixest_etable', all options are reset.")
opts = list()
options(fixest_etable = opts)
}
opts
}
#' Style definitions for Latex tables
#'
#' This function describes the style of Latex tables to be exported with the function [`etable`].
#'
#' @inheritParams etable
#'
#' @param main Either "base", "aer" or "qje". Defines the basic style to start from. The styles
#' "aer" and "qje" are almost identical and only differ on the top/bottom lines.
#' @param depvar.title A character scalar. The title of the line of the dependent variables
#' (defaults to `"Dependent variable(s):"` if `main = "base"` (the 's' appears only if just one
#' variable) and to `""` if `main = "aer"`).
#' @param model.title A character scalar. The title of the line of the models (defaults to
#' `"Model:"` if `main = "base"` and to `""` if `main = "aer"`).
#' @param model.format A character scalar. The value to appear on top of each column. It defaults
#' to `"(1)"`. Note that 1, i, I, a and A are special characters: if found, their values will be
#' automatically incremented across columns.
#' @param line.top A character scalar equal to `"simple"`, `"double"`, or anything else. The line
#' at the top of the table (defaults to `"double"` if `main = "base"` and to `"simple"` if
#' `main = "aer"`). `"simple"` is equivalent to `"\\toprule"`, and `"double"` to
#' `"\\tabularnewline \\midrule \\midrule"`.
#' @param line.bottom A character scalar equal to `"simple"`, `"double"`, or anything else. The
#' line at the bottom of the table (defaults to `"double"` if `main = "base"` and to `"simple"`
#' if `main = "aer"`). `"simple"` is equivalent to `"\\bottomrule"`, and `"double"` to
#' `"\\midrule \\midrule & \\tabularnewline"`.
#' @param var.title A character scalar. The title line appearing before the variables (defaults to
#' `"\\midrule \\emph{Variables}"` if `main = "base"` and to `"\\midrule"` if `main = "aer"`).
#' Note that the behavior of `var.title = " "` (a space) is different from `var.title = ""`
#' (the empty string): in the first case you will get an empty row, while in the second case
#' you get no empty row. To get a line without an empty row, use `"\\midrule"` (and not
#' `"\\midrule "`!--the space!).
#' @param fixef.title A character scalar. The title line appearing before the fixed-effects
#' (defaults to `"\\midrule \\emph{Fixed-effects}"` if `main = "base"` and to `" "` if
#' `main = "aer"`). Note that the behavior of `fixef.title = " "` (a space) is different from
#' `fixef.title = ""` (the empty string): in the first case you will get an empty row, while in the
#' second case you get no empty row. To get a line without an empty row, use `"\\midrule"`
#' (and not `"\\midrule "`!--the space!).
#' @param fixef.prefix A prefix to add to the fixed-effects names. Defaults to `""`
#' (i.e. no prefix).
#' @param fixef.suffix A suffix to add to the fixed-effects names. Defaults to `""` if
#' `main = "base"`) and to `"fixed-effects"` if `main = "aer"`).
#' @param fixef.where Either "var" or "stats". Where to place the fixed-effects lines?
#' Defaults to `"var"`, i.e. just after the variables, if `main = "base"`) and to
#' `"stats"`, i.e. just after the statistics, if `main = "aer"`).
#' @param slopes.title A character scalar. The title line appearing before the variables with
#' varying slopes (defaults to `"\\midrule \\emph{Varying Slopes}"` if `main = "base"`
#' and to `""` if `main = "aer"`). Note that the behavior of `slopes.title = " "` (a space)
#' is different from `slopes.title = ""` (the empty string): in the first case you will get
#' an empty row, while in the second case you get no empty row. To get a line without an
#' empty row, use `"\\midrule"` (and not `"\\midrule "`!--the space!).
#' @param slopes.format Character scalar representing the format of the slope variable name.
#' There are two special characters: "__var__" and "__slope__", placeholers for the variable
#' and slope names. Defaults to `"__var__ (__slope__)"` if `main = "base"`) and
#' to `"__var__ $\\times $ __slope__"` if `main = "aer"`).
#' @param fixef_sizes.prefix A prefix to add to the fixed-effects names. Defaults to `"# "`.
#' @param fixef_sizes.suffix A suffix to add to the fixed-effects names. Defaults
#' to `""` (i.e. no suffix).
#' @param stats.title A character scalar. The title line appearing before the statistics
#' (defaults to `"\\midrule \\emph{Fit statistics"}` if `main = "base"` and to `" "`
#' if `main = "aer"`). Note that the behavior of `stats.title = " "` (a space) is different
#' from `stats.title = ""` (the empty string): in the first case you will get an empty row,
#' while in the second case you get no empty row. To get a line without an empty row,
#' use `"\\midrule"` (and not `"\\midrule "`!--the space!).
#' @param notes.intro A character scalar. Some tex code appearing just before the notes,
#' defaults to `"\\par \\raggedright \n"`.
#' @param tablefoot A logical scalar. Whether or not to display a footer within the table.
#' Defaults to `TRUE` if `main = "base"`) and `FALSE` if `main = "aer"`).
#' @param tablefoot.value A character scalar. The notes to be displayed in the footer.
#' Defaults to `"default"` if `main = "base"`, which leads to custom footers informing on
#' the type of standard-error and significance codes, depending on the estimations.
#' @param yesNo A character vector of length 1 or 2. Defaults to `"Yes"` if `main = "base"`
#' and to `"$\\checkmark$"` if `main = "aer"` (from package `amssymb`). This is the message
#' displayed when a given fixed-effect is (or is not) included in a regression.
#' If `yesNo` is of length 1, then the second element is the empty string.
#' @param interaction.combine Character scalar, defaults to `" $\\times$ "`. When the estimation
#' contains interactions, then the variables names (after aliasing) are combined with this
#' argument. For example: if `dict = c(x1="Wind", x2="Rain")` and you have the following
#' interaction `x1:x2`, then it will be renamed (by default) `Wind $\\times$ Rain` -- using
#' `interaction.combine = "*"` would lead to `Wind*Rain`.
#' @param i.equal Character scalar, defaults to `" $=$ "`. Only affects factor variables created
#' with the function [`i`], tells how the variable should be linked to its value. For example if
#' you have the Species factor from the iris data set, by default the display of the variable is
#' `Species $=$ Setosa`, etc. If `i.equal = ": "` the display becomes `Species: Setosa`.
#' @param notes.tpt.intro Character scalar. Only used if `tpt = TRUE`, it is some tex code that is
#' passed before any `threeparttable` item (can be used for, typically, the font size). Default is
#' the empty string.
#' @param depvar.style Character scalar equal to either `" "` (default), `"*"` (italic), `"**"`
#' (bold), `"***"` (italic-bold). How the name of the dependent variable should be displayed.
#' @param no_border Logical, default is `FALSE`. Whether to remove any side border to the table (typically adds `@\{\` to the sides of the tabular).
#' @param caption.after Character scalar. Tex code that will be placed right after the caption.
#' Defaults to `""` for `main = "base"` and `"\\medskip"` for `main = "aer"`.
#' @param rules_width Character vector of length 1 or 2. This vector gives the width of the
#' `booktabs` rules: the first element the heavy-width, the second element the light-width. NA
#' values mean no modification. If of length 1, only the heavy rules are modified. The width are in
#' Latex units (ex: `"0.1 em"`, etc).
#' @param signif.code Named numeric vector, used to provide the significance codes with respect to
#' the p-value of the coefficients. Default is `c("***"=0.01, "**"=0.05, "*"=0.10)`. To suppress
#' the significance codes, use `signif.code=NA` or `signif.code=NULL`. Can also be equal to
#' `"letters"`, then the default becomes `c("a"=0.01, "b"=0.05, "c"=0.10)`.
#'
#' @details
#' The `\\checkmark` command, used in the "aer" style (in argument `yesNo`), is in the
#' `amssymb` package.
#'
#' The commands `\\toprule`, `\\midrule` and `\\bottomrule` are in the `booktabs` package.
#' You can set the width of the top/bottom rules with `\\setlength\\heavyrulewidth\{wd\}`,
#' and of the midrule with `\\setlength\\lightrulewidth\{wd\}`.
#'
#' Note that all titles (`depvar.title`, `depvar.title`, etc) are not escaped, so they
#' must be valid Latex expressions.
#'
#' @return
#' Returns a list containing the style parameters.
#'
#' @seealso
#' [`etable`]
#'
#' @examples
#'
#' # Multiple estimations => see details in feols
#' aq = airquality
#' est = feols(c(Ozone, Solar.R) ~
#' Wind + csw(Temp, Temp^2, Temp^3) | Month + Day,
#' data = aq)
#'
#' # Playing a bit with the styles
#' etable(est, tex = TRUE)
#' etable(est, tex = TRUE, style.tex = style.tex("aer"))
#'
#' etable(est, tex = TRUE, style.tex = style.tex("aer",
#' var.title = "\\emph{Expl. Vars.}",
#' model.format = "[i]",
#' yesNo = "x",
#' tabular = "*"))
#'
style.tex = function(main = "base", depvar.title, model.title, model.format, line.top,
line.bottom, var.title, fixef.title, fixef.prefix, fixef.suffix,
fixef.where, slopes.title, slopes.format, fixef_sizes.prefix,
fixef_sizes.suffix, stats.title, notes.intro,
notes.tpt.intro, tablefoot, tablefoot.value,
yesNo, tabular = "normal", depvar.style, no_border,
caption.after, rules_width, signif.code,
tpt, arraystretch, adjustbox = NULL, fontsize,
interaction.combine = " $\\times$ ", i.equal = " $=$ "){
# To implement later:
# fixef_sizes.where = "obs"
# se.par = "parentheses"
# check_set_arg(se.par, "match(parentheses, brackets, none)")
# align
# check_arg(align, "character vector no na len(,2)")
# Checking
check_set_arg(main, "match(base, aer, qje)")
check_set_arg(line.top, line.bottom, "match(simple, double) | character scalar")
check_arg("character scalar", depvar.title, model.title, var.title)
check_arg("character scalar",
fixef.title, fixef.prefix, fixef.suffix,
slopes.title, slopes.format)
check_arg("character scalar", fixef_sizes.prefix, fixef_sizes.suffix, stats.title)
check_arg("character scalar", notes.intro, interaction.combine, i.equal)
check_arg("character scalar", notes.tpt.intro, depvar.style, caption.after)
check_arg("character vector len(1,2)", rules_width)
if(!missing(depvar.style)){
depvar.style = trimws(depvar.style)
if(!depvar.style %in% c("", "*", "**", "***")){
stop("Argument 'depvar.style' must be one of ' ', '*', '**', '***', which means regular, italic, bold and italic-bold.")
}
}
check_arg(tablefoot.value, "character vector no na")
check_arg(tablefoot, tpt, no_border, "logical scalar")
check_set_arg(fixef.where, "match(var, stats)")
check_set_arg(tabular, "match(normal, *, X)")
check_arg(yesNo, "character vector len(,2) no na")
if(!missing(yesNo) && length(yesNo) == 1){
yesNo = c(yesNo, "")
}
check_arg(arraystretch, "numeric scalar GT{0}")
adjustbox = check_set_adjustbox(adjustbox)
check_set_arg(fontsize, "NULL match(tiny, scriptsize, footnotesize, small, normalsize, large, Large)")
check_set_arg(signif.code, "NULL NA | match(letters) | named numeric vector no na GE{0} LE{1}")
mc = match.call()
if("main" %in% names(mc)){
if(main == "base"){
res = list(depvar.title = "Dependent Variable(s):", model.title = "Model:",
model.format = "(1)",
line.top = "\\tabularnewline \\midrule \\midrule",
line.bottom = "double",
var.title = "\\midrule\n\\emph{Variables}",
fixef.title = "\\midrule\n\\emph{Fixed-effects}", fixef.prefix = "",
fixef.suffix = "", fixef.where = "var",
slopes.title = "\\midrule\n\\emph{Varying Slopes}",
slopes.format = "__var__ (__slope__)",
fixef_sizes.prefix = "\\# ", fixef_sizes.suffix = "",
stats.title = "\\midrule\n\\emph{Fit statistics}",
notes.intro = "\\par \\raggedright \n",
notes.tpt.intro = "",
tablefoot = TRUE,
caption.after = "",
tablefoot.value = "default", yesNo = c("Yes", ""),
depvar.style = "", no_border = FALSE)
if(!missing(tablefoot) && isFALSE(tablefoot)){
res$tablefoot = FALSE
res$line.bottom = "\\midrule \\midrule & \\tabularnewline"
}
} else {
res = list(depvar.title = "", model.title = "", model.format = "(1)",
line.top = "\\toprule", line.bottom = "\\bottomrule",
var.title = "\\midrule",
fixef.title = " ", fixef.prefix = "", fixef.suffix = " fixed effects",
fixef.where = "stats",
slopes.title = "", slopes.format = "__var__ $\\times $ __slope__",
fixef_sizes.prefix = "\\# ", fixef_sizes.suffix = "",
stats.title = " ", notes.intro = "\\par \\raggedright \n",
notes.tpt.intro = "", caption.after = "\\bigskip",
tablefoot = FALSE, tablefoot.value = "",
yesNo = c("$\\checkmark$", ""), depvar.style = "",
no_border = FALSE)
if(main == "aer"){
# just set
} else if(main == "qje"){
res$line.top = "\\tabularnewline\\midrule\\midrule"
res$line.bottom = "\\midrule \\midrule & \\tabularnewline"
}
}
res$tabular = tabular
res$interaction.combine = interaction.combine
res$i.equal = i.equal
} else {
res = list()
}
args2set = setdiff(names(mc)[-1], "main")
for(var in args2set){
res[[var]] = eval(as.name(var))
}
class(res) = "fixest_style_tex"
return(res)
}
#' Style of data.frames created by etable
#'
#' This function describes the style of data.frames created with the function [`etable`].
#'
#' @inheritParams etable
#'
#' @param depvar.title Character scalar. Default is `"Dependent Var.:"`.
#' The row name of the dependent variables.
#' @param fixef.title Character scalar. Default is `"Fixed-Effects:"`. The header preceding the
#' fixed-effects. If equal to the empty string, then this line is removed.
#' @param fixef.line A single character. Default is `"-"`. A character that will be used to create
#' a line of separation for the fixed-effects header. Used only if `fixef.title` is not the empty
#' string.
#' @param fixef.prefix Character scalar. Default is `""`. A prefix to appear
#' before each fixed-effect name.
#' @param fixef.suffix Character scalar. Default is `""`. A suffix to appear
#' after each fixed-effect name.
#' @param slopes.title Character scalar. Default is `"Varying Slopes:"`. The header preceding the
#' variables with varying slopes. If equal to the empty string, then this line is removed.
#' @param slopes.line Character scalar. Default is `"-"`. A character that will be used to create a
#' line of separation for the variables with varying slopes header. Used only if `slopes.line` is
#' not the empty string.
#' @param slopes.format Character scalar. Default is `"__var__ (__slope__)"`. The format of the
#' name of the varying slopes. The values `__var__` and `__slope__` are special characters that
#' will be replaced by the value of the variable name and slope name, respectively.
#' @param stats.title Character scalar. Default is `"_"`. The header preceding the statistics
#' section. If equal to the empty string, then this line is removed. If equal to single character
#' (like in the default), then this character will be expanded to take the full column width.
#' @param stats.line Character scalar. Default is `"_"`. A character that will be used to create a
#' line of separation for the statistics header. Used only if `stats.title` is not the empty string.
#' @param yesNo Character vector of length 1 or 2. Default is `c("Yes", "No")`. Used to inform on
#' the presence or absence of fixed-effects in the estimation. If of length 1, then automatically
#' the second value is considered as the empty string.
#' @param headers.sep Logical, default is `TRUE`. Whether to add a line of separation between the
#' headers and the coefficients.
#' @param interaction.combine Character scalar, defaults to `" x "`. When the estimation contains
#' interactions, then the variables names (after aliasing) are combined with this argument. For
#' example: if `dict = c(x1="Wind", x2="Rain")` and you have the following interaction `x1:x2`,
#' then it will be renamed (by default) `Wind x Rain` -- using `interaction.combine = "*"` would
#' lead to `Wind*Rain`.
#' @param i.equal Character scalar, defaults to `" = "`. Only affects factor variables created with
#' the function [`i`], tells how the variable should be linked to its value. For example if you
#' have the Species factor from the iris data set, by default the display of the variable is
#' `Species = Setosa`, etc. If `i.equal = ": "` the display becomes `Species: Setosa`.
#' @param default Logical, default is `FALSE`. If `TRUE`, all the values not provided by the user
#' are set to their default.
#' @param signif.code Named numeric vector, used to provide the significance codes with respect to
#' the p-value of the coefficients. Default is `c("***"=0.001, "**"=0.01, "*"=0.05, "."=0.10)`. To
#' suppress the significance codes, use `signif.code=NA` or `signif.code=NULL`. Can also be equal
#' to `"letters"`, then the default becomes `c("a"=0.01, "b"=0.05, "c"=0.10)`.
#'
#' @details
#' The title elements (`depvar.title`, `fixef.title`, `slopes.title` and `stats.title`) will be the
#' row names of the returned data.frame. Therefore keep in mind that any two of them should not be
#' identical (since identical row names are forbidden in data.frames).
#'
#' @return
#' It returns an object of class `fixest_style_df`.
#'
#' @examples
#'
#' # Multiple estimations => see details in feols
#' aq = airquality
#' est = feols(c(Ozone, Solar.R) ~
#' Wind + csw(Temp, Temp^2, Temp^3) | Month + Day,
#' data = aq)
#'
#'
#' # Default result
#' etable(est)
#'
#' # Playing a bit with the styles
#' etable(est, style.df = style.df(fixef.title = "", fixef.suffix = " FE",
#' stats.line = " ", yesNo = "yes"))
#'
#'
style.df = function(depvar.title = "Dependent Var.:", fixef.title = "Fixed-Effects:",
fixef.line = "-", fixef.prefix = "", fixef.suffix = "",
slopes.title = "Varying Slopes:", slopes.line = "-",
slopes.format = "__var__ (__slope__)", stats.title = "_",
stats.line = "_", yesNo = c("Yes", "No"), headers.sep = TRUE,
signif.code = c("***"=0.001, "**"=0.01, "*"=0.05, "."=0.10),
interaction.combine = " x ", i.equal = " = ", default = FALSE){
# Checking
check_arg("character scalar", depvar.title, fixef.title, fixef.line, fixef.prefix, fixef.suffix)
check_arg("character scalar", slopes.title, slopes.line, slopes.format, stats.title, stats.line)
check_arg("character scalar", interaction.combine, i.equal)
check_arg(yesNo, "character vector len(,2) no na")
if(length(yesNo) == 1){
yesNo = c(yesNo, "")
}
if(nchar(fixef.line) != 1){
stop("The argument 'fixef.line' must be a singe character! It's currently a string of length ", nchar(fixef.line), ".")
}
if(nchar(slopes.line) != 1){
stop("The argument 'slopes.line' must be a singe character! It's currently a string of length ", nchar(slopes.line), ".")
}
if(nchar(stats.line) != 1){
stop("The argument 'stats.line' must be a singe character! It's currently a string of length ", nchar(stats.line), ".")
}
check_arg(headers.sep, default, "logical scalar")
check_set_arg(signif.code, "NULL NA | match(letters) | named numeric vector no na GE{0} LE{1}")
res = list()
mc = match.call()
args2set = if(default) formalArgs(style.df) else names(mc)[-1]
args2set = setdiff(args2set, "default")
for(var in args2set){
res[[var]] = eval(as.name(var))
}
class(res) = "fixest_style_df"
return(res)
}
#' Register `extralines` macros to be used in `etable`
#'
#' This function is used to create `extralines` (which is an argument of [`etable`]) macros
#' that can be easily summoned in [`etable`].
#'
#' @param type A character scalar giving the type-name.
#' @param fun A function to be applied to a `fixest` estimation. It must return a scalar.
#' @param alias A character scalar. This is the alias to be used in lieu of the type name to
#' form the row name.
#'
#' @details
#' You can register as many macros as you wish, the only constraint is that the type name should not conflict with a [`fitstat`] type name.
#'
#'
#' @examples
#'
#'
#' # We register a function computing the standard-deviation of the dependent variable
#' my_fun = function(x) sd(model.matrix(x, type = "lhs"))
#' extralines_register("sdy", my_fun, "SD(y)")
#'
#' # An estimation
#' data(iris)
#' est = feols(Petal.Length ~ Sepal.Length | Species, iris)
#'
#' # Now we can easily create a row with the SD of y.
#' # We just "summon" it in a one-sided formula
#' etable(est, extralines = ~ sdy)
#'
#' # We can change the alias on the fly:
#' etable(est, extralines = list("_Standard deviation of the dep. var." = ~ sdy))
#'
#'
#'
#'
extralines_register = function(type, fun, alias){
check_arg(type, "character scalar mbt")
check_arg(fun, "function mbt")
check_arg(alias, "character scalar mbt")
# We check the type is not conflicting
existing_types = fitstat(give_types = TRUE)$types
opts = getOption("fixest_extralines")
if(type %in% setdiff(existing_types, names(opts))){
stop("The type name '", type, "' is the same as one of fitstat's built-in type. Please choose another one.")
}
if(missnull(alias)){
alias = type
}
# We test the function on a simple estimation
base = data.frame(y = rnorm(100), x = rnorm(100))
est = feols(y ~ x, base)
mc = match.call()
fun_name = deparse_long(mc$fun)
value = error_sender(fun(est), "The function '", fun_name, "' could not evaluated on a simple fixest object. Please try to improve it.")
if(length(value) != 1){
stop("The value returned by ", fun_name, " should be exactly of length 1. This is actually not the case (the result is of length ", length(value), ").")
}
res = list(fun = fun, alias = alias)
opts[[type]] = res
options(fixest_extralines = opts)
invisible(NULL)
}
#' @rdname etable
print.etable_tex = function(x, ...){
cat(x, sep = "\n")
}
#' @rdname etable
print.etable_df = function(x, ...){
# Almost equivalent to print.data.frame
# we're more flexible with row names since we use the first column as row names
# (and hence allow repetition)
signif.code = attr(x, "signif")
width = 0.99
MAX_WIDTH = getOption("width") * width
ROWNAMES = x[, 1]
x = x[, -1, drop = FALSE]
labels_width = max(nchar(ROWNAMES))
MAX_WIDTH = MAX_WIDTH - labels_width - 1
# names => first row
xx = rbind(names(x), x)
col_width = apply(xx, 2, function(x) max(nchar(x)))
ROWNAMES = sprintf("%- *s", labels_width, c(" ", ROWNAMES))
# We print on the fly
my_col = 1
nc = ncol(x)
while(my_col <= nc){
# We jump one line for readability
if(my_col > 1) cat("\n")
# Finding the nber of columns to print out
cs = cumsum(col_width + 1) - 1
if(cs[1] > MAX_WIDTH){
n_max = 1
} else {
n_max = max(which(cs <= MAX_WIDTH))
}
m = cbind(ROWNAMES, xx[, seq(my_col, length.out = n_max)])
for(j in 2:ncol(m)){
m[, j] = sprintf("% *s", col_width[j - 1], m[, j])
}
table_print = apply(m, 1, paste, collapse = " ")
cat(table_print, sep = "\n")
# Next
my_col = my_col + n_max
col_width = col_width[-(1:n_max)]
}
if(!is.null(signif.code)){
s = signif.code
s_fmt = paste0(insert_in_between(.dsb("q?names(s)"), s), collapse = " ")
cat("---\nSignif. codes: 0 ", s_fmt, " ' ' 1\n", sep = "")
}
}
####
#### Viewer ####
####
check_build_available = function(){
opt = getOption("fixest_build_available")
if(!isTRUE(opt)){
outcome = suppressWarnings(system2("pdflatex", "-help", FALSE, FALSE))
if(outcome == 127 && !requireNamespace("tinytex", quietly = TRUE)){
warn_up("The functionality you want to use requires the package 'tinytex' which is not installed or a working pdflatex installation which wasn't found.")
options(fixest_build_available = "pdflatex")
return("pdflatex")
}
outcome = suppressWarnings(system2("magick", "-help", FALSE, FALSE))
if(outcome == 127 && !requireNamespace("pdftools", quietly = TRUE)){
warn_up("The functionality you want to use requires the package 'pdftools' which is not installed or a working imagemagick + ghostscript installation which wasn't found.")
options(fixest_build_available = "magick")
return("magick")
}
options(fixest_build_available = TRUE)
}
return(TRUE)
}
build_tex_png = function(x, view = FALSE, export = NULL, markdown = NULL,
cache = FALSE, page.width = "fit", up = 0){
up = up + 1
set_up(up)
check_value(view, "logical scalar")
check_value(export, "NULL character scalar")
check_value(markdown, "NULL scalar(logical, character)")
page.width = check_set_page_width(page.width)
# numbers used to uniquely identify the images
NMAX = 10
cache = isTRUE(cache)
dir_cache = NULL
if(cache || isTRUE(markdown)){
is_global_dir = !is.null(find_config_path()) && !is.null(find_project_path())
if(is_global_dir){
dir_cache = config_get("cache_dir")
if(!DIR_EXISTS(dir_cache)){
# reset
dir_cache = file.path(normalizePath(tempdir(), "/"), "etable")
dir.create(dir_cache)
config_update("cache_dir", dir_cache)
}
}
}
if(isFALSE(markdown)){
markdown = NULL
} else if(isTRUE(markdown)){
# note that it's very important to have the default saving path to be relative
# to the working directory and not (as I initially wanted) in a temp dir.
# Why? To make it work properly in webpages, it should be self contained
# and should not use things out of the current working dir.
#
# (In regular markdown, the temp dir is a bit more elegant since
# it does not clutter the workspace)
markdown = "./images/etable/"
}
# we need to clean all the tmp tags otherwise the caching does not work
x_clean = paste0(c(page.width, x), collapse = "\n")
if(any(grepl("definecolor", x_clean, fixed = TRUE))){
my_colors = dsb("'(?<=definecolor\\{)[[:alpha:]]+(?=\\})'X, '|'c ? x_clean")
x_clean = gsub(my_colors, "", x_clean)
}
if(any(grepl("\\mark", x, fixed = TRUE))){
x_clean = gsub("mark[[:alpha:]]+", "", x_clean)
}
do_build = TRUE
export_markdown = id = NULL
if(!is.null(markdown)){
markdown_path = check_set_path(markdown, "w, dir", create = TRUE, up = up)
all_files = list.files(markdown_path, "\\.png$", full.names = TRUE)
id_all = gsub("^.+_|\\.png$", "", all_files)
id = substr(cpp_hash_string(x_clean), 1, NMAX)
if(!id %in% id_all){
time = gsub(" .+", "", Sys.time())
name = .dsb("etable_tex_.[time]_.[id].png")
export_markdown = file.path(markdown_path, name)
} else {
do_build = FALSE
export_markdown = png_name = normalizePath(all_files[id_all == id][1], "/")
}
}
if(!is.null(export)){
export_path = check_set_path(export, "w", up = up)
}
dir = NULL
if(do_build){
dir = if(cache && !is.null(dir_cache)) dir_cache else normalizePath(tempdir(), "/")
if(cache){
if(is.null(id)){
id = substr(cpp_hash_string(x_clean), 1, NMAX)
}
# If the file already exists, we don't recreate it
all_files = list.files(dir, "^etable.+\\.png$", full.names = TRUE)
id_all = gsub("^.+_|\\.png$", "", all_files)
if(id %in% id_all){
do_build = FALSE
png_name = normalizePath(all_files[id_all == id][1], "/")
} else {
time = gsub(" .+", "", Sys.time())
png_name = .dsb("etable_tex_.[time]_.[id].png")
}
} else {
png_name = "etable.png"
}
}
if(view || do_build){
# we set the working directory properly
# used to:
# - compile .tex to .pdf to .png
# - view the HTML document (needs to be on the WD)
if(is.null(dir)) dir = normalizePath(tempdir(), "/")
current_dir = getwd()
on.exit(setwd(current_dir))
setwd(dir)
}
if(do_build){
#
# Latex document
#
# packages increase build time, so we load them sparingly
# p: package ; pn: package name ; x: tex vector ; y: tex packages
add_pkg = function(p, x, y, pn = p, opt = "", fixed = TRUE){
if(any(grepl(p, x, fixed = fixed))){
c(y, .dsb("\\usepackage.[opt]{.[pn]}"))
} else {
y
}
}
minipage_start = minipage_end = ""
w = "35cm"
if(!identical(as.vector(page.width), "fit", )){
# page.width is guaranteed to be of length 2
w = page.width[1]
minipage_start = .dsb("\\begin{minipage}{.[page.width[1]]} ",
"\\centering ",
"\\begin{minipage}{.[page.width[2]]}\n ", sep = "\n")
minipage_end = "\n\\end{minipage}\n\\end{minipage}"
}
intro = .dsb0("\\documentclass[varwidth=.[w], border={ 10 5 10 5 }]{standalone}",
"\\usepackage[dvipsnames,table]{xcolor}",
"\\usepackage{.[/array, booktabs, multirow, helvet, amsmath, amssymb]}",
"\\renewcommand{\\familydefault}{\\sfdefault}", vectorize = TRUE)
tex_pkg = c()
tex_pkg = add_pkg("(row|cell)color", x, tex_pkg, "colortbl", fixed = FALSE)
tex_pkg = add_pkg("threeparttable", x, tex_pkg, opt = "[flushleft]")
tex_pkg = add_pkg("adjustbox", x, tex_pkg)
tex_pkg = add_pkg("tabularx", x, tex_pkg)
tex_pkg = add_pkg("makecell", x, tex_pkg)
do_rerun = FALSE
if(any(grepl("tikz", x, fixed = TRUE))){
# If tikz is present, we need to run pdflatex twice to make it work properly
do_rerun = TRUE
tex_pkg = c(tex_pkg, "\\usepackage{tikz}",
"\\usetikzlibrary{matrix, shapes, arrows, fit, tikzmark}")
}
doc_full = c(intro, tex_pkg,
"\n\n\\begin{document}\n", minipage_start,
x,
minipage_end, "\n\\end{document}\n")
#
# Compiling + exporting
#
tex_file = file("etable.tex", "w", encoding = "UTF-8")
writeLines(doc_full, tex_file)
close(tex_file)
options(fixest_log_dir = dir)
# We compile the document
draft = if(do_rerun) "-draftmode" else ""
# we delete the aux that can generate problems
if(file.exists("etable.aux")){
unlink("etable.aux")
}
# I keep the CMD because it is faster
# WITH: 2.5s
# SANS: 3.5s
DO_CMD = TRUE
warn_msg = NULL
ok_cmd_tex = TRUE && DO_CMD
if(DO_CMD){
outcome = suppressWarnings(system2("pdflatex",
sma("-halt-on-error -interaction=nonstopmode {draft}",
" etable.tex"),
"etable_shell_pdf.log", "etable_shell_pdf.err"))
if(outcome == 127){
ok_cmd_tex = FALSE
} else if(outcome == 1){
# Sometimes recompiling works!!!
outcome = suppressWarnings(system2("pdflatex",
sma("-halt-on-error -interaction=nonstopmode ",
"{draft} etable.tex"),
"etable_shell_pdf.log", "etable_shell_pdf.err"))
if(outcome == 1){
warn_msg = "pdflatex: error when compiling -- sorry! Check the log file with log_etable('pdflatex')."
ok_cmd_tex = FALSE
}
}
}
if(ok_cmd_tex && do_rerun){
outcome = suppressWarnings(system2("pdflatex",
"-halt-on-error -interaction=nonstopmode etable.tex",
"etable_shell_pdf.log", "etable_shell_pdf.err"))
# No reason for the 2nd run to fail, but I add it anyway just to be safe
if(outcome == 1){
warn_msg = "pdflatex: error when compiling -- sorry! Check the log file with log_etable('pdflatex')."
ok_cmd_tex = FALSE
}
}
if(!ok_cmd_tex){
# We use tinytex
if(!requireNamespace("tinytex", quietly = TRUE)){
if(!is.null(warn_msg)){
# we still give the warning to the user due to CMD pblm
warn_up(warn_msg)
return(NULL)
}
warn_up("The functionality you want to use requires the package 'tinytex' which is not installed or a working pdflatex installation which wasn't found.")
return(NULL)
}
info_compile = try(suppressMessages(tinytex::pdflatex("etable.tex",
clean = FALSE,
min_times = 1 + do_rerun)))
if(inherits(info_compile, "try-error")){
warn_up("pdflatex: error when compiling -- sorry! Check the log file with `log_etable('pdflatex')`.")
return(NULL)
}
}
#
# Creating the PNG
#
ok_cmd_magick = TRUE && DO_CMD
warn_msg = NULL
if(DO_CMD){
outcome = suppressWarnings(system2("magick",
sma("-density 600 etable.pdf -colorspace RGB {png_name}"),
"etable_shell_magick.log", "etable_shell_magick.err"))
if(outcome == 127){
ok_cmd_magick = FALSE
} else if(outcome == 1){
warn_msg = "magick: error when converting pdf to png. Check install of ghostscript? Check the log file with `log_etable('magick')`."
ok_cmd_magick = FALSE
}
}
if(!ok_cmd_magick){
if(!requireNamespace("pdftools", quietly = TRUE)){
if(!is.null(warn_msg)){
warn_up(warn_msg)
return(NULL)
}
warn_up("The functionality you want to use requires the package 'pdftools' which is not installed or a working imagemagick+ghostscript installation which wasn't found.")
return(NULL)
}
# We use pdftools, but to avoid an ugly warning, we need to provide a name we don't want
# and hence rename the file later...
pdftools::pdf_convert("etable.pdf", dpi = 600, verbose = FALSE,
filenames = sma("{png_name}_%d.%s"))
old_name = sma("{png_name}_1.png")
file.rename(old_name, png_name)
}
}
# Now porting to the viewer
if(view){
my_viewer = getOption("viewer")
if(is.null(my_viewer)){
warning("To preview the table, we need a viewer -- which wasn't found (it sjould work on RStudio and VScode).")
} else {
# setting up the html document
# NOTE: all the viewer's data must be in tempdir
# => when we cache, that can cause problems
# hence we copy the file there if necessary
tmp_dir = normalizePath(tempdir(), "/")
if(normalizePath(getwd(), "/") != tmp_dir){
old_name = png_name
png_name = gsub(".+/", "", png_name)
file.copy(old_name, file.path(tmp_dir, png_name))
setwd(tmp_dir)
}
html_file = viewer_html_template(png_name)
writeLines(html_file, "etable.html")
my_viewer("etable.html")
}
}
# And exporting
if(!is.null(export)){
file.copy(png_name, export_path, overwrite = TRUE)
}
if(!is.null(export_markdown) && export_markdown != png_name){
file.copy(png_name, export_markdown, overwrite = TRUE)
}
return(export_markdown)
}
check_set_path = function(x, type = "", create = TRUE, up = 0){
# type:
# + r: read (file or dir must exists), w (file is to be created)
# + dir: directory and not a document
# create:
# - if file: creates the parent dir if the grand parent exists
# - if dir: creates the dir only if grand parent exists
set_up(up + 1)
flags = strsplit(type, ", *")[[1]]
x_dp = deparse(substitute(x))
path = try(normalizePath(x, "/", mustWork = FALSE))
if("try-error" %in% class(path)){
path = try(normalizePath(paste0("./", x), "/", mustWork = FALSE))
if("try-error" %in% class(path)){
stop_up("The path ", x, " is not valid, please revise.")
}
}
# If path exists: fine!
is_dir = "dir" %in% flags
if(is_dir){
if(dir.exists(path)){
return(path)
}
} else if(file.exists(path)){
return(path)
}
# Now the path does not exist already, so we don't need to check
# here: it must be an error
if("r" %in% flags){
msg = if("dir" %in% flags) "directory" else "file"
stop_up("Argument '", x_dp, "' should be a path to a ", msg,
" that exists. \n Problem: '", path, "' does not exist.")
}
# Here we're in write
file_name = gsub(".+/", "", path)
path_dir = str_trim(path, -nchar(file_name))
if(nchar(path_dir) == 0) path_dir = "."
path_parent = dirname(path)
if(dir.exists(path_parent)){
if(is_dir && create){
dir.create(path)
}
return(path)
}
if(create){
path_grand_parent = dirname(path_parent)
if(dir.exists(path_grand_parent)){
dir.create(path_parent)
if(is_dir){
dir.create(path)
}
return(path)
}
}
msg = if("dir" %in% flags) "directory" else "file"
stop_up("Argument '", x_dp, "' should be a path to a ", msg,
". \n Problem: '", path_parent, "' does not exist.")
}
viewer_html_template = function(png_name){
# I really wanted to see the full table all the time, so I had to add some JS.
# There must be some straightforward way in CSS, but I don't know it...
.dsb0('
<!DOCTYPE html>
<html> <head>
<style>
#container {
width: 100%;
display: block;
height: 96vh;
text-align: center;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<div id="container" class = "etable">
<img src = ".[png_name]" alt="etable preview">
</div>
</body> </html>
')
}
#' @rdname etable
log_etable = function(type = "pdflatex"){
check_set_arg(type, "match(pdflatex, magick, tex, dir)")
dir = getOption("fixest_log_dir")
if(length(dir) == 0){
return("No log currently exists")
}
if(type == "pdflatex"){
path = file.path(dir, "etable.log")
} else if(type == "magick"){
path = file.path(dir, "etable_shell_magick.log")
} else if(type == "dir"){
return(dir)
} else {
path = file.path(dir, "etable.tex")
}
if(!file.exists(path)){
message(.dsb("No log currently exists for '.[type]'."))
return(invisible(NULL))
}
res = readLines(path)
class(res) = "etable_tex"
res
}
DIR_EXISTS = function(x){
if(length(x) != 1 || is.na(x) || !is.character(x)){
return(FALSE)
}
dir.exists(x)
}
fix_pkgwdown_path = function(){
# https://github.com/r-lib/pkgdown/issues/1218
# just because I use google drive... it seems pkgdown cannot convert to relative path...
# This is to ensure it only works for me
if(!isTRUE(renvir_get("fixest_ROOT"))) return(NULL)
# we check we're in the right directory (otherwise there can be prblms with Rmakdown)
if(!isTRUE(file.exists("R/alias_VCOV.R"))) return(NULL)
all_files = list.files("docs/articles/", full.names = TRUE, pattern = "html$")
for(f in all_files){
my_file = file(f, "r", encoding = "UTF-8")
text = readLines(f)
close(my_file)
if(any(grepl("../../../", text, fixed = TRUE))){
# We embed the images directly: safer
# A) we get the path
# B) we transform to URI
# C) we replace the line
done = FALSE
pat = "<img.+\\.\\./.+/fixest/.+/images/"
qui = which(grepl(pat, text))
for(i in qui){
if(!done){
message("Fixing pkgdown paths (", gsub(".+/", "", f), ").")
}
# ex: line = "<img src = \"../../../Google drive/fixest/fixest/vignettes/images/etable/etable_tex_2021-12-02_1.05477838.png\">"
line = text[i]
line_split = strsplit(line, "src *= *\"")[[1]]
path = gsub("\".*", "", line_split[2])
# ROOT is always fixest
path = gsub(".+fixest/", "", path)
path = gsub("^articles", "vignettes", path)
URI = knitr::image_uri(path)
rest = gsub("^[^\"]+\"", "", line_split[2])
new_line = dsb('.[line_split[1]] src = ".[URI]".[rest]')
text[i] = new_line
}
my_file = file(f, "w", encoding = "UTF-8")
writeLines(text, f)
close(my_file)
}
}
}
####
#### Utilities ####
####
style_apply = function(coef.style, coef_mat, coef_names){
# applies a style to specified coefficients
set_up(2)
if(length(coef.style) == 0){
return(coef_mat)
}
res = coef_mat
n_models = ncol(res) - 1
has_row_se = any(" " %in% res[, 1])
get_row_id = function(i) if(has_row_se) 1 + 2 * (i - 1) else i
for(i in seq_along(coef.style)){
cs = coef.style[[i]]
cs_name = paste0(trimws(names(coef.style)[i]), " ")
if(!grepl(":coef(_se)?:", cs_name)){
warn_up("In the argument 'coef.style', the names must contain the ",
"':coef:' (or :coef_se:) string. ",
"PROBLEM: this is not the case for the {nth?i} element (equal to {q?cs_name}).")
}
is_se = grepl(":coef_se:", cs_name)
if(is_se){
cs_name = sub(":coef_se:", ":coef:", cs_name, fixed = TRUE)
}
add_se = function(x) if(is_se) rbind(x, x + matrix(c(1, 0), nrow(x), 2, byrow = TRUE)) else x
style_split = strsplit(cs_name, ":coef:", fixed = TRUE)[[1]]
if(cs %in% c(".", "all")){
id = which(matrix(grepl("[^ ]", coef_mat), nrow(coef_mat)), arr.ind = TRUE)
# we remove the row names
id = id[id[, 2] != 1, ]
if(!is_se && has_row_se){
# we remove the se row!
id = id[id[, 1] %% 2 == 1, ]
}
# We apply the style
values = res[id]
new_values = paste0(style_split[1])
for(k in seq_along(style_split)[-1]){
new_values = paste0(new_values, values, style_split[k])
}
new_values = trimws(new_values)
res[id] = escape_latex(new_values)
} else {
coef_location_all = coef_location(cs, coef_names, n_models, "coef.style", pool = TRUE)
cell = coef_location_all$cell
# cell: list of n x 2 matrices
for(j in seq_along(cell)){
cell_mat = cell[[j]]
id = cbind(get_row_id(cell_mat[, 1]), cell_mat[, 2] + 1)
id = add_se(id)
id = id[grepl("[^ ]", res[id]), , drop = FALSE]
if(length(id) > 0){
values = res[id]
new_values = paste0(style_split[1])
for(k in seq_along(style_split)[-1]){
new_values = paste0(new_values, values, style_split[k])
}
new_values = trimws(new_values)
res[id] = escape_latex(new_values)
}
}
}
}
return(res)
}
highlight_apply = function(highlight, coef_mat, coef_names){
# applies coefficients highlighting
set_up(2)
if(length(highlight) == 0){
return(list(preamble = "", coef_mat = coef_mat))
}
if(is.null(names(highlight))){
names(highlight) = " "
}
# default color
COLOR = "#c80815"
res = coef_mat
n_models = ncol(res) - 1
# \definecolor{Mycolor2}{HTML}{00F9DE}
# \definecolor{mypink1}{rgb}{0.858, 0.188, 0.478}
# \definecolor{mypink2}{RGB}{219, 48, 122}
has_row_se = any(" " %in% res[, 1])
get_row_id = function(i) if(has_row_se) 1 + 2 * (i - 1) else i
# frame id
f_id = 0
all_colors = c()
tex_preamble = c()
for(i in seq_along(highlight)){
hl = highlight[[i]]
hl_name = trimws(strsplit(names(highlight)[i], "[, ]+")[[1]])
if(identical(hl_name, '')){
hl_name = character(0)
}
#
# Parsing the parameters
#
# TRUE/FALSE flags
is_se = "se" %in% hl_name && has_row_se
add_se = function(x) if(is_se) rbind(x, x + matrix(c(1, 0), nrow(x), 2, byrow = TRUE)) else x
is_rowcol = "rowcol" %in% hl_name
is_round = !"square" %in% hl_name
hl_name = setdiff(hl_name, c("se", "rowcol", "square"))
# Values
thick_raw = grep("^thick\\d$", hl_name, value = TRUE)
thick = if(length(thick_raw) == 0) 6 else as.numeric(sub("thick", "", thick_raw))
if(thick > 6){
stop_up("In the argument 'highlight', the name of the {nth?i}",
" element (equal to {bq?hl_name}) is ill formed. ",
"The item 'thick' can go only from 1 to 6.")
}
sep_raw = grep("^sep\\d+$", hl_name, value = TRUE)
sep = if(length(sep_raw) == 0) 3 else sub("sep", "", sep_raw)
hl_name = setdiff(hl_name, c(thick_raw, sep_raw))
# Color: the remaining
color = hl_name
is_col = length(color) == 1
if(length(color) > 1){
stop_up("In the argument 'highlight', the name of the {nth?i}",
" element (equal to {q?hl_name}) is ill formed. ",
"It should be a comma separated list of options, which include: ",
"'rowcol', 'square', 'thickd' (with d from 0 to 6), 'sepd' (d: 0-9), ",
"'se', and 'color!alpha' with 'color' a valid R color and, optionnaly, ",
"'alpha' in 0-100.")
}
# conversion of the color
is_alpha = FALSE
if(is_col){
if(grepl("!", color, fixed = TRUE)){
color_split = strsplit(color, "!", fixed = TRUE)[[1]]
is_alpha = TRUE
alpha = color_split[2]
color = color_split[1]
}
} else {
color = COLOR
if(is_rowcol){
# Default row color is not solid
alpha = "25"
is_alpha = TRUE
}
}
tex_color = error_sender(col2rgb(color),
"In the argument 'highlight', the color, in the name of the ",
n_th(i), " element (equal to '", color,
"'), could not be converted to RGB. ")
tex_color = paste0(as.vector(tex_color), collapse = ", ")
if(tex_color %in% all_colors){
tag_color = names(all_colors)[all_colors == tex_color]
} else {
tag_color = paste0("col", tag_gen())
all_colors[tag_color] = tex_color
tex_preamble = c(tex_preamble, paste0("\\definecolor{", tag_color, "}{RGB}{", tex_color, "}"))
}
if(is_alpha){
tag_color = paste0(tag_color, "!", alpha)
}
coef_location_all = coef_location(hl, coef_names, n_models, "highlight")
cell = coef_location_all$cell
row = coef_location_all$row
range = coef_location_all$range
if(is_rowcol){
# cell: list of n x 2 matrices
for(j in seq_along(cell)){
cell_mat = cell[[j]]
for(k in 1:nrow(cell_mat)){
my_cell = cell_mat[k, ]
id = cbind(get_row_id(my_cell[1]), my_cell[2] + 1)
id = add_se(id)
res[id] = paste0("\\cellcolor{", tag_color, "} ", res[id])
}
}
# row: list of integers
for(j in seq_along(row)){
my_row = row[[j]]
id = cbind(get_row_id(my_row), 1)
id = add_se(id)
res[id] = paste0("\\rowcolor{", tag_color, "} ", res[id])
}
# range: list of vectors of length 4 (i1, k1, i2, k2)
for(j in seq_along(range)){
my_range = range[[j]]
id = expand.grid(my_range[1]:my_range[3], my_range[2]:my_range[4])
id = add_se(id)
res[id] = paste0("\\cellcolor{", tag_color, "} ", res[id])
}
} else {
#
# FRAME
#
tag_frame = tag_gen()
tag_frame_NW = paste0("\\markNW", tag_frame)
tag_frame_SE = paste0("\\markSE", tag_frame)
# thickness
# thin, very thin, ultra thin
# thick, very thick, ultra thick
thickness = .dsb(".[/thin, very thin, ultra thin, thick, very thick, ultra thick]")[thick]
rounded = if(is_round) "rounded corners, " else ""
# On the frame:
# https://tex.stackexchange.com/questions/240542/adding-a-rectangular-box-with-tikz-to-table-beamer
#
tex_preamble = c(tex_preamble, .dsb(
"
\\newcommand.[tag_frame_NW][1]{%
\\tikz[overlay,remember picture]
\\node (marker-#1-a) at (0, .3em) {};%
}
\\newcommand.[tag_frame_SE][1]{%
\\tikz[overlay,remember picture]
\\node (marker-#1-b) at (0, .3em) {};%
\\tikz[overlay,remember picture,inner sep=.[sep]pt, .[thickness]]
\\node[draw=.[tag_color],.[rounded]fit=(marker-#1-a.north west) (marker-#1-b.south east)] {};%
}\n"))
# cell: list of n x 2 matrices
for(j in seq_along(cell)){
cell_mat = cell[[j]]
n_cells = nrow(cell_mat)
k_done = c()
for(k in 1:n_cells){
if(k %in% k_done) next
my_cell = cell_mat[k, ]
# we find the rightmost index
cell_right = my_cell
while(k + 1 <= n_cells &&
cell_mat[k + 1, 1] == my_cell[1] &&
cell_mat[k + 1, 2] == my_cell[2] + 1){
k_right = k + 1
cell_right = cell_mat[k_right, ]
k_done[length(k_done) + 1] = k_right
k = k + 1
}
id_NW = cbind(get_row_id(my_cell[1]), my_cell[2] + 1)
if(is_se){
id_SE = cbind(get_row_id(my_cell[1]) + 1, cell_right[2] + 1)
} else {
id_SE = cbind(get_row_id(my_cell[1]), cell_right[2] + 1)
}
f_id = f_id + 1
res[id_NW] = .dsb(".[tag_frame_NW]{f.[f_id]} .[res[id_NW]]")
res[id_SE] = .dsb(".[res[id_SE]] .[tag_frame_SE]{f.[f_id]}")
}
}
# row: list of integers
for(j in seq_along(row)){
my_row = row[[j]]
id_NW = cbind(get_row_id(my_row), 2)
if(is_se){
# We need to end at the line just below is is_se
id_SE = cbind(get_row_id(my_row) + 1, n_models + 1)
} else {
id_SE = cbind(get_row_id(my_row), n_models + 1)
}
f_id = f_id + 1
res[id_NW] = .dsb(".[tag_frame_NW]{f.[f_id]} .[res[id_NW]]")
res[id_SE] = .dsb(".[res[id_SE]] .[tag_frame_SE]{f.[f_id]}")
}
# range: list of vectors of length 4 (i1, k1, i2, k2)
for(j in seq_along(range)){
my_range = range[[j]]
id_NW = cbind(get_row_id(my_range[1]), my_range[2] + 1)
id_SE = cbind(get_row_id(my_range[3]), my_range[4] + 1)
if(is_se){
# We need to end at the line just below is is_se
id_SE[1] = id_SE[1] + 1
}
f_id = f_id + 1
res[id_NW] = .dsb(".[tag_frame_NW]{f.[f_id]} .[res[id_NW]]")
res[id_SE] = .dsb(".[res[id_SE]] .[tag_frame_SE]{f.[f_id]}")
}
}
}
return(list(coef_mat = res, preamble = tex_preamble))
}
coef_location = function(x, coef_names, n_models, arg_name, pool = FALSE){
# x: vector of coef location
# cell: "x1@2", "x1@1, 3, 5-8"
# range: "x2@2; x3@.N", "x2; x3"
# row: "x1"
cell = range = row = list()
for(i in seq_along(x)){
xi = x[i]
if(grepl(";", xi, fixed = TRUE)){
# => range
xi_split = trimws(strsplit(xi, ";", fixed = TRUE)[[1]])
if(length(xi_split) != 2){
stop_up(up = 3, "In the argument {bq?arg_name}, the value of the {nth?i}",
" element (equal to {q?xi}) is not valid. ",
"It should contain at most one semi-comma.")
}
xi_1 = xi_split[[1]]
xi_2 = xi_split[[2]]
xi_1_loc = coef_pos_parse(xi_1, coef_names, n_models, i, arg_name)
xi_2_loc = coef_pos_parse(xi_2, coef_names, n_models, i, arg_name)
# We always normalize the range, it must be two single positions
xi_1_pos = if(xi_1_loc$is_row) 1 else min(xi_1_loc$pos)
xi_2_pos = if(xi_2_loc$is_row) 1 else max(xi_2_loc$pos)
if(pool){
cell[[length(cell) + 1]] = expand.grid(xi_1_loc$coef:xi_2_loc$coef,
xi_1_pos:xi_2_pos)
} else {
range[[length(range) + 1]] = c(xi_1_loc$coef, xi_1_pos, xi_2_loc$coef, xi_2_pos)
}
} else {
xi_loc = coef_pos_parse(xi, coef_names, n_models, i, arg_name)
if(xi_loc$is_row){
if(pool){
all_cells = cbind(xi_loc$coef, 1:n_models)
cell[[length(cell) + 1]] = all_cells
} else {
row[[length(row) + 1]] = xi_loc$coef
}
} else {
all_cells = cbind(xi_loc$coef, xi_loc$pos)
cell[[length(cell) + 1]] = all_cells
}
}
}
res = list(cell = cell, row = row, range = range)
return(res)
}
coef_pos_parse = function(x, coef_names, n_models, i, arg_name){
# x: "x1@2", "x1", "x1@2, 5, 7-10, 13"
set_up(4)
x_split = strsplit(x, "@", fixed = TRUE)[[1]]
if(length(x_split) > 2){
stop_up("In the argument {bq?arg_name}, the location of the {nth?i}",
" element (equal to {q?x}) is not valid. ",
"It should contain at most one '@'.")
}
x_coef = x_split[1]
x_pos = if(length(x_split) == 2) x_split[2] else NULL
#
# Position
#
x_pos = gsub(".N", n_models, x_pos, fixed = TRUE)
if(length(x_pos) > 0){
# x: 3, 5, 6-8, 10
if(grepl("[^ ,-[[:digit:]]]", x_pos)){
stop_up("In the argument {bq?arg_name}, the location of the {nth?i}",
" element (equal to {q?x}) contains non valid characters. ",
"Please have a look at the help/example.")
}
# new DSB power
x_txt = paste0("c(", gsub("-", ":", x_pos), ")")
x_call = error_sender(str2lang(x_txt),
"In argument '", arg_name, "' the position in '", x,
"' is not valid. Please have a look at the help/example.",
up = 4)
x_pos = eval(x_call)
}
#
# Coefficient
#
coef_id = pmatch_varname(x_coef, coef_names, arg_name)
res = list(coef = coef_id, pos = x_pos, is_row = length(x_pos) == 0)
return(res)
}
pmatch_varname = function(x, coef_names, arg_name){
# We want a SINGLE match
# coef_names: vector of names
# names(coef_names): original names
# coef_names = setNames(c("Petal length", "Petal width"), c("Petal.Length", "Petal.Width"))
# x = "Petal.L"
is_regex = is_original = FALSE
if(grepl("^@", x)){
is_regex = TRUE
x = str_trim(x, 1)
if(grepl("^%", x)){
is_original = TRUE
x = str_trim(x, 1)
}
} else if(grepl("^%", x)){
is_original = TRUE
x = str_trim(x, 1)
if(grepl("^@", x)){
is_regex = TRUE
x = str_trim(x, 1)
}
}
# Pattern of match finding
if(is_regex && is_original){
do_regex = TRUE
do_original = TRUE
} else if(is_regex){
do_regex = c(TRUE, TRUE)
do_original = c(FALSE, TRUE)
} else if(is_original){
do_regex = c(FALSE, TRUE)
do_original = c(TRUE, TRUE)
} else {
do_regex = c(FALSE, FALSE, TRUE, TRUE)
do_original = c(FALSE, TRUE, FALSE, TRUE)
}
ok = FALSE
for(i in seq_along(do_regex)){
if(do_regex[i]){
if(do_original[i]){
qui = grepl(x, names(coef_names))
} else {
qui = grepl(x, coef_names)
}
if(sum(qui) == 1){
ok = TRUE
qui = which(qui)
}
} else {
if(do_original[i]){
qui = charmatch(x, names(coef_names))
} else {
qui = charmatch(x, coef_names)
}
if(!is.na(qui) && qui != 0) ok = TRUE
}
if(ok) break
}
if(!ok){
stop_up(up = 5, "In the argument '", arg_name, "', the value '", x,
"' does not match any variable name.")
}
qui
}
# escape_latex("Voici **une** *equation*: $5! = 5*4*3*2*1$. Est-ce que ***ca marche***?$^*$")
escape_latex = function(x_all, makecell = TRUE){
# escapes & _ % ^ #
# values within $ $ are not escaped
# if only one $ => will be escaped
# applies markdown markup
x_name = deparse(substitute(x_all))
res = cpp_escape_markup(x_all)
if(makecell){
who = grepl("\n", res, fixed = TRUE)
if(any(who)){
res[who] = paste0("\\makecell{", gsub("\n", "\\\\", res[who], fixed = TRUE), "}")
}
}
if(!is.null(names(x_all))){
names(res) = names(x_all)
}
res
}
format_se_type = function(x, width, by = FALSE, dict = c()){
# we make 'nice' se types
# format_se_type("Clustered (species & fe2)", 10, by = TRUE)
# format_se_type("vcovHC(x, type = \"HC0\")", 10, by = TRUE)
# format_se_type("Newey-West (L=10)", 10, by = TRUE)
# format_se_type("Driscoll-Kraay (L=10)", 10, by = TRUE)
# format_se_type("Conley (100km)", 10, by = TRUE)
if(identical(x, "NONE_FIXEF_ONLY")){
return("--")
}
if(!grepl("\\(", x) || !grepl("Clustered", x, fixed = TRUE)){
# means not clustered
if(nchar(x) <= width) return(x)
# Special case: sandwich
if(grepl("^vcov[^\\(]+\\(", x)){
x = gsub("\\(x, ", "(", x)
x = gsub("\\(\\)", "", x)
if(nchar(x) > width){
# we still reduce it
x = gsub(" = ", "=", x)
}
if(nchar(x) > width){
# ...even further
x = paste0(substr(x, 1, width - 2), "..")
}
return(x)
}
# We reduce each word to 3 letters (if needed)
x_split = c("$", strsplit(x, "")[[1]]) # we add a non-letter flag, marking the beginning
x_split_new = x_split
end_word = length(x_split)
non_letter_flag = grepl("[^[:alpha:]]", x_split) * (1:end_word)
letter_flag = grepl("[[:alpha:]]", x_split) * (1:end_word)
while(TRUE){
start_word = which.max(non_letter_flag[1:end_word]) + 1
# we truncate
word_length = end_word - start_word + 1
slack = length(x_split_new) - (width + 1)
letters_to_rm = min(word_length - 4, slack)
if(letters_to_rm > 0){
i_max = end_word - letters_to_rm
x_split_new = x_split_new[-((i_max+1):end_word)]
x_split_new[i_max] = "."
}
lf = letter_flag[1:(start_word - 1)]
if(all(lf == 0)) break
# new end_word
end_word = which.max(lf)
}
return(paste(x_split_new[-1], collapse = ""))
} else if(x == "NA (not-available)"){
return("not available")
}
# Now the FEs
all_fe = gsub(".+\\((.+)\\)", "\\1", x)
all_fe_split = gsub(" ", "", strsplit(all_fe, "&")[[1]])
n_fe = length(all_fe_split)
n_char = nchar(all_fe_split)
# renaming the FEs
all_fe_format = c()
for(i in 1:length(all_fe_split)){
fe = all_fe_split[i]
if(fe %in% names(dict)){
all_fe_format[i] = dict[fe]
} else if(grepl("\\^", fe)){
fe_split = strsplit(fe, "\\^")[[1]]
who = fe_split %in% names(dict)
fe_split[who] = dict[fe_split[who]]
all_fe_format[i] = paste(fe_split, collapse = "-")
} else {
all_fe_format[i] = fe
}
}
all_fe_split = all_fe_format
if(n_fe == 1 && !grepl("\\^", all_fe_split[1])){
if(by){
se_formatted = paste0("by: ", all_fe_split[1])
} else {
se_formatted = paste0("1-way: ", all_fe_split[1])
}
if(nchar(se_formatted) > width){
se_formatted = paste0(substr(se_formatted, 1, width - 2), "..")
}
return(se_formatted)
}
nb = ifelse(by, 3, 6)
if(width < nb + sum(n_char) + (n_fe-1) * 3){
qui = n_char > 5
for(i in which(qui)){
if(grepl("\\^", all_fe_split[i])){
single_split = strsplit(all_fe_split[i], "\\^")[[1]]
qui_bis = nchar(single_split) > 4
single_split[qui_bis] = paste0(substr(single_split[qui_bis], 1, 3), ".")
all_fe_split[i] = paste(single_split, collapse = "^")
} else {
all_fe_split[i] = paste0(substr(all_fe_split[i], 1, 4), ".")
}
}
}
if(by){
se_formatted = paste0("by: ", paste(all_fe_split, collapse = " & "))
} else {
se_formatted = paste0(n_fe, "-way: ", paste(all_fe_split, collapse = " & "))
}
# NOTA:
# we do not trim if still too large because the SE-type IS informative!
# A table without that information is useless, it's trimmed enough already
# if(nchar(se_formatted) > width){
# # se_formatted = gsub("-way: ", "way: ", se_formatted)
# se_formatted = paste0(substr(se_formatted, 1, width - 2), "..")
# }
se_formatted
}
format_se_type_latex = function(x, dict = c(), inline = FALSE){
# we make 'nice' se types
if(identical(x, "NONE_FIXEF_ONLY")){
return(" ")
}
if(!grepl("\\(", x)){
# means not clustered
# we escape all
return(escape_latex(x))
}
# Now the FEs
main_type = gsub(" \\(.*", "", x)
all_fe = gsub(".+\\((.+)\\)", "\\1", x)
all_fe_split = gsub(" ", "", strsplit(all_fe, "&")[[1]])
n_fe = length(all_fe_split)
# Renaming the FEs
all_fe_format = c()
for(i in 1:length(all_fe_split)){
fe = all_fe_split[i]
if(fe %in% names(dict)){
all_fe_format[i] = dict[fe]
} else if(grepl("\\^", fe)){
fe_split = strsplit(fe, "\\^")[[1]]
who = fe_split %in% names(dict)
fe_split[who] = dict[fe_split[who]]
all_fe_format[i] = paste(fe_split, collapse = "-")
} else {
all_fe_format[i] = fe
}
}
fe_format = paste(all_fe_format, collapse = " \\& ")
# We add some flexibility: anticipation of more VCOV types
main_type_dict = c("Clustered" = "Clustered", "Two-way" = "Clustered",
"Three-way" = "Clustered", "Four-way" = "Clustered")
if(main_type %in% names(main_type_dict)){
main_type = main_type_dict[main_type]
}
if(inline){
# The fact that it is clustered is deduced
se_formatted = fe_format
} else {
se_formatted = paste0(main_type, " (", fe_format, ")")
}
escape_latex(se_formatted)
}
tex_star = function(x){
qui = nchar(x) > 0
x[qui] = paste0("$^{", x[qui], "}$")
x
}
tex_multicol = function(x, add_rule = FALSE){
nb_multi = c(1)
index = 1
names_multi = x_current = x[1]
for(val in x[-1]){
if(val == x_current){
nb_multi[index] = nb_multi[index] + 1
} else {
index = index + 1
nb_multi[index] = 1
names_multi[index] = x_current = val
}
}
names_multi_raw = names_multi
for(i in seq_along(nb_multi)){
if(nb_multi[i] > 1){
names_multi[i] = paste0("\\multicolumn{", nb_multi[i], "}{c}{", names_multi[i], "}")
}
}
res = paste0(paste(names_multi, collapse = " & "), " \\\\ ")
if(add_rule){
my_rule = c()
start = 2 + c(0, cumsum(nb_multi))
end = 1 + cumsum(nb_multi)
for(i in seq_along(nb_multi)){
if(grepl("[^ ]", names_multi_raw[i])){
my_rule[i] = paste0("\\cmidrule(lr){", start[i], "-", end[i], "}")
}
}
res = paste0(res, paste0(my_rule, collapse = " "))
}
return(res)
}
rename_fe = function(fe_all, dict){
# Function used to rename the FEs
res = c()
for(i in seq_along(fe_all)){
fe = fe_all[i]
if(fe %in% names(dict)){
res[i] = dict[fe]
} else if(grepl("\\^", fe)){
fe_split = strsplit(fe, "\\^")[[1]]
who = fe_split %in% names(dict)
fe_split[who] = dict[fe_split[who]]
res[i] = paste(fe_split, collapse = "-")
} else {
res[i] = fe
}
}
res
}
uniquify_names = function(x){
# x: vector of names
# we make each value of x unique by adding white spaces
if(length(x) == 0) return(NULL)
x_unik = unique(x)
if(length(x_unik) == length(x)) return(x)
x[nchar(x) == 0] = " "
x = gsub(" +$", " ", x)
x_unik = unique(x)
tab = rep(0, length(x_unik))
names(tab) = x_unik
x_new = x
for(i in seq_along(x)){
n = tab[x[i]]
if(n > 0){
x_new[i] = paste0(x_new[i], sprintf("% *s", n, ""))
}
tab[x[i]] = n + 1
}
x_new
}
extralines_extractor = function(x, name = NULL, tex = FALSE){
# x must be a one sided formula
# name: name of the listed element (empty = "")
# => returns a named list
is_name = !is.null(name) && nchar(name) > 0
# extralines registered
el_default = getOption("fixest_extralines")
key_registered = names(el_default)
# fitstat
fitstat_fun_types = fitstat(give_types = TRUE)
fitstat_type_allowed = fitstat_fun_types$types
type_alias = fitstat_fun_types$type_alias
# The variable(s) requested
current_vars = attr(terms(x), "term.labels")
if(length(current_vars) > 1 && is_name){
stop_up("You cannot give list names in 'extralines' when several values are ",
"summoned via a formula. Simply remove the name associated to the ",
"formula to make it work (concerns '", name, "').", up = 2)
}
valid_keys = c(key_registered, fitstat_type_allowed)
if(!all(current_vars %in% valid_keys)){
pblm = setdiff(current_vars, valid_keys)
stop_up("Argument 'extralines' can be a one-sided formula whose variables refer ",
"to the macros registered using the function 'extralines_register' or ",
"fit statistics (valid fitstat keywords). ",
"PROBLEM: the values{$s, enum, were?pblm} not valid.", up = 2)
}
if(length(current_vars) == 0){
extralines = list()
} else {
el_tmp = list()
for(i in seq_along(current_vars)){
key = current_vars[i]
if(key %in% key_registered){
element = el_default[[key]]
if(is_name){
el_tmp[[name]] = element$fun
} else {
el_tmp[[element$alias]] = element$fun
}
} else {
# this is a fit statistic
fun = eval(str2lang(paste0("function(est) fitstat(est, '", key, "', simplify = TRUE)[[1]]")))
if(is_name){
el_tmp[[name]] = fun
} else {
my_alias = if(tex) fitstat_fun_types$tex_alias else fitstat_fun_types$R_alias
el_tmp[[my_alias[[key]]]] = fun
}
}
}
}
el_tmp
}
insert = function(x, y, i){
# x = list(a = 1, b = 2, c = 3) ; y = list(u = 33, v = 55) ; i = 2 ; insert(x, y, i)
# we insert y into x in location i
# we don't lose the names!
mode = mode(x)
if(!mode %in% c("list", "numeric", "logical", "character", "integer")){
stop("Internal error: the current mode (", mode, ") is node supported in insert().")
}
n_x = length(x)
if(n_x == 0){
return(y)
}
n_y = length(y)
res = vector(mode, n_x + n_y)
names_x = names(x)
if(is.null(names_x)) names_x = character(n_x)
names_y = names(y)
if(is.null(names_y)) names_y = character(n_y)
if(i > n_x){
res[1:n_x] = x
res[n_x + 1:n_y] = y
names(res) = c(names_x, names_y)
} else if(i == 1){
res[1:n_y] = y
res[n_y + 1:n_x] = x
names(res) = c(names_y, names_x)
} else {
res[1:(i-1)] = x[1:(i-1)]
res[(i-1) + 1:n_y] = y
res[(i+n_y):(n_x + n_y)] = x[i:n_x]
names(res) = c(names_x[1:(i-1)], names_y, names_x[i:n_x])
}
return(res)
}
is_fixest_model = function(x){
any(c("fixest", "fixest_list", "fixest_multi") %in% class(x))
}
expand_list_vector = function(x){
# we transform list("A" = 2, "B", "C" = 3) into c("A", "A", "B", "C", "C", "C")
x_names = names(x)
if(is.null(x_names)){
# either list("m", "f") or c("a", "a", "c")
x_new = unlist(x)
} else {
# ex: list("M", "F"=2)
x_new = c()
for(j in seq_along(x)){
if(nchar(x_names[j]) == 0){
x_new = c(x_new, x[j])
} else {
x_new = c(x_new, rep(x_names[j], x[j]))
}
}
}
return(x_new)
}
tex.nice = function(x, n_models){
x = unlist(strsplit(x, "\n"))
n = n_models + 1
#
# I) dealing with amps
#
x_split_amp = strsplit(x, "(?<!\\\\)&", perl = TRUE)
qui_amp = lengths(x_split_amp) == n & !sapply(x_split_amp, function(x) any(grepl("midrule", x)))
mat_amp = matrix(unlist(x_split_amp[qui_amp]), ncol = n, byrow = TRUE)
mat_amp[, 1:(n-1)] = apply(mat_amp[, 1:(n-1), drop = FALSE], 2, format)
if(any(grepl("\\", mat_amp, fixed = TRUE))){
# we fix the \\ problem that will count for one character eventually
who_slash = which(grepl("\\", mat_amp, fixed = TRUE))
slash_count = lengths(gregexpr("\\", mat_amp[who_slash], fixed = TRUE))
mat_amp[who_slash] = paste0(mat_amp[who_slash], sprintf("% *s", slash_count, " "))
}
amp_new = apply(mat_amp, 1, paste, collapse = "&")
x[qui_amp] = amp_new
#
# II) dealing with tabs
#
# We assume everything is properly formatted
x_begin = pmax(lengths(strsplit(x, "\\begin{", fixed = TRUE)) - 1, 0)
x_end = pmax(lengths(strsplit(x, "\\end{", fixed = TRUE)) - 1, 0)
tabs = pmax(cumsum(x_begin) - cumsum(x_end) - x_begin, 0)
res = paste0(sprintf("% *s", tabs*3, " "), x)
res = gsub("^ ([^ ])", "\\1", res)
res
}
check_set_adjustbox = function(adjustbox, up = 0){
set_up(up + 1)
check_arg(adjustbox, "NULL scalar(character, strict logical, numeric) GT{0}")
if(is.null(adjustbox)){
# nothing
} else if(isTRUE(adjustbox)){
adjustbox = "width = \\textwidth, center"
} else if(isFALSE(adjustbox)){
adjustbox = NULL
} else if(is.numeric(adjustbox)){
if(adjustbox > 3){
warn_up("When 'adjustbox' is a number, the unit is the text-width. ",
"Hence a value of {%.2f ? adjustbox} may be too large.")
}
adjustbox = paste0("width = ", adjustbox, "\\textwidth, center")
} else if(grepl("(?i)^[[:digit:]\\.]+ *t(w|h)$", trimws(adjustbox))){
adj_nbr = gsub("[^[:digit:]\\.]", "", adjustbox)
if(!is_numeric_in_char(adj_nbr)){
stop_up("The number in the argument 'adjustbox' (equal to '", adjustbox,
"') could not be parsed. Please revise.")
}
adj_unit = if(grepl("(?i)tw", adjustbox)) "\\textwidth" else "\\textheight"
adjustbox = paste0("width = ", adj_nbr, adj_unit, ", center")
}
adjustbox
}
tag_gen = function(){
# What's the fuss here?
# We want to ensure that the tags are unique even if
# the seed is reset, that's why we make all that.
# Why? because otherwise, if two identical tags are used in a tex
# document it will fail to compile.
# => granted that's almost impossible, but that's still possible.
# (ex: two identical code sections with set.seed in Rmarkdown)
#
# we only use letters because tex macro names only allow letters
id = getOption("fixest_tag")
if(is.null(id)) id = 1
options(fixest_tag = id + 1)
items = letters
# Below: to ensure unicity even if the seed is the same
n_shifts = id %% 26
n_reshuffle = id %/% 26
if(n_reshuffle > 0){
for(i in 1:n_reshuffle){
items = sample(items)
}
}
if(n_shifts > 0){
items = c(items[-(1:n_shifts)], items[1:n_shifts])
}
# 26 ** 5 = 11,881,376
tag = paste0(sample(items, 6, replace = TRUE), collapse = "")
return(tag)
}
check_set_page_width = function(page.width, up = 0){
set_up(up + 1)
check_set_value(page.width,
"match(fit, a4, us) | vector(character, numeric) GT{0} len(1, 2) no NA")
if(isTRUE(attr(page.width, "no-margin"))){
# Nothing => already set
} else if(identical(page.width, "fit")){
# nothing
} else if(identical(page.width, "a4")){
page.width = c("21cm", "17cm")
} else if(identical(page.width, "us")){
page.width = c("21.6cm", "16.5cm")
} else if(is.numeric(page.width)){
if(length(page.width) == 1){
page.width = paste0(c(page.width, page.width - 4), "cm")
} else {
page.width = paste0(c(page.width[1], page.width[1] - 2*page.width[2]), "cm")
}
} else {
# page.fit is a character scalar
# format: 21:2cm
# 21 is the total width
# 2 is "optional". The margin width of ONE side
# cm: the unit
if(length(page.width) > 1){
page.width = paste0(page.width, collapse = ", ")
}
unit = .dsb("'[[:alpha:] ]+$'x, w, L?page.width")
if(!unit %in% c("cm", "mm", "in", "pt", "em", "ex")){
stop_up("The argument 'page.width' must represent a 'Latex' measure, ex: 12cm. ",
"It can be optionally followed by a side margin width, as in '21, 2cm'. ",
"Problem: a valid unit could not be found in '", unit,"'.")
}
# Now the numbers
numbers = .dsb("'[[:alpha:]]'R, '[:,;]'S, w, s ? page.width")
if(!length(numbers) %in% 1:2 || !all(sapply(numbers, is_numeric_in_char))){
stop_up("The argument 'page.width' must represent a 'Latex' measure, ex: 12cm. ",
"It can be optionally followed by a side margin width, as in '21, 2cm'. ",
"Problem: the current format could not be parsed ('", page.width, "').")
}
if(length(numbers) == 1){
numbers[2] = numbers[1]
} else {
numbers = as.numeric(numbers)
numbers[2] = numbers[1] - 2*numbers[2]
}
page.width = paste0(c(numbers[1], numbers[2]), unit)
}
attr(page.width, "no-margin") = TRUE
page.width
}
is_Rmarkdown = function(){
"knitr" %in% loadedNamespaces() && !is.null(knitr::pandoc_to())
}
path_to_relative = function(x){
# orig = "C:/Users/berge028/Google Drive/R_packages/fixest/fixest"
# dest = "C:/Users/berge028/Google Drive/R_packages/automake/automake/NAMESPACE"
# I'm not sure it works perfectly well on linux...
dest = normalizePath(x, "/", mustWork = FALSE)
orig = normalizePath(".", "/", mustWork = FALSE)
if(dest == orig) return(".")
dest_split = strsplit(dest, "/")[[1]]
orig_split = strsplit(orig, "/")[[1]]
n_d = length(dest_split)
n_o = length(orig_split)
n_od = min(n_d, n_o)
i_common = which.max(dest_split[1:n_od] != orig_split[1:n_od])
# I'm not sure of that on linux....
if(i_common == 1){
if(dest_split[1] == orig_split[1]){
# means all are the same => inclusion
i_common = n_od
} else if(dest_split[1] != orig_split[1]){
# different roots? => can't do much
return(dest)
}
} else {
i_common = i_common - 1
}
# common = paste0(dest_split[1:i_common], collapse = "/")
# From orig, we need to go up to the common dir
if(i_common == n_o){
up = "./"
} else {
n_up = n_o - i_common
up = dsb("`n_up`*c!../")
}
if(i_common == n_d){
down = ""
} else {
down = paste(dest_split[(i_common + 1):n_d], collapse = "/")
}
relpath = paste0(up, down)
relpath
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.