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)
Retome el
DataFrame
creado en el ejercicio 14(a), y genere un nuevoDataFrame
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: >

s.plot(label='random series',style='r--',legend=True, title='Random Series', grid=True)
<Axes: title={'center': 'Random Series'}>

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: >

df.plot(legend=True, title='Random DataFrame', grid=True, style=['r--','g-','b.','co-'], kind='line')
<Axes: title={'center': 'Random DataFrame'}>

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'}>

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'>

plt.plot(df['A'],df['B'])
plt.plot(df['B'],df['D'])
[<matplotlib.lines.Line2D at 0x7f13c0a2c410>]

Y se pueden hacer fácilmente otros tipos de gráficos:
df.iloc[:10].abs().plot.bar()
<Axes: >

Ejercicio 15(b)
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
dondexx
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.