Clase 15: Módulos, Pandas y Animaciones

Importando módulos

Python tiene reglas para la importación de módulos a nuestro código. Recordemos que un módulo es un archivo cuya extensión es .py. Volviendo a usar el ejemplo de la clase anterior, tenemos un módulo rotacion_p.py.

  1. En primer lugar, el intérprete busca un módulo denominado rotación_p dentro de los módulos incorporados automáticamente. La lista de dichos módulos se encuentra usando el método sys.builtin_module_names:

import sys
sys.builtin_module_names
('_abc',
 '_ast',
 '_codecs',
 '_collections',
 '_functools',
 '_imp',
 '_io',
 '_locale',
 '_operator',
 '_signal',
 '_sre',
 '_stat',
 '_string',
 '_symtable',
 '_thread',
 '_tokenize',
 '_tracemalloc',
 '_typing',
 '_warnings',
 '_weakref',
 'atexit',
 'builtins',
 'errno',
 'faulthandler',
 'gc',
 'itertools',
 'marshal',
 'posix',
 'pwd',
 'sys',
 'time')
'rotacion_p' in sys.builtin_module_names
False
  1. En segundo lugar, busca un archivo rotacion_p.py en una lista de directorios dada por el atributo sys.path.

sys.path
['/Users/flavioc/Library/Mobile Documents/com~apple~CloudDocs/Documents/cursos/Python/GitLab/clase-python/clases',
 '/Users/flavioc/miniconda3/envs/clases/lib/python312.zip',
 '/Users/flavioc/miniconda3/envs/clases/lib/python3.12',
 '/Users/flavioc/miniconda3/envs/clases/lib/python3.12/lib-dynload',
 '',
 '/Users/flavioc/miniconda3/envs/clases/lib/python3.12/site-packages']

Este atributo contiene el directorio local (el primero que aparece en la lista de arriba), y una serie de directorios que provienen de - La variable de entorno PYTHONPATH - Un directorio dependiente de la instalación (en este caso, ’/Users/flavioc/miniconda3/envs/clases/lib/python3.12`).

La manera pythonística de chequear si la variable de entorno PYTHONPATH existe es utilizar el método get de os.environ:

import os
os.environ.get('PYTHONPATH')

En nuestro caso no imprime nada, pero de la misma forma se puede setear dicha variable de entorno:

os.environ['PYTHONPATH'] = '..' # seteo la variable PYTHONPATH al directorio padre del directorio actual
os.environ.get('PYTHONPATH')
'..'

Por supuesto, todo va a depender de cómo tenemos estructurado nuestro código. En principio, aún cuando uno no utilice completamente las facilidades de Python como lenguaje orientado a objeto, agrupamos funciones que están relacionadas entre sí en distintos módulos. A medida que el código crece, es posible organizar los distintos módulos distribuyéndolos en directorios.

Importando módulos de directorios hijos

Imaginemos que tenemos la siguente estructura de código:

/miproyecto
    main.py
    /lib
        rotacion.py
    /graficos
        simple.py
        complejo.py
        /tresd
        vector.py

Es sencillo importar los módulos en los directorios hijos (lib, graficos):

from graficos.simple import plot_data
from graficos.complejo import plot_data_complejo as plot_complejo
from graficos.tresd.vector import *
from lib.rotacion import rotate

Básicamente exponemos el módulo usando el operador . para ir incorporando los hijos. Por ejemplo, graficos.tresd.vector refiere al módulo que se encuentra en el archivo vector.py en el directorio graficos.tresd.

Importando módulos desde padres o hermanos

Imports relativos

Para importar módulos que están en directorios padres o hermanos (estos últimos son directorios al mismo nivel del directorio desde el cual quiero importar) podemos diferentes estrategias. La primera de ellas es usar la importación de paquetes relativos. Para ello, cada directorio desde el que quiera importar debe poseer un archivo (en principio vacío) denominado __init__.py. Esto permite a Python reconocer los directorios que contienen módulos aún cuando sean padres o hermanos.

Veamos ahora la estructura de directorios de miproyecto_relativo con los archivos __init__.py agregados:

/miproyecto
    main.py
    __init__.py
    /lib
        rotacion.py
        __init__.py
    /graficos
        __init__.py
        simple.py
        complejo.py
        /tresd
        __init__.py
        vector.py
    /tests
        test_rotacion.py

Notemos que tests/test_rotacion.py tiene también un main, que corre un test:

def test_rotacion():
  v = np.array([1, 0, 0])
  angle = np.array([0, 0, np.pi/2])
  assert np.allclose(rotate(angle, v), np.array([0, -1, 0]))


if __name__ == "__main__":
  test_rotacion()
  print("All tests passed")

Esta es una estructura típica de Python, donde tengo tests que prueban funciones en un módulo dado. Notemos cómo se importa el módulo rotacion desde test_rotacion.py:

from ..lib.rotacion import rotate

Al igual que con directorios, .. se refiere al directorio padre relativo al directorio actual.

Si probamos desde el directorio miproyecto/tests lo siguiente:

python test_rotacion.py

Nos encontraremos con el error:

ImportError: attempted relative import with no known parent package

Para correr el test, tenemos que ir al directorio padre del proyecto y correr el main del módulo explícitamente:

python -m miproyecto_relativo.tests.test_rotacion
All tests passed

Modificando sys.path

La forma anterior puede ser engorrosa en el caso en que se tengan muchos módulos en archivos distribuidos en una estructura de directorios complicada. Por otra parte, es posible modificar el atributo sys.path para incluir el directorio que sea de interés. En este caso, modificamos test_rotacion.py:

import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from lib.rotacion import rotate

Entonces, podemos correr desde el directorio tests:

python test_rotacion.py
All tests passed

o desde su padre como

python -m tests.test_rotacion

o

python tests/test_rotacion.py

Pandas

Pandas es una biblioteca de Python ampliamente utilizada para análisis de datos y manipulación de datos tabulares. Proporciona estructuras de datos potentes y flexibles, como DataFrame y Series, que son fundamentales para trabajar con conjuntos de datos.

No es la única biblioteca de Python para manejar tablas de datos, pueden consultar otras como Polars, DuckDB, etc. Pero se ha convertido en un módulo muy popular.

La instalamos como siempre

pip install pandas
import pandas as pd

Pandas tiene dos tipos fundamentales de estructuras de datos:

  1. DataFrame: Una estructura de datos tabular bidimensional con etiquetas en filas y columnas.

  2. Series: Un arreglo unidimensional etiquetado capaz de contener cualquier tipo de datos.

# Crear una Serie a partir de una lista
s = pd.Series([1, 2, 3, 4, 5])
print(s)
0    1
1    2
2    3
3    4
4    5
dtype: int64

Notar que el print ya muestra los datos en forma adecuada, indicando los índices (columna de la izquierda) y el tipo de dato de la serie.

s.index, s.values
(RangeIndex(start=0, stop=5, step=1), array([1, 2, 3, 4, 5]))
import numpy as np

s_np = s.array # cast de la serie a un arreglo de numpy.
print(s_np)
np.sum(s_np)
<PandasArray>
[1, 2, 3, 4, 5]
Length: 5, dtype: int64
15
# Crear un DataFrame a partir de un diccionario
data = {'Nombre': ['Juan', 'María', 'Pedro', 'Ana'],
        'Edad': [25, 30, 35, 40]}
df = pd.DataFrame(data)
print(df)
  Nombre  Edad
0   Juan    25
1  María    30
2  Pedro    35
3    Ana    40

Podemos agregar columnas como

df['Ocupacion'] = ['Estudiante', 'Ingeniera', 'Doctor', 'Profesora']
print(df)
  Nombre  Edad   Ocupacion
0   Juan    25  Estudiante
1  María    30   Ingeniera
2  Pedro    35      Doctor
3    Ana    40   Profesora

Y podemos agregar una nueva fila:

eva = pd.DataFrame({'Nombre': ['Eva'], 'Edad': [28], 'Ocupacion': ['Ingeniera']})
df = pd.concat([df,eva], ignore_index=True)
print(df)
  Nombre  Edad   Ocupacion
0   Juan    25  Estudiante
1  María    30   Ingeniera
2  Pedro    35      Doctor
3    Ana    40   Profesora
4    Eva    28   Ingeniera

Nota

En versiones anteriores de pandas a 2.0, existe una método .append. Sin embargo, ese método ya no está visible en la clase.

Notar también que cada valor del diccionario es una lista. Si se eligiera usar los correspondientes valores escalares, es necesario indicar el índice en el cual se quiere concatenar la fila:

lio = pd.DataFrame({'Nombre': 'Lionel', 'Edad': 37, 'Ocupacion': 'Futbolista'})
df = pd.concat([df,eva], ignore_index=True)
print(df)
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

Cell In[8], line 1
----> 1 lio = pd.DataFrame({'Nombre': 'Lionel', 'Edad': 37, 'Ocupacion': 'Futbolista'})
      2 df = pd.concat([df,eva], ignore_index=True)
      3 print(df)


File /usr/lib64/python3.11/site-packages/pandas/core/frame.py:664, in DataFrame.__init__(self, data, index, columns, dtype, copy)
    658     mgr = self._init_mgr(
    659         data, axes={"index": index, "columns": columns}, dtype=dtype, copy=copy
    660     )
    662 elif isinstance(data, dict):
    663     # GH#38939 de facto copy defaults to False only in non-dict cases
--> 664     mgr = dict_to_mgr(data, index, columns, dtype=dtype, copy=copy, typ=manager)
    665 elif isinstance(data, ma.MaskedArray):
    666     import numpy.ma.mrecords as mrecords


File /usr/lib64/python3.11/site-packages/pandas/core/internals/construction.py:493, in dict_to_mgr(data, index, columns, dtype, typ, copy)
    489     else:
    490         # dtype check to exclude e.g. range objects, scalars
    491         arrays = [x.copy() if hasattr(x, "dtype") else x for x in arrays]
--> 493 return arrays_to_mgr(arrays, columns, index, dtype=dtype, typ=typ, consolidate=copy)


File /usr/lib64/python3.11/site-packages/pandas/core/internals/construction.py:118, in arrays_to_mgr(arrays, columns, index, dtype, verify_integrity, typ, consolidate)
    115 if verify_integrity:
    116     # figure out the index, if necessary
    117     if index is None:
--> 118         index = _extract_index(arrays)
    119     else:
    120         index = ensure_index(index)


File /usr/lib64/python3.11/site-packages/pandas/core/internals/construction.py:656, in _extract_index(data)
    653         raise ValueError("Per-column arrays must each be 1-dimensional")
    655 if not indexes and not raw_lengths:
--> 656     raise ValueError("If using all scalar values, you must pass an index")
    658 elif have_series:
    659     index = union_indexes(indexes)


ValueError: If using all scalar values, you must pass an index

Si en lugar de la lista, utilizamos un valor escalar tenemos que decirle en qué lugar (índice) queremos agregar el elemento

lio = pd.DataFrame({'Nombre': 'Lionel', 'Edad': 37, 'Ocupacion': 'Futbolista'}, index=[14])
df = pd.concat([df,lio])
print(df)
    Nombre  Edad   Ocupacion
0     Juan    25  Estudiante
1    María    30   Ingeniera
2    Pedro    35      Doctor
3      Ana    40   Profesora
4      Eva    28   Ingeniera
14  Lionel    37  Futbolista

Notemos que si usamos ignore_index=True, la fila se agrega al final del DataFrame, mientras que si no lo usamos (ignore_index=False), se mantiene el indice provisto por el dataframe particular (en este caso, 14).

print(df.head(2))
  Nombre  Edad   Ocupacion
0   Juan    25  Estudiante
1  María    30   Ingeniera

Es fácil guardar los datos:

df.to_csv('../data/lista_gente.csv', index=False)
!cat ../data/lista_gente.csv
Nombre,Edad,Ocupacion
Juan,25,Estudiante
María,30,Ingeniera
Pedro,35,Doctor
Ana,40,Profesora
Eva,28,Ingeniera
Lionel,37,Futbolista

Del mismo modo, se puede leer un csv:

tasa_natalidad = pd.read_csv('../data/tasa-natalidad.csv')
# print(tasa_natalidad)
tasa_natalidad
indice_tiempo natalidad_argentina natalidad_ciudad_autonoma_de_buenos_aires natalidad_buenos_aires natalidad_catamarca natalidad_cordoba natalidad_corrientes natalidad_chaco natalidad_chubut natalidad_entre_rios ... natalidad_neuquen natalidad_rio_negro natalidad_salta natalidad_san_juan natalidad_san_luis natalidad_santa_cruz natalidad_santa_fe natalidad_santiago_del_estero natalidad_tucuman natalidad_tierra_del_fuego
0 2000 19.0 14.3 17.5 25.8 17.2 22.7 25.8 19.4 21.3 ... 17.9 18.9 24.0 22.7 22.3 19.6 16.9 21.6 22.6 19.9
1 2001 18.2 13.9 16.9 24.9 15.9 21.9 22.2 18.4 20.5 ... 16.8 17.8 24.2 23.7 22.3 19.8 16.2 21.1 21.1 17.9
2 2002 18.3 13.6 17.0 24.4 16.6 23.3 24.9 17.1 19.5 ... 16.1 17.3 24.7 22.3 22.0 19.2 16.7 22.1 22.6 17.2
3 2003 18.4 14.2 17.3 22.5 17.5 22.2 20.9 18.7 19.8 ... 19.4 18.6 22.0 22.2 21.7 21.9 17.3 17.8 21.8 20.8
4 2004 19.3 14.9 18.5 20.6 17.8 22.7 25.1 19.3 19.5 ... 19.8 18.3 23.9 22.5 21.9 22.4 17.6 19.7 21.0 19.5
5 2005 18.5 14.5 17.9 19.7 17.1 20.2 22.6 19.2 19.0 ... 20.0 18.7 22.5 21.4 19.9 22.9 16.3 21.0 19.9 20.7
6 2006 17.9 14.6 17.7 18.3 16.5 18.7 19.6 20.0 17.2 ... 20.5 18.6 21.0 20.7 20.2 23.5 15.8 20.0 18.5 20.1
7 2007 17.8 14.1 17.7 18.3 16.3 18.9 18.4 20.2 16.9 ... 20.5 18.9 20.6 20.8 19.3 23.8 15.7 19.4 19.7 20.8
8 2008 18.8 15.1 18.6 18.6 17.4 19.7 20.8 21.4 17.2 ... 21.0 19.9 21.4 20.7 19.3 25.0 16.9 20.5 19.9 20.8
9 2009 18.6 14.6 18.4 17.4 17.4 19.9 20.4 21.3 17.5 ... 20.9 19.6 21.0 20.6 18.2 25.2 16.5 21.9 19.1 20.2
10 2010 18.7 14.9 18.9 16.9 17.2 19.8 21.2 21.2 17.4 ... 21.6 20.0 21.9 19.8 17.4 26.0 16.2 19.9 20.1 18.8
11 2011 18.5 14.8 18.8 16.0 16.9 19.9 22.6 20.7 17.2 ... 19.6 19.8 21.7 19.9 16.7 25.0 16.4 20.2 19.5 18.6
12 2012 17.9 14.2 18.1 15.0 16.5 18.6 20.2 20.2 16.7 ... 19.1 19.4 20.2 19.6 16.0 24.0 16.3 17.9 18.7 19.2
13 2013 17.9 14.3 17.8 16.9 16.0 19.0 19.9 18.5 17.3 ... 18.7 17.9 21.1 20.6 16.7 19.7 16.9 18.8 18.9 19.8
14 2014 18.2 14.3 17.9 17.4 16.8 19.8 20.2 17.8 17.8 ... 19.5 17.9 21.6 21.3 16.9 19.8 17.2 19.8 19.3 20.5
15 2015 17.9 13.7 17.3 17.2 16.4 19.3 22.7 17.4 17.8 ... 19.1 18.2 21.3 20.5 17.0 19.8 16.9 20.5 19.0 19.9
16 2016 16.7 13.1 16.2 16.7 15.7 18.5 19.1 16.6 16.6 ... 17.9 16.8 19.4 18.8 15.5 18.6 16.3 18.7 17.5 18.1
17 2017 16.0 11.7 15.4 15.8 15.0 18.3 19.8 15.3 16.3 ... 16.3 16.0 19.7 18.1 14.8 17.0 15.4 18.8 16.7 16.9
18 2018 15.4 11.6 14.6 16.4 14.5 18.0 21.0 14.3 15.6 ... 15.6 14.6 18.2 17.7 14.2 14.7 14.8 18.9 16.9 16.3

19 rows × 26 columns

Operaciones

Pandas provee de numerosos métodos para realizar operaciones sobre los datos de un DataFrame.

tasa_natalidad.head(2)
indice_tiempo natalidad_argentina natalidad_ciudad_autonoma_de_buenos_aires natalidad_buenos_aires natalidad_catamarca natalidad_cordoba natalidad_corrientes natalidad_chaco natalidad_chubut natalidad_entre_rios ... natalidad_neuquen natalidad_rio_negro natalidad_salta natalidad_san_juan natalidad_san_luis natalidad_santa_cruz natalidad_santa_fe natalidad_santiago_del_estero natalidad_tucuman natalidad_tierra_del_fuego
0 2000 19.0 14.3 17.5 25.8 17.2 22.7 25.8 19.4 21.3 ... 17.9 18.9 24.0 22.7 22.3 19.6 16.9 21.6 22.6 19.9
1 2001 18.2 13.9 16.9 24.9 15.9 21.9 22.2 18.4 20.5 ... 16.8 17.8 24.2 23.7 22.3 19.8 16.2 21.1 21.1 17.9

2 rows × 26 columns

tasa_natalidad["natalidad_rio_negro"].mean()
18.273684210526316
for v,d,i in zip(tasa_natalidad.columns, tasa_natalidad.max(),tasa_natalidad.idxmax()):
    provincia = v.split('_')[1]
    print(f"la tasa máxima {d} de {provincia} ocurrió en {i}")
la tasa máxima 2018.0 de tiempo ocurrió en 18
la tasa máxima 19.3 de argentina ocurrió en 4
la tasa máxima 15.1 de ciudad ocurrió en 8
la tasa máxima 18.9 de buenos ocurrió en 10
la tasa máxima 25.8 de catamarca ocurrió en 0
la tasa máxima 17.8 de cordoba ocurrió en 4
la tasa máxima 23.3 de corrientes ocurrió en 2
la tasa máxima 25.8 de chaco ocurrió en 0
la tasa máxima 21.4 de chubut ocurrió en 8
la tasa máxima 21.3 de entre ocurrió en 0
la tasa máxima 26.5 de formosa ocurrió en 4
la tasa máxima 23.1 de jujuy ocurrió en 1
la tasa máxima 18.3 de la ocurrió en 3
la tasa máxima 22.6 de la ocurrió en 0
la tasa máxima 20.2 de mendoza ocurrió en 8
la tasa máxima 26.4 de misiones ocurrió en 4
la tasa máxima 21.6 de neuquen ocurrió en 10
la tasa máxima 20.0 de rio ocurrió en 10
la tasa máxima 24.7 de salta ocurrió en 2
la tasa máxima 23.7 de san ocurrió en 1
la tasa máxima 22.3 de san ocurrió en 0
la tasa máxima 26.0 de santa ocurrió en 10
la tasa máxima 17.6 de santa ocurrió en 4
la tasa máxima 22.1 de santiago ocurrió en 2
la tasa máxima 22.6 de tucuman ocurrió en 0
la tasa máxima 20.8 de tierra ocurrió en 3

Nótese que la primer columna ‘indice_tiempo’ también la toma como una columna de datos, cuando en realidad, en este caso convendría que fuera efectivamente la columna que indexa la tabla. Para eso, tenemos set_index.

tasa_natalidad.set_index('indice_tiempo',inplace=True)
tasa_natalidad.head(3)
natalidad_argentina natalidad_ciudad_autonoma_de_buenos_aires natalidad_buenos_aires natalidad_catamarca natalidad_cordoba natalidad_corrientes natalidad_chaco natalidad_chubut natalidad_entre_rios natalidad_formosa ... natalidad_neuquen natalidad_rio_negro natalidad_salta natalidad_san_juan natalidad_san_luis natalidad_santa_cruz natalidad_santa_fe natalidad_santiago_del_estero natalidad_tucuman natalidad_tierra_del_fuego
indice_tiempo
2000 19.0 14.3 17.5 25.8 17.2 22.7 25.8 19.4 21.3 25.7 ... 17.9 18.9 24.0 22.7 22.3 19.6 16.9 21.6 22.6 19.9
2001 18.2 13.9 16.9 24.9 15.9 21.9 22.2 18.4 20.5 22.4 ... 16.8 17.8 24.2 23.7 22.3 19.8 16.2 21.1 21.1 17.9
2002 18.3 13.6 17.0 24.4 16.6 23.3 24.9 17.1 19.5 25.1 ... 16.1 17.3 24.7 22.3 22.0 19.2 16.7 22.1 22.6 17.2

3 rows × 25 columns

También se pueden hacer otras operaciones, como ordenar por columnas

tasa_natalidad.sort_values('natalidad_argentina', ascending=True)
natalidad_argentina natalidad_ciudad_autonoma_de_buenos_aires natalidad_buenos_aires natalidad_catamarca natalidad_cordoba natalidad_corrientes natalidad_chaco natalidad_chubut natalidad_entre_rios natalidad_formosa ... natalidad_neuquen natalidad_rio_negro natalidad_salta natalidad_san_juan natalidad_san_luis natalidad_santa_cruz natalidad_santa_fe natalidad_santiago_del_estero natalidad_tucuman natalidad_tierra_del_fuego
indice_tiempo
2018 15.4 11.6 14.6 16.4 14.5 18.0 21.0 14.3 15.6 19.8 ... 15.6 14.6 18.2 17.7 14.2 14.7 14.8 18.9 16.9 16.3
2017 16.0 11.7 15.4 15.8 15.0 18.3 19.8 15.3 16.3 19.6 ... 16.3 16.0 19.7 18.1 14.8 17.0 15.4 18.8 16.7 16.9
2016 16.7 13.1 16.2 16.7 15.7 18.5 19.1 16.6 16.6 19.4 ... 17.9 16.8 19.4 18.8 15.5 18.6 16.3 18.7 17.5 18.1
2007 17.8 14.1 17.7 18.3 16.3 18.9 18.4 20.2 16.9 21.1 ... 20.5 18.9 20.6 20.8 19.3 23.8 15.7 19.4 19.7 20.8
2006 17.9 14.6 17.7 18.3 16.5 18.7 19.6 20.0 17.2 21.4 ... 20.5 18.6 21.0 20.7 20.2 23.5 15.8 20.0 18.5 20.1
2015 17.9 13.7 17.3 17.2 16.4 19.3 22.7 17.4 17.8 21.3 ... 19.1 18.2 21.3 20.5 17.0 19.8 16.9 20.5 19.0 19.9
2012 17.9 14.2 18.1 15.0 16.5 18.6 20.2 20.2 16.7 21.0 ... 19.1 19.4 20.2 19.6 16.0 24.0 16.3 17.9 18.7 19.2
2013 17.9 14.3 17.8 16.9 16.0 19.0 19.9 18.5 17.3 21.0 ... 18.7 17.9 21.1 20.6 16.7 19.7 16.9 18.8 18.9 19.8
2001 18.2 13.9 16.9 24.9 15.9 21.9 22.2 18.4 20.5 22.4 ... 16.8 17.8 24.2 23.7 22.3 19.8 16.2 21.1 21.1 17.9
2014 18.2 14.3 17.9 17.4 16.8 19.8 20.2 17.8 17.8 21.8 ... 19.5 17.9 21.6 21.3 16.9 19.8 17.2 19.8 19.3 20.5
2002 18.3 13.6 17.0 24.4 16.6 23.3 24.9 17.1 19.5 25.1 ... 16.1 17.3 24.7 22.3 22.0 19.2 16.7 22.1 22.6 17.2
2003 18.4 14.2 17.3 22.5 17.5 22.2 20.9 18.7 19.8 25.0 ... 19.4 18.6 22.0 22.2 21.7 21.9 17.3 17.8 21.8 20.8
2005 18.5 14.5 17.9 19.7 17.1 20.2 22.6 19.2 19.0 23.5 ... 20.0 18.7 22.5 21.4 19.9 22.9 16.3 21.0 19.9 20.7
2011 18.5 14.8 18.8 16.0 16.9 19.9 22.6 20.7 17.2 21.6 ... 19.6 19.8 21.7 19.9 16.7 25.0 16.4 20.2 19.5 18.6
2009 18.6 14.6 18.4 17.4 17.4 19.9 20.4 21.3 17.5 21.9 ... 20.9 19.6 21.0 20.6 18.2 25.2 16.5 21.9 19.1 20.2
2010 18.7 14.9 18.9 16.9 17.2 19.8 21.2 21.2 17.4 21.1 ... 21.6 20.0 21.9 19.8 17.4 26.0 16.2 19.9 20.1 18.8
2008 18.8 15.1 18.6 18.6 17.4 19.7 20.8 21.4 17.2 22.6 ... 21.0 19.9 21.4 20.7 19.3 25.0 16.9 20.5 19.9 20.8
2000 19.0 14.3 17.5 25.8 17.2 22.7 25.8 19.4 21.3 25.7 ... 17.9 18.9 24.0 22.7 22.3 19.6 16.9 21.6 22.6 19.9
2004 19.3 14.9 18.5 20.6 17.8 22.7 25.1 19.3 19.5 26.5 ... 19.8 18.3 23.9 22.5 21.9 22.4 17.6 19.7 21.0 19.5

19 rows × 25 columns

También se pueden filtrar los datos

tasa_natalidad[tasa_natalidad['natalidad_argentina'] > 18.5]
natalidad_argentina natalidad_ciudad_autonoma_de_buenos_aires natalidad_buenos_aires natalidad_catamarca natalidad_cordoba natalidad_corrientes natalidad_chaco natalidad_chubut natalidad_entre_rios natalidad_formosa ... natalidad_neuquen natalidad_rio_negro natalidad_salta natalidad_san_juan natalidad_san_luis natalidad_santa_cruz natalidad_santa_fe natalidad_santiago_del_estero natalidad_tucuman natalidad_tierra_del_fuego
indice_tiempo
2000 19.0 14.3 17.5 25.8 17.2 22.7 25.8 19.4 21.3 25.7 ... 17.9 18.9 24.0 22.7 22.3 19.6 16.9 21.6 22.6 19.9
2004 19.3 14.9 18.5 20.6 17.8 22.7 25.1 19.3 19.5 26.5 ... 19.8 18.3 23.9 22.5 21.9 22.4 17.6 19.7 21.0 19.5
2008 18.8 15.1 18.6 18.6 17.4 19.7 20.8 21.4 17.2 22.6 ... 21.0 19.9 21.4 20.7 19.3 25.0 16.9 20.5 19.9 20.8
2009 18.6 14.6 18.4 17.4 17.4 19.9 20.4 21.3 17.5 21.9 ... 20.9 19.6 21.0 20.6 18.2 25.2 16.5 21.9 19.1 20.2
2010 18.7 14.9 18.9 16.9 17.2 19.8 21.2 21.2 17.4 21.1 ... 21.6 20.0 21.9 19.8 17.4 26.0 16.2 19.9 20.1 18.8

5 rows × 25 columns

y contabilizarlos con el método .count():

tasa_natalidad[tasa_natalidad['natalidad_argentina'] > 18.5].count()
natalidad_argentina                          5
natalidad_ciudad_autonoma_de_buenos_aires    5
natalidad_buenos_aires                       5
natalidad_catamarca                          5
natalidad_cordoba                            5
natalidad_corrientes                         5
natalidad_chaco                              5
natalidad_chubut                             5
natalidad_entre_rios                         5
natalidad_formosa                            5
natalidad_jujuy                              5
natalidad_la_pampa                           5
natalidad_la_rioja                           5
natalidad_mendoza                            5
natalidad_misiones                           5
natalidad_neuquen                            5
natalidad_rio_negro                          5
natalidad_salta                              5
natalidad_san_juan                           5
natalidad_san_luis                           5
natalidad_santa_cruz                         5
natalidad_santa_fe                           5
natalidad_santiago_del_estero                5
natalidad_tucuman                            5
natalidad_tierra_del_fuego                   5
dtype: int64

Graficando

Los DataFrames de Pandas tienen integrados la funcionalidad para graficar de matplotlib, de modo tal que se pueden graficar los datos rápidamente:

import matplotlib.pyplot as plt

tasa_natalidad.plot()
plt.show()
_images/15_2_pandas_36_0.png
import plotly.express as px

fig = px.line(data_frame=tasa_natalidad)
fig.show()

Ejercicios 15 (a)

En el archivo data/imdb_top_1000.csv está la lista de las 1000 ‘mejores’ peliculas de acuerdo a lo que refiere dicha plataforma.

  • Lea el archivo con Pandas.

  • Inspeccione el DataFrame con funciones de pandas (head, columns pueden ser métodos útiles).

  • ¿Cuál es la película más antigua que figura en este ranking?

  • Encuentre la ‘mejor’ película de acuerdo al índice Meta_score. (Busque la documentación del método iloc de la clase DataFrame de pandas)

  • Encuentre la película más larga en la tabla (recuerde que el método max opera sobre números. Busque información sobre el método str de un DataFrame)

  • Construya una función del_director que recibe un string con el nombre (posiblemente parcial) de un director y devuelve una lista de sus películas (str puede serle también aquí útil).

Animaciones con Matploblib

Matplotlib tiene funciones para hacer animaciones de una manera conveniente. Hay excelente información sobre el tema en:

Vamos a ver brevemente cómo hacer animaciones, en pocos Pasos

Una animación simple en pocos pasos

pwd
'/home/fiol/Clases/IntPython/clases-python/clases'
%cd "./scripts/animaciones"
/home/fiol/Clases/IntPython/clases-python/clases/scripts/animaciones
%matplotlib tk
%run ejemplo_animation_1.py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
plt.ioff()
# Creamos los datos
xmax = 2*np.pi
Npts= 50

x = np.linspace(0, xmax, Npts)
data = np.vstack([x, np.sin(x)])

def update_line(num, data, line):
  line.set_data(data[:, :num])
  return (line,)

# Creamos la figura e inicializamos
# Graficamos una línea sin ningún punto
# Fijamos las condiciones de graficación
fig1, ax = plt.subplots(figsize=(12,8))
L, = plt.plot([], [], '-o') # equivalente a la siguiente
# L = plt.plot([],[] , '-o')[0]
ax.set_xlim(0, xmax)
ax.set_ylim(-1.1, 1.1)
ax.set_xlabel('x')
ax.set_title('Animación de una oscilación')


#
line_ani = animation.FuncAnimation(fig1, update_line, Npts, fargs=(data, L), interval=100, blit=True)

plt.show()

Este código da como resultado una función oscilante que se va creando. Este es un ejemplo simple que puede ser útil para graficar datos de una medición o de un cálculo más o menos largo.

_images/line.gif

Preparación general

Como vemos, después de importar el submódulo animation (además de lo usual):

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
plt.ioff()

nos aseguramos que estamos trabajando en modo no interactivo (con plt.ioff()).

Creamos los datos para graficar

Creación de datos para graficar

Creamos los datos para mostrar en la animación.

xmax = 2*np.pi
Npts = 50
x = np.linspace(0, xmax, Npts)
data = np.vstack([x, np.sin(x)])

Acá data es un array 2D, con los datos \(x\), \(y\).

Preparación de la figura

A continuación preparamos la zona de graficación:

  1. Creamos la figura y eje

  2. Creamos las líneas de graficación (una en este caso)

  3. Fijamos los límites de graficación

  4. Agregamos el texto, que va a ser invariante durante la animación

fig1, ax = plt.subplots(figsize=(12,8))
L, = plt.plot([0], [0], '-o', lw=3)
ax.set_xlim(0, xmax)
ax.set_ylim(-1.1, 1.1)
ax.set_xlabel('x')
ax.set_title('Animación de una oscilación')

Como sabemos, el llamado a plot() devuelve una lista de líneas (de un solo elemento). A este elemento lo llamamos L, y ya le damos las características que queremos que tenga. En este caso, fijamos el símbolo (círculos), con líneas de ancho 3. Vamos a modificar esta línea L en cada cuadro de la animación.

Función para actualizar la línea

Debemos crear una función que modifique las curvas en cada cuadro.

def update_line(num, data, line):
  line.set_data(data[:, :num])
  return line,

Esta función debe recibir como argumento el número de cuadro, que acá llamamos num. Además, en este caso recibe los datos a graficar, y la línea a modificar.

Esta función devuelve una línea L, que es la parte del gráfico que queremos que se actualice en cada frame.

Notemos acá que no es necesario que tome como argumento los datos guardados en data y la línea line, ya que son variables globales a las que hay acceso dentro del script. De la misma manera no es necesario que devuelva la línea, por la misma razón.

Animación de la figura

Finalmente llamamos a la función que hace la animación: animation.FuncAnimation()

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
np.info(animation.FuncAnimation)
 FuncAnimation(fig, func, frames=None, init_func=None, fargs=None,
               save_count=None, , cache_frame_data=True, **kwargs)

Makes an animation by repeatedly calling a function *func.

.. note::

    You must store the created Animation in a variable that lives as long
    as the animation should run. Otherwise, the Animation object will be
    garbage-collected and the animation stops.

Parameters
----------
fig : ~matplotlib.figure.Figure
    The figure object used to get needed events, such as draw or resize.

func : callable
    The function to call at each frame.  The first argument will
    be the next value in frames.   Any additional positional
    arguments can be supplied via the fargs parameter.

    The required signature is::

        def func(frame, fargs) -> iterable_of_artists

    If ``blit == True``, *func must return an iterable of all artists
    that were modified or created. This information is used by the blitting
    algorithm to determine which parts of the figure have to be updated.
    The return value is unused if blit == False and may be omitted in
    that case.

frames : iterable, int, generator function, or None, optional
    Source of data to pass func and each frame of the animation

    - If an iterable, then simply use the values provided.  If the
      iterable has a length, it will override the save_count kwarg.

    - If an integer, then equivalent to passing range(frames)

    - If a generator function, then must have the signature::

         def gen_function() -> obj

    - If None, then equivalent to passing itertools.count.

    In all of these cases, the values in frames is simply passed through
    to the user-supplied func and thus can be of any type.

init_func : callable, optional
    A function used to draw a clear frame. If not given, the results of
    drawing from the first item in the frames sequence will be used. This
    function will be called once before the first frame.

    The required signature is::

        def init_func() -> iterable_of_artists

    If blit == True, init_func must return an iterable of artists
    to be re-drawn. This information is used by the blitting algorithm to
    determine which parts of the figure have to be updated.  The return
    value is unused if blit == False and may be omitted in that case.

fargs : tuple or None, optional
    Additional arguments to pass to each call to func.

save_count : int, default: 100
    Fallback for the number of values from frames to cache. This is
    only used if the number of frames cannot be inferred from frames,
    i.e. when it's an iterator without length or a generator.

interval : int, default: 200
    Delay between frames in milliseconds.

repeat_delay : int, default: 0
    The delay in milliseconds between consecutive animation runs, if
    repeat is True.

repeat : bool, default: True
    Whether the animation repeats when the sequence of frames is completed.

blit : bool, default: False
    Whether blitting is used to optimize drawing.  Note: when using
    blitting, any animated artists will be drawn according to their zorder;
    however, they will be drawn on top of any previous artists, regardless
    of their zorder.

cache_frame_data : bool, default: True
    Whether frame data is cached.  Disabling cache might be helpful when
    frames contain large objects.


Methods:

  new_frame_seq  --  Return a new sequence of frame information.
  new_saved_frame_seq  --  Return a new sequence of saved/cached frame information.
  pause  --  Pause the animation.
  resume  --  Resume the animation.
  save  --  Save the animation as a movie file by drawing every frame.
  to_html5_video  --  Convert the animation to an HTML5 <video> tag.
  to_jshtml  --  Generate HTML representation of the animation.
line_anim = animation.FuncAnimation(fig1, update_line, Npts,
                   fargs=(data, L), interval=100, blit=True)

La función FuncAnimation() toma como argumentos:

  • la figura (fig1) donde se realiza el gráfico.

  • Una función a la que llama antes de dibujar cada frame (update_line),

  • El argumento interval da el tiempo entre cuadros, en milisegundos.

  • El argumento fargs es una tuple con los argumentos que necesita la función update_line(). En este caso (data, L).

  • El argumento blit=True hace que sólo se actualicen las partes que cambian en la animación, mientras que las partes estáticas no se dibujan en cada cuadro.

Es importante que el objeto creado por FuncAnimation() no se destruya. Esto lo podemos asegurar asignando el objeto resultante a una variable, en este caso line_anim.

Opcional: grabar la animación a un archivo

Podemos grabar la animación a un archivo usando el método save() o el método to_html5_video() del objeto (anim) que devuelve la función FuncAnimation().

Para poder grabar a archivo las animaciones se necesita tener instalados programas externos (alguno de ffmpeg, avconv, imagemagick). Ver https://matplotlib.org/api/animation_api.html para más información.

Segundo ejemplo

Veamos un ejemplo similar al primero, pero donde vamos cambiando los límites de los ejes en forma manual, a medida que los datos lo requieren

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# Calcula los datos en tiempo real.
def data_gen(t=0):
  cnt = 0
  while cnt < 1000:
    cnt += 1
    t += 0.1
    yield t, np.sin(2 * np.pi * t) * np.exp(-t / 10.)


# Necesitamos que se puede acceder a estas variables
# desde varias funciones -> globales
fig, ax = plt.subplots()
line, = ax.plot([], [], lw=2)
xdata, ydata = [], []

def init():
  ax.grid()
  ax.set_ylim(-1.1, 1.1)
  ax.set_xlim(0, 10)
  del xdata[:]
  del ydata[:]
  line.set_data(xdata, ydata)
  return line,


def run(data):
  # update the data
  t, y = data
  xdata.append(t)
  ydata.append(y)
  xmin, xmax = ax.get_xlim()

  # Si los datos salen del eje, agrandamos el eje
  # Después tenemos que redibujar el canvas manualmente
  if t >= xmax:
    ax.set_xlim(xmin, 2 * xmax)
    ax.figure.canvas.draw()
  line.set_data(xdata, ydata)

  return line,

ani = animation.FuncAnimation(fig, run, data_gen, blit=False,
                              interval=30,repeat=False, init_func=init)

plt.show()
%run animate_decay.py
/home/fiol/Clases/IntPython/clases-python/clases/scripts/animaciones/animate_decay.py:46: UserWarning: frames=<function data_gen at 0x7ff058744900> which we can infer the length of, did not pass an explicit save_count and passed cache_frame_data=True.  To avoid a possibly unbounded cache, frame data caching has been disabled. To suppress this warning either pass cache_frame_data=False or save_count=MAX_FRAMES.
  ani = animation.FuncAnimation(fig, run, data_gen, blit=False, interval=30,
plt.style.reload_library()
plt.style.use('default')

Tercer ejemplo: Quiver

Para hacer una animación de un campo de fuerzas o velocidades necesitamos datos en tres dimensiones. El siguiente ejemplo sigue los pasos de la animación anterior, excepto en la creación de datos y la graficación, que en lugar de usar plot() usa quiver():

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
plt.style.use('ggplot')

plt.ioff()

# ############################################################
# Creación de datos
x = np.linspace(-3, 3, 91)
t = np.linspace(0, 25, 30)
y = np.linspace(-3, 3, 91)
X3, Y3, T3 = np.meshgrid(x, y, t)
sinT3 = np.sin(2*np.pi*T3 /
               T3.max(axis=2)[..., np.newaxis])

G = (X3**2 + Y3**2)*sinT3
# Graficar una flecha cada step puntos
step = 10
x_q, y_q = x[::step], y[::step]

# Create U and V vectors to plot
U = G[::step, ::step, :-1].copy()
V = np.roll(U, shift=3, axis=2)

# ############################################################
# Figura y ejes.
fig1, ax = plt.subplots(figsize=(12,8))

ax.set_aspect('equal')

ax.set(xlim=(-4, 4), ylim=(-4, 4))


qax = ax.quiver(x_q, y_q, U[..., 0], V[..., 0],
                scale=100)

def animate(i):
  qax.set_UVC(U[..., i], V[..., i])

anim = animation.FuncAnimation(fig1, animate, interval=100, frames=len(t)-1, repeat=True)

# anim.save('quiver.gif', writer='imagemagick')
anim.save('quiver.mp4')
plt.show()
%run ejemplo_quiver.py
_images/quiver.gif

Comentarios:

  • Se utilizó la función quiver() para generar un campo vectorial. La forma de esta función es:

    quiver([X, Y], U, V, [C], **kw)

    X, Y define the arrow locations, U, V define the arrow directions, and C optionally sets the color.

  • Se utilizaron Ellipsis, por ejemplo en casos como:

    U[..., 0]
    

    Las elipsis (tres puntos o la palabra Ellipsis) indican todo el rango para todas las dimensiones que no se dan explícitamente. En este ejemplo el array U tiene tres dimensiones, por lo que tendremos:

    U[..., 0] = U[:, :, 0]
    

    En general, las elipses reemplazan a los dos puntos en todas las dimensiones no dadas explícitamente

a = np.arange(36)
a2 = a.reshape((6,-1))
a4 = a.reshape((2,3,2,3))
print(a2[:,0])
print(a2[..., 0])
[ 0  6 12 18 24 30]
[ 0  6 12 18 24 30]
print(a4[0,:,:,0])
print(a4[0,...,0])
[[ 0  3]
 [ 6  9]
 [12 15]]
[[ 0  3]
 [ 6  9]
 [12 15]]
(a4[...,0] == a4[:,:,:,0]).all()
True
  • Uso de np.roll(a, shift, axis=None) que mueve elementos una distancia shift a lo largo del eje axis, y cuando pasan la última posición los reintroduce al principio. Por ejemplo, en una dimensión:

x = np.arange(10)
print(x)
print(np.roll(x, 2))
[0 1 2 3 4 5 6 7 8 9]
[8 9 0 1 2 3 4 5 6 7]

Ejercicios 15 (b)

  1. Utilizando Matplotlib:

    • Hacer un gráfico donde dibuje una parábola \(y = x^{2}\) en el rango \([-5,5]\).

    • En el mismo gráfico, agregar un círculo en \(x=-5\).

    • El círculo debe moverse siguiendo la curva, como se muestra en la figura

    _images/pelota.gif
  2. Caída libre 2: Realizar animaciones del ejercicio de caída libre, de forma tal que:

    • La animación tenga un cartel indicando el tiempo, y la velocidad y altura correspondiente a ese tiempo.

    • Agregar una “cola fantasma” a la partícula, que muestre posiciones anteriores.


.