Calculating Gross-Up Adjustments for Variable Occupancy
Turning a month-by-month occupancy log into a single gross-up adjustment means deriving one time-weighted occupancy figure and then scaling only the variable slice of each CAM pool against a lease target. This page is the focused calculation walkthrough for the gross-up normalization logic stage: where the parent guide explains why occupancy-elastic costs are normalized and where the split between fixed and variable dollars comes from, here we take a concrete reconciliation period at one building, compute the actual occupancy the denominator needs, derive the factor, apply it to the variable component alone, recombine, and quantize the result to a payable figure. The worked property is Halstead Point, a 240,000-square-foot suburban office asset whose occupancy dipped partway through the year, and every number below is reproducible from the code.
Context & When to Use This Approach
Use this procedure whenever a lease carries a gross-up clause and the building did not hold a single steady occupancy across the reconciliation window. The parent stage frames gross-up as a multiplication against actual occupancy, but in practice actual occupancy is rarely a clean figure someone hands you. A suite goes dark in April, a new tenant takes half a floor in September, and the year ends at a physical occupancy that describes December but not the twelve months the expenses were spent over. Grossing up against the December snapshot would over-correct a pool whose costs were incurred while the building was fuller than it finished. So the real work of a per-period adjustment is two-part: first resolve a defensible time-weighted occupancy that reflects how full the building actually was while the money was being spent, then apply the factor to the variable slice only.
The difference between a snapshot and a time-weighted figure is not academic, and Halstead Point makes it concrete. The building ended December at 78%, but it spent the first half of the year at 86%, so the utilities and janitorial invoices booked in the spring were incurred while the building was materially fuller than its closing occupancy suggests. Grossing the whole year’s variable spend up from the 78% closing number would treat every dollar as if it had been spent in the emptiest months and would over-recover; grossing up from the time-weighted 82% recovers the vacancy the tenants genuinely did not cause without inflating the correction. The choice of denominator is therefore a defensibility decision as much as an arithmetic one, and it is the first thing a tenant’s auditor will test.
This calculation sits immediately before distribution. The grossed figure it produces becomes the recoverable amount that the pro-rata allocation algorithm divides across the tenant roster, which is exactly why the occupancy basis used here must match the rentable-area basis allocation uses downstream — if the two stages disagree about how large the building is, the tie-out at year end will not close. The approach assumes the pool has already arrived split into a fixed component and a variable component; deciding which dollars belong on each side is the concern of the parent guide, not this walkthrough. What this page owns is the arithmetic from a monthly occupancy log to a single quantized grossed pool, and it treats that arithmetic as reproducible rather than approximate: the same log and the same lease target must always return the same grossed figure to the cent, because a reconciliation a tenant can rerun and match is the only kind that survives a dispute.
Step-by-Step Implementation
The worked pool for Halstead Point’s 2025 reconciliation carries $268,400.00 of variable spend — common-area utilities and janitorial — and $143,600.00 of fixed spend — real estate taxes and structural insurance. The lease normalizes to a 95% occupancy target. The building ran at 86% for the first half of the year, then a 19,200-square-foot suite vacated at mid-year and it finished the second half at 78%.
Step 1 — Split the pool into fixed and variable components
The data contract keeps the fixed and variable dollars as separate fields rather than a single total with a flag, because the split is the auditable artifact a reviewer inspects. Every monetary field and every ratio is a Decimal; no money ever touches a binary float.
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
CENTS = Decimal("0.01")
RATIO = Decimal("0.000001") # occupancy carried to six decimal places
@dataclass(frozen=True)
class VariablePool:
"""One recoverable pool at Halstead Point split by occupancy elasticity."""
pool_id: str
fixed_component: Decimal # taxes, structural insurance — never scaled
variable_component: Decimal # common-area utilities, janitorial — elastic
occupancy_target: Decimal # lease gross-up target, e.g. Decimal("0.95")
@dataclass(frozen=True)
class OccupancyLog:
"""Month-by-month occupied rentable area for the reconciliation window."""
rentable_rsf: Decimal
occupied_rsf: tuple[Decimal, ...] # one entry per month in the period
Step 2 — Derive time-weighted actual occupancy
Actual occupancy is the mean of each month’s occupied fraction, so a suite that sits empty for six months lowers the figure in proportion to the months it was dark rather than only to its footprint. With months in the window, the occupancy the denominator uses is:
def time_weighted_occupancy(log: OccupancyLog) -> Decimal:
"""Average the monthly occupied fraction across the reconciliation window.
Each month contributes occupied_rsf / rentable_rsf; the mean of those
fractions is the occupancy the gross-up denominator uses, so a suite that
goes dark mid-year pulls the figure down by the months it sat empty, not by
the snapshot occupancy on the last day of the period.
"""
months = Decimal(len(log.occupied_rsf))
occupied_total = sum(log.occupied_rsf, Decimal("0"))
denominator = log.rentable_rsf * months
return (occupied_total / denominator).quantize(RATIO, ROUND_HALF_UP)
For Halstead Point the six full months at 206,400 occupied square feet and six at 187,200 sum to 2,361,600 occupied-month-feet against a denominator of 240,000 × 12 = 2,880,000, giving a time-weighted occupancy of exactly 0.820000 — well below the 86% the building finished its strongest month at, and the figure the gross-up must correct toward the target.
Step 3 — Compute the gross-up factor
The factor is the ratio of the lease target to the occupancy the building actually achieved:
At 82% actual against a 95% target the factor is 0.95 divided by 0.82, roughly 1.158537. The further the building fell below target, the larger the correction; a building that met or exceeded the target produces a factor of one and passes through unchanged, because is a one-directional adjustment that never grosses a full building down below the cost it actually incurred.
Step 4 — Scale only the variable component
Only the occupancy-elastic dollars are multiplied. Utilities and janitorial genuinely understate what a full building would have spent, so scaling them reconstructs the full-occupancy figure:
Step 5 — Recombine fixed and grossed variable
The untouched fixed component is added back to the scaled variable component to form the pool that leaves the stage. Real estate taxes and structural insurance never move with occupancy, so applying the factor to them would invent recovery the landlord never funded.
Step 6 — Quantize to cents and record the audit trail
Quantization happens once per money value, at the point each figure becomes a payable amount, using an explicit rounding mode. Steps 3 through 6 live in one small function:
@dataclass(frozen=True)
class GrossUpAdjustment:
"""The grossed result for one pool, ready to hand to allocation."""
pool_id: str
factor: Decimal
variable_grossed: Decimal
grossed_pool: Decimal
uplift: Decimal
def adjust_for_occupancy(
pool: VariablePool, actual_occupancy: Decimal
) -> GrossUpAdjustment:
"""Scale only the variable component up to the lease's occupancy target.
Gross-up normalizes upward only: at or above the target the factor is one and
the pool passes through, so a full building is never grossed below cost. The
fixed component is quantized and added, never multiplied.
"""
if actual_occupancy >= pool.occupancy_target:
factor = Decimal("1")
else:
factor = pool.occupancy_target / actual_occupancy
# Scale the variable slice, then quantize once — the point it becomes money.
variable_grossed = (pool.variable_component * factor).quantize(
CENTS, ROUND_HALF_UP
)
fixed = pool.fixed_component.quantize(CENTS, ROUND_HALF_UP)
grossed_pool = fixed + variable_grossed
uplift = variable_grossed - pool.variable_component.quantize(
CENTS, ROUND_HALF_UP
)
return GrossUpAdjustment(
pool_id=pool.pool_id,
factor=factor,
variable_grossed=variable_grossed,
grossed_pool=grossed_pool,
uplift=uplift,
)
Running the Halstead Point pool through these two functions produces a variable_grossed of Decimal("310951.22"), a grossed_pool of Decimal("454551.22"), and an uplift of Decimal("42551.22"). That uplift is the cost the sitting tenants collectively carry that they would otherwise have escaped purely because a neighboring suite went dark at mid-year. The fixed component leaves the stage at exactly $143,600.00, unchanged, and the grossed pool of $454,551.22 is the number allocation divides across the roster.
It is worth tracing what that uplift means for a single tenant, because that is the figure a dispute is argued over. A tenant holding 24,000 square feet in a fully occupied 240,000-square-foot building would carry a 10% pro-rata share, so its slice of the grossed variable pool is roughly $31,095.12 against $26,840.00 of un-grossed variable spend — a difference of about $4,255.12 for the year, or a little over $350 a month. That is not a penalty and it is not a markup; it is the tenant’s contribution to the occupancy-elastic cost the building would have run had the vacant suite been leased, which is precisely the bargain the gross-up clause struck. Being able to derive that per-tenant number from the same log and the same factor, and to show the tenant the monthly occupancy series it rests on, is what turns the adjustment from an assertion into an auditable calculation.
Gotchas & Known Limitations
- Month weighting is not the only weighting. Averaging twelve monthly fractions equally treats every month as the same length. If the lease specifies day-weighting, or a tenant took occupancy on the fifteenth, weight each month by its days rather than counting it whole; a mid-month move-in booked as a full occupied month overstates occupancy and shrinks the correction.
- The occupancy denominator basis must match allocation’s rentable-area basis. If this step measures the building at 240,000 square feet but the pro-rata allocation algorithm sums tenant areas to a different total, the grossed pool and the shares computed from it will not reconcile at year end.
- Physical occupancy and economic occupancy are different questions. A suite under a signed lease but not yet built out may be economically occupied while physically empty; decide per lease which one the gross-up denominator counts, and record the choice, because reviewers will ask.
- Do not round the factor before you multiply. Quantizing the factor to a few places and then scaling introduces drift that compounds across a large pool. Keep the factor at full
Decimalprecision and quantize only the resulting money. - A very low occupancy produces a very large factor. This walkthrough applies no floor or ceiling, so a building that collapsed to 30% would multiply the variable share aggressively. Bounding that behavior with a contractual floor and cap is the subject of handling gross-up caps and occupancy floors, and a production engine should apply those bounds before the figure reaches a tenant statement.
- A fixed line tagged variable will be grossed up silently. Nothing in the arithmetic flags a misclassified tax pool; only a check of the fixed/variable split against the lease catches it, which is why the split stays explicit in the contract.
Verification
Correctness here is proved arithmetically, not by inspection, because these figures become tenant charges. The load-bearing assertions are that the derived occupancy matches an independent recomputation, that the variable slice scales to the exact target figure to the cent, and that the fixed slice leaves the stage identical to what it entered as — the proof that no fixed dollar was ever scaled.
def test_halstead_point_variable_pool() -> None:
"""The 2025 utilities-and-janitorial pool grosses to the cent."""
log = OccupancyLog(
rentable_rsf=Decimal("240000"),
occupied_rsf=(
# Jan–Jun: an 86% building before a 19,200 sf suite went dark.
*([Decimal("206400")] * 6),
# Jul–Dec: 78% after the mid-year vacancy.
*([Decimal("187200")] * 6),
),
)
occ = time_weighted_occupancy(log)
assert occ == Decimal("0.820000")
pool = VariablePool(
pool_id="halstead-cam-variable-2025",
fixed_component=Decimal("143600.00"),
variable_component=Decimal("268400.00"),
occupancy_target=Decimal("0.95"),
)
adj = adjust_for_occupancy(pool, occ)
assert adj.variable_grossed == Decimal("310951.22")
assert adj.grossed_pool == Decimal("454551.22")
assert adj.uplift == Decimal("42551.22")
# The fixed slice equals its input to the cent — nothing was scaled.
assert adj.grossed_pool - adj.variable_grossed == Decimal("143600.00")
def test_full_building_passes_through() -> None:
"""At or above target the factor is one and the pool is unchanged."""
pool = VariablePool(
pool_id="halstead-full-2025",
fixed_component=Decimal("143600.00"),
variable_component=Decimal("268400.00"),
occupancy_target=Decimal("0.95"),
)
adj = adjust_for_occupancy(pool, Decimal("0.97"))
assert adj.factor == Decimal("1")
assert adj.grossed_pool == Decimal("412000.00")
assert adj.uplift == Decimal("0.00")
Beyond the per-pool checks, run a period-level tie-out: the sum of every grossed pool’s fixed component must equal the sum of the input fixed components exactly, and the total uplift across all pools must reconcile against an independent recomputation of variable spend times the factor, so a silent factor error in one pool cannot hide inside a portfolio total. A property worth asserting across a range of occupancies is monotonicity — for any actual occupancy below the target, a lower occupancy must never produce a smaller grossed pool.
Sweeping the same $268,400.00 variable pool across a band of time-weighted occupancies makes the shape of the adjustment visible and gives reviewers a curve to sanity-check a single period against. At 90% the factor is about 1.0556 and the grossed variable lands near $283,311.11; at the actual 82% it is 1.1585 for $310,951.22; and at a hypothetical 70% the factor climbs to about 1.3571 for $364,257.14. The correction accelerates as occupancy falls — halving the gap to the target does not halve the uplift — which is exactly the behavior a floor is meant to arrest and the reason the sweep is a better review artifact than any single grossed number in isolation.
This adjustment is the numeric heart of the gross-up normalization logic stage; once the factor is bounded by a floor and a ceiling in handling gross-up caps and occupancy floors, the grossed pool is safe to hand to the pro-rata allocation algorithm for distribution across the roster.
Related
- Gross-Up Normalization Logic — the parent stage: why occupancy-elastic costs are normalized and how the fixed/variable split that feeds this calculation is produced.
- Handling Gross-Up Caps and Occupancy Floors — bounding the factor so a thinly let building cannot run the adjustment away.
- Implementing Pro-Rata Allocation Algorithms — the distribution stage that divides the grossed pool this page produces across the tenant roster.