Spaces:
Runtime error
Runtime error
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'<h3 style="text-align: left; color:#F63366; font-size:28px;">{title}</h3>', 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("<h3 style='text-align: center; color:grey;'>Airbnb 🏠</h3>", 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("<h3 style='text-align: center; color:grey;'>Airbnb 🏠</h3>", 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) |