---
name: rustata-api
description: Программный доступ к данным rustata.ru - запросы к /api/v1 по токену, срез по территориям, поиск рядов, справочники кодов, лимиты и разбор ошибок. Использовать, когда надо забрать ряды с витрины снаружи, проверить ответ прода или написать клиента.
---

# API rustata.ru

Точные имена параметров, поля ответа, коды ошибок и лимиты - в `reference.md` рядом с этим файлом.
Здесь только то, что надо знать ДО запроса.

Два входа в задачу:

```
код продукта известен:  /reference/datasets → /reference/codes?dataset=… → /api/v1/data
кода нет, ищем словом:  /series/search?q=…&provider=…&freq=… → series_key → /observations
```

**Коды из головы не подставляем.** У каждого набора свой классификатор: `3203` в ценах и в
производстве это разные вещи. Запрос с чужим кодом вернёт данные, только не те.

## Разбор идёт от общего к частному

**Сначала головной ряд набора, потом разделы, потом виды.** Не наоборот и не вместо.

**Агрегат в данных ЕСТЬ - его надо найти, а не вывести.** Суммировать разделы, чтобы получить
«промышленность в целом», нельзя: у Росстата это отдельный опубликованный ряд со своей
методикой, и наша сумма с ним не сойдётся. То же правило, что с РФ: не Σ по федеральным округам,
а ряд России из источника.

Головной ряд легко пропустить, потому что называется он не «итого»:

| Набор | Код головного | Как называется в источнике |
|---|---|---|
| `fedstat/indicator_57806` (ИПП) | `1323500.029.31` | «Собирательная классификационная группировка видов экономической деятельности "Промышленность" на основе ОКВЭД2» |

Где искать:

1. `head_series` в манифесте набора (`manifests/<провайдер>/<набор>.yaml`) - если объявлен, это
   прямой ответ;
2. иначе в `/reference/codes` ищем по названию: «собирательная группировка», «в целом», «всего»,
   «итого» - и сверяем охват: у головного кода рядов обычно больше, чем у любого раздела;
3. на витрине головной ряд стоит первой строкой на странице набора.

Если головного ряда найти не удалось - так и пишем в ответе, а не подменяем его суммой разделов
и не объявляем, что «сводного индекса нет».

## Что из чего можно пересчитать

Правила не выдуманы: это `available_transforms` из `api/services/transform_service.py`, по ним
витрина решает, какие виды предложить на странице ряда. Расходиться с ней нельзя.

| Что в ряду лежит | Что из него получается | Чего не получится |
|---|---|---|
| уровень (`level`, цены в рублях, объёмы) | базисный индекс, % к предыдущему, % год к году, изменение в единицах | - |
| % к предыдущему периоду (`pct_mom`, `MOM`) | **только** базисный индекс - цепным перемножением | ни уровня, ни % год к году |
| накопленный итог в единицах (`level_ytd`, `YTD`, `TURNOVER`) | величину за период (разностью), 12-месячную сумму, индекс, % год к году | - |
| % год к году (`pct_yoy`, `YOY`), индекс, `DEC`, `YTD_YOY` | **ничего** | ни уровня, ни цепной динамики |

**Из «год к году» не выводится ничего** - и это главное. Значение 102.4 говорит про отношение к
другому году, а не про уровень; чтобы восстановить из него ряд, нужна база, которой в ряду нет.
Если нужен уровень или помесячная динамика - берём ДРУГОЙ ряд того же показателя, а не считаем.

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

**Накопленный итог бывает двух разных смыслов, не перепутайте.** `level_ytd` - это накопленный
УРОВЕНЬ (тонны с начала года), из него разностью получается месяц. `YTD_YOY` - это уже процент
накопленным итогом к прошлому году, из него не получается ничего.

## Вид ряда лежит в разных измерениях у разных источников

Одно и то же по смыслу у fedstat и у файлов Росстата записано по-разному, и фильтр не сработает,
если искать не там:

| Набор | Где вид | Значения | Единица |
|---|---|---|---|
| `fedstat/indicator_57806` (ИПП), `indicator_31074` (ИПЦ) | `measure` | `MOM`, `YOY`, `YTD_YOY`, `DEC` | `PCT` |
| `rosstat_files/prom_otrasl` | `transformation` | `level`, `level_ytd` | коды ОКЕИ (`168`, `796`, …) |
| `fedstat/indicator_31448` (цены) | ни там, ни там | - | `RUB` |

Проверяется одним запросом: взять страницу среза и посмотреть, какие `measure` и `transformation`
в ней встречаются. Пока не посмотрели - не фильтруем наугад.

## Оговорку про пересмотры берём из данных, а не из памяти

`revised` приходит в блоке `dataset` и в `/reference/datasets`. `false` - у ИПЦ и
зарегистрированных цен: там цифра окончательная, и писать «может уточниться» неверно. `true` -
везде остальном, и тогда оговорка обязательна: у производства и бюджетов последние периоды
правятся регулярно.

Структурные оговорки набора - `/api/v1/reference/methodology?dataset=…`: полный круг или малые
предприятия, без нефти, ОКПД или ОКПД2. Прежде чем сравнивать два набора «про производство»,
смотрим туда: расхождение в полтора раза обычно означает разный круг предприятий, а не ошибку.

«Что нового приехало» - `/api/v1/updates`: день, набор, добавленные периоды и сколько точек
пересмотрено. По `latest_period` пересмотр старых периодов не виден, там он не меняется.
`source: warehouse` означает, что журнал ночного прогона для этой строки пуст: «пересмотрено
0» тут читать нельзя, там просто нет данных.

## История ряда может продолжаться под другим кодом

`643` (Российская Федерация) сменилась на `643004.АГ` («без учёта новых субъектов») - и ряд под
прежним кодом обрывается, хотя данные не кончились. У ряда для этого есть `continues_as` и
`continues_from`: ключ той же истории под другим кодом. Увидели обрыв - сначала смотрим туда, а
не пишем «данных больше нет».

Чтобы получить длинную историю, берём обе половины и склеиваем: сначала предшественника, затем
наследника, на пересечении оставляем наследника. Полный список объявлений с причинами -
`/api/v1/reference/continuity`.

**Склеивать можно не всё.** Разрывы с `splice: false` (смена методологии) показывать надо, а
сшивать нельзя: эры ВВП по ОКВЭД-2007 и ОКВЭД2 расходятся на 7 процентов, это разные ряды.

## Тренды считаем только по действующим рядам

Вопрос «что происходило в июле», «последние тренды», «что изменилось» - это вопрос про
действующие ряды. Снятые с наблюдения (`status: stalled`) в такой разбор не берём вовсе: они
тянут вниз долю растущих, попадают в «без изменений» и съедают время на разбор мёртвых данных.

У недельных цен таких 47 из 161, у ИПП по ОКВЭД2 - каждый третий код. Клиент отбрасывает их сам
(`movers`, `regions`, `slice_data(active=True)`); в своём скрипте - фильтр по `status` сразу после
выгрузки, до любых расчётов.

## Токен

Выпускается в кабинете https://rustata.ru/account, показывается один раз. Живёт в переменной
окружения `RUSTATA_TOKEN` (или строкой в `.env`, он в `.gitignore`).

**В вывод терминала токен не печатаем**: `env`, `echo $RUSTATA_TOKEN` и печать окружения роняют
его в журнал сессии целиком. Случилось - выпускаем новый, старый отзываем.

Заголовок любой из двух, это одно и то же: `Authorization: Bearer rst_…` или `X-API-Key: rst_…`.
`Basic` и `?token=` в ссылке приняты только для канала OData (Excel не умеет заголовки). Токена
требуют все `/api/v1/*`; без токена открыт только `/health`.

## Клиент

`rustata.py` рядом с этим файлом: токен, страницы, конверт ошибок, ожидание по 429.

```bash
python .claude/skills/rustata-api/rustata.py movers fedstat/indicator_37426 geo=RU-45 periods=15
python .claude/skills/rustata-api/rustata.py regions fedstat/indicator_37426 7802 periods=15
python .claude/skills/rustata-api/rustata.py heads fedstat/indicator_57806
python .claude/skills/rustata-api/rustata.py movers fedstat/indicator_57806 geo=643 measure=YOY
python .claude/skills/rustata-api/rustata.py codes fedstat/indicator_31448
python .claude/skills/rustata-api/rustata.py search бензин provider=fedstat freq=W
```

```python
import sys; sys.path.insert(0, ".claude/skills/rustata-api")
import rustata
df = rustata.frame("fedstat/indicator_31448", geo="region", start="2024-01", **{"class": "3203"})
rustata.find_geo("Чукот")                          # [('RU-77', 'Чукотский автономный округ')]
rustata.latest_period("fedstat/indicator_37426")   # свежий период без выгрузки данных
rustata.limits()                                   # остаток квоты, request_id
```

`frame()` отдаёт polars и сам проходит страницы (pandas не используем, ADR-0013). Локальный стенд
вместо прода: `RUSTATA_API=http://localhost:8090`. Интерпретатор: `H:/conda/envs/py312/python.exe`.

**Широкий фильтр листается страницами, каждая - отдельный запрос к проду.** `slice_data` и `frame`
останавливаются после 20 страниц с `TooManyPages`: сузить `geo`, `class` или период. «Все продукты
по всем территориям за всю историю» это сотни обращений и минуты ожидания.

## Как отвечать быстро

API быстрый, экономить надо не на нём, а на числе шагов.

| Что | Время |
|---|---|
| `/reference/datasets`, `/codes` | 0.16-0.19 с |
| `/series/search` с отбором | 0.1-0.5 с |
| срез за 15 недель, 111 товаров | 0.5 с |
| `import polars` | 0.35 с |

Весь разбор по данным - полторы секунды. Если ответ собирается две минуты, время ушло на девять
запусков скрипта, а не на запросы. Отсюда:

- **Типовой вопрос закрывает готовая команда.** «Что интересного за период» - `movers`, «а что по
  регионам» - `regions`. Обе сами берут свежий период и ограничивают окно; разбор бензина по
  84 регионам - полторы секунды против шестнадцати запусков руками.
- **Нестандартный разрез - ОДИН скрипт**, который тянет срез и считает всё сразу. Данные после
  `slice_data` уже в памяти, второй запрос за тем же не нужен.
- **Поиск сужаем отбором, а не перебором выдачи**: `search?q=бензин&provider=fedstat&freq=W`.
  Имена параметров те же, что у `/api/v1/data`; строка поиска не обязательна - «все ряды набора»
  это `dataset=…&freq=M` без `q`.

## Территории: canonical это ОКАТО, а не ISO

`RU-46` читается как ISO или как автомобильный код, а означает другое: canonical это «RU» плюс
**первые две цифры ОКАТО**. Совпадения случайны и обманчивы:

| Код | Это | А выглядит как |
|---|---|---|
| `RU-77` | Чукотский автономный округ | Москва (автомобильный код 77) |
| `RU-45` | Москва | Курганская область или ISO RU-KGN |
| `RU-87` | Республика Коми | Чукотка (ISO RU-CHU) |
| `RU-40` | Санкт-Петербург | Калужская область |

Ошибка тихая: запрос отработает и вернёт данные, только по другому региону. Код берём из
справочника (`find_geo`) и **сверяем `geo_label` в ответе** - он приходит всегда.

Уровень территории берётся из canonical, а не из вида кода: у Краснодарского края с ФТ «Сириус»
код не оканчивается нулями, а Россия приходит и как `643`, и как `643004.АГ` («без учёта новых
субъектов») - оба `country`.

## Ревизии: у цен их нет

Цены и ИПЦ **не пересматриваются**: опубликованное значение таким и остаётся. Следствия:

- `as_of` в этих наборах не несёт информации, гоняться за винтажами незачем;
- **два разных значения на одном периоде в ценах - испорченный ингест, а не пересмотр.** Не
  усредняем и не берём «поновее», а идём разбираться с загрузкой;
- у производства, счетов и бюджета пересмотры есть, там канонично последнее по времени.

## Грабли на данных

- **«Снят с наблюдения» и «стоит на месте» - разные вещи.** Первое говорит API: `status`
  (`active`/`stalled`) и `last_period` по всей истории набора. Второе видно только в окне и часто
  настоящее: бензин на Чукотке стоит 76.0 руб/л девятнадцать недель подряд, метро и
  электроэнергия в Москве не двигаются месяцами - это тарифы, а не сбой. Ряд со `status: active`
  замершим называть нельзя.
- **Набор мог встать целиком** - отдельный признак в поле `dataset`: `fresh`, `overdue`,
  `archival`, `unknown`. Снятый ряд в живом наборе и вставший набор лечатся по-разному.
- **Рядов в наборе больше, чем товаров с данными в периоде.** У недельных цен на уровне РФ
  161 ряд, а наблюдений каждую неделю 114: 47 кодов сняты раньше. Долю подорожавших считаем от
  того, что есть В ПЕРИОДЕ.
- **Индекс - это уже процент, и менять его надо в ПУНКТАХ.** У ИПП значение 100.4 означает
  «+0.4% к тому же месяцу год назад»; отношение 100.4 к 41.4 даёт «+142%», и это не факт о
  экономике, а деление процентов друг на друга. Клиент различает наборы по единице (`PCT`) сам,
  в своём скрипте - смотрите `unit` до расчёта.
- **Показатели одного набора между собой не сравниваем.** В ИПП рядом лежат `YOY`, `MOM` и
  `YTD_YOY`: общий рейтинг по ним смешивает несравнимое. Сужаем `measure=` (клиент предупреждает,
  если в выборке их несколько).
- **Недельный набор - уровни цен в рублях, а не индекс.** Недельного ИПЦ на витрине нет; проценты
  по ценам не взвешены по корзине и официальной недельной инфляцией не являются.
- **Названий продуктов по умолчанию нет**: код устойчив и годится для join, формулировку источник
  меняет от выпуска к выпуску. Нужна подпись - `labels=1`. `geo_label` приходит всегда.
- **Свежий период спрашиваем у справочника** (`latest_period`), а не считаем максимум по
  выгруженным данным.
- **Источник это измерение, а не отдельный справочник**: `provider` стоит рядом с частотой и
  территорией - `fedstat`, `rosstat_files`, `cbr`, `roskazna`, `showdata`.
