Simulate different resource reductions in response to a recession.
import pandas as pd
import numpy as np
import microdf as mdf
import matplotlib.pyplot as plt
import matplotlib as mpl
import os
mdf.set_plot_style(dpi=300)
Looks for the 2019 March Supplement in a ~/data
folder.
ASEC_F = '~/data/asec/2019/pppub19.csv'
Otherwise download from https://www.census.gov/data/datasets/time-series/demo/cps/cps-asec.html.
if not os.path.isfile(os.path.expanduser(ASEC_F)):
!mkdir ~/data
!wget -O ~/data/asecpub19csv.zip http://thedataweb.rm.census.gov/pub/cps/march/asecpub19csv.zip
!unzip ~/data/asecpub19csv.zip -d ~/data
SPM_COLS = ['povthreshold', 'resources', 'poor', 'numper', 'numkids',
'numadults', 'id', 'weight']
OTHER_COLS = ['A_AGE', 'MARSUPWT', 'PRCITSHP']
cols = ['SPM_' + i.upper() for i in SPM_COLS] + OTHER_COLS
raw = pd.read_csv(ASEC_F, usecols=cols)
df = raw.copy(deep=True)
df.columns = map(str.lower, df.columns)
Add true weight by dividing by 100.
df['w'] = df.marsupwt / 100.
df['spm_w'] = df.spm_weight / 100.
Add citizenship indicator, per https://www2.census.gov/programs-surveys/cps/techdocs/cpsmar19.pdf.
df['is_citizen'] = df.prcitshp != 5
df['is_kid'] = df.a_age < 18
df['is_adult'] = df.a_age >= 18
df['is_kid_citizen'] = df.is_citizen & df.is_kid
df['is_adult_citizen'] = df.is_citizen & df.is_adult
Number of citizens per SPM unit.
spmu = df.groupby('spm_id')[['is_kid', 'is_adult',
'is_kid_citizen', 'is_adult_citizen']].sum()
spmu.columns = ['spm_nu18', 'spm_n18',
'spm_numkidcitizens', 'spm_numadultcitizens']
df = df.merge(spmu, on='spm_id')
Merge in other SPM unit characteristics.
spmu_raw = df[['spm_resources', 'spm_povthreshold', 'spm_id',
'spm_w']].drop_duplicates()
spmu = spmu.join(spmu_raw.set_index('spm_id'))
Percent citizen, overall and for kids and adults.
mdf.weighted_mean(df, 'is_citizen', 'w')
0.9274755680739586
mdf.weighted_mean(df, 'is_kid', 'w')
0.2275056617172911
mdf.weighted_mean(df[df.is_kid], 'is_citizen', 'w')
0.9718902678491073
mdf.weighted_mean(df[~df.is_kid], 'is_citizen', 'w')
0.9143950894716393
def rounded_pct(x):
print(str((x * 100).round(1)) + '%')
def print_pov(df, weight='w'):
rounded_pct(mdf.weighted_mean(df, 'spm_poor', weight))
def ubi_pov_rate(resource_cut=0, ubi_adult=0, ubi_kid_share=0,
include_noncitizens=True):
""" Calculate SPM poverty rate in billions, given a certain cut in
baselin resources and UBI amounts for each category.
Args:
resource_cut: Percentage cut in baseline resource, between 0 and 1.
ubi_adult: UBI per adult.
ubi_kid_share: UBI per child as a share of adult UBI.
include_noncitizens: Whether noncitizens should also get the UBI.
Defaults to true.
Returns:
Poverty rate (between 0 and 1).
"""
ubi_kid = ubi_adult * ubi_kid_share
resources = (
df.spm_resources * (1 - resource_cut) +
ubi_adult * np.where(include_noncitizens, df.spm_n18,
df.spm_numadultcitizens) +
ubi_kid * np.where(include_noncitizens, df.spm_nu18,
df.spm_numkidcitizens))
is_pov = resources < df.spm_povthreshold
return (is_pov * df.w).sum() / df.w.sum()
def ubi_pov_gap_b(resource_cut=0, ubi_adult=0, ubi_kid_share=0,
include_noncitizens=True):
""" Calculate SPM poverty gap in billions, given a certain cut in baseline
resources and UBI amounts for each category.
Args:
resource_cut: Percentage cut in baseline resource, between 0 and 1.
ubi_adult: UBI per adult.
ubi_kid_share: UBI per child as a share of adult UBI.
include_noncitizens: Whether noncitizens should also get the UBI.
Defaults to true.
Returns:
Poverty gap in billions.
"""
ubi_kid = ubi_adult * ubi_kid_share
# Use spmu dataset.
resources = (
spmu.spm_resources * (1 - resource_cut) +
ubi_adult * np.where(include_noncitizens, spmu.spm_n18,
spmu.spm_numadultcitizens) +
ubi_kid * np.where(include_noncitizens, spmu.spm_nu18,
spmu.spm_numkidcitizens))
pov_gap = np.maximum(spmu.spm_povthreshold - resources, 0)
return (pov_gap * spmu.spm_w).sum() / 1e9
def ubi_pov_gap_pp(resource_cut=0, ubi_adult=0, ubi_kid_share=0,
include_noncitizens=True):
""" Calculate SPM per-person poverty gap, given a certain cut in baseline
resources and UBI amounts for each category.
Args:
resource_cut: Percentage cut in baseline resource, between 0 and 1.
ubi_adult: UBI per adult.
ubi_kid_share: UBI per child as a share of adult UBI.
include_noncitizens: Whether noncitizens should also get the UBI.
Defaults to true.
Returns:
Poverty gap divided by total population.
"""
return (1e9 * ubi_pov_gap_b(resource_cut, ubi_adult, ubi_kid_share,
include_noncitizens) /
df.w.sum())
def ubi_pov_gap_per_pov(resource_cut=0, ubi_adult=0, ubi_kid_share=0,
include_noncitizens=True):
""" Calculate SPM per-person-in-poverty poverty gap, given a certain cut
in baseline resources and UBI amounts for each category.
Args:
resource_cut: Percentage cut in baseline resource, between 0 and 1.
ubi_adult: UBI per adult.
ubi_kid_share: UBI per child as a share of adult UBI.
include_noncitizens: Whether noncitizens should also get the UBI.
Defaults to true.
Returns:
Poverty gap divided by total population in poverty.
"""
return (ubi_pov_gap_pp(resource_cut, ubi_adult, ubi_kid_share,
include_noncitizens) /
ubi_pov_rate(resource_cut, ubi_adult, ubi_kid_share,
include_noncitizens))
def ubi_cost_b(ubi_adult=0, ubi_kid_share=0, include_noncitizens=True):
ubi_adult_cost = (
ubi_adult * df.is_adult * df.w *
np.where(include_noncitizens, 1, df.is_citizen)).sum()
ubi_kid_cost = (
ubi_adult * ubi_kid_share * df.is_kid * df.w *
np.where(include_noncitizens, 1, df.is_citizen)).sum()
return (ubi_adult_cost + ubi_kid_cost) / 1e9
print_pov(df)
12.7%
print_pov(df[df.is_citizen])
11.9%
print_pov(df[~df.is_citizen])
23.7%
print_pov(df[df.is_adult])
12.5%
print_pov(df[df.is_kid])
13.6%
ubi_pov_gap_b()
169.98995792551003
ubi_pov_gap_pp()
524.0847744869411
ubi_pov_gap_per_pov()
4115.75580934004
Simulate for each percentage point cut between 0 and 20.
CUTS = np.arange(21) / 100
POP_M = 327.2
cuts = pd.DataFrame(index=CUTS)
cuts['pov_rate'] = cuts.index.map(lambda x: ubi_pov_rate(resource_cut=x))
cuts['pov_gap_b'] = cuts.index.map(lambda x: ubi_pov_gap_b(resource_cut=x))
cuts['pov_m'] = cuts.pov_rate * POP_M
cuts
pov_rate | pov_gap_b | pov_m | |
---|---|---|---|
0.00 | 0.127336 | 169.989958 | 41.664410 |
0.01 | 0.130368 | 172.048194 | 42.656338 |
0.02 | 0.133005 | 174.184895 | 43.519106 |
0.03 | 0.136044 | 176.399871 | 44.513513 |
0.04 | 0.138870 | 178.695124 | 45.438231 |
0.05 | 0.142090 | 181.080439 | 46.491921 |
0.06 | 0.145322 | 183.553945 | 47.549216 |
0.07 | 0.148721 | 186.132003 | 48.661595 |
0.08 | 0.152685 | 188.824903 | 49.958619 |
0.09 | 0.155851 | 191.625085 | 50.994585 |
0.10 | 0.159426 | 194.530486 | 52.164289 |
0.11 | 0.163334 | 197.544359 | 53.442856 |
0.12 | 0.167200 | 200.677932 | 54.707992 |
0.13 | 0.171176 | 203.937196 | 56.008828 |
0.14 | 0.175477 | 207.332827 | 57.415974 |
0.15 | 0.179119 | 210.856836 | 58.607861 |
0.16 | 0.183952 | 214.513483 | 60.189162 |
0.17 | 0.188133 | 218.306625 | 61.557193 |
0.18 | 0.192242 | 222.243173 | 62.901733 |
0.19 | 0.196555 | 226.316838 | 64.312832 |
0.20 | 0.201451 | 230.552233 | 65.914891 |
ax = cuts.pov_rate.plot()
plt.title('How the poverty rate rises with lost income', loc='left')
plt.xticks(np.arange(0, 0.21, 0.02))
ax.xaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
ax.yaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
plt.xlabel('Fall in SPM resources')
plt.ylabel('SPM poverty rate')
plt.savefig('charts/pov_rate_income.png')
plt.show()
ax = cuts.pov_gap_b.plot()
plt.title('How the poverty gap rises with lost income', loc='left')
plt.xticks(np.arange(0, 0.21, 0.02))
ax.xaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
ax.yaxis.set_major_formatter(mdf.dollar_format('B'))
plt.xlabel('Fall in SPM resources')
plt.ylabel('SPM poverty gap')
plt.savefig('charts/pov_gap_income.png')
plt.show()
Compared to baseline.
cuts - cuts.iloc[0]
pov_rate | pov_gap_b | pov_m | |
---|---|---|---|
0.00 | 0.000000 | 0.000000 | 0.000000 |
0.01 | 0.003032 | 2.058236 | 0.991927 |
0.02 | 0.005668 | 4.194937 | 1.854696 |
0.03 | 0.008708 | 6.409913 | 2.849103 |
0.04 | 0.011534 | 8.705166 | 3.773821 |
0.05 | 0.014754 | 11.090481 | 4.827511 |
0.06 | 0.017985 | 13.563987 | 5.884806 |
0.07 | 0.021385 | 16.142045 | 6.997185 |
0.08 | 0.025349 | 18.834945 | 8.294208 |
0.09 | 0.028515 | 21.635127 | 9.330175 |
0.10 | 0.032090 | 24.540528 | 10.499878 |
0.11 | 0.035998 | 27.554401 | 11.778446 |
0.12 | 0.039864 | 30.687974 | 13.043582 |
0.13 | 0.043840 | 33.947238 | 14.344418 |
0.14 | 0.048140 | 37.342869 | 15.751564 |
0.15 | 0.051783 | 40.866878 | 16.943450 |
0.16 | 0.056616 | 44.523525 | 18.524751 |
0.17 | 0.060797 | 48.316667 | 19.892783 |
0.18 | 0.064906 | 52.253215 | 21.237323 |
0.19 | 0.069219 | 56.326881 | 22.648422 |
0.20 | 0.074115 | 60.562275 | 24.250481 |
cuts / cuts.iloc[0]
pov_rate | pov_gap_b | pov_m | |
---|---|---|---|
0.00 | 1.000000 | 1.000000 | 1.000000 |
0.01 | 1.023808 | 1.012108 | 1.023808 |
0.02 | 1.044515 | 1.024678 | 1.044515 |
0.03 | 1.068382 | 1.037708 | 1.068382 |
0.04 | 1.090577 | 1.051210 | 1.090577 |
0.05 | 1.115867 | 1.065242 | 1.115867 |
0.06 | 1.141243 | 1.079793 | 1.141243 |
0.07 | 1.167942 | 1.094959 | 1.167942 |
0.08 | 1.199072 | 1.110800 | 1.199072 |
0.09 | 1.223936 | 1.127273 | 1.223936 |
0.10 | 1.252011 | 1.144365 | 1.252011 |
0.11 | 1.282698 | 1.162094 | 1.282698 |
0.12 | 1.313063 | 1.180528 | 1.313063 |
0.13 | 1.344285 | 1.199701 | 1.344285 |
0.14 | 1.378058 | 1.219677 | 1.378058 |
0.15 | 1.406665 | 1.240408 | 1.406665 |
0.16 | 1.444618 | 1.261919 | 1.444618 |
0.17 | 1.477453 | 1.284232 | 1.477453 |
0.18 | 1.509723 | 1.307390 | 1.509723 |
0.19 | 1.543592 | 1.331354 | 1.543592 |
0.20 | 1.582043 | 1.356270 | 1.582043 |
ubi_10pct_example = pd.DataFrame(index=np.arange(0, 2000, 10))
ubi_10pct_example['pov_all'] = ubi_10pct_example.index.map(
lambda x: ubi_pov_rate(resource_cut=0.1, ubi_adult=x, ubi_kid_share=1,
include_noncitizens=True))
ubi_10pct_example['pov_adult_citizens'] = ubi_10pct_example.index.map(
lambda x: ubi_pov_rate(resource_cut=0.1, ubi_adult=x, ubi_kid_share=0,
include_noncitizens=False))
ax = ubi_10pct_example.pov_all.plot()
plt.title('How UBI affects the poverty rate when income falls 10%',
loc='left')
ax.xaxis.set_major_formatter(mdf.dollar_format())
ax.yaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
# Show current poverty rate.
plt.axhline(ubi_pov_rate(), color='gray', lw=1, ls='--', zorder=1)
# TODO: Figure out how to automate this.
plt.axvline(828, ymax=0.53, lw=1, ls='--', zorder=1)
# To make consistent with next graph.
plt.ylim(0.088, 0.162)
plt.annotate('Current poverty rate', (150, 0.124), color='gray')
plt.xlabel('Annual UBI amount')
plt.ylabel('SPM poverty rate')
plt.savefig('charts/pov_rate_by_ubi_10pct_all.png')
plt.show()
# Shortfall of using an $828 UBI for adult citizens only.
ubi_pov_rate(0.1, 828, 0, False)
0.14071983365949509
ax = ubi_10pct_example.pov_adult_citizens.plot()
plt.title('How an adult citizen UBI affects the poverty rate when income' +
' falls 10%', loc='left')
ax.xaxis.set_major_formatter(mdf.dollar_format())
ax.yaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
# Show current poverty rate.
plt.axhline(ubi_pov_rate(), color='gray', lw=1, ls='--', zorder=1)
# TODO: Figure out how to automate this.
plt.axvline(1515, ymax=0.53, lw=1, ls='--', zorder=1)
# To make consistent with previous graph.
plt.ylim(0.088, 0.162)
plt.annotate('Current poverty rate', (150, 0.124), color='gray')
plt.xlabel('Annual UBI amount')
plt.ylabel('SPM poverty rate')
plt.savefig('charts/pov_rate_by_ubi_10pct_adult_citizens.png')
plt.show()
ax = ubi_10pct_example.plot()
plt.title('How UBIs affect the poverty rate when income falls 10%',
loc='left')
ax.xaxis.set_major_formatter(mdf.dollar_format())
ax.yaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
# Show current poverty rate.
plt.axhline(ubi_pov_rate(), color='gray', lw=1, ls='--', zorder=1)
# TODO: Figure out how to automate this.
plt.axvline(828, ymax=0.53, lw=1, ls='--', zorder=1)
plt.axvline(1515, ymax=0.53, lw=1, ls='--', zorder=1, color='#ff7f0e')
plt.legend(['UBI for all adults and children', 'UBI only for adult citizens'])
plt.ylim(0.088, 0.162)
plt.annotate('Current poverty rate', (100, 0.124), color='gray')
plt.xlabel('Annual UBI amount')
plt.ylabel('SPM poverty rate')
plt.savefig('charts/pov_rate_by_ubi_10pct_by_type.png')
plt.show()
def mid(left, right):
return (left + right) / 2
def binary_search_opt(f, left, right, tol, max_runs=100, verbose=False):
""" Finds the value of a monotonically increasing function that comes
closest to zero, using binary search.
Args:
f: Monotonically increasing function with respect to its single
argument.
left: Minimum value at which f is evaluated.
right: Maximum value at which f is evaluated.
tol: Tolerance. Function stops searching once a value within tol
of zero is identified.
max_runs: Maximum number of loops to evaluate. Defaults to 100.
verbose: Whether to print the results of each loop. Defaults to False.
Returns:
First value in the binary search algorithm for which f evaluates
within tol of zero.
"""
# Start at the midpoint.
val = mid(left, right)
delta = f(val)
i = 1
while np.abs(delta) > tol and i < max_runs:
if verbose:
print('left: ' + str(round(left, 3)) +
', right: ' + str(round(right, 3)) +
', val: ' + str(round(val, 3)) +
', delta: ' + str(round(delta, 3)))
if delta < 0:
left = val
else:
right = val
val = mid(left, right)
delta = f(val)
i = i + 1
return val
Create backbone of a data frame to fill in.
ubis = mdf.cartesian_product({'cut': CUTS,
'kid_share': [0, 0.5, 1],
'include_noncitizens': [False, True]
})
Find the UBI that keeps the poverty rate from rising for each scenario.
TODO: Make this a single call with the poverty gap, and put that split in the backbone.
ubis['pov_rate_ubi'] = ubis.apply(
lambda row: binary_search_opt(
lambda x: ubi_pov_rate() - ubi_pov_rate(row.cut, x, row.kid_share,
row.include_noncitizens),
0, 4000, 0.0001), axis=1
).astype(int)
Find the UBI that keeps the poverty gap from rising for each scenario.
ubis['pov_gap_ubi'] = ubis.apply(
lambda row: binary_search_opt(
lambda x: ubi_pov_gap_b() - ubi_pov_gap_b(
row.cut, x, row.kid_share, row.include_noncitizens),
0, 4000, 0.0001), axis=1
).astype(int)
Calculate cost.
ubis['pov_rate_ubi_cost_b'] = ubis.apply(
lambda row: ubi_cost_b(row.pov_rate_ubi, row.kid_share,
row.include_noncitizens), axis=1).astype(int)
ubis['pov_gap_ubi_cost_b'] = ubis.apply(
lambda row: ubi_cost_b(row.pov_gap_ubi, row.kid_share,
row.include_noncitizens), axis=1).astype(int)
Add a single label based on kid_share
and include_noncitizens
for graphing.
ubis['label'] = np.where(ubis.include_noncitizens,
np.where(ubis.kid_share == 0, 'Adults',
np.where(ubis.kid_share == 0.5,
'Adults + half share for kids',
'Everyone')),
np.where(ubis.kid_share == 0, 'Adult citizens',
np.where(ubis.kid_share == 0.5,
'Adult citizens + half share for' +
' kids',
'All citizens')))
As an illustrative example.
ubis[ubis.cut == 0.1].set_index('label').drop(
['cut', 'kid_share', 'include_noncitizens'], axis=1)
pov_rate_ubi | pov_gap_ubi | pov_rate_ubi_cost_b | pov_gap_ubi_cost_b | |
---|---|---|---|---|
label | ||||
Adult citizens | 1515 | 836 | 347 | 191 |
Adults | 1250 | 691 | 313 | 173 |
Adult citizens + half share for kids | 1203 | 703 | 318 | 186 |
Adults + half share for kids | 1000 | 594 | 287 | 170 |
All citizens | 968 | 610 | 291 | 183 |
Everyone | 828 | 523 | 268 | 169 |
ax = ubis[ubis.cut == 0.1].sort_values('pov_rate_ubi').plot.barh(
'label', 'pov_rate_ubi')
plt.title('UBI needed to stabilize poverty rate given 10% income loss',
loc='left')
ax.xaxis.set_major_formatter(mdf.dollar_format())
plt.ylabel('')
plt.xlabel('Annual UBI amount')
ax.get_legend().remove()
# Use tight bbox to avoid left labels getting cut off.
plt.savefig('charts/ubi_pov_rate_10pct.png', bbox_inches='tight')
plt.show()
ax = ubis[ubis.cut == 0.1].sort_values('pov_gap_ubi').plot.barh(
'label', 'pov_gap_ubi')
plt.title('UBI needed to stabilize poverty gap given 10% income loss',
loc='left')
ax.xaxis.set_major_formatter(mdf.dollar_format())
plt.ylabel('')
plt.xlabel('Annual UBI amount')
ax.get_legend().remove()
plt.savefig('charts/ubi_pov_gap_10pct.png', bbox_inches='tight')
plt.show()
ax = ubis[ubis.cut == 0.1].sort_values('pov_rate_ubi_cost_b').plot.barh(
'label', 'pov_rate_ubi_cost_b')
plt.title('Cost of UBI needed to stabilize poverty rate given 10% income loss',
loc='left')
ax.xaxis.set_major_formatter(mdf.dollar_format('B'))
plt.ylabel('')
plt.xlabel('Total cost of UBI')
ax.get_legend().remove()
plt.savefig('charts/ubi_cost_pov_rate_10pct.png', bbox_inches='tight')
plt.show()
ax = ubis[ubis.cut == 0.1].sort_values('pov_gap_ubi_cost_b').plot.barh(
'label', 'pov_gap_ubi_cost_b')
plt.title('Cost of UBI needed to stabilize poverty gap given 10% income loss',
loc='left')
ax.xaxis.set_major_formatter(mdf.dollar_format('B'))
plt.ylabel('')
plt.xlabel('Total cost of UBI')
ax.get_legend().remove()
plt.savefig('charts/ubi_cost_pov_gap_10pct.png', bbox_inches='tight')
plt.show()
First pivot the data.
ubis_rate_pivot = ubis.pivot_table(['pov_rate_ubi', 'pov_rate_ubi_cost_b',
'pov_gap_ubi', 'pov_gap_ubi_cost_b'],
'cut', 'label')
Plot UBI amount and total cost both for keeping the poverty rate and the poverty gap steady.
TODO: Make a logical color scheme, e.g. blues for everyone and grays for citizens only, shaded by kid treatment.
ax = ubis_rate_pivot.pov_rate_ubi.plot.line()
plt.title('UBI needed to stabilize poverty rate by targeting method',
loc='left')
ax.xaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
ax.yaxis.set_major_formatter(mdf.dollar_format())
plt.xticks(np.arange(0, 0.21, 0.02))
plt.legend(title='')
plt.xlabel('Fall in disposable income')
plt.ylabel('Annual UBI amount')
plt.savefig('charts/ubi_pov_rate.png')
plt.show()
TODO: Make a version of this relative to the "Everyone" line.
ax = ubis_rate_pivot.pov_rate_ubi_cost_b.plot.line()
plt.title('Cost of UBI needed to stabilize poverty rate by targeting method',
loc='left')
ax.xaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
ax.yaxis.set_major_formatter(mdf.dollar_format('B'))
plt.xticks(np.arange(0, 0.21, 0.02))
plt.legend(title='')
plt.xlabel('Fall in disposable income')
plt.ylabel('Total cost of UBI')
plt.savefig('charts/ubi_cost_pov_rate.png')
plt.show()
ax = ubis_rate_pivot.pov_gap_ubi.plot.line()
plt.title('UBI needed to stabilize poverty gap by targeting method',
loc='left')
ax.xaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
ax.yaxis.set_major_formatter(mdf.dollar_format())
plt.xticks(np.arange(0, 0.21, 0.02))
plt.legend(title='')
plt.xlabel('Fall in disposable income')
plt.ylabel('Annual UBI amount')
plt.savefig('charts/ubi_pov_gap.png')
plt.show()
ax = ubis_rate_pivot.pov_gap_ubi_cost_b.plot.line()
plt.title('Cost of UBI needed to stabilize poverty gap by targeting method',
loc='left')
ax.xaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1, decimals=0))
ax.yaxis.set_major_formatter(mdf.dollar_format('B'))
plt.xticks(np.arange(0, 0.21, 0.02))
plt.legend(title='')
plt.xlabel('Fall in disposable income')
plt.ylabel('Total cost of UBI')
plt.savefig('charts/ubi_cost_pov_gap.png')
plt.show()