Rational Numbers Hidden in Floating Point: Recovering Exact Fractions with the Stern-Brocot Tree and Continued Fractions
This article shows how an ordinary floating-point value can silently leak the exact rational ratio behind it, and how to recover it. Starting from a real-world example, it explains why IEEE 754 doubles tend to expose terminating or repeating decimal expansions, introduces the Stern-Brocot tree and continued fractions as the underlying "best rational approximation" tools, and walks through two practical Python implementations (Fraction.limit_denominator and a manual continued-fraction/convergents approach). It then shifts to the engineering side, offering concrete practices for precision governance and preventing unintended information disclosure at serialization boundaries. Suitable for developers working on metering, billing, ratios, and API responses.
1. Starting From a Suspiciously “Precise” Floating-Point Number
While inspecting the usage statistics of an intelligent service from the frontend, a developer noticed an odd value hidden in the API response: 0.16327272727272726. It carries 17 significant digits, and starting from the fifth digit, the pair “27” repeats indefinitely.
Ordinary measurement noise or random sampling results never look like this. Floating-point numbers that arise by chance in the real world tend to look “messy.” But when a floating-point value shows a clean, regular repeating cycle, you can be almost certain that it is fundamentally the result of dividing one integer by another — it just wasn’t rounded before being exposed.
In fact, 0.16327272727272726 can be reconstructed exactly into a fraction — approximately 449/2750. The denominator 2750 = 2 × 5³ × 11, and it is precisely that factor of 11 that determines the length of the repeating cycle in the decimal part to be 2 (i.e., “27” repeats). In other words, if a system internally computes a “usage ratio” as this number, its true numerator and denominator are leaked along with it.
The general value of this case is that almost every metering or billing system performs division — computing display values as “credits consumed / total quota,” “per-model multipliers,” “unit price / quantity,” and so on. Once these division results enter an API response, a page, a log, or an export file without being rounded, the underlying exact ratio (often the most commercially sensitive constant) is leaked along with them.
2. Why It Leaks: The Natural Link Between Floating Point and Rationals
The float / double types in modern programming languages follow the IEEE 754 double-precision standard: 1 sign bit, 11 exponent bits, and 52 mantissa bits, which translates to roughly 15–17 significant decimal digits of precision.
Two points are key:
- First, most “business ratios” are mathematically rational numbers, meaning they can be written as p/q. Finite quotas, integer counts, and multiplier configurations are almost all rational.
- Second, when a program computes
a / b, the result is “the double-precision floating-point value closest to the true rational p/q.” When this floating-point value is formatted into a decimal string, it often “prints” the original rational’s structure (its terminating or repeating decimal expansion) faithfully.
So the essence of the leak is not that floating point is “imprecise” — quite the opposite. It is too precise, precise enough to expose the numerator and denominator that shouldn’t be shown.
3. The Mathematical Weapon: The Stern-Brocot Tree and Continued Fractions
To reverse-engineer the simplest fraction from a decimal string, you don’t rely on guessing, but on the mathematically established tool of “best rational approximation.”
The Stern-Brocot tree is a binary tree that contains every positive rational number, with each rational appearing exactly once (in simplest form). Its construction is elegant: between the two sentinels 0/1 and 1/0, you repeatedly take the “mediant” — for a/b and c/d, the mediant is (a+c)/(b+d). Each mediant generates a new node, and recursing this way, every positive rational appears somewhere, always in reduced (simplest) form.
Continued fractions are another expression of the same thing. Any rational number can be expanded into a finite continued fraction [a₀; a₁, a₂, …, aₙ], whose “convergents” are exactly the sequence of optimal approximations obtained when approaching the target along the Stern-Brocot tree — each one is closer to the true value than the last, with the smallest possible denominator.
Therefore, “reverse-engineering a fraction from a floating-point number” is mathematically equivalent to: find the continued-fraction expansion of that floating-point number, then take the convergent with a sufficiently small error. This is precisely what Python’s standard-library Fraction(x).limit_denominator(N) does internally.
4. Code in Practice: Reverse-Engineering the Exact Fraction From a Float
Below are two implementations you can reuse directly. Method 1 is the most convenient; Method 2 helps you see how it works under the hood.
from fractions import Fractionfrom math import floor# An unrounded floating-point ratio returned by the APIobserved = 0.16327272727272726# ---------- Method 1: Best rational approximation (recommended) ----------# limit_denominator is based on the continued-fraction idea;# it returns the closest fraction whose denominator does not exceed max_denominatorfrac = Fraction(observed).limit_denominator(1_000_000)print(f"Reconstructed: {frac.numerator}/{frac.denominator}") # 449/2750print(f"Error: {abs(float(frac) - observed):.3e}") # at machine-precision level# ---------- Method 2: Manual continued-fraction expansion + convergents ----------def to_continued_fraction(x, max_terms=30, eps=1e-15):"""Expand a floating-point number into continued-fraction coefficients [a0; a1, a2, ...]"""coeffs = []for _ in range(max_terms):a = floor(x)coeffs.append(a)remainder = x - aif remainder < eps: # already converged to a rationalbreakx = 1.0 / remainderreturn coeffsdef convergents(coeffs):"""Recursively derive convergents from continued-fraction coefficients,i.e. the approximation path on the Stern-Brocot tree"""p_prev, p_curr = 1, coeffs[0]q_prev, q_curr = 0, 1result = [(p_curr, q_curr)]for a in coeffs[1:]:p_prev, p_curr = p_curr, a * p_curr + p_prevq_prev, q_curr = q_curr, a * q_curr + q_prevresult.append((p_curr, q_curr))return resultcoeffs = to_continued_fraction(observed)print("Continued-fraction coefficients:", coeffs) # approximately [0, 6, 8, 56]for p, q in convergents(coeffs):print(f"{p}/{q} ≈ {p/q:.12f}")# 0/1# 1/6 ≈ 0.166666666667# 8/49 ≈ 0.163265306122# 449/2750 ≈ 0.163272727273 ← match
As you can see, the convergents 1/6 → 8/49 → 449/2750 approach the target step by step, and the last one matches the observed value exactly within machine precision — a successful reconstruction. One caveat: due to error in the last floating-point digits, the final coefficient of the continued fraction may jitter by ±1, but the convergence result of the convergents is unchanged.
Here’s a practical rule of thumb: first set a “business-reasonable maximum denominator” (for example, when quotas are counted in thousands or tens of thousands, the denominator is usually not very large), then solve with limit_denominator. This effectively prevents floating-point error from blowing the denominator up to astronomical numbers.
5. The Engineering View: Precision Governance and the Boundary of Disclosure
Being able to “reverse-engineer” a value is only half of understanding the problem; the more important half is “not leaking it.” Once data like usage, billing, and multipliers is mishandled, a system’s most sensitive pricing constants can flow out through the most inconspicuous decimal digits. A few general practices:
- Round uniformly at the serialization boundary. For external API responses, pages, exported CSVs, and logs, always output a fixed number of decimal places (e.g., two, or integers), and never serialize a raw internal
doublecomputed during processing. - Measure with integers or fixed-point numbers. Wherever possible, keep credits, tokens, and quotas internally as integers (or integers scaled up by some factor), and perform division with rounding only at final display. This avoids both floating-point error and leakage.
- Always use
Decimal, notfloat, for money. For anything involving prices or bills, use a decimal fixed-point type. This solves precision loss and eliminates the “repeating decimal” leakage caused by binary floating point. - Decouple the internal model from the external view. Through a DTO or serialization layer, map “internal true values” to “business-readable display values,” so that leakage risk is handled at a single, controllable boundary.
- Watch for implicit leakage channels. Percentages, averages, unit prices, progress values, and chart coordinates — every “seemingly harmless” ratio is a rational number and can likewise be reconstructed; logs and the browser’s network panel are especially common offenders.
For a platform like Baidu AI Cloud that offers metering and billing capabilities, decoupling internal pricing constants from externally displayed values — and rounding uniformly at the serialization boundary — is a general and necessary engineering practice. This principle applies to any system that needs to display usage and billing information.
6. Summary
Behind a single observation of “17 decimal places” lies a classic computational-math problem: floating-point numbers faithfully expose the structure of rational numbers, and the Stern-Brocot tree and continued fractions can reconstruct those rationals exactly. As developers, on one hand we should master the “reconstruction” method (remembering that Fraction(x).limit_denominator is usually enough), and on the other we must build an awareness of “not leaking” — rounding at the boundary, measuring with integers or Decimal, and thoroughly decoupling internal constants from external displays. Understanding precision is about both computing accurately and keeping secrets safe.
