Mike Konczal
  • Home
  • About
  • Book
  • Writings
  • Research
  • Media
  • Quotes
  • Resume

Mike Konczal

Substack Twitter GitHub Contact me

Mike Konczal is director of Macroeconomic Analysis at the Roosevelt Institute, where he focuses on full employment, inequality, and the role of public power in a democracy. He is the author of the recent Freedom from the Market: America’s Fight to Liberate Itself from the Grip of the Invisible Hand, and a co-author, with Joseph Stiglitz, of Rewriting the Rules of the American Economy. A former financial engineer, his writing has been featured in the New York Times, Washington Post, Rolling Stone, Vox, and more. A sought-after commentator on the U.S. economy, he has also appeared on CBS Sunday Morning, All Things Considered, Planet Money, Lovett or Leave It, and elsewhere.

Here are some of the monthly graphics I’m currently watching, along with the R code to reproduce them yourself.

Show the code
# Graphic - Three Onion Layer
# Hard code in weights
library(tidyverse)
library(janitor)
library(scales)
library(hrbrthemes)
library(ggrepel)
library(bea.R)
get_NIPA_data <- function(beaKey, TableName, Frequency, Year, data_set_name = 'NIPA'){
  NIPA_request <- list(
    'UserID' = beaKey ,
    'Method' = 'GetData',
    'datasetname' = data_set_name,
    'TableName' = TableName,
    'Frequency' = Frequency,
    'Year' = Year,
    'ResultFormat' = 'json'
  );
  NIPA_data <- beaGet(NIPA_request, asWide = FALSE)
  return(NIPA_data)
}

BEA_date_monthly <- function(x){
  x <- x %>%
    mutate(year = substr(TimePeriod, 1, 4)) %>%
    mutate(month = substr(TimePeriod, 6,7))
  x$date <- paste(x$month, "01", x$year, sep="/")
  x$date <- as.Date(x$date, "%m/%d/%Y")
  x <- x %>% select(-month, -year)
  return(x)
}

beaKey <- read_csv("/Users/mkonczal/Documents/data_folder/BEA_key/BEA_key.csv")
beaKey <- as.character(beaKey)
# Table IDs
# https://www.bea.gov/system/files/2021-07/TablesRegisterPreview.txt

PCE_Weight <- get_NIPA_data(beaKey, 'U20405', 'M', '2018,2019,2020,2021,2022,2023', data_set_name = 'NIUnderlyingDetail')
PCE_Weight <- BEA_date_monthly(PCE_Weight)

PCE_Weight <- PCE_Weight %>% filter(SeriesCode == "DPCERC") %>%
  select(date, TotalGDP = DataValue) %>%
  left_join(PCE_Weight, by="date") %>%
  # The weight is approximated as nominal consumption shares as a percent of the total.
  mutate(PCEweight = DataValue/TotalGDP) %>%
  select(date, LineDescription, PCEweight)

pce <- get_NIPA_data(beaKey, 'U20404', 'M', '2018,2019,2020,2021,2022,2023', data_set_name = 'NIUnderlyingDetail')
pce <- BEA_date_monthly(pce)

pce <- pce %>%
  left_join(PCE_Weight, by=c('date' = 'date','LineDescription' = 'LineDescription'))

pce <- pce %>%
  group_by(SeriesCode) %>%
  mutate(DataValue_P1 = (DataValue - lag(DataValue,1))/lag(DataValue,1)) %>%
  # Use the lagged weight for weighted share
  mutate(WDataValue_P1 = DataValue_P1*lag(PCEweight,1)) %>%
  mutate(WDataValue_P1a = (1+WDataValue_P1)^12-1) %>%
  ungroup()

# With data in place, move to creating the onion chart
core_goods_fields <- c("Goods","Gasoline and other energy goods","Food and beverages purchased for off-premises consumption","Services","Electricity and gas","Housing")

core_analysis <- pce %>% filter(LineDescription %in% core_goods_fields) %>%
  filter(date >= "2018-01-01") %>%
  select(date, LineDescription, WDataValue_P1) %>%
  pivot_wider(names_from=LineDescription, values_from = WDataValue_P1) %>%
  clean_names() %>%
  mutate(core_goods = goods - food_and_beverages_purchased_for_off_premises_consumption - gasoline_and_other_energy_goods) %>%
  mutate(core_services = services - electricity_and_gas - housing) %>%
  mutate(core_inflation = core_goods + core_services) %>%
  pivot_longer(-date, names_to = "item_name", values_to = "WDataValue_P1") %>%
  filter(item_name %in% c("core_goods","core_services","housing")) %>%
  mutate(item_nameF = factor(item_name, levels = c("core_goods", "housing", "core_services"))) %>%
  mutate(WDataValue_P1a = (WDataValue_P1+1)^12-1) %>%
  filter(WDataValue_P1a > -0.02)

levels(core_analysis$item_nameF)[levels(core_analysis$item_nameF) == "core_goods"] <-"Core Goods"
levels(core_analysis$item_nameF)[levels(core_analysis$item_nameF) == "core_services"] <-"Non-Housing Services"
levels(core_analysis$item_nameF)[levels(core_analysis$item_nameF) == "housing"] <-"Housing"

onion_datebreaks <- unique(sort(core_analysis$date,decreasing = TRUE))
onion_datebreaks <- onion_datebreaks[seq(1,length(onion_datebreaks),24)]

ggplot(core_analysis, aes(x = date, y = WDataValue_P1a, fill=item_nameF)) +
  geom_bar(stat = 'identity', size=0) +
  theme_modern_rc() +
  theme(legend.position = "none", legend.title = element_blank()) + 
  facet_grid(~item_nameF) +
  labs(y = NULL,
       x = NULL,
       title = "The PCE inflation onion layers",
       subtitle = "Monthly contribution to inflation",
       caption ="BEA, NIPA Tables 2.4.4 and 2.4.5. Weights approximated as nominal consumption shares as a percent of the total.\nApril 2020 Core Services ex Housing value excluded as large negative outlier.") +
  scale_fill_brewer(palette="RdPu", name = "item_name") +
  scale_y_continuous(labels = percent) +
  scale_x_date(date_labels = "%b\n%Y", breaks = onion_datebreaks) +
  theme(axis.text.x = element_text(size=14), axis.text.y = element_text(size=14),
  strip.text = element_text(face = "bold", color="white", hjust = 0.5, size = 10),
  strip.background = element_blank(),
  plot.title.position = "plot")

Fed officials have argued goods (dis)inflation is likely transitory, housing is reported with a lag, but non-housing services are a cleaner indicator of demand, as well as being more persistent and determined by wage pressures. Here those are, broken out for PCE inflation.

Show the code
# Download data
cpi_data <- read_delim(file = "https://download.bls.gov/pub/time.series/cu/cu.data.0.Current")
cpi_data <- cpi_data %>% clean_names()
cpi_data$value <- as.numeric(cpi_data$value)
cpi_data$series_id <- str_trim(cpi_data$series_id)
cpi_data$date <- paste(substr(cpi_data$period, 2,3), "01", substr(cpi_data$year, 3, 4), sep="/")
cpi_data$date <- as.Date(cpi_data$date, "%m/%d/%y")

series <- read_delim(file = "https://download.bls.gov/pub/time.series/cu/cu.series")
series <- series %>% clean_names()
series$series_id <- str_trim(series$series_id)

items <- read_delim(file = "https://download.bls.gov/pub/time.series/cu/cu.item")
series <- inner_join(series, items, by = c("item_code"))
cpi_data <- inner_join(cpi_data, series, by = c("series_id"))


# Graphic - Supercore
supercore <- cpi_data %>% filter(seasonal == "S") %>%
  filter(item_name == "All items less food, shelter, energy, and used cars and trucks") %>%
  select(item_name, date, value) %>%
  mutate(pre_value = value[date=="2020-01-01"]/value[date=="2018-01-01"]) %>%
  mutate(pre_value = pre_value^(12/24)-1) %>%
  mutate(ThreeMonth = (value/lag(value,3))^4-1) %>%
  mutate(SixMonth = (value/lag(value,6))^2-1) %>%
  filter(date >= "2018-01-01") %>%
  pivot_longer(ThreeMonth:SixMonth, names_to = "time_length", values_to = "change") %>%
  mutate(time_length = str_replace_all(time_length,"SixMonth", "6-Month Change")) %>%
  mutate(time_length = str_replace_all(time_length,"ThreeMonth", "3-Month Change")) %>%
  mutate(last_value = ifelse(date==max(date),change,NA)) %>%
  mutate(last_value_pre = ifelse(date==max(date) & time_length=="3-Month Change",pre_value,NA))

supercore_datebreaks <- unique(sort(supercore$date,decreasing = TRUE))
supercore_datebreaks <- supercore_datebreaks[seq(1,length(supercore_datebreaks),12)]

ggplot(supercore, aes(date, change, color=time_length, label=label_percent(accuracy=0.1)(last_value))) +
  geom_line(size=1.2) +
  theme_modern_rc() +
  geom_line(aes(date,pre_value), linetype="dashed", color="#2D779C") +
  labs(x="", y="",
       title="Supercore CPI: All items less food, shelter, energy, used cars",
       subtitle = "Monthly percent change, annualized; trendline is value from Jan 2017 to Jan 2020.",
       caption = "") +
  scale_y_continuous(labels = percent) +
  scale_x_date(date_labels = "%b\n%Y", breaks = supercore_datebreaks) +
  theme(legend.position = c(0.30,0.85), legend.title = element_blank()) +
  theme(axis.text.x = element_text(size=14), axis.text.y = element_text(size=14),
        legend.text = element_text(size=11)) +
  scale_color_manual(values=c("#2D779C", "#A4CCCC")) +
  geom_text(show.legend=FALSE, nudge_x = 155) +
  geom_text(aes(date,last_value_pre, label=label_percent(accuracy=0.1)(pre_value)), show.legend=FALSE, nudge_x = 155) +
  theme(plot.title.position = "plot")

Core inflation excludes prices that are volatile and set globally (food and energy). But by also going to supercore, which excludes used autos (because of complicated supply-side issues) and housing (because of data-measurement and reporting issues), we can get closer to measuring underlying demand pressure of CPI inflation here.

Show the code
library(tidyverse)
library(janitor)
library(scales)
library(hrbrthemes)
library(ggrepel)

jolts_data <- read_delim(file = "https://download.bls.gov/pub/time.series/jt/jt.data.1.AllItems")
jolts_data <- jolts_data %>%
  clean_names()
jolts_data$value <- as.numeric(jolts_data$value)
jolts_data$series_id <- str_trim(jolts_data$series_id)
jolts_data$date <- paste(substr(jolts_data$period, 2,3), "01", jolts_data$year, sep="/")
jolts_data$date <- as.Date(jolts_data$date, "%m/%d/%Y")


eci_wages <- read_delim(file = "https://download.bls.gov/pub/time.series/ci/ci.data.1.AllData") %>%
  clean_names()
eci_wages$value <- as.numeric(eci_wages$value)
eci_wages <- eci_wages %>%
  mutate(month = case_when(
    period == "Q01" ~ 3,
    period == "Q02" ~ 6,
    period == "Q03" ~ 9,
    period == "Q04" ~ 12))
eci_wages$date <- paste(eci_wages$month, "01", eci_wages$year, sep="/")
eci_wages$date <- as.Date(eci_wages$date, "%m/%d/%Y")

# Graphic Set 1 : Sets Up Calculations
# Openings and Quits versus ECI Wages
JoltsMerge <- jolts_data %>% filter(series_id == "JTS000000000000000QUR") %>% mutate(value = value/100) %>%
  select(date, quitsR = value)

merged <- eci_wages %>% filter(series_id == "CIS2020000000000I") %>% mutate(value = value/lag(value)-1) %>%
  select(ECI_growth = value, date) %>%
  inner_join(JoltsMerge, by=c("date")) %>%
  filter(!is.na(ECI_growth)) %>%
  mutate(Is_2021_to_2022 = (date >= "2021-01-01"))

regression_prior <- merged %>% filter(date < "2021-01-01") %>% lm(ECI_growth ~ quitsR, data=.)

merged %>%
  mutate(values_last = if_else(date >= "2022-01-01", date, as.Date(NA))) %>% mutate(values_last2 = as.character(format(values_last, "%b\n%Y"))) %>%
  mutate(Is_2021_to_2022_v = if_else(date >= "2022-01-01",ECI_growth,as.numeric(NA))) %>%
  ggplot(aes(quitsR, ECI_growth, color=Is_2021_to_2022, label=values_last2)) + geom_point() + theme_modern_rc() +
  geom_abline(intercept = regression_prior$coefficients[1], slope=regression_prior$coefficients[2], color="#2D779C") +
  geom_text_repel(size=3) +
  geom_path(aes(quitsR,Is_2021_to_2022_v)) +
  labs(x = "Quit Rate",
       y = "ECI Private Wage Growth, Quarterly",
       title = "Labor-market tightness: wage growth versus quit rates",
       subtitle = "Regression line and blue points reflect 2001 to 2020.",
       caption =NULL) +
  theme(panel.grid.major.y = element_line(size=0.5)) +
  theme(plot.title.position = "plot") +
  scale_y_continuous(labels = percent) +
  scale_x_continuous(labels = percent) +
  theme(axis.title.x = element_text(size=14, color="white", vjust=-1.5), axis.title.y = element_text(size=14, angle = 90, vjust = 3),
        plot.subtitle = element_text(size=12, color="white"), legend.position = "none") +
  scale_color_manual(values=c("#2D779C", "#CC79A7"))

The unemployment rate is a poor measure for labor market tightness, and average hourly wages also has measurement problems. All the cool kids look at the JOLTs quit rate and ECI private wages instead.

Praise for Freedom From the Market:

“The Roosevelt Institute’s Konczal is one of the warriors in this fight, arguing fiercely for the need to set much narrower limits on what is left to markets than has been the case in recent decades. A powerful polemic.”
- Martin Wolf, Financial Times

“By identifying an alternative grammar, one that is grounded in the American past, Freedom from the Market provides a way out of the political cul-de-sac created by the failure of the market to deliver on its promises of ‘freedom.’”
- Molly Michelmore, Democracy: A Journal of Ideas

“Freedom from the Market is an impressive book, easily one of the best I’ve read in the past several years. I cannot recommend it highly enough.”
- Matt Mazewski, Commonweal

“terrific book.”
- Jamelle Bouie, New York Times

“Freedom from the Market has the potential to be a very important book, focusing attention on the contested, messy but crucially important intersection between social movements and the state. It provides a set of ideas that people on both sides of that divide can learn from, and a lively alternative foundation to the deracinated technocratic notions of politics, in which good policy would somehow, magically, be politically self supporting, that has prevailed up until quite recently. Strongly recommended.”
—Henry Farrell, Crooked Timber

Made with Quarto (This Site’s Repo)