-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdata_loader.py
More file actions
165 lines (135 loc) · 7.01 KB
/
Copy pathdata_loader.py
File metadata and controls
165 lines (135 loc) · 7.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""
data_loader.py - FinSight Personal Finance Analytics Agent
===========================================================
Responsible for loading the raw transaction CSV, performing
all cleaning and type-casting steps, and returning a tidy
DataFrame that every other module can rely on.
Dataset: Credit Card Transactions (Kaggle)
Shape : ~1.3 M rows × 24 columns
Period : January 2019 – June 2020
"""
import os
import pandas as pd
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# Default path to the raw data file, relative to the project root.
DATA_PATH = os.path.join(os.path.dirname(__file__), "data", "credit_card_transactions.csv")
# Columns we actually need for financial analysis.
# Dropping PII (street, dob), geo coordinates, and internal IDs.
COLUMNS_TO_KEEP = [
"trans_date_trans_time", # full transaction timestamp
"merchant", # merchant name (will be cleaned)
"category", # raw spending category from the bank
"amt", # transaction amount in USD
"city", # cardholder city
"state", # cardholder state
"is_fraud", # fraud flag (0 = legitimate, 1 = fraud)
]
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def load_data(filepath: str = DATA_PATH, sample_size: int | None = None) -> pd.DataFrame:
"""
Load, clean, and return the transaction dataset as a tidy DataFrame.
Parameters
----------
filepath : str
Path to the raw CSV file. Defaults to ``data/credit_card_transactions.csv``.
sample_size : int or None
If provided, randomly sample this many rows (reproducible via seed=42).
Useful for fast prototyping; set to None to load the full ~1.3 M rows.
Returns
-------
pd.DataFrame
A cleaned DataFrame with standardized column names, parsed dates,
and derived time columns ready for analysis and visualization.
Raises
------
FileNotFoundError
If the CSV file does not exist at the given path.
"""
_validate_filepath(filepath)
# ------------------------------------------------------------------
# 1. Read raw CSV
# ------------------------------------------------------------------
print(f"[data_loader] Loading data from: {filepath}")
df = pd.read_csv(filepath, usecols=COLUMNS_TO_KEEP, low_memory=False)
print(f"[data_loader] Raw shape: {df.shape[0]:,} rows × {df.shape[1]} columns")
# ------------------------------------------------------------------
# 2. Optional random sample (for fast dev iteration)
# ------------------------------------------------------------------
if sample_size is not None:
df = df.sample(n=min(sample_size, len(df)), random_state=42).reset_index(drop=True)
print(f"[data_loader] Sampled down to: {len(df):,} rows")
# ------------------------------------------------------------------
# 3. Parse & enrich the timestamp column
# ------------------------------------------------------------------
df["trans_date_trans_time"] = pd.to_datetime(df["trans_date_trans_time"])
# Extract granular time fields used by the visualizations layer.
df["date"] = df["trans_date_trans_time"].dt.date # calendar date
df["year"] = df["trans_date_trans_time"].dt.year # 4-digit year
df["month"] = df["trans_date_trans_time"].dt.month # 1–12
df["month_name"] = df["trans_date_trans_time"].dt.strftime("%b %Y") # e.g. "Jan 2019"
df["day_of_week"]= df["trans_date_trans_time"].dt.day_name() # e.g. "Monday"
df["hour"] = df["trans_date_trans_time"].dt.hour # 0–23
# ------------------------------------------------------------------
# 4. Clean merchant names
# The raw dataset prefixes every merchant with "fraud_" as an
# artifact of the fraud-detection labelling process. Strip it.
# ------------------------------------------------------------------
df["merchant"] = df["merchant"].str.removeprefix("fraud_").str.strip()
# ------------------------------------------------------------------
# 5. Standardize column names & data types
# ------------------------------------------------------------------
df.rename(columns={
"trans_date_trans_time": "datetime",
"amt": "amount",
"is_fraud": "is_fraud", # kept as int (0/1) intentionally
}, inplace=True)
# Ensure amount is float (should already be, but be explicit)
df["amount"] = df["amount"].astype(float)
# city / state as clean strings
df["city"] = df["city"].str.strip().str.title()
df["state"] = df["state"].str.strip().str.upper()
# ------------------------------------------------------------------
# 6. Drop any remaining null rows (only merch_zipcode had nulls;
# we dropped that column, so this is a safety net)
# ------------------------------------------------------------------
before = len(df)
df.dropna(subset=["datetime", "amount", "merchant", "category"], inplace=True)
after = len(df)
if before != after:
print(f"[data_loader] Dropped {before - after:,} rows with nulls in key columns.")
# ------------------------------------------------------------------
# 7. Reset index so downstream code can rely on a clean 0-based index
# ------------------------------------------------------------------
df.reset_index(drop=True, inplace=True)
print(f"[data_loader] Clean shape : {df.shape[0]:,} rows × {df.shape[1]} columns")
print(f"[data_loader] Date range : {df['datetime'].min().date()} → {df['datetime'].max().date()}")
print(f"[data_loader] Amount range: ${df['amount'].min():.2f} – ${df['amount'].max():,.2f}")
print(f"[data_loader] Done.\n")
return df
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _validate_filepath(filepath: str) -> None:
"""Raise FileNotFoundError with a helpful message if the CSV is missing."""
if not os.path.exists(filepath):
raise FileNotFoundError(
f"Dataset not found at '{filepath}'.\n"
f"Make sure 'credit_card_transactions.csv' is inside the data/ folder."
)
# ---------------------------------------------------------------------------
# Quick self-test: run this file directly to verify the loader works
# ---------------------------------------------------------------------------
if __name__ == "__main__":
df = load_data(sample_size=10_000) # use 10 K rows for a fast smoke test
print("=== Column overview ===")
print(df.dtypes)
print()
print("=== First 5 rows ===")
print(df.head().to_string())
print()
print("=== Unique raw categories ===")
print(sorted(df["category"].unique()))