Clase 15: Más pandas


Manejando datos faltantes

Hemos visto que pandas automágicamente es capaz de manejar valores faltantes o inexistentes, a través de distintas etiquetas como NaN, NA, etc., dependiendo del tipo de dato que se esté utilizando. Más allá de la lectura de los datos, muchos de los métodos nativos de pandas son capaces de trabajar aún cuando falten datos. Sin embargo, puede ser necesario intervenir efectivamente sobre esos datos para poder continuar con ese procesamiento.

import numpy as np
import pandas as pd
d = np.array([['auto','moto',np.nan,None,'bicicleta'],[4,2,0.0,None,2]])
d
array([['auto', 'moto', nan, None, 'bicicleta'],
       [4, 2, 0.0, None, 2]], dtype=object)
s = pd.DataFrame(d.T,columns=['vehículos','ruedas'])
s
vehículos ruedas
0 auto 4
1 moto 2
2 NaN 0.0
3 None None
4 bicicleta 2
s.isna()
vehículos ruedas
0 False False
1 False False
2 True False
3 True True
4 False False
s.dropna() # equivalente a s[s.notna()]
vehículos ruedas
0 auto 4
1 moto 2
4 bicicleta 2
s.dropna(axis=1) # equivalente a s.dropna(axis='columns')
0
1
2
3
4
s.dropna(how='all') # elimina las filas que tengan todos los valores nulos
vehículos ruedas
0 auto 4
1 moto 2
2 NaN 0.0
4 bicicleta 2

También se puede usar el argumento ‘thresh=’ para acotar la cantidad de valores inexistentes que se quieren eliminar. Por ejemplo, thresh=3 eliminará todas aquellas filas que tienen 3 o más valores faltantes.

Es posible también que uno no pueda trabajar con valores inexistentes, y tiene que cambiarlos por algún valor. Para ello está el método fillna.

df = pd.DataFrame(np.random.standard_normal((7, 3)),columns=['A','B','C'])

df.iloc[:4,2] = np.nan
df.iloc[1:3,0] = None

df
A B C
0 0.377913 -0.770393 NaN
1 NaN -0.009935 NaN
2 NaN 0.904126 NaN
3 -0.077318 -0.022459 NaN
4 0.827397 2.037912 -0.418672
5 0.047666 -1.048824 0.248140
6 -0.943052 1.499018 0.048949
df.fillna(0) # rellena los valores nulos con 0
A B C
0 0.377913 -0.770393 0.000000
1 0.000000 -0.009935 0.000000
2 0.000000 0.904126 0.000000
3 -0.077318 -0.022459 0.000000
4 0.827397 2.037912 -0.418672
5 0.047666 -1.048824 0.248140
6 -0.943052 1.499018 0.048949
df.fillna({'A':0,'C':2}) # rellena los valores nulos con 0, 1 y 2 respectivamente
A B C
0 0.377913 -0.770393 2.000000
1 0.000000 -0.009935 2.000000
2 0.000000 0.904126 2.000000
3 -0.077318 -0.022459 2.000000
4 0.827397 2.037912 -0.418672
5 0.047666 -1.048824 0.248140
6 -0.943052 1.499018 0.048949
df.ffill() # rellena los valores nulos con el valor anterior
A B C
0 0.377913 -0.770393 NaN
1 0.377913 -0.009935 NaN
2 0.377913 0.904126 NaN
3 -0.077318 -0.022459 NaN
4 0.827397 2.037912 -0.418672
5 0.047666 -1.048824 0.248140
6 -0.943052 1.499018 0.048949
df.ffill(axis=1) # rellena los valores nulos con el valor anterior en la misma fila
A B C
0 0.377913 -0.770393 -0.770393
1 NaN -0.009935 -0.009935
2 NaN 0.904126 0.904126
3 -0.077318 -0.022459 -0.022459
4 0.827397 2.037912 -0.418672
5 0.047666 -1.048824 0.248140
6 -0.943052 1.499018 0.048949
df.bfill() # rellena los valores nulos con el valor siguiente
A B C
0 0.377913 -0.770393 -0.418672
1 -0.077318 -0.009935 -0.418672
2 -0.077318 0.904126 -0.418672
3 -0.077318 -0.022459 -0.418672
4 0.827397 2.037912 -0.418672
5 0.047666 -1.048824 0.248140
6 -0.943052 1.499018 0.048949

Estos métodos para reemplazar de valores inexistentes son un caso particular de un método para reemplazar valores en forma general, denominado replace y puede ser útil para reemplazar valores que, por alguna razón, se encuentran fuera del rango esperado de los datos (un precio negativo, una edad mayor a 120 años, etc.). Veamos cómo funciona:

p = pd.Series([23,4,-8,12,27,-9])
p
0    23
1     4
2    -8
3    12
4    27
5    -9
dtype: int64
p.replace(-9,np.nan) # reemplaza -9 por NaN
0    23.0
1     4.0
2    -8.0
3    12.0
4    27.0
5     NaN
dtype: float64
p.replace({-9:np.nan,-8:0}) # reemplaza -9 por NaN y 23 por 0
0    23.0
1     4.0
2     0.0
3    12.0
4    27.0
5     NaN
dtype: float64
p<0
0    False
1    False
2     True
3    False
4    False
5     True
dtype: bool
p[p < 0]
2   -8
5   -9
dtype: int64
list(p[p < 0])
[-8, -9]
p.replace(list(p[p < 0]),[86,22]) # encuentro los valores < 0 y los reemplazo por 86 y 22 respectivamente
 0    23
 1     4
 2    86
 3    12
 4    27
 5    22
 dtype: int64



Nota: el método replace() genera un nuevo dato.

Indicadores

Otro tipo de transformación para el modelado estadístico es convertir una variable en un indicador. Si una columna en un DataFrame tiene k valores distintos, se derivará una matriz o DataFrame con k columnas conteniendo unos y ceros, por ejemplo:

df = pd.DataFrame({'key': ['a','a','b','d','a','c','c'],'datos': np.random.standard_normal(7)})
df
key datos
0 a -0.680732
1 a 0.021716
2 b 0.722098
3 d -0.671279
4 a 1.547057
5 c -0.880381
6 c -0.633695
pd.get_dummies(df['key'],dtype=int) # crea variables dummy
a b c d
0 1 0 0 0
1 1 0 0 0
2 0 1 0 0
3 0 0 0 1
4 1 0 0 0
5 0 0 1 0
6 0 0 1 0

Manejando índices múltiples

Hemos visto hasta ahora que los índices nos etiquetan cada una de las filas de un DataFrame. Pandas tiene la posibilidad de utilizar índices múltiples o jerárquicos con el objeto de añadir dimensionalidad a las tablas. La implementación de esta característica consiste en utilizar tuplas como índices para etiquetar cada fila:

# Índices jerárquicos: ciudades, productos, años
index = [
    ("Buenos Aires", "Zapatos", 2022),
    ("Buenos Aires", "Ropa", 2022),
    ("Buenos Aires", "Ropa", 2023),
    ("Córdoba", "Zapatos", 2023),
    ("Córdoba", "Ropa", 2023),
    ("Rosario", "Zapatos", 2023),
]
index = pd.MultiIndex.from_tuples(index, names=["Ciudad", "Producto", "Año"])
print(type(index))
index
<class 'pandas.core.indexes.multi.MultiIndex'>
MultiIndex([('Buenos Aires', 'Zapatos', 2022),
            ('Buenos Aires',    'Ropa', 2022),
            ('Buenos Aires',    'Ropa', 2023),
            (     'Córdoba', 'Zapatos', 2023),
            (     'Córdoba',    'Ropa', 2023),
            (     'Rosario', 'Zapatos', 2023)],
           names=['Ciudad', 'Producto', 'Año'])
# Datos
data = {
    "Ventas": [200, 150, 300, 400, 250, 500],
    "Costo": [120, 80, 180, 240, 150, 300]
}
df = pd.DataFrame(data, index=index)
df
Ventas Costo
Ciudad Producto Año
Buenos Aires Zapatos 2022 200 120
Ropa 2022 150 80
2023 300 180
Córdoba Zapatos 2023 400 240
Ropa 2023 250 150
Rosario Zapatos 2023 500 300

Si queremos obtener ciertas filas específicas, usamos .loc.

# Acceso por niveles del índice
print("\nDatos de 'Buenos Aires' en 2022:")
print(df.loc[("Buenos Aires", slice(None), 2022), :])
Datos de 'Buenos Aires' en 2022:
                            Ventas  Costo
Ciudad       Producto Año
Buenos Aires Zapatos  2022     200    120
             Ropa     2022     150     80

La función slice se usa para determinar el rango de filas en cada componente del índice. slice(None) implica usar todos los valores posibles para dicha componente del índice.

Agrupando

La potencia de los índices múltiples radica en poder agrupar datos de acuerdo a una determinada componente del índice. Para ello se utiliza el método .groupby(), que agrupa los valores de acuerdo al nivel (level) indicado:

# Resumen por nivel del índice
print("\nVentas totales por ciudad:")
print(df.groupby(level="Ciudad")["Ventas"].sum())
Ventas totales por ciudad:
Ciudad
Buenos Aires    650
Córdoba         650
Rosario         500
Name: Ventas, dtype: int64

Si uno quisiera calcular el monto total de ventas por ciudad y por año, por ejemplo, se podría hacer:

# Agrupar por 'Ciudad' y 'Año' y sumar el costo
df["Total"] = df["Ventas"] * df["Costo"]
df
Ventas Costo Total
Ciudad Producto Año
Buenos Aires Zapatos 2022 200 120 24000
Ropa 2022 150 80 12000
2023 300 180 54000
Córdoba Zapatos 2023 400 240 96000
Ropa 2023 250 150 37500
Rosario Zapatos 2023 500 300 150000
costo_anual_ciudad_x_año = df.groupby(level=["Ciudad", "Año"])["Total"].sum()

# Mostrar el resultado
print("Costo anual por ciudad por año:")
print(costo_anual_ciudad_x_año)
Costo anual por ciudad por año:
Ciudad        Año
Buenos Aires  2022     36000
              2023     54000
Córdoba       2023    133500
Rosario       2023    150000
Name: Total, dtype: int64
costo_anual_ciudad = df.groupby(level=["Ciudad"])["Total"].sum()

# Mostrar el resultado
print("Costo anual por ciudad:")
print(costo_anual_ciudad)
Costo anual por ciudad:
Ciudad
Buenos Aires     90000
Córdoba         133500
Rosario         150000
Name: Total, dtype: int64

Apilando y desapilando

Otra operación es apilar o desapilar el dataframe de índices múltiples:

df.unstack() # desapila el índice
Ventas Costo Total
Año 2022 2023 2022 2023 2022 2023
Ciudad Producto
Buenos Aires Ropa 150.0 300.0 80.0 180.0 12000.0 54000.0
Zapatos 200.0 NaN 120.0 NaN 24000.0 NaN
Córdoba Ropa NaN 250.0 NaN 150.0 NaN 37500.0
Zapatos NaN 400.0 NaN 240.0 NaN 96000.0
Rosario Zapatos NaN 500.0 NaN 300.0 NaN 150000.0
df.unstack().columns
MultiIndex([('Ventas', 2022),
            ('Ventas', 2023),
            ( 'Costo', 2022),
            ( 'Costo', 2023),
            ( 'Total', 2022),
            ( 'Total', 2023)],
           names=[None, 'Año'])

Tal como se ve en el ejemplo anterior, las columnas también pueden ser descriptas con índices jerárquicos

df.unstack().unstack() # desapila el índice
Ventas Costo Total
Año 2022 2023 2022 2023 2022 2023
Producto Ropa Zapatos Ropa Zapatos Ropa Zapatos Ropa Zapatos Ropa Zapatos Ropa Zapatos
Ciudad
Buenos Aires 150.0 200.0 300.0 NaN 80.0 120.0 180.0 NaN 12000.0 24000.0 54000.0 NaN
Córdoba NaN NaN 250.0 400.0 NaN NaN 150.0 240.0 NaN NaN 37500.0 96000.0
Rosario NaN NaN NaN 500.0 NaN NaN NaN 300.0 NaN NaN NaN 150000.0

Categorías

Es muy posible que en nuestro conjunto de datos tengamos valores repetidos.

s = pd.Series(["Pequeño", "Mediano", "Grande", "Pequeño", "Grande", "Mediano"])
s
0    Pequeño
1    Mediano
2     Grande
3    Pequeño
4     Grande
5    Mediano
dtype: object
s_cat = s.astype("category")
s_cat
0    Pequeño
1    Mediano
2     Grande
3    Pequeño
4     Grande
5    Mediano
dtype: category
Categories (3, object): ['Grande', 'Mediano', 'Pequeño']
s_cat.cat.categories
Index(['Grande', 'Mediano', 'Pequeño'], dtype='object')
s_cat.cat.codes
0    2
1    1
2    0
3    2
4    0
5    1
dtype: int8
dict(enumerate(s_cat.cat.categories))
{0: 'Grande', 1: 'Mediano', 2: 'Pequeño'}

En el caso de un DataFrame, uno puede convertir una columna en una categoría reasignandola:

precios = [100, 200, 300, 150, 250, 180]

df = pd.DataFrame({'precios':precios,'tamaño':s})
df
precios tamaño
0 100 Pequeño
1 200 Mediano
2 300 Grande
3 150 Pequeño
4 250 Grande
5 180 Mediano
df['tamaño']
0    Pequeño
1    Mediano
2     Grande
3    Pequeño
4     Grande
5    Mediano
Name: tamaño, dtype: object
df['tamaño'] = df['tamaño'].astype('category')
df['tamaño']
0    Pequeño
1    Mediano
2     Grande
3    Pequeño
4     Grande
5    Mediano
Name: tamaño, dtype: category
Categories (3, object): ['Grande', 'Mediano', 'Pequeño']

El beneficio principal del uso de categorías tiene que ver con la eficiencia en la memoria y en las operaciones:

N = 10_000_000

s = pd.Series(['a','b','c','d']* (N//4))
s_cat = s.astype('category')
print(s.memory_usage(deep=True))
print(s_cat.memory_usage(deep=True))
500000132
10000504
%timeit s.value_counts()
363 ms ± 15.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit s_cat.value_counts()
38.9 ms ± 2.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Ejercicio 15 (a)

  1. Retome el DataFrame creado en el ejercicio 14(a), y genere un nuevo DataFrame utilizando adecuadamente índices jerárquicos.


Graficando DataFrames

Vamos a ver brevemente cómo usar matplotlib para graficar DataFrames de pandas. La mecánica para utilizar otras bibliotecas de graficación (seaborn, plotly, etc.) es similar

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
s = pd.Series(np.random.standard_normal(1000).cumsum(), index=pd.date_range('2021-1-1', periods=1000))
s
2021-01-01     0.446463
2021-01-02     0.979651
2021-01-03     1.640311
2021-01-04     0.619966
2021-01-05     0.561815
                ...
2023-09-23   -30.301808
2023-09-24   -30.761436
2023-09-25   -30.141886
2023-09-26   -30.961800
2023-09-27   -31.042287
Freq: D, Length: 1000, dtype: float64
s.plot()
<Axes: >
_images/15_2_pandas_y_plot_3_1.png
s.plot(label='random series',style='r--',legend=True, title='Random Series', grid=True)
<Axes: title={'center': 'Random Series'}>
_images/15_2_pandas_y_plot_4_1.png
df = pd.DataFrame({'A': np.random.standard_normal(100)+2, 'B': np.random.standard_normal(100), 'C': np.random.standard_normal(100)-2, 'D': np.random.randn(100) }, index=pd.date_range('2019-1-1', periods=100))
df.plot()
<Axes: >
_images/15_2_pandas_y_plot_5_1.png
df.plot(legend=True, title='Random DataFrame', grid=True, style=['r--','g-','b.','co-'], kind='line')
<Axes: title={'center': 'Random DataFrame'}>
_images/15_2_pandas_y_plot_6_1.png
fig, axes = plt.subplots(2, 2)

df['A'].plot(ax=axes[0,0], legend=True, title=df.columns[0])
df['B'].plot(ax=axes[0,1], legend=True, title=df.columns[1])
df['C'].plot(ax=axes[1,0], legend=True, title=df.columns[2])
df['D'].plot(ax=axes[1,1], legend=True, title=df.columns[3])
<Axes: title={'center': 'D'}>
_images/15_2_pandas_y_plot_7_1.png

Podría ser interesante graficar una columna respecto de otra, en lugar de usar el índice como etiquetas del eje x:

df.plot(x='A', y='B', kind='scatter')
<Axes: xlabel='A', ylabel='B'>
_images/15_2_pandas_y_plot_9_1.png
plt.plot(df['A'],df['B'])
plt.plot(df['B'],df['D'])
[<matplotlib.lines.Line2D at 0x7f13c0a2c410>]
_images/15_2_pandas_y_plot_10_1.png

Y se pueden hacer fácilmente otros tipos de gráficos:

df.iloc[:10].abs().plot.bar()
<Axes: >
_images/15_2_pandas_y_plot_12_1.png

Ejercicio 15(b)

  1. En el archivo com3500.csv se encuentra la cotización promedio del dolar en Argentina desde 2002. Lea el archivo en un DataFrame de pandas y

  • Realice un gráfico claro y bello

  • Observe que la información del mes está dada en nombre_del_mes-xx donde xx es el año. Separe dicha columna en dos, una correspondiente al mes, y otra correspondiente al año.

  • Agrupe la información por año y grafique la evolución del precio del dólar por año.