Деньги из CSV в ETL: проверяем масштаб до загрузки в витрину
Поставщик присылает сумму как текст. Одни строки содержат 12.30, другие — 12.345, иногда попадается NaN. Если сначала преобразовать всё в float, а затем округлить, загрузка смешает корректные платежи и значения, которые вообще не соответствовали договору.
Для платежей в рублях договоримся о неотрицательной сумме максимум с двумя дробными знаками, точке как разделителе и верхнем пределе двенадцать целых цифр. В витрину пойдут целые копейки и код валюты. Лишняя точность считается ошибкой поставки, поэтому молчаливого округления не будет.
Разберём формат до вычисления
Пример читает CSV из памяти. Поле amount остаётся строкой до проверки регулярным выражением. Decimal создаётся из этой строки, затем сумма переводится в копейки в собственном контексте. Ошибочная запись сохраняет идентификатор и причину.
import csv
import io
import re
from decimal import Decimal, Context, localcontext
pattern = re.compile(r"(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,2})?\Z")
text = "id,currency,amount\nA,RUB,12.30\nB,RUB,0.01\nC,RUB,12.345\nD,RUB,NaN\n"
accepted, rejected = [], []
for row in csv.DictReader(io.StringIO(text)):
if row["currency"] != "RUB" or not pattern.fullmatch(row["amount"]):
rejected.append((row["id"], "currency or scale"))
continue
with localcontext(Context(prec=32)):
kopeks = int(Decimal(row["amount"]) * 100)
accepted.append((row["id"], row["currency"], kopeks))
assert accepted == [("A", "RUB", 1230), ("B", "RUB", 1)]
assert [x[0] for x in rejected] == ["C", "D"]
print(accepted)
print(rejected)
В приёмник готовы 1230 и 1 копейка. Строки с тремя дробными знаками и NaN отклонены до арифметики. Пробелы, разделители тысяч, запятая и знак минус тоже не входят в выбранный формат. Если поставщик использует их законно, расширьте договор явно, сохранив контроль неоднозначных значений.
Представление копейками удобно для суммирования и записи в BIGINT. При поддержке других валют нельзя автоматически умножать каждое значение на сто: число дробных разрядов задаётся валютой или внутренним денежным контрактом. Храните валюту вместе с суммой и не складывайте разные валюты без отдельного преобразования.
Что сохранять при загрузке
Рядом с ошибкой полезны идентификатор файла, номер бизнес-записи и версия правила. Полный исходный ряд может содержать закрытые сведения, поэтому доступ к карантину должен соответствовать доступу к исходной поставке. В рабочую витрину ошибки не должны попадать как нули: нулевая сумма имеет собственный смысл.
Перед публикацией сверьте количество входных записей с принятыми и отклонёнными. Если доля ошибок превысила согласованную границу, остановите выпуск витрины и сохраните отчёт для разбора. Успешное чтение CSV ещё не означает успешную поставку финансовых данных.
Повторная поставка не должна удваивать деньги
Сохраняйте платежи по устойчивому бизнес-ключу и версии источника. Тогда исправленный файл обновит выбранные значения по понятному правилу, а повтор того же файла не добавит новую оплату. Сам Decimal отвечает за точное представление числа, но не выбирает ключ и порядок обновления.
Для проверки добавьте минимальную копейку, верхнюю разрешённую сумму, ноль, 0.005 и повторный id. Первые три проверяют диапазон, 0.005 — запрет лишнего масштаба, повторный id — отдельную логику загрузки. Такой набор охватывает именно денежный вход ETL, а не только одну красивую сумму.
В Data Engineering Fast-Track вы свяжете контракты данных, batch-обработку и публикацию результата в lakehouse, затем соберёте итоговый pipeline. Изучите программу и перенесите контроль поставок в регулярный конвейер.
Продолжить на практике
- Data Engineering Fast‑Track: Kafka → Airflow → Spark ↗
Практический fast-track по data engineering: за 4–6 недель собираем рабочий конвейер Kafka → Spark → Lakehouse под управлением Airflow. Разберём batch и streaming, окна и watermark, схемы и контракты. Мини‑проекты, Docker Compose, шаблоны DAG и пайплайнов. Курс подойдёт инженерам, аналитикам и тем, кто хочет освоить современные пайплайны данных с уверенным Python.
