.. _clase_14: ==================================== Clase 14: Introducción a ``pandas`` ==================================== ========================= El análisis y consumo de grandes cantidades de información requiere herramientas que permitan limpiar, filtrar, agrupar los datos. Una de las bibliotecas fundamentales para ello es ``pandas``. ``pandas`` contiene estructuras de datos y funciones que permiten la manipulación de los datos en forma rápida. ``pandas`` está inspirada fuertemente en ``numpy``, en el sentido en que adopta en su diseño el estilo idiomático de ``numpy``. La diferencia principal radica en que ``numpy`` se utiliza para conjuntos de datos homogéneos, mientras que ``pandas`` está diseñada para trabajar con tablas o datos heterogéneos. Otra biblioteca con características similares es `polars `__ Input/Output con ``pandas`` =========================== Para poder leer o escribir datos, ``pandas`` provee una serie de métodos específicos para todos los tipos de datos estructurados usuales, ya sea en formato texto o binario. Los métodos para leer de archivos se reconocen por el prefijo ``.read_``, mientras que para escribir usaremos ``.to_``. Para cada formato particular de archivo, estos métodos aceptarán un conjunto de argumentos adicionales que permiten adecuar nuestro código. Si se usa VSCode, se puede instalar la extensión `Data Wrangler `__ que permite inspeccionar ``DataFrames``. Puede ser útil cuando uno trabaja con muchos datos. JSON ---- JSON (JavaScript Object Notation) es un formato estándar de estructura de datos en modo texto, legible y de amplio uso en internet. .. code:: python import pandas as pd from pathlib import Path .. code:: python atomos_path = Path.cwd().parent / 'data' / 'atomos' print(atomos_path) .. parsed-literal:: /Users/flavioc/Library/Mobile Documents/com~apple~CloudDocs/Documents/cursos/Python/GitLab/clase-python/data/atomos .. code:: python df = pd.read_json(atomos_path / 'atomos.json') df .. raw:: html
Element Symbol Atomic Number Atomic Mass (u) Density (g/cm³) Melting Point (K) Boiling Point (K) Electronegativity State at Room Temp
0 Hydrogen H 1 1.0080 0.000090 14.01 20.28 2.20 Gas
1 Helium He 2 4.0026 0.000179 NaN NaN NaN Gas
2 Lithium Li 3 6.9400 0.534000 453.69 1615.00 0.98 Solid
3 Beryllium Be 4 9.0122 1.850000 1560.00 2742.00 1.57 Solid
4 Boron B 5 10.8100 2.340000 2349.00 4200.00 2.04 Solid
5 Carbon C 6 12.0110 2.267000 3800.00 4300.00 2.55 Solid
6 Nitrogen N 7 14.0070 0.001251 63.15 77.36 3.04 Gas
7 Oxygen O 8 15.9990 0.001429 54.36 90.20 3.44 Gas
8 Fluorine F 9 18.9980 0.001696 53.48 85.03 3.98 Gas
9 Neon Ne 10 20.1800 0.000900 NaN NaN NaN Gas
.. code:: python df.dtypes .. parsed-literal:: Element object Symbol object Atomic Number int64 Atomic Mass (u) float64 Density (g/cm³) float64 Melting Point (K) float64 Boiling Point (K) float64 Electronegativity float64 State at Room Temp object dtype: object .. code:: python df.columns .. parsed-literal:: Index(['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)', 'Density (g/cm³)', 'Melting Point (K)', 'Boiling Point (K)', 'Electronegativity', 'State at Room Temp'], dtype='object') Renombremos algunas columnas para que no tengan etiquetas tan complejas .. code:: python df.rename(columns={'Density (g/cm³)': 'Density'}, inplace=True) df.rename(columns={'Melting Point (K)': 'Melting Point'}, inplace=True) df.columns .. parsed-literal:: Index(['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)', 'Density', 'Melting Point', 'Boiling Point (K)', 'Electronegativity', 'State at Room Temp'], dtype='object') Extraigamos los datos atómicos (masa y número atómico) y escribámoslo en un json: .. code:: python df[['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)']] .. raw:: html
Element Symbol Atomic Number Atomic Mass (u)
0 Hydrogen H 1 1.0080
1 Helium He 2 4.0026
2 Lithium Li 3 6.9400
3 Beryllium Be 4 9.0122
4 Boron B 5 10.8100
5 Carbon C 6 12.0110
6 Nitrogen N 7 14.0070
7 Oxygen O 8 15.9990
8 Fluorine F 9 18.9980
9 Neon Ne 10 20.1800
.. code:: python df[['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)']].to_json(atomos_path / 'prop_atomos.json') .. code:: python # Guarda sólo los valores df[['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)']].to_json(atomos_path / 'prop_atomos_values.json', orient='values') # Guarda los registros indexados por el índice del DataFrame df[['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)']].to_json(atomos_path / 'prop_atomos_index.json', orient='index') # Guarda los registros indexados por el nombre de las columnas, valor por defecto df[['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)']].to_json(atomos_path / 'prop_atomos_columns.json', orient='columns') # Guarda los registros en formato de lista de diccionarios, sin indices df[['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)']].to_json(atomos_path / 'prop_atomos_records.json', orient='records') CSV ~~~ Podríamos querer escribirlos como valores separados por comas (u otro delimitador): .. code:: python df[['Element', 'Symbol', 'Atomic Number', 'Atomic Mass (u)']].to_csv(atomos_path / 'prop_atomos.csv', sep='|') Vemos que en ambos casos también guarda el índice en el archivo Si queremos que no guarde el índice, pasamos el argumento opcional ``index=False``. .. code:: python df.to_csv(atomos_path / 'prop_atomos_noindex.csv', sep='|', index=False) Se puede ver que al guardar el ``.csv``, los elementos inexistentes de la tabla (``NaN``) se guardan como cadenas de caracteres vacías. Se puede indicar otro tipo de valor para esos casos: .. code:: python df.to_csv(atomos_path / 'prop_atomos_noheader2.csv', sep='|', index=False, na_rep='N/A') Formatos binarios ----------------- En algunos casos como leer o escribir de archivos binarios, es necesario instalar algunos módulos. Por ejemplo, si queremos leer archivos ``.parquet``, tenemos que instalar ``pyarrow``. - ``.parquet``: ``conda install pyarrow`` - ``.xls(x)``: ``conda install openpyxl xlrd`` - ``hdf5``: ``conda install pytables`` .. code:: python df.to_parquet(atomos_path / 'prop_atomos.parquet') .. code:: python dfp = pd.read_parquet(atomos_path / 'prop_atomos.parquet') dfp .. raw:: html
Element Symbol Atomic Number Atomic Mass (u) Density Melting Point Boiling Point (K) Electronegativity State at Room Temp
0 Hydrogen H 1 1.0080 0.000090 14.01 20.28 2.20 Gas
1 Helium He 2 4.0026 0.000179 NaN NaN NaN Gas
2 Lithium Li 3 6.9400 0.534000 453.69 1615.00 0.98 Solid
3 Beryllium Be 4 9.0122 1.850000 1560.00 2742.00 1.57 Solid
4 Boron B 5 10.8100 2.340000 2349.00 4200.00 2.04 Solid
5 Carbon C 6 12.0110 2.267000 3800.00 4300.00 2.55 Solid
6 Nitrogen N 7 14.0070 0.001251 63.15 77.36 3.04 Gas
7 Oxygen O 8 15.9990 0.001429 54.36 90.20 3.44 Gas
8 Fluorine F 9 18.9980 0.001696 53.48 85.03 3.98 Gas
9 Neon Ne 10 20.1800 0.000900 NaN NaN NaN Gas
Web ~~~ .. code:: python # URL que contiene una tabla HTML url = "https://en.wikipedia.org/wiki/List_of_countries_by_population" # Leer todas las tablas de la página tablas = pd.read_html(url) .. code:: python print(f"Encontré {len(tablas)} tablas") print(type(tablas)) # La función devuelve una lista de DataFrames, cada uno correspondiente a una tabla en la página # Normalmente querrás seleccionar una tabla específica tabla_poblacion = tablas[0] # Primera tabla de la página # Mostrar las primeras filas tabla_poblacion.head() .. parsed-literal:: Encontré 3 tablas .. raw:: html
Location Population % of world Date Source (official or from the United Nations) Notes
0 World 8137000000 100% 10 Apr 2025 UN projection[1][3] NaN
1 India 1413324000 17.3% 1 Mar 2025 Official projection[4] [b]
2 China 1408280000 17.2% 31 Dec 2024 Official estimate[5] [c]
3 United States 340110988 4.2% 1 Jul 2024 Official estimate[6] [d]
4 Indonesia 282477584 3.5% 30 Jun 2024 National annual projection[7] NaN
Cuando se pone difícil ---------------------- En muchas situaciones los archivos pueden tener datos estructurados de alguna manera que no permiten directamente su lectura con ``pandas``. .. code:: python smn_path = Path.cwd().parent / 'data' / 'smn' .. code:: python pronostico = pd.read_csv(smn_path / 'pronostico_header4.txt') pronostico .. raw:: html
************************************************************************************************
Producto basado en un modelo de pronóstico numérico del tiempo
por lo tanto puede diferir del pronostico emitido por el SMN NaN
************************************************************************************************ NaN
AEROPARQUE NaN
================================================================================================ NaN
... ...
06/DIC/2024 12Hs. 13.1 314 | 12 0.0 NaN
06/DIC/2024 15Hs. 16.2 297 | 15 0.0 NaN
06/DIC/2024 18Hs. 15.2 304 | 23 0.0 NaN
06/DIC/2024 21Hs. 9.1 308 | 17 0.0 NaN
================================================================================================ NaN

187 rows × 1 columns

Probemos usando ``read_fwf``, que es capaz de leer tablas con columnas de ancho fijo: .. code:: python pronostico = pd.read_fwf(smn_path / 'pronostico_header4.txt',skiprows=5) pronostico.head(10) .. raw:: html
AEROPARQUE
0 ==============================================...
1 FECHA * TEMPERATURA VIENTO ...
2 (DIR | KM/H)
3 ==============================================...
4 02/DIC/2024 00Hs. 16.0 236 | 17 ...
5 02/DIC/2024 03Hs. 14.4 196 | 18 ...
6 02/DIC/2024 06Hs. 10.6 190 | 8 ...
7 02/DIC/2024 09Hs. 14.9 221 | 11 ...
8 02/DIC/2024 12Hs. 19.2 249 | 12 ...
9 02/DIC/2024 15Hs. 21.6 241 | 15 ...
Como se esperaba, no parece ser la solución porque la estructura del archivo no es estrictamente de columnas de ancho fijo, sino de secciones de columnas de ancho fijo. Entonces tenemos que preprocesar el archivo para llegar a obtener esas secciones: .. code:: python with open(smn_path / 'pronostico_5dias20250408.txt', 'r') as file: content = file.read() # Dividir el contenido en secciones por aeropuerto sep = "================================================================================================" sections = content.split(sep) for i,s in enumerate(sections[0:5]): print(f"Sección {i} {'-'*80}") print(f"{s}") .. parsed-literal:: Sección 0 -------------------------------------------------------------------------------- ************************************************************************************************ Producto basado en un modelo de pronóstico numérico del tiempo, por lo tanto puede diferir del pronostico emitido por el SMN ************************************************************************************************ AEROPARQUE Sección 1 -------------------------------------------------------------------------------- FECHA * TEMPERATURA VIENTO PRECIPITACION(mm) (DIR | KM/H) Sección 2 -------------------------------------------------------------------------------- 08/ABR/2025 00Hs. 15.4 116 | 27 7.5 08/ABR/2025 03Hs. 15.6 112 | 25 3.0 08/ABR/2025 06Hs. 16.0 114 | 26 3.0 08/ABR/2025 09Hs. 16.3 115 | 26 1.7 08/ABR/2025 12Hs. 16.6 123 | 22 1.1 08/ABR/2025 15Hs. 17.0 123 | 24 0.9 08/ABR/2025 18Hs. 16.7 117 | 24 1.8 08/ABR/2025 21Hs. 16.6 104 | 22 1.4 09/ABR/2025 00Hs. 16.7 100 | 22 0.6 09/ABR/2025 03Hs. 16.2 110 | 21 1.2 09/ABR/2025 06Hs. 16.0 107 | 18 1.3 09/ABR/2025 09Hs. 16.1 99 | 18 1.9 09/ABR/2025 12Hs. 17.3 99 | 14 0.7 09/ABR/2025 15Hs. 19.9 106 | 19 0.4 09/ABR/2025 18Hs. 18.2 104 | 17 0.0 09/ABR/2025 21Hs. 17.1 108 | 16 0.0 10/ABR/2025 00Hs. 17.1 109 | 15 0.0 10/ABR/2025 03Hs. 17.6 104 | 16 0.0 10/ABR/2025 06Hs. 17.0 94 | 13 0.0 10/ABR/2025 09Hs. 18.2 98 | 13 0.0 10/ABR/2025 12Hs. 19.7 102 | 11 0.1 10/ABR/2025 15Hs. 21.3 138 | 12 0.0 10/ABR/2025 18Hs. 19.5 120 | 14 0.0 10/ABR/2025 21Hs. 18.6 109 | 8 0.2 11/ABR/2025 00Hs. 17.2 22 | 4 0.0 11/ABR/2025 03Hs. 14.8 35 | 6 0.0 11/ABR/2025 06Hs. 15.2 26 | 4 0.0 11/ABR/2025 09Hs. 17.5 30 | 2 0.0 11/ABR/2025 12Hs. 19.6 6 | 3 0.2 11/ABR/2025 15Hs. 22.8 326 | 3 0.0 11/ABR/2025 18Hs. 20.1 187 | 4 0.0 11/ABR/2025 21Hs. 18.2 115 | 3 0.0 12/ABR/2025 00Hs. 16.6 292 | 3 0.0 12/ABR/2025 03Hs. 15.1 254 | 6 0.0 12/ABR/2025 06Hs. 14.4 239 | 6 0.0 12/ABR/2025 09Hs. 17.6 213 | 6 0.0 12/ABR/2025 12Hs. 22.4 149 | 7 0.0 12/ABR/2025 15Hs. 22.8 123 | 8 0.0 12/ABR/2025 18Hs. 19.5 72 | 4 0.0 12/ABR/2025 21Hs. 17.0 95 | 8 0.0 Sección 3 -------------------------------------------------------------------------------- AZUL_AERO Sección 4 -------------------------------------------------------------------------------- FECHA * TEMPERATURA VIENTO PRECIPITACION(mm) (DIR | KM/H) .. code:: python print("Estación : ", sections[0]) print("Header : ", sections[1]) print("Pronóstico : ", sections[2]) .. parsed-literal:: Estación : ************************************************************************************************ Producto basado en un modelo de pronóstico numérico del tiempo, por lo tanto puede diferir del pronostico emitido por el SMN ************************************************************************************************ AEROPARQUE Header : FECHA * TEMPERATURA VIENTO PRECIPITACION(mm) (DIR | KM/H) Pronóstico : 08/ABR/2025 00Hs. 15.4 116 | 27 7.5 08/ABR/2025 03Hs. 15.6 112 | 25 3.0 08/ABR/2025 06Hs. 16.0 114 | 26 3.0 08/ABR/2025 09Hs. 16.3 115 | 26 1.7 08/ABR/2025 12Hs. 16.6 123 | 22 1.1 08/ABR/2025 15Hs. 17.0 123 | 24 0.9 08/ABR/2025 18Hs. 16.7 117 | 24 1.8 08/ABR/2025 21Hs. 16.6 104 | 22 1.4 09/ABR/2025 00Hs. 16.7 100 | 22 0.6 09/ABR/2025 03Hs. 16.2 110 | 21 1.2 09/ABR/2025 06Hs. 16.0 107 | 18 1.3 09/ABR/2025 09Hs. 16.1 99 | 18 1.9 09/ABR/2025 12Hs. 17.3 99 | 14 0.7 09/ABR/2025 15Hs. 19.9 106 | 19 0.4 09/ABR/2025 18Hs. 18.2 104 | 17 0.0 09/ABR/2025 21Hs. 17.1 108 | 16 0.0 10/ABR/2025 00Hs. 17.1 109 | 15 0.0 10/ABR/2025 03Hs. 17.6 104 | 16 0.0 10/ABR/2025 06Hs. 17.0 94 | 13 0.0 10/ABR/2025 09Hs. 18.2 98 | 13 0.0 10/ABR/2025 12Hs. 19.7 102 | 11 0.1 10/ABR/2025 15Hs. 21.3 138 | 12 0.0 10/ABR/2025 18Hs. 19.5 120 | 14 0.0 10/ABR/2025 21Hs. 18.6 109 | 8 0.2 11/ABR/2025 00Hs. 17.2 22 | 4 0.0 11/ABR/2025 03Hs. 14.8 35 | 6 0.0 11/ABR/2025 06Hs. 15.2 26 | 4 0.0 11/ABR/2025 09Hs. 17.5 30 | 2 0.0 11/ABR/2025 12Hs. 19.6 6 | 3 0.2 11/ABR/2025 15Hs. 22.8 326 | 3 0.0 11/ABR/2025 18Hs. 20.1 187 | 4 0.0 11/ABR/2025 21Hs. 18.2 115 | 3 0.0 12/ABR/2025 00Hs. 16.6 292 | 3 0.0 12/ABR/2025 03Hs. 15.1 254 | 6 0.0 12/ABR/2025 06Hs. 14.4 239 | 6 0.0 12/ABR/2025 09Hs. 17.6 213 | 6 0.0 12/ABR/2025 12Hs. 22.4 149 | 7 0.0 12/ABR/2025 15Hs. 22.8 123 | 8 0.0 12/ABR/2025 18Hs. 19.5 72 | 4 0.0 12/ABR/2025 21Hs. 17.0 95 | 8 0.0 La lista ``sections`` contiene secciones que corresponden a una estación meteorológica, cuyos nombres son: .. code:: python print(sections[0].split('\n')[-2].strip()) # tratamiento especial para remover el comentario print(sections[3].strip()) print(sections[6].strip()) .. parsed-literal:: AEROPARQUE AZUL_AERO BAHIA_BLANCA_AERO La primer sección debe trabajarse a mano debido al comentario que posee el archivo Las siguientes serían las secciones de headers que en principio no nos harían falta: .. code:: python print(sections[1].strip()) print(sections[4].strip()) print(sections[7].strip()) .. parsed-literal:: FECHA * TEMPERATURA VIENTO PRECIPITACION(mm) (DIR | KM/H) FECHA * TEMPERATURA VIENTO PRECIPITACION(mm) (DIR | KM/H) FECHA * TEMPERATURA VIENTO PRECIPITACION(mm) (DIR | KM/H) Finalmente tenemos las secciones con los datos: .. code:: python print(sections[2].split('\n')[:5]) print(sections[5].split('\n')[:5]) print(sections[8].split('\n')[:5]) .. parsed-literal:: ['', ' 08/ABR/2025 00Hs. 15.4 116 | 27 7.5 ', ' 08/ABR/2025 03Hs. 15.6 112 | 25 3.0 ', ' 08/ABR/2025 06Hs. 16.0 114 | 26 3.0 ', ' 08/ABR/2025 09Hs. 16.3 115 | 26 1.7 '] ['', ' 08/ABR/2025 00Hs. 10.3 109 | 15 0.0 ', ' 08/ABR/2025 03Hs. 9.3 124 | 12 0.0 ', ' 08/ABR/2025 06Hs. 8.9 134 | 11 0.0 ', ' 08/ABR/2025 09Hs. 12.6 122 | 18 0.0 '] ['', ' 08/ABR/2025 00Hs. 13.5 35 | 20 0.0 ', ' 08/ABR/2025 03Hs. 11.7 31 | 16 0.0 ', ' 08/ABR/2025 06Hs. 10.7 42 | 12 0.0 ', ' 08/ABR/2025 09Hs. 12.6 50 | 15 0.0 '] Ahora sí podemos usar ``read_fwf`` para transformar las secciones en ``DataFrame``\ s. Para ello tenemos que convertir cada ``section`` (que es un string) en un tipo de buffer en memoria que se comporte como un archivo. Obsérvese que si hacemos: .. code:: python from io import StringIO with StringIO(sections[5]) as data_io: df_malo = pd.read_fwf(data_io) df_malo.head() .. raw:: html
08/ABR/2025 00Hs. 10.3 109 | 15 0.0
0 08/ABR/2025 03Hs. 9.3 124 | 12 0.0
1 08/ABR/2025 06Hs. 8.9 134 | 11 0.0
2 08/ABR/2025 09Hs. 12.6 122 | 18 0.0
3 08/ABR/2025 12Hs. 19.4 95 | 22 0.0
4 08/ABR/2025 15Hs. 21.4 99 | 28 0.0
Es decir que la primer fila del *stream* es tomada como los nombres de las columnas. Tenemos que pasar el argumento opcional ``names`` para definir los nombres de las columnas: .. code:: python with StringIO(sections[2]) as data_io: df = pd.read_fwf(data_io,names=['fecha','h','t','v_dir','l','v_vel','precip']) df.head() .. raw:: html
fecha h t v_dir l v_vel precip
0 08/ABR/2025 00Hs. 15.4 116 | 27 7.5
1 08/ABR/2025 03Hs. 15.6 112 | 25 3.0
2 08/ABR/2025 06Hs. 16.0 114 | 26 3.0
3 08/ABR/2025 09Hs. 16.3 115 | 26 1.7
4 08/ABR/2025 12Hs. 16.6 123 | 22 1.1
Si el archivo no contiene headers, y se desea que se generen automáticamente como una secuencia numérica (0, 1, 2, …), se puede usar la opción ``header=None`` en cualquiera de los métodos ``read_`` de pandas. .. code:: python with StringIO(sections[2]) as data_io: df_auto = pd.read_fwf(data_io,header=None) df_auto.head() .. raw:: html
0 1 2 3 4 5 6
0 08/ABR/2025 00Hs. 15.4 116 | 27 7.5
1 08/ABR/2025 03Hs. 15.6 112 | 25 3.0
2 08/ABR/2025 06Hs. 16.0 114 | 26 3.0
3 08/ABR/2025 09Hs. 16.3 115 | 26 1.7
4 08/ABR/2025 12Hs. 16.6 123 | 22 1.1
Para terminar podemos eliminar la columna ``l`` que no aporta información: .. code:: python df.drop(columns=['l'],inplace=True) df.head() .. raw:: html
fecha h t v_dir v_vel precip
0 08/ABR/2025 00Hs. 15.4 116 27 7.5
1 08/ABR/2025 03Hs. 15.6 112 25 3.0
2 08/ABR/2025 06Hs. 16.0 114 26 3.0
3 08/ABR/2025 09Hs. 16.3 115 26 1.7
4 08/ABR/2025 12Hs. 16.6 123 22 1.1
-------------- Ejercicios 14 (a) ================= 1. Complete la tabla de datos del ejemplo de clase. Para ello utilice el archivo con los pronósticos completos para todas las estaciones, ``pronostico_5dias20250408.txt`` y - Agregue una columna con el nombre de la estación al DataFrame - Procese todas las estaciones -------------- Estructuras de datos en ``pandas``: Series ========================================== Las series son un objeto unidimensional que contiene una secuencia de valores *del mismo tipo* y un conjunto de etiquetas denominado *índice*: .. code:: python import numpy as np import pandas as pd .. code:: python s = pd.Series([1,4,4,2]) print(s) print(type(s)) print(s.dtype) .. parsed-literal:: 0 1 1 4 2 4 3 2 dtype: int64 int64 En este ejemplo, ``s`` es una serie que contiene elementos ``int64``. Los tipos de datos son extensiones de aquellos que utiliza ``numpy``. .. Notar que usando directamente la función ``print`` obtenemos una representación textual de la serie. Se puede acceder a los valores de la serie con el método ``.array``, y a los índices con el método ``.index``: .. code:: python s.array .. parsed-literal:: [1, 4, 4, 2] Length: 4, dtype: int64 El tipo ``NumpyExtensionArray`` es una clase que encapsula al tipo array de ``numpy``, pero ha permitido agregarle flexibilidad para acomodar las características de datos más generales. Por otra parte, los índices se obtienen como: .. code:: python s.index .. parsed-literal:: RangeIndex(start=0, stop=4, step=1) Los índices son inmutables, en el mismo sentido que los caracteres de un string. Es decir, no se pueden cambiar ciertos índices, pero se puede *reasignar* el índice de una serie completamente: .. code:: python print(s.index[2]) s.index[2] = 5 .. parsed-literal:: 2 :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[5], line 2 1 print(s.index[2]) ----> 2 s.index[2] = 5 File /usr/lib64/python3.13/site-packages/pandas/core/indexes/base.py:5371, in Index.__setitem__(self, key, value) 5369 @final 5370 def __setitem__(self, key, value) -> None: -> 5371 raise TypeError("Index does not support mutable operations") TypeError: Index does not support mutable operations .. code:: python nuevo_index = index=['a','b','c','d'] s.index = nuevo_index print(s) .. parsed-literal:: a 1 b 4 c 4 d 2 dtype: int64 Se puede también construir una nueva serie indicando los índices específicamente en el constructor con el argumento ``index=``: .. code:: python s2 = pd.Series([1,4,4,2], index=['arquero','defensores','medios','delanteros']) print(s2) .. parsed-literal:: arquero 1 defensores 4 medios 4 delanteros 2 dtype: int64 Existen otras maneras de crear series en ``pandas``, como por ejemplo a partir de arreglos de ``numpy`` .. code:: python rnd = pd.Series(np.random.randn(5)) print(rnd) .. parsed-literal:: 0 0.370947 1 1.600918 2 0.161127 3 -0.929294 4 -0.121632 dtype: float64 A partir de otras variables .. code:: python values = [1,2,3,4,5] index = ['a','b','c','d','e'] s3 = pd.Series(values, index=index) s3 .. parsed-literal:: a 1 b 2 c 3 d 4 e 5 dtype: int64 o usando diccionarios: .. code:: python d = { 1: "a", 2: "b", 3: "c", 4: "d", 5: "e" } s4 = pd.Series(d,dtype='string') print(s4) .. parsed-literal:: 1 a 2 b 3 c 4 d 5 e dtype: string Asimismo, se puede usar el argumento ``dtype`` (igual que en NumPy) para indicar el tipo de dato que se quiere utilizar al crear la serie. Pandas soporta los tipos de datos de NumPy, además de proveer sus propios tipos de datos (por ejemplo, ``string``) propios .. code:: python n = pd.Series([4,5,6], dtype='string') print(n) print(n[0]+n[1]) .. parsed-literal:: 0 4 1 5 2 6 dtype: string 45 Accediendo a los valores ------------------------ Se puede acceder a los valores de una serie a través del índice como si fuera un array: .. code:: python s = pd.Series([ 4, 8, 15, 16, 23, 42 ],index = ['a','b','c','d','e','f']) print(s) .. parsed-literal:: a 4 b 8 c 15 d 16 e 23 f 42 dtype: int64 .. code:: python print(f"Tercer elemento: {s.array[2]}") s.array[2] = 108 print(s.array) print(f"Con un rango:\n{s[1:3]}") print(f"Con los índices:\n{s[['a','c']]}") print(f"Con una máscara:\n{s[s > 100]}") .. parsed-literal:: Tercer elemento: 15 [4, 8, 108, 16, 23, 42] Length: 6, dtype: int64 Con un rango: b 8 c 108 dtype: int64 Con los índices: a 4 c 108 dtype: int64 Con una máscara: c 108 dtype: int64 .. code:: python print(f"Con una lista de índices:\n{s[[0,1]]}") .. parsed-literal:: Con una lista de índices: a 4 b 8 dtype: int64 .. parsed-literal:: /tmp/ipykernel_213536/1722261588.py:1: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]` print(f"Con una lista de índices:\n{s[[0,1]]}") .. code:: python print(f"Con una lista de índices:\n{s.iloc[[0,3]]}") print(f"Con una lista de índices:\n{s[ s.index[[0,3]] ]}") .. parsed-literal:: Con una lista de índices: a 4 d 16 dtype: int64 Con una lista de índices: a 4 d 16 dtype: int64 Modificando valores ------------------- Para modificar un valor dentro de una serie de ``pandas``, se accede al mismo con la etiqueta correspondiente: .. code:: python v = pd.Series([1,2,3,4,5], index=['a','b','c','d','e']) print(v) v['b'] = 100 print(v) .. parsed-literal:: a 1 b 2 c 3 d 4 e 5 dtype: int64 a 1 b 100 c 3 d 4 e 5 dtype: int64 En caso en que dicha etiqueta no exista dentro del índice de la serie, se agrega el valor: .. code:: python v['f'] = 200 print(v) .. parsed-literal:: a 1 b 100 c 3 d 4 e 5 f 200 dtype: int64 Para eliminar valores, se utiliza el método ``.drop``. Por defecto se genera una nueva serie con el valor eliminado, para poder eliminar el valor en la misma serie, es necesario usar el argumento opcional ``inplace`` como ``True``. .. code:: python w = v.drop('b') print(v) print(w) .. parsed-literal:: a 1 b 100 c 3 d 4 e 5 f 200 dtype: int64 a 1 c 3 d 4 e 5 f 200 dtype: int64 .. code:: python v.drop('b', inplace=True) print(v) .. parsed-literal:: a 1 c 3 d 4 e 5 f 200 dtype: int64 .. code:: python # Generando un nuevo objeto print(v==w) # Comparando objetos print(f"v is w: {v is w}") print(f"v equals w: {v.equals(w)}") .. parsed-literal:: a True c True d True e True f True dtype: bool v is w: False v equals w: True .. code:: python v.drop(['b'], inplace=True) # KeyError: "['b'] not found in axis" :: --------------------------------------------------------------------------- KeyError Traceback (most recent call last) Cell In[21], line 1 ----> 1 v.drop(['b'], inplace=True) # KeyError: "['b'] not found in axis" File /usr/lib64/python3.13/site-packages/pandas/core/series.py:5347, in Series.drop(self, labels, axis, index, columns, level, inplace, errors) 5250 def drop( 5251 self, 5252 labels: IndexLabel | None = None, (...) 5259 errors: IgnoreRaise = "raise", 5260 ) -> Series | None: 5261 """ 5262 Return Series with specified index labels removed. 5263 (...) 5345 dtype: float64 5346 """ -> 5347 return super().drop( 5348 labels=labels, 5349 axis=axis, 5350 index=index, 5351 columns=columns, 5352 level=level, 5353 inplace=inplace, 5354 errors=errors, 5355 ) File /usr/lib64/python3.13/site-packages/pandas/core/generic.py:4785, in NDFrame.drop(self, labels, axis, index, columns, level, inplace, errors) 4783 for axis, labels in axes.items(): 4784 if labels is not None: -> 4785 obj = obj._drop_axis(labels, axis, level=level, errors=errors) 4787 if inplace: 4788 self._update_inplace(obj) File /usr/lib64/python3.13/site-packages/pandas/core/generic.py:4827, in NDFrame._drop_axis(self, labels, axis, level, errors, only_slice) 4825 new_axis = axis.drop(labels, level=level, errors=errors) 4826 else: -> 4827 new_axis = axis.drop(labels, errors=errors) 4828 indexer = axis.get_indexer(new_axis) 4830 # Case for non-unique axis 4831 else: File /usr/lib64/python3.13/site-packages/pandas/core/indexes/base.py:7070, in Index.drop(self, labels, errors) 7068 if mask.any(): 7069 if errors != "ignore": -> 7070 raise KeyError(f"{labels[mask].tolist()} not found in axis") 7071 indexer = indexer[~mask] 7072 return self.delete(indexer) KeyError: "['b'] not found in axis" Tipos de datos ~~~~~~~~~~~~~~ Como último comentario, obsérvese que si uno pretende crear una serie con valores de distinto tipo, el tipo de la serie se promueve al tipo general ``object``, sin embargo cada elemento es reconocido con su tipo particular: .. code:: python s = pd.Series([1,'a','b','c']) print(s) for index,elem in s.items(): print(f"Tipo de objeto del elemento {index}: {type(s[index])}") .. parsed-literal:: 0 1 1 a 2 b 3 c dtype: object Tipo de objeto del elemento 0: Tipo de objeto del elemento 1: Tipo de objeto del elemento 2: Tipo de objeto del elemento 3: .. code:: python print(f"Suma de enteros: {s[0] + 3}") print(f"Suma de enteros: {s[1] + 3}") # Error .. parsed-literal:: Suma de enteros: 4 :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[23], line 2 1 print(f"Suma de enteros: {s[0] + 3}") ----> 2 print(f"Suma de enteros: {s[1] + 3}") # Error TypeError: can only concatenate str (not "int") to str .. code:: python print(f"Concat de str : {s[1] + 'd'}") print(f"Concat de str : {s[1] + 3}") # Error .. parsed-literal:: Concat de str : ad :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[24], line 2 1 print(f"Concat de str : {s[1] + 'd'}") ----> 2 print(f"Concat de str : {s[1] + 3}") # Error TypeError: can only concatenate str (not "int") to str Si bien entonces se pueden usar series con objetos con distintos tipos, se recomienda que el tipo de dato sea homogéneo. Valores que faltan ------------------ Es muy común al procesar datos que uno encuentre valores que no existen. La manera en que ``pandas`` representa estos datos faltantes es a través de ``np.nan``, el tipo de dato de ``numpy`` que representa *not a number*, para aquellos tipos de datos heredados de ``numpy``: .. code:: python snan = pd.Series([1,4,np.nan,2]) print(snan) .. parsed-literal:: 0 1.0 1 4.0 2 NaN 3 2.0 dtype: float64 Notar que si bien los datos existentes son de tipo entero, al utilizar ``np.nan`` para representar un dato inexistente, el tipo de dato de la serie se promueve a ``float64``. De la misma forma, se puede usar ``None`` para representar el dato faltante: .. code:: python snone = pd.Series([1,4,None,5]) print(snone) .. parsed-literal:: 0 1.0 1 4.0 2 NaN 3 5.0 dtype: float64 En el caso de cadenas de caracteres, se puede usar ``np.nan`` o ``None``. Si no se indica el tipo de dato a través de ``dtype``, se promueve el tipo de dato de la serie a ``object``, como sucede por defecto. .. code:: python smixed_str = pd.Series(['a','b',np.nan,None,'d']) smixed_str .. parsed-literal:: 0 a 1 b 2 NaN 3 None 4 d dtype: object Esto tiene el inconveniente evidente de que el elemento faltante no está representado unívocamente por un solo valor. Para solventar este problema, se puede crear la serie con el tipo ``string``: .. code:: python smixed_str2 = pd.Series(['a','b',np.nan,None,'d'],dtype='string') print(smixed_str2) .. parsed-literal:: 0 a 1 b 2 3 4 d dtype: string En este caso, el singlete ``NA`` representa unívocamente el dato faltante. En cualquier caso, se cuenta con el método ``.isna``, que retorna una serie de ``boolean`` donde los valores faltantes son verdaderos (``True``): .. code:: python print(smixed_str.isna()) print(smixed_str2.isna()) .. parsed-literal:: 0 False 1 False 2 True 3 True 4 False dtype: bool 0 False 1 False 2 True 3 True 4 False dtype: bool Atención, la cadena de caracteres vacía ``''`` *NO* se considera un dato inexistente. Puede ser útil en algunos casos poder filtrar los datos inexistentes. Para eso se utiliza el método ``.dropna``, que crea una nueva serie sin dichos datos: .. code:: python print(smixed_str) print(smixed_str.dropna()) .. parsed-literal:: 0 a 1 b 2 NaN 3 None 4 d dtype: object 0 a 1 b 4 d dtype: object Operando con series ------------------- Las operaciones con series han sido diseñadas para que sean compatibles con NumPy, y sigan las convenciones de Python. He aquí algunos ejemplos: .. code:: python ones = pd.Series(np.ones(8))*0.5 print(ones) t = pd.Series(np.random.rand(8)) print(t) print(t.mean()) .. parsed-literal:: 0 0.5 1 0.5 2 0.5 3 0.5 4 0.5 5 0.5 6 0.5 7 0.5 dtype: float64 0 0.855919 1 0.316680 2 0.188950 3 0.535831 4 0.767803 5 0.836171 6 0.225345 7 0.559054 dtype: float64 0.5357193052760575 .. code:: python shifted = t - ones print(shifted) print(shifted.mean()) print(np.abs(shifted)) .. parsed-literal:: 0 0.355919 1 -0.183320 2 -0.311050 3 0.035831 4 0.267803 5 0.336171 6 -0.274655 7 0.059054 dtype: float64 0.0357193052760575 0 0.355919 1 0.183320 2 0.311050 3 0.035831 4 0.267803 5 0.336171 6 0.274655 7 0.059054 dtype: float64 Además de poder interactuar con NumPy en forma transparente, existen algunos métodos útiles para trabajar con los datos de una serie: .. code:: python data = {'Pedro':19, 'Oscar': 30, 'Carlos': 27, 'David': 26} seru = pd.Series(data) print("Person Series:\n") print(seru) # Perform operations on the Series print("Edad promedio:", seru.mean()) print("Edad del más viejo :", seru.max()) print("El más viejo :", seru.idxmax()) .. parsed-literal:: Person Series: Pedro 19 Oscar 30 Carlos 27 David 26 dtype: int64 Edad promedio: 25.5 Edad del más viejo : 30 El más viejo : Oscar .. code:: python temperaturas_ciudades = { "Buenos Aires": 17.6, "Córdoba": 18.0, "Rosario": 18.0, "Mendoza": 16.0, "Santa Fe": 21.3 } temp = pd.Series(temperaturas_ciudades) print(temp) .. parsed-literal:: Buenos Aires 17.6 Córdoba 18.0 Rosario 18.0 Mendoza 16.0 Santa Fe 21.3 dtype: float64 .. code:: python otras_ciudades = ['Buenos Aires', 'Córdoba', 'Rosario', 'Mendoza', 'Salta'] temp = pd.Series(temperaturas_ciudades, index=otras_ciudades) print(temp) .. parsed-literal:: Buenos Aires 17.6 Córdoba 18.0 Rosario 18.0 Mendoza 16.0 Salta NaN dtype: float64 .. code:: python temp_ciudades2 = { "Mendoza": 20.0, "Salta": 17.2, "Santa Fe": 18.6, "San Juan": 19.3 } temp2 = pd.Series(temp_ciudades2) temp2 .. parsed-literal:: Mendoza 20.0 Salta 17.2 Santa Fe 18.6 San Juan 19.3 dtype: float64 .. code:: python print(temp) .. parsed-literal:: Buenos Aires 17.6 Córdoba 18.0 Rosario 18.0 Mendoza 16.0 Salta NaN dtype: float64 .. code:: python print((temp + temp2)/2) .. parsed-literal:: Buenos Aires NaN Córdoba NaN Mendoza 18.0 Rosario NaN Salta NaN San Juan NaN Santa Fe NaN dtype: float64 Misceláneas ----------- Algunas otras operaciones interesantes: .. code:: python 'Rosario' in temp2 .. parsed-literal:: False .. code:: python temp2_dict = temp2.to_dict() print(temp2_dict) print(type(temp2_dict)) .. parsed-literal:: {'Mendoza': 20.0, 'Salta': 17.2, 'Santa Fe': 18.6, 'San Juan': 19.3} .. code:: python temp2_json = temp2.to_json() print(temp2_json) print(type(temp2_json)) .. parsed-literal:: {"Mendoza":20.0,"Salta":17.2,"Santa Fe":18.6,"San Juan":19.3} -------------- Ejercicios 14(b) ================ 2. Si no lo ha adivinado, los datos de la serie ``seru`` corresponden a las edades de los integrantes de la banda Serú Girán, al momento de conformarse, en el año 1978. Encuentre los años de nacimiento de cada uno de sus integrantes. 3. Una de las funciones más usadas en redes neuronales es *softmax*, cuyo objetivo es convertir los resultados de las distintas etapas del procesamiento de una red en probabilidades. Los primeros pueden ser números reales de cualquier valor, mientras que las probabilidades deben estar acotadas al intervalo [0,1]. La función *softmax* aplicada a un conjunto de valores :math:`z_1,\cdots,z_n` se calcula como .. math:: \sigma(z_i) = \frac {e^{z_i}} { \sum_{i=1}^n e^{z_i}} - Cree una serie con 10 valores al azar entre 0 y 5 - Obtenga la serie resultante de la aplicación de *softmax* a la serie anterior 4. El siguiente es un diccionario que representa el consumo eléctrico mensual de ciertos artefactos eléctricos: .. code:: python consumo_electrico = { "Artefacto": [ "Heladera", "Lavarropa", "Microondas", "Aire Acondicionado", "Televisor", "Computadora", "Lámpara LED", "Secador de Pelo", "Horno Eléctrico", "Ventilador" ], "Consumo Promedio (kWh/mes)": [ 30, 10, 15, 120, 20, 12, 2, 3, 25, 8 ] } - Obtenga los tres artefactos de mayor consumo. - Encuentre los artefactos que consumen más de 15 kWH/mes. - Calcule el consumo anual de cada artefacto, y el costo que implica suponiendo que el precio del kWH/mes es de $3145. 5. Un problema de índices enteros. Supongamos que tenemos la serie .. code:: python s = pd.Series(np.arange(3.0)) es decir, .. code:: python 0 0.0 1 1.0 2 2.0 dtype: float64 ¿Cuál es el resultado esperado de s[-1]? ¿Cuál es el real? Evalúe ``s[-2:-1]`` y también ``s.iloc[-1]``. Considere ahora la serie .. code:: python s = pd.Series(np.arange(3.0), index=['a','b','c']) y evalúe nuevamente ``s[-1]``. ¿Qué conclusiones puede sacar? -------------- Estructuras de datos en ``pandas``: ``DataFrame``\ s ==================================================== Hemos visto que una serie en ``pandas`` es representa una colección de valores indexados por sendas etiquetas. La extensión natural (?) de una serie es una colección con múltiples valores arreglados en columnas indexados por una misma colección de etiquetas. Es decir, una tabla… La estructura que representa tablas en ``pandas`` se llama *DataFrame*. Un ``DataFrame`` tiene un índice de filas (como las series) y un índice de columnas. Se puede pensar un ``DataFrame`` como un diccionario de series que comparten un mismo índice. Como las series, es habitual construir un ``DataFrame`` a partir de un diccionario. Veamos el siguiente diccionario para comenzar a trabajar. Como siempre, importamos los módulos indispensables: .. code:: python import numpy as np import pandas as pd .. code:: python data = { "Titulo": [ "La piedra filosofal", "La cámara secreta", "El prisionero de Azkaban", "El cáliz de fuego", "La orden del Fénix", "El misterio del príncipe", "Las reliquias de la muerte" ], "Año de edición": [1997, 1998, 1999, 2000, 2003, 2005, 2007], "Páginas": [223, 251, 317, 636, 766, 607, 607], } df = pd.DataFrame(data) df .. raw:: html
Titulo Año de edición Páginas
0 La piedra filosofal 1997 223
1 La cámara secreta 1998 251
2 El prisionero de Azkaban 1999 317
3 El cáliz de fuego 2000 636
4 La orden del Fénix 2003 766
5 El misterio del príncipe 2005 607
6 Las reliquias de la muerte 2007 607
El diccionario de origen asigna a cada clave una lista de elementos. Hemos generado un ``DataFrame`` a partir de un diccionario: los índices de filas se generan automáticamente, mientras que los índices (o etiquetas) de las columnas corresponden a las claves de cada una de las listas del diccionario. Si las listas que componen el diccionario de origen no son iguales, ``pd.DataFrame`` dará un error ``ValueError: All arrays must be of the same length``. Una conveniencia interesante en Jupyter Notebooks es que se puede imprimir la tabla en forma elegante: .. code:: python print(df) display(df) df # también funciona .. parsed-literal:: Titulo Año de edición Páginas 0 La piedra filosofal 1997 223 1 La cámara secreta 1998 251 2 El prisionero de Azkaban 1999 317 3 El cáliz de fuego 2000 636 4 La orden del Fénix 2003 766 5 El misterio del príncipe 2005 607 6 Las reliquias de la muerte 2007 607 .. raw:: html
Titulo Año de edición Páginas
0 La piedra filosofal 1997 223
1 La cámara secreta 1998 251
2 El prisionero de Azkaban 1999 317
3 El cáliz de fuego 2000 636
4 La orden del Fénix 2003 766
5 El misterio del príncipe 2005 607
6 Las reliquias de la muerte 2007 607
.. raw:: html
Titulo Año de edición Páginas
0 La piedra filosofal 1997 223
1 La cámara secreta 1998 251
2 El prisionero de Azkaban 1999 317
3 El cáliz de fuego 2000 636
4 La orden del Fénix 2003 766
5 El misterio del príncipe 2005 607
6 Las reliquias de la muerte 2007 607
Eventualmente uno podría querer ver sólo algunas filas: .. code:: python display(df.head()) # muestra las primeras 5 filas display(df.tail()) # muestra las últimas 5 filas .. raw:: html
Titulo Año de edición Páginas
0 La piedra filosofal 1997 223
1 La cámara secreta 1998 251
2 El prisionero de Azkaban 1999 317
3 El cáliz de fuego 2000 636
4 La orden del Fénix 2003 766
.. raw:: html
Titulo Año de edición Páginas
2 El prisionero de Azkaban 1999 317
3 El cáliz de fuego 2000 636
4 La orden del Fénix 2003 766
5 El misterio del príncipe 2005 607
6 Las reliquias de la muerte 2007 607
El ``DataFrame`` usa la variable ``index`` como en las series, y la variable ``columns`` para describir las etiquetas de las columnas. Por ejemplo: .. code:: python df2 = pd.DataFrame(data, index = ["a", "b", "c", "d", "e", "f", "g"], columns = ["Titulo", "Páginas", "Año de edición"]) df2 .. raw:: html
Titulo Páginas Año de edición
a La piedra filosofal 223 1997
b La cámara secreta 251 1998
c El prisionero de Azkaban 317 1999
d El cáliz de fuego 636 2000
e La orden del Fénix 766 2003
f El misterio del príncipe 607 2005
g Las reliquias de la muerte 607 2007
Notar que el orden de la lista de etiquetas que se pasa a ``columns`` se mantiene al crear el ``DataFrame``. La lista que se usa en la creación de un ``DataFrame`` usando ``columns`` debe contener exactamente los mismos valores que las claves del diccionario, sino generará una columna con datos faltantes. Para acceder a las etiquetas de filas y columnas, usamos los métodos ``.index`` y ``.columns``. .. code:: python print(f"Indices de filas: {df2.index}") print(f"Indices de columnas: {df2.columns}") .. parsed-literal:: Indices de filas: Index(['a', 'b', 'c', 'd', 'e', 'f', 'g'], dtype='object') Indices de columnas: Index(['Titulo', 'Páginas', 'Año de edición'], dtype='object') Accediendo a los valores ------------------------ Para acceder a las columnas podemos usar: .. code:: python print(f"Valores de las filas: {df2.values}") print(f"Valores de las columnas: {type(df2.values)}") .. parsed-literal:: Valores de las filas: [['La piedra filosofal' 223 1997] ['La cámara secreta' 251 1998] ['El prisionero de Azkaban' 317 1999] ['El cáliz de fuego' 636 2000] ['La orden del Fénix' 766 2003] ['El misterio del príncipe' 607 2005] ['Las reliquias de la muerte' 607 2007]] Valores de las columnas: El resultado de esta operación nos devuelve un arreglo 2D de NumPy, cuyo tipo de dato será el mínimo que pueda contener los tipos de datos de las columnas que lo componen. Por ejemplo, si nuestras columnas tienen datos en ``float32`` y ``float64``, el tipo del arreglo de NumPy resultante será ``float64``. En el caso anterior, los tipos de datos de las columnas son: .. code:: python df2.dtypes .. parsed-literal:: Titulo object Páginas int64 Año de edición int64 dtype: object Es decir que los datos en ``int64`` de las columnas ``Páginas`` y ``Año de edición`` se promueven a ``object``. El acceso a ciertas columnas en particular se hace como en los diccionarios: .. code:: python titulos = df2["Titulo"] print("La columna 'Titulo'") print(f"{titulos}") print(f"Tipo de la columna 'Titulo': {type(titulos)}") print(f"Valores de la columna 'Titulo': {titulos.values}") print(f"Tipo de la columna 'Titulo' .values: {type(titulos.values)}") .. parsed-literal:: La columna 'Titulo' a La piedra filosofal b La cámara secreta c El prisionero de Azkaban d El cáliz de fuego e La orden del Fénix f El misterio del príncipe g Las reliquias de la muerte Name: Titulo, dtype: object Tipo de la columna 'Titulo': Valores de la columna 'Titulo': ['La piedra filosofal' 'La cámara secreta' 'El prisionero de Azkaban' 'El cáliz de fuego' 'La orden del Fénix' 'El misterio del príncipe' 'Las reliquias de la muerte'] Tipo de la columna 'Titulo' .values: Vemos que una columna en un ``DataFrame`` de ``pandas`` es representada efectivamente por una serie, de este modo, sus valores (obtenidos mediante ``.values``) serán un arreglo de NumPy, como vimos antes. En el caso en que la etiqueta de una columna no contenga espacios, se puede acceder a la misma con el operador ``.``: .. code:: python df2.Titulo # es lo mismo que df2['Titulo'] .. parsed-literal:: a La piedra filosofal b La cámara secreta c El prisionero de Azkaban d El cáliz de fuego e La orden del Fénix f El misterio del príncipe g Las reliquias de la muerte Name: Titulo, dtype: object Si uno quisiera usar esta propiedad en forma exhaustiva para todas las columnas, debería reemplazar las etiquetas de las mismas por nombres de variables válidos en Python .. code:: python df2.columns = ["titulo", "paginas", "primer_edicion"] df2 .. raw:: html
titulo paginas primer_edicion
a La piedra filosofal 223 1997
b La cámara secreta 251 1998
c El prisionero de Azkaban 317 1999
d El cáliz de fuego 636 2000
e La orden del Fénix 766 2003
f El misterio del príncipe 607 2005
g Las reliquias de la muerte 607 2007
.. code:: python df2.primer_edicion .. parsed-literal:: a 1997 b 1998 c 1999 d 2000 e 2003 f 2005 g 2007 Name: primer_edicion, dtype: int64 En el caso en que uno cree un ``DataFrame`` desde una base de datos, una buena práctica es mantener la consistencia entre las etiquetas de las columnas del ``DataFrame`` y las de la base de datos. Se puede acceder a una determinada fila usando las etiquetas través de ``.loc`` o con índices enteros con ``.iloc``: .. code:: python print(f"Primer libro:\n{df2.iloc[0]}") print(f"Segundo libro:\n{df2.loc['b']}") print(f"Último libro:\n{df2.iloc[-1]}") .. parsed-literal:: Primer libro: titulo La piedra filosofal paginas 223 primer_edicion 1997 Name: a, dtype: object Segundo libro: titulo La cámara secreta paginas 251 primer_edicion 1998 Name: b, dtype: object Último libro: titulo Las reliquias de la muerte paginas 607 primer_edicion 2007 Name: g, dtype: object Y se puede acceder a un elemento particular de la tabla: .. code:: python print(f"Título primer libro: {df2.iloc[0].titulo}") print(f"Año de edición del segundo libro: {df2.loc['b', 'primer_edicion']}") print(f"Año de edición del último libro: {df2.iloc[-1, 2]}") .. parsed-literal:: Título primer libro: La piedra filosofal Año de edición del segundo libro: 1998 Año de edición del último libro: 2007 La figura presenta los distintos tipos de acceso a los datos de un ``DataFrame`` .. image:: figuras/loc.png .. note:: Si se quisiera acceder solo a ciertas regiones de la tabla, se pueden reemplazar los valores individuales por rangos, tal como uno haría con NumPy .. code:: python df2.loc[['a','d'],["titulo", "paginas"]] .. raw:: html
titulo paginas
a La piedra filosofal 223
d El cáliz de fuego 636
.. code:: python df2.loc[:'d',["titulo", "primer_edicion"]] .. raw:: html
titulo primer_edicion
a La piedra filosofal 1997
b La cámara secreta 1998
c El prisionero de Azkaban 1999
d El cáliz de fuego 2000
.. code:: python df2.iloc[0:3, :2] .. raw:: html
titulo paginas
a La piedra filosofal 223
b La cámara secreta 251
c El prisionero de Azkaban 317
También se puede acceder a una parte de la tabla a partir de la aplicación de una condición. Veamos por ejemplo qué libros salieron en el siglo XXI. Para eso vemos qué elementos tienen valor de ``primera_edicion`` mayor a 2000: .. code:: python df2['primer_edicion'] > 2000 .. parsed-literal:: a False b False c False d False e True f True g True Name: primer_edicion, dtype: bool .. code:: python df2[df2['primer_edicion'] > 2000] .. raw:: html
titulo paginas primer_edicion
e La orden del Fénix 766 2003
f El misterio del príncipe 607 2005
g Las reliquias de la muerte 607 2007
.. code:: python print(type(df2[df2['primer_edicion'] > 2000])) .. parsed-literal:: Modificando valores ------------------- Retomemos nuestro ``DataFrame`` inicial y agreguémosle una columna con la calificación promedio de cada libro dada por los lectores: .. code:: python df = pd.DataFrame(data) display(df) .. raw:: html
Titulo Año de edición Páginas
0 La piedra filosofal 1997 223
1 La cámara secreta 1998 251
2 El prisionero de Azkaban 1999 317
3 El cáliz de fuego 2000 636
4 La orden del Fénix 2003 766
5 El misterio del príncipe 2005 607
6 Las reliquias de la muerte 2007 607
.. code:: python df['Calificación'] = [4.5, 4.3, 4.6, 4.8, 4.7, 4.9, 4.9] df .. raw:: html
Titulo Año de edición Páginas Calificación
0 La piedra filosofal 1997 223 4.5
1 La cámara secreta 1998 251 4.3
2 El prisionero de Azkaban 1999 317 4.6
3 El cáliz de fuego 2000 636 4.8
4 La orden del Fénix 2003 766 4.7
5 El misterio del príncipe 2005 607 4.9
6 Las reliquias de la muerte 2007 607 4.9
Supongamos que queremos agregar una columna ``Siglo`` con el siglo en que fue escrito cada libro. Empecemos creando una columna: .. code:: python df['Siglo'] = "" df .. raw:: html
Titulo Año de edición Páginas Calificación Siglo
0 La piedra filosofal 1997 223 4.5
1 La cámara secreta 1998 251 4.3
2 El prisionero de Azkaban 1999 317 4.6
3 El cáliz de fuego 2000 636 4.8
4 La orden del Fénix 2003 766 4.7
5 El misterio del príncipe 2005 607 4.9
6 Las reliquias de la muerte 2007 607 4.9
Esto crea la columna ``Siglo`` que no existe en la tabla, y le asigna el valor ``""`` a todas las filas. Podríamos haber completado la columna con una lista *con la misma cantidad de filas que la tabla*, pero mejor vamos a seleccionar aquellos libros del siglo XX, y completaremos los correspondientes valores en la nueva columna. Lo mismo haremos con los del siglo XXI: .. code:: python df.loc[df['Año de edición'] <= 2000, 'Siglo'] = 'XX' df.loc[df['Año de edición'] > 2000, 'Siglo'] = 'XXI' df .. raw:: html
Titulo Año de edición Páginas Calificación Siglo
0 La piedra filosofal 1997 223 4.5 XX
1 La cámara secreta 1998 251 4.3 XX
2 El prisionero de Azkaban 1999 317 4.6 XX
3 El cáliz de fuego 2000 636 4.8 XX
4 La orden del Fénix 2003 766 4.7 XXI
5 El misterio del príncipe 2005 607 4.9 XXI
6 Las reliquias de la muerte 2007 607 4.9 XXI
Uno podría hacer algo como lo siguiente (se llama *chaining*): .. code:: python df.loc[df['Año de edición'] <= 2000]['Siglo'] = 'siglo XX' df .. parsed-literal:: /tmp/ipykernel_6835/1865813456.py:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy df.loc[df['Año de edición'] <= 2000]['Siglo'] = 'siglo XX' .. raw:: html
Titulo Año de edición Páginas Calificación Siglo
0 La piedra filosofal 1997 223 4.5 XX
1 La cámara secreta 1998 251 4.3 XX
2 El prisionero de Azkaban 1999 317 4.6 XX
3 El cáliz de fuego 2000 636 4.8 XX
4 La orden del Fénix 2003 766 4.7 XXI
5 El misterio del príncipe 2005 607 4.9 XXI
6 Las reliquias de la muerte 2007 607 4.9 XXI
¿Qué pasó? Por la forma en que ``pandas`` obtiene los valores pedidos por la máscara ``df['Año de edición'] <= 2000``, el proceso de *chaining* genera un ``DataFrame`` intermedio que es el que se pretende modificar con ``['Siglo']``. El resultado es que ``df`` no se modifica. Para evitar estos inconvenientes, no se recomienda usar *chaining* al hacer modificaciones en los ``DataFrame``\ s. Para borrar filas o columnas, usamos ``.drop``: .. code:: python df = df.drop(3) df .. raw:: html
Titulo Año de edición Páginas Calificación Siglo
0 La piedra filosofal 1997 223 4.5 XX
1 La cámara secreta 1998 251 4.3 XX
2 El prisionero de Azkaban 1999 317 4.6 XX
4 La orden del Fénix 2003 766 4.7 XXI
5 El misterio del príncipe 2005 607 4.9 XXI
6 Las reliquias de la muerte 2007 607 4.9 XXI
.. code:: python df .. raw:: html
Titulo Año de edición Páginas Calificación Siglo
0 La piedra filosofal 1997 223 4.5 XX
1 La cámara secreta 1998 251 4.3 XX
2 El prisionero de Azkaban 1999 317 4.6 XX
4 La orden del Fénix 2003 766 4.7 XXI
5 El misterio del príncipe 2005 607 4.9 XXI
6 Las reliquias de la muerte 2007 607 4.9 XXI
.. code:: python df.drop('Siglo',axis=1) # no modifica el dataframe original display(df) df.drop('Siglo',axis=1, inplace=True) # modifica el dataframe original display(df) .. raw:: html
Titulo Año de edición Páginas Calificación Siglo
0 La piedra filosofal 1997 223 4.5 XX
1 La cámara secreta 1998 251 4.3 XX
2 El prisionero de Azkaban 1999 317 4.6 XX
4 La orden del Fénix 2003 766 4.7 XXI
5 El misterio del príncipe 2005 607 4.9 XXI
6 Las reliquias de la muerte 2007 607 4.9 XXI
.. raw:: html
Titulo Año de edición Páginas Calificación
0 La piedra filosofal 1997 223 4.5
1 La cámara secreta 1998 251 4.3
2 El prisionero de Azkaban 1999 317 4.6
4 La orden del Fénix 2003 766 4.7
5 El misterio del príncipe 2005 607 4.9
6 Las reliquias de la muerte 2007 607 4.9
Supongamos ahora que tenemos un ``DataFrame`` (o una serie) con el número de capítulos de los libros: .. code:: python capitulos_data = { "Titulo": [ "The Philosopher's Stone", "The Chamber of Secrets", "The Prisoner of Azkaban", "The Goblet of Fire", "The Order of the Phoenix", "The Half-Blood Prince", "The Deathly Hallows" ], "Capítulos": [17, 18, 22, 37, 38, 30, 36] } capitulos = pd.DataFrame(capitulos_data) display(capitulos) .. raw:: html
Titulo Capítulos
0 The Philosopher's Stone 17
1 The Chamber of Secrets 18
2 The Prisoner of Azkaban 22
3 The Goblet of Fire 37
4 The Order of the Phoenix 38
5 The Half-Blood Prince 30
6 The Deathly Hallows 36
Quisiéramos agregar los datos de los capítulos a la primer tabla: .. code:: python df['Capítulos'] = capitulos['Capítulos'] display(df) .. raw:: html
Titulo Año de edición Páginas Calificación Capítulos
0 La piedra filosofal 1997 223 4.5 17
1 La cámara secreta 1998 251 4.3 18
2 El prisionero de Azkaban 1999 317 4.6 22
4 La orden del Fénix 2003 766 4.7 38
5 El misterio del príncipe 2005 607 4.9 30
6 Las reliquias de la muerte 2007 607 4.9 36
La columna queda agregada correctamente, y *no* se agrega el dato en el libro 3 porque ya no está en ``df``. Viceversa, si queremos agregar el número de páginas desde ``df`` a la tabla de ``capítulos``: .. code:: python capitulos['Páginas'] = df['Páginas'] display(capitulos) .. raw:: html
Titulo Capítulos Páginas
0 The Philosopher's Stone 17 223.0
1 The Chamber of Secrets 18 251.0
2 The Prisoner of Azkaban 22 317.0
3 The Goblet of Fire 37 NaN
4 The Order of the Phoenix 38 766.0
5 The Half-Blood Prince 30 607.0
6 The Deathly Hallows 36 607.0
Vemos como naturalmente completa con ``Nan`` el dato inexistente. Este comportamiento es consistente en todos los casos, ya sea copiando una sola columna o varias, o copiando partes de la tabla utilizando listas o rangos para su definición. Operaciones con ``DataFrames`` ------------------------------ Vamos a ver algunas operaciones de las muchas que se pueden hacer con ``DataFrame``\ s. Recordemos que la representación interna de los ``DataFrame``\ s es equivalente a los arreglos de NumPy, con lo cual siempre se podrán realizar cálculos a través de las funciones y métodos que provee dicha biblioteca. Aritméticas ~~~~~~~~~~~ .. code:: python s1 = pd.DataFrame(np.arange(1,11).reshape(5,2), index = ['a', 'b', 'c', 'd', 'e'],columns=['A', 'B']) s2 = pd.DataFrame(np.arange(1,11).reshape(2,5).T, index = ['a', 'g', 'h', 'd', 'e'],columns=['A', 'B']) display(s1,s2) .. raw:: html
A B
a 1 2
b 3 4
c 5 6
d 7 8
e 9 10
.. raw:: html
A B
a 1 6
g 2 7
h 3 8
d 4 9
e 5 10
.. code:: python 1/s1 .. raw:: html
A B
a 1.000000 0.500000
b 0.333333 0.250000
c 0.200000 0.166667
d 0.142857 0.125000
e 0.111111 0.100000
.. code:: python s1 - 42 .. raw:: html
A B
a -41 -40
b -39 -38
c -37 -36
d -35 -34
e -33 -32
Al usar dos o más ``DataFrame``\ s, el proceso de alineamiento de datos de acuerdo a las etiquetas de las filas introduce automáticamente los datos faltantes. Por lo tanto, aquellos valores que faltan no serán, en este caso, sumados y resultarán también en un dato inexistente: .. code:: python s1+s2 .. raw:: html
A B
a 2.0 8.0
b NaN NaN
c NaN NaN
d 11.0 17.0
e 14.0 20.0
g NaN NaN
h NaN NaN
Es posible operar con ``DataFrame``\ s asignando un valor definido a los datos inexistentes, con el argumento ``fill_value``: .. code:: python # Función de convenciencia para mostrar dos tablas una al lado de la otra from IPython.display import display, HTML def display_side_by_side(dfs:list, captions:list): """Display tables side by side to save vertical space Input: dfs: list of pandas.DataFrame captions: list of table captions """ output = "" combined = dict(zip(captions, dfs)) for caption, df in combined.items(): output += df.style.set_table_attributes("style='display:inline'").set_caption(caption)._repr_html_() output += "\xa0\xa0\xa0" display(HTML(output)) .. code:: python display_side_by_side([s1,s2],['s1','s2']) .. raw:: html
s1
  A B
a 1 2
b 3 4
c 5 6
d 7 8
e 9 10
   
s2
  A B
a 1 6
g 2 7
h 3 8
d 4 9
e 5 10
    .. code:: python display_side_by_side([s1,s2,s2.add(s1, fill_value=0)],['s1','s2','s2+s1']) .. raw:: html
s1
  A B
a 1 2
b 3 4
c 5 6
d 7 8
e 9 10
   
s2
  A B
a 1 6
g 2 7
h 3 8
d 4 9
e 5 10
   
s2+s1
  A B
a 2.000000 8.000000
b 3.000000 4.000000
c 5.000000 6.000000
d 11.000000 17.000000
e 14.000000 20.000000
g 2.000000 7.000000
h 3.000000 8.000000
    .. code:: python s3 = pd.DataFrame(np.array([1,2]*5).reshape(5,2), index = ['a', 'g', 'h', 'd', 'e'],columns=['A', 'C']) display(s3) .. raw:: html
A C
a 1 2
g 1 2
h 1 2
d 1 2
e 1 2
La misma alineación de datos ocurre con las columnas: .. code:: python display_side_by_side([s1,s3],['s1','s3']) .. raw:: html
s1
  A B
a 1 2
b 3 4
c 5 6
d 7 8
e 9 10
   
s3
  A C
a 1 2
g 1 2
h 1 2
d 1 2
e 1 2
    .. code:: python s1+s3 .. raw:: html
A B C
a 2.0 NaN NaN
b NaN NaN NaN
c NaN NaN NaN
d 8.0 NaN NaN
e 10.0 NaN NaN
g NaN NaN NaN
h NaN NaN NaN
En el caso anterior, la columna B (C) no está en el ``DataFrame`` s3 (s1), por lo tanto se completan como inexistentes. Si se utiliza ``fill_value``, se completan aquellos datos con ``fill_value`` que están en alguno de los ``DataFrame``\ s. Si el dato no existe en ninguno de los ``DataFrame``\ s, permanece como inexistente .. code:: python display_side_by_side([s1,s3,s3.add(s1, fill_value=0)],['s1','s3','s3+s1']) .. raw:: html
s1
  A B
a 1 2
b 3 4
c 5 6
d 7 8
e 9 10
   
s3
  A C
a 1 2
g 1 2
h 1 2
d 1 2
e 1 2
   
s3+s1
  A B C
a 2.000000 2.000000 2.000000
b 3.000000 4.000000 nan
c 5.000000 6.000000 nan
d 8.000000 8.000000 2.000000
e 10.000000 10.000000 2.000000
g 1.000000 nan 2.000000
h 1.000000 nan 2.000000
    .. code:: python s3.mul(s1,fill_value=1) # s3*s1 .. raw:: html
A B C
a 1.0 2.0 2.0
b 3.0 4.0 NaN
c 5.0 6.0 NaN
d 7.0 8.0 2.0
e 9.0 10.0 2.0
g 1.0 NaN 2.0
h 1.0 NaN 2.0
En resumen, las operaciones realizan una unión entre los ``DataFrame``\ s, y luego, en aquellos casos en que es factible, se ejecuta la operación elemento a elemento. Operaciones entre ``DataFrame`` y ``Series`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ También se pueden hacer operaciones matemáticas entre ``DataFrame`` y ``Series`` de manera similar. Sin embargo, hay que aclarar cómo se desarrollan las operaciones dado que mientras ``DataFrame`` representa una estructura 2D, ``Series`` es unidimensional. Por defecto, ``pandas`` propaga la serie por filas, al igual que NumPy. Esta extensión automática de los datos se conoce como *broadcasting*: .. code:: python arr2D = np.arange(12).reshape(4,3) arr1D = np.array([3,2,1]) print('arr2D\n',arr2D) print('arr1D\n',arr1D) print('Broadcast') print(np.broadcast_to(arr1D, arr2D.shape)) print(arr2D-arr1D) .. parsed-literal:: arr2D [[ 0 1 2] [ 3 4 5] [ 6 7 8] [ 9 10 11]] arr1D [3 2 1] Broadcast [[3 2 1] [3 2 1] [3 2 1] [3 2 1]] [[-3 -1 1] [ 0 2 4] [ 3 5 7] [ 6 8 10]] .. code:: python df = pd.DataFrame(arr2D, columns = ['A', 'B', 'C']) v = pd.Series(arr1D, index = ['A', 'B', 'C']) display_side_by_side([df, pd.DataFrame(v),pd.DataFrame(np.broadcast_to(v, df.shape), columns=['A','B','C'])], ['df', 'v', 'broadcast']) df - arr1D # broadcasting por filas, DataFrame - array 1D .. raw:: html
df
  A B C
0 0 1 2
1 3 4 5
2 6 7 8
3 9 10 11
   
v
  0
A 3
B 2
C 1
   
broadcast
  A B C
0 3 2 1
1 3 2 1
2 3 2 1
3 3 2 1
    .. raw:: html
A B C
0 -3 -1 1
1 0 2 4
2 3 5 7
3 6 8 10
.. code:: python df - v # broadcasting por filas, DataFrame - Serie .. raw:: html
A B C
0 -3 -1 1
1 0 2 4
2 3 5 7
3 6 8 10
.. code:: python v - df .. raw:: html
A B C
0 3 1 -1
1 0 -2 -4
2 -3 -5 -7
3 -6 -8 -10
Se puede hacer el *broadcasting* en forma explícita, usando el argumento ``axis``: - Por columnas: ``axis = "index"`` (o ``axis = 0``) - Por filas: ``axis = "columns"`` (o ``axis = 1``) que indica qué índice debe alinearse. .. code:: python w = df['A'] display_side_by_side([df, pd.DataFrame(w)], ['df', 'w']) df.sub(w, axis='index') # alinea por filas, broadcasting por columnas, DataFrame - Serie .. raw:: html
df
  A B C
0 0 1 2
1 3 4 5
2 6 7 8
3 9 10 11
   
w
  A
0 0
1 3
2 6
3 9
    .. raw:: html
A B C
0 0 1 2
1 0 1 2
2 0 1 2
3 0 1 2
.. code:: python df.sub(w, axis=0) # alinea por filas, broadcasting por columnas, DataFrame - Serie, idem axis='index' .. raw:: html
A B C
0 0 1 2
1 0 1 2
2 0 1 2
3 0 1 2
.. code:: python t = pd.Series([3,2,1,0], index = ['A', 'B', 'C', 'D']) display_side_by_side([df, pd.DataFrame(t)], ['df', 't']) .. raw:: html
df
  A B C
0 0 1 2
1 3 4 5
2 6 7 8
3 9 10 11
   
t
  0
A 3
B 2
C 1
D 0
    .. code:: python df.sub(t, axis=1) # alínea por columnas, broadcasting por filas explícito, DataFrame - Serie .. raw:: html
A B C D
0 -3 -1 1 NaN
1 0 2 4 NaN
2 3 5 7 NaN
3 6 8 10 NaN
En este último caso, dado que los índices de ``w`` y ``df`` son todos distintos, se realiza la unión y se completa con ``NaN``. **En Resumen**: el eje (``axis``) es aquel en el cual se van a alinear los índices. Así, ``axis=0`` implica **emparejar** los índices de filas y **propagar** por columnas. De la misma manera, ``axis=1`` implica **emparejar** los índices de columnas y **propagar** por filas. Orden ~~~~~ Para ordenar ``DataFrame``\ s se usa el método ``.sort_index()`` .. code:: python dfo = pd.DataFrame(np.random.rand(4,3), index= ['c','d','a','b'], columns = ['B', 'C', 'A']) dfo .. raw:: html
B C A
c 0.708606 0.269161 0.674871
d 0.760163 0.806327 0.364229
a 0.977312 0.865779 0.916780
b 0.167906 0.519749 0.732298
.. code:: python dfo.sort_index() # ordena por los índices de filas .. raw:: html
B C A
a 0.977312 0.865779 0.916780
b 0.167906 0.519749 0.732298
c 0.708606 0.269161 0.674871
d 0.760163 0.806327 0.364229
.. code:: python dfo.sort_index(axis=1) # ordena por los índices de columnas .. raw:: html
A B C
c 0.674871 0.708606 0.269161
d 0.364229 0.760163 0.806327
a 0.916780 0.977312 0.865779
b 0.732298 0.167906 0.519749
Pasando el argumento ``ascending`` (booleano) se puede cambiar el orden por defecto, que corresponde a ``ascending=True``. Se puede ordenar también de acuerdo a los valores de una (o múltiples) columnas utilizando el método ``sort_values``: .. code:: python dfo.sort_values('B') .. raw:: html
B C A
b 0.167906 0.519749 0.732298
c 0.708606 0.269161 0.674871
d 0.760163 0.806327 0.364229
a 0.977312 0.865779 0.916780
En el caso en que existieran valores faltantes, estos se ordenan al final por defecto, excepto que se pase el argumento opcional ``na_position='first'``. .. code:: python dfo.loc['c','A'] = None dfo.loc['a','C'] = None dfo .. raw:: html
B C A
c 0.708606 0.269161 NaN
d 0.760163 0.806327 0.364229
a 0.977312 NaN 0.916780
b 0.167906 0.519749 0.732298
.. code:: python dfo.sort_values('C') .. raw:: html
B C A
c 0.708606 0.269161 NaN
b 0.167906 0.519749 0.732298
d 0.760163 0.806327 0.364229
a 0.977312 NaN 0.916780
.. code:: python dfo.sort_values('C',na_position='first') # coloca los NaN al principio .. raw:: html
B C A
a 0.977312 NaN 0.916780
c 0.708606 0.269161 NaN
b 0.167906 0.519749 0.732298
d 0.760163 0.806327 0.364229
Una operación útil es *ranking*, esto es, asignar un valor desde 1 hasta el número de datos de acuerdo a un orden, desde el valor más bajo al más alto: .. code:: python s = pd.Series(np.random.rand(6)) s .. parsed-literal:: 0 0.395530 1 0.030781 2 0.572166 3 0.195011 4 0.986553 5 0.178980 dtype: float64 .. code:: python s.rank() .. parsed-literal:: 0 4.0 1 1.0 2 5.0 3 3.0 4 6.0 5 2.0 dtype: float64 .. code:: python display_side_by_side([dfo, df.rank(axis=0)],['df','df.rank->0']) # ordena a lo largo de columnas display_side_by_side([dfo, df.rank(axis=1)],['df','df.rank->1']) # ordena a lo largo de filas .. raw:: html
df
  B C A
c 0.708606 0.269161 nan
d 0.760163 0.806327 0.364229
a 0.977312 nan 0.916780
b 0.167906 0.519749 0.732298
   
df.rank->0
  A B C
0 1.000000 1.000000 1.000000
1 2.000000 2.000000 2.000000
2 3.000000 3.000000 3.000000
3 4.000000 4.000000 4.000000
    .. raw:: html
df
  B C A
c 0.708606 0.269161 nan
d 0.760163 0.806327 0.364229
a 0.977312 nan 0.916780
b 0.167906 0.519749 0.732298
   
df.rank->1
  A B C
0 1.000000 2.000000 3.000000
1 1.000000 2.000000 3.000000
2 1.000000 2.000000 3.000000
3 1.000000 2.000000 3.000000
    Estadística ~~~~~~~~~~~ Es muy común en el análisis de datos tener que calcular valores estadísticos que caracterizan la muestra. ``pandas`` provee una serie de métodos para realizar estos cálculos. .. code:: python df = pd.DataFrame(np.random.rand(4,3), index= ['a','b','c','d'], columns = ['A', 'B', 'C']) df .. raw:: html
A B C
a 0.020433 0.217049 0.949068
b 0.317689 0.684951 0.017181
c 0.622041 0.421691 0.776658
d 0.317763 0.377677 0.231512
.. code:: python df.describe() .. raw:: html
A B C
count 4.000000 4.000000 4.000000
mean 0.319482 0.425342 0.493605
std 0.245614 0.194137 0.440922
min 0.020433 0.217049 0.017181
25% 0.243375 0.337520 0.177930
50% 0.317726 0.399684 0.504085
75% 0.393833 0.487506 0.819760
max 0.622041 0.684951 0.949068
Cada una de las magnitudes se corresponde a algún parámetro estadístico. Estas operaciones devuelven un nuevo ``DataFrame``. Por ejemplo: .. code:: python df.sum() .. parsed-literal:: A 1.277927 B 1.701367 C 1.974419 dtype: float64 retorna un ``DataFrame`` donde los índices corresponden a las etiquetas de cada columna. Por defecto, los valores inexistentes (NA, Null, etc.) no son tenidos en cuenta al realizar estas operaciones. .. code:: python df['A'].sum() .. parsed-literal:: 1.2779272965340271 Aplicación de funciones y map ----------------------------- Es posible también aplicar funciones, por ejemplo .. code:: python dff = pd.DataFrame(np.random.rand(4,3), index= ['a','b','c','d'], columns = ['A', 'B', 'C']) dff .. raw:: html
A B C
a 0.397259 0.852559 0.395104
b 0.546917 0.361448 0.622568
c 0.425802 0.731688 0.053037
d 0.761319 0.653025 0.183615
.. code:: python def f(x): return x.max() - x.min() dff.apply(f) # aplica la función a cada columna, por defecto axis=0 .. parsed-literal:: A 0.364060 B 0.491111 C 0.569531 dtype: float64 .. code:: python dff.apply(f, axis=1) # aplica la función a cada fila, axis=1 .. parsed-literal:: a 0.457455 b 0.261119 c 0.678651 d 0.577704 dtype: float64 .. code:: python dff.apply(lambda x: x.max()-x.min(), axis=1) # por columnas, igual que df.apply(f, axis='columns') .. parsed-literal:: a 0.457455 b 0.261119 c 0.678651 d 0.577704 dtype: float64 La siguiente operación calcular el promedio de las columnas de un ``DataFrame`` e incorporarlos como una columna nueva: .. code:: python dff['mean'] = dff.mean(axis=1) dff .. raw:: html
A B C mean
a 0.397259 0.852559 0.395104 0.548308
b 0.546917 0.361448 0.622568 0.510311
c 0.425802 0.731688 0.053037 0.403509
d 0.761319 0.653025 0.183615 0.532653
Cuál es el resultado de ``dff['mean'] = dff.mean(axis=0)``? .. code:: python dff.drop('mean', axis=1, inplace=True) # elimina la columna 'mean' .. code:: python dff.mean(axis=0) .. parsed-literal:: A 0.532824 B 0.649680 C 0.313581 dtype: float64 .. code:: python dff['mean'] = dff.mean(axis=0) .. code:: python dff .. raw:: html
A B C mean
a 0.397259 0.852559 0.395104 NaN
b 0.546917 0.361448 0.622568 NaN
c 0.425802 0.731688 0.053037 NaN
d 0.761319 0.653025 0.183615 NaN
Vemos que, como no existen los índices ``A``, ``B``, ``C``, que corresponden a las columnas que acabamos de calcular, asigna ``NaN`` en los índices ``a``, ``b``, ``c``, ``d`` Puede ser necesario aplicar determinada función a todos los valores del ``DataFrame``, con ``map``: .. code:: python def round2(x): return round(x, 2) display_side_by_side([df, df.map(round2)], ['df', 'df.map(round2)']) # aplica la función a cada elemento del DataFrame .. raw:: html
df
  A B C
a 0.020433 0.217049 0.949068
b 0.317689 0.684951 0.017181
c 0.622041 0.421691 0.776658
d 0.317763 0.377677 0.231512
   
df.map(round2)
  A B C
a 0.020000 0.220000 0.950000
b 0.320000 0.680000 0.020000
c 0.620000 0.420000 0.780000
d 0.320000 0.380000 0.230000
    -------------- Ejercicios 14 (c) ================= 6. El archivo ‘clima argentina 1981 2010.txt’ contiene datos climáticos significativos de las distintas estaciones meteorológicas del Servicio Meteorológico Nacional. - Inspeccione el archivo y diseñe un tipo ``DataFrame`` adecuado para contener dichos datos. - Lea el archivo y cree el ``DataFrame`` que diseño en el item anterior. - Cree una función para obtener el promedio anual de las magnitudes climáticas referidas en el archivo. La función debe recibir como argumentos el ``DataFrame``, el nombre de la estación meteorológica y la magnitud, y devolver un valor (``float``) con el promedio. - Genere un ``DataFrame`` que represente todas las magnitudes promedio para cada estación meteorológica, usando la función del item anterior. - Cree funciones para poder realizar gráficos comparativos de los datos meteorológicos. En particular, reproduzca - El gráfico de Valores Medios de Temperatura y Precipitación - El gráfico de Valores extremos de Temperatura, usando la temperatura máxima y la mínima de cada mes. que se pueden ver en el `sitio de estadísticas del Servicio Meteorológico Nacional `__. --------------