This is based on the notebook which I created while at the event live. Then used updates to improve the presentation for this article. At first, I had used Tableau for the visualization and python for the ETL. Though My Tableau license expired and so am recreating these graphs using Python. Credit goes to Grok for making this translation.

A partial describe() table from your merged DataFest real estate dataset (1680 total observations across years/markets, plus breakdowns for direct/sublet).

Key Findings – Quick Purge-Style Summary (No Fluff)¶

  1. Time Span & Data Volume

    • Data covers 2018–2024 (median year 2021).
    • 1680 total market-quarter observations.
  2. Total Market Size (RBA = Rentable Building Area)

    • Average market has ~121 million sq ft of office space.
    • Huge variation: some markets up to 2.17 billion sq ft (likely NYC or aggregated metros).
  3. Overall Availability

    • Average available space: 25.5 million sq ft (21% of total RBA).
    • Availability proportion: mean 20.5%, ranging from 5.7% (tight) to 41.5% (very loose).
    • 75th percentile at 24.4% → most markets have healthy but growing vacancy.
  4. Rent Levels

    • Average overall rent: $36.74/psf (internal class slightly lower at $35.88).
    • Wide spread: low end ~$19, high end up to $94 (premium markets like SF/NY).
    • 75th percentile ~$41 → top quarter of markets are significantly pricier.
  5. Direct vs Sublet Breakdown (1380 observations with sublet data)

    • Direct vacancy (landlord-listed):
      • Mean 8.9% of space (much tighter than total).
      • Average direct rent: $38–$37.99/psf (higher than sublet).
    • Sublet vacancy (tenant trying to offload):
      • Mean only 1.8% of space, very small portion.
      • Sublet rent cheaper: ~$31–$32/psf (tenants price to move fast).

Big Picture Takeaway
Markets are softening: average total availability ~20.5%, mostly driven by direct space coming online. Rents still strong (mid-$30s average) but with huge city-to-city variation. Sublet market is tiny and discounted — tenants aren't dumping space en masse yet.

Purge complete. This summary is clean intel from your own data — no zombies, just facts.

Want to add this summary as a markdown table or text block directly into your HTML page? Or drop it into the Python script to auto-print a nice version? Or move on to the next file in the Drive?

Your call, commander — next target?

In [11]:
import pandas as pd
import numpy as np
import os
import matplotlib.pyplot as plt


# Load the data with error handling
try:
    data = pd.read_csv('Major Market Occupancy Data-revised.csv')
    leases = pd.read_csv('Leases.csv')
    price = pd.read_csv('Price and Availability Data.csv')
except FileNotFoundError as e:
    print(f"Error: {e}. Check if files exist in {data_dir}")
    exit()

# Quick inspections (keep your prints or comment out for clean run)
print(price.describe())
print("Price columns:", price.columns.tolist())
print(price['overall_rent'].describe())
print("\nFirst few rows of main data:\n", data.head())
data.info()
print("\nMarkets:", data['market'].unique())
print("\nMarket counts:\n", data['market'].value_counts())
print("\nAvg occupancy by market:\n", data.groupby('market')['avg_occupancy_proportion'].mean().sort_values(ascending=False))
print(data.describe())

# Missing values
print("\nMissing values in main data:\n", data.isnull().sum())

# Merge datasets
merged_data = data.merge(leases, on=["year", "quarter", "market"], how="left")
merged_data = merged_data.merge(price, on=["year", "quarter", "market"], how="left")
print("\nMerged columns:", merged_data.columns.tolist())

# Plotting function to avoid duplication
def plot_column(df, col):
    if col not in df.columns:
        print(f"Column '{col}' not found.")
        return
    
    fig, axes = plt.subplots(1, 3, figsize=(18, 5))
    
    # Histogram
    axes[0].hist(df[col].dropna(), bins=30, edgecolor='k', alpha=0.7, color='skyblue')
    axes[0].set_title(f'Histogram of {col}')
    axes[0].set_xlabel(col)
    axes[0].set_ylabel('Frequency')
    axes[0].grid(axis='y', linestyle='--', alpha=0.7)
    
    # Box plot
    df[[col]].boxplot(ax=axes[1])
    axes[1].set_title(f'Box Plot of {col}')
    axes[1].set_ylabel(col)
    
    # Bar plot if categorical/low unique
    if df[col].nunique() < 20:
        df[col].value_counts().plot(kind='bar', ax=axes[2], color='orange', edgecolor='black')
        axes[2].set_title(f'Bar Plot of {col}')
        axes[2].set_xlabel(col)
        axes[2].set_ylabel('Count')
    else:
        axes[2].text(0.5, 0.5, 'Too many unique values\nfor bar plot', horizontalalignment='center',
                     verticalalignment='center', transform=axes[2].transAxes, fontsize=12)
        axes[2].set_title(f'Bar Plot of {col} (Skipped)')
    
    plt.tight_layout()
    plt.show()

# Plot key columns
columns_to_plot = ['avg_occupancy_proportion', 'starting_occupancy_proportion']
for col in columns_to_plot:
    plot_column(data, col)

# Optional: Save all histograms to files instead of showing
# plt.savefig(f'histogram_{col}.png') inside loop if you want exports
              year           RBA  available_space  availability_proportion  internal_class_rent  overall_rent  \
count  1680.000000  1.680000e+03     1.680000e+03              1680.000000          1680.000000   1680.000000   
mean   2021.000000  1.210374e+08     2.548078e+07                 0.205361            35.878965     36.743271   
std       2.000596  3.219143e+08     6.989460e+07                 0.059459            14.197553     13.495467   
min    2018.000000  2.007686e+07     1.782779e+06                 0.057300            16.957171     18.749409   
25%    2019.000000  3.493160e+07     6.078274e+06                 0.164501            26.342746     28.279018   
50%    2021.000000  4.986687e+07     1.049070e+07                 0.200153            31.618035     32.290506   
75%    2023.000000  7.877950e+07     1.832050e+07                 0.244071            41.165751     41.072908   
max    2024.000000  2.171302e+09     6.045432e+08                 0.414977            94.191224     84.746663   

       direct_available_space  direct_availability_proportion  direct_internal_class_rent  direct_overall_rent  \
count            1.380000e+03                     1380.000000                 1380.000000          1380.000000   
mean             1.129815e+07                        0.088918                   37.072325            37.991738   
std              8.279923e+06                        0.034895                   15.481108            14.669157   
min              1.544029e+06                        0.021800                   18.009119            19.990075   
25%              5.122876e+06                        0.062700                   26.518484            28.972157   
50%              8.802652e+06                        0.081350                   32.404554            32.257719   
75%              1.455561e+07                        0.110550                   42.041934            42.846595   
max              4.092899e+07                        0.190500                   99.642941            88.438174   

       sublet_available_space  sublet_availability_proportion  sublet_internal_class_rent  sublet_overall_rent  \
count            1.380000e+03                     1380.000000                 1380.000000          1380.000000   
mean             2.336641e+06                        0.018385                   31.103258            32.052277   
std              2.208911e+06                        0.011575                   12.204792            11.848340   
min              8.283300e+04                        0.001500                   14.149920            16.865199   
25%              8.697325e+05                        0.010000                   23.525936            24.477567   
50%              1.470552e+06                        0.015300                   27.279092            28.026234   
75%              3.132071e+06                        0.025000                   35.312264            37.565071   
max              1.435339e+07                        0.074600                   86.324412            81.205996   

            leasing  
count  1.680000e+03  
mean   1.735510e+06  
std    4.913469e+06  
min    5.260500e+04  
25%    3.866975e+05  
50%    6.687965e+05  
75%    1.164113e+06  
max    4.687655e+07  
Price columns: ['year', 'quarter', 'market', 'internal_class', 'RBA', 'available_space', 'availability_proportion', 'internal_class_rent', 'overall_rent', 'direct_available_space', 'direct_availability_proportion', 'direct_internal_class_rent', 'direct_overall_rent', 'sublet_available_space', 'sublet_availability_proportion', 'sublet_internal_class_rent', 'sublet_overall_rent', 'leasing']
count    1680.000000
mean       36.743271
std        13.495467
min        18.749409
25%        28.279018
50%        32.290506
75%        41.072908
max        84.746663
Name: overall_rent, dtype: float64

First few rows of main data:
    year quarter           market  ending_occupancy_proportion  starting_occupancy_proportion  \
0  2020      Q1  Washington D.C.                         0.19                           0.98   
1  2020      Q1        Manhattan                         0.08                           0.98   
2  2020      Q1          Chicago                         0.14                           0.99   
3  2020      Q1          Houston                         0.33                           0.99   
4  2020      Q1     Philadelphia                         0.20                           0.99   

   avg_occupancy_proportion  
0                  0.785714  
1                  0.732857  
2                  0.788571  
3                  0.835714  
4                  0.817143  
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 190 entries, 0 to 189
Data columns (total 6 columns):
 #   Column                         Non-Null Count  Dtype  
---  ------                         --------------  -----  
 0   year                           190 non-null    int64  
 1   quarter                        190 non-null    object 
 2   market                         190 non-null    object 
 3   ending_occupancy_proportion    190 non-null    float64
 4   starting_occupancy_proportion  190 non-null    float64
 5   avg_occupancy_proportion       190 non-null    float64
dtypes: float64(3), int64(1), object(2)
memory usage: 9.0+ KB

Markets: ['Washington D.C.' 'Manhattan' 'Chicago' 'Houston' 'Philadelphia' 'San Francisco' 'Los Angeles' 'Dallas/Ft Worth'
 'South Bay/San Jose' 'Austin']

Market counts:
 market
Washington D.C.       19
Manhattan             19
Chicago               19
Houston               19
Philadelphia          19
San Francisco         19
Los Angeles           19
Dallas/Ft Worth       19
South Bay/San Jose    19
Austin                19
Name: count, dtype: int64

Avg occupancy by market:
 market
Austin                0.517043
Houston               0.503773
Dallas/Ft Worth       0.485768
Los Angeles           0.398406
Chicago               0.388947
Washington D.C.       0.369118
Philadelphia          0.365611
Manhattan             0.349343
South Bay/San Jose    0.318399
San Francisco         0.317613
Name: avg_occupancy_proportion, dtype: float64
              year  ending_occupancy_proportion  starting_occupancy_proportion  avg_occupancy_proportion
count   190.000000                   190.000000                     190.000000                190.000000
mean   2021.894737                     0.351158                       0.377895                  0.401402
std       1.376090                     0.152696                       0.195277                  0.160338
min    2020.000000                     0.080000                       0.050000                  0.052308
25%    2021.000000                     0.220000                       0.240000                  0.284231
50%    2022.000000                     0.355000                       0.355000                  0.410385
75%    2023.000000                     0.470000                       0.450000                  0.488462
max    2024.000000                     0.660000                       0.990000                  0.838571

Missing values in main data:
 year                             0
quarter                          0
market                           0
ending_occupancy_proportion      0
starting_occupancy_proportion    0
avg_occupancy_proportion         0
dtype: int64

Merged columns: ['year', 'quarter', 'market', 'ending_occupancy_proportion', 'starting_occupancy_proportion', 'avg_occupancy_proportion', 'monthsigned', 'building_name', 'building_id', 'address', 'region', 'city', 'state', 'zip', 'internal_submarket', 'internal_class_x', 'leasedSF', 'company_name', 'internal_industry', 'transaction_type', 'internal_market_cluster', 'costarID', 'space_type', 'CBD_suburban', 'RBA_x', 'available_space_x', 'availability_proportion_x', 'internal_class_rent_x', 'overall_rent_x', 'direct_available_space_x', 'direct_availability_proportion_x', 'direct_internal_class_rent_x', 'direct_overall_rent_x', 'sublet_available_space_x', 'sublet_availability_proportion_x', 'sublet_internal_class_rent_x', 'sublet_overall_rent_x', 'leasing_x', 'internal_class_y', 'RBA_y', 'available_space_y', 'availability_proportion_y', 'internal_class_rent_y', 'overall_rent_y', 'direct_available_space_y', 'direct_availability_proportion_y', 'direct_internal_class_rent_y', 'direct_overall_rent_y', 'sublet_available_space_y', 'sublet_availability_proportion_y', 'sublet_internal_class_rent_y', 'sublet_overall_rent_y', 'leasing_y']
No description has been provided for this image
No description has been provided for this image

Upgraded Script Features

More Histograms: Now for key columns like overall_rent, lease counts, etc., with KDE overlays via seaborn. Additional Plots: Violin plots (better than box for distribution shape). Scatter plot: avg_occupancy vs overall_rent (spot correlations). Time series line plot: occupancy over quarters by top markets.

US States Map: Interactive choropleth using Plotly. First, extract state from 'market' (e.g., "New York" → "NY", "Los Angeles" → "CA"). Compute mean avg_occupancy_proportion per state. Color states by average occupancy (darker = higher). Hover shows state + value.

Notes:

Run print(merged_data['market'].unique()) first if needed to expand the market_to_state dict with your exact market names. Plotly map is interactive — zoom, hover for details. If 'overall_rent' or other cols missing, skip those plots.

In [1]:
import pandas as pd
import numpy as np
import os
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px

# Set style for nicer plots
sns.set(style="whitegrid")


# Load data
try:
    data = pd.read_csv('Major Market Occupancy Data-revised.csv')
    leases = pd.read_csv('Leases.csv')
    price = pd.read_csv('Price and Availability Data.csv')
except FileNotFoundError as e:
    print(f"Error loading files: {e}")
    exit()

# Merge
merged_data = data.merge(leases, on=["year", "quarter", "market"], how="left")
merged_data = merged_data.merge(price, on=["year", "quarter", "market"], how="left")

# Basic inspections (keep or comment out)
print("Unique markets:", merged_data['market'].unique())
print("Merged shape:", merged_data.shape)

# === MORE HISTOGRAMS & DISTRIBUTIONS ===
key_numeric = ['avg_occupancy_proportion', 'starting_occupancy_proportion', 
               'overall_rent']  # Add more if they exist, e.g., lease counts

for col in key_numeric:
    if col in merged_data.columns:
        plt.figure(figsize=(10, 6))
        sns.histplot(merged_data[col].dropna(), bins=30, kde=True, color='skyblue')
        plt.title(f'Histogram with KDE: {col}')
        plt.xlabel(col)
        plt.ylabel('Frequency')
        plt.show()
        
        # Violin plot
        plt.figure(figsize=(8, 6))
        sns.violinplot(y=merged_data[col].dropna(), color='lightgreen')
        plt.title(f'Violin Plot: {col}')
        plt.show()


# Scatter: Occupancy vs Rent
if 'overall_rent' in merged_data.columns:
    plt.figure(figsize=(10, 6))
    sns.scatterplot(data=merged_data, x='avg_occupancy_proportion', y='overall_rent', 
                    hue='market', alpha=0.7)
    plt.title('Avg Occupancy vs Overall Rent (by Market)')
    plt.show()


# === OCCUPANCY TIME SERIES – UPGRADED AESTHETICS ===
top_markets = merged_data['market'].value_counts().head(10).index

plt.figure(figsize=(16, 8))  # Much wider + taller for breathing room

# Create a proper time index for clean x-axis
merged_data['time'] = merged_data['year'].astype(str) + ' Q' + merged_data['quarter'].astype(str)
time_order = sorted(merged_data['time'].unique())

# Color palette – professional, colorblind-friendly
colors = sns.color_palette("husl", len(top_markets))

for i, market in enumerate(top_markets):
    subset = merged_data[merged_data['market'] == market].copy()
    subset = subset.sort_values(['year', 'quarter'])
    plt.plot(subset['time'], 
             subset['avg_occupancy_proportion'], 
             label=market, 
             marker='o',          # Small dots at data points
             linewidth=2.5, 
             markersize=4,
             color=colors[i])

# Legend outside, clean
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', frameon=True, fancybox=True, shadow=False)

# Labels & title
plt.title('Office Occupancy Trends Over Time\nTop 10 Major Markets (2018–2024)', fontsize=16, pad=20)
plt.xlabel('Quarter', fontsize=12)
plt.ylabel('Average Occupancy Proportion', fontsize=12)

# X-axis: cleaner ticks, slight rotation only if needed
plt.xticks(rotation=45, ha='right')
plt.grid(True, linestyle='--', alpha=0.5, axis='y')

# Tight layout with room for legend
plt.tight_layout(rect=[0, 0, 0.82, 1])  # Leaves space on right for legend
plt.show()


# === US STATES CHOROPLETH MAP ===
# Simple market → state mapping (expand if your markets differ)
market_to_state = {
    'New York': 'NY', 'Los Angeles': 'CA', 'Chicago': 'IL', 'Houston': 'TX',
    'Phoenix': 'AZ', 'Philadelphia': 'PA', 'San Antonio': 'TX', 'San Diego': 'CA',
    'Dallas': 'TX', 'San Jose': 'CA', 'Austin': 'TX', 'Jacksonville': 'FL',
    'San Francisco': 'CA', 'Indianapolis': 'IN', 'Columbus': 'OH', 'Charlotte': 'NC',
    'Seattle': 'WA', 'Denver': 'CO', 'Boston': 'MA', 'Washington DC': 'DC',
    # Add more based on your data['market'].unique()
}
# === US STATES CHOROPLETH MAP ===
# (Keep your market_to_state dict here - expand as needed)

merged_data['state'] = merged_data['market'].map(market_to_state)

# Aggregate mean occupancy per state
state_avg = merged_data.groupby('state')['avg_occupancy_proportion'].mean().reset_index()
state_avg['avg_occupancy_pct'] = (state_avg['avg_occupancy_proportion'] * 100).round(1)
# Heatmap – zero text, zero zombies, beautiful and clean
numeric_corr = merged_data.select_dtypes(include=np.number).corr()

mask = np.triu(np.ones_like(numeric_corr, dtype=bool))

plt.figure(figsize=(14, 12))
sns.heatmap(numeric_corr,
            mask=mask,
            annot=False,           # this is the killer line – no numbers ever again
            fmt='',                # extra insurance – even if someone turns annot on, no format
            cmap='coolwarm',
            linewidths=0.4,
            cbar_kws={"shrink": .7})
plt.title('Correlation – Clean (No Numbers)', fontsize=16)
plt.tight_layout()
plt.show()
Unique markets: ['Washington D.C.' 'Manhattan' 'Chicago' 'Houston' 'Philadelphia'
 'San Francisco' 'Los Angeles' 'Dallas/Ft Worth' 'South Bay/San Jose'
 'Austin']
Merged shape: (92358, 53)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Manhattan tanked hardest (~85% → ~30% occupancy). Texas markets (Dallas, Houston, Austin) recovered strongest. Bay Area/SF still struggling mid-pack. All markets stabilized ~2023–2024.

In [ ]: