Manipulación de Datos

El principal objetivo de esta clase es introducir la biblioteca de análisis de datos pandas, la cual agrega mayor flexibilidad y opciones que Numpy, sin embargo el costo de esto se traduce en pérdida de rendimiento y eficiencia. Conocer los elemnos básicos pandas permite manejar desde pocos dados en un arhivo excel hasta miles y millones de registros de una base de datos.

Introducción a pandas

Desde de la página oficial Pandas se introduce de la siguiente forma:

pandas is a fast, powerful, flexible and easy to use open source data analysis and manipulation tool, built on top of the Python programming language.

Mientras que su misión:

pandas aims to be the fundamental high-level building block for doing practical, real world data analysis in Python. Additionally, it has the broader goal of becoming the most powerful and flexible open source data analysis / manipulation tool available in any language.

Principales Características

  • A fast and efficient DataFrame object for data manipulation with integrated indexing;

  • Tools for reading and writing data between in-memory data structures and different formats: CSV and text files, Microsoft Excel, SQL databases, and the fast HDF5 format;

  • Intelligent data alignment and integrated handling of missing data: gain automatic label-based alignment in computations and easily manipulate messy data into an orderly form;

  • Flexible reshaping and pivoting of data sets;

  • Intelligent label-based slicing, fancy indexing, and subsetting of large data sets;

  • Columns can be inserted and deleted from data structures for size mutability;

  • Aggregating or transforming data with a powerful group by engine allowing split-apply-combine operations on data sets;

  • High performance merging and joining of data sets;

  • Hierarchical axis indexing provides an intuitive way of working with high-dimensional data in a lower-dimensional data structure;

  • Time series-functionality: date range generation and frequency conversion, moving window statistics, date shifting and lagging. Even create domain-specific time offsets and join time series without losing data;

  • Highly optimized for performance, with critical code paths written in Cython or C.

  • Python with pandas is in use in a wide variety of academic and commercial domains, including Finance, Neuroscience, Economics, * Statistics, Advertising, Web Analytics, and more.

import pandas as pd
pd.__version__
'1.1.5'

Series

Arreglos unidimensionales con etiquetas. Se puede pensar como una generalización de los diccionarios de Python.

# Descomenta y ejecuta para ver la documentación sobre pd.Series
# pd.Series?

Para crear una instancia de una serie existen muchas opciones, las más comunes son:

  • A partir de una lista.

  • A partir de un numpy.array.

  • A partir de un diccionario.

  • A partir de un archivo (por ejemplo un csv).

my_serie = pd.Series(range(3, 33, 3))
my_serie
0     3
1     6
2     9
3    12
4    15
5    18
6    21
7    24
8    27
9    30
dtype: int64
type(my_serie)
pandas.core.series.Series
# Presiona TAB y sorpréndete con la cantidad de métodos y atributos que poseen!
# my_serie.

Las series son arreglos unidemensionales que constan de data e index.

# Data
my_serie.values
array([ 3,  6,  9, 12, 15, 18, 21, 24, 27, 30])
type(my_serie.values)
numpy.ndarray
# Index
my_serie.index
RangeIndex(start=0, stop=10, step=1)
type(my_serie.index)
pandas.core.indexes.range.RangeIndex

¿Te fijaste que el index es de otra clase?

A diferencia de Numpy, pandas ofrece más flexibilidad para los valores e índices.

my_serie_2 = pd.Series(range(3, 33, 3), index=list('abcdefghij'))
my_serie_2
a     3
b     6
c     9
d    12
e    15
f    18
g    21
h    24
i    27
j    30
dtype: int64

Acceder a los valores de una

my_serie_2['b']
6
my_serie_2.loc['b']
6
my_serie_2.iloc[1]
6

loc?? iloc?? ¿Qué es eso?

A modo de resumen:

  • loc es un método que hace referencia a las etiquetas (labels) del objeto .

  • iloc es un método que hace referencia posicional del objeto.

Consejo: Si quieres editar valores siempre utiliza loc y/o iloc.

my_serie_2.loc['d'] = 1000
my_serie_2
a       3
b       6
c       9
d    1000
e      15
f      18
g      21
h      24
i      27
j      30
dtype: int64

¿Y si quiero escoger más de un valor?

my_serie_2.loc["b":"e"]  # Incluso retorna el último valor!
b       6
c       9
d    1000
e      15
dtype: int64
my_serie_2.iloc[1:5]  # Incluso retorna el último valor!
b       6
c       9
d    1000
e      15
dtype: int64

Sorpresa! También puedes filtrar según condiciones!

En la mayoría de los tutoriales en internet encontrarás algo como lo siguiente:

my_serie_2[my_serie_2 % 2 == 0]
b       6
d    1000
f      18
h      24
j      30
dtype: int64

Lo siguiente se conoce como mask, y se basa en el siguiente hecho:

my_serie_2 % 2 == 0  # Retorna una serie con valores booleanos pero los mismos index!
a    False
b     True
c    False
d     True
e    False
f     True
g    False
h     True
i    False
j     True
dtype: bool

Si es una serie resultante de otra operación, tendrás que guardarla en una variable para así tener el nombre y luego acceder a ella. La siguiente manera puede qeu sea un poco más verboso, pero te otorga más flexibilidad.

my_serie_2.loc[lambda s: s % 2 == 0]
b       6
d    1000
f      18
h      24
j      30
dtype: int64

Una función lambda es una función pequeña y anónima. Pueden tomar cualquer número de argumentos pero solo tienen una expresión.

Trabajar con fechas

Pandas incluso permite que los index sean fechas! Por ejemplo, a continuación se crea una serie con fechas y valores de temperatura.

temperature = pd.read_csv(
    "https://raw.githubusercontent.com/jbrownlee/Datasets/master/daily-min-temperatures.csv",
    index_col="Date",
    squeeze=True
)
temperature.head(10)
Date
1981-01-01    20.7
1981-01-02    17.9
1981-01-03    18.8
1981-01-04    14.6
1981-01-05    15.8
1981-01-06    15.8
1981-01-07    15.8
1981-01-08    17.4
1981-01-09    21.8
1981-01-10    20.0
Name: Temp, dtype: float64
temperature.tail(10)
Date
1990-12-22    13.2
1990-12-23    13.9
1990-12-24    10.0
1990-12-25    12.9
1990-12-26    14.6
1990-12-27    14.0
1990-12-28    13.6
1990-12-29    13.5
1990-12-30    15.7
1990-12-31    13.0
Name: Temp, dtype: float64
temperature.dtype
dtype('float64')
temperature.index
Index(['1981-01-01', '1981-01-02', '1981-01-03', '1981-01-04', '1981-01-05',
       '1981-01-06', '1981-01-07', '1981-01-08', '1981-01-09', '1981-01-10',
       ...
       '1990-12-22', '1990-12-23', '1990-12-24', '1990-12-25', '1990-12-26',
       '1990-12-27', '1990-12-28', '1990-12-29', '1990-12-30', '1990-12-31'],
      dtype='object', name='Date', length=3650)

OJO! Los valores del Index son strings (object es una generalización).

Solución: Parsear a elementos de fecha con la función pd.to_datetime().

# pd.to_datetime?
temperature.index = pd.to_datetime(temperature.index, format='%Y-%m-%d')
temperature.index
DatetimeIndex(['1981-01-01', '1981-01-02', '1981-01-03', '1981-01-04',
               '1981-01-05', '1981-01-06', '1981-01-07', '1981-01-08',
               '1981-01-09', '1981-01-10',
               ...
               '1990-12-22', '1990-12-23', '1990-12-24', '1990-12-25',
               '1990-12-26', '1990-12-27', '1990-12-28', '1990-12-29',
               '1990-12-30', '1990-12-31'],
              dtype='datetime64[ns]', name='Date', length=3650, freq=None)

Para otros tipos de parse puedes visitar la documentación aquí.

La idea de los elementos de fecha es poder realizar operaciones que resulten naturales para el ser humano. Por ejemplo:

temperature.index.min()
Timestamp('1981-01-01 00:00:00')
temperature.index.max()
Timestamp('1990-12-31 00:00:00')
temperature.index.max() - temperature.index.min()
Timedelta('3651 days 00:00:00')

Volviendo a la Serie, podemos trabajar con todos sus elementos, por ejemplo, determinar rápidamente la máxima tendencia.

max_temperature = temperature.max()
max_temperature
26.3

Para determinar el index correspondiente al valor máximo usualmente se utilizan dos formas:

  • Utilizar una máscara (mask)

  • Utilizar métodos ya implementados

# Mask
temperature[temperature == max_temperature]
Date
1982-02-15    26.3
Name: Temp, dtype: float64
# Built-in method
temperature.idxmax()
Timestamp('1982-02-15 00:00:00')

DataFrames

Arreglo bidimensional y extensión natural de una serie. Podemos pensarlo como la generalización de un numpy.array.

Para motivar utilizaremos los datos de COVID-19 disponibilizados por el Ministerio de Ciencias, Tecnología, Conocimiento e Innovación de Chile. En particular, los casos totales por comuna, puedes leer más detalles en el repositorio oficial.

covid = pd.read_csv("https://raw.githubusercontent.com/MinCiencia/Datos-COVID19/master/output/producto1/Covid-19_std.csv")
covid.head()
Region Codigo region Comuna Codigo comuna Poblacion Fecha Casos confirmados
0 Arica y Parinacota 15 Arica 15101.0 247552.0 2020-03-30 6.0
1 Arica y Parinacota 15 Camarones 15102.0 1233.0 2020-03-30 0.0
2 Arica y Parinacota 15 General Lagos 15202.0 810.0 2020-03-30 0.0
3 Arica y Parinacota 15 Putre 15201.0 2515.0 2020-03-30 0.0
4 Arica y Parinacota 15 Desconocido Arica y Parinacota NaN NaN 2020-03-30 NaN
covid.tail()
Region Codigo region Comuna Codigo comuna Poblacion Fecha Casos confirmados
27145 Magallanes 12 Rio Verde 12103.0 211.0 2020-12-07 3.0
27146 Magallanes 12 San Gregorio 12104.0 681.0 2020-12-07 29.0
27147 Magallanes 12 Timaukel 12303.0 282.0 2020-12-07 20.0
27148 Magallanes 12 Torres del Paine 12402.0 1021.0 2020-12-07 3.0
27149 Magallanes 12 Desconocido Magallanes NaN NaN 2020-12-07 36.0
covid.query("Region == 'Valparaíso'")
Region Codigo region Comuna Codigo comuna Poblacion Fecha Casos confirmados
49 Valparaíso 5 Algarrobo 5602.0 15174.0 2020-03-30 7.0
50 Valparaíso 5 Cabildo 5402.0 20663.0 2020-03-30 0.0
51 Valparaíso 5 Calera 5502.0 53591.0 2020-03-30 6.0
52 Valparaíso 5 Calle Larga 5302.0 16482.0 2020-03-30 0.0
53 Valparaíso 5 Cartagena 5603.0 25357.0 2020-03-30 0.0
... ... ... ... ... ... ... ...
26871 Valparaíso 5 Valparaiso 5101.0 315732.0 2020-12-07 9002.0
26872 Valparaíso 5 Villa Alemana 5804.0 139310.0 2020-12-07 3426.0
26873 Valparaíso 5 Vina del Mar 5109.0 361371.0 2020-12-07 8474.0
26874 Valparaíso 5 Zapallar 5405.0 7994.0 2020-12-07 160.0
26875 Valparaíso 5 Desconocido Valparaiso NaN NaN 2020-12-07 439.0

2925 rows × 7 columns

type(covid)
pandas.core.frame.DataFrame
covid.info(memory_usage=True)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 27150 entries, 0 to 27149
Data columns (total 7 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   Region             27150 non-null  object 
 1   Codigo region      27150 non-null  int64  
 2   Comuna             27150 non-null  object 
 3   Codigo comuna      25950 non-null  float64
 4   Poblacion          25950 non-null  float64
 5   Fecha              27150 non-null  object 
 6   Casos confirmados  26731 non-null  float64
dtypes: float64(3), int64(1), object(3)
memory usage: 1.5+ MB
covid.dtypes
Region                object
Codigo region          int64
Comuna                object
Codigo comuna        float64
Poblacion            float64
Fecha                 object
Casos confirmados    float64
dtype: object

Puedes pensar que un dataframe es una colección de series

covid['Casos confirmados'].head()
0    6.0
1    0.0
2    0.0
3    0.0
4    NaN
Name: Casos confirmados, dtype: float64
type(covid['Casos confirmados'])
pandas.core.series.Series

Exploración

covid.describe().T
count mean std min 25% 50% 75% max
Codigo region 27150.0 8.784530 3.879182 1.0 6.0 8.0 13.0 16.0
Codigo comuna 25950.0 9034.997110 3812.699348 1101.0 6109.0 8313.5 13103.0 16305.0
Poblacion 25950.0 56237.890173 88819.050815 137.0 9546.0 19770.0 56058.0 645909.0
Casos confirmados 26731.0 928.728181 2458.360944 0.0 11.0 98.0 431.0 28772.0
covid.describe(include='all').T
count unique top freq mean std min 25% 50% 75% max
Region 27150 16 Metropolitana 3975 NaN NaN NaN NaN NaN NaN NaN
Codigo region 27150 NaN NaN NaN 8.78453 3.87918 1 6 8 13 16
Comuna 27150 362 Coronel 75 NaN NaN NaN NaN NaN NaN NaN
Codigo comuna 25950 NaN NaN NaN 9035 3812.7 1101 6109 8313.5 13103 16305
Poblacion 25950 NaN NaN NaN 56237.9 88819.1 137 9546 19770 56058 645909
Fecha 27150 75 2020-09-25 362 NaN NaN NaN NaN NaN NaN NaN
Casos confirmados 26731 NaN NaN NaN 928.728 2458.36 0 11 98 431 28772
covid.max()
Region                    Ñuble
Codigo region                16
Comuna                 Zapallar
Codigo comuna             16305
Poblacion                645909
Fecha                2020-12-07
Casos confirmados         28772
dtype: object

Para extraer elementos lo más recomendable es el método loc.

covid.loc[19271, 'Region']
'Valparaíso'

Evita acceder con doble corchete

covid[19271]['Region']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
/opt/hostedtoolcache/Python/3.7.9/x64/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2897             try:
-> 2898                 return self._engine.get_loc(casted_key)
   2899             except KeyError as err:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 19271

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
<ipython-input-48-16f1c11e5439> in <module>
----> 1 covid[19271]['Region']

/opt/hostedtoolcache/Python/3.7.9/x64/lib/python3.7/site-packages/pandas/core/frame.py in __getitem__(self, key)
   2904             if self.columns.nlevels > 1:
   2905                 return self._getitem_multilevel(key)
-> 2906             indexer = self.columns.get_loc(key)
   2907             if is_integer(indexer):
   2908                 indexer = [indexer]

/opt/hostedtoolcache/Python/3.7.9/x64/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2898                 return self._engine.get_loc(casted_key)
   2899             except KeyError as err:
-> 2900                 raise KeyError(key) from err
   2901 
   2902         if tolerance is not None:

KeyError: 19271

Aunque en ocasiones funcione, no se asegura que sea siempre así. Más info aquí.

covid['Region'].value_counts()
Metropolitana         3975
Valparaíso            2925
O’Higgins             2550
Biobío                2550
Araucanía             2475
Los Lagos             2325
Maule                 2325
Ñuble                 1650
Coquimbo              1200
Los Ríos               975
Magallanes             900
Aysén                  825
Atacama                750
Antofagasta            750
Tarapacá               600
Arica y Parinacota     375
Name: Region, dtype: int64

Valores perdidos/nulos

Pandas ofrece herramientas para trabajar con valors nulos, pero es necesario conocerlas y saber aplicarlas. Por ejemplo, el método isnull() entrega un booleano si algún valor es nulo.

Ejemplo: ¿Qué registros tienen casos confirmados nulos?

covid.index.shape
(27150,)
covid.loc[lambda x: x['Casos confirmados'].isnull()]
Region Codigo region Comuna Codigo comuna Poblacion Fecha Casos confirmados
4 Arica y Parinacota 15 Desconocido Arica y Parinacota NaN NaN 2020-03-30 NaN
12 Tarapacá 1 Desconocido Tarapaca NaN NaN 2020-03-30 NaN
22 Antofagasta 2 Desconocido Antofagasta NaN NaN 2020-03-30 NaN
32 Atacama 3 Desconocido Atacama NaN NaN 2020-03-30 NaN
48 Coquimbo 4 Desconocido Coquimbo NaN NaN 2020-03-30 NaN
... ... ... ... ... ... ... ...
9344 Araucanía 9 Desconocido Araucania NaN NaN 2020-06-15 NaN
9357 Los Ríos 14 Desconocido Los Rios NaN NaN 2020-06-15 NaN
9388 Los Lagos 10 Desconocido Los Lagos NaN NaN 2020-06-15 NaN
9399 Aysén 11 Desconocido Aysen NaN NaN 2020-06-15 NaN
9411 Magallanes 12 Desconocido Magallanes NaN NaN 2020-06-15 NaN

419 rows × 7 columns

Si deseamos encontrar todas las filas que contengan por lo menos un valor nulo.

covid.isnull()
Region Codigo region Comuna Codigo comuna Poblacion Fecha Casos confirmados
0 False False False False False False False
1 False False False False False False False
2 False False False False False False False
3 False False False False False False False
4 False False False True True False True
... ... ... ... ... ... ... ...
27145 False False False False False False False
27146 False False False False False False False
27147 False False False False False False False
27148 False False False False False False False
27149 False False False True True False False

27150 rows × 7 columns

rows_null_mask = covid.isnull().any(axis=1)  # axis=1 hace referencia a las filas.
rows_null_mask.head()
0    False
1    False
2    False
3    False
4     True
dtype: bool
covid[rows_null_mask].head()
Region Codigo region Comuna Codigo comuna Poblacion Fecha Casos confirmados
4 Arica y Parinacota 15 Desconocido Arica y Parinacota NaN NaN 2020-03-30 NaN
12 Tarapacá 1 Desconocido Tarapaca NaN NaN 2020-03-30 NaN
22 Antofagasta 2 Desconocido Antofagasta NaN NaN 2020-03-30 NaN
32 Atacama 3 Desconocido Atacama NaN NaN 2020-03-30 NaN
48 Coquimbo 4 Desconocido Coquimbo NaN NaN 2020-03-30 NaN
covid[rows_null_mask].shape
(1203, 7)

Para determinar aquellos que no tienen valors nulos el procedimiento es similar.

covid.loc[lambda x: x.notnull().all(axis=1)].head()
Region Codigo region Comuna Codigo comuna Poblacion Fecha Casos confirmados
0 Arica y Parinacota 15 Arica 15101.0 247552.0 2020-03-30 6.0
1 Arica y Parinacota 15 Camarones 15102.0 1233.0 2020-03-30 0.0
2 Arica y Parinacota 15 General Lagos 15202.0 810.0 2020-03-30 0.0
3 Arica y Parinacota 15 Putre 15201.0 2515.0 2020-03-30 0.0
5 Tarapacá 1 Alto Hospicio 1107.0 129999.0 2020-03-30 0.0

Pandas incluso ofrece opciones para eliminar elementos nulos!

# Cualquier registro con null
print(covid.dropna().shape)
# Filas con elementos nulos
print(covid.dropna(axis=0).shape)
# Columnas con elementos nulos
print(covid.dropna(axis=1).shape)
(25947, 7)
(25947, 7)
(27150, 4)

Resumen

  • Pandas posee una infinidad de herramientas para trabajar con datos, incluyendo la carga, manipulación, operaciones y filtrado de datos.

  • La documentación oficial (y StackOverflow) son tus mejores amigos.

  • La importancia está en darle sentido a los datos, no solo a coleccionarlos.