Análise de PERT/CPM#
Este notebook realiza uma análise de PERT/CPM baseada no projeto descrito, considerando as atividades, dependências e durações fornecidas.
Objetivos#
Determinar o caminho crítico.
Calcular os tempos de início e término (mais cedo e mais tarde).
Gerar gráficos para visualizar o progresso.
# Importar as bibliotecas necessárias
# Pandas: Utilizado para manipulação e análise de dados em tabelas (DataFrames)
import pandas as pd
# NetworkX: Utilizado para criar, manipular e analisar grafos (redes de atividades)
import networkx as nx
# Matplotlib: Utilizado para visualização gráfica dos dados e redes
import matplotlib.pyplot as plt
# NumPy: Utilizado para operações matemáticas e numéricas avançadas, como manipulação de arrays
import numpy as np
Carregar Dados do Projeto#
O arquivo CSV contém as informações das atividades, dependências e durações estimadas.
# Carregar dados do CSV
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]
})
# Exibir o DataFrame
data
Criar o Grafo do Projeto#
A partir das dependências, construímos um grafo dirigido para representar as relações entre as atividades.
# Importar as bibliotecas necessárias
import matplotlib.pyplot as plt # Usada para criar gráficos, neste caso, para visualizar o grafo
import networkx as nx # Biblioteca para manipulação de grafos, necessária para o modelo PERT/CPM
import pandas as pd # Usada para criar e manipular DataFrames, onde armazenamos os dados das atividades
# Exemplo de DataFrame contendo as informações do projeto
data = pd.DataFrame({
'Atividade': ['Início', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'Fim'], # Identificadores das atividades
'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'], # Descrição de cada atividade
'Atividades Precedentes': [None, 'Início', 'A', 'B', 'C', 'C', 'E', 'D', 'E,G', 'C', 'F,I', 'J', 'J', 'H', 'K,L', 'M,N'], # Dependências entre atividades
'Duração Estimada': [0, 2, 4, 10, 6, 4, 5, 7, 9, 7, 8, 4, 5, 2, 6, 0] # Duração estimada para cada atividade
})
# Criar um grafo direcionado para modelar as atividades
G = nx.DiGraph() # Grafo direcionado (DiGraph), onde as arestas têm direção
# Adicionar nós e arestas ao grafo com base nos dados do DataFrame
for i, row in data.iterrows(): # Iterar sobre as linhas do DataFrame
G.add_node(row['Atividade'], duração=row['Duração Estimada']) # Adicionar nó para cada atividade, com atributo de duração
if row['Atividades Precedentes']: # Verificar se a atividade tem predecessores
precedentes = row['Atividades Precedentes'].split(',') # Dividir as atividades precedentes (caso haja mais de uma)
for p in precedentes: # Para cada atividade predecessora
G.add_edge(p.strip(), row['Atividade']) # Adicionar uma aresta entre a atividade predecessora e a atividade atual
# Função para calcular os níveis das atividades (hierarquia top-down)
def calculate_levels(graph):
levels = {} # Dicionário para armazenar o nível de cada atividade
for node in nx.topological_sort(graph): # Realiza uma ordenação topológica das atividades
if len(list(graph.predecessors(node))) == 0: # Se a atividade não tem predecessores (como "Início")
levels[node] = 0 # Atribui nível 0
else:
# Atribui o nível como o maior nível entre os predecessores + 1
levels[node] = max([levels[p] for p in graph.predecessors(node)]) + 1
return levels # Retorna o dicionário de níveis das atividades
# Calcular os níveis das atividades no grafo
levels = calculate_levels(G)
# Atribuir os níveis calculados como atributos dos nós no grafo
nx.set_node_attributes(G, levels, "subset")
# Calcular o layout para os nós, ajustando o espaçamento entre eles
pos = nx.multipartite_layout(G, subset_key="subset") # Layout multipartite, baseado nos níveis (subsets)
vertical_spacing = 2.0 # Ajuste do espaçamento vertical entre os níveis
for node in pos: # Ajustar a posição de cada nó
x, y = pos[node] # Posições x, y de cada nó
pos[node] = (x, y * vertical_spacing) # Alterar a posição vertical com base no espaçamento definido
# Melhorar a visualização do grafo
plt.figure(figsize=(16, 9)) # Definir o tamanho da figura do gráfico
# Desenhar o grafo
nx.draw(
G, # O grafo a ser desenhado
pos, # A posição dos nós (gerada anteriormente)
with_labels=False, # Desativa os rótulos padrão (para desenhar nós customizados mais tarde)
node_size=4500, # Ajustar o tamanho dos nós
node_color='lightblue', # Cor de fundo dos nós
edge_color='gray', # Cor das arestas (conexões entre os nós)
arrowsize=10 # Tamanho das setas nas arestas
)
# Adicionar rótulos personalizados nos nós (nome da atividade e duração)
labels = nx.get_node_attributes(G, 'duração') # Obter os atributos de duração de cada nó
custom_labels = {n: f"{n}\n{d} sem" for n, d in labels.items()} # Criar rótulos personalizados no formato "Atividade\nDuração sem"
nx.draw_networkx_labels(G, pos, labels=custom_labels, font_size=10) # Desenhar os rótulos personalizados no grafo
# Adicionar título ao gráfico
plt.title("Diagrama de Rede PERT/CPM", fontsize=14)
# Exibir o gráfico final
plt.show()

# Importar as bibliotecas necessárias
import matplotlib.pyplot as plt # Biblioteca para criação de gráficos e visualização dos dados
import networkx as nx # Biblioteca para criar e manipular grafos (útil para criar o Diagrama de Rede PERT/CPM)
import pandas as pd # Biblioteca para manipulação de dados em formato de tabelas (DataFrame)
# Exemplo de DataFrame contendo as informações do projeto
data = pd.DataFrame({
'Atividade': ['Início', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'Fim'], # Identificadores das atividades
'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'], # Descrição de cada atividade
'Atividades Precedentes': [None, 'Início', 'A', 'B', 'C', 'C', 'E', 'D', 'E,G', 'C', 'F,I', 'J', 'J', 'H', 'K,L', 'M,N'], # Dependências entre atividades
'Duração Estimada': [0, 2, 4, 10, 6, 4, 5, 7, 9, 7, 8, 4, 5, 2, 6, 0] # Duração estimada para cada atividade
})
# Criar um grafo direcionado para modelar as atividades (Diagrama de Rede PERT/CPM)
G = nx.DiGraph() # DiGraph (grafo direcionado) será usado para representar as dependências de atividades
# Adicionar nós e arestas ao grafo com base nos dados do DataFrame
for i, row in data.iterrows(): # Itera sobre as linhas do DataFrame para adicionar cada atividade
G.add_node(row['Atividade'], duração=row['Duração Estimada']) # Adiciona cada atividade como um nó no grafo, com sua duração
if row['Atividades Precedentes']: # Verifica se a atividade tem predecessores
precedentes = row['Atividades Precedentes'].split(',') # Se houver mais de um, separa os predecessores por vírgula
for p in precedentes: # Para cada atividade predecessora
G.add_edge(p.strip(), row['Atividade']) # Adiciona uma aresta do predecessor para a atividade atual (representa a dependência)
# Função para calcular os níveis das atividades (hierarquia top-down)
def calculate_levels(graph):
levels = {} # Dicionário para armazenar o nível de cada atividade
for node in nx.topological_sort(graph): # Realiza uma ordenação topológica das atividades no grafo
if len(list(graph.predecessors(node))) == 0: # Se a atividade não tem predecessores (como o "Início")
levels[node] = 0 # Atribui nível 0 para a atividade inicial
else:
# Para atividades com predecessores, atribui o nível máximo dos predecessores + 1
levels[node] = max([levels[p] for p in graph.predecessors(node)]) + 1
return levels # Retorna o dicionário de níveis (profundidade hierárquica das atividades)
# Calcular os níveis das atividades no grafo
levels = calculate_levels(G)
# Atribuir os níveis calculados como atributos dos nós no grafo
nx.set_node_attributes(G, levels, "subset") # Armazena os níveis como atributos dos nós no grafo
# Calcular o layout dos nós com espaçamento ajustado (invertendo os eixos para orientação vertical)
pos = nx.multipartite_layout(G, subset_key="subset") # Usa o layout multipartite baseado no nível das atividades
vertical_spacing = 2.0 # Define o espaçamento vertical entre os níveis (maior número significa mais espaço entre as camadas)
horizontal_spacing = 2.0 # Define o espaçamento horizontal entre as atividades
for node in pos: # Ajusta a posição de cada nó
x, y = pos[node] # Obtém as coordenadas x e y
pos[node] = (y * horizontal_spacing, -x * vertical_spacing) # Ajusta as coordenadas, invertendo e ampliando a separação entre os nós
# Melhorar a visualização do grafo
plt.figure(figsize=(10, 12)) # Define o tamanho da figura do gráfico, em polegadas (largura, altura)
# Desenhar o grafo com as configurações ajustadas
nx.draw(
G, # O grafo a ser desenhado
pos, # As posições calculadas dos nós
with_labels=False, # Desativa a exibição dos rótulos padrão (serão desenhados rótulos personalizados mais tarde)
node_size=4500, # Ajusta o tamanho dos nós para torná-los visíveis e legíveis
node_color='lightblue', # Define a cor de fundo dos nós (azul claro)
edge_color='gray', # Define a cor das arestas (linha entre os nós)
arrowsize=10 # Ajusta o tamanho das setas nas arestas para indicar a direção
)
# Adicionar rótulos personalizados nos nós (Nome da Atividade e sua Duração Estimada)
labels = nx.get_node_attributes(G, 'duração') # Obtém o atributo de duração de cada nó (atividade)
custom_labels = {n: f"{n}\n{d} sem" for n, d in labels.items()} # Cria rótulos personalizados no formato "Atividade\nDuração sem"
nx.draw_networkx_labels(G, pos, labels=custom_labels, font_size=10) # Desenha os rótulos personalizados nos nós
# Adicionar título ao gráfico
plt.title("Diagrama de Rede PERT/CPM", fontsize=14) # Título do gráfico (diagramas PERT/CPM)
# Exibir o gráfico gerado
plt.show() # Mostra o gráfico na tela
Caminho crítico#
A saída do código apresenta todos os caminhos possíveis entre o início e o fim de um projeto, com suas respectivas durações. Cada linha mostra uma sequência de atividades (caminho) e a soma total das durações das atividades ao longo desse caminho. O “caminho crítico” é o caminho com a maior duração total, o que implica que qualquer atraso em uma atividade desse caminho afetará diretamente a duração total do projeto. Nesse caso, o caminho crítico é o mais longo, pois não há atividades que podem ser atrasadas sem impactar o prazo final do projeto.
import pandas as pd
import networkx as nx
# Criar o grafo de PERT/CPM novamente com base nos dados anteriores
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]
})
# Criar o grafo de PERT/CPM
G = nx.DiGraph()
# Adicionar nós e arestas ao grafo
for i, row in data.iterrows():
G.add_node(row['Atividade'], duração=row['Duração Estimada'])
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
# Ajustar para exibir as células com texto completo
pd.set_option('display.max_colwidth', None) # Não limitar a largura das células
# Exibir a tabela de caminhos e durações
print(paths_df)
O código abaixo gera uma tabela com todos os caminhos possíveis de um projeto, desde o início até o fim, utilizando o Diagrama PERT/CPM. Cada caminho é exibido com sua respectiva duração total. O caminho crítico, ou seja, o caminho com a maior duração total, é destacado em vermelho. Para isso, o código identifica o caminho com a maior duração e aplica um estilo visual em HTML para colorir as atividades que fazem parte desse caminho. Embora a tabela mostre todos os caminhos, o caminho crítico é facilmente identificado devido ao destaque em vermelho, permitindo uma rápida visualização da sequência de atividades mais longa, o que é crucial para a gestão eficiente do tempo no projeto.
import pandas as pd
import networkx as nx
# Criar o grafo de PERT/CPM novamente com base nos dados anteriores
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]
})
# Criar o grafo de PERT/CPM
G = nx.DiGraph()
# Adicionar nós e arestas ao grafo
for i, row in data.iterrows():
G.add_node(row['Atividade'], duração=row['Duração Estimada'])
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 = max(path_durations, key=lambda x: x[1])[0]
# Função para destacar o caminho crítico em vermelho
def highlight_critical(path):
return ' -> '.join([f"<span style='color:red'>{node}</span>" if node in critical_path else node for node in path])
# Aplicar o destaque para o caminho crítico na tabela
paths_df['Caminho'] = paths_df['Caminho'].apply(lambda x: highlight_critical(x.split(' -> ')))
# Ajustar para exibir as células com texto completo
pd.set_option('display.max_colwidth', None) # Não limitar a largura das células
# Exibir a tabela de caminhos e durações com destaque no caminho crítico
from IPython.core.display import display, HTML
display(HTML(paths_df.to_html(escape=False)))
C:\Users\DELL\AppData\Local\Temp\ipykernel_26884\3892950494.py:67: DeprecationWarning: Importing display from IPython.core.display is deprecated since IPython 7.14, please import from IPython.display
from IPython.core.display import display, HTML
Caminho | Duração Total | |
---|---|---|
0 | Início -> A -> B -> C -> D -> G -> H -> M -> Fim | 40 |
1 | Início -> A -> B -> C -> E -> F -> J -> K -> N -> Fim | 43 |
2 | Início -> A -> B -> C -> E -> F -> J -> L -> N -> Fim | 44 |
3 | Início -> A -> B -> C -> E -> H -> M -> Fim | 31 |
4 | Início -> A -> B -> C -> I -> J -> K -> N -> Fim | 41 |
5 | Início -> A -> B -> C -> I -> J -> L -> N -> Fim | 42 |
Representação do caminho crítico no grafo de rede#
O código apresentado anteriormente foi assim modificado para destacar o caminho crítico no Diagrama de Rede PERT/CPM, onde as atividades e os arcos que fazem parte desse caminho são exibidos em vermelho. O caminho crítico é a sequência de atividades que determina o tempo total do projeto, ou seja, qualquer atraso em uma dessas atividades resultará em atraso no projeto como um todo. O código identifica esse caminho com base na duração de cada caminho do início ao fim e destaca as atividades e arestas que compõem o caminho crítico. Isso é feito ajustando a cor dos nós e das arestas do grafo durante a renderização, utilizando o matplotlib para visualização e networkx para manipulação do grafo. Além disso, o código também permite ajustar a aparência do grafo, como o tamanho dos nós, a cor das setas e a disposição das atividades no gráfico.
import matplotlib.pyplot as plt
import networkx as nx
import pandas as pd
# Exemplo de DataFrame contendo as informações do projeto
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]
})
# Criar o grafo de PERT/CPM
G = nx.DiGraph()
# Adicionar nós e arestas ao grafo
for i, row in data.iterrows():
G.add_node(row['Atividade'], duração=row['Duração Estimada'])
if row['Atividades Precedentes']:
precedentes = row['Atividades Precedentes'].split(',')
for p in precedentes:
G.add_edge(p.strip(), row['Atividade'])
# Função para calcular os níveis das atividades (hierarquia top-down)
def calculate_levels(graph):
levels = {}
for node in nx.topological_sort(graph):
if len(list(graph.predecessors(node))) == 0:
levels[node] = 0
else:
levels[node] = max([levels[p] for p in graph.predecessors(node)]) + 1
return levels
# Calcular os níveis das atividades
levels = calculate_levels(G)
# Atribuir os níveis como atributos aos nós
nx.set_node_attributes(G, levels, "subset")
# 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))
# Determinar o caminho crítico (o caminho mais longo)
max_duration = max(path_durations, key=lambda x: x[1])[1]
critical_paths = [path for path, duration in path_durations if duration == max_duration]
# Identificar os nodos e arestas do caminho crítico
critical_nodes = set()
critical_edges = set()
for path in critical_paths:
for i in range(len(path) - 1):
critical_nodes.add(path[i])
critical_edges.add((path[i], path[i+1]))
# Calcular o layout dos nós
pos = nx.multipartite_layout(G, subset_key="subset")
vertical_spacing = 2.0
horizontal_spacing = 2.0
for node in pos:
x, y = pos[node]
pos[node] = (y * horizontal_spacing, -x * vertical_spacing)
# Visualização do grafo com caminho crítico em vermelho
plt.figure(figsize=(10, 12))
# Desenhar os arcos com cores diferentes para o caminho crítico
nx.draw(
G,
pos,
with_labels=False,
node_size=4500,
node_color='lightblue',
edge_color=['red' if edge in critical_edges else 'gray' for edge in G.edges()],
arrowsize=10
)
# Desenhar os nós
nx.draw_networkx_nodes(G, pos, nodelist=critical_nodes, node_color='red', node_size=4500)
nx.draw_networkx_nodes(G, pos, nodelist=[node for node in G.nodes() if node not in critical_nodes], node_color='lightblue', node_size=4500)
# Adicionar rótulos personalizados nos nós
labels = nx.get_node_attributes(G, 'duração')
custom_labels = {n: f"{n}\n{d} sem" for n, d in labels.items()}
nx.draw_networkx_labels(G, pos, labels=custom_labels, font_size=10)
# Título do gráfico
plt.title("Diagrama de Rede PERT/CPM com Caminho Crítico", fontsize=14)
# Exibir o gráfico
plt.show()

Programação das atividades: Cálculo dos tempos e folga#
O código utiliza um grafo dirigido para modelar as dependências entre atividades de um projeto e calcula os tempos mais cedo (ES e EF) e mais tarde (LS e LF) para cada atividade, além do slack (folga). Com isso, identifica o caminho crítico, ou seja, a sequência de atividades que determina a duração mínima do projeto. Primeiro, avançamos no grafo para calcular ES e EF com base nas durações das atividades e nos maiores valores de EF das predecessoras. Em seguida, retrocedemos para calcular LS e LF a partir dos menores valores de LS dos sucessores. O slack, calculado como Slack = LS - ES
, indica atividades críticas quando seu valor é zero.
Fórmulas Utilizadas#
Cálculo de ES (Earliest Start) e EF (Earliest Finish):
Fórmulas inline:
\(ES_i = \max(EF_{\text{predecessores}})\)
\(EF_i = ES_i + \text{duração}_i\)
Fórmula destacada: $\( EF_i = ES_i + \text{duração}_i \)$
Cálculo de LS (Latest Start) e LF (Latest Finish):
Fórmulas inline:
\(LF_i = \min(LS_{\text{sucessores}})\)
\(LS_i = LF_i - \text{duração}_i\)
Fórmula destacada: $\( LS_i = LF_i - \text{duração}_i \)$
Cálculo do Slack (Folga):
Fórmulas inline:
\(Slack = LS - ES\)
\(Slack = LF - EF\)
Fórmula destacada: $\( Slack = LF - EF \)$
O código automatiza esses cálculos usando o NetworkX para modelar e navegar pelo grafo, garantindo que dependências complexas sejam corretamente tratadas.
import pandas as pd
import networkx as nx
# Dados iniciais
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]
})
# Criar o grafo de PERT/CPM
G = nx.DiGraph()
# Adicionar nós e arestas ao grafo
for i, row in data.iterrows():
G.add_node(row['Atividade'], duração=row['Duração Estimada'])
if row['Atividades Precedentes']:
precedentes = row['Atividades Precedentes'].split(',')
for p in precedentes:
G.add_edge(p.strip(), row['Atividade'])
# Função para calcular ES e EF
def calculate_es_ef(data, graph):
for i, row in data.iterrows():
if row['Atividades Precedentes'] is None:
data.at[i, 'ES'] = 0
else:
predecessores = row['Atividades Precedentes'].split(',')
es_predecessores = [data.loc[data['Atividade'] == p.strip(), 'EF'].values[0] for p in predecessores]
data.at[i, 'ES'] = max(es_predecessores)
data.at[i, 'EF'] = data.at[i, 'ES'] + row['Duração Estimada']
# Função para calcular LS e LF
def calculate_ls_lf(data, graph):
for i in range(len(data) - 1, -1, -1):
row = data.iloc[i]
if row['Atividade'] == 'Fim':
data.at[i, 'LF'] = row['EF']
else:
sucessores = [edge[1] for edge in graph.edges(row['Atividade'])]
lf_sucessores = [data.loc[data['Atividade'] == s, 'LS'].values[0] for s in sucessores]
data.at[i, 'LF'] = min(lf_sucessores)
data.at[i, 'LS'] = data.at[i, 'LF'] - row['Duração Estimada']
# Adicionar colunas para ES, EF, LS, LF e Slack
data['ES'] = 0
data['EF'] = 0
data['LS'] = 0
data['LF'] = 0
data['Slack'] = 0
# Calcular ES, EF, LS, LF e Slack
calculate_es_ef(data, G)
calculate_ls_lf(data, G)
data['Slack'] = data['LS'] - data['ES']
# Identificar o caminho crítico
critical_path = data[data['Slack'] == 0]['Atividade'].tolist()
# Exibir resultados
print("Tabela final com ES, EF, LS, LF e Slack:")
print(data[['Atividade', 'Descrição', 'ES', 'EF', 'LS', 'LF', 'Slack']])
print("\nCaminho Crítico:", " -> ".join(critical_path))
Tabela final com ES, EF, LS, LF e Slack:
Atividade Descrição ES EF LS LF Slack
0 Início Início 0 0 0 0 0
1 A Escavação 0 2 0 2 0
2 B Fundação 2 6 2 6 0
3 C Paredes 6 16 6 16 0
4 D Telhado 16 22 20 26 4
5 E Encanamento Exterior 16 20 16 20 0
6 F Encanamento Interior 20 25 20 25 0
7 G Muros 22 29 26 33 4
8 H Pintura Exterior 29 38 33 42 4
9 I Instalação Elétrica 16 23 18 25 2
10 J Divisórias 25 33 25 33 0
11 K Piso 33 37 34 38 1
12 L Pintura Interior 33 38 33 38 0
13 M Acabamento Exterior 38 40 42 44 4
14 N Acabamento Interior 38 44 38 44 0
15 Fim Fim 44 44 44 44 0
Caminho Crítico: Início -> A -> B -> C -> E -> F -> J -> L -> N -> Fim