"Quem é o nosso cliente ideal?" Essa pergunta apareceu numa reunião de liderança e ninguém tinha uma resposta baseada em dado. Tinha opinião. Tinha o palpite do time de vendas. Tinha a intuição de quem estava há mais tempo na casa. Cada um puxava para um lado.
O custo disso é concreto. A empresa gastava a maior parte do orçamento de mídia trazendo volume, sem saber se aquele volume tinha alguma chance de virar cliente de verdade. O time de pré-vendas priorizava contas no feeling. O CRM disparava campanha para todo mundo igual.
A Cursora já sabia definir "ativação" (quando um instrutor faz a primeira venda de curso). O que faltava era o degrau seguinte. Entre os clientes ativos, quais realmente sustentam o negócio? E o que eles têm em comum que dê para procurar lá na porta de entrada?
Essa era a dor. Traduzir a carteira num perfil acionável.
A Cursora é um marketplace self-service de cursos online, onde instrutores publicam e vendem seus próprios cursos. O instrutor entra pelo app (iOS ou Android) ou pela web, monta o curso, define o preço e publica pra venda. Parte dos instrutores configura uma página de vendas própria, fora do checkout padrão da Cursora, integrada com ferramenta de pagamento e e-mail marketing.
O modelo de receita é simples. A Cursora ganha uma margem por venda de curso. Quanto mais o instrutor vende, mais a Cursora fatura.
O funil de referência tem alguns marcos: instalação, primeira montagem de curso, primeira venda de curso (ativação) e recorrência. Existe também um programa de indicação (member-get-member), onde clientes convidam outros clientes.
Guardando essa mecânica, dá para entender por que o melhor cliente aqui é medido por volume de vendas de curso e receita gerada. Não por número de logins ou tamanho declarado.
Adaptar um método clássico de descoberta de ICP (Curva ABC seguida de análise de padrões) para a realidade self-service da Cursora, e entregar três coisas:
Tudo validado contra a fonte oficial antes de virar conclusão.
Google BigQuery como warehouse, com SQL puro para toda a modelagem, uso pesado de CTEs e window functions (soma acumulada para a curva, partição por dimensão para o lift) e arquitetura em camadas no estilo medallion: bronze para dado cru, silver para dado limpo, gold para dado de negócio.
As fontes foram a base transacional de pedidos (grão pedido), a tabela de perfil de cliente (grão cliente), eventos de app vindos de um mobile measurement partner e web analytics, cada uma com seu papel, sem misturar. Toda query de análise foi reconciliada contra o dashboard oficial de BI da empresa antes de qualquer conclusão seguir.
Para entrega, gerei o deck executivo em código com PptxGenJS (Node), versionável e reproduzível, e o documento técnico com docx-js. Usei Chart.js para dashboards HTML autocontidos quando precisei. Para QA dos entregáveis, rodei LibreOffice em modo headless mais pdftoppm, renderizando cada slide em imagem pra revisar visualmente antes de entregar, o que pega overflow de texto e desalinhamento que passam batido no código.
Trabalhei sobre duas tabelas gold, cada uma com um grão claro.
A primeira é a base transacional de pedidos. Uma linha por pedido, com identificador do cliente, data, valores cobrados, margem da plataforma, canal de venda, integração de origem e os carimbos de status (vendeu, aluno acessou o curso, cancelou).
A segunda é a tabela de perfil do cliente. Uma linha por cliente, já consolidada. Tem região, faixa de volume de vendas de curso, sistema operacional, se tem página de vendas própria configurada, papel no programa de indicação, tempo de casa e uma série de marcos de venda (data da primeira venda de curso, da segunda, dias até a segunda, e assim por diante).
Essa separação importa. A tabela de pedidos me dá volume e receita por cliente. A tabela de perfil me dá os atributos descritivos. O ICP nasce do cruzamento das duas.
O método clássico de ICP foi desenhado para venda B2B com carteira e vendedor. A Cursora é self-service. Então a primeira coisa foi traduzir os conceitos.
Onde o método fala em receita gerada pelo cliente, usei volume de vendas de curso e GMV por cliente. Onde fala em vendedor responsável, usei página de vendas própria configurada. Onde fala em tipo de contrato, usei plataforma e integração. Onde fala em tempo de relacionamento, usei tempo de casa.
O melhor cliente da Cursora é quem vende muito curso, com valor e recorrência. Simples assim.
Aqui está a parte que separa análise séria de dashboard bonito.
Antes de rodar qualquer curva, eu precisava ter certeza de que a minha definição de "venda de curso" batia com a definição oficial da empresa. Tinha uma ambiguidade real: venda de curso (curso comprado e pago) e acesso ao curso (aluno efetivamente entrou e assistiu à primeira aula) são eventos diferentes, e usar um no lugar do outro muda toda a análise.
Levantei uma hipótese. A métrica oficial de ativação seria a coorte de primeira venda de curso por mês. Escrevi a query, contei clientes por mês da primeira venda de curso e comparei com o dashboard de BI da liderança.
Bateu. Diferença zero em todos os meses fechados, com variação de no máximo um cliente por ruído de snapshot. O único mês com diferença maior era o mês corrente, ainda parcial.
Depois fiz o mesmo com o gate de venda na tabela de pedidos, para garantir que o carimbo que eu ia usar reproduzia exatamente a mesma primeira venda de curso. Bateu de novo.
Só então segui. Toda a Curva ABC parte de uma régua auditada, não de palpite.
Com a régua travada, ordenei os clientes por GMV, calculei o acumulado com window function e classifiquei em três curvas. Curva A até 80% do GMV acumulado, Curva B de 80 a 95%, Curva C o resto.
Trouxe GMV e margem da plataforma lado a lado na mesma query, de propósito, para decidir o eixo vendo o dado em vez de escolher no escuro. As duas contaram a mesma história, então mantive GMV como eixo.
Curva A isolada, a pergunta vira: o que essa gente tem em comum?
A técnica é lift. Para cada característica, comparo a participação dela dentro da Curva A com a participação na base inteira. Lift acima de 1 significa sobre-representação, ou seja, aquela característica aparece mais nos melhores clientes do que na média. Lift abaixo de 1 é anti-padrão.
Empilhei todas as dimensões numa query só (região, porte, plataforma, integração, indicação, tempo de casa, velocidade de ativação) e ordenei por lift dentro de cada dimensão. Um retrato só, direto ao ponto.
Com os lifts na mão, o perfil se desenhou quase sozinho. Escrevi o ICP em uma frase e destilei três critérios objetivos, todos observáveis cedo, para o time comercial conseguir agir antes de o cliente virar Curva A.
Por último, transformei os três critérios num score aditivo por cliente e materializei uma tabela com o score e uma classificação (Alto, Médio, Baixo). Essa é a entrega que o CRM e a pré-vendas usam sem precisar entender de SQL.
(números ilustrativos e anonimizados: preservam a forma dos achados, não os valores reais)
Uma fatia de 12% dos clientes gerou cerca de 79% do GMV. A concentração ficou até maior que o benchmark de mercado, que costuma falar em 15 a 20%.
| Curva | Clientes | % Clientes | Vendas de curso | % GMV | Taxa de acesso |
|---|---|---|---|---|---|
| A | 33.600 | 12,0% | ~3,7 mi | 79,0% | 94% |
| B | 70.000 | 25,0% | ~0,9 mi | 15,5% | 92% |
| C | 176.400 | 63,0% | ~0,4 mi | 5,5% | 90% |
Em seis meses, o cliente médio da Curva A vendeu cerca de 110 cursos. O da Curva B, 12. O da Curva C, 2.
Em receita por cliente, um Curva A vale mais de cem vezes um Curva C. A qualidade acompanha o valor: a taxa de acesso ao curso comprado sobe junto com a curva (aluno de instrutor Curva A entra e usa o que comprou, o que reduz risco de reembolso), e o ticket médio por venda também.
Ordenados por poder preditivo:
| Sinal | Lift | Leitura |
|---|---|---|
| Porte: 30+ vendas de curso por mês | ~6,2 | o divisor de águas, disparado o mais forte |
| Página de vendas própria configurada (fora do checkout padrão da Cursora) | ~3,3 | instrutor que investe em infraestrutura de venda própria tende a ser o melhor cliente |
| Indicação: perfil "convidante" | ~1,9 | quem indica, não quem foi indicado |
| Velocidade: 2ª venda de curso em até 7 dias | ~1,8 | a régua de ativação se valida como preditora |
| Tempo de casa: 1+ ano | ~1,6 | maturidade e lealdade |
| Plataforma: iOS ou navegador | ~1,3 | Android fica abaixo da média |
Região deu ruído. Nenhum estado passou de lift 1,3. Não vale segmentar por geografia, e isso já economiza esforço de quem ia tentar.
O programa de indicação tinha fama de canal de alta qualidade. Os dados mostraram uma nuance.
Quem convida outros clientes tem lift alto, perto de 1,9. Quem entrou via indicação tem lift baixo, perto de 0,5. Ou seja, sub-representado na Curva A.
O valor está nos bons clientes indicarem. Os indicados, em média, não viram bons clientes. Isso muda a leitura de qualidade do canal e virou uma pergunta aberta para o time antes de escalar aquisição por indicação.
Instrutor com operação recorrente (30 ou mais vendas de curso por mês), que ativa rápido (segunda venda de curso em até sete dias), tende a ter página de vendas própria configurada, já tem mais de um ano de casa e opera por iOS ou navegador.
1. Frequência e porte. Faixa de vendas de curso a partir de 10 por mês, alvo 30 ou mais. Peso máximo. É o sinal mais forte de todos.
2. Página de vendas própria configurada. Checkout fora do padrão da Cursora, integrado com pagamento e e-mail marketing. Demonstra investimento em infraestrutura própria, sinal de operação séria.
3. Velocidade de ativação. Cruzou a marca de duas vendas de curso em sete dias. Sinal precoce e barato de observar.
Boosters secundários: tempo de casa acima de um ano, iOS ou web, e perfil convidante no programa de indicação. Anti-ICP, para despriorizar: zero a uma venda de curso, sem segunda venda em sete dias, Android puro, e menos de 90 dias com volume baixo.
O projeto gerou três artefatos, pensados para públicos diferentes.
Um deck executivo em 16:9, para a liderança. Estrutura de storytelling em cinco atos, do problema ao próximo passo.
Um documento técnico para o time, com metodologia, definições validadas e todas as queries, para qualquer pessoa replicar.
Uma tabela de score materializada no warehouse, para pré-vendas e CRM priorizarem por classificação de ICP.
A liderança saiu da reunião seguinte com uma definição de cliente ideal baseada em dado. Não em opinião.
O time de aquisição ganhou um alvo claro para mídia e um anti-alvo para parar de queimar orçamento. A pré-vendas ganhou uma fila priorizada. O CRM ganhou um critério para segmentar disparo em vez de tratar todo mundo igual.
E a área ganhou uma capacidade nova. O score é recalculável a qualquer momento, então o ICP deixa de ser um slide de uma reunião e vira um processo vivo.
Validar primeiro, analisar depois. Foi o que deu confiança para a liderança comprar o resultado. Se eu tivesse pulado a reconciliação, a primeira pergunta cética teria derrubado tudo.
Uma query por vez. Construí a base incrementalmente, validando cada etapa antes de seguir. Menos elegante que um script gigante. Bem mais confiável.
Escolher o eixo vendo o dado. Trouxe GMV e margem lado a lado antes de decidir por qual ranquear. Evita viés de quem escolhe a métrica que confirma a hipótese.
Lift em vez de contagem simples. Contar quantos Curva A são de São Paulo enganaria, porque São Paulo é grande em tudo. Lift corrige pelo tamanho da base e revela o que é sinal de verdade.
Deck e documento em código. Gerar apresentação por PptxGenJS parece overkill até a terceira revisão. Aí o valor aparece. Muda um número, roda o script, tudo se atualiza, e o QA visual pega o que os olhos cansados não pegam.
Investigar a fundo a qualidade do canal de indicação, já que os indicados não viram bons clientes na média. Testar o score em produção, comparando a conversão de contas priorizadas contra um grupo de controle. E agendar a recarga mensal do score, para o ICP acompanhar a base em vez de envelhecer num slide. É a mesma lógica de score simples e recalculável que usei no case de Signals, ali pra prever downgrade em vez de priorizar cliente ideal.
icp-curva-abc/
├── README.md # visão geral do projeto e como navegar
├── docs/
│ ├── case-study.md # este documento
│ ├── metodologia.md # detalhe passo a passo
│ └── deck-executivo.pdf # versão exportada do deck
├── sql/
│ ├── 00_validacao_ativacao.sql # reconciliação contra o BI oficial
│ ├── 01_curva_abc_base.sql # materializa cliente + curva + perfil
│ ├── 02_padroes_lift.sql # padrões da Curva A por lift
│ └── 03_score_icp.sql # base pronta com score por cliente
├── build/
│ ├── build_deck.js # gera o deck executivo (PptxGenJS)
│ └── build_doc.js # gera o documento técnico (docx-js)
└── assets/
└── charts/ # imagens e dashboards de apoio
Nomes de tabela e campo são genéricos. analytics.fct_orders é a base de pedidos (grão pedido), analytics.dim_sellers é o perfil do cliente (grão cliente, aqui o instrutor).
-- Hipótese: "ativação" = coorte de primeira venda de curso por mês.
-- Objetivo: bater com o dashboard oficial antes de confiar na régua.
WITH venda AS (
SELECT FORMAT_DATE('%Y-%m', first_order_date) AS mes,
COUNT(DISTINCT seller_id) AS ativacao_venda
FROM `analytics.dim_sellers`
WHERE first_order_date BETWEEN '2026-01-01' AND '2026-06-30'
GROUP BY mes
),
acesso AS (
SELECT FORMAT_DATE('%Y-%m', first_access_date) AS mes,
COUNT(DISTINCT seller_id) AS ativacao_acesso
FROM `analytics.dim_sellers`
WHERE first_access_date BETWEEN '2026-01-01' AND '2026-06-30'
GROUP BY mes
)
SELECT COALESCE(v.mes, c.mes) AS mes, v.ativacao_venda, c.ativacao_acesso
FROM venda v
FULL OUTER JOIN acesso c USING (mes)
ORDER BY mes;
-- Gate de venda validado: payment_confirmed_at IS NOT NULL.
-- Acesso ao curso = completed_at IS NOT NULL (lente de qualidade: aluno acessou o curso depois de comprar).
CREATE OR REPLACE TABLE `analytics.seller_curva_abc_2026h1` AS
WITH pedidos AS (
SELECT
seller_id, order_id,
COALESCE(revenue_charged, 0) AS gmv_pedido,
COALESCE(platform_fee, 0) AS receita_plataforma,
(payment_confirmed_at IS NOT NULL) AS vendeu,
(completed_at IS NOT NULL) AS acessou
FROM `analytics.fct_orders`
WHERE order_date BETWEEN '2026-01-01' AND '2026-06-30'
AND seller_id IS NOT NULL
),
por_cliente AS (
SELECT seller_id,
COUNTIF(vendeu) AS vendas,
COUNTIF(acessou) AS acessos,
SUM(IF(vendeu, gmv_pedido, 0)) AS gmv,
SUM(IF(vendeu, receita_plataforma, 0)) AS receita_plataforma
FROM pedidos
GROUP BY seller_id
HAVING vendas > 0
),
ranked AS (
SELECT *,
SUM(gmv) OVER () AS gmv_geral,
SUM(gmv) OVER (ORDER BY gmv DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS gmv_acum
FROM por_cliente
),
classificado AS (
SELECT *,
CASE WHEN gmv_acum/gmv_geral <= 0.80 THEN 'A'
WHEN gmv_acum/gmv_geral <= 0.95 THEN 'B'
ELSE 'C' END AS curva
FROM ranked
)
SELECT
c.seller_id, c.curva, c.vendas, c.acessos, c.gmv, c.receita_plataforma,
SAFE_DIVIDE(c.acessos, c.vendas) AS taxa_acesso,
SAFE_DIVIDE(c.gmv, c.vendas) AS ticket_medio,
d.state, d.volume_tier, d.platform_os, d.canal_integration,
d.referral_role, d.seller_level, d.signup_date, d.days_to_second_order,
DATE_DIFF(DATE '2026-06-30', d.signup_date, DAY) AS tenure_dias,
(d.days_to_second_order <= 7) AS icp_2em7
FROM classificado c
LEFT JOIN `analytics.dim_sellers` d USING (seller_id);
WITH base AS (
SELECT curva, state, volume_tier, platform_os, canal_integration,
referral_role, icp_2em7,
CASE WHEN tenure_dias <= 90 THEN '0-90d'
WHEN tenure_dias <= 180 THEN '91-180d'
WHEN tenure_dias <= 365 THEN '181-365d'
WHEN tenure_dias <= 730 THEN '1-2 anos'
ELSE '2+ anos' END AS tenure_faixa
FROM `analytics.seller_curva_abc_2026h1`
),
empilhado AS (
SELECT 'regiao' AS dimensao, IFNULL(state,'(nulo)') AS valor, curva FROM base
UNION ALL SELECT 'porte', IFNULL(volume_tier,'(nulo)'), curva FROM base
UNION ALL SELECT 'plataforma', IFNULL(platform_os,'(nulo)'), curva FROM base
UNION ALL SELECT 'integracao', IFNULL(canal_integration,'(sem)'), curva FROM base
UNION ALL SELECT 'indicacao', IFNULL(referral_role,'(nenhum)'), curva FROM base
UNION ALL SELECT 'tenure', tenure_faixa, curva FROM base
UNION ALL SELECT 'vel_2em7', CAST(icp_2em7 AS STRING), curva FROM base
),
agg AS (
SELECT dimensao, valor,
COUNTIF(curva='A') AS clientes_A,
COUNT(*) AS clientes_total
FROM empilhado GROUP BY dimensao, valor
)
SELECT dimensao, valor, clientes_A, clientes_total,
ROUND(clientes_A / SUM(clientes_A) OVER (PARTITION BY dimensao)*100, 1) AS pct_dentro_A,
ROUND(clientes_total / SUM(clientes_total) OVER (PARTITION BY dimensao)*100, 1) AS pct_base,
ROUND(SAFE_DIVIDE(
clientes_A / SUM(clientes_A) OVER (PARTITION BY dimensao),
clientes_total / SUM(clientes_total) OVER (PARTITION BY dimensao)), 2) AS lift
FROM agg
WHERE clientes_total >= 100
ORDER BY dimensao, lift DESC;
CREATE OR REPLACE TABLE `analytics.seller_icp_score` AS
WITH s AS (
SELECT seller_id, state, platform_os, volume_tier, canal_integration,
referral_role, signup_date, days_to_second_order,
DATE_DIFF(CURRENT_DATE(), signup_date, DAY) AS tenure_dias
FROM `analytics.dim_sellers`
),
scored AS (
SELECT *,
CASE volume_tier -- Critério 1: porte (0-50)
WHEN '100+' THEN 50 WHEN '30-99' THEN 42
WHEN '10-29' THEN 28 WHEN '7-9' THEN 12
WHEN '2-6' THEN 5 ELSE 0 END AS pts_porte,
CASE WHEN canal_integration IS NOT NULL
AND LOWER(canal_integration) NOT IN ('none','')
THEN 25 ELSE 0 END AS pts_integracao, -- Critério 2: página de vendas própria (0-25)
CASE WHEN days_to_second_order <= 7 THEN 25
WHEN days_to_second_order <= 14 THEN 12
ELSE 0 END AS pts_velocidade, -- Critério 3: velocidade (0-25)
(CASE WHEN tenure_dias >= 365 THEN 5 ELSE 0 END)
+ (CASE WHEN platform_os IN ('iOS','browser') THEN 5 ELSE 0 END)
+ (CASE WHEN referral_role IN ('inviter','both') THEN 5 ELSE 0 END)
AS pts_boost -- boosters (0-15)
FROM s
)
SELECT *,
pts_porte + pts_integracao + pts_velocidade + pts_boost AS icp_score,
CASE WHEN pts_porte + pts_integracao + pts_velocidade + pts_boost >= 70 THEN 'ICP Alto'
WHEN pts_porte + pts_integracao + pts_velocidade + pts_boost >= 40 THEN 'ICP Médio'
ELSE 'ICP Baixo' END AS icp_tier
FROM scored;
30 minutos, de graça. Olho sua base e mostro se o seu ICP é opinião ou é dado.
Quero meu diagnóstico grátis →