Start here →
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.
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
SAS2PY Parser
Deterministic parse
AI optional
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.
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.
The parser reads the flow and emits a LazyFrame plan, not a notebook full of collect().
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.
Same row-level compare. Different engine.
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.
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.
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.
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.
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.
A DATA step subset plus PROC MEANS — emitted as a lazy Polars plan that does not run until sink.
/* SAS */ data gold; set txn; if amount > 1000; run; proc means data=gold noprint; class segment; var amount; output out=sum sum=; run;
# 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.
Polars SQL is the simple path. Joins and windows go to DuckDB on the same box, then back to Polars.
/* 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;
# 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.
| SAS | Polars path | Notes |
|---|---|---|
| DATA step | LazyFrame / expressions | Lazy until sink |
| Simple PROC SQL | Polars SQL | Select, filter, light agg |
| Relational PROC SQL | DuckDB, result as Polars | Joins, CTEs, windows |
| Large Iceberg, little SQL | Iceberg scan → expressions | Existing catalog |
| SQL over Iceberg | DuckDB reads Iceberg | No full-table preload |
| EG project | Python module | One flow, one file |
| SAS dataset | Parquet / IPC / Iceberg | Columnar |
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 →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 →