Pandas#
En la sección anterior ya vimos como numpy proporciona soporte para arrays de con tipos homogéneos. En el último apartado vimos los arrays estructurados, un tipo de array para tratar con datos tabulares. Sin embargo, los propios desarrolladores de numpy indican que esta estructura solo es apropiada para los casos de uso más sencillos.
Pandas es un paquete construido sobre numpy que proporciona
un tipo de datos DataFrame
. Este tipo es, básicamente, un array
multidimensional heterogéneo, con capacidad de manejar datos faltantes
y con nombres para filas y columnas.
import pandas as pd
pd.__version__
'2.2.2'
Tipos de datos#
Los tipos de datos de pandas pueden entenderse como versiones mejoradas de los arrays estructurados de numpy, donde además se pueden añadir etiquetas tanto a filas como a columnas.
Pandas proporciona, básicamente, objetos para 1d, 2d y 3d.
El tipo 3d se denomina Panel
y no lo veremos aquí.
Series
#
El tipo 1D se denomina Series
data = pd.Series([-1.0, -0.5, 0, 0.5, 1.0])
data
0 -1.0
1 -0.5
2 0.0
3 0.5
4 1.0
dtype: float64
Podemos ver que además de tener los datos, también tenemos
una secuencia de índices. Se puede acceder a ambos con values
e indexp
, respectivamente
print(type(data.values))
data.values
<class 'numpy.ndarray'>
array([-1. , -0.5, 0. , 0.5, 1. ])
print(type(data.index))
data.index
<class 'pandas.core.indexes.range.RangeIndex'>
RangeIndex(start=0, stop=5, step=1)
Podemos acceder a los valores indexando como una lista:
data[2]
np.float64(0.0)
data[1:4]
1 -0.5
2 0.0
3 0.5
dtype: float64
Podría parecer que Series
es intercambiable con ndarray
.
Una diferencia fundamental es el índice. Mientras que en numpy es
implícito (entero, empieza en cero), en pandas es explícito.
Podemos, por ejemplo, utilizar una cadena como índice:
data = pd.Series([-1.0, -0.5, 0, 0.5, 1.0],
index=['a', 'b', 'c', 'd', 'e'])
data
a -1.0
b -0.5
c 0.0
d 0.5
e 1.0
dtype: float64
El acceso a los datos sigue funcionando:
data['a']
np.float64(-1.0)
data['a':'d']
a -1.0
b -0.5
c 0.0
d 0.5
dtype: float64
También podemos usar como índices enteros, pero no ordenados, por ejemplo:
data = pd.Series([-1.0, -0.5, 0, 0.5, 1.0],
index=[1, 3, 0, 2, 4])
data
1 -1.0
3 -0.5
0 0.0
2 0.5
4 1.0
dtype: float64
Y entonces tenemos que:
data[3]
np.float64(-0.5)
Como vemos, es el índice, y no la posición, lo que se utiliza
para acceder a los datos con el operador []
.
En esto, Series
se parece a una generalización de un diccionario:
datadic = {1: -1.0, 3:-0.5, 0:0.0, 2:0.5, 4:1.0}
datadic[3]
-0.5
En realidad, Series
puede crearse a partir de un diccionario;
index
se genera automáticamente a partir de las claves del diccionario
rocky = {'Mercurio': 0.39, 'Venus':0.72, 'Tierra':1.0, 'Marte':1.52}
pd.Series(rocky)
Mercurio 0.39
Venus 0.72
Tierra 1.00
Marte 1.52
dtype: float64
Incluso aquí podemos decidir reorder o no incluir valores:
pd.Series(rocky, index=['Marte', 'Tierra'])
Marte 1.52
Tierra 1.00
dtype: float64
Pueden encontrarse más formas de construir Series
en la documentación de Pandas.
DataFrame
#
La siguiente estructura es DataFrame
. Igual que con Series
, podemos
entendarla como una generalización de un ndarray
o de un diccionario.
Podemos imaginar un DataFrame
como una secuencia objetos Serie
con un índice común.
rocky_d = {'Mercurio': 0.39, 'Venus':0.72, 'Tierra':1.0, 'Marte':1.52}
dis = pd.Series(rocky_d)
rocky_l = {'Mercurio': 0, 'Venus':0, 'Tierra':1, 'Marte':2}
lunas = pd.Series(rocky_l)
Ahora que tenemos dos series, las combinamos en un DataFrame
rocky = pd.DataFrame({'distance': dis, 'moons': lunas})
rocky
distance | moons | |
---|---|---|
Mercurio | 0.39 | 0 |
Venus | 0.72 | 0 |
Tierra | 1.00 | 1 |
Marte | 1.52 | 2 |
El objeto DataFrame
tiene un index
, igual que Series
rocky.index
Index(['Mercurio', 'Venus', 'Tierra', 'Marte'], dtype='object')
Además, tiene un atributo columns
:
rocky.columns
Index(['distance', 'moons'], dtype='object')
Así que podemos imaginar DataFrame
como una generalización de
un array estructurado de numpy, en el que tanto filas como columnas
tienen un índice generalizado.
Pero quizá sea más claro pensar que DataFrame
es una generalización
de un diccionario, cuyos valores son Series
. Así:
Además, tiene un atributo columns
:
rocky['moons']
Mercurio 0
Venus 0
Tierra 1
Marte 2
Name: moons, dtype: int64
Mientras que un ndarray, data[0]
devuelve la primera fila,
en un DataFrame, data['valor']
devuelve una columna.
Se pueden construir objetos DataFrame
de muchas maneras diferentes,
atendiendo a su doble condición de seudo-array y seudo-diccionario.
Por ejemplo:
Con una Serie:
pd.DataFrame(dis, columns=['distance'])
distance | |
---|---|
Mercurio | 0.39 |
Venus | 0.72 |
Tierra | 1.00 |
Marte | 1.52 |
Con una lista de diccionarios:
data = [{'a': 1, 'b': 2}, {'b': 6, 'c': 1}]
pd.DataFrame(data)
a | b | c | |
---|---|---|---|
0 | 1.0 | 2 | NaN |
1 | NaN | 6 | 1.0 |
Aquí se usan las claves como columnas, y el número
de orden del diccionario en la lista como índice.
Y las claves no comunes se marcan como NaN
Como un diccionario de Series (que ya vimos más arriba):
pd.DataFrame({'distance': dis, 'moons': lunas})
distance | moons | |
---|---|---|
Mercurio | 0.39 | 0 |
Venus | 0.72 | 0 |
Tierra | 1.00 | 1 |
Marte | 1.52 | 2 |
A partir de un array:
import numpy as np
data = np.random.randint(10, size=(3,2))
pd.DataFrame(data)
0 | 1 | |
---|---|---|
0 | 5 | 5 |
1 | 4 | 2 |
2 | 5 | 0 |
Si no definimos ni columnas ni filas, se utizan secuencias de enteros.
pd.DataFrame(data, columns=['eggs', 'spam'],
index=['a', 'b', 'c'])
eggs | spam | |
---|---|---|
a | 5 | 5 |
b | 4 | 2 |
c | 5 | 0 |
Y de un array estructurado:
estrc = np.zeros(3, dtype=[('A', 'i8'), ('B', 'f8')])
pd.DataFrame(estrc)
A | B | |
---|---|---|
0 | 0 | 0.0 |
1 | 0 | 0.0 |
2 | 0 | 0.0 |
Index
#
Hemos visto que tanto Series
como DataFrame
tienen un objeto index
.
Este puede entenderse o bien como un array inmutable o bien como
un conjunto ordenado (como el tipo set
de python, pero manteniendo el
orden de inserción). Como las propiedades del índice tiene consecuencias
en la indexación en pandas, vamos a explorarlas.
ind = pd.Index([4, 2, 1, 7, 11])
Los objetos de índice tiene muchas propiedades en común con ndarray
print(ind.shape)
print(ind.dtype)
print(ind[1])
print(ind[::2])
(5,)
int64
2
Index([4, 1, 11], dtype='int64')
Una diferencias es que Index
es inmutable
ind[0] = 8
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[28], line 1
----> 1 ind[0] = 8
File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/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
Por otro lado, Index
comparte propiedades del tipo set
.
Al crear DataFrame
tenemos que poder unir los indices que proceden
de los objetos Serie
ind1 = pd.Index([1, 3, 5, 7, 9])
ind2 = pd.Index([2, 3, 5, 7, 11])
Tenemos las operaciones de intersección y unión de conjuntos:
ind1.intersection(ind2)
Index([3, 5, 7], dtype='int64')
ind1.union(ind2)
Index([1, 2, 3, 5, 7, 9, 11], dtype='int64')
Así como la diferencia simétrica (elementos de la unión que no están en la intersección)
ind1.symmetric_difference(ind2)
Index([1, 2, 9, 11], dtype='int64')
En versiones anteriores de pandas, era posible usar &
, |
y ^
como
en los objetos set
, pero esta opción se está eliminando:
ind1 | ind2
Index([3, 3, 5, 7, 11], dtype='int64')