Как стать автором
Обновить

Новый ЕМИСС 2.0 со сводными таблицами, API и погодой?

Уровень сложностиСредний
Время на прочтение12 мин
Количество просмотров779

Привет, Хабр! Около года назад мне пришла странная идея: а что, если сделать новую версию ЕМИСС, хранилища российской статистики, чтобы наконец-то было удобно сводить данные. А то постоянно сопоставлять несколько показателей из множества Excel файлов – сущий ад. И вот уже год прошел с момента создания и написания первой версии и сайта, и статьи (недавно был небольшой пост).

Что нового теперь в созданном приложении StatKit?

Обновление поиска: теперь поиск понимает и семантику за счет векторизации и сортировки по косинусной близости:

Поиск показателей
Поиск показателей

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

Фильтр по годам
Фильтр по годам
Фильтр по справочникам
Фильтр по справочникам
Фильтр по единицам измерения
Фильтр по единицам измерения

Свободные таблицы теперь позволяют объединить несколько показателей (я для примера выбрал население и число преступлений в целом и среди несовершеннолетних)

по субъектам РФ:

Кросс-секционный вид данных (все субъекты за один конкретный год)
Кросс-секционный вид данных (все субъекты за один конкретный год)

по одному субъекту в динамике:

Временной вид данных (за все годы по одному субъекту)
Временной вид данных (за все годы по одному субъекту)

все субъекты в динамике:

Панельный вид данных (все субъекты за все доступные годы)
Панельный вид данных (все субъекты за все доступные годы)

И для меня менее интересный вид, как в ЕМИСС, для одного показателя:

Один показатель в разрезе субъектов и по всем годам
Один показатель в разрезе субъектов и по всем годам

И, наверное, самое интересное теперь появился API для доступа к данным. 

Как начать работу с API StatKit?

1. Установка зависимостей

Откройте Jupyter Notebook. Убедитесь, что у вас установлены библиотеки requests и pandas

# установка и импорт зависимостей
!pip install requests pandas

import pandas as pd
import requests

2. Подготовка авторизации

Замените username и token на свои учетные данные API:

# получить имя пользователя и токен можно по адресу: https://go.statkit.ru/profile
username = "your_username"  # замените на Ваш username API
token = "your_token"        # замените на Ваш токен доступа

3. Настройка заголовков запроса

Добавьте headers для авторизации:

headers = {
    "X-API-User": username,              # username API
    "Authorization": f"Bearer {token}",  # токен
    "Content-Type": "application/json"   # Формат данных (JSON)
}

Получим список всех статистических показателей из API, преобразуем их в таблицу pandas:

api_url = "https://api.alexstat.ru/api/v1/indicators"

try:
    # Выполняем GET-запрос к API
    response = requests.get(api_url, headers=headers)

    data = None
    
    # Проверяем статус ответа сервера
    if response.status_code == 200:
        data = response.json()
        print("Данные успешно получены!")
    else:
        print(f"Ошибка запроса. Код состояния: {response.status_code}")
        print(f"Текст ответа сервера: {response.text}")

except requests.exceptions.RequestException as e:
    print(f"Произошла ошибка при выполнении запроса: {e}")

# Преобразуем полученные данные в DataFrame
df = pd.DataFrame(data)
# Конвертируем строковые даты в формат datetime
df['add_date'] = pd.to_datetime(df['add_date'])
df['lastmod_date'] = pd.to_datetime(df['lastmod_date'])
df
Результат запроса всех показателей
Результат запроса всех показателей

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

# URL API для получения данных о показателе "доля"
api_url = "https://api.alexstat.ru/api/v1/indicators/доля"

try:
    # Отправляем GET-запрос к API с указанными заголовками
    response = requests.get(api_url, headers=headers)

    # Инициализируем переменную для хранения данных
    data = None
    
    # Проверяем код статуса ответа
    if response.status_code == 200:
        # Если запрос успешен (код 200), преобразуем ответ в JSON
        data = response.json()
        print("Успешный запрос!")
    else:
        # Если сервер вернул ошибку, выводим код статуса и текст ответа
        print(f"Ошибка запроса. Код статуса: {response.status_code}")
        print(f"Ответ сервера: {response.text}")

# Обрабатываем возможные исключения при выполнении запроса
except requests.exceptions.RequestException as e:
    print(f"Ошибка при выполнении запроса: {e}")

# Преобразуем полученные данные в DataFrame pandas
df = pd.DataFrame(data)
df
Результат запроса всех показателей и указания ключевого слова
Результат запроса всех показателей и указания ключевого слова

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

# URL API для получения данных показателя с ID = 100
api_url = "https://api.alexstat.ru/api/v1/get_indicator/100"

try:
    # Отправляем GET-запрос к API с указанными заголовками
    response = requests.get(api_url, headers=headers)

    # Инициализируем переменную для хранения данных
    data = None
    
    # Проверяем статус ответа сервера
    if response.status_code == 200:
        # При успешном ответе преобразуем в JSON
        data = response.json()
        print("Данные успешно получены!")
    else:
        # Выводим информацию об ошибке, если статус не 200
        print(f"Ошибка запроса. Код статуса: {response.status_code}")
        print(f"Текст ответа: {response.text}")

# Обрабатываем возможные исключения при выполнении запроса
except requests.exceptions.RequestException as e:
    print(f"Ошибка при выполнении запроса: {e}")

# Преобразуем данные в pandas DataFrame
df = pd.DataFrame(data)

# Преобразуем значения показателя в числовой формат (float)
df['ObsValue'] = df['ObsValue'].astype(float)

df
Результат запроса данных по конкретному показателю
Результат запроса данных по конкретному показателю

Полный код и готовый Jupyter Notebook в интерактивном Colab размещены по ссылке: https://go.statkit.ru/api

Также там в коде по ссылке я предлагаю вариант, как из нескольких эндпоинтов API провести мини-исследование и построить регрессию: исследовать зависимость между показателями населения и преступности по субъектам в разрезе ОКАТО:

Найдем интересующие нас показатели для исследования:

# URL API для получения показателей по ключевому слову "численность"
api_url = "https://api.alexstat.ru/api/v1/indicators/численность"

try:
    # Отправляем GET-запрос к API с указанными заголовками
    response = requests.get(api_url, headers=headers)

    # Инициализируем переменную для хранения данных ответа
    data = None

    # Проверяем статус ответа сервера
    if response.status_code == 200:
        # При успешном ответе преобразуем в JSON
        data = response.json()
        print("Данные успешно получены!")
    else:
        # Выводим информацию об ошибке, если статус не 200
        print(f"Ошибка запроса. Код статуса: {response.status_code}")
        print(f"Текст ответа сервера: {response.text}")

# Обрабатываем возможные исключения при выполнении запроса
except requests.exceptions.RequestException as e:
    print(f"Произошла ошибка при выполнении запроса: {e}")

# Преобразуем полученные данные в DataFrame
df = pd.DataFrame(data)
df
Показатели по ключевому слову "численность"
Показатели по ключевому слову "численность"

Численность постоянного населения имеет ID 1278.

# URL API для получения показателей по ключевому слову "преступ"
api_url = "https://api.alexstat.ru/api/v1/indicators/преступ"

try:
    # Отправляем GET-запрос с заголовками
    response = requests.get(api_url, headers=headers)

    # Переменная для хранения данных
    data = None

    # Проверяем статус ответа
    if response.status_code == 200:
        # Если успешно, преобразуем JSON
        data = response.json()
        print("Успешно!")
    else:
        # Выводим ошибку, если статус не 200
        print(f"Ошибка запроса. Код: {response.status_code}")
        print(f"Ответ: {response.text}")

# Обрабатываем ошибки запроса
except requests.exceptions.RequestException as e:
    print(f"Ошибка запроса: {e}")

# Создаем DataFrame из полученных данных
df = pd.DataFrame(data)
df
Показатели по ключевому слову "преступ"
Показатели по ключевому слову "преступ"

Далее, возьмем доля лиц, совершивших преступления, с ID = 232. Получим данные по показателю численность населения:

# URL для получения показателя с ID 1278 (численность постоянного населения)
api_url = "https://api.alexstat.ru/api/v1/get_indicator/1278"

try:
    # Отправка GET-запроса к API с указанными заголовками
    response = requests.get(api_url, headers=headers)

    # Инициализация переменной для хранения данных
    data = None

    # Проверка статуса ответа сервера
    if response.status_code == 200:
        # Успешный ответ - преобразуем в JSON
        data = response.json()
        print("Данные успешно получены!")
    else:
        # Вывод информации об ошибке, если статус не 200
        print(f"Ошибка запроса. Код статуса: {response.status_code}")
        print(f"Текст ответа: {response.text}")

# Обработка возможных ошибок при выполнении запроса
except requests.exceptions.RequestException as e:
    print(f"Ошибка выполнения запроса: {e}")

# Создание DataFrame из полученных данных
population = pd.DataFrame(data)

# Преобразование значений показателя в числовой формат float
population['ObsValue'] = population['ObsValue'].astype(float)

population
Данные показателя постоянного населения по субъектам РФ
Данные показателя постоянного населения по субъектам РФ

Обратимся к справочникам ОКАТО (1) и Тип поселения (17):

# Находим все колонки, содержащие справочники (начинающиеся с index_)
print("Идентификация колонок со справочниками...")
index_columns = [col for col in population.columns if col.startswith('index_')]
print(f"Найдены колонки со справочниками: {index_columns}")

# Извлекаем ID справочников из названий колонок
index_ids = [col.split('_')[1] for col in index_columns]
print(f"Извлеченные ID справочников: {index_ids}")

# Функция для получения соответствия между кодами и их текстовыми описаниями
def get_index_mapping(index_id):
    """Получает маппинг кодов в текстовые описания для указанного справочника"""
    api_url = f"https://api.alexstat.ru/api/v1/get_index/{index_id}"
    try:
        print(f"Запрашиваем маппинг для справочника {index_id}...")
        response = requests.get(api_url, headers=headers)

        if response.status_code == 200:
            data = response.json()
            # Создаем словарь {код: описание}
            mapping = {str(item['code_value']): item['code_text'] for item in data}
            print(f"Найдено {len(mapping)} соответствий для справочника {index_id}")
            return mapping
        else:
            print(f"Ошибка получения справочника {index_id}: статус {response.status_code}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"Ошибка запроса для справочника {index_id}: {e}")
        return None
    except KeyError as e:
        print(f"Неожиданный формат данных для справочника {index_id}: {e}")
        return None

# Получаем маппинги для всех найденных справочников
index_mappings = {}
for index_id in index_ids:
    mapping = get_index_mapping(index_id)
    if mapping:
        index_mappings[index_id] = mapping

print(f"Созданы маппинги для {len(index_mappings)} из {len(index_ids)} справочников")

# Заменяем коды на текстовые описания в основном DataFrame
for col in index_columns:
    index_id = col.split('_')[1]
    if index_id in index_mappings:
        mapping = index_mappings[index_id]
        # Преобразуем к строковому типу, применяем маппинг, оставляем оригинальные значения при отсутствии маппинга
        population[col] = population[col].astype(str).map(mapping).fillna(population[col])
        print(f"Обработана колонка {col} (справочник {index_id})")
    else:
        print(f"Не найден маппинг для колонки {col} (справочник {index_id})")

# Выводим результат
population
Добавили текстовые значения вместо кодов из справочников
Добавили текстовые значения вместо кодов из справочников

Исследуем показатель, что он содержит:

# Проходим по всем колонкам в DataFrame population
for col in population.columns:
    # Пропускаем числовые колонки и идентификаторы
    if col in ['ObsValue', 'id']:
        continue

    # Выводим название колонки
    print(f'Уникальные значения в колонке {col}:')

    # Выводим все уникальные значения в колонке
    print(population[col].unique())

    # Пустая строка для разделения
    print()

Отфильтруем датафрейм со значениями показателя по населению. И возьмем статистику только с одного уровня – субъектного, исключив дублирования в виде РФ, федеральных округов, частей, входящих в состав субъектов:

OKATO_to_be_excluded = ['Российская Федерация',
                        'Российская Федерация в границах до 04.10.2022',
                        'Центральный федеральный округ',
                        'Северо-Западный федеральный округ',
                        'Ненецкий автономный округ (Архангельская область)',
                        'Архангельская область (без АО)',
                        'Архангельская область (кроме Ненецкого автономного округа)',
                        'Южный федеральный округ (с 2010 года)',
                        'Южный федеральный округ (с 29.07.2016)',
                        'Северо-Кавказский федеральный округ',
                        'Приволжский федеральный округ',
                        'Коми-Пермяцкий округ, входящий в состав Пермского края',
                        'Ханты-Мансийский автономный округ - Югра (Тюменская область)',
                        'Ямало-Ненецкий автономный округ (Тюменская область)',
                        'Тюменская область (без АО)',
                        'Тюменская область (кроме Ханты-Мансийского автономного округа-Югры и Ямало-Ненецкого автономного округа)',
                        'Сибирский федеральный округ',
                        'Таймырский (Долгано-Ненецкий) автономный округ (Красноярский край)',
                        'Эвенкийский автономный округ (Красноярский край)',
                        'Усть-Ордынский Бурятский округ' ,
                        'Агинский Бурятский округ (Забайкальский край)',
                        'Корякский округ, входящий в состав Камчатского края',
                        'Чеченская и Ингушская Республики',
                        'Северный район' 'Северо-Западный район' 'Центральный район',
                        'Волго-Вятский район',
                        'Центрально-Черноземный район',
                        'Поволжский район',
                        'Северо-Кавказский район',
                        'Уральский район',
                        'Западно-Сибирский район',
                        'Восточно-Сибирский район',
                        'Дальневосточный район']

# Фильтрация данных о населении по нескольким условиям:
population_filtered = population[
    # 1. Выбираем только данные за 2024 год
    (population['Time'] == 2024) &

    # 2. Выбираем только записи, где указано "все население" (из справочника index_17)
    (population['index_17'] == 'все население') &

    # 3. Исключаем записи, где код региона (index_1) находится в списке исключений OKATO_to_be_excluded
    # Оператор ~ означает отрицание (NOT IN)
    (~population['index_1'].isin(OKATO_to_be_excluded))
]

# Результат фильтрации сохраняется в новый DataFrame population_filtered
population_filtered
Результат фильтрации показателя по населению
Результат фильтрации показателя по населению

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

Делай раз:

api_url = "https://api.alexstat.ru/api/v1/get_indicator/232"

try:
    response = requests.get(api_url, headers=headers)

    data = None

    if response.status_code == 200:
        data = response.json()
        print("Success!")
    else:
        print(f"Request failed with status code: {response.status_code}")
        print(f"Response: {response.text}")

except requests.exceptions.RequestException as e:
    print(f"Request failed: {e}")

crime = pd.DataFrame(data)
crime['ObsValue'] = crime['ObsValue'].astype(float)
crime

Делай два:

for col in crime.columns:
    if col in ['ObsValue', 'id']:
        continue
    print(f'{col} unique values:')
    print(crime[col].unique())
    print()

Делай три:

for col in crime.columns:
    if col in ['ObsValue', 'id']:
        continue
    print(f'{col} unique values:')
    print(crime[col].unique())
    print()

Делай четыре:

crime_filtered = crime[
    (crime['Time'] == 2024) &
    (crime['PERIOD'] == 'январь-декабрь') &
    (crime['index_1'].isin(population_filtered['index_1'].tolist() ))]
crime_filtered

И готово:

Результат фильтрации показателя по преступности
Результат фильтрации показателя по преступности

Объединим два показателя по ОКАТО (субъектам РФ):

# Объединение данных о населении и преступности в один DataFrame
population_crime = pd.merge(
    # Левый DataFrame - данные о населении:
    population_filtered[['ObsValue', 'Time', 'index_1']].rename(columns={'ObsValue': 'population'}),

    # Правый DataFrame - данные о преступности:
    crime_filtered[['ObsValue', 'Time', 'index_1']].rename(columns={'ObsValue': 'crime'}),

    # Параметры объединения:
    on=['Time', 'index_1'],  # Ключи для объединения (год и код региона)
    how='outer'              # Тип объединения (внешнее)
)

# Результат объединения:
population_crime
Объединили показатели "Преступность" и "Население"
Объединили показатели "Преступность" и "Население"

Рассмотрим зависимость между численностью населения и преступлениями:

# раскомментируйте строчку ниже, чтобы установить библиотеку для рисования
#!pip install matplotlib
import matplotlib.pyplot as plt

plt.scatter(population_crime['population'], population_crime['crime'])
plt.xlabel('Population')
plt.ylabel('Crime')
plt.show()
График разброса значений показателей населения и преступности
График разброса значений показателей населения и преступности

Регрессия:

# раскомментируйте строчку ниже, чтобы установить библиотеку для построения стат. моделей
#!pip install statsmodels

import statsmodels.api as sm
import matplotlib.pyplot as plt
import numpy as np

X = population_crime['population']  # независимая переменная
y = population_crime['crime']       # зависимая переменная

# добавим константу в уравнение
X = sm.add_constant(X)

# регрессионная модель
model = sm.OLS(y, X).fit()

# статистики модели
print(model.summary())
print("\nCoefficients:")
print(f"Intercept: {model.params[0]:.2f}")
print(f"Population Coefficient: {model.params[1]:.4f}")
print(f"R-squared: {model.rsquared:.3f}")
print(f"P-value for Population: {model.pvalues[1]:.4f}")

# построим график
plt.figure(figsize=(10, 6))

# график разброса наблюдаемых данных
plt.scatter(
    population_crime['population'],
    population_crime['crime'],
    color='blue',
    alpha=0.6,
    label='Фактические данные'
)

# линия регрессии
regression_line = model.params[0] + model.params[1] * population_crime['population']
plt.plot(
    population_crime['population'],
    regression_line,
    color='red',
    linewidth=2,
    label=f'Regression Line\ny = {model.params[0]:.2f} + {model.params[1]:.4f}x'
)

# уравнение регрессии
equation_text = f'y = {model.params[0]:.2f} + {model.params[1]:.4f}x\nR² = {model.rsquared:.3f}'
plt.text(
    0.05, 0.95,
    equation_text,
    transform=plt.gca().transAxes,
    fontsize=12,
    verticalalignment='top',
    bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)
)

plt.xlabel('Population', fontsize=12)
plt.ylabel('Crime', fontsize=12)
plt.title('Линейная регрессия', fontsize=14)
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Коэффициенты и метрики регрессии
Коэффициенты и метрики регрессии

Модель показывает сильную зависимость уровня преступности от численности населения с R² = 0,829, что означает, что 82,9 % дисперсии преступности объясняется показателем населения. Коэффициент регрессии статистически значим: при росте населения на 1 000 человек число преступников увеличивается в среднем на 4 человека (как бы 4 человека из 1 000 значимо статистически могут стать преступниками). Однако, константа незначима (при "нулевом" населении). Такая вот псевдо-зависимость, но как пример использования API StatKit вполне себе подойдет.

И картинка:

Линейная регрессия
Линейная регрессия

И причем здесь погода скажите Вы? Хочу сообщить о запуске разработки нового формата – больших погодных массивов по Российской Федерации от ВНИИГМИ-МЦД в совместимом с ЕМИСС формате SDMX. Что скоро будет внедрено (по некоторым городам РФ данные доступны аж с конца 1800 годов!):

  • суточные данные о температуре почвы по станциям в РФ;

  • суточные данные о температуре атмосферного воздуха и осадках по станциям в РФ;

  • суточные данные о снежном покрове по станциям в РФ.

Заключительные положения

Проект изначально был моим личным небольшим кодом, который помогал мне работать с ЕМИСС, но, как мне кажется, может он поможет и Вам.

Сейчас не все показатели есть у меня по сравнению с ЕМИСС, но я стараюсь добавлять с ЕМИСС новые данные, и, как я указывал в посте, если Вы напиши мне с указанием, какой показатель добавить (а еще лучше прямая ссылка на этот показатель в ЕМИСС), я его постараюсь оперативно добавить.

Еще раз положу здесь:

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А Вы чем пользуетесь для доступа к статистике?
0% ЕМИСС0
0% Витрина данных Росстата0
0% Статистика с сайта Росстата0
0% Другое0
Никто еще не голосовал. Воздержавшихся нет.
Теги:
Хабы:
+2
Комментарии2

Публикации

Работа

Веб дизайнер
18 вакансий

Ближайшие события