惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
博客园_首页
N
News | PayPal Newsroom
有赞技术团队
有赞技术团队
The Hacker News
The Hacker News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
S
SegmentFault 最新的问题
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
P
Privacy & Cybersecurity Law Blog
AI
AI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Schneier on Security
Schneier on Security
博客园 - 三生石上(FineUI控件)
月光博客
月光博客
量子位
Forbes - Security
Forbes - Security
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
SecWiki News
SecWiki News
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tor Project blog
Recorded Future
Recorded Future
A
About on SuperTechFans
J
Java Code Geeks
The Register - Security
The Register - Security
PCI Perspectives
PCI Perspectives
H
Hacker News: Front Page
V2EX - 技术
V2EX - 技术
S
Secure Thoughts
V
Vulnerabilities – Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Scott Helme
Scott Helme
T
The Exploit Database - CXSecurity.com
Y
Y Combinator Blog
AWS News Blog
AWS News Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
IT之家
IT之家
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
C
CERT Recently Published Vulnerability Notes
L
LangChain Blog
F
Full Disclosure
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The GitHub Blog
The GitHub Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Раскидываем транзакции начислений Ozon по SKU
delffine · 2026-05-06 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение7 мин

Охват и читатели584

Кейс

Если вы работали с API Ozon, то наверняка испытывали смешанные чувства, поняв, что отчет по транзакциям формируется по отправлениям. а не по товарам, как в WB. И проблема в том, что в одной строке ответа API метода v3/finance/transaction/list мы имеем данные по отправлению, в котором может быть несколько товаров. При этом указанная сумма покупки (accruals_for_sale), комиссии, логистики и других начислений в отчете указана одна, то есть на все отправление в целом. А значит встает задача, посчитать сколько из общей суммы приходится на единицу товара.

В этом статья поделюсь, как я решил эту проблему в своей системе аналитики продаж через Wildberries и Ozon. Напомню, небольшой обзор своей системы WBOZYA-dash я делал в первой статье. А как получать данные из API маркетплейсов без ошибок 429 и 50x во второй статье.

Выход вообщем-то очевиден: как-то разделить общую сумму стоимости отправления по товарам.

Самый простой способ - разделить поровну по всем товарам в отправлении. Однако у такого подхода есть большой недостаток - могут быть погрешности, особенно если цены товаров существенно различаются. Например, если в одном отправлении есть товар за 1000 рублей и за 200 рублей, то после такого деления accruals_for_sale "поровну" у каждого будет стоимость по 600 рублей. Несмотря на этот недостаток, такой подход все равно имеет право на существования - все лучше чем, ничего или просто отбрасывать отправления с несколькими товарами.

Второй способ - разделить пропорционально стоимости товаров, буквально вычислять пропорцию стоимости одного товара к общей сумме. Очевидно, для этого потребуется откуда то брать стоимость товаров. С учетом того, что цены постоянно меняются, надо брать цену именно из тех отправлений, для которых мы делим раскладку транзакций. А значит должна быть еще и база данных, где будут храниться старые отправления. Вообщем второй способ технически выглядит более сложным, чем первый, но не тройной интеграл =)

Делим поровну

Рассмотрим реализацию такого первого подхода при ETL обработке данных из API. На Python код получения транзакций через API OZ выглядит примерно так:

url = “https://api-seller.ozon.ru/v3/finance/transaction/list”
ipage = 1
tract = pd.DataFrame([])
while True:
  data = { 
  "filter": {"date": { "from": fromdate, "to": todate }}, 
  "page": ipage, 
  "page_size": 1000 }

  dd = pd.DataFrame([])
  apioz_response = get_oz(url, headers, data)
  if apioz_response:
  	dd = pd.DataFrame(apioz_response['result']['operations'])
  	tract = pd.concat([tract, dd], ignore_index=True)
    
  if len(dd) < 1000:  break
  ipage +=1

Проблемные строки с несколькими товарами в одном отправлении выглядят так:

Первым делом надо определить SKU, которые есть в отправлении. Можно распарсить json в поле items, а можно представить его как строку и найти SKU через регулярные выражения. Также нам понадобиться общее количество товаров в отправлении, которое также можно найти через регулярные выражения:

tract['sku'] = tract['items'].apply(lambda x: list(re.findall(r"'sku': (\d+)", str(x))))
tract['quanti'] = tract['items'].apply(lambda x: len(re.findall(r"'sku': (\d+)", str(x))))

Получим две новые колонки: колонку, в которой сидит массив SKU, которые есть в отправлении, и колонку с общим количеством товаров в отправлении.

Полученный массив можно легко “раскрыть” командой .explode(). Однако после такого преобразования может получится одна неочевидная загвоздка - дубликаты. Если в одном отправлении несколько одинаковых SKU, то строки с ними после эксплода буду полными дубликатами. Если в логике ETL или при экспорте в БД есть проверка на дубли (а она должна быть), то часть данных потеряется. Чтобы этого избежать надо пронумеровать товары, который были в одной строке транзакций. А чтобы нумерация не зависела от порядка их вывода в поле items, предварительно отсортируем строчки по operation_id и SKU. Сделать это можно вот так:

tr_bysku = tract.explode('sku', ignore_index=True)
tr_bysku = tr_bysku.sort_values(by=['operation_id', 'sku']).reset_index(names='ii')
tr_bysku['add_id'] = tr_bysku.groupby('operation_id')['ii'].rank().astype('int')

В итоге получим новый датасет, в котором в каждой строке один SKU. Однако при этом в поле accruals_for_sale все еще общая сумма для всего отправления. Зато есть поле с количеством товаров в отправлении. То есть для распределения общей стоимости по SKU поровну надо будет просто разделить общую стоимость отправления accruals_for_sale на общее количество товаров quanti.

Можно применить такой же подход к полям sale_commission и amount и даже services, тем самым раскидываем по SKU еще и комиссии, затраты на логистику и прочие начисления Ozon.

Деление пропорционально цене товара

Так как второй способ деления предполагает подтягивание таблицы отправления из базы данных, реализуем его с помощью SQL запроса. Правда, сначала немного доработать код, описанный выше. В частности надо выделать номер отправления, который содержится в json поле posting..

Сделать это также можно регулярным выражением. Прчием можно заменить, что у части транзакций номер отправления имеет вид "dddddddd-dddd-ddd", а у других "dddddddd-dddd" - это номер заказа, а не отправления. Это объясняется, тем что таких транзакции отражено начисление на весь заказ, например, экваринг. Итого предлагаю выделять не только номер отправления, но и номер заказа, причем в разные колонки.

Итого python код будет такой:

url = “https://api-seller.ozon.ru/v3/finance/transaction/list”
ipage = 1
tract = pd.DataFrame([])
while True:
  data = { 
  "filter": {"date": { "from": fromdate, "to": todate }}, 
  "page": ipage, 
  "page_size": 1000 }

  dd = pd.DataFrame([])
  apioz_response = get_oz(url, headers, data)
  if apioz_response:
  	dd = pd.DataFrame(apioz_response['result']['operations'])
  	tract = pd.concat([tract, dd], ignore_index=True)
    
  if len(dd) < 1000:  break
  ipage +=1

# Ведяем sku, posting_id, order_id
tract['sku'] = tract['items'].apply(lambda x: list(re.findall(r"'sku': (\d+)", str(x))))
tract['quanti'] = tract['items'].apply(lambda x: len(re.findall(r"'sku': (\d+)", str(x))))
tract['posting_id'] = tract['posting'].apply(lambda x: ''.join(re.findall(r"'posting_number': '(\d+-\d+-\d+)", str(x))))
tract['order_id'] = tract['posting'].apply(lambda x: ''.join(re.findall(r"'posting_number': '(\d+-\d+)", str(x))))

# Раскрываем массив с товарами 
tr_bysku = tract.explode('sku', ignore_index=True)
tr_bysku = tr_bysku.sort_values(by=['operation_id', 'sku']).reset_index(names='ii')
tr_bysku['add_id'] = tr_bysku.groupby('operation_id')['ii'].rank().astype('int')

# Заполняем пропуски
tr_bysku['accruals_for_sale'] = tr_bysku['accruals_for_sale'].fillna(0)
tr_bysku['sale_commission'] = tr_bysku['sale_commission'].fillna(0)
tr_bysku['amount'] = tr_bysku['amount'].fillna(0)
tr_bysku['service_amount'] = tr_bysku['service_amount'].fillna(0)
tr_bysku['sku'] = tr_bysku['sku'].fillna('0').astype('int64')
            
export_to_sql('oz_transactions_bysku', tr_bysku)

Дальше переходим на SQL. Сначала надо выбрать из таблицы транзакций данные за нужный период. Фильтр sku > 1000 применяется, чтобы выбрать только транзакции в которых указан реальный sku (что делать с транзакциями без sku оставим за скобками)

SELECT
    operation_id 
    , operation_date
    , operation_type_name
    , posting_id
    , order_id
    , sku
    , 1 as ed_count
    , accruals_for_sale
    , sale_commission
    , service_amount
    , amount
FROM    
    oz_transactions_bysku
WHERE
    operation_date::date BETWEEN {{date_from}} AND {{date_to}}
	AND sku > 1000	

Затем соединяем полученную таблицу с таблицей отправлений. Соединение проходит по полям posting_id или order_id и sku. В полученной таблице вычисляем пропорцию для цены конкретного товара от суммы цен всех товаров входящих в отправление по данной транзакции. В отправлении товаров может быть больше, но какие из них могут быть возвращены/отменены, поэтому берем только SKU, которые указаны в транзакции.

SELECT
    operation_id
    , operation_date
    , operation_type_name  
    , t1.sku
	, ed_count
    , accruals_for_sale
    , sale_commission
    , service_amount
    , amount
    , COALESCE(po.saller_price / NULLIF(SUM(po.saller_price) 
            OVER (PARTITION BY t1.order_id, t1.posting_id, operation_type_name), 0)::NUMERIC, 1) as sku_koef      
FROM
    t1
LEFT JOIN
    oz_postings as po
    ON
    t1.sku > 1000
    -- если нет posting_id - соединяем по order_id
    AND CASE WHEN t5.posting_id IS NULL THEN t5.order_id = po.order_id
        ELSE t5.posting_id = po.posting_id END
    AND t5.sku = po.sku

И вот осталось только провести группировку по SKU и просуммировать различные статьи транзакций, предварительно применив вычисленные выше коэффициенты для каждого товара. Итоговый код SQL-запроса будет такой:

WITH
t1 as (
SELECT
    operation_id 
    , operation_date
    , operation_type_name
    , posting_id
    , order_id
    , sku
    , 1 as ed_count
    , accruals_for_sale
    , sale_commission
    , service_amount
    , amount
FROM    
    oz_transactions_bysku
WHERE
    operation_date::date BETWEEN {{date_from}} AND {{date_to}}
	AND sku > 1000 --отсекаем нулевые sku - они используются в другом месте	
),


t2 as (
SELECT
    operation_id
    , operation_date
    , operation_type_name  
    , t1.sku
	, ed_count
    , accruals_for_sale
    , sale_commission
    , service_amount
    , amount
    , COALESCE(po.saller_price / NULLIF(SUM(po.saller_price) 
            OVER (PARTITION BY t1.order_id, t1.posting_id, operation_type_name), 0)::NUMERIC, 1) as sku_koef      
FROM
    t1
LEFT JOIN
    oz_postings as po
    ON
    t1.sku > 1000
    -- если нет posting_id - соединяем по order_id
    AND CASE WHEN t5.posting_id IS NULL THEN t5.order_id = po.order_id
        ELSE t5.posting_id = po.posting_id END
    AND t5.sku = po.sku
)


SELECT
    sku
    , COALESCE(SUM(ed_count) FILTER (WHERE operation_type_name = 'Доставка покупателю'), 0) as ed_count
    , COALESCE(SUM(accruals_for_sale * sku_koef) FILTER (WHERE operation_type_name = 'Доставка покупателю'), 0) as accruals_for_sale     
    , COALESCE(SUM(sale_commission * sku_koef) FILTER (WHERE operation_type_name = 'Доставка покупателю'), 0) as sale_commission  
    , COALESCE(SUM(amount * sku_koef) FILTER (WHERE operation_type_name = 'Оплата эквайринга'), 0) as acquiring
FROM
    t2        
GROUP BY
    1

SQL-запрос, представленный выше, вычисляет только стоимость продавца, экваринг и логистику. Полная раскладка всех начисления по SKU несколько сложнее =)

Для сравнения приведу таблицы рентабельности, в которых применялись вычисления с делением по количеству товаров и по цене:

Деление начислений поровну на количество товаров в одной транзакции

Деление начислений поровну на количество товаров в одной транзакции

Деление начислений пропорционально ценам товаров в одной транзакции

Деление начислений пропорционально ценам товаров в одной транзакции

ЗЫ. пока дописывал статью Ozon объявил, что отключит метод /transaction/list 6 июля 2026 года. Вроде даже дает новый метод с раскидкой по SKU...