1 What this document does

Turns the raw EUKLEMS file into the dataset the analysis actually uses:

  1. keeps only the six focus sections and the usable comparator countries
  2. keeps only the variables the research questions need
  3. derives the indicators (intensities, shares, composition)
  4. checks the result adds up
  5. saves it as output/clean/pkp_clean.rds

Run 00_setup.Rmd first. Everything after this document loads pkp_clean.rds rather than the raw file.

load("output/clean/00_setup.RData")
suppressPackageStartupMessages({ library(dplyr) })

load("Data/Luiss_datasets.RData")
annual$year <- as.integer(annual$year)

dim(klems)
## [1] 61128   236

2 Filter: which rows do we keep?

Two filters. Industries are matched exactly against the lists from 00_setup.Rmd - never by pattern, because the industry column mixes four levels of one hierarchy. Countries are the usable comparators plus EU12, which is kept because it is the only aggregate carrying growth accounting.

Two industry levels are kept, following the mentors’ request for division-level detail:

  • sections (C, G, H, I, J, M) - the full analysis, including tangible and intangible investment and growth accounting
  • divisions (24 codes) - value added and economic competencies only; the national-accounts variables do not exist below section level for Slovenia
keep_geo  <- c(peer_countries, ga_benchmark)
keep_nace <- c(focus_sections, division_codes)

dat <- klems %>%
  filter(nace_r2_code %in% keep_nace,
         geo_code     %in% keep_geo)

nrow(dat)
## [1] 17010
table(dat$geo_code)
## 
##   AT   BG   CZ   DE   DK   ES EU11   FI   FR   IT   JP   LT   LU   LV   NL   RO 
##  810  810  810  810  810  810  810  810  810  810  810  810  810  810  810  810 
##   SE   SI   SK   UK   US 
##  810  810  810  810  810

2.1 Mark the hierarchy level

Critical. The dataset now holds two levels of the same hierarchy: section C contains division C26. Adding them together would double-count - the same error that overstates the Slovenian economy by 5.4 times.

Every row is therefore labelled section or division, and every division records its parent. Always filter on level before aggregating.

dat <- dat %>%
  mutate(
    level  = if_else(nace_r2_code %in% focus_sections, "section", "division"),
    parent = if_else(level == "section",
                     nace_r2_code,
                     unname(division_parent[nace_r2_code]))
  )

table(dat$level)
## 
## division  section 
##    13608     3402

3 Select: which variables do we keep?

Out of 236 columns. The selection covers every variable the team highlighted in Variable-List-Highlighted.xlsx, plus the growth-accounting block and the identifiers. intersect() guards against a mistyped name silently becoming a missing column later.

Note on duplicated names. Eight names appear twice in the source variable list - once in the statistical module and once in the intangibles analytical module: VA_CP, VA_Q, I_Soft_DB, I_RD, I_OIPP, K_Soft_DB, K_RD, K_OIPP. The merged file carries only one column for each, so one version won. Which one is not recoverable from the file, and it is worth confirming with the mentors before these variables carry weight in the analysis.

wanted <- c(
  # identifiers
  "nace_r2_code", "nace_r2_name", "geo_code", "year", "level", "parent",

  # output and labour input
  "VA_CP", "VA_Q", "VAadj", "VAadj_q", "EMP", "H_EMP",

  # investment, national accounts basis
  "I_GFCF", "I_IT", "I_CT", "I_Soft_DB", "I_RD", "I_OIPP",

  # investment, CHS basis
  "I_Tang", "I_Intang", "I_NatAcc", "I_NonNatAcc",
  "I_Innovprop", "I_EconComp",
  "I_OrgCap", "I_Brand", "I_Train", "I_Design",

  # investment in chained volumes (2015) - needed for anything over time
  "Iq_GFCF", "Iq_Tang", "Iq_Intang", "Iq_NatAcc", "Iq_NonNatAcc",

  # capital stocks
  "K_GFCF", "K_RD", "K_Soft_DB", "K_OIPP", "K_Tang",
  "K_NatAcc", "K_NonNatAcc", "K_Innovprop", "K_EconComp",
  "K_OrgCap", "K_Brand", "K_Train",
  # K_Intang is kept for reference only - it is unusable as delivered for
  # Slovenia (one year out of 27). Use K_Intang_rebuilt instead.
  "K_Intang",

  # growth accounting
  "VA_G", "VAConIntang", "VAConTangICT", "VAConTangNICT",
  "VAConLC", "VAConTFP",
  "CAPIntang_QI", "CAPTang_QI", "LAB_QI"
)

wanted <- intersect(wanted, names(dat))

dat <- dat %>%
  select(all_of(wanted)) %>%
  rename(nace = nace_r2_code,
         geo  = geo_code)

length(wanted)
## [1] 55

4 Derive the indicators

All monetary variables are in millions of national currency, so levels cannot be compared across countries. Every indicator below is a ratio, which is unit-free.

Two accounting bases exist and must not be mixed:

  • national accounts basis - I_GFCF over VA_CP
  • CHS basis - I_Intang and I_Tang over VAadj

4.1 Negative investment, and why two helpers are needed

Gross fixed capital formation can be negative in national accounts, when disposals of assets exceed acquisitions in a period. This is real data, not an error, and it appears in small or volatile industries - for example Slovak coke and refined petroleum, Danish chemicals, Swedish air transport. It affects 40 of roughly 10,000 division-level observations, and none in Slovenia.

It matters because a share is only meaningful when the part and the whole are both non-negative: a negative numerator produces shares like -1342 percent. Two helpers handle this explicitly rather than letting nonsense through.

# An INTENSITY (flow relative to output) may legitimately be negative - it means
# net disinvestment. Only the denominator has to be positive.
safe_intensity <- function(num, den) {
  if_else(!is.na(den) & den > 0, 100 * num / den, NA_real_)
}

# A SHARE (part of a whole) must lie between 0 and 100 by definition. It can
# fall outside that range only when some component of the whole is negative -
# for example intangible investment of 100 against tangible investment of -90
# gives a total of 10 and a "share" of 1000 percent. Such a figure is not
# meaningful, so it is set to NA and counted, never silently kept or clamped
# to a boundary.
safe_share <- function(part, whole) {
  x <- if_else(!is.na(whole) & whole != 0, 100 * part / whole, NA_real_)
  if_else(!is.na(x) & x >= 0 & x <= 100, x, NA_real_)
}
dat <- dat %>%
  mutate(

    # -- investment intensities (percent of value added) --------------------
    inv_intensity    = safe_intensity(I_GFCF,   VA_CP),   # national accounts basis
    intang_intensity = safe_intensity(I_Intang, VAadj),   # CHS basis
    tang_intensity   = safe_intensity(I_Tang,   VAadj),

    # -- the headline structural indicator ----------------------------------
    intang_share     = safe_share(I_Intang, I_Intang + I_Tang),

    # -- CHS composition (shares of total intangible investment) ------------
    # The three Corrado-Hulten-Sichel categories.
    sh_compinfo      = safe_share(I_Soft_DB,   I_Intang),  # computerised information
    sh_innovprop     = safe_share(I_Innovprop, I_Intang),  # innovative property
    sh_econcomp      = safe_share(I_EconComp,  I_Intang),  # economic competencies

    # -- the measurement gap -------------------------------------------------
    # Share of intangible investment that national accounts do NOT capitalise.
    sh_nonNA         = safe_share(I_NonNatAcc, I_Intang),

    # -- individual assets, as shares of intangible investment ---------------
    sh_rd            = safe_share(I_RD,      I_Intang),
    sh_orgcap        = safe_share(I_OrgCap,  I_Intang),
    sh_brand         = safe_share(I_Brand,   I_Intang),
    sh_train         = safe_share(I_Train,   I_Intang),
    sh_design        = safe_share(I_Design,  I_Intang),

    # -- tangible composition: ICT vs non-ICT (national accounts basis) ------
    ict_share        = safe_share(I_IT + I_CT + I_Soft_DB, I_GFCF),

    # -- labour productivity, CHS basis, real ---------------------------------
    # Per HOUR is the primary measure: it is what EUKLEMS itself uses, and it
    # does not confound productivity with differences in working time. Per
    # PERSON is reported alongside as a robustness check, since it is the more
    # familiar measure and the two diverge where part-time work is common.
    # UNITS. VAadj_q is millions of euro; H_EMP is thousands of hours; EMP is
    # thousands of persons. So:
    #   VAadj_q / H_EMP  = thousand EUR per hour   -> x1000 gives EUR per hour
    #   VAadj_q / EMP    = thousand EUR per person -> left as is
    #   H_EMP   / EMP    = hours per person        -> already correct
    lp_hour          = 1000 * VAadj_q / H_EMP,   # EUR per hour, 2015 prices
    lp_person        = VAadj_q / EMP,            # thousand EUR per person
    hours_per_person = H_EMP / EMP,              # hours per person per year

    # -- reconstructed intangible capital stock -------------------------------
    # K_Intang is unusable as delivered (one year for Slovenia). Its components
    # are complete from 2000, and their sum reproduces the published figure
    # exactly in the year it exists, so it is rebuilt here.
    K_Intang_rebuilt = K_NatAcc + K_NonNatAcc,

    # -- flag the known anomaly ----------------------------------------------
    # Slovenian stock variables spike implausibly in 2020 (K_OrgCap roughly
    # doubles and falls back). The anomaly is in the source LUISS module, not
    # introduced here. Flow variables are unaffected.
    stock_suspect    = (geo == "SI" & year == 2020)
  )

5 Add labels and groups

# Sections get the short labels from 00_setup; divisions keep their full name
# from the source data.
dat <- dat %>%
  mutate(
    industry = if_else(level == "section",
                       unname(industry_labels[nace]),
                       nace_r2_name),
    group    = unname(industry_groups[parent]),
    is_focus_country = geo %in% peer_countries
  ) %>%
  select(-nace_r2_name) %>%
  relocate(nace, industry, level, parent, group, geo, year)

head(dat[dat$level == "division", 1:7])
## # A tibble: 6 × 7
##   nace    industry                                level parent group geo    year
##   <chr>   <chr>                                   <chr> <chr>  <chr> <chr> <dbl>
## 1 C10-C12 Manufacture of food products; beverage… divi… C      Manu… AT     1995
## 2 C10-C12 Manufacture of food products; beverage… divi… C      Manu… AT     1996
## 3 C10-C12 Manufacture of food products; beverage… divi… C      Manu… AT     1997
## 4 C10-C12 Manufacture of food products; beverage… divi… C      Manu… AT     1998
## 5 C10-C12 Manufacture of food products; beverage… divi… C      Manu… AT     1999
## 6 C10-C12 Manufacture of food products; beverage… divi… C      Manu… AT     2000

6 Checks

If any of these fail, stop and investigate before using the dataset.

# 1. Does industry + country + year still uniquely identify a row?
stopifnot(nrow(dat) == nrow(distinct(dat, nace, geo, year)))

# 1b. Is every division assigned to a parent section?
stopifnot(!any(is.na(dat$parent)))

# 2. Does the CHS identity hold? (computerised info + innovative property +
#    economic competencies should equal total intangibles)
chs_gap <- dat %>%
  filter(!is.na(I_Intang), I_Intang > 0) %>%
  mutate(d = abs(I_Soft_DB + I_Innovprop + I_EconComp - I_Intang) / I_Intang) %>%
  summarise(max_deviation_pc = 100 * max(d, na.rm = TRUE)) %>%
  pull(max_deviation_pc)

# 3. Does the national-accounts split hold?
na_gap <- dat %>%
  filter(!is.na(I_Intang), I_Intang > 0) %>%
  mutate(d = abs(I_NatAcc + I_NonNatAcc - I_Intang) / I_Intang) %>%
  summarise(max_deviation_pc = 100 * max(d, na.rm = TRUE)) %>%
  pull(max_deviation_pc)

# 4. Are the shares in a sensible range? They must be, by construction -
#    safe_share() returns NA rather than a nonsensical value.
share_range <- range(dat$intang_share, na.rm = TRUE)

# 4b. How many observations were set aside because a component was negative?
n_suppressed <- sum(!is.na(dat$I_Intang) & !is.na(dat$I_Tang) &
                    is.na(dat$intang_share))

cat("unique key .................. OK\n")
## unique key .................. OK
cat("CHS identity, max deviation :", round(chs_gap, 6), "percent\n")
## CHS identity, max deviation : 0 percent
cat("NA split,     max deviation :", round(na_gap, 6), "percent\n")
## NA split,     max deviation : 0 percent
cat("intangible share range ......", round(share_range[1], 1), "to",
    round(share_range[2], 1), "percent\n")
## intangible share range ...... 0.3 to 100 percent
cat("shares set to NA (negative component):", n_suppressed, "\n")
## shares set to NA (negative component): 60
stopifnot(chs_gap < 0.01, na_gap < 0.01,
          share_range[1] >= 0, share_range[2] <= 100)

7 Save

saveRDS(dat, "output/clean/pkp_clean.rds")

cat("01_prepare_data complete\n")
## 01_prepare_data complete
cat("  rows      :", nrow(dat), "\n")
##   rows      : 17010
cat("  columns   :", ncol(dat), "\n")
##   columns   : 76
cat("  sections  :", sum(dat$level == "section")  / (length(unique(dat$geo)) * 27), "\n")
##   sections  : 6
cat("  divisions :", sum(dat$level == "division") / (length(unique(dat$geo)) * 27), "\n")
##   divisions : 24
cat("  countries :", length(unique(dat$geo)), "\n")
##   countries : 21
cat("  years     :", min(dat$year), "-", max(dat$year), "\n")
##   years     : 1995 - 2021
cat("  saved     :", round(file.size("output/clean/pkp_clean.rds") / 1024, 1),
    "KB to output/clean/pkp_clean.rds\n")
##   saved     : 5352 KB to output/clean/pkp_clean.rds

Later documents begin with:

load("output/clean/00_setup.RData")
dat <- readRDS("output/clean/pkp_clean.rds")

And then, always, one of these before aggregating:

sections  <- dat %>% filter(level == "section")    # the main analysis
divisions <- dat %>% filter(level == "division")   # economic competencies only

Mixing the two levels in one total double-counts.