# Aditividade — qual agregação a métrica aceita

## Conteúdo

- 1. Por que este arquivo existe
- 2. As quatro classes
- 3. A tabela de decisão
- 4. O caso que mais dá errado: razão
- 5. Semi-aditiva: a dimensão importa
- 6. Onde ler a classe da métrica
- 7. Checklist antes de escrever qualquer `dataSource`

Toda expressão de coluna no Horus carrega um sufixo de agregação
(`<colId>:SUM`, `:AVG`, `:COUNT`, `:MIN`, `:MAX`, `:AGP`, `:NONE`). Escolher o
sufixo errado não gera erro: gera um número plausível e errado, que vai para o
dashboard do cliente e só é descoberto quando alguém confere na mão.

Leia ANTES de escrever qualquer `dataSource`, `column`, `fieldValue` ou
`expression`.

## 1. Por que este arquivo existe

A `lumo-scope` já define aditividade com rigor: a ficha de métrica tem um campo
`aditividade:` e há checklist adversarial sobre isso
(`lumo-scope:references/validacao-adversarial.md`,
`lumo-scope:references/calibracao-qualidade.md`).

O problema é de **travessia**: a skill que define a regra não é a que escreve a
expressão. Sem este arquivo, `aditividade` vive só no bundle de escopo, enquanto
as recipes de [`widget-recipes.md`](widget-recipes.md) usam `:SUM` em todo
exemplo. Copiar a recipe de KPI apontando para "Ticket Médio" produz a soma dos
tickets médios, que não significa nada.

## 2. As quatro classes

A taxonomia clássica de data warehouse tem três classes. Este arquivo usa
**quatro**, porque a terceira esconde dois casos que pedem tratamento oposto.

| Classe | Soma no tempo? | Soma nas demais dimensões? | Exemplos |
|---|---|---|---|
| **Aditiva** | sim | sim | receita, custo, quantidade vendida, número de notas |
| **Semi-aditiva** | **não** | sim | saldo, estoque, carteira em aberto, headcount |
| **Razão** (não-aditiva) | não | não | ticket médio, margem %, conversão, inadimplência % |
| **Intensiva** (não-aditiva) | não | não | temperatura, score, nota, velocidade média, prazo médio |

Razão e intensiva são as duas **não-aditivas**, e é aí que mora o erro caro:
elas se parecem, e a agregação certa de uma é errada para a outra.

## 3. A tabela de decisão

| Classe | Expressão no Horus | Observação |
|---|---|---|
| Aditiva | `:SUM` | o caso simples |
| Semi-aditiva | `:LAST` no eixo de tempo, `:SUM` nas demais | ver §5 |
| **Razão** | **expression de aplicação**, `X<id>:EXPR`, com a fórmula `SUM(num)/COUNT(den)` dentro dela. Nunca um `:AVG` sobre coluna | ver §4 |
| Intensiva | `:AVG` | média aritmética é legítima aqui |
| Chave, código, identificador | `:AGP` (agrupa) ou `:NONE` | nunca agregue identificador |

## 4. O caso que mais dá errado: razão

Ticket médio, margem percentual, taxa de conversão e inadimplência são
**quocientes**. Um quociente agregado não é o quociente dos agregados:

```
ticket médio da semana  ≠  soma dos 7 tickets médios diários
ticket médio da rede    ≠  média dos tickets médios das 5 lojas
```

A segunda linha é a mais traiçoeira, porque `:AVG` **parece** a resposta certa.
Não é: uma loja que vendeu 10 pedidos pesa igual a uma que vendeu 10.000, e o
resultado não corresponde a ticket médio nenhum.

**A regra:** a métrica de razão guarda os **componentes aditivos** e divide
depois de agregar, no nível que está sendo exibido.

No Horus isso tem uma forma concreta: a métrica de razão vira **expression de
aplicação**, e o widget aponta para ela com o sufixo `:EXPR`. A expression é
avaliada no nível de agrupamento do widget, então o `SUM` e o `COUNT` agregam no
nível exibido e a divisão acontece depois, sozinha.

A expression carrega os componentes e a divisão:

```
SUM([fact]."VALOR_LIQUIDO") / COUNT(DISTINCT [fact]."NUNOTA")
```

e o widget aponta para ela:

```yaml
dataSource: "X1234:EXPR"     # certo: a expression agrega e divide no nível exibido
dataSource: "<col>:AVG"      # errado: agrega o quociente já calculado
```

> **A forma exata do bloco `expressions:` do app vem de `lumo scaffold app` e de
> `lumo info app:<id> --full`**, não deste arquivo. Esta skill é dona da regra de
> agregação, não do schema do YAML.

O erro é fácil de cometer justamente onde a regra está escrita: um teste de
reconciliação que consulta `ticket_medio:AVG` contradiz as notas da própria
ficha ("nunca tirar média das médias") sem que ninguém perceba, porque o comando
roda e devolve um número.

Isso vale igual para margem (`SUM(lucro)/SUM(receita)`, nunca `AVG(margem)`) e
para qualquer percentual.

**Nota de rigor, contra uma simplificação comum.** Referências de dataviz que
derivam agregação de tipo semântico costumam mapear "medida intensiva → média",
e o flint-chart da Microsoft faz exatamente isso em
`docs/design-semantics.md` §5.2, listando Percentage e Price sob `average`. Para
temperatura e score o mapeamento está certo; **para razão, está errado**, pelo
motivo acima. Esta skill mantém as duas classes separadas de propósito.

## 4b. O que importar de referência externa, e o que não

A regra acima veio de uma referência externa de dataviz, e a mesma leitura
oferecia junto um monte de coisa que não serve.

**O Horus já abstrai o gráfico.** O widget é declarado em DML própria
(`globalType`, `dataSource`, `chartProperties`) e quem renderiza é o Highcharts
dentro do GridStack. Bibliotecas como o flint são **compiladores de gráfico**:
elas resolvem, por conta própria, problemas que aqui já estão resolvidos uma
camada abaixo.

O critério de corte, aplicado antes de trazer qualquer coisa de fora:

> **Se a regra vira uma frase que um analista fala numa reunião, ela viaja. Se
> ela só faz sentido dentro de um renderizador, ela fica lá.**

| Viaja | Não viaja |
|---|---|
| "não some ticket médio" | largura de banda em px, expoente de elasticidade |
| "barra parte do zero" | `baseSize`, `canvasSize`, multiplicador de stretch |
| "cor divergente só quando o dado cruza o ponto médio" | registry de paleta por backend |
| "meta contra realizado não é linha × linha" | representação intermediária de canal |
| "mês e dia da semana têm ordem canônica" | como o motor filtra overflow de categoria |

Importar a segunda coluna produz regra que ninguém consegue aplicar (o Horus não
expõe esses parâmetros) e documentação que envelhece junto com a lib de outro
projeto. Importar a primeira produz regra que o cliente entende quando você
explica por que o número mudou.

## 5. Semi-aditiva: a dimensão importa

Saldo, estoque e headcount somam **entre** entidades e não somam **ao longo do
tempo**:

```
estoque total hoje       = soma do estoque de cada filial hoje        ✔ SUM
estoque do mês           ≠ soma do estoque de cada dia do mês         ✘
estoque do mês           = estoque do último dia (ou a média diária)  ✔ LAST / AVG
```

Escolher entre `LAST` e `AVG` no tempo é decisão de negócio e pertence à ficha
da métrica, não ao widget: "estoque de fechamento" pede `LAST`, "estoque médio
do período" pede `AVG`. Se a ficha não diz, isso é incógnita e volta para a
`lumo-scope`, não vira chute aqui.

## 6. Onde ler a classe da métrica

Campo `aditividade:` no frontmatter da ficha, em `metricas/<nome>.md` do bundle
de escopo. Exemplo real do gabarito canônico
(`lumo-scope:references/exemplo-canonico/escopo/metricas/ticket-medio.md`):

```yaml
aditividade: nao-aditiva
formula_lumo: 'SUM([fact_pedido_item]."VALOR_LIQUIDO") / COUNT(DISTINCT [fact_pedido_item]."NUNOTA")'
```

**Se a ficha não declara `aditividade`, pare e pergunte.** Não deduza do nome:
"Receita Média por Cliente" é razão, "Receita" é aditiva, e as duas começam
igual. Deduzir é como o erro entra.

## 7. Checklist antes de escrever qualquer `dataSource`

1. Li o campo `aditividade:` da ficha desta métrica?
2. Se é **razão**, o widget recompõe dos componentes em vez de agregar o
   quociente?
3. Se é **semi-aditiva**, a expressão usa `LAST`/`AVG` no eixo de tempo e `SUM`
   nas demais dimensões?
4. Se é **intensiva**, usei `:AVG` e não `:SUM`?
5. A linha de total ou subtotal da `Table`/`Matrix` respeita a mesma regra? (Um
   rodapé `Total` somando uma coluna de percentual é o mesmo erro, num lugar em
   que ninguém olha.)
6. O eixo temporal de uma métrica semi-aditiva não está somando por engano
   porque o widget agrupa por mês?

Nenhum desses itens é estético. Errar qualquer um produz número errado com cara
de certo.
