๐Ÿšง This site is under construction. Content and features are being added. Stay tuned! ๐Ÿšง

Leasing Budget Automation

Python + SQL + Excel = Fully Integrated Rent Forecasts

๐Ÿ’ผ Executive Summary

This solution automates leasing budget forecasting across 400+ tenants across 100+ commercial properties, covering more than 700 active lease exposures. It bridges Yardi data queried by SQL, Excel inputs, and Python processing to generate a 5โ€“7 year outlook for base rent, vacancy, and GLA โ€” fully integrated into a dynamic Excel workbook.

  • Portfolio: Office, Retail, Industrial properties
  • Forecasting Horizon: 2025 to 2029
  • Tools: Excel, Python (pandas, xlwings), SQL (Athena), SharePoint, Task Scheduler
  • Output: Excel workbook with dynamic projections, rent roll validations, and summary reports

Fully integrated into a dynamic Excel workbook.

๐Ÿ“Œ Table of Contents

๐Ÿ“ˆ Business Impact

  • โœ… Replaced manual workbooks with automated Python logic
  • โšก โฑ๏ธ Reduced from 3 weeks to 10 minutes
  • ๐Ÿ” Created a single source of truth for all leasing assumptions
  • ๐Ÿงพ Enabled quick summary generation for budget sign-off

Simplified Leasing Flow

This diagram provides a high-level overview of the leasing process used to generate projections from multiple data sources.

Simplified Leasing Flow

๐Ÿ”„ This diagram outlines how leasing data flows from raw Yardi exports and team assumptions through Python logic into monthly rent projections, vacancy tracking, and Excel reports โ€” all fully automated.

Leasing Automation in Action

This animation walks through the 6-step leasing forecast pipeline โ€” from pulling in-place leases from Yardi to auto-generating Excel dashboards.

Animated Leasing Pipeline

โ‘  SQL pull โ†’ โ‘ก Leasing inputs โ†’ โ‘ข Python logic โ†’ โ‘ฃ Writeback โ†’ โ‘ค Power Query โ†’ โ‘ฅ Dashboard output

๐Ÿ“ From Input to Output

Below are examples of how leasing team assumptions entered in Excel (purple fields) directly impact summary reports. After updating assumptions, simply run the Python script and click Refresh All in Excel to instantly generate updated visuals:

๐Ÿ”ง Leasing Inputs (Editable Fields)

Leasing Input Tab

โœ๏ธ Leasing assumptions are entered here โ€” including lease start/end dates, rates, and incentives โ€” which directly drive downstream rent projections and KPIs.

Net Rent Summary

๐Ÿ“ˆ Yearly Net Rent Trend (2023โ€“2029)

7 Year Net Rent Trend

๐Ÿ“Œ These visuals help validate budget assumptions and explain key variances across time, asset class, and tenant level โ€” making sign-off faster and more accurate.

๐Ÿ“Š Leasing Pipeline Overview

Explore how data flows from our Yardi systems through Excel and Python automation pipelines.

๐Ÿ“ˆ Dataflow Architecture (Detail)

This visual summarizes the full-stack integration across systems:

Leasing Data Architecture

Technical Flow

  1. SQL Data Pull: Extracts 4 core tables from 1,000+ via Athena, including historical and future rent rolls
  2. Load to Excel: Results populate 7yrBaseRent, RentRoll, and Sqft tabs
  3. Read via xlwings: Python loads both Yardi data and leasing inputs from Inputs tab
  4. DataFrame Logic:
    • Combine Yardi leases + leasing team assumptions
    • Unnest each row into monthly charges (2023โ€“2029)
    • Inject free rent as negative entries
    • Amortize TI, LC, admin fees across term
    • Calculate NER at tenant and property level
  5. Vacancy Integration: Identify unoccupied months by comparing against a unit-level GLA baseline
  6. Output to Excel: Write to all_unnest, GLA_Month, and Summary Report
  7. Validation: Merge with rent roll to detect mismatch
  8. Backup: Archive Inputs and validation data daily via SharePoint

๐Ÿ” Full Walkthrough: From Raw Data to Instant Rent Forecasts

This section walks through how the leasing budget automation works behind the scenes โ€” from SQL to Excel to Python โ€” and shows real code examples for each part of the pipeline.

1. ๐Ÿ”„ SQL Data Extraction

-- extract 7 year in-place lease data
WITH dates AS (SELECT CAST('2023-1-1' AS DATE) AS effect_date),
aa AS (
  SELECT *, un.scode as unit_no, round((unit_sqft * yearlyrate / 12),2) AS monthamount
  FROM (SELECT
      p.scode as property_code,cr.hunit as unit_code,cr.dcontractarea as unit_sqft,t.slastname as tenant_name,
    t.scode as tenant_code, COALESCE(t.dtleasefrom, t.dtmovein) as lease_from, COALESCE(t.dtmoveout, t.dtleaseto) as lease_to,
    cr.dtfrom as charge_from, cr.dtto as charge_to, ct.sname as charge_code, round(cr.dmonthlyamount*12/NULLIF(cr.dcontractarea,0),2) as yearlyrate, cr.hamendment, cr.hunit as dummy,
    cr.dadminpercent/100 as admin_fee, CASE WHEN cr.hchargecode = 70 THEN 'gross' ELSE 'net' END AS net_gross, d.effect_date
    FROM yardi.camrule cr
    JOIN yardi.tenant t ON t.hmyperson = cr.htenant JOIN yardi.property p ON p.hmy = t.hproperty
    LEFT JOIN yardi.attributes a ON p.scode = a.scode AND a.subgroup1 = 'IPP Core'
    JOIN yardi.listprop l ON l.hproperty = t.hproperty LEFT JOIN yardi.unit un ON un.hmy = cr.hunit
    LEFT JOIN yardi.chargtyp ct ON cr.hchargecode = ct.hmy CROSS JOIN dates d
    ----- omitted codes -------

2. ๐Ÿ“ฅ Load Queried Data into Excel

  • 7yrBaseRent
  • RentRoll
  • Sqft
  • Inputs (for leasing assumptions)

3. ๐Ÿ“Š Read Excel via xlwings


def load_and_combine_tables(wb):
    df_base = pd.read_excel(wb.fullname, sheet_name='7yrBaseRent')
    df_input = pd.read_excel(wb.fullname, sheet_name='Inputs')
    return pd.concat([df_base, df_input], ignore_index=True)
        

4. ๐Ÿงฎ Unnest Charges Over Time


def unnest_charges(df, wb):
    df_expanded = []
    for _, row in df.iterrows():
        for month in pd.date_range(row['start_date'], row['end_date'], freq='MS'):
            rent = row['yearly_rate'] / 12
            if month in row['free_rent_months']:
                rent = 0
            df_expanded.append({**row, 'month': month, 'monthly_rent': rent})
    return pd.DataFrame(df_expanded)
        

5. ๐Ÿš Vacancy Detection


def integrate_vacancy(df, wb, all_months):
    df_sqft = pd.read_excel(wb.fullname, sheet_name='Sqft')
    df['is_vacant'] = ~df.apply(
        lambda row: (row['unit_no'], row['month']) in
        df_sqft[['unit_no', 'month']].itertuples(index=False),
        axis=1
    )
    return df
        

6. ๐Ÿ’พ Output to Excel


sheet = wb.sheets['all_unnest']
sheet.range("A2").value = df_final
        

7. ๐Ÿงพ Validation


def update_status_with_rent_roll_and_inputs(wb):
    rentroll = pd.read_excel(wb.fullname, sheet_name='RentRoll')
    status = df_final.merge(rentroll, on='unit_no', how='left', suffixes=('', '_actual'))
    status['discrepancy'] = status['monthly_rent'] != status['monthly_rent_actual']
    return status
        

8. ๐Ÿ” Backup and Refresh

Backups of inputs and summary reports are archived daily to SharePoint using Windows Task Scheduler.

๐Ÿงช Python Modules

๐Ÿ” 1. load_and_combine_tables(wb)

def load_and_combine_tables(wb):
  all_charges_sheet = wb.sheets["7yrBaseRent"]
  all_charges_range = all_charges_sheet.range("A1").expand()
  all_charges_df = all_charges_range.options(pd.DataFrame, header=1, index=False).value
  all_charges_df['Source'] = 'yardi'
  all_charges_df['unit_no'] = all_charges_df['unit_no'].apply(lambda x: "'" + str(x) if pd.notna(x) else x)
  all_charges_df = all_charges_df[all_charges_df['unit_no'].str.strip("'").notna() & (all_charges_df['unit_no'].str.strip("'") != "")]
  inputs_sheet = wb.sheets["Inputs"]
  inputs_range = inputs_sheet.range("S27:AL3000")
    ----- omitted codes -------

๐Ÿ“† 2. unnest_charges(df1, wb)

def unnest_charges(df1, wb):
  global start_date, end_date, all_months; df_final = df1.copy(); df_final['unit_no'] = df_final['unit_no'].astype(str)
  df_final = df_final[df_final['unit_no'].notna() & (df_final['unit_no'] != '')]
  for month_end in all_months: df_final[month_end.strftime('%Y-%m-%d')] = np.nan
  for index, row in df_final.iterrows():
    if pd.notna(row['yearlyrate']) and pd.notna(row['unit_sqft']):       charge_from = max(pd.Timestamp(row['charge_from']), start_date)
      charge_to = min(pd.Timestamp(row['charge_to']), end_date)
      for month in all_months:
        month_start = month.to_timestamp(how='start')
        month_end = month.to_timestamp(how='end')
    ----- omitted codes -------

๐Ÿข 3. integrate_vacancy(df_final, wb, all_months)

def integrate_vacancy(df_final, wb, all_months):
  sqft_sheet = wb.sheets['sqft']; sqft_range = sqft_sheet.range('A1').expand()
  sqft_df = sqft_range.options(pd.DataFrame, header=1, index=False).value; sqft_df['unit_no'] = "'" + sqft_df['unit_no'].astype(str)
  unit_entries = []
  for _, unit in sqft_df.iterrows(): property_code = unit['property_code']; unit_no = unit['unit_no']; unit_record = {'Source': 'vacancy', 'property_code': property_code, 'prop_name': None, 'unit_no': unit_no}; ----- omitted codes -------     ----- omitted codes -------

๐Ÿ“ 4. process_sqft_month(wb)

def process_sqft_month(wb):
  global start_date, end_date, all_months; all_months = pd.date_range(start_date, end_date, freq='ME').strftime('%Y-%m-%d')
  sqft_sheet = wb.sheets['sqft']; sqft_data_range = sqft_sheet.range('M1').expand('table')
  sqft_data_df = sqft_data_range.options(pd.DataFrame, header=1, index=False).value.dropna(subset=['start_date']).sort_values(by='start_date')
  data_rows = []   columns = ['property_code', 'unit_no', 'start_date'] + list(all_months)
  for _, group in sqft_data_df.groupby(['property_code', 'unit_no']):
    group = group.reset_index(drop=True)
    for idx, row in group.iterrows():
    ----- omitted codes -------

๐Ÿ“Š 5. update_status_with_rent_roll_and_inputs(wb)

def update_status_with_rent_roll_and_inputs(wb):
  # Compares forecast vs actual data for reporting and control validation
  pass # (Function logic omitted here โ€” add if available)
    ----- omitted codes -------

All Excel interaction handled via xlwings โ€” ensuring seamless link to open or saved workbooks.

๐Ÿ›  Challenges Solved

  • Duplicate assumptions: Detected overlap with activated leases โ†’ override logic added
  • Excel memory limits: Large formula-based workbook โ†’ replaced with Python calculations
  • Manual errors in rent roll match: Created full validation logic across merged keys

๐Ÿ” Reflections

  • This was not just a coding task โ€” it redefined how leasing forecasts are created and validated
  • Team buy-in increased as confidence in data grew
  • Demonstrated value of domain-specific automation in Finance