Pandas#
¿Qué es Pandas?#
Seguramente conoces la aplicación de Microsoft llamada Excel para trabajar con hojas de cálculo. Seguramente lo has usado alguna vez. Pues si tenemos que contarte de una manera directa y sencilla qué es Pandas, Pandas es el «Excel» de Python. Bueno, cuando lo conozcas pensarás que la comparación no es del todo apropiada… pero para tener una primera idea, la imagen es muy útil.
Pandas es una muy potente librería para el análisis y la manipulación de datos. Estos datos se trabajan en dos tipos de estructuras: el marco de datos (DataFrame) y la serie de datos. Podrías pensar que la primera estructura es lo que conocías como «hoja de cálculo» y la serie como su propio nombre indica es una columna de esa «hoja de cálculo».
Pandas es una de las herramientas más populares para el trabajo en ciencia de datos. Encontrarás muchas herramientas para el análisis de tus datos, y como veremos con la librería Seaborn, el «marco de datos» de Pandas se ha convertido en otro standard de almacenamiento de datos usado al igual que los arreglos de Numpy en muchas otras librerías.
Pero mejor que describir qué es Pandas, es que veamos un poquito cómo funciona y qué es un marco de datos (DataFrame).
¿Cómo se instala?#
Para ver instrucciones generales de instalación puedes visitar la página oficial. Allí encontrarás indicaciones específicas para tu sistema operativo y/o tu gestor de entornos y paquetes. Aquí supondremos que estás trabajando en tu entorno de conda, así que el comando que debes teclear en la terminal es:
conda install -c conda-forge pandas
¿Cómo se usa?#
Importando Pandas#
import pandas as pd
El Pandas DataFrame#
Veamos con un ejemplo lo que es un DataFrame de Pandas.
Supongamos que tenemos un conjunto de datos de 5 especies distintas «A», «B», «C», «D» y «E». Estos datos son el valor numérico de 4 atributos: «Atributo 1», «Atributo 2», «Atributo 3» y «Atributo 4». Para generar los datos vamos a hacer uso de la misma estrategia que viste en uno de los retos de la semana pasada:
from sklearn import datasets
dataset, membership = datasets.make_blobs(n_samples=500, n_features=4, centers=5, cluster_std=1.0)
cluster_a_especie = {0:'A', 1:'B', 2:'C', 3:'D', 4:'E'}
especies = [cluster_a_especie[ii] for ii in membership]
Tenemos los datos numéricos de los atributos en un arreglo de Numpy de tamaño (500,4) y la especie a la que pertenece cada individuo en una lista de longitud 500:
type(dataset)
numpy.ndarray
dataset.shape
(500, 4)
type(especies)
list
len(especies)
500
¿No sería muy útil tener todas los datos en un solo objeto? Y si además ese objeto me proporciona herramientas de manipulación y análisis… Pues te presento al marco de datos (DataFrame) de Pandas:
dataframe = pd.DataFrame()
dataframe['Atributo 1']=dataset[:,0]
dataframe['Atributo 2']=dataset[:,1]
dataframe['Atributo 3']=dataset[:,2]
dataframe['Atributo 4']=dataset[:,3]
dataframe['Especie']=especies
dataframe
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
1 | 6.654302 | -0.522717 | 6.936169 | -3.029801 | C |
2 | 1.084041 | -1.428295 | -5.913394 | -5.159838 | A |
3 | -7.415053 | -6.990136 | 9.023421 | 6.577744 | D |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
... | ... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 9.433656 | 5.331978 | D |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
497 | 4.403590 | -1.998225 | 6.448401 | -3.669574 | C |
498 | -1.289086 | -1.707437 | -5.949482 | -5.019170 | A |
499 | 6.746350 | -0.256744 | 6.414771 | -2.695230 | C |
500 rows × 5 columns
Podemos acceder a un cualquiera de las columnas para obtener la serie correspondiente:
atributo_3 = dataframe['Atributo 3']
atributo_3
0 7.492949
1 6.936169
2 -5.913394
3 9.023421
4 6.599918
...
495 9.433656
496 8.379839
497 6.448401
498 -5.949482
499 6.414771
Name: Atributo 3, Length: 500, dtype: float64
Un primer vistazo estadístico#
Pandas cuenta con una función que arroja información estadística como el promedio, la desviación estandard, los valores mínimo y máximo y tres distintos percentiles:
dataframe.describe()
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | |
---|---|---|---|---|
count | 500.000000 | 500.000000 | 500.000000 | 500.000000 |
mean | 0.019549 | -2.593211 | 4.054476 | 0.344403 |
std | 4.420546 | 3.980898 | 5.173626 | 4.311903 |
min | -9.470652 | -10.078659 | -8.945344 | -7.708046 |
25% | -2.440873 | -6.318444 | 1.538972 | -2.992191 |
50% | 0.920906 | -2.379345 | 6.779586 | -1.157827 |
75% | 2.995962 | -0.497963 | 7.949467 | 4.626871 |
max | 8.557497 | 5.647253 | 10.619756 | 8.568374 |
Manipulando el Dataframe#
Veamos unos ejemplos de cómo manipular el DataFrame. Por ejemplo, vamos a ordenar los datos de acuerdo al valor de la column “Especie”:
dataframe.sort_values(by=["Especie", "Atributo 1"])
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
389 | -1.524446 | -0.663932 | -5.266990 | -3.919651 | A |
498 | -1.289086 | -1.707437 | -5.949482 | -5.019170 | A |
273 | -1.254491 | -2.593827 | -5.219352 | -7.708046 | A |
31 | -0.950144 | -1.690220 | -4.471158 | -3.836658 | A |
376 | -0.833456 | -1.700839 | -6.290292 | -4.612972 | A |
... | ... | ... | ... | ... | ... |
163 | -0.007812 | -4.793799 | 2.415810 | -1.691661 | E |
57 | 0.057350 | -4.858111 | 2.977075 | -0.621651 | E |
436 | 0.093176 | -4.046110 | 3.092237 | -1.443225 | E |
135 | 0.205028 | -6.151401 | 4.546470 | -0.702314 | E |
364 | 0.536152 | -6.333056 | 1.420014 | -1.305269 | E |
500 rows × 5 columns
Vamos a remover una columna del DataFrame:
dataframe = dataframe.drop('Atributo 3', axis = 1)
dataframe
Atributo 1 | Atributo 2 | Atributo 4 | Especie | |
---|---|---|---|---|
0 | 2.565065 | 3.380599 | 5.165165 | B |
1 | 6.654302 | -0.522717 | -3.029801 | C |
2 | 1.084041 | -1.428295 | -5.159838 | A |
3 | -7.415053 | -6.990136 | 6.577744 | D |
4 | 1.880746 | 5.647253 | 4.420377 | B |
... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 5.331978 | D |
496 | -0.076391 | 4.039119 | 3.556502 | B |
497 | 4.403590 | -1.998225 | -3.669574 | C |
498 | -1.289086 | -1.707437 | -5.019170 | A |
499 | 6.746350 | -0.256744 | -2.695230 | C |
500 rows × 4 columns
Y ahora vamos a añadir una columna:
dataframe['Atributo 3']=atributo_3
dataframe
Atributo 1 | Atributo 2 | Atributo 4 | Especie | Atributo 3 | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 5.165165 | B | 7.492949 |
1 | 6.654302 | -0.522717 | -3.029801 | C | 6.936169 |
2 | 1.084041 | -1.428295 | -5.159838 | A | -5.913394 |
3 | -7.415053 | -6.990136 | 6.577744 | D | 9.023421 |
4 | 1.880746 | 5.647253 | 4.420377 | B | 6.599918 |
... | ... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 5.331978 | D | 9.433656 |
496 | -0.076391 | 4.039119 | 3.556502 | B | 8.379839 |
497 | 4.403590 | -1.998225 | -3.669574 | C | 6.448401 |
498 | -1.289086 | -1.707437 | -5.019170 | A | -5.949482 |
499 | 6.746350 | -0.256744 | -2.695230 | C | 6.414771 |
500 rows × 5 columns
Podemos reordenar las columnas de las siguientes maneras:
dataframe = dataframe[[ 'Atributo 1', 'Atributo 2', 'Atributo 3', 'Atributo 4', 'Especie' ]]
dataframe
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
1 | 6.654302 | -0.522717 | 6.936169 | -3.029801 | C |
2 | 1.084041 | -1.428295 | -5.913394 | -5.159838 | A |
3 | -7.415053 | -6.990136 | 9.023421 | 6.577744 | D |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
... | ... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 9.433656 | 5.331978 | D |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
497 | 4.403590 | -1.998225 | 6.448401 | -3.669574 | C |
498 | -1.289086 | -1.707437 | -5.949482 | -5.019170 | A |
499 | 6.746350 | -0.256744 | 6.414771 | -2.695230 | C |
500 rows × 5 columns
Seleccionando elementos#
Vimos como seleccionar una columna:
dataframe['Atributo 2']
0 3.380599
1 -0.522717
2 -1.428295
3 -6.990136
4 5.647253
...
495 -7.541511
496 4.039119
497 -1.998225
498 -1.707437
499 -0.256744
Name: Atributo 2, Length: 500, dtype: float64
Veamos ahora como seleccionar una fila:
dataframe.iloc[2]
Atributo 1 1.084041
Atributo 2 -1.428295
Atributo 3 -5.913394
Atributo 4 -5.159838
Especie A
Name: 2, dtype: object
O podemos seleccionar un valor concreto:
dataframe['Atributo 2'].iloc[2]
-1.428295215827415
Haciendo búsquedas#
Podemos hacer búsquedas en los DataFrame, por ejemplo:
dataframe.query("Especie=='B' and `Atributo 2`<`Atributo 3`")
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
6 | 1.890776 | 1.374552 | 6.532341 | 3.206050 | B |
8 | 2.265224 | 3.618833 | 7.449796 | 4.040097 | B |
12 | 0.655601 | 3.235420 | 9.308905 | 2.938250 | B |
... | ... | ... | ... | ... | ... |
465 | 0.839746 | 3.408758 | 7.486306 | 2.980499 | B |
472 | 3.300358 | 3.577219 | 6.928896 | 2.829937 | B |
475 | 3.445820 | 4.494670 | 8.196021 | 3.586597 | B |
477 | 2.396685 | 4.062632 | 6.771329 | 2.375115 | B |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
100 rows × 5 columns
O podemos hacer uso de máscaras lógicas para seleccionar elementos del marco de datos:
mask = (dataframe['Especie']=='B') & (dataframe['Atributo 1'].abs()<7.0)
dataframe[mask]
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
6 | 1.890776 | 1.374552 | 6.532341 | 3.206050 | B |
8 | 2.265224 | 3.618833 | 7.449796 | 4.040097 | B |
12 | 0.655601 | 3.235420 | 9.308905 | 2.938250 | B |
... | ... | ... | ... | ... | ... |
465 | 0.839746 | 3.408758 | 7.486306 | 2.980499 | B |
472 | 3.300358 | 3.577219 | 6.928896 | 2.829937 | B |
475 | 3.445820 | 4.494670 | 8.196021 | 3.586597 | B |
477 | 2.396685 | 4.062632 | 6.771329 | 2.375115 | B |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
100 rows × 5 columns
Asignando valores#
Antes ver unos ejemplos de análisis de datos en el DataFrame, vamos como podemos asignar o reasignar valores:
dataframe.at[1,'Especie'] = 'H'
dataframe
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
1 | 6.654302 | -0.522717 | 6.936169 | -3.029801 | H |
2 | 1.084041 | -1.428295 | -5.913394 | -5.159838 | A |
3 | -7.415053 | -6.990136 | 9.023421 | 6.577744 | D |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
... | ... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 9.433656 | 5.331978 | D |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
497 | 4.403590 | -1.998225 | 6.448401 | -3.669574 | C |
498 | -1.289086 | -1.707437 | -5.949482 | -5.019170 | A |
499 | 6.746350 | -0.256744 | 6.414771 | -2.695230 | C |
500 rows × 5 columns
Algunos análisis como muestra#
Pandas ofrece junto al objeto DataFrame una larga batería de análisis. Veamos un par de ejemplos para que quien no conoce Pandas pueda tener una idea de qué cosas puede esperar de esta librería.
Calculemos por ejemplo el valor promedio de cada atributo para la especie “B”:
dataframe.groupby('Especie').mean()
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | |
---|---|---|---|---|
Especie | ||||
A | 1.108494 | -1.549203 | -5.181803 | -4.965392 |
B | 2.292240 | 3.614061 | 7.631267 | 4.204243 |
C | 5.620640 | -1.716874 | 7.595616 | -2.135704 |
D | -7.135466 | -7.042641 | 7.940210 | 6.213119 |
E | -1.798500 | -6.283341 | 2.293686 | -1.585311 |
H | 6.654302 | -0.522717 | 6.936169 | -3.029801 |
Vamos por ejemplo a suponer por ejemplo que el marco de datos tiene un defecto, falta concretamente el valor del Atributo 3 de la fila con índice 2. En su lugar encontramos «NaN» («not a number»):
dataframe
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
1 | 6.654302 | -0.522717 | 6.936169 | -3.029801 | H |
2 | 1.084041 | -1.428295 | -5.913394 | -5.159838 | A |
3 | -7.415053 | -6.990136 | 9.023421 | 6.577744 | D |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
... | ... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 9.433656 | 5.331978 | D |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
497 | 4.403590 | -1.998225 | 6.448401 | -3.669574 | C |
498 | -1.289086 | -1.707437 | -5.949482 | -5.019170 | A |
499 | 6.746350 | -0.256744 | 6.414771 | -2.695230 | C |
500 rows × 5 columns
from numpy import nan
print(f'El valor era: {dataframe["Atributo 3"][2]}')
dataframe.at[2,'Atributo 3'] = nan
El valor era: -5.913393757806304
dataframe
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
1 | 6.654302 | -0.522717 | 6.936169 | -3.029801 | H |
2 | 1.084041 | -1.428295 | NaN | -5.159838 | A |
3 | -7.415053 | -6.990136 | 9.023421 | 6.577744 | D |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
... | ... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 9.433656 | 5.331978 | D |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
497 | 4.403590 | -1.998225 | 6.448401 | -3.669574 | C |
498 | -1.289086 | -1.707437 | -5.949482 | -5.019170 | A |
499 | 6.746350 | -0.256744 | 6.414771 | -2.695230 | C |
500 rows × 5 columns
Vamos a corregir el error interpolando linealmente en el espacio de cuatro dimensiones para inferir el valor faltante:
new_dataframe = dataframe.interpolate(method='linear')
new_dataframe
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
1 | 6.654302 | -0.522717 | 6.936169 | -3.029801 | H |
2 | 1.084041 | -1.428295 | 7.979795 | -5.159838 | A |
3 | -7.415053 | -6.990136 | 9.023421 | 6.577744 | D |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
... | ... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 9.433656 | 5.331978 | D |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
497 | 4.403590 | -1.998225 | 6.448401 | -3.669574 | C |
498 | -1.289086 | -1.707437 | -5.949482 | -5.019170 | A |
499 | 6.746350 | -0.256744 | 6.414771 | -2.695230 | C |
500 rows × 5 columns
Comprobemos si el punto 2, con la corrección inferida, aun se encuentra más cerca del centro geométrico de su especie que del resto:
especie_del_punto = dataframe['Especie'][2]
print(especie_del_punto)
A
from scipy.spatial.distance import euclidean
dataframe_centros = dataframe.groupby('Especie').mean()
coordinates_2 = new_dataframe.loc[2, new_dataframe.columns!="Especie"]
for ii in dataframe_centros.index:
distancia = euclidean(dataframe_centros.loc[ii,:], coordinates_2)
print(f'Distancia con el centro de {ii}: {distancia}')
Distancia con el centro de A: 13.156223390791219
Distancia con el centro de B: 10.709462564271387
Distancia con el centro de C: 5.473297871564837
Distancia con el centro de D: 15.113797850925195
Distancia con el centro de E: 8.774370810688382
Distancia con el centro de H: 6.121609137382962
Exportando e importando datos#
Pandas es capaz de leer y/o escribir diversos formatos de fichero. Por mencionar algunos:
csv
hdf5
excel
json
sql
markdown
html
Vamos a probar a escribir un fichero excel con nuestro marco de datos y a leerlo:
new_dataframe.to_excel('datos.xlsx', sheet_name="Hoja1")
excel_dataframe=pd.read_excel("datos.xlsx", "Hoja1", index_col=0)
excel_dataframe
Atributo 1 | Atributo 2 | Atributo 3 | Atributo 4 | Especie | |
---|---|---|---|---|---|
0 | 2.565065 | 3.380599 | 7.492949 | 5.165165 | B |
1 | 6.654302 | -0.522717 | 6.936169 | -3.029801 | H |
2 | 1.084041 | -1.428295 | 7.979795 | -5.159838 | A |
3 | -7.415053 | -6.990136 | 9.023421 | 6.577744 | D |
4 | 1.880746 | 5.647253 | 6.599918 | 4.420377 | B |
... | ... | ... | ... | ... | ... |
495 | -7.451682 | -7.541511 | 9.433656 | 5.331978 | D |
496 | -0.076391 | 4.039119 | 8.379839 | 3.556502 | B |
497 | 4.403590 | -1.998225 | 6.448401 | -3.669574 | C |
498 | -1.289086 | -1.707437 | -5.949482 | -5.019170 | A |
499 | 6.746350 | -0.256744 | 6.414771 | -2.695230 | C |
500 rows × 5 columns
Alternativas#
Existen herramientas basadas en Pandas o con la misma filosofía pero con características que pueden ser interesantes según la naturaleza y propiedades de los datos o la infraestructura de cálculo.
De propósito general#
Modin
Polars
Dask
Vaes
Pyspark
De propósito específico#
Biopandas (para bioinformática)