Logo
Afbeelding

Checking for Split Transactions

Written by The Audit Analytics | 5 minutes

Sometimes, payments are intentionally split to stay below an approval threshold. This could indicate unauthorized transactions or attempts to bypass controls. In this analysis, we examine bank transactions from a CAMT.053 file to detect suspicious splits.

Duplicate Bank Accounts

We use Python, and while some basic knowledge is required, we keep it understandable. For this analysis, you need:

  • A CAMT.053 file containing transaction data.
  • The Python packages pandas and xml.etree.ElementTree

If you haven’t installed Pandas yet, you can do so with:

pip install pandas

For a sample file to try out this analysis yourself, you can download this test CAMT file

Step 1: Reading the CAMT.053 File

A CAMT.053 file is an XML format containing bank transactions. We use pandas and xml.etree.ElementTree to process the file.

import xml.etree.ElementTree as ET
import pandas as pd

# Function to parse a CAMT.053 XML file
def parse_camt053(file_path):
    tree = ET.parse(file_path)  # Parse the XML file
    root = tree.getroot()  # Get the root element
    ns = {'ns': 'urn:iso:std:iso:20022:tech:xsd:camt.053.001.02'}  # Define XML namespace

    transactions = []
    
    for entry in root.findall('.//ns:Ntry', ns):
        try:
            amount = float(entry.find('ns:Amt', ns).text) if entry.find('ns:Amt', ns) is not None else 0.0
        except ValueError:
            amount = 0.0  # Fallback for conversion errors
        
        currency = entry.find('ns:Amt', ns).attrib.get('Ccy', 'N/A') if entry.find('ns:Amt', ns) is not None else 'N/A'
        date = entry.find('.//ns:BookgDt/ns:Dt', ns).text if entry.find('.//ns:BookgDt/ns:Dt', ns) is not None else '-'
        debtor = entry.find('.//ns:Dbtr/ns:Nm', ns).text if entry.find('.//ns:Dbtr/ns:Nm', ns) is not None else '-'
        debtor_iban = entry.find('.//ns:DbtrAcct/ns:Id/ns:IBAN', ns).text if entry.find('.//ns:DbtrAcct/ns:Id/ns:IBAN', ns) is not None else '-'
        creditor = entry.find('.//ns:Cdtr/ns:Nm', ns).text if entry.find('.//ns:Cdtr/ns:Nm', ns) is not None else '-'
        creditor_iban_element = entry.find('.//ns:CdtrAcct/ns:Id/ns:IBAN', ns)
        creditor_iban = creditor_iban_element.text if creditor_iban_element is not None else '-'

        # Store extracted transaction details in a list
        transactions.append({
            "Date": date,
            "Debtor": debtor,
            "Debtor IBAN": debtor_iban,
            "Creditor": creditor,
            "Creditor IBAN": creditor_iban,
            "Amount": amount,
            "Currency": currency
        })
    
    return pd.DataFrame(transactions)  # Convert to a Pandas DataFrame

# Load and parse the CAMT.053 file
file_path = "camt053_dummy.xml"
df = parse_camt053(file_path)
print(df.head())  # Display the first few rows

Step 2: Grouping Payments by Supplier and Date

We want to determine whether multiple payments were made to the same supplier on the same day.

# You can replace Beneficiary with IBAN
df_grouped = df.groupby(['Date', 'Creditor']).agg(
    Transaction_Count=('Amount', 'count'),  # Count the number of rows per group
    Total_Amount=('Amount', 'sum')  # Sum the amounts
).reset_index()

print(df_grouped)

This shows how much was transferred per day per supplier.

Step 3: Detecting Split Payments

Now, we look for patterns where multiple payments add up to a large invoice or where amounts remain just below an approval threshold.

Let's assume the approval threshold is €1,000.

approval_limit = 1000

# Check if multiple payments approach the threshold
df_splits = df_grouped[
    (df_grouped['Total_Amount'] >= (approval_limit * 0.9)) & 
    (df_grouped['Transaction_Count'] > 1)
]
print(df_splits)

This filters transactions where the total amount per supplier and date is at least 90% of the approval limit.

Step 4: Marking Suspicious Splits

Now, we specifically check if individual payments within these group amounts remain just below the threshold.

suspicious_transactions = df[(df['Amount'] >= (approval_limit * 0.9)) & (df['Amount'] < approval_limit)]
print(suspicious_transactions)

This provides a list of suspicious transactions that may have been intentionally split.

Conclusion

With these steps, we can easily detect suspicious payments. Want to experiment further? Try different threshold values, analyze by IBAN, or visualize results with matplotlib.