| from __future__ import annotations |
|
|
| from pathlib import Path |
| import unicodedata |
| from collections import Counter |
|
|
| from sentence_transformers import SentenceTransformer, util |
|
|
| from .data_loader import DataBundle, load_csv_data |
|
|
| DEFAULT_MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" |
|
|
|
|
| class AgroScanClassifier: |
| def __init__( |
| self, |
| data: DataBundle, |
| model_name: str = DEFAULT_MODEL_NAME, |
| model: SentenceTransformer | None = None, |
| ) -> None: |
| self.data = data |
| self.model = model or SentenceTransformer(model_name) |
|
|
| self.diagnosticos_base = ( |
| self.data.df_base[self.data.diag_base_col].astype(str).tolist() |
| ) |
| self.embeddings_diagnosticos = self.model.encode( |
| self.diagnosticos_base, |
| convert_to_tensor=True, |
| normalize_embeddings=True, |
| ) |
| self.categorias_tratamento = ( |
| self.data.df_culturas_pragas[self.data.diag_trat_col].astype(str).tolist() |
| ) |
| self.embeddings_categorias = self.model.encode( |
| self.categorias_tratamento, |
| convert_to_tensor=True, |
| normalize_embeddings=True, |
| ) |
|
|
| @classmethod |
| def from_csv( |
| cls, |
| base_csv_path: str | Path, |
| culturas_csv_path: str | Path, |
| model_name: str = DEFAULT_MODEL_NAME, |
| model: SentenceTransformer | None = None, |
| ) -> "AgroScanClassifier": |
| data = load_csv_data(base_csv_path, culturas_csv_path) |
| return cls(data=data, model_name=model_name, model=model) |
|
|
| def diagnostico_e_tratamento(self, respostas: list[str]) -> dict[str, str]: |
| texto = " ".join([str(r).strip() for r in respostas if str(r).strip()]) |
| if not texto: |
| return {"error": "Respostas vazias ou inválidas"} |
|
|
| |
| |
| diagnostico = self._diagnostico_por_match_estruturado(respostas) |
| if diagnostico is None: |
| embedding_respostas = self.model.encode( |
| texto, convert_to_tensor=True, normalize_embeddings=True |
| ) |
| similaridades = util.cos_sim(embedding_respostas, self.embeddings_diagnosticos)[0] |
| indice = int(similaridades.argmax().item()) |
| diagnostico = self.diagnosticos_base[indice] |
|
|
| tratamento_df = self._buscar_tratamento(diagnostico) |
|
|
| if tratamento_df.empty: |
| return { |
| "diagnostico": diagnostico, |
| "tratamento_nivel_1": "Tratamento não encontrado", |
| "tratamento_nivel_2": "Tratamento não encontrado", |
| "tratamento_nivel_3": "Tratamento não encontrado", |
| } |
|
|
| tratamento = tratamento_df.iloc[0] |
| return { |
| "diagnostico": diagnostico, |
| "tratamento_nivel_1": str(tratamento[self.data.trat1_col]) if self.data.trat1_col else "Tratamento não encontrado", |
| "tratamento_nivel_2": str(tratamento[self.data.trat2_col]) if self.data.trat2_col else "Tratamento não encontrado", |
| "tratamento_nivel_3": str(tratamento[self.data.trat3_col]) if self.data.trat3_col else "Tratamento não encontrado", |
| } |
|
|
| def _diagnostico_por_match_estruturado(self, respostas: list[str]) -> str | None: |
| colunas_perguntas = [ |
| col for col in self.data.df_base.columns if col != self.data.diag_base_col |
| ] |
|
|
| |
| if len(respostas) < len(colunas_perguntas): |
| return None |
|
|
| respostas_norm = [self._normalizar_texto(r) for r in respostas[: len(colunas_perguntas)]] |
| if any(not r for r in respostas_norm): |
| return None |
|
|
| matches: list[str] = [] |
| for _, row in self.data.df_base.iterrows(): |
| valores_row = [self._normalizar_texto(row[col]) for col in colunas_perguntas] |
| if valores_row == respostas_norm: |
| matches.append(str(row[self.data.diag_base_col])) |
|
|
| if not matches: |
| return None |
|
|
| |
| return Counter(matches).most_common(1)[0][0] |
|
|
| def _buscar_tratamento(self, diagnostico: str): |
| |
| tratamento_df = self.data.df_culturas_pragas[ |
| self.data.df_culturas_pragas[self.data.diag_trat_col].astype(str) == diagnostico |
| ] |
| if not tratamento_df.empty: |
| return tratamento_df |
|
|
| |
| diagnostico_norm = self._normalizar_texto(diagnostico) |
| categoria_norm = self.data.df_culturas_pragas[self.data.diag_trat_col].astype(str).map(self._normalizar_texto) |
| tratamento_df = self.data.df_culturas_pragas[categoria_norm == diagnostico_norm] |
| if not tratamento_df.empty: |
| return tratamento_df |
|
|
| |
| embedding_diag = self.model.encode( |
| diagnostico, |
| convert_to_tensor=True, |
| normalize_embeddings=True, |
| ) |
| similaridades = util.cos_sim(embedding_diag, self.embeddings_categorias)[0] |
| indice = int(similaridades.argmax().item()) |
| categoria_mais_proxima = self.categorias_tratamento[indice] |
| return self.data.df_culturas_pragas[ |
| self.data.df_culturas_pragas[self.data.diag_trat_col].astype(str) |
| == categoria_mais_proxima |
| ] |
|
|
| @staticmethod |
| def _normalizar_texto(texto: str) -> str: |
| texto = unicodedata.normalize("NFD", str(texto)) |
| texto = "".join(ch for ch in texto if unicodedata.category(ch) != "Mn") |
| return " ".join(texto.lower().strip().split()) |
|
|