rustata экономическая статистика РФ - поиск по 5,6 млн рядов

методы API

Срез по территориям, поиск рядов и показателей, значения одного ряда, происхождение цифр и справочники - чтобы знать, что подставлять в параметры. У каждого метода - готовый пример на Python, R и Julia.

Во всех примерах токен передаётся заголовком Authorization: Bearer; подходит и X-API-Key. Тарифы и квоты - на отдельной странице.

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

GET /api/v1/data - срез по территориям

Отдаёт один показатель сразу по многим территориям: «цены на пиво по всем регионам» - это один запрос, а не восемьдесят пять. Квота считается в значениях, поэтому срез стоит ровно столько, сколько в нём точек.

Параметры

ПараметрПо умолчаниюЧто задаёт
datasetобязателеннабор: fedstat/indicator_31448 или просто код набора
georegionуровень территории или перечень кодов через запятую
classвсекод продукта, несколько через запятую, либо all
measureвсевид показателя (у ИПЦ - вид индекса)
transformationвсепреобразование: level, pct_yoy, level_ytd
freqвсечастота: M, Q, A, W
unitвсеединица измерения
start, endвся историяграницы периода; месяц допустим: 2024-01
formatjsonjson или csv
labels01 - добавить название продукта рядом с его кодом
bom0только для CSV: 1 добавляет BOM для Excel на Windows
limit100сколько рядов вернуть за раз, максимум 300
offset0с какого ряда начать страницу

Не переданное измерение означает «без фильтра»: вернутся все его значения, какие есть в наборе. Измерения, которых у набора нет, в ответе не появляются.

Уровни территории

geoЧто вернётсяТерриторий
countryРоссия целиком1
districtфедеральные округа13 с историческими
macroregionэкономические районы (прежнее деление)11
regionсубъекты федерациидо 85
cityгорода и районы внутри субъектовсотни
allвсе уровни сразу-
46000000000,RU-50только перечисленные территориипо списку

Коды принимаются в обоих видах: исходный ОКАТО и нормализованный (RU-46). Полный перечень - в справочнике территорий.

Продукты

В наборе обычно не один продукт, а сотни: в ценах на продовольствие их 875. Параметр class работает так же, как geo: один код, список через запятую или всё сразу.

classЧто вернётся
3203один продукт
3203,7802только перечисленные, порядок не важен
allвсе продукты набора
не переданто же, что all

Отсюда берётся «все цены по Москве одним запросом»: geo=45000000000&class=all. Ряды отдаются страницами по limit, всего их series_total.

Коды и их названия - в справочнике на сайте и методом /api/v1/reference/codes в программе.

Россия приходит двумя рядами

«Российская Федерация» и «Российская Федерация без учёта новых субъектов (с 01.01.2023)» - оба на уровне country.

Это не дубль, а две методики счёта. Различить их можно по полю geo_label, выбор между ними за вами.

Ответ в JSON

Ответ состоит из двух частей: series - описания рядов, observations - плоский список значений. Части соединяются по series_key. Такая форма разворачивается в таблицу «период на территории» одной строкой кода и не требует разбирать многоэтажную шапку исходных выгрузок.

{
  "series": [
    { "series_key": "s1_z74wcnnmmefy4tbn",
      "series_ref": "fedstat/indicator_31448/AVG_PRICE.45000000000.M.RUB.3203",
      "title": "Пиво, л - Средняя цена · Город Москва",
      "title_raw": "AVG_PRICE | Пиво, л | Город Москва",
      "geo": "45000000000", "geo_canonical": "RU-45",
      "geo_level": "region", "geo_label": "Москва",
      "indicator": "AVG_PRICE", "classification": "3203",
      "measure": null, "transformation": null,
      "unit": "RUB", "freq": "M",
      "last_period": "2025-06-01", "status": "active",
      "provider_code": "fedstat", "dataset_code": "indicator_31448" }
  ],
  "observations": [
    { "series_key": "s1_z74wcnnmmefy4tbn", "period": "2025-05-01",
      "value": 206.42, "as_of": "2025-05-01" },
    { "series_key": "s1_z74wcnnmmefy4tbn", "period": "2025-06-01",
      "value": 211.13, "as_of": "2025-06-01" }
  ],
  "dataset": { "status": "fresh", "latest_period": "2026-07", "freq": "M",
               "sla_months": 3, "lag_months": 2, "overdue_by": 0 },
  "series_count": 1, "observation_count": 2, "series_total": 1,
  "offset": 0, "has_more": false, "next_offset": null
}
Поле рядаЗначение
series_keyидентификатор ряда; с ним работают observations и evidence
series_refчитаемый адрес того же ряда: провайдер, набор, значения измерений
titleназвание ряда в том же виде, что на сайте: товар, показатель, территория
title_rawскладское название: код показателя и части через |, как в исходной выгрузке
indicatorкод показателя внутри набора
classificationкод продукта или раздела классификатора
measure, transformationкоды вида показателя и преобразования; null, если у набора такого измерения нет
unit, freqкоды единицы измерения и частоты
geo, geo_canonicalкод территории в ОКАТО и нормализованный код
geo_level, geo_labelуровень территории и её название
last_periodпоследний период, за который у ряда есть данные - по всей истории, а не по запрошенному окну
continues_as, continues_fromключ ряда, под которым та же история идёт дальше или шла раньше после смены кода (643 → 643004.АГ); null, если разрыва нет
statusactive - ряд выходит вместе с набором; stalled - перестал публиковаться
provider_code, dataset_codeисточник и набор, из которых пришёл ряд
Поле значенияЗначение
series_keyк какому ряду относится строка
periodпервый день периода: месяц - первое число, квартал - 1 января, 1 апреля, 1 июля или 1 октября, год - 1 января
valueзначение, число
as_ofдата публикации именно этой оценки
Поле ответаЗначение
datasetвыходит ли НАБОР: fresh, overdue (должен был выйти и не вышел), archival (закрытая эра), unknown; рядом - ожидаемый срок и просрочка в месяцах
series_count, observation_countсколько рядов и значений в этой странице
series_totalсколько рядов подходит под фильтры целиком
offset, has_more, next_offsetс какого ряда начата страница, есть ли ещё, с чего начинать следующую

У недельных наборов period - дата регистрации цен из бюллетеня Росстата. Источник публикует номер недели («33 неделя 2026»), а дату к нему мы берём из справочника привязок; недели, до которой справочник ещё не дошёл, датируется шагом в семь дней от последней известной - такая точка на сайте подписана одним номером, без даты.

Публикуется ли ряд ещё

Ряд может исчезнуть по-разному, и это разные новости. Товар сняли с наблюдения, а набор выходит как обычно - это status: stalled у ряда. Встал весь набор - это status: overdue в блоке dataset. По одному признаку их не различить, поэтому признака два.

Меряются они тоже по-разному. Набор считается от сегодняшнего дня: для недельных данных выпуск ждём в пределах месяца, для месячных - трёх, для годовых - до двадцати. Ряд считается не от «сегодня», а от того, чем набор закончился: отстал от своего набора больше чем на три месяца (недельные и дневные), четыре (месячные), девять (квартальные) или восемнадцать (годовые) - значит, перестал выходить. По календарю мерить ряд нельзя: набор сам выходит с задержкой, и снятыми выглядели бы все ряды сразу после публикации.

Это тот же счёт, по которому на странице ряда стоит пометка «не обновляется»: сайт и API отвечают на этот вопрос одинаково.

last_period считается по всей истории набора, а не по запрошенному окну. Иначе ряд, оборвавшийся в 2019 году, в срезе за последний год выглядел бы как живой: у обоих в окне пусто.

Ровная линия - не то же самое, что снятый ряд

Тарифы на метро, электроэнергию и воду не двигаются месяцами, а ряд при этом живой и выходит каждую неделю: у него status: active.

И наоборот, stalled - не приговор: наблюдение по товару иногда возобновляется спустя годы. Поэтому рядом всегда стоит last_period - судите по нему.

На каждый период приходит одно значение - последняя опубликованная оценка. Статистику уточняют задним числом, и по as_of видно, какая версия цифры у вас на руках. Все версии - в методе evidence.

Разбор ответа JSON: соединить описания рядов со значениями и развернуть в таблицу «период на территории»

import pandas as pd
import requests

TOKEN = "rst_ваш_токен"
resp = requests.get(
    "https://rustata.ru/api/v1/data",
    params={"dataset": "fedstat/indicator_31448", "class": "3203",
            "geo": "region", "start": "2024-01", "labels": 1},
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=60,
)
resp.raise_for_status()
payload = resp.json()

series = pd.DataFrame(payload["series"]).set_index("series_key")
obs = pd.DataFrame(payload["observations"])

# описание ряда подставляется к каждому значению по series_key
obs = obs.join(series[["classification", "class_label", "geo", "geo_label", "unit"]],
               on="series_key")
obs["period"] = pd.to_datetime(obs["period"])

table = obs.pivot(index="period", columns="geo_label", values="value")
print(table.tail())

# у тарифов без месячной квоты этого заголовка нет, поэтому .get, а не []
print("остаток квоты:", resp.headers.get("X-Quota-Values-Remaining", "без ограничения"))

Ответ в CSV

При format=csv приходят те же данные одной длинной (tidy) таблицей: строка на значение, разделитель ;, кодировка UTF-8 без BOM. В строке стоит полная идентичность ряда - series_key, коды всех измерений набора, территория, - поэтому каждая колонка сразу годится для группировки и соединения, и доставать что-либо из series_key не нужно.

series_key;class;freq;unit;geo;geo_label;geo_level;period;value;as_of;last_period;status
s1_kk3htnlek6p3y3v7;3203;M;RUB;46000000000;Московская область;region;2025-05-01;184.37;2025-05-01;2025-06-01;active
s1_kk3htnlek6p3y3v7;3203;M;RUB;46000000000;Московская область;region;2025-06-01;190.21;2025-06-01;2025-06-01;active
s1_z74wcnnmmefy4tbn;3203;M;RUB;45000000000;Москва;region;2025-05-01;206.42;2025-05-01;2025-06-01;active

Колонки измерений зависят от набора: выводятся только те, что у него есть (class, measure, transformation, freq, unit). Дальше всегда идут geo, geo_label, geo_level, period, value, as_of, а в конце - last_period и status. Две последние приписаны именно в конец: кто читает по именам колонок, ничего не заметит, а кто по порядку - найдёт прежние колонки на прежних местах.

UTF-8 без BOM - это то, что ждут pandas, readr и CSV.jl. Excel на Windows, наоборот, без BOM показывает кириллицу кракозябрами: для него добавьте &bom=1.

Названия рядом с кодами: labels=1

По умолчанию в ответе только коды. Код устойчив и годится для соединения таблиц, а формулировку источник меняет от выпуска к выпуску, и join по названию разваливается.

Флаг labels=1 добавляет название продукта рядом с его кодом. В CSV это колонка class_label сразу после class:

GET /api/v1/data?dataset=fedstat/indicator_31448&class=3203&geo=45000000000&format=csv

series_key;class;freq;unit;geo;geo_label;geo_level;period;value;as_of;last_period;status
s1_z74wcnnmmefy4tbn;3203;M;RUB;45000000000;Москва;region;2025-05-01;206.42;2025-05-01

GET /api/v1/data?dataset=fedstat/indicator_31448&class=3203&geo=45000000000&format=csv&labels=1

series_key;class;class_label;freq;unit;geo;geo_label;geo_level;period;value;as_of;last_period;status
s1_z74wcnnmmefy4tbn;3203;Пиво, л;M;RUB;45000000000;Москва;region;2025-05-01;206.42;2025-05-01

В JSON тот же флаг добавляет ряду поле class_label:

{ "series_key": "s1_z74wcnnmmefy4tbn", "classification": "3203",
  "class_label": "Пиво, л", "geo": "45000000000", "geo_label": "Москва", … }

Если у набора нет классификации или название кода неизвестно, class_label приходит пустым: строка не пропадает и форму не меняет. Название территории (geo_label) приходит всегда - территория общая для всех наборов, и её название устойчиво.

Тот же срез через CSV: все цены по Москве, с названиями продуктов

import io

import pandas as pd
import requests

TOKEN = "rst_ваш_токен"
resp = requests.get(
    "https://rustata.ru/api/v1/data",
    params={"dataset": "fedstat/indicator_31448", "class": "all",
            "geo": "45000000000", "start": "2024-01", "format": "csv", "labels": 1},
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=120,
)
resp.raise_for_status()

df = pd.read_csv(io.StringIO(resp.text), sep=";", parse_dates=["period", "as_of"])
table = df.pivot(index="period", columns="class_label", values="value")
print(table.tail())

Ищет ряды по строке и возвращает их series_key - с ним потом берутся значения и происхождение. Нужен, когда код продукта заранее неизвестен.

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

ПараметрЗначение
qстрока поиска; можно не указывать, если задан отбор
limitсколько рядов вернуть, 1..200, по умолчанию 20
providerисточник: fedstat, rosstat_files, cbr, roskazna, showdata
datasetнабор: fedstat/indicator_31448 или просто код набора
geoтерритория: RU-45 или код ОКАТО
freqчастота: M, Q, A, W
class, transformationкод продукта и преобразование

Несколько значений пишутся через запятую и означают «любое из»; разные измерения складываются по И. Имена те же, что у /api/v1/data, - отбор, однажды записанный, переносится из метода в метод без переписывания.

Строка поиска при этом не обязательна: «все ряды набора с месячной частотой» это dataset=fedstat/indicator_31448&freq=M без q.

Найти ряд по названию и получить его ключ

import requests

TOKEN = "rst_ваш_токен"
resp = requests.get(
    "https://rustata.ru/api/v1/series/search",
    params={"q": "бензин аи-92", "limit": 5},
    headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30,
)
resp.raise_for_status()

for item in resp.json()["items"]:
    print(item["series_key"], "|", item["title"])

GET /api/v1/series/{series_key}/observations

Значения одного ряда. Нужен, когда ключ уже известен: получен поиском или взят из адреса страницы ряда. Период задаётся start и end - годом, месяцем или датой; без них приходят свежие limit значений (500 по умолчанию, до 5000). Значения лежат в поле items, а не observations: последнее - у среза.

Для разового скачивания метод не нужен - на странице ряда есть кнопка «скачать CSV», она работает без токена.

История одного ряда с начала 2024 года

import pandas as pd
import requests

TOKEN = "rst_ваш_токен"
KEY = "s1_mydzoqiytm4xqjq3"
resp = requests.get(
    f"https://rustata.ru/api/v1/series/{KEY}/observations",
    params={"start": "2024-01"},
    headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30,
)
resp.raise_for_status()

df = pd.DataFrame(resp.json()["items"])
df["period"] = pd.to_datetime(df["period"])
print(df.set_index("period")["value"].tail())

GET /api/v1/series/{series_key}/evidence

Происхождение значений: из какого файла источника они пришли, каким релизом опубликованы, каким прогоном загружены. Этим ответ на вопрос «откуда цифра» отличается от «скачал где-то в интернете».

Откуда взялись значения ряда

import requests

TOKEN = "rst_ваш_токен"
KEY = "s1_mydzoqiytm4xqjq3"
resp = requests.get(
    f"https://rustata.ru/api/v1/series/{KEY}/evidence",
    headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30,
)
resp.raise_for_status()

for release in resp.json()["releases"][:3]:
    print(release["published_at"], "|", release["source_file"])

Показатели: /api/v1/concepts/search и /api/v1/concepts/{id}

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

Найти показатель и посмотреть его сводку

import requests

TOKEN = "rst_ваш_токен"
headers = {"Authorization": f"Bearer {TOKEN}"}

found = requests.get("https://rustata.ru/api/v1/concepts/search",
                     params={"q": "молоко", "limit": 3},
                     headers=headers, timeout=30).json()
for concept in found["items"]:
    print(concept["concept_id"], "|", concept["label"], "|", concept["series_count"], "рядов")

first_id = found["items"][0]["concept_id"]
card = requests.get(f"https://rustata.ru/api/v1/concepts/{first_id}",
                    headers=headers, timeout=30).json()
print(card["label"], "- территорий:", card.get("geo_count"))

GET /api/v1/reference/* - справочники

Три списка, из которых берутся значения параметров: наборы для dataset, территории для geo, коды продуктов для class. На сайте они есть страницами, но программе нужен ответ, а не страница - иначе коды переписывают с экрана руками.

МетодЧто отдаёт
/api/v1/reference/datasetsнаборы: код, название, частота, сколько рядов и территорий, за какой период есть данные, выходит ли набор (status)
/api/v1/reference/geoтерритории: код, название, уровень. Параметр level сужает до одного уровня
/api/v1/reference/codesкоды продуктов одного набора, с последним периодом и пометкой stalled у снятых с наблюдения. Параметр dataset обязателен
/api/v1/reference/methodologyоговорки набора: охват (полный круг или малые предприятия), исключения, классификатор, пересматриваются ли значения
/api/v1/reference/relatedсвязи наборов: другие эры показателя и соседние наборы - подробнее или с длиннее историей. Поле splice отвечает, можно ли сшивать: false - нельзя, у эр разошлась методология
/api/v1/reference/unitsединицы измерения: код, название, коды ОКЕИ с множителем mult, сколько рядов и наборов. Одна величина приходит несколькими кодами: 168 - тонна, 169 - тысяча тонн
/api/v1/reference/continuityобъявленные разрывы: где ряд продолжается под другим кодом (643 → 643004.АГ)

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

Оговорки и связи не обязательно спрашивать отдельно: ответ /api/v1/series/{key}/observations несёт их рядом с числами - series (единица и её название, частота, территория, множитель, пересматривается ли), notes (пометки именно этого ряда) и related (эры и соседние наборы с полем splice). Раньше ответ был голым, и единицу приходилось искать вторым запросом - а без неё значение не значит ничего.

Все принимают format=csv и bom=1 - справочник чаще всего нужен таблицей, чтобы положить рядом со своими данными и сматчить. Квоту в значениях они не расходуют: это метаданные, а не наблюдения.

Общего справочника кодов нет намеренно

У каждого набора свой классификатор: код 3203 в ценах и в производстве означает разные вещи.

Объединённый список подталкивал бы подставить код не в тот набор - и запрос вернул бы данные, только не те, что ожидались.

Найти набор, взять его коды и выгрузить нужные продукты

import pandas as pd
import requests

TOKEN = "rst_ваш_токен"
headers = {"Authorization": f"Bearer {TOKEN}"}
BASE = "https://rustata.ru/api/v1"

codes = pd.DataFrame(requests.get(f"{BASE}/reference/codes",
                                  params={"dataset": "fedstat/indicator_31448"},
                                  headers=headers, timeout=60).json()["items"])
beer = codes[codes["name"].str.contains("Пиво", case=False)]
print(beer[["class", "name"]])

# коды списком - один запрос вместо запроса на каждый продукт
data = requests.get(f"{BASE}/data",
                    params={"dataset": "fedstat/indicator_31448",
                            "class": ",".join(beer["class"]),
                            "geo": "region", "start": "2024-01"},
                    headers=headers, timeout=120).json()
print(data["series_total"], "рядов")

GET /api/v1/updates - что нового

Лента загрузок: день, набор, до какого периода есть данные, сколько выпусков и сколько точек пересмотрено. Отвечает на «что нового» одним запросом - иначе приходится опрашивать справочник наборов целиком и сравнивать с тем, что клиент запомнил сам.

Пересмотр СТАРЫХ периодов по latest_period не виден вовсе: он там не меняется. Поэтому в ленте есть отдельная колонка с числом пересмотренных точек.

Пересматривает ли источник значения этого набора вообще - поле revised в справочнике наборов и в блоке dataset ответа /api/v1/data. У индексов потребительских цен и у зарегистрированных цен стоит false: цена периода публикуется один раз и не уточняется, искать в ней правки бессмысленно.

Правила, общие для всех методов

Постраничная выдача

За один раз приходит 100 рядов, параметром limit можно взять до 300. Сколько подходит всего, видно в series_total, а next_offset подсказывает, с чего начать следующую страницу.

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

Если срез выбирает больше 60 000 значений, ответ не придёт - вернётся SLICE_TOO_BIG с подсказкой сузить запрос.

Кэширование

Ответы отдаются с ETag. Передайте его обратно в If-None-Match - если данные не менялись, придёт 304 без тела, и квота не израсходуется. На регулярных обновлениях это экономит и время, и лимит.

Ошибки

Ошибка приходит одинаково устроенным объектом: код, понятное сообщение и подсказка, что делать.

{ "error": { "code": "SLICE_TOO_BIG",
             "message": "Запрос выбирает 1 200 000 значений, максимум 200 000",
             "hint": "Сузьте период или список территорий" } }
КодHTTPКогда
UNAUTHORIZED401токен не передан или отозван
RATE_LIMITED429исчерпана квота тарифа
SLICE_TOO_BIG400запрос выбирает слишком много данных
NOT_FOUND404под фильтры ничего не подошло
INTERNAL500ошибка на нашей стороне

Интерактивный справочник

Запрос можно выполнить своим токеном прямо в браузере - Swagger. Машиночитаемое описание - openapi.json.