projectionmodels¶
Focused actuarial projections of claims, premium, and expenses on supplied
exposure. The package is intentionally organized around concrete workflows —
most users should not need to construct a calculation graph or define a
custom state engine. It depends on actuarialpy for its
completion, trend, seasonality, and credibility estimation.
Example 7 runs the whole renewal cycle end
to end — estimated assumptions, a credibility-blended claim projection,
indicated and capped rate actions fed back through RenewalRateActions,
expenses on three bases, and the projected loss-ratio path.
The package root contains the workflow objects most actuaries need:
Object |
Role |
|---|---|
|
Build the projection from the canonical |
|
Project claim rates and claims by claim type |
|
Roll premium forward, including renewal rate actions |
|
Supply effective-dated rate actions |
|
Project per-exposure, fixed, premium-based, and claim-based expenses |
|
Define monthly, quarterly, or annual projection periods |
|
Define entry, exit, renewal, and experience date columns |
|
Split records into existing/new or other date cohorts |
|
Run sensitivities and alternative assumptions |
|
Summarize without averaging ratios or duplicating exposure |
Lower-level modeling objects are available from projectionmodels.advanced,
but they are not part of the primary workflow.
Claims by claim type¶
project builds the projection straight from the canonical
actuarialpy.Experience: the record grain defaults to the
bound dimensions, and the claim-type dimension is inferred as the grain
column absent from the exposure frame (pass claim_type= when ambiguous).
A wide Experience built with a wide_by= pivot melts itself – the recorded
pivot becomes the claim-type dimension, and non-pivot expense columns are
announced as excluded. seasonality="estimate" fits factors from the bound
history; trend is always a stated selection, never a silent estimate:
from actuarialpy import Experience
experience = Experience(
hist,
expense="reported_claims",
exposure="exposure_units",
date="incurred_month",
dimensions=["group_id", "claim_type"],
valuation_date="2026-12-31",
)
projection = pm.project(
experience,
exposure=exposure,
exposure_col="exposure_units",
horizon=pm.ProjectionHorizon("2027-01-01", periods=12),
trend=pm.TrendAssumption.from_values("claim_trend", 0.06),
# optionally: completion=, seasonality=, credibility=, complement=
)
results = projection.project()
results.to_frame() # per key × claim type × period detail
Trend, seasonality, completion, and credibility may be supplied directly as
assumption tables (TrendAssumption.from_values and friends) or estimated
from history (below).
Cost levels and pipeline order¶
The claim workflow evaluates, in order: complete → deseasonalize → trend the
experience rate to the blend basis → credibility blend → trend from the basis
to each projection period → reseasonalize → add rate_loads → multiply by
exposure. Exposure is whatever unit the book uses — member-months,
policy-months, car-years — named with exposure_col.
The complement is used as stated. By default the blend basis is the
prospective midpoint of the horizon (complement_basis="prospective"), the
level at which manual and book rates are conventionally quoted — so a
zero-credibility projection reproduces the complement rather than a trended
copy of it. Set complement_basis="experience" if your complement is quoted
at experience-period cost level, or pass an explicit as-of date. Because the
month arithmetic is exactly additive, results at full credibility are
identical under every basis.
rate_loads (for example a pooling charge) are added to the projected rate
as stated: flat across periods, after seasonality, outside the blend.
Estimating assumptions with actuarialpy¶
Estimation is explicit and separate from projection execution. The
projectionmodels.integrations.actuarialpy adapters estimate each assumption
with the corresponding core primitive (completion_factors, fit_trend,
seasonality_factors, limited_fluctuation_z) and return assumption objects
that retain indicated values and diagnostics:
from projectionmodels.integrations.actuarialpy import (
estimate_completion,
estimate_credibility,
estimate_seasonality,
estimate_trend,
)
trend = estimate_trend(
"claim_trend",
deseasonalized_history,
by=["claim_type"],
date_col="incurred_month",
value_col="deseasonalized_claims",
exposure_col="exposure_units",
)
An actuary can replace an indication while preserving the audit trail — the assumption keeps both the estimate and the selection.
Results¶
ProjectionResults holds the per-period detail and summarizes it: amounts
are summed, and rates are recomputed from the summed amounts and exposure:
results.to_frame() # full detail
results.summarize(by="calendar_quarter") # weighted rates, summed exposure and amounts
API reference¶
Focused actuarial claim, premium, and expense projection workflows on supplied exposure.
- class Adjustment(target: str, method: str, value: ~typing.Any, name: str | None = None, filters: ~collections.abc.Mapping[str, ~typing.Any] = <factory>, effective_from: ~typing.Any | None = None, effective_to: ~typing.Any | None = None, priority: int = 100)[source]¶
Bases:
objectModify a named input, assumption, or roll-forward under a scenario.
- class Assumption(name: str, values: ~typing.Any, lookup: tuple[str, ...] | list[str] = <factory>, value_col: str | None = None, source: str = 'supplied', indicated_values: ~typing.Any | None = None, diagnostics: ~typing.Any | None = None, metadata: ~collections.abc.Mapping[str, ~typing.Any] = <factory>)[source]¶
Bases:
objectA named assumption resolved by explicit lookup fields.
valuesmay be a scalar, Series, or DataFrame. DataFrame assumptions are joined many-to-one onlookupand must containvalue_col.- resolve(frame: DataFrame, *, strict: bool = True) Series[source]¶
Resolve assumption values onto
framein its original row order.
- select(selection: Any, *, lookup: str | Iterable[str] | None = None, value_col: str | None = None, note: str | None = None) Assumption[source]¶
Replace indicated values with an actuarial selection while retaining audit data.
- class BookProjection(projections, labels=None)[source]¶
Bases:
objectRoll a set of group projections up into the book: totals, per-group, monthly.
Accepts
GroupProjectionobjects (or their results) and aggregates the expected – renewal-probability-weighted – premium and claims, so a group contributes in proportion to how likely it is to renew. Exposespremium/claims/loss_ratio(book totals),by_group(one row per group with its expected figures and renewal probability), andmonthly(expected premium and claims by projection month, summed over the book); the assembledBookResultsits onresult. All inputs must share one projection horizon – the monthly frames are summed elementwise.- Parameters:
projections (sequence of GroupProjection or GroupProjectionResult) – The in-force renewals and new business to aggregate.
labels (sequence of str, optional) – Group labels for
by_group(defaultgrp_0,grp_1, …); must matchprojectionsin length.
- base_rates(exp: Experience, *, grain: str | Iterable[str] | None = None, completion: CompletionAssumption | None = None, seasonality: SeasonalityAssumption | None = None, complement: Assumption | Any | None = None, extra_record_cols: Iterable[str] = (), claims_col: str | None = None) DataFrame[source]¶
Aggregate prepared experience to one row per projection record.
grainnames the record columns (projection keys plus the claim-type dimension) and defaults to thedimensionsbound on the Experience. Producesexperience_claim_rate, the exposure-weightedexperience_midpoint, and – when given –complement_claim_rate: the columnsClaimProjectionconsumes.
- prepare_experience(exp: Experience, *, completion: CompletionAssumption | None = None, seasonality: SeasonalityAssumption | None = None, claims_col: str | None = None) DataFrame[source]¶
Return experience with completed and deseasonalized claim columns.
Takes the canonical
actuarialpy.Experience– the bound expense, date, and exposure roles and the object’svaluation_datefill the column plumbing – and applies the projection assumptions in pipeline order (completion, then seasonality). The working column is carried toprojectionmodels_adjusted_claimsforbase_rates().
- project(exp: Experience, *, exposure: DataFrame, horizon: ProjectionHorizon, trend: TrendAssumption, seasonality: SeasonalityAssumption | None = None, credibility: CredibilityAssumption | None = None, completion: CompletionAssumption | None = None, complement: Assumption | Any | None = None, complement_basis: str | Timestamp = 'prospective', rate_loads: Any = (), grain: str | Iterable[str] | None = None, claim_type: str | None = None, extra_record_cols: Iterable[str] = (), claims_col: str | None = None, exposure_col: str | None = None, exposure_period_col: str = 'projection_period', dates: ProjectionDates | None = None) ClaimProjection[source]¶
Build a
ClaimProjectionfrom the canonical Experience.The single projection entrypoint: takes an
actuarialpy.Experienceplus only the concepts this package owns – the assumptions, the horizon, and the prospective exposure. Named parameters are the pipeline phases; ordering (complete, deseasonalize, aggregate, trend / blend) is fixed and never inferred from a list.graindefaults to the Experience’s bounddimensions. Of the grain columns, the ones present inexposureare the projection keys (rates join to exposure on them); the one absent is the claim-type dimension (rates vary by it, exposure does not). Passclaim_type=when the split is ambiguous.
- class ClaimProjection(base_rates: ~pandas.DataFrame, projection_keys: tuple[str, ...] | list[str], claim_type_col: str, exposure: ~pandas.DataFrame, horizon: ~projectionmodels.horizon.ProjectionHorizon, trend: ~projectionmodels.assumptions.TrendAssumption, seasonality: ~projectionmodels.assumptions.SeasonalityAssumption | None = None, credibility: ~projectionmodels.assumptions.CredibilityAssumption | None = None, complement_basis: str | ~pandas.Timestamp = 'prospective', rate_loads: ~typing.Any = (), exposure_col: str = 'exposure', exposure_period_col: str = 'projection_period', dates: ~projectionmodels.data.ProjectionDates | None = None, additional_assumptions: tuple[~projectionmodels.assumptions.Assumption, ...] | list[~projectionmodels.assumptions.Assumption] = <factory>)[source]¶
Bases:
objectProject credibility-blended claim rates onto supplied exposure.
Exposure is whatever unit the book uses — member-months, policy months, earned car-years — supplied by projection key and period and named with
exposure_col.Pipeline, in order:
experience_claim_rateis trended from each record’sexperience_midpointto the blend basis (trended_experience_rate).The trended experience is credibility blended with
complement_claim_rateas stated (credible_claim_rate).The blended rate is trended from the blend basis to each period’s midpoint (
trended_claim_rate).Seasonality redistributes within the year and
rate_loadsare added, flat and outside the blend (projected_claim_rate).Rates are multiplied by exposure (
projected_claims).
Cost levels —
complement_basisdeclares the level at which the complement is quoted:"prospective"(default): the horizon’s mean period midpoint, the conventional level for manual and book rates. Zero credibility therefore reproduces the complement as stated."experience": the record’s experience midpoint, so the complement is trended alongside experience (the pre-0.5.0 behaviour).an explicit date: any other as-of level.
Because the calendar month arithmetic is exactly additive, projections at full credibility are identical under every basis.
rate_loads(for example a pooling charge) are Assumptions or scalars quoted at prospective level; they are added to the projected rate as stated, per period, after seasonality, and are not credibility weighted.
- class CompletionAssumption(name: str, values: ~typing.Any, lookup: tuple[str, ...] | list[str] = <factory>, value_col: str | None = None, source: str = 'supplied', indicated_values: ~typing.Any | None = None, diagnostics: ~typing.Any | None = None, metadata: ~collections.abc.Mapping[str, ~typing.Any] = <factory>, development_col: str = 'development_month')[source]¶
Bases:
AssumptionClaim completion factors in the divide convention, supplied or estimated.
- class CredibilityAssumption(name: str, values: ~typing.Any, lookup: tuple[str, ...] | list[str] = <factory>, value_col: str | None = None, source: str = 'supplied', indicated_values: ~typing.Any | None = None, diagnostics: ~typing.Any | None = None, metadata: ~collections.abc.Mapping[str, ~typing.Any] = <factory>)[source]¶
Bases:
AssumptionCredibility weights supplied or estimated by actuarialpy.
- class DateCohort(name: str, date_col: str, split_date: Any | None = None, before_label: str = 'before', on_or_after_label: str = 'on_or_after', frequency: str | None = None, breaks: tuple[Any, ...] | None = None, labels: tuple[str, ...] | None = None, include_lowest: bool = True)[source]¶
Bases:
objectCreate a reportable classification from a date column.
- class ExpenseProjection(expenses: DataFrame, projection_keys: tuple[str, ...] | list[str], expense_type_col: str, base_value_col: str, basis_col: str, base_date_col: str, horizon: ProjectionHorizon, trend: TrendAssumption, exposure: DataFrame | None = None, premium: DataFrame | None = None, claims: DataFrame | None = None, exposure_col: str = 'exposure', premium_col: str = 'premium', claims_col: str = 'projected_claims', dates: ProjectionDates | None = None)[source]¶
Bases:
objectProject expenses on per-exposure, fixed-monthly, percent-of-premium, and percent-of-claims bases, one table for all four.
trendmay be keyed by expense type, so “contractually flat” is a per-type choice: a zero-trend type projects at its base value while the types around it trend (seeexamples/expenses.py). Theclaimsandpremiuminputs are keyed value streams joined on the projection keys and period — nothing requires the claims stream to contain claims, so a percentage of any per-period quantity (a claims subset such as hospital claims only, or another expense run’s output) is the same mechanism fed a different table. Percentage bases are not multiplied byactive_fraction: proration is already embedded in the stream they reference, and prorating again would double count.
- class GroupProjection(*, prospective_membership, seasonal_factors=None, current_premium, current_member_months, rate_action=0.0, plan_change=0.0, book_pmpm, claim_trend, exp_midpoint, prosp_midpoint, group_pmpm=None, group_claims=None, group_member_months=None, group_claim_count=None, credibility=None, full_credibility_claims=1082.0, pooling_pmpm=0.0, plan_affects_claims=True, renewal_prob=1.0)[source]¶
Bases:
objectOne group’s forward roll: premium and claims projected together on one membership.
Composes
PremiumRollforward(the stored premium rolled forward by rate action and plan change) andPMPMProjection(the credibility-blended, trended, pooled claims PMPM) on the supplied monthly membership, then weights both by the renewal probability – a lapsed group books neither, soloss_ratiois unaffected byrenewal_probwhileexpected_premium/expected_claimscarry it. This is the unitBookProjectionloops over the in-force book.Exposes
monthly(month, member-months, premium, claims – conditional on renewal), the conditional totalspremium/claims/loss_ratio, and the renewal-weightedexpected_premium/expected_claims; the full detail, including the claims and premium build-ups, sits onresult(GroupProjectionResult).- Parameters:
prospective_membership (array-like) – Projected member-months by prospective month; the horizon everything else is evaluated on.
seasonal_factors (array-like, optional) – Monthly claim seasonality (averaging one); redistributes claims across months without changing the annual total.
current_premium (float) – The stored premium and its member-months – rolled forward, never rebuilt from experience.
current_member_months (float) – The stored premium and its member-months – rolled forward, never rebuilt from experience.
rate_action (float, optional) – Renewal rate action and plan-value change, as decimals.
plan_change (float, optional) – Renewal rate action and plan-value change, as decimals.
book_pmpm (float) – Book claims PMPM and annual trend, applied midpoint-to-midpoint between the experience and projection periods.
claim_trend (float) – Book claims PMPM and annual trend, applied midpoint-to-midpoint between the experience and projection periods.
exp_midpoint (float) – Book claims PMPM and annual trend, applied midpoint-to-midpoint between the experience and projection periods.
prosp_midpoint (float) – Book claims PMPM and annual trend, applied midpoint-to-midpoint between the experience and projection periods.
group_pmpm (optional) – The group’s own experience – the PMPM directly, or claims and member-months to derive it.
group_claims (optional) – The group’s own experience – the PMPM directly, or claims and member-months to derive it.
group_member_months (optional) – The group’s own experience – the PMPM directly, or claims and member-months to derive it.
group_claim_count (optional) – Credibility, or the claim count to derive it by limited fluctuation (default full-credibility standard 1082 claims).
credibility (optional) – Credibility, or the claim count to derive it by limited fluctuation (default full-credibility standard 1082 claims).
full_credibility_claims (optional) – Credibility, or the claim count to derive it by limited fluctuation (default full-credibility standard 1082 claims).
pooling_pmpm (float, optional) – Large-claim pooling charge per member-month, trended alongside claims.
plan_affects_claims (bool, optional) – Whether
plan_changealso scales claims (default True).renewal_prob (float, optional) – Probability the group renews – supplied (e.g. from underwriting), not modelled here; weights the expected figures.
- class PMPMProjection(*, book_pmpm, claim_trend, exp_midpoint, prosp_midpoint, group_pmpm=None, group_claims=None, group_member_months=None, group_claim_count=None, credibility=None, full_credibility_claims=1082.0, plan_factor=1.0, pooling_pmpm=0.0)[source]¶
Bases:
objectThe claims engine: credibility-blend, trend, plan-adjust, and add pooling.
Blends the group’s own PMPM against the book PMPM with credibility
Z(limited fluctuation from the group’s claim count, capped at one, unless supplied), trends midpoint-to-midpoint, applies the plan factor, and adds the trended large-claim pooling charge:\[\text{projected} = \bigl[Z \cdot \text{group} + (1 - Z) \cdot \text{book}\bigr] \cdot \text{trend} \cdot \text{plan} + \text{pooling} \cdot \text{trend}\]Note the pooling charge is trended but not plan-adjusted.
projected_pmpmis a rate per member-month;claims()turns it into monthly claim dollars on a membership vector, with optional seasonal factors (averaging one) that redistribute across months without changing the annual total. The full build-up sits onresult(PMPMResult).- Parameters:
book_pmpm (float) – The book (manual) claims PMPM the blend shrinks toward.
claim_trend (float) – Annual claim trend, applied between the experience-period and projection-period midpoints.
exp_midpoint (float) – Annual claim trend, applied between the experience-period and projection-period midpoints.
prosp_midpoint (float) – Annual claim trend, applied between the experience-period and projection-period midpoints.
group_pmpm (optional) – The group’s experience PMPM, or claims and member-months to derive it.
group_claims (optional) – The group’s experience PMPM, or claims and member-months to derive it.
group_member_months (optional) – The group’s experience PMPM, or claims and member-months to derive it.
group_claim_count (optional) – Credibility, or the claim count to derive it by limited fluctuation (default full-credibility standard 1082 claims).
credibility (optional) – Credibility, or the claim count to derive it by limited fluctuation (default full-credibility standard 1082 claims).
full_credibility_claims (optional) – Credibility, or the claim count to derive it by limited fluctuation (default full-credibility standard 1082 claims).
plan_factor (float, optional) – Multiplicative plan-value adjustment on the blended PMPM.
pooling_pmpm (float, optional) – Large-claim pooling charge per member-month.
- class PremiumProjection(premium_data: DataFrame, projection_keys: tuple[str, ...] | list[str], exposure: DataFrame, horizon: ProjectionHorizon, current_rate_col: str = 'current_premium_rate', exposure_col: str = 'exposure', exposure_period_col: str = 'projection_period', renewal_date_col: str = 'renewal_date', recurring_rate_action_col: str | None = None, rate_actions: RenewalRateActions | None = None, dates: ProjectionDates | None = None)[source]¶
Bases:
objectProject premium rates and premium using supplied exposure (member-months, policy months, and so on — see
exposure_col).A recurring action column is applied in every period marked
is_renewal_period. ARenewalRateActionsschedule is applied once in the period containing each supplied effective date. The adjusted rate is carried forward to all subsequent periods.
- class PremiumRollforward(*, current_premium, current_member_months, rate_action=0.0, plan_change=0.0)[source]¶
Bases:
objectRoll the stored premium forward by known factors – never rebuilt from losses.
projected_pmpm = (current_premium / current_member_months) * (1 + rate_action) * (1 + plan_change). Premium is level per member-month (it earns evenly), sopremium()scales by the membership vector with no seasonal shape – unlike claims. Rebuilding premium from loss experience isratingmodels’ job; this projects the figure the database already holds. The build-up sits onresult(PremiumResult).- Parameters:
current_premium (float) – The stored premium and its member-months; their ratio is the current PMPM.
current_member_months (float) – The stored premium and its member-months; their ratio is the current PMPM.
rate_action (float, optional) – Renewal rate action and plan-value change, as decimals (default 0).
plan_change (float, optional) – Renewal rate action and plan-value change, as decimals (default 0).
Projected premium dollars by prospective month (level per member-month).
- class ProjectionDates(entry_date: str | None = None, exit_date: str | None = None, renewal_date: str | None = None, issue_date: str | None = None, experience_start: str | None = None, experience_end: str | None = None, exposure_timing: str = 'whole_period')[source]¶
Bases:
objectColumn roles for lifecycle and actuarial dates.
- class ProjectionHorizon(start: Any, periods: int | None = None, end: Any | None = None, frequency: str = 'monthly')[source]¶
Bases:
objectA deterministic projection timeline.
- Parameters:
start (Any) – First projection date. It is normalized to the beginning of the containing month, quarter, or year.
periods (int | None) – Number of projection periods. Supply either
periodsorend.end (Any | None) – Last date to include. Supply either
periodsorend.frequency (str) –
"monthly","quarterly", or"annual".
- property midpoint: Timestamp¶
Mean period midpoint of the horizon.
This is the prospective rating midpoint used as the default credibility blend basis: manual and book rates are conventionally stated at this level.
- class ProjectionResults(frame: ~pandas.DataFrame, measures: ~collections.abc.Mapping[str, ~projectionmodels.calculations.VariableDefinition], projection_keys: tuple[str, ...], component_keys: tuple[str, ...] = <factory>, assumption_audit_data: ~pandas.DataFrame | None = None, adjustment_audit_data: ~pandas.DataFrame | None = None)[source]¶
Bases:
objectDetailed projection output with mathematically explicit aggregation rules.
- classmethod combine(*results: ProjectionResults) ProjectionResults[source]¶
Combine compatible result sets column-wise on their common identifiers.
- summarize(by: str | Iterable[str], *, measures: str | Iterable[str] | None = None) DataFrame[source]¶
Summarize measures without double-counting coarser-grain values.
A measure’s declared
grainidentifies where it is unique. Requested grouping fields outside that grain are retained, which permits an entity- level exposure to repeat once for each displayed claim type while still counting only once when claim type is omitted from the summary.
- class RenewalRateActions(frame: DataFrame, projection_keys: tuple[str, ...] | list[str], effective_date_col: str = 'effective_date', rate_action_col: str = 'rate_action')[source]¶
Bases:
objectOne-time rate actions keyed by projection record and effective date.
rate_actionvalues are decimal changes:0.10means a 10% increase. Each action is applied once in the projection period containing its effective date. Use a rate-action column onPremiumProjectioninstead when the same action should recur at every renewal anniversary.- to_projection_table(horizon: ProjectionHorizon) DataFrame[source]¶
Return actions keyed to the horizon’s projection periods.
- class Scenario(name: str = 'baseline', adjustments: tuple[~projectionmodels.adjustments.Adjustment, ...] | list[~projectionmodels.adjustments.Adjustment] = <factory>)[source]¶
Bases:
objectA named collection of ordered adjustments.
- class SeasonalityAssumption(name: str, values: ~typing.Any, lookup: tuple[str, ...] | list[str] = <factory>, value_col: str | None = None, source: str = 'supplied', indicated_values: ~typing.Any | None = None, diagnostics: ~typing.Any | None = None, metadata: ~collections.abc.Mapping[str, ~typing.Any] = <factory>, season_col: str = 'season', frequency: str = 'M')[source]¶
Bases:
AssumptionNormalized seasonal multipliers.
- class TrendAssumption(name: str, values: ~typing.Any, lookup: tuple[str, ...] | list[str] = <factory>, value_col: str | None = None, source: str = 'supplied', indicated_values: ~typing.Any | None = None, diagnostics: ~typing.Any | None = None, metadata: ~collections.abc.Mapping[str, ~typing.Any] = <factory>)[source]¶
Bases:
AssumptionAnnual trend rate, supplied or fitted with
actuarialpy.fit_trend().
- exception ValidationError[source]¶
Bases:
ProjectionModelsError,ValueErrorRaised when projection inputs are structurally invalid.
- new_business(*, book_pmpm, claim_trend, exp_midpoint, prosp_midpoint, prospective_membership, manual_premium_pmpm, seasonal_factors=None, close_ratio=1.0, plan_change=0.0, pooling_pmpm=0.0) GroupProjectionResult[source]¶
A sold-but-new case: no experience, so claims are fully manual (credibility 0) and premium is the manual/target rate on projected membership; close_ratio plays the role of the renewal probability.