julio 22, 2020

~ 16 MIN

Pandas - Funcionalidad Esencial

< Blog RSS

Open In Colab

Pandas - Funcionalidad Esencial

En el post anterior vimos una introducción a la librería de análisis de datos Pandas. Esta librería nos ofrece el objeto DataFrame que podemos usar para estructurar datos de manera tabular y llevar a cabo operaciones para el análisis de estos datos. En este post vamos a explicar funcionalidad esencial de esta librería que nos hará la vida más fácil.

import pandas as pd

I/O

Hasta ahora hemos visto que podemos inicializar un DataFrame a partir de otras estructuras de datos, como por ejemplo un dict.

df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
df

a b
0 1 4
1 2 5
2 3 6

Sin embargo, en la gran mayoría de ocasiones necesitaremos ser capaces de cargar los datos a partir de grandes archivos. Pandas es capaz de leer multitud de archivos, aquí puedes encontrar la lista completa. Para ilustrar los diferentes ejemplos que vamos a ver en este post, primero necesitamos descargar algunos datos.

import wget

# descargar datos
url = 'https://mymldatasets.s3.eu-de.cloud-object-storage.appdomain.cloud/ml-1m.zip'
wget.download(url)
'ml-1m.zip'
import zipfile

# extraer datos
with zipfile.ZipFile('ml-1m.zip', 'r') as zip_ref:
    zip_ref.extractall()
import os

os.listdir('ml-1m')
['movies.dat', 'ratings.dat', 'README', 'users.dat']

Nuestro dataset está compuesto por tres archivos .dat que contienen opiniones de películas. En primer lugar tenemos que cargar estos datos en un DataFrame. Para ello usamos la función read_table que nos permite leer archivos de text en formato tabular, definiendo el carácter utilizado para separar valores (un archivo csv usa comas, un archivo tsv utiliza tabuladores, etc.). En este caso, al no tener el nombre de las columnas definidos en el archivo, tenemos que proveerlo nosotros.

unames = ['user_id', 'gender', 'age', 'occupation', 'zip']
users = pd.read_table('ml-1m/users.dat', sep='::', header=None, names=unames)
users.head()

user_id gender age occupation zip
0 1 F 1 10 48067
1 2 M 56 16 70072
2 3 M 25 15 55117
3 4 M 45 7 02460
4 5 M 25 20 55455

💡Puedes usar la función head directamente en un DataFrame para visualizar solo los primeros elementos. De la misma manera puedes usat tail o sample para visualizar los últimos elementos o un conjunto aleatorio, respectivamente.

En el caso en que los archivos siguen un formato más común, como por ejemplo csv o json, Pandas nos ofrece funciones especiales, como read_csv o read_json, que simplifican el proceso de carga de datos. Vamos a cargar el resto de datos de la misma manera.

rnames = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_table('ml-1m/ratings.dat', sep='::', header=None, names=rnames)
ratings.head()

user_id movie_id rating timestamp
0 1 1193 5 978300760
1 1 661 3 978302109
2 1 914 3 978301968
3 1 3408 4 978300275
4 1 2355 5 978824291
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('ml-1m/movies.dat', sep='::', header=None, names=mnames)
movies.head()

movie_id title genres
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy

De la misma manera que podemos leer archivos, Pandas nos permite guardar un DataFrame fácilmente. Por ejemplo, para guardar el objeto movies como un archivo csv

movies.to_csv('movies.csv', index=False)

Si cargamos ahora el csv generado podemos ver que tenemos exactamente los mismos datos.

_movies = pd.read_csv('movies.csv')
_movies.head()

movie_id title genres
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy

⚡Tanto al guardar como cargar archivos csv puedes indicar a Pandas si tener en cuenta las etiquetas de columna y fila, header e index repectivamente. Ves con cuidado con estas variables ya que es muy común que ésto de lugar a problemas.

Mezclar DataFrames

La segunda funcionalidad que vamos a ver es la de mezclar varios DataFrames en un solo. Para ello utilizamos la función merge.

data = pd.merge(pd.merge(ratings, users), movies)
data.head()

user_id movie_id rating timestamp gender age occupation zip title genres
0 1 1193 5 978300760 F 1 10 48067 One Flew Over the Cuckoo's Nest (1975) Drama
1 2 1193 5 978298413 M 56 16 70072 One Flew Over the Cuckoo's Nest (1975) Drama
2 12 1193 4 978220179 M 25 12 32793 One Flew Over the Cuckoo's Nest (1975) Drama
3 15 1193 4 978199279 M 25 7 22903 One Flew Over the Cuckoo's Nest (1975) Drama
4 17 1193 5 978158471 M 50 1 95350 One Flew Over the Cuckoo's Nest (1975) Drama

Primero mezclamos el objeto ratings y users. Para ellos Pandas utiliza la columna en común user_id para saber que filas corresponden al mismo usuario en cada objeto. Después, hacemos lo mismo con el objeto movies. En este caso, Pandas utiliza la columna movie_id para relacionar las filas de los dos DataFrames.

Pandas ofrece otras funciones para mezclar DataFrames tales como join o concat, cada una de ellas mezclando los datos de una manera determinada. Puedes aprender más aquí.

Información básica

Una vez mezclados todos los datos en un solo objeto podemos empezar a responder varias preguntas simples como por ejemplo: ¿cuántos elementos hay en el DataFrame?, ¿cuántas columnas tenemos?, ¿de qué tipo son los datos de cada columna?... Pandas nos oferece varias funciones para conseguir esta información.

# número de filas y columnas

data.shape
(1000209, 10)
# nombres de las columnas

data.columns
Index(['user_id', 'movie_id', 'rating', 'timestamp', 'gender', 'age',
       'occupation', 'zip', 'title', 'genres'],
      dtype='object')
# información general

data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1000209 entries, 0 to 1000208
Data columns (total 10 columns):
 #   Column      Non-Null Count    Dtype 
---  ------      --------------    ----- 
 0   user_id     1000209 non-null  int64 
 1   movie_id    1000209 non-null  int64 
 2   rating      1000209 non-null  int64 
 3   timestamp   1000209 non-null  int64 
 4   gender      1000209 non-null  object
 5   age         1000209 non-null  int64 
 6   occupation  1000209 non-null  int64 
 7   zip         1000209 non-null  object
 8   title       1000209 non-null  object
 9   genres      1000209 non-null  object
dtypes: int64(6), object(4)
memory usage: 83.9+ MB

Probablemente, la función info es la que más información proporciona ya que podemos ver el número de valores que tenemos por columna así como su tipo, la memoria que ocupa, etc. Junto a esta función, la segunda más útil para conocer nuestro dataset es describe (la cual nos aporta información estadística sobre las columnas numéricas).

data.describe()

user_id movie_id rating timestamp age occupation
count 1.000209e+06 1.000209e+06 1.000209e+06 1.000209e+06 1.000209e+06 1.000209e+06
mean 3.024512e+03 1.865540e+03 3.581564e+00 9.722437e+08 2.973831e+01 8.036138e+00
std 1.728413e+03 1.096041e+03 1.117102e+00 1.215256e+07 1.175198e+01 6.531336e+00
min 1.000000e+00 1.000000e+00 1.000000e+00 9.567039e+08 1.000000e+00 0.000000e+00
25% 1.506000e+03 1.030000e+03 3.000000e+00 9.653026e+08 2.500000e+01 2.000000e+00
50% 3.070000e+03 1.835000e+03 4.000000e+00 9.730180e+08 2.500000e+01 7.000000e+00
75% 4.476000e+03 2.770000e+03 4.000000e+00 9.752209e+08 3.500000e+01 1.400000e+01
max 6.040000e+03 3.952000e+03 5.000000e+00 1.046455e+09 5.600000e+01 2.000000e+01

Agrupar datos

Si te fijas en el dataset tenemos muchas entradas repetidas, tanto para usuarios como para películas (un usuario puede opinar sobre varias películas, y un película puede tener opiniones de varios usuarios). Para agrupar todos los datos según una columnas en concreto, podemos usar la función groupby.

data_title = data.groupby('title')
data_title.size()
title
$1,000,000 Duck (1971)                         37
'Night Mother (1986)                           70
'Til There Was You (1997)                      52
'burbs, The (1989)                            303
...And Justice for All (1979)                 199
                                             ... 
Zed & Two Noughts, A (1985)                    29
Zero Effect (1998)                            301
Zero Kelvin (Kj�rlighetens kj�tere) (1995)      2
Zeus and Roxanne (1997)                        23
eXistenZ (1999)                               410
Length: 3706, dtype: int64

Una función muy potente en Pandas es la función pivot_table, que nos permite agrupar los datos de un DataFrame según los valores de alguna columna. Por ejemplo, si queremos conocer la puntuación media de cada película en nuestro dataset separada por género, podemos conseguirlo de la siguiente manera.

mean_ratings = data.pivot_table('rating', index='title', columns='gender', aggfunc='mean')
mean_ratings.sample(5)

gender F M
title
Young Frankenstein (1974) 4.289963 4.239177
Love Jones (1997) 3.692308 3.214286
Condition Red (1995) 4.000000 NaN
Sacco and Vanzetti (Sacco e Vanzetti) (1971) NaN 4.000000
Simple Twist of Fate, A (1994) 3.000000 3.225000

Filtrar datos

¿Y si ahora quisiésemos quedarnos sólo con aquellas entradas en el DataFrame generado que tengan como mínimo 250 opiniones? Para ello tenemos que filtrar los datos, y ya vimos cómo podemos hacer esto en el post anterior gracias al masking. En primer lugar necesitamos conocer los índices de todos los elementos que cumplen esta condición.

ratings_by_title = data.groupby('title').size()
active_titles = ratings_by_title.index[ratings_by_title >= 250]
active_titles
Index([''burbs, The (1989)', '10 Things I Hate About You (1999)',
       '101 Dalmatians (1961)', '101 Dalmatians (1996)', '12 Angry Men (1957)',
       '13th Warrior, The (1999)', '2 Days in the Valley (1996)',
       '20,000 Leagues Under the Sea (1954)', '2001: A Space Odyssey (1968)',
       '2010 (1984)',
       ...
       'X-Men (2000)', 'Year of Living Dangerously (1982)',
       'Yellow Submarine (1968)', 'You've Got Mail (1998)',
       'Young Frankenstein (1974)', 'Young Guns (1988)',
       'Young Guns II (1990)', 'Young Sherlock Holmes (1985)',
       'Zero Effect (1998)', 'eXistenZ (1999)'],
      dtype='object', name='title', length=1216)

Ahora, podemos usar estos índices para indexar en el DataFrame que nos interesa

mean_ratings_250 = mean_ratings.loc[active_titles]
mean_ratings_250.sample(5)

gender F M
title
Kiss the Girls (1997) 3.381356 3.317690
Heaven Can Wait (1978) 3.588889 3.631373
Cape Fear (1991) 3.668831 3.671670
George of the Jungle (1997) 3.188119 3.026596
Cruel Intentions (1999) 3.160584 3.241706

Ordenar datos

Otra operación muy común es ordenar los datos en un DataFrame. Para ello utilizamos la función sort_values. Podemos conocer las películas con mejor opinión entre las mujeres de la siguiente manera.

top_female_ratings = mean_ratings_250.sort_values(by='F', ascending=False)
top_female_ratings.head()

gender F M
title
Close Shave, A (1995) 4.644444 4.473795
Wrong Trousers, The (1993) 4.588235 4.478261
Sunset Blvd. (a.k.a. Sunset Boulevard) (1950) 4.572650 4.464589
Wallace & Gromit: The Best of Aardman Animation (1996) 4.563107 4.385075
Schindler's List (1993) 4.562602 4.491415

Y si nos interesa conocer aquellas películas con mayor discrepancia entre hombres y mujeres

import numpy as np

mean_ratings_250['diff'] = np.abs(mean_ratings_250['M'] - mean_ratings_250['F'])
sorted_by_diff = mean_ratings_250.sort_values(by='diff', ascending=False)
sorted_by_diff.head()

gender F M diff
title
Dirty Dancing (1987) 3.790378 2.959596 0.830782
Good, The Bad and The Ugly, The (1966) 3.494949 4.221300 0.726351
Kentucky Fried Movie, The (1977) 2.878788 3.555147 0.676359
Jumpin' Jack Flash (1986) 3.254717 2.578358 0.676359
Dumb & Dumber (1994) 2.697987 3.336595 0.638608

Tratar datos ausentes

En ocasiones podemos encontrar que en nuestro DataFrame hay valores ausentes, lo que en inglés se conoce como missing data o missing values. Esto puede ser debido a que, realmente los datos no existen en la fuente de la que se han extraído (por ejemplo, un archivo csv) o bien que sean el resultado de alguna operación llevada a cabo en los datos (como ya vimos en el post anterior).

df = pd.DataFrame({
    "weight": {"alice":68, "charles": 112},
    "height": {"bob": 168, "charles": 182}
})

df

weight height
alice 68.0 NaN
charles 112.0 182.0
bob NaN 168.0

Podemos conocer cuantos valores ausentes tenemos con la función isna.

df.isna()

weight height
alice False True
charles False False
bob True False
# valores ausentes por columnas

df.isna().sum()
weight    1
height    1
dtype: int64
# valores ausentes por filas

df.isna().sum(axis=1)
alice      1
charles    0
bob        1
dtype: int64

En el ejemplo anterior tenemos un DataFrame con varios valores ausentes. La primera opción que nos da Pandas para tratar estos valores es simplemente reemplazarlos por otro con la función fillna.

# reemplazar NaN por 0

df.fillna(0)

weight height
alice 68.0 0.0
charles 112.0 182.0
bob 0.0 168.0
# reemplazar NaN por el valor medio de la columna

df.fillna(df.mean())

weight height
alice 68.0 175.0
charles 112.0 182.0
bob 90.0 168.0

La otra opción que tenemos es directamente descartar todos los valores NaN, ésto lo conseguimos con la función dropna. Como parámetros opcionales podemos indicar si queremos eliminarlos todos o bien por columnas o filas.

# elimina todas las filas con algún valor `NaN`

df.dropna()

weight height
charles 112.0 182.0
# elimina todas las filas con todos los valores en `NaN`

df.dropna(how='all')

weight height
alice 68.0 NaN
charles 112.0 182.0
bob NaN 168.0
# elimina todas las columnas con algún valor `NaN`

df.dropna(axis=1)

alice
charles
bob
# elimina todas las columnas con todos los valores en `NaN`

df.dropna(axis=1, how='all')

weight height
alice 68.0 NaN
charles 112.0 182.0
bob NaN 168.0

Resumen

En este post hemos visto la funcionalidad esencial que la librería Pandas nos ofrece para el análisis de datos. Algunos ejemplos son: cómo cargar y guardar datos en archivos, cómo mezclar datos de diferentes fuentes, cómo extraer la información básica de nuestros datos, cómo agrupar datos, cómo filtrarlos, ordenarlo y finalmente tratar posibles valores ausentes. Con esta colección de herramientas seremos capaces de tratar cualquier fuente de datos tabular que utilizaremos para entrenar nuestros modelos de Machine Learning.

< Blog RSS