PIBM2023_Python_y_lib_cientificas
PIBM2023_Python_y_lib_cientificas
5em
0.0.00.5em
0.0.0.00.5em
0em
,
Curso PIB-M 2023: Introducción a
Python y a las librerías científicas
Angel Silva
1. Introducción 1
1.1. ¿Por qué Python? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Python en meteorología . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3. Historia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.4. Consejos para programar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.5. Usuarios y acceso. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2. Tipos de datos 7
2.1. Diferentes tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2. Estructuras de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3. Cadenas de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4. Leer datos desde teclado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3. Control de flujo 23
3.1. Sangría ´indentación´ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.2. Codificación “Encoding” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3. Asignación múltiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.4. Condicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.5. Ejercicios con condicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.6. Bucles while y for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5. Funciones 43
5.1. Primera función . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.2. Parámetros y argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5.3. Documentación de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
5.4. Tipos de funciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.5. Consejos para programar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
I
6.2. Leer un archivo de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.3. Escribir un archivo de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.4. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
7. Librería Pandas 53
7.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
7.2. Creación de series y dataframes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
7.3. Indices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
7.4. Manejo de series temporales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
7.5. Visualización de las características de un DataFrame . . . . . . . . . . . . . . . . . . . . . . . . . . 58
7.6. Seleccionar y modificar valores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
7.7. Entrada / salida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
7.8. Datos nulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
7.9. Merge, concat, append . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
7.10. Agrupar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
7.11. Reshaping: stack, pivot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
7.12. Gráficos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
8. Librería Matplotlib 83
8.1. Primera gráfica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
8.2. Partes de una figura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
8.3. Explícito o implícito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
8.4. Estilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
8.5. Límites. Marcas (ticks) y etiquetas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
8.6. Leyenda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
8.7. Anotaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
8.8. Compartiendo ejes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
8.9. Subplots . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
8.10. Guardar en fichero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
8.11. Mapas de color . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
8.12. Consejos útiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
9. Librería Xarray 99
9.1. Lectura de datos. Estructuras Dataset y DataArray . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
9.2. Selección e indexado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
9.3. Gráficos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
9.4. Máscaras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
9.5. Estadísticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
9.6. Computación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
9.7. Escritura de ficheros netCDF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
9.8. Unos comentarios sobre Cartopy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
II
13. Agradecimientos 143
III
IV
Capı́tulo 1
Introducción
Python es un lenguaje sencillo de leer y escribir debido a su alta similitud con el lenguaje humano. Además, se trata
de un lenguaje multiplataforma de código abierto y, por lo tanto, gratuito, lo que permite desarrollar software sin
límites. Con el paso del tiempo, Python ha ido ganando adeptos gracias a su sencillez y a sus amplias posibili-
dades, sobre todo en los últimos años, ya que facilita trabajar con inteligencia artificial, big data, machine learning y
data science, entre muchos otros campos en auge.
Ahorrar tiempo
1.1.1. Ventajas
1
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Fácil de leer: tiene una sintaxis más sencilla que otros lenguajes, y al ser interpretado no hace falta compilar o
enlazar.
Con una gran comunidad de usuarios: hay miles de paquetes o librerías disponibles gratuitamente, y foros
donde resolver tus dudas.
Útil para muchos ámbitos: científico, técnico, diseño GUI, Web, bases de datos, etc.
Permite un flujo unificado: no es necesario utilizar distintas aplicaciones para diversas tareas, con python se
puede hacer prácticamente todo
Es “open source”
Inconvenientes
Al ser interpretado, es más lento que Fortran o C. Esto se puede evitar utilizando paquetes científicos optimiza-
dos, escritos en lenguajes más rápidos.
Menor soporte y documentación más dispersa
Este es un curso de 15 horas, por lo que sólo vamos a poder explicar los fundamentos de python, algunas de las librerías
básicas, y varias librerías científicas. Pero el árbol de python es mucho más grande:
2 Capítulo 1. Introducción
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
1.3. Historia
Python es un lenguaje de programación de propósito general, creado por Guido van Rossum, un informático holandés.
El nombre de Python es debido a que Guido van Rossum es un fan de la famosa serie británica *Monty Python Flying
Circus*.
4 Capítulo 1. Introducción
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Una vez dentro del Escritorio virtual de Linux y abierta una terminal, usaremos el siguiente comando para acceder al
servidor sur.
ssh -X sur
# o también podemos usar
ssh -X trXX@sur
Por último, vamos a realizar la carga de algunos módulos que nos permita disponer de Python 3.10 :
# lo haremos ejecutando la siguiente orden.
module load aemet/5.0
Algunos comandos útiles en LINUX.
# Ir al Directorio HOME (inicial)
cd
# Ir al Directorio de descargas
cd Descargas
# Listar los directorios
ls -lrt
# Abrir un fichero con Gedit
gedit fichero.py
# Ejecutar un fichero en python
python fichero.py
# Abrir un notebook de jupyter
jupyter notebook
6 Capítulo 1. Introducción
Capı́tulo 2
Tipos de datos
2.1.1. Variables
Las variables son fundamentales ya que permiten definir nombres para los valores que tenemos en memoria y que
vamos a usar en nuestro programa.
7
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
snake_case en el que utilizamos caracteres en minúsculas (incluyendo dígitos si procede) junto con guiones bajos –
Constantes:
Un caso especial y que vale la pena destacar son las constantes. Podríamos decir que es un tipo de variable pero que
su valor no cambia a lo largo de nuestro programa. Por ejemplo la velocidad de la luz.
Asignación:
En Python se usa el símbolo = para asignar un valor a una variable:
Python nos ofrece la posibilidad de hacer una asignación múltiple de la siguiente manera:
>>> tres = three = drei = 3
Las asignaciones que hemos hecho hasta ahora han sido de un valor literal a una variable. Pero nada impide que
podamos hacer asignaciones de una variable a otra variable:
>>> people = 157503
>>> total_population = people
>>> total_population
157503
Para poder descubrir el tipo de un literal o una variable, Python nos ofrece la función type(). Veamos algunos ejemplos
de su uso:
>>> type(9)
int
>>> type(1.2)
float
>>> height = 3718
>>> type(height)
int
>>> sound_speed = 343.2
>>> type(sound_speed)
Documentación oficial de Python: https://docs.python.org/es/3/library/functions.html?highlight=built
En Python podemos pedir ayuda con la función help(). Supongamos que queremos obtener información sobre id.
Desde el intérprete de Python ejecutamos lo siguiente:
>>> help(id)
Help on built-in function id in module builtins:
id(obj, /)
Return the identity of an object.
This is guaranteed to be unique among simultaneously existing objects.
(CPython uses the object s memory address.)
Existe una forma alternativa de obtener ayuda: añadiendo el signo de
interrogación ? al término de búsqueda:
>>> id?
Signature: id(obj, /)
Docstring:
Return the identity of an object.
This is guaranteed to be unique among simultaneously existing objects.
(CPython uses the object s memory address.)
Type: builtin_function_or_method
Entre los operadores aritméticos que Python utiliza, podemos encontrar los siguientes:
Símbolo Significado Ejemplo Resultado
+ Suma a = 10 + 5 a es 15
- Resta a = 12 - 7 a es 5
- Negación a = -5 a es -5
* Multiplicación a=7*5 a es 35
Exponente a = 2 ** 3 a es 8
**
/ División a = 12.5 / 2 a es 6.25
// División entera a = 12.5 / 2 a es 60
% Módulo a = 27 % 4 a es 3
Importante : Con operadores siempre colocar un espacio en blanco, antes y después de un operador
Un ejemplo sencillo con variables y operadores aritméticos:
>>> monto_bruto = 175
>>> tasa_interes = 12
>>> monto_interes = monto_bruto \* tasa_interes / 100
>>> tasa_bonificacion = 5
>>> importe_bonificacion = monto_bruto * tasa_bonificacion / 100
>>> monto_neto = (monto_bruto - importe_bonificacion) + monto_interes
2.1.9. Comentarios
Un archivo, no solo puede contener código fuente. También puede incluir comentarios(notas que como programadores,
indicamos en el código para poder comprenderlo mejor).
Los comentarios pueden ser de dos tipos: de una sola línea o multi-línea y se expresan de la siguiente manera:
# Esto es un comentario de una sola línea
mi_variable = 15
"""Y este es un comentario
de varias líneas"""
mi_variable = 15
mi_variable = 15 # Este comentario es de una línea también
En los comentarios, pueden incluirse palabras que nos ayuden a identificar además, el subtipo de comentario:
# TODO esto es algo por hacer
# FIXME esto es algo que debe corregirse
# XXX esto también, es algo que debe corregirse
2.2.1. Listas
Las listas permiten almacenar objetos mediante un orden definido y con posibilidad de duplicados, son además
mutables, lo que significa que podemos añadir, eliminar o modificar sus elementos.
Creando listas
>>> empty_list = []
>>> languages = [ "Python" , "Ruby" , "Javascript" ]
>>> fibonacci = [0, 1, 1, 2, 3, 5, 8, 13]
>>> data = [ "Tenerife" , { "cielo" : "limpio" , "temp" : 24},
3718, (28.2933947, -16.5226597)]
Importante : Aunque está permitido, NUNCA llames list a una variable porque destruirías la función que nos permite
crear listas.
Ejercicio
Cree una lista con las 5 tipos de nubes que le gusten.
Conversión
Para convertir otros tipos de datos en una lista podemos usar la función list():
>>> # conversión desde una cadena de texto
>>> list( "Python" )
[ P , y , t , h , o , n ]
Si nos fijamos en lo que ha pasado, al convertir la cadena de texto Python se ha creado una lista con 6 elementos,
donde cada uno de ellos representa un carácter de la cadena. Podemos extender este comportamiento a cualquier otro
tipo de datos que permita ser iterado (iterables).
Otro ejemplo interesante de conversión puede ser la de los rangos. En este caso queremos obtener una lista explícita
con los valores que constituyen el rango [0, 9]:
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Obtener un elemento Igual que en el caso de las cadenas de texto, podemos obtener un elemento de una lista a través
del índice (lugar) que ocupa. Veamos un ejemplo:
>>> shopping = [ Agua , Huevos , Aceite ]
>>> shopping[0]
Agua
>>> shopping[1]
Huevos
>>> shopping[2]
Aceite
>>> shopping[-1] # acceso con índice negativo
Aceite
Trocear una lista El troceado de listas funciona de manera totalmente análoga al troceado de cadenas. Veamos algunos
ejemplos:
>>> shopping = [ Agua , Huevos , Aceite , Sal , Limón ]
>>> shopping[0:3]
[ Agua , Huevos , Aceite ]
>>> shopping[:3]
[ Agua , Huevos , Aceite ]
>>> shopping[2:4]
[ Aceite , Sal ]
>>> shopping[-1:-4:-1]
[ Limón , Sal , Aceite ]
>>> # Equivale a invertir la lista
>>> shopping[::-1]
[ Limón , Sal , Aceite , Huevos , Agua ]
Nota: El índice que especificamos en la función insert() lo podemos interpretar como la posición delante (a la izquier-
da) de la cual vamos a colocar el nuevo valor en la lista.
Modificar una lista
Del mismo modo que se accede a un elemento utilizando su índice, también podemos modificarlo:
>>> shopping = [ Agua , Huevos , Aceite ]
>>> shopping[0]
Agua
>>> shopping[0] = Jugo
>>> shopping
[ Jugo , Huevos , Aceite ]
En el caso de acceder a un índice no válido de la lista, incluso para modificar, obtendremos un error:
>>> shopping[100] = Chocolate
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list assignment index out of range
Borrar elementos
Python nos ofrece, al menos, cuatro formas para borrar elementos en una lista:
Por su índice: Mediante la sentencia del:
>>> shopping = [ Agua , Huevos , Aceite , Sal , Limón ]
>>> del shopping[3]
>>> shopping
[ Agua , Huevos , Aceite , Limón ]
Por su valor: Mediante la función remove():
>>> shopping = [ Agua , Huevos , Aceite , Sal , Limón ]
>>> shopping.remove( Sal )
>>> shopping
[ Agua , Huevos , Aceite , Limón ]
Advertencia: Si existen valores duplicados, la función remove() sólo borrará la primera ocurrencia.
Ordenar una lista
Python proporciona, al menos, dos formas de ordenar los elementos de una lista:
Conservando lista original: Mediante la función sorted() que devuelve una nueva lista ordenada:
>>> shopping = [ Agua , Huevos , Aceite , Sal , Limón ]
>>> sorted(shopping)
[ Aceite , Agua , Huevos , Limón , Sal ]
2.2.2. Tuplas
El concepto de tupla es muy similar al de lista. Aunque hay algunas diferencias menores, lo fundamental es que,
mientras una lista es mutable y se puede modificar, una tupla no admite cambios y por lo tanto, es inmutable.
Creando tuplas
Podemos pensar en crear tuplas tal y como lo hacíamos con listas, pero usando paréntesis en lugar de corchetes:
>>> empty_tuple = ()
>>> tenerife_geoloc = (28.46824, -16.25462)
>>> three_wise_men = ( Melchor , Gaspar , Baltasar )
Tuplas de un elemento
Hay que prestar especial atención cuando vamos a crear una tupla de un único elemento. La intención primera sería
hacerlo de la siguiente manera:
>>> one_item_tuple = ( Papá Noel )
>>> one_item_tuple
Papá Noel
>>> type(one_item_tuple)
str
Realmente, hemos creado una variable de tipo str (cadena de texto). Para crear una tupla de un elemento debemos
añadir una coma al final:
>>> one_item_tuple = ( Papá Noel ,)
>>> one_item_tuple
( Papá Noel ,)
>>> type(one_item_tuple)
tuple
Como ya hemos comentado previamente, las tuplas son estructuras de datos inmutables. Una vez que las creamos con
un valor, no podemos modificarlas. Veamos qué ocurre si lo intentamos:
>>> three_wise_men = Melchor , Gaspar , Baltasar
>>> three_wise_men[0] = Tom Hanks
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: tuple object does not support item assignment
Con las tuplas podemos realizar todas las operaciones que vimos con listas salvo las que conlleven una modificación
«in-situ» de la misma:
reverse()
append()
extend()
remove()
clear()
sort()
Truco: Sí es posible aplicar sorted() o reversed() sobre una tupla ya que no estamos modificando su valor sino creando
un nuevo objeto.
Aunque puedan parecer estructuras de datos muy similares, sabemos que las tuplas carecen de ciertas operaciones,
especialmente las que tienen que ver con la modificación de sus valores, ya que no son inmutables. Si las listas son
más flexibles y potentes, ¿por qué íbamos a necesitar tuplas? Veamos 4 potenciales ventajas del uso de tuplas frente a
las listas:
1. Las tuplas ocupan menos espacio en memoria.
2. En las tuplas existe protección frente a cambios indeseados.
3. Las tuplas se pueden usar como claves de diccionarios (son «hashables»).
2.2.3. Diccionarios
Diríamos que en Python un diccionario es también un objeto indexado por claves (las palabras) que tienen asociados
unos valores (los significados). Los diccionarios en Python tienen las siguientes características:
Mantienen el orden en el que se insertan las claves.
Son mutables, con lo que admiten añadir, borrar y modificar sus elementos.
Las claves deben ser únicas. A menudo se utilizan las cadenas de texto como claves, pero en realidad podría ser
cualquier tipo de datos inmutable: enteros, flotantes, tuplas (entre otros).
Tienen un acceso muy rápido a sus elementos, debido a la forma en la que están implementados internamente.
Creando diccionarios
Para crear un diccionario usamos llaves {} rodeando asignaciones clave: valor que están separadas por comas. Veamos
algunos ejemplos de diccionarios:
>>> empty_dict = {}
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> population_can = {
... 2015: 2_135_209,
... 2016: 2_154_924,
... 2017: 2_177_048,
... 2018: 2_206_901,
... 2019: 2_220_270
... }
Ejercicio
Cree un diccionario con los nombres de 3 países y su capital.
Solución –> solucion_dicc.py
Conversión
Para convertir otros tipos de datos en un diccionario podemos usar la función dict():
>>> # Diccionario a partir de una lista de cadenas de texto
>>> dict([ a1 , b2 ])
{ a : 1 , b : 2 }
>>> # Diccionario a partir de una tupla de cadenas de texto
>>> dict(( a1 , b2 ))
{ a : 1 , b : 2 }
>>> # Diccionario a partir de una lista de listas
>>> dict([[ a , 1], [ b , 2]])
{ a : 1, b : 2}
Obtener un elemento Para obtener un elemento de un diccionario basta con escribir la clave entre corchetes.
Veamos un ejemplo:
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> rae[ anarcoide ]
Usando get()
Existe una función muy útil para «superar» los posibles errores de acceso por claves inexistentes. Se trata de get() y
su comportamiento es el siguiente:
1. Si la clave que buscamos existe, nos devuelve su valor.
2. Si la clave que buscamos no existe, nos devuelve None salvo que le indiquemos otro valor por defecto, pero en
ninguno de los dos casos obtendremos un error.
>>> rae
{ bifronte : De dos frentes o dos caras ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa }
])
>>> # Obtener todos los pares «clave-valor» de un diccionario:
>>> # Mediante la función items():
>>> rae.items()
dict_items([
( bifronte , De dos frentes o dos caras ),
( anarcoide , Que tiende al desorden ),
( montuvio , Campesino de la costa ),
( enjuiciar , Instruir, juzgar o sentenciar una causa )
])
Longitud de un diccionario
>>> rae
{ bifronte : De dos frentes o dos caras ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa ,
enjuiciar : Instruir, juzgar o sentenciar una causa }
>>> len(rae)
4
Borrar elementos
Una forma para borrar elementos en un diccionario:
Por su clave: Mediante la sentencia del:
>>> rae
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> del rae[ bifronte ]
>>> rae
{ anarcoide : Que tiende al desorden , montuvio : Campesino de la costa }
Una posible solución a este problema es hacer una «copia dura». Para ello Python proporciona la función copy():
>>> original_rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> copy_rae = original_rae.copy()
>>> original_rae[ bifronte ] = bla bla bla
>>> original_rae
{ bifronte : bla bla bla ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa }
>>> copy_rae
{ bifronte : De dos frentes o dos caras ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa }
2.2.4. Conjuntos
Un conjunto en Python representa una serie de valores únicos y sin orden establecido, con la única restricción de que
sus elementos deben ser «hashables». Mantiene muchas similitudes con el concepto matemático de conjunto.
>>> original_rae = {
>>> lottery = {21, 10, 46, 29, 31, 94}
>>> lottery
{10, 21, 29, 31, 46, 94}
# Tabulador
>>> msg = Valor = \t40
>>> print(msg)
Valor = 40
# Comilla simple
>>> msg = Necesitamos \escapar\ la comilla simple
>>> print(msg)
Necesitamos escapar la comilla simple
# Barra invertida
>>> msg = Capítulo \\ Sección \\ Encabezado
>>> print(msg)
Capítulo \ Sección \ Encabezado
True
>>> sayo in proverb
True
>>> pones in proverb
False
>>> # El separador por defecto entre las variables es un espacio, podemos cambiar
>>> # el carácter que se utiliza como separador entre cadenas.
>>> print(msg1, msg2, sep= | )
¿Sabes por qué estoy acá?|Porque me apasiona
Ejercicio
Escriba un programa en Python que lea por teclado dos números enteros y muestre por pantalla el resultado de
realizar las operaciones básicas entre ellos.
Una estructura de control, es un bloque de código que permite agrupar instrucciones de manera controlada. En este
capítulo, hablaremos sobre dos estructuras de control:
Estructuras de control condicionales
Estructuras de control iterativas
“utf-8” podría ser cualquier codificación de caracteres. Si no se indica una codificación de caracteres, Python podría
producir un error si encontrara caracteres “extraños”:
print "En el Ñágara encontré un Ñandú"
Producirá un error de sintaxis:
SyntaxError: Non-ASCII character[...]
Ejemplo
# -- coding: utf-8 --
print “En el Ñágara encontré un Ñandú”
# Produciendo la siguiente salida: # En el Ñágara encontré un Ñandú
23
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
>>> # Podemos imprimir todas las variables que queramos separándolas por comas.
>>> print a
string
>>> print b
15
>>> print c
True
3.4. Condicionales
Las estructuras de control condicionales, son aquellas que nos permiten evaluar si una o más condiciones se cumplen,
para decir qué acción vamos a ejecutar. La evaluación de condiciones, solo puede arrojar 1 de 2 resultados: verdadero
o falso (True o False).
Para describir la evaluación a realizar sobre una condición, se utilizan operadores relacionales (o de comparación):
Las estructuras de control de flujo condicionales, se definen mediante el uso de tres palabras claves reservadas, del
lenguaje: if (si), elif (sino, sí) y else (sino).
>>> semaforo = "verde"
>>> if semaforo == "verde":
... print "Cruzar la calle"
... else:
... print "Esperar"
...
Ejercicio 1
Escribir un programa que pregunte al usuario su edad y muestre por pantalla si es mayor de edad o no.
Ejercicio 2
Escribir un programa que almacene la cadena de caracteres contraseña en una variable, pregunte al usuario por
la contraseña e imprima por pantalla si la contraseña introducida por el usuario coincide con la guardada en la
variable sin tener en cuenta mayúscula y minúscula.
Ejercicio 3
Escribir un programa que pida al usuario dos números y muestre por pantalla su división. Si el divisor es cero el
programa debe mostrar un error.
Ejercicio 4
Escribir un programa que pida al usuario un número entero y muestre por pantalla si es par o impar.
Ejercicio 5
Para tributar un determinado impuesto se debe ser mayor de 16 años y tener unos ingresos iguales o superiores
a 1000 C mensuales. Escribir un programa que pregunte al usuario su edad y sus ingresos mensuales y muestre
por pantalla si el usuario tiene que tributar o no.
Ejercicio 6
Los alumnos de un curso se han dividido en dos grupos A y B de acuerdo al sexo y el nombre. El grupo A está
formado por las mujeres con un nombre anterior a la M y los hombres con un nombre posterior a la N y el grupo
B por el resto. Escribir un programa que pregunte al usuario su nombre y sexo, y muestre por pantalla el grupo
que le corresponde.
Ejercicio 7
Los tramos impositivos para la declaración de la renta en un determinado país son los siguientes: Escribir un
programa que pregunte al usuario su renta anual y muestre por pantalla el tipo impositivo que le corresponde.
Ejercicio 8
En una determinada empresa, sus empleados son evaluados al final de cada año. Los puntos que pueden obtener
en la evaluación comienzan en 0.0 y pueden ir aumentando, traduciéndose en mejores beneficios. Los puntos que
pueden conseguir los empleados pueden ser 0.0, 0.4, 0.6 o más, pero no valores intermedios entre las cifras men-
cionadas. A continuación se muestra una tabla con los niveles correspondientes a cada puntuación. La cantidad
de dinero conseguida en cada nivel es de 2.400C multiplicada por la puntuación del nivel.
Escribir un programa que lea la puntuación del usuario e indique su nivel de rendimiento, así como la cantidad
de dinero que recibirá el usuario.
Nivel Puntuación
Inaceptable 0.0
Aceptable 0.4
Meritorio 0.6 o más
Ejercicio 9
Escribir un programa para una empresa que tiene salas de juegos para todas las edades y quiere calcular de forma
automática el precio que debe cobrar a sus clientes por entrar. El programa debe preguntar al usuario la edad del
cliente y mostrar el precio de la entrada. Si el cliente es menor de 4 años puede entrar gratis, si tiene entre 4 y 18
años debe pagar 5C y si es mayor de 18 años, 10C.
Ejercicio 10
La pizzería “Bella Napoli” ofrece pizzas vegetarianas y no vegetarianas a sus clientes. Los ingredientes para
cada tipo de pizza aparecen a continuación.
Ingredientes vegetarianos: Pimiento y tofu.
Ingredientes no vegetarianos: Peperoni, Jamón y Salmón.
Escribir un programa que pregunte al usuario si quiere una pizza vegetariana o no, y en función de su respuesta
le muestre un menú con los ingredientes disponibles para que elija. Solo se puede elegir un ingrediente además
de la mozzarella y el tomate que están en todas la pizzas. Al final se debe mostrar por pantalla si la pizza elegida
es vegetariana o no y todos los ingredientes que lleva.
Este bucle, se encarga de ejecutar una misma acción “mientras que” una determinada condición se cumpla:
Ejemplo
Mientras que año sea menor o igual a 2012, imprimir la frase: “Informes del Año año”:
# – coding: utf-8 –
anio = 2001
while anio <= 2012: print(“Informes del Año”, str(anio) ) anio += 1
Ejemplo
El bucle no tiene que ser numérico, y se puede detener de otra forma, veamos el siguiente ejemplo donde tenemos
que introducir un nombre.
# – coding: utf-8 –
while True: nombre = raw_input(“Indique su nombre: ”) if nombre:
break
print(“Su nombre es :” , nombre )
El bucle anterior, incluye un condicional anidado que verifica si la variable nombre es verdadera (solo será verdadera
si el usuario escribe un texto en pantalla cuando el nombre le es solicitado). Si es verdadera, el bucle para (break).
Sino, seguirá ejecutándose hasta que el usuario, ingrese un texto en pantalla.”“”
Ejercicio
Escriba un programa que encuentre todos los múltiplos de 5 menores que un valor dado:
Bucle infinito
Imaginemos que queremos escribir un programa que ayude a un observador en meteorología a introducir las temperat-
uras. Si la temperatura no está en el intervalo [-50, 50] mostramos un mensaje de error, en otro caso seguimos pidiendo
valores:
>>> while True:
... mark = float(input( "Introduzca una nueva temperatura" ))
... if not(-50 <= mark <= 50):
... print( "Temperatura fuera de rango" )
... break
El bucle for, en Python, es aquel que nos permitirá iterar sobre una variable compleja, del tipo lista o tupla:
Por cada nombre en mi_lista, imprimir nombre
mi_lista = [’Juan’, ’Antonio’, ’Pedro’, ’Herminio’]
for nombre in mi_lista:
print nombre
Ejercicio
Dada una cadena de texto, indique el número de vocales que tiene.
Ejemplo
Entrada: “Una nube es un hidrometeoro consistente en diminutas partículas de agua líquida o hielo, o de
ambos, suspendidas en la atmósfera y que, por lo general, no tocan el suelo.”
Salida: 61
else:
# Esto se ejecutara si el bloque try se ejecuta sin errores
finally:
# Este bloque se ejecutara siempre
Veamos el uso de cada uno de estos bloques:
EL bloque try es el bloque con las sentencias que quieres ejecutar. Sin embargo, podría llegar a
haber errores de ejecución y el bloque se dejará de ejecutarse.
El bloque except se ejecutará cuando el bloque try falle debido a un error. Este bloque contiene
sentencias que generalmente nos dan un contexto de lo que salió mal en el bloque try.
Siempre deberías de mencionar el tipo de error que se espera, como una excepción dentro del
bloque except dentro de <tipo de error> como lo muestra el ejemplo anterior. Algunos tipos de error
pueden ser ´IndexError´, ´KeyError´ y ´FileNotFoundError´. Y tendrías que añadir un bloque except
por cada tipo de error que puedas anticipar, A continuación mencionare algunas:
• ImportError: falla una importación;
• IndexError: una lista es indexada con un número fuera de rango;
• NameError: una variable desconocida es utilizada;
• SyntaxError: el código no puede ser analizado correctamente;
• TypeError: una función es llamada con un valor de un tipo inapropiado;
• ValueError: Una función es llamada con un valor de tipo correcto pero con un valor incorrecto.
Podrías usar except sin especificar el <tipo de error>. Pero no es una práctica recomendable, ya que
no estarás al tanto de los tipos de errores que puedan ocurrir.
El bloque else se ejecutará solo si el bloque try se ejecuta sin errores. Esto puede ser útil cuando
quieras continuar el código del bloque try. Por ejemplo si abres un archivo en el bloque try, podrías
leer su contenido dentro del bloque else.
El bloque finally siempre es ejecutado sin importar que pase en los otros bloques, esto puede ser
útil cuando quieras liberar recursos después de la ejecución de un bloque de código, ( try, except o
else ).
Nota: Los bloques else y finally son opcionales. En muchos casos puedes solo ocupar el bloque
try para tratar de ejecutar algo y capturar los errores como excepciones en el bloque except.
ZeroDivisionError
En caso de que el divisor fuese cero, python arrojaría un error, que lo podemos tener controlado con la sentencia try
de la siguiente manera. Lo adecuado será considerar el error de tipo: ZeroDivisionError
num, div = 10,0
try:
res = num/div
print(res)
except ZeroDivisionError:
print("Trataste de dividir entre cero :( ")
TypeError
En caso de intentar sumar un número y una cadena de texto ´String´ también nos daría un error. Lo adecuado será
considerar el error de tipo : TypeError
mi_num = "cinco"
try:
resultado = 10 + mi_num
print(resultado)
except TypeError:
print("El argumento mi_num deberia ser un número")
IndexError
Queremos imprimir un elemento de la lista que no existe. Lo adecuado será considerar el error de tipo: IndexError
mi_lista = ["Python","C","C++","JavaScript"]
buscar_ind = 3
try:
print( mi_lista[buscar_ind] )
except IndexError:
print("Lo siento, el índice esta fuera de rango")
KeyError
Intentamos imprimir una clave de un diccionario que no existe. Lo adecuado será considerar el error de tipo: KeyError
mi_dict = {"clave1":"valor1", "clave2":"valor2", "clave3":"valor3"}
buscar_clave = "clave no existente"
try:
print( mi_dict[buscar_clave] )
except KeyError as msg_error::
print(f"¡Lo siento, {error_msg} no es una clave valida!")
FileNotFoundError
Intentamos abrir un fichero. Lo adecuado será considerar el error de tipo: FileNotFoundError
try:
mi_archivo = open("contenido/datos_meteorologicos/temperaturas.csv")
except FileNotFoundError:
print(f"Lo siento, el archivo no existe")
else:
contenido = mi_archivo.read()
print(contenido)
finally:
mi_archivo.close()
Hay situaciones en las que no necesitamos usar la variable. Para estos casos se suele recomendar usar el guion bajo _
como nombre de variable.
>>> for _ in range(10):
... print( "Repeat me 10 times!" )
...
Repeat me 10 times!
Repeat me 10 times!
Repeat me 10 times!
Repeat me 10 times!
Repeat me 10 times!
Repeat me 10 times!
Repeat me 10 times!
Repeat me 10 times!
Repeat me 10 times!
Repeat me 10 times!
Ejercicio
Determine si un número dado es un número primo.
No es necesario implementar ningún algoritmo en concreto. La idea es probar los números menores al dado e
ir viendo si las divisiones tienen resto cero o no.
Una vez definido, dicho módulo puede ser usado o importado en otro fichero. “otromodulo.py”
# otromodulo.py
import mimodulo
print(mimodulo.suma(4, 3)) # 7
print(mimodulo.resta(10, 9)) # 1
print(suma(4, 3)) # 7
print(resta(10, 9)) # 1
Por último, podemos importar todo el módulo haciendo uso de *. Pero esta práctica no está recomendada
from mimodulo import *
print(suma(4, 3)) # 7
print(resta(10, 9)) # 1
Podemos asignar un nombre más corto a un módulo haciendo uso de ‘as’.
import modulodenombresuperlargoydificil as breve
La función dir() nos permite ver los nombres (variables, funciones, clases, etc) existentes en nuestro namespace. Por
ejemplo, __file__ es creado automáticamente y alberga el nombre del fichero .py.
print(__file__)
#/tu/ruta/tufichero.py
Dependiendo de la situación, puede ser importante especificar que únicamente queremos que se ejecute el código si el
módulo es el __main__. Con la siguiente modificación
# modulo.py
def suma(a, b):
33
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
return a + b
O bien el módulo “random”, que implementa funciones para trabajar con números aleatorios.
>>> from random import randint, choice
4.4. Módulo os
El módulo os nos permite acceder a funcionalidades dependientes del Sistema Operativo. Sobre todo, aquellas que nos
refieren información sobre el entorno del mismo y nos permiten manipular la estructura de directorios.
os.access(path, modo_de_acceso): Saber si se puede acceder a un archivo o directorio.
os.getcwd(): Conocer el directorio actual.
os.chdir(nuevo_path): Cambiar de directorio de trabajo.
os.chroot(): Cambiar al directorio de trabajo raíz.
os.chmod(path, permisos):Cambiar los permisos de un archivo o directorio.
os.chmod(path, permisos): Cambiar el propietario de un archivo o directorio.
os.mkdir(path[, modo]): Crear un directorio.
os.mkdirs(path[, modo]): Crear directorios recursivamente.
os.remove(path): Eliminar un archivo.
os.rmdir(path): Eliminar un directorio.
os.removedirs(path): Eliminar directorios recursivamente.
os.rename(actual, nuevo): Renombrar un archivo.
os.symlink(path, nombre_destino): Crear un enlace simbólico.
“os.path” permite acceder a ciertas funcionalidades relacionadas con los nombres de las rutas de archivos y directorios
que resultan muy útiles:
os.path.abspath(path): Ruta absoluta
os.path.basename(path): Directorio base
os.path.exists(path): Saber si un directorio existe
os.path.getatime(path): Conocer último acceso a un directorio
4.4. Módulo os 35
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
El módulo OS de Python proporciona funciones para interactuar con el Sistema Operativo (SO).
1. os.environ es un objeto de mapeo que representa las variables del entorno del usuario y devuelve un diccionario
que tiene la variable de entorno del usuario como clave y sus valores como valor.
2. os.environ se comporta como un diccionario, por lo que se pueden realizar todas las operaciones comunes del
diccionario, como get y set. También podemos modificar os.environ pero cualquier cambio será efectivo solo
para el proceso actual donde fue asignado y no cambiará el valor de forma permanente
A continuación vemos como usar os.eviron para acceder a las variables de entrono.
# Programa Pyton para explicar el objeto os.environ
# importamos el módulo os
import os
import pprint
# Obtenemos la lista de
# variables de entorno del usuario
env_var = os.environ
# Imprimimos la lista de
# variables de entorno del usuario
print("User’s Environment variable:")
pprint.pprint(dict(env_var), width = 1)
# importamos el módulo os
import os
# Obtenemos el valor de
# ’HOME’ variable de entorno
home = os.environ[’HOME’]
# imprimimos el valor de
# la variable de entorno ’HOME’
print("HOME:", home)
# Obtenemos el valor de
# la variable de entorno ’JAVA_HOME’
# usando la operación get del diccionario.
java_home = os.environ.get(’JAVA_HOME’)
# Imprime el valor de
# la variable de entorno ’JAVA_HOME’
print("JAVA_HOME:", java_home)
También podemos exportar una variable con un script en BASH que además lance nuestro Python y usarla.
#!/bin/sh
# Script en bash
export fichero_entrada="/home/tr25/fichero.txt"
python ./entorno.py
# python --> entorno.py
import os
fich = os.environ.get(’fichero_entrada’)
Operaciones
En ocasiones tendremos la necesidad de realizar ciertas operaciones con fechas, ya sea agregar días, restar años.
from datetime import datetime
from datetime import timedelta
#Comparación
if now < new_date:
print("La fecha actual es menor que la nueva fecha")
El problema de la zona horaria
Lo resolvemos con el módulo pytz
from datetime import datetime
import pytz
fecha_y_hora_actuales = datetime.now()
print(fecha_y_hora_bogota_en_texto)
# Contenido de la respuesta
# Si se trata de un texto
>>> resp.text
’<!doctype html><html itemscope="" itemtype="h...’
Para archivos como los .json, la librería posee una función especial resp.json() que decodifica los datos para mostrarlo
en formato .json.
>>> import requests
>>> resp = requests.get(’https://www.aemet.es/’)
>>> j_son = resp.json()
parser = ConfigParser()
parser.read(’simple.config’)
print(parser.get(’bug_tracker’, ’url’))
Puede que sea necesario abrirlo con la codificación adecuada.
from configparser import ConfigParser
parser = ConfigParser()
# Open the file with the correct encoding
parser.read(’unicode.ini’, encoding=’utf-8’)
print(parser.get(’bug_tracker’, ’url’))
[wiki]
url = http://localhost:8080/wiki/
username = dhellmann
password = SECRET
Y vamos a ver como podemos acceder a cada sección y opción; de la siguiente forma nos devolverá listas.
from configparser import ConfigParser
parser = ConfigParser()
parser.read(’multisection.ini’)
print(’Section:’, section_name)
print(’ Options:’, parser.options(section_name))
for name, value in parser.items(section_name):
print(’ {} = {}’.format(name, value))
print()
Pero también admite la misma interfaz de programación de mapeo que dict, con el ConfigParser actuando como un
diccionario que contiene diccionarios separados para cada sección.
from configparser import ConfigParser
parser = ConfigParser()
parser.read(’multisection.ini’)
El concepto de función es básico en prácticamente cualquier lenguaje de programación. Se trata de una estructura que
nos permite agrupar código. Persigue dos objetivos claros:
1. No repetir fragmentos de código en un programa.
2. Reutilizar el código en distintos escenarios.
43
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
>>> one()
1
Podemos integrar las funciones de otras estructuras como en condicionales:
>>> if one() == 1:
... print( "¡¡¡ Funciona !!!" )
... else:
... print( "Algo va mal :( " )
...
¡¡¡ Funciona !!!
44 Capítulo 5. Funciones
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Es posible especificar valores por defecto en los parámetros de una función. En el caso de que no se proporcione
un valor al argumento en la llamada a la función, el parámetro correspondiente tomará el valor definido por defecto.
>>> def build_cpu(vendor, num_cores, freq=2.0):
... return dict(
... vendor=vendor,
... num_cores=num_cores,
... freq=freq
... )
...
Ejercicio
Escriba un programa que pida la anchura y altura de un rectángulo y lo dibuje con caracteres producto (*):
Anchura del rectángulo: 5
Altura del rectángulo: 3
*****
*****
*****
Ejercicio
Escriba un programa que pida un año y que escriba si es bisiesto o no.
Se recuerda que los años bisiestos son múltiplos de 4, pero los múltiplos de 100 no lo son, aunque
los múltiplos de 400 sí.
Estos son algunos ejemplos de posibles respuestas: 2012 es bisiesto, 2010 no es bisiesto, 2000 es
bisiesto, 1900 no es bisiesto.
COMPROBADOR DE AÑOS BISIESTOS
Escriba un año y le diré si es bisiesto: 2000
El año 2000 es un año bisiesto.
46 Capítulo 5. Funciones
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Veamos un primer ejemplo de función «lambda» que nos permite contar el número de palabras en una cadena de texto
dada. La transformación de su versión clásica en su versión anónima sería la siguiente:
>>> num_words = lambda t: len(t.split())
>>> type(num_words)
function
>>> num_words
<function __main__.<lambda>(t)>
>>> num_words( hola socio vamos a ver )
5
Las funciones «lambda» son bastante utilizadas como argumentos a otras funciones. Un ejemplo claro de ello es la
función sorted que recibe un parámetro opcional key donde se define la clave de ordenación. Veamos cómo usar una
función anónima «lambda» para ordenar una tupla de pares longitud-latitud:
>>> geoloc = (
... (15.623037, 13.258358),
... (55.147488, -2.667338),
48 Capítulo 5. Funciones
Capı́tulo 6
Trabajar con ficheros de texto
Todo lo que hemos visto hasta ahora no perdura, es decir, una vez cerrado el programa, toda interacción con el usuario
no tendrá efecto en las posteriores ejecuciones.
La ruta o (“path”) de un archivo es lo que nos indica su ubicación en el ordenador y esta puede ser de tipo relativa o
absoluta.
6.2.1. Readline
La función readline lee la primera línea del archivo cargado (identifica esta línea según los saltos de línea) y la retorna
como “string” (generalmente con el fin de almacenarlo en una variable), además, esta línea se elimina del archivo
cargado (no del archivo original).
Supongamos que tenemos el siguiente fichero “aemet.txt”:
49
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Vamos a leerlo:
>>> # Abrimos el archivo
>>> mi_archivo = open (’aemet.txt’, ’r’)
>>>
>>> # Leemos
>>> linea_1 = mi_archivo.readline()
>>> print( linea_1 )
>>> linea_2 = mi_archivo.readline()
>>> print( linea_2 )
>>> linea_3 = mi_archivo.readline()
>>> print( linea_3 )
>>>
>>> # Cerramos el archivo
>>> mi_archivo.close()
Podemos notar que el programa no ha podido leer las tildes, esto tiene que ver con la manera en que se codifi-
can/decodifican los archivos en tu ordenador, por lo que puede que no tengas este problema. Pero en caso de que
suceda, se puede solucionar de la siguiente forma: agregando el parámetro “encoding” a la función open(), con valor
’utf-8’
6.2.2. Readlines
La función readlines lee todas líneas y las retorna como una lista de “strings”.
>>> # Abrimos el archivo
>>> mi_archivo = open (’aemet.txt’,’r’, encoding =’utf -8’)
>>>
>>> # Leemos
>>> lineas = mi_archivo.readlines()
>>> print( lineas )
>>>
>>> # Cerramos el archivo
>>> mi_archivo .close()
Vemos que líneas es una lista donde cada elemento corresponde a una línea del archivo.
Ejercicio
Imprime en consola, como un solo string y sin saltos de línea, el contenido del archivo dado archivo ejemplo.txt:
Hola , soy un archivo .
Si el archivo no existe, Python lo creará. Si el archivo ya existe, se borrará su contenido. Usaremos la función write(),
que recibe como parámetro un “string” con el contenido que queremos escribir en el archivo.
>>> mi_archivo = open(’nombre_del_archivo.txt’,’w’)
>>> mi_archivo.write(’String con contenido a escribir’)
>>> mi_archivo.close()
Si el archivo no existe, Python lo creará, pero si el archivo ya existe, agregará el contenido en la línea siguiente a la
ultima escrita.
>>> mi_archivo = open(’nombre_del_archivo.txt’,’a’)
>>> mi_archivo.write(’String con contenido a escribir’)
>>> mi_archivo.close()
6.4. Ejercicios
6.4. Ejercicios 51
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Ejercicio 5.1
Crear un archivo llamado “observacioens.txt” que contendrá una lista de observaciones con los siguientes cam-
pos:
temperatura
precipitación
humedad
presión
Leer cada línea y colocar cada campo un diccionario y mostrar los datos del diccionario.
Ejercicio 5.2
Debe realizar un programa que lea del usuario una palabra. Luego, vaya a buscar esta palabra en un archivo de
texto llamado mi_texto.txt. En esta búsqueda su programa debe hacer lo siguiente:
1. Contar cuantas ocurrencias de la palabra hay en el archivo de texto (después de haber leído todo su con-
tenido) y desplegar en pantalla esa cantidad.
2. Crear otro archivo llamado “resultado.txt”, el cual contenga solamente las líneas en donde se encuentra la
palabra buscada, desplegando al principio de la línea, su número de línea. Es decir que si fuera la primera
línea, el número es 1, si es la tercera el número es 3, etc.
7.1. Introducción
Pandas (http://pandas.pydata.org) es una librería de acceso abierto (open source), con licencia BSD que proporciona
estructuras y herramientas de análisis de datos de altas prestaciones y fáciles de usar escritas en lenguaje Python. Es
parte del ecosistema Scipy (https://projects.scipy.org).
Numpy es la librería básica de cálculo en python, con la matriz multidimensional np.array como su estructura funda-
mental.
Sin embargo nosotros estudiaremos directamente Pandas, que es una librería especializada en el análisis de datos
que puede sernos más útil, aunque nos referiremos en alguna ocasión a Numpy. Pandas tiene dos características
que la hacen especialmente interesante: permite utilizar datos heterogéneos, y utilizar etiquetas para identificar datos
tabulados fácilmente.
Observar, en cualquier caso, que ambas librerías tienen muchos métodos comunes, por lo que gran parte de la fun-
cionalidad aprendida en Pandas será aplicable también en Numpy (aunque con una sintaxis diferente).
Características de Pandas
Ventajas:
Más versatilidad que numpy en el manejo de arrays: permite tablas no homogéneas, nombres de las columnas
Maneja muy bien series temporales
Gran cantidad de funciones “off-the-shelf”: implementadas, testeadas, y listas para usar
Código corto y fácil de leer
Desventajas:
Ocupa más memoria
Código mucho más difícil de escribir si esa funcionalidad no la hace ya pandas
Curva de aprendizaje dura
53
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
R dispone de herramientas más potentes de análisis estadístico, aunque Pandas es parte de un lenguaje de
propósito general.
s
0 angel
1 3
2 5
3 NaN
4 6
5 8
dtype: object
Un dataframe se puede crear de varias formas distintas. En primer lugar se puede crear a partir de un diccionario.
Tomamos datos de precipitación de tres estaciones del CENAOS, el Centro de Estudios Atmosféricos, Oceanográficos
y Sísmicos de Honduras, como ejemplo:
df = pd.DataFrame({’Estacion’:[’Los Platos’, ’Atima’, ’Bomberos-Tegucigalpa’],
’Date’: [datetime(2023,8,27,12),
datetime(2023,8,27,12),
datetime(2023,8,27,12)],
’PCP 1h’:[0.0, 0.0, 0.0],
’PCP 6h’:[7.6, 0.1, 0.0],
’PCP 12h’:[11.7, 0.1, 7.3],
’PCP 24h’:[11.7, 19.2, 18.8]})
df
Estacion Date PCP 1h PCP 6h PCP 12h PCP 24h
0 Los Platos 2023-08-27 12:00:00 0 7.6 11.7 11.7
1 Atima 2023-08-27 12:00:00 0 0.1 0.1 19.2
2 Bomberos-Tegucigalpa 2023-08-27 12:00:00 0 0 7.3 18.8
Observar que las columnas pueden tener distintos tipos de datos:
df.dtypes
Estacion object
Date datetime64[ns]
PCP 1h float64
PCP 6h float64
PCP 12h float64
PCP 24h float64
dtype: object
7.3. Indices
Hay una cierta equivalencia entre DataFrames y tablas de una base de datos.
Los índices pueden ser muy útiles cuando haya que seleccionar datos en un gran dataframe, o si hay que unir dos de
ellos.
Para crear un índice usaremos la función set_index:
df = pd.DataFrame({’Estacion’:[’Los Platos’, ’Atima’, ’Bomberos-Tegucigalpa’],
’Date’: [datetime(2023,8,27,12),
datetime(2023,8,27,12),
datetime(2023,8,27,12)],
’PCP 1h’:[0.0, 0.0, 0.0],
’PCP 6h’:[7.6, 0.1, 0.0],
’PCP 12h’:[11.7, 0.1, 7.3],
’PCP 24h’:[11.7, 19.2, 18.8]})
df
7.3. Indices 55
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
df.set_index(’Date’, inplace=True)
df
Estacion PCP 1h PCP 6h PCP 12h PCP 24h
Date
2023-08-27 12:00:00 Los Platos 0 7.6 11.7 11.7
2023-08-27 12:00:00 Atima 0 0.1 0.1 19.2
2023-08-27 12:00:00 Bomberos-Tegucigalpa 0 0 7.3 18.8
El índice también se puede definir simultáneamente al crear el dataframe:
df = pd.DataFrame({’Estacion’:[’Los Platos’, ’Atima’, ’Bomberos-Tegucigalpa’],
’PCP 1h’:[0.0, 0.0, 0.0],
’PCP 6h’:[7.6, 0.1, 0.0],
’PCP 12h’:[11.7, 0.1, 7.3],
’PCP 24h’:[11.7, 19.2, 18.8]},
index = [datetime(2023,8,27,12),
datetime(2023,8,27,12),
datetime(2023,8,27,12)])
df
Estacion PCP 1h PCP 6h PCP 12h PCP 24h
2023-08-27 12:00:00 Los Platos 0 7.6 11.7 11.7
2023-08-27 12:00:00 Atima 0 0.1 0.1 19.2
2023-08-27 12:00:00 Bomberos-Tegucigalpa 0 0 7.3 18.8
Para eliminar el índice:
df.reset_index(inplace=True)
Observar que muchas funciones, como set_index, devuelven un dataframe como salida:
df.set_index(’Date’)
Estacion PCP 1h PCP 6h PCP 12h PCP 24h
2023-08-27 12:00:00 Los Platos 0 7.6 11.7 11.7
2023-08-27 12:00:00 Atima 0 0.1 0.1 19.2
2023-08-27 12:00:00 Bomberos-Tegucigalpa 0 0 7.3 18.8
pero no modifican el dataframe original:
df
Date Estacion PCP 1h PCP 6h PCP 12h PCP 24h
2023-08-27 12:00:00 Los Platos 0 7.6 11.7 11.7
2023-08-27 12:00:00 Atima 0 0.1 0.1 19.2
2023-08-27 12:00:00 Bomberos-Tegucigalpa 0 0 7.3 18.8
Para modificar el dataframe original hay que incluir la opción inplace o modificar el propio dataframe:
df.set_index(’Date’, inplace=True)
# o:
df = df.set_index(’Date’, inplace=False) # inplace toma el valor False por defecto
Es posible crear multiíndices, índices de más de un campo:
df = df.set_index([’Date’, ’Estacion’])
df
PCP 1h PCP 6h PCP 12h PCP 24h
(Timestamp(‘2023-08-27 12:00:00’), ‘Los Platos’) 0 7.6 11.7 11.7
(Timestamp(‘2023-08-27 12:00:00’), ‘Atima’) 0 0.1 0.1 19.2
(Timestamp(‘2023-08-27 12:00:00’), ‘Bomberos-Tegucigalpa’) 0 0 7.3 18.8
o en un formato string:
pd.date_range(start="20230801", end="20230803").format()
[’2023-08-01’, ’2023-08-02’, ’2023-08-03’]
En meteorología, es muy común tener datos ordenados por fechas. Pandas se adapta muy bien a esta necesidad, y nos
permite utilizar la fecha como índice de nuestros DataFrames. De hecho dispone de un objeto específico para ello, el
DataTimeIndex:
df = pd.DataFrame({’Tmax’:[20.3, 21.5, 20.1, 22.0]},
index = pd.date_range("20230801", "20230804"))
df
Tmax
2023-08-01 00:00:00 20.3
2023-08-02 00:00:00 21.5
2023-08-03 00:00:00 20.1
2023-08-04 00:00:00 22
El índice es un objeto DataTimeIndex:
df.index
DatetimeIndex([’2023-08-01’, ’2023-08-02’, ’2023-08-03’, ’2023-08-04’],
dtype=’datetime64[ns]’, freq=’D’)
que tiene sus métodos específicos, por ejemplo:
print(df.index.month)
print(df.index.day)
Int64Index([8, 8, 8, 8], dtype=’int64’)
Int64Index([1, 2, 3, 4], dtype=’int64’)
print(df.index)
DatetimeIndex([’2023-08-01’, ’2023-08-02’, ’2023-08-03’, ’2023-08-04’],
dtype=’datetime64[ns]’, freq=’D’)
columns devuelve una lista con el nombre de cada columna (por cierto, es un objeto índice). Observar que el índice ya
no se considera una columna:
print(df.columns)
print(df.columns[3])
Index([’Estado’, ’T (°C)’, ’ST (°C)’, ’HR ( %)’, ’Viento dirección’,
’Viento velocidad’, ’Presión’, ’Visibilidad’],
dtype=’object’)
HR ( %)
Estado object
T (°C) float64
ST (°C) object
HR ( %) int64
Viento dirección object
Viento velocidad object
Presión object
Visibilidad object
dtype: object
df.quantile(0.666, axis=1)
Fecha_Hora
2023-08-14 00:00:00 28.9172
2023-08-13 23:00:00 28.9840
2023-08-13 22:00:00 33.0468
2023-08-13 21:00:00 29.9172
2023-08-13 20:00:00 26.9546
2023-08-13 19:00:00 26.2218
2023-08-13 18:00:00 22.5264
2023-08-13 17:00:00 22.0608
2023-08-13 16:00:00 22.3948
2023-08-13 15:00:00 21.6286
2023-08-13 14:00:00 20.7622
2023-08-13 13:00:00 21.7268
2023-08-13 12:00:00 22.7916
2023-08-13 11:00:00 24.2886
2023-08-13 10:00:00 26.2512
2023-08-13 09:00:00 32.1764
2023-08-13 08:00:00 41.0642
2023-08-13 07:00:00 47.9560
2023-08-13 06:00:00 48.3214
2023-08-13 05:00:00 48.3548
2023-08-13 04:00:00 47.7556
2023-08-13 03:00:00 49.1878
Name: 0.666, dtype: float64
Hay otras operaciones sencillas que nos pueden resultar útiles. La traspuesta (T):
df.T
2023-08-14 2023-08-13 2023-08-13 ... 2023-08-13 2023-08-13
00:00:00 23:00:00 22:00:00 04:00:00 03:00:00
Estado Nublado Nublado Nublado ... Despejado Despejado
T (°C) 12.8 13.0 13.2 ... 3.4 3.7
ST (°C) No se calcula No se calcula No se calcula ... No se calcula No se calcula
HR ( %) 37 37 43 ... 70 72
Viento Noroeste Noroeste Norte ... Calma Calma
dirección
Viento 9 km/h 16 km/h 7 km/h ... nan nan
velocidad
Presión 1014.8 hPa 1015.2 hPa 1015.6 hPa ... 1023.3 hPa 1023.7 hPa
Visibilidad 10 km 10 km 10 km ... 10 km 10 km
Y ordenar, ya sea por índice o por columna (sort_index y sort_values respectivamente). Recordar que
sort_index no cambia el dataframe, a no ser que añadamos la opción inplace=True
df.sort_index()
Para seleccionar una columna de un dataframe podemos utilizar el operador []. Esto nos devolverá una serie (reducimos
su dimensión):
s = df[’Estado’] # También podría ponerse s = df.Estado
s
Fecha_Hora
2023-08-14 00:00:00 Nublado
2023-08-13 23:00:00 Nublado
2023-08-13 22:00:00 Nublado
2023-08-13 21:00:00 Mayormente nublado
2023-08-13 20:00:00 Mayormente nublado
2023-08-13 19:00:00 Mayormente nublado
2023-08-13 18:00:00 Parcialmente nublado
2023-08-13 17:00:00 Parcialmente nublado
2023-08-13 16:00:00 Algo nublado
2023-08-13 15:00:00 Algo nublado
2023-08-13 14:00:00 Ligeramente nublado
2023-08-13 13:00:00 Ligeramente nublado
2023-08-13 12:00:00 Ligeramente nublado
2023-08-13 11:00:00 Ligeramente nublado
2023-08-13 10:00:00 Ligeramente nublado
2023-08-13 09:00:00 Ligeramente nublado
2023-08-13 08:00:00 Ligeramente nublado
2023-08-13 07:00:00 Despejado
2023-08-13 06:00:00 Despejado
2023-08-13 05:00:00 Despejado
2023-08-13 04:00:00 Despejado
2023-08-13 03:00:00 Despejado
Name: Estado, dtype: object
o ambos a la vez:
df.loc[’2023-08-13 12:00:00’:’2023-08-13 14:00:00’,
(’T (°C)’, ’Estado’)]
o también:
df.iloc[2, :]
Estado Nublado
T (°C) 13.2
ST (°C) No se calcula
HR ( %) 43
Viento dirección Norte
Viento velocidad 7 km/h
Presión 1015.6 hPa
Visibilidad 10 km
Name: 2023-08-13 22:00:00, dtype: object
df = df.reset_index()
df.loc[2:4, (’T (°C)’, ’Estado’)]
T (°C) Estado
2 13.2 Nublado
3 13.8 Mayormente nublado
4 14.9 Mayormente nublado
df.iloc[2:5, 1:3]
Estado T (°C)
2 Nublado 13.2
3 Mayormente nublado 13.8
4 Mayormente nublado 14.9
También se pueden hacer selecciones discontinuas, como haríamos en python estándar:
df.iloc[[1,2,4], [2,5]]
T (°C) Viento dirección
1 13 Noroeste
2 13.2 Norte
4 14.9 Norte
E incluso un elemento:
df.iloc[0,0]
Timestamp(’2023-08-14 00:00:00’)
Aunque para seleccionar elementos es mejor utilizar el método at (o el iat), que son más rápidos y eficientes:
df = df.set_index(’Fecha_Hora’)
df.at[’2023-08-13 23:00:00’, ’HR (%)’]
37
df.iat[1,4]
’Noroeste’
Otra forma de realizar selecciones es mediante la indexación booleana, que nos permite seleccionar valores de acuer-
do a una condición relativa al dataframe.
Por ejemplo, si queremos seleccionar las filas para las que la GHI es mayor de 500, tenemos que usar esta condición:
df.index.hour==15
array([False, False, False, False, False, False, False, False, False,
True, False, False, False, False, False, False, False, False,
False, False, False, False])
Fecha_Hora T (°C) HR ( %)
2023-08-14 00:00:00 False True
2023-08-13 23:00:00 False True
2023-08-13 22:00:00 False False
2023-08-13 21:00:00 False True
2023-08-13 20:00:00 False True
2023-08-13 19:00:00 False True
2023-08-13 18:00:00 True True
2023-08-13 17:00:00 True True
2023-08-13 16:00:00 True True
2023-08-13 15:00:00 True True
2023-08-13 14:00:00 True True
2023-08-13 13:00:00 True True
2023-08-13 12:00:00 False True
2023-08-13 11:00:00 False True
2023-08-13 10:00:00 False True
2023-08-13 09:00:00 False False
2023-08-13 08:00:00 False False
2023-08-13 07:00:00 False False
2023-08-13 06:00:00 False False
2023-08-13 05:00:00 False False
2023-08-13 04:00:00 False False
2023-08-13 03:00:00 False False
df_num[(df_num>15) & (df_num<40)]
Fecha_Hora T (°C) HR ( %)
2023-08-14 00:00:00 nan 37
2023-08-13 23:00:00 nan 37
2023-08-13 22:00:00 nan nan
2023-08-13 21:00:00 nan 38
2023-08-13 20:00:00 nan 33
2023-08-13 19:00:00 nan 32
2023-08-13 18:00:00 15.6 26
2023-08-13 17:00:00 16.2 25
2023-08-13 16:00:00 17.2 25
2023-08-13 15:00:00 16.9 24
2023-08-13 14:00:00 16.3 23
2023-08-13 13:00:00 15.2 25
2023-08-13 12:00:00 nan 27
2023-08-13 11:00:00 nan 30
2023-08-13 10:00:00 nan 34
2023-08-13 09:00:00 nan nan
2023-08-13 08:00:00 nan nan
2023-08-13 07:00:00 nan nan
2023-08-13 06:00:00 nan nan
2023-08-13 05:00:00 nan nan
2023-08-13 04:00:00 nan nan
2023-08-13 03:00:00 nan nan
Se pueden dar valores a filas, columnas o elementos determinados aplicando la sintaxis y métodos anteriores:
df.loc[’2023-08-13 09:00:00.0’:’2023-08-13 11:00:00.0’,
(’T (°C)’, ’HR (%)’)] = 5.0
df
Para aplicar una función a toda una columna usaremos la función apply:
from numpy import sqrt
df[’T (°C)’].apply(np.sqrt)
Fecha_Hora
2023-08-14 00:00:00 3.577709
2023-08-13 23:00:00 3.605551
2023-08-13 22:00:00 3.633180
2023-08-13 21:00:00 3.714835
2023-08-13 20:00:00 3.860052
2023-08-13 19:00:00 3.834058
2023-08-13 18:00:00 3.949684
2023-08-13 17:00:00 4.024922
2023-08-13 16:00:00 4.147288
2023-08-13 15:00:00 4.110961
2023-08-13 14:00:00 4.037326
2023-08-13 13:00:00 3.898718
2023-08-13 12:00:00 3.794733
2023-08-13 11:00:00 2.236068
2023-08-13 10:00:00 2.236068
2023-08-13 09:00:00 2.236068
2023-08-13 08:00:00 2.302173
2023-08-13 07:00:00 2.000000
2023-08-13 06:00:00 1.760682
2023-08-13 05:00:00 1.788854
2023-08-13 04:00:00 1.843909
2023-08-13 03:00:00 1.923538
Name: T (°C), dtype: float64
Hay una gran cantidad de funciones para modificar columnas. Por ejemplo, si queremos formatear la velocidad del
viento, y convertirla en un valor numérico para poder operar con ella, podemos hacer lo siguiente:
df[’Viento velocidad’].str.slice(stop=-5).apply(float)
Fecha_Hora
2023-08-14 00:00:00 9.0
2023-08-13 23:00:00 16.0
2023-08-13 22:00:00 7.0
2023-08-13 21:00:00 11.0
2023-08-13 20:00:00 9.0
encoding=”ISO-8859-1” indica que no estamos utilizando las letras del inglés, sino las del español, (así pode-
mos usar tildes).
header=0 indica que la primera línea es la cabecera.
usecols=[0,3] indica que tomamos la primera (la fecha) y la cuarta columna (la temperatura).
Notar que solo ha tomado la primera columna, la fecha, como índice
Observar que read_csv nos permite hacer conversiones automáticamente, por ejemplo nos permite crear el índice a la
vez que leemos el fichero. Pero podemos hacer esto mismo en dos pasos:
df = pd.read_csv("buenos_aires_20230813.txt",
sep=",", encoding = "ISO-8859-1", header=0, usecols=[0,3])
df = df.set_index(’Fecha’)
Aunque read_csv trata de convertir al tipo adecuado cada columna (por ejemplo para floats), no hace esta conversión
para objetos tipo datetime. Para ello podemos usar el argumento parse_dates, que además nos permite combinar el
campo de la fecha y de la hora y así obtener un campo datetime bien formado:
df = pd.read_csv("buenos_aires_20230813.txt", parse_dates=[[0,1]], sep=",",
index_col=0, encoding = "ISO-8859-1")
# También podríamos haber tomado parse_dates=[[’Fecha’, ’Hora’]]
df
Fecha_Hora Estado T ST (°C) HR Viento Viento Pre- Visi-
(°C) ( %) dirección velocidad sión bilidad
2023-08-14 Nublado 12.8 No se 37 Noroeste 9 km/h 1014.8 10 km
00:00:00 calcula hPa
2023-08-13 Nublado 13 No se 37 Noroeste 16 km/h 1015.2 10 km
23:00:00 calcula hPa
2023-08-13 Nublado 13.2 No se 43 Norte 7 km/h 1015.6 10 km
22:00:00 calcula hPa
2023-08-13 Mayormente 13.8 No se 38 Norte 11 km/h 1015 10 km
21:00:00 nublado calcula hPa
2023-08-13 Mayormente 14.9 No se 33 Norte 9 km/h 1015.2 10 km
20:00:00 nublado calcula hPa
2023-08-13 Mayormente 14.7 No se 32 Norte 13 km/h 1015.6 10 km
19:00:00 nublado calcula hPa
... ... ... ... ... ... ... ... ...
2023-08-13 Despejado 3.7 No se 72 Calma nan 1023.7 10 km
03:00:00 calcula hPa
Otra alternativa es hacer esta conversión separadamente, utilizando la función to_datetime:
df = pd.read_csv("buenos_aires_20230813.txt", sep=",", encoding = "ISO-8859-1")
df["Fecha_Hora"] = pd.to_datetime(df["Fecha"] + " " + df["Hora"], format="%d/%m/%Y %H:%M")
df
También disponemos del argumento converters para forzar cambios de tipo que read_csv no haga apropiadamente.
O, un poco más corto:
Nota: En general read_csv espera que el fichero csv tenga siempre el número de columnas correcto. De todas formas,
el argumento on_bad_lines y warn_bad_lines puede ayudarnos a controlar ficheros mal formateados, hasta cierto
punto.
También existe la función read_fwf, que permite leer ficheros donde no hay separadores, sino que cada columna tiene
una longitud fija.
Para volcar un dataframe a un fichero utilizaremos el método to_csv. Tiene argumentos parecidos.
Además de ficheros csv, Pandas es capaz de leer directamente muchos otros formatos, como HDF (read_hdf ), json
(read_json) o SQL (read_sql), o tablas de html (read_html).
En particular, es capaz de leer ficheros Excel también (utiliza el módulo xlrd, que no es parte de la librería estándar).
Las funciones isnull y notnull nos dirán si un valor es igual a NaN. ¡No hagáis comparaciones del tipo
x==np.nan!
pd.isnull(df)
Se pueden eliminar las filas con valores NaN con el método dropna:
df.dropna() # Notar que para que se guarden los cambios debemos incluir inplace=True
Fecha_Hora Estado T ST HR Viento Viento Pre- Visi- Vel.Viento
(°C) (°C) ( %) dirección velocidad sión bilidad
2023-08-14 Nublado 12.8 No se 37 Noroeste 9 km/h 1014.8 10 km 9
00:00:00 calcula hPa
2023-08-13 Nublado 13 No se 37 Noroeste 16 km/h 1015.2 10 km 16
23:00:00 calcula hPa
... ... ... ... ... ... ... ... ... ...
2023-08-13 Ligera- 8.6 7.5 44 Noroeste 7 km/h 1022.9 10 km 7
09:00:00 mente hPa
nublado
2023-08-13 Ligera- 5.3 No se 59 Norte 3 km/h 1023.2 10 km 3
08:00:00 mente calcula hPa
nublado
2023-08-13 Despejado 4 No se 70 Oeste 3 km/h 1023.2 10 km 3
07:00:00 calcula hPa
Nota: Para comprobar si un dataframe está vacío utilizaremos el atributo empty
Para rellenar con otros valores utilizaremos el método fillna. Podemos rellenar con un valor concreto,
df.fillna(-999.0)
Fecha_Hora Estado T ST HR Viento Viento Pre- Visi- Vel.Viento
(°C) (°C) ( %) dirección velocidad sión bilidad
... ... ... ... ... ... ... ... ... ...
2023-08-13 Ligera- 5.3 No se 59 Norte 3 km/h 1023.2 10 km 3
08:00:00 mente calcula hPa
nublado
2023-08-13 Despejado 4 No se 70 Oeste 3 km/h 1023.2 10 km 3
07:00:00 calcula hPa
2023-08-13 Despejado 3.1 No se 71 Calma -999.0 1022.8 10 km -999
06:00:00 calcula hPa
2023-08-13 Despejado 3.2 No se 71 Calma -999.0 1022.8 10 km -999
05:00:00 calcula hPa
2023-08-13 Despejado 3.4 No se 70 Calma -999.0 1023.3 10 km -999
04:00:00 calcula hPa
2023-08-13 Despejado 3.7 No se 72 Calma -999.0 1023.7 10 km -999
03:00:00 calcula hPa
o utilizar un método; por ejemplo rellenando con el próximo valor no nulo:
df.fillna(method=’bfill’)
También se puede interpolar entre los valores no nulos más cercanos, utilizando el método interpolate.
Ejercicio
El reindexado es un método que suele producir muchos valores nulos, que después habrá que rellenar de forma
adecuada. Puedes practicar los métodos anteriores en el dataframe dfmissing, que obtenemos con este código
que previamente ha leído datos de Buenos Aires cada 3 horas, y ha rellenado las horas faltantes con nulos:
La segunda forma consiste en unir los dos dataframes por medio de un campo común, de forma similar a como se hace
en el lenguaje SQL, utilizado habitualmente en bases de datos relacionales. Indicaremos los dos dataframes a unir, y
el campo (o campos) respectivo que usaremos como nexo de unión:
df = pd.merge(dfobs, dfpred, left_on=[’Estacion’, ’Date’], right_on=[’Estacion’, ’Date’])
7.10. Agrupar
Agrupar datos es un proceso con tres partes diferenciadas:
División de los datos en grupos según un cierto criterio
Aplicación de una función a cada uno de los grupos de forma independiente
Combinación de los grupos para formar una nueva estructura de datos
Esta operación proviene también del lenguaje SQL, donde se utiliza la expresión “group by” para hacer lo mismo.
Partimos de nuestro dataframe de ejemplo con 6 observaciones (2 observaciones para 3 estaciones):
dfobs
7.10. Agrupar 77
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Nota: En realidad esto es “azúcar sintáctico”, creado para facilitarnos la escritura de código. Los grupos no se crean
hasta que no son utilizados.
Es posible iterar el objeto groupby para ver qué contiene cada grupo:
for name, group in grouped_st:
print("*** Grupo %s ***"%str(name))
print(group)
* Grupo Atima *
PCP 1h PCP 6h PCP 12h PCP 24h
Estacion Date
Atima 2023-08-27 12:00:00 0.0 0.1 0.1 19.2
2023-08-28 02:00:00 0.0 0.4 8.3 8.4
* Grupo Bomberos-Tegucigalpa *
PCP 1h PCP 6h PCP 12h PCP 24h
Estacion Date
Bomberos-Tegucigalpa 2023-08-27 12:00:00 0.0 0.0 7.3 18.8
2023-08-28 02:00:00 0.0 0.0 64.0 71.3
* Grupo Los Platos *
PCP 1h PCP 6h PCP 12h PCP 24h
Estacion Date
Los Platos 2023-08-27 12:00:00 0.0 7.6 11.7 11.7
2023-08-28 02:00:00 1.5 1.5 1.5 13.2
grouped_pcp10 = dfobs.groupby(PCP_higher_than_10(dfobs))
for name, group in grouped_pcp10:
print("*** Grupo %s ***"%str(name))
print(group)
* Grupo False *
PCP 1h PCP 6h PCP 12h PCP 24h
Estacion Date
Atima 2023-08-27 12:00:00 0.0 0.1 0.1 19.2
Bomberos-Tegucigalpa 2023-08-27 12:00:00 0.0 0.0 7.3 18.8
Los Platos 2023-08-28 02:00:00 1.5 1.5 1.5 13.2
Atima 2023-08-28 02:00:00 0.0 0.4 8.3 8.4
* Grupo True *
PCP 1h PCP 6h PCP 12h PCP 24h
Estacion Date
Los Platos 2023-08-27 12:00:00 0.0 7.6 11.7 11.7
Bomberos-Tegucigalpa 2023-08-28 02:00:00 0.0 0.0 64.0 71.3
Además de las filas, también se pueden dividir las columnas, utilizando el argumento axis. Se puede encontrar una
lista detallada de las claves posibles en https://pandas.pydata.org/pandas-docs/stable/groupby.html
El atributo groups es un diccionario cuyas claves son los nombres de los grupos, y los valores las etiquetas del eje
correspondiente a cada grupo:
grouped_st.groups
{’Atima’: [(’Atima’, 2023-08-27 12:00:00),
(’Atima’, 2023-08-28 02:00:00)],
’Bomberos-Tegucigalpa’: [(’Bomberos-Tegucigalpa’, 2023-08-27 12:00:00),
(’Bomberos-Tegucigalpa’, 2023-08-28 02:00:00)],
’Los Platos’: [(’Los Platos’, 2023-08-27 12:00:00),
(’Los Platos’, 2023-08-28 02:00:00)]}
7.10. Agrupar 79
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Y también aplicar diferentes funciones a distintas columnas, por medio del método agg (o aggregate):
grouped_st.agg({’PCP 12h’: np.mean, ’PCP 24h’: np.std})
PCP 12h PCP 24h
Estacion
Atima 4.2 7.63675
Bomberos-Tegucigalpa 35.65 37.1231
Los Platos 6.6 1.06066
o si renombramos para que quede más bonito:
grouped_st.agg({’PCP 12h’: np.mean, ’PCP 24h’: np.std}).rename(
columns={’PCP 12h’:’mean_PCP 12h’, ’PCP 24h’: ’std_PCP 24h’})
mean_PCP 12h std_PCP 24h
Estacion
Atima 4.2 7.63675
Bomberos-Tegucigalpa 35.65 37.1231
Los Platos 6.6 1.06066
Ejercicio
Para este ejercicio utilizaremos datos de un fichero csv de la estación de Atlacomulco, extraído de la web del
Servicio Meteorológico Nacional de México. Realice las siguientes operaciones:
1. Cargue el fichero en un dataframe de Pandas, convirtiendo las fechas a Timestamps, y tomando la fecha
local como índice (pista: tendrá que saltarse las primeras líneas de metadatos con la opción skiprows).
2. Seleccione los registros con una temperatura mayor de 20 °C y humedad relativa menor de 40 %
3. Seleccione los registros que corresponden a las 12 horas hora local únicamente (para cada día).
4. Calcule la media y desviación estandar de la radiación solar
5. Cree dos dataframes nuevos, el primero incluyendo solo los datos correspondientes al día 25 de agosto y
el segundo los datos del día 27 de agosto.
6. Una los dos dataframes en uno (con concat)
7. Divida el dataframe en tres más pequeños, de forma que cada uno contenga información únicamente sobre
la temperatura, humedad relativa y radiación solar, respectivamente.
8. Vuelva a unir los tres dataframes en uno (con merge).
9. Calcule la temperatura máxima y mínima para cada día, del 24 al 28 de agosto (pista: tendrá que agrupar
con groupby).
dfobs
PCP 1h PCP 6h PCP 12h PCP 24h
Estacion Date
Los Platos 2023-08-27 12:00:00 0 7.6 11.7 11.7
Atima 2023-08-27 12:00:00 0 0.1 0.1 19.2
Bomberos-Tegucigalpa 2023-08-27 12:00:00 0 0 7.3 18.8
Los Platos 2023-08-28 02:00:00 1.5 1.5 1.5 13.2
Atima 2023-08-28 02:00:00 0 0.4 8.3 8.4
Bomberos-Tegucigalpa 2023-08-28 02:00:00 0 0 64 71.3
El método stack “comprime” una de las columnas de un dataframe. El método inverso es unstack.
dfobs.stack()
Estacion Date
Los Platos 2023-08-27 12:00:00 PCP 1h 0.0
PCP 6h 7.6
PCP 12h 11.7
PCP 24h 11.7
Atima 2023-08-27 12:00:00 PCP 1h 0.0
PCP 6h 0.1
PCP 12h 0.1
PCP 24h 19.2
Bomberos-Tegucigalpa 2023-08-27 12:00:00 PCP 1h 0.0
PCP 6h 0.0
PCP 12h 7.3
PCP 24h 18.8
Los Platos 2023-08-28 02:00:00 PCP 1h 1.5
PCP 6h 1.5
PCP 12h 1.5
PCP 24h 13.2
Atima 2023-08-28 02:00:00 PCP 1h 0.0
PCP 6h 0.4
PCP 12h 8.3
PCP 24h 8.4
Bomberos-Tegucigalpa 2023-08-28 02:00:00 PCP 1h 0.0
PCP 6h 0.0
PCP 12h 64.0
PCP 24h 71.3
dtype: float64
Para “pivotar” un dataframe usaremos la función pivot_table:
dfobs = dfobs.reset_index()
df_pivot = pd.pivot_table(dfobs, values=’PCP 12h’, index=’Date’, columns=’Estacion’)
df_pivot
Date Atima Bomberos-Tegucigalpa Los Platos
2023-08-27 12:00:00 0.1 7.3 11.7
2023-08-28 02:00:00 8.3 64 1.5
Se puede pivotar más de una columna:
pd.pivot_table(dfobs, values=[’PCP 1h’, ’PCP 6h’, ’PCP 12h’, ’PCP 24h’],
index=’Date’, columns=’Estacion’)
7.12. Gráficos
El objeto dataframe dispone de un método plot para realizar gráficos. Este método es simplemente un wrapper de la
función plot de matplolib.pyplot, y se usa de forma similar:
df = pd.read_csv("buenos_aires_20230813.txt", parse_dates=[[’Fecha’, ’Hora’]],
sep=",", encoding = "ISO-8859-1")
df["T (°C)"].plot()
83
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Simplemente hemos especificado las coordenadas x e y de los puntos que queremos conectar con líneas utilizando dos
listas y todo lo demás ha funcionado solo.
Matplotlib ha realizado una serie de acciones por defecto como crear la figura (lienzo), crear una gráfica en el lienzo,
asignar valores al eje X, etc.
Podemos especificar estas coordenadas como una función:
x = np.linspace(0,15,30)
y = np.sin(x)
plt.plot(x,y)
Un Axes es lo que identificamos como una gráfica. Puede ser una línea, un histograma, etc. A través de este objeto
podemos acceder y modificar la apariencia, los ejes, etiquetas, título, leyenda, etc, por medio de distintos métodos.
fig = plt.figure() # creamos un objeto Figure
ax = plt.subplot() # cremos un objeto Axes
# Método explícito
El primer ejemplo, el método implícito, parece más claro y sencillo. Lo que está ocurriendo es que todos los métodos
del objeto Axes están disponibles directamente desde pyplot. Así pues cuando ejecutamos plt.plot(), pyplot
llama internamente a ax.plot(). Esto da un nivel extra de sencillez pero reduce en parte la versatilidad. En cambio
en el segundo ejemplo se usa el método explícito, llamando directamente a un objeto Axes.
Conclusión: para ejemplos sencillos y rápidos podemos recurrir a la versión “abreviada” o implícita, más fácil de usar.
Pero cuando queramos realizar gráficas más elaboradas es recomendable utilizar la versión explícita, que nos da más
control sobre los diferentes elementos individuales.
Cuando copiemos código de los ejemplos nos encontraremos en este segundo caso. Con objeto de poder entender y
reutilizar dicho código, en el resto de este tutorial usaremos la forma explícita.
8.4. Estilos
Usaremos plot para dibujar una línea. Podemos modificar el estilo cambiando el estilo de línea, su grosor o su color,
por ejemplo. Esto se puede hacer con argumentos de la función plot:
x = np.linspace(0, 10, 100)
fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(x, np.sin(x), linestyle=’--’, linewidth=4, color=’red’)
# Esto es idéntico, usando RGB:
#ax.plot(x, np.sin(x), linestyle=’--’, linewidth=4, color=(1.0, 0.0, 0.0))
Además de líneas, hay otros muchos gráficos que se pueden utilizar en matplotlib. Por ejemplo, aquí representamos
algunos datos de estaciones extraídos de la web de ONAMET (Oficina Nacional Meteorológica de la República Do-
minicana) por medio de marcadores (markers) en forma de rombos, gracias al método ax.scatter:
df = pd.read_csv("onamet_20230824.csv", sep=",", skiprows=4, encoding = "utf-8")
df =df[4:10] # Nos quedamos con seis filas para simplificar
fig, ax = plt.subplots()
ax.scatter(df["ESTACIÓN"], df["LLUVIA (mm)"], marker="D")
plt.show()
Se puede modificar también el estilo de dibujo. Por ejemplo para usar el estilo seaborn:
plt.style.use([’seaborn’])
x = np.linspace(0, 10, 100)
fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(x, np.sin(x), linestyle=’--’, linewidth=4, color=’red’)
8.4. Estilos 89
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Podemos establecer únicamente el límite inferior o superior y dejar que matplotlib se encargue del resto:
fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(x, np.sin(x))
ax.set_xlim(right=6)
ax.set_ylim(top=2)
Matplotlib nos da también métodos para dar título de la gráfica (ax.set_title), a las etiquetas de los ejes
(ax.set_xlabel y ax.set_ylabel), y a la frecuencia de las marcas y sus etiquetas (ax.xticks y
ax.yticks). Alternativamente se puede usar el objeto axis, con la función ax.axis.set_ticks.
Si los incluímos en el diagrama de barras del apartado anterior:
fig, ax = plt.subplots()
ax.bar(df["ESTACIÓN"], df["LLUVIA (mm)"])
ax.set_ylabel(’Lluvia (mm)’)
ax.set_xlabel(’Estación’)
ax.set_xticks(range(0, len(df["ESTACIÓN"])), df["ESTACIÓN"], rotation=45, ha="right")
# o también:
# ax.xaxis.set_ticks(range(0, len(df["ESTACIÓN"])), df["ESTACIÓN"], rotation=45, ha="right")
ax.set_title("Lluvia para el día 24 de agosto de 2023")
plt.show()
En el caso de las etiquetas de las marcas (los “ticks”) observar que el primer argumento indica la posición de cada
marca, y el segundo el texto de cada etiqueta:
list(range(0, len(df["ESTACIÓN"]))) # Las barras están en las posiciones 0 a 5
[0, 1, 2, 3, 4, 5]
df["ESTACIÓN"]
4 ARPT. DEL CIBAO
5 ARPT. EL CATEY
6 ENRIQUILLO
7 CABRERA
8 VILLA RIVA
9 LA VEGA
Name: ESTACIÓN, dtype: object
Además hemos girado las etiquetas para evitar que se pisen unas a otras (argumento opcional rotation), y las hemos
desplazado a la derecha (argumento opcional ha) para que estén bien situadas bajo su marca.
Hay una gran variedad de formatos de ticks posibles (por ejemplo se pueden dibujar ticks mayores y menores). En
la documentación se puede encontrar una explicación más detallada (por ejemplo, para usar fechas como etiquetas se
puede consultar https://matplotlib.org/stable/gallery/text_labels_and_annotations/date.html).
Matplotlib puede utilizar el lenguaje de markup TeX para escribir expresiones matemáticas (en el título, una etiqueta,
la leyenda, etc). Para ello basta con poner la expresión en código TeX dentro de signos de dólar($).
Por ejemplo, la expresión de TeX $e^{i\pi}+1=0$ queda así:
Veamos un caso práctico en el que tomamos datos de una semana de la estación de Atlacomulco de la web del Servicio
Meteorológico Nacional de México:
df = pd.read_csv("Estacion_ATLACOMULCO_1_semana.csv", parse_dates=[0], skiprows=9,
sep=",", encoding = "ISO-8859-1", index_col=[0])
df = df.sort_index()
Si queremos representar la radiación, que tiene como unidad watios por metro cuadrado, podemos incluir el símbolo
del cuadrado fácilmente con TeX:
import matplotlib.dates as mdates
fig, ax = plt.subplots(figsize=(12,4))
ax.plot(df.index, df[’Radiación Solar (W/m2)’])
ax.set_ylabel(’Radiación ($W/m^2$)’) # Con los $, escribe el cuadrado correctamente
ax.set_xlabel(’t’)
myFmt = mdates.DateFormatter(’%m-%d %H:%M’) # Ejemplo de formateo de fechas para xticks
ax.xaxis.set_major_formatter(myFmt)
8.6. Leyenda
El método ax.legend nos da la opción de incluir una leyenda. Hay distintos argumentos para modificar el contenido
de la leyenda y su posición en el gráfico (poniéndola incluso fuera de él). Usando el ejemplo anterior:
fig, ax = plt.subplots(figsize=(12,4))
ax.plot(df.index, df[’Rapidez de viento (km/h)’], label=’Rapidez de viento’)
ax.plot(df.index, df[’Temperatura del Aire (°C)’], label=’Temperatura’)
ax.legend()
plt.show()
8.7. Anotaciones
Para añadir una anotación sobre la propia gráfica se puede utilizar ax.annotate:
import matplotlib.pyplot as plt
from numpy import sqrt, meshgrid, arange
eq1 = ((x/7)**2*sqrt(abs(abs(x)-3)/(abs(x)-3))
+(y/3)**2*sqrt(abs(y+3/7*sqrt(33))/(y+3/7*sqrt(33)))-1)
eq2 = (abs(x/2)-((3*sqrt(33)-7)/112)*x**2-3+sqrt(1-(abs(abs(x)-2)-1)**2)-y)
eq3 = (9*sqrt(abs((abs(x)-1)*(abs(x)-.75))/((1-abs(x))*(abs(x)-.75)))-8*abs(x)-y)
eq4 = (3*abs(x)+.75*sqrt(abs((abs(x)-.75)*(abs(x)-.5))/((.75-abs(x))*(abs(x)-.5)))-y)
eq5 = (2.25*sqrt(abs((x-.5)*(x+.5))/((.5-x)*(.5+x)))-y)
eq6 = (6*sqrt(10)/7+(1.5-.5*abs(x))*sqrt(abs(abs(x)-1)/(abs(x)-1))
-(6*sqrt(10)/14)*sqrt(4-(abs(x)-1)**2)-y)
for f in [eq1,eq2,eq3,eq4,eq5,eq6]:
ax.contour(x, y, f, [0])
plt.show()
8.7. Anotaciones 93
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Ejercicio
Utilizar el fichero de Buenos Aires (buenos_aires_20230813.txt) para representar en una misma gráfica la tem-
peratura y la humedad relativa, compartiendo el eje X (eje de tiempo). Incluir una leyenda con los dos parámetros.
8.9. Subplots
Cada Figure puede contener varios Axes, se conocen como subaxes o subplots.
Cuando añadimos una gráfica a la figura podemos especificar cuantas filas y columnas queremos, y que posición ocupa
la gráfica actual, con los argumentos de subplot(ncol, nrow, n).
Hay varias maneras de añadir y modificar gráficas en una figura. Veamos las más comunes:
fig, axes = plt.subplots(1, 3) # devuelve una Figure y una 3-tupla de Axes
...
ax = fig.add_axes([.6, .6, .2, .2]) # añade Axe con geometria [left, bottom, width, height]
for i in [0,1]:
for j in [0,1]:
axes[i,j].plot(df[parameter[i][j]], color=parcolors[i][j])
axes[i,j].set_title(parameter[i][j])
axes[i,j].set_xticks([]) # quito las etiquetas del eje X para que no molesten
Otras instrucciones útiles: plt.subplots_adjust nos permite jugar con el espaciado entre gráficas. Si tenemos
problemas con el ajuste de espacios podemos usar plt.tight_layout, que realiza un auto ajuste.
fig.savefig(’atlacomulco.ps’)
fig, ax = plt.subplots(figsize=(12,3))
pc = ax.imshow(df_rad, cmap=’plasma’, aspect=’auto’,
norm=mpl.colors.Normalize(vmin=0, vmax=1000))
ax.set_yticks(range(0, len(df_rad.index)),df_rad.index)
ax.set_xlabel("hora")
ax.set_ylabel("día")
ax.set_title("Radiación Solar en Atlacomulco ($W/m^2$)")
fig.colorbar(pc)
Warning: Notar que en este caso hemos cometido un error, al tomar la observación de la hora en punto como
representativa de toda la hora.
Dado que tenemos observaciones cada 10 minutos, podemos afinar incluso más:
df[’minuto’] = df.index.minute
df_rad_min = pd.pivot_table(df, values=’Radiación Solar (W/m2)’,
index=’dia’, columns=[’hora’, ’minuto’])
fig, ax = plt.subplots(figsize=(12,3))
pc = ax.imshow(df_rad_min, cmap=’plasma’, aspect=’auto’,
norm=mpl.colors.Normalize(vmin=0, vmax=1000))
xFmt = mdates.DateFormatter(’%H’)
yFmt = mdates.DateFormatter(’%Y-%m-%d’)
ax.xaxis.set_major_formatter(xFmt)
ax.yaxis.set_major_formatter(yFmt)
ax.set_xticks(range(0, len(xtickvalues),6),xtickvalues[range(0, len(xtickvalues),6)])
ax.set_yticks(range(0, len(df_rad_min.index)),df_rad_min.index)
ax.set_xlabel("hora")
ax.set_ylabel("día")
ax.set_title("Radiación Solar en Atlacomulco ($W/m^2$)")
fig.colorbar(pc)
Como diría un buen gallego, depende. Depende del gráfico que estemos generando, pero en general para empezar
escoged PNG. Una breve explicación sobre formatos que puede aclarar algunas ideas:
El formato GIF está limitado a 256 colores y es un formato de compresión sin pérdida de información, una
elección habitual para su uso en la Web. GIF es una buena opción para guardar dibujos de líneas, texto o iconos
con un tamaño de fichero pequeño.
El formato PNG es también un formato de compresión sin pérdida de información, una elección habitual para
su uso en la Web. PNG es una buena opción para guardar dibujos de líneas, texto o iconos con un tamaño de
fichero pequeño.
El formato JPG es un formato de compresión con pérdida de información. Esto lo hace útil para almacenar
fotografías con un tamaño menor que un BMP. JPG es una opción habitual en la Web porque está comprimido.
Para guardar dibujos de líneas, texto, e iconos con un tamaño de fichero menor GIF o PNG son mejores opciones
porque no tienen pérdidas.
JPEGs son para fotografías e imágenes realistas. PNGs son para dibujos con líneas, imágenes con una gran cantidad
de texto e imágenes con pocos colores. GIFs no sirven en general.
Este consejo va más enfocado al dibujo de mapas, donde la generación de gráficos puede llevar bastante tiempo. Si
tenemos que crear una gran cantidad de gráficos o mapas veremos que los procesos llevan bastante tiempo. Si dichos
gráficos tienen características en común, por ejemplo el mapa de fondo (proyección, líneas de costa, colores, extensión)
es adecuado reutilizar dicho trabajo para los gráficos siguientes.
Una opción es limpiar parte de la figura que hemos realizado en lugar de construirla de nuevo. De este modo única-
mente cambiamos los datos sobre el gráfico de fondo ya creado.
Cabe también pensar en la serialización de objetos a través del módulo pickle, para ser reutilizados posteriormente.
En general, no solo para la generación de gráficos, lo primero es hacer que nuestro programa funcione, lo segundo es
optimizarlo.
Pandas es una librería muy útil para tratar datos tabulados, en dos dimensiones, pero no es tan conveniente para tres
dimensiones o más. Sin embargo en meteorología es muy habitual tratar con datos multidimensionales, por ejemplo
en campos meteorológicos que dependen de las tres coordenadas espaciales y del tiempo.
Xarray (https://docs.xarray.dev) es una librería construida sobre Numpy que permite tratar datos multidimensionales
con etiquetas, y comparte muchas de las características de Pandas. Hace que los datos sean más legibles tratándolos
de forma sencilla y eficiente.
Además está estrechamente relacionado con Cartopy, otra librería especializada en el dibujo de mapas.
open_dataset es una función de xarray que nos permite abrir ficheros netCDF (o URLs de un servidor remoto
OpenDAP). El formato netCDF se utiliza comúnmente en meteorología para guardar información de los modelos
meteorológicos (junto al formato grib), y xarray está especialmente adaptado para leerlo.
Para ilustrar xarray vamos a utilizar aquí varios ficheros del reanálisis ERA5
(https://www.ecmwf.int/en/forecasts/dataset/ecmwf-reanalysis-v5), descargados del Climate Data Store
del C3S (Climate Change Service) del programa Copernicus. Se puede encontrar más información en
https://climate.copernicus.eu/climate-reanalysis.
ds = xr.open_dataset(’curso_PIBM/era5_america_2022.nc’)
open_dataset abre un fichero netcDF y nos devuelve una de las dos estructuras básicas de datos de xarray, el
Dataset. El Dataset es una especie de diccionario de DataArrays (otra estructura que veremos después):
Si se quiere abrir varios ficheros netCDF simultáneamente y cargarlos en un único dataset (por ejemplo, si tenemos 24
ficheros netCDF, uno por hora de predicción) podemos usar la función open_mfdataset. Para liberar los recursos
usaremos close.
99
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Nota: Por otra parte, si se quieren abrir ficheros con formato grib con xarray es necesario instalar previamente la
librería cfgrib y dar al argumento engine de open_dataset el valor ‘cfgrib’ para que abra el fichero correctamente.
Veamos que contiene esta estructura de datos:
ds
<xarray.Dataset>
Dimensions: (longitude: 361, latitude: 373, time: 568)
Coordinates:
* longitude (longitude) float32 -120.0 -119.8 -119.5 ... -30.5 -30.25 -30.0
* latitude (latitude) float32 33.0 32.75 32.5 32.25 ... -59.5 -59.75 -60.0
* time (time) datetime64[ns] 2022-01-05 ... 2022-12-30T21:00:00
Data variables:
t2m (time, latitude, longitude) float32 ...
sst (time, latitude, longitude) float32 ...
Attributes:
Conventions: CF-1.6
history: 2023-09-02 16:18:24 GMT by grib_to_netcdf-2.25.1: /opt/ecmw...
Dentro de un dataset tenemos distintos atributos que nos informan sobre su contenido: variables, coordenadas, y
dimensiones (los ejes de nuestros datos):
ds.data_vars
Data variables:
t2m (time, latitude, longitude) float32 ...
sst (time, latitude, longitude) float32 ...
ds.coords
Coordinates:
* longitude (longitude) float32 -120.0 -119.8 -119.5 ... -30.5 -30.25 -30.0
* latitude (latitude) float32 33.0 32.75 32.5 32.25 ... -59.5 -59.75 -60.0
* time (time) datetime64[ns] 2022-01-05 ... 2022-12-30T21:00:00
ds.dims
Frozen({’longitude’: 361, ’latitude’: 373, ’time’: 568})
Vemos que este fichero contiene información sobre dos variables, la temperatura a 2 metros (t2m) y la temperatura
superficial del agua del mar (sst), para la región comprendida entre las longitudes -120 y 30 W, y entre las latitudes 33
N y 60 S (Centroamérica y Sudamérica, aproximadamente), para el año 2022.
Las variables del dataset son datos de tipo DataArray y se puede acceder ellos de la misma forma que se accede a las
columnas en Pandas:
ds[’t2m’]
o, de forma equivalente:
ds.t2m
<xarray.DataArray ’t2m’ (time: 568, latitude: 373, longitude: 361)>
[76482904 values with dtype=float32]
Coordinates:
* longitude (longitude) float32 -120.0 -119.8 -119.5 ... -30.5 -30.25 -30.0
* latitude (latitude) float32 33.0 32.75 32.5 32.25 ... -59.5 -59.75 -60.0
* time (time) datetime64[ns] 2022-01-05 ... 2022-12-30T21:00:00
Attributes:
units: K
long_name: 2 metre temperature
En este caso tenemos dos dimensiones espaciales (latitud y longitud), y la temporal
ds.t2m.dims
(’time’, ’latitude’, ’longitude’)
ds.t2m.shape
(568, 373, 361)
Dentro de un datarray tenemos metadatos y datos. Además de los metadatos anteriores podemos guardar otros
metadatos adicionales en attrs:
ds.t2m.attrs
{’units’: ’K’, ’long_name’: ’2 metre temperature’}
272.8537 ],
[275.15402, 275.19138, 275.22992, ..., 272.814 , 272.8245 ,
272.83618]],
...,
Se puede seleccionar un subconjunto de este darray de varias formas. La primera es utilizando la posición en el array,
como haríamos con Numpy:
t2m[1, 10:13, 20:25]
Xarray dispone de dos operadores muy fáciles de usar que permiten hacer selecciones:
isel : selecciona por posición
sel : selecciona por valores o etiquetas
# Selecciona los datos que corresponden al segundo valor del tiempo
# (para el primero sería time=0)
t2m.isel(time=1)
# Lo mismo que antes, pero seleccionando por valor
tt=’2022-01-05T03:00:00’
t2m.sel(time=tt)
Se pueden seleccionar varias dimensiones. Y también rangos, por medio de la función slice:
# Selección de una región geográfica
# (en ERA5 la latitud se almacena invertida)
t2m.sel(time=tt, latitude=slice(30.0, 20.0), longitude=slice(-90.0,-80.0))
# También pueden encadenarse. El resultado es el mismo:
t2m.sel(time=tt).sel(latitude=slice(30.0,20.0).sel(longitude=slice(-90.0,-80.0))
Si el valor específico de la dimensión no está en los datos se puede usar method para que busque el valor más próximo
que sí esté en los datos:
t2m.sel(time=tt).sel(latitude=20.34, method=’nearest’)
method no solo tiene la opción nearest, también tiene otras (pad, ffill, backfill, bfill).
9.3. Gráficos
El método plot nos permite dibujar un datarray. Aquí vamos a dar unas cuantas “pinceladas” y mostrar algunas de
sus capacidades, pero xarray puede hacer muchas cosas más. Si se quiere profundizar más se puede consultar esta
página: https://docs.xarray.dev/en/stable/user-guide/plotting.html.
Xarray tiene una propiedad muy interesante a la hora de pintar un gráfico: intenta deducir a partir de las dimensiones
de los datos qué tipo de dibujo queremos.
Por ejemplo si eliminamos la dimensión temporal, plot representará un mapa:
t2m.isel(time=0).plot(size=4, vmax=30)
xarray hace buena parte del trabajo por nosotros (añadiendo por defecto etiquetas y la barra de colores). El ajuste de
la escala de color es automático, pero podemos usar los argumentos vmin/vmax para ajustarlos.
Observar que al ajustar el nivel de temperatura máxima vmax=30, la barra de color indica que hay puntos por encima
de ella acabando en un triángulo, en lugar de con un rectángulo
xarray ofrece una gran versatilidad a la hora de pintar gráficos. Por ejemplo, con el argumento levels podemos elegir
pintar intervalos de forma discreta:
t2m.isel(time=0).plot(size=4, levels=8)
Si seleccionamos la dimensión tiempo y la coordenada vertical en el fichero con niveles volvemos a obtener un mapa:
¿Qué ocurre si eliminamos otra coordenada, por ejemplo la latitud, en lugar del nivel? En ese caso obtenemos un corte
Podemos utilizar los objetos Figure y Axes como hacíamos en matplotlib si queremos tener un mayor control sobre el
gráfico:
fig, ax = plt.subplots()
# Personalizamos el gráfico:
ax.grid(True, color=’black’, linestyle=’--’)
ax.set_xlabel(’Longitud’)
ax.set_ylabel(’Latitud’)
ax.set_title(’Mapa temperatura a 2m el 17 de septiembre de 2010’)
También podemos variar la dimensión tiempo y dejar el resto constantes. Vamos a hacer esto con nuestro fichero
original del continente, tomando las coordenadas de Santo Domingo (aproximadamente 18.5N 70W):
fig, ax = plt.subplots(figsize=(10,4))
t2m.sel(latitude=18.5, longitude=-69.9, method=’nearest’).plot(ax=ax)
ax.set_title("Temperatura en Santo Domingo en 2022")
9.3.1. Facetas
Las facetas nos permiten dividir un array según una dimensión y pintar cada grupo resultante, reduciendo en uno la
dimensión a dibujar. Por ejemplo, si tenemos como dimensiones la latitud, la longitud y el tiempo, podemos hacer
“faceting” en el tiempo, pintando un mapa por cada instante de tiempo. No es recomendable utilizar esta opción si van
a crearse demasiadas gráficas.
En el siguiente ejemplo tenemos como dimensiones la latitud, longitud, nivel de presión y tiempo. Hacemos “faceting”
en nivel de presión y tiempo (argumentos col y row). Como tenemos 5 niveles y 3 tiempos diferentes, nos salen 5 * 3
= 15 gráficas:
ds_karl_sfc
tem_karl = ds_karl_lev.t - 273.15
tem_karl.isel(time=[8,12,16,20,24]).sel(level=[700,850,1000]).plot(col=’time’, row=’level’)
9.4. Máscaras
Las máscaras permiten elegir un subconjunto de los datos queremos usar. A diferencia de las funciones isel() y sel(),
una máscara preserva las dimensiones originales de los datos. La función where nos permite hacer esta selección,
aceptando una o más condiciones. Esta función puede encadenarse a otras (como suele suceder en general). Cuando
se encadenen varias funciones es importante tener en cuenta el orden para optimizar recursos.
La condición de la máscara puede depender de los valores de los datos, o de los valores de las coordenadas. En el
ejemplo siguiente queremos superponer varios campos, y evitar que la lluvia nos tape el campo de la máscara de tierra-
mar (land sea mask), que está debajo. Para ello imponemos que solo se seleccionen los puntos con una precipitación
mayor de 1mm:
tp = ds_karl_sfc[’tp’]*1000.0 # Precipitación en mm
tpval = tp.where(tp > 1)
lsm = ds_karl_sfc[’lsm’]
lsm = lsm.where(lsm > 0.5) # Puntos de tierra
msl = ds_karl_sfc[’msl’]
lsm.sel(time=’2010-09-17T06:00:00’).plot(cmap=’Pastel2’, figsize=(8,5))
tpval.sel(time=’2010-09-17T06:00:00’).plot(cmap=’cool’, levels=6)
msl.sel(time=’2010-09-17T06:00:00’).plot.contour()
Se puede rellenar la zona enmascarada con valores preestablecidos. Esto se indica con el segundo argumento de
where.
Las máscaras pueden depender también de las coordenadas. En el siguiente ejemplo se pinta la precipitación y el
contorno de la presión a nivel del mar para las 6 UTC del día 17 de septiembre, pero solo para zonas al oeste del
meridiano 90W, y se completa la gráfica pintando la precipitación y la presión para las 9 UTC del día 15 de septiembre
para el resto de zonas (al este del meridiano 90W). El efecto que produce es el de “dos huracanes Karl”: antes y
después de cruzar la península del Yucatán (se muestra la gráfica solo para mostrar las capacidades de xarray, pero
esto probablemente no tenga mucho sentido).
post_t = ’2010-09-17T06:00:00’
pre_t = ’2010-09-15T09:00:00’
lsm.sel(time=post_t).plot(cmap=’Pastel2’, figsize=(8,5))
tpval.sel(time=post_t).where(tpval.longitude<-90,
tpval.sel(time=pre_t)).plot(cmap=’cool’, levels=6)
msl.sel(time=post_t).where(msl.longitude<-90, msl.sel(time=pre_t)).plot.contour()
plt.axvline(x=-90, color=’k’, linestyle=’--’)
Notar que el campo máscara de tierra-mar toma valores un valor 0 en el mar y 1 en tierra, lo que nos puede servir para
discriminar qué coordenadas corresponden a tierra o mar. En este ejemplo pintamos la temperatura a 2m, pero solo
para los puntos de tierra:
t2m_karl_sfc = ds_karl_sfc[’t2m’] - 273.15
t2m_karl_sfc.isel(time=0).where(lsm.isel(time=0)>0.5).plot(cmap=’jet’)
9.5. Estadísticas
Xarray puede calcular todo tipo de estadísticos, como Pandas. Por ejemplo, si queremos calcular la media:
t2m.mean()
Notar que es un datarray de un solo elemento, porque hemos hecho la media de todas las dimensiones (tiempo, latitud
y longitud). Podemos extraer el valor con item:
t2m.mean().item()
19.10647964477539
Si se quiere calcular el estadístico solo sobre algunas dimensiones las podemos especificar con el argumento dim.
Por ejemplo, si se quiere calcular el percentil 10 para la dimensión tiempo (o sea, el valor que solo tiene por debajo un
10 % de los datos para cada latitud y longitud), y pintar el mapa resultante:
t2m.quantile(0.1, dim=’time’).plot(size=4)
Recordar que podemos ver las coordenadas del datarray con coords:
t2m.coords
Coordinates:
* longitude (longitude) float32 -120.0 -119.8 -119.5 ... -30.5 -30.25 -30.0
* latitude (latitude) float32 33.0 32.75 32.5 32.25 ... -59.5 -59.75 -60.0
* time (time) datetime64[ns] 2022-01-05 ... 2022-12-30T21:00:00
Ejercicio
Calcular y pintar un mapa con el máximo intervalo de temperaturas para cada latitud y longitud con los datos de
ERA5 de 2022.
Calcular el mínimo de la temperatura en tierra durante el episodio del huracán Karl.
Solucion –> solucion_intervalo.py
9.6. Computación
Xarray nos da varios métodos para hacer cálculos más avanzados. A modo de ejemplo, vamos a explicar uno de ellos,
las agrupaciones (hay varios más).
9.6.1. Agrupaciones
De la misma forma que en pandas, podemos separar y combinar datos de acuerdo a un cierto criterio. Para ello se
pueden usar los métodos groupby y groupby_bins. Vamos a mostrarlo con dos ejemplos.
En el primer ejemplo, vamos a agrupar los datos de temperatura para Santo Domingo por cada mes y calculamos la
media:
t2m_stdom = t2m.sel(latitude=18.5, longitude=-69.9, method=’nearest’)
fig, ax = plt.subplots(figsize=(10,4))
t2m_stdom.groupby(’time.month’).mean().plot(ax=ax)
ax.set_title("Temperatura en Santo Domingo en 2022")
ax.set_ylim([18, 33])
Vemos que de esta forma eliminamos el ruido y vemos la variación de la temperatura durante todo el año más clara-
mente.
En el segundo ejemplo, agrupamos los datos de temperatura del continente por estación del año (season), creando un
objeto DataArrayGroupBy:
t2m.groupby("time.season")
DataArrayGroupBy, grouped over ’season’
4 groups with labels ’DJF’, ’JJA’, ’MAM’, ’SON’.
lst_axes = []
numx = 3
numy = 3
for i in range(0,numx*numy):
id_time = 2+4*i
lst_axes.append(fig.add_subplot(numx,numy,i+1,projection=ccrs.PlateCarree()))
ax = lst_axes[i]
ax.add_feature(cfeature.LAND, color=’sandybrown’)
ax.add_feature(cfeature.OCEAN, color=’lightcyan’)
ax.add_feature(cfeature.COASTLINE, edgecolor=’black’, lw=0.5)
plt.tight_layout()
plt.show()
ax1 = fig.add_subplot(2,2,1,projection=ccrs.PlateCarree())
ax2 = fig.add_subplot(2,2,2,projection=ccrs.Orthographic(central_latitude=0,
central_longitude=-80))
ax3 = fig.add_subplot(2,2,3,projection=ccrs.Mollweide(central_longitude=-80))
ax4 = fig.add_subplot(2,2,4,projection=ccrs.Sinusoidal(central_longitude=-80))
temp = t2m.isel(time=0)
for ax in (ax1,ax2,ax3,ax4) :
temp.plot(ax=ax, transform=ccrs.PlateCarree(),cmap=’jet’)
ax.set_global()
gl = ax.gridlines(color=’#888888’, draw_labels=True, x_inline=False, y_inline=False)
gl.top_labels=False
gl.right_labels=False
ax.add_feature(cfeature.COASTLINE, edgecolor=’black’, lw=0.5)
plt.tight_layout()
plt.show()
A continuación se enumeran algunas fuentes gratuitas de datos meteorológicos que pueden ser de interés:
Datos del modelo del Centro Europeo de Predicciones a Medio Plazo (ECMWF):
Web: https://www.ecmwf.int/en/forecasts/datasets/wmo-essential, https://www.ecmwf.int/en/forecasts/datasets/wmo-
additional
El ECMWF ofrece a los servicios meteorológicos miembros de la Organización Meteorológica Mundial (OMM)
varios productos de su modelo de forma gratuita (algunos de ellos también disponibles para el público).
Datos de CAMS del ECMWF (predicciones de aerosoles, contaminantes, gases de efecto invernadero, ozono
estratosférico e índice ultravioleta):
Web: https://atmosphere.copernicus.eu/global-forecast-plots
Además de las gráficas disponibles se pueden descargar datos automáticamente a travé de su API.
Datos del reanálisis ERA5 del ECMWF, explicado en más detalle en el siguiente apartado. Parte del C3S de
Copernicus.
Datos de predicción estacional del C3S de Copernicus
Web: https://climate.copernicus.eu/seasonal-forecasts
119
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
Hay que darse de alta como usuario previamente. Se pueden descargar datos de superficie o de niveles de presión,
entre otros. Por ejemplo, en esta página se descargan los datos de superficie:
Basta con indicar qué variables, periodo de tiempo y región se quieren descargar, así como el formato (grib o netCDF).
Para poder asimilar y afianzar los conocimientos adquiridos, en esta unidad vamos a realizar algunos prácticas.
1. Nos llegará un correo a nuestra bandeja de entrada y debemos confirmar que somos nosotros
2. Nos llegará un segundo correo con nuestra api_key
3. Ya tenemos nuestra api_key que tendrá un aspecto similar a este:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqbWudGVyb2dAYWVtZXQuZXMiLCJqdGkiOi...
Paso 2
1. Visitaremos en AEMET OpenData –> Acceso a desarrolladores –> Documentación HATEOAS la docu-
mentación, o podemos ir directamente a *HATEOAS*
121
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
2. Buscaremos la API de Datos de observación. Tiempo actual. que son Datos de observación horarios de las
últimas 24 horas todas las estaciones meteorológicas de las que se han recibido datos en ese período. Frecuencia
de actualización: continuamente., como nos indica AEMET OpenData.
3. Copiaremos la API, que es :
/api/observacion/convencional/todas
Paso 3
Estamos en disposición de crear nuestro programa en Python para recuperar los datos de observación.
Notar que son datos que se están actualizando constantemente y por tanto disponer de un programa que nos permita
recuperarlos es muy útil.
Para realizar una consulta a AEMET OpenData la URL completa sería :
/api/observacion/convencional/todas + ?api_key= + nuestra_api_key
El estado 200 quiere decir que nuestra consulta a AEMET OpenData ha tenido éxito y tenemos un enlace con los
datos para que nos los descarguemos. Comprobar en un programa que nuestro estado es un 200 es una más que
recomendable.
Paso 4
Ejercicio, opendata.py
Escribe un programa que recupere los datos de observación y los guarde en un fichero cuyo nombre contenga al
menos la fecha en la que se han consultado.
Avanzado, guarda tu api_key en una variable de entorno, así como el directorio donde quieres que se guarde el
fichero con los datos.
Pistas:
1. Guarda tu api_key en una variable.
2. Necesitas importar el módulo “request”, “datetime” y “os”.
3. AEMET OpenData dispone de programas ejemplo que te pueden ayudar.
Paso 4
Este paso es para explicar cómo resolver el ejercicio.
Escribimos con un editor de texto, nuestras primeras líneas, importando los módulos necesarios y guardando en vari-
ables algunas constantes.
# -- coding: utf-8 --
import os
import datetime
import request
url = "https://opendata.aemet.es/opendata/api/observacion/convencional/todas/"
akey = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqbWudGVyb2dAYWVtZXQuZXMiL..."
Ahora con “request” tenemos que descargar el JSON con el enlace de descarga de los datos. Este sería el modo de
hacerlo:
querystring = {"api_key": akey}
#Hacemos la llamada
response = requests.request("GET", url, headers=headers, params=querystring, verify=False)
print(response.text)
Para poder continuar y como pista, también necesitamos el módulo JSON que nos permitirá cargar la respuesta re-
sponse.text como un diccionario, de la siguiente forma:
import json
#Obtenemos el json de la respuesta
data = json.loads(response.text)
Para terminar, terminar con nuestro programa tenemos que comprobar que el estado es un 200 y recuperar los datos
en caso de que así sea, veamos como :
estado = data["estado"]
#Si el estado es 200 (éxito) obtenemos los datos y los guardamos en un fichero
if (estado == 200):
urlDatos = data["datos"]
datos = requests.request("GET", urlDatos, headers=headers,
params=querystring, verify=False)
exit(0)
Si queremos introducir la fecha actual al nombre del fichero con los datos de observación ‘fich_productos’, tendremos:
import datetime
d = datetime.datetime.now()
ft = "%Y%m%dT%H%M%S"
t = datetime.datetime.now().strftime(ft)
# 20220505T090424
fich_name = "observacion_" + t + ".json"
import json
fichero = ’datos_observacion.json’
def leer_archivo_json():
with open(fichero,’r’, encoding=’latin-1’) as file:
data = json.load(file)
return data
leer_archivo_json()
Paso 2
Pasar el nombre del fichero de entrada como un argumento en la ejecución del python.
leer_observacion.py datos_observacion.json
Se haría lo siguiente:
# -- coding: utf-8 --
import pytz
fichero = sys.argv[1]
def leer_archivo_json():
with open(fichero,’r’, encoding=’latin-1’) as file:
data = json.load(file)
return data
leer_archivo_json()
Paso 3
Paso 4
Ahora disponemos de un Python que lee un fichero JSON, que nos imprime todos los campos que encuentra y nos
permite seleccionar un grupo de ellos.
Paso 5
Llegados a este punto solo nos queda escribir un JSON con nuestra selección:
Ejercicio,
1. Escribimos un JSON que contenga todas las estaciones con los datos seleccionados.
2. Queremos que el nombre del fichero de salida contenga la fecha actual.
Paso 6
Por último, queremos almacenar las fechas como una lista de objetos fecha.
Disponemos de predicciones para el día 13 de agosto de 2023 realizadas con el modelo WRF para el cono sur del
continente, cortesía del Servicio Meteorológico Nacional de Argentina. Vamos a verificar estas predicciones frente a las
observaciones medidas durante ese día en algunas estaciones meteorológicas de INUMET, el Servicio Meteorológico
de Uruguay.
Utilizaremos las librerías anteriores para formatear y manejar todos estos datos, así como para mostrar resultados
numéricos y gráficas y mapas que nos ayuden a entender esta situación.
En la vida real no tiene sentido hacer una verificación de solo un día. Un periodo más largo, por ejemplo, un año,
sería más apropiado. Sin embargo el procedimiento a seguir sería muy similar, y solo variaría la cantidad de datos a
procesar, por lo que este puede ser un ejercicio interesante y útil.
Podemos distinguir tres tareas diferentes para conseguir nuestro objetivo, que especificamos en los siguientes aparta-
dos:
Paso 1: extraer y procesar las predicciones del modelo meteorológico
El Servicio Meteorológico Nacional de Argentina dispone de un estupendo repositorio en la Web donde se pueden
descargar las predicciones de su modelo WRF en formato netCDF, ya sea de forma manual, o automáticamente (esto
último sería preferible si fueramos a realizar las verificaciones periódicamente de forma automática, sin intervención
humana).
En la dirección https://odp-aws-smn.github.io/documentation_wrf_det/Acceso_a_los_datos/ se explican los pasos para
descargar los ficheros que se deseen. Entre otras alternativas, es posible utilizar una librería de python llamada s3fs
para realizar la descarga.
Como paso previo, se han descargado ya los ficheros a utilizar (instalando previamente la librería s3fs), por lo que
en la práctica ya disponemos de ellos en nuestro directorio local. A modo de curiosidad, este es el código que se ha
utilizado para ello (siguiendo las indicaciones de la página anterior):
import os
import s3fs
fs = s3fs.S3FileSystem(anon=True)
# Descargamos las predicciones entre las 9 y las 24 UTC (de h+9 a h+24)
for f in [’WRFDETAR_01H_20230813_00_%s.nc’%str(h).zfill(3) for h in range(9,25)]:
print("Descargando "+os.path.join(s3_dir, f))
fs.get(rpath=os.path.join(s3_dir, f),
lpath=os.path.join(aemet_dir, f))
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_009.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_010.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_011.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_012.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_013.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_014.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_015.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_016.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_017.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_018.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_019.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_020.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_021.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_022.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_023.nc
Descargando s3://smn-ar-wrf/DATA/WRF/DET/2023/08/13/00/WRFDETAR_01H_20230813_00_024.nc
1. Mostrar mapas en mosaico de la temperatura a 2 metros, la humedad relativa a 2 metros, y la velocidad del viento a
10 metros para las 9, 15 y 21 UTC.
2. Mostrar gráficas en mosaico del corte para la latitud xx de la temperatura a 2 metros, la humedad relativa a 2 metros,
y la velocidad del viento a 10 metros para las 9, 15 y 21 UTC.
3. Mostrar mapas con la temperatura media, máxima y mínima durante el día 13.
4. Extraer las predicciones de temperatura a 2 metros para la estación “Aeropuerto Melilla G3” (por ejemplo). Pista:
el alumno necesitará seguir las indicaciones de https://odp-aws-smn.github.io/documentation_wrf_det/Tutoriales/
Paso 2: formateo de las observaciones
Se han descargado observaciones correspondientes al día 13 de agosto de 2023 de 24 estaciones de INUMET desde su
página web, y se ha creado un fichero Excel con nombre inumet_20230813.xlsx para guardarlas.
Este fichero Excel consta de una hoja llamada “metadatos” que contiene información sobre cada estación (entre otros,
su nombre, longitud y latitud), y de 15 hojas más, una para cada hora del día (desde las 6 a las 21 hora local), con las
observaciones de todas las estaciones para esa hora.
En este apartado cargaremos en memoria estos datos con una estructura de datos (un dataframe de pandas) que nos
permita manejarlos cómodamente. Para ello el alumno debe seguir estos pasos:
1. Cargar el fichero Excel en un DataFrame. Pista: habrá que utilizar el argumento sheet_name en la función
pd.read_excel para identificar cada hoja. Los valientes pueden intentar usar la opción sheet_name=None, que
carga todas las hojas de golpe.
2. Crear una columna de tipo fecha-hora (“datetime” o “timestamp”), para que así cada registro este identificado por su
estación y su fecha y hora. Pista: se puede utilizar el módulo datetime, o alternativamente, la función pd.Timestamp
de pandas, que es muy similar.
3. Concatenar todas las horas en un gran dataframe
Paso 3: verificación de las predicciones
Ahora que ya tenemos observaciones y predicciones listas, podemos compararlas. El alumno tiene que realizar estas
tareas:
1. Crear un gráfico que muestre la evolución de la temperatura a 2 metros observada y predicha para la estación
“Aeropuerto Melilla G3” (por ejemplo) durante el día 13.
2. Calcular el sesgo y el error cuadrático medio de la predicción para esta estación.
Recordar que el sesgo y el error cuadrático medio son dos índices de verificación que se definen de esta forma:
Sesgo (o bias):
donde fi es cada una de las predicciones, oi cada una de las observaciones, y n el número de parejas obser-
vación/predicción
url = "https://opendata.aemet.es/opendata/api/observacion/convencional/todas/"
akey = "<clave que corresponda>"
d = datetime.datetime.now()
ft = "%Y%m%dT%H%M%S%z"
t = datetime.datetime.now().strftime(ft)
# 20220505T090424+0000
fich_name = "observacion_" + t + ".json"
#Hacemos la llamada
response = requests.request("GET", url, headers=headers, params=querystring, verify=False)
print(response.text)
estado = data["estado"]
#Si el estado es 200 (exito) obtenemos los datos y los guardamos en un fichero
if (estado == 200):
urlDatos = data["datos"]
datos = requests.request("GET", urlDatos, headers=headers,
params=querystring, verify=False)
129
Curso PIB-M 2023: Introducción a Python y a las librerías científicas,
exit(0)
import sys
import json
fichero = sys.argv[1]
def leer_archivo_json():
with open(fichero,’r’, encoding=’latin-1’) as file:
data = json.load(file)
return data
def obtener_campos_disponibles(data):
campos_todos = []
for item in data:
campos = list(item.keys())
for c in campos :
if c not in campos_todos:
campos_todos.append(c)
return campos_todos
def seleccionar_campos(campos):
print("Campos disponibles:")
for i, campo in enumerate(campos, 1):
print(f"{i}. {campo}")
seleccionados = []
opcion = input("Selecciona los campos que te interesen (separados por coma): ")
indices_seleccionados = opcion.split(",")
return seleccionados
Paso 4
# -*- coding: utf-8 -*-
import sys
import json
fichero = sys.argv[1]
def leer_archivo_json():
with open(fichero,’r’, encoding=’latin-1’) as file:
data = json.load(file)
return data
def obtener_campos_disponibles(data):
campos_todos = []
for item in data:
campos = list(item.keys())
for c in campos :
if c not in campos_todos:
campos_todos.append(c)
return campos_todos
def seleccionar_campos(campos):
print("Campos disponibles:")
for i, campo in enumerate(campos, 1):
if campo not in [’lon’,’lat’,’alt’,’idema’,’fint’]:
print(f"{i}. {campo}")
seleccionados = []
opcion = input("Selecciona los campos que te interesen (separados por coma): ")
indices_seleccionados = opcion.split(",")
return seleccionados_mas
import sys
import json
from datetime import datetime
fichero = sys.argv[1]
fich_salida = "resultado_" + datetime.now().strftime(’ %Y %m %d_ %H %M %S’) + ".json"
def leer_archivo_json():
with open(fichero,’r’, encoding=’latin-1’) as file:
data = json.load(file)
return data
def obtener_campos_disponibles(data):
campos_todos = []
for item in data:
campos = list(item.keys())
for c in campos :
if c not in campos_todos:
campos_todos.append(c)
return campos_todos
def seleccionar_campos(campos):
print("Campos disponibles:")
for i, campo in enumerate(campos, 1):
if campo not in [’lon’,’lat’,’alt’,’idema’,’fint’]:
print(f"{i}. {campo}")
seleccionados = []
opcion = input("Selecciona los campos que te interesen (separados por coma): ")
indices_seleccionados = opcion.split(",")
return seleccionados
Paso 6
# -*- coding: utf-8 -*-
import sys
import json
from datetime import datetime
import pytz
fichero = sys.argv[1]
def leer_archivo_json():
with open(fichero,’r’, encoding=’latin-1’) as file:
data = json.load(file)
fecha_objetos = []
for item in data:
fint_str = item.get(’fint’, ’’)
if fint_str:
fecha_utc = datetime.strptime(fint_str, ’ %Y- %m- %dT %H: %M: %S’).replace(tzinfo=pytz.UTC)
if fecha_utc not in fecha_objetos:
fecha_objetos.append(fecha_utc)
# item[’fint’] = fecha_utc
return data,fecha_objetos
def obtener_campos_disponibles(data):
campos_todos = []
for item in data:
campos = list(item.keys())
for c in campos :
if c not in campos_todos:
campos_todos.append(c)
return campos_todos
def seleccionar_campos(campos):
print("Campos disponibles:")
for i, campo in enumerate(campos, 1):
if campo not in [’lon’,’lat’,’alt’,’idema’,’fint’]:
print(f"{i}. {campo}")
seleccionados = []
opcion = input("Selecciona los campos que te interesen (separados por coma): ")
indices_seleccionados = opcion.split(",")
return seleccionados
2. Mostrar mapas en mosaico de la temperatura a 2 metros, la humedad relativa a 2 metros, y la velocidad del
viento a 10 metros para las 9, 15 y 21 UTC.
Podemos cargar primero todos los ficheros netCDF y concatenarlos para formar un dataset que contenga todos los
datos (open_mfdataset parece fallar aquí, no conozco el motivo):
for step in [str(i).zfill(3) for i in range(9,25)]:
if step == "009":
ds = xr.open_dataset(’WRFDETAR_01H_20230813_00_%s.nc’%step,
decode_coords = ’all’, engine = ’h5netcdf’)
else:
ds_step = xr.open_dataset(’WRFDETAR_01H_20230813_00_%s.nc’%step,
decode_coords = ’all’, engine = ’h5netcdf’)
ds = xr.concat([ds, ds_step], dim="time")
ds_step.close()
ds
Una forma de pintar todos los mapas de golpe es mediante un bucle que vaya escogiendo cada objeto Axes, de forma
que cada Axes corresponda a una hora y variable distintas:
fig = plt.figure(figsize=[15, 10])
lst_axes = []
numx = 3
numy = 3
for i in range(0,numx*numy):
lst_axes.append(fig.add_subplot(numx,numy,i+1))
ax = lst_axes[i]
variable = ds[variables[int(i/3)]]
variable.sel(time=id_times[i%3]).plot(ax=ax)
plt.tight_layout()
plt.show()
Otra alternativa es hacer un bucle más simple, que solo recorra las variables, y utilizar Faceting para recorrer la
dimensión tiempo:
variables = [’T2’, ’HR2’, ’magViento10’]
plt.show()
3. Mostrar gráficas en mosaico del corte para la latitud xx de la temperatura a 2 metros, la humedad relativa a 2
metros, y la velocidad del viento a 10 metros para las 9, 15 y 21 UTC.
Basta con eliminar la dimensión latitud, y ya sacamos las gráficas:
fig = plt.figure(figsize=[15, 10])
lst_axes = []
numx = 3
numy = 3
for i in range(0,numx*numy):
lst_axes.append(fig.add_subplot(numx,numy,i+1))
ax = lst_axes[i]
variable = ds[variables[int(i/3)]]
variable.sel(time=id_times[i%3]).sel(y=100, method=’nearest’).plot(ax=ax)
plt.tight_layout()
plt.show()
4. Mostrar mapas con la temperatura media, máxima y mínima durante el día 13.
Podemos ejecutar cada estadística individualmente, pintando cada una por separado:
ds.T2.mean(dim=’time’).plot(size=4)
ds.T2.max(dim=’time’).plot(size=4)
ds.T2.min(dim=’time’).plot(size=4)
Una alternativa más general (aunque quizás un poco extravagante) sería crear una función general que tenga como
argumento la función que queremos pintar:
def plot_statistic(name, func):
’’’
Función que pinta una estadística
Parameters
----------
name : str
Título de la gráfica
func : function
Función a pintar
’’’
func(dim=’time’).plot(size=4)
plt.title(name)
all_stats = {"Media": ds.T2.mean, "Máxima": ds.T2.max, "Mínima": ds.T2.min}
for st in all_stats:
plot_statistic(st, all_stats[st])
4. Extraer las predicciones de temperatura a 2 metros para la estación “Aeropuerto Melilla G3” (por ejemplo)
Hay un problema para extraer las predicciones: las coordenadas son x e y, los metros desde un determinado pun-
to, no la latitud y la longitud. Afortunadamente en uno de los tutoriales de la página de descarga (https://odp-aws-
smn.github.io/documentation_wrf_det/Get_lat_lon_fecha/) se da un ejemplo de cómo transformar las coordenadas
x/y a latitud/longitud.
Para ello hay que usar Cartopy, dado que el modelo meteorológico utiliza una proyección distinta, la proyección
Lambert, y con Cartopy podemos hacer la transformación. El código (copiado de esa página) es este:
Cargamos el fichero Excel en un dataframe de pandas. Notar que d_all es un diccionario de dataframes (uno por cada
pestaña del fichero Excel):
d_all.metadatos (la primera pestaña) es un dataframe de pandas con los metadatos de las estaciones:
d_all[’metadatos’][0:5]
Ahora concatenamos todos los dataframes creando uno más grande, mejor estructurado que antes. Así tenemos la
información en un único dataframe, no en un diccionario de muchos más pequeños:
df = pd.DataFrame()
for hour in [str(i).zfill(2) for i in range(6,21)]:
df = pd.concat([df, dfall[hour]])
Nos basta con unir los dataframes de predicciones y observaciones obtenidos antes:
df_verif = pd.merge(df_obs, df_fcst, on="Fecha")
df_verif
Ya podemos dibujar la gráfica:
fig, ax = plt.subplots(figsize=(10,5))
ax.set_ylabel(’$Temperatura (^{o}C)$’)
ax.set_xlabel(’Fecha’)
ax.set_title("Comparación observación y predicción en el Aeropuerto de Melilla")
ax.plot(df_verif[’Fecha’],
df_verif[’t2m pred’],
label=’Predicción Temperatura’)
ax.plot(df_verif[’Fecha’],
df_verif[’Temperatura del Aire [ºC]’],
label=’Observación Temperatura’)
ax.grid()
ax.legend()
Observamos que el modelo infraestima la temperatura máxima y mínima. Esto es bastante habitual en un modelo
meteorológico: notar que cada punto de un modelo representa un área bastante grande, mientras que una observación
es puntual. Suele haber problemas de representatividad. Esto se puede corregir en gran medida con un programa de
postproceso.
2. Calcular el sesgo y el error cuadrático medio de la predicción para esta estación.
df[’error’] = df_verif[’t2m pred’] - df_verif[’Temperatura del Aire [ºC]’]
sesgo = df.error.mean()
rmse = np.sqrt((df.error**2).mean())
print("sesgo=%f"%sesgo)
print("rmse=%f"%rmse)
sesgo=2.143853
rmse=2.772770
Efectivamente el sesgo es de 2 grados (como ya intuíamos al ver la gráfica). De todas formas esto ha ocurrido un día
concreto: para sacar buenas conclusiones habría que verificar un periodo mucho más largo, y para más estaciones.
Queremos expresar nuestro agradecimiento por los datos de observaciones tomados de las páginas web del Servi-
cio Meteorológico Nacional de Argentina, CENAOS (Centro de Estudios Atmosféricos, Oceanográficos y Sísmicos
de Honduras), INUMET (Servicio Meteorológico de Uruguay), ONAMET (Oficina Nacional Meteorológica de la
República Dominicana),y del Servicio Meteorológico Nacional de México, que se han usado en estas prácticas. Tam-
bién agradecemos los datos de predicciones del modelo WRF del Servicio Meteorológico Nacional de Argentina y los
datos de reanálisis tomados del servicio C3S de Copernicus.
Asimismo queremos reconocer la ayuda de nuestros compañeros de AEMET, especialmente de Marcos Gómez y
Ernesto Barrera. Y por supuesto nuestro agradecimiento a XKCD (https://xkcd.com) y sus cómics.
143