Análise de PERT/CPM - Incertezas nas durações das atividades

Análise de PERT/CPM - Incertezas nas durações das atividades#

Conforme visto, o método PERT (Program Evaluation and Review Technique) é uma técnica de gerenciamento de projetos que considera a incerteza nas durações das atividades. A técnica utiliza três estimativas: otimista (o), mais provável (m) e pessimista (p). A partir dessas estimativas, calcula-se a média (\(\mu\)) e a variância (\(\sigma^2\)) de cada atividade, permitindo uma melhor previsão dos prazos do projeto, mesmo diante de incertezas. Essa abordagem, combinada com o CPM (Critical Path Method), auxilia na identificação do caminho crítico, ou seja, a sequência de atividades que determina a duração mínima do projeto.

As fórmulas utilizadas no cálculo são: Aqui estão as fórmulas em LaTeX adequadas para Markdown:

  • Duração esperada (\(\mu\)):
    [ \(\mu = \frac{o + 4m + p}{6}\) ]

  • Variância (\(\sigma^2\)):
    [ \(\sigma^2 = \left( \frac{p - o}{6} \right)^2\) ]

import pandas as pd
from fractions import Fraction

# Dados de entrada (sem as colunas Média e Variância)
data = {
    'Atividade': ['Início', 'A', 'B', 'C', 'D', 
                  'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'Fim'],
    'Descrição': ['Início', 'Escavação', 'Fundação', 'Paredes', 'Telhado', 
                  'Encanamento<br>Exterior', 'Encanamento<br>Interior', 'Muros', 
                  'Pintura<br>Exterior', 'Instalação<br>Elétrica', 'Divisórias', 'Piso', 
                  'Pintura<br>Interior', 'Acabamento<br>Exterior', 'Acabamento<br>Interior', 'Fim'],
    #'Atividades Precedentes': [None, 'Início', 'A', 'B', 'C', 'C', 'E', 'D', 'E,G', 'C', 'F,I', 'J', 'J', 'H', 'K,L', 'M,N'],
    'Duração Estimada': [0, 2, 4, 10, 6, 4, 5, 7, 9, 7, 8, 4, 5, 2, 6, 0],
    'o': [None, 1, 2, 6, 4, 1, 4, 5, 5, 3, 3, 4, 1, 1, 5, None],
    'm': [None, 2, 3.5, 9, 5.5, 4.5, 4, 6.5, 8, 7.5, 9, 4, 5.5, 2, 5.5, None],
    'p': [None, 3, 8, 18, 10, 5, 10, 11, 17, 9, 9, 4, 7, 3, 9, None]
}

# Função para calcular a média e a variância
def calculate_mean_variance(o, m, p):
    if pd.isna(o) or pd.isna(m) or pd.isna(p):
        return None, None
    # Média: (o + 4m + p) / 6
    mean = (o + 4 * m + p) / 6
    # Variância: ((p - o) / 6) ** 2
    variance = ((p - o) / 6) ** 2
    # Converter variância para fração mínima
    variance_fraction = str(Fraction(variance).limit_denominator())
    return mean, variance_fraction

# Criar o DataFrame com o cálculo da média e variância no momento da criação
df = pd.DataFrame(data)

# Calcular a média e a variância para cada linha
df['Média'], df['Variância'] = zip(*df.apply(lambda row: calculate_mean_variance(row['o'], row['m'], row['p']), axis=1))

# Exibindo o DataFrame com formatação HTML (para mostrar as quebras de linha corretamente)
from IPython.display import display, HTML

# Exibindo o DataFrame com HTML
display(HTML(df.to_html(escape=False)))

Além dos caminhos que consideram a média do projeto, considerando as estimativas pessimistas e otimistas, é possível calcular o pior e o melhor cenário.

import pandas as pd
import networkx as nx
from IPython.core.display import display, HTML

# Criar o DataFrame com as informações das atividades
data = pd.DataFrame({
    'Atividade': ['Início', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'Fim'],
    'Descrição': ['Início','Escavação', 'Fundação', 'Paredes', 'Telhado', 'Encanamento Exterior', 
                  'Encanamento Interior', 'Muros', 'Pintura Exterior', 'Instalação Elétrica', 'Divisórias', 
                  'Piso', 'Pintura Interior', 'Acabamento Exterior', 'Acabamento Interior', 'Fim'],
    'Atividades Precedentes': [None, 'Início', 'A', 'B', 'C', 'C', 'E', 'D', 'E,G', 'C', 'F,I', 'J', 'J', 'H', 'K,L', 'M,N'],
    'Duração Estimada': [0, 2, 4, 10, 6, 4, 5, 7, 9, 7, 8, 4, 5, 2, 6, 0],
    'Duração Otimista': [0, 1, 2, 6, 4, 1, 4, 5, 5, 3, 3, 4, 1, 1, 5, 0],
    'Duração Pessimista': [0, 3, 8, 18, 10, 5, 10, 11, 17, 9, 9, 4, 7, 3, 9, 0]
})

# Função para calcular os caminhos e destacar o caminho crítico
def calculate_paths_and_critical(data, duration_column, highlight_color):
    G = nx.DiGraph()

    # Adicionar nós e arestas ao grafo com base na coluna de duração especificada
    for i, row in data.iterrows():
        G.add_node(row['Atividade'], duração=row[duration_column])
        if row['Atividades Precedentes']:
            precedentes = row['Atividades Precedentes'].split(',')
            for p in precedentes:
                G.add_edge(p.strip(), row['Atividade'])

    # Função para encontrar todos os caminhos do início ao fim
    def find_paths(graph, start, end, path=[]):
        path = path + [start]
        if start == end:
            return [path]
        if start not in graph:
            return []
        paths = []
        for node in graph[start]:
            if node not in path:
                new_paths = find_paths(graph, node, end, path)
                for p in new_paths:
                    paths.append(p)
        return paths

    # Encontrar todos os caminhos do "Início" para "Fim"
    all_paths = find_paths(G, 'Início', 'Fim')

    # Calcular a duração de cada caminho
    path_durations = []
    for path in all_paths:
        duration = sum(G.nodes[node]['duração'] for node in path)
        path_durations.append((path, duration))

    # Criar uma tabela com os caminhos e suas durações
    paths_df = pd.DataFrame(path_durations, columns=['Caminho', 'Duração Total'])
    paths_df['Caminho'] = paths_df['Caminho'].apply(lambda x: ' -> '.join(x))  # Formatar o caminho como uma string

    # Encontrar o caminho crítico (o de maior duração)
    critical_path, critical_duration = max(path_durations, key=lambda x: x[1])

    # Função para destacar o caminho crítico na cor especificada
    def highlight_critical(path):
        return ' -> '.join(
            [f"<span style='color:{highlight_color}'>{node}</span>" if node in critical_path else node for node in path]
        )

    # Destacar o caminho crítico e a duração total na tabela
    paths_df['Caminho'] = paths_df['Caminho'].apply(lambda x: highlight_critical(x.split(' -> ')))
    paths_df['Duração Total'] = paths_df['Duração Total'].apply(
        lambda x: f"<span style='color:{highlight_color}'><b>{x}</b></span>" if x == critical_duration else x
    )

    # Retornar o DataFrame formatado
    return paths_df

# Tabela para a duração otimista (destacado em azul)
optimistic_paths_df = calculate_paths_and_critical(data, 'Duração Otimista', 'blue')

# Tabela para a duração estimada (destacado em verde)
estimated_paths_df = calculate_paths_and_critical(data, 'Duração Estimada', 'green')

# Tabela para a duração pessimista (destacado em vermelho)
pessimistic_paths_df = calculate_paths_and_critical(data, 'Duração Pessimista', 'red')

# Exibir as tabelas
display(HTML("<h3>Duração Otimista</h3>"))
display(HTML(optimistic_paths_df.to_html(escape=False)))

display(HTML("<h3>Duração Estimada</h3>"))
display(HTML(estimated_paths_df.to_html(escape=False)))

display(HTML("<h3>Duração Pessimista</h3>"))
display(HTML(pessimistic_paths_df.to_html(escape=False)))

Distribuição Normal e Cálculo da Probabilidade#

A distribuição normal pode ser utilizada para modelar o tempo total de um projeto no contexto do PERT/CPM, considerando as incertezas nas durações das atividades. Ela é caracterizada pela média (\(\mu\)), que representa o tempo esperado do projeto, e o desvio padrão (\(\sigma\)), que reflete a variabilidade no tempo de conclusão do projeto. O desvio padrão total do caminho crítico é calculado somando as variâncias das atividades nesse caminho.

A probabilidade de concluir o projeto em um tempo \(T\) é dada pela função de distribuição acumulada normal (CDF):

\( P(T) = P(X \leq T) = \Phi\left( \frac{T - \mu_p}{\sigma_p} \right) \)

Onde:

  • \(\Phi\) é a função de distribuição acumulada da normal.

  • \(\mu_p\) é a média do tempo total do caminho crítico.

  • \(\sigma_p\) é o desvio padrão do caminho crítico, calculado pela raiz quadrada da soma das variâncias das atividades no caminho crítico.

  • \(T\) é o tempo fornecido para calcular a probabilidade.

O desvio padrão do caminho crítico é calculado por:

\( \sigma_p = \sqrt{\sum_{i=1}^{n} \sigma_i^2} \)

Onde \(\sigma_i^2\) é a variância de cada atividade no caminho crítico, e \(n\) é o número de atividades nesse caminho.

import math
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import norm
from fractions import Fraction

# Função para calcular a probabilidade
def calcular_probabilidade(mu_p, variancia_atividades, T):
    # Passo 1: Calcular o desvio padrão total (σ_p)
    sigma_p = math.sqrt(sum(variancia_atividades))
    
    # Passo 2: Calcular o número de desvios padrão (k_a)
    k_a = (T - mu_p) / sigma_p
    
    # Passo 3: Calcular a probabilidade usando a distribuição normal
    probabilidade = norm.cdf(k_a)  # Função de distribuição acumulada da normal
    return probabilidade, sigma_p, k_a

# Função para converter frações em números decimais
def fração_para_decimal(fração_str):
    try:
        return float(Fraction(fração_str.strip()))
    except ValueError:
        print(f"Erro ao converter a fração: {fração_str}. Certifique-se de usar o formato correto, como '1/9'.")
        return 0.0

# Solicitar ao usuário a duração total do Caminho Crítico e as variâncias das atividades
mu_p = float(input("Informe a duração total do Caminho Crítico (μ_p) em unidades de tempo: ")) # Exemplo anterior: Caminho crítico Médio: Início -> A -> B -> C -> E -> F -> J -> L -> N -> Fim:	44
variancia_atividades = list(map(fração_para_decimal, input("Informe as variâncias das atividades separadas por vírgula (por exemplo, '1/9'): ").split(','))) # Exemplo anterior atividades: 1/9, 1, 4, 4/9, 1, 1, 1, 4/9 

# Solicitar o tempo T para calcular a probabilidade
T = float(input("Informe a duração do projeto (T) para calcular a probabilidade: ")) # Exemplo anterior o desvio padrão é 3

# Calcular a probabilidade
probabilidade, sigma_p, k_a = calcular_probabilidade(mu_p, variancia_atividades, T)

# Exibir o resultado
print(f"A probabilidade de o projeto ser completado em {T} unidades de tempo é: {probabilidade:.4f}")

# Gerar o gráfico da distribuição normal
x = np.linspace(mu_p - 4*sigma_p, mu_p + 4*sigma_p, 1000)
y = norm.pdf(x, mu_p, sigma_p)

plt.figure(figsize=(10, 6))

# Plotar a distribuição normal
plt.plot(x, y, label="Distribuição Normal", color='blue')

# Destacar a área da probabilidade em amarelo
x_fill = np.linspace(mu_p - 4*sigma_p, T, 1000)
y_fill = norm.pdf(x_fill, mu_p, sigma_p)
plt.fill_between(x_fill, y_fill, color='yellow', alpha=0.5, label=f'Área até T = {T}')

# Adicionar marcação de T
plt.axvline(x=T, color='red', linestyle='--', label=f'T = {T}')

# Adicionar títulos e rótulos
plt.title('Distribuição Normal da Duração do Projeto')
plt.xlabel('Duração (T)')
plt.ylabel('Densidade de Probabilidade')
plt.legend()

# Exibir o gráfico
plt.grid(True)
plt.show()
A probabilidade de o projeto ser completado em 47.0 unidades de tempo é: 0.8413
../../../_images/51e50bf1038dabf1aae81e8f353b9698e228b867623dd6148eaa006413d0b44a.png