spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1# -----------------------------------------------------------------------------2# Lou-Ka — tests du text mining des descriptions (louka/textmine.py)3# -----------------------------------------------------------------------------4import json56from louka.textmine import analyser78NBSP = " " # espace insécable (typographie québécoise : « 450 $ »)910# Cas de référence : VRAIE description de la chambre Gestipro du11# 2435 boul. Laurier (source gestipro:24813, copiée telle quelle de la base).12DESC_2435_LAURIER = (13 "Chambre meublé de base en location située au 2435 boul. Laurier à "14 "Sillery. Disponible disponible\n"15 "dès maintenant\n"16 "à 450$ par mois, chauffage, électricité, eau chaude et wifi inclus. "17 "Location pour minimum 3 mois.\n"18 "L’ESPACE DE VIE\n"19 "Total de 11 chambreurs dans la maison\n"20 "Cuisine et salle de bain commune aux autres occupants de l’appartement\n"21 "Réfrigérateur, cuisinière, laveuse et sécheuse inclus dans les pièces communes\n"22 "Chambre meublée de base comme sur les photos, pour occupation simple seulement\n"23 " \n"24 "FRAIS ET CHARGES\n"25 "Chauffage, eau chaude et électricité inclus\n"26 "Wifi inclus\n"27 "À PROXIMITÉ / SECTEUR\n"28 "Secteur très prisé de la ville\n"29 "À proximité des ponts, de l’autoroute Robert-Bourassa et de l’autoroute Henri IV\n"30 "Près de tous les services de Sillery et Ste-Foy dont Laurier Québec, "31 "Place de la Cité et Place Ste-Foy.\n"32 "Université Laval à distance de marche\n"33 "Prendre note qu’afin d’offrir à nos locataires une tranquillité, les "34 "animaux ne sont pas acceptés. Enquête de crédit obligatoire pour tous "35 "nos nouveaux locataires.\n"36 "Pour visiter, communiquez avec Gestipro au 418-522-2525 poste 1\n"37 "Pour consulter tous nos appartements à louer, visitez le www.gestipro.info"38)394041# -- garde d'entrée -------------------------------------------------------------4243def test_description_vide_ou_trop_courte():44 assert analyser("") is None45 assert analyser(" ") is None46 assert analyser("Beau logement à louer.") is None # < 40 caractères47 assert analyser("x" * 39) is None48 assert analyser("Grand appartement lumineux à louer au centre-ville.") is not None495051# -- étape A : nettoyage ---------------------------------------------------------5253def test_titres_en_majuscules_deviennent_sections():54 res = analyser("Magnifique appartement au bord de l'eau du fleuve.\n"55 "L'ESPACE DE VIE\n"56 "Grande pièce ouverte avec beaucoup de rangement pratique.")57 assert "L'espace de vie" in res["texte_nettoye"]58 assert "L'ESPACE DE VIE" not in res["texte_nettoye"]59 assert [s["titre"] for s in res["sections"]] == ["Description", "L'espace de vie"]60 assert res["sections"][1]["texte"] == \61 "Grande pièce ouverte avec beaucoup de rangement pratique."626364def test_segment_majuscules_dans_une_ligne():65 # titre EN MAJUSCULES enchâssé au milieu d'une ligne (≥ 2 mots, ≥ 9 car.)66 res = analyser("Belle chambre au centre-ville. FRAIS ET CONDITIONS "67 "Le loyer inclut le chauffage et l'électricité.")68 assert "Frais et conditions" in [s["titre"] for s in res["sections"]]697071def test_doublons_de_mots_consecutifs():72 # insensible à la casse et aux accents73 res = analyser("Logement rénové RÉNOVÉ récemment. Disponible disponible "74 "dès maintenant pour les visites de groupe.")75 assert "rénové RÉNOVÉ" not in res["texte_nettoye"]76 assert "Disponible disponible" not in res["texte_nettoye"]77 assert "Disponible dès maintenant" in res["texte_nettoye"]787980def test_phrases_collees():81 res = analyser("Appartement très lumineuxVenez le visiter rapidement, "82 "près du McDonald et du parc du quartier.")83 assert "lumineux. Venez" in res["texte_nettoye"]84 assert "McDonald" in res["texte_nettoye"] # nom propre intact858687def test_typographie_quebecoise():88 res = analyser("Grand 4 1/2 à 875$ par mois avec rabais de 5% !!! "89 "Venez visiter l'appartement dès aujourd'hui.")90 assert f"875{NBSP}$ par mois" in res["texte_nettoye"] # insécable avant $91 assert f"5{NBSP}%" in res["texte_nettoye"] # insécable avant %92 assert "!!!" not in res["texte_nettoye"] # « !!! » -> « ! »93 assert " " not in res["texte_nettoye"] # espaces multiples949596def test_puces_normalisees():97 res = analyser("Inclusions du logement :\n- Frigo inclus\n* Cuisinière "98 "incluse\n· Balcon avant avec vue")99 lignes = res["texte_nettoye"].split("\n")100 assert lignes[1:] == ["• Frigo inclus", "• Cuisinière incluse",101 "• Balcon avant avec vue"]102103104# -- étape B : extraction --------------------------------------------------------105106def test_prix_mensuel_et_confiance():107 res = analyser("Beau 4 1/2 chauffé et éclairé à 995$ par mois, dans un "108 "immeuble tranquille près des services.")109 assert res["faits"]["prix_mensuel"] == 995110 assert res["confiance"]["prix_mensuel"] == "haute"111 # montant sans contexte mensuel -> confiance faible, exclu de « en_bref »112 res2 = analyser("Grand studio lumineux au centre-ville, rabais de 500 $ "113 "à la signature avant la fin du mois de septembre.")114 assert res2["faits"]["prix_mensuel"] == 500115 assert res2["confiance"]["prix_mensuel"] == "faible"116 assert res2["en_bref"] is None or "Loyer" not in res2["en_bref"]117118119def test_incoherence_prix_annonce():120 res = analyser("Chambre à louer à 450$ par mois dans une grande maison "121 "près de l'université et des autobus.", price=500.0)122 assert any("450" in i and "500" in i for i in res["incoherences"])123124125def test_date_disponibilite_ancree():126 res = analyser("Grande chambre à louer, disponible dès maintenant, dans "127 "un secteur paisible près des autobus.")128 assert res["faits"]["date_disponibilite"] == "now"129 # une date évènementielle sans ancre de disponibilité n'est PAS une dispo130 res2 = analyser("Grande porte ouverte samedi 15 août pour venir voir le "131 "logement au centre-ville avec nos agents.")132 assert res2["faits"]["date_disponibilite"] is None133134135def test_duree_bail_minimale():136 txt = "Chambre dans une maison partagée du centre-ville. Location pour "137 assert analyser(txt + "minimum 3 mois.")["faits"]["duree_bail_minimale_mois"] == 3138 assert analyser(txt + "un bail de 12 mois.")["faits"]["duree_bail_minimale_mois"] == 12139 assert analyser(txt + "un bail d'un an.")["faits"]["duree_bail_minimale_mois"] == 12140 # durée liée à une promotion -> confiance faible141 res = analyser("Un mois gratuit sur un bail de 13 mois pour toute "142 "signature avant la fin du mois prochain.")143 assert res["faits"]["duree_bail_minimale_mois"] == 13144 assert res["confiance"]["duree_bail_minimale_mois"] == "faible"145146147def test_nb_occupants_total():148 res = analyser("Total de 11 chambreurs dans la maison, cuisine équipée "149 "et salon partagés entre les occupants.")150 assert res["faits"]["nb_occupants_total"] == 11151 res2 = analyser("Belle grande chambre dans une colocation de 5 située "152 "tout près du cégep et des commerces.")153 assert res2["faits"]["nb_occupants_total"] == 5154155156def test_salle_de_bain_et_cuisine():157 res = analyser("Chambre avec salle de bain commune et cuisine partagée "158 "avec les autres locataires de l'étage.")159 assert res["faits"]["salle_de_bain"] == "commune"160 assert res["faits"]["cuisine"] == "commune"161 res2 = analyser("Grande suite avec salle de bain privée attenante, dans "162 "un immeuble récent proche du terminus.")163 assert res2["faits"]["salle_de_bain"] == "privee"164 # tournure combinée « cuisine et salle de bain commune »165 res3 = analyser("Cuisine et salle de bain commune aux autres occupants "166 "de l'appartement, au deuxième étage.")167 assert res3["faits"]["salle_de_bain"] == "commune"168 assert res3["faits"]["cuisine"] == "commune"169170171def test_electromenagers():172 res = analyser("Réfrigérateur, cuisinière, laveuse et sécheuse inclus, "173 "ainsi qu'un lave-vaisselle et un micro-ondes neufs.")174 assert res["faits"]["electromenagers"] == \175 ["frigo", "cuisiniere", "laveuse", "secheuse", "lave_vaisselle", "micro_ondes"]176 # « entrées laveuse-sécheuse » = branchement, pas un appareil fourni177 res2 = analyser("Logement avec entrées laveuse-sécheuse et grande cuisine "178 "éclairée donnant sur la cour arrière.")179 assert "laveuse" not in res2["faits"]["electromenagers"]180181182def test_inclusions():183 res = analyser("Chambre meublée à louer, chauffage, électricité, eau "184 "chaude et wifi inclus pour tous les locataires.")185 assert res["faits"]["inclusions"] == \186 ["chauffage", "electricite", "eau_chaude", "wifi", "meuble"]187 res2 = analyser("Grand logement au dernier étage, électricité à la "188 "charge du locataire et chauffage non inclus.")189 assert "electricite" not in res2["faits"]["inclusions"]190 assert "chauffage" not in res2["faits"]["inclusions"]191192193def test_contraintes():194 res = analyser("Logement non-fumeur. Les animaux ne sont pas acceptés. "195 "Enquête de crédit obligatoire. Réservé aux étudiants seulement.")196 assert res["faits"]["contraintes"] == \197 ["non_fumeur", "pas_d_animaux", "references_requises", "etudiants_seulement"]198199200def test_depot_mentionne():201 res = analyser("Un dépôt de garantie de 300 $ est demandé à la remise "202 "des clés du logement, remboursable au départ.")203 assert res["faits"]["depot_mentionne"] is not None204 assert "300" in res["faits"]["depot_mentionne"]205 res2 = analyser("Grand logement lumineux à louer dans un secteur calme "206 "près des parcs et des écoles du quartier.")207 assert res2["faits"]["depot_mentionne"] is None208209210def test_quartier_et_incoherence_secteur():211 txt = ("Chambre située au 2435 boul. Laurier à Sillery, tout près des "212 "services et de l'université.")213 res = analyser(txt, sector="Sainte-Foy")214 assert res["faits"]["quartier_mentionne"] == "Sillery"215 assert res["incoherences"] == \216 ["quartier mentionné (Sillery) ≠ secteur de l'annonce (Sainte-Foy)"]217 # secteur cohérent -> aucune incohérence218 res2 = analyser(txt, sector="Sillery")219 assert res2["incoherences"] == []220 # quartier d'une autre ville que celle de l'annonce221 res3 = analyser("Grande chambre à louer dans Verdun, près du métro et "222 "du canal, disponible immédiatement.", city="Québec")223 assert res3["faits"]["quartier_mentionne"] == "Verdun"224 assert any("Montréal" in i for i in res3["incoherences"])225226227def test_quartier_confiance_faible_sans_preposition():228 res = analyser("Sillery offre de nombreux services et une belle qualité "229 "de vie à distance de marche du fleuve.")230 assert res["faits"]["quartier_mentionne"] == "Sillery"231 assert res["confiance"]["quartier_mentionne"] == "faible"232233234# -- étape C : sections thématiques ----------------------------------------------235236def test_sections_thematiques_sans_titres():237 texte = ("Ce grand logement lumineux se trouve au coeur d'un secteur "238 "paisible et verdoyant, à quelques minutes des principaux axes "239 "routiers de la ville. "240 "La colocation compte quatre personnes et les espaces communs "241 "sont spacieux et bien entretenus. "242 "Le loyer est de 600 $ par mois et le chauffage est inclus dans "243 "le prix demandé chaque mois. "244 "Les animaux ne sont pas acceptés et il est interdit de fumer "245 "partout dans l'immeuble en tout temps. "246 "Une visite guidée peut être organisée sur rendez-vous durant "247 "la semaine ou la fin de semaine.")248 assert len(texte) > 400249 res = analyser(texte)250 titres = [s["titre"] for s in res["sections"]]251 assert titres == ["Description", "L'espace de vie",252 "Frais et conditions", "Bon à savoir"]253 par_titre = {s["titre"]: s["texte"] for s in res["sections"]}254 assert "colocation compte quatre personnes" in par_titre["L'espace de vie"]255 assert "600" in par_titre["Frais et conditions"]256 assert "animaux" in par_titre["Bon à savoir"]257 # chaque section est du texte source nettoyé, jamais reformulé258 for s in res["sections"]:259 for phrase in s["texte"].split(". "):260 assert phrase.rstrip(".") in res["texte_nettoye"]261262263def test_texte_court_sans_titres_section_unique():264 res = analyser("Joli studio à louer au centre-ville, tout près des "265 "cafés, des parcs et des transports en commun.")266 assert res["sections"] == [{"titre": "Description",267 "texte": res["texte_nettoye"]}]268269270# -- en_bref et complétude -------------------------------------------------------271272def test_en_bref_sans_invention():273 # aucun fait extrait -> aucun « en bref » généré274 res = analyser("Venez découvrir ce charmant logement situé tout près du "275 "fleuve et des grands parcs de la ville.")276 assert res["en_bref"] is None277 # seuls les faits présents apparaissent (pas de prix -> pas de « Loyer »)278 res2 = analyser("Grande chambre disponible dès maintenant avec salle de "279 "bain commune, dans une maison chaleureuse.")280 assert "Loyer" not in res2["en_bref"]281 assert "maintenant" in res2["en_bref"]282283284def test_completude():285 # prix (20) + texte > 200 caractères (15) = 35286 res = analyser("Le loyer est de 700$ par mois pour ce logement. Situé "287 "sur une rue paisible bordée d'arbres matures, à distance "288 "de marche des commerces, des cafés et des parcs du "289 "secteur, ce logement saura vous charmer par sa grande "290 "luminosité naturelle.")291 assert res["completude"] == 35292 # aucun fait, texte court -> 0293 res2 = analyser("Venez découvrir ce charmant logement situé tout près "294 "du fleuve et des grands parcs.")295 assert res2["completude"] == 0296297298def test_schema_et_json_serialisable():299 res = analyser(DESC_2435_LAURIER)300 assert set(res) == {"version", "texte_nettoye", "en_bref", "sections",301 "faits", "confiance", "incoherences", "completude"}302 assert set(res["faits"]) == {303 "prix_mensuel", "date_disponibilite", "duree_bail_minimale_mois",304 "nb_occupants_total", "salle_de_bain", "cuisine", "electromenagers",305 "inclusions", "contraintes", "depot_mentionne", "quartier_mentionne"}306 assert res["version"] == 1307 assert all(v in ("haute", "faible") for v in res["confiance"].values())308 json.dumps(res, ensure_ascii=False) # sérialisable tel quel309310311# -- test d'or : 2435 boul. Laurier (Gestipro) -----------------------------------312313def test_or_2435_laurier():314 """Faits attendus vérifiés à la main sur la vraie description Gestipro."""315 res = analyser(DESC_2435_LAURIER, price=450.0,316 sector="Sainte-Foy", city="Québec")317318 # étape A : doublon retiré, typographie, titres capitalisés319 assert "Disponible disponible" not in res["texte_nettoye"]320 assert "Sillery. Disponible\ndès maintenant" in res["texte_nettoye"]321 assert f"450{NBSP}$ par mois" in res["texte_nettoye"]322 assert "L’ESPACE DE VIE" not in res["texte_nettoye"]323324 # étape B : les faits, vérifiés à la main325 assert res["faits"] == {326 "prix_mensuel": 450.0,327 "date_disponibilite": "now",328 "duree_bail_minimale_mois": 3,329 "nb_occupants_total": 11,330 "salle_de_bain": "commune",331 "cuisine": "commune",332 "electromenagers": ["frigo", "cuisiniere", "laveuse", "secheuse"],333 "inclusions": ["chauffage", "electricite", "eau_chaude", "wifi", "meuble"],334 "contraintes": ["pas_d_animaux", "references_requises", "occupation_simple"],335 "depot_mentionne": None,336 "quartier_mentionne": "Sillery",337 }338 assert all(v == "haute" for v in res["confiance"].values())339340 # étape C : sections découpées selon les titres du texte source341 assert [s["titre"] for s in res["sections"]] == \342 ["Description", "L’espace de vie", "Frais et charges",343 "À proximité / secteur"]344 assert "Total de 11 chambreurs" in res["sections"][1]["texte"]345 assert "Wifi inclus" in res["sections"][2]["texte"]346347 # en_bref : gabarit strict, uniquement des faits extraits348 assert res["en_bref"] == (349 f"Loyer de 450{NBSP}$ par mois, disponible dès maintenant. "350 "Meublé ; chauffage, électricité, eau chaude et wifi inclus. "351 "11 occupants au total, cuisine et salle de bain communes, "352 "bail minimum de 3 mois.")353354 # étape D : incohérence de quartier + complétude maximale355 assert res["incoherences"] == \356 ["quartier mentionné (Sillery) ≠ secteur de l'annonce (Sainte-Foy)"]357 assert res["completude"] == 100358