File size: 20,653 Bytes
64ef276 28d3ad3 64ef276 4c726ef 64ef276 d64b73e 64ef276 28d3ad3 64ef276 4c726ef 64ef276 28d3ad3 64ef276 28d3ad3 64ef276 4c726ef 64ef276 28d3ad3 64ef276 28d3ad3 64ef276 28d3ad3 64ef276 c3b38f1 28d3ad3 64ef276 28d3ad3 64ef276 c3b38f1 64ef276 4c726ef 64ef276 c3b38f1 64ef276 c3b38f1 64ef276 c3b38f1 64ef276 c3b38f1 64ef276 c3b38f1 64ef276 28d3ad3 c3b38f1 28d3ad3 c3b38f1 28d3ad3 4c726ef 28d3ad3 4c726ef 28d3ad3 4c726ef 28d3ad3 4c726ef 28d3ad3 c3b38f1 28d3ad3 64ef276 24222b3 42d39c2 64ef276 c3b38f1 64ef276 28d3ad3 6fc3c5d 28d3ad3 64ef276 28d3ad3 64ef276 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
import panel as pn
import pandas as pd
import matplotlib
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import numpy as np
matplotlib.use("agg")
pn.extension('ipywidgets')
### Part 0: Getting data
@pn.cache # Add caching to only download data once
def get_data():
#df = pd.read_excel('fbc_data_2024.xlsx', sheet_name='County', header=1)
df = pd.read_excel('https://files.epi.org/uploads/fbc_data_2024.xlsx', sheet_name='County', header=1)
#Drop unneeded columns
col_to_drop = list(df.columns[:1]) + list(df.columns[13:21])
df = df.drop(columns=col_to_drop)
#One apparently random county is missing median income data, but is still ranked.
#By taking the average of the counties ranked immediately above and below it, a quick estimate of $55,122 can be made.
df.fillna(55122.0,inplace=True)
df['median_family_income'] = df['median_family_income'].astype('int64')
#Add leading zeroes to FIPS codes makes them readable to Tableau.
df['county_fips'] = df['county_fips'].astype(str).str.zfill(5)
df.columns = ['state_abbr','FIPS','county','family','housing','food','transportation','healthcare',
'other_necessities','childcare','taxes','total','median_family_income',
'num_counties_in_st','st_cost_rank','st_med_aff_rank','st_income_rank']
#Some counties share names across states; adding their state code makes them unique.
df['county_state'] = df.county + ', ' + df.state_abbr
df['median_monthly_family_income'] = df.median_family_income.div(12).round(0).astype('int64')
df['remaining_money'] = df.median_monthly_family_income - df.total
return df
df = get_data()
### Part 1: Find and display model budget
#Function to represent data in US dollars
def dol(value,df,row=0):
return '${:0,}'.format(df[value][row])
#Function that takes user input and displays the corresponding row from the df
def calculate_model(user_county,user_parents,user_children):
user_family = f'{user_parents}p{user_children}c'
user_df = df.loc[(df.county_state == user_county) & (df.family == user_family)].reset_index(drop=True)
return (
f'Moderately frugal families of {user_parents} {"adult" if user_parents == "1" else "adults"} and {user_children} {"child" if user_children == 1 else "children"} living in {user_county} '
'tend to have a monthly family budget similar to the following:\n'
f'\nHousing: {dol("housing",user_df)}'
f'\nFood: {dol("food",user_df)}'
f'\nTransportation: {dol("transportation",user_df)}'
f'\nHealthcare: {dol("healthcare",user_df)}'
f'\nChildcare: {dol("childcare",user_df)}'
f'\nOther necessities: {dol("other_necessities",user_df)}'
f'\nTaxes: {dol("taxes",user_df)}'
f'\nTotal: {dol("total",user_df)}'
f'\n\nThe median monthly family income for {user_county} is {dol("median_monthly_family_income",user_df)}, which would '
f'leave {dol("remaining_money",user_df)} each month for emergencies, saving, and discretionary spending.'
)
#Widgets for receiving user input
user_county = pn.widgets.AutocompleteInput(
name='County of primary residence:', options=df.county_state.unique().tolist(),
case_sensitive=False,
placeholder='Input county name')
adult_title = pn.pane.Markdown('Number of adults in household:') #Radio buttons don't have their name displayed as a title, so I add it here
user_parents = pn.widgets.RadioButtonGroup(
options=['1', '2'], button_type='default', margin=(12,0,0,0))
user_children = pn.widgets.IntSlider(
name='Number of children', start=0, end=4, step=1, value=0)
#Bind widgets to function
county_model = pn.bind(calculate_model, user_county=user_county,user_parents=user_parents,user_children=user_children)
#Submit button
county_submit = pn.widgets.Button(name="Submit Family/Location", button_type="primary")
#Function to make model function respond to submit button
def county_result(clicked):
if clicked:
if user_county.value == '':
return 'County information is required.'
return county_model()
return "Click Submit Family/Location to see a typical budget."
#Binding button to button function
county_result = pn.pane.Markdown(pn.bind(county_result,county_submit))
### Part 2: Calculate and display user budget's comparison to model
#Function to calculate the user budget's comparison to the model. Accounts for dividing by zero.
def calculate_percentage(user_value,column,df):
if df[column][0] == 0:
return 1.0
else:
return 1+round(((user_value-df[column][0])/df[column][0]),3)
#Function that takes user budget input and returns how it compares to model.
def calculate_budget_percentage(user_county,user_parents,user_children,user_income,user_housing,user_food,user_transportation,user_healthcare,user_childcare,user_other,user_taxes):
#Repeated code. Could potentially be avoided with more binding, but that wouldn't change performance
user_family = f'{user_parents}p{user_children}c'
user_df = df.loc[(df.county_state == user_county) & (df.family == user_family)].reset_index(drop=True)
#New code
income_p = calculate_percentage(user_income,'median_monthly_family_income',user_df)
housing_p = calculate_percentage(user_housing,'housing',user_df)
food_p = calculate_percentage(user_food,'food',user_df)
transportation_p = calculate_percentage(user_transportation,'transportation',user_df)
healthcare_p = calculate_percentage(user_healthcare,'healthcare',user_df)
childcare_p = calculate_percentage(user_childcare,'childcare',user_df)
other_p = calculate_percentage(user_other,'other_necessities',user_df)
taxes_p = calculate_percentage(user_taxes,'taxes',user_df)
total_p = 1+round((((user_housing+user_food+user_transportation+user_healthcare+
user_childcare+user_other+user_taxes)-user_df.total[0])/user_df.total[0]),3)
return (
f"Your family's income is {income_p:.1%} that of the median family income in your area. "
'Your budget compares to the typical model as follows:\n'
f'\nHousing: {housing_p:.1%}'
f'\nFood: {food_p:.1%}'
f'\nTransportation: {transportation_p:.1%}'
f'\nHealthcare: {healthcare_p:.1%}'
f'\nChildcare: {"N/A" if user_children==0 else str(round(childcare_p*100,1))+"%"}'
f'\nOther necessities: {other_p:.1%}'
f'\nTaxes: {taxes_p:.1%}'
f'\nTotal: {total_p:.1%}'
)
#Function for budget-input widgets
def input_budget(prompt):
return pn.widgets.IntInput(name=prompt, value=0, start=0)
#Budget input widgets
user_income = input_budget('What is your monthly family income? (Pre-tax.)')
budget_title = pn.pane.Markdown("#### For each item, input your family's monthly spending.")
#How much do you spend on housing per month? Inclu
user_housing = input_budget('Housing (Include utilities.)')
user_food = input_budget('Food')
user_transportation = input_budget('Transportation (Gas, car payment/repair, bus pass, etc.)')
user_healthcare = input_budget('Healthcare (Both out-of-pocket and premium)')
user_childcare = input_budget('Childcare')
user_other = input_budget('Other necessities (Clothes, house/school supplies, etc.)')
user_taxes = input_budget('Taxes')
#Binding widgets to function
budget_percentage = pn.bind(calculate_budget_percentage,user_county,user_parents,user_children,user_income,user_housing,user_food,user_transportation,user_healthcare,user_childcare,user_other,user_taxes)
#Submit button, function to respond to button, binding to button
budget_submit = pn.widgets.Button(name="Submit Budget", button_type="primary")
def budget_result(clicked):
if clicked:
if user_county.value == '':
return 'County information is required.'
return budget_percentage()
return "Click Submit Budget to see how your budget compares."
budget_result = pn.pane.Markdown(pn.bind(budget_result,budget_submit))
### Part 3: Calculate and display most afforable counties and budgets
#Function to take user constraints, find most affordable counties, and calculate budgets
def calculate_comparison(user_county,user_parents,user_children,user_income,user_housing,user_food,user_transportation,user_healthcare,user_childcare,user_other,user_taxes,
bring_income,income_cap,income_cap_amount,include_all_states,states_allowed,result_count):
#Repeated code:
user_family = f'{user_parents}p{user_children}c'
user_df = df.loc[(df.county_state == user_county) & (df.family == user_family)].reset_index(drop=True)
income_p = calculate_percentage(user_income,'median_monthly_family_income',user_df)
housing_p = calculate_percentage(user_housing,'housing',user_df)
food_p = calculate_percentage(user_food,'food',user_df)
transportation_p = calculate_percentage(user_transportation,'transportation',user_df)
healthcare_p = calculate_percentage(user_healthcare,'healthcare',user_df)
childcare_p = calculate_percentage(user_childcare,'childcare',user_df)
other_p = calculate_percentage(user_other,'other_necessities',user_df)
taxes_p = calculate_percentage(user_taxes,'taxes',user_df)
#New code:
new_df = df.loc[(df.family == user_family)].reset_index(drop=True)
if bring_income:
new_df.median_monthly_family_income = user_income
else:
new_df.median_monthly_family_income = new_df.median_monthly_family_income.mul(income_p).round(0).astype('int64')
new_df['median_monthly_family_income_uncapped'] = new_df.median_monthly_family_income
if income_cap:
new_df.median_monthly_family_income.clip(upper=income_cap_amount,inplace=True)
if not include_all_states:
new_df = new_df.loc[(new_df.state_abbr.isin(states_allowed))].reset_index(drop=True)
new_df.housing = new_df.housing.mul(housing_p).round(0).astype('int64')
new_df.food = new_df.food.mul(food_p).round(0).astype('int64')
new_df.transportation = new_df.transportation.mul(transportation_p).round(0).astype('int64')
new_df.healthcare = new_df.healthcare.mul(healthcare_p).round(0).astype('int64')
new_df.other_necessities = new_df.other_necessities.mul(other_p).round(0).astype('int64')
new_df.childcare = new_df.childcare.mul(childcare_p).round(0).astype('int64')
new_df.taxes = new_df.taxes.mul(taxes_p).round(0).astype('int64')
new_df.total = new_df.housing + new_df.food + new_df.transportation + new_df.healthcare + new_df.other_necessities + new_df.childcare + new_df.taxes
new_df.remaining_money = new_df.median_monthly_family_income - new_df.total
new_df = new_df.sort_values('remaining_money',ascending=False).reset_index(drop=True)
county_results = []
limit = result_count
if limit > len(new_df):
limit = len(new_df)
county_results.append(f'There are only {limit} counties to show!\n\n')
if limit == 1:
second_line = 'here is the most affordable county for your family, along with what your budget might look living there:\n'
else:
second_line = f'here are the {limit} most affordable counties for your family, along with what your budget might look living there:\n'
if not bring_income and not income_cap:
county_results.append("With both your spending and income recalculated based on each county's norm, "
+ second_line)
elif bring_income:
county_results.append("With your exact income maintained but your spending recalculated based on each county's norm, "
+ second_line)
else:
county_results.append(f"With both your spending and income recalculated based on each county's norm (and an income cap of ${income_cap_amount:,.0f}), "
+ second_line)
for i in range(limit):
current_result = [f'\n#{i+1}\n{new_df.county_state[i]}']
for [j,k] in [['housing','Housing'],['food','Food'],['transportation','Transportation'],['healthcare','Healthcare'],
['childcare','Childcare'],['other_necessities','Other necessities'],['taxes','Taxes'],['total','Total'],
['median_monthly_family_income','Income'],['remaining_money','Remaining money']]:
if j == 'median_monthly_family_income' and income_cap == True and new_df.median_monthly_family_income_uncapped[i] > new_df.median_monthly_family_income[i]:
current_result.append(f" {k}: {dol(j,new_df,row=i)} (Uncapped: {dol('median_monthly_family_income_uncapped',new_df,row=i)})")
else:
current_result.append(f" {k}: {dol(j,new_df,row=i)}")
county_results.append('\n'.join(current_result))
return '\n'.join(county_results)
#Constraint widgets
bring_income_title = pn.pane.Markdown('Maintain income exactly, regardless of county:')
bring_income = pn.widgets.Switch(margin=(19,0,0,0))
income_cap_title = pn.pane.Markdown('Enforce income cap:')
income_cap = pn.widgets.Switch(margin=(19,0,0,0),disabled=bring_income)
income_cap_amount = pn.widgets.IntInput(name='Income cap value:', value=0, start=0,disabled= bring_income)
include_all_states_title = pn.pane.Markdown('Include all states:')
include_all_states = pn.widgets.Switch(margin=(19,0,0,0),value=True)
states_allowed = pn.widgets.MultiChoice(name='States to include:', options=df.state_abbr.unique().tolist(),disabled= include_all_states) #Would be nice to disable when include_all_states is not active
result_count = pn.widgets.IntInput(name='Results to view:', value=5, start=1)
#Bind to widgets
comparison = pn.bind(calculate_comparison, user_county,user_parents,user_children,
user_income,user_housing,user_food,user_transportation,user_healthcare,user_childcare,user_other,user_taxes,
bring_income,income_cap,income_cap_amount,include_all_states,states_allowed,result_count)
#Submit button, bind
comparison_submit = pn.widgets.Button(name="Submit Constraints", button_type="primary")
def comparison_result(clicked):
if clicked:
if user_county.value == '':
return 'County information is required.'
return comparison()
return "Click Submit Constraints to see the final results."
comparison_result = pn.pane.Markdown(pn.bind(comparison_result,comparison_submit))
### Part 4: Bar chart of results
def calculate_bar(user_county,user_parents,user_children,user_income,user_housing,user_food,user_transportation,user_healthcare,user_childcare,user_other,user_taxes,
bring_income,income_cap,income_cap_amount,include_all_states,states_allowed,result_count,bar_type):
#Repeated code:
user_family = f'{user_parents}p{user_children}c'
user_df = df.loc[(df.county_state == user_county) & (df.family == user_family)].reset_index(drop=True)
income_p = calculate_percentage(user_income,'median_monthly_family_income',user_df)
housing_p = calculate_percentage(user_housing,'housing',user_df)
food_p = calculate_percentage(user_food,'food',user_df)
transportation_p = calculate_percentage(user_transportation,'transportation',user_df)
healthcare_p = calculate_percentage(user_healthcare,'healthcare',user_df)
childcare_p = calculate_percentage(user_childcare,'childcare',user_df)
other_p = calculate_percentage(user_other,'other_necessities',user_df)
taxes_p = calculate_percentage(user_taxes,'taxes',user_df)
new_df = df.loc[(df.family == user_family)].reset_index(drop=True)
if bring_income:
new_df.median_monthly_family_income = user_income
else:
new_df.median_monthly_family_income = new_df.median_monthly_family_income.mul(income_p).round(0).astype('int64')
new_df['median_monthly_family_income_uncapped'] = new_df.median_monthly_family_income
if income_cap:
new_df.median_monthly_family_income.clip(upper=income_cap_amount,inplace=True)
if not include_all_states:
new_df = new_df.loc[(new_df.state_abbr.isin(states_allowed))].reset_index(drop=True)
new_df.housing = new_df.housing.mul(housing_p).round(0).astype('int64')
new_df.food = new_df.food.mul(food_p).round(0).astype('int64')
new_df.transportation = new_df.transportation.mul(transportation_p).round(0).astype('int64')
new_df.healthcare = new_df.healthcare.mul(healthcare_p).round(0).astype('int64')
new_df.other_necessities = new_df.other_necessities.mul(other_p).round(0).astype('int64')
new_df.childcare = new_df.childcare.mul(childcare_p).round(0).astype('int64')
new_df.taxes = new_df.taxes.mul(taxes_p).round(0).astype('int64')
new_df.total = new_df.housing + new_df.food + new_df.transportation + new_df.healthcare + new_df.other_necessities + new_df.childcare + new_df.taxes
new_df.remaining_money = new_df.median_monthly_family_income - new_df.total
new_df = new_df.sort_values('remaining_money',ascending=False).reset_index(drop=True)
#New code:
new_df = new_df[:result_count]
fig = Figure(figsize=(result_count,3))
ax = fig.add_subplot(111)
if bar_type == 'Stacked':
stacked_value=True
else:
stacked_value=False
ax = new_df[['remaining_money','housing','food','transportation','healthcare','childcare','other_necessities','taxes']].plot.bar(
stacked=stacked_value, width=0.8, ax=ax, color=['limegreen','wheat','gold','paleturquoise','salmon','violet','silver','dimgrey'])
ax.legend(labels=['Remaining Money','Housing','Food','Transportation','Healthcare','Childcare','Other','Taxes'],reverse=True,loc="upper right",bbox_to_anchor=(1 + 2.41/result_count, 1))
ax.set_xticks(ticks=np.arange(len(new_df)),labels=new_df.county_state.values)
ax.yaxis.set_major_formatter('${x:,.0f}')
ax.yaxis.grid(linestyle='--',alpha=.4)
ax.set_xlim([-.5, len(new_df)-.5])
plt.close(fig)
return fig
def nothing_fig():
fig = Figure(figsize=(5,4))
ax = fig.add_subplot(111)
return fig
bar_type_title = pn.pane.Markdown('Type of bar chart:')
bar_type = pn.widgets.RadioButtonGroup(
options=['Stacked', 'Clustered'], button_type='default', margin=(12,0,0,0))
bar = pn.bind(calculate_bar, user_county,user_parents,user_children,
user_income,user_housing,user_food,user_transportation,user_healthcare,user_childcare,user_other,user_taxes,
bring_income,income_cap,income_cap_amount,include_all_states,states_allowed,result_count,bar_type)
def bar_result(clicked):
if clicked:
if user_county.value == '':
return nothing_fig()
return bar()
return nothing_fig()
bar_result = pn.pane.Matplotlib(pn.bind(bar_result,comparison_submit), dpi=144, tight=True)
### Part 5: Templating
template = pn.template.BootstrapTemplate(title='Affordable County Locator')
#Sidebar formatting
intro = pn.pane.Markdown('If you and your family could maintain your relative income and spending habits, '
'in which US county would you be able to save the most money every month? Use this app to find out! '
'Check out the README in the "Files" tab to the top-right for detailed instructions, take-aways, and sources.')
part1_title = pn.pane.Markdown('## Part 1: County and family size')
part2_title = pn.pane.Markdown('## Part 2: Budget')
part3_title = pn.pane.Markdown('## Part 3: Calculation constraints')
template.sidebar.extend([intro, part1_title, user_county, pn.Row(adult_title, user_parents), user_children, county_submit,
part2_title, user_income, budget_title, user_housing, user_food, user_transportation, user_healthcare, user_childcare, user_other, user_taxes, budget_submit,
part3_title, pn.Row(bring_income_title, bring_income),pn.Row(income_cap_title, income_cap),income_cap_amount,pn.Row(include_all_states_title, include_all_states),states_allowed,result_count,pn.Row(bar_type_title,bar_type),comparison_submit])
#Main formatting
main1 = pn.Card(
county_result,
title='Typical Budget',
styles={'background': 'WhiteSmoke'}
)
main2 = pn.Card(
budget_result,
title='Budget Comparison',
styles={'background': 'WhiteSmoke'}
)
main3 = pn.Card(
comparison_result,
title='Most Affordable Counties',
styles={'background': 'WhiteSmoke'},
max_height = 300
)
main4 = pn.Card(
bar_result,
title='Most Affordable Counties, bar chart',
styles={'background':'WhiteSmoke'}
)
template.main.append(
pn.Column(main1,main2,main3,main4)
)
template.servable(); |