.. _clase_15:
=========================
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.
.. code:: python
import numpy as np
import pandas as pd
.. code:: python
d = np.array([['auto','moto',np.nan,None,'bicicleta'],[4,2,0.0,None,2]])
d
.. parsed-literal::
array([['auto', 'moto', nan, None, 'bicicleta'],
[4, 2, 0.0, None, 2]], dtype=object)
.. code:: python
s = pd.DataFrame(d.T,columns=['vehículos','ruedas'])
s
.. raw:: html
|
vehículos |
ruedas |
0 |
auto |
4 |
1 |
moto |
2 |
2 |
NaN |
0.0 |
3 |
None |
None |
4 |
bicicleta |
2 |
.. code:: python
s.isna()
.. raw:: html
|
vehículos |
ruedas |
0 |
False |
False |
1 |
False |
False |
2 |
True |
False |
3 |
True |
True |
4 |
False |
False |
.. code:: python
s.dropna() # equivalente a s[s.notna()]
.. raw:: html
|
vehículos |
ruedas |
0 |
auto |
4 |
1 |
moto |
2 |
4 |
bicicleta |
2 |
.. code:: python
s.dropna(axis=1) # equivalente a s.dropna(axis='columns')
.. raw:: html
.. code:: python
s.dropna(how='all') # elimina las filas que tengan todos los valores nulos
.. raw:: html
|
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``.
.. code:: python
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
.. raw:: html
|
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 |
.. code:: python
df.fillna(0) # rellena los valores nulos con 0
.. raw:: html
|
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 |
.. code:: python
df.fillna({'A':0,'C':2}) # rellena los valores nulos con 0, 1 y 2 respectivamente
.. raw:: html
|
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 |
.. code:: python
df.ffill() # rellena los valores nulos con el valor anterior
.. raw:: html
|
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 |
.. code:: python
df.ffill(axis=1) # rellena los valores nulos con el valor anterior en la misma fila
.. raw:: html
|
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 |
.. code:: python
df.bfill() # rellena los valores nulos con el valor siguiente
.. raw:: html
|
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:
.. code:: python
p = pd.Series([23,4,-8,12,27,-9])
p
.. parsed-literal::
0 23
1 4
2 -8
3 12
4 27
5 -9
dtype: int64
.. code:: python
p.replace(-9,np.nan) # reemplaza -9 por NaN
.. parsed-literal::
0 23.0
1 4.0
2 -8.0
3 12.0
4 27.0
5 NaN
dtype: float64
.. code:: python
p.replace({-9:np.nan,-8:0}) # reemplaza -9 por NaN y 23 por 0
.. parsed-literal::
0 23.0
1 4.0
2 0.0
3 12.0
4 27.0
5 NaN
dtype: float64
.. code:: python
p<0
.. parsed-literal::
0 False
1 False
2 True
3 False
4 False
5 True
dtype: bool
.. code:: python
p[p < 0]
.. parsed-literal::
2 -8
5 -9
dtype: int64
.. code:: python
list(p[p < 0])
.. parsed-literal::
[-8, -9]
.. code:: python
p.replace(list(p[p < 0]),[86,22]) # encuentro los valores < 0 y los reemplazo por 86 y 22 respectivamente
.. parsed-literal::
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:
.. code:: python
df = pd.DataFrame({'key': ['a','a','b','d','a','c','c'],'datos': np.random.standard_normal(7)})
df
.. raw:: html
|
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 |
.. code:: python
pd.get_dummies(df['key'],dtype=int) # crea variables dummy
.. raw:: html
|
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:
.. code:: python
# Í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),
]
.. code:: python
index = pd.MultiIndex.from_tuples(index, names=["Ciudad", "Producto", "Año"])
print(type(index))
index
.. parsed-literal::
.. parsed-literal::
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'])
.. code:: python
# Datos
data = {
"Ventas": [200, 150, 300, 400, 250, 500],
"Costo": [120, 80, 180, 240, 150, 300]
}
.. code:: python
df = pd.DataFrame(data, index=index)
df
.. raw:: html
|
|
|
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``.
.. code:: python
# Acceso por niveles del índice
print("\nDatos de 'Buenos Aires' en 2022:")
print(df.loc[("Buenos Aires", slice(None), 2022), :])
.. parsed-literal::
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:
.. code:: python
# Resumen por nivel del índice
print("\nVentas totales por ciudad:")
print(df.groupby(level="Ciudad")["Ventas"].sum())
.. parsed-literal::
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:
.. code:: python
# Agrupar por 'Ciudad' y 'Año' y sumar el costo
df["Total"] = df["Ventas"] * df["Costo"]
df
.. raw:: html
|
|
|
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 |
.. code:: python
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)
.. parsed-literal::
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
.. code:: python
costo_anual_ciudad = df.groupby(level=["Ciudad"])["Total"].sum()
# Mostrar el resultado
print("Costo anual por ciudad:")
print(costo_anual_ciudad)
.. parsed-literal::
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:
.. code:: python
df.unstack() # desapila el índice
.. raw:: html
|
|
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 |
.. code:: python
df.unstack().columns
.. parsed-literal::
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**
.. code:: python
df.unstack().unstack() # desapila el índice
.. raw:: html
|
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.
.. code:: python
s = pd.Series(["Pequeño", "Mediano", "Grande", "Pequeño", "Grande", "Mediano"])
s
.. parsed-literal::
0 Pequeño
1 Mediano
2 Grande
3 Pequeño
4 Grande
5 Mediano
dtype: object
.. code:: python
s_cat = s.astype("category")
s_cat
.. parsed-literal::
0 Pequeño
1 Mediano
2 Grande
3 Pequeño
4 Grande
5 Mediano
dtype: category
Categories (3, object): ['Grande', 'Mediano', 'Pequeño']
.. code:: python
s_cat.cat.categories
.. parsed-literal::
Index(['Grande', 'Mediano', 'Pequeño'], dtype='object')
.. code:: python
s_cat.cat.codes
.. parsed-literal::
0 2
1 1
2 0
3 2
4 0
5 1
dtype: int8
.. code:: python
dict(enumerate(s_cat.cat.categories))
.. parsed-literal::
{0: 'Grande', 1: 'Mediano', 2: 'Pequeño'}
En el caso de un ``DataFrame``, uno puede convertir una columna en una
categoría reasignandola:
.. code:: python
precios = [100, 200, 300, 150, 250, 180]
df = pd.DataFrame({'precios':precios,'tamaño':s})
df
.. raw:: html
|
precios |
tamaño |
0 |
100 |
Pequeño |
1 |
200 |
Mediano |
2 |
300 |
Grande |
3 |
150 |
Pequeño |
4 |
250 |
Grande |
5 |
180 |
Mediano |
.. code:: python
df['tamaño']
.. parsed-literal::
0 Pequeño
1 Mediano
2 Grande
3 Pequeño
4 Grande
5 Mediano
Name: tamaño, dtype: object
.. code:: python
df['tamaño'] = df['tamaño'].astype('category')
df['tamaño']
.. parsed-literal::
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:
.. code:: python
N = 10_000_000
s = pd.Series(['a','b','c','d']* (N//4))
s_cat = s.astype('category')
.. code:: python
print(s.memory_usage(deep=True))
print(s_cat.memory_usage(deep=True))
.. parsed-literal::
500000132
10000504
.. code:: python
%timeit s.value_counts()
.. parsed-literal::
363 ms ± 15.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
.. code:: python
%timeit s_cat.value_counts()
.. parsed-literal::
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
.. code:: python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
.. code:: python
s = pd.Series(np.random.standard_normal(1000).cumsum(), index=pd.date_range('2021-1-1', periods=1000))
s
.. parsed-literal::
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
.. code:: python
s.plot()
.. parsed-literal::
.. image:: figuras/15_2_pandas_y_plot_3_1.png
.. code:: python
s.plot(label='random series',style='r--',legend=True, title='Random Series', grid=True)
.. parsed-literal::
.. image:: figuras/15_2_pandas_y_plot_4_1.png
.. code:: python
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()
.. parsed-literal::
.. image:: figuras/15_2_pandas_y_plot_5_1.png
.. code:: python
df.plot(legend=True, title='Random DataFrame', grid=True, style=['r--','g-','b.','co-'], kind='line')
.. parsed-literal::
.. image:: figuras/15_2_pandas_y_plot_6_1.png
.. code:: python
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])
.. parsed-literal::
.. image:: figuras/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:
.. code:: python
df.plot(x='A', y='B', kind='scatter')
.. parsed-literal::
.. image:: figuras/15_2_pandas_y_plot_9_1.png
.. code:: python
plt.plot(df['A'],df['B'])
plt.plot(df['B'],df['D'])
.. parsed-literal::
[]
.. image:: figuras/15_2_pandas_y_plot_10_1.png
Y se pueden hacer fácilmente `otros tipos de
gráficos `__:
.. code:: python
df.iloc[:10].abs().plot.bar()
.. parsed-literal::
.. image:: figuras/15_2_pandas_y_plot_12_1.png
--------------
Ejercicio 15(b)
===============
2. 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.
--------------