Differences from SAS language Data Step#
This page summarizes the key differences between the SAS language DATA step and limulus.
IO Support#
Standard support:
pyarrow.Table/polars.DataFrameOptional support:
pandas.DataFrame(pip install limulus[pandas])session.loads()accepts all three formats listed above
Support for sas7bdat input/output is not planned, as the format specification is proprietary.
If you need to work with sas7bdat files, use pandas or other libraries to load them first.
Type Mapping#
Arrow Type |
SAS language Equivalent |
Python |
|---|---|---|
|
Numeric |
|
|
Numeric |
|
|
Character |
|
|
Numeric (0/1) |
|
|
Numeric |
|
Arrow supports a wide variety of data types.
Complex types such as structs that do not exist in SAS are not expected to be used in logic, but can be retained as columns without issue.
Behavioral Differences#
Labels#
Dataset labels and column labels are supported, but they are stored as Arrow metadata rather than as a separate display-layer construct. Dataset labels live in schema metadata under memlabel, and column labels live in per-field custom metadata.
Dataset Option Type Preservation#
Physical types are usually preserved through processing.
If exact physical types matter, it is still recommended to perform explicit type conversion at the final output stage. In practice, prefer DatasetView.cast(...) or DatasetView.astype(...) as the last transformation before using the result.
SQL API#
Session.sql() is available for read-oriented queries, CREATE TABLE ... AS ... style result persistence, and DROP TABLE ... dataset removal. The feature is backed by the Polars SQL engine and is intended as a practical session-level query helper rather than a full PROC SQL reimplementation.
dictionary.tables and dictionary.columns are available in SQL. limulus also registers dictionary_tables and dictionary_columns aliases because some SQL engines treat dotted names as schema-qualified identifiers.
Dictionary Tables#
Dictionary metadata is generated dynamically from Arrow schema metadata rather than from a separate catalog store.
MEMLABELcomes from table schema metadata keymemlabelLABELcomes from per-field metadata keylabelTYPEreports the Arrow type string such asint64orstringLENGTHis not exposed indictionary.columns
This means the metadata contract is Arrow-centric even when the API shape follows DICTIONARY-style naming.
Name Resolution#
Dataset names are case-insensitive, and a work. prefix is stripped during lookup. Column-oriented Session / DatasetView helpers also resolve column names case-insensitively when the match is unique.
Transpose / Assign API#
Session.transpose() / DatasetView.transpose() and Session.assign() / DatasetView.assign() are Python-side column APIs rather than DATA step statements.
Current transpose scope is intentionally narrow:
Without
id, output follows a PROC TRANSPOSE-like shape with_NAME_andCOL1..COLncolumns.With
id, the current release accepts exactly oneidcolumn name and onevarcolumn.Duplicate or missing
idvalues within a group raise an error instead of applying name mangling rules.
transpose() is a convenience helper. Performance is not a primary design goal; for performance-critical reshaping, prefer Arrow or Polars directly.
For assign(), string values are interpreted as expressions. To assign a string literal, quote it inside the expression, for example flag="'A'".
assign() is now executed through a column-oriented expression pipeline. The current release guarantees column-oriented execution for literals, arithmetic / comparison expressions, case when, and the built-in functions.
PUT / INPUT Scope#
limulus now provides a limited put(...) / input(...) helper surface in both helper expressions and the Python runtime.
Current built-in families are:
numeric to text:
best.,w.d.,zw.d,zw.d.,commaw.d.text to numeric:
best.text to date / datetime / time:
e8601da.,e8601dt.,yymmdd6.,yymmdd8.,yymmdd10.,time.date / datetime / time to text:
e8601da.,e8601dt.,yymmdd6.,yymmdd8.,yymmdd10.,time.
input(...) returns Python / Arrow-compatible numeric, date, datetime, and time values rather than date/time numeric values. This is intentionally a practical subset, not a full format catalog.
Session.register_format(...) and Session.register_informat(...) now support two extension styles:
exact-match dict catalogs, which are serializable and can stay on the Rust-first DATA step path when the rest of the workload is eligible
callable hooks, which remain Python-side extension points
For dict catalogs, put(...) returns text and falls back to the original value on no-match, while input(...) returns null on no-match. Character custom formats use $name. in DATA step syntax but register as name plus namespace="character" in Python. Dict custom input(...) is intentionally float64-only in this phase; broader typed custom informat support is deferred.
PROC / Macro Skip Scope#
Some unsupported syntax is skipped before Data Step parsing so mixed legacy source can still be processed.
Skipped forms:
%let ...;%put ...;%* ...;%macro ... %mend;%macro ... %mend name;proc ... run;proc ... quit;
Skipped syntax is ignored rather than executed. Macro variable expansion, open-code macros, and procedure semantics remain unsupported.
length#
Character length is variable by default, so no character truncation occurs.
BY Groups#
Pre-sorting is not required to use BY groups.
Missing Values#
SAS language numeric missing values use the special . symbol, but limulus treats them as null (Arrow).
SAS language character missing values are "", but in limulus, following the Arrow specification, "" and null are treated as distinct values.
Use missing() to check for both "" and null together.
Assign null using =. for numeric types, or =None for both numeric and character types. (Support for call missing is also under consideration.)
Data Joining#
Merge operates like SQL; if there are duplicate column names, an error is raised.
Array#
Both numeric and character types can be included in the same array without issues.
result = session.submit("""
data out;
set iris;
array var [*] species sepal_length ;
do i = 1 to dim(var);
vname = vname(var[i]);
output ;
end ;
stop ;
keep vname ;
run;
""")
Additional Operators#
Operator |
Added |
|---|---|
|
|
|
|
|
|
Custom Functions#
lead function
Looks ahead to a future row’s value, opposite to lag. Useful for cases where you need the next value, such as computing differences.
shift function
A general-purpose shift function: negative values behave like lag, positive values like lead.
apply function
Allows calling Python lambda functions or custom functions from within a Data Step.
Useful for extending functionality not covered by limulus’s built-in functions, such as complex string operations.
Unsupported Features#
SAS language Feature |
Notes |
|---|---|
Attrib |
Full ATTRIB parity is not implemented yet; use dataset labels and LABEL statements for supported metadata cases |
Format ( |
Exact-match dict catalogs and Python callable registry hooks are available; SAS |
Macro variables ( |
|
PROC statements |
|
|
Not planned; handle on the Python side |
|
Not planned; handle on the Python side |
Colon-based options ( |
Workarounds available; under consideration |
Range notation ( |
Workarounds available; under consideration |
Variable groups ( |
Workarounds available; under consideration |
Character operators (e.g., |
May be implemented if there is demand |
CALL subroutines |
|
In general, features that have no practical workaround and are commonly used will be prioritized.
For features that are costly to implement and have good Python-side alternatives, those alternatives are recommended instead, freeing resources for areas like performance.
For example, external file I/O (INFILE / FILE) is well served by pandas, polars, or jinja2.
Sorting and Column Reordering#
Operations typically performed by procedures such as sorting are executed through the column-oriented API.
The current release exposes practical session-local sorting and column reordering helpers rather than a separate procedure layer.
Column reordering can be done with select or keep.
session.dataset("ds").sort("x")
session.dataset("ds").sort(["x", "y"])
session.dataset("ds").sort(["x"], nodupkey=True)
session.dataset("ds").sort([("x", "Ascending"),("y", "Descending")])
session.dataset("ds").select(["x","y"])
Recommended Patterns#
%LETmacro variables → Python variables + f-string to build DSL stringsINFILE→ pass Polars / Pandas tosession.loads()Missing value checks → use
missing()function orcmiss()|nmiss()