import heapq
from dataclasses import dataclass
@dataclass(frozen=True)
class Produto:
nome: str
categoria: str
probabilidade_conversao: float
def __post_init__(self):
if not (0.0 <= self.probabilidade_conversao <= 1.0):
raise ValueError(
f"Probabilidade de conversão inválida para {self.nome}: "
f"{self.probabilidade_conversao}"
)
def __repr__(self):
return f"{self.nome} ({self.categoria}, conv={self.probabilidade_conversao:.2f})"
class AStarRecommendation:
def __init__(self, produtos: list[Produto]):
self.produtos = produtos
self.grafo = self._construir_grafo_completo(produtos)
def _construir_grafo_completo(self, produtos: list[Produto]):
grafo = {p: [] for p in produtos}
for origem in produtos:
for destino in produtos:
if origem != destino:
custo = self._custo_aresta(destino)
grafo[origem].append((destino, custo))
return grafo
@staticmethod
def _custo_aresta(destino: Produto) -> float:
# custo mínimo pequeno para evitar aresta de custo zero
return max(1.0 - destino.probabilidade_conversao, 0.01)
@staticmethod
def heuristica(produto_atual: Produto, objetivo: Produto) -> float:
return max(1.0 - objetivo.probabilidade_conversao, 0.0)
def buscar_melhor_caminho(self, inicio: Produto, objetivo: Produto):
if inicio not in self.grafo or objetivo not in self.grafo:
raise ValueError("Produto de início ou objetivo não está no grafo.")
# fila de prioridade: (f_score, contador, produto)
contador = 0
fronteira = [(self.heuristica(inicio, objetivo), contador, inicio)]
veio_de = {}
custo_g = {p: float("inf") for p in self.grafo}
custo_g[inicio] = 0.0
visitados = set()
while fronteira:
_, _, atual = heapq.heappop(fronteira)
if atual == objetivo:
return self._reconstruir_caminho(veio_de, atual), custo_g[atual]
if atual in visitados:
continue
visitados.add(atual)
for vizinho, custo_aresta in self.grafo[atual]:
novo_custo = custo_g[atual] + custo_aresta
if novo_custo < custo_g[vizinho]:
custo_g[vizinho] = novo_custo
f_score = novo_custo + self.heuristica(vizinho, objetivo)
veio_de[vizinho] = atual
contador += 1
heapq.heappush(fronteira, (f_score, contador, vizinho))
return None, float("inf")
@staticmethod
def _reconstruir_caminho(veio_de: dict, atual: Produto) -> list[Produto]:
caminho = [atual]
while atual in veio_de:
atual = veio_de[atual]
caminho.append(atual)
caminho.reverse()
return caminho
def recomendar(self, produto_atual: Produto, top_n: int = 3):
resultados = []
for candidato in self.produtos:
if candidato == produto_atual:
continue
caminho, custo = self.buscar_melhor_caminho(produto_atual, candidato)
resultados.append((candidato, custo, caminho))
resultados.sort(key=lambda x: x[1])
return resultados[:top_n]
if __name__ == "__main__":
produtos = [
Produto("Notebook", "Eletrônicos", 0.35),
Produto("Mouse", "Acessórios", 0.72),
Produto("Cadeira", "Móveis", 0.48),
Produto("Teclado", "Acessórios", 0.65),
Produto("Monitor", "Eletrônicos", 0.55),
Produto("Fone", "Acessórios", 0.80),
]
sistema = AStarRecommendation(produtos)
origem = produtos[0]
destino = produtos[-1]
caminho, custo = sistema.buscar_melhor_caminho(origem, destino)
print("=== Melhor caminho (A*) ===")
print(f"De: {origem}")
print(f"Para: {destino}")
print(f"Caminho: {' -> '.join(p.nome for p in caminho)}")
print(f"Custo total: {custo:.4f}\n")
print("=== Recomendações a partir do Notebook Gamer ===")
for produto, custo, caminho in sistema.recomendar(origem, top_n=3):
print(
f"- {produto.nome} (custo={custo:.4f}) "
f"via {' -> '.join(p.nome for p in caminho)}"
)