fx

DAX

Cookbook DAX

15 casos de uso reais com IA e DAX (desafio, prompt, medidas prontas para a DAX Query View e impacto ou resultado num modelo real) e o padrão de fuso horário e configuração por localidade.

Cada caso traz o desafio, o prompt usado com a IA, as medidas corrigidas e o impacto relatado (nos casos 11 a 15, o resultado num modelo real de teste). O bloco de código vai inteiro para a DAX Query View: o EVALUATE do fim mostra o resultado, e Update model with changes grava as medidas no modelo.

Modelo usado nos exemplos

TabelaColunas usadasRelacionamento
TabDimCalendariogerada por DataHora.Calendario, marcada como Tabela de Datas1 → * com TabFatVenda[Data], TabFatMeta[Data] e TabFatConsumo[Data]
TabFatVendaData, CodigoPedido, CodigoCliente, CodigoProduto, CodigoLoja, ValorVenda, CustoVenda* → 1 com TabDimCliente, TabDimProduto e TabDimLoja
TabDimCliente · TabDimProduto · TabDimLojaCodigoCliente, NomeCliente · CodigoProduto, NomeProduto · CodigoLoja, NomeLojalado 1
TabFatMetaData (1º dia do mês), ValorMeta* → 1 com o calendário
TabFatEstoque · TabFatConsumoCodigoProduto, QuantidadeAtual · CodigoProduto, Data, QuantidadeConsumida* → 1 com TabDimProduto
TabFatAssinaturaCodigoCliente, DataInicio, DataFim (em branco se ativo)sem relacionamento com o calendário
TabFatFunil · TabDimEtapauma linha por oportunidade e etapa alcançada · CodigoEtapa, Ordem, NomeEtapa* → 1
TabIntAtualizacaoData da Atualizaçãosem relacionamento (ver Padrão: fuso horário)
TabFatTransacaoData da Emissão, Data do Vencimento, Data da Transação, Transação (Recebimento/Pagamento), Valor da Transação (recebimento positivo, pagamento negativo)* → 1 com o calendário por Data da Transação (ativo); Data da Emissão e Data do Vencimento inativos
TabDimFeriadoData (uma linha por feriado)sem relacionamento
TabFatColaboradorMatrícula, Entrada, Saída (em branco se ativo)* → 1 com o calendário por Entrada (ativo) e por Saída (inativo)
TabFatNPSData, CodigoCliente, Nota (0 a 10), uma linha por resposta* → 1 com o calendário e com TabDimCliente

1. Vendas acumuladas no ano com comparativo AA

Setor: Varejo · Complexidade: Intermediária

Desafio: comparar as vendas acumuladas no ano com o mesmo período do ano anterior, com variação percentual e farol.

Prompt:

"Tenho TabFatVenda (Data, ValorVenda) e TabDimCalendario[Data Referência] marcada como Tabela de Datas. Crie as medidas no padrão AV/AA/Δ%/Δi: vendas acumuladas no ano, mesmo período do ano anterior, variação percentual e farol. Use VAR e DIVIDE e não mostre acumulado em meses sem venda."

DEFINE
    MEASURE TabFatVenda[Total de Vendas] =
        SUM ( TabFatVenda[ValorVenda] )

    MEASURE TabFatVenda[Total de Vendas (ytd) AV] =
        VAR vUltimaVenda = CALCULATE ( MAX ( TabFatVenda[Data] ), REMOVEFILTERS () )
        RETURN
            -- Meses depois da última venda ficam em branco, em vez de repetir o acumulado
            IF (
                MIN ( TabDimCalendario[Data Referência] ) <= vUltimaVenda,
                TOTALYTD ( [Total de Vendas], TabDimCalendario[Data Referência] )
            )

    MEASURE TabFatVenda[Total de Vendas (ytd) AA] =
        CALCULATE (
            TOTALYTD ( [Total de Vendas], TabDimCalendario[Data Referência] ),
            SAMEPERIODLASTYEAR ( TabDimCalendario[Data Referência] )
        )

    MEASURE TabFatVenda[Δ% Total de Vendas (ytd)] =
        VAR vAV = [Total de Vendas (ytd) AV]
        VAR vAA = [Total de Vendas (ytd) AA]
        RETURN
            IF ( NOT ISBLANK ( vAV ), DIVIDE ( vAV - vAA, vAA ) )

    MEASURE TabFatVenda[Δi Total de Vendas (ytd)] =
        VAR vVariacao = [Δ% Total de Vendas (ytd)]
        RETURN
            SWITCH (
                TRUE (),
                ISBLANK ( vVariacao ), BLANK (),
                vVariacao > 0, "✔️",
                vVariacao = 0, "⚠️",
                "❗"
            )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimCalendario[Ano],
    TabDimCalendario[Mês #],
    "AV", [Total de Vendas (ytd) AV],
    "AA", [Total de Vendas (ytd) AA],
    "Δ%", [Δ% Total de Vendas (ytd)],
    "Δi", [Δi Total de Vendas (ytd)]
)
ORDER BY TabDimCalendario[Ano], TabDimCalendario[Mês #]
  • 💡 Formate Δ% como percentual na medida. Um número continua ordenável e serve de base para formatação condicional; o texto "12,0% ▲" do prompt original não.
  • 💡 As UDFs Delta.Percentual e Delta.Info da Code Library fazem o mesmo cálculo e o mesmo símbolo.
  • ⚠️ No mês e no ano correntes, o AV vai até a última venda e o AA cobre o período inteiro do ano anterior. Para comparar dias iguais, limite o AA a EDATE ( última venda, -12 ).

Impacto relatado: redução de 3h/semana em análises manuais; identificação de queda de 12% nas vendas de março com ação corretiva em 48h; reuniões 40% mais rápidas com os indicadores visuais.


2. Análise RFM de clientes

Setor: E-commerce · Complexidade: Avançada

Desafio: classificar clientes por Recência, Frequência e Monetário para segmentar campanhas.

Prompt:

"Tenho TabFatVenda (Data, CodigoPedido, CodigoCliente, ValorVenda) e TabDimCliente. Crie medidas RFM: recência em dias até a última venda do modelo, número de pedidos e total gasto. Classifique cada cliente em Campeão (recência ≤ 30, 3 ou mais pedidos e total acima da mediana dos clientes), Em Risco (recência ≥ 60), Novo (1 pedido) ou Regular."

DEFINE
    MEASURE TabFatVenda[Total de Vendas] =
        SUM ( TabFatVenda[ValorVenda] )

    MEASURE TabFatVenda[# Pedidos] =
        DISTINCTCOUNT ( TabFatVenda[CodigoPedido] )

    MEASURE TabFatVenda[Recência (dias)] =
        VAR vUltimaCompra = MAX ( TabFatVenda[Data] )
        -- Data de referência: última venda do modelo, não TODAY (ver Padrão: fuso horário)
        VAR vDataReferencia = CALCULATE ( MAX ( TabFatVenda[Data] ), REMOVEFILTERS () )
        RETURN
            IF ( NOT ISBLANK ( vUltimaCompra ), DATEDIFF ( vUltimaCompra, vDataReferencia, DAY ) )

    MEASURE TabFatVenda[Segmento RFM] =
        VAR vRecencia = [Recência (dias)]
        VAR vFrequencia = [# Pedidos]
        VAR vMonetario = [Total de Vendas]
        -- Mediana do total gasto POR CLIENTE, entre todos os clientes com compra
        VAR vMediana =
            CALCULATE (
                MEDIANX (
                    FILTER ( VALUES ( TabDimCliente[CodigoCliente] ), NOT ISBLANK ( [Total de Vendas] ) ),
                    [Total de Vendas]
                ),
                REMOVEFILTERS ( TabDimCliente )
            )
        RETURN
            IF (
                HASONEVALUE ( TabDimCliente[CodigoCliente] ) && NOT ISBLANK ( vMonetario ),
                SWITCH (
                    TRUE (),
                    vRecencia <= 30 && vFrequencia >= 3 && vMonetario > vMediana, "Campeão",
                    vRecencia >= 60, "Em Risco",
                    vFrequencia = 1, "Novo",
                    "Regular"
                )
            )

EVALUATE
TOPN (
    20,
    SUMMARIZECOLUMNS (
        TabDimCliente[CodigoCliente],
        TabDimCliente[NomeCliente],
        "Recência", [Recência (dias)],
        "Frequência", [# Pedidos],
        "Monetário", [Total de Vendas],
        "Segmento", [Segmento RFM]
    ),
    [Monetário], DESC
)
ORDER BY [Monetário] DESC
  • 💡 Frequência e monetário respeitam o período filtrado; a recência sempre mede até a última venda do modelo.
  • ⚠️ A mediana é recalculada para cada cliente do visual. Com dezenas de milhares de clientes, grave mediana e segmento na origem.
  • ⚠️ Medida não vira eixo nem segmentação. Para filtrar por segmento, grave a classificação como coluna de TabDimCliente na origem (SQL ou Power Query).
  • ⚠️ A versão anterior usava uma coluna calculada na tabela de vendas: MAX(TabVendas[Data]) sem CALCULATE devolvia a última data da tabela inteira, e o CALCULATE com EARLIER mantinha o filtro da linha atual, por isso a frequência dava 1.

Impacto relatado: segmentação de 45.000 clientes automatizada (antes: 2 semanas no Excel); campanha para "Em Risco" recuperou R$ 280.000 em 30 dias; recompra dos "Campeões" subiu de 23% para 41%.


3. Meta × realizado com farol

Setor: Saúde e Bem-Estar · Complexidade: Básica

Desafio: exibir o atingimento da meta em verde (≥ 90%), amarelo (75% a 89%) ou vermelho (< 75%).

Prompt:

"Tenho TabFatVenda (Data, ValorVenda) e TabFatMeta (Data, ValorMeta), ambas ligadas ao calendário. Crie o percentual de atingimento da meta e um farol 🟢 🟡 🔴 com o percentual ao lado. Sem meta, não mostre nada."

DEFINE
    MEASURE TabFatVenda[Total de Vendas] =
        SUM ( TabFatVenda[ValorVenda] )

    MEASURE TabFatMeta[Total da Meta] =
        SUM ( TabFatMeta[ValorMeta] )

    MEASURE TabFatMeta[Atingimento da Meta (%)] =
        VAR vMeta = [Total da Meta]
        VAR vUltimaVenda = CALCULATE ( MAX ( TabFatVenda[Data] ), REMOVEFILTERS () )
        RETURN
            -- Sem meta ou período ainda sem vendas: BLANK. Com meta e sem venda: 0%
            IF (
                NOT ISBLANK ( vMeta ) && MIN ( TabDimCalendario[Data Referência] ) <= vUltimaVenda,
                DIVIDE ( [Total de Vendas] + 0, vMeta )
            )

    MEASURE TabFatMeta[Farol da Meta] =
        VAR vAtingimento = [Atingimento da Meta (%)]
        RETURN
            IF (
                NOT ISBLANK ( vAtingimento ),
                SWITCH (
                    TRUE (),
                    vAtingimento >= 0.90, "🟢",
                    vAtingimento >= 0.75, "🟡",
                    "🔴"
                ) & " " & FORMAT ( vAtingimento, "0.0%" )
            )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimCalendario[Ano/Mês],
    "Vendas", [Total de Vendas],
    "Meta", [Total da Meta],
    "Atingimento", [Atingimento da Meta (%)],
    "Farol", [Farol da Meta]
)
ORDER BY TabDimCalendario[Ano/Mês]
  • ⚠️ A meta é mensal: num visual por dia, ela aparece só no dia 1. Use o farol em mês, trimestre ou ano.
  • 💡 Para colorir o número em vez de concatenar o emoji, use Atingimento da Meta (%) com formatação condicional por regras.

Impacto relatado: reuniões de status caíram de 45 para 15 minutos; a visibilidade imediata acelerou a ação nas regiões no vermelho.


4. Previsão de ruptura de estoque

Setor: Manufatura · Complexidade: Avançada

Desafio: apontar produtos com risco de ruptura nos próximos 30 dias, pelo consumo médio dos últimos 90 dias.

Prompt:

"Tenho TabFatEstoque (CodigoProduto, QuantidadeAtual) e TabFatConsumo (CodigoProduto, Data, QuantidadeConsumida). Crie: consumo médio diário dos 90 dias até o último consumo registrado, dias de cobertura e alerta de ruptura quando a cobertura for menor que 30 dias. Use DATESINPERIOD."

DEFINE
    MEASURE TabFatConsumo[Consumo Médio Diário (90d)] =
        -- Janela de 90 dias terminando no último consumo do modelo (de qualquer produto),
        -- e não no fim do calendário, que pode estar no futuro
        VAR vFim = CALCULATE ( MAX ( TabFatConsumo[Data] ), REMOVEFILTERS () )
        VAR vConsumo =
            CALCULATE (
                SUM ( TabFatConsumo[QuantidadeConsumida] ),
                DATESINPERIOD ( TabDimCalendario[Data Referência], vFim, -90, DAY )
            )
        RETURN
            DIVIDE ( vConsumo, 90 )

    MEASURE TabFatEstoque[Estoque Atual] =
        SUM ( TabFatEstoque[QuantidadeAtual] )

    MEASURE TabFatEstoque[Dias de Cobertura] =
        -- BLANK sem consumo, em vez de um 9999 artificial
        DIVIDE ( [Estoque Atual], [Consumo Médio Diário (90d)] )

    MEASURE TabFatEstoque[Alerta de Ruptura] =
        SWITCH (
            TRUE (),
            ISBLANK ( [Consumo Médio Diário (90d)] ), "Sem consumo em 90 dias",
            [Dias de Cobertura] < 30, "⚠️ Risco de ruptura",
            "✔️ Estoque OK"
        )

EVALUATE
FILTER (
    SUMMARIZECOLUMNS (
        TabDimProduto[NomeProduto],
        "Estoque", [Estoque Atual],
        "Consumo por Dia", [Consumo Médio Diário (90d)],
        "Cobertura (dias)", [Dias de Cobertura],
        "Alerta", [Alerta de Ruptura]
    ),
    [Alerta] = "⚠️ Risco de ruptura"
)
ORDER BY [Cobertura (dias)]
  • 💡 Use o alerta por produto. No total, a cobertura soma estoques de itens diferentes e perde o sentido.
  • 💡 Estoque zerado com consumo dá cobertura 0 e entra como risco.

Impacto relatado: identificação proativa de 47 SKUs em risco; redução de 35% nas rupturas no trimestre seguinte.


5. Retenção por coorte de aquisição

Setor: SaaS / Assinaturas · Complexidade: Avançada

Desafio: medir quantos clientes de cada coorte de aquisição — o grupo de clientes que assinou no mesmo mês — continuam ativos 1, 2, 3, 6 e 12 meses depois.

💡 O que é coorte? É um grupo de pessoas que viveram o mesmo evento inicial no mesmo período: os clientes que assinaram em janeiro de 2025 formam uma coorte; os de fevereiro, outra. O termo vem da estatística e da epidemiologia ("estudo de coorte") e corresponde ao inglês cohort. Não confunda com corte, de um "o" só, que aparece no código como vDataCorte: a data até onde a análise olha.

Por que analisar por coorte? A retenção média mistura clientes antigos e novos e esconde mudanças. Separando por mês de entrada, dá para ver se quem entrou depois de uma campanha ou de uma mudança de preço fica mais ou menos tempo.

Coorte (mês de aquisição)ClientesAtivos após 3 mesesRetenção em 3 meses
2025-0120015075%
2025-0218011765%

Leia a tabela por linha: cada coorte é comparada com ela mesma ao longo do tempo, e as linhas entre si mostram se a retenção está melhorando.

Prompt:

"Tenho TabFatAssinatura (CodigoCliente, DataInicio, DataFim em branco para ativos). Crie a retenção por coorte: para cada mês de aquisição e cada N em 1, 2, 3, 6 e 12, o percentual de clientes ainda ativos N meses depois do próprio início. Só conte clientes que já completaram N meses."

Crie antes a coluna e a tabela de apoio no modelo:

-- Coluna calculada em TabFatAssinatura
Mês de Aquisição = FORMAT ( TabFatAssinatura[DataInicio], "YYYY-MM" )

-- Tabela calculada, sem relacionamento: meses de retenção analisados
TabIntMesRetencao = SELECTCOLUMNS ( { 1, 2, 3, 6, 12 }, "Meses", [Value] )
DEFINE
    MEASURE TabFatAssinatura[Retenção (%)] =
        VAR vMeses = SELECTEDVALUE ( TabIntMesRetencao[Meses] )
        VAR vDataCorte = TODAY ()
        -- Clientes que já completaram N meses desde o próprio início
        VAR vElegiveis =
            FILTER ( TabFatAssinatura, EDATE ( TabFatAssinatura[DataInicio], vMeses ) <= vDataCorte )
        -- Destes, os que continuavam ativos na data-alvo (DataFim em branco = ativo)
        VAR vAtivos =
            FILTER (
                vElegiveis,
                VAR vAlvo = EDATE ( TabFatAssinatura[DataInicio], vMeses )
                RETURN
                    ISBLANK ( TabFatAssinatura[DataFim] ) || TabFatAssinatura[DataFim] >= vAlvo
            )
        RETURN
            IF (
                NOT ISBLANK ( vMeses ),
                DIVIDE (
                    CALCULATE ( DISTINCTCOUNT ( TabFatAssinatura[CodigoCliente] ), vAtivos ) + 0,  -- 0% em vez de branco
                    CALCULATE ( DISTINCTCOUNT ( TabFatAssinatura[CodigoCliente] ), vElegiveis )
                )
            )

EVALUATE
SUMMARIZECOLUMNS (
    TabFatAssinatura[Mês de Aquisição],
    TabIntMesRetencao[Meses],
    "Retenção", [Retenção (%)]
)
ORDER BY TabFatAssinatura[Mês de Aquisição], TabIntMesRetencao[Meses]
  • ⚠️ A versão anterior filtrava DataFim >= DataAlvo: com DataFim em branco (cliente ativo), a comparação trata BLANK como 0 e o cliente ativo contava como perdido.
  • 💡 Troque TODAY () pela data da última atualização (ver Padrão: fuso horário) para o corte não mudar com o fuso do Service.
  • 💡 Numa matriz, coloque Mês de Aquisição nas linhas e Meses nas colunas.

Impacto relatado: clientes vindos de indicação retêm 2,3× mais; a realocação do orçamento de marketing elevou o ROI em 180%.


6. Curva ABC (Pareto 80/20) dinâmica

Setor: Universal · Complexidade: Intermediária

Desafio: identificar os produtos que somam 80% das vendas, respeitando os filtros de período e região.

Prompt:

"Tenho TabFatVenda (ValorVenda) e TabDimProduto (NomeProduto). Crie o ranking do produto, o percentual acumulado de vendas do maior para o menor e a classe A (até 80%), B (até 95%) ou C, respeitando as segmentações."

DEFINE
    MEASURE TabFatVenda[Total de Vendas] =
        SUM ( TabFatVenda[ValorVenda] )

    MEASURE TabFatVenda[Ranking do Produto] =
        -- A medida dentro do RANKX faz a transição de contexto; SUM direto daria o mesmo valor para todos
        IF (
            HASONEVALUE ( TabDimProduto[NomeProduto] ),
            RANKX ( ALLSELECTED ( TabDimProduto[NomeProduto] ), [Total de Vendas], , DESC, DENSE )
        )

    MEASURE TabFatVenda[Vendas Acumuladas (%)] =
        VAR vVendaProduto = [Total de Vendas]
        VAR vProdutos =
            ADDCOLUMNS ( ALLSELECTED ( TabDimProduto[NomeProduto] ), "@Vendas", [Total de Vendas] )
        VAR vAcumulado = SUMX ( FILTER ( vProdutos, [@Vendas] >= vVendaProduto ), [@Vendas] )
        VAR vTotal = SUMX ( vProdutos, [@Vendas] )
        RETURN
            IF (
                HASONEVALUE ( TabDimProduto[NomeProduto] ) && NOT ISBLANK ( vVendaProduto ),
                DIVIDE ( vAcumulado, vTotal )
            )

    MEASURE TabFatVenda[Classe ABC] =
        VAR vAcumulado = [Vendas Acumuladas (%)]
        RETURN
            SWITCH (
                TRUE (),
                ISBLANK ( vAcumulado ), BLANK (),
                vAcumulado <= 0.80, "A",
                vAcumulado <= 0.95, "B",
                "C"
            )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimProduto[NomeProduto],
    "Vendas", [Total de Vendas],
    "Ranking", [Ranking do Produto],
    "Acumulado", [Vendas Acumuladas (%)],
    "Classe", [Classe ABC]
)
ORDER BY [Vendas] DESC
  • ⚠️ A versão anterior usava RANKX(ALL(Produto), SUM(Valor)): sem medida nem CALCULATE, a soma não muda por produto e todos ficavam em 1º lugar.
  • 💡 Produtos com vendas iguais entram juntos no acumulado.
  • 💡 Se houver produtos com o mesmo nome, use TabDimProduto[CodigoProduto] no ranking e no acumulado.
  • ⚠️ No RANKX, o ALLSELECTED precisa cobrir todas as colunas da entidade que podem estar no visual. Ranquear por TabDimProduto[CodigoProduto] com TabDimProduto[NomeProduto] também nas linhas deixa o filtro do nome ativo durante a iteração: os outros códigos ficam sem venda e todos aparecem em 1º. Use ALLSELECTED ( TabDimProduto[CodigoProduto], TabDimProduto[NomeProduto] ) ou ALLSELECTED ( TabDimProduto ). Num modelo real com 3.689 códigos e 3.287 nomes de cliente, o ranking por nome somava homônimos, e o ranking por código com o nome no visual dava 1º para todos.

Impacto relatado: 23 de 847 SKUs respondem por 80% do faturamento; o foco das promoções nesses 23 gerou +15% de margem.


7. Ticket médio da loja × rede

Setor: Restaurante / Food Service · Complexidade: Básica

Desafio: comparar o ticket médio de cada loja com o da rede.

Prompt:

"Tenho TabFatVenda (CodigoPedido, CodigoLoja, ValorVenda) e TabDimLoja (NomeLoja). Crie o ticket médio, o ticket médio da rede ignorando o filtro de loja, a diferença percentual e um status: acima (≥ +10%), na média ou abaixo (≤ −10%)."

DEFINE
    MEASURE TabFatVenda[Total de Vendas] =
        SUM ( TabFatVenda[ValorVenda] )

    MEASURE TabFatVenda[# Pedidos] =
        DISTINCTCOUNT ( TabFatVenda[CodigoPedido] )

    MEASURE TabFatVenda[Ticket Médio] =
        DIVIDE ( [Total de Vendas], [# Pedidos] )

    MEASURE TabFatVenda[Ticket Médio da Rede] =
        CALCULATE ( [Ticket Médio], REMOVEFILTERS ( TabDimLoja ) )

    MEASURE TabFatVenda[Ticket Médio vs Rede (%)] =
        VAR vLoja = [Ticket Médio]
        VAR vRede = [Ticket Médio da Rede]
        RETURN
            -- Loja sem venda fica em branco, e não em −100%
            IF ( NOT ISBLANK ( vLoja ), DIVIDE ( vLoja - vRede, vRede ) )

    MEASURE TabFatVenda[Status vs Rede] =
        VAR vDiferenca = [Ticket Médio vs Rede (%)]
        RETURN
            SWITCH (
                TRUE (),
                ISBLANK ( vDiferenca ), BLANK (),
                vDiferenca >= 0.10, "✔️ Acima da rede",
                vDiferenca > -0.10, "⚠️ Na média",
                "❗ Abaixo da rede"
            )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimLoja[NomeLoja],
    "Ticket Médio", [Ticket Médio],
    "Rede", [Ticket Médio da Rede],
    "Diferença", [Ticket Médio vs Rede (%)],
    "Status", [Status vs Rede]
)
ORDER BY [Diferença] DESC
  • 💡 REMOVEFILTERS ( TabDimLoja ) tira só o filtro de loja: período e produto continuam valendo para a rede.

8. Conversão do funil de vendas

Setor: Vendas B2B / CRM · Complexidade: Intermediária

Desafio: calcular a conversão entre etapas do funil e apontar o gargalo.

Prompt:

"Tenho TabFatFunil (CodigoOportunidade, CodigoEtapa), com uma linha por etapa alcançada, e TabDimEtapa (Ordem, NomeEtapa). Crie o número de oportunidades por etapa, a conversão em relação à etapa anterior e o nome da etapa com a menor conversão."

DEFINE
    MEASURE TabFatFunil[# Oportunidades] =
        DISTINCTCOUNT ( TabFatFunil[CodigoOportunidade] )

    MEASURE TabFatFunil[Conversão da Etapa (%)] =
        VAR vOrdem = SELECTEDVALUE ( TabDimEtapa[Ordem] )
        -- REMOVEFILTERS tira o filtro da etapa atual antes de filtrar a anterior
        VAR vAnterior =
            CALCULATE (
                [# Oportunidades],
                REMOVEFILTERS ( TabDimEtapa ),
                TabDimEtapa[Ordem] = vOrdem - 1
            )
        RETURN
            IF ( vOrdem > 1, DIVIDE ( [# Oportunidades], vAnterior ) )

    MEASURE TabFatFunil[Etapa Gargalo] =
        VAR vEtapas =
            FILTER (
                ADDCOLUMNS (
                    ALLSELECTED ( TabDimEtapa[NomeEtapa], TabDimEtapa[Ordem] ),
                    "@Conversao", [Conversão da Etapa (%)]
                ),
                NOT ISBLANK ( [@Conversao] )
            )
        RETURN
            MAXX ( TOPN ( 1, vEtapas, [@Conversao], ASC ), TabDimEtapa[NomeEtapa] )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimEtapa[Ordem],
    TabDimEtapa[NomeEtapa],
    "Oportunidades", [# Oportunidades],
    "Conversão", [Conversão da Etapa (%)]
)
ORDER BY TabDimEtapa[Ordem]

EVALUATE
{ [Etapa Gargalo] }
  • ⚠️ A versão anterior tinha dois erros:
    • FILTER(Etapas, Ordem = anterior) percorria só a etapa já filtrada e devolvia vazio.
    • FIRSTNONBLANK(TOPN(...), 1) recebia uma tabela de várias colunas no lugar de uma coluna e dava erro.
  • 💡 A primeira etapa não tem conversão e fica fora do gargalo.

9. Churn mensal

Setor: Telecom / SaaS · Complexidade: Avançada

Desafio: calcular o churn do mês e projetá-lo para 12 meses.

Prompt:

"Tenho TabFatAssinatura (CodigoCliente, DataInicio, DataFim em branco para ativos), sem relacionamento com TabDimCalendario. Para cada mês: clientes ativos no primeiro dia, cancelamentos no mês entre esses clientes, churn mensal e churn anualizado. Deixe em branco os meses depois da última atualização (TabIntAtualizacao)."

DEFINE
    MEASURE TabFatAssinatura[# Clientes Ativos no Início] =
        VAR vInicio = MIN ( TabDimCalendario[Data Referência] )
        VAR vHoje = MAX ( TabIntAtualizacao[Data da Atualização] )
        RETURN
            -- Meses depois da última atualização ficam em branco
            IF (
                vInicio <= vHoje,
                CALCULATE (
                    DISTINCTCOUNT ( TabFatAssinatura[CodigoCliente] ),
                    REMOVEFILTERS ( TabDimCalendario ),
                    TabFatAssinatura[DataInicio] < vInicio,
                    ISBLANK ( TabFatAssinatura[DataFim] ) || TabFatAssinatura[DataFim] >= vInicio
                )
            )

    MEASURE TabFatAssinatura[# Cancelamentos] =
        VAR vInicio = MIN ( TabDimCalendario[Data Referência] )
        VAR vFim = MAX ( TabDimCalendario[Data Referência] )
        RETURN
            CALCULATE (
                DISTINCTCOUNT ( TabFatAssinatura[CodigoCliente] ),
                REMOVEFILTERS ( TabDimCalendario ),
                TabFatAssinatura[DataInicio] < vInicio,   -- só quem já estava ativo no início
                TabFatAssinatura[DataFim] >= vInicio,
                TabFatAssinatura[DataFim] <= vFim
            )

    MEASURE TabFatAssinatura[Churn Mensal (%)] =
        DIVIDE ( [# Cancelamentos] + 0, [# Clientes Ativos no Início] )  -- mês sem cancelamento = 0%

    MEASURE TabFatAssinatura[Churn Anualizado (%)] =
        VAR vChurn = [Churn Mensal (%)]
        RETURN
            IF ( NOT ISBLANK ( vChurn ), 1 - POWER ( 1 - vChurn, 12 ) )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimCalendario[Ano/Mês],
    "Ativos no Início", [# Clientes Ativos no Início],
    "Cancelamentos", [# Cancelamentos],
    "Churn Mensal", [Churn Mensal (%)],
    "Churn Anualizado", [Churn Anualizado (%)]
)
ORDER BY TabDimCalendario[Ano/Mês]
  • ⚠️ A versão anterior contava quem começou no mês anterior, e não quem estava ativo. Também aplicava DATESMTD sobre a coluna da própria tabela de fatos, fora do filtro do calendário.
  • 💡 Use as medidas no nível de mês; em trimestre ou ano, o "início" é o primeiro dia do período inteiro.

10. Painel executivo em 10 medidas

Setor: Universal · Complexidade: Todas

Desafio: reunir os indicadores executivos de acompanhamento diário.

Prompt:

"Tenho TabFatVenda (Data, CodigoPedido, CodigoCliente, CodigoProduto, ValorVenda, CustoVenda), TabDimCalendario, TabFatMeta (Data, ValorMeta) e TabIntAtualizacao (Data da Atualização). Crie as 10 medidas de um painel executivo: faturamento, margem, ticket médio, atingimento da meta, variação sobre o mês anterior, variação sobre o ano anterior, novos clientes, produto mais vendido, dias sem venda e projeção do mês."

DEFINE
    -- 1. Faturamento
    MEASURE TabFatVenda[Total de Vendas] =
        SUM ( TabFatVenda[ValorVenda] )

    -- 2. Margem bruta
    MEASURE TabFatVenda[Margem Bruta (%)] =
        VAR vReceita = [Total de Vendas]
        VAR vCusto = SUM ( TabFatVenda[CustoVenda] )
        RETURN
            DIVIDE ( vReceita - vCusto, vReceita )

    -- 3. Ticket médio
    MEASURE TabFatVenda[# Pedidos] =
        DISTINCTCOUNT ( TabFatVenda[CodigoPedido] )
    MEASURE TabFatVenda[Ticket Médio] =
        DIVIDE ( [Total de Vendas], [# Pedidos] )

    -- 4. Atingimento da meta (mesmas medidas do caso 3)
    MEASURE TabFatMeta[Total da Meta] =
        SUM ( TabFatMeta[ValorMeta] )
    MEASURE TabFatMeta[Atingimento da Meta (%)] =
        VAR vMeta = [Total da Meta]
        VAR vUltimaVenda = CALCULATE ( MAX ( TabFatVenda[Data] ), REMOVEFILTERS () )
        RETURN
            IF (
                NOT ISBLANK ( vMeta ) && MIN ( TabDimCalendario[Data Referência] ) <= vUltimaVenda,
                DIVIDE ( [Total de Vendas] + 0, vMeta )
            )

    -- 5. Variação sobre o mês anterior
    MEASURE TabFatVenda[Δ% Total de Vendas vs Mês Anterior] =
        VAR vMesAnterior =
            CALCULATE ( [Total de Vendas], DATEADD ( TabDimCalendario[Data Referência], -1, MONTH ) )
        VAR vAtual = [Total de Vendas]
        RETURN
            -- Mês sem venda (inclusive futuro) fica em branco, e não em −100%
            IF ( NOT ISBLANK ( vAtual ), DIVIDE ( vAtual - vMesAnterior, vMesAnterior ) )

    -- 6. Variação sobre o ano anterior (AA)
    MEASURE TabFatVenda[Δ% Total de Vendas] =
        VAR vAA =
            CALCULATE ( [Total de Vendas], SAMEPERIODLASTYEAR ( TabDimCalendario[Data Referência] ) )
        VAR vAV = [Total de Vendas]
        RETURN
            IF ( NOT ISBLANK ( vAV ), DIVIDE ( vAV - vAA, vAA ) )

    -- 7. Novos clientes: primeira compra da história dentro do período
    MEASURE TabFatVenda[# Novos Clientes] =
        VAR vInicio = MIN ( TabDimCalendario[Data Referência] )
        RETURN
            COUNTROWS (
                FILTER (
                    VALUES ( TabFatVenda[CodigoCliente] ),
                    CALCULATE ( MIN ( TabFatVenda[Data] ), REMOVEFILTERS ( TabDimCalendario ) ) >= vInicio
                )
            )

    -- 8. Produto mais vendido
    MEASURE TabFatVenda[Produto Mais Vendido] =
        -- Sem vendas, todos empatam em BLANK e o TOPN devolveria todos
        IF (
            NOT ISBLANK ( [Total de Vendas] ),
            MAXX (
                TOPN ( 1, VALUES ( TabDimProduto[NomeProduto] ), [Total de Vendas], DESC ),
                TabDimProduto[NomeProduto]
            )
        )

    -- 9. Dias desde a última venda, contados até a última atualização
    MEASURE TabFatVenda[Dias sem Venda] =
        VAR vUltimaVenda = CALCULATE ( MAX ( TabFatVenda[Data] ), REMOVEFILTERS ( TabDimCalendario ) )
        VAR vHoje = MAX ( TabIntAtualizacao[Data da Atualização] )
        RETURN
            IF ( NOT ISBLANK ( vUltimaVenda ), DATEDIFF ( vUltimaVenda, vHoje, DAY ) )

    -- 10. Projeção linear do mês selecionado
    MEASURE TabFatVenda[Projeção do Mês] =
        VAR vInicioMes = MIN ( TabDimCalendario[Data Referência] )
        VAR vFimMes = EOMONTH ( vInicioMes, 0 )
        VAR vHoje = MAX ( TabIntAtualizacao[Data da Atualização] )
        -- Mês encerrado: corta no fim do mês e a projeção é o próprio realizado
        VAR vCorte = MIN ( vFimMes, vHoje )
        RETURN
            IF (
                HASONEVALUE ( TabDimCalendario[Ano/Mês] ) && vCorte >= vInicioMes,
                DIVIDE ( [Total de Vendas], DAY ( vCorte ) ) * DAY ( vFimMes )
            )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimCalendario[Ano/Mês],
    "Faturamento", [Total de Vendas],
    "Margem", [Margem Bruta (%)],
    "Ticket", [Ticket Médio],
    "Meta", [Atingimento da Meta (%)],
    "Δ% Mês", [Δ% Total de Vendas vs Mês Anterior],
    "Δ% AA", [Δ% Total de Vendas],
    "Novos Clientes", [# Novos Clientes],
    "Top Produto", [Produto Mais Vendido],
    "Projeção", [Projeção do Mês]
)
ORDER BY TabDimCalendario[Ano/Mês]

EVALUATE
{ [Dias sem Venda] }
  • ⚠️ A versão anterior de Novos Clientes fazia CALCULATE(MIN(Data)) sem tirar o filtro do período: todo cliente com compra no período contava como novo.
  • ⚠️ A projeção anterior usava TODAY(): num mês passado projetava com o dia de hoje, e no Service TODAY() segue o UTC.
  • 💡 # Novos Clientes respeita os outros filtros: com um produto selecionado, conta a primeira compra daquele produto.

Impacto relatado: painel montado em 2 horas (antes, 2 dias); 10 indicadores cobrindo 90% das perguntas da diretoria.


11. Variação sobre base negativa (saldo de caixa)

Setor: Finanças / Tesouraria · Complexidade: Intermediária

Desafio: comparar o saldo de caixa com o ano anterior quando esse saldo pode ser negativo (uma base negativa: o AA ficou no vermelho), sem que o Δ% e o farol invertam o sentido.

Prompt:

"Tenho TabFatTransacao (Data da Transação, Valor da Transação), com recebimentos positivos e pagamentos negativos, e TabDimCalendario[Data Referência] marcada como Tabela de Datas. Crie a família AV/AA/Δ/Δ%/Δi do saldo de caixa. O saldo do ano anterior pode ser negativo: o Δ% e o farol precisam mostrar melhora quando o saldo sobe, mesmo saindo de uma base negativa."

DEFINE
    MEASURE TabFatTransacao[Saldo de Caixa AV] =
        -- Recebimentos positivos, pagamentos negativos: a soma é o saldo líquido
        SUM ( TabFatTransacao[Valor da Transação] )

    MEASURE TabFatTransacao[Saldo de Caixa AA] =
        CALCULATE ( [Saldo de Caixa AV], SAMEPERIODLASTYEAR ( TabDimCalendario[Data Referência] ) )

    MEASURE TabFatTransacao[Δ Saldo de Caixa] =
        VAR vAV = [Saldo de Caixa AV]
        VAR vAA = [Saldo de Caixa AA]
        RETURN
            -- Sem os dois períodos não há variação: evita −100% no início e no fim da série
            IF ( NOT ISBLANK ( vAV ) && NOT ISBLANK ( vAA ), vAV - vAA )

    MEASURE TabFatTransacao[Δ% Saldo de Caixa] =
        -- ABS na base: o sinal do Δ% segue o Δ (melhorou = positivo) mesmo com AA negativo
        DIVIDE ( [Δ Saldo de Caixa], ABS ( [Saldo de Caixa AA] ) )

    MEASURE TabFatTransacao[Δi Saldo de Caixa] =
        -- Farol pelo Δ em reais, que nunca inverte; o Δ% explode com base pequena
        VAR vDelta = [Δ Saldo de Caixa]
        RETURN
            SWITCH (
                TRUE (),
                ISBLANK ( vDelta ), BLANK (),
                vDelta > 0, "✔️",
                vDelta = 0, "⚠️",
                "❗"
            )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimCalendario[Ano/Mês],
    "AV", [Saldo de Caixa AV],
    "AA", [Saldo de Caixa AA],
    "Δ", [Δ Saldo de Caixa],
    "Δ%", [Δ% Saldo de Caixa],
    "Δi", [Δi Saldo de Caixa]
)
ORDER BY TabDimCalendario[Ano/Mês]
  • ⚠️ A fórmula dos casos 1 e 10, DIVIDE ( vAV - vAA, vAA ), inverte o sinal quando o AA é negativo: o saldo que saiu de −R$ 1,13 mi para +R$ 2,74 mi aparece como −341,5%. Com ABS (valor absoluto, sem sinal) no denominador, vira +341,5%.
  • 💡 Com AA pequeno ou de sinal oposto ao AV, o Δ% fica enorme. Por isso o farol lê o Δ em reais, e não o Δ%.
  • 💡 Serve para qualquer medida que cruza o zero: lucro, resultado operacional, fluxo de caixa, variação de estoque.
  • 💡 Se o calendário termina na data de hoje, o mês corrente só tem os dias já passados, e o SAMEPERIODLASTYEAR compara os mesmos dias do ano anterior. É uma alternativa ao EDATE do ⚠️ do caso 1.

Resultado num modelo real: dos meses com saldo negativo no ano anterior, os dois que melhoraram apareciam como queda na fórmula anterior (−341,5% e −101,1%).


12. Prazos em dias úteis: PMR, PMP e atraso

Setor: Finanças / Contas a Receber e a Pagar · Complexidade: Avançada

Desafio: medir, em dias úteis (sem fins de semana e feriados), quanto tempo os títulos levam para virar caixa — o PMR, prazo médio de recebimento —, para sair do caixa — o PMP, prazo médio de pagamento — e quanto atrasam em relação ao vencimento. Os prazos são ponderados pelo valor: um título de R$ 500 mil pesa mais na média que um de R$ 50.

Prompt:

"Tenho TabFatTransacao (Data da Emissão, Data do Vencimento, Data da Transação, Transação, Valor da Transação) e TabDimFeriado (Data). Crie, em dias úteis e ponderados pelo valor: prazo médio de recebimento, prazo médio de pagamento, ciclo financeiro, atraso médio sobre o vencimento e o percentual do valor baixado até o vencimento. Mesma data conta 0 dia; baixa antes do vencimento conta negativo. Inclua o valor analisado pela data de vencimento."

DEFINE
    MEASURE TabFatTransacao[Prazo Efetivo (dias úteis)] =
        -- Dias úteis da emissão até a baixa, ponderados pelo valor de cada transação
        VAR vLinhas =
            FILTER (
                TabFatTransacao,
                NOT ISBLANK ( TabFatTransacao[Data da Emissão] )
                    && NOT ISBLANK ( TabFatTransacao[Data da Transação] )
            )
        VAR vPonderado =
            SUMX (
                vLinhas,
                VAR vIni = TabFatTransacao[Data da Emissão]
                VAR vFim = TabFatTransacao[Data da Transação]
                -- NETWORKDAYS conta os dois extremos; começar em vIni + 1 faz a mesma data valer 0
                VAR vDias =
                    SWITCH (
                        TRUE (),
                        vFim > vIni, NETWORKDAYS ( vIni + 1, vFim, 1, TabDimFeriado ),
                        vFim < vIni, - NETWORKDAYS ( vFim + 1, vIni, 1, TabDimFeriado ),
                        0
                    )
                RETURN
                    vDias * ABS ( TabFatTransacao[Valor da Transação] )
            )
        RETURN
            DIVIDE ( vPonderado, SUMX ( vLinhas, ABS ( TabFatTransacao[Valor da Transação] ) ) )

    MEASURE TabFatTransacao[PMR (dias úteis)] =
        CALCULATE ( [Prazo Efetivo (dias úteis)], TabFatTransacao[Transação] = "Recebimento" )

    MEASURE TabFatTransacao[PMP (dias úteis)] =
        CALCULATE ( [Prazo Efetivo (dias úteis)], TabFatTransacao[Transação] = "Pagamento" )

    MEASURE TabFatTransacao[Ciclo Financeiro (dias úteis)] =
        -- Positivo: a empresa recebe depois de pagar e financia a diferença
        [PMR (dias úteis)] - [PMP (dias úteis)]

    MEASURE TabFatTransacao[Atraso Médio (dias úteis)] =
        -- Positivo = baixa depois do vencimento; negativo = antecipação
        VAR vLinhas =
            FILTER (
                TabFatTransacao,
                NOT ISBLANK ( TabFatTransacao[Data do Vencimento] )
                    && NOT ISBLANK ( TabFatTransacao[Data da Transação] )
            )
        VAR vPonderado =
            SUMX (
                vLinhas,
                VAR vIni = TabFatTransacao[Data do Vencimento]
                VAR vFim = TabFatTransacao[Data da Transação]
                VAR vDias =
                    SWITCH (
                        TRUE (),
                        vFim > vIni, NETWORKDAYS ( vIni + 1, vFim, 1, TabDimFeriado ),
                        vFim < vIni, - NETWORKDAYS ( vFim + 1, vIni, 1, TabDimFeriado ),
                        0
                    )
                RETURN
                    vDias * ABS ( TabFatTransacao[Valor da Transação] )
            )
        RETURN
            DIVIDE ( vPonderado, SUMX ( vLinhas, ABS ( TabFatTransacao[Valor da Transação] ) ) )

    MEASURE TabFatTransacao[Baixado até o Vencimento (%)] =
        VAR vTotal = SUMX ( TabFatTransacao, ABS ( TabFatTransacao[Valor da Transação] ) )
        VAR vEmDia =
            SUMX (
                FILTER ( TabFatTransacao, TabFatTransacao[Data da Transação] <= TabFatTransacao[Data do Vencimento] ),
                ABS ( TabFatTransacao[Valor da Transação] )
            )
        RETURN
            DIVIDE ( vEmDia + 0, vTotal )  -- 0% em vez de branco quando nada foi pago em dia

    MEASURE TabFatTransacao[Valor por Vencimento] =
        -- Troca o relacionamento ativo (baixa) pelo inativo (vencimento) só nesta medida
        CALCULATE (
            SUM ( TabFatTransacao[Valor da Transação] ),
            USERELATIONSHIP ( TabFatTransacao[Data do Vencimento], TabDimCalendario[Data Referência] )
        )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimCalendario[Ano],
    "PMR", [PMR (dias úteis)],
    "PMP", [PMP (dias úteis)],
    "Ciclo", [Ciclo Financeiro (dias úteis)],
    "Atraso", [Atraso Médio (dias úteis)],
    "Em Dia", [Baixado até o Vencimento (%)],
    "Por Vencimento", [Valor por Vencimento],
    "Por Baixa", SUM ( TabFatTransacao[Valor da Transação] )
)
ORDER BY TabDimCalendario[Ano]
  • ⚠️ A média simples dá o mesmo peso a um título de R$ 50 e a um de R$ 500 mil. Num modelo real, o PMP simples era 14,1 dias úteis e o ponderado, 18,5.
  • ⚠️ NETWORKDAYS linha a linha roda no motor de fórmulas: 0,9 s no modelo de teste. Com volume alto, grave os dias úteis como coluna (Power Query ou coluna calculada) e troque o SWITCH por essa coluna no SUMX.
  • ⚠️ Se o calendário termina hoje, vencimentos futuros ficam fora dele: no Valor por Vencimento, eles aparecem numa linha em branco. Estenda o calendário até o maior vencimento se precisar dessa análise.
  • ⚠️ A tabela tem só títulos baixados. Aging (idade dos títulos em aberto) precisa de uma tabela de saldos em aberto.
  • 💡 Ciclo financeiro = PMR − PMP: quantos dias a empresa financia entre pagar o fornecedor e receber do cliente.
  • 💡 O 3º argumento 1 de NETWORKDAYS define sábado e domingo como fim de semana. TabDimFeriado precisa ter uma única coluna de datas.

Resultado num modelo real: o ciclo financeiro caiu de +12,2 dias úteis em 2025 para −0,8 em 2026, porque o PMP subiu de 13,7 para 26,6 dias úteis.


13. Headcount e turnover de RH

Setor: Recursos Humanos · Complexidade: Avançada

Desafio: medir, por período, o headcount (quantos colaboradores estavam ativos) no início e no fim, quantos entraram e saíram, a taxa de desligamento e o turnover (rotatividade: quanto o quadro se renovou), numa tabela com uma linha por colaborador e datas de entrada e saída.

Prompt:

"Tenho TabFatColaborador (Matrícula, Entrada, Saída em branco para ativos), com relacionamento ativo de Entrada e inativo de Saída com TabDimCalendario[Data Referência]. Para cada período, crie: ativos no início, admissões, desligamentos, ativos no fim, headcount médio, taxa de desligamento e turnover de RH ((admissões + desligamentos) ÷ 2 ÷ headcount médio). Início + admissões − desligamentos precisa bater com o fim."

DEFINE
    MEASURE TabFatColaborador[# Colaboradores] =
        COUNTROWS ( TabFatColaborador )

    MEASURE TabFatColaborador[# Admissões] =
        -- O relacionamento ativo já é por Entrada
        [# Colaboradores]

    MEASURE TabFatColaborador[# Desligamentos] =
        CALCULATE (
            [# Colaboradores],
            USERELATIONSHIP ( TabFatColaborador[Saída], TabDimCalendario[Data Referência] ),
            NOT ISBLANK ( TabFatColaborador[Saída] )   -- ativos fora do total
        )

    MEASURE TabFatColaborador[# Ativos no Início] =
        VAR vInicio = MIN ( TabDimCalendario[Data Referência] )
        RETURN
            -- Linha (Em branco) do calendário, criada pelas Saídas em branco: sem período, sem valor
            IF (
                NOT ISBLANK ( vInicio ),
                CALCULATE (
                    [# Colaboradores],
                    REMOVEFILTERS ( TabDimCalendario ),   -- tira o filtro que chega por Entrada
                    TabFatColaborador[Entrada] < vInicio,
                    TabFatColaborador[Saída] >= vInicio || ISBLANK ( TabFatColaborador[Saída] )
                ) + 0                                     -- 0 no primeiro período, e não branco
            )

    MEASURE TabFatColaborador[# Ativos no Fim] =
        VAR vFim = MAX ( TabDimCalendario[Data Referência] )
        RETURN
            IF (
                NOT ISBLANK ( vFim ),
                CALCULATE (
                    [# Colaboradores],
                    REMOVEFILTERS ( TabDimCalendario ),
                    TabFatColaborador[Entrada] <= vFim,
                    TabFatColaborador[Saída] > vFim || ISBLANK ( TabFatColaborador[Saída] )
                )
            )

    MEASURE TabFatColaborador[Headcount Médio] =
        DIVIDE ( [# Ativos no Início] + [# Ativos no Fim], 2 )

    MEASURE TabFatColaborador[Taxa de Desligamento (%)] =
        -- Desligados sobre todos que passaram pela empresa no período
        DIVIDE ( [# Desligamentos] + 0, [# Ativos no Início] + [# Admissões] )

    MEASURE TabFatColaborador[Turnover (%)] =
        DIVIDE ( ( [# Admissões] + [# Desligamentos] ) / 2, [Headcount Médio] )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimCalendario[Ano],
    "Início", [# Ativos no Início],
    "Admissões", [# Admissões],
    "Desligamentos", [# Desligamentos],
    "Fim", [# Ativos no Fim],
    "Confere", IF (
        NOT ISBLANK ( [# Ativos no Fim] ),
        [# Ativos no Início] + [# Admissões] - [# Desligamentos] = [# Ativos no Fim]
    ),
    "Taxa de Desligamento", [Taxa de Desligamento (%)],
    "Turnover", [Turnover (%)]
)
ORDER BY TabDimCalendario[Ano]
  • ⚠️ Com o relacionamento ativo em Entrada, um CALCULATE só com filtros de Entrada e Saída continua filtrado pelo período do visual e conta apenas quem foi admitido nele. Num modelo real, a versão sem REMOVEFILTERS dava turnover de 2,5% onde o correto era 9,5%. No caso 9 isso não acontece porque a tabela de assinaturas não tem relacionamento com o calendário.
  • ⚠️ USERELATIONSHIP em Saída sem NOT ISBLANK joga todos os ativos na linha (Em branco) do calendário, e o total geral de desligamentos vira o total de colaboradores (9.999 em vez de 6.296 no modelo de teste).
  • ⚠️ As Saídas em branco criam uma linha (Em branco) no calendário. O IF ( NOT ISBLANK ( vFim ) … ) evita que as medidas com REMOVEFILTERS mostrem valor nela.
  • 💡 A coluna Confere fecha a conta: início + admissões − desligamentos = fim. Se der FALSE, a regra de quem está ativo no dia da saída não está igual nas medidas.
  • 💡 No primeiro período da base, o início é 0 e o turnover passa de 100%. Comece a série no segundo período.
  • 💡 Headcount Médio usa a média de início e fim. Para uma média mais fiel em períodos longos, faça a média dos # Ativos no Fim de cada mês com AVERAGEX ( VALUES ( TabDimCalendario[Ano/Mês] ), [# Ativos no Fim] ).
  • 💡 Taxa de Desligamento e Turnover respondem perguntas diferentes: a primeira mede perda; o turnover mede renovação e sobe também quando a empresa só contrata.

Resultado num modelo real: em 2021, início 3.368 + 197 admissões − 403 desligamentos = 3.162 no fim; taxa de desligamento de 11,3% e turnover de 9,2%.


14. Participação %: qual é o total?

Setor: Universal · Complexidade: Intermediária

Desafio: mostrar a participação de cada produto no faturamento sem que o denominador (o "total" da divisão) ignore o período filtrado, e escolher conscientemente entre o total do visual, o total do período e o total geral.

Prompt:

"Tenho TabFatVenda (Data, ValorVenda), TabDimProduto (NomeProduto) e TabDimCalendario[Data Referência] marcada como Tabela de Datas. Crie a participação de cada produto no faturamento de três formas: sobre o total do visual (respeitando segmentações), sobre todos os produtos no período e sobre o total geral de todos os anos. Explique quando usar cada uma."

DEFINE
    MEASURE TabFatVenda[Total de Vendas] =
        SUM ( TabFatVenda[ValorVenda] )

    MEASURE TabFatVenda[Participação no Visual (%)] =
        -- Total do visual: tira os filtros das linhas e colunas, mantém segmentações e período
        DIVIDE ( [Total de Vendas], CALCULATE ( [Total de Vendas], ALLSELECTED () ) )

    MEASURE TabFatVenda[Participação no Período (%)] =
        -- Todos os produtos, mas só no período e nos demais filtros (loja, cliente...)
        DIVIDE ( [Total de Vendas], CALCULATE ( [Total de Vendas], REMOVEFILTERS ( TabDimProduto ) ) )

    MEASURE TabFatVenda[Participação no Total Geral (%)] =
        -- Todos os produtos, todas as lojas, todos os anos
        DIVIDE ( [Total de Vendas], CALCULATE ( [Total de Vendas], REMOVEFILTERS () ) )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimProduto[NomeProduto],
    TREATAS ( { 2025 }, TabDimCalendario[Ano] ),
    "Vendas", [Total de Vendas],
    "No Visual", [Participação no Visual (%)],
    "No Período", [Participação no Período (%)],
    "No Total Geral", [Participação no Total Geral (%)]
)
ORDER BY [Vendas] DESC
DenominadorSoma das fatias do visualQuando usar
ALLSELECTED ()100%, sempreGráfico de rosca, barra 100%, "quanto cada um representa do que estou vendo"
REMOVEFILTERS ( TabDimProduto )100% se não houver segmentação de produtoParticipação no mercado do período, mesmo com só alguns produtos selecionados
REMOVEFILTERS ()Menos de 100% quando há filtro de períodoPeso do item na história inteira da empresa
  • ⚠️ ALL ( TabFatVenda ) não é "todos os produtos". A tabela de fatos "expandida" inclui as colunas das dimensões ligadas a ela, e o ALL apaga também o filtro do calendário. Num modelo real, as regiões de 2025 somavam 13,7% em vez de 100%, porque cada uma era dividida pelo faturamento de todos os anos. Numa tabela monolítica (TabFlt), o efeito é o mesmo.
  • ⚠️ REMOVEFILTERS ( TabDimProduto ) também ignora a segmentação de produto. Se o leitor selecionar três produtos, a soma fica abaixo de 100%. Isso é o esperado nessa medida: é participação no mercado, não no visual.
  • ⚠️ Use ALLSELECTED () sem argumento só em medidas exibidas diretamente no visual. Dentro de um iterador (SUMX, RANKX, ADDCOLUMNS), ele pode devolver um total diferente do esperado.
  • 💡 Formate as três medidas como percentual na própria medida, e não com FORMAT: o valor continua numérico, ordenável e disponível para formatação condicional.

Resultado num modelo real: Sudeste 62,1%, Nordeste 14,2%, Sul 10,0%, Centro-Oeste 7,9% e Norte 5,9% de 2025. Com ALLSELECTED () somam 100%; com ALL na tabela, somavam 13,7%.


15. NPS com zona, margem de erro e ranking confiável

Setor: Universal (Experiência do Cliente) · Complexidade: Intermediária

Desafio: calcular o NPS (Net Promoter Score: % de promotores, que dão nota 9 ou 10, menos % de detratores, que dão de 0 a 6), classificá-lo na zona certa, mostrar a margem de erro (quanto o número pode oscilar só pelo tamanho da amostra) e ranquear clientes sem premiar quem respondeu uma vez só.

Prompt:

"Tenho TabFatNPS (Data, CodigoCliente, Nota de 0 a 10) e TabDimCliente. Crie: número de respostas, promotores (9 e 10), detratores (0 a 6), NPS de −100 a +100, margem de erro de 95%, zona (Crítica até 0, Aperfeiçoamento até 50, Qualidade até 75, Excelência acima), cor da zona e ranking de clientes por NPS só para quem tem pelo menos 10 respostas. Sem respostas, nada deve aparecer, nem a cor."

DEFINE
    MEASURE TabFatNPS[# Respostas] =
        COUNTROWS ( TabFatNPS )

    MEASURE TabFatNPS[# Promotores] =
        CALCULATE ( [# Respostas], TabFatNPS[Nota] >= 9 )   -- nota 9 ou 10

    MEASURE TabFatNPS[# Detratores] =
        CALCULATE ( [# Respostas], TabFatNPS[Nota] <= 6 )   -- nota 0 a 6

    MEASURE TabFatNPS[NPS] =
        -- % promotores − % detratores, de −100 a +100; não é a média das notas
        VAR vN = [# Respostas]
        RETURN
            IF ( vN > 0, DIVIDE ( [# Promotores] - [# Detratores], vN ) * 100 )

    MEASURE TabFatNPS[Margem de Erro do NPS] =
        -- Intervalo de 95%: ±1,96 × erro-padrão da diferença de duas proporções da mesma amostra
        VAR vN = [# Respostas]
        VAR vP = DIVIDE ( [# Promotores], vN )
        VAR vD = DIVIDE ( [# Detratores], vN )
        VAR vVariancia = ( vP + vD ) - POWER ( vP - vD, 2 )
        RETURN
            IF ( vN > 1, 1.96 * SQRT ( vVariancia / vN ) * 100 )

    MEASURE TabFatNPS[Zona do NPS] =
        -- Arredonda antes de classificar: a zona segue o número exibido no card
        VAR vNPS = [NPS]
        VAR vInteiro = ROUND ( vNPS, 0 )
        RETURN
            SWITCH (
                TRUE (),
                ISBLANK ( vNPS ), BLANK (),          -- sem resposta, sem zona
                vInteiro <= 0, "Crítica",
                vInteiro <= 50, "Aperfeiçoamento",
                vInteiro <= 75, "Qualidade",
                "Excelência"
            )

    MEASURE TabFatNPS[Cor do NPS] =
        -- Formatação condicional (Formatar por: Valor do campo); herda o BLANK da zona
        SWITCH (
            [Zona do NPS],
            "Crítica", "#EE3E2A",
            "Aperfeiçoamento", "#F69322",
            "Qualidade", "#79A607",
            "Excelência", "#2B579A"
        )

    MEASURE TabFatNPS[Ranking de Clientes por NPS] =
        VAR vMinimo = 10   -- respostas mínimas para entrar no ranking
        RETURN
            IF (
                HASONEVALUE ( TabDimCliente[CodigoCliente] ) && [# Respostas] >= vMinimo,
                RANKX (
                    FILTER ( ALLSELECTED ( TabDimCliente[CodigoCliente] ), [# Respostas] >= vMinimo ),
                    [NPS], , DESC, DENSE
                )
            )

EVALUATE
SUMMARIZECOLUMNS (
    TabDimCalendario[Ano],
    "Respostas", [# Respostas],
    "NPS", [NPS],
    "± Margem", [Margem de Erro do NPS],
    "Zona", [Zona do NPS],
    "Cor", [Cor do NPS]
)
ORDER BY TabDimCalendario[Ano]
  • ⚠️ Uma cor por faixa sem tratar o BLANK pinta contextos vazios: BLANK () <= 49 é verdadeiro, porque o BLANK vale 0 na comparação. Num modelo real, filtros sem nenhuma resposta ficavam amarelos. A zona começa com ISBLANK, e a cor herda o vazio.
  • ⚠️ Classificar o NPS com decimais contra limites inteiros separa o card da cor: 50,4 aparece como "50" e cai em Qualidade. Arredonde antes, como em vInteiro.
  • ⚠️ Ranquear por NPS sem mínimo de respostas coloca no topo quem respondeu uma vez com nota 10. Num modelo real, a mediana era de 5 respostas por cliente e 1.983 clientes empatavam em 1º. Com o mínimo de 10, entram 179 clientes e 36 empatam em 1º.
  • ⚠️ A margem de erro usa a aproximação normal e fica pouco confiável com menos de ~30 respostas. É justamente aí que ela mais importa como alerta: com 5 respostas, ±35 pontos.
  • 💡 NPS não se soma nem se tira média: o NPS da empresa não é a média dos NPS das lojas. Sempre recalcule a partir das contagens, como a medida faz em qualquer nível do visual.
  • 💡 Média das notas não é NPS. Uma média 8 pode vir de muitos neutros (NPS perto de 0) ou de promotores e detratores equilibrados (NPS também perto de 0, com clientes muito mais insatisfeitos).
  • 💡 As faixas das zonas são as mais usadas no Brasil. Confirme com a área de negócio, e mantenha os limites só na medida Zona do NPS: a cor, os ícones e os textos derivam dela.
  • 💡 Para escolher o mínimo no relatório, use um parâmetro numérico (segmentação de 1 a 30) no lugar de vMinimo.
  • 💡 Mostre a margem ao lado do NPS ("86 ± 2"). Se os intervalos de dois períodos se sobrepõem, trate a variação com cautela: pode ser só oscilação da amostra.

Resultado num modelo real: 20.000 respostas, NPS 85,7 ± 0,6 no geral; por ano, entre 83,6 e 87,1, com margem de ±1,4 a ±1,7, todos na zona de Excelência.


Fontes

Padrão: fuso horário e configuração por localidade

1. Última atualização no fuso local

No Power BI Service, NOW() e TODAY() devolvem a hora em UTC, e no Power Query Online DateTime.LocalNow() também devolve UTC. Uma medida de "última atualização" com essas funções mostra a hora errada depois de publicada: às 22h de Brasília, o Service já está no dia seguinte.

A solução é gravar o momento do refresh no Power Query, em UTC, e converter para o fuso local de forma explícita. Crie a consulta TabIntAtualizacao:

let
    // Momento do refresh em UTC; a versão Fixed mantém o mesmo valor em toda a consulta
    HoraUtc = DateTimeZone.FixedUtcNow(),
    // Converte para o fuso de Brasília (UTC−3) e descarta o fuso depois da conversão
    HoraLocal = DateTimeZone.RemoveZone ( DateTimeZone.SwitchZone ( HoraUtc, -3, 0 ) ),
    Resultado = #table ( type table [ #"Data da Atualização" = datetime ], { { HoraLocal } } )
in
    Resultado
DEFINE
    MEASURE TabIntAtualizacao[Última Atualização] =
        "Atualizado em "
            & FORMAT ( MAX ( TabIntAtualizacao[Data da Atualização] ), "dd/mm/yyyy hh:nn" )

EVALUATE
{ [Última Atualização] }
  • ⚠️ O deslocamento -3 é o de Brasília, que não tem horário de verão desde 2019 (Decreto 9.772/2019). O Brasil tem quatro fusos, de UTC−2 a UTC−5: ajuste o valor para usuários de outras regiões.
  • 💡 TabIntAtualizacao[Data da Atualização] também serve de "hoje" para medidas como Dias sem Venda e Projeção do Mês (Cookbook, caso 10).

1.1 Farol de frescor do refresh

A medida de última atualização diz quando o modelo foi atualizado; o farol diz se isso é um problema. Compare a data do refresh com o "hoje" no fuso local: ✔️ atualizado hoje, ⚠️ ontem, ❗ antes de ontem (provável falha no refresh agendado). O formato e o idioma vêm da configuração em cascata (item 3).

DEFINE
    MEASURE TabIntMedida[Idioma do Projeto] =
        "pt-BR"

    MEASURE TabIntMedida[Formato de Data] =
        SWITCH (
            [Idioma do Projeto],
            "en-US", "ddd mm/dd/yyyy",
            "de-DE", "ddd dd.mm.yyyy",
            "ddd dd/mm/yyyy"
        )

    MEASURE TabIntAtualizacao[Status da Atualização] =
        VAR vMomento = MAX ( TabIntAtualizacao[Data da Atualização] )
        -- DATE ( ANO, MÊS, DIA ) descarta a hora sem passar por texto
        VAR vData = DATE ( YEAR ( vMomento ), MONTH ( vMomento ), DAY ( vMomento ) )
        -- "Hoje" em Brasília: UTCNOW é igual no Desktop e no Service
        VAR vAgora = UTCNOW () - TIME ( 3, 0, 0 )
        VAR vHoje = DATE ( YEAR ( vAgora ), MONTH ( vAgora ), DAY ( vAgora ) )
        VAR vEmoji =
            SWITCH (
                TRUE (),
                vData = vHoje, "✔️",
                vData = vHoje - 1, "⚠️",
                "❗"
            )
        RETURN
            IF (
                NOT ISBLANK ( vMomento ),
                vEmoji & " " & FORMAT ( vMomento, [Formato de Data] & " hh:nn", [Idioma do Projeto] )
            )

EVALUATE
{ [Status da Atualização] }
  • ⚠️ Não use DATEVALUE numa data e hora: ela espera texto e depende da conversão implícita pela localidade.
  • 💡 Refresh só em dia útil: com refresh agendado de segunda a sexta, a segunda-feira mostra ❗ para o refresh de sexta. Troque vHoje - 1 pelo último dia útil se isso for esperado.
  • 💡 Resultado num modelo real: ✔️ sáb 26/09/2026 16:49.

2. USERCULTURE() e o calendário

USERCULTURE() devolve o idioma de quem está vendo o relatório em medidas, regras de RLS e itens de cálculo. Numa tabela ou coluna calculada em modo Import, ela é avaliada no refresh: o resultado fica fixo e, no refresh agendado, usa uma cultura invariável, e não a de quem abre o relatório.

Por isso o calendário usa uma localidade definida pelo autor do modelo, e não detectada automaticamente.

  • 💡 Colunas calculadas com Expression Context = User Context (nível de compatibilidade 1705) são avaliadas na consulta e aceitam USERCULTURE(). Como não são materializadas, não servem para gerar uma tabela calendário.

3. Configuração em cascata: uma medida, vários comportamentos

Uma medida manual guarda a localidade do projeto; as outras derivam dela. Trocar "pt-BR" por "en-US" num só lugar muda idioma, hemisfério, início da semana e formato de data, sem risco de metade do relatório ficar em cada idioma. Nos exemplos, as medidas ficam em TabIntMedida, a tabela de medidas do modelo; qualquer tabela serve.

DEFINE
    MEASURE TabIntMedida[Idioma do Projeto] =
        "pt-BR"   -- fonte única: troque só aqui

    MEASURE TabIntMedida[Hemisfério do Projeto] =
        -- Por localidade completa: pt-BR e pt-PT têm o mesmo idioma e hemisférios opostos
        SWITCH (
            [Idioma do Projeto],
            "pt-PT", "Norte", "es-ES", "Norte", "en-US", "Norte",
            "fr-FR", "Norte", "de-DE", "Norte", "it-IT", "Norte",
            "Sul"   -- pt-BR, es-AR, en-AU e demais
        )

    MEASURE TabIntMedida[Primeiro Dia da Semana do Projeto] =
        -- Códigos de WEEKNUM: 1 = domingo, 2 = segunda-feira
        SWITCH ( [Idioma do Projeto], "en-US", 1, 2 )

    MEASURE TabIntMedida[Formato de Data] =
        SWITCH (
            [Idioma do Projeto],
            "en-US", "ddd mm/dd/yyyy",
            "de-DE", "ddd dd.mm.yyyy",
            "ddd dd/mm/yyyy"
        )

EVALUATE
ROW (
    "Idioma", [Idioma do Projeto],
    "Hemisfério", [Hemisfério do Projeto],
    "Primeiro dia", [Primeiro Dia da Semana do Projeto],
    "Hoje formatado", FORMAT ( TODAY (), [Formato de Data], [Idioma do Projeto] )
)

As medidas alimentam direto a UDF DataHora.Calendario da Code Library, que já recebe localidade, início da semana e hemisfério:

TabDimCalendario =
DataHora.Calendario (
    DATE ( 2023, 1, 1 ),
    DATE ( 2026, 12, 31 ),
    [Idioma do Projeto],                  -- nomes de mês, dia e estação
    [Primeiro Dia da Semana do Projeto],  -- WEEKNUM e Semana do Mês
    1,                                    -- ano fiscal começando em janeiro
    [Hemisfério do Projeto]               -- "Norte" ou "Sul": estação do ano
)
  • 💡 Tabela calculada pode usar medida: o valor é lido no refresh, sem filtro. Por isso a localidade é uma constante, e não USERCULTURE().
  • ⚠️ Depois de criar a tabela, marque-a como Tabela de Datas.

4. O mesmo padrão em outros domínios

Moeda de exibição: a medida escolhe o símbolo; para o número manter o tipo numérico no gráfico, aplique-o numa cadeia de formato dinâmica, e não com FORMAT, que transforma o valor em texto.

DEFINE
    MEASURE TabIntMedida[Moeda do Projeto] =
        "BRL"

    MEASURE TabIntMedida[Formato da Moeda] =
        SWITCH (
            [Moeda do Projeto],
            "USD", "\$#,0.00",
            "EUR", "€ #,0.00",
            "JPY", "¥ #,0",        -- iene sem casas decimais
            "R\$ #,0.00"
        )

EVALUATE
ROW ( "Moeda", [Moeda do Projeto], "Cadeia de formato", [Formato da Moeda] )

Na medida de valor, em Formato › Dinâmico, use [Formato da Moeda] como cadeia de formato.

Unidade de medida: o mesmo esqueleto serve para rótulo e fator de conversão.

DEFINE
    MEASURE TabIntMedida[Sistema de Unidades] =
        "Métrico"

    MEASURE TabIntMedida[Rótulo de Distância] =
        SWITCH ( [Sistema de Unidades], "Imperial", "mi", "km" )

    MEASURE TabIntMedida[Fator de Distância] =
        SWITCH ( [Sistema de Unidades], "Imperial", 0.621371, 1 )

EVALUATE
ROW ( "Rótulo", [Rótulo de Distância], "Fator", [Fator de Distância] )
  • ⚠️ Uma medida não troca o agrupamento de um visual: devolver o nome de uma coluna como texto não muda o eixo. Para o leitor escolher a granularidade (dia, semana, mês), use parâmetros de campo.

5. Quando usar a cascata e quando usar um parâmetro

SituaçãoUse
O autor decide uma vez por modelo (template regional, idioma do cliente)Medida em cascata
Três ou mais comportamentos precisam mudar juntosMedida em cascata
O valor precisa chegar a uma tabela calculada, como o calendárioMedida em cascata
O leitor escolhe no relatório, com segmentaçãoParâmetro numérico ou parâmetro de campo

Fontes