Nomear a população e a unidade
No piloto fictício Íris, há sessenta pessoas elegíveis na mesma janela. Vinte e quatro iniciam pelo menos uma tarefa e dezoito concluem pelo menos uma. A adoção definida como início é 24/60=40%; conclusão entre iniciadores é 18/24=75%; cobertura de conclusão entre elegíveis é 18/60=30%. Nenhuma destas medidas deve ser apresentada sem denominador. Uma pessoa inicia e conclui duas operações, pelo que existem 25 operações iniciadas e 19 concluídas. A conclusão por operação é 19/25=76%, diferente da conclusão por pessoa. O export contém 51 linhas, das quais sete repetem exatamente eventos anteriores. Existem 44 eventos únicos, não 44 utilizadores. Estabelece o significado de event_id, user_id e operation_id antes de usar contagens do dashboard. Os valores deste laboratório são dados inventados, não estatísticas de um banco nem evidência de que uma funcionalidade causa adoção ou cria valor.
Verificar a recolha antes da conclusão
O contrato do exercício permite repetições apenas se identidade e conteúdo forem iguais. Duas linhas com o mesmo event_id e estados diferentes devem produzir erro, não uma escolha silenciosa da última linha. O script também rejeita pessoa fora da população declarada, tipo desconhecido, identificador vazio e operação atribuída a duas pessoas. A janela sintética é completa e contém todos os inícios; por isso, uma conclusão sem início é rejeitada. Num export real com janela parcial ou eventos atrasados, seria necessário outro contrato de recolha e reconciliação. O código não tenta resolver esses casos. Não transforma duração vazia em zero: no exemplo 10, vazio e 20, a média das duas observações é quinze e a cobertura é dois terços. Essa média parcial não descreve automaticamente as tarefas sem medição. Conserva a lacuna e investiga se a perda de eventos depende do resultado ou do tipo de utilizador.
Interpretar benefício e experiência
No caso Delta, o mesmo conjunto de tarefas exige 180 minutos dos operadores e 42 de APS antes; depois exige 140 e 60. A redução total é 222−200=22 minutos, não quarenta. Outro registo mostra quatro erros em 80 operações e nove em 90: 5% e 10%, uma subida de cinco pontos percentuais e duplicação da taxa. Estes cálculos não isolam causas. Para uma experiência, define antecipadamente hipótese, métrica principal, população, janela, recolha e condições de interrupção com responsáveis capazes de atuar. A orientação técnica GOV.UK sobre A/B ajuda a preparar desenho e QA, mas o seu registo interno e ferramentas não são exigências bancárias. Uma equipa que usa A de manhã e B à tarde com tarefas diferentes não criou aleatorização por usar duas variantes. Mantém análises posteriores identificadas como exploratórias. Não compares retenção de trinta dias de uma coorte observada durante apenas sete como se a janela estivesse completa.
Laboratório Íris reproduzível
Copia o código completo apresentado nesta aula para um ficheiro local e executa-o com Python 3.13. Usa apenas a biblioteca padrão e dados gerados em memória; não precisa de acesso à plataforma nem a sistemas bancários. Antes de executar, prevê pessoas, operações e taxas. O resultado inclui vinte verificações, as contagens e um exemplo de duração ausente. Depois altera uma repetição para ter conteúdo diferente e observa a rejeição prevista no código de teste. Experimenta também uma população vazia: a taxa fica indefinida, representada por null no JSON, em vez de zero. Explica por escrito por que motivo 76% por operação não responde à pergunta sobre pessoas, e por que motivo a média parcial não prova cobertura completa. O código foi executado localmente para verificar estes dados sintéticos. Não faz teste estatístico, não demonstra causalidade, não valida um pipeline real e não substitui desenho de experiência com apoio analítico adequado.
"""Original synthetic teaching model. No files, network, or real user data.
PT: copiar este código completo e executar com Python 3.13.
EN: copy this complete script and run with Python 3.13.
Rates describe this fixture; they establish no causal or statistical inference.
"""
import csv
import io
import json
import math
FIELDS = ['event_id', 'user_id', 'operation_id', 'kind']
ELIGIBLE = {f'u{i:02}' for i in range(1, 61)}
def export_fixture():
rows = []
for i in range(1, 25):
rows.append([f's{i:02}', f'u{i:02}', f'op{i:02}', 'start'])
for i in range(1, 19):
rows.append([f'c{i:02}', f'u{i:02}', f'op{i:02}', 'complete'])
rows += [['s25', 'u01', 'op25', 'start'],
['c25', 'u01', 'op25', 'complete']]
rows += [rows[i][:] for i in [0, 1, 2, 3, 24, 25, 26]]
stream = io.StringIO()
writer = csv.writer(stream)
writer.writerow(FIELDS)
writer.writerows(rows)
return stream.getvalue()
def parse(text):
reader = csv.DictReader(io.StringIO(text))
if reader.fieldnames != FIELDS:
raise ValueError('unexpected columns')
return list(reader)
def ratio(numerator, denominator):
return None if denominator == 0 else numerator / denominator
def summarize(rows, eligible):
events = {}
for row in rows:
if set(row) != set(FIELDS) or any(not row[k] for k in FIELDS):
raise ValueError('missing or extra field')
if row['user_id'] not in eligible:
raise ValueError('user outside declared population')
if row['kind'] not in {'start', 'complete'}:
raise ValueError('unknown event kind')
key = row['event_id']
if key in events and events[key] != row:
raise ValueError('conflicting event identity')
events[key] = row.copy()
starts, completes, owners = set(), set(), {}
for row in events.values():
op, user = row['operation_id'], row['user_id']
if op in owners and owners[op] != user:
raise ValueError('conflicting operation owner')
owners[op] = user
(starts if row['kind'] == 'start' else completes).add(op)
if not completes <= starts:
raise ValueError('completion without start in declared complete window')
starters = {owners[op] for op in starts}
completers = {owners[op] for op in completes}
return dict(rows=len(rows), unique_events=len(events),
duplicate_rows=len(rows)-len(events),
starters=len(starters), completers=len(completers),
started_operations=len(starts), completed_operations=len(completes),
adoption=ratio(len(starters), len(eligible)),
completion_among_starters=ratio(len(completers), len(starters)),
completion_among_eligible=ratio(len(completers), len(eligible)),
operation_completion=ratio(len(completes), len(starts)))
def durations(values):
observed = [float(v) for v in values if v != '']
if any(v < 0 or not math.isfinite(v) for v in observed):
raise ValueError('invalid observed duration')
return dict(observed=len(observed), missing=len(values)-len(observed),
mean=ratio(sum(observed), len(observed)),
coverage=ratio(len(observed), len(values)))
checks = []
def check(name, condition):
if not condition:
raise AssertionError(name)
checks.append(name)
def rejects(name, action):
try:
action()
except ValueError:
checks.append(name)
else:
raise AssertionError(name)
rows = parse(export_fixture())
r = summarize(rows, ELIGIBLE)
check('row identities', (r['rows'], r['unique_events'], r['duplicate_rows']) == (51, 44, 7))
check('people', (r['starters'], r['completers']) == (24, 18))
check('operations', (r['started_operations'], r['completed_operations']) == (25, 19))
check('adoption', r['adoption'] == .4)
check('completion among starters', r['completion_among_starters'] == .75)
check('completion among eligible', r['completion_among_eligible'] == .3)
check('operation completion', r['operation_completion'] == .76)
check('input order is irrelevant', summarize(list(reversed(rows)), ELIGIBLE) == r)
extra = summarize(rows + [rows[0].copy()], ELIGIBLE)
check('extra duplicate changes only raw and duplicate counts',
extra == {**r, 'rows': 52, 'duplicate_rows': 8})
rejects('conflicting event', lambda: summarize(rows + [{**rows[0], 'kind':'complete'}], ELIGIBLE))
rejects('unknown user', lambda: summarize([{**rows[0], 'user_id':'u99'}], ELIGIBLE))
rejects('unknown kind', lambda: summarize([{**rows[0], 'kind':'clicked'}], ELIGIBLE))
rejects('orphan completion', lambda: summarize([rows[24]], ELIGIBLE))
rejects('operation owner mismatch', lambda: summarize(rows + [dict(event_id='x',user_id='u02',operation_id='op01',kind='complete')], ELIGIBLE))
rejects('blank identity', lambda: summarize([{**rows[0], 'event_id':''}], ELIGIBLE))
check('empty population is undefined not zero', summarize([], set())['adoption'] is None)
d = durations(['10', '', '20'])
check('missing differs from zero', d == dict(observed=2,missing=1,mean=15,coverage=2/3))
rejects('negative duration', lambda: durations(['-1']))
rejects('nonfinite duration', lambda: durations(['nan']))
check('net effort includes support', (180+42)-(140+60) == 22)
print(json.dumps(dict(python_scope='3.13 standard library',checks_passed=len(checks),
checks=checks,fixture=r,duration_example=d,
limitation='Synthetic arithmetic only; no real users, causal inference or production experiment.'), indent=2))
24/60 mede adoção; 18/24 mede conclusão entre iniciadores; 19/25 mede conclusão por operação. São perguntas diferentes sobre a mesma amostra.
Armadilhas comuns
Linhas como pessoas; duplicação como adoção; ausente como zero; antes/depois como causa; métrica escolhida após o resultado como hipótese original.
Tópicos relacionados: Métricas de produto · Qualidade dos dados · Experiências e decisão
A confiança no resultado começa pela definição da medida e pela qualidade dos dados, e termina numa conclusão proporcional à evidência.
Referência: How to set performance metrics for your service · Scrum Guide November2020; EBM May2024; primary product practice reviewed 2026-09-30