Pandas#
En esta sección haremos una introducción a la librería pandas de Python, una herramienta muy útil para el análisis de datos. Proporciona estructuras de datos y operaciones para manipular y analizar datos de manera rápida y eficiente, así como funcionalidades de lectura y escritura de datos en diferentes formatos, como CSV, Excel, SQL, entre otros. También permite realizar operaciones matemáticas y estadísticas en los datos, así como visualizarlos en gráficos y tablas de manera cómoda gracias a su integración con numpy y matplotlib. En resumen, pandas es una librería muy útil para cualquier persona que trabaje con datos y necesite realizar análisis y operaciones en ellos de manera rápida y eficiente.
La integración entre numpy y pandas se realiza mediante el uso de los arrays de numpy como el tipo de dato subyacente en las estructuras de datos de pandas. Esto permite que pandas utilice la eficiencia y la velocidad de cálculo de numpy en sus operaciones, mientras que proporciona una interfaz de usuario más amigable y especializada para trabajar con datos tabulares.
Normalmente el módulo se suele importar con el alias pd
import pandas as pd
import numpy as np
Series#
Una serie de pandas es una estructura de datos unidimensional, junto con una secuencia de etiquetas para cada dato denominada índice. Podemos crear una serie de pandas a través de una lista de Python
s = pd.Series([4, 7, -5, 3])
s
0 4
1 7
2 -5
3 3
dtype: int64
En este ejemplo vemos que pandas asigna por defecto un índice numérico que etiqueta los datos que le hemos pasado mediante la lista. Pandas gestiona estos datos como un array de numpy, que es accesible mediante el atributo values
. También observamos que el tipo de numpy elegido ha sido int64
s.values
array([ 4, 7, -5, 3])
El índice está disponible en el atributo index
. En este caso crea un objeto similar al range
de Python, pero más generalmente serán instancias de pd.Index
s.index
RangeIndex(start=0, stop=4, step=1)
Podemos proporcionar un índice cuando creemos la serie
s2 = pd.Series([4, 7, -5, 3], index=['d', 'b', 'a', 'c'])
s2.index
Index(['d', 'b', 'a', 'c'], dtype='object')
También podemos añadirle un atributo name
tanto pd.Series
como a un índice
s2.name = "test"
s2.index.name = "letras"
s2
letras
d 4
b 7
a -5
c 3
Name: test, dtype: int64
La noción de índice en pandas generaliza en cierto sentido los índices de numpy. Igual que en numpy, podemos acceder a los elementos de la series a través del índice y modificarlos
s2["a"]
-5
s2["a"] = 6
s2
letras
d 4
b 7
a 6
c 3
Name: test, dtype: int64
Podemos tambíen indicar una subserie
s2[['c', 'a', 'd']]
letras
c 3
a 6
d 4
Name: test, dtype: int64
Las operaciones que estarían disponibles sobre el array subyacente a la serie se pueden aplicar directemante a la misma
s2[s2 > 5]
letras
b 7
a 6
Name: test, dtype: int64
s2*2
letras
d 8
b 14
a 12
c 6
Name: test, dtype: int64
np.exp(s2)
letras
d 54.598150
b 1096.633158
a 403.428793
c 20.085537
Name: test, dtype: float64
De hecho, una manera muy frecuente de crear una serie es a partir de un diccionario. Las claves se ordenarán y formarán el índice de la serie, como en el siguiente ejemplo:
sdata = {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}
s3 = pd.Series(sdata)
s3
Ohio 35000
Texas 71000
Oregon 16000
Utah 5000
dtype: int64
Si queremos introducir un orden específico entre las claves del diccionario, entonces podemos combinar el pasar el diccionario junto con la lista de etiquetas ordenadas:
states = ['California', 'Ohio', 'Oregon', 'Texas']
s4 = pd.Series(sdata, index=states)
s4
California NaN
Ohio 35000.0
Oregon 16000.0
Texas 71000.0
dtype: float64
Nótese que solo se incluye en el índice lo incluido en la lista (por ejemplo Utah
no forma parte del índice a pesar de que es una clave del diccionario). Como California
no es una clave del diccionario, pero se ha incluido en el índice, se incluye con valor NaN
(Not a Number), que es la manera en pandas para indicar valores inexistentes.
Con isnull
podemos localizar qué entradas de la serie tienen valores inexistentes:
s4.isnull()
California True
Ohio False
Oregon False
Texas False
dtype: bool
En las series, como con los arrays de numpy, podemos realizar operaciones vectorizadas. Lo interesante aquí es que las operaciones se alinean por las correspondientes etiquetas. Por ejemplo:
s3 + s4
California NaN
Ohio 70000.0
Oregon 32000.0
Texas 142000.0
Utah NaN
dtype: float64
Los tipos que solemos manejar en las series de pandas son similares que los de numpy, aunque existe un tipo particular de pandas bastante útil, que nos permite usar funcionalidades y ahorrar memoria, el tipo category
s = pd.Series(
["s", "m", "l", "xs", "xl"],
dtype="category"
)
s
0 s
1 m
2 l
3 xs
4 xl
dtype: category
Categories (5, object): ['l', 'm', 's', 'xl', 'xs']
Exercise 60
Carga las series city_mpg
y highway_mpg
con el siguiente código
url = "https://github.com/mattharrison/datasets/raw/master/data/vehicles.csv.zip"
df = pd.read_csv(url)
city_mpg = df.city08
highway_mpg = df.highway08
¿Cuántos elementos hay en las series? ¿De qué tipo son?
Calcula el mínimo, el máximo y la mediana de la Serie utilizando las funciones
min
,max
ymedian
respectivamente.Utiliza la función
pd.cut
para dividir la Serie de precios en cuatro categorías: «bajo», «medio-bajo», «medio-alto» y «alto», utilizando los cuartiles como límites de las categorías.Cuenta el número de elementos en cada categoría utilizando la función
value_counts
.Realiza un histograma y un gráfico de barras.
DataFrames#
Un DataFrame de pandas es una tabla bidimensional, con las columnas y las filas en un determinado orden. Cada columna puede ser de un tipo diferente. En términos de índices: tanto las filas como las columnas están indexadas.
Puede ver un DataFrame como un diccionario en el que las claves son las etiquetas de las columnas, y todos los valores son Series de pandas que comparten el mismo índice.
Aunque hay muchas maneras de crear un DataFrame, una de las más frecuentes es mediante un diccionario cuyos valores asociados a las claves son listas de la misma longitud. Por ejemplo:
data = {
'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada', 'Nevada'],
'year': [2000, 2001, 2002, 2001, 2002, 2003],
'pop': [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]
}
frame = pd.DataFrame(data)
Nótese la forma en que se muestra un DataFrame en Jupyter:
frame
state | year | pop | |
---|---|---|---|
0 | Ohio | 2000 | 1.5 |
1 | Ohio | 2001 | 1.7 |
2 | Ohio | 2002 | 3.6 |
3 | Nevada | 2001 | 2.4 |
4 | Nevada | 2002 | 2.9 |
5 | Nevada | 2003 | 3.2 |
Podemos obtener infomación del DataFrame con el método info
frame.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 state 6 non-null object
1 year 6 non-null int64
2 pop 6 non-null float64
dtypes: float64(1), int64(1), object(1)
memory usage: 272.0+ bytes
Cuando los DataFrames son grandes, puede ser útil el método head
, que muestar solo las primeras \(n\) filas de la tabla:
frame.head()
state | year | pop | |
---|---|---|---|
0 | Ohio | 2000 | 1.5 |
1 | Ohio | 2001 | 1.7 |
2 | Ohio | 2002 | 3.6 |
3 | Nevada | 2001 | 2.4 |
4 | Nevada | 2002 | 2.9 |
Como en el caso de las Series, podemos proporcionar las columnas en un orden determinado:
pd.DataFrame(data, columns=['year', 'state', 'pop'])
year | state | pop | |
---|---|---|---|
0 | 2000 | Ohio | 1.5 |
1 | 2001 | Ohio | 1.7 |
2 | 2002 | Ohio | 3.6 |
3 | 2001 | Nevada | 2.4 |
4 | 2002 | Nevada | 2.9 |
5 | 2003 | Nevada | 3.2 |
Y también podemos indicar expresamente el índice de las filas:
pd.DataFrame(
data,
columns=['pop', 'year', 'state'],
index=['one', 'two', 'three', 'four', 'five', 'six']
)
pop | year | state | |
---|---|---|---|
one | 1.5 | 2000 | Ohio |
two | 1.7 | 2001 | Ohio |
three | 3.6 | 2002 | Ohio |
four | 2.4 | 2001 | Nevada |
five | 2.9 | 2002 | Nevada |
six | 3.2 | 2003 | Nevada |
Si al proporcionar los nombres de las columnas damos una de ellas que no aparece en el diccionario con los datos, entonces se crea la columnos con valores no determinados:
df2 = pd.DataFrame(
data,
columns=['year', 'state', 'pop', 'debt'],
index=['one', 'two', 'three', 'four', 'five', 'six']
)
df2
year | state | pop | debt | |
---|---|---|---|---|
one | 2000 | Ohio | 1.5 | NaN |
two | 2001 | Ohio | 1.7 | NaN |
three | 2002 | Ohio | 3.6 | NaN |
four | 2001 | Nevada | 2.4 | NaN |
five | 2002 | Nevada | 2.9 | NaN |
six | 2003 | Nevada | 3.2 | NaN |
Los atributos index
y columns
nos devuelven los correspondientes índices de las filas y de las columnas (ambos son objetos Index
de pandas):
df2.index
Index(['one', 'two', 'three', 'four', 'five', 'six'], dtype='object')
df2.columns
Index(['year', 'state', 'pop', 'debt'], dtype='object')
Para acceder a una columna en concreto del DataFrame, podemos hacerlo usando la notación de diccionario, o también como atributo. En ambos casos se devuelve la correspondiente columna como un objeto Series de pandas:
df2['state']
one Ohio
two Ohio
three Ohio
four Nevada
five Nevada
six Nevada
Name: state, dtype: object
df2.year
one 2000
two 2001
three 2002
four 2001
five 2002
six 2003
Name: year, dtype: int64
De igual manera, podemos acceder a una fila del DataFrame mediante el método loc
, que veremos con más detenimiento en lo siguiente. La fila también se devuelve como un objeto Series, cuyo índice está formado por los nombres de las columnas:
df2.loc['three']
year 2002
state Ohio
pop 3.6
debt NaN
Name: three, dtype: object
Veamos ahora ejemplos sobre cómo podemos modificar columnas mediante asignaciones. En general, muchas de los procedimientos de numpy aquí también son válidos, pero teniendo en cuenta que indexamos mediante el nombre de la columna:
Por ejemplo, asignar el mismo valor a toda una columna:
df2['debt'] = 16.5
df2
year | state | pop | debt | |
---|---|---|---|---|
one | 2000 | Ohio | 1.5 | 16.5 |
two | 2001 | Ohio | 1.7 | 16.5 |
three | 2002 | Ohio | 3.6 | 16.5 |
four | 2001 | Nevada | 2.4 | 16.5 |
five | 2002 | Nevada | 2.9 | 16.5 |
six | 2003 | Nevada | 3.2 | 16.5 |
O asignar mediante una secuencia:
df2['debt'] = np.arange(6.)
df2
year | state | pop | debt | |
---|---|---|---|---|
one | 2000 | Ohio | 1.5 | 0.0 |
two | 2001 | Ohio | 1.7 | 1.0 |
three | 2002 | Ohio | 3.6 | 2.0 |
four | 2001 | Nevada | 2.4 | 3.0 |
five | 2002 | Nevada | 2.9 | 4.0 |
six | 2003 | Nevada | 3.2 | 5.0 |
Cuando a una columna le asignamos una lista o un array, como en el ejemplo anterior, la longitud de la secuencia debe de coincidir con el número de filas del DataFrame. Sin embargo, podemos asignar con un objeto Series y los valores se asignarán alineando por el valor del índice, incluso parcialmente (al resto se el asignará NaN):
val = pd.Series(
[-1.2, -1.5, -1.7],
index=['two', 'four', 'five']
)
df2['debt'] = val
df2
year | state | pop | debt | |
---|---|---|---|---|
one | 2000 | Ohio | 1.5 | NaN |
two | 2001 | Ohio | 1.7 | -1.2 |
three | 2002 | Ohio | 3.6 | NaN |
four | 2001 | Nevada | 2.4 | -1.5 |
five | 2002 | Nevada | 2.9 | -1.7 |
six | 2003 | Nevada | 3.2 | NaN |
Si asignamos una columna que no existe, ésta se creará. Por ejemplo:
df2['eastern'] = df2.state == 'Ohio'
df2
year | state | pop | debt | eastern | |
---|---|---|---|---|---|
one | 2000 | Ohio | 1.5 | NaN | True |
two | 2001 | Ohio | 1.7 | -1.2 | True |
three | 2002 | Ohio | 3.6 | NaN | True |
four | 2001 | Nevada | 2.4 | -1.5 | False |
five | 2002 | Nevada | 2.9 | -1.7 | False |
six | 2003 | Nevada | 3.2 | NaN | False |
Podemos borrar una colunma con el método drop
df2.drop(columns="eastern", inplace=True)
# alternativa -> del df2['eastern']
df2.columns
Index(['year', 'state', 'pop', 'debt'], dtype='object')
df2
year | state | pop | debt | |
---|---|---|---|---|
one | 2000 | Ohio | 1.5 | NaN |
two | 2001 | Ohio | 1.7 | -1.2 |
three | 2002 | Ohio | 3.6 | NaN |
four | 2001 | Nevada | 2.4 | -1.5 |
five | 2002 | Nevada | 2.9 | -1.7 |
six | 2003 | Nevada | 3.2 | NaN |
Otra forma de crear un DataFrame es a partir de un diccionario de diccionarios, en el que las claves externas constituyen las etiquetas de las columnas, y las internas como las de las filas:
pop = {
'Nevada': {2001: 2.4, 2002: 2.9},
'Ohio': {2000: 1.5, 2001: 1.7, 2002: 3.6}
}
df3 = pd.DataFrame(pop)
df3
Nevada | Ohio | |
---|---|---|
2001 | 2.4 | 1.7 |
2002 | 2.9 | 3.6 |
2000 | NaN | 1.5 |
Como en numpy, podemos también obtener la traspuesta de un DataFrame, quedando las filas como columns y viceversa:
df3.T
2001 | 2002 | 2000 | |
---|---|---|---|
Nevada | 2.4 | 2.9 | NaN |
Ohio | 1.7 | 3.6 | 1.5 |
También se puede dar un DataFrame como un diccionario en el que cada clave (columna) tiene asociada una serie:
pdata = {
'Ohio': df3['Ohio'][:-1],
'Nevada': df3['Nevada'][:2]
}
pd.DataFrame(pdata)
Ohio | Nevada | |
---|---|---|
2001 | 1.7 | 2.4 |
2002 | 3.6 | 2.9 |
Con el atributo name
(tanto de index
como de columns
) podemos acceder y/o modificar el nombre de las filas y las columnas, que se mostrarán al mostrarse la tabla:
df3.index.name = 'year'
df3.columns.name = 'state'
df3
state | Nevada | Ohio |
---|---|---|
year | ||
2001 | 2.4 | 1.7 |
2002 | 2.9 | 3.6 |
2000 | NaN | 1.5 |
Por último, mediante values
, accedemos a un array bidimensional con los valores de cada entrada de la tabla:
df3.values
array([[2.4, 1.7],
[2.9, 3.6],
[nan, 1.5]])
df2.values # el dtype se acomoda a lo más general.
array([[2000, 'Ohio', 1.5, nan],
[2001, 'Ohio', 1.7, -1.2],
[2002, 'Ohio', 3.6, nan],
[2001, 'Nevada', 2.4, -1.5],
[2002, 'Nevada', 2.9, -1.7],
[2003, 'Nevada', 3.2, nan]], dtype=object)
Resumen de algunas maneras de crear un DataFrame:#
Array bidimensional, opcionalmente con
index
y/ocolumns
Diccionario de arrays, listas o tuplas de la misma longitud; cada clave se refiere a una columna
Diccionario de Series; cada clave es una columna y las filas se alinean según los índices de las series, o bien se le pasa explícitamente el índice.
Diccionario de diccionarios: las claves externas son las columnas, las internas las filas.
Lista de listas o tuplas: como en el caso de array bidimensional.
Exercise 61
Crea un DataFrame de 5 filas y columnas Nombre
, Edad
, Peso
con alguno de los métodos mencionados arriba.
Funcionalidades básicas#
Eliminando entradas de un eje#
Mediante drop
, podemos crear nuevos objetos resultantes de eliminar filas o columnas completas. Veamos algunos ejemplos. En primer lugar, con las Series:
s = pd.Series(
np.arange(5.),
index=['a', 'b', 'c', 'd', 'e']
)
s
a 0.0
b 1.0
c 2.0
d 3.0
e 4.0
dtype: float64
new_s = s.drop('c') # notemos que inplace=False (valor por defecto)
new_s
a 0.0
b 1.0
d 3.0
e 4.0
dtype: float64
# varias entradas a la vez
s.drop(['d', 'c'])
a 0.0
b 1.0
e 4.0
dtype: float64
Ahora veamos el uso de drop
con DataFrames:
data = pd.DataFrame(
np.arange(16).reshape((4, 4)),
index=['Ohio', 'Colorado', 'Utah', 'New York'],
columns=['one', 'two', 'three', 'four']
)
data
one | two | three | four | |
---|---|---|---|---|
Ohio | 0 | 1 | 2 | 3 |
Colorado | 4 | 5 | 6 | 7 |
Utah | 8 | 9 | 10 | 11 |
New York | 12 | 13 | 14 | 15 |
Por defecto, se eliminan del eje 0 (las filas):
data.drop(['Colorado', 'Ohio'])
one | two | three | four | |
---|---|---|---|---|
Utah | 8 | 9 | 10 | 11 |
New York | 12 | 13 | 14 | 15 |
Podemos eliminar columnas, indicándo que se quiere hacer en axis=1
o axis='columns'
:
data.drop('two', axis=1)
one | three | four | |
---|---|---|---|
Ohio | 0 | 2 | 3 |
Colorado | 4 | 6 | 7 |
Utah | 8 | 10 | 11 |
New York | 12 | 14 | 15 |
data.drop(['two', 'four'], axis='columns')
one | three | |
---|---|---|
Ohio | 0 | 2 |
Colorado | 4 | 6 |
Utah | 8 | 10 |
New York | 12 | 14 |
Como hemos dicho, por defecto, drop
devuelve un nuevo objeto. Pero como otras funciones, podrían actuar de manera destructiva, modificando el objeto original. Para ello, hay que indicarlo con el argumento clave inplace
:
data.drop('c', inplace=True)
data
a 0.0
b 1.0
d 3.0
e 4.0
dtype: float64
Indexado, selección y filtrado#
El acceso a los elementos de un objeto Series se hace de manera similar a los arrays de numpy, excepto que también podemos usar el correspondiente valor del índice, además de la posición numérica. Veámoslo con un ejemplo:
s = pd.Series(np.arange(4.), index=['a', 'b', 'c', 'd'])
s
a 0.0
b 1.0
c 2.0
d 3.0
dtype: float64
Podemos acceder al segundo elemento de la serie anterir, bien mediante el valor 'b'
, o por la posición 1, ambos accesos son equivalentes:
s['b'], s[1]
(1.0, 1.0)
Más ejemplos de indexado en objetos de tipo Series:
s[2:4]
c 2.0
d 3.0
dtype: float64
s[['b', 'a', 'd']]
b 1.0
a 0.0
d 3.0
dtype: float64
s[[1, 3]]
b 1.0
d 3.0
dtype: float64
s[s < 2]
a 0.0
b 1.0
dtype: float64
Podemos hacer también slicing con las etiquetas de un índice. Existe una diferencia importante, y es que el límite superior se considera incluido:
s['b':'c']
b 1.0
c 2.0
dtype: float64
Podemos incluso hacer asignaciones usando slicing, como en los arrays de numpy:
s['b':'c'] = 5
s
a 0.0
b 5.0
c 5.0
d 3.0
dtype: float64
Para DataFrames, el acceso mediante una etiqueta, extrae por defecto la correspondiente columna en forma de Series, como ya habíamos visto anteriormente. En el siguiente ejemplo:
data = pd.DataFrame(
np.arange(16).reshape((4, 4)),
index=['Ohio', 'Colorado', 'Utah', 'New York'],
columns=['one', 'two', 'three', 'four']
)
data
one | two | three | four | |
---|---|---|---|---|
Ohio | 0 | 1 | 2 | 3 |
Colorado | 4 | 5 | 6 | 7 |
Utah | 8 | 9 | 10 | 11 |
New York | 12 | 13 | 14 | 15 |
data['two'] ###
Ohio 1
Colorado 5
Utah 9
New York 13
Name: two, dtype: int64
También se admite indexado mediante una lista de etiquetas:
data[['three', 'one']]
three | one | |
---|---|---|
Ohio | 2 | 0 |
Colorado | 6 | 4 |
Utah | 10 | 8 |
New York | 14 | 12 |
Hay un par de casos particulares, que no funciona seleccionando columnas: si hacemos slicing con enteros, nos estamos refiriendo a las filas:
data[:2]
one | two | three | four | |
---|---|---|---|---|
Ohio | 0 | 1 | 2 | 3 |
Colorado | 4 | 5 | 6 | 7 |
También el indexado booleano filtar por filas:
data[data['three'] > 5]
one | two | three | four | |
---|---|---|---|---|
Colorado | 4 | 5 | 6 | 7 |
Utah | 8 | 9 | 10 | 11 |
New York | 12 | 13 | 14 | 15 |
Selección con loc e iloc#
Además de los métodos directos de indexado que acabamos de ver, existen otros dos métodos para seleccionar datos en pandas
loc
: es manera de acceder a los datos de un DataFrame usando las etiquetas de las filas y columnas. También se utiliza para indexar con booleanos.iloc
: podemos usar índices enteros, como con numpy.
Veamos algunos ejemplos.
data
one | two | three | four | |
---|---|---|---|---|
Ohio | 0 | 1 | 2 | 3 |
Colorado | 4 | 5 | 6 | 7 |
Utah | 8 | 9 | 10 | 11 |
New York | 12 | 13 | 14 | 15 |
Para acceder a la fila etiquetada como Colorado
y sólo a las columnas two
y three
, en ese orden (nótese que se devuelve una serie):
data.loc['Colorado', ['two', 'three']]
two 5
three 6
Name: Colorado, dtype: int64
Un ejemplo, similar, pero ahora con índices numéricos. La fila de índice 2, sólo con las columnas 3, 0 y 1.
data.iloc[2, [3, 0, 1]]
four 11
one 8
two 9
Name: Utah, dtype: int64
La fila de índice 2:
data.iloc[2]
one 8
two 9
three 10
four 11
Name: Utah, dtype: int64
Podemos especificar una subtabla por sus filas y columnas
data.iloc[[1, 2], [3, 0, 1]]
four | one | two | |
---|---|---|---|
Colorado | 7 | 4 | 5 |
Utah | 11 | 8 | 9 |
Podemos usar slicing con las etiquetas (recordar que el límite superior es inclusive):
data.loc[:'Utah', 'two']
Ohio 1
Colorado 5
Utah 9
Name: two, dtype: int64
Un ejemplo algo más complicado. Seleccionamos primero las tres primeras columnas mediante slicing con enteros, y luego seleccionamos las filas que en la columna etiquetada con 'three'
tienen un valor mayor que 5:
data.iloc[:, :3][data.three > 5]
one | two | three | |
---|---|---|---|
Colorado | 4 | 5 | 6 |
Utah | 8 | 9 | 10 |
New York | 12 | 13 | 14 |
Exercise 62
Carga el siguiente dataframe
url = "https://github.com/mattharrison/datasets/raw/master/data/vehicles.csv.zip"
df = pd.read_csv(url)
Utiliza el método
set_index
para hacer que la columnamake
se convierta en el índiceDevuelve las primera 3 filas que corresponden a
make == 'Ferrari'
.Devuelve las 5 primeras columnas de aquellas filas que tengan
city08
mayor que 50
Operaciones aritméticas con valores de relleno#
df1 = pd.DataFrame(
np.arange(12.).reshape((3, 4)),
columns=list('abcd')
)
df2 = pd.DataFrame(
np.arange(20.).reshape((4, 5)),
columns=list('abcde')
)
df1.loc[1, 'b'] = np.nan
df2.loc[1, 'b'] = np.nan
df1
a | b | c | d | |
---|---|---|---|---|
0 | 0.0 | 1.0 | 2.0 | 3.0 |
1 | 4.0 | NaN | 6.0 | 7.0 |
2 | 8.0 | 9.0 | 10.0 | 11.0 |
df2
a | b | c | d | e | |
---|---|---|---|---|---|
0 | 0.0 | 1.0 | 2.0 | 3.0 | 4.0 |
1 | 5.0 | NaN | 7.0 | 8.0 | 9.0 |
2 | 10.0 | 11.0 | 12.0 | 13.0 | 14.0 |
3 | 15.0 | 16.0 | 17.0 | 18.0 | 19.0 |
Al sumar ambos DataFrames,téngase en cuanta que la fila 3 no existe en una de ellas, al igual que la columna 'e'
. POr tanto, en el resultado, se crearán en esas fila y columna con valores no determinados:
df1 + df2
a | b | c | d | e | |
---|---|---|---|---|---|
0 | 0.0 | 2.0 | 4.0 | 6.0 | NaN |
1 | 9.0 | NaN | 13.0 | 15.0 | NaN |
2 | 18.0 | 20.0 | 22.0 | 24.0 | NaN |
3 | NaN | NaN | NaN | NaN | NaN |
Sin embargo, podemos usar el método add
con «valor de relleno» 0, y en ese caso, cuando uno de los operandos no esté definido, se tome ese valor por defecto (cero en este caso). Nótese que si ninguno de los operandos está definido (como en (1,'b')
), entonces no se aplica el relleno.
df1.add(df2, fill_value=0)
a | b | c | d | e | |
---|---|---|---|---|---|
0 | 0.0 | 2.0 | 4.0 | 6.0 | 4.0 |
1 | 9.0 | NaN | 13.0 | 15.0 | 9.0 |
2 | 18.0 | 20.0 | 22.0 | 24.0 | 14.0 |
3 | 15.0 | 16.0 | 17.0 | 18.0 | 19.0 |
Como add
, existe otras operaciones aritméticas que permiten fill_value
: sub
, mul
, div
, pow
, …
Relacionado con esto, también es interesante destacar que cuando se reindexa un objeto, podemos especificar el valor de relleno, cuando el valor no esté especificado en el objeto original:
df1.reindex(columns=df2.columns, fill_value=0)
a | b | c | d | e | |
---|---|---|---|---|---|
0 | 0.0 | 1.0 | 2.0 | 3.0 | 0 |
1 | 4.0 | NaN | 6.0 | 7.0 | 0 |
2 | 8.0 | 9.0 | 10.0 | 11.0 | 0 |
Aplicación de funciones a las «vectorizadas»#
Como en numpy, podemos aplicar funciones de forma vectorizada a todos los valores de un Series o un DataFrame:
frame = pd.DataFrame(
np.random.randn(4, 3),
columns=list('bde'),
index=['Utah', 'Ohio', 'Texas', 'Oregon']
)
frame
b | d | e | |
---|---|---|---|
Utah | 0.497856 | 0.100599 | 0.044469 |
Ohio | 1.006873 | 0.508566 | 1.075500 |
Texas | 0.468307 | 0.198138 | 2.652674 |
Oregon | -1.376750 | 0.278911 | -0.017388 |
Por ejemplo, aplicamos valor absoluto a cada elemento de la tabla:
np.abs(frame)
b | d | e | |
---|---|---|---|
Utah | 0.497856 | 0.100599 | 0.044469 |
Ohio | 1.006873 | 0.508566 | 1.075500 |
Texas | 0.468307 | 0.198138 | 2.652674 |
Oregon | 1.376750 | 0.278911 | 0.017388 |
Otra operación muy frecuente consiste en aplicar una función definida sobre arrays unidimensionales, a lo largo de uno de los ejes (filas o columnas). Esto se hace con el método apply
de los DataFrames.
Supongamos que queremos calcular la diferencia entre el máximo y el mínimo de cada columna de una tabla. Lo podemos hacer así:
f = lambda x: x.max() - x.min()
frame.apply(f)
b 2.383623
d 0.407967
e 2.670062
dtype: float64
El eje por defecto para hacer un apply
es el 0, es decir el de las filas (y por tanto aplica la opración sobre cada columna). Podemos usar el argumento axis
para especificar que queremos aplicar en el sentido de las columnas (y por tanto, hacer el cálculo sobre las filas):
frame.apply(f, axis='columns')
Utah 0.734510
Ohio 1.376317
Texas 1.811918
Oregon 1.867884
dtype: float64
En realidad, hay muchas funciones (como sum
o mean
) que de por sí ya están adaptadas a DataFrames y no necesitan usar apply, como veremos más adelante.
Compara las siguientes ejecuciones usando apply
recorriendo filas o una función vectorizada
url = "https://github.com/mattharrison/datasets/raw/master/data/vehicles.csv.zip"
df = pd.read_csv(url)
/usr/local/lib/python3.8/dist-packages/IPython/core/interactiveshell.py:3326: DtypeWarning: Columns (68,70,71,72,73,74,76,79) have mixed types.Specify dtype option on import or set low_memory=False.
exec(code_obj, self.user_global_ns, self.user_ns)
def gt20(val):
return val > 20
%%timeit
df.city08.apply(gt20)
7.88 ms ± 226 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%timeit
df.city08.gt(20)
125 µs ± 4.63 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Exercise 63
Selecciona las columnas numéricas de df
con el método select_dtypes
y normaliza las columnas.
Ordenación#
En pandas tenemos posibilidad de ordenar bien teniendo en cuenta las etiquetas de las filas o columnas, o bien con los valores propiamente dichos:
Comenzamos con un ejemplo con Series:
s = pd.Series(range(4), index=['d', 'a', 'b', 'c'])
s
d 0
a 1
b 2
c 3
dtype: int64
Con sort_index
ordenamos la serie por las etiquetas del índice:
s.sort_index()
a 1
b 2
c 3
d 0
dtype: int64
Con DataFrame, podemos ordenar por las etiquetas de las filas, o también por las columnas, usando el argumento axis
:
frame = pd.DataFrame(
np.arange(8).reshape((2, 4)),
index=['three', 'one'],
columns=['d', 'a', 'b', 'c']
)
frame
d | a | b | c | |
---|---|---|---|---|
three | 0 | 1 | 2 | 3 |
one | 4 | 5 | 6 | 7 |
frame.sort_index()
d | a | b | c | |
---|---|---|---|---|
one | 4 | 5 | 6 | 7 |
three | 0 | 1 | 2 | 3 |
frame.sort_index(axis=1)
a | b | c | d | |
---|---|---|---|---|
three | 1 | 2 | 3 | 0 |
one | 5 | 6 | 7 | 4 |
frame.sort_index(axis=1, ascending=False) # se puede especificar si es ascendente o descendente
d | c | b | a | |
---|---|---|---|---|
three | 0 | 3 | 2 | 1 |
one | 4 | 7 | 6 | 5 |
Con sort_values
, ordenamos por los valores de las entradas. Por ejemplo, en una serie:
s = pd.Series([4, 7, -3, 2])
s.sort_values()
2 -3
3 2
0 4
1 7
dtype: int64
Si ordenamos por valores, los NaN se sitúan al final:
s = pd.Series([4, np.nan, 7, np.nan, -3, 2])
s.sort_values()
4 -3.0
5 2.0
0 4.0
2 7.0
1 NaN
3 NaN
dtype: float64
Con sort_values
aplicado a DataFrames, podemos ordenar por el valor de alguna columna, o incluso por el valor de una fila, usando el argumento clave 'by'
:
frame = pd.DataFrame(
{
'b': [4, 7, -3, 2],
'a': [0, 1, 0, 1]
},
index=['j','k','l','m']
)
frame
b | a | |
---|---|---|
j | 4 | 0 |
k | 7 | 1 |
l | -3 | 0 |
m | 2 | 1 |
Ordenación de la tabla según la columna 'b'
:
frame.sort_values(by='b')
b | a | |
---|---|---|
l | -3 | 0 |
m | 2 | 1 |
j | 4 | 0 |
k | 7 | 1 |
Por el valor de dos columnas (lexicográficamente):
frame.sort_values(by=['a', 'b'])
b | a | |
---|---|---|
l | -3 | 0 |
j | 4 | 0 |
m | 2 | 1 |
k | 7 | 1 |
Por el valor de una fila:
frame.sort_values(axis=1, by='k')
a | b | |
---|---|---|
j | 0 | 4 |
k | 1 | 7 |
l | 0 | -3 |
m | 1 | 2 |
Funciones estadísticas descriptivas#
Los objetos de pandas incorporan una serie de métodos estadísticos que calculan un valor a partir de los valores de una serie o de filas o columnas de un DataFrame. Una particularidad interesante es que manejan adecuadamente los valores no especificados. Veamos algunos ejemplos:
frame = pd.DataFrame(
[
[1.4, np.nan],
[7.1, -4.5],
[np.nan, np.nan],
[0.75, -1.3]
],
index=['a', 'b', 'c', 'd'],
columns=['one', 'two']
)
frame
one | two | |
---|---|---|
a | 1.40 | NaN |
b | 7.10 | -4.5 |
c | NaN | NaN |
d | 0.75 | -1.3 |
Por defecto, el método sum
calcula la suma de cada columna de un DataFrame. Los valores NaN se tratan como 0 (a no ser que toda la serie sea de valores NaN):
frame.sum()
one 9.25
two -5.80
dtype: float64
Como es habitual, con el parámetro axis
podemos hacerlo por el eje de las columnas:
frame.sum(axis='columns')
a 1.40
b 2.60
c 0.00
d -0.55
dtype: float64
Con mean
calculamos la media de filas o columnas según el eje elegido con axis. El parámetro skipna
nos permite indicar si se excluyen o no los valores NaN:
frame.mean(axis='columns', skipna=False)
a NaN
b 1.300
c NaN
d -0.275
dtype: float64
El método idxmax
nos da la etiqueta donde se alcanza el mínimo de cada columna (o cada fila)
frame.idxmax()
one b
two d
dtype: object
El método cumsum
nos da los acumulados por fila o por columna:
frame.cumsum()
one | two | |
---|---|---|
a | 1.40 | NaN |
b | 8.50 | -4.5 |
c | NaN | NaN |
d | 9.25 | -5.8 |
Por último el método describe
produce un resumen con las estadísticas más importantes:
frame.describe()
one | two | |
---|---|---|
count | 3.000000 | 2.000000 |
mean | 3.083333 | -2.900000 |
std | 3.493685 | 2.262742 |
min | 0.750000 | -4.500000 |
25% | 1.075000 | -3.700000 |
50% | 1.400000 | -2.900000 |
75% | 4.250000 | -2.100000 |
max | 7.100000 | -1.300000 |
Para tipos no numéricos, describe
también devuelve información
s = pd.Series(['a', 'a', 'b', 'c'] * 4)
s.describe()
count 16
unique 3
top a
freq 8
dtype: object
Exercise 64
Descarga el dataframe housing
utilizando el siguiente código
import os
import tarfile
import urllib
import pandas as pd
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
HOUSING_PATH = os.path.join("data", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.tgz"
def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
os.makedirs(housing_path, exist_ok=True)
tgz_path = os.path.join(housing_path, "housing.tgz")
urllib.request.urlretrieve(housing_url, tgz_path)
housing_tgz = tarfile.open(tgz_path)
housing_tgz.extractall(path=housing_path)
housing_tgz.close()
def load_housing_data(housing_path=HOUSING_PATH):
csv_path = os.path.join(housing_path, "housing.csv")
return pd.read_csv(csv_path)
fetch_housing_data()
housing = load_housing_data()
Realiza un análisis rápido de las variables númericas y categóricas. Rellena los valores faltantes con fillna
y realiza las visualizaciones que veas adecuadas.