Несколько версий заказа: выбираем актуальную запись по времени

Выгрузка содержит историю заказов: сначала сумма 100, потом исправление до 90. Обычный drop_duplicates оставляет строку, которая случайно оказалась первой. Через неделю файл приходит в другом порядке, и итог меняется. Актуальность нужно связать с версией.

История состояния требует своего ключа

Если каждая строка является отдельной операцией, удалять повторный order_id нельзя. Но в таблице состояния из нескольких снимков нужен последний. Подтвердите, что updated_at задаёт время изменения и что версия содержит все поля. Частичные обновления сначала требуют восстановления состояния: последняя строка с одним статусом может не содержать суммы.

Два разных состояния с одинаковым временем нельзя упорядочить только по дате. В примере источник даёт возрастающий revision. Если такого поля нет, конфликт надо вывести для разбора. Расположение строк в файле не является правилом актуальности.

Сначала порядок, затем удаление прежних версий

Мы запрещаем повтор пары order_id и revision, сортируем историю и выбираем последнюю запись заказа. Ревизия разрешает равенство времени. В вашем источнике порядок может определяться только монотонным номером версии: используйте именно подтверждённое правило.

Сортировка итоговой таблицы по номеру заказа нужна для чтения. Она уже не участвует в выборе актуального состояния.

import pandas as pd
history = pd.DataFrame({"order_id": [1, 2, 1, 1],
    "updated_at": ["2026-10-01", "2026-10-02", "2026-10-03", "2026-10-03"],
    "revision": [1, 1, 2, 3], "amount": [100, 50, 95, 90]})
history["updated_at"] = pd.to_datetime(history["updated_at"], format="%Y-%m-%d")
assert not history.duplicated(["order_id", "revision"]).any()
latest = (history.sort_values(["updated_at", "revision"])
          .drop_duplicates("order_id", keep="last").sort_values("order_id"))
print(latest.to_string(index=False))
assert latest.set_index("order_id")["amount"].to_dict() == {1: 90, 2: 50}
assert latest["amount"].sum() == 140

Выбраны ревизия 3 заказа 1 и единственная версия заказа 2. Итог равен 140. Суммирование исходной истории дало бы бессмысленную смесь прежних и новых состояний.

Состояние сегодня и состояние на дату

Для исторического отчёта сначала отберите обновления, известные к моменту закрытия, затем выбирайте последнюю версию внутри среза. Иначе октябрьский отчёт задним числом получит ноябрьские исправления. Храните дату среза и выбранные ревизии, чтобы объяснять изменения результата.

Заранее обработайте удаление. Последняя версия может содержать deleted. Сначала выберите последнее состояние, а затем исключите удалённую сущность. Фильтрация deleted до выбора версии способна воскресить старую запись. Полное отсутствие заказа в новом снимке — другой протокол; смешивать его с журналом изменений нельзя.

Сохраните исходную историю, число версий, число сущностей и конфликтные пары. Нормальная история бывает в несколько раз длиннее актуального состояния; эта разница сама по себе не является браком данных.

Переставьте исходные строки и повторите расчёт. При определённом порядке версий результат должен остаться тем же. Добавьте контроль равного времени и разных ревизий, а также повторной доставки одной ревизии. Тогда изменение формата выгрузки не вернёт скрытую зависимость от порядка строк.

Закрепить на практике

В бесплатном уроке о дублях вы потренируете duplicated и drop_duplicates по ключевым столбцам. Затем примените их к истории: повтор заказа ожидаем, а повтор заказа вместе с ревизией требует проверки. Откройте бесплатный урок и задачи. Полная последовательность тем — в бесплатном курсе.

Продолжить на практике

  • NumPy и pandas: практический тренажёр ↗

    Бесплатный тренажёр NumPy и pandas для тех, кто знает основы Python. Научитесь очищать таблицы, объединять данные и собирать отчёты: 31 урок и 124 задачи с кодом и автоматической проверкой.