Computing the Weighted-Average Discount Rate and Lease Term Across a Portfolio
A focused, decimal-precise how-to for computing the ASC 842-20-50-4(g) weighted-average discount rate and remaining lease term over a portfolio, with a balance-weighted versus simple-average comparison.
Problem Statement Link to this section
This page answers one narrow, practical question: given a portfolio of leases each with its own discount rate, remaining liability, and remaining term, exactly how do you collapse them into the two scalars ASC 842 asks for — the weighted-average discount rate and the weighted-average remaining lease term — so the numbers reconcile to the balance sheet? The calculation looks trivial, and that is precisely why it is so often wrong: the naive instinct is to average the rates, but ASC 842-20-50-4(g) intends a figure weighted by the remaining lease liability balance, so a large obligation counts for more than a small one. Getting the weight wrong shifts the disclosed rate by enough to change how a reader models the lessee's financing cost, and because the output is a single number distilled from hundreds of leases, no one notices until an auditor recomputes it. The rate that enters this average is the one locked at commencement or last remeasurement by discount rate determination and mapping, and the weight is the remaining balance produced by the effective interest method.
Standard Anchor Link to this section
One provision governs this calculation directly. ASC 842-20-50-4(g) requires a lessee to disclose the weighted-average remaining lease term (item (g)(3)) and the weighted-average discount rate (item (g)(4)) as of the reporting date, separately for finance and operating leases. The Codification states that the figures are weighted averages but does not print the weighting algorithm; the FASB Basis for Conclusions and consistent practice supply it — the discount rate is weighted by each lease's remaining liability balance, and the remaining term is most commonly weighted the same way so both disclosures tie to the liability on the balance sheet.
IFRS 16 imposes no equivalent requirement. IFRS 16.53 lists profit-or-loss and cash-flow amounts, and its item (g) is the total cash outflow for leases, not a rate; a weighted-average incremental borrowing rate appears in IFRS accounts only when a preparer elects to give it under the additional-information provision of IFRS 16.59. So this how-to is, strictly, a US GAAP computation that IFRS groups may reuse voluntarily.
Algorithm Specification Link to this section
Work within a single lease class. Let leases be indexed
where
Annotated Python Snippet Link to this section
The function below computes both figures for one class with decimal.Decimal, so the four-decimal rate never inherits binary-float drift. It filters expired leases, accumulates in a single pass, and ends with a terminal assertion proving the weighted rate is bounded by the per-lease rates.
from decimal import Decimal, ROUND_HALF_UP, getcontext
getcontext().prec = 28 # audit-grade precision for the weighted sums
def weighted_rate_and_term(leases: list[dict]) -> dict:
"""ASC 842-20-50-4(g): balance-weighted rate and remaining term for one class.
Each lease dict has: rate (Decimal, annual), liability (Decimal, remaining
carrying value), term_months (int, remaining). Expired leases are excluded.
"""
active = [x for x in leases if x["liability"] > Decimal("0")]
total_liab = sum((x["liability"] for x in active), Decimal("0"))
if total_liab == Decimal("0"):
return {"wavg_rate": None, "wavg_term_years": None, "count": 0}
rate_num = sum((x["rate"] * x["liability"] for x in active), Decimal("0"))
term_num = sum((Decimal(x["term_months"]) * x["liability"]
for x in active), Decimal("0"))
wavg_rate = (rate_num / total_liab).quantize(
Decimal("0.0001"), rounding=ROUND_HALF_UP)
wavg_term = (term_num / total_liab / Decimal("12")).quantize(
Decimal("0.1"), rounding=ROUND_HALF_UP)
return {"wavg_rate": wavg_rate, "wavg_term_years": wavg_term,
"count": len(active)}
portfolio = [
{"rate": Decimal("0.045"), "liability": Decimal("1200000.00"), "term_months": 84},
{"rate": Decimal("0.060"), "liability": Decimal("800000.00"), "term_months": 48},
{"rate": Decimal("0.075"), "liability": Decimal("400000.00"), "term_months": 24},
{"rate": Decimal("0.090"), "liability": Decimal("0.00"), "term_months": 0},
]
out = weighted_rate_and_term(portfolio)
rates = [x["rate"] for x in portfolio if x["liability"] > 0]
# Terminal reconciliation gate: a weighted mean is bounded by its inputs
assert min(rates) <= out["wavg_rate"] <= max(rates), "rate outside per-lease range"
assert out["count"] == 3, "expired lease was not excluded"
print(out) # {'wavg_rate': Decimal('0.0540'), 'wavg_term_years': Decimal('5.5'), ...}
The assertion is the guardrail: if the weight is negated, the numerator and denominator are mismatched, or a float leaks into rate, the result escapes the min–max band and the disclosure cannot be published. Running the check inside the function means every portfolio close exercises it.
Balance-Weighted vs. Simple Average Link to this section
The single most consequential choice here is the weight. On a skewed portfolio the two methods diverge materially, and only the balance-weighted figure is compliant.
| Aspect | Balance-weighted (correct) | Simple average (incorrect) |
|---|---|---|
| Weight applied | Remaining lease liability of each lease | Each lease counts once |
| Formula | sum of rate times liability, over total liability | sum of rates, over lease count |
| Result on the example | 5.40% | 6.00% |
| Ties to balance sheet | Yes — reconciles to the disclosed liability | No — ignores obligation size |
| ASC 842-20-50-4(g) status | Compliant | Not compliant |
| Failure it causes | None | Overstates the rate on a large low-rate book |
Neither the numbers nor the compliance status is close: the simple average here reports 6.00% against a true balance-weighted 5.40%, because it lets a small 7.5% lease count as much as an obligation three times its size. The same divergence applies to the remaining term.
Gotcha: The Zero-Balance Lease That Drags the Term Down Link to this section
The most common defect is not the rate weight — it is leaving fully amortized or terminated leases in the population. A lease that has unwound to a zero liability often lingers in the register with its original rate and a remaining term that has gone to zero or negative. Its zero weight leaves a balance-weighted rate untouched, which is why the bug hides, but any term logic that averages by count, or any population count reported to readers, is corrupted by it.
Before (WRONG — averages the term by lease count, so a zero-term lease pulls it down):
active = leases # no filter applied
avg_term = sum(x["term_months"] for x in active) / len(active) # count-weighted, dilutes
After (correct — filter to live obligations, then weight by liability):
active = [x for x in leases if x["liability"] > Decimal("0")] # drop expired
total = sum((x["liability"] for x in active), Decimal("0"))
avg_term = sum((Decimal(x["term_months"]) * x["liability"]
for x in active), Decimal("0")) / total # balance-weighted
Debug checklist when the disclosed figures look off:
- Confirm the filter. Every lease in the population must have a remaining liability strictly greater than zero; a lingering zero-balance row is the number-one cause of an understated term.
- Check the weight, not the count. Divide the weighted numerator by the total liability, never by the number of leases.
- Separate the classes. Finance and operating are disclosed independently; a blended average fails ASC 842-20-50-4(g) outright. Whether a marginal lease is even large enough to move the figure is a question for threshold tuning for materiality.
- Use the annual rate. Weight the annual discount rate, not the monthly periodic rate the amortization engine consumes.
Frequently Asked Questions Link to this section
Why not just average the discount rates of all the leases?
Because a simple average treats a USD 5 million liability and a USD 50,000 liability as equally important, which is not how the reader models financing cost. ASC 842-20-50-4(g)(4) intends a figure weighted by the remaining lease liability balance, so the reported rate reflects the obligation actually sitting on the balance sheet. On a skewed portfolio the two methods can differ by a full percentage point, and only the balance-weighted figure is compliant.
What happens to a lease that has fully amortized during the year?
It drops out of the population. Once the remaining liability reaches zero there is no obligation to weight and no remaining term to disclose, so filtering on a positive remaining liability before aggregating is the correct treatment. Leaving it in does not move a balance-weighted rate, but it corrupts any term averaged by count and overstates the disclosed lease count.
Do I weight the remaining term the same way as the rate?
Most preparers do, weighting both by the remaining lease liability balance so the two disclosures stay internally consistent and tie to the balance sheet. ASC 842 does not forbid weighting the remaining term by the remaining undiscounted payments instead; either is defensible, but the choice is an accounting policy that must be applied consistently across periods and lease classes.
Related Link to this section
- Sibling: Disclosing variable lease costs under ASC 842 — the companion how-to for the cost side of the same footnote.
- Parent: Weighted-average discount rate and term disclosures — the full treatment of both figures, their standards basis, and the routing into per-class disclosures.
- Section: Lease disclosure and financial statement reporting — the reporting framework these weighted averages feed, alongside the maturity analysis and the liability rollforward.