Perth.jl: cronogramas de projeto que você pode calcular

O que é o Perth.jl, os conceitos por trás dele (EAP, CPM, folga, caminho crítico, PERT), como instalar, como funciona por dentro e um exemplo real completo: o cronograma de um estudo de tempos de atendimento, do REPL ao navegador.

Todo mundo que já coordenou um projeto — uma pesquisa, uma obra, um TCC com prazo de banca — passou pelo mesmo momento: alguém pergunta “e se essa etapa atrasar dois dias, o que mais anda?”, e a resposta exige redesenhar o cronograma à mão.

O problema não é a pergunta. É que o cronograma virou figura. Uma planilha com barrinhas coloridas, ou um PNG exportado de alguma ferramenta, é ótima para apresentar e péssima para calcular. Ninguém consegue perguntar a uma imagem qual tarefa é o gargalo, quanto de folga sobra, ou qual a probabilidade de entregar antes do dia 30.

Este post apresenta o Perth.jl, um pacote Julia que escrevi justamente para tratar cronograma como aquilo que ele é: uma estrutura de dados sobre a qual se calcula. Vamos ver o conceito, a instalação, como ele funciona por dentro e, no fim, um exemplo real do começo ao fim.


1. O que é o Perth.jl

O Perth.jl é um motor de caminho crítico (CPM) em Julia, com uma interface web local acoplada. A frase que resume o projeto é: o modelo e o cálculo vivem em Julia; o navegador é uma vista do plano, não a fonte da verdade.

Na prática, isso significa três coisas:

  1. Você monta o plano em código. Tarefas, dependências, responsáveis, prazos — tudo com funções normais de Julia, versionáveis no git como qualquer script.
  2. Você faz perguntas ao plano. schedule!, critical_path, slack, workload, pert_finish devolvem linhas compatíveis com Tables.jl — ou seja, caem direto num DataFrame, num CSV, num gráfico.
  3. Você (e sua equipe) edita no navegador. Perth.run() sobe um servidor local no espírito do Pluto.jl: arrastar uma barra no Gantt altera o objeto no REPL, e alterar o objeto no REPL recarrega o navegador. É o mesmo dado, ao vivo, nos dois lados.
Interface web do Perth.jl mostrando um gráfico de Gantt com estrutura analítica de projeto, barras de tarefas, setas de dependência e painel lateral
Figura 1: a interface do Perth.jl no navegador. A mesma tela existe em cinco idiomas (português, inglês, espanhol, francês e chinês) e edita o mesmo projeto que está aberto no REPL.

O pacote é MIT, exige Julia ≥ 1.10 e não tem nenhum passo de build no front-end: nada de node_modules, nada de framework. O navegador recebe HTML, CSS e JavaScript puros servidos pelo próprio Julia.


2. Os conceitos por trás

Vale gastar alguns parágrafos com o vocabulário, porque ele é o que dá sentido a cada função da API. Quem já conhece CPM pode pular para a seção 3.

2.1 Tarefa, EAP e rolagem

A unidade é a tarefa: um nome, uma data de início, uma duração, opcionalmente um responsável, um custo, um esforço e um percentual concluído. Tarefas se organizam em hierarquia através do campo parent, formando a EAP (Estrutura Analítica do Projeto, ou WBS em inglês).

Uma tarefa que tem filhas vira resumo: sua data e sua duração deixam de ser digitadas e passam a ser calculadas a partir das filhas. É por isso que, no exemplo mais adiante, a fase “1. Coleta e preparação” nasce com duration = 1 e termina com 28 dias — ninguém digitou 28.

2.2 Dependências: quatro maneiras de dizer “depois”

A dependência padrão é término-início: a tarefa B só começa quando A terminar. Mas a vida real precisa de mais nuance, e o Perth aceita as variantes clássicas na própria string do identificador:

julia
dependencies = [a.id]           # término-início: b começa depois que a termina
dependencies = ["$(a.id)+3"]    # término-início com 3 dias de defasagem (lag)
dependencies = ["SS:$(a.id)"]   # início-início: b começa quando a começa
dependencies = ["SS:$(a.id)+2"] # início-início com 2 dias de defasagem
dependencies = ["FF:$(a.id)"]   # término-término: b não pode terminar antes de a

A defasagem (lag) é o número depois do +. O início-início com defasagem é especialmente útil para trabalho que “acompanha” outro: o ajuste de um modelo pode começar dois dias depois que a análise descritiva começou, sem esperar ela acabar.

2.3 CPM: as duas passadas

schedule!(p) roda o Método do Caminho Crítico. São duas varreduras do grafo de dependências.

Na passada para frente, cada tarefa é empurrada para o mais cedo que ela pode acontecer:

\[ES_j = \max_{i \to j} \left( EF_i \right) + 1, \qquad EF_j = ES_j + d_j - 1\]

Na passada para trás, partindo do fim do projeto, calcula-se o mais tarde que cada tarefa pode acontecer sem atrasar o todo:

\[LF_i = \min_{i \to j} \left( LS_j \right) - 1, \qquad LS_i = LF_i - d_i + 1\]

A diferença entre as duas é a folga:

\[\text{folga}_i = LS_i - ES_i\]

Tarefas com folga zero não podem escorregar um dia sequer sem empurrar a entrega: são o caminho crítico. É a informação que uma figura de cronograma nunca dá de graça.

Um detalhe importante do Perth: schedule! só empurra tarefas para frente, nunca puxa para trás. A data que você digitou funciona como restrição de “não antes de”. Isso evita que o motor invente para você um plano que começa antes do que a realidade permite.

2.4 Prazo não é o mesmo que data

Aqui está a decisão de projeto que mais muda a experiência de uso. Existem dois tipos de compromisso, e o Perth trata cada um do seu jeito:

  • deadline — um prazo assumido com alguém de fora (a submissão do congresso, a entrega ao cliente). Ele nunca move uma tarefa. O que ele faz é limitar a passada para trás: se o plano não cabe, a folga da tarefa e de tudo que a alimenta fica negativa. O atraso aparece como número, em vez de sumir num arraste de barra.
  • pinned — uma data contratada que já não se negocia (a reunião marcada, o dia da coleta em campo). O schedule! a deixa quieta; se o resto do plano deixou de caber a partir dali, isso aparece como um early_start posterior ao início da tarefa.

2.5 Dias úteis

Com o BusinessDays.jl carregado, set_calendar!(p, "Brazil") faz o motor contar durações em dias úteis, feriados nacionais incluídos. Uma tarefa de cinco dias que começa numa quinta termina na quarta seguinte, e o 2 de novembro não conta.

2.6 Recursos: capacidade e esforço

Cada pessoa pode declarar uma capacity — quanto trabalho ela absorve num dia útil — e cada tarefa um effort, na mesma unidade. Com os dois números, workload(p) devolve a carga diária por pessoa e overallocations(p) aponta os dias em que alguém foi alocado além do que cabe. level!(p) vai além e reprograma: empurra o que tem folga até ninguém estourar a capacidade, começando por quem tem mais folga — de modo que o caminho crítico e as tarefas com prazo são as últimas a ceder.

2.7 PERT: quando a duração é um chute

Duração é quase sempre uma estimativa. O PERT formaliza isso pedindo três números por tarefa — otimista $o$, mais provável $m$ e pessimista $p$ — e resumindo-os numa duração esperada e num desvio padrão:

\[t_e = \frac{o + 4m + p}{6}, \qquad \sigma = \frac{p - o}{6}\]

A variância se acumula ao longo do caminho crítico, e o término do projeto passa a ter uma distribuição em vez de uma data:

\[\sigma_{\text{projeto}} = \sqrt{\sum_{i \,\in\, \text{crítico}} \sigma_i^2}, \qquad P(\text{término} \le D) = \Phi\!\left( \frac{D - E}{\sigma_{\text{projeto}}} \right)\]

O Perth implementa a fórmula clássica (pert_finish, finish_probability, pert_date) e também uma simulação de Monte Carlo (pert_simulate) que sorteia durações de todas as tarefas estimadas e roda o CPM milhares de vezes. A diferença entre as duas respostas é instrutiva, e vamos ver isso acontecer no exemplo.


3. Instalação

O pacote está no registro geral, então:

julia
using Pkg
Pkg.add("Perth")

Três dependências são opcionais e detectadas automaticamente — o pacote funciona sem elas, e ganha recursos com elas:

Pacote O que habilita
BusinessDays calendários de dias úteis (set_calendar!(p, "Brazil"))
CairoMakie figuras estáticas do Gantt (ganttplot, save_chart)
QRCoders QR code do link quando você compartilha na rede local
julia
Pkg.add(["BusinessDays", "CairoMakie", "QRCoders"])

Para abrir a interface:

julia
using Perth

Perth.run()          # abre http://localhost:8123 no navegador

E um plano mínimo, só para sentir a API:

julia
using Perth

p = create_project("Meu primeiro plano")

a = add_task!(p, "Escrever o roteiro"; start = Date(2026, 9, 1), duration = 5, assignee = "Ana")
b = add_task!(p, "Gravar";             duration = 3, dependencies = [a.id], assignee = "Bruno")
c = add_task!(p, "Editar";             duration = 4, dependencies = [b.id], assignee = "Bruno")

schedule!(p)
project_finish(p)

4. Como funciona por dentro

A arquitetura cabe em um desenho:

arquitetura
REPL  ──►  AppState (memória + contador de revisão)  ◄──  API HTTP
             │                                                  ▲
             ▼                                                  │
      JSON em ~/.perth                                  Navegador (JS/CSS puros)
      espelho .perth.jl                                 presença via WebSocket

O estado do projeto vive em memória, num AppState com um contador de revisão. O REPL escreve nele chamando as funções da API; o navegador escreve nele por HTTP. O contador de revisão é o que faz o navegador perceber que algo mudou do outro lado e recarregar — e o WebSocket carrega a presença (os cursores de quem mais está olhando o plano).

Em disco, cada projeto é um JSON em ~/.perth/ (mude com a variável PERTH_DATA_DIR ou com Perth.run(data_dir = ...)). Além dele, existe o formato .perth.jl, que é o plano escrito como código Julia legível — feito para entrar no git e produzir diffs que uma pessoa consegue ler:

julia
Perth.save(p, "planos/estudo.perth.jl")   # texto legível, versionável no git
q = Perth.load("planos/estudo.perth.jl")  # leitor restrito, sem eval

set_file_path!(p, "planos/estudo.perth.jl")   # espelho automático a cada mudança

O leitor de .perth.jl é restrito: ele aceita apenas os construtores do próprio pacote (Project, GanttTask, Person, Date e alguns outros) e não usa eval. Abrir o arquivo de um colega não executa o código dele.

E como toda função de consulta devolve linhas Tables.jl, o plano conversa com o resto do ecossistema sem adaptador:

julia
using CSV, DataFrames

CSV.write("tarefas.csv", DataFrame(tasktable(p)))
write("submissao.ics", icalendar(p))       # marcos e prazos no calendário

5. Exemplo real: um estudo de tempos de atendimento

Vamos ao caso concreto. Um pronto-socorro autorizou um estudo sobre tempos de atendimento: extrair os registros do sistema, descrever os tempos, ajustar um modelo de filas M/M/c, simular cenários de escala e escrever o artigo. A equipe tem três pessoas. Existe um compromisso externo: a submissão ao congresso em 30 de outubro de 2026.

A pergunta que o cronograma precisa responder não é “como isso fica bonito num slide”. É: dá para entregar?

5.1 O plano

Começamos pelo projeto, pelo calendário e pela equipe. As capacidades estão em horas por dia útil, e os esforços das tarefas virão na mesma unidade:

julia
using BusinessDays, Perth

inicio = Date(2026, 9, 1)
p = create_project("Estudo de tempos de atendimento — Pronto-socorro")
set_calendar!(p, "Brazil")          # durações passam a ser dias úteis

people!(p, [
    (name = "Ana",   role = "Coordenadora", team = "Pesquisa", capacity = 8),
    (name = "Bruno", role = "Analista",     team = "Dados",    capacity = 8),
    (name = "Clara", role = "Estatística",  team = "Pesquisa", capacity = 8),
])

A primeira fase — coleta e preparação. Repare que todas as tarefas nascem em inicio: é o schedule! do fim que vai espalhá-las pelas dependências.

julia
f1 = add_task!(p, "1. Coleta e preparação"; start = inicio)

etica = add_task!(p, "Protocolo e aprovação no comitê de ética";
    start = inicio, duration = 10, assignee = "Ana", parent = f1.id, effort = 20)

extrair = add_task!(p, "Extração dos registros do sistema";
    start = inicio, duration = 3, assignee = "Bruno", parent = f1.id,
    dependencies = [etica.id], effort = 18)

limpar = add_task!(p, "Limpeza e checagem de consistência";
    start = inicio, duration = 5, assignee = "Bruno", parent = f1.id,
    dependencies = [extrair.id], effort = 30)

base = add_task!(p, "Base pronta";
    start = inicio, milestone = true, parent = f1.id, dependencies = [limpar.id])

A segunda fase. Aqui aparece o início-início com defasagem: o ajuste do modelo começa dois dias depois que a análise descritiva começou, porque os dois se retroalimentam. E a revisão bibliográfica é um ramo paralelo, que só precisa estar pronta a tempo da redação:

julia
f2 = add_task!(p, "2. Análise"; start = inicio)

descritiva = add_task!(p, "Análise descritiva dos tempos";
    start = inicio, duration = 4, assignee = "Clara", parent = f2.id,
    dependencies = [base.id], effort = 24)

modelo = add_task!(p, "Ajuste do modelo de filas M/M/c";
    start = inicio, duration = 6, assignee = "Clara", parent = f2.id,
    dependencies = ["SS:$(descritiva.id)+2"], effort = 42)

simulacao = add_task!(p, "Simulação de cenários de escala";
    start = inicio, duration = 5, assignee = "Bruno", parent = f2.id,
    dependencies = [modelo.id], effort = 30)

biblio = add_task!(p, "Revisão bibliográfica";
    start = inicio, duration = 6, assignee = "Ana", parent = f2.id,
    dependencies = [base.id], effort = 18)

A terceira fase, com o prazo do congresso amarrado ao marco final:

julia
f3 = add_task!(p, "3. Comunicação"; start = inicio)

redacao = add_task!(p, "Redação do artigo";
    start = inicio, duration = 8, assignee = "Ana", parent = f3.id,
    dependencies = [descritiva.id, simulacao.id, biblio.id], effort = 48)

revisao = add_task!(p, "Revisão interna";
    start = inicio, duration = 3, assignee = "Clara", parent = f3.id,
    dependencies = [redacao.id], effort = 15)

submissao = add_task!(p, "Submissão ao congresso";
    start = inicio, milestone = true, parent = f3.id,
    dependencies = [revisao.id], deadline = Date(2026, 10, 30))

schedule!(p)

5.2 A primeira pergunta

julia
project_finish(p)
saída
2026-11-04

O plano termina em 4 de novembro. O congresso fecha em 30 de outubro. Já temos um problema — e, mais importante, temos um problema medido.

Gráfico de Gantt do estudo de tempos de atendimento, com as três fases, as barras das tarefas, as setas de dependência e as tarefas críticas destacadas em vermelho
Figura 2: o mesmo plano renderizado com save_chart(p, "gantt-estudo.png"), via CairoMakie. As barras com contorno vermelho estão no caminho crítico; a única sem contorno é a revisão bibliográfica.

5.3 Onde está a folga

julia
using DataFrames

DataFrame(slack(p))
Tarefa Início cedo Fim cedo Folga Crítica Gargalo
Protocolo e aprovação no comitê de ética 2026-09-01 2026-09-15 −2 sim não
Extração dos registros do sistema 2026-09-16 2026-09-18 −2 sim não
Limpeza e checagem de consistência 2026-09-21 2026-09-25 −2 sim não
Base pronta 2026-09-28 2026-09-28 −2 sim sim
Análise descritiva dos tempos 2026-09-29 2026-10-02 −2 sim sim
Revisão bibliográfica 2026-09-29 2026-10-06 5 não não
Ajuste do modelo de filas M/M/c 2026-10-01 2026-10-08 −2 sim não
Simulação de cenários de escala 2026-10-09 2026-10-16 −2 sim não
Redação do artigo 2026-10-19 2026-10-28 −2 sim não
Revisão interna 2026-10-29 2026-11-03 −2 sim não
Submissão ao congresso 2026-11-04 2026-11-04 −2 sim não

Três leituras dessa tabela:

A folga negativa é o prazo falando. Nenhuma tarefa foi movida por causa do deadline — o plano é exatamente o mesmo. O que mudou é que a passada para trás agora tem um teto: 30 de outubro. Toda a cadeia que desemboca na submissão está dois dias úteis atrasada, e o número aparece em cada linha, não só na última.

Uma única tarefa tem folga. A revisão bibliográfica pode escorregar cinco dias úteis sem consequência nenhuma. É a única coisa do plano que dá para despriorizar sem custo — e isso não era óbvio olhando o Gantt.

O gargalo tem nome. A coluna bottleneck marca tarefas críticas das quais mais de uma outra depende. “Base pronta” e “Análise descritiva” são os dois pontos onde um atraso não se propaga por um caminho, e sim por dois. São os lugares onde vale colocar atenção antes de acontecer.

E o atraso, em dias de calendário:

julia
deadline_slip(p)
saída
1-element Vector{NamedTuple}:
 (id = "93a7d306", name = "Submissão ao congresso", deadline = Date("2026-10-30"),
  finish = Date("2026-11-04"), slip_days = 5)

Repare na diferença entre os dois números: a folga é −2 dias úteis e o atraso é 5 dias corridos. Não há contradição — entre 30 de outubro (sexta) e 4 de novembro há cinco dias de calendário, mas apenas dois dias úteis, porque 2 de novembro é feriado. A folga do CPM é medida no calendário do projeto; o atraso, no calendário da vida.

5.4 Quem está sobrecarregado

julia
overallocations(p)
saída
1-element Vector{NamedTuple}:
 (assignee = "Clara",
  task1 = "026b2caf", task1_name = "Análise descritiva dos tempos",
  task2 = "4b5172e8", task2_name = "Ajuste do modelo de filas M/M/c",
  from = Date("2026-10-01"), to = Date("2026-10-02"))
julia
filter(r -> r.assignee == "Clara", workload(p)) |> DataFrame
saída
  assignee   date        tasks   effort   capacity   over
  Clara      2026-09-29      1      6.0        8.0    false
  Clara      2026-09-30      1      6.0        8.0    false
  Clara      2026-10-01      2     13.0        8.0    true
  Clara      2026-10-02      2     13.0        8.0    true
  Clara      2026-10-05      1      7.0        8.0    false
  ...

Clara tem capacidade de 8 horas por dia útil. Nos dias 1 e 2 de outubro, a análise descritiva (6 h/dia) e o ajuste do modelo (7 h/dia) se sobrepõem: 13 horas em um dia de 8. A sobreposição foi criada por nós, quando escrevemos "SS:$(descritiva.id)+2" — a dependência início-início é conveniente para o cronograma e desconfortável para quem executa. O plano parecia razoável; a conta não fecha.

5.5 E se a duração for um chute?

Até aqui as durações foram tratadas como fatos. Não são. “Aprovação no comitê de ética em 10 dias” é otimismo institucionalizado, e “extração dos registros em 3 dias” depende de um setor de TI que ninguém controla. Vamos declarar as três pontas:

julia
set_estimate!(p, etica.id,    8, 10, 20)   # otimista, mais provável, pessimista
set_estimate!(p, extrair.id,  2,  3, 10)
set_estimate!(p, modelo.id,   4,  6, 14)
set_estimate!(p, redacao.id,  5,  8, 15)

schedule!(p)
pert(p) |> DataFrame
Tarefa $o$ $m$ $p$ $t_e$ $\sigma$ Duração no plano
Protocolo e aprovação no comitê de ética 8 10 20 11,33 2,00 11
Extração dos registros do sistema 2 3 10 4,00 1,33 4
Ajuste do modelo de filas M/M/c 4 6 14 7,00 1,67 7
Redação do artigo 5 8 15 8,67 1,67 9

Por padrão, set_estimate! já aplica a duração esperada ao plano (passe apply = false para só registrar a estimativa). E isso, sozinho, já entrega um recado: o plano original era otimista em toda tarefa estimada. As caudas pessimistas são longas o bastante para puxar cada $t_e$ acima do “mais provável” que tínhamos digitado.

julia
pert_finish(p)
saída
(expected = Date("2026-11-10"), sd_days = 3.3665, variance = 11.333,
 critical = 10, estimated = 4)

O término esperado escorregou de 4 para 10 de novembro, com desvio padrão de 3,4 dias. Agora dá para fazer a pergunta que interessa:

julia
finish_probability(p, Date(2026, 10, 30))   # a data que prometemos
finish_probability(p, Date(2026, 11, 20))
pert_date(p, 0.8)                          # a data que dá para prometer
saída
0.0005
0.9985
2026-11-13

A probabilidade de cumprir o prazo de 30 de outubro é de 0,05%. Não é “difícil”: é não. E a data que dá para prometer com 80% de confiança é 13 de novembro — duas semanas depois do combinado. Esse é o número que se leva para uma conversa com o orientador ou com o cliente, porque ele não é uma opinião sobre otimismo alheio.

Vale ainda comparar a fórmula com a simulação:

julia
sim = pert_simulate(p; n = 10_000)

(sim.p10, sim.p50, sim.p80, sim.p90)
saída
(Date("2026-11-10"), Date("2026-11-11"), Date("2026-11-16"), Date("2026-11-17"))

A fórmula clássica dá P80 em 13 de novembro; o Monte Carlo dá 16 de novembro. A diferença tem nome: viés de convergência (merge bias). A fórmula propaga variância apenas ao longo do caminho crítico de hoje; a simulação sorteia todos os caminhos, e um ramo paralelo com pouca folga e estimativa larga tem chance real de virar o crítico. Sempre que houver caminhos quase críticos, a fórmula é otimista — e o tamanho dessa diferença é uma medida direta de quanto o plano depende de tudo dar certo em paralelo.

5.6 O que muda se aquilo atrasar?

A pergunta do começo do post, agora respondida em duas linhas:

julia
update_task!(p, extrair.id; duration = 8)   # a extração empacou no TI
schedule!(p)

project_finish(p)
deadline_slip(p)
saída
2026-11-16

1-element Vector{NamedTuple}:
 (id = "93a7d306", name = "Submissão ao congresso", deadline = Date("2026-10-30"),
  finish = Date("2026-11-16"), slip_days = 17)

A extração passando de 3 para 8 dias joga a submissão para 16 de novembro: 17 dias de atraso sobre o prazo. O update_task! percorreu a cadeia inteira sem que ninguém redesenhasse nada.

É aqui que o Perth deixa de ser um desenhador de Gantt. O cronograma responde porque ele é dado, e não figura.

5.7 Levando o plano para a equipe

Com o plano fechado, Perth.run() abre a mesma coisa no navegador — e, se a equipe estiver na mesma rede, dá para compartilhar:

julia
Perth.run(share = true)          # publica o Gantt na rede local, com QR code
Perth.key!("estudo-2026")        # exige chave de acesso para editar
Perth.view_key!("estudo-2026-v") # link só de leitura, para mostrar ao orientador
Perth.share!(false)              # desliga a transmissão sem derrubar o servidor

Cada máquina conectada aparece como um cursor com nome e IP, no estilo de programação em par, e há um chat integrado. Também dá para virar o plano em quadro Kanban:

julia
kanban_from_project!(p)          # cada tarefa do plano vira um cartão
Perth.kanban(share = true)       # quadro colaborativo na rede local

Cartões vindos do plano ficam ligados à tarefa de origem: arrastar um cartão para “concluído” completa a tarefa correspondente no Gantt, ao vivo.

E a figura da seção 5.2 sai com duas linhas:

julia
using CairoMakie

save_chart(p, "gantt-estudo.png")

6. O que o Perth.jl não é

Ser honesto sobre limites economiza frustração:

  • Não é multiusuário por identidade. O controle de acesso é por rede e por chave, não por login. Duas pessoas editando o mesmo campo resolvem no último a escrever vence.
  • Não deve ser exposto à internet. É uma ferramenta de rede local, para uma sala de gente que já confia uma na outra. Não coloque a porta 8123 num IP público.
  • Não faz nivelamento de recursos automático. level! existe e é uma heurística defensável (menor folga primeiro), mas nivelamento é um problema NP-difícil: o resultado é bom, não é ótimo.
  • Não substitui MS Project ou Primavera em organizações que precisam de governança corporativa, múltiplos calendários por recurso e trilha de auditoria formal. O alvo é outro: quem já trabalha em Julia e quer que o cronograma seja mais um objeto no workspace.

7. Para continuar

O Perth.jl está no registro geral (] add Perth) e o código, os exemplos comentados e o changelog estão no repositório:

Sugestões, dúvidas e relatos de uso são muito bem-vindos — abra uma issue no repositório ou comente aqui embaixo.

Escrito em 19/08/2026

Comentários

MorrisonKühlsen

Estatística descomplicada.

Artigos, fórmulas e tabelas
para estudantes e
profissionais.

morrisonkuhlsen.com