Que fait Extraction d'états financiers ?
Transforme les rapports PDF en faits vérifiés : financial_fact(document, période, libellé, périmètre, valeur, unité, statut d'audit).
Développée et éprouvée sur deux années de dépôts à la Bourse de Colombo (2 707 rapports, 720 000 chiffres, 99,4 % complètement chargés).
La méthode est ce qui la rend rapide : un analyseur automatique traite la plupart des rapports, des vérifications arithmétiques décident ce qui est fiable, une
étape de tri explique exactement pourquoi chaque état restant a échoué, et seuls ces états sont lus à partir d'images de pages,
par lots, par des sous-agents. Chaque modification n'atteint la base publiée qu'après un chargement par étapes et un audit.
La règle unique sur laquelle tout repose
Un chiffre ne se charge que si sa colonne passe les vérifications arithmétiques (actif = passif + capitaux propres ; résultat avant impôt - impôt = résultat net ; part du groupe + intérêts minoritaires = résultat ; sections du tableau de flux = variation nette ; solde d'ouverture + variation = solde de clôture). Rien n'est ajusté pour faire passer une vérification. Les vérifications n'autorisent que les arrondis imprimés (quelques unités du dernier chiffre imprimé : 2 pour deux termes, 3 pour trois, 5 pour plus), donc un total décalé de 1 à 2 unités par rapport à ses parties passe, et davantage est une erreur de lecture ou d'impression. C'est pourquoi un analyseur, un moteur OCR, une transcription visuelle et un humain peuvent tous alimenter la même base : les vérifications, pas la source, décident. Lorsqu'une vérification échoue, soit la lecture est erronée (relire), soit l'impression est erronée (enregistrer pourquoi, laisser de côté).
Corollaires importants en pratique :
- Les libellés restent exactement tels qu'imprimés et correspondent aux lignes standard uniquement via des alias exacts (
label_alias), jamais par correspondance floue. Un libellé mal imprimé rend un état non chargeable plutôt que silencieusement erroné. - Un total imprimé sans libellé peut recevoir un libellé entre crochets (
[Total actif]) car sa position le définit. - "UNCHECKED" (aucune vérification possible) n'est pas "FAIL". Cela signifie généralement une mise en page que le vérificateur ne connaît pas encore - corrigez le vérificateur une fois, et chaque rapport avec cette mise en page se chargera (voir références/checks.md).
Où se trouvent les choses
scripts/pipeline/- le pipeline :parse_report.py(PDF -> tableaux JSON),segment.py,columns.py,tokens.py,checks.py,ocr_words.py(OCR en tant que couche de texte factice),load_facts.py(JSON -> BD, uniquement colonnes passantes),vision_tables.py(transcriptions d'images -> tableaux JSON),extract_all.py,normalize.py. Carte, formes de données et comment l'adapter à un autre marché : références/pipeline.md.scripts/triage.py,scripts/make_batches.py,scripts/render.py,scripts/stage_publish.py- les outils pour les étapes ci-dessous.assets/schema.sql(base de données),assets/dictionary.sql(252 lignes standard + 471 alias exacts de libellés),assets/worker_brief.md(brief pour les travailleurs de transcription),assets/examples/(fichiers de transcription).- Le répertoire du projet : là où se trouvent la base de données, les PDF et les sorties. L'utilisateur le nomme dans
CLAUDE.md/AGENTS.md(par ex. "Projet financier : ~/stocks/cse_financials") ; demandez s'il n'y est pas. Structure :db/<nom>.db,reports/<SYMBOLE>/<doc>.pdf, sortie de l'analyseurextracted_final/, source publiéeextracted_final_v/, transcriptionsreviewed/vision/, notesdocs/. Si le projet a son proprepipeline/, traitez-le comme le code actuel (cette copie de la compétence peut être plus ancienne).
Exécutez toujours Python avec -I (isolé) et CSE_OCR=0 sauf si l'OCR est souhaité (le préfixe CSE_ est historique).
Le PDF d'un rapport : select local_path from report_document where document_id=? (relatif au répertoire du projet), puis
python -I scripts/render.py <pdf> <pages> <out_dir> --text pour extraire la couche de texte et rendre les pages.
Flux de travail
1. Périmètre et téléchargement
Choisissez la plage de périodes ; prenez les dates de fin de période dans les titres des rapports (les dates de dépôt boursières ne sont pas fiables). Remplissez
les lignes report_document (une par rapport : société, catégorie ANNUAL/INTERIM, titre, fin de période, URL source), puis
téléchargez en parallèle et de manière reprenable. La partie spécifique à la bourse (API, URL, particularités) appartient à une compétence
associée - cse-financials pour Colombo. Conservez chaque bloc d'années comme sa propre exécution de ces étapes. Avant un nouveau marché ou de nouvelles
années, lisez références/traps.md : il liste les pièges de données qui coûtent le plus de temps.
2. Analysez tout avec une version de l'analyseur
python -I scripts/pipeline/extract_all.py <db> <out_dir> --workers N avec CSE_OCR=0 (environ 9 min pour 2 700
rapports), puis un passage OCR séparé pour les scans (GPU si disponible ; voir les pièges). Après toute modification de l'analyseur, ré-analysez tous
les rapports et comparez par rapport - des ré-analyses partielles rendent la base de données non reproductible.
3. Chargez et lisez le statut
load_facts.py (via stage_publish.py, étape 6) donne à chaque rapport le statut EXTRACTED (IS et BS chargés), PARTIAL ou
FAILED. Attendez-vous à environ 80 % EXTRACTED rien qu'avec l'analyseur.
4. Trions ce qui reste - avant de regarder une page
python -I scripts/triage.py <db> <extracted_dir> triage.json --pdf-hints
python -I scripts/make_batches.py triage.json batches/ --target=12
Le tri donne pour chaque état manquant la raison (not_found, UNCHECKED, FAIL, no_columns, no_months,
unit_unproven, pass_refused...) et où chercher. Lisez les comptages : un grand groupe avec une seule raison (par ex. 100
UNCHECKED) signifie souvent un manque dans le vérificateur ou une règle du chargeur - corriger le code vaut mieux que transcrire des centaines de pages. Vérifiez également
si les rapports FAILED sont bien des états : les avis, listes d'actionnaires et erratas sont courants et nécessitent seulement une
raison enregistrée.
5. Transcrivez à partir d'images, par lots
Chaque travailleur reçoit un fichier de lot et assets/worker_brief.md (remplissez les chemins). Il trouve la page
d'état, la lit (d'abord la couche de texte, puis l'image pour la mise en page et tout ce qui est douteux), écrit reviewed/vision/<doc>.txt, exécute
vision_tables.py sur ses documents jusqu'à ce que chaque colonne PASSE, et écrit bNN.result.json. Le travailleur a besoin d'un modèle qui
peut lire les images. Format et règles : références/vision-format.md. Exécution des travailleurs en parallèle,
limites d'utilisation, reprise et examen des résultats : références/orchestration.md. Sans sous-agents, travaillez
sur les lots un après l'autre avec le même brief.
Pendant l'exécution des lots, lisez chaque résumé de résultat dès son arrivée :
- "UNCHECKED parce que le vérificateur ne reconnaît pas X" récurrent -> étendez
checks.py(références/checks.md), retestez les fichiers concernés avecvision_tables.py, ajoutez la règle au brief pour que les lots suivants la connaissent. Les tableaux déjà écrits passent alors sans retranscription. - Décisions discrétionnaires (statut d'audit, colonnes retraitées, périodes de longueur inhabituelle, libellés fournis) -> vérifiez qu'elles suivent le
brief ; corrigez centralement (par ex. un
sedsur les lignes COLS) plutôt que de relancer un travailleur.
6. Mettre en scène, auditer, publier
python -I scripts/stage_publish.py --project <dir> # mise en scène + audit uniquement
python -I scripts/stage_publish.py --project <dir> --publish # après avoir lu l'audit
La publication est refusée si l'audit montre des rapports quittant le statut EXTRACTED, des rapports devenant vides, des erreurs d'intégrité/FK, des valeurs aberrantes d'unités, ou des faits perdus/modifiés dans des rapports sans fichier de transcription (cela signifie qu'un changement de code a touché des données que personne n'a relues). Les gains en dehors des rapports vision sont normaux (un conflit résolu libère la colonne d'un autre rapport) mais regardez chacun d'eux. Publiez par étapes (les lots terminés d'abord) pour que le travail survive aux interruptions ; exécutez ensuite les tests du projet s'il en a.
7. Clôturez chaque état
L'objectif est : chaque état manquant est soit chargé, soit a une raison indiquée. Construisez la table de résultats par état
(chargé / non chargé + raison à partir des lignes MISSING et des fichiers de résultats). Pour chaque état non chargé, vérifiez
si les mêmes chiffres sont déjà chargés à partir d'un autre rapport (comparatifs de l'année suivante, l'intermédiaire correspondant) - généralement
ils le sont, et cela transforme un "écart" en "couvert". Enregistrez les résultats dans la documentation du projet.
Économie de jetons et de temps
- Trions d'abord ; n'envoyez jamais un travailleur pour "trouver ce qui ne va pas" sans la raison et les indices de page.
- Couche de texte avant l'image : une couche de texte propre confirme les chiffres gratuitement ; l'image règle la mise en page. Rendez à 1500 px ; rognez
(
render.py --clip) au lieu de rendre des pages entières plus grandes. Les scans avec des couches de texte indésirables ("9,O4349L") nécessitent des images. - Regroupez par société : une mise en page, plusieurs trimestres. ~10 rapports ou ~12 unités de coût par lot, environ 6 travailleurs à la fois.
- Les travailleurs écrivent le tableau complet même lorsqu'une vérification reste UNCHECKED, plus une ligne
MISSING: si le vérificateur est étendu plus tard, le tableau se charge sans une autre lecture. - Corrigez les problèmes systématiques dans le code (vérificateur, chargeur) une fois, de manière centrale ; réexécutez
vision_tables.py(quelques secondes) au lieu de relire les pages. - L'orchestrateur ne visualise pas les images de page sauf pour régler un litige ; il lit les résumés, exécute les vérifications et les audits.
- Limitez les résumés des travailleurs à 3-6 lignes ; les détails appartiennent aux commentaires des fichiers.
Définition de "terminé"
Par rapport : EXTRACTED, ou PARTIAL/FAILED avec une raison pour chaque état manquant qui nomme la preuve
imprimée (par ex. "total imprimé 3 115 995 977 vs equity + liabilities 3 115 995 968"), ou "couvert par le rapport N". Publié uniquement
via stage_publish.py, avec une sauvegarde, un audit propre (ou chaque drapeau expliqué), des tests réussis, et la table
de résultats plus une note datée dans la documentation du projet.