SPB Git

spb/lou-ka Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

HTML 99.7%
17.1 KB · 358 lines python
Raw Blame History
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