CSCI-2910 Resource
Class Session II: Financial Fraud Analysis In Python

Back to Resources Code Examples

Type: ZIP archive

This guide walks you back through everything we built in class, in order, so you can rebuild it yourself, review the concepts, or extend it further. By the end you'll have two files:

  • fraud_summary.py — a small class that stores summary statistics
  • main.py — the analysis script that loads the data, filters it, groups it, and charts it

Make sure synthetic_fraud_dataset.csv is in the same folder as your scripts before you start.


Step 1: DataFrames and Series (Warm-Up)

Before touching the real dataset, we practiced with a tiny hand-made one to remember two things: how a DataFrame is built, and what you get back when you pull out a single column.

import pandas as pd

names = ["Alice", "Joe", "Bob"]
ages = [45, 34, 57]
data = {'names': names, 'ages': ages}
df = pd.DataFrame(data)

print(df)
print(type(df["names"]))

What to remember:
- A DataFrame can be built from a dictionary where each key becomes a column name.
- Pulling out one column with df["names"] doesn't give you a plain Python list — it gives you a Series, pandas' 1-dimensional labeled array. A DataFrame is really a collection of Series objects sharing an index.

This step doesn't appear in the final project — it was just a warm-up. You can delete it once you're comfortable with the concept.


Step 2: Loading Real Data and Filtering

Now we switch to the real dataset and start main.py.

import pandas as pd

df = pd.read_csv("synthetic_fraud_dataset.csv")

Filtering with one condition

To pull out just the fraudulent transactions:

fraud_df = df[df["is_fraud"] == 1]

Notice this is really two steps happening at once:
1. df["is_fraud"] == 1 produces a Series of True/False values — one for every row.
2. df[ ... ] uses that True/False Series as a mask, keeping only the rows marked True.

Try printing just the mask on its own to see it:

print(df["is_fraud"] == 1)

The same pattern works for any column:

us_transactions = df[df["country"] == "US"]

Combining conditions

To find fraudulent transactions in the US specifically, combine both masks with &:

fraud_us_transactions = df[(df["country"] == "US") & (df["is_fraud"] == 1)]
print(fraud_us_transactions)

Important gotcha: the parentheses around each condition are required. In plain Python you'd write a and b, but pandas needs (a) & (b) — leaving out the parentheses will throw an error. Also note it's & and |, not and/or, when comparing whole columns.

Try it yourself: modify the filter to find fraud transactions in the US or Nigeria ("NG"). Hint: you'll need | and check the country values are compared correctly for each condition.


Step 3: Grouping and Aggregating

Filtering finds specific rows; groupby answers "how does this number change across categories?"

print(df.groupby("country")["is_fraud"].mean())

How to read this line, left to right:
1. df.groupby("country") splits the whole dataset into one bucket per country.
2. ["is_fraud"] says: within each bucket, look at the is_fraud column.
3. .mean() averages that column within each bucket and combines the results into one summary.

Why does averaging a column of 0s and 1s give you a fraud rate? Because the mean of a binary column is literally the proportion of 1s. If a country has 100 transactions and 5 are fraud, the average of that column is 0.05 — 5%. This trick (mean of a 0/1 column = a rate) comes up constantly in data analysis, so it's worth internalizing now.

Try it yourself: group by merchant_category instead of country and see which category has the highest fraud rate.


Step 4: Packaging Results in a Class (fraud_summary.py)

Instead of juggling separate variables for each summary statistic, we bundled them into a class. Create a new file called fraud_summary.py:

class FraudSummary:
    def __init__(self, avg_amount, fraud_rate, avg_device_risk):
        self.avg_amount = avg_amount
        self.fraud_rate = fraud_rate
        self.avg_device_risk = avg_device_risk

    def __str__(self):
        return (
            f"Average Transaction amount : {self.avg_amount:.2f}\n"
            f"Average Fraud Rate: {self.fraud_rate:.2%}\n"
            f"Average device risk score: {self.avg_device_risk:.2f}\n"
        )

What each piece is doing:
- __init__ is the constructor — it runs when you create a FraudSummary(...) and stores the three values you pass in as self. attributes.
- __str__ controls what shows up when you print() an object. Without it, print(summary) would show something unhelpful like <__main__.FraudSummary object at 0x7f...>. With it, you control exactly what gets displayed.
- The format specifiers matter: :.2f rounds a plain number to 2 decimal places, while :.2% treats the number as a proportion, multiplies by 100, and adds a % sign — that's why fraud_rate (which is between 0 and 1) uses % but the dollar amounts use f.


Step 5: Bringing It Together — Analysis + Visualization (main.py)

Now main.py uses everything from Steps 2–4, plus charts.

5a. Compute summary stats and use the class

import pandas as pd
from fraud_summary import FraudSummary
import matplotlib.pyplot as plt

df = pd.read_csv("synthetic_fraud_dataset.csv")

avg_amount = df["amount"].mean()
fraud_rate = df["is_fraud"].mean()
avg_device_risk = df["device_risk_score"].mean()

summary = FraudSummary(avg_amount, fraud_rate, avg_device_risk)
print(summary)

This is the exact same .mean() idea from Step 3 — we're just feeding the results into the class from Step 4 instead of printing them raw.

5b. Group transaction amounts by hour

amount_by_hour = df.groupby("hour")["amount"].sum()
print(amount_by_hour)

Same groupby pattern as before, but note we used .sum() this time instead of .mean() — we want the total dollar amount transacted per hour, not an average.

5c. A basic line chart

plt.plot(amount_by_hour.index, amount_by_hour.values)
plt.xlabel("Hour of Day")
plt.ylabel("Total Transactions")
plt.title("Transactions occurred per hour")
plt.show()

This is matplotlib's "quick and easy" style — fine for one chart, but it gets awkward once you want multiple charts side by side.

5d. The same chart, the object-oriented way

fig, ax = plt.subplots()
ax.plot(amount_by_hour.index, amount_by_hour.values)
ax.set_xlabel("Hour of Day")
ax.set_ylabel("Total Transactions")
ax.set_title("Transactions occurred per hour")
plt.show()

Same exact chart — the only thing that changed is how we talk to matplotlib. plt.subplots() gives you back a fig (the whole canvas) and an ax (a single set of axes to draw on). This pattern scales to multiple charts, which is exactly what's next.

5e. Two charts side by side

count_by_hour = df.groupby("hour")["amount"].count()

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

# First chart: total dollar amount by hour
ax1.plot(amount_by_hour.index, amount_by_hour.values, color="blue")
ax1.set_xlabel("Hour of Day")
ax1.set_ylabel("Total Transactions")
ax1.set_title("Transactions occurred per hour")

# Second chart: number of transactions by hour
ax2.bar(count_by_hour.index, count_by_hour.values, color="green")
ax2.set_title("Transactions count by hour")
ax2.set_ylabel("Number of transactions")

plt.show()

Key distinction to notice: amount_by_hour is a sum of dollars, while count_by_hour is a count of transactions. They're grouped by the same "hour" column, but they answer different questions — a lot of money moving in one hour doesn't necessarily mean a lot of transactions happened, and vice versa. Before running this, guess: do you think the two charts will have the same shape?

plt.subplots(1, 2, figsize=(12, 5)) means "1 row, 2 columns of charts, in a canvas 12 inches wide by 5 inches tall." That's why you unpack it into (ax1, ax2) instead of a single ax.


Checklist: Does Your Project Work End-to-End?

  • [ ] fraud_summary.py defines the FraudSummary class with __init__ and __str__.
  • [ ] main.py loads the CSV successfully (check the file is in the same folder).
  • [ ] Filtering with one condition and with & (two conditions) both work.
  • [ ] groupby(...).mean() produces a fraud rate per country.
  • [ ] print(summary) shows a nicely formatted block, not a <... object at 0x...> line.
  • [ ] The single line chart displays correctly.
  • [ ] The two-chart side-by-side version displays correctly, with different colors and titles.

Ideas to Extend This on Your Own

  • Group by merchant_category or transaction_type instead of country or hour.
  • Add a median_amount field to FraudSummary alongside the average.
  • Try .min() and .max() on groupby results to see the extremes.
  • Filter for high-risk transactions (device_risk_score > 0.8) and see how many are actually fraud — is a high risk score a good predictor?