Pandas: Tipos de Dados
Última atualização: 2026-08-26
O tipo de dados (dtype) determina como o Pandas armazena e opera em seus dados. Escolha o dtype correto e seu código pode rodar 10 vezes mais rápido, usando 80% menos memória; escolha o errado e você enfrentará perda de precisão, erros de operação e explosões de memória. Esta seção percorre o sistema de dtypes do Pandas, ajudando você a entender as características e métodos de conversão de cada tipo, juntamente com as técnicas de otimização de memória mais práticas.
1. O Que Você Aprenderá
- ❶ As diferenças entre o sistema de dtype do Pandas e os dtypes do NumPy
- ❷ Escolher entre objeto, StringDtype e category
- ❸ Conversão de tipos com astype / convert_dtypes / infer_objects
- ❹ Tipos anuláveis (Int64 / Float64 / booleano)
- ❺ Estratégias de otimização de memória (category / downcast)
2. A Crise de Explosão de Memória do Bob
(1) O Ponto de Dor: Uma Tabela de Clientes com 500 MB
Bob carregou um conjunto de dados de clientes em um DataFrame e verificou o uso de memória — 500 MB! 100 mil linhas não deveriam ocupar tanto espaço:
import pandas as pd
# Load customer data (simulated)
df = pd.DataFrame({
'customer_id': range(100000),
'name': ['Customer_' + str(i) for i in range(100000)],
'city': ['New York', 'London', 'Tokyo', 'Paris', 'Sydney'] * 20000,
'age': [25 + i % 50 for i in range(100000)],
'loyalty_points': [100 + i * 10 for i in range(100000)]
})
print(f"Memory before optimization: {df.memory_usage(deep=True).sum() / 1024 / 1024:.1f} MB")
# Typical output: ~18 MB (this example is small)
# Real 100K-row dataset with many string columns can reach 500+ MB
> **Output:** Run this in a local Python environment (pandas 2.x). The Piston server does not have pandas pre-installed — install it locally (`pip install pandas`) to follow along. Actual values may vary slightly depending on your pandas version.
O culpado: o tipo objeto. Por padrão, o Pandas armazena colunas de strings como objeto (ponteiro de objeto Python), onde cada elemento é um objeto string Python completo — um grande consumo de memória.
(2) A Solução: Compressão de 10x com o Tipo category
▶ Exemplo
> **Output:** Run this in a local Python environment (pandas 2.x). The Piston server does not have pandas pre-installed — install it locally (`pip install pandas`) to follow along. Actual values may vary slightly depending on your pandas version.
: Otimização de Memória com category (Dificuldade ⭐⭐)
import pandas as pd
df = pd.DataFrame({
'customer_id': range(100000),
'name': ['Customer_' + str(i) for i in range(100000)],
'city': ['New York', 'London', 'Tokyo', 'Paris', 'Sydney'] * 20000,
'age': [25 + i % 50 for i in range(100000)],
'loyalty_points': [100 + i * 10 for i in range(100000)]
})
# Check memory before optimization
mem_before = df.memory_usage(deep=True).sum() / 1024 / 1024
# Optimize: city has only 5 unique values → category
df['city'] = df['city'].astype('category')
# Optimize: age range 25-74 → int8 is enough (max 127)
df['age'] = df['age'].astype('int8')
# Optimize: loyalty_points range → int32 is enough
df['loyalty_points'] = df['loyalty_points'].astype('int32')
mem_after = df.memory_usage(deep=True).sum() / 1024 / 1024
print(f"Before: {mem_before:.1f} MB")
print(f"After: {mem_after:.1f} MB")
print(f"Saved: {(1 - mem_after/mem_before)*100:.0f}%")
# Before: ~18 MB
# After: ~5 MB
# Saved: ~72%
> **Output:** Run this in a local Python environment (pandas 2.x). The Piston server does not have pandas pre-installed — install it locally (`pip install pandas`) to follow along. Actual values may vary slightly depending on your pandas version.
(3) O Resultado: 3 Técnicas-Chave de Otimização
| Técnica | Efeito | Quando Usar |
|---|---|---|
| objeto→category | Redução de memória de 90%+ | Strings com baixa cardinalidade (valores únicos < 50% do número de linhas) |
| int64→int8/int16/int32 | Redução de memória de ~75% | Faixas numéricas pequenas |
| float64→float32 | Redução de memória de 50% | Requisitos de baixa precisão |
3. O Sistema dtype do Pandas
(1) O Panorama Completo de Tipos
graph TB
DT["Pandas dtype"] --> NUM["Numeric"]
DT --> STR["String-like"]
DT --> DT_TYPE["Datetime"]
DT --> BOOL["Boolean"]
DT --> CAT["Categorical"]
DT --> NULL["Nullable"]
NUM --> I["int8/16/32/64"]
NUM --> UI["uint8/16/32/64"]
NUM --> F["float32/float64"]
STR --> O["object (legacy)"]
STR --> SD["StringDtype (new)"]
DT_TYPE --> DT64["datetime64[ns]"]
DT_TYPE --> TD64["timedelta64[ns]"]
BOOL --> PB["bool (NumPy)"]
BOOL --> NB["boolean (Nullable)"]
NULL --> NI["Int8/16/32/64"]
NULL --> NF["Float32/64"]
NULL --> NBO["boolean"]
NULL --> NS["string"]
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
(2) dtype do Pandas vs dtype do NumPy
| Característica | dtype do NumPy | dtype do Pandas |
|---|---|---|
| Tipos numéricos | int8~64 / float32~64 | Mesmo que o NumPy + Nullable (Int64/Float64) |
| Strings | objeto | objeto / StringDtype / string |
| Booleano | bool | bool / booleano (Nullable) |
| Valores ausentes | np.nan (apenas float) | NaN / NaT / pd.NA (cobertura completa) |
| Categórico | Nenhum | category |
| Tempo | datetime64 | datetime64[ns] / timedelta64[ns] |
| Extensão | Nenhum | ExtensionDtype (personalizado) |
pd.NA para representar valores ausentes de forma uniforme, resolvendo o problema antigo de que "colunas de inteiros não podem conter NaN."
4. objeto vs StringDtype vs category
(1) Comparando os Três
| Característica | objeto | StringDtype | category |
|---|---|---|---|
| Armazenamento subjacente | Ponteiros de objeto Python | Armazenamento dedicado para strings | Códigos inteiros + tabela de lookup |
| Valores ausentes | None / np.nan | pd.NA | pd.NA |
| Memória | Maior | Média | Menor (para baixa cardinalidade) |
| Métodos de string | Acessor .str | Acessor .str | Acessor .str |
| Comparações | Podem ser inconsistentes | Consistentes | Consistentes |
| Ordenação | Lexicográfica | Lexicográfica | Suporta ordem personalizada |
| Ideal para | Compatibilidade com código legado | Recomendado para novos projetos | Colunas com baixa cardinalidade |
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado — instale localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
: Comparação de Memória dos Três Tipos (Dificuldade ⭐⭐)
import pandas as pd
import numpy as np
# 1000 linhas, 5 cidades únicas
cities = ['New York', 'London', 'Tokyo', 'Paris', 'Sydney']
data = [cities[i % 5] for i in range(1000)]
s_object = pd.Series(data, dtype='object')
s_string = pd.Series(data, dtype='string')
s_category = pd.Series(data, dtype='category')
print(f"object: {s_object.memory_usage(deep=True) / 1024:.1f} KB")
print(f"string: {s_string.memory_usage(deep=True) / 1024:.1f} KB")
print(f"category: {s_category.memory_usage(deep=True) / 1024:.1f} KB")
# object: ~62 KB
# string: ~55 KB
# category: ~5 KB ← 12x menor!
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado — instale localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
5. Métodos de Conversão de Tipo
(1) astype: Conversão Explícita
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
: Conversão de Tipo com astype (Dificuldade ⭐)
import pandas as pd
df = pd.DataFrame({
'price_str': ['9.99', '19.99', '29.99'],
'quantity_int': [1, 3, 5],
'flag_str': ['True', 'False', 'True']
})
# String para float
df['price'] = df['price_str'].astype(float)
# Int para string (para colunas de ID)
df['qty_str'] = df['quantity_int'].astype(str)
# String para booleano (cuidado: 'True'/'False' como strings → bool)
df['flag'] = df['flag_str'].map({'True': True, 'False': False})
print(df.dtypes)
# price_str object
# quantity_int int64
# flag_str object
# price float64
# qty_str object
# flag bool
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
(2) convert_dtypes: Inferir Automaticamente o Melhor Tipo
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
: Conversão Automática com convert_dtypes (Dificuldade ⭐⭐)
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'age': [28, 34, 25],
'score': [85.5, 92.0, 78.3],
'active': [True, True, False]
})
# Converter para os melhores tipos anuláveis
df_converted = df.convert_dtypes()
print(df_converted.dtypes)
# name string ← object → StringDtype
# age Int64 ← int64 → nullable Int64
# score Float64 ← float64 → nullable Float64
# active boolean ← bool → nullable boolean
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
(3) infer_objects: Inferir o Melhor Tipo para Colunas do Tipo objeto
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
: Inferência com infer_objects (Dificuldade ⭐)
import pandas as pd
df = pd.DataFrame({
'a': [1, 2, 3], # armazenado como object
'b': ['x', 'y', 'z'] # permanece como object
}, dtype='object')
print(df.dtypes)
# a object
# b object
df_inferred = df.infer_objects()
print(df_inferred.dtypes)
# a int64 ← inferido a partir do conteúdo
# b object ← ainda object (strings permanecem)
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
(4) Comparação dos Métodos de Conversão
| Método | Propósito | Automático/Manual | Anulável |
|---|---|---|---|
| astype() | Forçar conversão para um tipo especificado | Manual | Não automático |
| convert_dtypes() | Inferir o melhor tipo Anulável | Automático | Sim |
| infer_objects() | Inferir o tipo de colunas do tipo objeto | Automático | Não automático |
6. Tipos Anuláveis: Resolvendo o Problema de Valores Ausentes em Inteiros
(1) Por que os Tipos Anuláveis São Necessários
O int64 do NumPy não pode armazenar NaN — quando uma coluna de inteiros tem valores ausentes, o Pandas é forçado a promover toda a coluna para float64, perdendo o tipo original.
▶ Exemplo
> **Output:** Run this in a local Python environment (pandas 2.x). The Piston server does not have pandas pre-installed — install it locally (`pip install pandas`) to follow along. Actual values may vary slightly depending on your pandas version.
: Tipos Anuláveis vs Tradicionais (Dificuldade ⭐⭐)
import pandas as pd
import numpy as np
# Traditional: int column with NaN → forced to float
s_trad = pd.Series([1, 2, np.nan, 4])
print(s_trad) # 1.0, 2.0, NaN, 4.0 — floats!
print(s_trad.dtype) # float64
# Nullable: Int64 keeps integer type with <NA>
s_null = pd.Series([1, 2, pd.NA, 4], dtype='Int64')
print(s_null) # 1, 2, <NA>, 4 — integers!
print(s_null.dtype) # Int64
# Nullable boolean
b_null = pd.Series([True, False, pd.NA], dtype='boolean')
print(b_null)
# Nullable string
str_null = pd.Series(['Alice', pd.NA, 'Charlie'], dtype='string')
print(str_null)
> **Output:** Run this in a local Python environment (pandas 2.x). The Piston server does not have pandas pre-installed — install it locally (`pip install pandas`) to follow along. Actual values may vary slightly depending on your pandas version.
(2) Tipos Anuláveis de Relance
| Tipo Anulável | Equivalente Tradicional | Marcador de Valor Ausente | Quando Usar |
|---|---|---|---|
| Int8/16/32/64 | int8/16/32/64 | pd.NA | Colunas de inteiros com valores ausentes |
| Float32/64 | float32/64 | pd.NA | Colunas de floats com valores ausentes |
| booleano | bool | pd.NA | Colunas booleanas com valores ausentes |
| string | objeto | pd.NA | Colunas de strings (substitui objeto) |
pd.NA para representar valores ausentes de forma uniforme, substituindo a mistura complicada de np.nan / None / NaT. Durante as operações, o pd.NA segue a "regra de propagação" — qualquer operação envolvendo pd.NA retorna pd.NA.
7. Otimização de Memória na Prática
(1) Verificando o Uso de Memória
▶ Exemplo
> **Output:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
: Análise de memory_usage (Dificuldade ⭐)
import pandas as pd
df = pd.DataFrame({
'id': range(10000),
'city': ['New York', 'London', 'Tokyo', 'Paris', 'Sydney'] * 2000,
'score': [85.5 + i * 0.1 for i in range(10000)],
'status': ['Active', 'Inactive', 'Pending'] * 3333 + ['Active']
})
# Uso de memória por coluna (deep=True para colunas de objetos)
print(df.memory_usage(deep=True))
# Index 80
# id 80000 ← int64 para 10K linhas
# city 630000 ← object (enorme!)
# score 80000 ← float64
# status 460000 ← object (enorme!)
# Total
total = df.memory_usage(deep=True).sum() / 1024 / 1024
print(f"Total: {total:.1f} MB")
> **Output:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
(2) Estratégias de Otimização
▶ Exemplo
> **Output:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
: Otimização de Memória End-to-End (Dificuldade ⭐⭐⭐)
import pandas as pd
df = pd.DataFrame({
'id': range(10000),
'city': ['New York', 'London', 'Tokyo', 'Paris', 'Sydney'] * 2000,
'score': [85.5 + i * 0.1 for i in range(10000)],
'status': ['Active', 'Inactive', 'Pending'] * 3333 + ['Active']
})
mem_before = df.memory_usage(deep=True).sum() / 1024 / 1024
# 1. Colunas de string com baixa cardinalidade → category
df['city'] = df['city'].astype('category')
df['status'] = df['status'].astype('category')
# 2. Downcast numérico
df['id'] = pd.to_numeric(df['id'], downcast='integer') # int64 → int16
df['score'] = pd.to_numeric(df['score'], downcast='float') # float64 → float32
mem_after = df.memory_usage(deep=True).sum() / 1024 / 1024
print(f"Antes: {mem_before:.1f} MB")
print(f"Depois: {mem_after:.1f} MB")
print(f"Economizado: {(1 - mem_after/mem_before)*100:.0f}%")
print(f"\nTipos otimizados:")
print(df.dtypes)
> **Output:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale-o localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
(3) Tabela de Referência para Downcast
| Tipo Original | downcast='integer' | downcast='float' | Economia |
|---|---|---|---|
| int64 | int8/int16/int32 (seleciona o menor automaticamente) | — | 50%~75% |
| float64 | — | float32 | 50% |
| uint64 | uint8/uint16/uint32 | — | 50%~75% |
8. Exemplo Completo: Otimizando uma Tabela de Clientes Grande
▶ Exemplo
> **Output:** Execute este código em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
: Pipeline de Otimização de Tabela de Clientes (Dificuldade ⭐⭐⭐)
import pandas as pd
import numpy as np
# ============================================
# Exemplo abrangente: tabela de clientes
# pipeline de otimização completo
# ============================================
# 1. Cria dados realistas de clientes
n = 50000
df = pd.DataFrame({
'customer_id': range(n),
'name': [f'Customer_{i:05d}' for i in range(n)],
'city': np.random.choice(['New York', 'London', 'Tokyo', 'Paris',
'Sydney', 'Berlin', 'Toronto', 'Seoul'], n),
'age': np.random.randint(18, 80, n),
'membership': np.random.choice(['Basic', 'Silver', 'Gold', 'Platinum'], n),
'points': np.random.randint(0, 100000, n),
'satisfaction': np.random.choice([1, 2, 3, 4, 5, np.nan], n, p=[0.05,0.1,0.2,0.3,0.3,0.05])
})
# 2. Analisa antes da otimização
print("=== ANTES da Otimização ===")
print(df.dtypes)
mem_before = df.memory_usage(deep=True).sum() / 1024 / 1024
print(f"Memória total: {mem_before:.1f} MB")
# 3. Otimiza
# Strings com baixa cardinalidade → category
for col in ['city', 'membership']:
df[col] = df[col].astype('category')
# Downcast de inteiros
for col in ['customer_id', 'age', 'points']:
df[col] = pd.to_numeric(df[col], downcast='integer')
# Float com NaN → Int64 anulável
df['satisfaction'] = df['satisfaction'].astype('Int8')
# 4. Analisa após a otimização
print("\n=== DEPOIS da Otimização ===")
print(df.dtypes)
mem_after = df.memory_usage(deep=True).sum() / 1024 / 1024
print(f"Memória total: {mem_after:.1f} MB")
print(f"Salvo: {(1 - mem_after/mem_before)*100:.0f}%")
# 5. Verifica a integridade dos dados
print(f"\n=== Verificação de Integridade ===")
print(f"Linhas: {len(df)}")
print(f"Satisfação com NaN: {df['satisfaction'].isna().sum()}")
print(f"Cidades únicas: {df['city'].nunique()}")
> **Output:** Execute este código em um ambiente Python local (pandas 2.x). O servidor Piston não tem o pandas pré-instalado — instale localmente (`pip install pandas`) para acompanhar. Os valores reais podem variar ligeiramente dependendo da sua versão do pandas.
❓ Perguntas Frequentes
P: Qual é a diferença entre objeto e string? R: objeto é um array de ponteiros de objetos Python — cada elemento é um objeto string Python separado com sobrecarga significativa de memória. string (StringDtype) é um tipo de string dedicado introduzido no Pandas 1.0+ que usa pd.NA para valores ausentes e oferece uma semântica de operação mais consistente. Para novos projetos, prefira string em vez de objeto.
P: Quais são os benefícios dos tipos Nullable? R: Eles resolvem o problema antigo em que "uma coluna de inteiros com valores ausentes é forçada a se tornar float." Int64 pode conter tanto inteiros quanto pd.NA, preservando a semântica de inteiro. Por exemplo, uma coluna de idade do cliente com valores ausentes permanece como tipo inteiro (Int8) em vez de se tornar float64.
P: Quando devo usar category? R: Quando os valores únicos de uma coluna são muito inferiores ao número de linhas (regra prática: valores únicos < 50% das linhas). Casos típicos: colunas de categorias finitas como cidade, gênero, status ou nível. category substitui strings repetidas por códigos inteiros mais uma tabela de consulta, reduzindo a memória em 90%+. Não é adequado para colunas de alta cardinalidade (como IDs de usuário).
P: Qual é a diferença entre convert_dtypes e astype? R: astype exige que você especifique manualmente o tipo de destino (ex.:
astype('int64')), enquanto convert_dtypes infere automaticamente o melhor tipo Nullable (objeto→string, int64→Int64, float64→Float64). astype é mais preciso; convert_dtypes é mais conveniente.
P: Como verifico o uso de memória? R:
df.memory_usage(deep=True)retorna a contagem de bytes para cada coluna. deep=True calcula a memória real das strings para colunas objeto (não apenas os tamanhos dos ponteiros).df.info(memory_usage='deep')também mostra a memória total.
P: O downcast é seguro? R: Sim — o
pd.to_numeric(downcast=...)do Pandas seleciona automaticamente o menor tipo que pode conter a faixa de dados. Por exemplo, inteiros de 0-100 escolherão uint8 (0-255) sem estouro. Mas se você adicionar dados fora dessa faixa posteriormente, eles podem ser truncados silenciosamente ou upcasted. É melhor executar o downcast como etapa final após a limpeza dos dados estar concluída.
P: Por que o uso de memória é tão grande para colunas string? R: Cada elemento em uma coluna objeto/string é um objeto Python separado com mais de 50 bytes de sobrecarga no cabeçalho do objeto. 10.000 linhas de strings podem ocupar 600 KB, enquanto o mesmo número de linhas int64 ocupa apenas 80 KB. Solução: use category para dados de baixa cardinalidade e considere o processamento em chunks para dados de alta cardinalidade.
📖 Resumo
- O sistema de dtype do Pandas é mais rico do que o do NumPy: ele adiciona tipos anuláveis (Nullable), category e StringDtype
- objeto consome muita memória — colunas de strings com baixa cardinalidade devem ser convertidas para category (redução de memória de mais de 90%)
- Tipos anuláveis (Int64/Float64/booleano/string) usam pd.NA para unificar valores ausentes, resolvendo o problema de "colunas inteiras não podem conter NaN"
- astype converte tipos manualmente, convert_dtypes infere tipos anuláveis automaticamente e infer_objects infere tipos para colunas objeto
- Três etapas para otimização de memória: strings com baixa cardinalidade→category → downcast numérico → float64→float32
- Use memory_usage(deep=True) para ver o uso real de memória — a memória real das colunas objeto excede em muito o tamanho do ponteiro
- Execute o downcast após a limpeza dos dados estar completa para evitar que dados futuros ultrapassem o intervalo do tipo
📝 Exercícios
- Básico (Dificuldade ⭐): Crie um DataFrame com 3 colunas (string/inteiro/float). Verifique os tipos com
dtypes, depois useastypepara converter a coluna de inteiro paraint8e a coluna de float parafloat32. Compare a mudança nomemory_usage. - Intermediário (Dificuldade ⭐⭐): Crie uma Series de inteiro contendo NaN. Trate-a da maneira tradicional (que converte automaticamente para
float64) e com o tipo NullableInt64, depois compare odtypee o comportamento das operações. Em seguida, otimize uma coluna de string com 5 valores únicos usando o tipocategory. - Desafio (Dificuldade ⭐⭐⭐): Crie um DataFrame simulado com 10K linhas e 4 colunas (string de baixa cardinalidade / string de alta cardinalidade / inteiro / float). Execute o pipeline completo de otimização de memória: verificação → baixa cardinalidade → category → downcast → Nullable → verificação. Imprima a mudança na memória em cada etapa e a porcentagem final de economia.