ScottScroggins commited on
Commit
64ef276
·
verified ·
1 Parent(s): 4f3c9d9

Rename app.py to affordable_county_locator.py

Browse files
Files changed (2) hide show
  1. affordable_county_locator.py +295 -0
  2. app.py +0 -108
affordable_county_locator.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import panel as pn
2
+ import pandas as pd
3
+ pn.extension()
4
+
5
+ ### Part 0: Getting data
6
+
7
+ @pn.cache # Add caching to only download data once
8
+ def get_data():
9
+ #df = pd.read_excel('fbc_data_2024.xlsx', sheet_name='County', header=1)
10
+ df = pd.read_excel('https://files.epi.org/uploads/fbc_data_2024.xlsx', sheet_name='County', header=1)
11
+
12
+ #Drop unneeded columns
13
+ col_to_drop = list(df.columns[:1]) + list(df.columns[13:21])
14
+ df = df.drop(columns=col_to_drop)
15
+
16
+ #One apparently random county is missing median income data, but is still ranked.
17
+ #By taking the average of the counties ranked immediately above and below it, a quick estimate of $55,122 can be made.
18
+ df.fillna(55122.0,inplace=True)
19
+
20
+ df['median_family_income'] = df['median_family_income'].astype('int64')
21
+
22
+ #Add leading zeroes to FIPS codes makes them readable to Tableau.
23
+ df['county_fips'] = df['county_fips'].astype(str).str.zfill(5)
24
+
25
+ df.columns = ['state_abbr','FIPS','county','family','housing','food','transportation','healthcare',
26
+ 'other_necessities','childcare','taxes','total','median_family_income','num_counties_in_st','st_cost_rank','st_med_aff_rank','st_income_rank']
27
+
28
+ #Some counties share names across states; adding their state code makes them unique.
29
+ df['county_state'] = df.county + ', ' + df.state_abbr
30
+
31
+ df['median_monthly_family_income'] = df.median_family_income.div(12).round(0).astype('int64')
32
+
33
+ df['remaining_money'] = df.median_monthly_family_income - df.total
34
+ return df
35
+
36
+ df = get_data()
37
+
38
+ ### Part 1: Find and display model budget
39
+
40
+ #Function to represent data in US dollars
41
+ def dol(value,df,row=0):
42
+ return '${:0,}'.format(df[value][row])
43
+
44
+ #Function that takes user input and displays the corresponding row from the df
45
+ def calculate_model(user_county,user_parents,user_children):
46
+ user_family = f'{user_parents}p{user_children}c'
47
+ user_df = df.loc[(df.county_state == user_county) & (df.family == user_family)].reset_index(drop=True)
48
+ return (
49
+ 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} '
50
+ 'tend to have a monthly family budget similar to the following:\n'
51
+ f'\nHousing: {dol("housing",user_df)}'
52
+ f'\nFood: {dol("food",user_df)}'
53
+ f'\nTransportation: {dol("transportation",user_df)}'
54
+ f'\nHealthcare: {dol("healthcare",user_df)}'
55
+ f'\nChildcare: {dol("childcare",user_df)}'
56
+ f'\nOther necessities: {dol("other_necessities",user_df)}'
57
+ f'\nTaxes: {dol("taxes",user_df)}'
58
+ f'\nTotal: {dol("total",user_df)}'
59
+ f'\n\nThe median monthly family income for {user_county} is {dol("median_monthly_family_income",user_df)}, which would '
60
+ f'leave {dol("remaining_money",user_df)} each month for emergencies, saving, and discretionary spending.'
61
+ )
62
+
63
+ #Widgets for receiving user input
64
+ user_county = pn.widgets.AutocompleteInput(
65
+ name='County of primary residence:', options=df.county_state.unique().tolist(),
66
+ case_sensitive=False,
67
+ placeholder='Input county name')
68
+
69
+ adult_title = pn.pane.Markdown('Number of adults in household:')
70
+ user_parents = pn.widgets.RadioButtonGroup(
71
+ name='Number of adults', options=['1', '2'], button_type='default', margin=(12,0,0,0))
72
+
73
+ user_children = pn.widgets.IntSlider(
74
+ name='Number of children', start=0, end=4, step=1, value=0)
75
+
76
+ #Bind widgets to function
77
+ county_model = pn.bind(calculate_model, user_county=user_county,user_parents=user_parents,user_children=user_children)
78
+
79
+ #Submit button
80
+ county_submit = pn.widgets.Button(name="Submit Family/Location", button_type="primary")
81
+
82
+ #Function to make model function respond to submit button
83
+ def county_result(clicked):
84
+ if clicked:
85
+ if user_county.value == '':
86
+ return 'County information is required.'
87
+ return county_model()
88
+ return "Click Submit Family/Location to see a typical budget."
89
+
90
+ #Binding button to button function
91
+ county_result = pn.pane.Markdown(pn.bind(county_result,county_submit))
92
+
93
+
94
+ ### Part 2: Calculate and display user budget's comparison to model
95
+
96
+ #Function to calculate the user budget's comparison to the model. Accounts for dividing by zero.
97
+ def calculate_percentage(user_value,column,df):
98
+ if df[column][0] == 0:
99
+ return 1.0
100
+ else:
101
+ return 1+round(((user_value-df[column][0])/df[column][0]),3)
102
+
103
+ #Function that takes user budget input and returns how it compares to model.
104
+ 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):
105
+ user_family = f'{user_parents}p{user_children}c'
106
+ user_df = df.loc[(df.county_state == user_county) & (df.family == user_family)].reset_index(drop=True)
107
+ income_p = calculate_percentage(user_income,'median_monthly_family_income',user_df)
108
+ housing_p = calculate_percentage(user_housing,'housing',user_df)
109
+ food_p = calculate_percentage(user_food,'food',user_df)
110
+ transportation_p = calculate_percentage(user_transportation,'transportation',user_df)
111
+ healthcare_p = calculate_percentage(user_healthcare,'healthcare',user_df)
112
+ childcare_p = calculate_percentage(user_childcare,'childcare',user_df)
113
+ other_p = calculate_percentage(user_other,'other_necessities',user_df)
114
+ taxes_p = calculate_percentage(user_taxes,'taxes',user_df)
115
+ total_p = 1+round((((user_housing+user_food+user_transportation+user_healthcare+
116
+ user_childcare+user_other+user_taxes)-user_df.total[0])/user_df.total[0]),3)
117
+ return (
118
+ f"Your family's income is {income_p:.1%} that of the median family income in your area. "
119
+ 'Your budget compares to the typical model as follows:\n'
120
+ f'\nHousing: {housing_p:.1%}'
121
+ f'\nFood: {food_p:.1%}'
122
+ f'\nTransportation: {transportation_p:.1%}'
123
+ f'\nHealthcare: {healthcare_p:.1%}'
124
+ f'\nChildcare: {"N/A" if user_children==0 else str(round(childcare_p*100,1))+"%"}'
125
+ f'\nOther necessities: {other_p:.1%}'
126
+ f'\nTaxes: {taxes_p:.1%}'
127
+ f'\nTotal: {total_p:.1%}'
128
+ )
129
+
130
+ #Function for budget-input widgets
131
+ def input_budget(prompt):
132
+ return pn.widgets.IntInput(name=prompt, value=0, start=0)
133
+
134
+ #Budget input widgets
135
+ user_income = input_budget('What is your monthly family income? (Pre-tax.)')
136
+ budget_title = pn.pane.Markdown("#### For each item, input your family's monthly spending.")
137
+ #How much do you spend on housing per month? Inclu
138
+ user_housing = input_budget('Housing (Include utilities.)')
139
+ user_food = input_budget('Food')
140
+ user_transportation = input_budget('Transportation (Gas, car payment/repair, bus pass, etc.)')
141
+ user_healthcare = input_budget('Healthcare (Both out-of-pocket and premium)')
142
+ user_childcare = input_budget('Childcare')
143
+ user_other = input_budget('Other necessities (Clothes, house/school supplies, etc.)')
144
+ user_taxes = input_budget('Taxes')
145
+
146
+ #Binding widgets to function
147
+ 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)
148
+
149
+ #Submit button, function to respond to button, binding to button
150
+ budget_submit = pn.widgets.Button(name="Submit Budget", button_type="primary")
151
+
152
+ def budget_result(clicked):
153
+ if clicked:
154
+ if user_county.value == '':
155
+ return 'County information is required.'
156
+ return budget_percentage()
157
+ return "Click Submit Budget to see how your budget compares."
158
+
159
+ budget_result = pn.pane.Markdown(pn.bind(budget_result,budget_submit))
160
+
161
+ ### Part 3: Calculate and display most afforable counties and budgets
162
+
163
+ #Function to take user constraints, find most affordable counties, and calculate budgets
164
+ 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,
165
+ bring_income,income_cap,income_cap_amount,state_restriction,states_allowed,result_count):
166
+ user_family = f'{user_parents}p{user_children}c'
167
+ user_df = df.loc[(df.county_state == user_county) & (df.family == user_family)].reset_index(drop=True)
168
+ income_p = calculate_percentage(user_income,'median_monthly_family_income',user_df)
169
+ housing_p = calculate_percentage(user_housing,'housing',user_df)
170
+ food_p = calculate_percentage(user_food,'food',user_df)
171
+ transportation_p = calculate_percentage(user_transportation,'transportation',user_df)
172
+ healthcare_p = calculate_percentage(user_healthcare,'healthcare',user_df)
173
+ childcare_p = calculate_percentage(user_childcare,'childcare',user_df)
174
+ other_p = calculate_percentage(user_other,'other_necessities',user_df)
175
+ taxes_p = calculate_percentage(user_taxes,'taxes',user_df)
176
+ new_df = df.loc[(df.family == user_family)].reset_index(drop=True)
177
+ if bring_income:
178
+ new_df.median_monthly_family_income = user_income
179
+ else:
180
+ new_df.median_monthly_family_income = new_df.median_monthly_family_income.mul(income_p).round(0).astype('int64')
181
+ new_df['median_monthly_family_income_uncapped'] = new_df.median_monthly_family_income
182
+ if income_cap:
183
+ new_df.median_monthly_family_income.clip(upper=income_cap_amount,inplace=True)
184
+ if state_restriction:
185
+ new_df = new_df.loc[(new_df.state_abbr.isin(states_allowed))].reset_index(drop=True)
186
+ new_df.housing = new_df.housing.mul(housing_p).round(0).astype('int64')
187
+ new_df.food = new_df.food.mul(food_p).round(0).astype('int64')
188
+ new_df.transportation = new_df.transportation.mul(transportation_p).round(0).astype('int64')
189
+ new_df.healthcare = new_df.healthcare.mul(healthcare_p).round(0).astype('int64')
190
+ new_df.other_necessities = new_df.other_necessities.mul(other_p).round(0).astype('int64')
191
+ new_df.childcare = new_df.childcare.mul(childcare_p).round(0).astype('int64')
192
+ new_df.taxes = new_df.taxes.mul(taxes_p).round(0).astype('int64')
193
+ 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
194
+ new_df.remaining_money = new_df.median_monthly_family_income - new_df.total
195
+ new_df = new_df.sort_values('remaining_money',ascending=False).reset_index(drop=True)
196
+ county_results = []
197
+ limit = result_count
198
+ if limit > len(new_df):
199
+ limit = len(new_df)
200
+ county_results.append(f'There are only {limit} counties to show!\n\n')
201
+ if limit == 1:
202
+ second_line = 'here is the most affordable county for your family, along with what your budget might look living there:\n'
203
+ else:
204
+ second_line = f'here are the {limit} most affordable counties for your family, along with what your budget might look living there:\n'
205
+ if not bring_income and not income_cap:
206
+ county_results.append("With both your spending and income recalculated based on each county's norm, "
207
+ + second_line)
208
+ elif bring_income:
209
+ county_results.append("With your exact income maintained but your spending recalculated based on each county's norm, "
210
+ + second_line)
211
+ else:
212
+ 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}), "
213
+ + second_line)
214
+ for i in range(limit):
215
+ current_result = [f'\n#{i+1}\n{new_df.county_state[i]}']
216
+ for [j,k] in [['housing','Housing'],['food','Food'],['transportation','Transportation'],['healthcare','Healthcare'],
217
+ ['childcare','Childcare'],['other_necessities','Other necessities'],['taxes','Taxes'],['total','Total'],
218
+ ['median_monthly_family_income','Median family income'],['remaining_money','Remaining money']]:
219
+ 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]:
220
+ current_result.append(f"    {k}: {dol(j,new_df,row=i)} (Uncapped: {dol('median_monthly_family_income_uncapped',new_df,row=i)})")
221
+ else:
222
+ current_result.append(f"    {k}: {dol(j,new_df,row=i)}")
223
+ county_results.append('\n'.join(current_result))
224
+ return '\n'.join(county_results)
225
+
226
+ #Constraint widgets
227
+ bring_income_title = pn.pane.Markdown('Maintain income exactly, regardless of county:')
228
+ bring_income = pn.widgets.Switch(margin=(19,0,0,0))
229
+
230
+ income_cap_title = pn.pane.Markdown('Enforce income cap:') #Would be nice to disable when bring_income is active
231
+ income_cap = pn.widgets.Switch(margin=(19,0,0,0))
232
+
233
+ income_cap_amount = pn.widgets.IntInput(name='Income cap value:', value=0, start=0)
234
+
235
+ state_restriction_title = pn.pane.Markdown('Restrict results to particular states:')
236
+ state_restriction = pn.widgets.Switch(margin=(19,0,0,0))
237
+
238
+ states_allowed = pn.widgets.MultiChoice(name='States to include:', options=df.state_abbr.unique().tolist()) #Would be nice to disable when state_restriction is not active
239
+
240
+ result_count = pn.widgets.IntInput(name='Results to view:', value=5, start=1)
241
+
242
+ #Bind to widgets
243
+ comparison = pn.bind(calculate_comparison, user_county,user_parents,user_children,
244
+ user_income,user_housing,user_food,user_transportation,user_healthcare,user_childcare,user_other,user_taxes,
245
+ bring_income,income_cap,income_cap_amount,state_restriction,states_allowed,result_count)
246
+
247
+ #Submit button, bind
248
+ comparison_submit = pn.widgets.Button(name="Submit Constraints", button_type="primary")
249
+
250
+ def comparison_result(clicked):
251
+ if clicked:
252
+ if user_county.value == '':
253
+ return 'County information is required.'
254
+ return comparison()
255
+ return "Click Submit Constraints to see the final results."
256
+
257
+ comparison_result = pn.pane.Markdown(pn.bind(comparison_result,comparison_submit))
258
+
259
+ ### Part 4: Templating
260
+
261
+ template = pn.template.BootstrapTemplate(title='Affordable County Locator')
262
+
263
+ #Sidebar formatting
264
+ intro = pn.pane.Markdown('If you and your family could maintain your relative income and spending habits,'
265
+ 'in which US county would you be able to save the most money every month? Use this app to find out!')
266
+ part1_title = pn.pane.Markdown('## Part 1: County and family size')
267
+ part2_title = pn.pane.Markdown('## Part 2: Budget')
268
+ part3_title = pn.pane.Markdown('## Part 3: Calculation constraints')
269
+
270
+ template.sidebar.extend([intro, part1_title, user_county, pn.Row(adult_title, user_parents), user_children, county_submit,
271
+ part2_title, user_income, budget_title, user_housing, user_food, user_transportation, user_healthcare, user_childcare, user_other, user_taxes, budget_submit,
272
+ part3_title, pn.Row(bring_income_title, bring_income),pn.Row(income_cap_title, income_cap),income_cap_amount,pn.Row(state_restriction_title, state_restriction),states_allowed,result_count,comparison_submit])
273
+
274
+ #Main formatting
275
+ main1 = pn.Card(
276
+ county_result,
277
+ title='Typical Budget',
278
+ styles={'background': 'WhiteSmoke'}
279
+ )
280
+ main2 = pn.Card(
281
+ budget_result,
282
+ title='Budget Comparison',
283
+ styles={'background': 'WhiteSmoke'}
284
+ )
285
+ main3 = pn.Card(
286
+ comparison_result,
287
+ title='Most Affordable Counties',
288
+ styles={'background': 'WhiteSmoke'},
289
+ max_height = 300
290
+ )
291
+ template.main.append(
292
+ pn.Column(main1,main2,main3)
293
+ )
294
+
295
+ template.servable();
app.py DELETED
@@ -1,108 +0,0 @@
1
- import panel as pn
2
- import pandas as pd
3
- import hvplot.pandas
4
-
5
- pn.extension("tabulator")
6
-
7
- ACCENT="teal"
8
-
9
- styles = {
10
- "box-shadow": "rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px",
11
- "border-radius": "4px",
12
- "padding": "10px",
13
- }
14
-
15
- image = pn.pane.JPG("https://assets.holoviz.org/panel/tutorials/wind_turbines_sunset.png")
16
-
17
- # Extract Data
18
-
19
- @pn.cache() # only download data once
20
- def get_data():
21
- return pd.read_csv("https://assets.holoviz.org/panel/tutorials/turbines.csv.gz")
22
-
23
- # Transform Data
24
-
25
- source_data = get_data()
26
- min_year = int(source_data["p_year"].min())
27
- max_year = int(source_data["p_year"].max())
28
- top_manufacturers = (
29
- source_data.groupby("t_manu").p_cap.sum().sort_values().iloc[-10:].index.to_list()
30
- )
31
-
32
- def filter_data(t_manu, year):
33
- data = source_data[(source_data.t_manu == t_manu) & (source_data.p_year <= year)]
34
- return data
35
-
36
- # Filters
37
-
38
- t_manu = pn.widgets.Select(
39
- name="Manufacturer",
40
- value="Vestas",
41
- options=sorted(top_manufacturers),
42
- description="The name of the manufacturer",
43
- )
44
- p_year = pn.widgets.IntSlider(name="Year", value=max_year, start=min_year, end=max_year)
45
-
46
- # Transform Data 2
47
-
48
- df = pn.rx(filter_data)(t_manu=t_manu, year=p_year)
49
- count = df.rx.len()
50
- total_capacity = df.t_cap.sum()
51
- avg_capacity = df.t_cap.mean()
52
- avg_rotor_diameter = df.t_rd.mean()
53
-
54
- # Plot Data
55
-
56
- fig = (
57
- df[["p_year", "t_cap"]].groupby("p_year").sum() / 10**6
58
- ).hvplot.bar(
59
- title="Capacity Change",
60
- rot=90,
61
- ylabel="Capacity (GW)",
62
- xlabel="Year",
63
- xlim=(min_year, max_year),
64
- color=ACCENT,
65
- )
66
-
67
- # Display Data
68
-
69
- indicators = pn.FlexBox(
70
- pn.indicators.Number(
71
- value=count, name="Count", format="{value:,.0f}", styles=styles
72
- ),
73
- pn.indicators.Number(
74
- value=total_capacity / 1e6,
75
- name="Total Capacity (GW)",
76
- format="{value:,.1f}",
77
- styles=styles,
78
- ),
79
- pn.indicators.Number(
80
- value=avg_capacity/1e3,
81
- name="Avg. Capacity (MW)",
82
- format="{value:,.1f}",
83
- styles=styles,
84
- ),
85
- pn.indicators.Number(
86
- value=avg_rotor_diameter,
87
- name="Avg. Rotor Diameter (m)",
88
- format="{value:,.1f}",
89
- styles=styles,
90
- ),
91
- )
92
-
93
- plot = pn.pane.HoloViews(fig, sizing_mode="stretch_both", name="Plot")
94
- table = pn.widgets.Tabulator(df, sizing_mode="stretch_both", name="Table")
95
-
96
- # Layout Data
97
-
98
- tabs = pn.Tabs(
99
- plot, table, styles=styles, sizing_mode="stretch_width", height=500, margin=10
100
- )
101
-
102
- pn.template.FastListTemplate(
103
- title="Wind Turbine Dashboard",
104
- sidebar=[image, t_manu, p_year],
105
- main=[pn.Column(indicators, tabs, sizing_mode="stretch_both")],
106
- main_layout=None,
107
- accent=ACCENT,
108
- ).servable()