1
Install the required packages and create a new app.py file. Streamlit turns Python scripts into interactive web apps with zero frontend code. We will use Plotly for charts and Pandas for data manipulation.
python
pip install streamlit requests pandas plotly
# app.py
import streamlit as st
import requests
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.airroi.com"
HEADERS = {"X-API-KEY": API_KEY}
st.set_page_config(
page_title="STR Investment Dashboard",
page_icon="📊",
layout="wide",
)
st.title("STR Investment Dashboard")
st.markdown("Powered by [AirROI API](https://airroi.com/api)")2
Add a sidebar with a market search input. When the user types a city name, call the GET /markets/search endpoint and display matching markets in a dropdown. Store the selected market in session state so it persists across tab switches.
python
# Sidebar: Market Search
st.sidebar.header("Market Selection")
search_query = st.sidebar.text_input(
"Search for a market",
placeholder="e.g., Miami, Nashville, Austin"
)
if search_query:
response = requests.get(
f"{BASE_URL}/markets/search",
headers=HEADERS,
params={"query": search_query},
)
markets = response.json().get("results", [])
if markets:
market_options = {
f"{m['locality']}, {m['region']}, {m['country']}": m
for m in markets
}
selected_label = st.sidebar.selectbox(
"Select a market",
options=list(market_options.keys()),
)
st.session_state["selected_market"] = market_options[selected_label]
else:
st.sidebar.warning("No markets found. Try a different search.")
if "selected_market" not in st.session_state:
st.info("Search for a market in the sidebar to get started.")
st.stop()
market = st.session_state["selected_market"]
st.sidebar.success(
f"Selected: {market['locality']}, {market['region']}"
)3
Build the revenue estimation tab with inputs for property coordinates, bedrooms, bathrooms, and guests. Call GET /calculator/estimate and display revenue at multiple percentiles with a monthly revenue bar chart.
python
# Revenue Estimation Tab
tab1, tab2, tab3 = st.tabs([
"Revenue Estimator", "Market Metrics", "Comp Set"
])
with tab1:
st.header("Revenue Estimation")
col1, col2 = st.columns(2)
with col1:
address = st.text_input("Property Address or Coordinates")
latitude = st.number_input("Latitude", value=0.0, format="%.6f")
longitude = st.number_input("Longitude", value=0.0, format="%.6f")
with col2:
bedrooms = st.number_input("Bedrooms", min_value=1, max_value=20, value=2)
baths = st.number_input("Bathrooms", min_value=1, max_value=10, value=1)
guests = st.number_input("Max Guests", min_value=1, max_value=30, value=4)
if st.button("Estimate Revenue", type="primary"):
estimate = requests.get(
f"{BASE_URL}/calculator/estimate",
headers=HEADERS,
params={
"lat": latitude,
"lng": longitude,
"bedrooms": bedrooms,
"baths": baths,
"guests": guests,
"currency": "usd",
},
).json()
# Metric cards
m1, m2, m3 = st.columns(3)
m1.metric("Annual Revenue", f"${estimate['revenue']:,.0f}")
m2.metric("Avg Daily Rate", f"${estimate['average_daily_rate']:,.0f}")
m3.metric("Occupancy", f"{estimate['occupancy']:.0%}")
# Percentile breakdown
st.subheader("Revenue Percentiles")
p = estimate["percentiles"]["revenue"]
p1, p2, p3, p4 = st.columns(4)
p1.metric("25th Percentile", f"${p['p25']:,.0f}")
p2.metric("50th (Median)", f"${p['p50']:,.0f}")
p3.metric("75th Percentile", f"${p['p75']:,.0f}")
p4.metric("90th Percentile", f"${p['p90']:,.0f}")
# Monthly revenue chart
months = ["Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"]
monthly = estimate["monthly_revenue_distributions"]
fig = px.bar(
x=months, y=monthly,
labels={"x": "Month", "y": "Revenue ($)"},
title="Monthly Revenue Distribution",
)
fig.update_traces(marker_color="#1976d2")
st.plotly_chart(fig, use_container_width=True)4
Pull 12 months of market metrics using POST /markets/metrics/all. Plot occupancy and ADR trends as line charts with p25/p50/p75 percentile bands for context.
python
with tab2:
st.header("Market Metrics")
response = requests.post(
f"{BASE_URL}/markets/metrics/all",
headers={**HEADERS, "Content-Type": "application/json"},
json={
"market": {
"country": market["country"],
"region": market["region"],
"locality": market["locality"],
},
"num_months": 12,
"currency": "usd",
},
)
metrics = response.json()
months_data = metrics.get("months", [])
if months_data:
df = pd.DataFrame(months_data)
# Occupancy trend with percentile bands
fig_occ = go.Figure()
fig_occ.add_trace(go.Scatter(
x=df["month"], y=df["occupancy_p75"],
fill=None, mode="lines", name="75th %ile",
line=dict(color="rgba(25,118,210,0.2)"),
))
fig_occ.add_trace(go.Scatter(
x=df["month"], y=df["occupancy_p25"],
fill="tonexty", mode="lines", name="25th %ile",
line=dict(color="rgba(25,118,210,0.2)"),
fillcolor="rgba(25,118,210,0.1)",
))
fig_occ.add_trace(go.Scatter(
x=df["month"], y=df["occupancy_p50"],
mode="lines+markers", name="Median",
line=dict(color="#1976d2", width=3),
))
fig_occ.update_layout(
title="Occupancy Rate Trend (12 Months)",
yaxis_tickformat=".0%",
)
st.plotly_chart(fig_occ, use_container_width=True)
# ADR trend
fig_adr = go.Figure()
fig_adr.add_trace(go.Scatter(
x=df["month"], y=df["average_daily_rate_p75"],
fill=None, mode="lines", name="75th %ile",
line=dict(color="rgba(76,175,80,0.2)"),
))
fig_adr.add_trace(go.Scatter(
x=df["month"], y=df["average_daily_rate_p25"],
fill="tonexty", mode="lines", name="25th %ile",
line=dict(color="rgba(76,175,80,0.2)"),
fillcolor="rgba(76,175,80,0.1)",
))
fig_adr.add_trace(go.Scatter(
x=df["month"], y=df["average_daily_rate_p50"],
mode="lines+markers", name="Median",
line=dict(color="#4caf50", width=3),
))
fig_adr.update_layout(
title="Average Daily Rate Trend (12 Months)",
yaxis_tickprefix="$",
)
st.plotly_chart(fig_adr, use_container_width=True)5
Fetch the 25 most comparable listings using GET /listings/comparables. Display them in an interactive table with sorting and filtering, plus summary statistics.
python
with tab3:
st.header("Comparable Properties")
if latitude and longitude:
comps_response = requests.get(
f"{BASE_URL}/listings/comparables",
headers=HEADERS,
params={
"latitude": latitude,
"longitude": longitude,
"bedrooms": bedrooms,
"baths": baths,
"guests": guests,
"currency": "usd",
},
)
comps = comps_response.json().get("comparable_listings", [])
if comps:
df_comps = pd.DataFrame(comps)
# Select and rename columns for display
display_cols = {
"listing_id": "Listing ID",
"bedrooms": "Beds",
"revenue": "Revenue",
"average_daily_rate": "ADR",
"occupancy": "Occupancy",
"rating_overall": "Rating",
"superhost": "Superhost",
}
df_display = df_comps[
[c for c in display_cols if c in df_comps.columns]
].rename(columns=display_cols)
# Format columns
if "Revenue" in df_display.columns:
df_display["Revenue"] = df_display["Revenue"].apply(
lambda x: f"${x:,.0f}"
)
if "ADR" in df_display.columns:
df_display["ADR"] = df_display["ADR"].apply(
lambda x: f"${x:,.0f}"
)
if "Occupancy" in df_display.columns:
df_display["Occupancy"] = df_display["Occupancy"].apply(
lambda x: f"{x:.0%}"
)
st.dataframe(
df_display,
use_container_width=True,
hide_index=True,
)
# Summary stats
st.subheader("Comp Set Summary")
s1, s2, s3 = st.columns(3)
s1.metric(
"Avg Revenue",
f"${df_comps['revenue'].mean():,.0f}"
)
s2.metric(
"Avg ADR",
f"${df_comps['average_daily_rate'].mean():,.0f}"
)
s3.metric(
"Avg Occupancy",
f"{df_comps['occupancy'].mean():.0%}"
)
else:
st.warning("No comparable listings found for this location.")
else:
st.info(
"Enter coordinates in the Revenue Estimator tab first."
)6
Add a full ROI calculator with inputs for purchase price, down payment, interest rate, and annual expenses. Calculate monthly mortgage (P&I), NOI, cash flow, cash-on-cash return, and cap rate.
python
# ROI Calculator Section (add below tabs)
st.divider()
st.header("ROI Calculator")
r1, r2 = st.columns(2)
with r1:
purchase_price = st.number_input(
"Purchase Price ($)", value=500000, step=10000
)
down_payment_pct = st.number_input(
"Down Payment (%)", value=25.0, step=1.0
) / 100
interest_rate = st.number_input(
"Interest Rate (%)", value=7.0, step=0.25
) / 100
loan_term_years = st.number_input(
"Loan Term (years)", value=30, step=5
)
with r2:
annual_revenue = st.number_input(
"Annual Revenue ($)",
value=estimate.get("revenue", 52000) if "estimate" in dir() else 52000,
step=1000,
)
annual_expenses = st.number_input(
"Annual Expenses ($)", value=18000, step=1000
)
st.caption(
"Include: property tax, insurance, maintenance, "
"utilities, supplies, management fees, HOA"
)
# Calculations
down_payment = purchase_price * down_payment_pct
loan_amount = purchase_price - down_payment
monthly_rate = interest_rate / 12
num_payments = loan_term_years * 12
if monthly_rate > 0:
monthly_mortgage = loan_amount * (
monthly_rate * (1 + monthly_rate) ** num_payments
) / ((1 + monthly_rate) ** num_payments - 1)
else:
monthly_mortgage = loan_amount / num_payments
annual_debt_service = monthly_mortgage * 12
noi = annual_revenue - annual_expenses
cash_flow = noi - annual_debt_service
cash_on_cash = (cash_flow / down_payment * 100) if down_payment > 0 else 0
cap_rate = (noi / purchase_price * 100) if purchase_price > 0 else 0
# Display ROI metrics
st.subheader("Investment Returns")
c1, c2, c3, c4 = st.columns(4)
c1.metric("Monthly Mortgage", f"${monthly_mortgage:,.0f}")
c2.metric("Annual Cash Flow", f"${cash_flow:,.0f}")
c3.metric("Cash-on-Cash Return", f"{cash_on_cash:.1f}%")
c4.metric("Cap Rate", f"{cap_rate:.1f}%")
# Detailed breakdown
with st.expander("Detailed Breakdown"):
st.write(f"**Down Payment:** ${down_payment:,.0f}")
st.write(f"**Loan Amount:** ${loan_amount:,.0f}")
st.write(f"**Annual Debt Service:** ${annual_debt_service:,.0f}")
st.write(f"**Net Operating Income:** ${noi:,.0f}")
st.write(f"**Annual Cash Flow:** ${cash_flow:,.0f}")7
Push your code to GitHub and deploy to Streamlit Cloud for free. Use Streamlit Secrets to keep your API key secure. Your dashboard will be accessible at a public URL you can share with clients or team members.
python
# requirements.txt streamlit>=1.30.0 requests>=2.31.0 pandas>=2.1.0 plotly>=5.18.0 # Deploy to Streamlit Cloud: # # 1. Push your code to a GitHub repository: # git init # git add app.py requirements.txt # git commit -m "STR Investment Dashboard" # git remote add origin https://github.com/you/str-dashboard.git # git push -u origin main # # 2. Go to https://share.streamlit.io # # 3. Click "New app" and connect your GitHub repo # # 4. Set the main file path to "app.py" # # 5. Add your API key as a secret: # In the Streamlit Cloud dashboard, go to Settings > Secrets # Add: API_KEY = "your_airroi_api_key" # # 6. In your app.py, replace the API_KEY line with: # API_KEY = st.secrets["API_KEY"] # # 7. Click "Deploy" — your dashboard is live! # To run locally: # streamlit run app.py
Keep exploring the AirROI API with these related tutorials.
Basic Python knowledge is helpful, but Streamlit is designed to be beginner-friendly. If you can write simple Python scripts, you can build this dashboard. The tutorial provides all the code you need — just copy, paste, and customize.
Streamlit Cloud offers free hosting for public apps. The AirROI API costs $0.01-$0.10 per call depending on the endpoint. A typical dashboard session makes 3-5 API calls, so daily use costs a few cents. Even heavy use rarely exceeds $50-100/month.
Absolutely. Streamlit makes it easy to add new tabs, charts, and data sources. You could add future pacing data, seasonality analysis, historical trends, or even a listing search feature using additional AirROI API endpoints.
Yes. Streamlit dashboards are interactive web apps that you can share via URL. You can deploy a private version for your team or create client-specific dashboards by customizing the market and property parameters.
You can build similar dashboards with Dash (by Plotly), Flask + Chart.js, React + Recharts, or Jupyter notebooks with ipywidgets. Streamlit is recommended because it requires the least code and has the fastest setup time.
Use Streamlit Secrets management. Store your API key in the Streamlit Cloud dashboard under Settings > Secrets, then access it with st.secrets['API_KEY'] in your code. Never hardcode API keys in source files pushed to public repositories.
Stay ahead of the curve
Join our newsletter for exclusive insights and updates. No spam ever.