Autor: Luis Fernando Apáez Álvarez
-Curso PyM-
Clase 5: Agregación y group by
Fecha: 09 de diciembre del 2022
En esta clase estaremos trabajando, inicialmente, con el conjunto de datos de propinas que tiene integrado seaborn:
import seaborn as sns
df = sns.load_dataset("tips")
df.head()
total_bill | tip | sex | smoker | day | time | size | |
---|---|---|---|---|---|---|---|
0 | 16.99 | 1.01 | Female | No | Sun | Dinner | 2 |
1 | 10.34 | 1.66 | Male | No | Sun | Dinner | 3 |
2 | 21.01 | 3.50 | Male | No | Sun | Dinner | 3 |
3 | 23.68 | 3.31 | Male | No | Sun | Dinner | 2 |
4 | 24.59 | 3.61 | Female | No | Sun | Dinner | 4 |
Cabe mencionar que ya conocemos varios tipos de agregaciones, tales como los promedios con el método mean()
y también sumas (sum()
) o conteos (como value_counts()
), no obstante, en esta parte de la clase nos concentraremos a detalle en sumas y conteos.
Podemos obtener la suma de los valores de una columna en específico. Por ejemplo, el total de propinas almacenadas en el dataset:
# dataframe.columna.sum()
df.tip.sum()
731.5799999999999
# Podemos obtener la suma de dos o mas columnas
df[['total_bill', 'tip']].sum()
total_bill 4827.77 tip 731.58 dtype: float64
Podemos obtener la suma de las propinas pero solo para la cetegoría Female
de la columna sex
. Para ello deberemos aplicar un filtro primero
# registros solo para sex = Female,
# luego seleccionamos la columna tip
# y aplicamos la funcion sum()
df[df.sex == 'Female'].tip.sum()
246.50999999999996
# hacemos lo mismo para male
df[df.sex == 'Male'].tip.sum()
485.07
Por otro lado, supongamos que queremos agregar una columna nueva con un total de gastos, donde sumemos el valor de total_bill
más el valor de tip
. Para ello:
# Dado que queremos sumar por filas, dentro de la funcion
# sum() configuramos axis=1 para realizar la suma
# por filas
df[['total_bill', 'tip']].sum(axis=1)
0 18.00 1 12.00 2 24.51 3 26.99 4 28.20 ... 239 34.95 240 29.18 241 24.67 242 19.57 243 21.78 Length: 244, dtype: float64
Luego agregamos la información anterior en una nueva columna de nuestro dataframe
df['total_gasto'] = df[['total_bill', 'tip']].sum(axis=1)
df.head()
total_bill | tip | sex | smoker | day | time | size | total_gasto | |
---|---|---|---|---|---|---|---|---|
0 | 16.99 | 1.01 | Female | No | Sun | Dinner | 2 | 18.00 |
1 | 10.34 | 1.66 | Male | No | Sun | Dinner | 3 | 12.00 |
2 | 21.01 | 3.50 | Male | No | Sun | Dinner | 3 | 24.51 |
3 | 23.68 | 3.31 | Male | No | Sun | Dinner | 2 | 26.99 |
4 | 24.59 | 3.61 | Female | No | Sun | Dinner | 4 | 28.20 |
Luego, diremos que un valor es nulo si no tiene asignado ningún valor. Los valores nulos se suelen representar por NaN, NAN o nan, y también son conocidos como datos faltantes. Podemos obtener valores nulos de la librería numpy
import numpy as np
# las siguientes tres alternativas representan lo mismo
print(np.nan)
print(np.NaN)
print(np.NAN)
nan nan nan
Despues, agregaremos una fila nueva a nuestro dataframe
# creamos un diccionario para especificar los valores de la fila
# que queremos agregara
nueva_fila = {'total_bill': 17, 'tip': 1,
'sex': np.nan, 'smoker': np.nan,
'day': 'sun', 'time': 'Dinner',
'size': np.nan, 'total_gasto': np.nan}
# agregamos la fila anterior con el metodo append()
df = df.append(nueva_fila, ignore_index=True)
df
C:\Users\usuario\AppData\Local\Temp\ipykernel_18948\3376625475.py:8: FutureWarning: The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead. df = df.append(nueva_fila, ignore_index=True)
total_bill | tip | sex | smoker | day | time | size | total_gasto | |
---|---|---|---|---|---|---|---|---|
0 | 16.99 | 1.01 | Female | No | Sun | Dinner | 2.0 | 18.00 |
1 | 10.34 | 1.66 | Male | No | Sun | Dinner | 3.0 | 12.00 |
2 | 21.01 | 3.50 | Male | No | Sun | Dinner | 3.0 | 24.51 |
3 | 23.68 | 3.31 | Male | No | Sun | Dinner | 2.0 | 26.99 |
4 | 24.59 | 3.61 | Female | No | Sun | Dinner | 4.0 | 28.20 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
240 | 27.18 | 2.00 | Female | Yes | Sat | Dinner | 2.0 | 29.18 |
241 | 22.67 | 2.00 | Male | Yes | Sat | Dinner | 2.0 | 24.67 |
242 | 17.82 | 1.75 | Male | No | Sat | Dinner | 2.0 | 19.57 |
243 | 18.78 | 3.00 | Female | No | Thur | Dinner | 2.0 | 21.78 |
244 | 17.00 | 1.00 | NaN | NaN | sun | Dinner | NaN | NaN |
245 rows × 8 columns
# observemos que ya se han detectado los valores nulos:
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 245 entries, 0 to 244 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 total_bill 245 non-null float64 1 tip 245 non-null float64 2 sex 244 non-null object 3 smoker 244 non-null object 4 day 245 non-null object 5 time 245 non-null object 6 size 244 non-null float64 7 total_gasto 244 non-null float64 dtypes: float64(4), object(4) memory usage: 15.4+ KB
Tenemos entonces que en las columnas sex, smoker, size, total_gasto
tenemos un valor nulo, o alternativamente tenemos 244 valores no nulos.
Podemos detectar valores nulos utilizando el método isnull()
, el cual nos arrojará una serie con valores booleanos
df.sex.isnull()
0 False 1 False 2 False 3 False 4 False ... 240 False 241 False 242 False 243 False 244 True Name: sex, Length: 245, dtype: bool
# o alternativamente
df.sex.isna()
0 False 1 False 2 False 3 False 4 False ... 240 False 241 False 242 False 243 False 244 True Name: sex, Length: 245, dtype: bool
y podemos contar el total de valores nulos combinando.
df.sex.isnull().sum()
1
# o para todas las columnas
df.isnull().sum()
total_bill 0 tip 0 sex 1 smoker 1 day 0 time 0 size 1 total_gasto 1 dtype: int64
Mediante el método count()
podemos contar el total de registros de una columna
df.smoker.count()
244
# o contar dada una condicion
df[df.smoker == 'Yes'].count()
total_bill 93 tip 93 sex 93 smoker 93 day 93 time 93 size 93 total_gasto 93 dtype: int64
# mas sutil
df[df.smoker == 'Yes']['smoker'].count()
93
# No fumadores
df[df.smoker == 'No']['smoker'].count()
151
Podríamos también realizar conteos de manera horizontal configurando count(axis=1)
, pero para ello debemos actuar sobre dataframes. Por ejemplo, para la celda de código anterior no podemos configurar count(axis=1)
pues dicho método está actuando sobre una serie y no sobre un dataframe.
Podemos contar el números de entradas por fila:
# El conteo se hace de manera horizontal
df.count(axis=1)
0 8 1 8 2 8 3 8 4 8 .. 240 8 241 8 242 8 243 8 244 4 Length: 245, dtype: int64
lo que nos indica que todas ls filas tienen 8 entradas referentes a las 8 columnas, pero la última fila solo tiene 4 entradas puesto que count()
cuenta entradas no nulas.
También, podemos utilizar el método que ya conocemos de value_counts()
# Contamos por categorias de la columna sex
df.value_counts('sex')
sex Male 157 Female 87 dtype: int64
# alternativamene
df.sex.value_counts()
Male 157 Female 87 Name: sex, dtype: int64
Donde vemos que los valores faltantes no son contmplados. Si configuramos el parámetro dropna=False
entonces los valores faltantes serán contemplados
df.sex.value_counts(dropna=False)
Male 157 Female 87 NaN 1 Name: sex, dtype: int64
Asimismo, podemos configurar para que los conteos no aparezcan ordenados con en los casos anteriores
df.sex.value_counts(sort=False, dropna=False)
Female 87 Male 157 NaN 1 Name: sex, dtype: int64
O podemos ordenar de menor a mayor
df.sex.value_counts(ascending=True, dropna=False)
NaN 1 Female 87 Male 157 Name: sex, dtype: int64
Finalmente, en vez de ver los conteos por categoría, o también de valores faltantes, como en los casos anteriores, podemos ver en su lugar el porcentaje del total que representan:
df.sex.value_counts(normalize=True, dropna=False)
Male 0.640816 Female 0.355102 NaN 0.004082 Name: sex, dtype: float64
Donde, de todos los registros y referentes a la columna sex
, Male representa el 64% del total, Female 35% y los valores faltantes el 0.4%.
Notemos que la función value_counts()
está agrupando primero, la columna en cuestión, por categorías y después cuenta los elementos para esas categorías.
Por ejemplo:
df.smoker.value_counts()
No 151 Yes 93 Name: smoker, dtype: int64
Primero, de la columna smoker
, se agrupan los datos por las dos categorías No y Yes, y después se cuentan los elementos que son de la categoría No y los que son de la categoría Yes.
Lo que hace la función groupby()
es agrupar elementos de una columna de un dataframe por categorías, pero a diferencia de value_counts()
que después de agrupar cuenta, groupby()
permite realizar cualquier otro tipo de agregación después de agrupar.
Por ejemplo, obtengamos el mismo resultado de la celda anterior pero ahora utilizando groupby()
df.groupby('smoker').count()
total_bill | tip | sex | day | time | size | total_gasto | |
---|---|---|---|---|---|---|---|
smoker | |||||||
No | 151 | 151 | 151 | 151 | 151 | 151 | 151 |
Yes | 93 | 93 | 93 | 93 | 93 | 93 | 93 |
donde colocamos df.gropuby()
para utilizar dicho método, después pasamos como parámetro la columna de la cual queremos realizar la agrupación, esto es, configuramos que las agrupaciones se hagan de acuerdo a las categorías de la columna smoker
, y finalmente colocamos el tipo de agregación, el cual es en este caso un conteo.
Notamos que el resultado es un dataframe con los conteos de los registros de acuerdo a las categorías de la columna smoker
para todas las columnas, donde vemos que tenemos información redundante y donde vemos que el índice son las categorías de la columna smoker
. De tal manera, lo conveniente sería elegir cualquier columna disponible de dicho dataframe
df.groupby('smoker').count()['tip']
smoker No 151 Yes 93 Name: tip, dtype: int64
# o alternativamente
df.groupby('smoker')['tip'].count()
smoker No 151 Yes 93 Name: tip, dtype: int64
con lo cual obtenemos el "mismo" resultado que
df.smoker.value_counts()
No 151 Yes 93 Name: smoker, dtype: int64
Aunque en df.groupby('smoker').count()['tip']
tenemos como información adicional:
Name: tip, dtype: int64
que se ha seleccionado la columna tip
, donde dicha información no la tenemos al utilizar df.smoker.value_counts()
y donde, en realidad, esa información no es necesaria pues estamos únicamente interesados en conocer el número total de fumadores y no fumadores.
Podemos agregar dicho problema si escribimos
df.groupby('smoker')['smoker'].count()
smoker No 151 Yes 93 Name: smoker, dtype: int64
Por otro lado, podemos obtener agrupaciones con otras agregaciones:
sex
y sumaremos todos los valores para esas categorías para las columnas que tengan entradas numéricas df.groupby('sex').sum()
total_bill | tip | size | total_gasto | |
---|---|---|---|---|
sex | ||||
Female | 1570.95 | 246.51 | 214.0 | 1817.46 |
Male | 3256.82 | 485.07 | 413.0 | 3741.89 |
donde podemos ver que el total de propinas dejadas por mujeres es de 246.51 y el de hombres es de 485.07. Asimismo, vemos que el total de la factura fue mayor para los hombres que para las mujeres, pero recordemos que hubo en total más clientes hombres que mujeres.
De lo anterior simplemente podemos seleccionar alguna columna de interés:
# Agrupamos por las categorias de la columna sex,
# seleccionamos la columna total_bill y
# sumamos todos los valores de total_bill
# por categorias
df.groupby('sex')['total_bill'].sum()
sex Female 1570.95 Male 3256.82 Name: total_bill, dtype: float64
# Agrupamos por las categorias de la columna sex,
# seleccionamos cualquier columna y
# contamos todos los registros
# por categorias
df.groupby('sex')['tip'].count()
sex Female 87 Male 157 Name: tip, dtype: int64
lo cual nos dice, como ya sabíamos, que los clientes fueron en su mayoría hombres que mujeres.
# Agrupamos por las categorias de la columna day,
# seleccionamos la columna tip y
# obtenemos el promedio de todos los registros
# por categorias
df.groupby('day')['tip'].mean()
day Fri 2.734737 Sat 2.993103 Sun 3.255132 Thur 2.771452 sun 1.000000 Name: tip, dtype: float64
# o el promedio del total de facturas por dia
df.groupby('day')['total_bill'].mean()
day Fri 17.151579 Sat 20.441379 Sun 21.410000 Thur 17.682742 sun 17.000000 Name: total_bill, dtype: float64
Notemos que cuando aplicamos un group by sin colocar la agregación obtenemos
df.groupby('day')['total_bill']
<pandas.core.groupby.generic.SeriesGroupBy object at 0x00000209FA5BC2E0>
type(df.groupby('day')['total_bill'])
pandas.core.groupby.generic.SeriesGroupBy
un objeto group by de pandas. Lo que está ocurriendo es que groupby divide inicialmente al dataframe por grupos, con lo cual puede decirse que obtenemos varios "dataframes" o "series" (según sea el caso). Luego de dicha división se procederá a efectuar operaciones de acuerdo al tipo de agregación que sea de nuestro interés.
De tal manera, dado que no hemos configurado en la celda de código anterior algún tipo de agregación para el groupby, entonces el objeto pandas.core.groupby.generic.SeriesGroupBy
"tiene" las divisiones hechas del dataframe original de acuerdo a los grupos o categorías.
Por ejemplo, consideremos los registros para los cuales el día es viernes:
df[df.day == 'Fri']['total_bill']
90 28.97 91 22.49 92 5.75 93 16.32 94 22.75 95 40.17 96 27.28 97 12.03 98 21.01 99 12.46 100 11.35 101 15.38 220 12.16 221 13.42 222 8.58 223 15.98 224 13.42 225 16.27 226 10.09 Name: total_bill, dtype: float64
Luego, a partir del objeto pandas.core.groupby.generic.SeriesGroupBy
podemos obtener esos mismos registros para la categoría o grupo del viernes, para ello
# convertimos en lista y accedemos al primer elemento
list(df.groupby('day')['total_bill'])[0]
('Fri', 90 28.97 91 22.49 92 5.75 93 16.32 94 22.75 95 40.17 96 27.28 97 12.03 98 21.01 99 12.46 100 11.35 101 15.38 220 12.16 221 13.42 222 8.58 223 15.98 224 13.42 225 16.27 226 10.09 Name: total_bill, dtype: float64)
de donde vemos que los elementos de la lista anterior son tuplas de la forma (categoria, dataframe/serie con los registros del dataframe original para esa categoría). Con base en los elementos anteriores se realizan algunas operaciones para poder obtener la información conjunta
# Por ejemplo
df.groupby('day')['total_bill'].mean()
day Fri 17.151579 Sat 20.441379 Sun 21.410000 Thur 17.682742 sun 17.000000 Name: total_bill, dtype: float64
Además, notamos que tenemos dos tipos de objetos resultantes a partir del groupby:
print(type(df.groupby('day')['total_bill']))
print(type(df.groupby('day')))
<class 'pandas.core.groupby.generic.SeriesGroupBy'> <class 'pandas.core.groupby.generic.DataFrameGroupBy'>
donde, como en primero accedemos a una columna en específico (total_bill
) entonces estaremos considerando únicamente la información de esa columna de acuerdo a los distintos grupos, como pudimos ver anterirmente, de modo que el objeto será, en parte, del tipo series. En el segundo, como estamos considerando todas las columnas, entonces el objeto resultante será, en parte, del tipo dataframe. Para este último
# convertimos en lista y accedemos al primer elemento
list(df.groupby('day'))[0]
('Fri', total_bill tip sex smoker day time size total_gasto 90 28.97 3.00 Male Yes Fri Dinner 2.0 31.97 91 22.49 3.50 Male No Fri Dinner 2.0 25.99 92 5.75 1.00 Female Yes Fri Dinner 2.0 6.75 93 16.32 4.30 Female Yes Fri Dinner 2.0 20.62 94 22.75 3.25 Female No Fri Dinner 2.0 26.00 95 40.17 4.73 Male Yes Fri Dinner 4.0 44.90 96 27.28 4.00 Male Yes Fri Dinner 2.0 31.28 97 12.03 1.50 Male Yes Fri Dinner 2.0 13.53 98 21.01 3.00 Male Yes Fri Dinner 2.0 24.01 99 12.46 1.50 Male No Fri Dinner 2.0 13.96 100 11.35 2.50 Female Yes Fri Dinner 2.0 13.85 101 15.38 3.00 Female Yes Fri Dinner 2.0 18.38 220 12.16 2.20 Male Yes Fri Lunch 2.0 14.36 221 13.42 3.48 Female Yes Fri Lunch 2.0 16.90 222 8.58 1.92 Male Yes Fri Lunch 1.0 10.50 223 15.98 3.00 Female No Fri Lunch 3.0 18.98 224 13.42 1.58 Male Yes Fri Lunch 2.0 15.00 225 16.27 2.50 Female Yes Fri Lunch 2.0 18.77 226 10.09 2.00 Female Yes Fri Lunch 2.0 12.09)
Vemos que el primer elemento de la lista anterior es una tupla con una categoría de la columna day
seguido de la información de todos los registros para esa categoría.
Veamos que el segundo elemento de la tupla anterior es un dataframe
type(list(df.groupby('day'))[0][1])
pandas.core.frame.DataFrame
De tal manera, el segundo elemento de la tupla
list(df.groupby('day')['total_bill'])[0]
('Fri', 90 28.97 91 22.49 92 5.75 93 16.32 94 22.75 95 40.17 96 27.28 97 12.03 98 21.01 99 12.46 100 11.35 101 15.38 220 12.16 221 13.42 222 8.58 223 15.98 224 13.42 225 16.27 226 10.09 Name: total_bill, dtype: float64)
tiene que ser una serie de pandas. En efecto:
type(list(df.groupby('day')['total_bill'])[0][1])
pandas.core.series.Series
De manera alternativa a convertir los objetos resultantes de un groupby en listas para poder visualizarlos, podemos utilizar el método ger_group()
como sigue:
# para mayor facilidad definimos una variable
gr = df.groupby('day')['total_bill']
# obtenemos los registros de total_bill para el viernes
gr.get_group('Fri')
90 28.97 91 22.49 92 5.75 93 16.32 94 22.75 95 40.17 96 27.28 97 12.03 98 21.01 99 12.46 100 11.35 101 15.38 220 12.16 221 13.42 222 8.58 223 15.98 224 13.42 225 16.27 226 10.09 Name: total_bill, dtype: float64
Podemos realizar groupby agrupando con base en más de una columna. Por ejemplo, supongamos que queremos ver la información de las personas que son fumadores o no de acuerdo a los días de la semana, esto es, nos gustaría la siguiente consideración:
smoker | day |
---|---|
Yes | Thur |
Yes | Fri |
Yes | Sat |
Yes | Sun |
No | Thur |
No | Fri |
No | Sat |
No | Sun |
esto es, tendríamos con lo anterior la información para las personas que fuman o no para cada uno de los días.
Por ejemplo, manualmente podemos obtener información de los fumadores para el domingo
df[(df.smoker == 'Yes') & (df.day == 'Sun')].iloc[:, [3,4, 1, 2, 5, 6, 7]]
smoker | day | tip | sex | time | size | total_gasto | |
---|---|---|---|---|---|---|---|
164 | Yes | Sun | 3.00 | Female | Dinner | 2.0 | 20.51 |
172 | Yes | Sun | 5.15 | Male | Dinner | 2.0 | 12.40 |
173 | Yes | Sun | 3.18 | Male | Dinner | 2.0 | 35.03 |
174 | Yes | Sun | 4.00 | Male | Dinner | 2.0 | 20.82 |
175 | Yes | Sun | 3.11 | Male | Dinner | 2.0 | 36.01 |
176 | Yes | Sun | 2.00 | Male | Dinner | 2.0 | 19.89 |
177 | Yes | Sun | 2.00 | Male | Dinner | 2.0 | 16.48 |
178 | Yes | Sun | 4.00 | Female | Dinner | 2.0 | 13.60 |
179 | Yes | Sun | 3.55 | Male | Dinner | 2.0 | 38.18 |
180 | Yes | Sun | 3.68 | Male | Dinner | 4.0 | 38.33 |
181 | Yes | Sun | 5.65 | Male | Dinner | 2.0 | 28.98 |
182 | Yes | Sun | 3.50 | Male | Dinner | 3.0 | 48.85 |
183 | Yes | Sun | 6.50 | Male | Dinner | 4.0 | 29.67 |
184 | Yes | Sun | 3.00 | Male | Dinner | 2.0 | 43.55 |
186 | Yes | Sun | 3.50 | Female | Dinner | 3.0 | 24.40 |
187 | Yes | Sun | 2.00 | Male | Dinner | 5.0 | 32.46 |
188 | Yes | Sun | 3.50 | Female | Dinner | 3.0 | 21.65 |
189 | Yes | Sun | 4.00 | Male | Dinner | 3.0 | 27.10 |
190 | Yes | Sun | 1.50 | Male | Dinner | 2.0 | 17.19 |
pero para lograr la división que se quiere tendríamos que escribir varias líneas de código para tener todas las combinaciones de la tabla 1 anterior.
Con base en la división de la tabla 1 de los datos podemos sacar información relevante como cuál es el gasto total promedio de de los fumadores en viernes o cuántos hombres fumadores asisten los domingos. Para responder las cuestiones anteriores escribimos
# para agrupar con base en mas de una columna
# utilizamos una lista con las columnas de interes para
# la agrupacion, luego colocaremos el nombre de la
# columna de la informacion que nos interesa y al final
# colocamos la agregacion del promedio
df.groupby(['smoker', 'day'])['total_gasto'].mean()
smoker day No Fri 21.232500 Sat 22.764667 Sun 23.674561 Thur 19.786889 Yes Fri 19.527333 Sat 24.152143 Sun 27.636842 Thur 22.220588 Name: total_gasto, dtype: float64
que nos brinda la información que queríamos, donde vemos que los no fumadores gastan más los domingos al igual que los fumadores o que los viernes los fumadores gastan en promedio 19.52 dólares.
Luego
# En este caso agrupamos con base en tres columna
# y seleccionamos despues cualquier columna, en nuestro
# caso seleccionaremos la columna tip
df.groupby(['smoker', 'day', 'sex'])['tip'].count()
smoker day sex No Fri Female 2 Male 2 Sat Female 13 Male 32 Sun Female 14 Male 43 Thur Female 25 Male 20 Yes Fri Female 7 Male 8 Sat Female 15 Male 27 Sun Female 4 Male 15 Thur Female 7 Male 10 Name: tip, dtype: int64
Tenemos que los domingos asisten 15 hombres fumadores.