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\) whencv_severityis 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().nandn_fullare 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 aBuhlmannStraubResultwith per-group credibility and credibility-weighted means.- Parameters:
data (DataFrame) – Long-format data: one row per (group, period).
group (str) – Column names.
valueis the per-unit observation (e.g. loss per member-month);exposureis the weight \(m_{ij}\).period (str) – Column names.
valueis the per-unit observation (e.g. loss per member-month);exposureis the weight \(m_{ij}\).value (str) – Column names.
valueis the per-unit observation (e.g. loss per member-month);exposureis the weight \(m_{ij}\).exposure (str) – Column names.
valueis the per-unit observation (e.g. loss per member-month);exposureis 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:
objectResult 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:
objectA 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_levelhas 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
GLMRelativitieswhen variables are correlated.
- class GLMRelativities(family: str = 'poisson', var_power: float | None = None, max_iter: int = 100, tol: float = 1e-08)[source]¶
Bases:
objectGLM (log-link) relativity estimator fit by IRLS.
- Parameters:
family ({"poisson", "gamma", "tweedie"}) – Response distribution.
"tweedie"requiresvar_powerin (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
- 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
predictorsagainstresponse.exposureenters as a log offset (the natural choice for counts and pure premium). An explicitoffsetcolumn (already on the log scale) and priorweightsmay also be supplied.base_levelsmaps a predictor to its reference level (relativity 1.0); unspecified predictors use their most populous level as the base.
- gini_coefficient(actual, predicted, exposure=None, normalize: bool = True) float[source]¶
Ordered-Lorenz Gini of
predictedas a risk ranker foractual.- 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
actualitself, 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
predictedand split inton_bandsbands of (approximately) equal total exposure. Within each band the table reports exposure, the exposure-weighted actual and predicted means, andlift– 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:
objectBuild 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
retentionis 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.
- 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:
objectDevelop 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.
- 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)whereexcess = 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
extremelosspackage); 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_costto a charged base rate with aratingmodels.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
relativityis not supplied).
- class BaseRateResult(base_loss_cost: float, average_relativity: float, average_loss_cost: float, total_exposure: float)[source]¶
Bases:
objectResult 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
relativitycolumn or asfactor_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:
objectA single build-up operation.
operandis the factor (multiply / segment), amount (add), or value (start);weightis used bysegment_multiplyonly.
- 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
amountto the running total (negative for a copay credit).
- segment_multiply(label: str, factor: float, weight: float) Step[source]¶
Apply
factorto a fractionweightof the running total.\(\text{running} \leftarrow \text{running}\,(1 - w + w f)\).
- evaluate(steps: Sequence[Step]) BuildUpResult[source]¶
Run an ordered sequence of
Stepand return aBuildUpResult.The running total starts at 0; a leading
start()sets the base.
- class BuildUp[source]¶
Bases:
objectFluent 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:
objectResult of evaluating a build-up.
- value¶
Final running total.
- Type:
float
- breakdown¶
One row per step:
step, operation, label, operand, running_total. Forsegment_multiplytheoperandshown 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
- 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_rateis the in-network (par) sharep.
- 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:
objectExpense 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:
objectDevelop 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
Zassigned 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.
- 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 whenZ = 0),benefitanddemographic– supplied adjustment factors.
Any remaining movement (rate adequacy / loading) is absorbed by an explicit
residualfactor 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, aresidualfactor 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:
objectResult 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 tostepgrid.
- 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:
objectResult 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_rateandpremiumcolumns; the group total is the sum ofpremium.
- class PricingEvaluation(loss_cost: float, current_rate: float, retention: RetentionLoad | None = None, exposure: float | None = None, persistency: float | None = None)[source]¶
Bases:
objectA 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:
marginequalsgross 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’sprofit_margin. Without one the indication grosses by target loss ratio, expenses are unmodeled here, and margin equals gross margin.
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.
- 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:
objectResult of evaluating one case at one rate action.
Per-exposure fields are always present. Dollar fields require
exposureon the evaluation and areNoneotherwise;expected_*fields additionally requirepersistencyand are the renewal-probability-weighted expectations (the deterministic counterpart of a retention Bernoulli).
- 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.