Advanced
30 min
Python

Build an STR Investment Dashboard

Create an interactive Streamlit dashboard with market search, revenue estimation, performance charts, comparable properties, and a full ROI calculator — all powered by the AirROI API.

1

Set Up Streamlit

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

Market Search Sidebar

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

Revenue Estimation Tab

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

Market Metrics Tab

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

Comp Set Table

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

ROI Calculator

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

Deploy to Streamlit Cloud

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

Continue Learning

Keep exploring the AirROI API with these related tutorials.

Frequently Asked Questions

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.

Ready to Build?

Get your API key and start making calls in minutes.
made with