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