import streamlit as st import numpy as np import seaborn as sns import matplotlib.pyplot as plt import numpy_financial as npf import pandas as pd from streamlit_folium import folium_static import leafmap.foliumap as leafmap import folium from shapely.geometry import Point, Polygon import geopandas import geopy from geopy.geocoders import Nominatim from geopy.extra.rate_limiter import RateLimiter from scipy.spatial import cKDTree #----------------------------------------- # Set page settings st.set_page_config(layout="wide") #----------------------------------------- # Sidebar with st.sidebar: st.header('Welcome to the Airbnb Investment Tool!') nav = st.selectbox('Navigation', ['Heuristic Pricing', 'Investment Analysis']) #----------------------------------------- # Additional Functions def p_title(title): st.markdown(f'

{title}

', unsafe_allow_html=True) # Function to return a GeoPandas DataFrame containing the listings # that are within a specified radius from a specified lat, long. def getNearbyListings(gdf_proj, input_long, input_lat, radius): # Build Tree airbnbCoords = np.array(list(gdf_proj.geometry.apply(lambda x: (x.x, x.y)))) airbnbTree = cKDTree(airbnbCoords) # Convert lat-long to projected coords gdf_input = geopandas.GeoSeries.from_xy(x=[input_long], y=[input_lat], crs=4326) gdf_input_proj = gdf_input.to_crs(crs=32634) coords = np.array(list((gdf_input_proj.x[0], gdf_input_proj.y[0]))) # Returns list of indices whose distance is <= radius neighbours_indices = airbnbTree.query_ball_point(coords, radius) gdf_neighbours_proj = gdf_proj.iloc[neighbours_indices, :] gdf_neighbours = gdf_neighbours_proj.to_crs(crs=4326) return gdf_neighbours # Function to return IRR. # Financial Modelling Tool. def investment_tool(house_price, loan_amount, loan_period, percentage_loan_interest_annual, rental_charged_monthly, percentage_rental_tax, percentage_increase_in_rental_yearly, utilisation_rate, yearly_refurbishment_costs, percentage_increase_in_refurbishment_yearly, ending_value_of_house): #expected format of percentage parameters is whole number and not decimals i.e., 5 instead of 0.05 #all non-% parameters are expected to be positive house_price = int(house_price) loan_amount = int(loan_amount) loan_period = int(loan_period) percentage_loan_interest_annual = int(percentage_loan_interest_annual) rental_charged_monthly = int(rental_charged_monthly) percentage_rental_tax = int(percentage_rental_tax) percentage_increase_in_rental_yearly = int(percentage_increase_in_rental_yearly) percentage_utilisation_rate = int(utilisation_rate) yearly_refurbishment_costs = int(yearly_refurbishment_costs) percentage_increase_in_refurbishment_yearly = int(percentage_increase_in_refurbishment_yearly) ending_value_of_house = int(ending_value_of_house) #ensuring the figures make sense if loan_amount > house_price: return print("Loan Amount cannot exceed House Price") #creating the list of cash flows to be used to calculate internal rate of return initial_cashflow = -(1 - loan_amount/house_price) * house_price cashflow_list = [initial_cashflow] #finding the annual mortgage assuming equal amortization mortgage = npf.pmt(percentage_loan_interest_annual / 100, loan_period, loan_amount) #the np.pmt function will automatically put mortgage as a negative cashflow #finding the annual cashflows & loan balance changes during the loan period and appending them to the respective lists for i in range(loan_period): rental = 12 * rental_charged_monthly * ((1 + (percentage_increase_in_rental_yearly / 100)) ** i) * (1 - (percentage_rental_tax / 100)) * utilisation_rate / 100 refurbishment_cost = -1 * yearly_refurbishment_costs * ((1 + (percentage_increase_in_refurbishment_yearly / 100)) ** i) #the condition here is to include the salvage/ending value of the house to cashflows after loan repayments are finished if i == (loan_period-1): yearly_cashflow = ending_value_of_house + rental + mortgage + refurbishment_cost else: yearly_cashflow = rental + mortgage + refurbishment_cost cashflow_list.append(yearly_cashflow) #finding the internal rate of return irr = round(npf.irr(cashflow_list), 4) #----------------------------------- #Dataframe for plotting of graph loan_dict = {'Year': [0], 'Starting Loan Balance': [0], 'Cumulative Interest Paid': [0], 'Cumulative Principal Paid': [0], 'Remaining Loan Balance': [0]} # Create DataFrame loan_dataframe = pd.DataFrame(loan_dict) #finding the annual mortgage assuming equal amortization mortgage = npf.pmt(percentage_loan_interest_annual / 100, loan_period, loan_amount) #the np.pmt function will automatically put mortgage as a negative cashflow #updating the global dataframe loan_dataframe.loc[0,'Starting Loan Balance'] = loan_amount for i in range(loan_period): loan_dataframe.loc[i,'Year'] = i+1 #the condition here is to calculate principal and interest paid if i == 0: loan_dataframe.loc[i,'Cumulative Interest Paid'] = loan_dataframe.loc[i,'Starting Loan Balance'] * (percentage_loan_interest_annual / 100) loan_dataframe.loc[i,'Cumulative Principal Paid'] = (-1 * mortgage) - (loan_dataframe.loc[i,'Starting Loan Balance'] * (percentage_loan_interest_annual / 100)) else: 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'] 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'] 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 #condition to update starting loan balance if i != loan_period-1: loan_dataframe.loc[i+1,'Starting Loan Balance'] = loan_dataframe.loc[i,'Remaining Loan Balance'] loan_dataframe['Remaining Loan Balance'] = pd.to_numeric(loan_dataframe['Remaining Loan Balance']) return irr, loan_dataframe #----------------------------------------- # Load Airbnb listings data df_raw = pd.read_csv("data/listings_sf_withamenities.csv") df = df_raw.copy() gdf = geopandas.GeoDataFrame( df, geometry=geopandas.points_from_xy(df.longitude, df.latitude), crs=4326) gdf_proj = gdf.to_crs(crs=32634) #----------------------------------------- # Tab 1: Heuristic Pricing if nav == 'Heuristic Pricing': st.markdown("

Airbnb 🏠

", unsafe_allow_html=True) st.text('') p_title('Heuristic Pricing') st.text('') # Get address inputs st.caption('Enter your address:') with st.form("heuristics_form"): col1, col2 = st.columns(2) with col1: postalcode = st.text_input("Postal Code", "94109") street = st.text_input("Street", "1788 Clay Street") city = st.selectbox("City", ["San Francisco"]) with col2: state = st.selectbox("State", ["California"]) country = st.selectbox("Country", ["United States"]) radius = st.slider("Distance of nearest listings (metres)", min_value=500, max_value=2000, value=500, step=500) submitted = st.form_submit_button("Submit") if submitted: # Get geolocation geolocator = Nominatim(user_agent="GTA Lookup") geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1) location = geolocator.geocode({"postalcode": postalcode, "street": street, "city": city, "state": state, "country": country}) # If the search address yields no result, set to default coords of San Fran if location is None: lat = 37.773972 lon = -122.431297 st.error("Address is not found. Please try again.") else: lat = location.latitude lon = location.longitude # Compute Stats st.markdown('___') st.caption('Recommended Pricing:') gdf_nearby_listings = getNearbyListings(gdf_proj, lon, lat, radius=radius) if len(gdf_nearby_listings) == 0: st.error("There are no nearby listings.") else: col3, col4 = st.columns(2) with col3: df_nearby_stats = gdf_nearby_listings[["price"]].describe().round(2) df_nearby_stats.columns = pd.Index(["Price"]) st.table(df_nearby_stats.style.format("{:.2f}")) with col4: # Plot Stats fig = plt.figure(figsize=(10, 4)) sns.boxplot(x="price", data=gdf_nearby_listings, showfliers=False) st.pyplot(fig) # Plot using leafmap. Responsive width. m = leafmap.Map(tiles="OpenStreetMap", location=[lat, lon], zoom_start=15) m.add_marker(location=[lat, lon]) m.add_points_from_xy(gdf_nearby_listings, x="longitude", y="latitude", popup=["id", "price", "review_scores_rating"], color_options=['red']) m.add_heatmap(data=gdf_nearby_listings, latitude="latitude", longitude="longitude", value="price", min_opacity=0.1, name="Price Heatmap", blue=50) m.to_streamlit() #----------------------------------------- # Tab 2: Investment Analysis if nav == 'Investment Analysis': st.markdown("

Airbnb 🏠

", unsafe_allow_html=True) st.text('') p_title('Investment Analysis') # Financial Projections st.caption("Enter data here") with st.form("investment_form"): col1_2, col2_2, col3_2 = st.columns(3) with col1_2: house_price = st.number_input("Purchase Price of House ($)", min_value=0, value=250000) loan_amount = st.number_input("Loan Amount ($)", min_value=0, value=150000) loan_period = st.number_input("Loan Period (Years)", min_value=0, value=15) percentage_loan_interest_annual = st.number_input("Annual Loan I/R (%)", min_value=0.0, max_value=100.0, value=2.1) with col2_2: rental_charged_monthly = st.number_input("Monthly Rental ($)", min_value=0, value=2000) percentage_rental_tax = st.number_input("Rental Tax (%)", min_value=0.0, value=0.0) percentage_increase_in_rental_yearly = st.number_input("Annual Rental Increase (%)", min_value=0.0, value=1.0) utilisation_rate = st.number_input("Utilisation Rate (%)", min_value=0.0, value=50.0) with col3_2: yearly_refurbishment_costs = st.number_input("Yearly Refurbishment Costs ($)", min_value=0, value=3000) percentage_increase_in_refurbishment_yearly = st.number_input("Yearly Refurbishment Costs Increase (%)", min_value=0.0, value=2.0) ending_value_of_house = st.number_input("Ending Value of House ($)", min_value=0, value=300000) submitted2 = st.form_submit_button("Submit") if submitted2: irr, loan_dataframe = investment_tool(house_price, loan_amount, loan_period, percentage_loan_interest_annual, rental_charged_monthly, percentage_rental_tax, percentage_increase_in_rental_yearly, utilisation_rate, yearly_refurbishment_costs, percentage_increase_in_refurbishment_yearly, ending_value_of_house) st.markdown('___') st.caption("Expected Internal Rate of Return") st.text("{:.2%}".format(irr)) # Print plots fig = plt.figure(figsize=(10, 4)) plt.bar(loan_dataframe['Year'], loan_dataframe['Cumulative Principal Paid'], color='lightcoral') plt.bar(loan_dataframe['Year'], loan_dataframe['Cumulative Interest Paid'], bottom=loan_dataframe['Cumulative Principal Paid'], color='lightsalmon') plt.plot(loan_dataframe['Year'], loan_dataframe['Remaining Loan Balance'], color='crimson') plt.ylabel('Amount') plt.title('Loan Balance') plt.legend(('Loan Balance Remaining','Cumulative Principal Paid', 'Cumulative Interest Paid')) st.pyplot(fig)