Performance Trends in State-Facilitated Retirement Savings Programs

Author
Affiliation

Parsa Keyvani

Georgetown University Center for Retirement Initiatives

1 Introduction

A review of the data over a 3-year period of program performance — December 2019 to December 2022 — for OregonSaves, Illinois Secure Choice, and CalSavers, highlighting several positive trends.

2 Total Assets

2.1 Raw Data at a Glance

2.2 Data Cleaning and Preprocessing

In the data preprocessing and cleaning steps, several important procedures were carried out to ensure the quality and completeness of the dataset. These steps are detailed below:

1. Removing Duplicate Rows: Two duplicate rows in the dataset were identified and eliminated using the unique function in R. This process helps to ensure that each data point in the dataset is unique, preventing any duplication that might lead to inaccurate analysis or skewed results.

2. Handling Missing Values in “CalSavers”: In the “CalSavers” column, four rows contained missing values. This occurred because CalSavers did not initiate monthly reporting until June 30, 2020, and instead had a previous practice of reporting on a quarterly basis. To resolve this issue and align the data with the reporting frequency, we applied the Last Observation Carried Forward (LOCF) imputation method. The LOCF technique involves replacing missing values with the most recent observed value in the time series. This method was chosen to ensure that the imputed values accurately reflect the temporal context of the data. By utilizing the most recent available data point, we maintain the continuity of the information, addressing the missing values without disrupting the dataset’s chronological integrity. This approach enhances the dataset’s completeness and allows for more meaningful analysis and interpretation.

3. Filling in Missing “Total” Values: The “Total” column had four missing values as well because Calsavers was missing four values. To calculate the Total value for all the three states, the sum of the “OregonSaves,” “IL Secure Choice,” and “CalSavers” columns for their respective rows was computed using the rowSums function. This step allowed us to ensure that the “Total” column is complete and accurately reflects the total assets under management for each corresponding row.

After completing the data cleaning and preprocessing of the Total Assets dataset, we have generated the following dataset, which will be used for analyses.

2.3 Visualizations

Dynamic Visualization

d3_data_race <-Assets_by_Program %>%
  select(-Total) %>%
  pivot_longer(cols = OregonSaves:CalSavers) 

write.csv(d3_data_race, "d3_data_race.csv")

Static Visualization

# in scoobydoo.Rmd
library("dplyr")
library("tidyr")
library("stringr")
library("lubridate")
library("tidytuesdayR")
# load data from tidytuesday
tuesdata = tidytuesdayR::tt_load(2021, week = 29)
--- Compiling #TidyTuesday Information for 2021-07-13 ----
--- There is 1 file available ---
--- Starting Download ---

    Downloading file 1 of 1: `scoobydoo.csv`
--- Download complete ---
scoobydoo = tuesdata$scoobydoo

# wrangling data into nice shape
monsters_caught = scoobydoo %>%
  select(date_aired, starts_with("caught")) %>%
  mutate(across(starts_with("caught"), ~ as.logical(.))) %>%
  pivot_longer(cols = caught_fred:caught_not,
               names_to = "character",
               values_to = "monsters_caught") %>%
  drop_na()  %>%
  filter(!(character %in% c("caught_not", "caught_other"))) %>%
  mutate(year = year(date_aired), .keep = "unused") %>%
  group_by(character, year) %>%
  summarise(caught = sum(monsters_caught),
            .groups = "drop_last") %>%
  mutate(
    cumulative_caught = cumsum(caught),
    character = str_remove(character, "caught_"),
    character = str_to_title(character),
    character = recode(character, "Daphnie" = "Daphne")
  )


# setting up colors for each character
character_hex = tribble(
  ~ character, ~ color,
  "Fred", "#76a2ca",
  "Velma", "#cd7e05",
  "Scooby", "#966a00",
  "Shaggy", "#b2bb1b",
  "Daphne", "#7c68ae"
)

monsters_caught = monsters_caught %>% 
  inner_join(character_hex, by = "character")

library("r2d3")
r2d3(data = monsters_caught,
     script = "scoobydoo.js",
     d3_version = "5")
// set up constants used throughout script
const margin = {top: 80, right: 100, bottom: 40, left: 60}
const plotWidth = 800 - margin.left - margin.right
const plotHeight = 400 - margin.top - margin.bottom

const lineWidth = 3
const mediumText = 18
const bigText = 28

// set width and height of svg element (plot + margin)
svg.attr("width", plotWidth + margin.left + margin.right)
   .attr("height", plotHeight + margin.top + margin.bottom)
   
// create plot group and move it
let plotGroup = svg.append("g")
                   .attr("transform",
                         "translate(" + margin.left + "," + margin.top + ")")

// x-axis values to year range in data
// x-axis goes from 0 to width of plot
let xAxis = d3.scaleLinear()
    .domain(d3.extent(data, d => { return d.year; }))
    .range([ 0, plotWidth ]);
    
// y-axis values to cumulative caught range
// y-axis goes from height of plot to 0
let yAxis = d3.scaleLinear()
    .domain(d3.extent(data, d => { return d.cumulative_caught; }))
    .range([ plotHeight, 0]);
    
    
plotGroup.append("g")
   .attr("transform", "translate(0," + plotHeight + ")")
   .call(d3.axisBottom(xAxis).tickFormat(d3.format("d")))
   .attr("stroke-width", lineWidth)
   .attr("font-size", mediumText);

// add y-axis to plot
// set stroke width and font size
plotGroup.append("g")
    .call(d3.axisLeft(yAxis))
    .attr("stroke-width", lineWidth)
    .attr("font-size", mediumText);
    
    
let nestedData = d3.nest()
    .key(d => { return d.character;})
    .entries(data);
    
let path = plotGroup.selectAll(".drawn_lines")
    .data(nestedData)
    .enter()
    .append("path")
    // set up class so only this path element can be removed
    .attr("class", "drawn_lines")
    .attr("fill", "none")
    // color of lines from hex codes in data
    .attr("stroke", d => {return d.values[0].color}) 
    .attr("stroke-width", lineWidth)
     // draw line according to data
    .attr("d", d => {
      return d3.line()
        .x(d => { return xAxis(d.year);})
        .y(d => { return yAxis(d.cumulative_caught);})
        (d.values)
    })    

YAYYYYY

3 Total Funded Accounts

4 Average Funded Account Balance

5 Average Monthly Contributions

6 Withdrawals and Opt-Out Rates

7 Employers Registered