All articles
Build Guides

Categorize Bank CSV Exports Into Budgets Automatically with Groq Function Calling

FDE Coach EditorialAugust 18, 20269 min read

What We’re Building

You have a raw bank CSV export full of cryptic descriptions like “SQ *FROTHY MONKEY NASHVILLE TN” or “ACH CREDIT PAYROLL”. You need to know, instantly, how much went to dining, income, or software subscriptions. We’re building a zero-cost Python script that does exactly that.

Feature List:

  • Ingests a typical bank CSV (date, description, amount columns)
  • Uses Groq Cloud’s free tier and Mixtral 8x7B tool-use to categorize every transaction with a structured function call
  • Outputs a clean, aggregated budget summary: total per category, transaction count, and average amount
  • Runs entirely locally except for the single API call—your financial data never touches a persistent remote database
  • Extensible to any CSV format with a simple column-mapping config

This is the kind of tool an FDE ships in an afternoon to unblock a finance team drowning in spreadsheets. Let’s build it.

Architecture Overview

The pipeline is linear, but the function-calling step is where the magic happens. Here’s the flow:

We’re not streaming transactions one-by-one. That would burn through rate limits. Instead, we batch 25–30 transactions per API call and use Groq’s tool-use mode to force the model to return a strict JSON array of categorized objects. No parsing free-text. No regex hell.

Prerequisites

Everything here is free-tier or open-source. No credit card required to start.

  1. Python 3.10+python.org/downloads
  2. Groq Cloud API Key – Sign up at console.groq.com. Free tier gives you generous requests per minute on Mixtral and Llama models.
  3. Pandaspip install pandas
  4. Groq Python SDKpip install groq
  5. A bank CSV export – Any bank’s export works. We’ll assume columns: Date, Description, Amount. You’ll map yours in the config.

Step 1: Project Setup and Dependencies

Create a project directory and a virtual environment:

mkdir budget-categorizer && cd budget-categorizer
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install pandas groq

Set your API key as an environment variable (never hardcode it):

export GROQ_API_KEY="gsk_your_key_here"

Create a single file: categorize.py. All logic lives here.

Step 2: Parsing the Bank CSV

Bank CSVs are messy. Headers vary, date formats vary, and amounts might be in a single “Amount” column or split into “Debit”/“Credit”. We’ll write a flexible parser that normalizes everything into a standard schema.

import pandas as pd
from datetime import datetime

def parse_bank_csv(filepath: str, date_col: str, desc_col: str, amount_col: str, debit_col: str = None, credit_col: str = None, date_format: str = None) -> pd.DataFrame:
    df = pd.read_csv(filepath)
    
    # Normalize date
    if date_format:
        df['date'] = pd.to_datetime(df[date_col], format=date_format)
    else:
        df['date'] = pd.to_datetime(df[date_col], infer_datetime_format=True)
    
    # Normalize description
    df['description'] = df[desc_col].astype(str).str.strip()
    
    # Normalize amount: single column or debit/credit split
    if debit_col and credit_col:
        df['amount'] = df[credit_col].fillna(0) - df[debit_col].fillna(0)
    else:
        df['amount'] = pd.to_numeric(df[amount_col], errors='coerce')
    
    # Drop rows with missing critical fields
    df = df.dropna(subset=['date', 'description', 'amount'])
    return df[['date', 'description', 'amount']]

Real-world example: Chase exports use “Posting Date”, “Description”, and “Amount” (negative for debits). A single config mapping handles it.

Step 3: Defining the Groq Tool Schema

This is the core of the build. We’re not asking the model to chat. We’re giving it a function signature and forcing it to call that function with structured arguments. Groq’s tool-use mode guarantees the response matches our schema.

We define a single tool: categorize_transactions. It takes an array of transaction objects and returns an array of categorized transaction objects.

CATEGORIZE_TOOL = {
    "type": "function",
    "function": {
        "name": "categorize_transactions",
        "description": "Categorize a batch of bank transactions into predefined budget categories.",
        "parameters": {
            "type": "object",
            "properties": {
                "transactions": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "index": {"type": "integer", "description": "Original index of the transaction in the batch"},
                            "category": {
                                "type": "string",
                                "enum": [
                                    "Income",
                                    "Housing",
                                    "Utilities",
                                    "Groceries",
                                    "Dining Out",
                                    "Transportation",
                                    "Healthcare",
                                    "Entertainment",
                                    "Shopping",
                                    "Software & Subscriptions",
                                    "Travel",
                                    "Transfer",
                                    "Uncategorized"
                                ]
                            },
                            "confidence": {"type": "number", "minimum": 0.0, "maximum": 1.0},
                            "note": {"type": "string", "description": "Brief reasoning for the category choice"}
                        },
                        "required": ["index", "category", "confidence"]
                    }
                }
            },
            "required": ["transactions"]
        }
    }
}

Why an enum? It constrains the model to your budget categories. No “Food & Drink” vs “Restaurants” inconsistency. You define the taxonomy once.

Step 4: Calling Groq with Function Calling

We batch transactions into chunks of 25 to stay well within token limits and rate limits. Each chunk is sent as a tool-use request. The model must respond with a function call containing the categorized array.

from groq import Groq
import json
import os

def categorize_batch(descriptions: list[str], amounts: list[float], indices: list[int]) -> list[dict]:
    client = Groq(api_key=os.environ["GROQ_API_KEY"])
    
    # Build the user message: a compact JSON of the batch
    batch_input = []
    for i, desc, amt in zip(indices, descriptions, amounts):
        batch_input.append({"index": i, "description": desc, "amount": amt})
    
    messages = [
        {
            "role": "system",
            "content": "You are a financial transaction categorizer. You MUST call the categorize_transactions function with every transaction in the batch. Use the description and amount to infer the correct category. Be precise: payroll ACH credits are Income, Uber rides are Transportation, Netflix is Software & Subscriptions. Do not skip any transaction."
        },
        {
            "role": "user",
            "content": json.dumps(batch_input)
        }
    ]
    
    response = client.chat.completions.create(
        model="mixtral-8x7b-32768",
        messages=messages,
        tools=[CATEGORIZE_TOOL],
        tool_choice={"type": "function", "function": {"name": "categorize_transactions"}},
        temperature=0.1  # Low temp for consistency
    )
    
    # Extract the function call arguments
    tool_call = response.choices[0].message.tool_calls[0]
    categorized = json.loads(tool_call.function.arguments)["transactions"]
    return categorized

Key details:

  • tool_choice is forced to our function. The model cannot respond with plain text.
  • temperature=0.1 keeps categorizations deterministic. You want the same result every run.
  • We pass the original index so we can join results back to the DataFrame later.

Step 5: Aggregating the Budget Summary

Once all batches are categorized, we merge results back with the original data and compute the summary.

def aggregate_budget(df: pd.DataFrame, categorizations: list[dict]) -> pd.DataFrame:
    # Create a lookup from index to category
    cat_map = {c['index']: c for c in categorizations}
    
    # Apply categories to the original dataframe
    df['category'] = df.index.map(lambda i: cat_map.get(i, {}).get('category', 'Uncategorized'))
    df['confidence'] = df.index.map(lambda i: cat_map.get(i, {}).get('confidence', 0.0))
    
    # Aggregate
    summary = df.groupby('category').agg(
        total_amount=('amount', 'sum'),
        transaction_count=('amount', 'count'),
        avg_amount=('amount', 'mean'),
        avg_confidence=('confidence', 'mean')
    ).round(2)
    
    # Sort by absolute total (largest impact first)
    summary['abs_total'] = summary['total_amount'].abs()
    summary = summary.sort_values('abs_total', ascending=False).drop(columns=['abs_total'])
    
    return summary

Step 6: Running the Full Pipeline

Now we wire everything together with a main function that handles batching and progress logging.

def main(filepath: str, date_col: str, desc_col: str, amount_col: str, debit_col: str = None, credit_col: str = None, batch_size: int = 25):
    # Parse
    df = parse_bank_csv(filepath, date_col, desc_col, amount_col, debit_col, credit_col)
    print(f"Parsed {len(df)} transactions.")
    
    # Batch and categorize
    all_categorizations = []
    for start in range(0, len(df), batch_size):
        end = min(start + batch_size, len(df))
        batch_df = df.iloc[start:end]
        indices = batch_df.index.tolist()
        descriptions = batch_df['description'].tolist()
        amounts = batch_df['amount'].tolist()
        
        print(f"Categorizing batch {start//batch_size + 1} ({start}-{end-1})...")
        batch_results = categorize_batch(descriptions, amounts, indices)
        all_categorizations.extend(batch_results)
    
    # Aggregate
    summary = aggregate_budget(df, all_categorizations)
    
    print("\n=== BUDGET SUMMARY ===")
    print(summary.to_string())
    
    # Optionally save to CSV
    summary.to_csv("budget_summary.csv")
    df.to_csv("categorized_transactions.csv", index=False)
    print("\nSaved budget_summary.csv and categorized_transactions.csv")

if __name__ == "__main__":
    # Example: Chase CSV with columns "Posting Date", "Description", "Amount"
    main(
        filepath="chase_export.csv",
        date_col="Posting Date",
        desc_col="Description",
        amount_col="Amount"
    )

Run it:

python categorize.py

You’ll see progress per batch and then a clean terminal table:

=== BUDGET SUMMARY ===
                     total_amount  transaction_count  avg_amount  avg_confidence
category
Income                   8500.00                  2     4250.00            0.98
Housing                -2200.00                  1    -2200.00            0.99
Dining Out              -342.18                 12      -28.52            0.91
Groceries               -567.40                  4     -141.85            0.94
Software & Subscriptions -45.99                  3      -15.33            0.97

Extensions and Next Steps

This script is a foundation. Here’s where you take it next:

  • Add a config file: A config.yaml that maps column names, batch size, and custom categories. No more hardcoding.
  • Slack/Email digest: Run it weekly via cron, pipe the summary DataFrame into a formatted Slack message or email.
  • Confidence threshold flagging: Append a needs_review column for anything under 0.8 confidence. A human can spot-check those 5% of transactions.
  • Historical trend analysis: Store each run’s summary in a SQLite database. Query month-over-month changes in discretionary spending.
  • Multi-account support: Point it at a directory of CSVs from different banks. Unify into one household budget.

If you’re thinking about building more AI-powered internal tools like this, our breakdown of what an FDE ships in a week shows the pattern: small, composable scripts that chain LLMs into existing workflows.

Common Pitfalls

  1. Token limit exceeded. Mixtral’s context is 32k tokens. A batch of 25 transactions with long descriptions is safe. If your bank has verbose memos, drop to 15 per batch.
  2. Rate limiting. Groq free tier is generous but not infinite. Add a time.sleep(1) between batches if you hit 429 errors.
  3. Inconsistent categories. The enum prevents most drift, but model updates can shift behavior. Pin your model version if this becomes a production tool.
  4. Negative amount confusion. Some banks use negative for debits, positive for credits. Our parser normalizes this, but verify your export’s convention. A quick df.head() after parsing catches this.
  5. CSV encoding. Banks love Latin-1 or Windows-1252 encodings. If pd.read_csv chokes, add encoding='latin1' or encoding='cp1252'.

FAQ

Q: Does Groq store my transaction data? A: No. Groq’s API does not persist prompts or responses. Your data is in-flight only. For extra paranoia, you can strip account numbers from descriptions before sending.

Q: Can I use this with Llama 3 instead of Mixtral? A: Yes. Switch the model string to llama3-70b-8192 or llama3-8b-8192. The 8B model is faster but slightly less accurate on edge-case descriptions. Test with your data.

Q: What if a transaction legitimately fits two categories? A: The current schema picks one. For split transactions (e.g., a Costco run with groceries and electronics), you’d extend the schema to allow an array of {category, amount} per transaction. That’s a v2 feature.

Q: How do I handle transfers between my own accounts? A: The “Transfer” category in our enum catches those. The model is surprisingly good at identifying “Transfer to Savings” or “Online Banking Payment” patterns.

Q: What’s the total cost? A: Zero. Groq’s free tier covers thousands of transactions per day. Mixtral on Groq is also extremely fast—a 25-transaction batch categorizes in under 2 seconds.

If you’re looking to chain this into a larger automation—like a competitor monitor that alerts on meaningful changes or a resume tailoring agent—the pattern is identical: free LLM + structured output + a thin Python wrapper. Ship it.

#personal-finance#csv-parsing#groq#function-calling

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now