Skip to content
About
Main content

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 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 (adaptive floating-point ).

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

How much does zlib compress the raw doubles?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    are-your-floats-decimals-in-disguise.py37 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat it changes
--values1,000,000 Values.
--decimals6 Decimal places in the data (and the exponent tried).

Our run

Reference runApple M2 · 8 cores · 8 GB
Output · uv run are-your-floats-decimals-in-disguise.py
raw doubles + zlib       : 1.30x
as integers + FOR        : 2.78x  (width 23 bits; round-trips exactly: True)
as integers + FOR + zlib : 2.45x
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20

What 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.