.. _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
0
1
2
3
4
.. 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. --------------