Data pipeline examples
Follow complete Polars pipelines for messy sales exports, schema drift, ledger reconciliation, and files larger than memory.
Complete pipeline: clean a messy sales export#
Read raw strings, quarantine bad rows, check IDs, calculate totals, and export a report.
This input has whitespace, mixed date formats, a duplicate order, an invalid amount, and a negative quantity. One row represents one order in this export, so order ID must be unique after the winner rule. The accepted amount format is USD with comma thousands separators and two decimal places. Quarantine means retaining invalid source rows in a separate output instead of silently dropping or repairing them.
order_id,updated,day,customer,qty,price
001,1,2026-09-01, a ,2,"USD 1,000.00"
002,1,02/09/2026,b,1,USD 250.00
002,2,02/09/2026,b,2,USD 250.00
003,1,2026-09-03,c,1,unknown
004,1,2026-09-04,d,-1,USD 100.00from pathlib import Path
from tempfile import TemporaryDirectory
from decimal import Decimal
import polars as pl
csv = """order_id,updated,day,customer,qty,price
001,1,2026-09-01, a ,2,"USD 1,000.00"
002,1,02/09/2026,b,1,USD 250.00
002,2,02/09/2026,b,2,USD 250.00
003,1,2026-09-03,c,1,unknown
004,1,2026-09-04,d,-1,USD 100.00
"""
with TemporaryDirectory() as folder:
root = Path(folder)
source = root / "sales.csv"
source.write_text(csv, encoding="utf-8")
raw = pl.scan_csv(source, infer_schema=False).with_row_index("source_row")
parsed = raw.with_columns(
pl.col("order_id").str.strip_chars(),
pl.col("customer").str.strip_chars().str.to_uppercase(),
pl.col("updated").cast(pl.Int64, strict=False).alias("version"),
pl.col("qty").cast(pl.Int64, strict=False).alias("quantity"),
pl.coalesce(
pl.col("day").str.to_date("%Y-%m-%d", strict=False),
pl.col("day").str.to_date("%d/%m/%Y", strict=False),
).alias("date"),
pl.col("price")
.str.strip_chars()
.str.replace_all("USD ", "", literal=True)
.str.replace_all(",", "", literal=True)
.cast(pl.Decimal(18, 2), strict=False)
.alias("unit_price"),
)
checked = parsed.with_columns(
(
pl.col("order_id").is_not_null()
& (pl.col("order_id") != "")
& pl.col("customer").is_not_null()
& (pl.col("customer") != "")
& pl.col("version").is_not_null()
& pl.col("date").is_not_null()
& (pl.col("quantity") > 0)
& (pl.col("unit_price") >= 0)
)
.fill_null(False)
.alias("valid")
)
rejected = checked.filter(~pl.col("valid")).collect()
rejected.write_parquet(root / "rejected.parquet")
accepted = (
checked.filter(pl.col("valid"))
.sort(["order_id", "version", "source_row"])
.unique(subset="order_id", keep="last", maintain_order=True)
.with_columns((pl.col("quantity") * pl.col("unit_price")).alias("revenue"))
)
lines = accepted.collect()
assert lines["order_id"].n_unique() == lines.height
report = (
lines.group_by("customer")
.agg(pl.len().alias("orders"), pl.col("revenue").sum())
.sort("customer")
)
report.write_parquet(root / "report.parquet")
report.write_csv(root / "report.csv")
print("rejected IDs:", rejected["order_id"].to_list())
print(
"accepted orders:",
lines.select("order_id", "quantity").sort("order_id").to_dicts(),
)
print("report:", report.to_dicts())
assert report["revenue"].sum() == Decimal("2500.00")
print("written: rejected.parquet, report.parquet, report.csv")rejected IDs: ['003', '004']
accepted orders: [{'order_id': '001', 'quantity': 2}, {'order_id': '002', 'quantity': 2}]
report: [{'customer': 'A', 'orders': 1, 'revenue': Decimal('2000.00')}, {'customer': 'B', 'orders': 1, 'revenue': Decimal('500.00')}]
written: rejected.parquet, report.parquet, report.csv| Step | Effect on this input |
|---|---|
| Read as strings | Preserves 001 and other IDs; retains original date and price text. |
| Parse and normalize | Customer a becomes A, both date formats become Date, price becomes exact Decimal. |
| Quality check | 003 fails amount parsing; 004 has a negative quantity. Both remain in the reject file. |
| Latest valid version | 002 version 2 replaces version 1; two accepted orders remain. |
| Transform and summarize | A totals 2000.00, B totals 500.00. |
| Export | Parquet retains decimal types; CSV needs a schema when read back. |
This example chooses the latest valid version, not “the latest version, then reject if invalid.” Those policies can differ. A production job should record invalid reasons, duplicate losers, source file identities, input/accepted/rejected counts, and whether superseded rows are expected. Keep transformations lazy while useful; collect only when the result size and validation needs are understood.
Official reference: Complete pipeline: clean a messy sales export.
Complete pipeline: merge evolving customer files#
Normalize schemas, keep provenance, choose the latest record, and write a checked snapshot.
The files use different ID fields and schemas. Normalize them to one schema, retain the source name, and break ties by version and source. Versions are increasing integers; timestamps need an explicit time zone.
from pathlib import Path
from tempfile import TemporaryDirectory
import polars as pl
with TemporaryDirectory() as folder:
root = Path(folder)
(root / "v1.csv").write_text(
"id,version,email\nA,1,old@a.test\nB,1,b@b.test\n", encoding="utf-8"
)
(root / "v2.csv").write_text(
"customer_id,version,email,country\nA,2,new@a.test,BD\nC,1,c@c.test,BD\n",
encoding="utf-8",
)
old = (
pl.scan_csv(root / "v1.csv", infer_schema=False)
.rename({"id": "customer_id"})
.with_columns(
pl.lit(None, dtype=pl.String).alias("country"),
pl.lit("v1.csv").alias("source"),
)
)
new = pl.scan_csv(root / "v2.csv", infer_schema=False).with_columns(
pl.lit("v2.csv").alias("source")
)
schema = {
"customer_id": pl.String,
"version": pl.Int64,
"email": pl.String,
"country": pl.String,
"source": pl.String,
}
def normalize(frame):
return frame.select(
*(pl.col(name).cast(dtype).alias(name) for name, dtype in schema.items())
)
merged = pl.concat([normalize(old), normalize(new)])
invalid_count = (
merged.filter(pl.col("customer_id").is_null() | (pl.col("customer_id") == ""))
.select(pl.len())
.collect()
.item()
)
if invalid_count:
raise ValueError("snapshot has missing IDs")
snapshot = (
merged.sort(["customer_id", "version", "source"])
.unique(subset="customer_id", keep="last", maintain_order=True)
.sort("customer_id")
)
target = root / "customers.parquet"
snapshot.sink_parquet(target, engine="streaming")
result = pl.read_parquet(target)
assert dict(result.schema) == schema
assert result["customer_id"].n_unique() == result.height
print(result.to_dicts())
print("written: customers.parquet")[{'customer_id': 'A', 'version': 2, 'email': 'new@a.test', 'country': 'BD', 'source': 'v2.csv'}, {'customer_id': 'B', 'version': 1, 'email': 'b@b.test', 'country': None, 'source': 'v1.csv'}, {'customer_id': 'C', 'version': 1, 'email': 'c@c.test', 'country': 'BD', 'source': 'v2.csv'}]
written: customers.parquetThe newer record supplies A’s email and country; B’s missing country remains null. The source column records each winner. Strict casts reject incompatible versions. Use stable input files when validation and export run separate queries.
Official reference: Complete pipeline: merge evolving customer files.
Complete pipeline: reconcile ledger and bank files#
Use source-presence flags so missing records and missing amounts stay distinguishable.
from pathlib import Path
from tempfile import TemporaryDirectory
import polars as pl
with TemporaryDirectory() as folder:
root = Path(folder)
(root / "ledger.csv").write_text(
"tx,cents\n t1 ,100\nt2,200\nt3,\n", encoding="utf-8"
)
(root / "bank.csv").write_text(
"tx,cents\nT1,100\nT2,190\nT4,400\n", encoding="utf-8"
)
def load(name, side):
raw = pl.read_csv(root / name, infer_schema=False)
data = raw.select(
pl.col("tx").str.strip_chars().str.to_uppercase(),
pl.col("cents").cast(pl.Int64).alias(side + "_cents"),
pl.lit(True).alias(side + "_present"),
)
if data["tx"].null_count() or data.filter(pl.col("tx") == "").height:
raise ValueError("missing transaction key")
if data["tx"].n_unique() != data.height:
raise ValueError("duplicate transaction key")
return data
ledger, bank = load("ledger.csv", "ledger"), load("bank.csv", "bank")
report = ledger.join(bank, on="tx", how="full", coalesce=True, validate="1:1")
report = report.with_columns(
pl.when(pl.col("ledger_present").is_null())
.then(pl.lit("bank_only"))
.when(pl.col("bank_present").is_null())
.then(pl.lit("ledger_only"))
.when(pl.col("ledger_cents").is_null() | pl.col("bank_cents").is_null())
.then(pl.lit("missing_amount"))
.when(pl.col("ledger_cents") != pl.col("bank_cents"))
.then(pl.lit("amount_diff"))
.otherwise(pl.lit("matched"))
.alias("status")
)
report = report.sort("tx")
report.write_parquet(root / "reconciliation.parquet")
report.filter(pl.col("status") != "matched").write_csv(root / "exceptions.csv")
print(report.select("tx", "ledger_cents", "bank_cents", "status").to_dicts())
print("exceptions:", report.filter(pl.col("status") != "matched").height)[{'tx': 'T1', 'ledger_cents': 100, 'bank_cents': 100, 'status': 'matched'}, {'tx': 'T2', 'ledger_cents': 200, 'bank_cents': 190, 'status': 'amount_diff'}, {'tx': 'T3', 'ledger_cents': None, 'bank_cents': None, 'status': 'ledger_only'}, {'tx': 'T4', 'ledger_cents': None, 'bank_cents': 400, 'status': 'bank_only'}]
exceptions: 3T3 has a ledger row with a null amount and no bank row, so it is ledger_only. If both rows exist but an amount is null, it is missing_amount. Strict parsing rejects malformed numbers; add quarantine handling if processing should continue.
Official reference: Complete pipeline: reconcile ledger and bank files.
Complete pipeline: files larger than memory#
Scan stable inputs, validate small summaries, and stream accepted rows into partitioned output.
Scan source files, validate summary counts, and write accepted rows by day. The separate region summary stays small. Keep generated outputs out of input globs to avoid reprocessing them.
from pathlib import Path
from tempfile import TemporaryDirectory
import polars as pl
with TemporaryDirectory() as folder:
root = Path(folder)
source = root / "source"
source.mkdir()
for part in range(3):
pl.DataFrame(
{
"day": ["2026-09-01", "2026-09-02", "2026-09-02"],
"region": ["Dhaka", "Sylhet", "Dhaka"],
"cents": [100 + part, 200 + part, -10],
}
).write_parquet(source / f"part-{part}.parquet")
raw = pl.scan_parquet(str(source / "part-*.parquet"))
if dict(raw.collect_schema()) != {
"day": pl.String,
"region": pl.String,
"cents": pl.Int64,
}:
raise TypeError("unexpected input schema")
checked = raw.with_columns(pl.col("day").str.to_date(strict=False).alias("date"))
valid = (
pl.col("date").is_not_null()
& pl.col("region").is_not_null()
& (pl.col("cents") >= 0)
).fill_null(False)
counts = checked.select(
pl.len().alias("input"),
valid.sum().alias("accepted"),
(~valid).sum().alias("rejected"),
).collect(engine="streaming")
accepted = checked.filter(valid).select("date", "region", "cents")
rejected = checked.filter(~valid)
rejected.sink_parquet(root / "rejected.parquet", engine="streaming")
accepted.sink_parquet(
pl.PartitionBy(
root / "clean", key="date", include_key=True, max_rows_per_file=100_000
),
mkdir=True,
engine="streaming",
)
summary = accepted.group_by("region").agg(pl.col("cents").sum()).sort("region")
summary.sink_parquet(root / "summary.parquet", engine="streaming")
print("quality:", counts.to_dicts())
print("summary:", pl.read_parquet(root / "summary.parquet").to_dicts())
print("written partitions:", len(list((root / "clean").rglob("*.parquet"))) > 0)
assert counts.item(0, "input") == counts.item(0, "accepted") + counts.item(
0, "rejected"
)quality: [{'input': 9, 'accepted': 6, 'rejected': 3}]
summary: [{'region': 'Dhaka', 'cents': 303}, {'region': 'Sylhet', 'cents': 603}]
written partitions: TruePartitionBy is unstable. Test its layout and peak memory with representative files, partition counts, and operations. Use immutable inputs and a new output directory for each run. Verify all files before publishing a manifest; file sinks do not form a transaction across outputs.
Official reference: Complete pipeline: files larger than memory.
Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice