The Python-vs-R debate in actuarial circles has a frustrating quality: most of the discussion is conducted by people who strongly prefer one and have never seriously used the other. The Python people say R is archaic; the R people say Python’s actuarial ecosystem is thin. Both camps are partly right.

We use both. This post is not an argument for switching: it is an honest account of what each language actually gives you for UK personal lines pricing work, so you can make a sensible decision about where to invest your team’s tooling effort.

Our own libraries are Python. That is a deliberate choice we will explain. But we are not going to pretend R is a poor tool for actuarial work, because it is not.


Data wrangling: Polars vs the tidyverse

For manipulating claims and policy data before modelling, the comparison is between Python’s Polars (or pandas, though we prefer Polars) and R’s tidyverse, principally dplyr and tidyr.

The tidyverse approach to data wrangling is genuinely excellent. The dplyr grammar - filter, mutate, group_by, summarise, left_join - is readable, consistent, and well-documented. For an actuary doing exploratory work on a claims dataset, a few hundred lines of dplyr is about as concise as it gets. The pipe operator (|> in base R 4.1+, or %>% from magrittr) makes multi-step transformations read naturally.

library(dplyr)

# UK motor: compute earned exposure and loss ratio by vehicle group
claims_summary <- policies |>
  filter(accident_year >= 2020) |>
  mutate(
    earned_exposure = pmin(earned_car_years, 1.0),
    incurred_loss   = claim_count * avg_severity
  ) |>
  group_by(abi_group, accident_year) |>
  summarise(
    exposure      = sum(earned_exposure),
    claim_count   = sum(claim_count),
    incurred_loss = sum(incurred_loss),
    frequency     = sum(claim_count) / sum(earned_exposure),
    loss_ratio    = sum(incurred_loss) / sum(premium),
    .groups       = "drop"
  )

Polars does the same job with a different syntax. The key difference is that Polars uses lazy evaluation by default - operations are planned into an optimised query plan and executed together, which matters when you are working with 50 million claim records that do not fit comfortably in memory.

import polars as pl

# Same computation in Polars - lazy execution plan
claims_summary = (
    pl.scan_parquet("policies/*.parquet")  # lazy - no data loaded yet
    .filter(pl.col("accident_year") >= 2020)
    .with_columns([
        pl.col("earned_car_years").clip(0.0, 1.0).alias("earned_exposure"),
        (pl.col("claim_count") * pl.col("avg_severity")).alias("incurred_loss"),
    ])
    .group_by(["abi_group", "accident_year"])
    .agg([
        pl.col("earned_exposure").sum().alias("exposure"),
        pl.col("claim_count").sum(),
        pl.col("incurred_loss").sum(),
        (pl.col("claim_count").sum() / pl.col("earned_exposure").sum()).alias("frequency"),
        (pl.col("incurred_loss").sum() / pl.col("premium").sum()).alias("loss_ratio"),
    ])
    .collect()  # execute the query plan here
)

For a team whose data lives in parquet files on S3 or ADLS, Polars is materially faster than dplyr at scale. On a 10GB policy extract, the difference is seconds vs minutes for a typical aggregation pipeline. pandas is slower than both at that scale; we would not recommend it for production data wrangling in 2026.

For datasets that fit in memory (which is still most UK personal lines portfolios below 5 million policies), the performance difference is negligible. The dplyr code is arguably more readable, particularly for actuaries who are not full-time software engineers. This is an honest point in R’s favour.

One specific R advantage: data.table. If you are working with very large datasets in R and do not want to move to Python, data.table is competitive with Polars on speed and has better memory efficiency than dplyr. Many pricing teams use dplyr for the main pipeline and data.table for the performance-critical sections.


GLMs: statsmodels/glum vs R’s glm

GLMs are the workhorse of UK personal lines pricing. Both languages have solid implementations, but the character of the tools differs significantly.

R’s glm() function is integrated into base R and has been there since the beginning. It is correct, well-tested, and has a huge body of documentation. The formula interface is elegant:

library(tidyverse)

# Frequency GLM in R - clean formula interface
freq_model <- glm(
  claim_count ~ offset(log(exposure)) +
    factor(abi_group) + factor(ncd_years) + driver_age + vehicle_age +
    factor(postcode_district),
  data    = training_data,
  family  = poisson(link = "log"),
  weights = NULL
)

summary(freq_model)
# Coefficients are in log space; exp() gives multiplicative relativities
relativities <- exp(coef(freq_model))
confint(freq_model)  # Wald confidence intervals

The glm() formula interface handles offsets, interactions, and factor encoding natively. You get standard errors, z-statistics, and deviance statistics automatically. For a pricing actuary who has used Emblem, the summary.glm() output maps naturally to Emblem’s factor diagnostics.

The R ecosystem around GLMs is also strong: car::Anova() for type II/III likelihood ratio tests, emmeans for estimated marginal means, ggeffects for plotting factor effects. These are mature tools.

The Python alternative we use is glum, built by QuantCo. It is faster than statsmodels.GLM for large datasets (Cholesky factorisation path vs pure IRLS), supports per-coefficient L2 penalty matrices (useful for applying different regularisation strengths to main effects vs interactions), and returns coefficient standard errors - which sklearn.linear_model.PoissonRegressor does not.

from glum import GeneralizedLinearRegressor
import numpy as np

# Frequency GLM in Python with glum
freq_model = GeneralizedLinearRegressor(
    family="poisson",
    link="log",
    alpha=1e-4,       # light L2 regularisation
    solver="irls-cd",
    fit_intercept=True,
)

freq_model.fit(
    X_encoded,             # scipy sparse matrix from OneHotEncoder
    y_claim_counts,
    offset=np.log(exposure),  # log(exposure) as proper offset, not weight
)

# Extract factor table with standard errors
import polars as pl
factor_table = pl.DataFrame({
    "feature":    feature_names,
    "relativity": np.exp(freq_model.coef_),
    "std_err":    freq_model.std_errors_,
    "z_stat":     freq_model.coef_ / freq_model.std_errors_,
})

The glum syntax is less elegant than R’s formula interface. Encoding categoricals requires a separate OneHotEncoder step, which adds code and the risk of reference-level errors. R’s glm() handles factor() directly in the formula and picks sensible defaults for reference levels.

Our view: for quick exploratory GLM fitting and visualising factor effects, R’s formula interface is faster to write. For production GLM pipelines with regularisation, parallelism, and integration with Python-based downstream steps, glum is better. For a team that fits GLMs interactively and then ships coefficients to Emblem or Radar, R is perfectly adequate.


GBMs: CatBoost vs R’s gbm ecosystem

This is where the gap between Python and R is most pronounced.

R has gbm (the original Friedman implementation), xgboost (the R binding), lightgbm (the R binding), and catboost (an unofficial R wrapper that lags behind the Python version). The xgboost and lightgbm R bindings are reasonably current and functional for most use cases.

The problem is that the R GBM ecosystem treats these tools as statistical packages: you fit, you get predictions, you inspect variable importance. The production ML pipeline tooling - versioned model artefacts, REST API serving, integration with feature stores, monitoring in production - does not have mature R support. MLflow has an R client (mlflow package), but it is less capable than the Python SDK. Databricks’ R support exists but is second-class compared to Python.

In Python, the CatBoost workflow integrates directly with MLflow, with Databricks Unity Catalog, and with monitoring tools like Evidently. The entire path from training to production deployment is in one language.

import catboost as cb
import mlflow
import mlflow.catboost

# CatBoost with Tweedie loss, native categoricals, MLflow tracking
cat_cols = ["abi_group", "ncd_years", "postcode_district",
            "occupation_code", "payment_freq"]

pool = cb.Pool(
    data=X_train,
    label=y_pure_premium,
    weight=exposure_train,
    cat_features=cat_cols,
)

with mlflow.start_run():
    model = cb.CatBoostRegressor(
        loss_function="Tweedie:variance_power=1.5",
        iterations=2000,
        learning_rate=0.03,
        depth=6,
        monotone_constraints={"vehicle_age": 1, "ncd_years": -1},
        random_seed=42,
        verbose=200,
    )
    model.fit(pool)
    mlflow.catboost.log_model(model, "catboost-pricing-model")
    mlflow.log_metric("train_rmse", model.get_best_score()["learn"]["RMSE"])

The equivalent in R, using the xgboost R binding, is possible but requires more manual plumbing to get artefacts into MLflow and then into a serving endpoint. In practice, most UK insurers deploying GBMs in production use Python for the model training pipeline even if their actuarial team’s preferred language is R.

One genuine R advantage in the GBM space: tidymodels. If your team is already embedded in the tidyverse, tidymodels gives you a consistent interface to xgboost, lightgbm, and other engines with integrated hyperparameter tuning and cross-validation. It is better-designed than sklearn pipelines for interactive modelling work. The tradeoff is that it abstracts away some control that matters in production.

library(tidymodels)

# GBM via tidymodels - clean but less direct control
xgb_spec <- boost_tree(
  trees      = 2000,
  tree_depth = 6,
  learn_rate = 0.03,
  loss_reduction = 0
) |>
  set_engine("xgboost", objective = "reg:tweedie",
             tweedie_variance_power = 1.5) |>
  set_mode("regression")

xgb_fit <- xgb_spec |>
  fit(pure_premium ~ ., data = training_data)

The xgboost R binding does not natively support ordered target statistics for high-cardinality categoricals the way Python CatBoost does. For UK motor pricing with 400+ vehicle makes and 2,000+ model groups, this matters. See our post on CatBoost vs XGBoost for Insurance Pricing for the detail.


Deployment and Databricks

This is where the Python advantage is clearest and the decision is largely made for you.

If your insurer runs on Databricks - which is now the dominant data platform for UK insurers above a certain size - your operational environment is Python-first. Databricks Notebooks run Python and R, but the platform integrations that matter are Python-native: Delta Lake connectors, Unity Catalog model registry, MLflow experiment tracking, Feature Store, Model Serving endpoints.

Running R on Databricks works. The sparklyr package gives you a dplyr-compatible interface to Spark. But when your data engineering team has built the data pipelines in PySpark, when your feature store is Unity Catalog, and when your model serving endpoint expects a Python scoring function, the friction of running R in the same pipeline is real. You end up with a hybrid architecture where actuaries do model fitting in R notebooks and data engineers wrap it in Python for deployment. That handover is a persistent source of bugs and version drift.

Python GLMs and GBMs deploy to a Databricks Model Serving endpoint in a handful of lines:

import mlflow
from mlflow.models import infer_signature

# Register model in Unity Catalog
mlflow.set_registry_uri("databricks-uc")

with mlflow.start_run():
    signature = infer_signature(X_train, freq_model.predict(X_train))
    mlflow.sklearn.log_model(
        freq_model,
        "glm-frequency-model",
        registered_model_name="prod.insurance_pricing.motor_freq_glm",
        signature=signature,
    )
# Enable endpoint via Databricks UI or REST API

If your team is on AWS SageMaker or Azure ML rather than Databricks, the same pattern applies: the serving infrastructure expects Python. R models can be containerised and served, but it requires more bespoke engineering. The tooling ecosystem assumes Python.


Where R genuinely wins

We promised to be honest, so here it is.

Reserving packages. ChainLadder (Mack, bootstrap, Munich chain ladder) is the reference implementation of claims reserving methods. It is mature, correct, and widely used in UK general insurance. The Python equivalent is chainladder-python, which covers the core methods but has less community adoption and fewer actuaries stress-testing edge cases. For an actuarial team that does both pricing and reserving, R unifies your tooling in a way Python cannot yet match.

Actuarial-specific distributions. The actuar package implements the Klugman-Panjer-Willmot distribution family: Pareto, Burr, inverse Gaussian, Weibull, lognormal with full MLE support, coverage modifications, and loss elimination ratios. If you are fitting a severity distribution for excess of loss pricing, actuar is the standard tool. Python has scipy.stats with a subset of these distributions and fitter for automated distribution fitting, but the actuarial workflow around deductibles and coverage modifications is not as complete.

EDA and visualisation. ggplot2 is better than anything in Python for exploratory data analysis. That is a strong statement, but we believe it. The grammar-of-graphics approach, the layered theming system, the publication-quality defaults, the facet_wrap and facet_grid for small multiples - these genuinely accelerate the EDA phase of a pricing project. Python’s matplotlib requires too much manual adjustment for similar output; seaborn is closer but less flexible; plotnine (a ggplot2 port) is a good option but has occasional compatibility issues with pandas.

For producing factor charts - the double-lift charts, loss ratio by band, frequency curves - that actuaries use to justify factor selections, ggplot2 is faster to work with. This matters in a pricing review where you are producing 50 charts for an actuarial signoff meeting.

The actuarial community. Most UK actuarial education (IFoA CT4/CS2 modules, CAS materials, GIRO working party papers) uses R examples. If your team has junior actuaries coming through the IFoA exams, their R skills transfer directly to the pricing team’s tooling. Python is growing in actuarial education but R is still dominant in the structured curriculum as of 2026.


The advanced techniques gap

Where Python has pulled ahead in ways that matter for a modern UK pricing function is in the new methodological frontier: causal inference, uncertainty quantification, and production model monitoring.

These areas do have R implementations - grf (causal forests by Athey et al.) is excellent; conformalInference exists; modeltime covers some monitoring patterns. But the Python ecosystem is deeper, more actively developed, and better integrated with the deployment infrastructure.

Our Burning Cost libraries are a concrete example of what the Python ecosystem enables:

None of these exist as mature, production-ready R packages. The research base is often in R (the causal forests paper uses R; most conformal prediction papers have R code), but the production implementation tooling is Python.

This is not an argument that Python is inherently better at these methods. It reflects where the research-to-production pipeline is currently better developed. If your team wants to implement conformal prediction intervals on your GBM frequency model and run them in a Databricks endpoint, Python is the only practical path in 2026.


Package ecosystem summary

Task Python R Our view
Data wrangling (large scale) Polars data.table Python - Polars faster at scale
Data wrangling (interactive) Polars/pandas dplyr R - cleaner syntax, faster iteration
GLM fitting glum / statsmodels glm() Roughly equivalent; R formula interface is nicer
GBM fitting CatBoost / XGBoost xgboost / lightgbm Python - better categorical handling, native MLflow integration
Deployment (Databricks) First-class Second-class Python - no contest
Reserving chainladder-python ChainLadder R - more mature, better tested
Actuarial distributions scipy.stats + fitter actuar R - more complete actuarial workflow
EDA / visualisation plotnine / seaborn ggplot2 R - ggplot2 remains the standard
Conformal prediction insurance-conformal conformalInference Python - better production tooling
Causal inference insurance-causal / grf Python bindings grf Python - deployment integration
Model monitoring insurance-monitoring / Evidently modeltime (partial) Python - more complete monitoring ecosystem
Proxy discrimination insurance-fairness none (2026) Python only

The “use both” reality

Most UK pricing teams that have adopted Python have not abandoned R. The pattern we see is:

This is not a failure of standardisation. It reflects the genuine fact that both languages are better at different parts of the workflow, and the cost of switching is lower than the cost of using the wrong tool.

If you are setting up a new pricing team from scratch, we would recommend Python as the default with R skills expected for reserving and EDA. If you are an established team with deep R expertise, the case for a wholesale switch is not strong - the incremental gain in GLM and GBM quality does not justify the disruption. The case for adding Python capability alongside R is strong, particularly if you are on Databricks or want to implement any of the advanced monitoring or fairness techniques.


Our recommendation

Python wins if you are on Databricks or building a production ML pipeline. The deployment integration alone justifies it. If you want to implement any of the advanced techniques - conformal prediction intervals, causal forest factor analysis, automated proxy discrimination testing - Python is the only practical path today.

R wins for traditional actuarial analysis: reserving with ChainLadder, severity distribution fitting with actuar, and exploratory factor analysis with ggplot2. It also wins if your team’s primary output is actuarial reports and Excel, rather than production pricing engines.

Many teams should use both. That is not fence-sitting. It is the correct answer for most UK insurers with a mixed function covering pricing, reserving, and regulatory reporting. The languages interoperate reasonably well through reticulate (R calling Python) and rpy2 (Python calling R) for teams that need to share objects across environments, though in practice most teams keep the two cleanly separated by task.

The mistake we see most often is teams that have defaulted to one language for historical reasons and are now paying the cost in the one area where the other language is clearly better: R teams that cannot get models into Databricks without heroic engineering effort, and Python teams that have spent six months writing a ChainLadder implementation that R gives you for free.

Know which tool does which job. Use both.


Libraries mentioned in this post: glum, insurance-causal, insurance-conformal, insurance-monitoring, insurance-fairness. All Burning Cost Python libraries are on PyPI and GitHub.

Back to all articles