Tipos de Dados no R e no Python

Da classificação conceitual à implementação no software

Introdução

Nos capítulos anteriores, aprendemos a classificar variáveis: numéricas ou categóricas, discretas ou contínuas, nominais ou ordinais. Essa classificação guia a escolha de estatísticas descritivas, gráficos e testes. Mas entre a classificação no papel e a análise no computador é preciso dizer ao software o que cada variável representa.

O problema é que R e Python não leem o dicionário de variáveis. Quando você importa uma planilha, o software faz sua própria classificação — e nem sempre acerta. Uma variável categórica codificada como 1, 2 e 3 será tratada como numérica. Uma variável numérica com vírgula decimal pode ser importada como texto. E o software não reclama: ele segue em frente, calcula o que você pedir e entrega um resultado — silenciosamente errado.

Este capítulo ensina a verificar e corrigir essa tradução. Vamos usar o nosso banco de dados pacientes.csv e mostrar, lado a lado, como R e Python lidam com cada tipo de dado.

O que o software vê quando você importa dados

Antes de classificar variáveis, vale entender o que acontece nos bastidores. Quando R ou Python importam um arquivo CSV, cada coluna recebe automaticamente um tipo de dado (data type) com base nos valores que encontra.

str(pacientes)
spc_tbl_ [403 × 18] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ n               : num [1:403] 1 2 3 4 5 6 7 8 9 10 ...
 $ id              : num [1:403] 1000 1001 1002 1003 1005 ...
 $ colesterol      : num [1:403] 203 165 228 78 249 248 195 227 177 263 ...
 $ glicose         : num [1:403] 82 97 92 93 90 94 92 75 87 89 ...
 $ hdl             : num [1:403] 56 24 37 12 28 69 41 44 49 40 ...
 $ ratio           : num [1:403] 3.6 6.9 6.2 6.5 8.9 ...
 $ glicohemoglobina: num [1:403] 4.31 4.44 4.64 4.63 7.72 ...
 $ cidade          : chr [1:403] "Buckingham" "Buckingham" "Buckingham" "Buckingham" ...
 $ idade           : num [1:403] 46 29 58 67 64 34 30 37 45 55 ...
 $ sexo            : chr [1:403] "female" "female" "female" "male" ...
 $ altura          : num [1:403] 157 163 155 170 173 ...
 $ peso            : num [1:403] 54.9 98.9 116.1 54 83 ...
 $ biotipo         : chr [1:403] "medium" "large" "large" "large" ...
 $ sistolica       : num [1:403] 118 112 190 110 138 132 161 NA 160 108 ...
 $ diastolica      : num [1:403] 59 68 92 50 80 86 112 NA 80 72 ...
 $ cintura         : num [1:403] 29 46 49 33 44 36 46 34 34 45 ...
 $ quadril         : num [1:403] 38 48 57 38 41 42 49 39 40 50 ...
 $ time.ppn        : num [1:403] 720 360 180 480 300 195 720 1020 300 240 ...
 - attr(*, "spec")=
  .. cols(
  ..   n = col_double(),
  ..   id = col_double(),
  ..   colesterol = col_double(),
  ..   glicose = col_double(),
  ..   hdl = col_double(),
  ..   ratio = col_double(),
  ..   glicohemoglobina = col_double(),
  ..   cidade = col_character(),
  ..   idade = col_double(),
  ..   sexo = col_character(),
  ..   altura = col_double(),
  ..   peso = col_double(),
  ..   biotipo = col_character(),
  ..   sistolica = col_double(),
  ..   diastolica = col_double(),
  ..   cintura = col_double(),
  ..   quadril = col_double(),
  ..   time.ppn = col_double()
  .. )
 - attr(*, "problems")=<externalptr> 

No R, str() mostra a estrutura do data frame. Cada coluna tem um tipo: num (numérico), chr (texto), int (inteiro). Note que sexo, cidade e biotipo foram importadas como chr — o R não sabe que são categóricas.

import pandas as pd

pacientes_py = pd.read_csv("data/pacientes.csv")
print(pacientes_py.dtypes)
n                     int64
id                    int64
colesterol          float64
glicose               int64
hdl                 float64
ratio               float64
glicohemoglobina    float64
cidade               object
idade                 int64
sexo                 object
altura              float64
peso                float64
biotipo              object
sistolica           float64
diastolica          float64
cintura             float64
quadril             float64
time.ppn            float64
dtype: object

No Python (pandas), .dtypes mostra o tipo de cada coluna: float64 (numérico decimal), int64 (inteiro), object (texto). Assim como no R, sexo, cidade e biotipo aparecem como object — o pandas não sabe que são categóricas.

A mensagem é a mesma nos dois ambientes: a importação automática não é suficiente. O software adivinha tipos com base nos valores, não no significado das variáveis. Cabe ao pesquisador corrigir.

Variáveis numéricas

Variáveis numéricas são as mais simples de lidar — na maioria dos casos, a importação automática acerta. Peso, colesterol, glicose e pressão arterial entram como números decimais; idade, como inteiro.

class(pacientes$peso)       # "numeric" (decimal)
[1] "numeric"
class(pacientes$idade)      # "numeric" (importado como decimal, mesmo sendo inteiro)
[1] "numeric"
is.numeric(pacientes$peso)  # TRUE
[1] TRUE

No R, tanto integer quanto numeric (que internamente é double) são tratados como numéricos. Na prática, essa diferença raramente importa: o R converte automaticamente entre os dois quando necessário. O importante é que is.numeric() retorne TRUE.

Para forçar uma variável a ser inteira (por exemplo, contagens):

pacientes$idade_int <- as.integer(pacientes$idade)
class(pacientes$idade_int)
[1] "integer"
print(pacientes_py['peso'].dtype)       # float64
float64
print(pacientes_py['idade'].dtype)      # int64
int64
# Verificar se é numérico
print(pd.api.types.is_numeric_dtype(pacientes_py['peso']))  # True
True

No pandas, int64 e float64 correspondem à distinção inteiro/decimal. Assim como no R, a conversão é simples:

# Converter para inteiro (se não houver decimais)
pacientes_py['idade'] = pacientes_py['idade'].astype('int64')

# Converter para decimal
pacientes_py['idade_float'] = pacientes_py['idade'].astype('float64')
print(pacientes_py[['idade', 'idade_float']].dtypes)
idade            int64
idade_float    float64
dtype: object
DicaQuando inteiro vs. decimal importa?

Na grande maioria das análises em saúde, não importa. Média, desvio-padrão, teste t e regressão linear funcionam da mesma forma para ambos. A exceção são modelos de contagem (Poisson, binomial negativa), que esperam valores inteiros. Fora isso, não perca tempo convertendo tipos numéricos entre si.

Variáveis categóricas nominais

Aqui começa a parte que exige atenção do pesquisador. Quando o R ou o Python importam uma coluna com valores como “female” e “male”, eles a armazenam como texto — não como uma variável categórica com níveis definidos. Isso significa que o software não sabe quantas categorias existem, não sabe ordená-las, e não sabe que “Female” e “female” deveriam ser a mesma coisa.

# Antes da conversão: texto puro
class(pacientes$sexo)
[1] "character"
# Converter para fator
pacientes$sexo <- factor(pacientes$sexo)
class(pacientes$sexo)
[1] "factor"
levels(pacientes$sexo)
[1] "female" "male"  

O factor é o tipo do R para variáveis categóricas. Ao converter, o R identifica automaticamente os níveis únicos. Você pode definir os níveis explicitamente — útil para controlar a ordem em gráficos e tabelas:

pacientes$sexo <- factor(pacientes$sexo,
                          levels = c("female", "male"),
                          labels = c("Feminino", "Masculino"))
levels(pacientes$sexo)
[1] "Feminino"  "Masculino"
table(pacientes$sexo)

 Feminino Masculino 
      234       169 

Cuidado: se você define levels que não existem nos dados, as observações correspondentes viram NA. Se um valor nos dados não está na lista de levels, ele também vira NA. Isso é uma proteção, mas pode ser uma armadilha se houver erros de digitação.

# Antes da conversão: object (texto)
print(pacientes_py['sexo'].dtype)
object
# Converter para category
pacientes_py['sexo'] = pacientes_py['sexo'].astype('category')
print(pacientes_py['sexo'].dtype)
category
print(pacientes_py['sexo'].cat.categories)
Index(['female', 'male'], dtype='object')

O tipo category no pandas é o equivalente ao factor do R. Para renomear as categorias:

pacientes_py['sexo'] = pacientes_py['sexo'].cat.rename_categories({
    'female': 'Feminino',
    'male': 'Masculino'
})
print(pacientes_py['sexo'].value_counts())
sexo
Feminino     234
Masculino    169
Name: count, dtype: int64

Cuidado: diferentemente do R, o pandas não transforma em NaN os valores que não estão nas categorias — ele simplesmente os mantém. Isso pode mascarar erros de digitação.

Variáveis categóricas ordinais

Variáveis ordinais exigem um passo a mais: além de dizer ao software que são categóricas, precisamos informar a ordem dos níveis. Sem isso, o software trata “small”, “medium” e “large” como categorias sem hierarquia — e gráficos, tabelas e testes podem apresentar os níveis em ordem alfabética ou aleatória.

# Criar fator ordenado
pacientes$biotipo <- factor(pacientes$biotipo,
                             levels = c("small", "medium", "large"),
                             ordered = TRUE)
class(pacientes$biotipo)
[1] "ordered" "factor" 
levels(pacientes$biotipo)
[1] "small"  "medium" "large" 
# Agora o R sabe que small < medium < large
pacientes$biotipo[1] < pacientes$biotipo[2]
[1] TRUE

A diferença entre factor e ordered factor é sutil mas importante: com um fator ordenado, operações de comparação (<, >) funcionam, e algumas funções estatísticas (como polr() para regressão ordinal) reconhecem automaticamente a ordem.

from pandas.api.types import CategoricalDtype

# Definir tipo ordinal
biotipo_type = CategoricalDtype(
    categories=['small', 'medium', 'large'],
    ordered=True
)
pacientes_py['biotipo'] = pacientes_py['biotipo'].astype(biotipo_type)

# Verificar
print(pacientes_py['biotipo'].dtype)
category
print("Ordenado:", pacientes_py['biotipo'].cat.ordered)
Ordenado: True
# Comparações funcionam
print("small < medium:", pacientes_py['biotipo'].iloc[0] < pacientes_py['biotipo'].iloc[1])
small < medium: False

No pandas, a distinção nominal/ordinal é controlada pelo parâmetro ordered do CategoricalDtype. Com ordered=True, comparações entre categorias passam a funcionar.

AvisoOrdem errada é pior que nenhuma ordem

Definir a ordem errada dos níveis (por exemplo, large < medium < small) não gera erro — o software aceita silenciosamente. Mas todas as análises que dependem da ordem (medianas, testes ordinais, gráficos) estarão invertidas. Sempre verifique com levels() no R ou .cat.categories no Python após definir a ordem.

Variáveis binárias

Como vimos no Capítulo 2, variáveis binárias vivem na fronteira entre o categórico e o numérico. No software, essa dupla natureza se traduz em uma escolha prática: armazenar como fator de dois níveis ou como 0/1 numérico?

# Opção 1: como fator (categórica)
obito <- factor(c("sim", "não", "sim", "não", "sim"),
                levels = c("não", "sim"))
table(obito)
obito
não sim 
  2   3 
# Opção 2: como numérico 0/1
obito_num <- as.numeric(obito) - 1  # factor começa em 1, então subtrai 1
mean(obito_num)  # proporção de óbitos
[1] 0.6
# Opção 3: como lógico
obito_log <- c(TRUE, FALSE, TRUE, FALSE, TRUE)
mean(obito_log)  # R converte TRUE=1, FALSE=0 automaticamente
[1] 0.6

No R, factor é a escolha padrão para análises descritivas e tabelas de contingência. Mas funções como glm() (regressão logística) aceitam tanto factor quanto 0/1 — e muitos usuários preferem a codificação numérica pela transparência: fica claro qual categoria é 0 e qual é 1.

import numpy as np

# Opção 1: como category
obito = pd.Categorical(['sim', 'não', 'sim', 'não', 'sim'],
                        categories=['não', 'sim'])
print(pd.Series(obito).value_counts())
sim    3
não    2
Name: count, dtype: int64
# Opção 2: como numérico 0/1
obito_num = pd.Series([1, 0, 1, 0, 1])
print("Proporção de óbitos:", obito_num.mean())
Proporção de óbitos: 0.6
# Opção 3: como booleano
obito_bool = pd.Series([True, False, True, False, True])
print("Proporção (bool):", obito_bool.mean())
Proporção (bool): 0.6

No pandas, category funciona bem para tabelas e gráficos, mas a maioria dos modelos do scikit-learn espera codificação numérica 0/1. A conversão é direta com .cat.codes ou .map().

A regra prática: se a variável vai entrar em um modelo estatístico, verifique o que a função espera. Se vai aparecer em uma tabela ou gráfico descritivo, factor/category com rótulos legíveis é a melhor escolha.

Armadilhas comuns na importação

A importação automática funciona bem na maioria dos casos — mas quando falha, falha silenciosamente. Aqui estão os problemas mais frequentes.

O número que virou texto

O separador decimal no Brasil é a vírgula (72,5 kg), mas o padrão internacional é o ponto (72.5). Se o CSV usa vírgula e o software espera ponto, a coluna inteira é importada como texto.

# Simulando o problema
peso_texto <- c("72,5", "68,3", "81,0")
class(peso_texto)  # "character"
[1] "character"
# mean(peso_texto)  # ERRO: argumento não é numérico

# Solução 1: converter manualmente
peso_num <- as.numeric(gsub(",", ".", peso_texto))
peso_num
[1] 72.5 68.3 81.0
# Solução 2: usar read_csv2() do readr, que espera ; como separador
# e , como decimal (padrão europeu/brasileiro)
# pacientes <- read_csv2("data/pacientes.csv")
# Simulando o problema
peso_texto = pd.Series(["72,5", "68,3", "81,0"])
print("Tipo:", peso_texto.dtype)  # object (texto)
Tipo: object
# Solução 1: substituir e converter
peso_num = peso_texto.str.replace(',', '.').astype(float)
print(peso_num)
0    72.5
1    68.3
2    81.0
dtype: float64

# Solução 2: usar decimal=',' na importação
# pacientes = pd.read_csv("data/pacientes.csv", decimal=',')

A categórica disfarçada de número

Códigos numéricos para categorias (1 = leve, 2 = moderado, 3 = grave) são importados como números — e o software não tem como saber que representam categorias. A média de 2,3 “graus de gravidade” não tem sentido, mas o software a calcula sem hesitar.

# O problema: parece numérica, mas é categórica
gravidade_cod <- c(1, 2, 3, 2, 1, 3, 2)
mean(gravidade_cod)  # 2.0 — sem sentido clínico!
[1] 2
# A solução: converter para fator
gravidade <- factor(gravidade_cod,
                     levels = c(1, 2, 3),
                     labels = c("Leve", "Moderado", "Grave"),
                     ordered = TRUE)
gravidade
[1] Leve     Moderado Grave    Moderado Leve     Grave    Moderado
Levels: Leve < Moderado < Grave
# mean(gravidade)  # ERRO — agora o R protege contra o uso indevido
# O problema
gravidade_cod = pd.Series([1, 2, 3, 2, 1, 3, 2])
print("Média sem sentido:", gravidade_cod.mean())
Média sem sentido: 2.0
# A solução
gravidade = gravidade_cod.map({1: 'Leve', 2: 'Moderado', 3: 'Grave'})
gravidade = gravidade.astype(
    CategoricalDtype(categories=['Leve', 'Moderado', 'Grave'], ordered=True)
)
print(gravidade)
0        Leve
1    Moderado
2       Grave
3    Moderado
4        Leve
5       Grave
6    Moderado
dtype: category
Categories (3, object): ['Leve' < 'Moderado' < 'Grave']

O NA invisível

Valores ausentes podem aparecer de muitas formas: célula vazia, “NA”, “N/A”, “.”, “-”, “999”, “não informado”. O R e o Python reconhecem alguns desses automaticamente, mas não todos.

# O R reconhece automaticamente: NA, "", "NA"
# Mas não reconhece: "999", ".", "-", "não informado"

# Solução na importação:
# pacientes <- read_csv("data/pacientes.csv", na = c("", "NA", "999", "."))

# Verificar missings após importação
colSums(is.na(pacientes[, c("colesterol", "glicose", "hdl", "peso")]))
colesterol    glicose        hdl       peso 
         1          0          1          1 
# O pandas reconhece automaticamente: NaN, "", "NA", "N/A", "null"
# Mas não reconhece: "999", ".", "-", "não informado"

# Solução na importação:
# pacientes = pd.read_csv("data/pacientes.csv", na_values=["999", ".", "-"])

# Verificar missings após importação
print(pacientes_py[['colesterol', 'glicose', 'hdl', 'peso']].isnull().sum())
colesterol    1
glicose       0
hdl           1
peso          1
dtype: int64

Um workflow de verificação

Após importar os dados, um workflow rápido de verificação evita surpresas na análise. São poucos comandos, mas podem salvar horas de trabalho.

# 1. Estrutura geral
glimpse(pacientes)
Rows: 403
Columns: 19
$ n                <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16…
$ id               <dbl> 1000, 1001, 1002, 1003, 1005, 1008, 1011, 1015, 1016,…
$ colesterol       <dbl> 203, 165, 228, 78, 249, 248, 195, 227, 177, 263, 242,…
$ glicose          <dbl> 82, 97, 92, 93, 90, 94, 92, 75, 87, 89, 82, 128, 75, …
$ hdl              <dbl> 56, 24, 37, 12, 28, 69, 41, 44, 49, 40, 54, 34, 36, 4…
$ ratio            <dbl> 3.6, 6.9, 6.2, 6.5, 8.9, 3.6, 4.8, 5.2, 3.6, 6.6, 4.5…
$ glicohemoglobina <dbl> 4.31, 4.44, 4.64, 4.63, 7.72, 4.81, 4.84, 3.94, 4.84,…
$ cidade           <chr> "Buckingham", "Buckingham", "Buckingham", "Buckingham…
$ idade            <dbl> 46, 29, 58, 67, 64, 34, 30, 37, 45, 55, 60, 38, 27, 4…
$ sexo             <fct> Feminino, Feminino, Feminino, Masculino, Masculino, M…
$ altura           <dbl> 157.48, 162.56, 154.94, 170.18, 172.72, 180.34, 175.2…
$ peso             <dbl> 54.88463, 98.88306, 116.11955, 53.97745, 83.00734, 86…
$ biotipo          <ord> medium, large, large, large, medium, large, medium, m…
$ sistolica        <dbl> 118, 112, 190, 110, 138, 132, 161, NA, 160, 108, 130,…
$ diastolica       <dbl> 59, 68, 92, 50, 80, 86, 112, NA, 80, 72, 90, 68, 80, …
$ cintura          <dbl> 29, 46, 49, 33, 44, 36, 46, 34, 34, 45, 39, 42, 35, 3…
$ quadril          <dbl> 38, 48, 57, 38, 41, 42, 49, 39, 40, 50, 45, 50, 41, 4…
$ time.ppn         <dbl> 720, 360, 180, 480, 300, 195, 720, 1020, 300, 240, 30…
$ idade_int        <int> 46, 29, 58, 67, 64, 34, 30, 37, 45, 55, 60, 38, 27, 4…
# 2. Verificar se os tipos estão corretos
sapply(pacientes[, c("colesterol", "peso", "idade")], is.numeric)
colesterol       peso      idade 
      TRUE       TRUE       TRUE 
sapply(pacientes[, c("sexo", "cidade", "biotipo")], is.factor)
   sexo  cidade biotipo 
   TRUE   FALSE    TRUE 
# 1. Estrutura geral
pacientes_py.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 403 entries, 0 to 402
Data columns (total 19 columns):
 #   Column            Non-Null Count  Dtype   
---  ------            --------------  -----   
 0   n                 403 non-null    int64   
 1   id                403 non-null    int64   
 2   colesterol        402 non-null    float64 
 3   glicose           403 non-null    int64   
 4   hdl               402 non-null    float64 
 5   ratio             402 non-null    float64 
 6   glicohemoglobina  390 non-null    float64 
 7   cidade            403 non-null    object  
 8   idade             403 non-null    int64   
 9   sexo              403 non-null    category
 10  altura            398 non-null    float64 
 11  peso              402 non-null    float64 
 12  biotipo           391 non-null    category
 13  sistolica         398 non-null    float64 
 14  diastolica        398 non-null    float64 
 15  cintura           401 non-null    float64 
 16  quadril           401 non-null    float64 
 17  time.ppn          400 non-null    float64 
 18  idade_float       403 non-null    float64 
dtypes: category(2), float64(12), int64(4), object(1)
memory usage: 54.7+ KB
# 2. Verificar tipos
print(pacientes_py[['colesterol', 'peso', 'idade']].dtypes)
colesterol    float64
peso          float64
idade           int64
dtype: object
print(pacientes_py[['sexo', 'cidade', 'biotipo']].dtypes)
sexo       category
cidade       object
biotipo    category
dtype: object

O workflow completo segue três passos:

  1. Olhar a estruturaglimpse() ou .info() revelam imediatamente se alguma variável está no tipo errado.
  2. Corrigir os tipos — converter categóricas para factor/category, definir ordem para ordinais, tratar missings.
  3. Confirmar — repetir o passo 1 para verificar que as correções foram aplicadas.
DicaFaça isso uma vez, no início

A conversão de tipos deve ser feita uma única vez, logo após a importação, e de preferência em um script separado ou em um chunk de setup. Isso garante que todas as análises subsequentes partam de dados corretamente tipados — e evita a situação em que um gráfico funciona mas um teste falha porque a variável estava como texto em vez de fator.

Tabela de correspondência

A tabela abaixo resume a correspondência entre a classificação estatística e os tipos de dados no R e no Python:

Tabela 1: Da classificação estatística ao código — correspondência entre tipos conceituais e tipos no software.
Classificação Tipo no R Tipo no Python Como verificar (R) Cuidado
Numérico contínuo numeric / double float64 is.numeric() Inteiro importado como double
Numérico discreto integer int64 is.integer() Pode ser categórica disfarçada
Categórico nominal factor category is.factor() Texto ≠ fator
Categórico ordinal ordered factor CategoricalDtype(ordered=True) is.ordered() Sem ordered=TRUE vira nominal
Binário (como categórica) factor (2 níveis) category (2 níveis) is.factor() + nlevels() == 2 Definir nível de referência
Binário (como numérica) numeric (0/1) ou logical int64 (0/1) ou bool is.numeric() ou is.logical() Saber qual é 0 e qual é 1

Resumo do capítulo

Tabela 2: Resumo — tipos de dados no R e no Python.
Conceito Resumo
Importação automática O software adivinha tipos, mas nem sempre acerta — verificação manual é obrigatória
Variáveis numéricas Geralmente importadas corretamente; integer vs double raramente importa na prática
Variáveis categóricas nominais Importadas como texto; converter para factor/category para que o software reconheça os níveis
Variáveis categóricas ordinais Exigem definição explícita da ordem dos níveis; sem isso, são tratadas como nominais
Variáveis binárias Podem ser armazenadas como fator ou como 0/1 — a escolha depende da análise
Armadilhas comuns Vírgula vs ponto decimal, códigos numéricos para categorias, valores ausentes não reconhecidos
Workflow de verificação Importar → verificar tipos (glimpse/info) → corrigir → confirmar
NotaA ponte entre os capítulos

Este capítulo fecha o ciclo que começou no Capítulo 2. Lá, aprendemos a classificar variáveis conceitualmente. Aqui, traduzimos essa classificação para a linguagem do software. Entre esses dois pontos — a classificação no papel e o tipo no código — está a maioria dos erros silenciosos que comprometem análises em saúde. A boa notícia: poucos minutos de verificação após a importação são suficientes para evitá-los.