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.

The package root contains the workflow objects most actuaries need:

Object

Role

ClaimExperience

Prepare a base claim rate from experience

ClaimProjection

Project claim rates and claims by claim type

PremiumProjection

Roll premium forward, including renewal rate actions

RenewalRateActions

Supply effective-dated rate actions

ExpenseProjection

Project per-exposure, fixed, premium-based, and claim-based expenses

ProjectionHorizon

Define monthly, quarterly, or annual projection periods

ProjectionDates

Define entry, exit, renewal, and experience date columns

DateCohort

Split records into existing/new or other date cohorts

Adjustment / Scenario

Run sensitivities and alternative assumptions

ProjectionResults

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.

Quickstart: premium at renewal

import pandas as pd
import projectionmodels as pm

premium_data = pd.DataFrame({
    "group_id": ["A", "B"],
    "renewal_date": pd.to_datetime(["2027-03-01", "2027-07-01"]),
    "current_premium_rate": [100.0, 100.0],
    "rate_action": [0.10, 0.20],
})

periods = pd.period_range("2027-01", periods=12, freq="M").astype(str)
exposure = pd.DataFrame(
    [{"group_id": g, "projection_period": p, "member_months": 1_000.0}
     for g in ("A", "B") for p in periods]
)

results = pm.PremiumProjection(
    premium_data=premium_data,
    projection_keys=["group_id"],
    exposure=exposure,
    exposure_col="member_months",
    horizon=pm.ProjectionHorizon("2027-01-01", periods=12),
    recurring_rate_action_col="rate_action",
).project()

Group A remains at $100 through February, increases to $110 at its March renewal, and carries that rate forward; group B increases to $120 in July. For different actions at different renewals, provide an effective-dated RenewalRateActions table instead of a recurring action column.

Claims by claim type

ClaimExperience prepares a base rate from history, and ClaimProjection.from_experience carries it through the projection:

experience = pm.ClaimExperience(
    hist,
    projection_keys=["group_id"],
    claim_type_col="claim_type",
    date_col="incurred_month",
    claims_col="reported_claims",
    exposure_col="member_months",
    valuation_date="2026-12-31",
)

projection = pm.ClaimProjection.from_experience(
    experience,
    exposure=exposure,
    exposure_col="member_months",
    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="member_months",
)

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 correctly: exposure-weighted rates, summed amounts — never a naive average of ratios, never exposure counted twice across claim types:

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: object

Modify 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: object

A named assumption resolved by explicit lookup fields.

values may be a scalar, Series, or DataFrame. DataFrame assumptions are joined many-to-one on lookup and must contain value_col.

audit_frame() DataFrame[source]

Return a compact assumption-audit table.

resolve(frame: DataFrame, *, strict: bool = True) Series[source]

Resolve assumption values onto frame in 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 ClaimExperience(data: DataFrame, projection_keys: tuple[str, ...] | list[str], claim_type_col: str, date_col: str, claims_col: str, exposure_col: str, valuation_date: Any | None = None)[source]

Bases: object

Historical claims used to establish projected base rates.

The input may contain one row per month, claim transaction, or another experience grain. to_base_rates develops immature claims, removes seasonality, aggregates to projection record + claim type, and calculates a per-exposure experience rate.

prepare(*, completion: CompletionAssumption | None = None, seasonality: SeasonalityAssumption | None = None) DataFrame[source]

Return experience with completed and deseasonalized claim columns.

to_base_rates(*, completion: CompletionAssumption | None = None, seasonality: SeasonalityAssumption | None = None, complement: Assumption | Any | None = None, extra_record_cols: Iterable[str] = ()) DataFrame[source]

Aggregate prepared experience to one row per record and claim type.

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: object

Project 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:

  1. experience_claim_rate is trended from each record’s experience_midpoint to the blend basis (trended_experience_rate).

  2. The trended experience is credibility blended with complement_claim_rate as stated (credible_claim_rate).

  3. The blended rate is trended from the blend basis to each period’s midpoint (trended_claim_rate).

  4. Seasonality redistributes within the year and rate_loads are added, flat and outside the blend (projected_claim_rate).

  5. Rates are multiplied by exposure (projected_claims).

Cost levels — complement_basis declares 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: Assumption

Claim 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: Assumption

Credibility 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: object

Create 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: object

Project expenses with per-exposure, fixed, premium, or claims bases.

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: object

Project 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. A RenewalRateActions schedule is applied once in the period containing each supplied effective date. The adjusted rate is carried forward to all subsequent periods.

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: object

Column roles for lifecycle and actuarial dates.

class ProjectionHorizon(start: Any, periods: int | None = None, end: Any | None = None, frequency: str = 'monthly')[source]

Bases: object

A 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 periods or end.

  • end (Any | None) – Last date to include. Supply either periods or end.

  • 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.

to_frame() DataFrame[source]

Return one row per projection period.

exception ProjectionModelsError[source]

Bases: Exception

Base exception for projectionmodels.

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: object

Detailed 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 grain identifies 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: object

One-time rate actions keyed by projection record and effective date.

rate_action values are decimal changes: 0.10 means a 10% increase. Each action is applied once in the projection period containing its effective date. Use a rate-action column on PremiumProjection instead 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: object

A 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: Assumption

Normalized 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: Assumption

Annual trend rate, supplied or fitted with actuarialpy.fit_trend().

factor(frame: DataFrame, months: Any) Series[source]

Resolve annual rates and return factors for scalar or row-wise months.

exception ValidationError[source]

Bases: ProjectionModelsError, ValueError

Raised when projection inputs are structurally invalid.