{ "cells": [ { "cell_type": "markdown", "id": "7b5f31e9", "metadata": {}, "source": [ "<img src=\"./imgs/logo.png\" align=\"right\" width=\"150\"/>\n", "\n", "# <div style=\"text-align: center;font-size: 90%;\"><span style=\"color:#000000\">Trabalho final para disciplina Geoinformática (CAP-395)</span>\n", "\n", "\n", "# <div style=\"text-align: center;font-size: 90%;\"><span style=\"color:#336699\">Biblioteca para análise, visualização de trajetória e geometrias de sistemas atmosféricos rastreados</span>\n", "<hr style=\"border:2px solid #0077b9;\"></div>\n", "\n", "\n", "\n", "<div style=\"text-align: left;\">\n", " <a href=\"https://nbviewer.jupyter.org/github/helvecioneto/stanalyzer/blob/main/docs/Example.ipynb\"><img src=\"https://raw.githubusercontent.com/jupyter/design/master/logos/Badges/nbviewer_badge.svg\" align=\"center\"/></a>\n", "</div>\n", "\n", "<br/>\n", "\n", "<div style=\"text-align: justify;font-size: 90%;\">\n", " Helvécio B. Leal Neto<sup><a href=\"https://orcid.org/0000-0002-7526-2094\"><i class=\"fab fa-lg fa-orcid\" style=\"color: #a6ce39\"></i></a></sup>\n", " <br/><br/>\n", " Instituto Nacional de Pesquisas Espaciais (INPE)\n", " <br/>\n", " Avenida dos Astronautas, 1758, Jardim da Granja, São José dos Campos, SP 12227-010, Brazil\n", " <br/><br/>\n", " Contato: <a href=\"mailto:helvecio.neto@inpe.br\">helvecio.neto@inpe.br</a>\n", " <br/><br/>\n", " Última atualização: 30 de Agosto de 2021\n", "</div>\n", "\n", "</br>\n", "\n", "<div style=\"text-align: justify; margin-left: 25%; margin-right: 25%;\">\n", "<b>Resumo.</b> A\n", "A dinâmica de propagação dos sistemas atmosféricos precipitantes pode ser estudada por meio de algoritmos computacionais que abstraem informações com base em dados de observação. Os resultados de saída destes algoritmos podem ser interpretados para realização de pesquisas, produção de gráficos, tabelas e imagens. Neste trabalho, foi desenvolvida uma biblioteca na linguagem Python que reúne diversas funcionalidades para visualização e análise dos dados da saída de um algoritmo que rastreia sistemas atmosféricos precipitantes na Amazônia. A biblioteca desenvolvida aplica conceitos de Geoinformática para processamento, visualização e análise de trajetória de sistemas atmosféricos precipitantes. Conceitos relacionados à geometria e trajetória de objetos foram utilizados para representar a morfologia de clusters de chuva e a trajetória em que os sistemas propagaram-se. Os módulos desenvolvidos nesta biblioteca realizam a leitura dos dados de saída do algoritmo desenvolvido por Leal Neto (2021) e os ajustam para uma melhor abstração das informações de rastreio. A biblioteca contém funções de filtragem de eventos com base na sua duração e tipo de evento, exibição da trajetória de deslocamento, visualização das características morfológicas, além de possibilitar a produção de gráficos dinâmicos com base nas estatísticas abstraídas pelo algoritmo de rastreio.\n", "</div> \n", "\n", "<br/>\n", "<div style=\"text-align: justify; margin-left: 25%; margin-right: 25%;font-size: 75%; border-style: solid; border-color: #0077b9; border-width: 1px; padding: 5px;\">\n", " <b>Este Jupyter Notebook foi desenvolvido para auxiliar na visualização e análise dos dados de saída do algoritimo desenvovido no trabalho:</b>\n", " <div style=\"margin-left: 10px; margin-right: 10px\">\n", " Leal Neto, H. B. <a href=\"http://urlib.net/rep/8JMKD3MGP3W34R/44HGF8E\" target=\"_blank\">Rastreio e previsão de sistemas precipitantes e convectivos na bacia Amazônica utilizando aprendizado de máquina não-supervisionado</a>. Dissertação (Mestrado em Computação Aplicada) - Instituto Nacional de Pesquisas Espaciais (INPE), São José dos Campos, 2021. Disponível em: <<a href=\"http://urlib.net/rep/8JMKD3MGP3W34R/44HGF8E\">http://urlib.net/rep/8JMKD3MGP3W34R/44HGF8E</a>>.\n", " </div>\n", "</div>" ] }, { "cell_type": "markdown", "id": "e87ed882", "metadata": {}, "source": [ "# Sumário\n", "<hr style=\"border:1px solid #0077b9;\">\n", "\n", "\n", "[1. Introdução](#intro) <br></br>\n", "[2. O Algoritmo de rastreio e estrutura dos dados](#algoritmo_estrutura)<br></br>\n", " [2.1 Algoritmo](#algoritmo)<br></br>\n", " [2.2 Estrutura dos dados](#dados)<br></br>\n", "\n", "[3. Metodologia](#metodologia)<br></br>\n", " [3.1 Download e instalação da biblioteca](#download_instalacao)<br></br>\n", " [3.2 Organização dos dados](#organizacao)<br></br>\n", " [3.2.1 Função: \"fam_generator()\"](#fam_generator)<br></br>\n", " [3.3 Trajetória](#trajectory)<br></br>\n", " [3.4 Leitura dos dados Função: “stanalyzer.read_data()](#read_data)<br></br>\n", " [3.5 Ciclo de vida “stanalyzer.life_cycle()”](#life_cyle)<br></br>\n", " [3.6 Filtro de eventos e consultas”](#consultas)<br></br>\n", " [3.6.1 Filtro por duração Função: “stanalyzer.time_filter()”.](#time_filter)<br></br>\n", " [3.6.2 Filtro por tipo de evento “stanalyzer.fam_type(data)\".](#fam_type)<br></br>\n", "\n", "[3.7 Visualização”](#visualizacao)<br></br>\n", " [3.7.1 Rosa dos Ventos “stanalyzer.plot_wind()](#wind_rose)<br></br>\n", " [3.7.2 Linhas “stanalyzer.plot_lines()](#line_plot)<br></br>\n", "\n", "\n", "[4. Considerações finais](#conclusao)<br></br>\n", "[Referências bibliográficas](#bibliografia)<br></br>\n", "[Anexo 1](#anexo)<br></br>" ] }, { "cell_type": "markdown", "id": "8982638c", "metadata": {}, "source": [ "## 1. Introdução\n", "<a id='intro'></a>\n", "<hr style=\"border:1px solid #0077b9;\">\n", "\n", "<div style=\"text-align: justify\">\n", " \n", "A atmosfera do planeta é composta por diferentes níveis, onde a interação entre os gases que a compõem apresentam características distintas, provocando a alteração nos campos de pressão e a formação de sistemas atmosféricos. Nos níveis mais próximos à superfície, onde ocorrem os principais sistemas atmosféricos precipitantes, as gotículas de água e os cristais de gelo aglutinados formam as nuvens de chuva, que deslocam-se de acordo com a variação dos fluxos atmosféricos [1]. As estruturas dos sistemas atmosféricos precipitantes podem ser do tipo estratiforme (regiões com menor intensidade de precipitação) ou convectivo (maior intensidade e núcleos convectivos) [2].\n", "<br></br>\n", " \n", "Alguns instrumentos são utilizados para mensurar os níveis de precipitação e o deslocamento dos sistemas atmosféricos precipitantes, dentre estes destacam-se os radares meteorológicos. As medições realizadas pelos radares meteorológicos são varreduras volumétricas relacionadas ao conteúdo de precipitação contido nas nuvens, onde, os índices de precipitação são obtidos por meio de pulsos eletromagnéticos [3].\n", "<br></br>\n", " \n", "Monitorar o deslocamento e evolução dos sistemas atmosféricos precipitantes por meio de instrumentos e softwares contribui para o entendimento dos processos de formação e ciclo de vida destes sistemas. Para este fim, diversos algoritmos computacionais são utilizados para abstração de informações a partir dos dados de sensores. Esses algoritmos aplicam diversas técnicas com intuito de identificar os sistemas atmosféricos precipitantes, e rastrear sua trajetória de deslocamento. Um destes algoritmos foi desenvolvido por Leal Neto (2021) e utiliza técnicas baseadas nas geometrias de clusters de chuva presentes em dados de radar, aplicando operações espaciais de sobreposição para determinar a trajetória de deslocamento de sistemas precipitantes.\n", "<br></br>\n", " \n", "A saída do algoritmo é composta por uma tabela que contém informações sobre os clusters identificados. Estas informações correspondem às feições de contorno (boundary), centróide, vetores de deslocamento e estatísticas do conjunto de clusters identificados pelo algoritmo. Como o objetivo do algoritmo é apenas explorar informações estatísticas dos sistemas rastreados, as geometrias de cada sistema atmosférico são representadas de forma simplificada, ou seja, não foram associados nenhum tipo de sistemas de coordenadas as geometrias.\n", "<br></br>\n", " \n", "Com base nos dados de saída do algoritmo desenvolvido por Leal Neto (2021), este trabalho apresenta uma biblioteca que faz a interpretação dos dados, e implementa conceitos de Geoinformática para melhor representar os sistemas precipitantes rastreados. Além disso, algumas funcionalidades para visualização dos resultados obtidos pelo algoritmo são demonstradas neste trabalho.\n", "</div>" ] }, { "cell_type": "markdown", "id": "4c121185", "metadata": {}, "source": [ "## 2. O Algoritmo de rastreio e estrutura dos dados\n", "<a id='algoritmo_estrutura'></a>\n", "<hr style=\"border:1px solid #0077b9;\">\n", "\n", "### 2.1 Algoritmo\n", "<a id='algoritmo'></a>\n", "\n", "<div style=\"text-align: justify\">\n", " \n", "O algoritmo abordado neste trabalho tem como propósito rastrear sistemas atmosféricos, partindo da definição de parâmetros relacionados à morfologia e valores de intensidade presentes em dados de radar meteorológico. Nesta metodologia, utilizou-se da técnica baseada no centróide das células precipitantes identificadas após um processo de clusterização, e a sobreposição entre geometrias correspondentes às feições de contorno provenientes dos limiares de refletividade, para identificar a trajetória individual dos sistemas precipitantes. Na Figura 1. é demonstrado o fluxo de processamento do algoritmo até a sua saída, onde, um arquivo no formato de tabela (Tabela de rastreio) é gerado e corresponde a informações de rastreio das células precipitantes.\n", "</div>\n", "\n", "<br></br>\n", "\n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Figura 1. Fluxograma com o funcionamento do algoritmo.</span></div>\n", "<img src=\"./imgs/Fluxograma_Geral.png\" align=\"center\" width=\"600\" height=\"600\"/>\n", "<br></br>\n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Fonte: Adaptado de Leal Neto (2021).\n", "</span></div>\n", "<br></br>\n", " \n", "<div style=\"text-align: justify\"> \n", " \n", "A tabela de rastreio (Tr) é a entidade responsável por armazenar as informações e estatísticas do algoritmo. Esta tabela é utilizada por outros processos do algoritmo, onde, uma interação ocorre a cada ciclo de leitura e processamento. Ao final de todos os ciclos de leitura dos dados, a Tr será a saída do algoritmo. Uma das principais\n", "consultas realizadas na tabela tem como objetivo retornar a geometria das células precipitantes. Neste processo, as informações e estatísticas são carregadas para memória do computador e utilizadas para identificação da trajetória das células entre dois tempos consecutivos [4].\n", "</div>\n", "\n", "### 2.2 Estrutura dos dados\n", "<a id='dados'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "Como supracitado, a biblioteca desenvolvida neste trabalho utiliza os dados de saída do algoritmo de rastreio. O conjunto destes dados são informações correspondentes aos parâmetros de rastreio selecionados pelo usuário, onde, um arquivo compactado que contém diversas informações referentes ao período de rastreio é gerado. A estrutura do arquivo de saída é composta pelos itens demonstrados abaixo:\n", "<br></br>\n", "\n", "<img src=\"./imgs/Arvore_diretorios_IARA.png\" align=\"center\" width=\"600\" height=\"600\"/>\n", " \n", "Essa estrutura agrupa diversas informações, desde os clusters (clusters/) identificados em cada iteração do e os clusters previstos (predict_clusters/) pelo algoritmo. Um arquivo de log (LOG_SYYMMDDHHMM_EYYMMDDHHMM_VAR_THR_LVL_FLAGS.txt) armazena informações sobre o início e fim do rastreio, e os parâmetros definidos pelo usuário. O arquivo que armazena as habilidades do algoritmo também é criado (SKILL_SCORE.txt). E por fim, a TABELA_DE_RASTREIO.csv que armazena as geometrias e estatísticas de rastreio.\n", "<br></br>\n", "<br></br>\n", " \n", "A Tabela de Rastreio será o principal arquivo a ser adaptado pela biblioteca desenvolvida neste trabalho, isso porque as geometrias armazenadas não possuem nenhum sistema de referência de coordenadas (crs) associados. Portanto, o primeiro ajuste a ser feito é nas geometrias de contorno de cada cluster, transformando os pontos x e y de cada geometria em pontos de coordenadas (latitude e longitude), esse tema será abordado na seção de Metodologias deste trabalho. Na Figura 2. demonstra-se a estrutura da Tabela de Rastreio no formato de DataFrame com as respectivas informações sobre as estatísticas e geometrias de contorno de cada cluster.\n", "</div>\n", "<br></br>\n", "\n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Figura 2. Exemplo da Tabela de Rastreio.</span></div>\n", "<img src=\"./imgs/TRACK_TABLE.png\" align=\"center\" width=\"600\" height=\"600\"/>\n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Fonte: Produção do Autor.</span></div>" ] }, { "cell_type": "markdown", "id": "9eeae03a", "metadata": {}, "source": [ "### 3. Metodología: A biblioteca “stanalyzer”\n", "<a id='metodologia'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "Esta seção demonstra o processo de instalação e utilização da biblioteca denominada stanalyzer ( Storm Track Analyzer ). Um dos objetivos da biblioteca é realizar a adaptação dos dados de saída do algoritmo desenvolvido por Leal Neto (2021). Funções com intuito de ler, processar e visualizar os resultados foram implementadas na biblioteca stanalyzer. A utilização da biblioteca será demonstrada nas próximas seções. \n", "</div>\n", "<br></br>\n", "\n", "#### 3.1. Download e instalação da biblioteca\n", "<a id='download_instalacao'></a> \n", "<br>\n", "<div style=\"text-align: justify\"> \n", " \n", "A biblioteca stanalyzer está em contínuo desenvolvimento, que pode ser acompanhado diretamente na plataforma de hospedagem de código-fonte e arquivos com controle de versão Github, através do link: github.com/helvecioneto/stanalyzer. A biblioteca foi desenvolvida na linguagem Python na versão 3.7, sua instalação necessita de algumas dependências que podem ser encontradas no arquivo README também disponível no Github. Para instalar basta entrar com o seguinte comando no terminal:\n", "</div>\n", "\n", "<p style=\"background:black\">\n", "<code style=\"background:black;color:white\">\n", "$ git clone https://github.com/helvecioneto/stanalyzer\n", "$ cd stanalyzer\n", "$ conda env create --file stanalyzer.yml\n", "$ conda activate stanalyzer\n", " \n", "</code>\n", "</p>\n", "\n", "\n", "#### 3.2. Organização dos dados\n", "<a id='organizacao'></a> \n", "<br>\n", "#### 3.2.1 Fam Generator:\n", "<a id='fam_generator'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "O Fam Generator é uma função da biblioteca stanalyzer que recebe parâmetros para adaptar as saídas do algoritmo. O processamento realizado pela função fam_generator() cria um novo conjunto de arquivos. Estes arquivos seguem um padrão que facilita o manuseio das informações, geração de gráficos e estatísticas. Abaixo é demonstrado os códigos necessários para importação da biblioteca e os parâmetros necessários para realizar o processamento dos dados de rastreio. \n", "</div>\n", "\n", "```python\n", "## Importação da biblioteca\n", "import stanalyzer as sta\n", "\n", "## Parâmetro que indica o arquivo de saída gerado pelo algoritmo de rastreio.\n", "PATH= '~/S201409070000_E201409100000_VDBZc_T20_L5_SPLTTrue_MERGTrue_TCORTrue_PCORFalse.zip'\n", "\n", "## Caminho que indica os arquivos que o algoritmo utilizou para realizar o rastreio (arquivos netCDF).\n", "DATA_PATH = '~/DADOS/sbandradar/'\n", "\n", "## Nome da variável presente nos arquivos netCDF.\n", "VAR_NAME = 'DBZc'\n", "\n", "## Nível de elevação dos dados da matriz 3 dimensional referente às varreduras volumétricas de radar. \n", "LEVEL = 5\n", "\n", "## Limiares de rastreio.\n", "THRESHOLD = [20,35,40]\n", "\n", "## Diretório de saída para os novos arquivos.\n", "OUTPUT = './output/'\n", "NC_OUTPUT = './output/data/'\n", "OUTPUT_FILE = './output/output_file_tracked'\n", "\n", "## Função que inicia o processo de geração das famílias.\n", "sta.fam_generator()\n", "\n", "\n", "```\n", "<div style=\"text-align: justify\"> \n", " \n", "Um dos conceitos abordados no trabalho Leal Neto (2021) fala sobre as famílias de clusters. Uma família (Fam) é um conjunto de observações para um mesmo cluster no decorrer do seu ciclo de vida, e cada Fam é enumerada com um UID (Identificador único) utilizado para identificar os clusters individuais [4]. O UID será utilizado para agrupar as Fams rastreadas, na Figura 3. exibe-se um exemplo de como será organizado o novo DataFrame. A primeira coluna será o índice de identificação que agrupa as famílias de acordo com seu UID, as demais informações correspondem a estatísticas e identificadores dos clusters rastreados. \n", "</div>\n", " \n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Figura 3. Exemplo de organização das Fams na nova tabela de rastreio.</span></div>\n", "<img src=\"./imgs/Fam_Gem01.png\" align=\"center\" width=\"600\" height=\"600\"/>\n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Fonte: Produção do Autor.</span></div>\n", "<br></br>\n", "\n", "<div style=\"text-align: justify\"> \n", " \n", "Além de organizar os dados de saída do algoritmo utilizando os conceitos de famílias, foi aplicado um processo de transformação das geometrias (Figura 3). A transformação geográfica é uma operação matemática que converte as coordenadas de um ponto em um sistema de coordenadas geográficas nas coordenadas do mesmo ponto em outro sistema de coordenadas geográficas [5]. Essa operação foi realizada pois o algoritmo de rastreio apenas gera geometrias de contorno dos clusters com as mesmas dimensões do arquivo original (241 linhas e 241 colunas). A operação foi realizada utilizando as matrizes de latitude e longitude contidas nos arquivos netCDF que contém as informações de pontos de coordenadas geográficas, tais informações foram armazenadas no sistema de referências WGS84. Cada geometria corresponde ao contorno dos clusters individuais, e após a operação de transformação as geometrias foram armazenadas no novo DataFrame.\n", "</div>\n", "<br></br>\n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Figura 4. Exemplo de transformação dos vértices de um polígono (x,y) para um polígono georreferenciado.</span></div>\n", "<img src=\"./imgs/Conversion.png\" align=\"center\" width=\"500\" height=\"500\"/>\n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Fonte: Produção do Autor.</span></div>\n", "<br></br>" ] }, { "cell_type": "markdown", "id": "5e16bd89", "metadata": {}, "source": [ "#### 3.3. Trajetórias\n", "<a id='trajectory'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "A trajetória das células precipitantes no algoritmo desenvolvido por Leal Neto (2021), pode ser descrita como um vetor de deslocamento gerado a partir dos centróides de duas ou mais células em tempos sucessivos, e que atendem a um critério mínimo de sobreposição. Segundo Ferreira et al. 2014, uma trajetória representa como as localizações ou limites de um objeto variam ao longo do tempo, ou seja, aplicando este conceito ao estudo aqui apresentado e implementado na biblioteca stanalyzer, a trajetória de uma Fam pode ser representada pelo deslocamento das geometrias que correspondem ao contorno dos clusters de células precipitantes e seu vetor de deslocamento. Com base nisso, a biblioteca stanalyzer utiliza os pontos de latitude e longitude correspondentes aos centróides de cada geometria de contorno das células para criar uma LineString entre os pontos em tempos sucessivos (t e t-1). O conjunto de LineStrings de uma mesma Fam representa o deslocamento do centróide e o ciclo de vida de um cluster rastreado pelo algoritmo. A Figura 4.a representa o DataFrame com as geometrias de uma Fam (UID 6) no decorrer do seu ciclo de vida, e na Figura 4.b demonstra-se o deslocamento do cluster por meio das geometrias (linhas e polígonos) rastreados pelo algoritmo.\n", "</div>\n", "\n", "\n", "<br></br>\n", "<div style=\"text-align: justify;font-size: 100%;\"><span style=\"color:#000000\">Figura 4. DataFrame com as informações da Fam 6, a coluna trajectory representa o deslocamento dos centróides e a coluna geom_20 as geometrias de contorno dos clusters para o limiar de 20 dBZ. b) Visualização de trajetórias dos LINESTRING’s e POLYGON’s da Fam 6.\n", "</span></div>\n", "<img src=\"./imgs/table_fig.png\" align=\"center\" width=\"800\" height=\"800\"/>\n", "<div style=\"text-align: center;font-size: 100%;\"><span style=\"color:#000000\">Fonte: Produção do Autor.</span></div>\n", "<br></br>" ] }, { "cell_type": "markdown", "id": "00857151", "metadata": {}, "source": [ "#### 3.4. Leitura dos dados “stanalyzer.read_data()”.\n", "<a id='read_data'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "Após o processo de organização dos dados feito pela função fam_generator() (seção anterior 3.2) as informações de rastreio são organizadas no formato de DataFrame e armazenadas como um arquivo no formato Pickle da linguagem Python. Este arquivo foi gerado e compactado para ocupar menos espaço de armazenamento, esta compactação foi feita no modo de compressão XZ (https://tukaani.org/xz/). Para ler este arquivo é possível chamar a função “read_file(path)” como demonstrado na célular de código abaixo, ou diretamente pela biblioteca pandas (https://pandas.pydata.org/docs/) com o comando: pandas.read_pickle(path,compression='xz')." ] }, { "cell_type": "markdown", "id": "59558a54", "metadata": {}, "source": [ "#### Exemplo 2:" ] }, { "cell_type": "code", "execution_count": null, "id": "df6bcfd7", "metadata": {}, "outputs": [], "source": [ "### Flags para autoreload\n", "%load_ext autoreload\n", "%autoreload 2\n", "\n", "## Importação biblioteca do sistema\n", "import sys\n", "sys.path.append(\"../\")\n", "\n", "## Importação da biblioteca stanalyzer\n", "import stanalyzer as sta" ] }, { "cell_type": "code", "execution_count": null, "id": "b58bfe90", "metadata": {}, "outputs": [], "source": [ "## Chamada da função read_file() e armazenamento em um DataFrame (track_frame)\n", "track_frame = sta.read_file('../output/tracking_compressed.pkl')" ] }, { "cell_type": "code", "execution_count": null, "id": "5d6e3c46", "metadata": {}, "outputs": [], "source": [ "## Cabeçalho do DataFrame\n", "track_frame.head()" ] }, { "cell_type": "markdown", "id": "cc2ff481", "metadata": {}, "source": [ "#### 3.5. Ciclo de vida “stanalyzer.life_cycle()”.\n", "<a id='life_cyle'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "O ciclo de vida dos sistemas atmosféricos é um assunto bastante discutido em vários trabalhos [7][8][9]. O estudo sobre a duração do ciclo de vida dos sistemas atmosféricos é importante para descrever as características de deslocamento e evolução dos processos envolvidos em sua formação. Com base nisso, desenvolveu-se uma função que exibe a duração dos sistemas rastreados pelo algoritmo. Esta função exibe o início e o final do ciclo de vida de cada família com seus respectivos UIDs. Abaixo demonstra-se um exemplo da função “stanalyzer.life_cycle()”.\n", "\n", " \n", " No DataFrame abaixo (lifes) as informações estão armazenadas de acordo com o uid (Identificador Único), times (Corresponde ao número de vezes que o cluster permaneceu ativo), begin (Data de inicio do evento), end (Data de dissipação ou fusão do cluster) e duration (Tempo de duração do evento)." ] }, { "cell_type": "code", "execution_count": null, "id": "51dad9fa", "metadata": {}, "outputs": [], "source": [ "## Esta função retorna a duração dos eventos\n", "lifes = sta.life_cycle(track_frame,sort=True)\n", "lifes" ] }, { "cell_type": "markdown", "id": "b962b9d8", "metadata": {}, "source": [ "#### 3.6. Filtro de eventos e consultas\n", "<a id='consultas'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "A estrutura bidimensional dos DataFrames fornece uma vasta possibilidade de aplicações, desde consultas diretas por meio das buscas por indexação loc e iloc, a buscas que lembram a estrutura de consulta em SQL com a função “query” do Pandas. Algumas funções foram implementadas na biblioteca stanalyzer para facilitar sua utilização pelos usuários. O primeiro filtro foi realizado para duração dos eventos e o segundo para o tipo de evento com base em seus “status”.\n", "</div>\n", "\n", "##### 3.6.1. Filtro por duração “stanalyzer.time_filter()”.\n", "<a id='time_filter'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "A biblioteca stanalyzer tem uma função que faz a filtragem de eventos com base em sua duração, e foi implementada para facilitar os estudos de eventos rastreados com tempo mínimo e máximo de duração. Neste caso a função recebe um DataFrame no formato pré-definido pelo fam_generator() e mais três parâmetros relacionados à duração, sendo estes:\n", "</div>" ] }, { "cell_type": "code", "execution_count": null, "id": "b126321f", "metadata": {}, "outputs": [], "source": [ "### Exemplo 4.\n", "\n", "## PAR METROS\n", "TIME_MIN = 2 # Tempo mínimo de duração de um evento\n", "TIME_MAX = 4 # Tempo máximo\n", "UNIT = 'h' # String para unidade temporal (h = hora, m = minuto)\n", "\n", "## Uso do filtro de eventos baseado no tempo mínimo e máximo\n", "track_frame_filtered = sta.time_filter(track_frame, TIME_MIN, TIME_MAX, UNIT)\n", "track_frame_filtered.head()" ] }, { "cell_type": "markdown", "id": "48806789", "metadata": {}, "source": [ "<div style=\"text-align: justify\"> \n", " \n", "Para verificar se o track_frame_filtered armazenou apenas eventos de acordo com o filtro temporal basta chamar a função de duração de eventos novamente. No exemplo acima a função time_filter() retorna um novo DataFrame com eventos que possuem duração mínima por Fam de 2 Horas e duração máxima de até 4 Horas." ] }, { "cell_type": "code", "execution_count": null, "id": "187a759e", "metadata": {}, "outputs": [], "source": [ "## Esta função retorna a duração dos eventos\n", "verification = sta.life_cycle(track_frame_filtered,sort=True)\n", "verification.head()" ] }, { "cell_type": "markdown", "id": "b012212d", "metadata": {}, "source": [ "##### 3.6.2. Filtro por tipo de evento “stanalyzer.fam_type(data,)”.\n", "<a id='fam_type'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "Outra forma bastante utilizada em alguns trabalhos [10][11] é o filtro por tipo de eventos. A função fam_type() recebe como parâmetros um DataFrame gerado pela função fam_generator() e uma String especificando qual tipo de evento a ser filtrado. Os tipos de eventos são:\n", "<br></br>\n", " \n", "- <b>NEW</b>: Células que foram identificadas no tempo atual, ou geradas a partir de uma divisão entre clusters.\n", "- <b>CONT</b>: Eventos cuja dinâmica de propagação dos clusters manteve-se contínua durante todo ciclo de vida.\n", "- <b>SPLIT</b>: Famílias de clusters que apresentaram UM ou mais eventos de divisão entre os seus clusters.\n", "- <b>MERG</b>: Famílias que apresentaram UM ou mais eventos de fusão entre seus clusters durante todo ciclo de vida.\n", "<br></br>\n", "\n", "<div style=\"text-align: justify\"> \n", " \n", "Selecionar o tipo de evento com base na dinâmica dos clusters corrobora para compreensão dos movimentos de propagação das células precipitantes. Com isso, a função fam_type() torna-se bastante útil pois desmembra eventos com base na sua dinâmica. Abaixo segue um exemplo de operação que retorna famílias com eventos onde houveram um ou mais divisões entre células durante o ciclo de vida dos clusters. " ] }, { "cell_type": "code", "execution_count": null, "id": "cd4b1c7e", "metadata": {}, "outputs": [], "source": [ "## Função fam_type para eventos famílias apenas com eventos de continuidade\n", "cont_events = sta.fam_type(track_frame,'CONT')\n", "\n", "## Função fam_type para eventos famílias apenas com eventos de continuidade\n", "splt_events = sta.fam_type(track_frame,'SPLT')\n", "\n", "## Função fam_type para eventos famílias apenas com eventos de continuidade\n", "merg_events = sta.fam_type(track_frame,'MERG')" ] }, { "cell_type": "code", "execution_count": null, "id": "df9a37fe", "metadata": {}, "outputs": [], "source": [ "print('Tempo médio das famílias do tipo CONT:',sta.life_cycle(cont_events,sort=True)['duration'].mean())\n", "print('Tempo médio das famílias do tipo SPLT:',sta.life_cycle(splt_events,sort=True)['duration'].mean())\n", "print('Tempo médio das famílias do tipo MERG:',sta.life_cycle(merg_events,sort=True)['duration'].mean())" ] }, { "cell_type": "markdown", "id": "ab31a642", "metadata": {}, "source": [ "### 3.7. Visualização de eventos\n", "<a id='visualizacao'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "Uma das aplicações que podem ser utilizadas para compreensão da dinâmica de propagação de células precipitantes é por meio da visualização dos eventos. Para este fim, a biblioteca stanalyzer conta com funções de visualização dos rastreios com informações individualizadas de cada cluster rastreado. Os módulos de visualização possuem funções interativas que permitem a seleção de características e eventos de forma mais específica. Além da interação com os gráficos, os usuários também podem aplicar os filtros e consultas para uma melhor visualização dos eventos.\n", "\n", " \n", "A saída da “stanalyzer.track()” retorna uma imagem com base no último registro da coluna “timestamp” do DataFrame utilizado pela biblioteca stanalyzer. Na imagem gerada pela função é possível selecionar e visualizar características relacionadas às geometrias, clusters e a trajetória das células precipitantes rastreadas. Os botões laterais na esquerda da Figura 5. e Figura 6. possuem funções interativas que alteram as informações diretamente na imagem gerada. Além destes botões, informações relacionadas aos clusters podem ser visualizadas ao passar o indicador do mouse sobre o centróide de cada cluster. Estas informações surgem como um “balão” na tela. A Figura 6. demonstra esta funcionalidade, e também é demonstrado como estão distribuídos os clusters rastreados em 20 dBZ com o botão “Clusters 20 dBZ” selecionado. \n", "</div>\n", "<br>\n", " \n", "<b>Opções de visualização</b>\n", "- <b>None</b>: Exibe apenas a imagem extraída do arquivo netCDF.\n", "- <b>All</b>: Exibe todas as camadas de rastreio.\n", "- <b>Clusters {Primeiro Limiar}</b>: Exibe os clusters do primeiro Limiar. \n", "- <b>Clusters {Segundo Limiar}</b>: Exibe os clusters do primeiro Limiar, mais intensos que o primeiro limiar.\n", "- <b>Clusters {Terceiro Limiar}</b>: Exibe os clusters do segundo Limiar, clusters mais internos e intensos.\n", "- <b>Geometrias {Primeiro Limiar}</b>: Exibe as geometrias de contorno do primeiro Limiar. \n", "- <b>Geometrias {Segundo Limiar}</b>: Exibe as geometrias de contorno do segundo Limiar.\n", "- <b>Geometrias {Terceiro Limiar}</b>: Exibe as geometrias de contorno do terceiro Limiar.\n", "- <b>Trajectory</b>: Exibe a trajetória dos clusters durante todo seu ciclo de vida.\n", "#### Exemplo 6" ] }, { "cell_type": "code", "execution_count": null, "id": "4c0b8b00", "metadata": {}, "outputs": [], "source": [ "## Nome da variável com as matrizes de dados nos arquivos netCDF.\n", "VAR = \"DBZc\"\n", "\n", "## Caso dados contenham múltiplos níveis, se não deixar vazio.\n", "LEVEL = 5\n", "\n", "## Operação para filtrar dados de visualização com time <= 100 (coluna time)\n", "query_frame = track_frame.query('time <= 100')\n", "\n", "## Função para visualização\n", "sta.track(query_frame, var=VAR, level=LEVEL)" ] }, { "cell_type": "markdown", "id": "d00db9cf", "metadata": {}, "source": [ "#### 3.7.1 Rosa dos Ventos “stanalyzer.plot_wind()”\n", "<a id='wind_rose'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "A rosa dos ventos (em inglês, wind rose) é um artifício gráfico bastante utilizado na meteorologia para representar a velocidade e direção do vento. No caso da função “stanalyzer.plot_wind()” foi implementado um conjunto de operações que agrupa as informações das componentes vetoriais (velocidade e direção) de uma Fam individual, e as representa graficamente por meio de uma rosa dos ventos. Dois métodos de representação foram implementados, o primeiro (Figura 7.a) tem como propósito representar o deslocamento médio de cada sistema com o uso do desvio vetorial padrão (desvios-padrão dos componentes velocidade e direção) [12] pelo modo de gráfico de barras. E o segundo modo (dispersão) (Figura 7.b) demonstra o acumulado das informações vetoriais de cada Fam no decorrer do rastreio. Abaixo um exemplo para cada um dos modos de visualização da função stanalyzer.plot_wind():" ] }, { "cell_type": "code", "execution_count": null, "id": "c9bad723", "metadata": {}, "outputs": [], "source": [ "## Filtro no dataFrame para eventos no time <= 100.\n", "query = track_frame.query('time <= 100')\n", "\n", "# ## Modo gráfico para wind rose com estilo scatter (Figura 7.b).\n", "wind_frame = sta.plot_wind(query, style = 'scatter')" ] }, { "cell_type": "markdown", "id": "f15bbd79", "metadata": {}, "source": [ "Caso seja necessário trabalhar com os dados de direção e vento a função plot_wind também retorna um DataFrame." ] }, { "cell_type": "code", "execution_count": null, "id": "25cbf536", "metadata": {}, "outputs": [], "source": [ "wind_frame" ] }, { "cell_type": "markdown", "id": "22f89a32", "metadata": {}, "source": [ "Uma outra maneira de visualização dos dados de direção e velocidade de propagação dos clusters é por meio do wind_plot() como gráfico de barras. Abaixo segue um exemplo que foi aplicado filtro individual por tempo." ] }, { "cell_type": "code", "execution_count": null, "id": "1b919cbf", "metadata": {}, "outputs": [], "source": [ "## Filtro no dataFrame para eventos no tempo <= 40\n", "query2 = track_frame.query('time <= 40')\n", "\n", "# ## Modo gráfico para wind rose com estilo bar (Figura 7.b).\n", "wind_frame2 = sta.plot_wind(query2, style = 'bar')" ] }, { "cell_type": "markdown", "id": "4557dc4d", "metadata": {}, "source": [ "#### 3.7.2 Linhas “stanalyzer.plot_lines()”\n", "<a id='line_plot'></a> \n", "<div style=\"text-align: justify\"> \n", " \n", "A visualização das informações de rastreios por meio dos gráficos de linhas é utilizada para analisar diversas variáveis na forma de séries temporais. Para isso, a função stanalyzer.plot_lines() foi desenvolvida com intuito de facilitar a análise dos dados. É possível visualizar um conjunto de colunas do DataFrame por meio do parâmetro ‘analyze_columns’ que irá selecionar apenas as colunas necessárias, por exemplo, as informações de refletividade dos clusters: 'mean_ref_20','mean_total_ref_35','mean_total_ref_40' ou o tamanho dos clusters com as colunas: 'size_20','total_size_35','total_size_40'. Uma query pode ser utilizada para filtrar informações de acordo com a necessidade dos usuários. No Exemplo 8. e na Figura 8. é demonstrado como a função stanalyzer.plot_lines() pode ser utilizada para acompanhar o ciclo de vida e as características de refletividade dos clusters para cada limiar de rastreio (Figura 8a.) e o desenvolvimento no tamanho dos clusters (em pixels) (Figura 8b.) da Fam com UID igual a 182. Demais informações sobre as colunas presentes no DataFrame podem ser encontradas no ANEXO 1. no final deste trabalho.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "30ad9d5f", "metadata": {}, "outputs": [], "source": [ "## Lista dos UIDs\n", "LIST_UIDS = [182]\n", "\n", "## Filtro no DataFrame.\n", "query = track_frame.query('uid == @LIST_UIDS')\n", "\n", "\n", "## Colunas analisadas e nome do eixo y na Figura 8a.\n", "ANALIZE_a = ['mean_ref_20','mean_total_ref_35','mean_total_ref_40']\n", "AXIS_NAME_a = 'dBZ'\n", "\n", "## Colunas analisadas e nome do eixo y na Figura 8b.\n", "ANALIZE_b = ['size_20','total_size_35','total_size_40']\n", "AXIS_NAME_b = 'Tamanho (pixels)'\n", "\n", "## Função plot_lines da Figura 8a.\n", "sta.plot_lines(track_frame.query('uid == @LIST_UIDS'), analyze_columns=ANALIZE_a, axis_name=AXIS_NAME_a)\n", "\n", "## Função plot_lines da Figura 8b.\n", "sta.plot_lines(track_frame.query('uid == @LIST_UIDS'), analyze_columns=ANALIZE_b, axis_name=AXIS_NAME_b)" ] }, { "cell_type": "markdown", "id": "b6ee02cd", "metadata": {}, "source": [ "### Estudo de caso\n", "\n", "O arquivo utilizado para análise corresponde eventos que ocorreram entre os dias 2014-09-07 00:00:00 e 2014-09-09 23:48:00 na área de cobertura do radar SIPAM-MANAUS." ] }, { "cell_type": "code", "execution_count": null, "id": "b8564f0b", "metadata": {}, "outputs": [], "source": [ "print('Eventos rastreados entre: ',track_frame['timestamp'].min(),' e ',track_frame['timestamp'].max())" ] }, { "cell_type": "markdown", "id": "36f5bcb0", "metadata": {}, "source": [ "Agrupando o grupo de clusters que apresentaram apenas continuidade no seu movimento temos o seguinte caso, para refletividade média." ] }, { "cell_type": "code", "execution_count": null, "id": "a30f5427", "metadata": {}, "outputs": [], "source": [ "### Eventos Continous\n", "sta.plot_lines(cont_events, analyze_columns=['mean_ref_20','mean_total_ref_35','mean_total_ref_40'], axis_name='Reflectivity (dBZ)')" ] }, { "cell_type": "markdown", "id": "3e2178c7", "metadata": {}, "source": [ "Observando a rosa dos ventos que retrata a direção e intensidade de propagação dos sistemas, é possível observar na imagem abaixo que os sistemas deslocaram-se preferêncialmente na direção 180°, o que corresponde aos padrões de propagação para região de cobertura do Radar SIPAM-MANAUS." ] }, { "cell_type": "code", "execution_count": null, "id": "f4b49735", "metadata": {}, "outputs": [], "source": [ "wind_frame_cont = sta.plot_wind(cont_events, style = 'scatter')" ] }, { "cell_type": "code", "execution_count": null, "id": "98b6a747", "metadata": {}, "outputs": [], "source": [ "wind_frame_cont" ] }, { "cell_type": "code", "execution_count": null, "id": "7ced83f7", "metadata": {}, "outputs": [], "source": [ "### Eventos Split\n", "sta.plot_lines(splt_events,analyze_columns=['mean_ref_20','mean_total_ref_35','mean_total_ref_40'], axis_name='Reflectivity (dBZ)')" ] }, { "cell_type": "code", "execution_count": null, "id": "b9b26904", "metadata": {}, "outputs": [], "source": [ "### Eventos Split\n", "sta.plot_lines(merg_events,analyze_columns=['mean_ref_20','mean_total_ref_35','mean_total_ref_40'], axis_name='Reflectivity (dBZ)')" ] }, { "cell_type": "markdown", "id": "a357475a", "metadata": {}, "source": [ "### 4. Considerações finais\n", "<a id='conclusao'></a> " ] }, { "cell_type": "markdown", "id": "30a70295", "metadata": {}, "source": [ "### Referêncial\n", "<a id='conclusao'></a> \n", "\n", "\n", "[1] JACOB, Daniel J. Introduction to atmospheric chemistry. Princeton University Press, 1999.<br>\n", "[2] HOUZE JR, Robert A. Structures of atmospheric precipitation systems: A global survey. Radio Science, v. 16, n. 5, p. 671-689, 1981.<br>\n", "[3] DE QUEIROZ, Antônio Paulo. Monitoramento e previsão imediata de tempestades severas usando dados de radar. 2009.<br>\n", "[4] LEAL NETO, Helvécio. B. Rastreio e previsão de sistemas precipitantes e convectivos na bacia Amazônica utilizando aprendizado de máquina não-supervisionado. 144 p. IBI: <8JMKD3MGP3W34R/44HGF8E>. Dissertação (Mestrado em Computação Aplicada) - Instituto Nacional de Pesquisas Espaciais (INPE), São José dos Campos, 2021. Disponível em: <http://urlib.net/rep/8JMKD3MGP3W34R/44HGF8E>. Acesso em: 20 ago. 2021. <br>\n", "[5] RUSSELL, Christopher T. Geophysical coordinate transformations. Cosmic Electrodynamics, v. 2, n. 2, p. 184-196, 1971.<br>\n", "[6] FERREIRA, Karine R.; CAMARA, Gilberto; MONTEIRO, Antônio M. V. An algebra for spatiotemporal data: From observations to events. Transactions in GIS, v. 18, n. 2, p. 253-269, 2014.<br>\n", "[7] MACHADO, L. A. T. et al. Life cycle variations of mesoscale convective systems over the Americas. Monthly Weather Review, v. 126, n. 6, p. 1630-1654, 1998.<br>\n", "[8] MACHADO, Luiz A. T.; LAURENT, Henri. The convective system area expansion over Amazonia and its relationships with convective system life duration and high-level wind divergence. Monthly weather review, v. 132, n. 3, p. 714-725, 2004.<br>\n", "[9] ANSELMO, Evandro M. et al. Amazonian mesoscale convective systems: Life cycle and propagation characteristics. International Journal of Climatology, 2021.<br>\n", "[10] MACHADO, Luiz A.T. et al. Overview: Precipitation characteristics and sensitivities to environmental conditions during GoAmazon2014/5 and ACRIDICON-CHUVA. Atmospheric Chemistry and Physics, v. 18, n. 9, p. 6461-6482, 2018.<br>\n", "[11] EICHHOLZ, Christiano. W. Análise cinemática e dinâmica da propagação de células de chuva e aglomerados de nuvens. 2017. 157 p. IBI: <8JMKD3MGP3W34P/3NQ5D2P>. (sid.inpe.br/mtc-m21b/2017/04.28.15.17-TDI). Tese (Doutorado em Meteorologia) - Instituto Nacional de Pesquisas Espaciais (INPE), São José dos Campos, 2017. Disponível em: <http://urlib.net/rep/8JMKD3MGP3W34P/3NQ5D2P>.<br>\n", "[12] CRUTCHER, Harold L. On the standard vector-deviation wind rose. Journal of Atmospheric Sciences, v. 14, n. 1, p. 28-33, 1957.\n" ] }, { "cell_type": "markdown", "id": "0037b10a", "metadata": {}, "source": [ "### ANEXO 1. \n", "<a id='anexo'></a> \n", "\n", "<b>Variável -> Especificação\n", "\n", "<b>Fam_N -></b> Refere-se ao número da Família rastreada.<br>\n", "<b>timestamp -></b> Um registro da hora de ocorrência de um determinado evento.<br>\n", "<b>time -></b> Refere-se ao tempo de rastreio no algoritmo.<br>\n", "<b>uid -></b> Identificador único, é usado para gerar as famílias.<br>\n", "<b>id_t -></b> Identificador de cluster de referência no momento da ocorrência de rastreamento. Do algoritmo de armazenamento em cluster DBSCAN.<br>\n", "<b>lat -></b> Refere-se ao centróide de latitude, obtido da matriz de referência dos arquivos nc originais.<br>\n", "<b>lon -></b> Refere-se ao centróide da longitude, obtido da matriz de referência dos arquivos nc originais.<br>\n", "<b>p0 -></b> O primeiro ponto de coordenada do centróide na matriz (clusters ou nc_file): (p0, p1) = (x, y) = (lon, lat).<br>\n", "<b>p1 -></b> O segundo ponto de coordenada do centróide na matriz (clusters ou nc_file): (p0, p1) = (x, y) = (lon, lat).<br>\n", "<b>size_%THRESHOLD -></b> Número total de pixels no cluster principal. Cada ponto depende da resolução espacial do sensor (tamanho do pixel): RADAR 2x2km.<br>\n", "<b>mean_ref_%THRESHOLD -></b> Refletividade média do cluster. Valor em dBZ.<br>\n", "<b>max_ref_%THRESHOLD -></b> Refletividade máxima do cluster. Valor em dBZ.<br>\n", "<b>angle_%THRESHOLD_orig -></b> ngulo de deslocamento original do cluster no momento atual.<br>\n", "<b>angle_%THRESHOLD_cor -></b> ngulo de deslocamento corrigido do cluster no momento atual.<br>\n", "<b>vel_%THRESHOLD_orig -></b> Velocidade de deslocamento original do aglomerado no tempo atual em quilômetros por hora (km / h).<br>\n", "<b>vel_%THRESHOLD_cor -></b> Velocidade de deslocamento corrigida do aglomerado no tempo atual em quilômetros por hora (km / h).<br>\n", "<b>mean_total_ref_%THRESHOLD -></b> Refletividade média dos clusters internos por limite (valor em dBZ).<br>\n", "<b>total_size_%THRESHOLD -></b> Tamanho total dos clusters internos por limite (número de pixels).<br>\n", "<b>n_cluster_%THRESHOLD -></b> Número total de clusters internos por Limite.<br>\n", "<b>avg_angle_%THRESHOLD -></b> ngulo médio para o cluster interno por limite (valor em graus).<br>\n", "<b>avg_vel_%THRESHOLD -></b> Velocidade média para clusters internos por limite (valor em km / h).<br>\n", "<b>status -></b> Estado de ocorrência, tipo: NEW> Novo cluster; CONT-> Cluster contínuo; SPLT -> Cluster dividido; MERG -> Cluster mesclado.<br>\n", "<b>delta_t -></b> Intervalo de tempo para o ciclo de vida do cluster.<br>\n", "<b>nc_file -></b> Caminho do arquivo netCDF.<br>\n", "<b>cluster_file -></b> Caminho do arquivo de cluster (cluster do DBSCAN).<br>\n", "<b>dsize_%THRESHOLD -></b> Diferença entre os tamanhos de dois clusters consecutivos (em Pixel).<br>\n", "<b>dmean_ref_%THRESHOLD -></b> Diferença entre as refletividades médias de dois clusters consecutivos para o limite principal (em dBZ).<br>\n", "<b>dmean_total_ref_%THRESHOLD -></b> Diferença entre as refletividades médias de todos os clusters entre duas vezes consecutivas para um limite interno (em dBZ).<br>\n", "<b>dtotal_size_%THRESHOLD -></b> Diferença entre o tamanho total (em pixel) de todos os clusters entre duas vezes consecutivas para um limite interno (valores em pixel).<br>" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.11" } }, "nbformat": 4, "nbformat_minor": 5 }