Are your floats decimals in disguise?
Measure your machineNeeds Python with NumPy
Problem
Many float columns are decimals in disguise: prices, coordinates and sensor readings recorded to a fixed number of places, stored as binary doubles. In binary their low mantissa bits look random, so a general-purpose compressor finds little to remove. Multiplying by 10ᵉ turns them back into integers that round-trip exactly, and integers in a narrow range pack into a few bits each. That is the idea behind ALP (adaptive lossless floating-point compression).
Reproduce the idea behind ALP in a few lines: one million latitude-like values with six decimals, compressed as raw doubles with zlib, then scaled by 10⁶ and stored with frame of reference. If the round-trip check fails for your data, the values are not decimals with that exponent, and the idea does not apply to that column.
The question. How much does zlib get from the raw doubles, and how much does treating them as scaled integers get?
Predict
zlib gives only about 1.1–1.5× on the raw doubles, because the low mantissa bits look random. The integer version needs about 23–24 bits per value, about 2.7×, before any general-purpose codec. (The exact ratios depend on the data and are only an illustration.)
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
are-your-floats-decimals-in-disguise.py # /// script # requires-python = ">=3.10" # dependencies = ["numpy"] # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Are your floats decimals in disguise? The idea behind ALP: a column of doubles that are really six-decimal numbers compresses badly as doubles, and well as integers after multiplying by 10^6, when that round-trips exactly. """ import argparse, json, zlib import numpy as np p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--values", type=int, default=1_000_000, help="Values.") p.add_argument("--decimals", type=int, default=6, help="Decimal places in the data (and the exponent tried).") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() rng = np.random.default_rng(0) x = np.round(rng.normal(52.37, 0.5, args.values), args.decimals) raw = x.tobytes() zr = len(raw) / len(zlib.compress(raw, 6)) scaled = np.round(x * 10**args.decimals).astype(np.int64) exact = bool(np.array_equal(scaled / 10**args.decimals, x)) w = int(scaled.max() - scaled.min()).bit_length() print(f"raw doubles + zlib : {zr:.2f}x") print(f"as integers + FOR : {64 / w:.2f}x (width {w} bits; round-trips exactly: {exact})") packed = (scaled - scaled.min()).astype(np.uint32).tobytes() if w <= 32 else b"" zi = len(raw) / len(zlib.compress(packed, 6)) if packed else 0 print(f"as integers + FOR + zlib : {zi:.2f}x") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"summary": [{"zlib_double": zr, "for_ratio": 64 / w, "width": w, "exact": "yes" if exact else "no", "for_zlib": zi}]}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run are-your-floats-decimals-in-disguise.py uv run are-your-floats-decimals-in-disguise.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --values1,000,000 | Values. |
| --decimals6 | Decimal places in the data (and the exponent tried). |
Our run
raw doubles + zlib : 1.30x
as integers + FOR : 2.78x (width 23 bits; round-trips exactly: True)
as integers + FOR + zlib : 2.45xWhat we found
zlib on the raw doubles gave 1.3×. Scaled to integers, the values round-tripped exactly (yes) and needed 23 bits each with frame of reference: 2.78×, without any general-purpose codec. Running zlib over the packed integers made it slightly worse (2.45×): the packed bits were already close to random.
Read it
The gain comes from knowing what the numbers are, not from a better compressor. A general-purpose codec sees binary noise; an encoder that tests whether a column is a decimal in disguise sees a narrow integer per value. The test is what makes it safe: the encoding applies only where the round trip is exact, and falls back elsewhere.