15  Ekonometrie v R

Skupina balíků sdružená v tidyverse představuje kompaktní sadu nástrojů pro práci s daty. Sdílejí datové struktury (tabulky v tidy formátu) i logiku ovládání. Bohužel podobná unifikace chybí u nástrojů, které výzkumník potřebuje pro ekonometrickou (statistickou) analýzu dat, která následuje po zpracování dat. Ekonometrické nástroje jsou v R rozptýleny v mnoha balících různé kvality, které nejsou žádným způsobem koordinovány nebo unifikovány. To velmi znepříjemňuje ekonometrickou analýzu dat v R. I přesto R poskytuje všechny základní a mnoho pokročilých nástrojů a v zásadě pokrývá všechny běžné potřeby výzkumníků.

Účelem této kapitoly není vysvětlovat teorii ekonometrické analýzy nebo diskutovat vhodnost nasazení jednotlivých metod nebo designu regresní analýzy (identifikační strategie), ale seznámit čtenáře se základní logikou fungování ekonometrických nástrojů v R. Právě základní logika fungování je totiž něco, co drtivá většina ekonometrických nástrojů v R sdílí.

Na počátku ekonometrické analýzy má výzkumník data a představu o tom, jak svět funguje (o data generujícím procesu). Pro naprostou většinu ekonometrických nástrojů (a všechny základní) je vhodné připravit si data do podoby tabulky v tidy formátu. Představa o fungování světa (tj. o vztazích mezi proměnnými v datové tabulce) se formalizuje do podoby modelu. Jeho podoba se předává R v podobě speciální datové třídy formula.

Data a specifikovaný model jsou nezbytnými vstupy pro estimátor. Estimátor je typicky funkce, která provede odhad parametrů modelu a vytvoří výstupní objekt. Tento objekt obsahuje typicky pouze samotný odhad parametrů modelu, tabulku dat použitých pro odhad, rezidua, vyrovnané hodnoty a další doplňující údaje. Typicky neobsahuje žádné testy, robustní směrodatné chyby a podobně. Analýza a zobrazení odhadu (výsledků) se v R typicky provádí pomocí specializovaných funkcí, pro které je výstup estimační funkce vstupem.

V této kapitole se naučíte:

Základní část kapitoly spočívá na moderním ekonometrickém balíku fixest.

V rozšiřujících materiálech najdete jak:

15.1 Specifikace modelu pomocí formula

Fungování ekonometrických nástrojů můžeme ilustrovat na příkladu datasetu s výškou, váhou, věkem, sportem, a pohlavím sportovců, kteří se zúčastnili olympijských her v Londýně v roce 2012 (dostupný v VGAMdata::oly12):

VGAMdata::oly12 %>% 
  mutate(
    Sport = str_extract(Sport, "[:alnum:]*")
  ) %>% 
  select(
    Height, Weight, Sex, Age, Sport
  ) %>% 
  as_tibble %>%
  drop_na() %>% 
  mutate_if(is.factor,as.character) -> oly12

print(oly12, n=5)
# A tibble: 9,038 × 5
  Height Weight Sex     Age Sport    
   <dbl>  <int> <chr> <int> <chr>    
1   1.7      60 M        23 Judo     
2   1.93    125 M        33 Athletics
3   1.87     76 M        30 Athletics
4   1.78     85 F        26 Athletics
5   1.82     80 M        27 Handball 
# ℹ 9,033 more rows

Nejprve nás bude zajímat vztah mezi váhou a výškou sportovců. Ten si můžeme vykreslit pomocí jednoduchého bodového grafu:

Zdá se, že existuje lineární vztah mezi váhou a výškou sportovců, který můžeme popsat rovnicí: \[Weight = \alpha + \beta Height + \varepsilon\] kde \(\alpha\) a \(\beta\) jsou neznámé parametry, které chceme odhadnout a \(\varepsilon\) je náhodná složka.

Tuto rovnici si můžeme představit jako přímku, která je proložena mrakem pozorování:

`geom_smooth()` using formula = 'y ~ x'

Rovnici však musíme zapsat tak, aby byla srozumitelná i pro R. K tomu slouží objekty třídy formula. Jejich formulace následuje syntax:

LHS ~ RHS

Tedy výrazy na levé straně (LHS) \(=\) (zastoupené znakem ~) výrazy na pravé straně (RHS). Rovnici vysvětlující váhu bychom tedy mohli popsat jako:

model <- Weight ~ Height
class(model)
[1] "formula"

V ekonomii je závislá (vysvětlovaná) proměnná (tedy proměnná na levé straně rovnice) funkcí více než jedné nezávislé (vysvětlující) proměnné. Pokud bychom chtěli vysvětlovat váhu nejen výškou, ale i věkem, potom bychom rozšířili specifikaci následujícím způsobem:

model <- Weight ~ Height + Age

Dodatečná proměnné jsou přidávány pomocí +.

Za povšimnutí stojí, že při použití takovéto specifikace by byl model odhadnut s úrovňovou konstantou \(\alpha\) – i když není explicitně v rovnici přítomna. Explicitně se naopak musí zadat její nepřítomnost připojením -1 nebo 0:

model <- Weight ~ Height - 1
model <- Weight ~ Height + 0

Jména proměnných v rovnici musí odpovídat jménům sloupců v datové tabulce. Zásadní výhodou R je, že do rovnice je možné zadat i transformace dat:

model <- Weight ~ log(Height)
model <- Weight ~ Height >= 1.8

První varianta například odpovídá specifikaci:

\[Weight = \alpha + \beta \log(Height) + \varepsilon\]

Ve druhé je váha vysvětlována dummy (umělou proměnnou). Výraz Height >= 1.8 vytvoří logickou proměnnou, která je při volání estimační funkce transformována na vektor s hodnotami 1 a 0.

Není tedy potřeba vytvářet nové transformované sloupce v datové tabulce. To je velký rozdíl mezi R a Statou, Gretlem,… (Jakkoliv je v některých případech vytvoření transformovaných proměnných do tabulky doporučeníhodné.)

V rovnici může být přímo přítomna celá řada funkcí – s výjimkou těch, jejichž interpretace by nebyla jasná. Pokud bychom například chtěli odhadnout modelovou specifikaci

\[Weight = \alpha + \beta (Height + Age) + \varepsilon\]

potom je nutná sdělit R, že má nejdříve sečíst Height a Age a pro vysvětlení Weight použít až výsledný součet. Pro tyto účely se používá funkce I(). Ta sděluje, že se mají nejprve provést operace definované uvnitř funkce a pro odhad použít až výsledek.

Výše uvedený příklad by se tak R tlumočil jako

model <- Weight ~ I(Height + Age)

Třída formula umožňuje jednoduše jednoduše definovat i interakční členy pomocí operátorů dvojtečka (:) a hvězdička (*):

model <- Weight ~ Height:Sex

Chápe R jako

\[Weight = \alpha + \beta Height\times Sex_{male} + \gamma Height\times Sex_{female} + \varepsilon\] Tato rovnice vyjadřuje možný odlišný mezní efekt výšky na váhu u mužů a žen. Výsledné proložení mrakem pozorování by se tak v bodovém grafu výše vlastně rozpadlo na dvě přímky s odlišným sklonem (parametry \(\beta\) a \(\gamma\)) a shodnou úrovňovou konstantou \(\alpha\).

Další možností jak popsat interakci proměnných je *:

model <- Weight ~ Height*Sex

Tato formula odpovídá jiné rovnici:

\[Weight = \alpha + \beta Height + \epsilon Sex_{male} + \gamma Height\times Sex_{male} + \varepsilon\] Tato specifikace umožňuje odlišnost regresní křivky pro muže a ženy jak ve sklonu, tak v úrovňové konstantě.

Zvláštní význam má i symbol stříšky (^). Rovnici

\[Weight = \alpha + \beta_1 Height + \beta_2 Age + \beta_3 Sex_{male} + \gamma_1 Height\times Sex_{male} + \gamma_2 Height\times Age + \gamma_3 Sex_{male}\times Age + \varepsilon\] můžeme zapsat jako:

model <- Weight ~ (Height + Age + Sex)^2

Výraz se přeloží tak, že výsledná rovnice obsahuje jednotlivé proměnné a také jejich interakce do, v tomto případě, druhého řádu.

Pro třídu formula jsou implementovány i některé zajímavé metody. Velmi užitečná je například funkce update() s velmi prostou syntaxí:

update(old,new,...)

Kde old je původní rovnice a new pravidla, která stanovují, jak se původní rovnice má upravit.

model <- Weight ~ Height + Age
print(model)
Weight ~ Height + Age

Takto můžeme zachovat všechny proměnné – reprezentované pomocí . a jen některou přidat nebo odebrat:

model %>% 
  update(. ~ . -Age)
Weight ~ Height

Nebo je možné nahradit celou stranu rovnice:

model %>% 
  update(. ~ Age)
Weight ~ Age

Objekty třídy formula a moderní balíčky v R

Většina moderních balíků pro datovou analýzu používá specifikaci modelů pomocí objektů třídy formula. Její možnosti jsou však omezené – zejména pokud jde o tzv. víceprvkové rovnice, které se používají stále později. Příkladem užití takové víceprvkové rovnice může být specifikace fixních efektů, který používá balík fixest (nebo—trochu jinak—i balík lfe):

Weight ~ Height + Age | country

Druhý “prvek” rovnice je oddělen znakem |. V tomto případě druhý prvek rovnice specifikuje proměnnou, který určuje použité fixní efekty. Víceprvkové rovnice nefungují správně, pokud jsou definovány jako objekty třídy formula. Je potřeba je definovat s pomocí balíku Formula. Objekt třídy formula lze konvertovat do třídy Formula pomocí funkce Formula::as.Formula().

15.2 Odhad modelu

Při volání estimační funkce se dohromady spojí všechny tři komponenty: modelová specifikace, data a estimátor.

Základní estimační funkcí je lm(), určená pro odhad lineárních modelů. Funkce přijímá celou řadu parametrů:

lm(formula, data, subset, weights, na.action, method = "qr", 
    model = TRUE, x = FALSE, y = FALSE, qr = TRUE, singular.ok = TRUE, 
    contrasts = NULL, offset, ...) 

Mezi základní patří:

  • formula je objekt třídy “formula”, který obsahuje symbolický popis rovnice, jejíž parametry se mají odhadnout.
  • data je tabulka (data.frame) která obsahuje data, která se mají použít. Jména sloupců v tabulce musí odpovídat proměnným použitým v parametru formula. Datová tabulka nemusí být specifikována. Pokud je tento parametr prázdný, potom R interpretuje jména v modelu jako jména proměnných z prostředí, ze kterého je funkce lm() volána.
  • subset je nepovinný parametr, který určuje, které pozorování (řádky) se mají použít pro odhad
  • weights je vektor vah. Pokud není zadán je pro odhad použit obyčejný OLS estimátor, ve kterém mají všechna pozorování stejnou váhu. V případě zadání vah je vráceno WLS.

Estimačních funkcí v R je mnoho. Většina z nich však používá (teměř) stejný interface jako lm().

Vytvoření matice plánu

Pro odhad použijeme model v následující specifikaci:

model <- Weight ~ Height + Age + I(Age^2) + Sex

Proměnné Weight, Height a Age jsou numerické spojité proměnné. Proměnná Sex je zcela jiný případ. Jsou to kategoriální proměnná, se dvěma úrovněmi (M – muž, F – žena). Taková proměnná nemůže vstoupit do regrese přímo. Je nutné ji transformovat na matici nul a jedniček. Transformovaná matice bude mít vždy o jeden sloupec méně, než kolik má transformovaná proměnná úrovní.

Estimační funkce si všechny vstupní faktory transformuje sama pomocí interního volání funkce model.matrix(). V rámci volání tento funkce se provedou i požadované transformace dat. V případě našeho modelu vytvoření druhé mocniny věku. Výsledkem volání model.matrix() je tedy matice plánu:

model.matrix(model, data = oly12) %>% head()
  (Intercept) Height Age I(Age^2) SexM
1           1   1.70  23      529    1
2           1   1.93  33     1089    1
3           1   1.87  30      900    1
4           1   1.78  26      676    0
5           1   1.82  27      729    1
6           1   1.82  30      900    0

Funkce model.matrix() provede požadované transformace numerických proměnných (přidán sloupec I(Age^2)) a pokud najde jinou než numerickou proměnnou (factor) provede jejich transformaci do matice binárních proměnných. Při této transformaci postupuje následovně. Prohlédne si faktor a určí referenční úroveň (level). Ta nebude ve výsledné matici obsažena, pokud by byla, model by nešel odhadnout. Pro každou zbývající úroveň vytvoří vektor, který nabývá hodnoty 1 pro pozorování s danou úrovní faktoru. V příkladu výše si model.frame() vybral jako referenční úroveň hodnotu F a vytvořil vektor SexM, který nabývá hodnoty 1 pro muže a 0 ve všech ostatních případech.

Pokud by byla vstupní kategoriální proměnná třídy character, potom by ji model.frame()nejprve transformoval na factor a pokračoval tak, jak je popsáno výše. Toto chování může být motivem pro to, aby uživatel sám provedl konverzi na faktor. Třída faktor totiž umožňuje nastavit, která úroveň má být použita jako referenční. Toho můžeme docílit pomocí několika cest.

První variantou je konverze do factor s pomocí base::factor(). Tato funkce umožňuje uživateli zadat vektor možných úrovní. První z nich se potom nastaví jako referenční:

factor(Sex$oly12, c("M","F"))

Tento postup lze aplikovat i na proměnné, které již jsou factor. Nevýhodou je nutnost zadávat všechny úrovně. Podobnou variantou je použití funkce forcats::as_factor(). Ta má vlastnost, že pořadí úrovní stanoví podle podle pořadí jejich výskytu v transformovaném vektoru. Problém této funkce je, že pracuje zvláštně s chybějícími hodnotami. (To se pravděpodobně bude během vývoje balíku ještě měnit.)

Další principiálně odlišnou variantou je nastavení referenční úrovně pomocí funkce stats::relevel() (alternativně forcats::fct_relevel()). Ta umožňuje prosté nastavení referenční úrovně:

factor(Sex$oly12) %>% relevel("M")

Po změně referenční úrovně bude výsledek následující:

oly12 %>% 
  mutate(
    Sex = factor(Sex) %>% relevel("M")
  ) %>% 
  model.matrix(model,.) %>% 
  head()
  (Intercept) Height Age I(Age^2) SexF
1           1   1.70  23      529    0
2           1   1.93  33     1089    0
3           1   1.87  30      900    0
4           1   1.78  26      676    1
5           1   1.82  27      729    0
6           1   1.82  30      900    1

Zejména v rámci průzkumu dat je zajímavá varianta provést konverzi na faktor v rámci modelu (formula) – všimněte si, že může být aplikována i na numerickou proměnnou:

Weight ~ Height + factor(Age) + Sex

Konverze numerických hodnot do faktoru je poměrně častá zvláště u dotazníkových dat, kde čísla mohou pouze kódovat hodnoty kategoriální proměnné. Dalším případem je například vytvoření fixních efektů pro roky.

Funkce model.matrix(), kterou si estimační funkce volá interně tedy vytvoří matici plánu, která je následně použité při samotném odhadu parametrů modelu. Odhadová procedura si liší podle použité estimační funkce. V případě lm() je to QR dekompozice (https://en.wikipedia.org/wiki/QR_decomposition), při použití glm() jsou to například různé numerické optimalizační algoritmy (defaultně IWLS:https://en.wikipedia.org/wiki/Iteratively_reweighted_least_squares).

15.3 Odhad lineárního modelu s balíkem fixest

V datové analýze se často používá balíček fixest, který obsahuje odhadové funkce feols(), feglm() () a fepois(). Výhodou tohoto balíku je:

  1. schopnost rychle odhadovat modely s velkým počtem fixníxh efektů,
  2. díky modifikovaným formulím (fml) odhadovat v rámci jednoho volání funkce více specifikací,
  3. stejně jako více specifikací na více subsamplech a
  4. zahrnout odhad robustních chyb do výstupního objektu bez nutnosti jeho zpětné modifikace.

Základní odahdová funkce má syntax podobnou lm():

feols(
  fml,
  data,
  vcov,
  weights,
  cluster,
  se,
  offset, subset, split, fsplit, split.keep, split.drop, ssc,
  panel.id, fixef, fixef.rm = "none",
  fixef.tol = 1e-06, fixef.iter = 10000, collin.tol = 1e-10,
  nthreads = getFixest_nthreads(), lean = FALSE, verbose = 0,
  warn = TRUE, notes = getFixest_notes(),
  only.coef = FALSE, combine.quick, demeaned = FALSE, mem.clean = FALSE,
  only.env = FALSE, env, ...
)

V tento moment jsou podstatné dvě odlišnosti oproti lm(). fixest rozumí vlastní modifikované verzi třídy fromula, která se jmenuje fml. Ta umožňuje nejen specifikovat víceprvkové rovnice, ale navíc i používat dodatečné speciální funkce.

Víceprvková rovnice se používá pro identifikaci proměnných, které se mají použít specifikaci fixních efektů. Rovnice

y ~ x | a + b

tak říká, že se y má regresovat na konstantu a proměnnou x s použitím proměnných a a b jako fixních efektů. V tomto případě není důležitý datový typ proměnných a a b – mouhý být typu factor, character, nebo třeba integer. Odhadová funkce použije tyto proměnné k demeanování matice plánu. To zásadně zrychlí odahad modelu. Toto zrychlení je zvláště markantní u ML modelů a při velkém počtu fixních efektů. Funkce ale následně už nevrátí odhady koeficientů pro jednotlivé fixní efekty. Pokud je potřeba je odhadnout, potom je nutné příslušnou proměnnou zařadit do “hlavní” části rovnice – zde například pro a:

y ~ x + factor(a) | b

fixest respektive fml nepoužívá jen víceprvkové rovnice. Umožňuje i použití speciálních funkcí – jak ukazuje následující příklad:

c(y1, y2) ~ x1 + csw(x2, x2^2) | sw0(species)

Použity jsou v ní tři speciální funkce. Na levé straně rovnice je použita funkce c(), která říká, že se modelová specifikace má odhadnout zvlášt pro dvě (mohlo bych jich být samozřejmě i více) závislé proměnné: y1 a y2. Zbývající dvě speciální funkce jsou příkladem tzv. stepwise funkcí. fixest jich má naimplementováno celkem pět:

  • sw()…nahrazuje sekvenčně členy uvedené ve funkci. Odhad rovnice y ~ x0 + sw(x1, x2) by tak vedl k odhadu dvou rovnic: y ~ x0 + x1 a y ~ x0 + x2.
  • sw0()…funguje podobně jako sw(), ale přidává i odhad bez proměnných uvedených ve funkci sw0(). Příklad y ~ x0 + sw0(x1, x2) by tak vedl k odhadu tří specifikací: y ~ x0 + x1, y ~ x0 + x2 a y ~ x0.
  • csw()…přidává uvedené proměnné do modelu kumulativně. Výsledkem y ~ x0 + csw(x1, x2) by tak byl odhad specifikací y ~ x0 + x1 a y ~ x0 + x1 +x2.
  • csw0()…podobně jako sw0() přidává variantu bez proměnných uvedených v csw0().
  • mvsw()…odhaduje odhady se všemi kombinacemi proměnných uvedených ve funkci.

Balík fixest nám tedy v jedné rovnici umožňuje “schovat” celý balík specifikací.

Oproti lm() umožňuje fixest odhadnout různé typy standardních chyb již v rámci volání základní estimační funkce. Pro nastavení této funkcionality slouží parametry vcov a cluster.

Parametr vcov přijímá (pro detaily viz nápověda):

  • typ standardních chyb: iid, hetero, twoway a další. Pokud je daný typ určený pro panelová data, potom je nutné zadat paramtr panel.id, který specifikuje, které proměnné identifikují pruřezovou (např. ID země) a časovou jednotku (např. rok).
  • výstupní objekt ze speciálních funkcí. Tímto zpúsobem fixest implemetnuje Conley standard errors, které zohledňují prostorovou autokorelace reziduí.
  • externě vypočítanou koverianční matici.

V případě klastrování chyb je potřeba použít parametr cluster, který přijímá jednostrannou rovnici obshující proměnné definující klastry pozorování.

Následující volání funkce vrací OLS odhady koeficientů v regresy váhy na výšku. Pohlaví je použito jako fixní efekty a standardní chyby jsou klastrovány na úrovni jednotlivých sportů:

feols(
    Height ~ Weight | Sex,
    data = oly12,
    vcov = hetero ~ Sport
)

Aplikace I

Na datech ISPV zkusím odhadnout tzv. gender pay gap. Připomínám, že data nejsou čištěná a jsou náhodně zpermutované – výsledky analýz tudíž nebudou dávat žádný smysl.

load("data/dplyr/ispv_training.RData")
ispv <- ispv %>% 
    mutate(
        POHLAVI = case_when(
            POHLAVI %in% c("1","M") ~ "M",
            TRUE ~ "Z"
        ),
        PRUMVYD = PRUMVYD %>% str_replace_all(",",".") %>% as.double(),
        DOBAZAM = DOBAZAM %>% str_replace_all(",",".") %>% as.double(),
        ISCO1 = CZISCO %>% str_sub(1,1),
        OBORVZD = as.integer(OBORVZD), 
        year = as.integer(year),
        foreigner = str_to_lower(STOBC) != "cz"
    )

Specificky budeme regresovat logaritmus mzdy na dummy pro ženu a sadu vysvětlujících proměnných: tenure (dobu zaměstnání), ISCO, délku zkušeností a její druhou mocninu a dummy pro cizince.

Hint: Baterii odhadnutých modelů je potřeba nějak vypsat do tabulky. K tomu slouží funkce fixest::etable().

library(fixest)

feols(
    log(PRUMVYD) ~ I(POHLAVI == "Z") + I(year - OBORVZD) + I((year - OBORVZD)^2) + csw0(DOBAZAM, ISCO1),
    data = ispv
) %>% 
    etable()
NOTE: 829 observations removed because of NA and infinite values (LHS: 101, RHS: 808).
      |-> this msg only concerns the variables common to all estimations
Notes from the estimations:
[x 1] The variable 'ISCO19' has been removed because of collinearity (see $collin.var).
                                    ..1               ..2               ..3
Dependent Var.:            log(PRUMVYD)      log(PRUMVYD)      log(PRUMVYD)
                                                                           
Constant              5.165*** (0.0935) 5.200*** (0.0998) 5.126*** (0.1564)
I(POHLAVI=="Z")TRUE    -0.0173 (0.0842)  -0.0134 (0.0843)  -0.0093 (0.0861)
I(year-OBORVZD)         0.0085 (0.0101)   0.0087 (0.0101)   0.0077 (0.0103)
(year-OBORVZD) square  -0.0002 (0.0003)  -0.0003 (0.0003)  -0.0002 (0.0003)
DOBAZAM                                  -0.0033 (0.0033)  -0.0034 (0.0034)
ISCO11                                                      0.1880 (0.3396)
ISCO12                                                     -0.0004 (0.1635)
ISCO13                                                      0.0910 (0.1645)
ISCO14                                                      0.2581 (0.1927)
ISCO15                                                      0.0746 (0.1696)
ISCO16                                                      0.7034 (0.5625)
ISCO17                                                      0.1820 (0.1710)
ISCO18                                                     -0.0003 (0.1553)
_____________________ _________________ _________________ _________________
S.E. type                           IID               IID               IID
Observations                        171               171               171
R2                              0.00636           0.01215           0.04508
Adj. R2                        -0.01149          -0.01166          -0.02744
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Data obsahují IČO zaměstnavatelů. Zkusíme je vložit do regrese jako fixní efekt a porovnáme výsledky s nimi a bez nich. ISCO nyní také definujeme jako fixní efekt.

feols(
    log(PRUMVYD) ~ I(POHLAVI == "Z") + I(year - OBORVZD) + I((year - OBORVZD)^2) + csw0(DOBAZAM) | ISCO1 + sw0(ICO),
    data = ispv
) %>% 
    etable()
NOTE: 829 observations removed because of NA and infinite values (LHS: 101, RHS: 808).
      |-> this msg only concerns the variables common to all estimations
                                   ..1              ..2              ..3
Dependent Var.:           log(PRUMVYD)     log(PRUMVYD)     log(PRUMVYD)
                                                                        
I(POHLAVI=="Z")TRUE   -0.0129 (0.0823) -0.0093 (0.0794) -0.0237 (0.2563)
I(year-OBORVZD)        0.0073 (0.0093)  0.0077 (0.0097) -0.0261 (0.0696)
(year-OBORVZD) square -0.0002 (0.0002) -0.0002 (0.0002)  0.0005 (0.0019)
DOBAZAM                                -0.0034 (0.0041)                 
Fixed-Effects:        ---------------- ---------------- ----------------
ISCO1                              Yes              Yes              Yes
ICO                                 No               No              Yes
_____________________ ________________ ________________ ________________
S.E.: Clustered              by: ISCO1        by: ISCO1        by: ISCO1
Observations                       171              171              171
R2                             0.03892          0.04508          0.78571
Within R2                      0.00436          0.01075          0.02203

                                   ..4
Dependent Var.:           log(PRUMVYD)
                                      
I(POHLAVI=="Z")TRUE    0.0890 (0.5862)
I(year-OBORVZD)       -0.0337 (0.0766)
(year-OBORVZD) square  0.0007 (0.0022)
DOBAZAM               -0.0169 (0.0477)
Fixed-Effects:        ----------------
ISCO1                              Yes
ICO                                Yes
_____________________ ________________
S.E.: Clustered              by: ISCO1
Observations                       171
R2                             0.79973
Within R2                      0.08599
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Nyní můžeme zkusit odhadnout specifikaci bez fixních efektů pro každé ISCO zvlášť. Pro odhad stejné sady rovnic na různých dílčích vzorcích lze ve fixest použít parametry split nebo fsplit z níchž oba přijímají jednostranou rovnici idetifikující proměnnou, která definuje vzorky. V případě použití split se rovnice odhnou jen pro dílčí vzorky. U fsplit se specifikace odhadne i pro plný vzorek.

Omezení této varianty je, že se vzorky nemohou překrývat.

feols(
    log(PRUMVYD) ~ I(POHLAVI == "Z") + I(year - OBORVZD) + I((year - OBORVZD)^2) + DOBAZAM,
    data = ispv,
    fsplit = ~ISCO1,
    # Standardní chyby klastrujeme podle firmy.
    cluster = ~ICO
) %>% 
    etable()
NOTE: 829 observations removed because of NA and infinite values (LHS: 101, RHS: 808, split: 2).
Notes from the estimations:
[x 2] Some variables have been removed because of collinearity (see $collin.var).
                                    ..1           ..2               ..3
Sample (ISCO1)              Full sample             1                 2
Dependent Var.:            log(PRUMVYD)  log(PRUMVYD)      log(PRUMVYD)
                                                                       
Constant              5.200*** (0.1001)   5.198 (NaN) 5.165*** (0.2280)
I(POHLAVI=="Z")TRUE    -0.0134 (0.0833)  0.4551 (NaN)  -0.1446 (0.1674)
I(year-OBORVZD)         0.0087 (0.0100) -0.0143 (NaN)   0.0027 (0.0171)
(year-OBORVZD) square  -0.0003 (0.0002)                2.34e-5 (0.0003)
DOBAZAM                -0.0033 (0.0029)                -0.0046 (0.0039)
_____________________ _________________ _____________ _________________
S.E. type                       by: ICO NA (not-ava.)           by: ICO
Observations                        171             3                28
R2                              0.01215             1           0.09461
Adj. R2                        -0.01166      NaNe+Inf          -0.06285

                                    ..4               ..5               ..6
Sample (ISCO1)                        3                 4                 5
Dependent Var.:            log(PRUMVYD)      log(PRUMVYD)      log(PRUMVYD)
                                                                           
Constant              5.325*** (0.2493) 5.564*** (0.2658) 4.833*** (0.2547)
I(POHLAVI=="Z")TRUE     0.2681 (0.2511)  -0.0908 (0.4902)   0.1642 (0.2019)
I(year-OBORVZD)         0.0016 (0.0237)   0.0367 (0.0356)   0.0051 (0.0263)
(year-OBORVZD) square  -0.0003 (0.0007)  -0.0010 (0.0010) -8.73e-5 (0.0006)
DOBAZAM                -0.0121 (0.0124) -0.0240* (0.0110)  0.0216* (0.0089)
_____________________ _________________ _________________ _________________
S.E. type                       by: ICO           by: ICO           by: ICO
Observations                         26                14                23
R2                              0.15073           0.36220           0.27670
Adj. R2                        -0.01104           0.07873           0.11597

                                ..7               ..8               ..9
Sample (ISCO1)                    6                 7                 8
Dependent Var.:        log(PRUMVYD)      log(PRUMVYD)      log(PRUMVYD)
                                                                       
Constant                5.833 (NaN) 5.493*** (0.1905) 5.156*** (0.3523)
I(POHLAVI=="Z")TRUE                 -0.5454. (0.2770)  -0.1785 (0.1839)
I(year-OBORVZD)                     -0.0704* (0.0313)   0.0267 (0.0238)
(year-OBORVZD) square                0.0021* (0.0010)  -0.0007 (0.0006)
DOBAZAM                             0.0291** (0.0085)  -0.0110 (0.0117)
_____________________  ____________ _________________ _________________
S.E. type             NA (not-ava.)           by: ICO           by: ICO
Observations                      1                22                35
R2                               --           0.36008           0.07818
Adj. R2                          --           0.20951          -0.04473

                                   ..10
Sample (ISCO1)                        9
Dependent Var.:            log(PRUMVYD)
                                       
Constant              4.927*** (0.2249)
I(POHLAVI=="Z")TRUE    -0.2053 (0.3180)
I(year-OBORVZD)         0.0567 (0.0401)
(year-OBORVZD) square  -0.0012 (0.0009)
DOBAZAM                -0.0172 (0.0121)
_____________________ _________________
S.E. type                       by: ICO
Observations                         19
R2                              0.19542
Adj. R2                        -0.03446
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Zvláštní hodnoty v tabulkách jsou dány malým počtem pozorování.)

fixest a panelová data

Odhadhové funkce mají i další vlastnosti, které se hodí pro práci s panelovými daty. První z nich je parametr panel.id, který umožnǔje (jako jednostrannou rovnici) identifikovat ID průřezové a časové jednotky.

Vytvoříme si nový (nesmyslný) dataset:

ispv2 <- ispv %>% 
    mutate(
        ICO = rep(1:4, n())[1:n()] 
    ) %>% 
    group_by(
        ICO, year
    ) %>% 
    summarise(
        HW = mean(PRUMVYD, na.rm = TRUE), 
        ISCO = min(ISCO1), 
        .groups = "drop"
    ) %>% 
    drop_na()

Demonstrujeme nastavení panel.id:

feols(
    log(HW) ~ ISCO,
    data = ispv2,
    cluster = ~ICO,
    panel.id = ~ICO+year
) %>% 
    etable()
                                  .
Dependent Var.:             log(HW)
                                   
Constant        5.283*** (1.14e-13)
ISCO1              -0.0097 (0.0762)
ISCO2             -0.1332* (0.0382)
_______________ ___________________
S.E.: Clustered             by: ICO
Observations                     34
R2                          0.08990
Adj. R2                     0.03119
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

V případě panelových dat je možné použít speciální funkci, která vytváří zpožděné (\(t-n\))/zrychlené (\(t+n\)) proměnné: l()/f(). Existuje i funkce, která vytváří diference: d(). Například můžeme nyní vysvětlovat mzdy zpožděným ISCO:

feols(
    log(HW) ~ l(ISCO),
    data = ispv2,
    cluster = ~ ICO,
    panel.id = ~ ICO + year
) %>% 
    etable()
NOTE: 5 observations removed because of NA values (RHS: 5).
                                .
Dependent Var.:           log(HW)
                                 
Constant        5.226*** (0.0536)
l(ISCO,1)2       -0.0322 (0.0289)
_______________ _________________
S.E.: Clustered           by: ICO
Observations                   29
R2                        0.00529
Adj. R2                  -0.03155
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

fixest a grafický výstup

Z fixest můžete používat i alternativní prezentace výsledků. Buď jejich zobrazení pomocí funkce coefplot():

feols(
    log(PRUMVYD) ~ I(POHLAVI == "Z") + I(year - OBORVZD) + I((year - OBORVZD)^2) + csw0(DOBAZAM, ISCO1),
    data = ispv
) %>% 
    coefplot() 
NOTE: 829 observations removed because of NA and infinite values (LHS: 101, RHS: 808).
      |-> this msg only concerns the variables common to all estimations
Notes from the estimations:
[x 1] The variable 'ISCO19' has been removed because of collinearity (see $collin.var).

…nebo pro prezentaci můžete využít funkce z jiných balíků. Za pozornost slouží zejména moderní balík modelsummary a funkce modelplot().

library(modelsummary)
p <- feols(
    log(PRUMVYD) ~ I(POHLAVI == "Z") + I(year - OBORVZD) + I((year - OBORVZD)^2) + csw0(DOBAZAM, ISCO1),
    data = ispv
) %>% 
    modelplot()
NOTE: 829 observations removed because of NA and infinite values (LHS: 101, RHS: 808).
      |-> this msg only concerns the variables common to all estimations
Notes from the estimations:
[x 1] The variable 'ISCO19' has been removed because of collinearity (see $collin.var).
p

Její obrovskou výhodou je, že produkuje objekt třídy ggplot, který lze dále modifikovat:

p +
    scale_color_brewer(
        type = "qual"
    ) +
    theme_classic(
        base_size = 8
    ) +
    theme(
        legend.position = "bottom",
        legend.direction = "vertical"
    )

Největší svobodu ve vizualizaci výsledků dává uživateli manuální zpracování odahdů. Zde je ale malý rpoblém Balí ggplot umožňuje pracovat s tabulkami – nikoliv objekty, které produkují odhadové funkce. Ty je nutné konverotvat na tabulky. Přesně tuto funkcionalitu posktuje balík broom. Základní funkcí balíků, je tidy(), která konvertuje různé objekty na tabulky – z výsledků regrese tak například vytváří tabulku s koeficienty:

library(broom)

feols(
    log(PRUMVYD) ~ I(POHLAVI == "Z") + I(year - OBORVZD) + I((year - OBORVZD)^2) + DOBAZAM,
    data = ispv
) %>%
  tidy()
NOTE: 829 observations removed because of NA and infinite values (LHS: 101, RHS: 808).
# A tibble: 5 × 5
  term                       estimate std.error statistic   p.value
  <chr>                         <dbl>     <dbl>     <dbl>     <dbl>
1 "(Intercept)"              5.20      0.0998      52.1   8.92e-105
2 "I(POHLAVI == \"Z\")TRUE" -0.0134    0.0843      -0.158 8.74e-  1
3 "I(year - OBORVZD)"        0.00873   0.0102       0.860 3.91e-  1
4 "I((year - OBORVZD)^2)"   -0.000254  0.000257    -0.989 3.24e-  1
5 "DOBAZAM"                 -0.00325   0.00330     -0.986 3.26e-  1

Nevýhodou broom je omezená podpora modelů/objektů. Pro každou třídu objektů musí být v balíku implementována zvláštní funkce – například tidy.lm() atp.

S tabulku vzniklou z tidy() můžeme vytvořit zcela vlastní vizualizaci:

feols(
    log(PRUMVYD) ~ I(POHLAVI == "Z") + I(year - OBORVZD) + I((year - OBORVZD)^2) + DOBAZAM,
    data = ispv
) %>%
  tidy(conf.int = TRUE) %>% 
  filter(!str_detect(term,"Intercept")) %>% 
  ggplot(
    aes(x = term, y = estimate, ymin = conf.low, ymax = conf.high)
  ) +
  geom_errorbar(
    width = 0.1
  ) +
  geom_point() +
  theme_bw()
NOTE: 829 observations removed because of NA and infinite values (LHS: 101, RHS: 808).

15.4 Odhad více modelů

Často můžeme chtít odhadnout model v různých specifikacích, nebo na různých vzorcích dat.

Odhad různých specifikací na stejných datech

Častá úloha je odhad různých modelových specifikací na stejném vzorku pozorování. fixest má podobný iterátor vestavěný a lze Pro tyto účely doporučuji následující postup:

  1. Vytvořit si list se všemi rovnicemi (specifikacemi)
  2. Odhadnout model pro všechny specifikace pomocí funkce map() z balíku purrr

Nejprve vytvoříme různé modelové specifikace s pomocí funkce update():

Models <- list(
    # Baseline model
    model,
    # Přidána interakce Sex*Height
    model %>% update(.~. + Sex*Height),
    # Přidán fixní efekt na zemi původu atleta
    model %>% update(.~. + factor(Country))
)

Nyní pomocí purrr::map() zavoláme lm() na všechny prvky listu Models – tedy na všechny modelové specifikace:

Spec_models <- purrr::map(Models, lm, data = VGAMdata::oly12)
Spec_models <- purrr::map(Models, function(x) lm(x, data = VGAMdata::oly12))
Spec_models <- purrr::map(Models, ~lm(., data = VGAMdata::oly12))

Všechny tři výše uvedené způsoby použití map() jsou v pořádku.

Výsledné objekty jsou nyní uložené jako položky listu Spec_models. K nim můžeme přistupovat pomocí subsetování:

Spec_models[[2]] %>% summary

Call:
lm(formula = ., data = VGAMdata::oly12)

Residuals:
    Min      1Q  Median      3Q     Max 
-59.122  -5.646  -1.235   3.622 135.458 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept) -78.890968   3.402356 -23.187  < 2e-16 ***
Height       78.901728   1.814233  43.490  < 2e-16 ***
Age           0.392204   0.108875   3.602 0.000317 ***
I(Age^2)     -0.003842   0.001807  -2.126 0.033527 *  
SexM        -39.474467   4.065283  -9.710  < 2e-16 ***
Height:SexM  25.695023   2.313338  11.107  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 10.06 on 9032 degrees of freedom
  (1346 observations deleted due to missingness)
Multiple R-squared:  0.6084,    Adjusted R-squared:  0.6082 
F-statistic:  2807 on 5 and 9032 DF,  p-value: < 2.2e-16

Odhad modelu na dílčích datech

Základní model můžeme odhadnout na různých vzorcích dat – v tomto případě pro různé sporty (proměnná Sport). Pro podobnou operaci existuje více přístupů. Historicky byla pro takové operace určena funkce dplyr::do(). Ta však bude v budoucnosti v balíku dplyr nahrazena a není ještě zcela jasné jak.

Dobrou variantou je postup analogický k odhadu více modelů na stejných datech. Nyní chceme odhadnout stejný model na různých datech. V předchozím případě jsme přes list rovnic iterovali pomocí funkce map(). Nyní bychom chtěli iterovat přes list tibblů. Ten vytvoříme pomocí funkce split() z base R.

Vstupy split() bude úplný tibble a pravostranná formule se jménem vektoru, podle kterého se má tento tibble rozdělit na menší tibblíky (bylo by možné použít přímo i tento vektor ve tvaru .$Sport). Typicky je tento vektor přímo součástí vstupního tibble. To není problém – můžeme využí placeholder .. Dejme tomu, že chceme odhadnout parametry pro každý sport zvlášť:

Sport_models <- oly12 %>%
    split(~ Sport) %>% 
    map(
        # Zde si pomáháme anonymní funkcí, která nám pomůže doručit data
        # do správného parametru funkce lm()
        function(x) lm(Models[[1]], data = x)
    )

Analogicky lze použít funkci group_split() z dplyr, která rozdělí tabulku na list dílčích tabulek podle zgrupování. V takovém případě by celý kód vypadal následovně:

oly12 %>%
    group_by(Sport) %>% 
    group_split() %>% 
    map(
        function(x) lm(Models[[1]], data = x)
    )

Výsledná proměnná je list, který obsahuje odhadnuté modely:

Sport_models[1:3] %>% print()
$Archery

Call:
lm(formula = Models[[1]], data = x)

Coefficients:
(Intercept)       Height          Age     I(Age^2)         SexM  
 -45.146045    56.714558     0.459166     0.002153    10.191336  


$Athletics

Call:
lm(formula = Models[[1]], data = x)

Coefficients:
(Intercept)       Height          Age     I(Age^2)         SexM  
 -145.12558    123.04350     -0.40467      0.01024      2.15605  


$Badminton

Call:
lm(formula = Models[[1]], data = x)

Coefficients:
(Intercept)       Height          Age     I(Age^2)         SexM  
 -70.285717    75.702484     0.093450     0.001788     3.787492  

S modely uloženými v seznamu. Následující kód například opět za využívá map(). Pomocí broom::glance() vytvoří tabulku modelů seřazenou sestupně podle adjustovaného koeficientu determinace:

Sport_models %>% 
  # Varinata map_dfr() je ekvivalentní k map() %>% bind_rows()
    map_dfr(glance) %>% 
    arrange(desc(adj.r.squared)) %>% 
    print()
# A tibble: 26 × 12
   r.squared adj.r.squared sigma statistic   p.value    df logLik   AIC   BIC
       <dbl>         <dbl> <dbl>     <dbl>     <dbl> <dbl>  <dbl> <dbl> <dbl>
 1     0.879         0.874  2.98      179. 4.32e- 44     4  -256.  524.  540.
 2     0.878         0.870  3.81      110. 3.90e- 27     4  -179.  371.  384.
 3     0.874         0.869  4.56      153. 9.52e- 39     4  -271.  553.  568.
 4     0.850         0.848  6.01      423. 9.22e-122     4  -974. 1960. 1983.
 5     0.837         0.835  4.29      485. 2.43e-147     4 -1099. 2209. 2233.
 6     0.833         0.830  6.50      315. 6.52e- 97     4  -846. 1705. 1726.
 7     0.819         0.815  4.49      171. 4.96e- 55     4  -453.  918.  936.
 8     0.815         0.813  5.84      557. 5.13e-184     4 -1627. 3267. 3292.
 9     0.806         0.805  5.04      887. 2.58e-302     4 -2605. 5223. 5251.
10     0.797         0.794  5.08      293. 3.55e-102     4  -923. 1858. 1880.
# ℹ 16 more rows
# ℹ 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>

(Pozn. takové řazení je užitečné jenom ve velmi omezeném počtu případů.)

15.5 Tvorba regresních tabulek

Existuje mnoho balíků, které umožňují vytvořit regresní tabulku, formátovat ji, a nakonec ji exportovat do různých formátů. Historicky oblíbený balík je stargazer, ten už ale byl nahrazen modernějšími alternativami. Těch je velmi mnoho a není jednoduché některou z nich doporučit.

Základní funkcionalitu poskytuje funkce etable() z balíku fixest, která nabízí exporty do data.frame a formátu tex. Pro jednoduché aplikace může být zcela dostačující. Pro složitější aplikace se jako perspektivní jeví kombinace balíků modelsummary pro tvorbu tabulek a gt pro jejich formátování. Balík modelsummary navíc implementuje nástroje pro tvorbu deskriptivních statistik.

Pro následné ilustrace odhadneme soubor regresí:

eml <-
    feols(
    Weight ~ I(Sex == "F") + Height + csw0(Age) | sw0(Sport),
    data = oly12
)

Všechny jsou uloženy do jednoho objektu eml třídy fixest_multi, což je fakticky list objektů třídy fixest. Tuto třídu modelsummary podporuje – což není automatické.

Základní funkcí je modelsummary(), která umí zpracovat jednolivý model, jejich list a pojemnovaný list. Tato funkce má (v porovnání třeba se stargazer) relativně uměřený počet parametrů:

modelsummary(
  eml,
  output = "default",
  fmt = 3,
  estimate = "estimate",
  statistic = "std.error",
  vcov = NULL,
  conf_level = 0.95,
  exponentiate = FALSE,
  stars = FALSE,
  shape = term + statistic ~ model,
  coef_map = NULL,
  coef_omit = NULL,
  coef_rename = FALSE,
  gof_map = NULL,
  gof_omit = NULL,
  group_map = NULL,
  add_columns = NULL,
  add_rows = NULL,
  align = NULL,
  notes = NULL,
  title = NULL,
  escape = TRUE
)
 fixef: 1; rhs: I(Sex == "F") + Height  fixef: 1; rhs: I(Sex == "F") + Height + Age  fixef: Sport; rhs: I(Sex == "F") + Height  fixef: Sport; rhs: I(Sex == "F") + Height + Age
(Intercept) −93.805 −97.467
(2.088) (2.124)
I(Sex == "F")TRUE −5.672 −5.573 −3.682 −3.613
(0.257) (0.256) (0.855) (0.831)
Height 95.340 94.898 107.567 107.021
(1.139) (1.136) (8.535) (8.547)
Age 0.167 0.172
(0.020) (0.030)
Num.Obs. 9038 9038 9038 9038
R2 0.600 0.603 0.662 0.664
R2 Adj. 0.600 0.603 0.661 0.663
R2 Within 0.613 0.616
R2 Within Adj. 0.612 0.615
AIC 67575.1 67504.8 66105.4 66037.0
BIC 67596.4 67533.2 66304.5 66243.1
RMSE 10.17 10.13 9.35 9.31
Std.Errors IID IID by: Sport by: Sport
FE: Sport X X

Důležité parametry jsou následující:

  • output…umožňuje zadat jméno výstupního souboru (formáty docx, html, tex, xlsx, txt, csv a další) nebo typ výstupního objektu. Zajímavou varinatou je třeba data.frame a gt, který vytvoří výstupní objekt, který lze dále upravovat s pomocí balíku gt.
  • fmt…upravuje formátování čísel. V základním nastavení přijímá počet desetinných míst, na které se má zaokrouhlovat. Možností je však mnohem více – viz nápověda.
  • estimate…umožňuje formátovat reportování odhadů. Nastavení “{estimate}{stars}” například produkuje u nás obvyklé značení statistické významnosti s pomocí hvězdiček.
  • statistic…parametr analogický k estimate.
  • stars…definuje, jak jsou vytvářeny hvězdičky signalizující statistickou významnost. Pro nás obvyklou varinatu například generuje nastavení c('*' = .1, '**' = .05, '***' = 0.01)

S těmito paramatry mužeme tabulku velmi přiblížit požadovanému výstupu:

modelsummary(
  eml,
  fmt = 3,
  estimate = "{estimate}{stars}",
  statistic = "std.error",
  stars = c('*' = .1, '**' = .05, '***' = 0.01)
)
 fixef: 1; rhs: I(Sex == "F") + Height  fixef: 1; rhs: I(Sex == "F") + Height + Age  fixef: Sport; rhs: I(Sex == "F") + Height  fixef: Sport; rhs: I(Sex == "F") + Height + Age
(Intercept) −93.805*** −97.467***
(2.088) (2.124)
I(Sex == "F")TRUE −5.672*** −5.573*** −3.682*** −3.613***
(0.257) (0.256) (0.855) (0.831)
Height 95.340*** 94.898*** 107.567*** 107.021***
(1.139) (1.136) (8.535) (8.547)
Age 0.167*** 0.172***
(0.020) (0.030)
Num.Obs. 9038 9038 9038 9038
R2 0.600 0.603 0.662 0.664
R2 Adj. 0.600 0.603 0.661 0.663
R2 Within 0.613 0.616
R2 Within Adj. 0.612 0.615
AIC 67575.1 67504.8 66105.4 66037.0
BIC 67596.4 67533.2 66304.5 66243.1
RMSE 10.17 10.13 9.35 9.31
Std.Errors IID IID by: Sport by: Sport
FE: Sport X X

Taková tabulka však stále obsahuje pro čtenáře nesrozumitelná jména proměnných a obrovský počet (často zbytečných) statistik. To se dá ovlivnit nastavením dalších parametrů coef_map a gof_map, pomocí kterých lze měnit nejen popisky, ale i pořadí proměnných:

modelsummary(
    eml,
    fmt = 3,
    estimate = "{estimate}{stars}",
    statistic = "std.error",
    stars = c('*' = .1, '**' = .05, '***' = 0.01),
    coef_map = c(
        "Height" = "Height (meters)",
        'I(Sex == "F")TRUE' = "Female (=1)",
        "Age" = "Age (years)"
    ),
    gof_map = c("nobs","adj.r.squared")
)
 fixef: 1; rhs: I(Sex == "F") + Height  fixef: 1; rhs: I(Sex == "F") + Height + Age  fixef: Sport; rhs: I(Sex == "F") + Height  fixef: Sport; rhs: I(Sex == "F") + Height + Age
Height (meters) 95.340*** 94.898*** 107.567*** 107.021***
(1.139) (1.136) (8.535) (8.547)
Female (=1) −5.672*** −5.573*** −3.682*** −3.613***
(0.257) (0.256) (0.855) (0.831)
Age (years) 0.167*** 0.172***
(0.020) (0.030)
Num.Obs. 9038 9038 9038 9038
R2 Adj. 0.600 0.603 0.661 0.663

Jména sloupců jsou převzata ze jmen jednotlivých položek v objektu eml:

names(eml)
[1] "fixef: 1; rhs: I(Sex == \"F\") + Height"          
[2] "fixef: 1; rhs: I(Sex == \"F\") + Height + Age"    
[3] "fixef: Sport; rhs: I(Sex == \"F\") + Height"      
[4] "fixef: Sport; rhs: I(Sex == \"F\") + Height + Age"

Ty lze samozřejmě změnit:

eml %>% 
    magrittr::set_names(
        c("(1)","(2)","(3)","(4)")
    ) %>% 
    modelsummary(
    fmt = 3,
    estimate = "{estimate}{stars}",
    statistic = "std.error",
    stars = c('*' = .1, '**' = .05, '***' = 0.01),
    coef_map = c(
        "Height" = "Height (meters)",
        'I(Sex == "F")TRUE' = "Female (=1)",
        "Age" = "Age (years)"
    ),
    gof_map = c("nobs","adj.r.squared")
)
 (1)   (2)   (3)   (4)
Height (meters) 95.340*** 94.898*** 107.567*** 107.021***
(1.139) (1.136) (8.535) (8.547)
Female (=1) −5.672*** −5.573*** −3.682*** −3.613***
(0.257) (0.256) (0.855) (0.831)
Age (years) 0.167*** 0.172***
(0.020) (0.030)
Num.Obs. 9038 9038 9038 9038
R2 Adj. 0.600 0.603 0.661 0.663

15.6 Export a fromátování tabulek s balíkem gt (TBD)

Balík modelsummary umí vytvářet a prostřednistvím parametru output i exportovat tabulky do mnoha různých formátů. Jednou z možností je i output = "gt". V tom případě vytoří modelsummary datovou sturkturu, kterou lze dále modifikovat nástroji z balíku gt, který je určen výhradně pro porkočilé formátování tabulek. Taková funkcionalita je zvláště užitečná v případě tvorby automatizovaných dokumentů v RMarkdown (soubory .Rmd) nebo Quarto (soubory .qmd), které umožňují v jednom souboru kombinovat text, kódy i grafické výstupy. (Například tato kniha je vytvořená v Quartu.)

Poznámka: gt je jedním z mnoha balíků, které umožňují fromátování tabulek. Mezi další patří třeba DT, flextable, formattable, huxtable a ftextra. Balík gt se však aktivně vyvíjí a v kombinaci s modelsummary představuje mocnou kombinaci. Naučit se gt je však docela velká investice, která se vyplatí právě zejména u tvorby automatizovaných dokumentů.

Pro ukázku použití gt vytvoříme datovou sturkturu ve formátu připraveného pro gt:

eml_gt <- 
    modelsummary(
        eml,
        # Zde se nastaví formát výstupu:
        output = "gt",
        fmt = 3,
        estimate = "{estimate}{stars}",
        statistic = "std.error",
        stars = c('*' = .1, '**' = .05, '***' = 0.01),
        coef_map = c(
            "Height" = "Height (meters)",
            'I(Sex == "F")TRUE' = "Female (=1)",
            "Age" = "Age (years)"
        ),
        gof_map = c("nobs","adj.r.squared")
    )

Výstupní proměnná eml_gt má class gt_tbl. Možnosti následné modifakce jsou skutečně velké. Zde je malá ukázka:

library(gt)

eml_gt %>% 
    tab_header(
        title = "Baseline results",
        subtitle = md("*Dependent variable: Weight (kg)*")
    ) %>% 
    cols_label(
        contains("fixef: 1") & !contains("Age") ~ "(1)",
        contains("fixef: 1") & contains("Age") ~ "(2)",
        contains("fixef: Sport") & !contains("Age") ~ "(3)",
        contains("fixef: Sport") & contains("Age") ~ "(4)"
    ) %>% 
    tab_style(style = cell_fill(color = 'lightblue'),
              locations = cells_body(rows = 1:2))
Baseline results
Dependent variable: Weight (kg)
(1) (2) (3) (4)
Height (meters) 95.340*** 94.898*** 107.567*** 107.021***
(1.139) (1.136) (8.535) (8.547)
Female (=1) -5.672*** -5.573*** -3.682*** -3.613***
(0.257) (0.256) (0.855) (0.831)
Age (years) 0.167*** 0.172***
(0.020) (0.030)
Num.Obs. 9038 9038 9038 9038
R2 Adj. 0.600 0.603 0.661 0.663

Materiály k balíku gt jsou na webové stránce projektu: https://gt.rstudio.com/

15.7 Rozšiřující materiály

Výstupy estimační funkce lm()

Funkce lm() vrací S3 objekt třídy lm:

lm(model, data = oly12) -> est_model

class(est_model)
[1] "lm"

Jiné estimační funkce vrací jiné objekty, které jsou ovšem často potomky lm. Objekt třídy lm má následující strukturu (viz ?lm):

str(est_model, max.level = 1)
List of 13
 $ coefficients : Named num [1:5] -1.06e+02 9.46e+01 4.10e-01 -4.09e-03 5.59
  ..- attr(*, "names")= chr [1:5] "(Intercept)" "Height" "Age" "I(Age^2)" ...
 $ residuals    : Named num [1:9038] -7.71 33.72 -9.15 14.68 0.11 ...
  ..- attr(*, "names")= chr [1:9038] "1" "2" "3" "4" ...
 $ effects      : Named num [1:9038] -6926.6 -1161.4 -96.3 -15.7 -221.1 ...
  ..- attr(*, "names")= chr [1:9038] "(Intercept)" "Height" "Age" "I(Age^2)" ...
 $ rank         : int 5
 $ fitted.values: Named num [1:9038] 67.7 91.3 85.2 70.3 79.9 ...
  ..- attr(*, "names")= chr [1:9038] "1" "2" "3" "4" ...
 $ assign       : int [1:5] 0 1 2 3 4
 $ qr           :List of 5
  ..- attr(*, "class")= chr "qr"
 $ df.residual  : int 9033
 $ contrasts    :List of 1
 $ xlevels      :List of 1
 $ call         : language lm(formula = model, data = oly12)
 $ terms        :Classes 'terms', 'formula'  language Weight ~ Height + Age + I(Age^2) + Sex
  .. ..- attr(*, "variables")= language list(Weight, Height, Age, I(Age^2), Sex)
  .. ..- attr(*, "factors")= int [1:5, 1:4] 0 1 0 0 0 0 0 1 0 0 ...
  .. .. ..- attr(*, "dimnames")=List of 2
  .. ..- attr(*, "term.labels")= chr [1:4] "Height" "Age" "I(Age^2)" "Sex"
  .. ..- attr(*, "order")= int [1:4] 1 1 1 1
  .. ..- attr(*, "intercept")= int 1
  .. ..- attr(*, "response")= int 1
  .. ..- attr(*, ".Environment")=<environment: R_GlobalEnv> 
  .. ..- attr(*, "predvars")= language list(Weight, Height, Age, I(Age^2), Sex)
  .. ..- attr(*, "dataClasses")= Named chr [1:5] "numeric" "numeric" "numeric" "numeric" ...
  .. .. ..- attr(*, "names")= chr [1:5] "Weight" "Height" "Age" "I(Age^2)" ...
 $ model        :'data.frame':  9038 obs. of  5 variables:
  ..- attr(*, "terms")=Classes 'terms', 'formula'  language Weight ~ Height + Age + I(Age^2) + Sex
  .. .. ..- attr(*, "variables")= language list(Weight, Height, Age, I(Age^2), Sex)
  .. .. ..- attr(*, "factors")= int [1:5, 1:4] 0 1 0 0 0 0 0 1 0 0 ...
  .. .. .. ..- attr(*, "dimnames")=List of 2
  .. .. ..- attr(*, "term.labels")= chr [1:4] "Height" "Age" "I(Age^2)" "Sex"
  .. .. ..- attr(*, "order")= int [1:4] 1 1 1 1
  .. .. ..- attr(*, "intercept")= int 1
  .. .. ..- attr(*, "response")= int 1
  .. .. ..- attr(*, ".Environment")=<environment: R_GlobalEnv> 
  .. .. ..- attr(*, "predvars")= language list(Weight, Height, Age, I(Age^2), Sex)
  .. .. ..- attr(*, "dataClasses")= Named chr [1:5] "numeric" "numeric" "numeric" "numeric" ...
  .. .. .. ..- attr(*, "names")= chr [1:5] "Weight" "Height" "Age" "I(Age^2)" ...
 - attr(*, "class")= chr "lm"

Důležité jsou zejména následující položky:

  • coefficients…pojmenovaný vektor odhadnutých koeficientů
est_model$coefficients
  (Intercept)        Height           Age      I(Age^2)          SexM 
-1.060127e+02  9.463456e+01  4.096565e-01 -4.094392e-03  5.591553e+00 
  • residuals…vektor reziduí – tj. odhadů náhodné složky \(\varepsilon\)
est_model$residuals %>% head
         1          2          3          4          5          6 
-7.7137568 33.7165890 -9.1502080 14.6799373  0.1103484 -1.8269273 
  • fitted.values…vyrovnané hodnoty
est_model$fitted.values %>% head
       1        2        3        4        5        6 
67.71376 91.28341 85.15021 70.32006 79.88965 74.82693 
  • model…modifikovanou tabulku dat, která byla skutečně použita při odhadu (tj, například s vypuštěnými nepoužitými proměnnými nebo neúplnými řádky).
est_model$fitted.values %>% head
       1        2        3        4        5        6 
67.71376 91.28341 85.15021 70.32006 79.88965 74.82693 

Za povšimnutí stojí, že objekt obsahuje skutečně pouze odhad – data, odhadnuté koeficienty, rezidua, vyrovnané hodnoty a dodatečné informace. Neobsahuje žádné dodatečné výpočty jako například diagnostické testy.

Pro objekty třídy lm existuje metoda generické funkce print(). Její výstupy však nejsou nijak zvlášť informativní:

print(est_model)

Call:
lm(formula = model, data = oly12)

Coefficients:
(Intercept)       Height          Age     I(Age^2)         SexM  
 -1.060e+02    9.463e+01    4.097e-01   -4.094e-03    5.592e+00  

Obsahuje jen použité volání a odhadnuté hodnoty koeficientů. Více se dozvíme pomocí funkce summary():

summary(est_model)

Call:
lm(formula = model, data = oly12)

Residuals:
    Min      1Q  Median      3Q     Max 
-56.334  -5.717  -1.336   3.756 135.665 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept) -1.060e+02  2.385e+00 -44.444  < 2e-16 ***
Height       9.463e+01  1.141e+00  82.918  < 2e-16 ***
Age          4.097e-01  1.096e-01   3.738 0.000187 ***
I(Age^2)    -4.094e-03  1.819e-03  -2.250 0.024442 *  
SexM         5.592e+00  2.560e-01  21.840  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 10.13 on 9033 degrees of freedom
Multiple R-squared:  0.6031,    Adjusted R-squared:  0.6029 
F-statistic:  3431 on 4 and 9033 DF,  p-value: < 2.2e-16

V tomto případě vidíme kromě odhadů koeficientů i směrodatné chyby a výsledky testů statistické významnosti koeficientů (v tomto případě t-testů). Výstup je také doplněn několika dalšími údaji jako je výsledek F-testu a (adjustovaný) koeficient determinace. Funkce summary() poskytuje rychlý pohled na výsledky, nicméně ten může být zavádějící. Funkce totiž vrací pouze standardní výstupy. Nijak například nereflektuje možné zkreslení odhadu směrodatných odchylek kvůli heteroskedasticitě a podobně. Všimněte si, že summary() používá méně obvyklé – a přísnější – hvězdičkové značení!

Práce s výsledkem odhadů

V R najdete řadu specializovaných funkcí, které dokážou z výsledného modelu získávat, nebo na jeho základě dopočítávat užitečné informace. Tabulka obsahuje jejich základní přehled:

Funkce Popis
coefficients(), coefs() Vrací odhadnuté koeficienty – obsah .$coefficients
residuals(), resid() Vrací vektor reziduí – pro objekty lm je výstup identický s .$residuals
fitted.values() Vrací očekávané hodnoty – obsah .$fitted.values
predict() Umožňuje dopočítat predikce pro jiná data, než na kterých byl model odhadnut
plot() Vrací diagnostické grafy vytvořené pomocí základní grafiky (ne v ggplot2)
confint() Vrací konfidenční intervaly pro odhady koeficientů
deviance() Vrací MSE – průměr čtverců reziduí
vcov() Vrací odhad kovarianční matice
logLik() Vrací log-likelihood
AIC() Vrací Akaikovo informační krítérium (AIC)
BIC() Vrací Schwarzovo Bayesian kritérium (BIC)
nobs() Vrací počet pozorování

Pro některé účely je vhodné seznámit se i se sofistikovanějšími funkcemi. Pro účely diagnostiky nebo okometrického posouzení kvality vyrovnání může být užitečné výsledky odhadu vizualizovat. Výsledky jsou však schované ve zvláštním objektu a ne v tabulce, kterou potřebujeme pro použití s ggplot2. Pro transformaci různých objektů do tabulky existuje v ggplot2 funkce fortify(). Stejnou funkcionalitu poskytuje i balík broom. Pro objekty třídy lm je ekvivalentem funkce broom::augment():

library(broom)
augment(est_model) %>% as_tibble %>% print(n = 5)
# A tibble: 9,038 × 11
  Weight Height   Age `I(Age^2)` Sex   .fitted .resid     .hat .sigma    .cooksd
   <int>  <dbl> <int>   <I<dbl>> <chr>   <dbl>  <dbl>    <dbl>  <dbl>      <dbl>
1     60   1.7     23        529 M        67.7 -7.71  0.000453   10.1    5.26e-5
2    125   1.93    33       1089 M        91.3 33.7   0.000474   10.1    1.05e-3
3     76   1.87    30        900 M        85.2 -9.15  0.000299   10.1    4.89e-5
4     85   1.78    26        676 F        70.3 14.7   0.000336   10.1    1.41e-4
5     80   1.82    27        729 M        79.9  0.110 0.000235   10.1    5.58e-9
# ℹ 9,033 more rows
# ℹ 1 more variable: .std.resid <dbl>

Pozor, broom navrací tibble. Proto je užitečné výstup do tibble konvertovat. Výsledek je již jednoduše použitelný pro vizualizaci pomocí ggplot2. Následující obrázek ukazuje korelaci pozorovaných (observed) a vyrovnaných (fitted) hodnot. Do obrázku je přidána pomocí geom_abline() i osa kvadrantu na která se pozorované a vyrovnané hodnoty rovnají.

est_model %>% 
  augment() %>% 
  ggplot(
    aes(x = Weight, y = .fitted)
  ) +
  geom_point(alpha = 0.05) +
  geom_abline(slope = 1, intercept = 0, color = "red") +
  scale_x_continuous(name = "Observed values") +
  scale_y_continuous(name = "Fitted values")

Balík broom obsahuje i další užitečné funkce: tidy() a glance(). Funkce tidy() vrací tabulku s odhady parametrů, směrodatných odchylek, testových statistik a p-hodnot:

tidy(est_model)
# A tibble: 5 × 5
  term          estimate std.error statistic   p.value
  <chr>            <dbl>     <dbl>     <dbl>     <dbl>
1 (Intercept) -106.        2.39       -44.4  0        
2 Height        94.6       1.14        82.9  0        
3 Age            0.410     0.110        3.74 1.87e-  4
4 I(Age^2)      -0.00409   0.00182     -2.25 2.44e-  2
5 SexM           5.59      0.256       21.8  4.38e-103

Funkce glance() vrací opět tabulku. Jejím obsahem ovšem nejsou údaje týkající se jednotlivých proměnných, ale modelu jako celku:

glance(est_model)
# A tibble: 1 × 12
  r.squared adj.r.squared sigma statistic p.value    df  logLik    AIC    BIC
      <dbl>         <dbl> <dbl>     <dbl>   <dbl> <dbl>   <dbl>  <dbl>  <dbl>
1     0.603         0.603  10.1     3431.       0     4 -33746. 67504. 67546.
# ℹ 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>

Tyto funkce jsou mimořádně užitečné při strojovém zpracovávání velkého množství odhadnutých modelů. Funkce z balíku broom přitom neslouží jenom pro převádění objektů lm do tabulky. Jejich záběr je mnohem obecnější a obsahují metody pro mnohem více tříd objektů.

Zvláštní pozornost se vyplatí věnovat funkci predict(). Ty v základní konfiguraci vrací prostě vyrovnané hodnoty:

all.equal(predict(est_model),fitted(est_model))
[1] TRUE

Při mnoha aplikacích nás spíše zajímá schopnost modelu predikovat out-of-sample. Tedy predikovat hodnoty vysvětlované proměnné na jiném vzorku dat, než který byl použit k odhadu parametrů. I pro tento účel se hodí funkce predict().

Nejprve si tabulku oly12 rozdělíme na dvě dílčí tabulky oly12_a a oly12_b:

idx <- sample(nrow(oly12), round(nrow(oly12)/2))

oly12[idx,]  -> oly12_a
oly12[-idx,]  -> oly12_b

Pro odhad parametrů použijeme tabulku oly12_a:

oly12_a %>% 
    lm(model, data=.) -> est_model_a

Na základě odhadnutého modelu vypočítáme očekávané hodnoty Weight pro pozorování ze vzorku oly12_b:

predict(est_model_a, newdata = oly12_b) -> prediction_oly12_b

Výsledkem je vektor predikovaných hodnot. Podívejme se, jak si model vedl při predikci out-of-sample:

oly12_b %>%
    bind_cols(.fitted = prediction_oly12_b) %>% 
    mutate(
        sample = "B"
    ) -> oly12_b

est_model_a %>% 
  augment() %>% 
  mutate(
    sample = "A"
  ) %>% 
  bind_rows(oly12_b) %>% 
     ggplot(
        aes(
            x = Weight,
            y = .fitted
        )
    ) +
    geom_point(
        alpha = 0.1
    ) +
  geom_abline(slope = 1, intercept = 0, color = "red") +
  facet_wrap("sample", labeller = as_labeller(
    c("A"="In-sample predictions","B"="Out-of-sample predictions")
    ))

Obrázky zhruba ukazují, že odhadnutý model si vede dobře i v out-of-sample predikcích. tedy predikcích vypočítaných na jiných datech, než jaké byly použity pro odhad modelu. Vzhledem k tomu, že vzorek byl rozdělen náhodně to asi není žádné velké překvapení.

Diagnostika

Vlivná pozorování

Populárním způsobem, jak najít pozorování, která významně ovlivňují výsledky modelu, je použití leave-one-out strategií. V nich se sleduje efekt vyloučení jednoho pozorování na odhady koeficientů, popřípadě na vyrovnaná pozorování. V R jsou implementovány v influence.measures():

influence.measures(est_model) -> infl_obs

infl_obs %>% class
[1] "infl"

influence.measures() najednou volá celou řadu diagnostických testů – mj. dfbetas() (vliv jednoho pozorování na odhadnuté koeficienty) a dffits() (vliv jednoho pozorování na vyrovnané hodnoty).

Výstupní objekt třídy infl obsahuje dva prvky (matice). .$infmat obsahuje hodnoty jednotlivých statistik a .$is.inf rozhodnutí (o podezření), zda se jedná o vlivné pozorování.

Viz obrázek:

augment(est_model) %>%
    bind_cols(.,as.data.frame(infl_obs$is.inf)) %>% 
    ggplot(
        aes(x=Weight,
            y=.fitted)
    ) +
    geom_point(
        aes(
            color = dffit
            ),
        alpha = 0.2
    ) +
    xlab("Observed values") +
    ylab("Fitted values") +
    scale_color_discrete(name="Influential\nobservation\n(DFFIT)")

Hodnoty, u kterých se predikce velmi odlišuje od pozorovaných hodnot jsou podle statistiky DFFIT jsou navrženy jako vlivné.

Tyto výsledky naznačují, že se v datech může dít něco zvláštního, nebo že nemáme k dispozici dostatečný počet pozorování z určité části prostoru – v tomto případě například u sportovců s vyšší hmotností.

Normalita reziduí

Prvním způsobem, jak ověřit normalitu reziduí je pohledem na jejich rozdělení. Pomoci může například Q-Q plot, který porovnává kvantily normálního rozdělení a standardizovaných reziduí. V případě, že je rozdělení reziduí normální, by pozorování měla ležet na ose kvadrantu:

augment(est_model) %>% 
    ggplot(
        aes(sample = .std.resid)
    ) +
    geom_qq() +
    geom_abline(
        slope = 1, 
        intercept = 0,
        color = "red"
        )

Ani jeden okometrický test pro normalitu příliš nehovoří. Nicméně je potřeba nepodlehnout zdání. R poskytuje baterii statistických testů.

Například můžeme zkusit otestovat normalitu pomocí Kolmogorov-Smirnovova testu:

residuals(est_model) %>% 
    ks.test(., 'pnorm') -> ks
Warning in ks.test.default(., "pnorm"): ties should not be present for the
Kolmogorov-Smirnov test
class(ks)
[1] "ks.test" "htest"  
print(ks)

    Asymptotic one-sample Kolmogorov-Smirnov test

data:  .
D = 0.44131, p-value < 2.2e-16
alternative hypothesis: two-sided

Výstupem ks.test() je objekt typu htest. Tato třída je oblíbená pro výsledky statistických testů. Pro tuto třídu existují metody v balíku broom, např.:

tidy(ks)
# A tibble: 1 × 4
  statistic p.value method                                        alternative
      <dbl>   <dbl> <chr>                                         <chr>      
1     0.441       0 Asymptotic one-sample Kolmogorov-Smirnov test two-sided  

Nulovou hypotézou K-S testu je normalita, kterou tento test zamítá.

Testy linearity

Z testů linearity je dostupný například Rainbow test, který srovnává kvalitu vyrovnání v modelech odhadnutých “uprostřed” pozorování a na celém vzorku.

V R je Rainbow test dostupný v balíku lmtest ve funkci raintest:

raintest(formula, fraction = 0.5, order.by = NULL, center = NULL,
   data=list())

Parametr fraction udává velikost samplu pro odhad modelu “uprostřed”. V parametru order.by se udává proměnná, podél které se mají pozorování řadit.

Parametr formula může být naplněn i odhadnutým lm modelem. V tom případě není nutné zabývat se parametrem data.

library(lmtest)
raintest(model, order.by = ~ Height, data = oly12)

    Rainbow test

data:  model
Rain = 1.1287, df1 = 4519, df2 = 4514, p-value = 2.397e-05

Rain test v tomto případě zamítá nulovou hypotézu. To znamená, že fit na neomezeném vzorku je významně horší, než na podmnožině “prostředních” pozorování. Tento výsledek indikuje možný problém s linearitou.

Heteroskedasticita a odhad robustních chyb

Pokud je porušena homoskedasticita – tzn. konstantní rozptyl náhodné složky – potom není OLS estimátor BLUE (Best linear unbiased estimator), nicméně odhady parametrů jsou stále nevychýlené (unbiased). To bohužel neplatí pro odhady jejich rozptylu – ty naopak vychýlené jsou a v důsledku jsou nedůvěryhodné i testy statistické významnosti parametrů.

est_model %>% 
    augment() %>% 
    arrange(Height) %>% 
    ggplot(
        aes(
            x = Height,
            y = .resid
            )
    ) +
    geom_point(
        alpha = 0.1
    ) +
    ylab("Residuals")

V reálném světě je podmínka homoskedasticity porušena velmi často. Jestli tomu tak je můžeme testovat pomocí mnoha testů. Jako příklad mohou sloužit příbuzné testy bptest() z balíku lmtest a ncvTest z balíku car:

lmtest::bptest(est_model)

    studentized Breusch-Pagan test

data:  est_model
BP = 73.288, df = 4, p-value = 4.585e-15
car::ncvTest(est_model)
Non-constant Variance Score Test 
Variance formula: ~ fitted.values 
Chisquare = 535.0632, Df = 1, p = < 2.22e-16

Oba testy testují nulovou hypotézu o homoskedasticitě oba ji suverénně zamítají – p-hodnoty obou testů jsou v podstatě nerozlišitelné od nuly.

Odhady směrodatných chyb lze korigovat použitím tzv. HC (heteroskedasticity consistent) estimátorů kovarianční matice. Ty jsou implementovány například v balících car (car::hccm), ale zejména v balíku sandwich.

Balík sandwich obsahuje celou řadu nástrojů pro odhad kovarianční matice pro průřezová i panelová data. Bližší pozornost budeme věnovat dvěma. Základní funkci vcovHC() a také relativně nově implementované funkci pro výpočet klastrovaných robustních chyb vcovCL().

Obě dvě funkce mají podobnou syntax:

vcovHC(x,
  type = c("HC3", "const", "HC", "HC0", "HC1", "HC2", "HC4", "HC4m", "HC5"),
  omega = NULL, sandwich = TRUE, ...)
vcovCL(x, cluster = NULL, type = NULL, sandwich = TRUE, fix = FALSE, ...) 

Vstupem funkce je odhadnutý model (x). V parametru type se potom specifikuje způsob korekce heteroskedasticity (viz ?vcovHC()). V případě funkce vcovCL() je navíc nutné specifikovat proměnnou, nebo v případě více dimenzí proměnné, podle kterých se mají standardní chyby klastrovat. Klastrování se typicky používá v situaci, kdy určitá pozorování na sobě nejsou nezávislá. Klasický příklad je prospěch dětí z jedné třídy. Podstata dat sportovců naznačuje, že i v tomto případě je klastrování namístě. Jako specifické skupiny můžeme chápat sportovce věnující se jedné disciplině. Po vcovCL() tedy bude chtít spočítat kovarianční maticí klastrovanou podél proměnné Sport. Tu musíme dodat jako samostatný vektor. V objektu totiž není zahrnuta:

library(sandwich)
vcovCL(est_model, cluster = oly12$Sport)
              (Intercept)       Height          Age      I(Age^2)         SexM
(Intercept) 144.744530958 -87.73269188  0.544857828 -7.405519e-03  9.080987791
Height      -87.732691881  57.87470724 -0.914600493  1.437195e-02 -6.275686441
Age           0.544857828  -0.91460049  0.079432487 -1.394717e-03  0.137949915
I(Age^2)     -0.007405519   0.01437195 -0.001394717  2.566568e-05 -0.002251259
SexM          9.080987791  -6.27568644  0.137949915 -2.251259e-03  0.979011051

Výsledkem je odhad kovarianční matice. Zpravidla nás ovšem více zajímá odhad významnosti parametrů – provedení statistických testů s použitím robustní kovarianční matice jako vstupu. Pro tento účel použijeme funkci coeftest() z balíku lmtest:

coeftest(x, vcov. = NULL, df = NULL, ...)

Prvním vstupem funkce coeftest() je odhadnutý model. (Funkce má implementovány metody pro modely třídy lm a glm.) Druhým parametrem je kovarianční matice nebo funkce, která se má použít pro její výpočet. Pokud je tento parametr ponechán prázdný, potom coeftest() použije nekorigovanou kovarianční matici:

library(lmtest)
coeftest(est_model)

t test of coefficients:

               Estimate  Std. Error  t value  Pr(>|t|)    
(Intercept) -1.0601e+02  2.3853e+00 -44.4438 < 2.2e-16 ***
Height       9.4635e+01  1.1413e+00  82.9183 < 2.2e-16 ***
Age          4.0966e-01  1.0960e-01   3.7378 0.0001868 ***
I(Age^2)    -4.0944e-03  1.8193e-03  -2.2505 0.0244416 *  
SexM         5.5916e+00  2.5603e-01  21.8397 < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Zadaní jména funkce jako parametru vcov. je vhodné tehdy, pokud nepotřebujete provádět žádné další nastavení parametrů výpočtu kovarianční matice:

coeftest(est_model, vcov. = vcovHC)

t test of coefficients:

               Estimate  Std. Error  t value  Pr(>|t|)    
(Intercept) -1.0601e+02  2.4526e+00 -43.2250 < 2.2e-16 ***
Height       9.4635e+01  1.2120e+00  78.0841 < 2.2e-16 ***
Age          4.0966e-01  9.2268e-02   4.4398 9.109e-06 ***
I(Age^2)    -4.0944e-03  1.5164e-03  -2.7002  0.006944 ** 
SexM         5.5916e+00  2.4404e-01  22.9123 < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

V případě klastrovaných chyb však potřebuje takové nastavení provést – minimálně potřebujeme zadat vektor podél kterého se má klastrovat. Do vcov. v tomto případě potřebujeme zadat celou kovarianční matici – tedy výstup funkce vcovCL:

coeftest(est_model, vcov. = vcovCL(est_model, cluster = oly12$Sport))

t test of coefficients:

               Estimate  Std. Error t value  Pr(>|t|)    
(Intercept) -1.0601e+02  1.2031e+01 -8.8116 < 2.2e-16 ***
Height       9.4635e+01  7.6075e+00 12.4396 < 2.2e-16 ***
Age          4.0966e-01  2.8184e-01  1.4535    0.1461    
I(Age^2)    -4.0944e-03  5.0661e-03 -0.8082    0.4190    
SexM         5.5916e+00  9.8945e-01  5.6512 1.642e-08 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Klastrovat lze podél více proměnných. V tomto případě je potřeba do parametru cluster vložit list vektorů nebo data.frame. Nyní zkusíme cvičně klastrovat nejen podle sportovního odvětví, ale i podle pohlaví:

coeftest(est_model, vcov. = vcovCL(est_model, cluster = list(oly12$Sport, oly12$Sex)))

t test of coefficients:

               Estimate  Std. Error t value  Pr(>|t|)    
(Intercept) -1.0601e+02  2.6150e+01 -4.0540 5.076e-05 ***
Height       9.4635e+01  1.2569e+01  7.5295 5.584e-14 ***
Age          4.0966e-01  3.3167e-01  1.2351    0.2168    
I(Age^2)    -4.0944e-03  4.2600e-03 -0.9611    0.3365    
SexM         5.5916e+00  1.1367e+00  4.9189 8.857e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Výstupem funkce coeftest() je objekt speciální třídy coeftest. Pokud ho chceme převést na tabulku můžeme použít funkci broom::tidy():

coeftest(est_model, vcov. = vcovCL(est_model, cluster = list(oly12$Sport, oly12$Sex))) %>% tidy()
# A tibble: 5 × 5
  term          estimate std.error statistic  p.value
  <chr>            <dbl>     <dbl>     <dbl>    <dbl>
1 (Intercept) -106.       26.1        -4.05  5.08e- 5
2 Height        94.6      12.6         7.53  5.58e-14
3 Age            0.410     0.332       1.24  2.17e- 1
4 I(Age^2)      -0.00409   0.00426    -0.961 3.37e- 1
5 SexM           5.59      1.14        4.92  8.86e- 7

Dignostika graficky

V mnoha případech je užitečné se na diagnostiku podívat prostřednictvím vizualizací. Tuto možnost nabízí například funkce plot(), která má pro tento účel implementovanou metodu. Její fungování můžete vyzkoušet zadáním plot(est_model). Tento postup logicky pracuje se základní grafikou. Existují však i řešení, které podobně uživatelsky přítulně vykreslí diagnostické grafy s pomocí ggplot2. Jedním z nich je funkce ggplot2::autoplot(), která využívá služeb balíku ggfortify:

library(ggfortify)
autoplot(est_model)