Calculating CAM Pro-Rata Shares in Mixed-Use Buildings
A single-use office tower has one denominator: divide each tenant’s rentable square footage by the building total and the pro-rata allocation algorithm is essentially done. A mixed-use asset breaks that assumption on the first line item. The ground-floor retail concourse cleaning belongs only to the retail tenants; the shared parking structure spans retail and office but not the residential condos above; the office HVAC plant serves neither. Run one building-wide denominator across pools like these and you cross-subsidize — the office tenants quietly pay for concourse pressure-washing they never touch, and an auditor catches it before you do. This page is a focused implementation recipe, part of the pro-rata allocation stage, for the specific case where each expense pool has a different eligible population and every tenant’s share must be computed against the correct scoped denominator.
Context & When to Use This Approach
Reach for scoped, component-weighted allocation the moment a building’s expense pools no longer share one population of tenants. The concrete triggers on a mixed-use asset are consistent:
- Component-restricted pools. A pool such as “retail concourse janitorial” or “hotel porte-cochère lighting” is recoverable only from tenants in that component. Its denominator is the sum of that component’s rentable area, not the building’s.
- Overlapping shared pools. A parking deck or a shared loading dock serves two or three components but not all of them. Each pool carries its own explicit list of eligible components, and the same tenant appears in some denominators and not others.
- Tenant-level carve-outs inside a component. An anchor grocer negotiates out of common-area marketing, or a restaurant runs dedicated after-hours HVAC excluded from the shared pool. These removals belong in exclusion mapping for tenant-specific CAM and must shrink the denominator, not just the numerator.
- Vacancy and partial occupancy. Vacant suites cannot be silently left in the divisor — that undercharges everyone and leaves the pool under-recovered. Vacant space is either excluded (landlord absorbs it) or grossed up to a stated occupancy floor.
If your asset is genuinely single-use with one recoverable pool, this scoping machinery is overhead — the base pro-rata formula is enough. Reach for the pattern below only when pools and populations diverge. The lease terms that decide which component each tenant belongs to, and which pools they are eligible for, should already be structured in your lease abstraction database before any allocation runs; this recipe consumes that structured data rather than re-reading leases.
Step-by-Step Implementation
The pipeline below turns a set of scoped pools and a tenant roster into per-tenant, per-pool charges that sum back to each pool exactly. Every monetary value stays in decimal.Decimal — never float — because sub-cent binary drift compounds across dozens of pools and hundreds of tenants and breaks year-end tie-out. See the Python decimal documentation for the deterministic arithmetic this relies on.
Step 1 — Model pools and tenants with explicit eligibility. A pool names the set of components it may be recovered from; a tenant names its component, its rentable area, and whether it is currently occupied. Eligibility is data, not code branches — that is what keeps a mixed-use roster maintainable.
from __future__ import annotations
from dataclasses import dataclass, field
from decimal import Decimal
@dataclass(frozen=True)
class ExpensePool:
"""A recoverable CAM pool scoped to one or more building components."""
pool_id: str
amount: Decimal # total recoverable expense for the pool
eligible_components: frozenset[str] # e.g. {"retail"} or {"retail", "office"}
@dataclass(frozen=True)
class Tenant:
tenant_id: str
component: str # "retail" | "office" | "hotel" | ...
rentable_sf: Decimal
occupied: bool = True
excluded_pools: frozenset[str] = field(default_factory=frozenset)
Step 2 — Resolve rentable square footage through a fallback chain. Legacy mixed-use portfolios have gaps: a suite is mid-remeasurement, a certified survey is stale, a lease abstract disagrees with the property-management system. Never default a missing area to zero (that silently drops the tenant from every denominator) — walk an ordered chain and record which source answered so the choice is auditable.
from collections.abc import Mapping
from typing import Optional
# Ordered by trust: certified survey first, portfolio default last.
FALLBACK_ORDER = ("certified_survey", "lease_abstract", "pms_record", "portfolio_default")
def resolve_rsf(tenant_id: str,
sources: Mapping[str, Mapping[str, Decimal]]) -> tuple[Decimal, str]:
"""Return (rentable_sf, source_used); raise if every source is empty."""
for source in FALLBACK_ORDER:
value: Optional[Decimal] = sources.get(source, {}).get(tenant_id)
if value is not None and value > 0:
return value, source
raise ValueError(f"no rentable area for tenant {tenant_id!r} in any source")
Step 3 — Build the scoped denominator for one pool. This is the step that a single-use allocator does not have. Include a tenant only if its component is eligible for the pool, it is occupied, and it is not individually carved out. Everything else — anchor carve-outs, vacant space, wrong-component tenants — falls out of the divisor here.
from collections.abc import Iterable
def pool_denominator(pool: ExpensePool, tenants: Iterable[Tenant]) -> Decimal:
"""Sum the rentable area of every tenant eligible to share this pool."""
total = Decimal("0")
for t in tenants:
if t.component not in pool.eligible_components:
continue # wrong component: not their concourse/parking
if not t.occupied:
continue # vacant space handled by gross-up, not here
if pool.pool_id in t.excluded_pools:
continue # lease-level carve-out shrinks the divisor
total += t.rentable_sf
if total <= 0:
raise ValueError(f"pool {pool.pool_id!r} has no eligible occupied tenants")
return total
Step 4 — Compute each tenant’s raw share and allocation. The factor is tenant_rsf / pool_denominator; the allocation is pool_amount * factor, quantized to whole cents. Carry the raw (un-rounded) amount alongside the rounded one — Step 5 needs the residual to reconcile the pool.
from decimal import ROUND_HALF_EVEN
CENTS = Decimal("0.01")
@dataclass
class Allocation:
tenant_id: str
pool_id: str
factor: Decimal # tenant_rsf / denominator
exact: Decimal # pool_amount * factor, unrounded
rounded: Decimal # exact quantized to cents
def allocate_pool(pool: ExpensePool, tenants: list[Tenant]) -> list[Allocation]:
"""Raw pro-rata pass over one scoped pool; rounding is reconciled in Step 5."""
denom = pool_denominator(pool, tenants)
out: list[Allocation] = []
for t in tenants:
if (t.component not in pool.eligible_components
or not t.occupied
or pool.pool_id in t.excluded_pools):
continue
factor = t.rentable_sf / denom
exact = pool.amount * factor
out.append(Allocation(
tenant_id=t.tenant_id,
pool_id=pool.pool_id,
factor=factor,
exact=exact,
rounded=exact.quantize(CENTS, rounding=ROUND_HALF_EVEN),
))
return out
Step 5 — Reconcile rounding so the pool ties out to the cent. Independent banker’s rounding of each share almost never sums to the pool exactly; you are typically one or two cents off. Distribute that residual with the largest-remainder method — the tenants whose exact amount was cut by the most rounding get the leftover pennies — so the allocation reconstructs the pool with zero drift.
def reconcile(pool: ExpensePool, allocations: list[Allocation]) -> list[Allocation]:
"""Push rounding residual onto the largest fractional remainders."""
rounded_total = sum((a.rounded for a in allocations), Decimal("0"))
residual = pool.amount - rounded_total # signed; usually a few cents
if residual == 0:
return allocations
step = CENTS.copy_sign(residual) # +0.01 or -0.01 per pass
# Rank by how much each tenant lost to rounding (exact minus rounded).
ranked = sorted(allocations, key=lambda a: a.exact - a.rounded, reverse=residual > 0)
pennies = int(abs(residual) / CENTS)
for i in range(pennies):
ranked[i % len(ranked)].rounded += step
return allocations
Gotchas & Known Limitations
- One building-wide denominator is the default bug. The most common mixed-use error is summing all rentable area once and reusing it for every pool. Each pool must recompute its own scoped denominator; the
pool_denominatorguard oneligible_componentsis what prevents cross-subsidy. - Vacant space cannot sit in the divisor untouched. Leaving vacant suites in the denominator under-recovers the pool; dropping them entirely can over-recover it under a gross-up lease. Decide per pool whether vacancy is landlord-absorbed or grossed up to an occupancy floor, and apply that before Step 3.
- Float anywhere is a silent time bomb. A single
floatin the factor or amount reintroduces the driftDecimalexists to remove. Keep rentable areas and money asDecimalend to end, including in your ingest layer. - Caps interact with, but do not replace, allocation. When a tenant’s computed share exceeds a negotiated ceiling, the overflow is redistributed to uncapped tenants in the same pool — that logic lives in managing expense caps and controllable limits and runs after this raw allocation, not inside it.
- Component assignment must be authoritative. If a tenant’s
componentis guessed from its unit number rather than read from the lease, one misclassification silently moves its area into the wrong denominators. Source the component from the abstracted lease, not from geometry. - Largest-remainder ties need a stable order. When two tenants have identical fractional remainders, sort on a deterministic secondary key (tenant_id) so the penny lands in the same place on every rerun and your statements are reproducible.
Verification
Because these numbers become tenant statements, correctness is proven arithmetically, not by inspection. Two invariants are load-bearing: within every pool the share factors sum to exactly one, and the reconciled allocations sum back to the pool amount to the cent.
from decimal import Decimal
def verify_pool(pool: ExpensePool, allocations: list[Allocation]) -> None:
"""Assert the two invariants a mixed-use allocation must never violate."""
# 1. Factors over a single scoped pool sum to 1 within Decimal tolerance.
factor_sum = sum((a.factor for a in allocations), Decimal("0"))
assert abs(factor_sum - Decimal("1")) < Decimal("0.0000001"), (
f"{pool.pool_id}: factors sum to {factor_sum}, not 1"
)
# 2. Reconciled money reconstructs the pool exactly — no rounding drift.
money_sum = sum((a.rounded for a in allocations), Decimal("0"))
assert money_sum == pool.amount, (
f"{pool.pool_id}: allocated {money_sum} != pool {pool.amount}"
)
retail = [
Tenant("R1", "retail", Decimal("4000")),
Tenant("R2", "retail", Decimal("6000")),
]
office = [Tenant("O1", "office", Decimal("10000"))]
concourse = ExpensePool("concourse_jan", Decimal("1000.00"), frozenset({"retail"}))
allocs = reconcile(concourse, allocate_pool(concourse, retail + office))
verify_pool(concourse, allocs)
# O1 is office: excluded from the retail concourse denominator entirely.
# R1 pays 400.00, R2 pays 600.00 -> sums to 1000.00 exactly.
The first assertion catches a denominator that included the wrong population — if an office tenant leaked into the retail concourse pool, the retail factors would sum to less than one. The second catches rounding drift and any largest-remainder bug. A pool that clears both invariants is safe to hand downstream; anything that fails routes to manual review with the pool id and source-of-record for each rentable area attached, preserving the audit trail a CAM reconciliation must reproduce on demand.
Once every pool ties out, these allocations feed back into the pro-rata allocation algorithm stage’s tenant ledger, and the acceptable variance bands that decide which residuals trigger a human are set in threshold tuning for allocation accuracy.
Related
- Implementing Pro-Rata Allocation Algorithms — the parent stage: the base weighted formula, occupancy handling, and Decimal rounding this recipe scopes per pool.
- Managing Expense Caps and Controllable Limits — the post-allocation layer that redistributes any share exceeding a lease-negotiated ceiling back onto uncapped tenants.
- Handling Controllable vs Non-Controllable CAM Expenses — how the pools you scope here split by controllability before caps are applied.