Elementos de Dados Estruturados

Autor

Douglas Braga

Todo modelo dos capítulos anteriores recebeu números prontos: um vetor de entrada, um alvo, um gradiente que decrescia passo a passo. Dado bruto não chega assim. Chega em uma tabela, com uma coluna que conta pessoas, outra que mede uma taxa, outra que nomeia um lugar — e cada uma pede um tratamento diferente antes de qualquer conta começar. A primeira pergunta, antes de qualquer ajuste, é de que tipo é cada coluna.

A taxonomia dos dados

Dado estruturado se divide em duas famílias. Numérico é o que se mede: contínuo é qualquer valor dentro de um intervalo — uma taxa, um comprimento, um preço —, discreto é contagem, sempre inteiro — número de filhos, de vendas, de acessos. Categórico é o que se rotula: nominal não tem ordem — uma cor, a sigla de um estado, o nome de uma cidade —, ordinal tem ordem mas não tem distância — uma faixa “baixa/média/alta” diz que alta vem depois de média, mas não diz quanto maior —, e binário é o caso particular de só dois valores, do tipo “sim ou não”.

Família Subtipo Tem ordem? Tem distância? Exemplo
Numérico Contínuo sim sim uma taxa, um preço
Numérico Discreto sim sim uma contagem
Categórico Nominal não não uma sigla, uma cor
Categórico Ordinal sim não uma faixa “baixa/média/alta”
Categórico Binário não “sim”/“não”

A distância é o que separa numérico de ordinal: em ambos dá para dizer “isto vem depois daquilo”, mas só no numérico faz sentido perguntar “quanto depois”.

O tipo de uma coluna decide qual gráfico faz sentido para ela, qual estatística pode ser calculada sobre ela e como um programa consegue validar o que entra nela; tratar uma variável ordinal como se fosse numérica produz conclusão errada com aparência de rigor — a média entre “baixa” e “alta” devolve um número, só que um número que não significa nada, porque a distância entre as duas nunca foi definida.

O que o pandas infere sozinho

Para ver a distinção aplicada a um caso concreto, considere as unidades federativas do Brasil, com população e taxa de homicídios:

estados = pd.read_csv("dados/estados.csv")
estados.dtypes
estado              object
populacao            int64
taxa_homicidios    float64
sigla               object
dtype: object

O pandas acerta metade da tarefa sozinho, só olhando a forma dos valores: populacao vira int64 — é contagem, portanto numérico discreto —, e taxa_homicidios vira float64 — é uma taxa, portanto numérico contínuo. estado e sigla viram object, o tipo genérico de texto que o pandas usa quando não sabe o que mais dizer sobre uma coluna: ele enxerga strings, não que “RO” e “AC” vêm de um conjunto fechado de rótulos possíveis. É inferência sobre a forma do valor, não sobre o que ele significa — só quem lê o dado sabe que sigla é categórico nominal.

len(estados), estados["sigla"].nunique()
(27, 27)

As 27 linhas trazem 27 siglas distintas — nenhuma se repete.

Declarando o categórico

Dizer ao pandas o que já se sabe sobre a coluna é uma linha:

antes = estados.memory_usage(deep=True)
estados["sigla"] = estados["sigla"].astype("category")
estados["sigla"].cat.categories
Index(['AC', 'AL', 'AM', 'AP', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MG', 'MS',
       'MT', 'PA', 'PB', 'PE', 'PI', 'PR', 'RJ', 'RN', 'RO', 'RR', 'RS', 'SC',
       'SE', 'SP', 'TO'],
      dtype='object')

Antes de acreditar que a conversão economiza alguma coisa, meça. Comparando o consumo de memória coluna a coluna, de antes para depois:

depois = estados.memory_usage(deep=True)
comparacao = pd.DataFrame({"object": antes, "category": depois})
comparacao.loc["total"] = comparacao.sum()
comparacao["variação (%)"] = (
    (comparacao["category"] - comparacao["object"]) / comparacao["object"] * 100
).round(1)
comparacao
object category variação (%)
Index 132 132 0.0
estado 1753 1753 0.0
populacao 216 216 0.0
taxa_homicidios 216 216 0.0
sigla 1377 2476 79.8
total 3694 4793 29.8

sigla sobe de 1.377 para 2.476 bytes — quase 80% a mais —, e como nenhuma outra coluna muda, o DataFrame inteiro sobe de 3.694 para 4.793 bytes, quase 30% a mais. Com 27 valores todos distintos, não há repetição nenhuma para o category amortizar: o array de códigos mais o índice de categorias, por cima do que já existia, custam mais do que os ponteiros de texto do object. O ganho aqui é semântico, não de memória — e, sem repetição para amortizar, nem podia ser: o que muda é que operações que só fazem sentido sobre um conjunto fechado de rótulos passam a existir, como listar as categorias possíveis ou recusar um valor que não está entre elas. A economia de memória aparece quando poucos valores se repetem em muitas linhas — é o caso que a seção 6.3 mostra, ao tipar uma coluna de cidade.

Ordem sem distância: o categórico ordinal

Nem todo categórico é nominal. Uma faixa de população tem ordem — “6 a 15 milhões” vem depois de “2 a 6 milhões” — mesmo sem ter distância: a faixa não diz o quanto maior. pd.cut corta um numérico em faixas categóricas, e ordered=True guarda essa ordem em vez de descartá-la:

faixa = pd.cut(
    estados["populacao"],
    bins=[0, 2_000_000, 6_000_000, 15_000_000, 50_000_000],
    labels=["até 2 milhões", "2 a 6 milhões", "6 a 15 milhões", "mais de 15 milhões"],
    ordered=True,
)
faixa.value_counts()
populacao
2 a 6 milhões         10
6 a 15 milhões         9
até 2 milhões          5
mais de 15 milhões     3
Name: count, dtype: int64

Dez estados ficam na faixa intermediária, de 2 a 6 milhões; só três passam de 15 milhões. Com a ordem declarada, comparar deixa de ser comparar texto e passa a ser comparar posição:

(faixa < "6 a 15 milhões").sum()
np.int64(15)

Quinze estados ficam abaixo da faixa “6 a 15 milhões” — o < olha a posição de cada faixa na ordem que ordered=True fixou, não a ordem alfabética dos rótulos (que poria “6 a 15 milhões” antes de “até 2 milhões”, e a conta sairia errada).

Pedir a mesma comparação a uma coluna nominal não tem resposta, e o pandas recusa a pergunta em vez de inventar uma:

try:
    estados["sigla"] < "SP"
except TypeError as erro:
    print(erro)
Unordered Categoricals can only compare equality or not

Sem ordem declarada, “menor que” não está definido para sigla — é a mesma pergunta que devolveria um número, e uma falsa sensação de precisão, se a coluna tivesse ficado como texto solto em vez de category.

O tipo escolhe o gráfico

A mesma distinção decide o gráfico. Um categórico com um número por rótulo pede barras — a pergunta é “quanto vale cada um”; um numérico contínuo pede histograma — a pergunta é “como os valores se distribuem”:

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5))

top10 = estados.nlargest(10, "populacao").sort_values("populacao")
ax1.barh(top10["estado"], top10["populacao"] / 1e6)
ax1.set_xlabel("população (milhões)")
ax1.set_title("10 maiores populações")

ax2.hist(estados["taxa_homicidios"], bins=8)
ax2.set_xlabel("taxa de homicídios (por 100 mil hab.)")
ax2.set_ylabel("estados")
ax2.set_title("distribuição da taxa de homicídios")

plt.tight_layout()
plt.show()
Figura 28.1: Um categórico (a unidade federativa) pede barras; um contínuo (a taxa de homicídios) pede histograma — o tipo escolhe o gráfico

estados ainda é só quatro colunas soltas, cada uma com o seu tipo. A próxima seção olha a estrutura que as mantém juntas — o DataFrame em si, a sua forma, o seu índice, e as duas formas de escolher linha e coluna dentro dele.