Executive Summary
A US property and casualty carrier ran actuarial, reserving, and statutory reporting on a 16-node SAS Grid. The job count was the problem — 2,700 Enterprise Guide programs, 2.1 million lines, 860 macros — not the data size. Median working set was 3.4 GB. P95 was 38 GB. A Spark cluster would have spent more time in task scheduling than in compute, and the actuarial staff would have inherited executor-memory folklore they did not ask for. MigryX converted the estate to Polars LazyFrames (expressions + pl.scan_parquet) running as Kubernetes Jobs. Eleven months later the Grid is gone. Three-year savings: $3.6 million. The 41 programs that spilled past a 64 GB worker were not “forced into Polars”; they went to a small Spark fallback the parser already knew how to emit.
Client Overview
The analytics org is actuaries and statutory accountants, not a data-platform army. They live in SAS because PROC SUMMARY, PROC UNIVARIATE, and a 15-year reserving macro library are how triangle and indication work gets signed. Data arrives from a policy admin system and a claims warehouse as nightly extracts — wide, not web-scale. The Grid existed because SAS licensing was sold as a grid, not because 3 GB files needed 16 nodes.
IT had a standing offer from the enterprise data team: “put it on the Spark lakehouse.” A two-week spike of 40 programs showed what anyone who has profiled SAS EG jobs already knows. Startup + shuffle for a 2 GB join was slower than SAS on one box, and the error messages were worse. Polars was already in a few Python sandboxes. The question was whether 2,700 programs could become that, with lineage and a replay harness, without each actuary rewriting their book.
Business Challenge
- Thousands of jobs, medium data. Treating this like a petabyte migration would have been the expensive mistake. The architecture had to be a job factory, not a cluster.
- Reserving macros are a product. 860 macros, some four levels deep, encode triangle logic and statutory exhibit mappings. Expanding them into unreadable Python would have failed model-risk review.
- SAS missing and formats. Actuarial code depends on
.propagating through arithmetic and on format catalogs for line-of-business and state. Polars nulls are not SAS missing. The emitter had to implement the rule, not hope. - first./last. and RETAIN. Loss triangles are sequential by accident-year / development. These map to
over()/shift(), not togroup_by().agg()slogans. - Model risk / SR 11-7 adjacent. Even as a P&C shop (not a Fed bank), internal audit required output equivalence and change control. “It’s faster in Rust” is not a control.
- 41 jobs over 64 GB. Honest split: Polars workers have a memory ceiling. Those jobs were classified up front, not discovered in production OOM.
The MigryX Approach
Discovery measured working-set size from Grid logs (real mem, not file size). That classifier is the architecture: ≤64 GB peak → Polars Job; above → Spark module using the same logical plan where possible. 2,659 programs went Polars. 41 went Spark. No third runtime.
The emitter prefers LazyFrame expressions over eager DataFrame and over Python loops. PROC MEANS/SUMMARY/UNIVARIATE become group_by + expressions. PROC SQL becomes Polars SQL or an expression plan when the SQL used SAS extensions. DATA-step MERGE with BY becomes join plus explicit coalesce rules that match SAS. Formats become a sidecar Parquet lookup joined once, cached on the worker.
Macros that were functions became Python functions with typed parameters. Macros that were include-and-mutate-global became explicit argument passing — that is the only “rewrite” style audit accepted. We did not emit a SAS macro emulator.
Orchestration is existing Airflow on EKS. Each program is a Kubernetes Job with a memory request taken from the Grid profile (p95 × 1.4, capped at 64 GB). That mapping is why the first month of production did not page the platform team every night.
Target Architecture
SAS Grid → MigryX → Polars workers on Kubernetes (Spark only if >64 GB)
RuntimePolars is the default because the data fits. The 41 Spark jobs are listed in the runbook by name. That list is the difference between an architecture and a hope.
Expression map that survived actuarial review
| SAS | Polars | Note |
|---|---|---|
| PROC MEANS / SUMMARY | group_by().agg() | CLASS → keys; VAR → expressions; _TYPE_ only if used |
| PROC UNIVARIATE | quantiles + custom moments | Extreme-value options documented per program |
| PROC SQL | Polars SQL or expression plan | CALCULATED / remerge flattened |
| DATA MERGE + BY | join + coalesce | IN= as boolean columns |
| RETAIN / first. last. | over() + shift() | Triangle AY/dev order locked |
| Formats | LUT join | No map_dict surprises on unseen codes |
| PROC REG (small) | statsmodels on to_pandas() | Only when n is small; else Spark fallback |
Estate Inventory and Cutover Waves
| Domain | Programs | LOC | p95 working set | Wave |
|---|---|---|---|---|
| Personal reserving | 620 | 480k | 12 GB | 1 |
| Commercial reserving | 410 | 390k | 28 GB | 2 |
| Statutory / exhibits | 540 | 410k | 8 GB | 2–3 |
| Rating / relativities | 480 | 360k | 18 GB | 3 |
| Finance close | 310 | 240k | 6 GB | 4 |
| Shared macros + 41 large | 340 + 41 | 220k | 41 jobs 70–140 GB → Spark | All |
Close week is still close week. Finance programs dual-ran two month-ends before Grid disable. Reserving dual-ran one quarter. We did not “go live on a Friday.”
What we will not claim
- Polars did not replace a 200-node lakehouse. It replaced a SAS Grid that was oversized for 3–40 GB jobs.
- 2.1M LOC is real because macros are counted once and EG project junk (.egp XML) is not counted as SAS.
- Typical job: 4–12 minutes on Grid → 45–90 seconds on a 16–32 GB Polars worker. The 10× stories are the 8 GB SUMMARY jobs, not the 38 GB commercial triangles.
- $3.6M / 3 years is SAS license + Grid VM + SAS admin, minus EKS + S3. It is not a productivity fairy tale.
Results
"If you give actuaries Spark they will ask you to debug an executor. If you give them a Python module that prints the same triangle, they will sign it. Size the runtime to the file, not to the vendor deck."
— Chief Actuary, US P&C carrier
SAS jobs that do not need a cluster
Polars LazyFrames, Arrow, Parquet — thousands of programs, honest memory limits, Spark only where the profile says so.
Explore Polars modernization →
Start here →