Fuzzy matching — logique de correspondance du grading Accounting
À quoi sert le fuzzy matching ?
Le grading doit retrouver quelle facture ou écriture attendue correspond à une ligne ou section étudiante, même si son identifiant manque ou est erroné. Le matching choisit cette correspondance; les correcteurs évaluent ensuite les valeurs, options, dates, références, soldes et l’ordre. Un match trouvé ne rend donc pas automatiquement la réponse correcte et son score de ressemblance n’est pas la note finale.
Cette logique est exécutée dans le script serveur BrainCloud node.js/backend/braincloud/server_scripts/_utils/by_module/byModule_utils_grade_module.js, à l’intérieur de byModuleGradeModule(). Elle n’utilise pas un modèle IA ni une distance entre chaînes de texte. Voir Grading, reassemble et calcul des scores pour la pondération finale.
Le helper commun : matchUidsToAnswers
Signature : matchUidsToAnswers(uids, answersByKey, getStrictAnswerKey, getMatchScore). Il reçoit les UID étudiants ordonnés, les réponses candidates indexées par clé, une fonction d’identification stricte et une fonction de ressemblance. Il retourne uniquement answerKeyByUid; une entrée sans correspondance est absente de ce résultat.
flowchart TD
A[Entrées étudiantes et réponses candidates] --> B[Passage strict dans l'ordre des UID]
B --> C[Réserver chaque clé stricte une seule fois]
C --> D[Entrées et réponses restantes]
D --> E[Seuil initial 1]
E --> F[Meilleure réponse disponible au-dessus du seuil pour chaque entrée]
F --> G[Consommer la réponse et retirer l'entrée appariée]
G --> H{Au moins un match dans ce passage ?}
H -->|Oui| F
H -->|Non| I{Score restant au moins égal à 0.5 ?}
I -->|Oui| J[Abaisser le seuil au meilleur score observé]
J --> F
I -->|Non| K[Retourner la correspondance partielle]
1. Priorité stricte
Le passage strict traite toutes les entrées avant tout fuzzy matching. Une clé non nulle est retenue si elle n’a pas déjà été utilisée. Elle est retirée des réponses restantes. Une deuxième entrée demandant la même clé passe dans le lot fuzzy; elle ne peut pas réutiliser cette réponse, mais peut être associée à une autre.
La validation métier de la clé appartient au callback. getStrictBillUid lit bill_id, retrouve la facture dans _billsByNumber et vérifie getBillAffectedPageUid(). Un identifiant valide pour la page est prioritaire même si les montants ou la date ressemblent davantage à une autre facture; ces champs seront corrigés ensuite.
2. Passages fuzzy
- Commencer à
currentThreshold = 1. - Pour chaque UID restant, parcourir les réponses disponibles et retenir le plus grand score satisfaisant
score ≥ currentThreshold. - Consommer immédiatement la réponse retenue. Aucun retour arrière ne réaffecte un match déjà choisi.
- Lorsqu’un passage n’a produit aucun match, abaisser le seuil au plus grand score inférieur au seuil courant mais supérieur ou égal à
0.5. - Arrêter si aucune paire admissible ne reste, ou si l’un des lots est vide.
Le seuil descend vers les scores réellement observés : ce n’est pas une suite fixe 1 → 0,9 → 0,8. Une égalité conserve la première réponse rencontrée, car une nouvelle candidate doit satisfaire score > bestScore. Le résultat dépend donc de l’ordre des UID et des clés des réponses.
Qui utilise le helper commun ?
| Correcteur | Unité et priorité stricte | Score fuzzy |
|---|---|---|
computeAuxJournalScoreBase : achats, ventes, retraits, encaissements, paie |
Section; première row contenant bill_id → facture de cette page | calculateMatchScore : partenaire, mois/année, comptes et montants |
computePettyCashJournalScore |
Row; bill_id → facture de cette page | calculateRowMatchScore : mois/année et comptes/montants |
computeSubledgersJournalTableScore : clients/fournisseurs |
Row; clé construite table/partenaire + send/receive + bill_id, hors upfront_payment | calculateRowAmountMatchScore : mois/année, sens et total de la facture |
computeLedgerAccountTableScore |
Row admissible du compte; bill_id présent dans ses réponses attendues | calculateRowAmountMatchScore : mois/année, sens et montant de l’écriture de ce compte |
Le helper n’est pas branché sur tous les correcteurs du fichier. Les stocks et le General Journal possèdent d’autres règles.
Comment sont calculés les scores ?
Journaux auxiliaires : section
calculateMatchScore retourne 0 si la facture ne concerne pas cette page. Les poids sont partenaire 0.3, date 0.3, comptes 0.4. Le partenaire correspond à l’expéditeur ou au destinataire de la facture; la date compare le mois et l’année. Les poids disponibles sont additionnés au dénominateur.
Les comptes sont extraits des rows, en excluant date, partner_uid, bill_id, report_to_account, account_uid. Les colonnes débit/crédit sont rattachées à account_uid, avec un signe négatif pour le crédit. Pour un compte présent dans la facture, le code attribue une contribution de présence ou de sens d’impact, puis une contribution de proximité du montant. La proximité utilise max(0, 1 - abs(saisi - attendu) / attendu) lorsque l’attendu est non nul.
Détails de l’implémentation actuelle : le test de date utilise if (sectionMonth && sectionYear). Comme janvier vaut 0 dans Date.getMonth(), le facteur date est omis en janvier. Le cumul de contributions par compte peut aussi produire un score supérieur à 1 : il n’est pas borné. Il faut conserver ces détails lorsqu’on diagnostique un résultat, plutôt que supposer une probabilité normalisée.
Petite caisse : row et comptes
calculateRowMatchScore utilise date 0.33 et comptes 0.66. Les comptes renseignés sont comparés à ceux de la facture; date, partner_uid, bill_id, petty_cash_total sont exclus. Un compte absent de la facture contribue 0. Un montant attendu nul et saisi nul contribue 1; sinon le code utilise la proximité relative du montant. La contribution moyenne des comptes est pondérée par 0.66.
La date compare uniquement mois/année, avec un test de présence différent de celui des sections. Sans facteur disponible, le résultat est 0. Le partenaire et le total petite caisse ne servent pas au matching de cette row.
Subledgers et ledger : row et montant
calculateRowAmountMatchScore utilise date 0.33 et montant 0.66. Le sens saisi doit être le sens attendu; si débit et crédit sont tous deux renseignés, le débit est prioritaire. Pour un attendu non nul, la contribution montant est max(0, 1 - abs(saisi - attendu) / abs(attendu)). Les facteurs présents sont renormalisés par leur somme; sans facteur, le score vaut 0.
Une date présente mais différente reste dans le dénominateur avec une contribution nulle. Une date absente est exclue. Si seul le mois/année correspond et que le montant est incorrect, le score est environ 1/3 : insuffisant pour le seuil 0.5. Si la date est absente et le sens/montant sont corrects, le score peut atteindre 1.
Exemple chiffré
Écriture attendue : débit 100 en septembre. L’étudiant saisit débit 90 dans le même mois et la même année, sans identifiant strict valide.
proximité montant = 1 - |90 - 100| / 100 = 0.9
score = (0.33 × 1 + 0.66 × 0.9) / (0.33 + 0.66)
= 0.9333…
La row peut donc être appariée lors d’un passage fuzzy, si la réponse reste disponible. Le montant 90 est ensuite comparé à 100 dans le grading des cellules; cet appariement n’accorde pas 93,33 % de la note.
Ce qui a changé dans le ledger le 30 septembre
Le ledger construit les réponses attendues par compte et numéro de facture ou formulaire, avec sens, montant, page de référence et date. Le nouveau helper remplace le fallback local d’appariement et permet de retrouver une écriture à partir de son sens, montant et mois/année quand le bill_id ne permet pas de l’identifier.
Seules les rows ayant une reference participent à cet appariement. Avec les journaux auxiliaires actifs, la référence doit contenir general_journal; sans eux, toutes les rows portant une référence sont candidates. Les reports agrégés des journaux auxiliaires et les réponses de régularisation/clôture conservent leur traitement spécialisé. La recherche ne traverse pas tous les comptes : chaque table possède ses candidates.
Après matching, le correcteur prépare bill_id, reference, la date, le débit/crédit attendu, puis corrige le solde et l’ordre. Les réponses non rencontrées peuvent produire table_bill_missing ou table_form_missing. Une référence invalide peut produire row_reference_missing.
Le General Journal garde un algorithme distinct
computeGeneralJournalScore ne passe pas par matchUidsToAnswers. Il construit wantedExplanationBySectionUids pour factures, formulaires et relations de régularisation/clôture, avec ses propres passages de seuil, priorités et traitement des doublons.
getMatchRateForGeneralJournalSection pondère la ressemblance des comptes par 4, puis ajoute l’explication sélectionnée et la date exacte lorsqu’elles sont disponibles. Les factures/formulaires comparent aussi montants et sens; les relations peuvent comparer la structure des comptes sans leurs valeurs. Le plancher des passages est 0.4 (le code arrête avant un passage dont le prochain seuil est ≤ 0.4), et une recherche complémentaire de relation utilise 0.8. Ces seuils ne remplacent pas le 0.5 du helper commun.
Ambiguïtés et diagnostic
Le helper est glouton : il cherche une bonne paire au seuil courant, pas la meilleure affectation globale de toutes les rows. Exemple : R1→A=0.9, R1→B=0.8, R2→A=0.85, R2→B=0.1. Il peut retenir R1→A puis laisser R2 sans match, alors que R1→B et R2→A permettraient deux correspondances. Les égalités dépendent de l’ordre. Une valeur NaN ne satisfait aucune comparaison de seuil.
- Identifier le correcteur de page et la liste exacte des candidates.
- Vérifier si un bill_id valide a verrouillé une réponse au passage strict.
- Examiner les champs présents : page, partenaire, date, compte, débit/crédit et montant selon le helper.
- Recalculer le score avec les poids réellement disponibles et vérifier le seuil.
- Contrôler l’ordre des UID, les doublons et les réponses consommées par d’autres entrées.
- Examiner ensuite
errors,expectedValueset les scores bruts; vérifier la pondération danspages_settingsseulement après.
Repères dans le source
byModuleGradeModulematchUidsToAnswerscalculateMatchScorecalculateRowMatchScorecalculateRowAmountMatchScorecomputeAuxJournalScoreBasecomputePettyCashJournalScorecomputeSubledgersJournalTableScorecomputeLedgerAccountTableScorenode.js/backend/braincloud/tests/unit/by_module_tests/grade_module_tests/