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 y median 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 indexcomo 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/o columns

  • 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 columna make se convierta en el índice

  • Devuelve 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.