# ----------------------------------------------------------------------------- # Lou-Ka — tests du text mining des descriptions (louka/textmine.py) # ----------------------------------------------------------------------------- import json from louka.textmine import analyser NBSP = " " # espace insécable (typographie québécoise : « 450 $ ») # Cas de référence : VRAIE description de la chambre Gestipro du # 2435 boul. Laurier (source gestipro:24813, copiée telle quelle de la base). DESC_2435_LAURIER = ( "Chambre meublé de base en location située au 2435 boul. Laurier à " "Sillery. Disponible disponible\n" "dès maintenant\n" "à 450$ par mois, chauffage, électricité, eau chaude et wifi inclus. " "Location pour minimum 3 mois.\n" "L’ESPACE DE VIE\n" "Total de 11 chambreurs dans la maison\n" "Cuisine et salle de bain commune aux autres occupants de l’appartement\n" "Réfrigérateur, cuisinière, laveuse et sécheuse inclus dans les pièces communes\n" "Chambre meublée de base comme sur les photos, pour occupation simple seulement\n" " \n" "FRAIS ET CHARGES\n" "Chauffage, eau chaude et électricité inclus\n" "Wifi inclus\n" "À PROXIMITÉ / SECTEUR\n" "Secteur très prisé de la ville\n" "À proximité des ponts, de l’autoroute Robert-Bourassa et de l’autoroute Henri IV\n" "Près de tous les services de Sillery et Ste-Foy dont Laurier Québec, " "Place de la Cité et Place Ste-Foy.\n" "Université Laval à distance de marche\n" "Prendre note qu’afin d’offrir à nos locataires une tranquillité, les " "animaux ne sont pas acceptés. Enquête de crédit obligatoire pour tous " "nos nouveaux locataires.\n" "Pour visiter, communiquez avec Gestipro au 418-522-2525 poste 1\n" "Pour consulter tous nos appartements à louer, visitez le www.gestipro.info" ) # -- garde d'entrée ------------------------------------------------------------- def test_description_vide_ou_trop_courte(): assert analyser("") is None assert analyser(" ") is None assert analyser("Beau logement à louer.") is None # < 40 caractères assert analyser("x" * 39) is None assert analyser("Grand appartement lumineux à louer au centre-ville.") is not None # -- étape A : nettoyage --------------------------------------------------------- def test_titres_en_majuscules_deviennent_sections(): res = analyser("Magnifique appartement au bord de l'eau du fleuve.\n" "L'ESPACE DE VIE\n" "Grande pièce ouverte avec beaucoup de rangement pratique.") assert "L'espace de vie" in res["texte_nettoye"] assert "L'ESPACE DE VIE" not in res["texte_nettoye"] assert [s["titre"] for s in res["sections"]] == ["Description", "L'espace de vie"] assert res["sections"][1]["texte"] == \ "Grande pièce ouverte avec beaucoup de rangement pratique." def test_segment_majuscules_dans_une_ligne(): # titre EN MAJUSCULES enchâssé au milieu d'une ligne (≥ 2 mots, ≥ 9 car.) res = analyser("Belle chambre au centre-ville. FRAIS ET CONDITIONS " "Le loyer inclut le chauffage et l'électricité.") assert "Frais et conditions" in [s["titre"] for s in res["sections"]] def test_doublons_de_mots_consecutifs(): # insensible à la casse et aux accents res = analyser("Logement rénové RÉNOVÉ récemment. Disponible disponible " "dès maintenant pour les visites de groupe.") assert "rénové RÉNOVÉ" not in res["texte_nettoye"] assert "Disponible disponible" not in res["texte_nettoye"] assert "Disponible dès maintenant" in res["texte_nettoye"] def test_phrases_collees(): res = analyser("Appartement très lumineuxVenez le visiter rapidement, " "près du McDonald et du parc du quartier.") assert "lumineux. Venez" in res["texte_nettoye"] assert "McDonald" in res["texte_nettoye"] # nom propre intact def test_typographie_quebecoise(): res = analyser("Grand 4 1/2 à 875$ par mois avec rabais de 5% !!! " "Venez visiter l'appartement dès aujourd'hui.") assert f"875{NBSP}$ par mois" in res["texte_nettoye"] # insécable avant $ assert f"5{NBSP}%" in res["texte_nettoye"] # insécable avant % assert "!!!" not in res["texte_nettoye"] # « !!! » -> « ! » assert " " not in res["texte_nettoye"] # espaces multiples def test_puces_normalisees(): res = analyser("Inclusions du logement :\n- Frigo inclus\n* Cuisinière " "incluse\n· Balcon avant avec vue") lignes = res["texte_nettoye"].split("\n") assert lignes[1:] == ["• Frigo inclus", "• Cuisinière incluse", "• Balcon avant avec vue"] # -- étape B : extraction -------------------------------------------------------- def test_prix_mensuel_et_confiance(): res = analyser("Beau 4 1/2 chauffé et éclairé à 995$ par mois, dans un " "immeuble tranquille près des services.") assert res["faits"]["prix_mensuel"] == 995 assert res["confiance"]["prix_mensuel"] == "haute" # montant sans contexte mensuel -> confiance faible, exclu de « en_bref » res2 = analyser("Grand studio lumineux au centre-ville, rabais de 500 $ " "à la signature avant la fin du mois de septembre.") assert res2["faits"]["prix_mensuel"] == 500 assert res2["confiance"]["prix_mensuel"] == "faible" assert res2["en_bref"] is None or "Loyer" not in res2["en_bref"] def test_incoherence_prix_annonce(): res = analyser("Chambre à louer à 450$ par mois dans une grande maison " "près de l'université et des autobus.", price=500.0) assert any("450" in i and "500" in i for i in res["incoherences"]) def test_date_disponibilite_ancree(): res = analyser("Grande chambre à louer, disponible dès maintenant, dans " "un secteur paisible près des autobus.") assert res["faits"]["date_disponibilite"] == "now" # une date évènementielle sans ancre de disponibilité n'est PAS une dispo res2 = analyser("Grande porte ouverte samedi 15 août pour venir voir le " "logement au centre-ville avec nos agents.") assert res2["faits"]["date_disponibilite"] is None def test_duree_bail_minimale(): txt = "Chambre dans une maison partagée du centre-ville. Location pour " assert analyser(txt + "minimum 3 mois.")["faits"]["duree_bail_minimale_mois"] == 3 assert analyser(txt + "un bail de 12 mois.")["faits"]["duree_bail_minimale_mois"] == 12 assert analyser(txt + "un bail d'un an.")["faits"]["duree_bail_minimale_mois"] == 12 # durée liée à une promotion -> confiance faible res = analyser("Un mois gratuit sur un bail de 13 mois pour toute " "signature avant la fin du mois prochain.") assert res["faits"]["duree_bail_minimale_mois"] == 13 assert res["confiance"]["duree_bail_minimale_mois"] == "faible" def test_nb_occupants_total(): res = analyser("Total de 11 chambreurs dans la maison, cuisine équipée " "et salon partagés entre les occupants.") assert res["faits"]["nb_occupants_total"] == 11 res2 = analyser("Belle grande chambre dans une colocation de 5 située " "tout près du cégep et des commerces.") assert res2["faits"]["nb_occupants_total"] == 5 def test_salle_de_bain_et_cuisine(): res = analyser("Chambre avec salle de bain commune et cuisine partagée " "avec les autres locataires de l'étage.") assert res["faits"]["salle_de_bain"] == "commune" assert res["faits"]["cuisine"] == "commune" res2 = analyser("Grande suite avec salle de bain privée attenante, dans " "un immeuble récent proche du terminus.") assert res2["faits"]["salle_de_bain"] == "privee" # tournure combinée « cuisine et salle de bain commune » res3 = analyser("Cuisine et salle de bain commune aux autres occupants " "de l'appartement, au deuxième étage.") assert res3["faits"]["salle_de_bain"] == "commune" assert res3["faits"]["cuisine"] == "commune" def test_electromenagers(): res = analyser("Réfrigérateur, cuisinière, laveuse et sécheuse inclus, " "ainsi qu'un lave-vaisselle et un micro-ondes neufs.") assert res["faits"]["electromenagers"] == \ ["frigo", "cuisiniere", "laveuse", "secheuse", "lave_vaisselle", "micro_ondes"] # « entrées laveuse-sécheuse » = branchement, pas un appareil fourni res2 = analyser("Logement avec entrées laveuse-sécheuse et grande cuisine " "éclairée donnant sur la cour arrière.") assert "laveuse" not in res2["faits"]["electromenagers"] def test_inclusions(): res = analyser("Chambre meublée à louer, chauffage, électricité, eau " "chaude et wifi inclus pour tous les locataires.") assert res["faits"]["inclusions"] == \ ["chauffage", "electricite", "eau_chaude", "wifi", "meuble"] res2 = analyser("Grand logement au dernier étage, électricité à la " "charge du locataire et chauffage non inclus.") assert "electricite" not in res2["faits"]["inclusions"] assert "chauffage" not in res2["faits"]["inclusions"] def test_contraintes(): res = analyser("Logement non-fumeur. Les animaux ne sont pas acceptés. " "Enquête de crédit obligatoire. Réservé aux étudiants seulement.") assert res["faits"]["contraintes"] == \ ["non_fumeur", "pas_d_animaux", "references_requises", "etudiants_seulement"] def test_depot_mentionne(): res = analyser("Un dépôt de garantie de 300 $ est demandé à la remise " "des clés du logement, remboursable au départ.") assert res["faits"]["depot_mentionne"] is not None assert "300" in res["faits"]["depot_mentionne"] res2 = analyser("Grand logement lumineux à louer dans un secteur calme " "près des parcs et des écoles du quartier.") assert res2["faits"]["depot_mentionne"] is None def test_quartier_et_incoherence_secteur(): txt = ("Chambre située au 2435 boul. Laurier à Sillery, tout près des " "services et de l'université.") res = analyser(txt, sector="Sainte-Foy") assert res["faits"]["quartier_mentionne"] == "Sillery" assert res["incoherences"] == \ ["quartier mentionné (Sillery) ≠ secteur de l'annonce (Sainte-Foy)"] # secteur cohérent -> aucune incohérence res2 = analyser(txt, sector="Sillery") assert res2["incoherences"] == [] # quartier d'une autre ville que celle de l'annonce res3 = analyser("Grande chambre à louer dans Verdun, près du métro et " "du canal, disponible immédiatement.", city="Québec") assert res3["faits"]["quartier_mentionne"] == "Verdun" assert any("Montréal" in i for i in res3["incoherences"]) def test_quartier_confiance_faible_sans_preposition(): res = analyser("Sillery offre de nombreux services et une belle qualité " "de vie à distance de marche du fleuve.") assert res["faits"]["quartier_mentionne"] == "Sillery" assert res["confiance"]["quartier_mentionne"] == "faible" # -- étape C : sections thématiques ---------------------------------------------- def test_sections_thematiques_sans_titres(): texte = ("Ce grand logement lumineux se trouve au coeur d'un secteur " "paisible et verdoyant, à quelques minutes des principaux axes " "routiers de la ville. " "La colocation compte quatre personnes et les espaces communs " "sont spacieux et bien entretenus. " "Le loyer est de 600 $ par mois et le chauffage est inclus dans " "le prix demandé chaque mois. " "Les animaux ne sont pas acceptés et il est interdit de fumer " "partout dans l'immeuble en tout temps. " "Une visite guidée peut être organisée sur rendez-vous durant " "la semaine ou la fin de semaine.") assert len(texte) > 400 res = analyser(texte) titres = [s["titre"] for s in res["sections"]] assert titres == ["Description", "L'espace de vie", "Frais et conditions", "Bon à savoir"] par_titre = {s["titre"]: s["texte"] for s in res["sections"]} assert "colocation compte quatre personnes" in par_titre["L'espace de vie"] assert "600" in par_titre["Frais et conditions"] assert "animaux" in par_titre["Bon à savoir"] # chaque section est du texte source nettoyé, jamais reformulé for s in res["sections"]: for phrase in s["texte"].split(". "): assert phrase.rstrip(".") in res["texte_nettoye"] def test_texte_court_sans_titres_section_unique(): res = analyser("Joli studio à louer au centre-ville, tout près des " "cafés, des parcs et des transports en commun.") assert res["sections"] == [{"titre": "Description", "texte": res["texte_nettoye"]}] # -- en_bref et complétude ------------------------------------------------------- def test_en_bref_sans_invention(): # aucun fait extrait -> aucun « en bref » généré res = analyser("Venez découvrir ce charmant logement situé tout près du " "fleuve et des grands parcs de la ville.") assert res["en_bref"] is None # seuls les faits présents apparaissent (pas de prix -> pas de « Loyer ») res2 = analyser("Grande chambre disponible dès maintenant avec salle de " "bain commune, dans une maison chaleureuse.") assert "Loyer" not in res2["en_bref"] assert "maintenant" in res2["en_bref"] def test_completude(): # prix (20) + texte > 200 caractères (15) = 35 res = analyser("Le loyer est de 700$ par mois pour ce logement. Situé " "sur une rue paisible bordée d'arbres matures, à distance " "de marche des commerces, des cafés et des parcs du " "secteur, ce logement saura vous charmer par sa grande " "luminosité naturelle.") assert res["completude"] == 35 # aucun fait, texte court -> 0 res2 = analyser("Venez découvrir ce charmant logement situé tout près " "du fleuve et des grands parcs.") assert res2["completude"] == 0 def test_schema_et_json_serialisable(): res = analyser(DESC_2435_LAURIER) assert set(res) == {"version", "texte_nettoye", "en_bref", "sections", "faits", "confiance", "incoherences", "completude"} assert set(res["faits"]) == { "prix_mensuel", "date_disponibilite", "duree_bail_minimale_mois", "nb_occupants_total", "salle_de_bain", "cuisine", "electromenagers", "inclusions", "contraintes", "depot_mentionne", "quartier_mentionne"} assert res["version"] == 1 assert all(v in ("haute", "faible") for v in res["confiance"].values()) json.dumps(res, ensure_ascii=False) # sérialisable tel quel # -- test d'or : 2435 boul. Laurier (Gestipro) ----------------------------------- def test_or_2435_laurier(): """Faits attendus vérifiés à la main sur la vraie description Gestipro.""" res = analyser(DESC_2435_LAURIER, price=450.0, sector="Sainte-Foy", city="Québec") # étape A : doublon retiré, typographie, titres capitalisés assert "Disponible disponible" not in res["texte_nettoye"] assert "Sillery. Disponible\ndès maintenant" in res["texte_nettoye"] assert f"450{NBSP}$ par mois" in res["texte_nettoye"] assert "L’ESPACE DE VIE" not in res["texte_nettoye"] # étape B : les faits, vérifiés à la main assert res["faits"] == { "prix_mensuel": 450.0, "date_disponibilite": "now", "duree_bail_minimale_mois": 3, "nb_occupants_total": 11, "salle_de_bain": "commune", "cuisine": "commune", "electromenagers": ["frigo", "cuisiniere", "laveuse", "secheuse"], "inclusions": ["chauffage", "electricite", "eau_chaude", "wifi", "meuble"], "contraintes": ["pas_d_animaux", "references_requises", "occupation_simple"], "depot_mentionne": None, "quartier_mentionne": "Sillery", } assert all(v == "haute" for v in res["confiance"].values()) # étape C : sections découpées selon les titres du texte source assert [s["titre"] for s in res["sections"]] == \ ["Description", "L’espace de vie", "Frais et charges", "À proximité / secteur"] assert "Total de 11 chambreurs" in res["sections"][1]["texte"] assert "Wifi inclus" in res["sections"][2]["texte"] # en_bref : gabarit strict, uniquement des faits extraits assert res["en_bref"] == ( f"Loyer de 450{NBSP}$ par mois, disponible dès maintenant. " "Meublé ; chauffage, électricité, eau chaude et wifi inclus. " "11 occupants au total, cuisine et salle de bain communes, " "bail minimum de 3 mois.") # étape D : incohérence de quartier + complétude maximale assert res["incoherences"] == \ ["quartier mentionné (Sillery) ≠ secteur de l'annonce (Sainte-Foy)"] assert res["completude"] == 100