Polypress · makes data tables much smaller, without changing them
A table is not
a stream of bytes.
Polypress makes data tables far smaller — typically 14× smaller — and gives them back exactly as they were. It works on CSV, Parquet, and anything else shaped like rows and columns.
"Lossless" means nothing is approximated or thrown away: every cell comes back as the exact text it was, in the original row and column order. If that is all you needed to know, install it. The rest of this page is how it works and where it fails.
General-purpose compressors (zip, gzip, xz, zstd) see a table as a stream of bytes. Columnar
formats (Parquet, ORC, Feather) see it as columns, but compress each column on its own. Polypress
is built on the observation that the columns of a real table are not independent of each
other — City is almost determined by Postal Code,
latitude is repeated inside location, adjacent sensor columns hold nearly
the same number — and it exploits those relationships explicitly.
The output is a .ppz file. The original table comes back exactly:
same columns, same column order, same row order, every cell as the exact string it was.
The codec, the C encoder, the tests and the
benchmark harness, with the sweep evidence committed under OUT/results/. Public source
is not reviewed source — nobody outside this project has run it.
The three ideas it is built on
-
Predict down a column
Fit a low-degree polynomial to the last few values in a column, extrapolate one step, store only the error. Because the fit is re-centred at every cell, the coefficients never need to be stored — the decoder already has the neighbours. Order-k extrapolation turns out to be exactly the k-th finite difference, so this is one
np.diffcall. -
The same thing in two dimensions
Where adjacent numeric columns are commensurable — same decimal places, same magnitude — a cell is predicted from its left, upper, and upper-left neighbours. This is what wins on matrix-shaped tables, and it is the one thing no shipped columnar codec does: Gorilla, DoubleDelta, T64, zfp and fpzip all predict down a single column. The detection matters as much as the predictor — differencing
Model YearagainstMakeis meaningless, so groups form only where columns are genuinely comparable. -
Reorder the rows so a column collapses into runs
This is the central idea. Rather than model the fact that
Citydepends onPostal Code, sort the rows by the parent column: equal parents become adjacent, the child collapses into long runs, and the entropy coder eats it. Parents are picked by conditional entropy, then confirmed by actually compressing both ways, and arranged into a tree so a parent is always decoded before its children.
| Postal | City |
|---|---|
| 60614 | Chicago |
| 94110 | San Francisco |
| 10025 | New York |
| 60614 | Chicago |
| 94110 | San Francisco |
| 60614 | Chicago |
| 10025 | New York |
| 94110 | San Francisco |
parent
→
| Postal | City |
|---|---|
| 10025 | New York |
| 10025 | New York |
| 60614 | Chicago |
| 60614 | Chicago |
| 60614 | Chicago |
| 94110 | San Francisco |
| 94110 | San Francisco |
| 94110 | San Francisco |
Two details distinguish this from most of the prior work. The ordering is per-column, not global — a 421-column table can carry hundreds of different row orderings at once. And the permutation is neither stored nor imposed on the output: you get your table back in its original row order.
500 datasets nobody chose
The obvious objection to any compression result is “you picked the files”, and there is no way to answer that by picking more files. So the main benchmark does not pick. It asks the Socrata open-data catalog — the index behind several hundred government data portals — for its datasets in descending order of page views, and takes the first 500 that survive four mechanical filters: the CSV downloads; at least 2 columns and 20 rows; at least 50 KB; not a byte-identical duplicate of one already taken.
Rank order is public and fixed, so the list reproduces. Every rejection is recorded with its reason. That gives 500 tables, 3.57 GB of CSV, 14.3 million rows, 10,557 columns, measured against 17 competing codecs.
| Competitor | Polypress wins | Median margin |
|---|---|---|
xz -9e | 500 / 500 | 1.30× |
zstd -22 ultra | 500 / 500 | 1.39× |
gzip -9 | 500 / 500 | 2.24× |
lz4 -9 | 500 / 500 | 2.65× |
parquet + zstd | 449 / 450 | 1.71× |
parquet + snappy | 450 / 450 | 2.56× |
feather + zstd | 450 / 450 | 3.36× |
orc + zstd | 435 / 436 | 2.00× |
bzip2 -9 | 491 / 500 | 1.46× |
brotli -q 11 | 487 / 500 | 1.32× |
Counts differ because pyarrow could not read every CSV: 50 files defeated its Parquet/Feather reader, 64 its ORC writer.
Two things that must be said next to those numbers
Parquet did not reproduce the data on 356 of the 500 tables
Read with type inference — the way a data engineer actually reads a CSV — Parquet returned the
exact printed text on only 81 of 500. Turning "1.50" into
1.5, or 007 into 7, makes a smaller file for reasons that have
nothing to do with compression. Polypress guarantees the exact printed cell. So on 71% of the corpus
the Parquet rows above are flattering to Parquet — and it still loses all but one of them.
“You just picked a better final compressor” is answered
Polypress finishes with xz, and Parquet cannot use xz at all. So the same modelled streams were
re-finished with the competitor's own entropy coder: polypress+zstd beats
parquet+zstd 449/450 (median 1.57×), and polypress+brotli beats
parquet+brotli 449/450 (median 1.63×). The margin is the modelling, not the
finisher.
Where it wins, and where it doesn't
The win is decided by what fraction of the output is free text, not by how wide
the table is. On chicago_permits — 116 columns, 70.9% free text — the margin is 1.12×.
On cdc_nndss, at 7.5% free text, it is 3.70×.
| Genre | Result | Why |
|---|---|---|
| Densely-coded administrative and survey data | 2–4× | Disease surveillance, health surveys, permit and incident logs. These tables are made of codes, and the reordering trick does nearly all the work. |
| Matrix-shaped numeric tables | 1.86× · 2.14× · 1.66× | A Treasury yield curve and two sensor grids. This is the 2D predictor's own case. |
| Free-text-heavy tables | weak | Addresses, names, descriptions. LZ77 substring matching is already good here, and modelling adds little. |
It loses on 22 of 500, and all 22 are listed publicly
Twelve losses are to brotli -q 11, which is not one of the
carried fallbacks. Nine are to bzip2 -9 run on the original file — Polypress's
fallback compresses the table re-rendered canonically, and on those tables normalising the quoting
removed redundancy the Burrows–Wheeler transform had been exploiting; the gap is 0.1%–1.6%. That
leaves one genuine loss, sars_cov_2_variant_proportions at 0.65× to
orc+zstd, and it is undiagnosed.
The honest form of the guarantee is never worse than our own plain fallback — not “never worse than any tool on your original bytes.”
What it is not — the withdrawn claim
An earlier version of the README claimed the reordering trick was novel. That claim is withdrawn. A prior-art search found US 8,312,026 B2 (Kiem-Phong Vo, AT&T, filed 2009, granted 2012), which discloses the whole of it — including the rule that a parent must be chosen by measured compressed size rather than an entropy score, which this project learned the hard way.
The predictors are also prior art: the planar predictor is Lorenzo (2003, used in fpzip and SZ); MED is JPEG-LS. Row reordering for compression is a studied problem (Lemire, Kaser & Gutarra, ACM TODS 2012).
The design was reached here independently, without knowledge of any of it. Independent convergence twenty years apart is decent evidence the design is right; it is not evidence of priority. Both relevant patents have expired, so there is no restriction on using the code.
The honest framing: a careful, measured, verified implementation of a good idea, with a table-aware front end, taken further and tested harder than the prior art was.
Honest limitations
It is slow to compress
Median across 100 unselected datasets: 2.0 MB/s encode, 134 MB/s decode. Decompression is fast; compression is in the slow tier and will stay there. The “never worse” guarantee is what costs it — tables get encoded twice so the smaller result can win.
| Tool | Encode | Decode |
|---|---|---|
| polypress | 2.0 MB/s | 134 MB/s |
xz -9e | 3.9 MB/s | 203 MB/s |
brotli -q 11 | 1.1 MB/s | 501 MB/s |
zstd -22 | 2.8 MB/s | 871 MB/s |
zstd -3 | 187 MB/s | 716 MB/s |
The parent search is O(columns²)
A 209-column table means 38,220 scored pairs. The worst case for speed is not a big file — it is a 60 KB table with 106 columns and 79 rows, at 0.5 MB/s.
The 2× results are matrix-shaped tables
1.3–1.5× is the typical result.
Every benchmark dataset is a government open-data table
That is what the Socrata catalog indexes. Census microdata, NOAA grids, genomics and financial tick data are unmeasured, and there is no reason to assume the result transfers.
Nobody outside this project has run it
No independent replication of any number on this page.
The competitor set had a hole
All 17 were LZ-family, Huffman or block-sort — no context-mixing model. PPMd was added later and Polypress still wins 13/13 against it, but on the seven tables where row reordering does not fire, PPMd gets within 1–5%. Sweeps run before 2026-08-06 should be quoted as “17 LZ-family and columnar competitors”.
The Mac app is unsigned
Gatekeeper warns on first open; right-click → Open clears it.
How you actually use it
# the codec and the `polypress` command
pip install polypress
# add pyarrow, for .parquet in and out
pip install 'polypress[parquet]'
polypress compress data.csv # -> data.csv.ppz
polypress restore data.csv.ppz # -> data.csv
polypress restore data.csv.ppz -o out.parquet # doubles as a converter
polypress info data.csv.ppz # plan, shape, how much was reordered
Version 0.2.0, MIT.
Python 3.9+ and numpy; a C compiler is optional — the accelerator builds itself on first import and
falls back to numpy without one. The source is public, so
git clone then pip install . from work/ also works, and
python3 work/tzip.py runs out of the checkout with nothing installed.
For files larger than RAM, a block-at-a-time mode with a settable memory budget
(--budget 1.0 for roughly 1 GB peak). Blocks compress independently, so peak memory is
one block rather than one file; the cost is that reordering only sees correlations inside a block.
There is also a standalone C binary — no Python, no numpy — that produces
byte-identical output to the Python encoder, verified on 108 real datasets. And a Mac
app: launch it and pick a table, double-click a .ppz to restore it, or drop
files on the Dock icon. Nothing is written until the compressed blob has been decoded in memory and
compared to the original.