Qiwei97 commited on
Commit
3b88404
1 Parent(s): 8239d62

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +265 -0
app.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import streamlit as st
3
+ import numpy as np
4
+ import seaborn as sns
5
+ import matplotlib.pyplot as plt
6
+ import numpy_financial as npf
7
+ import pandas as pd
8
+ from streamlit_folium import folium_static
9
+ import leafmap.foliumap as leafmap
10
+ import folium
11
+ from shapely.geometry import Point, Polygon
12
+ import geopandas
13
+ import geopy
14
+ from geopy.geocoders import Nominatim
15
+ from geopy.extra.rate_limiter import RateLimiter
16
+ from scipy.spatial import cKDTree
17
+
18
+ #-----------------------------------------
19
+ # Set page settings
20
+ st.set_page_config(layout="wide")
21
+
22
+ #-----------------------------------------
23
+ # Sidebar
24
+ with st.sidebar:
25
+ st.header('Welcome to the Airbnb Investment Tool!')
26
+ nav = st.selectbox('Navigation', ['Heuristic Pricing',
27
+ 'Investment Analysis',
28
+ 'Customer View'])
29
+
30
+ #-----------------------------------------
31
+ # Additional Functions
32
+
33
+ def p_title(title):
34
+ st.markdown(f'<h3 style="text-align: left; color:#F63366; font-size:28px;">{title}</h3>', unsafe_allow_html=True)
35
+
36
+
37
+ # Function to return a GeoPandas DataFrame containing the listings
38
+ # that are within a specified radius from a specified lat, long.
39
+ def getNearbyListings(gdf_proj, input_long, input_lat, radius):
40
+ # Build Tree
41
+ airbnbCoords = np.array(list(gdf_proj.geometry.apply(lambda x: (x.x, x.y))))
42
+ airbnbTree = cKDTree(airbnbCoords)
43
+
44
+ # Convert lat-long to projected coords
45
+ gdf_input = geopandas.GeoSeries.from_xy(x=[input_long], y=[input_lat], crs=4326)
46
+ gdf_input_proj = gdf_input.to_crs(crs=32634)
47
+
48
+ coords = np.array(list((gdf_input_proj.x[0], gdf_input_proj.y[0])))
49
+
50
+ # Returns list of indices whose distance is <= radius
51
+ neighbours_indices = airbnbTree.query_ball_point(coords, radius)
52
+ gdf_neighbours_proj = gdf_proj.iloc[neighbours_indices, :]
53
+ gdf_neighbours = gdf_neighbours_proj.to_crs(crs=4326)
54
+
55
+ return gdf_neighbours
56
+
57
+ # Function to return IRR.
58
+ # Financial Modelling Tool.
59
+ def investment_tool(house_price, loan_amount, loan_period, percentage_loan_interest_annual,
60
+ rental_charged_monthly, percentage_rental_tax, percentage_increase_in_rental_yearly, utilisation_rate,
61
+ yearly_refurbishment_costs, percentage_increase_in_refurbishment_yearly, ending_value_of_house):
62
+
63
+ #expected format of percentage parameters is whole number and not decimals i.e., 5 instead of 0.05
64
+ #all non-% parameters are expected to be positive
65
+
66
+ house_price = int(house_price)
67
+ loan_amount = int(loan_amount)
68
+ loan_period = int(loan_period)
69
+ percentage_loan_interest_annual = int(percentage_loan_interest_annual)
70
+ rental_charged_monthly = int(rental_charged_monthly)
71
+ percentage_rental_tax = int(percentage_rental_tax)
72
+ percentage_increase_in_rental_yearly = int(percentage_increase_in_rental_yearly)
73
+ percentage_utilisation_rate = int(utilisation_rate)
74
+ yearly_refurbishment_costs = int(yearly_refurbishment_costs)
75
+ percentage_increase_in_refurbishment_yearly = int(percentage_increase_in_refurbishment_yearly)
76
+ ending_value_of_house = int(ending_value_of_house)
77
+
78
+ #ensuring the figures make sense
79
+ if loan_amount > house_price:
80
+ return print("Loan Amount cannot exceed House Price")
81
+
82
+ #creating the list of cash flows to be used to calculate internal rate of return
83
+ initial_cashflow = -(1 - loan_amount/house_price) * house_price
84
+ cashflow_list = [initial_cashflow]
85
+
86
+ #finding the annual mortgage assuming equal amortization
87
+ mortgage = npf.pmt(percentage_loan_interest_annual / 100, loan_period, loan_amount) #the np.pmt function will automatically put mortgage as a negative cashflow
88
+
89
+ #finding the annual cashflows & loan balance changes during the loan period and appending them to the respective lists
90
+ for i in range(loan_period):
91
+ rental = 12 * rental_charged_monthly * ((1 + (percentage_increase_in_rental_yearly / 100)) ** i) * (1 - (percentage_rental_tax / 100)) * utilisation_rate / 100
92
+ refurbishment_cost = -1 * yearly_refurbishment_costs * ((1 + (percentage_increase_in_refurbishment_yearly / 100)) ** i)
93
+
94
+ #the condition here is to include the salvage/ending value of the house to cashflows after loan repayments are finished
95
+ if i == (loan_period-1):
96
+ yearly_cashflow = ending_value_of_house + rental + mortgage + refurbishment_cost
97
+ else:
98
+ yearly_cashflow = rental + mortgage + refurbishment_cost
99
+ cashflow_list.append(yearly_cashflow)
100
+
101
+ #finding the internal rate of return
102
+ irr = round(npf.irr(cashflow_list), 4)
103
+
104
+ #-----------------------------------
105
+ #Dataframe for plotting of graph
106
+ loan_dict = {'Year': [0], 'Starting Loan Balance': [0], 'Cumulative Interest Paid': [0], 'Cumulative Principal Paid': [0], 'Remaining Loan Balance': [0]}
107
+
108
+ # Create DataFrame
109
+ loan_dataframe = pd.DataFrame(loan_dict)
110
+
111
+ #finding the annual mortgage assuming equal amortization
112
+ mortgage = npf.pmt(percentage_loan_interest_annual / 100, loan_period, loan_amount) #the np.pmt function will automatically put mortgage as a negative cashflow
113
+
114
+ #updating the global dataframe
115
+ loan_dataframe.loc[0,'Starting Loan Balance'] = loan_amount
116
+
117
+ for i in range(loan_period):
118
+ loan_dataframe.loc[i,'Year'] = i+1
119
+
120
+ #the condition here is to calculate principal and interest paid
121
+ if i == 0:
122
+ loan_dataframe.loc[i,'Cumulative Interest Paid'] = loan_dataframe.loc[i,'Starting Loan Balance'] * (percentage_loan_interest_annual / 100)
123
+ loan_dataframe.loc[i,'Cumulative Principal Paid'] = (-1 * mortgage) - (loan_dataframe.loc[i,'Starting Loan Balance'] * (percentage_loan_interest_annual / 100))
124
+ else:
125
+ loan_dataframe.loc[i,'Cumulative Interest Paid'] = loan_dataframe.loc[i,'Starting Loan Balance'] * (percentage_loan_interest_annual / 100) + loan_dataframe.loc[i-1,'Cumulative Interest Paid']
126
+ loan_dataframe.loc[i,'Cumulative Principal Paid'] = (-1 * mortgage) - (loan_dataframe.loc[i,'Starting Loan Balance'] * (percentage_loan_interest_annual / 100)) + loan_dataframe.loc[i-1,'Cumulative Principal Paid']
127
+
128
+ loan_dataframe.loc[i,'Remaining Loan Balance'] = loan_dataframe.loc[i,'Starting Loan Balance'] + (loan_dataframe.loc[i,'Starting Loan Balance'] * (percentage_loan_interest_annual / 100)) + mortgage
129
+
130
+ #condition to update starting loan balance
131
+ if i != loan_period-1:
132
+ loan_dataframe.loc[i+1,'Starting Loan Balance'] = loan_dataframe.loc[i,'Remaining Loan Balance']
133
+
134
+ loan_dataframe['Remaining Loan Balance'] = pd.to_numeric(loan_dataframe['Remaining Loan Balance'])
135
+
136
+ return irr, loan_dataframe
137
+
138
+ #-----------------------------------------
139
+ # Load Airbnb listings data
140
+ df_raw = pd.read_csv("data/listings_sf_withamenities.csv")
141
+ df = df_raw.copy()
142
+ gdf = geopandas.GeoDataFrame(
143
+ df,
144
+ geometry=geopandas.points_from_xy(df.longitude, df.latitude),
145
+ crs=4326)
146
+ gdf_proj = gdf.to_crs(crs=32634)
147
+
148
+ #-----------------------------------------
149
+ # Tab 1: Heuristic Pricing
150
+ if nav == 'Heuristic Pricing':
151
+
152
+ st.markdown("<h3 style='text-align: center; color:grey;'>Airbnb &#127968;</h3>", unsafe_allow_html=True)
153
+ st.text('')
154
+ p_title('Heuristic Pricing')
155
+ st.text('')
156
+
157
+ # Get address inputs
158
+ st.caption('Enter your address:')
159
+ with st.form("heuristics_form"):
160
+ col1, col2 = st.columns(2)
161
+ with col1:
162
+ postalcode = st.text_input("Postal Code", "94109")
163
+ street = st.text_input("Street", "1788 Clay Street")
164
+ city = st.selectbox("City", ["San Francisco"])
165
+ with col2:
166
+ state = st.selectbox("State", ["California"])
167
+ country = st.selectbox("Country", ["United States"])
168
+ radius = st.slider("Distance of nearest listings (metres)", min_value=500, max_value=2000, value=500, step=500)
169
+ submitted = st.form_submit_button("Submit")
170
+
171
+ if submitted:
172
+ # Get geolocation
173
+ geolocator = Nominatim(user_agent="GTA Lookup")
174
+ geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
175
+ location = geolocator.geocode({"postalcode": postalcode, "street": street, "city": city, "state": state, "country": country})
176
+
177
+ # If the search address yields no result, set to default coords of San Fran
178
+ if location is None:
179
+ lat = 37.773972
180
+ lon = -122.431297
181
+ st.error("Address is not found. Please try again.")
182
+ else:
183
+ lat = location.latitude
184
+ lon = location.longitude
185
+
186
+ # Compute Stats
187
+ st.markdown('___')
188
+ st.caption('Recommended Pricing:')
189
+ gdf_nearby_listings = getNearbyListings(gdf_proj, lon, lat, radius=radius)
190
+ if len(gdf_nearby_listings) == 0:
191
+ st.error("There are no nearby listings.")
192
+ else:
193
+ col3, col4 = st.columns(2)
194
+ with col3:
195
+ df_nearby_stats = gdf_nearby_listings[["price"]].describe()
196
+ st.table(df_nearby_stats)
197
+ with col4:
198
+ # Plot Stats
199
+ fig = plt.figure(figsize=(10, 4))
200
+ sns.boxplot(x="price", data=gdf_nearby_listings, showfliers=False)
201
+ st.pyplot(fig)
202
+
203
+ # Plot using leafmap. Responsive width.
204
+ m = leafmap.Map(tiles="OpenStreetMap", location=[lat, lon], zoom_start=15)
205
+ m.add_marker(location=[lat, lon])
206
+ m.add_points_from_xy(gdf_nearby_listings, x="longitude", y="latitude",
207
+ popup=["id", "price", "review_scores_rating"],
208
+ color_options=['red'])
209
+ m.add_heatmap(data=gdf_nearby_listings,
210
+ latitude="latitude", longitude="longitude",
211
+ value="price", min_opacity=0.1,
212
+ name="Price Heatmap", blue=50)
213
+ m.to_streamlit()
214
+
215
+ #-----------------------------------------
216
+ # Tab 2: Investment Analysis
217
+ if nav == 'Investment Analysis':
218
+ st.markdown("<h3 style='text-align: center; color:grey;'>Airbnb &#127968;</h3>", unsafe_allow_html=True)
219
+ st.text('')
220
+ p_title('Investment Analysis')
221
+
222
+ # Financial Projections
223
+ st.caption("Enter data here")
224
+ with st.form("investment_form"):
225
+ col1_2, col2_2, col3_2 = st.columns(3)
226
+ with col1_2:
227
+ house_price = st.number_input("Purchase Price of House ($)", min_value=0, value=250000)
228
+ loan_amount = st.number_input("Loan Amount ($)", min_value=0, value=150000)
229
+ loan_period = st.number_input("Loan Period (Years)", min_value=0, value=15)
230
+ percentage_loan_interest_annual = st.number_input("Annual Loan I/R (%)", min_value=0.0, max_value=100.0, value=2.1)
231
+ with col2_2:
232
+ rental_charged_monthly = st.number_input("Monthly Rental ($)", min_value=0, value=2000)
233
+ percentage_rental_tax = st.number_input("Rental Tax (%)", min_value=0.0, value=0.0)
234
+ percentage_increase_in_rental_yearly = st.number_input("Annual Rental Increase (%)", min_value=0.0, value=1.0)
235
+ utilisation_rate = st.number_input("Utilisation Rate (%)", min_value=0.0, value=50.0)
236
+ with col3_2:
237
+ yearly_refurbishment_costs = st.number_input("Yearly Refurbishment Costs ($)", min_value=0, value=3000)
238
+ percentage_increase_in_refurbishment_yearly = st.number_input("Yearly Refurbishment Costs Increase (%)", min_value=0.0, value=2.0)
239
+ ending_value_of_house = st.number_input("Ending Value of House ($)", min_value=0, value=300000)
240
+ submitted2 = st.form_submit_button("Submit")
241
+
242
+ if submitted2:
243
+ irr, loan_dataframe = investment_tool(house_price, loan_amount, loan_period, percentage_loan_interest_annual,
244
+ rental_charged_monthly, percentage_rental_tax, percentage_increase_in_rental_yearly, utilisation_rate,
245
+ yearly_refurbishment_costs, percentage_increase_in_refurbishment_yearly, ending_value_of_house)
246
+ st.markdown('___')
247
+ st.caption("Expected Internal Rate of Return")
248
+ st.text("{:.2%}".format(irr))
249
+ # Print plots
250
+ fig = plt.figure(figsize=(10, 4))
251
+ plt.bar(loan_dataframe['Year'], loan_dataframe['Cumulative Principal Paid'], color='lightcoral')
252
+ plt.bar(loan_dataframe['Year'], loan_dataframe['Cumulative Interest Paid'], bottom=loan_dataframe['Cumulative Principal Paid'], color='lightsalmon')
253
+ plt.plot(loan_dataframe['Year'], loan_dataframe['Remaining Loan Balance'], color='crimson')
254
+ plt.ylabel('Amount')
255
+ plt.title('Loan Balance')
256
+ plt.legend(('Loan Balance Remaining','Cumulative Principal Paid', 'Cumulative Interest Paid'))
257
+ st.pyplot(fig)
258
+
259
+ if nav == "Customer View":
260
+ st.markdown("<h3 style='text-align: center; color:grey;'>Airbnb &#127968;</h3>", unsafe_allow_html=True)
261
+ st.text('')
262
+ p_title('Customer View')
263
+
264
+ customer_tableau_embed_code = "<div class='tableauPlaceholder' id='viz1650723513927' style='position: relative'><noscript><a href='#'><img alt=' ' src='https:&#47;&#47;public.tableau.com&#47;static&#47;images&#47;Da&#47;Dashboard_1_16505589498970&#47;EDA&#47;1_rss.png' style='border: none' /></a></noscript><object class='tableauViz' style='display:none;'><param name='host_url' value='https%3A%2F%2Fpublic.tableau.com%2F' /> <param name='embed_code_version' value='3' /> <param name='site_root' value='' /><param name='name' value='Dashboard_1_16505589498970&#47;EDA' /><param name='tabs' value='yes' /><param name='toolbar' value='yes' /><param name='static_image' value='https:&#47;&#47;public.tableau.com&#47;static&#47;images&#47;Da&#47;Dashboard_1_16505589498970&#47;EDA&#47;1.png' /> <param name='animate_transition' value='yes' /><param name='display_static_image' value='yes' /><param name='display_spinner' value='yes' /><param name='display_overlay' value='yes' /><param name='display_count' value='yes' /><param name='language' value='en-US' /></object></div> <script type='text/javascript'> var divElement = document.getElementById('viz1650723513927'); var vizElement = divElement.getElementsByTagName('object')[0]; if ( divElement.offsetWidth > 800 ) { vizElement.style.width='1600px';vizElement.style.height='1150px';} else if ( divElement.offsetWidth > 500 ) { vizElement.style.width='1600px';vizElement.style.height='1150px';} else { vizElement.style.width='100%';vizElement.style.height='2850px';} var scriptElement = document.createElement('script'); scriptElement.src = 'https://public.tableau.com/javascripts/api/viz_v1.js'; vizElement.parentNode.insertBefore(scriptElement, vizElement); </script>"
265
+ st.components.v1.html(customer_tableau_embed_code, height=2000, scrolling=True)