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();