Capítulo 19, Intermediário
Categorias e faixas
Uma coluna com poucos valores distintos repetidos milhares de vezes (departamento, canal, estado) é desperdício como texto. O tipo `category` a guarda uma vez, ocupa uma fração da memória e ainda traz uma ordem.
O tipo `category`
Em vez de guardar a palavra Engenharia cem mil vezes, o category guarda uma tabela de categorias e, para cada linha, só o número da categoria. A diferença de memória é grande, e os valores são os mesmos:
import numpy as np
import pandas as pd
pd.set_option("display.width", 170)
pd.set_option("display.max_columns", 20)
departamentos = ["Engenharia", "Marketing", "Finanças", "Ciência de Dados", "RH"]
grande = pd.Series(np.random.default_rng(0).choice(departamentos, 100_000))
como_categoria = grande.astype("category")
print(grande.dtype, como_categoria.dtype)
print(bool(como_categoria.memory_usage(deep=True) < 0.2 * grande.memory_usage(deep=True)))
print(como_categoria.cat.categories.tolist())
print(como_categoria.cat.codes.head(3).tolist())
str category
True
['Ciência de Dados', 'Engenharia', 'Finanças', 'Marketing', 'RH']
[4, 0, 2]
A tabela de categorias está em .cat.categories, e os números por linha em .cat.codes. O ganho é maior quanto menos valores distintos e mais linhas houver. Para uma coluna de identificadores (quase todos diferentes), o category não ajuda.
Categorias com ordem
Uma categoria ordenada sabe que Iniciante < Pleno < Sênior, e por isso permite comparações e ordenações corretas. Ordenar texto comum é ordenar em ordem alfabética, e a ordem alfabética raramente é a ordem do negócio (Alto viria antes de Baixo, e Médio ficaria entre os dois):
niveis = pd.CategoricalDtype(["Iniciante", "Pleno", "Sênior"], ordered=True)
equipe = pd.Series(["Sênior", "Iniciante", "Pleno", "Sênior"]).astype(niveis)
print(equipe.sort_values().tolist())
print((equipe >= "Pleno").tolist())
print(equipe.max(), equipe.value_counts(sort=False).to_dict())
['Iniciante', 'Pleno', 'Sênior', 'Sênior']
[True, False, True, True]
Sênior {'Iniciante': 1, 'Pleno': 1, 'Sênior': 2}
O value_counts(sort=False) mantém a ordem das categorias, e não a da frequência.
Faixas: `cut` e `qcut`
O pd.cut divide um número em faixas de limites que você escolhe. O pd.qcut divide em faixas com o mesmo número de elementos (quartis, decis), e os limites vêm dos dados:
funcionarios = pd.read_csv("dados/funcionarios_100.csv").drop_duplicates("id_funcionario", keep="last")
salario = pd.to_numeric(funcionarios["salario"].str.replace("R$ ", "", regex=False))
faixas = pd.cut(salario, bins=[0, 5000, 8000, np.inf], labels=["Baixa", "Média", "Alta"])
quartis = pd.qcut(salario, q=4, labels=["Q1", "Q2", "Q3", "Q4"])
print(faixas.value_counts().to_dict())
print(quartis.value_counts().sort_index().to_dict())
print(int(faixas.isna().sum()), int(salario.isna().sum()))
{'Alta': 60, 'Média': 29, 'Baixa': 4}
{'Q1': 24, 'Q2': 23, 'Q3': 23, 'Q4': 23}
7 7
As faixas do cut têm tamanhos diferentes (os limites são seus), e as do qcut têm tamanhos quase iguais. Os salários ausentes ficam sem faixa (NaN), e por isso as contagens somam menos que o total.
Os limites do `cut`
Por padrão, as faixas são fechadas à direita: (0, 5000] inclui o 5000 e exclui o 0. Com right=False, ficam [0, 5000). Um valor exatamente no limite cai em faixas diferentes conforme essa escolha, e esse é um erro comum:
valor = pd.Series([5000])
print(pd.cut(valor, [0, 5000, 8000], labels=["A", "B"]).tolist())
print(pd.cut(valor, [0, 5000, 8000], labels=["A", "B"], right=False).tolist())
['A']
['B']
Agrupar por categoria
No Pandas 3, o groupby por uma coluna category só mostra as categorias que aparecem nos dados (observed=True é o padrão). Para listar também as categorias vazias (útil em relatórios que precisam de todas as linhas, mesmo as zeradas), peça observed=False:
nivel = pd.Series(pd.Categorical(["Pleno", "Sênior", "Pleno"], categories=["Iniciante", "Pleno", "Sênior"]))
horas = pd.Series([10, 20, 30])
print(horas.groupby(nivel).sum().to_dict())
print(horas.groupby(nivel, observed=False).sum().to_dict())
{'Pleno': 40, 'Sênior': 20}
{'Iniciante': 0, 'Pleno': 40, 'Sênior': 20}
O que uma categoria não aceita
Uma coluna category só aceita os valores que estão na lista de categorias. Atribuir outro é um erro (e isso é uma vantagem: valida os dados):
try:
nivel[0] = "Estagiário"
except TypeError as erro:
print("TypeError:", str(erro)[:56])
TypeError: Cannot setitem on a Categorical with a new category (Est
E, ao juntar duas colunas category com categorias diferentes, o resultado perde o tipo:
a = pd.Series(pd.Categorical(["x", "y"]))
b = pd.Series(pd.Categorical(["z"]))
print(pd.concat([a, b]).dtype)
str
Exercício 1
Faixas de tempo de casa
Escreva faixa_de_casa(anos), que receba uma Series com os anos de casa e devolva uma coluna de categorias ordenadas (Recente com menos de 2 anos, Estabelecido de 2 a menos de 6, e Veterano com 6 ou mais). Confira que a soma das contagens é igual ao total.