{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Múltiplas Visões" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
Quando procuramos informação em um conjunto de dados com muitos campos, nos sentimos tentados a carregar a visualização com o máximo de variáveis visuais possíveis: x, y, color, size, shape, e mais. Entretanto, mapear muita informação ao mesmo tempo torna o gráfico difícil de entender. Utilizando Altair, temos duas opções para evitar isso: utilizar gráficos com múltiplas visões e interação.
\n", "\n", "O conteúdo desse notebook tem as técnicas que diferenciam o Vega-Lite e Altair de programas como Excel e Tableau, com o controle desses aspectos multivisão sobre visualizações.
" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import altair as alt" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "url = \"https://raw.githubusercontent.com/tiagodavi70/vl-altair-tutorial/master/datasets/completo.csv\"\n", "df = pd.read_csv(\"https://raw.githubusercontent.com/tiagodavi70/vl-altair-tutorial/master/datasets/dados.csv\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Altair apresenta algumas funções para combinar gráficos e apresentar os dados em visões múltiplas e coordenadas.Uma única visão complexa pode ser cognitivamente incompreensível para os usuários. Visões múltiplas podem ajudar na estratégia de \"dividir e conquistar\", reduzindo o volume de dados que são consultados de cada vez. Essas visões evem ser usadas com cuidado para evitar mais complexidade, já que cada visão é um contexto novo. Mesmo assim, quando usada de maneira correta pode ter bons resultados, já que a visão sempre é melhor que a memória para comparações, e visões múltiplas podem potencializar a percepção entre a relação dos dados.
\n", "\n", "Agora vamos ver algumas funções para manipular visões em Altair." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Camadas" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Um jeito comum de combinar gráficos é sobrepor as variáveis visuais uma em cima da outra. Se os domínios forem iguais os eixos podem ser compartilhados, senão dá separar os eixos.
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vamos comparar os valores de Temperatura de ponto de orvalho entre duas cidades." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.Chart(...)" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "alt.Chart(url, title=\"Comparação de ponto de orvalho de Capitão Poço e Altamira\").mark_area(opacity=.45).transform_filter(\n", " \"(datum.Cidade == 'Capitão Poço' || datum.Cidade == 'Altamira')\"\n", ").encode(\n", " alt.X(\"hours(Data):T\",title=\"Hora no dia\"),\n", " alt.Y(\"average(Temperatura orvalho máxima):Q\",scale=alt.Scale(zero=False),title=\"Média dos pontos de orvalho °C\"),\n", " alt.Y2(\"average(Temperatura orvalho mínima):Q\"),\n", " alt.Color(\"Cidade:N\")\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Temos a distribuição pela área e agora vamos fazer um gráfico com o ponto médio." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.Chart(...)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "alt.Chart(url, title=\"Comparação de temperatura de Capitão Poço e Altamira\").mark_line(opacity=.85).transform_filter(\n", " \"(datum.Cidade == 'Capitão Poço' || datum.Cidade == 'Altamira')\"\n", ").transform_calculate(\n", " temp_mid=\"(+datum['Temperatura orvalho máxima'] + +datum['Temperatura orvalho mínima']) / 2\"\n", ").encode(\n", " alt.X(\"hours(Data):T\",title=\"Hora no dia\"),\n", " alt.Y(\"average(temp_mid):Q\",scale=alt.Scale(zero=False), title=\"Ponto médio °C\"),\n", " alt.Color(\"Cidade:N\")\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Agora que temos os dois gráficos separados, podemos sobrepor as áreas e linhas no mesmo gráfico para comparar mínimo, máximo e ponto médio. Para isso podemos usar o operador `+` salvando os dois gráficos." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.LayerChart(...)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "orvalhoMinMax = alt.Chart(url, title=\"Comparação de ponto de orvalho de Capitão Poço e Altamira\").mark_area(opacity=.45).transform_filter(\n", " \"(datum.Cidade == 'Capitão Poço' || datum.Cidade == 'Altamira')\"\n", ").encode(\n", " alt.X(\"hours(Data):T\",title=\"Hora no dia\"),\n", " alt.Y(\"average(Temperatura orvalho máxima):Q\",scale=alt.Scale(zero=False),title=\"Média dos pontos de orvalho °C\"),\n", " alt.Y2(\"average(Temperatura orvalho mínima):Q\"),\n", " alt.Color(\"Cidade:N\")\n", ")\n", "\n", "orvalhoMid = alt.Chart(url).mark_line(opacity=.85).transform_filter(\n", " \"(datum.Cidade == 'Capitão Poço' || datum.Cidade == 'Altamira')\"\n", ").transform_calculate(\n", " temp_mid=\"(+datum['Temperatura orvalho máxima'] + +datum['Temperatura orvalho mínima']) / 2\"\n", ").encode(\n", " alt.X(\"hours(Data):T\",title=\"Hora no dia\"),\n", " alt.Y(\"average(temp_mid):Q\",scale=alt.Scale(zero=False)),\n", " alt.Color(\"Cidade:N\")\n", ")\n", "\n", "orvalhoMinMax + orvalhoMid" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "O operador `+` é um atalho para a função `alt.layer`. Quando tem somente um título de eixo ele é o único que é apresentado no gráfico. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "_Os pontos de orvalho entre 9:00 e 18:00 tem uma área grande, e com o ponto médio dá pra acompanhar a tendência._" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vamos comparar agora os valores de temperatura de orvalho com a umidade do ar." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.LayerChart(...)" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "orvalhoMinMax = alt.Chart(url, title=\"Comparação de ponto de orvalho e Umidade\").mark_area(opacity=.45).encode(\n", " alt.X(\"hours(Data):T\",title=\"Hora no dia\"),\n", " alt.Y(\"average(Temperatura orvalho máxima):Q\",scale=alt.Scale(zero=False),title=\"Média do pontos de orvalho °C\"),\n", " alt.Y2(\"average(Temperatura orvalho mínima):Q\")\n", ")\n", "\n", "umidade = alt.Chart(url).mark_line(opacity=.85).encode(\n", " alt.X(\"hours(Data):T\",title=\"Hora no dia\"),\n", " alt.Y(\"average(Umidade Relativa do Ar):Q\", scale=alt.Scale(zero=False))\n", ")\n", "\n", "orvalhoMinMax + umidade" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Compartilhar o mesmo eixo não deu nada certo, então vamos separar os eixos." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.LayerChart(...)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "orvalhoMinMax = alt.Chart(url, title=\"Comparação do Ponto de Orvalho\").mark_area(opacity=.45).encode(\n", " alt.X(\"hours(Data):T\",title=None),\n", " alt.Y(\"average(Temperatura orvalho máxima):Q\",scale=alt.Scale(zero=False),title=\"Média do Ponto de orvalho °C\"),\n", " alt.Y2(\"average(Temperatura orvalho mínima):Q\")\n", ")\n", "\n", "umidade = alt.Chart(url).mark_line(\n", " interpolate='monotone',\n", " opacity=.85,\n", " color=\"#333333\"\n", ").transform_calculate(\n", " perc_umid=\"+datum['Umidade Relativa do Ar'] / 100\" # criar nova coluna para as etiquetas\n", ").encode(\n", " alt.X(\"hours(Data):T\"),\n", " alt.Y(\"average(perc_umid):Q\",\n", " scale=alt.Scale(zero=False),\n", " axis=alt.Axis(format=\"%\"),\n", " title=\"Umidade Relativa do Ar %\")\n", ")\n", "\n", "alt.layer(orvalhoMinMax, umidade).resolve_scale(y='independent')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "_Quando a umidade é alta, o ponto de orvalho varia pouco e quando a umidade é baixa tem muita variação de ponto de carvalho._" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Esse tipo de eixo deve ser usado com cuidado, já que é fácil confundir uma escala com outra." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Facetas" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nós já vimos alguns aspectos de uma faceta, usando os parâmetros row e column. Uma faceta é uma subdivisão de um conjunto de dados em grupos e cria um gráfico novo para cada um deles. Vamos mostrar também um operador `facet` mais genérico.
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vamos começar com um histograma da temperatura do ar." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.Chart(...)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "alt.Chart(df).mark_bar().encode(\n", " alt.X(\"Temperatura do ar - bulbo seco:Q\",bin=alt.Bin(maxbins=20)),\n", " alt.Y(\"count()\")\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "E agora vamos separar por estação, criando um novo campo marcando o verão e o inverno." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.Chart(...)" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "alt.Chart(url).mark_bar(\n", ").transform_filter(\n", " \"year(datum['Data']) == 2019\"\n", ").transform_calculate(\n", " estação=\"month(datum.Data) < 5 || month(datum.Data) == 11 ? 'Chove muito':'Chove pouco'\"\n", ").encode(\n", " alt.X(\"Temperatura do ar - bulbo seco:Q\",bin=alt.Bin(maxbins=12),title=None),\n", " alt.Y(\"count()\", title=\"Contagem de Registros\"),\n", " alt.Column(\"month(Data):T\",title=None),\n", " alt.Color(\"estação:N\",\n", " scale=alt.Scale(range=[\"DeepSkyBlue\",\"Brown\"]),\n", " title=\"Estação\")\n", ").properties(width=40, height=100)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "_Conseguimos ver os meses de março e setembro se destacando no inverno e verão com temperaturas baixas e altas, respectivamente._" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Utilizando o nosso exemplo do orvalho anterior, podemos usar o operador `facet` para mostrar as cidades lado a lado." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.FacetChart(...)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "orvalhoMinMax = alt.Chart(title=\"Comparação de ponto de orvalho de Altamira\").mark_area(opacity=.45).transform_filter(\n", " \"(datum.Cidade == 'Capitão Poço' || datum.Cidade == 'Altamira')\"\n", ").encode(\n", " alt.X(\"hours(Data):T\",title=\"Hora no dia\"),\n", " alt.Y(\"average(Temperatura orvalho máxima):Q\",scale=alt.Scale(zero=False),title=\"Média dos pontos de orvalho °C\"),\n", " alt.Y2(\"average(Temperatura orvalho mínima):Q\"),\n", " alt.Color(\"Cidade:N\")\n", ")\n", "\n", "orvalhoMid = alt.Chart(title=\"Comparação de ponto de orvalho de Capitão Poço\").mark_line(opacity=.85).transform_filter(\n", " \"(datum.Cidade == 'Capitão Poço' || datum.Cidade == 'Altamira')\"\n", ").transform_calculate(\n", " temp_mid=\"(+datum['Temperatura orvalho máxima'] + +datum['Temperatura orvalho mínima']) / 2\"\n", ").encode(\n", " alt.X(\"hours(Data):T\",title=\"Hora no dia\"),\n", " alt.Y(\"average(temp_mid):Q\",scale=alt.Scale(zero=False)),\n", " alt.Color(\"Cidade:N\")\n", ")\n", "\n", "alt.layer(orvalhoMinMax, orvalhoMid).facet(\n", " data=url,\n", " column='Cidade:N'\n", ").resolve_axis(y='independent')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alteramos também a função `layer`, tirando os dados de ambos os gráficos, e definimos somente na fusão, assim como o parâmetro `column`. Também mudamos o eixo para que cada gráfico tenho o seu." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "_Troque `resolve_axis(y='independent')` por `resolve_scale(y='independent')` e veja a direfença. Talvez não seja uma boa ideia trocar quando as escalas são tão próximas, mas em outros casos talvez ajude a análise._" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Concatenar" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As funções que vimos até agora são visões múltiplas muito parecidas com _small multiples_, baseados no mesmo conjunto de dados. Usando concatenação podemos misturar gráficos com dados diferentes.\n", "\n", "O operador `hconcat` (atalho `|` ) concatena horizontalmente e o operador `vconcat` (atalho `&`) concatena verticalmente." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vamos começar comparando rajada de vento de duas cidades durante o ano." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.Chart(...)" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "alt.Chart(df).mark_line(opacity=.85).transform_filter(\n", " \"(datum.Cidade == 'Castanhal' || datum.Cidade == 'Altamira')\"\n", ").encode(\n", " alt.X(\"month(Data):T\"),\n", " alt.Y(\"average(Rajada Máxima de Vento):Q\"),\n", " alt.Color(\"Cidade:N\")\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Além da rajada de vento, vamos ver também temperatura e a pressão atmosférica. Vamos criar um gráfico base e mudar somente a variável visual para cada gráfico, depois concatenar. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.HConcatChart(...)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "base = alt.Chart(df).mark_line(opacity=.85).transform_filter(\n", " \"(datum.Cidade == 'Castanhal' || datum.Cidade == 'Altamira')\"\n", ").encode(\n", " alt.X(\"month(Data):T\"), # sem encode do Y\n", " alt.Color(\"Cidade:N\")\n", ").properties(width=240, height=180)\n", "\n", "vento = base.encode(alt.Y(\"average(Rajada Máxima de Vento):Q\", scale=alt.Scale(zero=False)))\n", "temp = base.encode(alt.Y(\"average(Temperatura do ar - bulbo seco):Q\", scale=alt.Scale(zero=False)))\n", "press = base.encode(alt.Y(\"average(Pressão Atmosférica ao nível da estação):Q\", scale=alt.Scale(zero=False)))\n", "\n", "vento | temp | press" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Podemos até combinar os operadores." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.VConcatChart(...)" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(vento | temp) & press.properties(width=540)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Repetição" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "No caso de pequenas mudanças nos gráficos, podemos usar a repetição. A repetição permite usar uma especificação _template_ e preenche com os campos que escolhermos." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.RepeatChart(...)" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "alt.Chart(df).mark_line().transform_filter(\n", " \"(datum.Cidade == 'Castanhal' || datum.Cidade == 'Altamira')\"\n", ").encode(\n", " alt.X(\"month(Data):T\"),\n", " alt.Y(alt.repeat(\"column\"), aggregate='average', type='quantitative', scale=alt.Scale(zero=False)),\n", " alt.Color(\"Cidade:N\")\n", ").properties(\n", " width=240, \n", " height=180\n", ").repeat(\n", " column=[\"Rajada Máxima de Vento\", \"Velocidade Horária do Vento\", \"Temperatura do ponto de orvalho\"]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Com essa estrutura podemos criar uma matriz de scatterplot. Vamos usar alguns atributos do conjunto de dados." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "colunas = [\"Precipitação\", \"Temperatura mínima\", \"Umidade Relativa do Ar\"]\n", "linhas = colunas[::-1]" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.RepeatChart(...)" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "splom = alt.Chart(df).mark_circle().encode(\n", " alt.X(alt.repeat(\"column\"), type='quantitative', scale=alt.Scale(zero=False)),\n", " alt.Y(alt.repeat(\"row\"), type='quantitative', scale=alt.Scale(zero=False)),\n", " tooltip=[alt.Tooltip(\"day(Data)\"), alt.Tooltip(\"Cidade\")]\n", ").properties(\n", " width=120,\n", " height=120\n", ").repeat(\n", " column=colunas,\n", " row=linhas\n", ")\n", "\n", "splom" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Por fim, podemos agregar tudo que fizemos até aqui em um dashboard. " ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "" ], "text/plain": [ "alt.VConcatChart(...)" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "barras = alt.Chart().mark_bar().encode(\n", " alt.X(alt.repeat(\"row\"), type='quantitative', bin=True, title=\"Histograma com média\"),\n", " alt.Y(\"count()\",title=None)\n", ")\n", "\n", "regua = alt.Chart().mark_rule(color=\"firebrick\").encode(\n", " alt.X(alt.repeat(\"row\"), aggregate='average', type='quantitative')\n", ")\n", "\n", "hist = alt.layer(barras, regua, data=df).properties(\n", " width=120,\n", " height=120\n", ").repeat(\n", " row=linhas\n", ")\n", "\n", "tempo = alt.Chart(df).mark_line().encode(\n", " alt.X(\"month(Data):T\"),\n", " alt.Y(alt.repeat(\"column\"), aggregate='average', type='quantitative',scale=alt.Scale(zero=False))\n", ").properties(\n", " width=130,\n", " height=120\n", ").repeat(\n", " column=colunas\n", ")\n", "\n", "(splom | hist) & tempo" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Agora temos meios de ver várias dimensões dos dados sem carregar as váriaveis visuais, recuperando e mostrando os valores para uma análise visual. Ainda sim, falta um aspecto interativo nos nossos gráficos, e veremos no próximo notebook.
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercícios" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "1. Crie um _small multiples_ com a temperatura máxima pela hora, com a faceta sendo as cidades.\n", "\n", "1. Crie um _small multiples_ com precipitação de cada cidade, facetando o mês.\n", "\n", "1. Crie um gráfico de linhas que também tenha pontos.\n", "\n", "1. Crie um gráfico de barras horizontais com o texto ao lado das barras.\n", "\n", "1. Crie dois heatmaps lado a lado, um para dia da semana e outro para o mês. Devem mapear a média de um atributo númerico por cidade e o mês.\n", "\n", "1. Crie um scatterplot e em cima e do lado direito ficam os histogramas alinhados das dimensões selecionadas.\n", "\n", "1. Crie um dashboard com 4 tipos de gráficos diferentes.\n", "\n", "1. Crie um scatterplot de duas camadas, com os pontos na frente e um histograma em forma de heatmap atrás. Ajuste os atributos, as cores e a opacidade de acordo.\n", "\n", "1. Use o scatterplot do exercício anterior e amplie para usar em cada cidade.\n", "\n", "1. Escolha um atributo númerico e apresenta os valores da média pelo mês em um gráfico de linha. Sobreponha esse gráfico com os pontos de cada medição.\n", "\n", "1. Crie um gráfico lollipop ([exemplo](https://datavizproject.com/wp-content/uploads/2017/09/DVP_101_200-64.png)) .\n", "\n", "1. Crie um gráfico de horizonte (_horizon chart_ - [exemplo](https://camo.githubusercontent.com/c0437ff0743c97b3d933be35fecd4737284f5a8c/68747470733a2f2f76617374757269616e6f2e6769746875622e696f2f686f72697a6f6e2d74696d657365726965732d63686172742f6578616d706c652f62617369632f73637265656e73686f742e706e67)).\n", "\n", "1. Crie um gráfico de pirâmide ([exemplo](https://upload.wikimedia.org/wikipedia/commons/a/a7/Population_pyramid_of_South_Korea_2015.png)) com os histogramas de um atributo comparando duas cidades." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.8.0" } }, "nbformat": 4, "nbformat_minor": 4 }