Curso de introducción a la programación con Python¶

    Autor: Luis Fernando Apáez Álvarez
    -Curso PyM-
    Clase 5: Agregación y group by
    Fecha: 09 de diciembre del 2022


Contenido¶

  • Agregaciones
    • Sumas
      • Valores nulos
    • Conteos
  • Group by
    • Groupby un poco más a fondo
    • Agrupaciones múltiples

En esta clase estaremos trabajando, inicialmente, con el conjunto de datos de propinas que tiene integrado seaborn:

In [1]:
import seaborn as sns

df = sns.load_dataset("tips")
df.head()
Out[1]:
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

Agregaciones ¶

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.

Sumas¶

Podemos obtener la suma de los valores de una columna en específico. Por ejemplo, el total de propinas almacenadas en el dataset:

In [2]:
# dataframe.columna.sum()
df.tip.sum()
Out[2]:
731.5799999999999
In [4]:
# Podemos obtener la suma de dos o mas columnas
df[['total_bill', 'tip']].sum()
Out[4]:
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

In [8]:
# registros solo para sex = Female,
# luego seleccionamos la columna tip
# y aplicamos la funcion sum()
df[df.sex == 'Female'].tip.sum()
Out[8]:
246.50999999999996
In [9]:
# hacemos lo mismo para male
df[df.sex == 'Male'].tip.sum()
Out[9]:
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:

In [12]:
# 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)
Out[12]:
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

In [13]:
df['total_gasto'] = df[['total_bill', 'tip']].sum(axis=1)
df.head()
Out[13]:
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

Valores nulos¶

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

In [44]:
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

In [45]:
# 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)
Out[45]:
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

In [46]:
# 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

In [49]:
df.sex.isnull()
Out[49]:
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
In [64]:
# o alternativamente
df.sex.isna()
Out[64]:
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.

In [50]:
df.sex.isnull().sum()
Out[50]:
1
In [51]:
# o para todas las columnas
df.isnull().sum()
Out[51]:
total_bill     0
tip            0
sex            1
smoker         1
day            0
time           0
size           1
total_gasto    1
dtype: int64

Conteos¶

Mediante el método count() podemos contar el total de registros de una columna

In [17]:
df.smoker.count()
Out[17]:
244
In [54]:
# o contar dada una condicion
df[df.smoker == 'Yes'].count()
Out[54]:
total_bill     93
tip            93
sex            93
smoker         93
day            93
time           93
size           93
total_gasto    93
dtype: int64
In [55]:
# mas sutil
df[df.smoker == 'Yes']['smoker'].count()
Out[55]:
93
In [63]:
# No fumadores
df[df.smoker == 'No']['smoker'].count()
Out[63]:
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:

In [61]:
# El conteo se hace de manera horizontal
df.count(axis=1)
Out[61]:
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()

In [66]:
# Contamos por categorias de la columna sex
df.value_counts('sex')
Out[66]:
sex
Male      157
Female     87
dtype: int64
In [67]:
# alternativamene
df.sex.value_counts()
Out[67]:
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

In [68]:
df.sex.value_counts(dropna=False)
Out[68]:
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

In [69]:
df.sex.value_counts(sort=False, dropna=False)
Out[69]:
Female     87
Male      157
NaN         1
Name: sex, dtype: int64

O podemos ordenar de menor a mayor

In [71]:
df.sex.value_counts(ascending=True, dropna=False)
Out[71]:
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:

In [72]:
df.sex.value_counts(normalize=True, dropna=False)
Out[72]:
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%.

Group by ¶

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:

In [73]:
df.smoker.value_counts()
Out[73]:
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()

In [83]:
df.groupby('smoker').count()
Out[83]:
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

In [87]:
df.groupby('smoker').count()['tip']
Out[87]:
smoker
No     151
Yes     93
Name: tip, dtype: int64
In [92]:
# o alternativamente
df.groupby('smoker')['tip'].count()
Out[92]:
smoker
No     151
Yes     93
Name: tip, dtype: int64

con lo cual obtenemos el "mismo" resultado que

In [85]:
df.smoker.value_counts()
Out[85]:
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

In [88]:
df.groupby('smoker')['smoker'].count()
Out[88]:
smoker
No     151
Yes     93
Name: smoker, dtype: int64

Por otro lado, podemos obtener agrupaciones con otras agregaciones:

  • Agruparemos por las categorías de la columna sex y sumaremos todos los valores para esas categorías para las columnas que tengan entradas numéricas
In [94]:
df.groupby('sex').sum()
Out[94]:
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:

In [96]:
# 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()
Out[96]:
sex
Female    1570.95
Male      3256.82
Name: total_bill, dtype: float64
  • Contaremos el número de clientes por sexo
In [97]:
# Agrupamos por las categorias de la columna sex,
# seleccionamos cualquier columna y
# contamos todos los registros
# por categorias 
df.groupby('sex')['tip'].count()
Out[97]:
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.

  • Agruparemos por día y obtendremos la propina promedio por día
In [99]:
# 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()
Out[99]:
day
Fri     2.734737
Sat     2.993103
Sun     3.255132
Thur    2.771452
sun     1.000000
Name: tip, dtype: float64
In [100]:
# o el promedio del total de facturas por dia
df.groupby('day')['total_bill'].mean()
Out[100]:
day
Fri     17.151579
Sat     20.441379
Sun     21.410000
Thur    17.682742
sun     17.000000
Name: total_bill, dtype: float64

Groupby un poco más a fondo¶

Notemos que cuando aplicamos un group by sin colocar la agregación obtenemos

In [101]:
df.groupby('day')['total_bill']
Out[101]:
<pandas.core.groupby.generic.SeriesGroupBy object at 0x00000209FA5BC2E0>
In [102]:
type(df.groupby('day')['total_bill'])
Out[102]:
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:

In [115]:
df[df.day == 'Fri']['total_bill']
Out[115]:
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

In [116]:
# convertimos en lista y accedemos al primer elemento
list(df.groupby('day')['total_bill'])[0]
Out[116]:
('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

In [118]:
# Por ejemplo
df.groupby('day')['total_bill'].mean()
Out[118]:
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:

In [119]:
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

In [120]:
# convertimos en lista y accedemos al primer elemento
list(df.groupby('day'))[0]
Out[120]:
('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

In [121]:
type(list(df.groupby('day'))[0][1])
Out[121]:
pandas.core.frame.DataFrame

De tal manera, el segundo elemento de la tupla

In [124]:
list(df.groupby('day')['total_bill'])[0]
Out[124]:
('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:

In [125]:
type(list(df.groupby('day')['total_bill'])[0][1])
Out[125]:
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:

In [148]:
# 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')
Out[148]:
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

Agrupaciones múltiples¶

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
Tabla 1

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

In [157]:
df[(df.smoker == 'Yes') & (df.day == 'Sun')].iloc[:, [3,4, 1, 2, 5, 6, 7]]
Out[157]:
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

In [159]:
# 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()
Out[159]:
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

In [162]:
# 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()
Out[162]:
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.