Dataframe — Columnar Data in U

U's Dataframe is not a library — it is a compile-time template tag that builds typed, columnar query plans. The compiler sees the entire pipeline and optimizes it: predicate pushdown, projection pruning, stage fusion, and GPU kernel emission. The same .on() / .filter() / .map() / .reduce() chain that works on lists works on Dataframes, with the modifier system controlling execution strategy.

Construction

Dataframes are built from columns, CSV, JSON, or the backtick template:

// From columns:
df = Dataframe.from_columns({
    "name": ["Alice", "Bob", "Carol", "Dave"],
    "age": [30, 25, 35, 28],
    "salary": [70000, 55000, 90000, 62000]
})

// From CSV (async):
df = Dataframe.from_csv("data.csv") +A

// From JSON rows:
df = Dataframe.from_json([
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25}
])

// Backtick template with interpolation:
min_age: I = 25
cols = ["name", "salary"]
df2 = Dataframe`select {{cols}} from {{df}} where age > {{min_age}}`

The backtick form produces a Formats.Dataframe — a typed query plan, not a string. The compiler's __validate__ dunder checks column names, types, and predicates at compile time. Interpolated variables ({{cols}}, {{min_age}}) are type-checked against the schema.

Schema

Every Dataframe carries a compile-time schema. The compiler knows column names and types:

d UserRow
    name: S
    age: I
    salary: I
    active: B = true

df = Dataframe[UserRow].from_csv("users.csv")
// Compiler knows: df has columns name:S, age:I, salary:I, active:B
// df["email"] → compile error: 'email' is not a column in UserRow

When no schema type is given, the Dataframe infers column types from the data (first row for CSV, all rows for JSON).

Pipeline Operations

Dataframes use the same .on() primitives as lists. Every operation returns a new Dataframe (immutable by default):

Selection

df.select(["name", "salary"])       // keep only these columns
df.drop(["active"])                  // remove columns
df.head(10)                          // first 10 rows
df.tail(5)                           // last 5 rows
df.slice(100, 200)                   // rows 100–199
df.sample(50)                        // random 50 rows

Filtering

df.filter(row => row["age"] > 30)
df.where("salary", ">", 60000)
df.filter(row => row["name"].starts_with("A") & row["active"])

Transforms

df.with_column("bonus", row => row["salary"] * 0.1)
df.rename("salary", "annual_pay")
df.sort("age", desc: true)
df.unique("name")
df.cast("age", "N")                  // I → N

Aggregation

df.sum("salary")                     // → I
df.mean("age")                       // → N
df.min("salary")                     // → I
df.max("salary")                     // → I
df.count()                           // → I

// GroupBy:
df.groupby(["active"])
    .agg("salary", "mean")           // mean salary per group
    .agg("age", "max")               // max age per group
    .collect()

Joins

orders = Dataframe.from_csv("orders.csv")
users = Dataframe.from_csv("users.csv")

joined = users.join(orders, on: "user_id", how: "inner")
cross = users.cross_join(products)

Column Expressions

Direct column access returns a Column — a typed array with element-wise operations:

col = df["salary"]                    // Column[I]
col.sum()                             // → I
col.mean()                            // → N
col * 1.1                             // → Column[N], element-wise
col > 60000                           // → Column[B], boolean mask

// Column arithmetic:
df.with_column("tax", df["salary"] * 0.22)
df.with_column("net", df["salary"] - df["salary"] * 0.22)

Modifiers

The modifier system controls how the pipeline executes:

+W — Lazy Evaluation

// Nothing executes until .collect():
result = df
    .filter(row => row["age"] > 30)
    .select(["name", "salary"])
    .sort("salary", desc: true)
    .head(10)
    +W                                // lazy: build plan only

// Execute the plan:
result.collect()

// The compiler applies:
// 1. Predicate pushdown: filter before sort
// 2. Projection pruning: only read name, salary, age columns
// 3. Limit pushdown: stop after 10 rows

+V — Vectorized / GPU

// CPU vectorized (SIMD):
total = df["salary"].sum() +V

// GPU compute shader:
df.with_column("norm", df["value"] / df["value"].max()) +V
// → compiler emits WGSL:
//   @compute @workgroup_size(64)
//   fn main(@builtin(global_invocation_id) id: vec3<u32>) {
//       outp[id.x] = inp[id.x] / max_val;
//   }

// Fused GPU pipeline:
result = df
    .filter(row => row["price"] > 100)
    .with_column("margin", row => row["price"] * 0.15)
    .sum("margin")
    +V
// → one GPU kernel: filter + multiply + reduce

+R(GPU) — GPU-Resident Data

// Load data to GPU once:
gpu_df = df +R(GPU)

// All operations stay on GPU — no CPU↔GPU transfer:
filtered = gpu_df.filter(row => row["value"] > threshold) +V
transformed = filtered.with_column("scaled", col => col * factor) +V
total = transformed.sum("scaled") +V

// One transfer at the end:
result = total.to_cpu()

Printing

Dataframes print as aligned tables:

print(df.head(3))
// ┌──────┬─────┬────────┐
// │ name │ age │ salary │
// ├──────┼─────┼────────┤
// │ Alice│  30 │  70000 │
// │ Bob  │  25 │  55000 │
// │ Carol│  35 │  90000 │
// └──────┴─────┴────────┘

The __string__ dunder on Dataframe produces the table format. __pack__ produces a Tree (JSON-serializable). Dataframe.from_tree(tree) round-trips.

Tensor Integration

Dataframe columns can be extracted as Tensors for numerical computation:

// Column → Tensor:
ages = df["age"].to_tensor()          // Tensor[I], shape [n]
salaries = df["salary"].to_tensor()   // Tensor[I], shape [n]

// Tensor operations:
correlation = ages.dot(salaries) / (ages.norm() * salaries.norm())

// Tensor → Column:
df.with_column("predicted", Tensor.matmul(weights, features).to_column())

Compile-Time Validation

The __validate__ dunder runs at compile time on Dataframe backtick templates:

Dataframe`select name, agee from {{users}}`
//                       ^^^^
// Compile error: column 'agee' not in schema UserRow
//   Did you mean: 'age'?

Dataframe`select name from {{users}} where salary > "high"`
//                                                  ^^^^^^
// Compile error: cannot compare salary (I) with "high" (S)

This is the same mechanism that makes SQL catch injection and HTML catch XSS — the template tag's __validate__ sees the full expression and the schema at compile time.

Implementation Notes

Storage: Columns are contiguous typed arrays ([I], [N], [S], [B]) with 64-byte aligned allocation for SIMD. The Dataframe struct holds a {S: Column} map plus schema metadata.

Memory: Dataframes are +R (heap, refcounted) by default. Column data is shared between Dataframes that select subsets (zero-copy slicing via offset + length).

Serialization: __pack__ produces {"columns": {"name": [...], "age": [...]}, "schema": {...}}. CSV and JSON I/O use streaming parsers that allocate columns incrementally.

Pipeline fusion: When the compiler sees df.filter(p).map(f).reduce(r), it generates a single fused loop instead of three passes. With +V, it generates a single GPU kernel. With +W, it builds a pull-based iterator that processes one chunk at a time.

GPU dispatch: Column operations with +V emit WGSL compute shaders via the same codegen/wgsl.py emitter used for [I].map(). The +R(GPU) modifier keeps columns in WebGPU storage buffers between pipeline stages, avoiding CPU↔GPU round-trips.