Student debt cancellation is less progressive than universal payments

By Nate Golden, 2020-11-17

Over 40 million Americans possess over $1.5 trillion in student debt. And as new borrowers take out loans faster than old borrowers pay them back, that number is increasing with time.

This problem has led to Democrats across the ideological spectrum to advocate for student debt relief. In the 2020 primary, Bernie Sanders had the most generous plan, calling for the cancellation of all outstanding student debt regardless of a person’s income. Also in the primary, Elizabeth Warren proposed cancelling up to $50,000 in student debt, phasing out for households with income above $100,000; in September 2020, she and Senate Minority Leader Chuck Schumer introduced a resolution calling on Biden to cancel $50,000 in debt per person through executive order (the resolution didn’t mention phasing out the cancellation with income). Back in April, President-elect Joe Biden recommended forgiving a minimum of $10,000 of student debt per person.

In this paper, I analyze which Americans would benefit the most from student debt cancellation and examine how it compares to budget-equivalent universal payments. I find that, across a range of distributional outcomes, each student debt cancellation plan would be less progressive than a universal payment of the same total cost.

Who holds the debt?

First, a caveat: data on student debt is incomplete. The Federal Reserve’s Survey of Consumer Finances (SCF) is the primary source of wealth microdata, powering inequality statistics, detailed breakdowns of assets and liabilities, and microsimulations like mine. However, it only counts people in a household’s “Primary Economic Unit,” meaning economically independent young adults living with parents are excluded. As a result, the SCF understates total student debt by about a third, compared to aggregate data sources like the G.19 and Consumer Credit Panel. The missing student debt is disproportionately held by young people and people in the bottom and top income quintiles. While my colleagues and I aim to refine the data, the SCF is currently the best available source for this sort of analysis, so I use it here while acknowledging its limitations.

That said, the 2019 SCF reports $1.1 trillion of total student debt,1 held by households representing one in four Americans. Some demographics are more likely to hold debt than others:2

  • Black Americans are the most likely to have student debt, while Hispanic Americans are the least likely, with 33 percent and 18 percent of people possessing student debt respectively.

  • Young people are more likely to hold student debt than any other age group: 40 percent of people under 35 have student debt compared to just 2 percent of those who are 75 or older.

  • Income quintiles3 follow a bell curve, with the highest amount of debt held by the middle class and smaller amounts held by the lowest and highest quintile.

  • Americans living in poverty4 are less likely to possess student debt than Americans living above the poverty line.

The chart below (and others like it included in this paper) allow you to compare the results across race, education level, age groups, income quintiles, net worth quintiles, and poverty status.

# Import libraries
import pandas as pd
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import microdf as mdf
import plotly.graph_objects as go

race = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/race_debt_ubi%20(1).csv')
education = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/education_debt_ubi.csv')
age = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/age_debt_ubi.csv')
income = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/income_debt_ubi%20(1).csv')
networth = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/networth_debt_ubi%20(1).csv')
poor = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/poor_debt_ubi.csv')
all2 = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/all2.csv')
racial_wealth_gap = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/racial_wealth_gap%20(2)')
scf = pd.read_csv('https://github.com/UBICenter/ed_debt_vs_ubi/raw/main/scf_raw2')

race2 = race.drop([4])
education2 = education.drop([4])
age2 = age.drop([6])
income2 = income.drop([5])
networth2 = networth.drop([5])
poor2 = poor.drop([2])

# Colors from https://material.io/design/color/the-color-system.html
BLUE = '#1976D2'
DARK_BLUE = '#1565C0'
LIGHT_BLUE = '#90CAF9'
GRAY = '#BDBDBD'

colors = [GRAY,] * 5

colors2 = [GRAY,] * 7

colors3 = [GRAY,] * 6

colors4 = [GRAY,] * 3

fig = go.Figure()

fig.add_trace(go.Bar(x=race2['race'], y=race2['percent_has_debt'], 
                     text=race2['percent_has_debt'], name='race',
                     showlegend=False, marker_color=colors))

fig.add_trace(go.Bar(x=education2['edcl'], y=education2['percent_has_debt'],
                     text=education2['percent_has_debt'], name='education',
                     visible = False, showlegend=False, marker_color=colors))

fig.add_trace(go.Bar(x=age2['agecl'], y=age2['percent_has_debt'], name='age',
                     text=age2['percent_has_debt'], visible = False,
                     showlegend=False, marker_color=colors2))

fig.add_trace(go.Bar(x=income2['income_pp_quintile'], y=income2['percent_has_debt'],
                     text=income2['percent_has_debt'], name='income', visible = False,
                     showlegend=False, marker_color=colors3))

fig.add_trace(go.Bar(x=networth2['networth_pp_quintile2'],
                     text=networth2['percent_has_debt'], y=networth2['percent_has_debt'],
                     name='networth', visible = False, showlegend=False, marker_color=colors3))

fig.add_trace(go.Bar(x=poor2['original_poor'], y=poor2['percent_has_debt'],
                     text=poor2['percent_has_debt'], name='poor',
                     visible = False, showlegend=False, marker_color=colors4))


fig.update_layout(uniformtext_minsize=13, uniformtext_mode='hide', plot_bgcolor='white')
fig.update_traces(texttemplate='%{text}%', textposition='outside')

fig.update_xaxes(
        tickangle = 0,
        title_text = "Demographic of head of household",
        tickfont = {"size": 14},
        title_standoff = 25)

fig.update_yaxes(
        title_text = "Share of people in households with student debt",
        ticksuffix ="%",
        tickfont = {'size':14},
        title_standoff = 25,
        range=[0,50])

fig.update_xaxes(title_font=dict(size=14, family='Roboto', color='black'))
fig.update_yaxes(title_font=dict(size=14, family='Roboto', color='black'))
fig.update_layout(title_text='Population share with student debt by race')

fig.update_layout(
    updatemenus=[go.layout.Updatemenu(
        active=0,
        buttons=list([
            dict(label="Race",
                 method="update",
                 args=[{'visible':[True,False,False,False,False,False]},
                       {'title':'Population share with student debt by race',
                        'showlegend':True}]),
            dict(label="Education",
                 method="update",
                 args=[{'visible':[False,True,False, False,False, False]},
                       {'title':'Population share with student debt by education level',
                        'showlegend':True}]),
            dict(label="Age",
                 method="update",
                 args=[{'visible':[False,False,True, False, False, False]},
                       {'title':'Population share with student debt by age',
                        'showlegend':True}]),
            dict(label="Income",
                 method="update",
                 args=[{'visible':[False,False,False, True, False, False]},
                       {'title':'Population share with student debt by income quintile',
                        'showlegend':True}]),
            dict(label="Networth",
                 method="update",
                 args=[{'visible':[False,False,False, False, True, False]},
                       {'title':'Population share with student debt by net worth quintile',
                        'showlegend':True}]),
            dict(label="Poverty Status",
                 method="update",
                 args=[{'visible':[False,False,False, False, False, True]},
                       {'title':'Population share with student debt by poverty status',
                        'showlegend':True}]), 
            ]),
            direction="down",
            pad={"r": 10, "t": 10},
            showactive=True,
            x=-0.35,
            xanchor="left",
            y=1.1,
            yanchor="top"
    
    )])

config = {'displayModeBar': False}

fig.show(config=config)