Как подсветить временные отрезки на графиках

Вывести временной интервал на график временного ряда с помощь Python

Часто для анализа временных рядов нужно перебрать много факторов в поисках возможных связей. И обычно, факторы — это просто какие-то события происходившие в некоторый период времени и на прямую не влиявшие на целевой показатель. То есть хочется «подсветить» диапазон времени на графике временного ряда, а ещё так чтобы каждый тип события имел свой цвет.
Как оказалось, найти решение гораздо сложнее, чем его реализовать. Код базируется на статье с geeksforgeeks.org, однако моё решение представлено в виде функции, пусть и не очень изящной, позволяет автоматически генерировать разные цвета для разных диапазонов времени, а также собирает легенду.
Для иллюстрации посмотрим, есть ли какие-то взаимосвязи между извержениями вулканов России и средним отклонением мировой температуры от базовой. Кривую отклонения температур берём от сюда, а базу с извержением вулканов можно скачать здесь. Чтобы не загружать график выберем только извержения с VEI от 4 и больше (Volcanic Explosivity Index — метрика силы извержения).

Последовательность действий

В целом, всё что нужно сделать укладывается в 4 пункта:

  1. объявляем subplots

  2. «строим» scatter или plot с целевым значением

  3. с помощью axvspan добавляем нужные временные отрезки

  4. plt.show() — ура, временные интервалы подсветились на графике

Строим график сами

Библиотеки и загрузка данных

Для сборки графика потребуется функции subplots и axvspan из библиотеки matplotlib.pyplot. Данные удобнее всего хранить в датафреймах, поэтому также подгружаем pandas и для генерации цветов, учитывая возможную потребность в их большом количестве воспользуемся методом random из numpy.

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

Загружаем данные. Почему-то база данных извержений вулканов выгружает в очень старых версиях экселя, файлик придется либо пересохранить вручную, либо искать подходящий параметр engine(я не нашла).
Важно, чтобы все временный данные имели одинаковый тип и я везде для удобства использую datetime, но строго говоря это не обязательно — отображать можно и просто числовые данные.

Загрузка и предобработка данных

# загружаем данные в датафремы
df_temp = pd.read_csv('annual_temp.csv')
df_eruptions = pd.read_excel('eruptions.xlsx')
# приводим данные в datetime для температурных данных
df_temp['Year'] = pd.to_datetime(df_temp['Year'], format='%Y')
df_temp.dropna(inplace=True) # убираем пропуски
df_temp = df_temp.groupby('Year').mean() # данные за каждый год представлены несколькими источниками, группируем и берем среднее
df_temp['Year'] = df_temp.index # для удобства создадим столбец с годами
# данные о изврежения нужно фильтровать
df_eruptions = df_eruptions[['Volcano Name', 'VEI', 'Start Year', 'Start Month', 'Start Day', 'End Year','End Month', 'End Day']] # нужные столбцы
df_eruptions.dropna(inplace=True) # убираем пропуски
df_eruptions = df_eruptions[(df_eruptions['VEI'] >=4) &(df_eruptions['Start Year'] >=1880)] # фильтруем данные по VEI 
# это строки посвящены формированию столбцов с датами начала и конца изврежения
df_eruptions[['Start Year', 'Start Month', 'Start Day', 
               'End Year', 'End Month', 'End Day']] = df_eruptions[['Start Year', 'Start Month', 'Start Day', 'End Year', 'End Month', 'End Day']].astype(str)
df_eruptions['start_date'] = pd.to_datetime(df_eruptions['Start Year'] + '/' 
                                            + df_eruptions['Start Month'] + '/' + df_eruptions['Start Day'], format='%Y/%m.0/%d.0' )
df_eruptions['end_date'] = pd.to_datetime(df_eruptions['End Year'] + '/' 
                                            + df_eruptions['End Month'] + '/' + df_eruptions['End Day'], format='%Y.0/%m.0/%d.0')

График

Данные готовы — пора создать визуализацию.
plt.subplots() — позволят создавать сет субграфиков и общий макет подзаголовков. Например, здесь можно задать размер итогового изображения
ax.plot — строим кривую целевых значений (вместо plot, может быть например scatter или другой). В функцию подаем x и y, для легенды можно обозначить label.
ax.axvspan — выделит цветом временной диапазон. На вход нужно подать даты начала и конца интервала. В дополнительные параметры можно передать цвет и лейбл.
В этом коде цвет присваивается извержению, т.е. извержения Шивелуча 1964 года и записанное в одно большое извержения с 1999 года имеют разные цвета. Соответственно и легенда раздувается. В функции ниже реализованы одинаковые цвета.
Поскольку временных интервалов много — реализован цикл пробегающий по спискам с данными. Преобразование в списки продиктовано удобством и понятностью кода.

fig, ax = plt.subplots(figsize=(20, 6)) # задаем сабплот и размеры графика
ax.plot(df_temp['Year'], df_temp['Mean'], marker='x',label='Среднее отклонение от базовой температуры')
eruption_started = df_eruptions['start_date'].to_list()
eruption_ended = df_eruptions['end_date'].to_list()
for i in range(len(eruption_started)):
    ax.axvspan(eruption_started[i], eruption_ended[i], alpha=0.3, color=np.random.rand(3,), label=df_eruptions['Volcano Name'].to_list()[i] )
plt.legend()
plt.show()

Получившийся график с изменением температуры и периодами изверженийПолучившийся график с изменением температуры и периодами извержений

Функция

Реализуем в виде функции highlighted_date. На вход она принимает:

  • pandas.Series с координатами х и у для целевого графика;

  • str — название целевого графика для легенды;

  • pandas.Series с координатами х и у для временных интервалов;

  • pandas.Series с лейблами временных интервалов Функция выводит график с scatter целевой функции (для соединений можно заменить на plot), временные диапазоны окрашиваются по label — т.е. все извержения Шивелуча имеют одинаковый цвет и обозначен в легенде единожды. Отображается легенда.

def highlighted_date (x, y, label, x_2, y_2, label_2):
    """
    main_prepare_data(series,series, str, series,series,series)
    подсвечивает временные отрезки и выводит целевую кривую     
    """
    fig, ax = plt.subplots(figsize=(20, 6)) # задаём параметры графика
    ax.scatter(x, y, marker='x', label=label) # строим график целевого значения
    x_2 = x_2.to_list()
    y_2 = y_2.to_list()
    color_dict = {}
    already_labeled = [] # для фильтрации уже вынесенных в легенду
    for j in label_2.unique(): # создаём словарь цветов для уникальных названий
        color_dict[j] = np.random.rand(3,) # цвета задаются рандомом
    label_2 = label_2.to_list()
    for i in range(len(x_2)):
        if(label_2[i] in already_labeled):
                ax.axvspan(x_2[i], y_2[i], alpha=0.3, color=color_dict[label_2[i]])
        else:
            ax.axvspan(x_2[i], y_2[i], alpha=0.3, color=color_dict[label_2[i]], label=label_2[i])
            already_labeled.append(label_2[i])
    plt.legend()
    plt.show()

Вызов выглядит так:

highlighted_date(df_temp['Year'], df_temp['Mean'], 
                 'Среднее отклонение от базовой температуры',
                 df_eruptions['start_date'], df_eruptions['end_date'], 
                 df_eruptions['Volcano Name'])

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

Вывод

Интересно, насколько велико влияние извержения вулкана Безымянного 1955 года на резкий скачок температуры?

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

© Habrahabr.ru