Labor Cost Equity Strategy

by Jonathan Regenstein

A recent piece on Business Insider, available here if you are a BI Prime subscriber and similar piece available from CNBC here, discusses an idea from Goldman Sachs that rising labor costs (expected in 2018) should lead to outperformance by companies with relatively low labor costs as a percentage of expenses.1 In today’s post, we will construct an R code flow/template to examine the relationship between stock performance and labor costs, visualize the recent historical relationships that might have led to this hypothesis, and think about extensions at the end. I’m going to be writing weekly posts like this in 2018 where I take an interesting hypothesis or idea and try to reproduce it, or extend it, or explore/visualize it with R code. Suggestions welcome!

There are two main motivations for this overall project: (1) reproducing an idea or its data foundations helps us to firmly understand it and (2) when market research includes reproducible code and data provenance, that research can serve as the foundation for further development by curious data scientists or quants. Note that the motivation is not try to prove true, or refute, or debunk the original research!

With that, some caveats on today’s post.

First, I have read only a summary of this research, not the actual notes published by Goldman. It sounded interesting so I wanted to put together some visualizations and explore. I don’t know the complete list of stocks used for the low labor cost index. The articles mentioned a few tickers so we’ll roll with those. The ‘low labor cost index’ might be available on Bloomberg if you have access to that data set. I don’t.

Second caveat is that I chose a proxy to measure labor costs, the BEA series on compensation of employees, available via FRED. Perhaps there’s a better way to measure wage growth and labor costs (in the code flow below it’s not cumbersome to substitute in different time series). I’m also not taking inflation into account.

Let’s get to it.

We will choose a few stocks from the low labor cost bucket and save their tickers as symbols_low.
I’m going to include the SP500 ETF ‘SPY’ as well for benchmarking.

library(tidyquant)
library(tidyverse)
library(timetk)
library(tibbletime)

symbols_low <- c( "TAP", "VLO", "AFL", "ESRX", 
             "ABC", "QCOM", "SWKS", "LRCX",
             "HST", "HCN", "NFLX", "NKE", "AAPL", "SPY")

Have a look at those tickers and notice both Apple and Netflix are included.

Next, we need to choose a start date since we are looking backwards here. Let’s go with 2010 for a starting year.

start_date <- "2010-01-01"

Now we want to import prices for those tickers, convert them to returns, and then calculate how a dollar would have grown if invested in each stock. We will use the tq_get() function from the tidyquant package to import prices, then call select(date, symbol, adjusted) to keep just the date, symbol and adjusted prices columns.

To transform to log monthly returns, we will first use the tibbletime package to convert to monthly prices with as_tbl_time(index = date) %>% as_period(period = "monthly", side = "end"). Then we calculate log returns and store them in a column called monthly.returns, by invoking mutate(monthly.returns = log(adjusted) - log(lag(adjusted))).

Finally, we can calculate the growth of a dollar in each equity and save the result in a column called growth with mutate(growth = cumprod(1 + monthly.returns)).

returns_low <- symbols_low %>%
  tq_get(get = "stock.prices", from = start_date, collapse = "monthly") %>% 
  select(date, symbol, adjusted) %>% 
  group_by(symbol) %>% 
  as_tbl_time(index = date) %>% 
  as_period(period = "monthly", side = "end") %>%
  mutate(monthly.returns = log(adjusted) - log(lag(adjusted))) %>% 
  replace_na(list(monthly.returns = 0)) %>% 
  mutate(growth = cumprod(1 + monthly.returns)) %>% 
  as_tibble()

head(returns_low)
## # A tibble: 6 x 5
## # Groups:   symbol [1]
##   date       symbol adjusted monthly.returns growth
##   <date>     <chr>     <dbl>           <dbl>  <dbl>
## 1 2010-01-29 TAP        34.6          0       1.00 
## 2 2010-02-26 TAP        33.4         -0.0334  0.967
## 3 2010-03-31 TAP        34.8          0.0408  1.01 
## 4 2010-04-30 TAP        36.7          0.0532  1.06 
## 5 2010-05-28 TAP        34.2         -0.0709  0.984
## 6 2010-06-30 TAP        35.3          0.0317  1.02

Have a quick peak and note that we have a tidy, long formatted data frame that holds adjusted prices, monthly returns and dollar growth for each of our stocks.

Now we repeat the same process for a bucket of high labor cost stocks, again I gleaned these names from the Business Insider article.

symbols_high <- c("DRI", "FISV", "ADP",  "SRCL")

returns_high <-
  symbols_high %>% 
  tq_get(get = "stock.prices", from = start_date, collapse = "monthly") %>% 
  group_by(symbol) %>%
  select(date, symbol, adjusted) %>%
  as_tbl_time(index = date) %>% 
  as_period(period = "monthly", side = "end") %>%
  mutate(monthly.returns = log(adjusted) - log(lag(adjusted))) %>% 
  replace_na(list(monthly.returns = 0)) %>% 
  mutate(growth = cumprod(1 + monthly.returns))  %>% 
  as_tibble()

head(returns_high)
## # A tibble: 6 x 5
## # Groups:   symbol [1]
##   date       symbol adjusted monthly.returns growth
##   <date>     <chr>     <dbl>           <dbl>  <dbl>
## 1 2010-01-29 DRI        24.0          0        1.00
## 2 2010-02-26 DRI        26.3          0.0927   1.09
## 3 2010-03-31 DRI        28.9          0.0939   1.20
## 4 2010-04-30 DRI        29.2          0.0109   1.21
## 5 2010-05-28 DRI        28.0         -0.0422   1.16
## 6 2010-06-30 DRI        25.3         -0.0992   1.04

The data frame of high labor cost equities should look the same as the previous data frame.

Now we want some data on those labor costs and how they have changed over time. We will import FRED data from Quandl and ask for it in monthly increments by adding collapse = "monthly". Note that we are using tq_get() again, but accessing a different data source.

I also want to see the monthly change in labor costs, so will add a monthly_increase column with mutate(monthly_increase = (value - lag(value))/lag(value)).

We’re not done yet though. Let’s include three more transformations on wage increase: the 3-month lagged increase, the 6-month lagged increase and the cumulative wage increase if wages had started at $1. I think of the cumulative increase as a kind of wage index, and so label the column wage_index and create it with mutate(wage_index = cumprod(1 + monthly_increase)). The 3-month lagged monthly increase is created with mutate(wage_lag_3 = lag(monthly_increase, 3)) and I include in case we want to examine how the 3 month lagged increase in wages effects current monthly returns for an equity. That lag might not be enough - perhaps returns need more like 6 months to take account of wage increases so we will add that column as well.

# Change to your key here
quandl_api_key("d9EidiiDWoFESfdk5nPy")

wage_growth <- 
  "FRED/A576RC1" %>%
    tq_get(get      = "quandl",
           collapse = "monthly",
           from = start_date) %>%
  mutate(monthly_increase = (value - lag(value))/lag(value)) %>% 
  replace_na(list(monthly_increase = 0)) %>% 
  mutate(wage_index = cumprod(1 + monthly_increase)) %>% 
  mutate(wage_lag_3 = lag(monthly_increase, 3)) %>% 
  mutate(wage_lag_6 = lag(monthly_increase, 6)) %>% 
  dplyr::filter(date <= "2018-01-01")
  

head(wage_growth)
## # A tibble: 6 x 6
##   date       value monthly_increase wage_index wage_lag_3 wage_lag_6
##   <date>     <dbl>            <dbl>      <dbl>      <dbl>      <dbl>
## 1 2010-01-31  6254        0              1.00    NA               NA
## 2 2010-02-28  6217       -0.00590        0.994   NA               NA
## 3 2010-03-31  6247        0.00476        0.999   NA               NA
## 4 2010-04-30  6321        0.0119         1.01     0               NA
## 5 2010-05-31  6388        0.0105         1.02   - 0.00590         NA
## 6 2010-06-30  6388        0.0000313      1.02     0.00476         NA

Now we can create two objects: one to hold the equity prices, returns and dollar growth of the low labor cost bucket and the wage data, and a second to hold the equity prices, returns and dollar growth of the high labor cost bucket and the wage data.

returns_wages_low <- 
  returns_low %>% 
  dplyr::filter(date <= "2018-01-01") %>% 
  mutate(wage_change = wage_growth$monthly_increase,
         wage_index = wage_growth$wage_index, 
         wage_lag_3 = wage_growth$wage_lag_3,
         wage_lag_6 = wage_growth$wage_lag_6) %>% 
  as_tibble()

returns_wages_high <- 
  returns_high %>% 
  dplyr::filter(date <= "2018-01-01") %>% 
  mutate(wage_change = wage_growth$monthly_increase,
         wage_index = wage_growth$wage_index, 
         wage_lag_3 = wage_growth$wage_lag_3,
         wage_lag_6 = wage_growth$wage_lag_6)  %>% 
  as_tibble()

head(returns_wages_low %>% select(date, symbol, monthly.returns, growth, wage_lag_6, wage_index))
## # A tibble: 6 x 6
## # Groups:   symbol [1]
##   date       symbol monthly.returns growth wage_lag_6 wage_index
##   <date>     <chr>            <dbl>  <dbl>      <dbl>      <dbl>
## 1 2010-01-29 TAP             0       1.00          NA      1.00 
## 2 2010-02-26 TAP            -0.0334  0.967         NA      0.994
## 3 2010-03-31 TAP             0.0408  1.01          NA      0.999
## 4 2010-04-30 TAP             0.0532  1.06          NA      1.01 
## 5 2010-05-28 TAP            -0.0709  0.984         NA      1.02 
## 6 2010-06-30 TAP             0.0317  1.02          NA      1.02
head(returns_wages_high %>% select(date, symbol, monthly.returns, growth, wage_lag_6, wage_index))
## # A tibble: 6 x 6
## # Groups:   symbol [1]
##   date       symbol monthly.returns growth wage_lag_6 wage_index
##   <date>     <chr>            <dbl>  <dbl>      <dbl>      <dbl>
## 1 2010-01-29 DRI             0        1.00         NA      1.00 
## 2 2010-02-26 DRI             0.0927   1.09         NA      0.994
## 3 2010-03-31 DRI             0.0939   1.20         NA      0.999
## 4 2010-04-30 DRI             0.0109   1.21         NA      1.01 
## 5 2010-05-28 DRI            -0.0422   1.16         NA      1.02 
## 6 2010-06-30 DRI            -0.0992   1.04         NA      1.02

We have some data on equities and wage growth, now let’s visualize and see if we notice anything interesting. Remember, the goal is to have a template we can use going forward, as wage growth potentially accelerates through 2018.

Charts of low labor cost companies and wage growth

Let’s start with a scatter plot of monthly stock returns for the low cost bucket against the 6-month lagged wage growth. We’ll also include a regression line by adding geom_smooth(method = "lm", se = FALSE, size = .5).

returns_wages_low %>% 
  ggplot(aes( x = wage_lag_6, y = monthly.returns, color = symbol)) + 
  geom_point(size = .5) +
  geom_smooth(method = "lm", se = FALSE, size = .5) +
  facet_wrap(~symbol) +
  theme_minimal()

Taking a quick glance, Netflix seems to show the most positive correlation.

Let’s look at growth of a dollar in each equity charted alongside the wage growth index. We will make wage growth index a blue dot dash line by calling geom_line(aes( y = wage_index), color = "cornflowerblue", linetype = "dotdash").

returns_wages_low %>% 
  ggplot(aes(x = date)) + 
  geom_line(aes(y = growth, color = symbol)) +
  geom_line(aes( y = wage_index), color = "cornflowerblue", linetype = "dotdash") +
  facet_wrap(~symbol, scales = "free") +
  theme_minimal()

Apple, Netflix, Nike, Valero, Skyworks - all have outperformed the SP500 since 2010. Will that outperformacne accelerate as wage growth accelerates (and those rising wages drag down other SP500 constituents)? We can update those charts throughout 2018 and see.

Charts of high labor cost companies and wage growth

Let’s run the same visualizations for the high cost companies.

returns_wages_high %>% 
  ggplot(aes( x = wage_lag_6, y = monthly.returns, color = symbol)) + 
  geom_point(size = .5) +
  geom_smooth(method = "lm", se = FALSE, size = .5) +
  facet_wrap(~symbol) +
  theme_minimal()

Ah, noticing a slight negative relationship for FISV and SRCL.

returns_wages_high %>% 
  ggplot(aes(x = date)) + 
  geom_line(aes(y = growth, color = symbol)) +
  geom_line(aes( y = wage_index), color = "cornflowerblue", linetype = "dotdash") +
  facet_wrap(~symbol, scales = "free") + 
  theme_minimal()

We can see that the high cost bucket contains one large underperformer in SRCL, and perhaps if the bucket were bigger (if we knew more of the constituents) it would contain more large underperformers. FISERV has been a very strong performer, but perhaps rising wage costs will slow it down in 2018.

Extensions

In a follow up post, we’ll turn those 14 low labor cost stocks (and can include more if anyone has suggestions for others that fit the low cost criteria) and 4 high labor cost stocks into their own indices. That is very similar to turning them into a portfolio which we covered in a previous post. Then we can revisit the indices throughout the year, along with wage data, and examine the relationship.

Another extension could explore a way to search fundamental data for those companies with low and high labor costs, which would allow us to dynamically reconstruct and rebalance our buckets over time. So little time so much to do!

Oh yeah

It should go without saying but: nothing in this post is in any way financial or investing advice.


  1. On the subject of wages in 2018, this piece from Bloomberg has a bit more on rising labor costs as we head into 2018, and another from CNBC is here.

Share Comments