Skip to content

    Chapter 13, Basic

    Group and aggregate

    "What is the average salary of each department?" You could filter each department and calculate the average, five blocks of almost identical code. Or ask Pandas once. The `groupby` is the most used operation in data analysis.

    Separate, apply, combine

    The groupby works in three stages: separates the data into groups, applies a calculation to each group and combines the results into a table:

    basico/cap13_agrupar_e_agregar.pylinhas 10 a 27
    import pandas as pd
    
    pd.set_option("display.width", 170)
    pd.set_option("display.max_columns", 20)
    
    CANONICO = {"engenharia": "Engenharia", "marketing": "Marketing", "finanças": "Finanças",
                "ciência de dados": "Ciência de Dados", "rh": "RH"}
    
    
    def carregar_limpo() -> pd.DataFrame:
        df = pd.read_csv("dados/funcionarios_15.csv")
        df["salario"] = pd.to_numeric(df["salario"].str.replace("R$ ", "", regex=False))
        df["departamento"] = df["departamento"].str.strip().str.lower().map(CANONICO)
        return df.drop_duplicates(subset=["id_funcionario"], keep="first").reset_index(drop=True)
    
    
    df = carregar_limpo()
    print(df.groupby("departamento")["salario"].mean().round(1))
    
    Output
    departamento
    Ciência de Dados    6450.0
    Engenharia          5100.0
    Finanças            5925.0
    Marketing           5362.5
    RH                  5100.0
    Name: salario, dtype: float64
    

    `size` vs `count`

    The most used aggregation functions are sum, mean, median, min, max, count, size and std. Two of them seem the same until they appear missing: the size counts all the rows in the group, and the count counts only the non-missing:

    basico/cap13_agrupar_e_agregar.pylinha 32
    print(df.groupby("departamento")["salario"].agg(["size", "count"]))
    
    Output
                      size  count
    departamento                 
    Ciência de Dados     2      1
    Engenharia           3      3
    Finanças             2      2
    Marketing            4      4
    RH                   2      1
    

    In departments where someone does not have a salary, the size is greater than the count. Which to use depends on the question: "how many people" is size, "how many known salaries" is count.

    The trap: the group that disappears

    By default, the groupby discards excludes the rows where the key is missing. Here, a person there is no department, and it disappears from any department summary without notice:

    basico/cap13_agrupar_e_agregar.pylinhas 37 a 39
    print(len(df), int(df.groupby("departamento")["id_funcionario"].size().sum()))
    com_ausente = df.groupby("departamento", dropna=False)["id_funcionario"].size()
    print(int(com_ausente.sum()), com_ausente.index.isna().sum())
    
    Output
    14 13
    14 1
    

    The total number of employees is 14, and the sum of the groups is only 13. With dropna=False, the group of "without department" appears, and the sum matches. In a report, a missing employee in the count is an error that no one notices, and that's why I check that the sum of the groups matches the total.

    Multiple columns, multiple functions

    basico/cap13_agrupar_e_agregar.pylinhas 44 a 45
    print(df.groupby(["departamento", "cidade"])["salario"].mean().round(0).head(5))
    print(df.groupby("departamento")[["salario", "nota_desempenho"]].mean().round(2))
    
    Output
    departamento      cidade        
    Ciência de Dados  Rio de Janeiro    6450.0
    Engenharia        Porto Alegre      4800.0
                      São Paulo         5250.0
    Finanças          Porto Alegre      5925.0
    Marketing         Belo Horizonte    5025.0
    Name: salario, dtype: float64
                      salario  nota_desempenho
    departamento                              
    Ciência de Dados   6450.0             6.00
    Engenharia         5100.0             6.00
    Finanças           5925.0             4.50
    Marketing          5362.5             6.75
    RH                 5100.0             7.50
    

    The first groups by two keys (one group for each combination), and the second calculates the average of two columns at the same time. The agg calculates several functions at once, and the named aggregation gives readable names to the columns of the result, instead of nested headers:

    basico/cap13_agrupar_e_agregar.pylinhas 47 a 53
    resumo = df.groupby("departamento").agg(
        salario_medio=("salario", "mean"),
        maior_salario=("salario", "max"),
        nota_media=("nota_desempenho", "mean"),
        funcionarios=("id_funcionario", "count"),
    )
    print(resumo.round(1))
    
    Output
                      salario_medio  maior_salario  nota_media  funcionarios
    departamento                                                            
    Ciência de Dados         6450.0         6450.0         6.0             2
    Engenharia               5100.0         6000.0         6.0             3
    Finanças                 5925.0         6300.0         4.5             2
    Marketing                5362.5         6150.0         6.8             4
    RH                       5100.0         5100.0         7.5             2
    

    The group became an index

    After grouping, the group column becomes the index of the result, and not a common column. Accessing it as a column gives a KeyError. The reset_index() returns (or, from the beginning, the as_index=False):

    basico/cap13_agrupar_e_agregar.pylinhas 58 a 64
    try:
        resumo["departamento"]
    except KeyError as erro:
        print("KeyError:", erro)
    
    print(resumo.reset_index().columns.tolist())
    print(df.groupby("departamento", as_index=False)["salario"].mean().columns.tolist())
    
    Output
    KeyError: 'departamento'
    ['departamento', 'salario_medio', 'maior_salario', 'nota_media', 'funcionarios']
    ['departamento', 'salario']
    

    Use the reset_index() when you need to treat the result as a common table: sort by it, filter or join with another table.

    One calculation per group, keeping the original shape

    The agg reduces each group to one row. The transform calculates by group, but returns one row for each original row, which allows comparing each person with the average of their department: Chapter 23 delves into the

    basico/cap13_agrupar_e_agregar.pylinhas 69 a 71
    media_do_depto = df.groupby("departamento")["salario"].transform("mean")
    df["acima_da_media_do_depto"] = df["salario"] > media_do_depto
    print(df[["nome", "departamento", "salario", "acima_da_media_do_depto"]].head(5))
    
    Output
                  nome      departamento  salario  acima_da_media_do_depto
    0        Ana Souza        Engenharia   4500.0                    False
    1       Bruno Lima         Marketing   4650.0                    False
    2     Carla Mendes        Engenharia   4800.0                    False
    3      Diego Rocha  Ciência de Dados      NaN                    False
    4  Elisa Fernandes                RH   5100.0                    False
    

    by group. transform, the filter and the apply The groupby hidden in every business question

    "Which region sells the most?", "Which category has the highest margin?", "Which segment cancels the most?": almost every business question is a

    disguised. When a question takes the form "what, groupby for what", it is a grouping. what", is a grouping.

    Exercise 1

    The summary by city

    Write resumo_por_cidade(tabela), which returns a table with one row per city (including the absent one, as a group), with the number of employees and the average salary, and check that the total number of employees in the summary is equal to that of the table.