8  Comparison with other packages

Almost all of this package’s functionality can be replicated using individual functions from other packages. However, I believe the main advantage of the paneldesc package is that it combines all the functions necessary for panel data analysis and implements them in a single format. Below is a comparison with individual functions from other packages.

8.1 Set up

Import packages to compare.

library(paneldesc)
library(tidyr)
library(gplots)
library(modelsummary)
library(plm)
library(fixest)
library(pdynmc)
library(xtsum)

8.2 Data import

Import the built-in dataset with simulated unbalanced panel data.

data(production)

Create unbalanced panel for the analysis purposes.

unbalanced <- production[rowSums(!is.na(production[, c("sales", "capital", "labor", "industry")])) > 0, ]

8.3 paneldesc vs. pdynmc

paneldesc::plot_patterns() and pdynmc::strucUPD.plot() visualize the participation patterns of entities in an unbalanced panel. pdynmc::strucUPD.plot() groups similar participation patterns by the number of periods and separates different patterns by bar color, while preserving the original order of the entities. In turn, paneldesc::plot_patterns() reorders entities by their patterns to show the dominance of each pattern using bar width.

paneldesc::plot_patterns(unbalanced, index = c("firm", "year"))

pdynmc::strucUPD.plot(unbalanced, i.name = "firm", t.name = "year")

As can be seen above, the two functions produce similar results for an apparently unbalanced panel. However, applying pdynmc::strucUPD.plot() to the original production data frame results in an error, since it is technically balanced (the same number of years for each firm), while paneldesc::plot_patterns() detects entity-time pairs without other data and produces the same plot, showing the unbalanced panel structure.

paneldesc::plot_patterns(production, index = c("firm", "year"))

8.4 paneldesc vs. modelsummary

Among many other functions, paneldesc::summarize_numeric() and modelsummary::datasummary() provide tables with descriptive statistics for numeric variables and also allow the user to calculate statistics within groups. The main advantage of modelsummary::datasummary() is the flexibility of the output appearance, allowing various parameters to be specified for the table structure and output format. In turn, paneldesc::summarize_numeric() always returns a data frame, preserving the user’s freedom of further work with it. Furthermore, in my opinion, the syntax of paneldesc::summarize_numeric() is simpler compared to modelsummary::datasummary(), although this is a matter of taste.

paneldesc::summarize_numeric(
  production, 
  select = c("capital", "labor"), 
  group = "year"
)
year variable count mean std min max
1 capital 25 40.154 39.004 3.206 148.942
1 labor 24 74.349 69.766 18.414 333.377
2 capital 28 30.210 30.596 5.032 122.332
2 labor 28 81.758 66.571 17.666 327.581
3 capital 28 30.111 30.372 2.030 121.740
3 labor 29 82.256 106.869 8.172 579.024
4 capital 28 34.889 32.046 7.146 148.994
4 labor 29 55.533 46.448 5.972 207.572
5 capital 26 28.076 21.871 4.637 78.244
5 labor 25 74.056 71.255 15.879 266.829
6 capital 19 37.145 39.400 3.690 160.085
6 labor 19 101.003 67.261 18.669 259.851
modelsummary::datasummary(
  Factor(year) * (capital + labor) ~ N + Mean + SD + Min + Max, 
  data = production
)
year N Mean SD Min Max
1 capital 25 40.15 39.00 3.21 148.94
labor 24 74.35 69.77 18.41 333.38
2 capital 28 30.21 30.60 5.03 122.33
labor 28 81.76 66.57 17.67 327.58
3 capital 28 30.11 30.37 2.03 121.74
labor 29 82.26 106.87 8.17 579.02
4 capital 28 34.89 32.05 7.15 148.99
labor 29 55.53 46.45 5.97 207.57
5 capital 26 28.08 21.87 4.64 78.24
labor 25 74.06 71.26 15.88 266.83
6 capital 19 37.15 39.40 3.69 160.08
labor 19 101.00 67.26 18.67 259.85

8.5 paneldesc vs. gplots

The functions paneldesc::plot_heterogeneity() and gplots::plotmeans() allow the user to analyze heterogeneity between and within groups. The results of these functions are quite similar: they visualize group means and the within-group variance of the selected variable. The main difference is that gplots::plotmeans() displays group size labels and plots confidence intervals, while paneldesc::plot_heterogeneity() plots a point for each observation to show both the variance and the group size.

paneldesc::plot_heterogeneity(production, select = "sales", group = "year")

gplots::plotmeans(sales ~ year, production)

8.6 paneldesc vs. xtsum

paneldesc::decompose_numeric() and xtsum::xtsum() provide descriptive statistics for numeric variables in panel data. Specifically, they decompose the standard deviation into between-value and within-value components, mimicking the xtsum command in Stata. The syntax and behavior of the functions are quite similar. By default, xtsum::xtsum() returns output in HTML format, although it allows customization. In turn, paneldesc::summarize_numeric() always returns a data frame, preserving the user’s freedom for further manipulation.

paneldesc::decompose_numeric(
  production, 
  select = c("sales", "capital", "labor"), 
  index = "firm"
)
variable dimension mean std min max count
sales overall 68.402 45.025 11.999 292.850 154.000
sales between NA 29.060 34.263 166.364 30.000
sales within NA 34.127 -12.444 234.297 5.133
capital overall 33.152 32.044 2.030 160.085 154.000
capital between NA 17.414 9.019 74.225 30.000
capital within NA 27.072 -25.567 149.329 5.133
labor overall 76.883 74.150 5.972 579.024 154.000
labor between NA 41.068 31.021 190.645 30.000
labor within NA 61.202 -58.040 483.217 5.133
xtsum::xtsum(
  production, 
  variables = c("sales", "capital", "labor"), 
  id = "firm", 
  t = "year", 
  na.rm = TRUE
)
Variable Dim Mean SD Min Max Observations
___________ _________
sales overall 68.402 45.025 11.999 292.85 N = 154
between 29.06 34.263 166.364 n = 30
within 34.127 -12.444 234.297 T = 5.133
___________ _________
capital overall 33.152 32.044 2.03 160.085 N = 154
between 17.414 9.019 74.225 n = 30
within 27.072 -25.567 149.329 T = 5.133
___________ _________
labor overall 76.883 74.15 5.972 579.024 N = 154
between 41.068 31.021 190.645 n = 30
within 61.202 -58.04 483.217 T = 5.133

8.7 paneldesc vs. plm

paneldesc::make_balanced() and plm::make.pbalanced() allow the user to obtain various types of balanced panel datasets.

paneldesc_bal_1 <- paneldesc::make_balanced(
  unbalanced, 
  index = c("firm", "year"), 
  balance = "entities"
)
dim(paneldesc_bal_1)
[1] 96  8
paneldesc_bal_2 <- paneldesc::make_balanced(
  unbalanced, 
  index = c("firm", "year"), 
  balance = "periods"
)
dim(paneldesc_bal_2)
[1] 30  8
paneldesc_bal_3 <- paneldesc::make_balanced(
  unbalanced, 
  index = c("firm", "year"), 
  balance = "rows"
)
dim(paneldesc_bal_3)
[1] 180   8
plm_bal_1 <- plm::make.pbalanced(
  unbalanced, 
  index = c("firm", "year"), 
  balance.type = "shared.individuals"
)
dim(plm_bal_1)
[1] 96  8
plm_bal_2 <- plm::make.pbalanced(
  unbalanced, 
  index = c("firm", "year"), 
  balance.type = "shared.times"
)
dim(plm_bal_2)
[1] 30  8
plm_bal_3 <- plm::make.pbalanced(
  unbalanced, 
  index = c("firm", "year"), 
  balance.type = "fill"
)
dim(plm_bal_3)
[1] 180   8

As can be seen above, the two functions produce similar results for an apparently unbalanced panel. However, applying plm::make.pbalanced() to the original production dataframe results in no changes, as it is technically balanced (the same number of years for each firm), while paneldesc::make_balanced() automatically detects entity-time pairs without other data, revealing the unbalanced panel structure. This allows the function to balance the panel in the same way as shown above.

paneldesc_bal_1b <- paneldesc::make_balanced(
  production, 
  index = c("firm", "year"), 
  balance = "entities"
)
dim(paneldesc_bal_1b)
[1] 96  8
paneldesc_bal_2b <- paneldesc::make_balanced(
  production, 
  index = c("firm", "year"), 
  balance = "periods"
)
dim(paneldesc_bal_2b)
[1] 30  8
paneldesc_bal_3b <- paneldesc::make_balanced(
  production, 
  index = c("firm", "year"), 
  balance = "rows"
)
dim(paneldesc_bal_3b)
[1] 180   8
plm_bal_1b <- plm::make.pbalanced(
  production, 
  index = c("firm", "year"), 
  balance.type = "shared.individuals"
)
dim(plm_bal_1b)
[1] 180   8
plm_bal_2b <- plm::make.pbalanced(
  production, 
  index = c("firm", "year"), 
  balance.type = "shared.times"
)
dim(plm_bal_2b)
[1] 180   8
plm_bal_3b <- plm::make.pbalanced(
  production, 
  index = c("firm", "year"), 
  balance.type = "fill"
)
dim(plm_bal_3b)
[1] 180   8

8.8 paneldesc vs. fixest

paneldesc::make_demeaned() and fixest::demean() allow the user to demean variables using various grouping patterns. As can be seen below, basic applications of these functions to a single variable produce similar results. When working with multiple variables, the behavior of the functions differs slightly. fixest::demean() is aimed at regression analysis and therefore removes all rows with missing values ​​for at least one variable and performs calculations based on the resulting dataset. paneldesc::make_demeaned(), in contrast, preserves missing values ​​in the dataset and performs transformations on the full dataset (when calculating within-group means, missing values ​​are removed for each variable separately, which often allows for more data to be used compared to fixest::demean()). Thus, the paneldesc::make_demeaned() function may be preferable for exploratory data analysis, as it preserves more observations. In turn, fixest::demean() is more flexible in configuration, allowing the user to select many additional parameters.

demeaned_paneldesc <- paneldesc::make_demeaned(
    production[, c("sales", "firm", "year")], 
    group = c("firm", "year")
)
Demeaning numeric variables: sales
summary(demeaned_paneldesc)
     sales               firm           year    
 Min.   :-75.9166   Min.   : 1.0   Min.   :1.0  
 1st Qu.:-22.7824   1st Qu.: 8.0   1st Qu.:2.0  
 Median : -0.6208   Median :15.5   Median :3.5  
 Mean   :  0.0000   Mean   :15.5   Mean   :3.5  
 3rd Qu.: 16.3642   3rd Qu.:23.0   3rd Qu.:5.0  
 Max.   :163.1807   Max.   :30.0   Max.   :6.0  
 NA's   :26                                     
demeaned_fixest <- fixest::demean(
  X = production[, "sales"],
  f = production[, c("firm", "year")]
)
NOTE: 26 observations removed because of NA values (Breakup: X: 26, f: 0).
summary(demeaned_fixest)
       V1          
 Min.   :-75.9166  
 1st Qu.:-22.7824  
 Median : -0.6208  
 Mean   :  0.0000  
 3rd Qu.: 16.3642  
 Max.   :163.1807  

8.9 paneldesc vs. tidyr

Among other functions, paneldesc::make_wide() and paneldesc::make_long(), as well as tidyr::pivot_wider() and tidyr::pivot_longer(), allow the user to transform panel data from long to wide format and from wide to long, respectively. The key advantage of the paneldesc functions is their simpler and more automatic implementation in a panel data context, including the ability to handle time-varying and static variables separately. In turn, the tidyr functions are more adaptable for a broader range of applications, although they lack automatic solutions for panel data. For example, the region variable in the production dataset is a time-invariant variable, but has missing observations due to the unbalanced structure of the panel data. In this case, the tidyr functions require data preprocessing, which somewhat complicates their implementation (omitted here for brevity). Also, I find the syntax of the paneldesc function simpler than tidyr, although that’s a matter of taste.

wide_paneldesc <- paneldesc::make_wide(
    production, 
    select = c("sales", "capital", "labor", "industry", "ownership"),
    index = c("firm", "year")
)
  Static variables: region 
Reshaped variables: sales_1, sales_2, sales_3, sales_4, sales_5, sales_6 
                    capital_1, capital_2, capital_3, capital_4, capital_5, capital_6 
                    labor_1, labor_2, labor_3, labor_4, labor_5, labor_6 
                    industry_1, industry_2, industry_3, industry_4, industry_5, industry_6 
                    ownership_1, ownership_2, ownership_3, ownership_4, ownership_5, ownership_6 
long_paneldesc <- paneldesc::make_long(
    wide_paneldesc, 
    select = c("sales", "capital", "labor", "industry", "ownership"), 
    index = c("firm", "year")
)
  Static variables: region 
Reshaped variables: sales, capital, labor, industry, ownership 
wide_tidyr <- tidyr::pivot_wider(
  production,
  id_cols = firm,
  names_from = year,
  values_from = !c(firm, year)
)
names(wide_tidyr)
 [1] "firm"        "sales_1"     "sales_2"     "sales_3"     "sales_4"    
 [6] "sales_5"     "sales_6"     "capital_1"   "capital_2"   "capital_3"  
[11] "capital_4"   "capital_5"   "capital_6"   "labor_1"     "labor_2"    
[16] "labor_3"     "labor_4"     "labor_5"     "labor_6"     "industry_1" 
[21] "industry_2"  "industry_3"  "industry_4"  "industry_5"  "industry_6" 
[26] "ownership_1" "ownership_2" "ownership_3" "ownership_4" "ownership_5"
[31] "ownership_6" "region_1"    "region_2"    "region_3"    "region_4"   
[36] "region_5"    "region_6"   
long_tidyr <- tidyr::pivot_longer(
  wide_tidyr,
  cols = !firm,
  names_to = c(".value", "year"),
  names_pattern = "(.*)_(.*)"
)
names(long_tidyr)
[1] "firm"      "year"      "sales"     "capital"   "labor"     "industry" 
[7] "ownership" "region"