Calculation catalog › Longevity

Longevity survival percentile age

The oldest integer age a person reaches with probability at least pct/100 on the SSA period life table, optionally with a health hazard multiplier and a joint (either-survives) curve for a couple.

Field: Survival-percentile planning age Unit: age in years Basis: no dollar basis Computed by: Engine Status: Complete chain

Appears in: assumptions-card (SurvivalPercentileModal planning-age preview (household section), clamped to 60..120 before it becomes the plan's planning age)

Calculation records

  • Joint (either-survives) percentile age on the primary's age clock Formula record, justified by derivation

    Purpose: The couple's planning age: the oldest primary age at which at least one of two independent lives is alive with probability at least pct/100.

    Statement: With from = floor(max(primary.age, 0)), partnerFrom = floor(max(partner.age, 0)) and threshold = min(max(p, 0.1), 100)/100: for t = 0, 1, ... while from + t <= 120 or partnerFrom + t <= 120 (either person still inside the table; decision D-PEOPLE-ORDER, rule R6), let P(t) = 1 - (1 - S_primary(t))(1 - S_partner(t)), where each S(t) is that person's survival curve from their own floored age under their hazard (default 1), read t years on (survival-probability-product; for 'average' the mixture). Record best = from + t while P(t) >= threshold; stop at the first t below the threshold, or once both survivals are 0. Returns best; P(0) = 1, so the result is always >= from and never below the primary's single-life answer. The result is on the primary's clock and can exceed 120 when the partner is much younger; the calendar year it names, primary's birth year + best, is the same whichever person is passed as primary (a male 70 with a female 35 at 25 percent: 126 on his clock, 2082; stopped at his table end the walk returned 120, 2076). Units: integer primary age. Rounding: none.

    Formula: age* = max{ from + t : 1 - (1 - S_primary(t))(1 - S_partner(t)) >= p/100 }

    • from, partnerFrom: Floored current ages of primary and partner (unit: years) (domain: integer >= 0)
    • t: Years elapsed on the primary's clock (unit: years) (domain: integer >= 0, from + t <= 120 or partnerFrom + t <= 120)
    • S_primary(t), S_partner(t): Each life's survival over t more years (survival-probability-product) (unit: 1) (domain: [0, 1])
    • p: Percent chance that at least one is alive, clamped to [0.1, 100] (unit: percent) (domain: 0.1 <= p <= 100)

    Timing: annual; both curves advance one year per step on the primary's clock

    Rounding: none

    Limits:

    • Independent lifetimes; the either-alive probability is the complement of both being dead, 1 - (1 - S_a)(1 - S_b)
    • The partner's clock is offset by the age difference and advances with the primary's; the answer is a primary age, not a partner age
    • The percentage is clamped to [0.1, 100] before use, a bound the signature comment does not state
    • Each person's hazard power applies to that person's own curve; a missing hazard is 1
    • Two 'average' people are independent mixtures, so P(t) is the average over the four equally likely sex pairings (survival-probability-product)
    • The year-by-year projection reads this age for a couple, at hazard 1, as the horizon a plan's amortization-based spending amortizes to when it is set to the age at least one of them reaches with a 25 or 10 percent chance (projection/simulate.ts), worked out again on every projection: example-couple on the 10 percent horizon ends with an after-tax estate of $1,378,752 on the 2022 table and $1,659,513 on 2023

    Provenance: derived by claude, implemented by claude.

    Independent review: codex

    Gates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass

    Worksheet: joint-survival-percentile-age.md Mutation receipt: joint-survival-percentile-age.mutation.md

    Evidence tests: survival.evidence.test.ts

    Implemented in: survival.ts

    Is the number of: longevity-survival-percentile-age

    Feeds: spending-base-annual

  • Survival-percentile planning age: oldest age reached with probability at least pct/100 Formula record, justified by derivation

    Purpose: The single-life planning age the assumptions card previews: the age a person has a pct% chance of reaching.

    Statement: For current age c, sex, percentage p and hazard power h (default 1): threshold = min(max(p, 0.1), 100)/100. With from = floor(max(c, 0)) and best = from, for t = 1, 2, ... while from + t <= 120, read S(t), the survival curve from `from` under h (survival-probability-product; for 'average' the mixture); while S(t) >= threshold record best = from + t, and stop at the first t whose S(t) falls below the threshold. Returns best: the oldest integer age whose conditional survival from c is at least the threshold, always >= from. Units: integer age. Rounding: none.

    Formula: age* = max{ g >= from : S(from -> g) >= p/100 }, with S(from -> from) = 1

    • from: Floored current age (unit: years) (domain: integer >= 0)
    • p: Percent chance of reaching the age, clamped to [0.1, 100] (unit: percent) (domain: 0.1 <= p <= 100)
    • S(from -> g): Conditional survival to g, read off the survival curve (survival-probability-product) (unit: 1) (domain: [0, 1], nonincreasing in g)
    • age*: Survival-percentile planning age (unit: years) (domain: from <= age* <= max(119, from))

    Timing: annual survival steps over integer ages

    Rounding: none

    Limits:

    • The percentage is clamped to [0.1, 100] before use, a bound the signature comment does not state; a value passed as a probability (0.97) is read as 0.97%
    • Survival is nonincreasing, so the qualifying ages form an initial interval and the loop may stop at the first failure
    • The signature comment bounds the result by MAX_AGE + 1 (120); the table's last row is closed (q(119) = 1, mortality-published-death-probability), so the survival to 120 is 0 and the largest value actually returned is 119, or the floored current age when that is older (a current age of 125 returns 125, which it has already reached)
    • For 'average' the age is read off the mixture's curve and is not the mean of the male and female ages (at 65, 50/25/10 percent: 85/91/95, against 83/89/94 for a man and 86/92/96 for a woman)
    • The planner-ui SurvivalPercentileModal clamps the result to 60..120 before it becomes the plan's planning age; that clamp is presentation and is not part of this record
    • The year-by-year projection reads this age for a single, at hazard 1, as the horizon a plan's amortization-based spending amortizes to when it is set to the age reached with a 25 or 10 percent chance (projection/simulate.ts). Unlike a percentile pick, which is stored once with its edition, that horizon is worked out again on every projection, so the plan's figures move when the table changes: under-saved-single on the 25 percent horizon (age 90 on the 2022 table, 91 on 2023) ends with an after-tax estate of $9,840 on the 2022 table and $212,538 on 2023

    Provenance: derived by claude, implemented by claude.

    Independent review: codex

    Gates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass

    Worksheet: survival-percentile-age.md Mutation receipt: survival-percentile-age.mutation.md

    Evidence tests: survival.evidence.test.ts

    Implemented in: survival.ts

    Is the number of: longevity-survival-percentile-age

    Feeds: spending-base-annual

Records that feed this number

  • One-year death probability: SSA's published q(x) Formula record, justified by derivation

    Purpose: The annual death probability of a man or a woman that every survival curve and stochastic-longevity draw reads, straight from SSA's published table.

    Statement: montecarlo/deathProbability.ts#annualMortality(age, sex) (re-exported by montecarlo/mortality.ts), for a man or a woman of age a with x = floor(a): the probability of dying within one year is the death probability SSA publishes for that sex and exact age x in Table 4C6 (ssa-period-life-table), for x from 0 to 118. At 119 and above it is 1: the table's last row is closed, so a life at 119 dies during that year, although SSA prints 0.926604 there. Below 0 it is 0. A person whose sex is 'average' has no single death probability, and the function refuses 'average' with a RangeError; the survival curve builds 'average' as the mixture of the two sexes (survival-probability-product). Units: probability. Rounding: none; the published six decimals.

    Formula: q(a) = q_SSA(sex, floor(a)) for 0 <= floor(a) <= 118; q(a) = 1 for floor(a) >= 119; q(a) = 0 for floor(a) < 0

    • a: Age as passed (unit: years) (domain: real)
    • q_SSA(sex, x): SSA's published probability of dying within one year at exact age x, male or female column (ssa-period-life-table) (unit: 1) (domain: [0, 1], six decimals)
    • q(a): Probability of dying between ages floor(a) and floor(a) + 1 (unit: 1) (domain: 0 <= q(a) <= 1)

    Timing: one-year transition between consecutive integer ages

    Rounding: none

    Limits:

    • The table end: SSA prints q(119) = 0.926604 for both sexes, and e(119) = 0.58, which counts life past 120; the engine closes the table at 119 instead, as the Monte Carlo horizon and the expected-value loops assume. Reading the printed q(119) and closing the table at 120 would change the chance of being alive at 120 from 0 to 6.8e-12 for a man and 2.3e-11 for a woman of 65, 2.8e-7 at 110, 0.00138 at 117 and 0.0734 for someone already 119, and would change no survival probability to an age below 120
    • The period table is applied unchanged to the person's whole remaining life: it describes 2023's death rates, not an individual's risk or future mortality improvement
    • No interpolation within a year of age: a fractional age reads the row of its whole years
    • A negative age returns 0 and is not flagged

    Provenance: derived by claude, implemented by claude.

    Independent review: codex

    Gates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass

    Worksheet: mortality-published-death-probability.md Mutation receipt: mortality-published-death-probability.mutation.md

    Evidence tests: mortality.evidence.test.ts

    Implemented in: deathProbability.ts

    Feeds: longevity-survival-percentile-age, monte-carlo-success-rate, monte-carlo-ending-investable-histogram, social-security-expected-present-value, social-security-survivor-switch-pv, social-security-fica-return-ratio, income-annuity-annual, spending-base-annual

  • SSA period life table, 2023 (2026 Trustees Report) Data record, justified by derivation

    Purpose: The death probabilities and life expectancies by age and sex that every survival figure and the longevity questionnaire read.

    Statement: longevity/ssaPeriodLifeTable.ts#SSA_PERIOD_LIFE_TABLE carries SSA's Table 4C6 period life table for 2023, as used in the 2026 Trustees Report: the probability of dying within one year, q(x), and the period life expectancy, e(x), at each exact age x from 0 to 119, for men and for women, as printed (q to six decimals, e to two), read from SSA's page on 2026-09-27. Its source record names the page, the table's caption, the period and Trustees Report years, the read date, an Internet Archive capture of the page and the SHA-256 of the four columns in a canonical text, which the evidence rebuilds from the numbers. #LAST_TABLE_AGE is the last row, 119. #baselineRemainingYears reads e: the printed e(x) at a whole age, linear between the two rows at a fractional one, and for 'average' the mean of the male and female values; the longevity questionnaire prints it as its baseline. #CURRENT_LIFE_TABLE_EDITION names the edition a percentile pick or a saved questionnaire result records, and #storedLifeTableEdition reads a stored figure that names none as made on the 2022 period table of the 2025 Trustees Report, the table the engine carried before. Units: probability; years. Rounding: none; the printed figures.

    Limits:

    • The number-of-lives columns SSA also prints are not carried; nothing reads them
    • A period table: 2023's death rates at every age, applied to a person's whole remaining life, not a cohort projection with future mortality improvement
    • When SSA publishes the next edition, the embedded table trails it until a reviewed data change replaces it; the tax-rule record for the table's edition (SSA Table 4C6) names it and is re-verified yearly (annuallyIndexed, the rules:due queue and verify:quotes, whose quoted introduction and row 65 stop matching the page when the edition changes)
    • The last row is closed: the engine reads q(119) as 1 (mortality-published-death-probability) although the table prints 0.926604 there for both sexes; the printed value is carried, not read
    • Which projections read it. The year-by-year projection reads the table in two cases only: when a plan's amortization-based spending is set to amortize to the age reached with a 25 or 10 percent chance (projection/simulate.ts, through survival-percentile-age and joint-survival-percentile-age, worked out again on every projection), and when a plan holds a joint-and-survivor annuity whose exclusion ratio is fixed (projection/annuityForms.ts, through mortality-joint-last-survivor-expectancy). Such a plan gets new year-by-year figures when the table changes: under-saved-single with its spending amortized to the 25 percent age ends with an after-tax estate of $9,840 on the 2022 table and $212,538 on the 2023 table. The Social Security analysis page, the percentile picker, the questionnaire and Monte Carlo with longevity modelled read it on their own pages; a percentile pick is stored once with its edition and not recomputed

    Provenance: derived by claude, implemented by claude.

    Independent review: codex

    Gates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass

    Worksheet: ssa-period-life-table.md Mutation receipt: ssa-period-life-table.mutation.md

    Evidence tests: ssaPeriodLifeTable.evidence.test.ts

    Implemented in: ssaPeriodLifeTable.ts

    Feeds: longevity-survival-percentile-age, monte-carlo-success-rate, monte-carlo-ending-investable-histogram, social-security-expected-present-value, social-security-survivor-switch-pv, social-security-fica-return-ratio, income-annuity-annual, spending-base-annual

  • Hazard power for a remaining-years multiplier: exactly 1 at m = 1, otherwise solved by bisection Model record, justified by derivation

    Purpose: Turns the longevity questionnaire's remaining-years multiplier into the proportional-hazards power the percentile ages use.

    Statement: Given age, sex and multiplier m: at m = 1 the power is exactly 1. Otherwise target = max(0.1, m) x E(1), where E(h) = 0.5 + the sum over t = 1, 2, ... (while from + t <= 120) of S_h(t), the survival curve from from = floor(max(age, 0)) under power h (survival-probability-product; for 'average' the mixture, one power for both sexes), and the sum stops once S_h(t) <= 1e-12. E is strictly decreasing in h for a starting age below 119, the table's last age; from 119 on, the closed last row (q = 1) makes every S_h(t) with t >= 1 zero, so E(h) = 0.5 for every h. If E(0.2) <= target return 0.2; if E(8) >= target return 8; otherwise bisect [0.2, 8] for 40 halvings, raising lo to the midpoint when E(mid) > target and lowering hi otherwise, and return the final midpoint. So, for a starting age below 119 and a target E reaches inside [0.2, 8], the adjusted curve's expectancy is m times the unadjusted curve's own, the ratio the questionnaire's multiplier means (its central estimate is its baseline times m). Units: dimensionless hazard power. Rounding: none.

    Formula: h* = 1 when m = 1; otherwise h* solves E(h) = max(0.1, m) E(1) on [0.2, 8]; E(h) = 0.5 + sum_t S_h(t)

    • m: Remaining-years multiplier from the longevity questionnaire (m < 1 is shorter) (unit: 1) (domain: floored at 0.1)
    • E(1): The survival curve's own remaining expectancy at power 1, at the floored age (unit: years) (domain: > 0)
    • E(h): Remaining expectancy under hazard power h (unit: years) (domain: strictly decreasing in h below age 119; 0.5 for every h from 119)
    • h*: Solved hazard power (unit: 1) (domain: 0.2 <= h* <= 8)

    Timing: annual survival steps; the 0.5 is the within-year death convention

    Rounding: none; the bisection stops after 40 halvings

    Limits:

    • Proportional hazards is the modeling choice: the multiplier is matched on remaining expectancy only, not on the shape of the survival curve
    • The result is clamped to [0.2, 8] and the multiplier floored at 0.1, so extreme questionnaire answers cannot degenerate the curve; the clamps are unstated in the signature comment
    • A clamped result need not reach its target, and from age 119, the table's last age, none can for m other than 1: E(h) is 0.5 for every h, so m = 0.8 returns 8 and m = 1.2 returns 0.2 with the expectancy still 0.5, not 0.4 or 0.6; m = 1 still returns 1
    • The questionnaire's factors are not validated here; the record maps a given multiplier, whatever its source
    • The expectancy sum stops once the running survival falls to 1e-12, and the bisection runs a fixed 40 halvings rather than to a stated tolerance
    • expectancyUnderHazard is module-private; the evidence recomputes the expectancies through survivalCurve
    • The multiplier is matched as a ratio of the curve's own expectancies. The questionnaire prints SSA's e(x) as its baseline, which differs from the curve's E(1) by at most 0.005 years at every questionnaire age from 18 to 110 (largest 0.00496, a man of 46; -0.0045 at 110; at 65 a man's E(1) is 18.1163 against the printed 18.12), because e is printed to two decimals, SSA's person-years within a year of age need not be exactly one half, and SSA's table continues past 119 while the engine closes it. The gap is published with the table as longevity/ssaPeriodLifeTable.ts#CURVE_EXPECTANCY_GAP, which the questionnaire's results card prints rounded up to the thousandth and longevity/curveExpectancyGap.test.ts recomputes from the columns
    • m = 1 returns 1 exactly rather than bisected; the bisection cannot land on 1 (on the 2022 table it returned 0.9999999999989995 at every age and sex). Aiming at m times the curve's own E(1), rather than m times the printed e, moves a questionnaire-adjusted percentile age by one year in 1, 1, 2 and 3 of 837 picks (ages 18 to 110, three sexes, 50, 25 and 10 percent) at m = 0.8, 0.9, 1.1 and 1.12, compared with aiming at the printed e on the same table; at m = 1 the printed-e target would move one pick of 837 (a woman of 25 at 10 percent, 95 to 96) and this one moves none
    • For 'average' one power applies to both halves of the mixture, and it is not the mean of the male and female powers (m = 0.8 at 65: 1.70016, against 1.63807 for a man and 1.76442 for a woman)
    • The worksheet pins the identity point m = 1 at all 279 age and sex points from 18 to 110 and one non-identity case, m = 0.8 at 65 for each sex; the survival tests (survival.test.ts) cover direction and monotonicity for m = 0.8 and 1.12

    Provenance: derived by claude, implemented by claude.

    Independent review: codex

    Gates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass

    Worksheet: survival-hazard-from-expectancy-multiplier.md Mutation receipt: survival-hazard-from-expectancy-multiplier.mutation.md

    Evidence tests: survival.evidence.test.ts

    Implemented in: survival.ts

    Feeds: longevity-survival-percentile-age

  • The survival curve: product of hazard-adjusted one-year survivals, and the 50/50 mixture for 'average' Formula record, justified by derivation

    Purpose: The probability someone alive today is still alive at a later integer age, on SSA's published death probabilities with an optional health hazard.

    Statement: montecarlo/survival.ts#survivalCurve is the engine's one survival curve, for a whole starting age a (at least 0), sex and hazard power h (a positive finite number, default 1). For a man or a woman, survivalTo(t) = product over x = a..a+t-1 of (1 - q(x))^h (mortality-published-death-probability), multiplied left to right, where the factor is 0 when q(x) >= 1; 1 for t <= 0, and 0 once the running product reaches 0; deathProbabilityInYear(t) = survivalTo(t) x (1 - (1 - q(a + t))^h); and deathProbabilityGivenAlive(t) = 1 - (1 - q(a + t))^h, which at h = 1 is q(a + t) itself. For 'average', survivalTo(t) is the mean of the man's and the woman's survivalTo(t) from a, each under the power h: the chance for someone equally likely to be either; deathProbabilityInYear(t) = survivalTo(t) - survivalTo(t + 1); and deathProbabilityGivenAlive(t) = 1 - survivalTo(t + 1)/survivalTo(t), or 1 once survivalTo(t) is 0. Both death probabilities are 0 for t < 0. A fractional age or an invalid hazard is refused. #survivalProbabilityTo(c, sex, g, h) is a view of it: with from = floor(max(c, 0)) and to = floor(g), it returns 1 when to <= from and otherwise the curve from `from` at t = to - from, the same product in the same order. The percentile ages, the hazard solver, the death-age draw, the joint expectancy and the Social Security analysis models read the curve. Units: probability. Rounding: none.

    Formula: S(t) = prod_{x=a}^{a+t-1} (1 - q(x))^h for a man or a woman; S_average(t) = (S_male(t) + S_female(t))/2; S = 1 when t <= 0

    • a, t: Whole starting age and whole years elapsed (unit: years) (domain: integer, a >= 0)
    • q(x): SSA's published one-year death probability at x for the sex (mortality-published-death-probability) (unit: 1) (domain: [0, 1])
    • h: Proportional-hazards power (q' = 1 - (1 - q)^h; h > 1 is worse health), applied to each sex before mixing (unit: 1) (domain: h > 0)
    • S: Probability of being alive t years on (unit: 1) (domain: [0, 1])

    Timing: annual; one factor per integer age from the current age up to, not including, the target

    Rounding: none

    Limits:

    • Ages are floored; a fractional current or target age is not interpolated
    • A target at or below the current age returns exactly 1 without consulting the table
    • survivalProbabilityTo does not validate the hazard power (a non-positive h is used as passed); survivalCurve refuses one
    • annualSurvival is module-private; survivalCurve and survivalProbabilityTo read it, and the evidence reaches it through both
    • 'average' is the 50/50 mixture of the two sexes' curves from the person's current age, not a q(x) table: its one-year chance of dying given alive depends on the starting age, because the survivors of a mixed group become more female with age (at 90: 0.14086 for a mixture formed at 22, 0.14155 at 65 and 0.14327 at 85, against the mean q 0.144317). Every survival probability, the life expectancy (0.5 plus the sum of the survivals) and every expected value linear in the curve are the means of the male and female ones; ages read off the curve and hazard powers solved on it are not (at 65 the ages reached with 50, 25 and 10 percent chance are 85, 91 and 95, against 83, 89 and 94 for a man and 86, 92 and 96 for a woman)
    • The hazard power applies to each sex's curve before mixing: the person is one sex or the other, with that sex's death rates raised to h, and one power serves both halves
    • Two 'average' people are independent mixtures: each is a man or a woman with probability 1/2, independently of the partner, so a couple's expected value, joint expectancy or either-alive probability is the average over the four equally likely sex pairings (man-man, man-woman, woman-man, woman-woman). It is not the reading "an opposite-sex couple, order unknown", which averages only the two mixed pairings; that reading differs by up to 0.135 percent of a benefits-only row on the examples' two couples of 'average' people (all-401k-no-bridge and brokerage-bridge-401k) and changes no top claim age at 0, 2 or 4 percent

    Provenance: derived by claude, implemented by claude.

    Independent review: codex

    Gates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass

    Worksheet: survival-probability-product.md Mutation receipt: survival-probability-product.mutation.md

    Evidence tests: survival.evidence.test.ts

    Implemented in: survival.ts

    Feeds: longevity-survival-percentile-age, social-security-expected-present-value, social-security-survivor-switch-pv, social-security-fica-return-ratio, income-annuity-annual, spending-base-annual

Engine source: engine/src/montecarlo/survival.ts (survivalPercentileAge)

The evidence links on this page (each record's worksheet, mutation receipt, evidence tests and implementation, and the source links) are pinned to engine commit 2624dbdba9fd, committed 2026-10-11. The live calculation index and output-family census links point at the repository's main branch, so you can check this snapshot against them.