Como extrair e transformar colunas JSON em Python para dados
Extrair e transformar colunas contendo JSON é uma tarefa comum quando se trabalha com APIs, logs ou dumps que misturam texto e estrutura. Saber como normalizar campos aninhados num DataFrame pandas poupa tempo e permite análises e modelação mais fiáveis. Além disso, quando os JSON estão embebidos como strings numa coluna, é importante sistematizar estes passos para garantir reprodutibilidade e desempenho, sobretudo quando se processam milhares a milhões de registos.
Pré-requisitos
- Python 3.8+ instalado
- pandas e json instalados (pip install pandas)
- Noções básicas de DataFrame e dicionários Python
Passo 1: Identificar a coluna JSON e o formato
Primeiro é importante ver como os JSON estão armazenados: strings JSON, dicionários já convertidos ou listas aninhadas. Abra algumas linhas e verifique tipos e exemplos extremos (linhas com valores nulos ou JSON inválido). Para conjuntos maiores, amostre 100–1 000 linhas para perceber a variabilidade. Decidir a estratégia correcta (json.loads, ast.literal_eval ou deixar como dict) depende do formato: se a coluna for do tipo object com strings, há que desserializar; se for já dict, pode ir directamente para a normalização.
import pandas as pd
import json
# Exemplo de DataFrame com coluna JSON como string
df = pd.DataFrame({
'id': [1, 2, 3],
'payload': [
'{"user": {"name": "Ana", "age": 30}, "tags": ["a","b"]}',
'{"user": {"name": "Bruno", "age": 25}, "tags": ["b"]}',
'{"user": {"name": "Carla", "age": null}, "tags": []}'
]
})
# Verificar primeiras linhas
print(df.head())
Passo 2: Converter strings JSON em dicionários
Se a coluna é texto, converta para dict com json.loads. Trate erros comuns: valores nulos, JSON inválido ou encoding inesperado (por ex., aspas simples). Para produção, crie uma função robusta que registe falhas e devolva None em caso de erro. Em cenários maiores (10k+ linhas), processe em blocos para reduzir memória e registar a taxa de sucesso da desserialização (ex.: 99% válidos, 1% inválidos).
def safe_load(s):
if pd.isna(s):
return None
try:
return json.loads(s)
except Exception:
# Pode registar a linha problemática aqui
return None
# Aplicar a conversão
df['payload_json'] = df['payload'].apply(safe_load)
print(df['payload_json'])
Passo 3: Normalizar campos aninhados com pandas.json_normalize
pandas.json_normalize é ideal para expandir dicionários em colunas planas. Para estruturas com listas de objectos pode usar record_path e meta para controlar a expansão. Para conjuntos de dados médios (p.ex. 50k registos) a normalização directa cria um DataFrame com colunas como user.name e user.age; verifique os tipos e renomeie colunas conforme necessário. Se se espera uma média de 3 tags por registo, explodir vai multiplicar o número de linhas — planeie memória e índices.
from pandas import json_normalize
# Normalizar a coluna de dicionários
normalized = json_normalize(df['payload_json'])
# Se o resultado tiver colunas como 'user.name', renomear ou juntar ao original
print(normalized)
# Juntar ao DataFrame original
df2 = pd.concat([df.drop(columns=['payload_json']), normalized], axis=1)
print(df2)
Passo 4: Tratar listas e campos opcionais
Listas (ex.: tags) podem ser convertidas para uma string separada por vírgulas, contadas ou explodidas em várias linhas. Escolha conforme a análise: se precisa de métricas por utilizador, conte; se precisa de análise de frequência, exploda. Campos ausentes (None) devem receber valores por defeito ou tipos coerentes para evitar problemas em agregações (por ex., preencher age nulo com a média ou com um sentinel como -1). Para colunas numéricas, converta explicitamente com astype ou pd.to_numeric com errors='coerce'.
# Converter lista em string e criar contagem
df2['tags_str'] = df2['tags'].apply(lambda x: ','.join(x) if isinstance(x, list) else '')
df2['tags_count'] = df2['tags'].apply(lambda x: len(x) if isinstance(x, list) else 0)
# Tratar age nulo: preencher com média ou valor sentinel
df2['user.age'] = df2['user.age'].fillna(df2['user.age'].mean())
print(df2[['id','user.name','user.age','tags_str','tags_count']])
Passo 5: Explodir listas em várias linhas (quando necessário)
Se precisar de uma linha por tag, use explode. Isto é útil para análises de frequência, mapas ou joins posteriores com tabelas de referência. Atenção: explodir transforma listas vazias em NaN; preencha ou filtre conforme necessário. Exemplos práticos: 3 registos com média 2 tags → após explode terá ~6 linhas; em produção, estime o factor de multiplicação para prever o tamanho final.
# Explodir para uma linha por tag
exploded = df2.explode('tags')
# Se existirem entradas vazias, cuidado com NaN
exploded['tags'] = exploded['tags'].fillna('')
print(exploded[['id','user.name','tags']])
Verificar o resultado
Confirme que as colunas esperadas existem, os tipos estão correctos e as contagens batem com o original. Faça verificações simples: comparar a soma das tags_count com o número total de tags antes de explodir, verificar percentagem de valores nulos por coluna e visualizar amostras de linhas problemáticas. Em pipelines, escreva testes unitários que validem essas invariantes (ex.: total_tags antes == total_tags depois).
# Verificações rápidas
print(df2.info())
print(df2[['id','user.name','user.age']].head())
# Conferir soma das tags_count com número total de tags antes de explodir
print('total tags (contagem):', df2['tags_count'].sum())
print('total tags (explodidas):', exploded['tags'].shape[0])
Conclusão
Extrair e transformar colunas JSON em Python com pandas permite passar de dados semiestruturados para tabelas prontas para análise ou modelação. Para produção, integre este processo num ETL, valide esquemas com jsonschema, registe erros de parsing e utilize processamento em blocos quando lidar com grandes volumes. Dica prática: mantenha sempre um passo de validação e alguns exemplos de linhas reais (10–20 linhas representativas) para evitar surpresas nos dados.