Rename app.py to affordable_county_locator.py
Browse files- affordable_county_locator.py +295 -0
- 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|