Salida#
Fig. 35 Ilustración realizada por Bernardo Dinamarca.#
¡Hola! Este notebook estará basado en recopilar, a través de la Visualización de Datos, las cifras históricas y actuales de la pandemia en Tarapacá. Aún no he indagado cómo hacer que se actualice de forma automática, pero eventualmente, ¡lo lograremos!
Importando paquetes#
Trabajaremos con las librerías del primer notebook y otras. Respecto a las librerías para visualización, no utilizaremos Matplotlib o Seaborn, dado que deseo realizar gráficas interactivas que sean visualmente limpias y atractivas. Además, incorporaremos al equipo Beautiful Soup, que es una librería para manipular lenguaje HTML.
Todo ésto, será en fin de generar dos salidas:
Un reporte para el libro.
Una página externa para incrustarse en el sitio de la Universidad Arturo Prat.
Anteriormente, trabajaba con Infogram, la cual es la misma plataforma que utiliza el Gobierno de Chile en su página de Cifras Oficiales. Sin embargo, la plataforma carece de automatización gratuita, y se debe pagar para lograr esa automatización.
En ese sentido, exportaremos una página HTML, para poder incrustarse en el sitio de la Universidad Arturo Prat.
Respecto a nuestro equipo de librerías habitual, sumamos:
Plot.ly (librería de visualización dinámica e interactiva a partir de JavaScript).
Beautiful Soup (librería de Python para extraer datos de archivos HTML y XML).
Fig. 37 Logo de la librería Beautiful Soup.#
# Importando paquetes
### Librería de manipulación de datos
import pandas as pd
pd.set_option('use_inf_as_na', True)
pd.options.display.float_format = '{:,.2f}'.format
### Librería de álgebra
import numpy as np
### Librerías para graficar
import plotly.graph_objects as go
### Customizamos opciones de Plot.ly
config = {'displayModeBar': False, 'showTips': False, 'scrollZoom': False}
layout = go.Layout(dragmode=False, font=dict(color='white'),
xaxis=dict(
showline=True,
showgrid=True,
showticklabels=True,
linecolor='rgb(204, 204, 204)',
linewidth=2,
ticks='outside',
tickfont=dict(
family='Arial',
size=12,
color='rgb(82, 82, 82)',
),
),
yaxis=dict(
showgrid=True,
showticklabels=True,
),
autosize=True,
margin=dict(
autoexpand=True,
l=0,
r=0,
t=0,
),
showlegend=True,
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
legend=dict(
itemclick="toggleothers",
itemdoubleclick="toggle",
orientation="h",
yanchor="bottom",
xanchor='right',
x=1,
y=1.02
)
)
### Librería BeautifulSoup para manipular HTML
from bs4 import BeautifulSoup
### Para formato local
import locale
### Según Windows o Ubuntu
try:
### Windows
locale.setlocale(locale.LC_ALL, 'esp')
except Exception:
### Ubuntu (action)
locale.setlocale(locale.LC_ALL, 'es_CL.UTF-8')
### Otros paquetes
import math
import os, os.path
import json as json
import datetime
import time
from IPython.display import display, Markdown, HTML, Javascript, IFrame
### Gracias a joelostblom (https://gitlab.com/joelostblom/session_info)
import session_info
Importando datos#
Hacemos el mismo proceso habitual, con la diferencia que importaremos datos ya procesados.
### Gracias a Daniel Stutzbach y Bruno Bronosky (stackoverflow.com/a/2632251/13746427) ###
sum_ = 0
for string in [name for name in os.listdir('../../out/site/csv/')]:
sum_ += string.count('data')
## Cargamos cada uno de los csv (basado en el primer notebook)
x = range(1, sum_+1)
data = []
for i in x:
exec('data += [pd.read_csv("../../out/site/csv/data{}.csv", parse_dates=["Fecha"], index_col=["Fecha"])]'.format(i, i))
Tendencia#
La diferencia de nuestras publicaciones (en contraste a la forma en que se entregaba información en otras páginas informativas en Instagram) se basaban en la visualización de datos, y en particular, en las tendencias semanales. También, se agregaban cálculos o datos que, en los reportes del Minsal, no se encontraban procesados.
Imaginemos que, lo único que hiciéramos fuese entregar cifras aisladas. ¿Qué retratan? ¿De dónde venimos? No es correcto que, por un día, debamos ser positivistas o negativistas. De hecho, esos pensamientos deparan en sesgos, y lo peor, en sesgos que no contemplan la integralidad de la pandemia. Por eso es relevante formar una mirada holística en la visualización de los datos. Las cifras de un día \(X\) pueden ser sumamente variables al día \(X+1\) en función de depender de la muestra (cantidad de exámenes PCR informados en el día \(X\) o \(X+1\)). Por ello, es importante regirse por las tendencias, o por los estadísticos.
El problema es que, al ser Instagram, una plataforma homogénea, el storytelling debe ser accesible para todo público (con estudios matemáticos o no). De esa forma, los reportes tenían una alta presencia de tendencias a través de gráficas, y no así de estadísticos (a excepción de medias móviles semanales).
La comprensión de la pandemia debía partir desde el mejor storytelling (el contar una historia detrás de los datos). El tomar una idea, o un incidente, y contarla como una historia: Cada día particular de la pandemia es una hoja de esa historia (una hoja del “libro” COVID-19 en Tarapacá). Por esa razón, desarrollamos el reporte diario no solo con datos duros, sino también con gráficas de tendencia semanal.
Cifras significativas#
Las tablas y gráficos visualizados en la presente sección tienen una a dos cifras significativas. Cualquier sugerencia es bienvenida.
Para los datos, descargar los archivos .CSV procesados. Éstos están disponibles en el propio libro (sección Legado 🔀), o bien, en el repositorio.
¿Cuántos gráficos se exportarán?#
display(Markdown('> Se exportarán un total de **{} gráficos**.'.format(sum_)))
Se exportarán un total de 27 gráficos.
Automatizando salida de gráficos#
A continuación, se desarrolla un código que recorre cada uno de los archivos .CSV que generamos en el primer notebook, del cual, realiza una “especie” de resumen de los estadísticos, visualiza los datos en un gráfico en Plot.ly y culmina con información adicional sobre:
Descarga de los datos (solo funciona al presionar desde el libro publicado, no desde el notebook).
La fecha de inicio y fin del gráfico.
%%capture reportediario
x = 0
### Título y otras cosas
display(Markdown('<h2 style="font-size:60px;">REPORTE DIARIO</h2>'))
display(Markdown('<h3 style="font-size:20px;">Región de Tarapacá, {}</h3>'.format(data[0].last_valid_index().strftime('%d de %B de %Y'))))
### Recorremos el vector que almacena los DataFrames, uno a uno
for dataframe in data:
### Para guardar el número del gráfico (un poco ordinario el método, lo sé)
x += 1
### Definimos una nueva figura
fig = go.Figure(layout=layout)
### Algunos datos y título
display(Markdown('<h3> Gráfico {}</h3>'.format(x)))
display(Markdown('El gráfico contiene las siguientes <b>columnas</b>: '))
### Recorremos cada una de las columnas del DataFrame anterior
for col in dataframe.columns:
### Vector de fechas desde primer y último dato válido por cada columna
index = dataframe[col].first_valid_index()
index_ = dataframe[col].last_valid_index()
### DataFrame según filtro de primer dato válido
_df = dataframe[index:]
### Índice de DataFrame según filtro anterior
fecha = dataframe[index:].index
### Columna específica
_col = dataframe[index:][col]
### Añadimos un trazado por cada columna, conectamos los valores para no tener discontinuidad y
### suavizamos por interpolación spline
fig.add_trace(go.Scatter(x=_df.index.strftime('%d %b %Y'),
y=_col,
mode='lines',
name=col,
connectgaps=True,
line_shape='linear',
hovertemplate =
'<b>{}</b>: '.format(col) + '%{y:.2f}'+'<br><b>Fecha</b>: %{x}<br>' + "<extra></extra>"))
### Para colocar en 35° las etiquetas del eje X, con el número de etiquetas proporcional al número de meses
### desde el primer dato válido
fig.update_layout(xaxis = go.layout.XAxis(tickangle = 90,
nticks=len(_df.index.month.unique())))
### Más datos
display(Markdown(' - <b>{}</b>.'.format(col)))
display(Markdown("""El mayor valor es de <b>{}</b>, registrado el <b>{}</b>.
Asimismo, la mediana es de <b>{}</b>.
Respecto a la dispersión de los datos, la desviación estándar es del <b>{}</b>. """
.format(dataframe[col].max(), dataframe[dataframe[col] == dataframe[col].max()].index[0].strftime('%d de %B de %Y'),
round(dataframe[col].median(), 2), round(dataframe[col].std(), 2))))
display(Markdown('> El valor en base al último reporte diario o epidemiológico ({}) es de <b>{}</b>.'.format(dataframe[index_:].index[0].strftime('%d de %B de %Y'), dataframe[col][index_])))
### Mostramos la figura procesada en el ciclo anterior y otros datos. Añadimos espaciado
display(Markdown('<h4>Visualización del gráfico {}</h4> <br> El gráfico, visualizado en <a href="https://plotly.com/python/">Plot.ly</a>: <br>'.format(x)))
fig.show(config=config)
display(Markdown("""> <b>Notas</b>:
<br> - El gráfico <b>inicia en el {}</b> y <b>termina el {}</b> en base a los datos disponibles.
<br> - Para aislar una curva, presionar en el nombre o color en la leyenda.
<br> - Para remover una curva, seguir instrucción anterior, con la diferencia de presionar dos veces.""".format(\
_df.index[0].strftime('%d de %B de %Y'),
dataframe[index_:].index[0].strftime('%d de %B de %Y'))))
display(Markdown('<h4>Información adicional sobre el gráfico {}</h4> <br>'.format(x)))
display(Markdown(
"""El <b>gráfico {}</b> utilizó los datos procesados en <a href="https://raw.githubusercontent.com/pandemiaventana/pandemiaventana/main/out/site/csv/data{}.csv">data{}.csv</a>.
La tabla de datos resumida:""".format(x, x, x, x)))
display(dataframe)
Automatizando salida para asistenciacovid19#
En razón de brindar una página web que se pueda incrustar en el sitio de la Universidad Arturo Prat, como también en el presente sitio, generamos las siguientes líneas de codigo.
¿Dónde estará la salida?#
La salida estará disponible en “Balance histórico 📊”.
¿Cómo funciona?#
La salida completa de la celda anterior es capturada como lenguaje HTML, pero solo el cuerpo de la página [Ben20].
En este sentido, dar gracias al usuario Benvida, de Stackoverflow [1]. El código original que desarrolló se encuentra a continuación, el cual modificaremos.
%%js
{
let outputs=[...document.querySelectorAll(".cell")].map(
cell=> {
let output=cell.querySelector(".output_text")
if(output) return output.innerText
output=cell.querySelector(".rendered_html")
if(output) return output.innerHTML
return ""
}
)
IPython.notebook.kernel.execute("cell_outputs="+JSON.stringify(outputs))
}
Inconveniente#
En primer lugar, el lenguaje de marcado (HTML) solo brinda la estructura básica del sitio, que se compone de texto, imágenes y scripts. Para que funcionen los scripts, en este caso, Plot.ly, se tienen múltiples dependencias de otras librerías de JavaScript.
JavaScript es un lenguaje de programación de alto nivel, el cual, tal como Python, tiene múltiples librerías que se especializan en distintas tareas y funciones. Plot.ly depende de otras librerías para funcionar, por lo que, para que el script de JavaScript, realizado por los creadores de Plot.ly, funcione, debemos no solo incorporar el código de los gráficos, sino también sus dependencias.
Para ello, incorporamos en el <head> las CDN. Estos son servidores, a partir de los cuales descargamos los archivos que tienen las dependencias.
Cabe recalcar que, además de código JavaScript, también incorporamos lenguaje de hojas de estilo en cascada (CSS), el cual le da formato y estilo al solitario HTML.
En analogía, y para simple comprensión, HTML es el esqueleto (estructura), JS es la musculación (animaciones y movimiento) y CSS es la apariencia externa (piel y atributos físicos).
Algunos arreglos#
La función por BenVida es ejecutada una vez se terminó de ejecutar todas las celdas, y por ende, los outputs no son recopilados hasta que el Kernel culmina su ejecución.
html = open('balance.html','w')
html.write('''<html><head><link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css'><script src='https://cdn.plot.ly/plotly-latest.min.js'></script><script src='https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js'></script></head><body>''' +cell_outputs[9] +'''</body></html>''')
html.close()
Aquí se nos genera el siguiente problema: La variable cell_outputs no es asignada hasta que el Kernel termina su ejecución, por lo que si deséaramos actualizar el archivo HTML, en base a la celda anterior de código (donde están los gráficos Plot.ly), no podríamos, porque la variable cell_outputs todavía no está asignada. ¿La solución? Ejecutar el código Python tras la ejecución del código JavaScript, y de esta forma, no tenemos inconvenientes. Para ello, utilizaremos la misma función que utilizó BenVida, IPython.notebook.kernel.execute("") donde en " " debe ir el código Python.
Por otro lado, al ser código JavaScript con Python, ¡se debe tener cuidado al utilizar comillas sobre doble comillas o viceversa! De otra forma, tendremos un error. Por esta razón, utilizamos la función format() de Python (ya que las URL deben ir entre comillas, le decimos a Python que se encargue de esa situación, sin que el input de JavaScript nos resulte en error).
Detalles#
El sitio se encuentra en el directorio ./out/site. Respecto al funcionamiento del código para procesarlo:
Modificamos al función para no solamente obtener el texto, sino que todo el HTML.
Para no desperdiciar los estilos CSS que utiliza Jupyter Notebooks, copié los mismos archivos CSS en la carpeta del sitio (
ipython.min.cssystyle.min.css). Los archivos se pueden encontrar en el directorio de Anaconda,usuario/anaconda3/Lib/site-packages/notebook/static/style.Además, en el mismo directorio se encuentra el archivo JavaScript,
plotly.js, que debe estar en el directorio para que funcionen los gráficos interactivos.Se remueven los
<div>con claseprompt, que es lo que brinda el margen a la izquierda en Jupyter Notebooks.
IPython.notebook.kernel.execute("removals = soup.find_all(attrs={'class': 'prompt'})")
IPython.notebook.kernel.execute("for removal in removals: removal.decompose()")
IPython.notebook.kernel.execute("soup = str(soup)")
Nuevo inconveniente#
Al automatizar el código en GitHub, logré apreciar que el código Python a través de JavaScript no es ejecutado. Ésto produce que los archivos no se actualicen.
Por ello, tuve que aproximar otra solución.
Con el comando mágico de Jupyter Notebook,
%%capture, capturamos la salida de una celda específica.
En este caso puntual, almacenamos la salida en la variable reportediario con el comando mágico %%capture. Esa salida debemos convertirla a texto, o string en inglés, el problema es que, al ser un output de una celda, posee algunos textos que no deseamos:
El tipo de variable que se está imprimiendo.
Configuración de Plot.ly en diccionario.
Datos de tabla sin formato.
En las siguientes líneas de código:
Limpieza de textos.
Damos formato de documento HTML.
Añadimos Bootstrap [2] para que el documento HTML no quede plano, añadiendo código desde sus Docs [Boo20].
Entre otros.
outputs_ = reportediario.outputs
vec_out = []
for outs_ in outputs_:
### Convertimos a lista los outputs
vec_out += list(outs_.data.values())
### Nuestro string
vec_ = ''
### Recorremos la lista de outputs
for vec in vec_out:
vec = str(vec)
### Quitamos configuración de Plot.ly
if vec.startswith("{'data':"):
pass
else:
### Quitamos el str de datos de tabla sin formato
if vec.startswith(" "):
pass
else:
### Quitamos el str del tipo de variable
vec = vec.replace('<IPython.core.display.Markdown object>', '')
### Damos formato a tablas
if vec.startswith('<div>\n<style scoped>'):
vec = vec.replace('NaN', 'Sin datos')
vec = BeautifulSoup(vec, 'html.parser').find('table')
vec['class'] = vec.get('class', []) + [' table table-dark table-striped table-hover table-sm']
vec = '<div class="table-responsive">' + str(vec) + '</div>'
### Finalmente, añadimos al vector
vec_ += '<div class="row"><br><div class="col text-light">' + vec + '</div></div>'
### Abrimos y modificamos el HTML
with open('../../_build/html/dinamic/balance.html', 'w', encoding='UTF-8') as f:
f.write('''<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous">
</script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous">
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.plot.ly/plotly-latest.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js">
</script>
<link rel="icon" href="./favicon.ico" type="image/x-icon"/>
<meta charset="UTF-8">
<title>Balance histórico 📊 — La pandemia por la ventana</title>
</head>
<body>
<nav class="navbar nnavbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="https://pandemiaventana.github.io/pandemiaventana/">
<img src="./logo.png" width="35" height="35" class="d-inline-block align-top" alt="">
Numeral.lab
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-item nav-link active" href="#">Reporte diario <span class="sr-only">(current)</span></a>
<a class="nav-item nav-link" href="https://pandemiaventana.github.io/pandemiaventana/dinamic/indicadorfase.html">Indicador de fase</a>
<a class="nav-item nav-link" href="https://pandemiaventana.github.io/pandemiaventana/">La pandemia por la ventana</a>
</div>
</div>
</nav>
<div class="container-fluid bg-dark text-light">'''
+ vec_ +
'''</div>
</body>
</html>''')
Información de sesión#
session_info.show(cpu=True, jupyter=True, std_lib=True, write_req_file=True, dependencies=True, req_file_name='3_requeriments.txt')
Click to view session information
-----
bs4 4.12.2
datetime NA
json 2.0.9
locale NA
math NA
numpy 1.20.3
os NA
pandas 1.3.4
plotly 5.5.0
session_info 1.0.0
time NA
-----
Click to view modules imported as dependencies
PIL 8.4.0
abc NA
argparse 1.1
array NA
ast NA
asyncio NA
atexit NA
attr 23.1.0
backcall 0.2.0
base64 NA
bdb NA
binascii NA
bisect NA
bz2 NA
cProfile NA
calendar NA
charset_normalizer 2.0.12
cmath NA
cmd NA
code NA
codecs NA
codeop NA
collections NA
colorsys NA
concurrent NA
configparser NA
contextlib NA
contextvars NA
copy NA
copyreg NA
csv 1.0
ctypes 1.1.0
curses NA
cython_runtime NA
dataclasses NA
dateutil 2.8.2
debugpy 1.6.7
decimal 1.70
decorator 5.1.1
difflib NA
dis NA
distutils 3.7.16
email NA
encodings NA
entrypoints 0.4
enum NA
errno NA
fastjsonschema NA
faulthandler NA
fcntl NA
filecmp NA
fnmatch NA
fractions NA
functools NA
gc NA
genericpath NA
getopt NA
getpass NA
gettext NA
glob NA
grp NA
gzip NA
hashlib NA
heapq NA
hmac NA
html NA
http NA
idna 3.4
importlib NA
importlib_metadata NA
importlib_resources NA
inspect NA
io NA
ipaddress 1.0
ipykernel 6.16.2
itertools NA
jedi 0.18.2
jsonschema 4.17.3
keyword NA
linecache NA
logging 0.5.1.2
lzma NA
marshal 4
mimetypes NA
mmap NA
mpl_toolkits NA
multiprocessing NA
nbformat 5.8.0
ntpath NA
numbers NA
opcode NA
operator NA
packaging 23.1
parso 0.8.3
pathlib NA
pdb NA
pexpect 4.8.0
pickle NA
pickleshare 0.7.5
pkg_resources NA
pkgutil NA
platform 1.0.8
plistlib NA
posix NA
posixpath NA
pprint NA
profile NA
prompt_toolkit 3.0.38
pstats NA
psutil 5.9.5
pty NA
ptyprocess 0.7.0
pwd NA
pydev_ipython NA
pydevconsole NA
pydevd 2.9.5
pydevd_file_utils NA
pydevd_plugins NA
pydevd_tracing NA
pydoc NA
pydoc_data NA
pyexpat NA
pygments 2.15.1
pyrsistent NA
pytz 2023.3
queue NA
quopri NA
random NA
re 2.2.1
reprlib NA
resource NA
runpy NA
secrets NA
select NA
selectors NA
shlex NA
shutil NA
signal NA
site NA
six 1.16.0
socket NA
socketserver 0.4
soupsieve 2.4.1
sphinxcontrib NA
sqlite3 2.6.0
sre_compile NA
sre_constants NA
sre_parse NA
ssl NA
stat NA
storemagic NA
string NA
stringprep NA
struct NA
subprocess NA
sys 3.7.16 (default, Mar 6 2023, 12:45:08)
[GCC 11.3.0]
sysconfig NA
tempfile NA
tenacity NA
termios NA
textwrap NA
threading NA
timeit NA
token NA
tokenize NA
tornado 6.2
traceback NA
traitlets 5.9.0
tty NA
types NA
typing NA
typing_extensions NA
unicodedata NA
urllib NA
uu NA
uuid NA
warnings NA
wcwidth 0.2.6
weakref NA
webbrowser NA
xml NA
xmlrpc NA
zipfile NA
zipimport NA
zipp NA
zlib 1.0
zmq 25.1.0
-----
IPython 7.34.0
jupyter_client 7.4.9
jupyter_core 4.12.0
-----
Python 3.7.16 (default, Mar 6 2023, 12:45:08) [GCC 11.3.0]
Linux-5.15.0-1037-azure-x86_64-with-debian-bookworm-sid
2 logical CPU cores, x86_64
-----
Session information updated at 2023-06-02 00:04