dimhold.by
← Writing

A model behind my bank statement regexes turned 300 loud failures into 249 silent ones

Since ChatGPT came out 2 weeks ago, my feed is full of screenshots where someone pastes a document into it and gets the fields back. A bank statement is the classic case for this. Every bank exports it in its own way. The usual answer is a parser with a regex for each bank, which breaks the day the bank changes the export. I have never measured how a model compares with such a parser. I assumed that a model would at least copy a date correctly. So the question for me was only how often it gets the amount wrong.

The setup

I took 4 statement layouts and generated 150 transactions in each with a script, seed 2022, so every right answer is known. Every transaction should become 3 fields: the date, the amount with a sign (money out is negative) and the counterparty. The layouts look like this:

A  14.06.2022  CARD PAYMENT MEDIA EXPERT 7734 GDANSK  -248,22  18 848,37
B  Date: 2022-06-20  Description: Transfer to Piotr Wisniewski, title: gift  Amount: -2,327.19 PLN  Balance: -5,575.15 PLN
C  12 Jun 2022  CARD PAYMENT BOOKING.COM 33.40 EUR  157.32 DR  25,180.67 CR
D  15/06/2022  Card payment                4.55                 53,361.89
               AUCHAN 5293 GDANSK

The last number in each line is the balance after the transaction. Card payments abroad also carry the amount in the original currency. I wrote the regex parser for A and B only, 34 lines of Python, before I looked at C and D. C and D play the bank that changed its export.

The model is Flan-T5 from Google, base (250M parameters) and large (780M), released in October. It runs on my machine, 4 cores of an AMD EPYC virtual machine, Python 3.9, torch 1.13, transformers 4.25.1, greedy decoding. Each transaction gets 4 questions: the date, the amount, the direction of the money and who the other party is. The prompt also has the line of the statement with the first and the last day of the period. A real page has it and it tells which date format the bank uses. Both methods get the transactions already cut into blocks, so splitting the page is not measured here.

I count a transaction as right only if all 3 fields are right. A wrong result is loud when it does not parse. That means the regex did not match or the model gave a date that is not a date. It is silent when it parses and is wrong.

The regexes

On A and B the parser got 293 of 300 transactions right. The 7 wrong ones were mine. In layout A the name and the transfer title run together in capitals: TRANSFER FROM JAN KOWALSKI LOAN BACK. I took the first 2 words as the name. For the salary from SOFTHOUSE SP. Z O.O. this gives SOFTHOUSE SP., without any error. I wrote that rule without thinking about companies.

On C and D the parser matched nothing, 300 of 300 loud. This was what I expected. It is also the usual argument for the model.

The model

Flan-T5 large got 91 of 600 transactions right. Base got 107, so the larger model was not better.

transactions per layout, 150 each right wrong, loud wrong, silent regex for A and B A known 143/0/7 B known 150/0/0 C new 0/150/0 D new 0/150/0 Flan-T5 base A known 1/64/85 B known 74/0/76 C new 25/23/102 D new 7/1/142 Flan-T5 large A known 2/23/125 B known 47/0/103 C new 15/9/126 D new 27/0/123
Right on all 3 fields, of 600: regex 293, base 107, large 91. The regex never saw C and D.

I expected mistakes in the amount, but the first thing that broke was the date. I spent some time thinking my date parser was broken before I looked at the raw answers. With the period line in the prompt, large answered with the first day of the period in 360 of 600 transactions. For a payment made on 14 June it gave 01.06.2022. So I removed the period line and asked again. Large got the date right in 291 of 600 and base in 567. Often the answer of large is not the date from the line: 01.10.2022 for 10.01.2022, 01/26/2022 in the American order for 26/01/2022. Sometimes the year was 2020. It got 148 of 150 right only in layout B, where the date was already written as 2022-06-20. When I asked it to copy the date exactly as written, it did so in 162 of 600. 143 of them were in B.

Large answered “out” for every incoming transfer except in layout D, where the line says Incoming transfer in words. It answered “in” for 33 of 129 incoming transfers, all 33 in D. Large still answered “out” on lines without a minus and on lines with CR. I thought the order of the options in my question was to blame. So I asked again with “in or out” instead of “out or in”. Large did not change a single answer. Base did depend on the order. It answered “out” in all 600 when “out” came first and “in” in 583 of 600 when “in” came first.

The amount without the sign was right in 423 of 600. In 114 of the wrong ones the model returned the balance, the last number in the line. In 22 it returned the amount in euros or dollars from the description. As a counterparty large often returned more than the name, like SPOTIFY INTERNET. 61 times it returned only the city. If I accept any answer that contains the right name, it gets 526 of 600. With that lenient match large would get 160 transactions right instead of 91.

period line Statement for the period 01.06.2022 - 30.06.2022 transaction, layout A 14.06.2022 CARD PAYMENT MEDIA EXPERT 7734 GDANSK -248,22 18 848,37 large answered right date 01.06.2022 14.06.2022 first day of the period 360 of 600 amount 18 848,37 -248,22 balance after it 114 of 600 direction out out right incoming: 96 of 129 out counterparty GDANSK Media Expert city 61 of 600
One line, 3 wrong fields, each of them parses. The right column counts the same mistake over all 600 transactions.

Almost all of this is silent, because a balance parses as an amount just as well as the amount does. Of 509 wrong transactions of large, 32 were loud.

Regex first, model when it does not match

This is the setup I would actually build: the parser for the formats I know and the model only for lines it does not recognise. It got 335 of 600 right, more than each of the 2 alone. But 256 transactions came out wrong without any error. Apart from my 7 salary lines, all of them came from C and D, the 300 lines that had failed loudly before.

600 transactions, all 4 layouts right wrong, loud wrong, silent regex for A and B 293/300/7 Flan-T5 large 91/32/477 regex, else large 335/9/256 regex for all 4 582/0/18
The model in the gap adds 42 right transactions. Its wrong answers in C and D raise no error. 29 more lines of regex leave 18 wrong, all of them the same salary bug.

Then I did what I would do anyway when a bank changes its export: I wrote the regexes for C and D. That took 29 more lines. The parser got 582 of 600 right and 0 loud. The 18 wrong are the same salary bug, which I copied into layout C together with the rest of the code. It costs about 6 microseconds per transaction. Large needs a median of 1.2 seconds per question, about 5 seconds per transaction.

What I did not check

This is one small model that I can run myself. It is not the GPT-3 behind the OpenAI API and not ChatGPT, which has no API yet. Nothing larger was tested. I used one prompt per field without examples and did not train anything. Making the model output all fields at once in JSON is also something I did not try. The statements are synthetic and clean text, without PDF and OCR.

For now I keep the regex per bank. The model I would use only to suggest a parse for a line the parser does not know. A person checks it before it goes anywhere.