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.