2026, Jan 09 21:03

Почему после понижения до float32 в Polars меняется вывод чисел

Разбираем, почему при понижении разрядности с float64 до float32 в Polars меняется вывод чисел: что происходит с точностью и как настраивать формат отображения.

Понижение разрядности вещественных столбцов часто удивляет даже опытных инженеров: «ровное» число вдруг приобретает лишние цифры. В Polars это проявляется при преобразовании столбца из float64 в float32. Сами значения по‑прежнему сравниваются как ожидается, но вывод на экран выглядит странно. Разберёмся, почему так происходит, что именно вы наблюдаете и как об этом думать.

Как воспроизвести «сюрприз» в выводе

Ниже минимальный пример: создаём DataFrame в Polars, проверяем разность и равенство, затем понижаем разрядность до float32 и смотрим на вывод. Вычисления ведут себя как прежде, но в печати внезапно появляется больше знаков после запятой.

import polars as pl

frame_a = pl.DataFrame({
    "metric": [2021.9952, 2024.0, 2024.25, 2024.456]
})

print("original values")
print(frame_a)

print("Original: diff row1 - row0")
delta_a = frame_a["metric"][1] - frame_a["metric"][0]
print(delta_a)

print("Original value equality")
print(frame_a["metric"][0] == 2021.9952)

print("Downcasting to float32")
print(frame_a.cast({"metric": pl.Float32}))

print("Downcasted: diff row1 - row0")
delta_b = frame_a["metric"][1] - frame_a["metric"][0]
print(delta_b)

print("Downcasted value equality")
print(frame_a["metric"][0] == 2021.9952)

После понижения разрядности вывод показывает значения вроде 2021.995239 и 2024.456055 вместо «аккуратных» 2021.9952 и 2024.456. На вид будто к числам приписали случайные цифры. Это не так.

Что происходит на самом деле

Такое поведение — следствие способа представления и печати чисел с плавающей запятой. В самом Python нет типа float32 — только float64. Когда вы понижаете разрядность в Polars, данные становятся float32. Но при печати в Python эти float32 продвигаются обратно до float64 для отображения. При этом биты float32 сохраняются ровно как есть, а преобразование числа в строку в Python показывает ближайшее десятичное представление этого значения float64. В результате вы видите точное отображение полученного float32, а не исходного float64.

Ключ — точность. У float64 — 53 бита точности, у float32 — лишь 24. При понижении разрядности последние 29 бит отбрасываются округлением. На экране вы видите ближайшее число формата binary32 (float32), выведенное через путь печати binary64 (float64).

Чтобы увидеть это наглядно, посмотрим на одно и то же значение как на float64 и как на float32 с помощью модуля struct — он показывает «сырое» представление без участия библиотек DataFrame:

import struct

x64 = 2021.9952
print(x64)
print(x64.hex())  # 53 бита точности в float64

# Преобразуем в float32 и обратно в float64, не изменяя биты за пределами точности float32
packed32 = struct.pack('f', x64)
x32_as_64 = struct.unpack('f', packed32)[0]
print(x32_as_64)
print(x32_as_64.hex())  # отображает округлённое значение float32

Представление 2021.9952 в формате float64:

0x1.f97fb15b573ebp+10

После преобразования в float32 и обратно получаем:

2021.9952392578125
0x1.f97fb20000000p+10

Обратите внимание: шестнадцатеричное представление округлённого значения оканчивается нулями. Потерянные биты исчезают безвозвратно при понижении разрядности. Значит, на печати вы видите именно ближайшее число float32, а не «добавку».

Есть и другой аспект, который часто сбивает с толку. Числа вроде 2024.456 аккуратны в десятичной базе, но числа с плавающей запятой хранятся как суммы степеней двойки, а не десяти. Это десятичное число нельзя представить точно ни в float64, ни в float32. В float64 просто хватает бит, чтобы форматирование по умолчанию скрывало аппроксимацию. Стоит показать больше знаков — и правда проступает. Например:

"%.16f" % 2024.456  # -> '2024.4559999999999036'

Почему вывод меняется после понижения разрядности

Понижение разрядности уменьшает точность с 53 до 24 бит. При выводе Python продвигает float32 обратно к float64, но эта операция сохраняет только то, что было в float32. Затем механизм печати показывает ближайшее десятичное значение для продвинутого числа. Поэтому вы видите 2021.995239 вместо 2021.9952 и 2024.456055 вместо 2024.456. Это корректные ближайшие значения float32.

Цифры, которые появляются после понижения разрядности, — это и есть ближайшее представление вашего числа в формате float32, а не «мусор». Это нормальное и ожидаемое следствие ограничений типа float32. Если нужна визуальная единообразность, ограничьте точность отображения; сами значения всё равно будут отличаться из‑за потери точности.

Точная демонстрация в коде

Чтобы явно увидеть границу округления, вот самодостаточный фрагмент, который сопоставляет биты исходного float64 с округлённым float32 и показывает, почему печать меняется после понижения разрядности:

import struct

x64 = 2021.9952
print("float64 value:", x64)
print("float64 hex:", x64.hex())

# Округляем до float32, затем вновь продвигаем до float64 для печати
p32 = struct.pack('f', x64)
x32_promoted = struct.unpack('f', p32)[0]
print("float32 as printed by Python (promoted to float64):", x32_promoted)
print("float32-promoted hex:", x32_promoted.hex())

Это наглядно показывает, как меняются биты и почему вслед за этим меняется десятичный вывод. Это не специфика Polars: любая система, понижающая разрядность до float32 и выводящая через python-овский float, поведёт себя так же.

Почему это важно на практике

Различие между float32 и float64 критично для отладки, проверки данных и аудита конвейеров. Без этого контекста легко принять вывод за порчу данных. На деле математика остаётся согласованной: сравнения и разности с исходными данными могут давать ожидаемые результаты при одинаковой точности вычислений, но строковое представление после понижения разрядности явно демонстрирует меньшую точность.

Если важна визуальная идентичность с исходными файлами, помните: CSV хранит десятичный текст, а вычисления идут в двоичной плавающей точке. Число, аккуратное в текстовом редакторе, в бинарном виде может никогда не храниться «как есть». После понижения разрядности эта аппроксимация становится заметнее.

Практические выводы и напоследок

Если цель — удобный просмотр, настройте ограничение точности вывода, чтобы рутинная печать не выносила на поверхность бинарные артефакты. Если важна численная точность, оставляйте чувствительные столбцы в float64 или явно фиксируйте компромисс по точности при понижении разрядности. В любом случае «лишние цифры» в печати не случайны — это корректные ближайшие значения float32, продвинутые для отображения. Понимание этого помогает отличать косметические различия от реальных проблем с данными и сосредоточивать внимание на сути, а не на шуме форматирования.