Введение
Недавно открыл для себя платформу Streamlit и был впечатлен простотой интеграции в питоновский проект. По детски, очень радовался тому что контроллеры на дашборде напрямую меняют питоновские переменные. И вот для тестирования решил поиграть с одной из тем которая мне очень интересна – климат. Начал с самого простого параметра который можно проанализировать – температуру воздуха с метеостанции города в котором я живу — Алматы (Казахстан). Интересно было посмотреть эффект глобального изменения климата на отдельно взятый город.
Для анализа скачал данные за 100 лет (1922-2022). Для начала использовал только месяцы с ноября по апрель включительно, во-первых, потому что в несвободное от работы время я работаю с морским льдом и зимний климат мне понятней, а во-вторых потому что в данных была пропущена неделя наблюдений в августе, в одном из годов.
Один из плюсов Streamlit, то что дашборды можно бесплатно публиковать в Streamlit Cloud, что я и сделал:
https://share.streamlit.io/yevkad/alaclim/main/app.py
Пройдя по этой ссылке, можно поиграть с интерактивными графиками. А в этом посте я поподробней покажу что и как я делал (с кодом) и опишу некоторые свои наблюдения.
Инструменты
Все было сделано на Python и следующими библиотеками:
-
Streamlit — платформа для быстрого создания DS/ML веб-приложений
-
Pandas — манипуляции с таблицами
-
Numpy — использовал некоторые вспомогательные функции
-
Plotly — Все интерактивные графики были построены с помощью этой библиотеки
-
SciPy — для подбора распределения вероятностей
-
pyMannKendall — для Теста Манна-Кендалла, который оценивает статистическую значимость тренда
import streamlit as st import pandas as pd import numpy as np import plotly.express as px import plotly.figure_factory as ff import plotly.graph_objects as go import scipy import pymannkendall as mk from datetime import datetime
Данные
Основная часть данных была взята с NOAA Climate Data Center, к слову — один из любимых источников открытых данных, наравне с европейским Copernicus.
Почему-то было много пропусков после 2006го года, заполнил их данными rp5 — там трехчасовые данные с метеостанций доступны для большинства локаций с 2005го года. На rp5 данные в местном времени, предварительно перевел время в UTC и усреднил подневно.
Данные подготавливаются функцией представленной ниже. Декоратор st.cahce
для того чтобы данные не обрабатывались каждый раз при обновлении контроллеров.
@st.cache def get_winter_data(fl): #initial columns: date, at - daily mean air temperature df_winter=pd.read_csv(fl,parse_dates=['date']) df_winter['fd']=np.abs(np.minimum(0,df_winter['at'])) #freezing days df_winter['month']=df_winter['date'].dt.strftime('%b') #relative date for timeseries. df_winter['date_rel']=pd.to_datetime(df_winter['date'].dt.strftime('2016-%m-%d'), format='%Y-%m-%d') df_winter.loc[df_winter['mon']>8, 'date_rel']=df_winter['date_rel'] - pd.DateOffset(years=1) df_winter.loc[df_winter['date']<datetime(1972,6,1), 'period']='1922-1972' df_winter.loc[df_winter['date']>datetime(1972,6,1), 'period']='1972-2022' df_winter['dt']=df_winter['date'].dt.strftime('%Y%m').astype(int) df_fdd=df_winter.groupby(['seas'])['fd'].sum().reset_index(name='fdd') return df_winter,df_fdd
В результате получаем два датафрейма df_winter
и df_fdd
. У df_winter следующие колонки:
-
date — дата
-
at — среднедневная температура воздуха
-
seas — сезон, строка в виде начального и конечного года (1922-1923)
-
month — месяц в формате строчном формате (Nov, Dec, etc). От нее можно было бы избавиться и оптимизировать обработку, но руки пока не дошли
-
mon — месяц в числовом формате (11, 12 и тд.)
-
period — фиксированный период по 50 лет с 1922 по 1972 и 1972 по 2022
-
fd — freezing days, градусы ниже нуля в абсолюте
-
dt — полукостыль, год-месяц в виде целого числа в формате YYYYMM, для группирования нескольких сезонов в одну группу функцией
np.digitize
(пример будет в следующих секциях) -
rel_date — дата с заменой года для всех дней на 2015 до января и 2016 с января, для того что бы можно было агрегировать 100 лет за каждый день.
У df_fdd
всего 100 рядов, по ряду за каждый сезон и следующие колонки:
-
seas — сезон
-
fdd — Freezing Degree Days — аккумулированные градусы ниже нуля в абсолюте. Более подробное описание будет ниже.
Временной ряд
Временной ряд был составлен выведением на график агрегированных за каждый день с ноября по апрель столетних температур (максимум, минимум, среднее) и возможностью сравнивать эти параметры с выбранными периодами.
Если визуально сравнить первую половину периода со второй, то видно, что в основном средние температуры с 1922 по 1972 ниже столетнего среднего значения, а с 1972 по 2022 выше.
Распределения
Далее были построены гистограммы с подбором распределения вероятностей для двух периодов по 50 лет: 1922-1972 и 1972-2022. В падающем меню можно выбрать тип распределения — Нормальное или Гамма. В данном случае визуально понятно, что Нормальное больше подходит, но в будущем добавлю метрику «goodness of fit».
Левая часть распределения 1972-2022 «сужается» в право, по сравнению с периодом 1922-1972. Это показывает, что за последние 50 лет количество наблюдений экстремально холодных температур уменьшилось. Это не противоречит выведенными параметрами распределения — среднее (Mean) увеличилось на 0.6 градуса, а среднеквадратическое отклонение (SD) уменьшилось по сравнению с начальным периодом.
Усатые Ящики
Помимо сравнения двух фиксированных периодов в виде гистограмм и распределений описанных в предыдущей секции, также была добавлена возможность сравнения распределения выбранных групп периодов (1 год, 5 лет, 10 лет и тд). Наиболее подходящей визуализацией для этого, по моему мнению, являются графики, которые я называл всегда боксплотами, и только сегодня выяснил что русское называние для них — «ящик с усами».
Для тех кто не сталкивался с такими графиками, подробное описание «ящика с усами» можно почитать, например, здесь, но грубо и приближенно, вот как можно его интерпретировать:
-
«Ящик» содержит 50% наблюдений
-
Линия внутри ящика — медиана, или 2-й квартиль — выше и ниже по 50% наблюдений
-
Нижний и верхний «ус» — 25% нижних и верхних наблюдений соответственно
-
Точки — выбросы (наблюдения вне 2.7 среднеквадратических отклонений)
-
Нижняя граница «ящика» — 1-й квартиль — ниже него 25% наблюдений, выше 75%
-
Верхняя граница «ящика» — 3-й квартиль — выше него 25% наблюдений, ниже 75%
Сгруппировав по длительным периодам, например 20 лет, как на примере ниже, видно что, медиана и 1-й квартиль стабильно возрастает с периода который включает в себя 1970е, а 3-й квартиль в последнем периоде. Также как на распределениях описанных выше, изменение зим по большей части выражается в уменьшении «холодов», а в более поздние периоды еще и в увеличении «тепла».
Вот часть кода, где группируются температуры по годам и выводятся в график:
# Years grouping: nmax=df_fdd.shape[0] yr_start=df_winter['year'].min() # list of only divisible groups group_lst=[i for i in range(1,nmax+1,1) if nmax % i==0] step=st.selectbox('Group years ', group_lst, index=group_lst.index(10)) date_range=np.array([datetime(yr_start+i,6,1).strftime('%Y%m') for i in range(step,nmax+1,step) ]).astype(int) date_bin_n=np.array([f'{yr_start+i}-{yr_start+step+i}' for i in range(0,nmax,step)]) df_winter_g=df_winter.copy() #assigning bin name to date range df_winter_g['date_bin']=date_bin_n[np.digitize(df_winter_g['dt'], date_range)] #month filtering mon_lst=df_winter['month'].unique() st_mon_lst = st.multiselect("Months Used", mon_lst, default=mon_lst) df_mon=df_winter_g[df_winter_g['month'].isin(st_mon_lst)] fig_box = px.box(df_mon, x="date_bin", y="at", labels={'seas':'Winter Season', 'at':'Air Temperature (\u00B0C)', 'date_bin':'Date Range'},) st.write(fig_box)
Суровость зим по FDD
Сумма градусодней мороза (Freezing Degree Days — FDD) рассчитывается как абсолютная сумма всех отрицательных температур воздуха за период – обычно за зимний сезон. На всякий случай формула FDD:
Где at — среднедневная температура воздуха, N — число дней в сезоне.
FDD часто используется для примерного определения прироста толщины льда, уровня промерзания почвы, и может служить индексом суровости зим: чем меньше FDD тем мягче зима.
Отобразив на графике FDD за каждый сезон и положив линию тренда, то можно увидеть тенденцию снижения суровости зим, особенно с 1970х годов.
Тренд был посчитан в NumPy как полиномиальная кривая (с возможностью управлять порядком).
poly_deg=st.slider('Degree of a Polynomial Trend', min_value=1, max_value=20, value=1, step=1) trend_x = np.arange(df_fdd.shape[0]) trend_fit = np.polyfit(trend_x, df_fdd['fdd'], poly_deg) trend_fit_fn = np.poly1d(trend_fit)
Помимо визуальной оценки, тренд и его статистическая значимость была оценена тестом Манна-Кенадалла при помощи библиотеки pyMannKendall вот так:
mk_res=mk.original_test(df_fdd['fdd']) # results of Mann-Kendall Test
И вывести результат..
trend_mk=mk_res.trend # Trend: Increasing, Decreasing or No Trend pval_mk=mk_res.p # P-Value of MK Test if pval_mk<0.05: # alpha=0.05 p_str='**statistically significant**' alpha_compr='smaller' else: p_str='**not statistically significant**' alpha_compr='greater' #Markdown string generated to present MK Test results of FDD Trend mk_str=f'''Mann-Kendall Test shows that FDD trend is **{trend_mk}**. \n\n**P-value** is **{pval_mk:.3e}**, which is **{alpha_compr}** than **0.05**, meaning that the trend in the data is {p_str}'''
И в таком виде результаты теста выводятся на страницу:
Ну и напоследок можно посмотреть указанное число наиболее мягких и наиболее суровых зим ранжированных по FDD. В пятерку наиболее мягких попали сезоны последних 30 лет. В пятерку наиболее суровых попали зимы не позже 1970х.
Заключение
Анализ столетних наблюдений температуры воздуха, показал что зимы в Алматы становятся мягче.
Весь дашборд уложился в менее чем 300 строк питоновского файла, значительную часть из которых занимает пояснительный текст. На составление основной части веб-приложения ушло несколько часов, ну и потом периодически что-то добавлял. Для сравнения, до знакомства со Streamlit я делал визуализацию с Bokeh+Flask, и на составление одного графика, когда я только начал изучать Bokeh, ушло полдня.
В будущем буду расширять дашборд новыми элементами и локациями. Добавлю сравнение средних максимумов и минимумов за период. Еще планирую добавить другие параметры, такие как ветер и осадки. Ну и если руки дойдут, может и моделирование какое-нибудь.
ссылка на оригинал статьи https://habr.com/ru/post/668672/
Добавить комментарий