ratingmodels

The pricing layer of the ecosystem: manual and experience rate construction, credibility blending, rate indication and rate-change decomposition, GLM relativity estimation, model evaluation (Gini and lift), and renewal constraints — an auditable build-up from base rate to filed rate. Depends on actuarialpy for its credibility and trend primitives.

Quickstart

Blend an experience rate with a manual rate and read the indicated change:

import ratingmodels as rm

z = rm.limited_fluctuation_credibility(n=96_000, n_full=120_000)

manual = rm.ManualRate(base_loss_cost=480, factors={"area": 1.05, "industry": 0.97})

indication = rm.RateIndication(
    experience_loss_cost=512,
    manual_loss_cost=manual.loss_cost(),
    credibility=z,
    current_rate=560,
    target_loss_ratio=0.85,
)

indication.indicated_rate_change()        # blended, credibility-weighted change
indication.rate_change_decomposition()    # attribute the change to each driver

The build-up engine

Rate build-ups are a sequence of typed steps — start, add, multiply, checkpoint — evaluated into a result that carries the full audit trail:

import ratingmodels as rm

result = rm.evaluate([
    rm.start("Par base claim cost", 941.63),
    rm.add("$30 specialist copay", -11.44),
    rm.multiply("Rating region", 1.083),
    rm.checkpoint("Net claim cost"),
])

result.value        # final per-unit value
result.to_frame()   # every step as a DataFrame — inputs, factors, running total

Because each step is explicit, the build-up is reproducible and reviewable: the same object renders the number and the audit trail behind it.

GLM relativities

GLMRelativities estimates rating factors jointly — correcting for the correlation between rating variables that one-way analysis cannot — with a log-link GLM fit by iteratively reweighted least squares. Poisson, gamma, and Tweedie variance functions; exposure as a log offset; prior weights; categorical predictors (base level = most populous, or set explicitly) and continuous covariates in the same linear predictor:

import ratingmodels as rm

model = rm.GLMRelativities(family="poisson").fit(
    df,
    response="claims",
    predictors=["area", "industry"],     # categorical -> relativities
    continuous=["age"],                  # numeric, enters the predictor directly
    exposure="member_months",            # log offset
    base_levels={"area": "A"},
)

model.relativities_["area"]     # multiplicative factors, base level = 1.0
model.base_value_               # exp(intercept): the base rate
model.summary()                 # coef, SE, z, relativity per term
model.predict(new_df, exposure="member_months")

Standard errors use the Pearson-estimated dispersion (quasi-likelihood — the robust default for pricing data, where overdispersion is the norm); dispersion_, null_deviance_, and a converged_ flag are exposed alongside. Unseen levels at prediction time fall back to the base level.

Model evaluation

A rating plan is judged on segmentation — how well predictions order risks. gini_coefficient is the exposure-weighted ordered-Lorenz Gini of pricing practice (normalized by the perfect model, so 1.0 means perfect segmentation and 0.0 none), and lift_table bands the book into equal-exposure groups by predicted risk:

pred = model.predict(df, exposure="member_months")

rm.gini_coefficient(df["claims"], pred, exposure=df["member_months"])

rm.lift_table(df["claims"], pred, exposure=df["member_months"], n_bands=10)
# band | n | exposure | predicted_mean | actual_mean | lift

A model that segments shows lift rising monotonically across bands; the Gini summarizes the same ordering in one number, comparable across books.

Pricing scenarios and margin

The indication answers what does the formula say; management pricing asks what margin falls out at the action actually issued, after concessions, at plan — and what action produces zero or a target margin. PricingEvaluation evaluates a case at any rate action with the same expense algebra as the gross-up, so at the indicated rate the margin ratio equals the retention’s profit_margin exactly:

import ratingmodels as rm

ret = rm.RetentionLoad(fixed_expense=8, variable_expense_ratio=0.10,
                       profit_margin=0.03, lae_ratio=0.02)
case = rm.PricingEvaluation(loss_cost=410, current_rate=470, retention=ret,
                            exposure=14_400, persistency=0.85)

case.at(0.062, name="issued")        # premium, gross margin, margin, ratio
case.rate_change_for_margin(0.03)    # closed form: P(m) = (L(1+lae)+F)/(1-V-m)
case.zero_margin_rate_change()       # the m = 0 special case

Evaluate named actions across a book into one tidy long table — cohort rollups and key-case exhibits are then pivots of library output — and solve the exhibit input “actions must be X% higher to hold the target margin” in closed form:

tidy = rm.scenario_frame(book, {"formula": formula_actions,
                                "issued": issued_actions, "plan": 0.118})
tidy.pivot(index="case", columns="scenario", values="margin_ratio")

rm.uplift_for_target_margin(book, issued_actions, target_margin=0.03)

Scenario names are your vocabulary — the library evaluates actions and reports margin; what “issued” or a concession budget means stays with the caller. Margin definitions are shared ecosystem-wide; see conventions.

API reference

ratingmodels – actuarial pricing and rate-indication tools.

A small, dependency-light toolkit for the group rating workflow: credibility, trend, manual and experience rate construction, credibility blending, rate indication, rate-change decomposition, GLM relativity estimation, and renewal constraints. Part of the OpenActuarial ecosystem.

Quick start:

import ratingmodels as rm

exp = rm.ExperienceRate(
    incurred_claims=4_200_000, exposure=96_000,
    trend_annual=0.075, trend_years=1.5,
    pooled_excess=350_000, pooling_charge=4.0,
    target_loss_ratio=0.85,
)
man = rm.ManualRate(base_loss_cost=480, factors={"area": 1.05, "industry": 0.97})
z = rm.limited_fluctuation_credibility(n=96_000, n_full=120_000)
ind = rm.RateIndication(
    experience_loss_cost=exp.loss_cost(),
    manual_loss_cost=man.loss_cost(),
    credibility=z, current_rate=560, target_loss_ratio=0.85,
    trend_total_factor=exp.trend_factor(),
)
round(ind.indicated_rate_change(), 4)
full_credibility_standard(p: float = 0.90, k: float = 0.05, cv_severity: float | None = None) float[source]

Expected claim count required for full credibility.

Delegates to actuarialpy.full_credibility_claims(). Returns \((z_{(1+p)/2}/k)^2\), inflated by \(1 + \mathrm{cv}^2\) when cv_severity is supplied (aggregate losses rather than pure frequency).

>>> round(full_credibility_standard(0.90, 0.05))
1082
limited_fluctuation_credibility(n: float, n_full: float) float[source]

Partial credibility by the square-root rule, min(1, sqrt(n / n_full)).

Delegates to actuarialpy.limited_fluctuation_z(). n and n_full are in consistent units (claims, policies, exposure units, …).

buhlmann_credibility(exposure: float, epv: float, vhm: float) float[source]

Bühlmann credibility factor \(Z = n / (n + k)\), k = EPV/VHM.

This is the credibility factor given structural parameters; the greatest-accuracy estimators (fitting EPV/VHM from data) live in actuarialpy.Buhlmann / actuarialpy.BuhlmannStraub.

buhlmann_straub(data: DataFrame, group: str, period: str, value: str, exposure: str) BuhlmannStraubResult[source]

Empirical Bühlmann-Straub credibility from grouped exposure data.

Thin wrapper over actuarialpy.BuhlmannStraub.from_frame() (the general unbiased estimators) that returns a BuhlmannStraubResult with per-group credibility and credibility-weighted means.

Parameters:
  • data (DataFrame) – Long-format data: one row per (group, period).

  • group (str) – Column names. value is the per-unit observation (e.g. loss per member-month); exposure is the weight \(m_{ij}\).

  • period (str) – Column names. value is the per-unit observation (e.g. loss per member-month); exposure is the weight \(m_{ij}\).

  • value (str) – Column names. value is the per-unit observation (e.g. loss per member-month); exposure is the weight \(m_{ij}\).

  • exposure (str) – Column names. value is the per-unit observation (e.g. loss per member-month); exposure is the weight \(m_{ij}\).

class BuhlmannStraubResult(k: float, epv: float, vhm: float, overall_mean: float, group_means: Series, credibility: Series, credibility_weighted: Series)[source]

Bases: object

Result of an empirical Bühlmann-Straub fit, keyed by group.

trend_factor(annual_trend: float, years: float) float[source]

\((1 + \text{annual\_trend})^{\text{years}}\).

trend_factor_between(annual_trend: float, experience_period: tuple[str | date | datetime, str | date | datetime], rating_period: tuple[str | date | datetime, str | date | datetime]) float[source]

Midpoint-to-midpoint trend factor from two date ranges.

apply_trend(value: float, annual_trend: float, years: float) float[source]

Trend a value forward (or back, for negative years).

combine_trend(util_trend: float, cost_trend: float) float[source]

Combine utilization and unit-cost trends: \((1+t_u)(1+t_c)-1\).

split_total_trend(total_trend: float, util_trend: float) float[source]

Back out the unit-cost trend implied by a total and a utilization trend.

period_midpoint(start: str | date | datetime, end: str | date | datetime) date[source]

Midpoint date of a period [start, end] (inclusive endpoints).

years_between(start: str | date | datetime, end: str | date | datetime) float[source]

Fractional years between two dates using a 365.25-day year.

class FactorTable(name: str, factors: Mapping, default: float = 1.0)[source]

Bases: object

A named lookup of level -> multiplicative relativity.

Parameters:
  • name (str) – Rating variable name (e.g. "area").

  • factors (mapping) – Level -> relativity. The base level should map to 1.0 by convention.

  • default (float) – Relativity returned for unknown levels. Default 1.0.

normalized(base_level) FactorTable[source]

Rebase so base_level has relativity 1.0.

one_way_relativities(data: DataFrame, factor: str, response: str, exposure: str | None = None, base_level=None) Series[source]

One-way relativities: each level’s (exposure-weighted) mean / overall mean.

Does not adjust for correlation with other rating variables; use GLMRelativities when variables are correlated.

class GLMRelativities(family: str = 'poisson', var_power: float | None = None, max_iter: int = 100, tol: float = 1e-08)[source]

Bases: object

GLM (log-link) relativity estimator fit by IRLS.

Parameters:
  • family ({"poisson", "gamma", "tweedie"}) – Response distribution. "tweedie" requires var_power in (1, 2).

  • var_power (float, optional) – Tweedie variance power \(p\) in \(V(\mu)=\mu^p\).

  • max_iter (int) – Maximum IRLS iterations.

  • tol (float) – Convergence tolerance on the relative change in deviance.

coefficients_

Fitted \(\beta\) including the intercept.

Type:

pandas.Series

relativities_

Per-variable multiplicative relativities (base level = 1.0).

Type:

dict[str, pandas.Series]

base_value_

\(\exp(\text{intercept})\), the fitted base level.

Type:

float

n_iter_

IRLS iterations used.

Type:

int

deviance_

Final deviance. These attributes are populated by fit().

Type:

float

fit(data: DataFrame, response: str, predictors: Sequence[str], exposure: str | None = None, offset: str | None = None, weights: str | None = None, base_levels: Mapping[str, object] | None = None, continuous: Sequence[str] = ()) GLMRelativities[source]

Fit relativities for predictors against response.

exposure enters as a log offset (the natural choice for counts and pure premium). An explicit offset column (already on the log scale) and prior weights may also be supplied. base_levels maps a predictor to its reference level (relativity 1.0); unspecified predictors use their most populous level as the base.

predict(data: DataFrame, exposure: str | None = None, offset: str | None = None) ndarray[source]

Predicted mean for new rows.

Categorical levels unseen in fitting fall back to the base level (relativity 1.0). exposure multiplies the mean; offset is a column already on the log scale.

summary() DataFrame[source]

Coefficient table: estimate, quasi-likelihood SE, z, relativity.

Standard errors use the Pearson-estimated dispersion (quasi-likelihood / quasi-Poisson style), which is the robust default for pricing data where overdispersion is the norm.

gini_coefficient(actual, predicted, exposure=None, normalize: bool = True) float[source]

Ordered-Lorenz Gini of predicted as a risk ranker for actual.

Parameters:
  • actual (array-like) – Observed outcome per record (losses, claim counts, pure premium).

  • predicted (array-like) – Model prediction used to order records from lowest to highest risk.

  • exposure (array-like, optional) – Weights (earned exposure). Equal weights if omitted.

  • normalize (bool) – If True (default), divide by the Gini of the perfect model that sorts by actual itself, so 1.0 means perfect segmentation and 0.0 means no segmentation. If False, return the raw ordered-Lorenz Gini.

lift_table(actual, predicted, exposure=None, n_bands: int = 10) DataFrame[source]

Exposure-weighted lift table: records banded by predicted risk.

Records are sorted by predicted and split into n_bands bands of (approximately) equal total exposure. Within each band the table reports exposure, the exposure-weighted actual and predicted means, and lift – the band’s actual mean relative to the overall actual mean. A model that segments well shows lift rising monotonically across bands.

Returns:

Indexed 1..n_bands with columns n, exposure, predicted_mean, actual_mean, lift.

Return type:

pandas.DataFrame

class ManualRate(base_loss_cost: float, factors: ~typing.Mapping[str, float] = <factory>, target_loss_ratio: float = 0.85, retention: ~ratingmodels.loading.RetentionLoad | None = None)[source]

Bases: object

Build a manual rate from a base and a set of named relativities.

Parameters:
  • base_loss_cost (float) – Base loss cost (per exposure unit) at the rating-period level (see ratingmodels.base_rate_from_experience() to derive it).

  • factors (mapping) – Named relativities, e.g. {"area": 1.05, "industry": 0.97, ...}.

  • target_loss_ratio (float) – Claims / premium target used to gross up to a charged rate. Ignored when retention is supplied.

  • retention (RetentionLoad, optional) – Full expense / profit loading. When provided, the charged rate is built with the fundamental insurance equation instead of a single loss ratio, and fixed expense is applied per exposure unit (flat across cells).

breakdown() BuildUpResult[source]

Audit trail of the manual claims build-up (base x each relativity).

The final running total equals loss_cost() up to floating point.

loss_cost() float[source]

Expected manual loss cost (before expense/margin loading).

rate() float[source]

Charged manual rate per exposure unit.

Uses retention (the full gross-up) when supplied, otherwise loss cost / target_loss_ratio.

steps() list[source]

The manual claims build-up as an ordered list of steps.

manual_loss_cost(base_loss_cost: float, factors: Sequence[float]) float[source]

Base loss cost scaled by the product of relativities.

aggregate_demographic_factor(census: DataFrame, factor_col: str, weight_col: str = 'count') float[source]

Weighted average of a unit-level demographic factor (e.g. an age/sex factor weighted by member counts).

class ExperienceRate(incurred_claims: float, exposure: float, trend_annual: float = 0.0, trend_years: float = 1.0, pooled_excess: float = 0.0, pooling_charge: float = 0.0, benefit_factor: float = 1.0, demographic_factor: float = 1.0, target_loss_ratio: float = 0.85, retention: RetentionLoad | None = None)[source]

Bases: object

Develop an experience rate from incurred claims and exposure.

Parameters:
  • incurred_claims (float) – Total incurred (completed) claims over the experience period.

  • exposure (float) – Exposure units (member-months, policy months, earned exposures, …).

  • trend_annual (float) – Annual claims trend.

  • trend_years (float) – Years from experience midpoint to rating midpoint.

  • pooled_excess (float) – Claim dollars removed by pooling (from pool_claims()). Default 0.

  • pooling_charge (float) – Pooling charge added back, per exposure unit. Default 0.

  • benefit_factor (float) – Multiplicative adjustments for benefit/demographic changes between the experience and rating periods. Default 1.0.

  • demographic_factor (float) – Multiplicative adjustments for benefit/demographic changes between the experience and rating periods. Default 1.0.

  • target_loss_ratio (float) – Claims / premium target used to load to a charged rate.

loss_cost() float[source]

Trended, pooled, adjusted experience loss cost (charge added back).

pooled_loss_cost() float[source]

Pooled (capped) claims per exposure unit, before trend.

rate() float[source]

Charged experience rate per exposure unit.

Uses retention (the full gross-up) when supplied, otherwise loss cost / target_loss_ratio.

pool_claims(claims, pooling_point: float) tuple[float, float][source]

Split claims into a pooled (capped) total and the excess above P.

Returns (capped_total, excess) where excess = sum(max(0, claim - pooling_point)).

expected_excess_charge(claims, pooling_point: float, exposure: float) float[source]

Naive pooling charge per exposure unit: observed excess spread over exposure.

A filed pooling charge is normally derived from book-wide excess experience or an EVT tail model (see the extremeloss package); this helper gives the simple group-level estimate.

base_rate_from_experience(data: DataFrame, exposure: str, loss: str, relativity: str | None = None, factor_cols: Sequence[str] | None = None) BaseRateResult[source]

Indicated base loss cost from book experience (off-balance method).

Returns \(B = \sum_i L_i / \sum_i e_i r_i\) together with the average relativity and average loss cost. Gross base_loss_cost to a charged base rate with a ratingmodels.RetentionLoad.

Parameters:
  • data (DataFrame) – One row per risk or rating cell.

  • exposure (str) – Column names for exposure (e.g. member-months) and trended/developed loss.

  • loss (str) – Column names for exposure (e.g. member-months) and trended/developed loss.

  • relativity (str, optional) – Column of precomputed relativities \(r_i\).

  • factor_cols (sequence of str, optional) – Columns of individual rating factors to multiply into \(r_i\) (used when relativity is not supplied).

class BaseRateResult(base_loss_cost: float, average_relativity: float, average_loss_cost: float, total_exposure: float)[source]

Bases: object

Result of base_rate_from_experience().

average_relativity(data: DataFrame, exposure: str, relativity: str | None = None, factor_cols: Sequence[str] | None = None) float[source]

Exposure-weighted average relativity \(\bar r = \sum e_i r_i / \sum e_i\).

Supply relativities either as a single relativity column or as factor_cols (per-row factors that are multiplied together).

off_balance_factor(current_avg_relativity: float, new_avg_relativity: float) float[source]

Off-balance correction \(\bar r_0 / \bar r_1\) from revising relativities.

rebalance_base_rate(current_base: float, current_avg_relativity: float, new_avg_relativity: float, overall_change: float = 0.0) float[source]

Off-balanced new base rate \(B_1 = B_0 (\bar r_0/\bar r_1)(1+\Delta)\).

Holds the overall premium level neutral when relativities change, then applies the intended overall rate change overall_change (\(\Delta\)).

class Step(op: str, label: str, operand: float = 1.0, weight: float = 1.0)[source]

Bases: object

A single build-up operation. operand is the factor (multiply / segment), amount (add), or value (start); weight is used by segment_multiply only.

start(label: str, value: float) Step[source]

Set the running total to value (normally the first step).

multiply(label: str, factor: float) Step[source]

Multiply the running total by factor (a relativity or trend).

add(label: str, amount: float) Step[source]

Add amount to the running total (negative for a copay credit).

segment_multiply(label: str, factor: float, weight: float) Step[source]

Apply factor to a fraction weight of the running total.

\(\text{running} \leftarrow \text{running}\,(1 - w + w f)\).

checkpoint(label: str) Step[source]

Record a labeled subtotal without changing the running total.

evaluate(steps: Sequence[Step]) BuildUpResult[source]

Run an ordered sequence of Step and return a BuildUpResult.

The running total starts at 0; a leading start() sets the base.

class BuildUp[source]

Bases: object

Fluent builder for a build-up; sugar over a list of Step.

>>> r = (BuildUp()
...      .start("Par Base", 941.63)
...      .add("$30 specialist copay", -11.44)
...      .multiply("Rating Region", 1.083)
...      .checkpoint("Medical Par Base Claim Cost")
...      .evaluate())
class BuildUpResult(value: float, breakdown: ~pandas.DataFrame, subtotals: dict, steps: list = <factory>)[source]

Bases: object

Result of evaluating a build-up.

value

Final running total.

Type:

float

breakdown

One row per step: step, operation, label, operand, running_total. For segment_multiply the operand shown is the effective factor \((1 - w + w f)\), so the column reconciles by multiplication.

Type:

pandas.DataFrame

subtotals

Ordered mapping of checkpoint label -> running total at that point.

Type:

dict

steps

The raw steps (nominal factor and weight preserved).

Type:

list[Step]

subtotal(label: str) float[source]

Running total recorded at the named checkpoint.

participation_blend(par: BuildUpResult | float, nonpar: BuildUpResult | float, participation_rate: float, label: str = 'PPO Claim Cost') BuildUpResult[source]

In-/out-of-network blend \(\text{par}\,p + \text{nonpar}\,(1-p)\).

participation_rate is the in-network (par) share p.

combine_streams(streams: Mapping[str, BuildUpResult | float], label: str = 'Combined') BuildUpResult[source]

Additively combine named streams (e.g. {"Medical": ..., "Drug": ...}).

Implemented as a build-up (start + adds) so the result carries a running total and an audit trail.

class RetentionLoad(fixed_expense: float = 0.0, variable_expense_ratio: float = 0.0, profit_margin: float = 0.0, lae_ratio: float = 0.0)[source]

Bases: object

Expense and profit loads used to gross claims up to a charged rate.

Parameters:
  • fixed_expense (float) – Flat operating expense per exposure unit (a dollar amount, not a percentage of premium). Default 0.

  • variable_expense_ratio (float) – Sum of percent-of-premium loads: commission, premium tax, exchange / regulatory fees, and any admin expressed as a percentage of premium. Default 0.

  • profit_margin (float) – Target underwriting profit / contribution to surplus, as a percentage of premium. Default 0.

  • lae_ratio (float) – Loss adjustment expense as a percentage of claims. Default 0.

expense_and_profit_ratio(loss_cost: float) float[source]

Share of the gross rate going to expense and profit (1 - loss ratio).

classmethod from_items(fixed_expense: float = 0.0, variable_items: Mapping[str, float] | None = None, profit_margin: float = 0.0, lae_ratio: float = 0.0) RetentionLoad[source]

Construct from an itemized mapping of percent-of-premium loads.

variable_items (e.g. {"commission": 0.04, "premium_tax": 0.023, "aca_fees": 0.005, "admin_pct": 0.06}) is summed into the variable expense ratio.

gross_rate(loss_cost: float) float[source]

Gross a loss cost up to a charged rate via \((L(1+\text{lae})+F)/(1-V-Q)\).

implied_loss_ratio(loss_cost: float) float[source]

Loss ratio implied at a given claims level (claims / gross rate).

With a non-zero fixed expense this varies with the claims level; with only percentage loads it equals 1 - variable_expense_ratio - profit_margin.

property variable_and_profit: float

Combined percent-of-premium load \(V + Q\).

gross_rate(loss_cost: float, retention: RetentionLoad) float[source]

Functional form of RetentionLoad.gross_rate().

permissible_loss_ratio(retention: RetentionLoad, loss_cost: float) float[source]

Functional form of RetentionLoad.implied_loss_ratio().

blend(experience: float, manual: float, credibility: float) float[source]

\(Z \cdot \text{experience} + (1-Z)\cdot \text{manual}\).

class RateIndication(experience_loss_cost: float, manual_loss_cost: float, credibility: float, current_rate: float, target_loss_ratio: float = 0.85, current_premium: float | None = None, exposure: float | None = None, trend_total_factor: float = 1.0, benefit_factor: float = 1.0, demographic_factor: float = 1.0, retention: RetentionLoad | None = None)[source]

Bases: object

Develop an indicated rate from experience and manual inputs.

Parameters:
  • experience_loss_cost (float) – Trended, pooled, adjusted experience loss cost (per exposure unit) (see ratingmodels.ExperienceRate).

  • manual_loss_cost (float) – Manual loss cost at the rating-period level (see ratingmodels.ManualRate).

  • credibility (float) – Credibility Z assigned to experience, in [0, 1].

  • current_rate (float) – Current charged rate per exposure unit.

  • target_loss_ratio (float) – Claims / premium target used to load claims to a charged rate.

  • current_premium (float, optional) – On-level earned premium over the experience period; required only for the loss-ratio method.

  • exposure (float, optional) – Exposure units over the experience period; required for the loss-ratio method (with current_premium) to form an experience loss ratio.

  • trend_total_factor (float) – Total claims trend factor \((1+t)^\Delta\); used by the loss-ratio method’s trend-only side and by the decomposition. Default 1.0.

  • benefit_factor (float) – Driver factors for the rate-change decomposition. Default 1.0.

  • demographic_factor (float) – Driver factors for the rate-change decomposition. Default 1.0.

indicated_rate() float[source]

Indicated charged rate (build-up method).

indicated_rate_change() float[source]

Proportional change implied by the build-up indicated rate.

loss_ratio_indication() float[source]

Credibility-weighted loss-ratio rate change.

Experience side: exp_LR / target_LR - 1. Trend-only side: trend_total_factor - 1.

rate_change_decomposition() RateChangeDecomposition[source]

Decompose the build-up indicated change into named drivers.

Drivers:

  • trend – the claims trend factor,

  • experience – the credibility effect, blended/manual claims (equals 1 when Z = 0),

  • benefit and demographic – supplied adjustment factors.

Any remaining movement (rate adequacy / loading) is absorbed by an explicit residual factor so the parts reconcile to the total.

decompose_rate_change(factors: Mapping[str, float], total_factor: float | None = None) RateChangeDecomposition[source]

Attribute a rate change to multiplicative drivers.

Parameters:
  • factors (mapping) – Named driver factors (e.g. {"trend": 1.075, "experience": 0.96, "benefit": 1.02, "demographic": 1.01}). Each must be positive.

  • total_factor (float, optional) – Independently computed total change factor (indicated / current). If given and it differs from the product of factors, a residual factor is appended so the decomposition reconciles exactly. If omitted, the total is taken to be the product of the supplied factors.

class RateChangeDecomposition(total_factor: float, factors: Series, contributions: Series)[source]

Bases: object

Result of decompose_rate_change().

cap_change(change: float, cap: float | None = None, floor: float | None = None) float[source]

Clip a proportional rate change to [floor, cap] (either may be None).

apply_cap(current_rate: float, indicated_rate: float, cap: float | None = None, floor: float | None = None) float[source]

Return the charged rate after capping the implied change.

band(change: float, deadband: float = 0.0, step: float | None = None) float[source]

Snap a change to zero within deadband; optionally to step grid.

round_rate(rate: float, ndigits: int = 2) float[source]

Round a rate to a filed precision (default cents).

corridor(current_rate: float, indicated_rate: float, max_up: float, max_down: float) float[source]

Limit a single renewal move to [-max_down, +max_up] proportionally.

renew(current_rate: float, indicated_rate: float, cap: float | None = None, floor: float | None = None, round_to: int | None = 2) RenewalAction[source]

Apply caps/floors (and optional rounding) to an indicated rate.

class RenewalAction(current_rate: float, indicated_rate: float, proposed_rate: float, indicated_change: float, proposed_change: float, capped: bool)[source]

Bases: object

Result of renew().

unit_level_renewal(census: DataFrame, base_rate: float, factor_cols: list[str], count_col: str = 'count') DataFrame[source]

Re-rate each census row as base_rate * product(factor_cols).

Returns the census with unit_rate and premium columns; the group total is the sum of premium.

class PricingEvaluation(loss_cost: float, current_rate: float, retention: RetentionLoad | None = None, exposure: float | None = None, persistency: float | None = None)[source]

Bases: object

A case’s pricing state, evaluable at arbitrary rate actions.

Parameters:
  • loss_cost (float) – Expected loss cost per exposure unit over the rating period (trended, pooled, credibility-blended – e.g. RateIndication.blended_loss_cost()).

  • current_rate (float) – Current charged rate per exposure unit that rate changes apply to.

  • retention (RetentionLoad, optional) – Expense structure. When omitted, no expenses are modeled: margin equals gross margin (premium less losses) and the inverse solve reduces to \(P = L / (1 - m)\).

  • exposure (float, optional) – Rating-period exposure units; enables dollar outputs.

  • persistency (float in [0, 1], optional) – Renewal probability; enables expected_* outputs (premium and margin scaled by the probability the case is still on the books).

at(rate_change: float, *, name: str | None = None) ScenarioOutcome[source]

Evaluate the case at a given proportional rate change.

classmethod from_indication(indication: RateIndication, *, exposure: float | None = None, persistency: float | None = None) PricingEvaluation[source]

Adopt a RateIndication’s blended loss cost, rate, and retention.

With a retention on the indication, evaluating at indicated_rate_change() returns a margin ratio equal to the retention’s profit_margin. Without one the indication grosses by target loss ratio, expenses are unmodeled here, and margin equals gross margin.

premium_for_margin(target_margin: float) float[source]

Charged rate (per exposure unit) at which the margin ratio equals the target.

Closed form: \(P = (L(1+\text{lae}) + F) / (1 - V - m)\). The target may be negative (a planned loss) but must satisfy \(m < 1 - V\) for a positive, finite rate.

rate_change_for_margin(target_margin: float) float[source]

Proportional rate change that yields the target margin ratio.

zero_margin_rate_change() float[source]

Rate change at which the underwriting margin is exactly zero.

class ScenarioOutcome(name: str | None, rate_change: float, premium_rate: float, loss_cost: float, loss_and_lae: float, expense_rate: float, loss_ratio: float, gross_margin_rate: float, margin_rate: float, margin_ratio: float, exposure: float | None = None, persistency: float | None = None, premium: float | None = None, gross_margin: float | None = None, margin: float | None = None, expected_premium: float | None = None, expected_margin: float | None = None)[source]

Bases: object

Result of evaluating one case at one rate action.

Per-exposure fields are always present. Dollar fields require exposure on the evaluation and are None otherwise; expected_* fields additionally require persistency and are the renewal-probability-weighted expectations (the deterministic counterpart of a retention Bernoulli).

as_dict() dict[str, Any][source]

Plain-dict view, one tidy row.

scenario_frame(cases: Mapping[Any, PricingEvaluation], scenarios: Mapping[str, float | Mapping[Any, float]]) DataFrame[source]

Evaluate named rate actions across cases into one tidy long table.

Parameters:
  • cases (Mapping[case_id, PricingEvaluation]) – The book, keyed however the caller identifies cases.

  • scenarios (Mapping[str, float | Mapping[case_id, float]]) – Each scenario is a rate change: a single float applied to every case, or a per-case mapping. A per-case mapping must cover every case – a missing action is an error, not a silent skip.

Returns:

One row per (case, scenario): case, scenario, rate_change, per-exposure economics, and dollar / persistency-weighted columns where the evaluation carries exposure and persistency. Any summary view – a cohort rollup, a key-case exhibit – is a pivot or groupby of this table.

Return type:

pd.DataFrame

uplift_for_target_margin(cases: Mapping[Any, PricingEvaluation], base_changes: Mapping[Any, float] | float, target_margin: float, *, mode: str = 'multiplicative', weight_by_persistency: bool = True) float[source]

Uniform uplift to a book’s rate actions that holds an aggregate margin.

Answers the exhibit input “to achieve the same target margin, rate actions must be X% higher”: when achieved actions slip below formula (concessions, caps), this is the across-the-board adjustment that restores the book’s aggregate margin ratio to the target.

Let case \(g\) have base premium \(P_g\) (at its base change), per-unit cost \(K_g = L_g(1+\text{lae}_g) + F_g\), variable load \(V_g\), and weight \(w_g\) = exposure units (times persistency when weight_by_persistency). The aggregate margin ratio is

\[m(P) = \frac{\sum_g w_g \left(P_g (1 - V_g) - K_g\right)} {\sum_g w_g P_g},\]

which is a ratio of functions affine in the uplift, so the solve is closed-form – no iteration:

  • multiplicative – new change \(a_g' = (1 + a_g)(1 + u) - 1\), so \(P_g(u) = P_g (1+u)\) and with \(A = \sum w P (1-V)\), \(B = \sum w K\), \(C = \sum w P\):

    \[1 + u = \frac{B}{A - m^\* C}.\]
  • additive – new change \(a_g' = a_g + u\), so \(P_g(u) = P_g + r_g u\) with current rate \(r_g\), and with \(A' = \sum w r (1-V)\), \(C' = \sum w r\):

    \[u = \frac{B + m^\* C - A}{A' - m^\* C'}.\]

Returns the uplift u. Feasibility (a positive solution exists and every resulting premium is positive) is validated with explicit errors.