Start here →

Convert SAS programs to Polars

Enterprise Guide projects and DATA step parsed structurally. Emitted as Polars you run on a machine or Kubernetes — with DuckDB when PROC SQL is relational, and Iceberg when the lake is already the source.

Architecture

SAS in. Polars out — with the right engine for the job.

Deterministic parsers read the SAS estate and emit native Polars. Simple SQL stays in Polars. Heavy joins run in DuckDB on the same machine. Iceberg tables feed expressions without standing up Spark for a mid-size job.

SAS programs → SAS2PY parser → Polars, DuckDB, Iceberg

SAS
Base SAS DATA step / macros
DI Studio Jobs + mappings
EG / EM Projects + flows
Viya / CAS CASL + actions
SAS2PY Parser
Deterministic parse AI optional
Expression emit DATA step → LazyFrame
SQL routing Polars or DuckDB
Lake ingest Iceberg → Polars
Run
Polars LazyFrames + SQL
DuckDB Same machine
Iceberg Your catalog
Kubernetes When you need a fleet

AI is an optional add-on, off by default — the conversion runs end to end without it, air-gapped if your estate requires it. DuckDB and Iceberg use what you already installed; we do not stand up a new warehouse.

Why Polars

Most SAS jobs are not lakehouse-scale

Spark tax on mid-size work

500MB to 40GB jobs pay a cluster bill they do not earn. Polars runs them in-process, lazily. DuckDB sits beside it when PROC SQL needs real joins.

EG projects are not Spark jobs

The parser reads the flow and emits a LazyFrame plan, not a notebook full of collect().

The lake is already there

If the heavy tables live in Iceberg, we scan them into Polars — or let DuckDB query Iceberg and hand the result back. Same catalog you already configured.

You still need parity

Same row-level compare. Different engine.

How the job runs

Four modes. One Polars program.

DATA step always becomes Polars expressions. PROC SQL and lake reads pick the engine that fits the workload — chosen per conversion, not hardcoded in the SAS.

Polars SQL

Default for simple SELECT / filter / project on in-memory frames.

Short PROC SQL stays in Polars. No extra process. Fits filters, projections, and light aggregations.

DuckDB SQL

When PROC SQL looks like Spark SQL — joins, CTEs, windows.

DuckDB runs on the same machine as Polars, registers the frames, executes the SQL, and returns a Polars DataFrame. Closest in-process stand-in for a Spark SQL job without a cluster.

Iceberg → expressions

Little SQL. A lot of lake data. DATA step still wins.

Scan Iceberg from the catalog you already run. Then filter, derive, and aggregate in Polars — the same LazyFrame path as a SAS dataset.

DuckDB on Iceberg

Relational SQL over lake tables, result back in Polars.

DuckDB reads Iceberg directly. We do not pull the whole table into Polars first. Warehouse pass-through (Oracle, Teradata, …) is unchanged — those connections still go to the database.

DuckDB is assumed installed next to Polars. Iceberg credentials stay in your existing catalog setup.

Parser output

SAS filter to a LazyFrame

A DATA step subset plus PROC MEANS — emitted as a lazy Polars plan that does not run until sink.

SAS
/* SAS */
data gold;
  set txn;
  if amount > 1000;
run;
proc means data=gold noprint;
  class segment;
  var amount;
  output out=sum sum=;
run;
SAS2PY
converts
Polars
# DATA step + MEANS → Polars
import polars as pl
gold = pl.scan_parquet("txn.parquet").filter(
    pl.col("amount") > 1000
)
summary = gold.group_by("segment").agg(
    pl.col("amount").sum()
).collect()

scan_ + filter + group_by is the plan. collect() is the only action.

Parser output

PROC SQL that Spark would have handled

Polars SQL is the simple path. Joins and windows go to DuckDB on the same box, then back to Polars.

SAS
/* SAS */
proc sql;
  create table work.out as
  select a.segment,
         sum(b.amount) as total
  from gold a
  inner join lookup b
    on a.id = b.id
  group by a.segment;
quit;
SAS2PY
converts
DuckDB → Polars
# Relational SQL on the same machine
import duckdb
out = duckdb.sql("""
  SELECT a.segment, sum(b.amount) AS total
  FROM gold a
  JOIN lookup b ON a.id = b.id
  GROUP BY a.segment
""").pl()

Iceberg sources use your catalog: scan into Polars for DATA-step work, or query in DuckDB when the SQL is the point.

Coverage

SAS to Polars — artifact mapping

SASPolars pathNotes
DATA stepLazyFrame / expressionsLazy until sink
Simple PROC SQLPolars SQLSelect, filter, light agg
Relational PROC SQLDuckDB, result as PolarsJoins, CTEs, windows
Large Iceberg, little SQLIceberg scan → expressionsExisting catalog
SQL over IcebergDuckDB reads IcebergNo full-table preload
EG projectPython moduleOne flow, one file
SAS datasetParquet / IPC / IcebergColumnar
Validation

Every conversion validated to row-level parity

SAS output compared to Polars output — row by row, column by column. Differences flagged before sign-off. DuckDB and Iceberg paths use the same compare.

See how Data Matching works →
2,700
programs
2.1M
LOC
$3.6M
saved
11 mo
duration

P&C Carrier: 2,700 SAS programs to Polars

Actuarial and statutory jobs left a 16-node Grid for Polars LazyFrames. 41 programs over 64 GB used Spark on purpose, not by accident.

Read the case study →