julio 22, 2020
~ 16 MIN
Pandas - Funcionalidad Esencial
< Blog RSSPandas - 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 unDataFrame
para visualizar solo los primeros elementos. De la misma manera puedes usattail
osample
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 aPandas
si tener en cuenta las etiquetas de columna y fila,header
eindex
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 DataFrame
s.
⚡
Pandas
ofrece otras funciones para mezclarDataFrame
s tales comojoin
oconcat
, 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
.