A pricing model that treats age, BMI, and smoking status as separate, additive factors assumes each one adds a fixed amount to expected cost. Some risk factors may not work that way. The link between BMI and cost can differ for smokers and non-smokers, and a model term called an interaction lets the data show it.

This page tests that idea on made-up data. The worked example below builds a BMI by smoker effect into simulated claims, fits two models, and shows whether the added term is found. It cannot say how real policyholders or claims behave.

Worked Example (Simulated): Does the BMI Link to Cost Change for Smokers?

Question: In this made-up example, does the link between BMI and yearly claim cost change by smoker status?

Inputs: Simulated data, seed 2026. The script makes 1,200 made-up policyholders with ages from 18 to 64, BMI values from 18 to 42, and about one in four smokers. Yearly claim cost is 3,000 + 110 x age + 85 x BMI + 4,500 x smoker + 210 x BMI x smoker + random noise with a standard deviation of 2,200 dollars. So a BMI by smoker term of 210 dollars per BMI point is built in.

Method: The script sets 240 rows (one in five) aside for a test and fits two least squares models (lines fit to data) on the other 960 rows. One has no BMI by smoker term. The other lets the BMI link to cost change by smoker status. It prints the added term's estimate, its approximate 95% range of likely values, and the test error (RMSE, the usual size of a miss in a guess) for each model.

import math
import random

SEED = 2026
N = 1200
rng = random.Random(SEED)
rows = []
for i in range(N):
    age = rng.uniform(18.0, 64.0)
    bmi = rng.uniform(18.0, 42.0)
    smoker = 1.0 if rng.random() < 0.25 else 0.0
    noise = rng.gauss(0.0, 2200.0)
    charge = (3000.0 + 110.0 * age + 85.0 * bmi + 4500.0 * smoker
              + 210.0 * bmi * smoker + noise)
    rows.append((age, bmi, smoker, charge))

order = list(range(N))
rng.shuffle(order)
test_ids = set(order[:N // 5])
train = [rows[i] for i in range(N) if i not in test_ids]
test = [rows[i] for i in range(N) if i in test_ids]

def features(row, interaction):
    age, bmi, smoker, _ = row
    x = [1.0, age, bmi, smoker]
    if interaction:
        x.append(bmi * smoker)
    return x

def solve(a, b):
    m = [list(a[i]) + [b[i]] for i in range(len(b))]
    n = len(b)
    for col in range(n):
        pivot = max(range(col, n), key=lambda r: abs(m[r][col]))
        m[col], m[pivot] = m[pivot], m[col]
        scale = m[col][col]
        if abs(scale) < 1e-12:
            raise ValueError('singular matrix')
        m[col] = [v / scale for v in m[col]]
        for r in range(n):
            if r != col:
                factor = m[r][col]
                m[r] = [m[r][j] - factor * m[col][j] for j in range(n + 1)]
    return [m[i][-1] for i in range(n)]

def inverse(a):
    n = len(a)
    cols = []
    for j in range(n):
        unit = [1.0 if i == j else 0.0 for i in range(n)]
        cols.append(solve(a, unit))
    return [[cols[j][i] for j in range(n)] for i in range(n)]

def predict(beta, row, interaction):
    return sum(b * x for b, x in zip(beta, features(row, interaction)))

def fit(data, interaction):
    p = len(features(data[0], interaction))
    xtx = [[0.0] * p for _ in range(p)]
    xty = [0.0] * p
    for row in data:
        x = features(row, interaction)
        y = row[3]
        for j in range(p):
            xty[j] += x[j] * y
            for k in range(p):
                xtx[j][k] += x[j] * x[k]
    beta = solve(xtx, xty)
    residuals = [row[3] - predict(beta, row, interaction) for row in data]
    sigma2 = sum(e * e for e in residuals) / (len(data) - p)
    cov = inverse(xtx)
    se = [math.sqrt(max(0.0, sigma2 * cov[j][j])) for j in range(p)]
    return beta, se

def rmse(data, beta, interaction):
    errors = [row[3] - predict(beta, row, interaction) for row in data]
    return math.sqrt(sum(e * e for e in errors) / len(errors))

add_beta, _ = fit(train, False)
int_beta, int_se = fit(train, True)
interaction_est = int_beta[-1]
interaction_se = int_se[-1]
ci_low = interaction_est - 1.96 * interaction_se
ci_high = interaction_est + 1.96 * interaction_se
add_rmse = rmse(test, add_beta, False)
int_rmse = rmse(test, int_beta, True)
print(f'Simulated data, seed {SEED}')
print(f'Rows: {N}; train: {len(train)}; test: {len(test)}')
print('Outcome: annual claim cost in simulated dollars')
print(f'Interaction estimate (BMI x smoker): '
      f'{interaction_est:.1f} dollars per BMI point')
print(f'Approximate 95% confidence interval: [{ci_low:.1f}, {ci_high:.1f}]')
print(f'Additive model test RMSE: {add_rmse:.1f} dollars')
print(f'Interaction model test RMSE: {int_rmse:.1f} dollars')

Printed output:

Simulated data, seed 2026
Rows: 1200; train: 960; test: 240
Outcome: annual claim cost in simulated dollars
Interaction estimate (BMI x smoker): 240.3 dollars per BMI point
Approximate 95% confidence interval: [191.4, 289.2]
Additive model test RMSE: 2122.2 dollars
Interaction model test RMSE: 2103.5 dollars

Result: The fit finds 240.3 dollars per BMI point, with a range of 191.4 to 289.2 dollars. That range contains the built-in value of 210. The model with the term has a bit less test error (2103.5 dollars) than the model without it (2122.2 dollars). A small gain is expected, because the noise of 2,200 dollars sets the floor for both models; both test errors land near it. A gap of about 19 dollars on 240 test rows is one noisy draw, so do not read it as precise.

Limit: These made-up data cannot tell us how real people or claims act. The costs were built from the formula above, so finding the term here is expected. This is one run, and the test cannot show that a model will work on new real data.

Why Test for an Interaction

An additive model gives one BMI slope to everyone:

E[Cost] = b0 + b1(Age) + b2(BMI) + b3(Smoker)

An interaction model adds a term that lets the BMI slope change with smoker status:

E[Cost] = b0 + b1(Age) + b2(BMI) + b3(Smoker) + b4(BMI x Smoker)

Here b4 is the difference between the BMI slope for smokers and the BMI slope for non-smokers. In the example, the built-in slope is 85 dollars per BMI point for non-smokers and 295 dollars for smokers (85 + 210). The fitted model can show a link between smoker status, BMI, and charges in this data. By itself, it cannot show a cause in the body.

What an Additive Model Misses

With data built like the example, one shared BMI slope falls between the two true slopes. It is too steep for non-smokers and too shallow for smokers. That leaves the model under-predicting cost for smokers at high BMI and over-predicting it for smokers at low BMI. The interaction model fits a separate slope for each group, which is why it can do better on rows it has not seen.

How the Example Differs From Real Claims

The noise in the example is symmetric and has the same spread everywhere. Real claim costs may not behave that way, so check residual plots for patterns across risk segments before you trust a least squares fit. A different model for the outcome may be needed.

How to Check That an Interaction Term Earns Its Place

Fit two models on the same training rows, one additive and one with the interaction. Then compare them on rows set aside for a test.

  • The estimate and its range. A range that is wide, or that includes zero, means the data do not pin the term down.
  • Error on held-out rows. Compare the two models on the same test rows, and compare both to the noise in the data. A small gain may be within chance.
  • Residual patterns. Plot residuals against fitted values and against each predictor. A pattern means the model form is missing something.

When to Use This Approach

  • You suspect risk factors combine. If domain knowledge suggests one factor changes the effect of another, test for an interaction rather than assume an additive form.
  • You have rows in every group combination. The term is estimated from combinations such as high-BMI smokers. Too few rows in a combination give a wide range.
  • Your outcome is a number. Yearly cost or claim amount fits. Check how the errors behave before you rely on the fit.
  • You can test on rows the model has not seen. An in-sample fit is always optimistic.

Your turn

Bring your own data and the question you actually need answered.

MCP AnalyticsData science by email Send us your data and your question, and we will reply. ds@mcpanalytics.ai

Frequently Asked Questions

Why do interaction effects matter in insurance pricing?

An interaction term lets the effect of one factor depend on another. If the link between BMI and cost is steeper for smokers, a model with one BMI slope for everyone fits both groups worse. In the made-up example above, the built-in smoker slope is steeper by 210 dollars per BMI point.

What sample size do I need for reliable interaction terms?

There is no fixed number. It depends on how large the interaction is, how much the data varies, and how many rows fall in each group combination, such as high-BMI smokers. In the example above, 960 training rows with about one in four smokers gave a range of 191.4 to 289.2 dollars for a built-in term of 210. Small groups give wider ranges.

How do I know if my predictions are accurate enough for pricing?

Compare error on rows the model did not see, check residual plots for patterns across risk segments, and compare the error to the noise in the data. In the example, test error was 2103.5 dollars with the interaction term and 2122.2 without it, both near the built-in noise of 2,200 dollars. Any pricing cutoff is a business choice, not a statistical rule.

Can I use this model for individual premium quotes or just portfolio analysis?

Individual costs vary more than an average over many policies, so ranges for one person are wider than ranges for a portfolio average. The made-up example on this page cannot tell you the size of either range for real policies. Check your own model on held-out rows for each use.

Should I re-fit this model monthly, quarterly, or annually?

How often depends on how fast your costs and policy mix change. Track the model's error on new data over time and re-fit when it drifts. The example on this page cannot say how often real models drift.

What to Do Next

To try this on your own claims data, fit two models, one without and one with a BMI by smoker term, and compare their error on rows set aside for a test. Report the term's estimate and its range, not only whether it passes a test.

Related Articles