Sequence length and convergence¶
How much data each measure needs before it means anything, with the bias measured.
Every convergence result behind this library is asymptotic. h converges to the entropy rate of
an ergodic source as n → ∞, and the finite-size bias falls off like a power of 1/log n — so it
is large, it shrinks slowly, and at the lengths real experiments produce it never quite goes away.
This page gives the measured numbers, an honest table of what each measure needs, and one firm rule about comparing sequences of different lengths.
The measured bias of h¶
A fair coin has an entropy rate of exactly 1 bit per symbol. Here is what lz.h(seq, log_base=2)
returns on i.i.d. fair-coin sequences, averaged over independent draws at each length
(random.Random(n) seeded per row).
| n | draws | h (bits) |
bias | s.d. over draws |
|---|---|---|---|---|
| 100 | 400 | 1.1243 | +0.1243 | 0.0719 |
| 300 | 300 | 1.0818 | +0.0818 | 0.0334 |
| 1 000 | 200 | 1.0522 | +0.0522 | 0.0196 |
| 3 000 | 100 | 1.0360 | +0.0360 | 0.0101 |
| 10 000 | 60 | 1.0269 | +0.0269 | 0.0045 |
| 30 000 | 40 | 1.0209 | +0.0209 | 0.0026 |
| 100 000 | 25 | 1.0171 | +0.0171 | 0.0016 |
| 300 000 | 40 | 1.0142 | +0.0142 | 0.0006 |
| 1 000 000 | 25 | 1.0120 | +0.0120 | 0.0003 |
Four decades of extra data — 100 symbols to a million — shrink the bias by 10.4×. That is nowhere near what an ordinary statistical error would do.
| n | bias | observed shrink | if bias ∝ 1/√n | if ∝ 1/log₂n | if ∝ 1/(log₂n)² |
|---|---|---|---|---|---|
| 100 | 0.1243 | 1.00× | 1.00× | 1.00× | 1.00× |
| 1 000 | 0.0522 | 2.38× | 3.16× | 1.50× | 2.25× |
| 10 000 | 0.0269 | 4.63× | 10.00× | 2.00× | 4.00× |
| 100 000 | 0.0171 | 7.27× | 31.62× | 2.50× | 6.25× |
| 1 000 000 | 0.0120 | 10.35× | 100.00× | 3.00× | 9.00× |
The observed column tracks the logarithmic forms and diverges further from 1/√n at every decade:
by n = 10⁶ an ordinary sampling error would have shrunk 100×, and the measured bias has shrunk
10×. Over this window the observed shrink sits a little above the 1/(log n)² curve. That is a fit
to nine points on one source, not a law — a separate Monte-Carlo run with different seeds and only
20 draws per length lands 4 % away at n = 100 and 1 % away at n = 10⁶. Treat the columns as
interpolation and do not read a required sample size off them.
Halving the bias costs you squaring n
A 1/log n error law means the sample size you need grows exponentially in the precision you
want. Fit 1/log₂n to the fair-coin column and halving the +1.2 % bias at n = 10⁶ takes
n ≈ 10¹²; fit the more optimistic 1/(log₂n)² and it still takes n ≈ 3·10⁸. Neither is
"twice the data". Do not plan an experiment on the assumption that h behaves like a sample
mean.
There is a harder result behind the slow decay: no distribution-free sample-size answer exists. Shields (1993) proved there is no universal redundancy rate over the class of all ergodic sources — for any n and any claimed error bound, some stationary ergodic process violates it. Every number on this page is a rule of thumb calibrated on particular sources, and is labelled as such.
The bias is not always upward¶
h overestimates high-entropy sources and underestimates low-entropy ones. Measured on i.i.d.
Bernoulli sources at n = 100 000 with log_base=2, mean of 10 draws per row:
| p | true H(p) bits | h (bits) |
ratio |
|---|---|---|---|
| 0.50 | 1.0000 | 1.0172 | 1.017 |
| 0.40 | 0.9710 | 0.9848 | 1.014 |
| 0.30 | 0.8813 | 0.8897 | 1.009 |
| 0.20 | 0.7219 | 0.7200 | 0.997 |
| 0.10 | 0.4690 | 0.4563 | 0.973 |
| 0.05 | 0.2864 | 0.2726 | 0.952 |
| 0.01 | 0.0808 | 0.0734 | 0.908 |
At p = 0.01 the estimate is 9 % low at a hundred thousand samples. Never state "LZ
overestimates entropy" as a blanket rule; the sign of the error depends on the source.
Correlated sources behave the same way. A two-state chain that flips with probability p, same
length and same number of draws:
| p(flip) | true h | h (bits) |
ratio |
|---|---|---|---|
| 0.50 | 1.0000 | 1.0174 | 1.017 |
| 0.30 | 0.8813 | 0.8971 | 1.018 |
| 0.20 | 0.7219 | 0.7326 | 1.015 |
| 0.10 | 0.4690 | 0.4712 | 1.005 |
| 0.05 | 0.2864 | 0.2842 | 0.992 |
| 0.02 | 0.1414 | 0.1368 | 0.967 |
| 0.01 | 0.0808 | 0.0774 | 0.958 |
The sign flips at a different place — between p = 0.05 and p = 0.1 here, against between
p = 0.2 and p = 0.3 for the i.i.d. sources. The crossover is a property of the source, not a
constant of the estimator.
Why short sequences are biased upward¶
The mechanism is the empty dictionary at the start of the parse. An LZ76 factor can only be as long
as the match it finds in the prefix that precedes it, and at the beginning of a sequence there is no
prefix. Asymptotically the mean factor length approaches log_k(n)/h; at small n it falls short of
that, so the parse produces too many factors and h = c·log_k(n)/n comes out too high.
mean factor length = n ⁄ c → logk n ⁄ h
- c
- number of complete LZ76 factors
- n
- sequence length in symbols (bytes)
- k
- alphabet size
- h
- entropy rate — equals 1 for a fair coin measured in bits
Ornstein & Weiss (1993) proved the Wyner–Ziv conjecture: the longest match into the past grows like (log n)/h almost surely, and that match is what an LZ76 factor is.
For a fair coin the target is exactly log₂ n. Measured on single draws, CPython random.seed(0)
re-seeded per row:
| n | c | mean factor length | log₂ n | ratio | h |
|---|---|---|---|---|---|
| 100 | 18 | 5.56 | 6.64 | 0.836 | 1.1959 |
| 1 000 | 107 | 9.35 | 9.97 | 0.938 | 1.0663 |
| 10 000 | 777 | 12.87 | 13.29 | 0.969 | 1.0325 |
| 100 000 | 6 138 | 16.29 | 16.61 | 0.981 | 1.0195 |
| 1 000 000 | 50 789 | 19.69 | 19.93 | 0.988 | 1.0123 |
h is exactly the reciprocal of that ratio — for a binary sequence read in bits, h = log₂n / (n/c)
by definition. At n = 100 the average factor is 16 % shorter than its asymptotic length and h is
20 % too high. These are single draws, so the h column sits above the averaged table further up —
at n = 100 the across-draw standard deviation of h is 0.072, which is itself a reason not to
report a single short-sequence value.
Low-entropy sources buy you less data than their length suggests
Lesne, Blanc & Pezard (2009) give the rule that a sequence behaves as if it had effective
length n_eff = n·h / ln k. A near-constant sequence of a million symbols carries the
statistical weight of a much shorter random one, which is why the p = 0.01 row above is
still 9 % off at n = 10⁵.
epsilon — how untrustworthy a short value is¶
lz.lz76() returns epsilon, the Lempel–Ziv 1976 finite-size term. It is the most readable
indicator of whether a short-sequence result is worth reporting, and it costs nothing extra: it is
computed during the factorization you already ran.
ε(n) = 2 · (1 + logb logb(α·n)) ⁄ logb n
- α
- the
alphabetargument, or the auto-detected byte alphabet - b
- the
log_baseargument, or the same auto-detected value - n
- sequence length
The LZ76 bound c < n ⁄ [(1 − ε) logσ n] is stated only for ε < 1.
Read it as a ceiling on h: the bound implies h < 1/(1 − ε), and while ε ≥ 1 there is no bound
at all. Measured on random binary sequences:
| n | epsilon |
implied ceiling on h |
measured h |
|---|---|---|---|
| 100 | 1.1843 | none — bound inapplicable | 1.1959 |
| 361 | 0.9999 | 6 747 | 1.1061 |
| 1 000 | 0.8940 | 9.44 | 1.0663 |
| 10 000 | 0.7280 | 3.68 | 1.0325 |
| 100 000 | 0.6187 | 2.62 | 1.0195 |
| 1 000 000 | 0.5406 | 2.18 | 1.0123 |
For binary input ε first drops below 1 at n = 361. Even at a million symbols it is still 0.54.
epsilon is a bound parameter, not a correction you subtract from h.
h above 1 at short lengths is expected, not a bug
A normalised h greater than 1 does not mean the estimator is broken — the theory permits it
at every length you will ever handle. h ≤ 1 is an asymptotic property. The binary ceiling is
still 2.18 at n = 10⁶, and below n = 361 there is no ceiling at all. If you see h a little
above 1 on high-entropy data, check epsilon before you check your pipeline.
epsilon is 0.0 on single-symbol sequences, and that zero is meaningless
lz.lz76("0" * 10000)["epsilon"] returns 0.0, which reads as "the bound is tight here". It
is not: the true binary ε(10⁴) is 0.7280. Constant sequences take a short-circuit path that
never computes ε. Only trust epsilon when the sequence contains at least two distinct
symbols.
epsilon gets worse as the alphabet grows, because the bound is stated in base-σ logarithms. The
length at which it first drops below 1:
| alphabet k | typical data | first n with ε < 1 | ε(10³) | ε(10⁶) |
|---|---|---|---|---|
| 2 | binarised spikes, bits | 361 | 0.894 | 0.541 |
| 4 | DNA | 475 | 0.919 | 0.547 |
| 20 | amino acids | 6 117 | 1.214 | 0.683 |
| 26 | lowercase text | 9 882 | 1.273 | 0.711 |
| 64 | coarse-grained bins | 53 630 | 1.487 | 0.814 |
| 256 | raw bytes | 778 351 | 1.840 | 0.984 |
See Alphabets and encoding for how k is detected and why it counts bytes.
How much data do you need¶
There is no distribution-free answer. What follows is calibrated on i.i.d. and Markov sources at
the alphabet sizes shown. Correlated sources need more, not less: at n = 1 000 a two-state chain
with p_flip = 0.1 sits +3.9 % above its entropy rate while an i.i.d. source of the same
entropy rate sits +0.7 % above — roughly five times further out, for the same length and the
same alphabet (mean of 200 draws each).
| Goal | k = 2 | k = 4 (DNA) | k ≈ 20–30 (text, AA) | k = 256 (bytes) |
|---|---|---|---|---|
| Rank-order comparison, equal length and alphabet | n ≳ 10³ | n ≳ 10³ | n ≳ 10⁴ | n ≳ 10⁵ |
| ε < 1 — the LZ bound applies at all | n > 361 | n > 475 | n > ~10⁴ | n > 7.8·10⁵ |
Absolute h good to ~5 % |
n ≳ 10³–10⁴ | n ≳ 10³ | not reached by n = 10⁷ | not reached by n = 10⁷ |
Absolute h good to ~1 % |
n ≳ 10⁶–10⁷ | plateaus at 1.2–1.4 % low, never cleanly reached | not practically reachable | not practically reachable |
Absolute h good to ~0.1 % |
not practically reachable | — | — | — |
Those rows come from lz.h on maximum-entropy i.i.d. sources, where the true normalised value is
exactly 1.0 at every alphabet size:
| n | k = 2 | k = 4 | k = 20 | k = 26 | k = 256 |
|---|---|---|---|---|---|
| 10³ | 1.0464 | 0.9982 | 0.8778 | 0.8728 | 0.7334 |
| 10⁴ | 1.0232 | 0.9859 | 0.9105 | 0.9023 | 0.8174 |
| 10⁵ | 1.0170 | 0.9860 | 0.9247 | 0.9077 | 0.8433 |
| 10⁶ | 1.0124 | 0.9869 | 0.9311 | 0.9237 | 0.8448 |
| 10⁷ | 1.0092 | 0.9878 | 0.9388 | 0.9380 | 0.8990 |
Only the binary column approaches 1 from above; every larger alphabet reads low, and at
k = 256 it is still 10 % low at ten million symbols. The k = 4 column is not even monotone — it
happens to land within 0.2 % at n = 10³ and then drifts away to 1.4 % low, so a single
agreement at one length proves nothing. Absolute h is not comparable across alphabets; see
Alphabets and encoding.
By measure, rather than by alphabet:
| Measure | Worth reporting from | What it needs, and why |
|---|---|---|
complexity |
any n | Exact at every length — it is a count, not an estimate. But it is comparable only at fixed n and fixed alphabet. |
h |
n ≳ 10³ to rank, 10⁶ to quote | Bias falls off like a power of 1/log n, and its sign depends on the source. Report epsilon alongside it. |
emc |
n ≳ 10⁴, and only to rank | Rests on one surrogate per block size, at a block-size ceiling that is itself a function of n. Non-negative by construction, so its noise on structureless input is one-sided: over 50 i.i.d. binary draws at n = 2 048 the null sits at mean 0.15, s.d. 0.17, up to 0.58, and is exactly 0.0 in 19 of them. Subtract that floor before reading a small value as signal. |
nid |
n ≳ 10³ for shape, 10⁴–10⁵ for a number | A ratio, so the leading length factors cancel — but both endpoints are compressed toward the middle at short n (below). |
multi_information |
diagnostic only | Built from the block-size-1 shuffle, which performs n/2 single-symbol swaps and so never touches about 37 % of the positions (e⁻¹) at any n. |
See Entropy density, Effective measure complexity and Normalised information distance for what each of these measures is.
Never compare raw complexity or emc across lengths¶
This is the one hard rule on the page. complexity grows with n for any source; emc depends on
a block size that is itself computed from n. Comparing either between sequences of different
lengths measures the lengths, not the sequences.
Two sequences from the same sticky-Markov source (p_flip = 0.05), one of 4 000 symbols and
one of 12 000:
| n = 4 000 | n = 12 000 | verdict | |
|---|---|---|---|
complexity |
102 | 252 | 2.5× apart — an artefact of length |
h (bits) |
0.3051 | 0.2846 | comparable |
complexity after truncating both to 4 000 |
102 | 90 | comparable |
| surrogate-normalised LZc | 0.296 | 0.278 | comparable |
emc is worse, because the estimator's largest block size mm is derived from n. The identical
period-8 square wave, at three lengths, read off lz.lz76(seq) (mm is
result["emc"]["max_block_size"]):
| n | complexity |
h |
emc |
mm |
|---|---|---|---|---|
| 2 000 | 3 | 0.016449 | +5.2992 | 18 |
| 5 000 | 3 | 0.007373 | +5.7433 | 19 |
| 20 000 | 3 | 0.002143 | +5.4308 | 21 |
Same source, same generating rule, emc moves by 8 % — and it does not even move monotonically with
n. Widen the range and the drift is larger: the same motif measured from n = 1 000 to
n = 100 000 falls from 5.9296 to 4.9701, 16 % on a source whose true excess entropy is a constant
3 bits.
emc is evaluated on a ladder of block sizes whose top rung is a function of n
The estimator builds one rung per block size, Ê(l) = l · g · (C_LZ(shuffled at l) −
C_LZ(original)) with g = log_k(n)/n, and reads the value off the top of the ladder after
projecting it onto the non-negative non-decreasing cone. Every scale contributes, but the ceiling
mm is derived from n and the rungs keep climbing past the block length the sample can support
— so two lengths mean two different ladders and the totals are not on a common axis. Comparing
emc across lengths compares two different estimators. See
Effective measure complexity.
One ambiguity that used to live here is gone. Up to 1.0.2 the total telescoped to its own top
rung, so a periodic sequence whose period divided mm and a structureless random one both landed
at zero, for opposite reasons. A zero now means only the second thing.
Fix 1 — truncate to the shortest¶
The cheapest fix and the one to reach for first. It costs you data and nothing else.
import random
import lzcomplexity as lz
def sticky(n, p_flip, seed):
"""Two-state Markov chain that flips with probability p_flip."""
rng = random.Random(seed)
bit, out = "0", ["0"]
for _ in range(n - 1):
if rng.random() < p_flip:
bit = "1" if bit == "0" else "0"
out.append(bit)
return "".join(out)
a = sticky(4000, 0.05, 1)
b = sticky(12000, 0.05, 2)
def compare_at_common_length(sequences):
"""Truncate every sequence to the shortest, then take raw complexity."""
n = min(len(s) for s in sequences)
return [lz.factorization(s[:n])[0] for s in sequences]
print(lz.factorization(a)[0], lz.factorization(b)[0]) # 102 252 <- not comparable
print(compare_at_common_length([a, b])) # [102, 90] <- comparable
Truncate from the same end for every sequence and say which end in your methods. If the sequences are non-stationary — a recording that drifts — truncating to a common window is more defensible than truncating to a common prefix.
Fix 2 — normalise against shuffled surrogates of the same length¶
Divide the complexity by the mean complexity of random permutations of the same symbols. The surrogate has the same length, the same alphabet and the same symbol frequencies, so both the length dependence and the composition dependence divide out. What is left is temporal structure.
import random, statistics
import lzcomplexity as lz
def lzc_normalised(seq, n_surrogates=20, seed=0):
"""Complexity divided by the mean complexity of shuffles of the same symbols."""
rng = random.Random(seed)
c = lz.factorization(seq)[0]
ref = []
for _ in range(n_surrogates):
shuffled = list(seq)
rng.shuffle(shuffled)
ref.append(lz.factorization("".join(shuffled))[0])
return c / statistics.fmean(ref)
print("%.3f %.3f" % (lzc_normalised(a), lzc_normalised(b))) # 0.296 0.278
The shuffles cost more than the factorizations
A single lz.factorization on a 100 000-symbol binary sequence takes 6.9 ms
(Performance), so 20 surrogates spend 0.14–0.25 s in the library.
Building the 20 shuffled strings in Python costs four times that. End-to-end wall clock for
the call above measured between 0.7 s and 1.5 s across repeated runs on a shared machine, and
the split between the two stages was stable at about 4:1 in every one of them. Surrogate
normalisation is cheap enough to run on every sequence in a study rather than on a subsample —
and if it is not, replace random.shuffle before you blame the factorization.
The normalised value has a natural reading: 1.0 means "indistinguishable from a shuffle of
itself", and lower means "carries temporal structure that a shuffle destroys". Measured on five
signals at n = 10 000, random.Random(0) for the stochastic ones and x₀ = 0.4 with 2 000 burn-in
steps for the map:
| signal | LZc | LZc (shuffled) | normalised | h (bits) |
|---|---|---|---|---|
| fair coin (i.i.d.) | 777 | 772.0 | 1.006 | 1.032 |
| biased coin p = 0.1 | 351 | 351.2 | 0.999 | 0.466 |
| sticky Markov p_flip = 0.05 | 229 | 772.9 | 0.296 | 0.304 |
| period-8 square wave | 3 | 772.6 | 0.004 | 0.004 |
| logistic map r = 4.0 | 768 | 773.1 | 0.993 | 1.020 |
The two structureless rows land at 1.006 and 0.999, not at exactly 1.000 — with 20 surrogates the ratio scatters a few thousandths either side, so treat 1.0 as a neighbourhood rather than a line.
Read the last row against the third. The logistic map at r = 4 is fully deterministic, and the normalised score correctly reports that it looks random to LZ76 — a shuffle of it is no more complex than the original. Determinism and low complexity are not the same thing; see Dynamical systems.
Both fixes on one pair, start to finish
Run after the two snippets above, which define sticky, a, b and lzc_normalised.
print("raw complexity :", lz.factorization(a)[0], lz.factorization(b)[0])
print("truncated to 4000:", lz.factorization(a[:4000])[0], lz.factorization(b[:4000])[0])
print("normalised LZc : %.3f %.3f" % (lzc_normalised(a), lzc_normalised(b)))
print("h (bits) : %.4f %.4f" % (lz.h(a, log_base=2), lz.h(b, log_base=2)))
raw complexity : 102 252
truncated to 4000: 102 90
normalised LZc : 0.296 0.278
h (bits) : 0.3051 0.2846
Raw complexity says the second sequence is 2.5× more complex. Every length-controlled measure
says the two came from the same source, which they did.
A textbook count is one higher when the parse overruns the end
complexity counts only complete LZ76 components. When the final component runs past the
end of the sequence it is not counted, so the library's number is one less than the textbook
exhaustive-history count in exactly that case:
Checked against a brute-force exhaustive-history parser over all 131 070 binary strings of
length 1 … 16, this is exact for every string containing at least two distinct symbols, and
wrong for every constant one — lz.factorization("0000") returns (1, [0, 1, 4]), so the
formula gives 1 where the textbook gives 2. That is the same short-circuit that zeroes
epsilon above.
The difference is O(1) and irrelevant to h asymptotically, but it matters when you are
checking a short worked example against a paper.
nid is a ratio, so it degrades more gracefully — but it still degrades¶
nid is built from a ratio of factor counts, so the log_k(n)/n factor that dominates the
finite-size behaviour of h cancels. That makes it the least length-sensitive measure in the
library. It is not length-independent.
Two independent fair-coin sequences should be maximally distant, nid ≈ 1. Five draws at each
length:
| n | nid between two independent fair coins |
|---|---|
| 10 | 0.7500 0.2500 0.4000 0.5000 0.7500 |
| 50 | 0.7000 0.7000 0.7778 0.6364 0.7000 |
| 100 | 0.6667 0.7500 0.6667 0.6667 0.7059 |
| 1 000 | 0.8148 0.8037 0.7944 0.7798 0.8019 |
| 10 000 | 0.8562 0.8501 0.8544 0.8566 0.8588 |
| 100 000 | 0.8862 0.8844 0.8834 0.8826 0.8798 |
The true value is approached from below and is still 0.88 at n = 10⁵. The other endpoint moves too — a sequence compared with itself should give exactly 0:
| n | nid(a, a) |
|---|---|
| 10 | 0.333333 |
| 50 | 0.111111 |
| 100 | 0.062500 |
| 1 000 | 0.009709 |
| 10 000 | 0.001292 |
Both endpoints are pulled toward the middle at short n, so short-sequence distances have less dynamic range than they appear to. The span from "identical" to "unrelated" is 0.06 → 0.69 at n = 100, and 0.001 → 0.86 at n = 10⁴.
Below about 100 symbols nid takes a handful of distinct values and stops discriminating
nid on very short inputs is a ratio of small integers, so it collapses onto a coarse grid:
nid("0", "1") is 1.0, nid("01", "10") is 0.5, and nid("abc", "abd") and
nid("0110", "1001") are both exactly 0.3333333333333333. Two genuinely different pairs
landing on the same value is the normal case at that length, not a coincidence. Any tree or
embedding built from a distance matrix over sequences that short is reading quantization noise.
For clustering and distance matrices: use sequences of comparable length, prefer n ≳ 10⁴ before reading absolute distances, and treat differences of a few hundredths as noise unless you have checked them against a permutation null.
Checklist before you report a number¶
Fix the length¶
Truncate every sequence in the comparison to the shortest, or normalise each against surrogates of
its own length. Never compare raw complexity or emc at different n.
seq[:n_min]
Fix the alphabet¶
Pass log_base= explicitly. The default is the alphabet auto-detected in that sequence, so two
files drawn from the same four-letter alphabet get different normalisations if one happens to be
missing a letter.
lz.h(seq, log_base=4)
Report epsilon¶
Quote epsilon next to any absolute h. If it is at or above 1, the LZ bound does not apply to
your sequence and the value is a ranking statistic, nothing more.
lz.lz76(seq)["epsilon"]
Build your own error bars¶
normal_error and poison_error are heuristic dispersion indicators inherited from the C++
library, not confidence intervals. Use surrogates or sub-block resampling instead.
lzc_normalised(seq)
Continue to Reading the numbers for what each field of the result dictionary means, or to References for the papers behind the convergence results quoted here.