Handling Base-Year Gross-Up in CAM Recoveries
A base year captured while a building sat two-thirds leased records only two-thirds of its variable operating cost, and storing that raw figure as a tenant’s expense floor quietly bills them for every dollar of the eventual lease-up. This page is the gross-up recipe inside modeling expense stops and base-year clauses, and it deals with one specific operation: scaling the base-year pool up to a stated occupancy before that pool is turned into a stored stop. That is a different job from grossing up a current reconciliation year, and conflating the two is where recoveries go wrong. When a landlord signs a base-year lease during initial lease-up, the base year is deliberately a snapshot of a half-empty building. Left untouched, its low variable costs become an artificially low floor, and as the building fills, the tenant reimburses the cost of occupancy they never caused. Grossing the base year up corrects the floor once, at storage time, so every subsequent year-end true-up measures genuine cost growth rather than the building’s own lease-up curve.
Context & When to Use This Approach
Apply a base-year gross-up whenever a lease sets its recovery floor from a stated base year and the building was not stabilized during that year. The trigger is a combination of two lease facts that live in the lease abstraction database: a base_year recovery type, and a gross-up provision naming a target occupancy — commonly 95 percent, sometimes “100 percent occupied and operating.” When both are present, the raw base-year pool is never the floor; the grossed pool is.
The concrete situations that make this non-optional:
- Initial lease-up base years. A newly delivered asset signs its first tenants at 60–80 percent occupancy. The base year those leases reference reflects that occupancy, so its janitorial, utility, water, and trash costs are understated relative to a full building. Every such lease needs its base year grossed to the target before the floor is stored.
- A gross-up clause that names the variable set. Sophisticated leases state that “variable” or “occupancy-sensitive” expenses are grossed up while fixed expenses are not. That clause dictates which lines scale — the classification is a lease term, not an accounting preference.
- Portfolio comparability. When a landlord holds base-year leases across buildings at different occupancies, only grossed base years are comparable. A stored floor that still carries each building’s raw occupancy is not a stable basis for any downstream reporting.
Actual occupancy is itself a measured quantity, and the measurement has to be defensible. Whether occupancy is expressed as leased rentable area over total rentable area, or as physically occupied area, the areas underneath it should be drawn from a consistent measurement standard such as the BOMA measurement standards; a base year grossed against a rentable-area figure that was later remeasured is a floor built on shifting ground. The occupancy fraction that feeds the factor is therefore not a casual input — it is a stored, sourced number that the reconciliation must be able to reproduce years later when a tenant audits the floor.
This page is deliberately narrow. It grosses the base year — the historical pool that becomes the floor — exactly once, when the stop is first derived. Grossing up each current reconciliation year to its own occupancy is a separate, recurring operation handled by the portfolio-wide gross-up normalization logic; the mechanics of scaling a single year to occupancy are detailed in calculating gross-up adjustments for variable occupancy. Both operations use the same arithmetic, but they act on different pools and run on different schedules. Keeping them separate in code is what prevents a base year from being grossed twice, or a current year from silently inheriting the base year’s occupancy.
The multiplier itself is the ratio of the target occupancy to the base year’s actual occupancy:
and it is applied to the variable component of the base-year pool alone, leaving the fixed component untouched:
Because for a lease-up base year, the factor is at least one and the grossed pool is at least the raw pool — the floor moves up, never down, which is what protects the tenant from paying for the building filling in.
Step-by-Step Implementation
The pipeline below takes a classified base-year pool and produces a stored stop, then applies that stop to a current-year share. Every monetary value stays in decimal.Decimal, never float, because a base-year gross-up multiplies a large pool by a repeating ratio and then differences the result against a nearly equal current share — exactly the arithmetic where binary rounding surfaces as a disputable cent on the tenant statement. The Python decimal documentation covers the deterministic behavior this depends on.
Step 1 — Identify the variable component of the base-year pool. Gross-up scales only occupancy-sensitive lines. Fixed lines — property taxes, insurance, structural reserves — do not change when the building fills, so they must be excluded from the multiplier or the floor is overstated. Tag every base-year line by variability, sourced from the lease’s gross-up clause and the expense categorization, then sum the two components separately.
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from enum import Enum
from pydantic import BaseModel, Field, model_validator
CENTS = Decimal("0.01")
RATIO = Decimal("0.00000001")
class Variability(str, Enum):
"""Whether a base-year line scales with building occupancy."""
FIXED = "fixed" # property tax, insurance, structural reserves
VARIABLE = "variable" # janitorial, utilities, water, trash, elevator
class BaseYearLine(BaseModel):
"""One classified line of the base-year operating pool."""
gl_code: str
amount: Decimal = Field(gt=0)
variability: Variability
class BaseYearPool(BaseModel):
"""The base-year pool plus the occupancy facts needed to gross it up.
`occupancy_actual` is the average occupancy during the base year;
`occupancy_target` is the gross-up target named in the lease. Both are
fractions in (0, 1]. The target may not sit below the actual occupancy,
because a base-year gross-up only ever raises the floor.
"""
lease_id: str
base_year: int
lines: list[BaseYearLine]
occupancy_actual: Decimal = Field(gt=0, le=Decimal("1"))
occupancy_target: Decimal = Field(gt=0, le=Decimal("1"))
@model_validator(mode="after")
def _target_not_below_actual(self) -> "BaseYearPool":
if self.occupancy_target < self.occupancy_actual:
raise ValueError("gross-up target below actual occupancy would lower the floor")
return self
def fixed_total(self) -> Decimal:
"""Sum of lines that do not move with occupancy."""
return sum((ln.amount for ln in self.lines
if ln.variability is Variability.FIXED), Decimal("0"))
def variable_total(self) -> Decimal:
"""Sum of occupancy-sensitive lines — the only lines that scale."""
return sum((ln.amount for ln in self.lines
if ln.variability is Variability.VARIABLE), Decimal("0"))
Step 2 — Gross the base year up to the occupancy target. Compute the factor as target over actual occupancy and apply it to the variable total only, then add back the untouched fixed total. Keep the factor un-quantized inside the money calculation so the pool is rounded exactly once, at the end; expose a separately quantized factor purely for display on the reconciliation worksheet.
def gross_up_factor(pool: BaseYearPool) -> Decimal:
"""occ_target / occ_actual, rounded for display only (>= 1 for a lease-up)."""
return (pool.occupancy_target / pool.occupancy_actual).quantize(
RATIO, rounding=ROUND_HALF_UP
)
def grossed_base_year(pool: BaseYearPool) -> Decimal:
"""E_fixed + E_var * (occ_target / occ_actual), quantized once to cents.
Only the variable component is multiplied; the fixed component is added
back unchanged, so the grossed pool reflects a normalized full-building
cost rather than the base year's actual partial occupancy.
"""
factor = pool.occupancy_target / pool.occupancy_actual # full precision
grossed = pool.fixed_total() + pool.variable_total() * factor
return grossed.quantize(CENTS, rounding=ROUND_HALF_UP)
Step 3 — Recompute the stored stop from the grossed base year. The floor a lease enforces is the tenant’s pro-rata share of the grossed pool, not of the raw pool. Convert once and store that single dollar figure alongside a flag recording that the base year was grossed, so the allocation engine never re-derives it and no downstream step can accidentally use the raw base year.
def base_year_stop(grossed_pool: Decimal,
tenant_rsf: Decimal,
building_rsf: Decimal) -> Decimal:
"""Stored floor = tenant pro-rata share of the grossed base-year pool."""
share = tenant_rsf / building_rsf # A_t / A_b, full precision
return (grossed_pool * share).quantize(CENTS, rounding=ROUND_HALF_UP)
Step 4 — Apply the grossed stop to the current-year recovery. With the floor now normalized, the recovery is the familiar clamp: the tenant owes the amount of their current-year share that rises above the stop, floored at zero. The current-year share arrives from the pro-rata allocation algorithm; this step only differences it against the grossed floor.
def recoverable_above_stop(current_share: Decimal, stop: Decimal) -> Decimal:
"""max(0, current_year_share - grossed base-year stop), to the cent."""
recoverable = current_share - stop
return max(recoverable, Decimal("0")).quantize(CENTS, rounding=ROUND_HALF_UP)
Step 5 — Verify parity between the base year and the current year. A grossed floor is only defensible if the current-year share it is differenced against sits on the same occupancy basis, the same variable-line definition, and the same pro-rata basis. If the current year was grossed to a different target, or a line counted as variable in one year and fixed in the other, the subtraction compares two incompatible quantities. Parity is the assertion that closes that gap.
def assert_parity(pool: BaseYearPool, current_year_target: Decimal) -> None:
"""The base year and current year must be normalized to the same target.
Grossing only isolates genuine cost growth when both sides share the
gross-up target; a mismatch means the floor and the current share are
measured on different occupancy bases and cannot be differenced.
"""
if pool.occupancy_target != current_year_target:
raise ValueError(
f"base year grossed to {pool.occupancy_target} but current year "
f"grossed to {current_year_target}: bases are not comparable"
)
# A base year already at target grosses up to itself: factor is exactly 1.
if pool.occupancy_actual == pool.occupancy_target:
assert grossed_base_year(pool) == (
pool.fixed_total() + pool.variable_total()
).quantize(CENTS, rounding=ROUND_HALF_UP)
Gotchas & Known Limitations
- Grossing the wrong pool. The single most damaging error is grossing up the current reconciliation year here, or grossing the base year a second time each year. This routine touches the base year once, at stop-derivation time. Current-year normalization belongs to gross-up normalization logic and runs on its own schedule; never let one code path do both.
- Scaling fixed expenses. Multiplying property taxes or insurance by the occupancy factor inflates the floor for costs that do not move with occupancy. The variability tag must come from the lease’s gross-up clause, not a guess — misclassifying one large fixed line as variable overstates the floor materially.
- Target below actual. A stabilized base year at 97 percent under a lease naming a 95 percent target must not be scaled down — a base-year gross-up only raises the floor. The
model_validatorrejects a target below actual occupancy so the floor is never lowered by the gross-up. - Double rounding the factor. Quantizing the factor and then multiplying reintroduces drift the exact ratio avoids. Multiply the variable total by the full-precision ratio and quantize the pool once; keep the rounded factor for display only.
- Occupancy definition drift. Actual occupancy can be measured by leased area, by physically occupied area, or as a rolling average across the base year. Whichever the lease specifies, the base year and every current year must use the identical definition, or the parity check in Step 5 passes on paper while the numbers compare different things.
- Ungrossed base year reaching recovery. A base-year provision fed to the recovery clamp without ever being grossed is a latent overcharge. Persist a grossed flag on the stored stop and refuse any base-year recovery whose flag is unset, so the omission fails loudly rather than mispricing quietly.
Verification
Because the grossed floor becomes a number on a tenant statement, its correctness is proven arithmetically. The worked example below uses a lease-up base year and shows both what grossing produces and what it prevents.
from decimal import Decimal
pool = BaseYearPool(
lease_id="L-118",
base_year=2021,
lines=[
BaseYearLine(gl_code="6100", amount=Decimal("250000.00"), variability=Variability.FIXED), # property tax
BaseYearLine(gl_code="6200", amount=Decimal("150000.00"), variability=Variability.FIXED), # insurance
BaseYearLine(gl_code="7100", amount=Decimal("360000.00"), variability=Variability.VARIABLE), # janitorial
BaseYearLine(gl_code="7200", amount=Decimal("240000.00"), variability=Variability.VARIABLE), # utilities/water/trash
],
occupancy_actual=Decimal("0.82"), # base year captured at 82% leased
occupancy_target=Decimal("0.95"), # lease grosses to 95%
)
# Fixed = 400,000; Variable = 600,000; raw pool = 1,000,000.
# factor = 0.95 / 0.82 = 1.15853658...; grossed variable = 695,121.95
# grossed pool = 400,000 + 695,121.95 = 1,095,121.95
assert pool.fixed_total() == Decimal("400000.00")
assert pool.variable_total() == Decimal("600000.00")
assert grossed_base_year(pool) == Decimal("1095121.95")
# Tenant: 15,000 RSF in a 200,000 RSF building -> pro-rata share 0.075.
stop_grossed = base_year_stop(Decimal("1095121.95"), Decimal("15000"), Decimal("200000"))
assert stop_grossed == Decimal("82134.15") # normalized floor
# What the raw base year would have stored instead:
stop_raw = base_year_stop(Decimal("1000000.00"), Decimal("15000"), Decimal("200000"))
assert stop_raw == Decimal("75000.00") # floor too low by 7,134.15
# Current year, building now full, recoverable pool 1,200,000 -> share 90,000.
current_share = Decimal("90000.00")
assert recoverable_above_stop(current_share, stop_grossed) == Decimal("7865.85")
# Against the ungrossed floor the tenant would owe nearly double:
assert recoverable_above_stop(current_share, stop_raw) == Decimal("15000.00")
assert_parity(pool, current_year_target=Decimal("0.95")) # bases match
Three properties are worth pinning as permanent tests. First, a base year already at the target occupancy must gross up to itself — set occupancy_actual equal to occupancy_target and confirm the grossed pool equals the raw pool, which catches any code that scales the fixed component by accident. Second, the fixed total must be invariant under gross-up: assert it is identical before and after, because only the variable component may move. Third, the grossed floor must be greater than or equal to the raw floor for any lease-up base year; a grossed floor that came out lower means the factor was inverted or the variable and fixed sets were swapped. The example above makes the stakes concrete: the grossed floor of $82,134.15 versus the raw floor of $75,000.00 is the difference between a tenant owing $7,865.85 and being overbilled at $15,000.00 for a full year — the cost of the building’s own lease-up, charged to a tenant who never caused it.
Once the base year is grossed and its stop stored, the day-to-day stop-and-clamp arithmetic is covered in the parent stage, modeling expense stops and base-year clauses, and the concrete implementation of computing and storing that floor is walked through in calculating base-year expense stops in Python.
Related
- Calculating base-year expense stops in Python — the stop-and-clamp implementation that consumes the grossed floor this page produces.
- Modeling expense stops and base-year clauses — the parent stage that stores each provision’s recovery type and canonical stop.
- Gross-up normalization logic — the portfolio-wide engine that grosses up each current reconciliation year, the recurring counterpart to this one-time base-year gross-up.