1. Recuperar a decisão que o relatório suporta
Imagina um fecho diário fictício de fundos. As tabelas foram restauradas, o job terminou e o dashboard voltou a abrir. Antes de declarar recuperação, identifica a decisão que o relatório suporta: aceitar posições por book, investigar diferenças ou libertar uma publicação. Escreve o corte temporal, a população esperada, a unidade de medida e a identidade do consumidor. Um gráfico com números plausíveis pode representar o dia errado ou omitir precisamente as operações que exigem intervenção. Prepara uma ficha curta: versão do input, versão das taxas, versão da consulta, resultado esperado por book, exceções admitidas e responsável pela aceitação. A ficha permite separar uma falha na origem de uma transformação incorreta. Se ninguém consegue reconstruir o conjunto esperado, a validação continua incompleta mesmo que todos os controlos técnicos estejam verdes. Combina a evidência da engenharia com uma referência de negócio independente, evitando usar a mesma consulta defeituosa dos dois lados da comparação. Nesta aula, o laboratório avalia apenas um contrato limitado de cardinalidade. Não tem datas de taxa, calendário de fecho, identificação de clientes ou ligação a sistemas reais. O seu resultado ajuda a localizar erros; não aprova uma publicação bancária. Os exemplos são originais e fictícios, sem representar procedimentos BNP Paribas. Reserva tempo para explicar estes limites ao responsável de negócio antes de apresentar um estado de recuperação.
2. Diagnosticar multiplicidade antes de agregar
O laboratório recebe operações identificadas e uma referência de taxas por moeda. Cada operação deve encontrar exatamente uma taxa. Este contrato não equivale a exigir que o número final de linhas seja igual ao inicial: uma ausência pode compensar uma duplicação. Na fixture, a operação EUR encontra duas taxas e a operação GBP não encontra nenhuma. A consulta produz duas linhas para duas operações recebidas, mas perdeu a identidade GBP e repetiu EUR. Executa primeiro o diagnóstico por identificador. Para cada operação, regista matches, leftRows e scaledSum. O campo matches conta as correspondências efetivas da referência. Usa missingTradeIds para localizar ausências e multipliedTradeIds para localizar multiplicações. A lista é mais útil para a investigação do que um indicador global isolado, porque aponta a operação e permite procurar a causa na preparação das taxas. Não apliques DISTINCT ao valor convertido como correção automática. Duas operações legítimas podem ter o mesmo montante; eliminar uma delas reduz atividade válida. Também não escolhas a primeira taxa sem uma regra de versão aprovada. Corrige a preparação da referência segundo o contrato real e repete o diagnóstico. No exercício, taxas duplicadas de moedas não utilizadas não bloqueiam o relatório. Esta escolha é explícita: o controlo mede correspondências exigidas pelo input, não certifica a qualidade de toda a referência.
3. Interpretar NULL, zero e unidades
Uma operação sem taxa continua presente no diagnóstico com LEFT JOIN. Nesse caso, leftRows é um, matches é zero e scaledSum é null. O resultado distingue a existência da operação da existência da referência. Não transformes imediatamente null em zero no dashboard: a substituição faria uma ausência de taxa parecer uma conversão válida sem valor. Encaminha o identificador para a investigação e conserva o significado do estado. O caso oposto também importa. Duas operações distintas de +100 e -100, com a mesma taxa única, dão soma zero. O book deve continuar no resultado com duas operações. Uma soma líquida não mede atividade nem demonstra ausência de movimentos. Na passagem para produção, combina contagens, identidades e montantes; explica qual destes indicadores sustenta cada critério de aceitação. O laboratório usa montantes inteiros em unidades menores e taxas inteiras em partes por milhão. scaledSum é um numerador com denominador 1000000; o programa não escolhe uma política de arredondamento. Por exemplo, 100 vezes 1200000 produz 120000000, que corresponde a 120 unidades menores após divisão conceptual. Os limites de 100 operações, 100 taxas, montante absoluto de 1000000 e taxa até 100000000 mantêm até a pior multiplicação dentro do intervalo inteiro usado. A escolha evita confundir um erro de junção com um erro de precisão numérica.
4. Executar e contrariar a fixture
A partir da raiz do projeto, executa python3 content/labs/pde-report-reconcile/run.py < content/labs/pde-report-reconcile/case.json. O código apresentado abaixo é o mesmo ficheiro executável. Usa apenas Python e SQLite da biblioteca padrão, cria uma base em memória e elimina o estado no fim. Não recebe SQL arbitrário, caminhos de base de dados, credenciais ou nomes de recursos cloud. O input é validado antes das consultas fixas. Prevê o resultado antes de executar: inputTrades e joinedRows são dois, missingTradeIds contém missing, multipliedTradeIds contém duplicated e reconciledReport é null. unsafePreview conserva o resultado da consulta problemática para permitir comparação. Não o uses como relatório aprovado. Faz uma cópia da fixture, remove uma taxa EUR e acrescenta uma GBP. Volta a executar e confirma que cada operação tem uma correspondência e que reconciledReport deixa de ser null. Depois altera o montante GBP para zero, remove a taxa GBP e observa que a falta continua a bloquear. Experimenta duas operações opostas no mesmo book e verifica a atividade com soma zero. Reordena operações e taxas: o resultado deve manter-se, porque o diagnóstico e os grupos são ordenados. Por fim, fornece uma lista vazia. O contrato passa sem operações e devolve uma lista vazia; só o contexto de negócio pode decidir se essa ausência é esperada ou um incidente de alimentação.
5. Avaliar o modelo sobre o conjunto certo
Um relatório recuperado pode alimentar um classificador de exceções. Antes de comparar métricas, fixa o modelo, a população, o corte e os labels. Para classificação em BigQuery ML, ML.EVALUATE sem tabela ou query de input devolve métricas geradas no treino. Uma chamada bem-sucedida desse tipo não é evidência de que o conjunto recuperado foi avaliado. Regista explicitamente o input e conserva a consulta que o selecionou. Imagina que o modelo foi treinado com o label is_exception e a tabela recuperada guarda a mesma informação em outcome. Uma query pode adaptar o nome, desde que o tipo e o significado correspondam. Acrescentar um label constante apenas para satisfazer o schema produz uma avaliação enganadora. Confirma também se os resultados reais já estão disponíveis: uma operação ainda sem desfecho não deve receber uma verdade inventada para acelerar a aprovação. Se o modelo contém TRANSFORM, o pré-processamento guardado é aplicado durante a avaliação e previsão. Inspeciona o novo wrapper para evitar transformar duas vezes o mesmo valor. Num teste guiado, documenta um exemplo de input bruto, a transformação esperada e a previsão observada. Uma diferença deve ser atribuída a uma mudança identificável. Conserva versões do wrapper, do modelo e do input, para que outro engenheiro consiga repetir a comparação sem depender da tua memória.
6. Ligar erros do modelo a uma decisão
Uma métrica global não substitui a decisão sobre erros toleráveis. Num exemplo fictício, a regra de avaliação atribui custo 500 a um falso negativo e 5 a um falso positivo. O limiar A produz oito falsos negativos e dez falsos positivos; B produz dois e cem. Calcula primeiro os totais: A custa 4050 e B custa 1500. Segundo esta regra, B é preferível, embora crie mais trabalho de investigação. A conclusão tem limites claros. O exemplo não demonstra que a equipa consegue tratar cem alertas, que os custos refletem perdas reais ou que a distribuição futura será igual. Leva essas perguntas ao responsável do processo. Pode ser necessário ajustar capacidade, escalonamento ou critério de aceitação. Não transformes uma conta didática numa recomendação financeira ou numa regra operacional de uma instituição. Para comparar limiares num classificador binário BigQuery ML, mantém modelo e input com labels constantes e varia o limiar de ML.CONFUSION_MATRIX. O parâmetro personalizado exige input e não se aplica à classificação multiclasse. Guarda a matriz, a definição de positivo e a regra de custo. Se mudarem simultaneamente a população, o modelo e o limiar, uma melhoria aparente deixa de identificar qual a alteração responsável. Prepara a decisão com evidência reproduzível e com uma explicação legível para quem vai assumir o trabalho gerado.
7. Recuperar o caminho autorizado do consumidor
A recuperação analítica inclui o caminho de acesso. Uma authorized view pode continuar a existir depois de a sua origem ter mudado. Desenha três relações: autorização da view na origem, acesso do consumidor à view e capacidade de criar query jobs no projeto de execução. Um erro numa relação não é resolvido concedendo indiscriminadamente leitura de todas as tabelas. Identifica primeiro a permissão e o recurso mencionados pelo erro. Num exercício de mudança, a view passa do dataset antigo para um novo dataset recuperado. Revê a projeção e os filtros, configura a autorização necessária na nova origem e verifica os grants do consumidor. Um teste bem-sucedido através da view não demonstra restrição efetiva se o mesmo utilizador também tem leitura direta da origem. Inclui na revisão os caminhos diretos e herdados, e regista qual a identidade utilizada em cada evidência de aceitação. Em BigQuery sharing, revogar uma subscrição impede consultar o linked dataset, mas o recurso permanece no projeto do subscriber. A presença na consola não prova que o acesso continua válido. Para um listing multi-region, a revogação também bloqueia consultas às réplicas secundárias ligadas. Não uses uma réplica como atalho de autorização. No handover, distingue problemas de disponibilidade, subscrição e permissão, atribuindo a investigação ao responsável que pode corrigir cada relação.
8. Produzir evidência para a passagem ao RUN
Conclui o exercício com um pequeno pacote de aceitação. Inclui o input original, o diagnóstico falhado, a alteração efetuada, o novo resultado, as versões utilizadas e a decisão tomada. Para o laboratório, explica por que motivo as contagens globais coincidiam e quais os identificadores afetados. Acrescenta uma observação sobre o que ainda não foi provado: completude da origem e validade temporal das taxas continuam fora do modelo. Num projeto real, junta os responsáveis de dados, APS, negócio e segurança para rever os critérios que lhes pertencem. Define quem investiga uma moeda sem referência, quem aprova uma taxa substituta, quem valida a população e quem gere a autorização de partilha. Um incidente às duas da manhã exige instruções executáveis, não apenas uma apresentação do projeto. O RUN precisa de saber onde encontrar evidência, quais os sinais de bloqueio e a quem escalar uma discrepância antes do fecho. Entrega também uma alternativa operacional concreta: conservar a última publicação aprovada enquanto o novo conjunto é investigado, se essa opção for aceite pelo negócio e pelos requisitos de atualidade. Não suponhas que dados antigos são sempre aceitáveis. Regista a tolerância e a decisão. Resume a aprendizagem em três verificações distintas: as operações têm as correspondências certas, o modelo foi avaliado sobre o conjunto declarado e o consumidor só acede através dos caminhos aprovados.
"""Original offline exercise: reconcile join cardinality with fixed in-memory SQL."""
import json
import re
import sqlite3
import sys
def exact_keys(row, keys):
if not isinstance(row, dict) or set(row) != set(keys):
raise ValueError('Unexpected object fields')
def integer(value, low, high):
if type(value) is not int or not low <= value <= high:
raise ValueError('Integer outside contract')
def token(value, currency=False):
pattern = r'[A-Z]{3}' if currency else r'[A-Za-z0-9_-]{1,48}'
if not isinstance(value, str) or re.fullmatch(pattern, value) is None:
raise ValueError('Invalid identifier')
def reconcile(data):
exact_keys(data, ['trades', 'rates'])
for key in ['trades', 'rates']:
if not isinstance(data[key], list) or len(data[key]) > 100:
raise ValueError('Expected at most100rows per collection')
seen = set()
for trade in data['trades']:
exact_keys(trade, ['id', 'book', 'currency', 'amountMinor'])
token(trade['id']); token(trade['book']); token(trade['currency'], True)
integer(trade['amountMinor'], -1000000, 1000000)
if trade['id'] in seen:
raise ValueError('Duplicate trade identifier')
seen.add(trade['id'])
for rate in data['rates']:
exact_keys(rate, ['currency', 'partsPerMillion'])
token(rate['currency'], True)
integer(rate['partsPerMillion'], 1, 100000000)
# Even100x100matches with products of10^14 stay below signed64-bit SUM.
db = sqlite3.connect(':memory:')
db.row_factory = sqlite3.Row
try:
db.executescript('''
CREATE TABLE trades(id TEXT PRIMARY KEY,book TEXT NOT NULL,
currency TEXT NOT NULL,amount INTEGER NOT NULL);
CREATE TABLE rates(currency TEXT NOT NULL,ppm INTEGER NOT NULL);
''')
db.executemany('INSERT INTO trades VALUES(?,?,?,?)',
[(t['id'],t['book'],t['currency'],t['amountMinor']) for t in data['trades']])
db.executemany('INSERT INTO rates VALUES(?,?)',
[(r['currency'],r['partsPerMillion']) for r in data['rates']])
diagnostics = [dict(r) for r in db.execute('''
SELECT t.id,COUNT(*) AS leftRows,COUNT(r.currency) AS matches,
SUM(t.amount*r.ppm) AS scaledSum
FROM trades t LEFT JOIN rates r ON t.currency=r.currency
GROUP BY t.id ORDER BY t.id
''')]
preview = [dict(r) for r in db.execute('''
SELECT t.book,COUNT(*) AS joinedRows,SUM(t.amount*r.ppm) AS scaledSum
FROM trades t INNER JOIN rates r ON t.currency=r.currency
GROUP BY t.book ORDER BY t.book
''')]
missing = [r['id'] for r in diagnostics if r['matches'] == 0]
duplicated = [r['id'] for r in diagnostics if r['matches'] > 1]
passed = not missing and not duplicated
return dict(sqliteVersion=sqlite3.sqlite_version,inputTrades=len(data['trades']),
joinedRows=sum(r['matches'] for r in diagnostics),
diagnostics=diagnostics,missingTradeIds=missing,
multipliedTradeIds=duplicated,unsafePreview=preview,
cardinalityPassed=passed,reconciledReport=preview if passed else None,
scaleDenominator=1000000,stateLifetime='one invocation; memory only',
sourceCompletenessProven=False,rateDateValidityProven=False,
cloudSemanticsValidated=False,productionPublicationApproved=False)
finally:
db.close()
def unique_object(pairs):
row = {}
for key, value in pairs:
if key in row:
raise ValueError('Duplicate JSON key')
row[key] = value
return row
if __name__ == '__main__':
try:
raw = sys.stdin.buffer.read(2000001)
if len(raw) > 2000000:
raise ValueError('Input exceeds2MB')
result = reconcile(json.loads(raw, object_pairs_hook=unique_object))
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
except (ValueError, TypeError, UnicodeError, RecursionError) as exc:
print(json.dumps({'error': str(exc)}), file=sys.stderr)
sys.exit(2)
Fecho fictício: duas linhas no relatório escondem uma operação perdida e outra multiplicada pela referência.
Armadilhas comuns
Aceitar contagens iguais; usar DISTINCT como reparação universal; converter NULL em zero; apresentar métricas de treino como avaliação dos dados recuperados; confundir recurso visível com autorização.
Tópicos relacionados: Cardinalidade e reconciliação · BigQuery ML e labels · Authorized views e subscrições
Reconcilia identidades antes de somar, fixa o input antes de avaliar e verifica cada relação de autorização antes da passagem.
Referência: Professional Data Engineer standard exam guide · Current linked standard guide (document title v4.2); edition date unconfirmed (2026-09-30 inspection)