9. DataFrames de pandas#

Pandas es una librería de Python para leer, manipular y analizar datos que se combina muy bien con NumPy y Matplotlib

Pandas nos provee de:

  • Dos nuevas estructuras de datos: DataFrame y Series

  • Herramientas de análisis de datos que operan sobre estas estructuras de datos

A continuación nos enfocaremos en la creación y manipulación de objetos tipo DataFrame

import pandas as pd
pd.__version__
'1.4.1'

9.1. Objeto pandas.DataFrame#

El objeto pandas.DataFrame es la estructura de datos principal de pandas

  • Es un arreglo de dos dimensiones (matriz) que representa una tabla

  • Las filas y columnas de la tabla se identifican con un índice etiquetado denominado label

  • Cada columna puede tener tipo distinto

Ejemplo: Considere el siguiente DataFrame que representa datos de la bolsa de Santiago:

Monto [M$]

Variación [%]

AGUAS-A

1653

0.36

BSANTANDER

3531

-0.31

CMPC

5998

-0.6

ENTEL

1408

0.0

En este ejemplo

  • La etiqueta de las filas son los nombres de las empresas

  • La etiqueta de las columnas son los nombres de los atributos medidas por la bolsa

  • El atributo Monto es un entero

  • El atributo Variación es un flotante

A continuación veremos como crear y manipular objetos DataFrame

Construcción de un DataFrame

Se usa el constructor

pandas.DataFrame(data=None, 
                 index: Optional[Collection] = None, 
                 columns: Optional[Collection] = None, 
                 dtype: Union[str, numpy.dtype, ExtensionDtype, None] = None, 
                 copy: bool = False)

El argumento más importante es data, que puede ser un iterable (lista), un diccionario, un ndarray, entre otros

Consideremos lo siguiente

  • Si usamos un diccionario los keys se interpretan como etiquetas de columnas

  • Si usamos un diccionario de diccionarios los keys más internos se interpretan como etiquetas de filas

  • Si usamos un ndarray y no especificamos index y/o columns se crean etiquetas por defecto

A continuación se muestra como se crea un DataFrame en base a un diccionario

names = ["AGUAS-A", "BSANTANDER", "CMPC", "ENTEL"]

ventas = {
    'Monto [M$]': [1653, 3351, 5998, 1408],
    'Variación [%]': [0.36, 0.31, -0.6, 0.0]
}

df = pd.DataFrame(ventas, index=names)
df
Monto [M$] Variación [%]
AGUAS-A 1653 0.36
BSANTANDER 3351 0.31
CMPC 5998 -0.60
ENTEL 1408 0.00

Atributos básicos de un DataFrame

Una vez que haz creado un DataFrame es conveniente revisarlo

Si tu DataFrame se llama df, puedes usar

  • df.head(5) y df.tail(5) para imprimir las 5 primeras y 5 últimas filas, respectivamente

  • df.columns para recuperar un objeto pandas.Index con las etiquetas de columna

  • df.index para recuperar un objeto pandas.Index con las etiquetas de fila

  • df.shape para recuperar una tupla con el número de filas y número de columnas

  • df.dtypes para recuperar una lista con los tipos asignados a cada columna

  • df.info() nos da un resumen de lo anterior

Por ejemplo:

df.info()
<class 'pandas.core.frame.DataFrame'>
Index: 4 entries, AGUAS-A to ENTEL
Data columns (total 2 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   Monto [M$]     4 non-null      int64  
 1   Variación [%]  4 non-null      float64
dtypes: float64(1), int64(1)
memory usage: 96.0+ bytes

9.2. Indexación y slicing de DataFrames#

Indexado en base a etiquetas

Podemos usar las etiquetas para indexar un DataFrame ya sea en sus filas o en sus columnas

También podemos hacer slicing, es decir recuperar un subconjunto de un DataFrame

Importante

Se usa la función loc[] para indexar y hacer slicing en base a la etiqueta. Notar que se ocupa paréntesis cuadrado

Ejemplos

Consideremos el DataFrame anterior con información de la bolsa de Santiago

Para recuperar la fila de la empresa Entel:

df.loc["ENTEL"]
Monto [M$]       1408.0
Variación [%]       0.0
Name: ENTEL, dtype: float64

Para recuperar un slice con las filas entre Aguas andina hasta CMPC:

df.loc["AGUAS-A":"CMPC"]
Monto [M$] Variación [%]
AGUAS-A 1653 0.36
BSANTANDER 3351 0.31
CMPC 5998 -0.60

Nota

El operador A:B indica una secuencia que empieza con A y termina con B

Para recuperar un slice con las filas Entel y Aguas Andina:

df.loc[["ENTEL", "AGUAS-A"]]
Monto [M$] Variación [%]
ENTEL 1408 0.00
AGUAS-A 1653 0.36

Nota

Esto se conoce como fancy indexing, corresponde a indexar en base a una lista (o ndarray) que contiene los índices

Para recuperar un slice con las filas anteriores pero sólo con la columna monto total:

df.loc[["ENTEL", "AGUAS-A"], "Monto [M$]"]
ENTEL      1408
AGUAS-A    1653
Name: Monto [M$], dtype: int64

Advertencia

Si una etiqueta está mal escrita pandas retornará un KeyError

Consejo

Si un nombre es complicado de escribir o tiene caracteres especiales podemos usar en su lugar df.columns, por ejemplo

df.loc[["ENTEL", "AGUAS-A"], df.columns[0]] 

que sería equivalente a la expresión anterior

Para recuperar la columna de monto /completa) de forma eficiente:

df["Monto [M$]"] 

Advertencia

Tenga presente la siguiente “ambigüedad” de pandas para no confudirse:

df["Monto [M$]"]  # Esto es equivalente a df.loc[:, "Monto [M$]"]  
df["Monto [M$]":]  # Esto retorna error
df["BSANTANDER"]  # Esto retorna error
df["BSANTANDER":]  # Esto es equivalente a df.loc["BSANTANDER":]

Indexado en base a posición

Internamente, pandas asigna un número entero a cada fila y cada columna que corresponde a su posición

0

1

0

1653

0.36

1

3531

-0.31

2

5998

-0.6

3

1408

0.0

Importante

Se usa la función iloc[] para indexar y hacer slicing en base a la posición

Por ejemplo

df.loc["ENTEL"]

y

df.iloc[3]

son equivalentes.

Nota

iloc[] soporta todo lo que vimos anteriormente para loc[]

Indexado rápido de un elemento

Si queremos recuperar sólo un elemento del DataFrame se recomienda usar las funciones at[] e iat[]

df.at["ENTEL", "Monto [M$]"]
df.iat[3, 0]

Estas son mucho más rápidas que loc[] e iloc[] pero no permiten hacer slicing ni fancy indexing

9.3. Combinación y manipulación de DataFrames#

A continuación veremos como

  • Combinar dos o más tablas

  • Intersectar dos tablas

  • Eliminar filas y columnas de nuestra tabla

Para combinar dos o más DataFrames a nivel de fila o columna se utiliza

pd.concat(objs, # Lista de DataFrames 
          axis=0, # Eje de concatenación 0:filas, 1: columnas
          join='inner', # Especifica que se hace con el eje que no se une
          ignore_index: bool = False, # Igual que append
          verify_integrity: bool = False, # Igual que append

El argumento principal es una lista de DataFrames

  • Podemos juntar más de dos DataFrames

  • Podemos juntarlos en filas o en columnas con axis

  • Podemos especificar que ocurre cuando las filas/columnas no calzan con join

Nota

Por defecto los valores que no se alinean al juntar dos dataframes se llenarán con NaNs

Por ejemplo:

df_nuevo = pd.DataFrame([[100, -30.0]], 
                        index=['JabonCopito'], 
                        columns=['Monto [M$]', 'Variación [%]'])

pd.concat([df, df_nuevo], axis=0)
Monto [M$] Variación [%]
AGUAS-A 1653 0.36
BSANTANDER 3351 0.31
CMPC 5998 -0.60
ENTEL 1408 0.00
JabonCopito 100 -30.00

Finalmente podemos agregar una columna nueva usando el atributo insert

df.insert(loc,  # Ubicación de la nueva columna
          column, # Nombre de la nueva columna
          value,  # Lista o ndarray con valores
          allow_duplicates=False # Impide agregar una columna con una etiqueta existente
         )
                  

La operación insert es por defecto inplace es decir que modifica directamente a df

Unión e intersección de DataFrames

Podemos combinar DataFrames de acuerdo a sus contenidos con la función pandas.merge

pandas.merge(left, # Primer DataFrame
             right, # Segundo DataFrame
             how: str = 'inner', # Especifica como se uniran los DataFrames
             on=None, # Especifica la columna que se usa para combinar la DataFrames
             ...
            )

Algunos detalles:

  • La columna que se especifica en on debe existir en ambos DataFrames

  • El argumento how tiene las siguientes opciones

    • left: Se usan las llaves del primer DataFrame

    • right: Se usan las llaves del segundo DataFrame

    • inner: Se usa una intersección de las llaves de ambos DataFrames

    • outer: Se usa una unión de las llaves de ambos DataFrames

Eliminando filas y columnas

Podemos eliminar filas o columnas de un DataFrame df con el atributo drop

df.drop(labels=None, # Etiquetas que quiero elimina, un string o una lista
        axis=0, # Eliminar la etiqueta de las filas: 0 o de las columnas: 1
        inplace=False, # No modifica df, en su lugar retorna un DataFrame nuevo
        ...
        )

También podemos extraer y eliminar una columna usando el atributo pop

Ordenando DataFrames

Se puede ordenar un DataFrame según los valores de una columna usando el atributo sort_values

df.sort_values(by, # Columna que guia el ordenamiento 
               axis=0, # Se ordena según 0: filas o 1:columnas
               ascending=True, # Se ordenan los valores de menor a mayor
               inplace=False, # No se modifica df, se retorna un nuevo DataFrame

El argumento by recibe una etiqueta o una lista de etiquetas. En el segundo caso se ordenan jerarquicamente

También existe el atributo sort_index que permite ordenar filas y columnas según su etiqueta

9.4. Operando con DataFrames#

Operaciones aritméticas y lógicas

Podemos hacer operaciones aritméticas simples sobre DataFrames con los atributos

  • add y sub

  • mul y div

  • pow

  • y sus versiones reversas radd, rdiv, rpow etc

También podemos hacer operaciones lógicas con los atributos

  • eq y nq (igual y no igual)

  • lt, gt (menor que y mayor que)

  • le, ge (menor o igual y mayor o igual

Todos estos atributos tienen argumento axis para especificar si la operación es en fila o columna

Por ejemplo

df.add(10) # Le suma 10 a todos los valores de df
df.add([0, 2]) # Le suma 0 a la primera columna y 2 a la segunda columna de df
df.add(df2) # Suma los valores de df y df2 siguiendo los índices

Si usamos una lista tiene que ser tan larga como columnas tenga el DataFrame

En general las operaciones entre columnas se guian por el índice de fila

  • Si un índice no es compartido se rellena con un NaN

  • También podemos especificar fill_value

Si tenemos NaN en filas o columnas podemos

  • Eliminarlos con el atributo dropna(axis='columns', how='any')

  • Rellenarlos con el atributo fillna(valor)

Operaciones de reducciones

Se pueden aplicar las reducciones que vimos en NumPy directamente sobre las columnas

Algunos atributos para hacer reducción son

  • count()

  • sum() y prod()

  • mean() y std()

  • max() e idxmax()

  • entre otros

Consejo

También podemos usar el atributo describe() que nos entrega varios estadísticos de nuestras columnas

df.describe()
Monto [M$] Variación [%]
count 4.000000 4.00000
mean 3102.500000 0.01750
std 2114.872179 0.44139
min 1408.000000 -0.60000
25% 1591.750000 -0.15000
50% 2502.000000 0.15500
75% 4012.750000 0.32250
max 5998.000000 0.36000

Conectando con NumPy

Podemos extraer un ndarray a partir de un DataFrame usando el atributo values

df.values
array([[ 1.653e+03,  3.600e-01],
       [ 3.351e+03,  3.100e-01],
       [ 5.998e+03, -6.000e-01],
       [ 1.408e+03,  0.000e+00]])

Esto permite aplicar cualquiera de las funciones de librería NumPy

Nota

Si nuestro DataFrame tiene strings entonces values retornará un arreglo de tipo genérico object

En este caso tenemos dos opciones:

  • Podemos usar el atributo select_dtypes para obtener un nuevo DataFrame con columnas de tipo numérico

  • Podemos convertir los atributos string a numérico (más adelante detallaremos esto)