Detecting Transaction Tables in Bank Statements

By StatementSheet Engineering Team

Detecting transaction tables is the step that decides whether the rest of a bank statement parser can be trusted.

Bank statements look structured when opened in a PDF viewer. You see dates, descriptions, debit amounts, credit amounts, and balances arranged in rows. But a parser does not receive a clean table. It receives positioned text fragments.

At StatementSheet, we found that transaction table detection is often harder than row extraction itself. If the parser chooses the wrong region of the page, every later step becomes unreliable.

Why Transaction Table Detection Matters

A bank statement can contain many regions that look partially structured:

The parser must identify the region that contains actual transaction rows, not just any table-like block.

What a Transaction Table Usually Looks Like

DateDescriptionDebitCreditBalance
2024-03-14Amazon Marketplace49.90950.10

Different banks use different schemas:

Layout TypeColumns
Debit / Credit layoutDate | Description | Debit | Credit | Balance
Single amount layoutDate | Description | Amount
Running balance layoutDate | Transaction | Amount | Balance

Detection Signals

A transaction table usually has several visual and semantic signals:

PDF page ↓ Extract text positions ↓ Group text into visual rows ↓ Score row regions ↓ Detect transaction table candidate ↓ Validate with dates and amounts

Step 1 — Extract Positioned Text

Using Java and Apache PDFBox, we extract text fragments with their page coordinates. These coordinates are the raw material used for table detection.

public record TextElement(
    int page,
    String text,
    float x,
    float y,
    float width,
    float height
) {}

Step 2 — Group Text into Visual Rows

A transaction table is row-based, so the parser first groups text fragments by similar Y coordinates.

public class VisualRowBuilder {

    private static final float Y_TOLERANCE = 3f;

    public List<List<TextElement>> buildRows(List<TextElement> elements) {

        List<List<TextElement>> rows = new ArrayList<>();

        for (TextElement element : elements) {

            boolean added = false;

            for (List<TextElement> row : rows) {
                TextElement reference = row.get(0);

                if (Math.abs(reference.y() - element.y()) < Y_TOLERANCE) {
                    row.add(element);
                    added = true;
                    break;
                }
            }

            if (!added) {
                List<TextElement> row = new ArrayList<>();
                row.add(element);
                rows.add(row);
            }
        }

        return rows;
    }
}

Step 3 — Score Candidate Regions

Instead of assuming the first table-like structure is the transaction table, we score candidate regions.

public class TransactionTableScorer {

    public int score(TableRegion region) {

        int score = 0;

        if (region.hasDateLikeRows()) score += 30;
        if (region.hasAmountLikeValues()) score += 25;
        if (region.hasConsistentXPositions()) score += 20;
        if (region.hasKnownHeaders()) score += 15;
        if (region.rowCount() >= 5) score += 10;

        return score;
    }

    public boolean isLikelyTransactionTable(TableRegion region) {
        return score(region) >= 60;
    }
}

Detecting Dates and Amounts

Date and amount detection provide strong signals.

public class TransactionPatterns {

    private static final Pattern DATE =
        Pattern.compile("\\\\b\\\\d{2}/\\\\d{2}/\\\\d{4}\\\\b|\\\\b\\\\d{4}-\\\\d{2}-\\\\d{2}\\\\b");

    private static final Pattern AMOUNT =
        Pattern.compile("[-(]?\\\\d{1,3}(?:[.,]\\\\d{3})*(?:[.,]\\\\d{2})[)]?");

    public static boolean looksLikeDate(String value) {
        return value != null && DATE.matcher(value).find();
    }

    public static boolean looksLikeAmount(String value) {
        return value != null && AMOUNT.matcher(value).find();
    }
}

Real Parsing Failures We Encountered

One common failure happens when a transaction description contains text that looks like metadata, a date, or part of a numeric column.
03/14   ATM Withdrawal
        Card Ending 0345
        100.00

A naive parser might split this into multiple rows. A layout-aware parser keeps the continuation line attached to the previous transaction.

Edge Cases

Validation After Detection

After selecting a candidate region, we apply lightweight validation:

previous_balance - debit + credit = next_balance

Detection Accuracy in Practice

During internal testing across multiple bank statement layouts, this scoring approach helped identify the correct transaction table more reliably than selecting the first visible table-like region on the page.

In our tests, the most useful signals were date density, amount-like values, consistent X positions, and balance validation. When these signals agreed, the parser was much less likely to confuse summaries, fee explanations, or footer sections with the actual transaction table.

This does not make detection perfect. Scanned documents, rotated pages, and highly irregular layouts still require fallback logic. But combining visual, semantic, and business validation signals produced a much more reliable detector than using headers or regex rules alone.

Connection to Product

At StatementSheet, transaction table detection is one of the core building blocks behind converting bank statements into structured Excel and CSV datasets.
Date,Description,Debit,Credit,Balance
2024-03-14,Amazon Marketplace,49.90,,950.10

Known Limitations

No transaction table detector is perfect. Highly irregular layouts, scanned documents, rotated pages, and low-quality OCR output may require fallback logic or bank-specific tuning.

Conclusion

Detecting transaction tables in bank statements is not just about finding a table. It is about finding the correct region that contains financial transaction rows.

Reliable detection combines visual layout analysis, date and amount recognition, column consistency, and business validation.

← Previous article Extracting Tables from Financial PDFs
Next article → Rebuilding Rows from Positioned PDF Text