Skip to content

Commit be4564a

Browse files
authored
Create LoanGuardian.py
1 parent 1fa3602 commit be4564a

1 file changed

Lines changed: 282 additions & 0 deletions

File tree

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
"""
2+
LoanGuardian v2.1 - Enterprise Edition
3+
AI-Powered Loan Approval Prediction System
4+
Handles large CSV datasets efficiently with auto feature detection, normalization, and fixed categorical encoding
5+
"""
6+
7+
import os, sys, threading
8+
import tkinter as tk
9+
from tkinter import filedialog, messagebox, ttk
10+
import pandas as pd
11+
import numpy as np
12+
13+
import ttkbootstrap as tb
14+
from ttkbootstrap.constants import *
15+
16+
try:
17+
from tkinterdnd2 import TkinterDnD, DND_FILES
18+
DND_ENABLED = True
19+
except ImportError:
20+
DND_ENABLED = False
21+
print("Drag & Drop requires tkinterdnd2: pip install tkinterdnd2")
22+
23+
from sklearn.ensemble import RandomForestClassifier
24+
from sklearn.preprocessing import LabelEncoder, MinMaxScaler
25+
26+
# ---------------------- UTIL ----------------------
27+
def resource_path(file_name):
28+
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
29+
return os.path.join(base_path, file_name)
30+
31+
# ---------------------- PREDICTION WORKER ----------------------
32+
class LoanWorker:
33+
def __init__(self, files, callbacks, chunk_size=500):
34+
self.files = files
35+
self.callbacks = callbacks
36+
self._running = True
37+
self.chunk_size = chunk_size
38+
self.encoders = {} # Store encoders for categorical columns
39+
self.model, self.feature_info = self._train_model()
40+
41+
def stop(self):
42+
self._running = False
43+
44+
def _train_model(self):
45+
# Dummy training dataset
46+
data = pd.DataFrame({
47+
"Gender": ["Male","Female","Female","Male","Male","Female"],
48+
"Married": ["Yes","No","Yes","Yes","No","No"],
49+
"Education": ["Graduate","Graduate","Not Graduate","Graduate","Not Graduate","Graduate"],
50+
"ApplicantIncome": [5000,3000,4000,6000,3500,4200],
51+
"LoanAmount": [200,100,150,250,120,140],
52+
"Loan_Approved": ["Yes","No","Yes","Yes","No","Yes"]
53+
})
54+
X = data.drop("Loan_Approved", axis=1)
55+
y = data["Loan_Approved"]
56+
57+
feature_info = {"categorical": [], "numeric": []}
58+
for col in X.columns:
59+
if X[col].dtype == object:
60+
feature_info["categorical"].append(col)
61+
le = LabelEncoder()
62+
X[col] = le.fit_transform(X[col])
63+
self.encoders[col] = le
64+
else:
65+
feature_info["numeric"].append(col)
66+
67+
scaler = MinMaxScaler()
68+
if feature_info["numeric"]:
69+
X[feature_info["numeric"]] = scaler.fit_transform(X[feature_info["numeric"]])
70+
71+
le_y = LabelEncoder()
72+
y_encoded = le_y.fit_transform(y)
73+
74+
model = RandomForestClassifier(n_estimators=100, random_state=42)
75+
model.fit(X, y_encoded)
76+
77+
return model, {
78+
"categorical": feature_info["categorical"],
79+
"numeric": feature_info["numeric"],
80+
"le_y": le_y,
81+
"scaler": scaler
82+
}
83+
84+
def _preprocess_chunk(self, df):
85+
fi = self.feature_info
86+
# Encode categorical using stored encoders
87+
for col in fi["categorical"]:
88+
if col in df.columns:
89+
le = self.encoders[col]
90+
df[col] = df[col].astype(str)
91+
# Map unseen categories to -1
92+
df[col] = df[col].apply(lambda x: le.transform([x])[0] if x in le.classes_ else -1)
93+
# Normalize numeric columns
94+
if fi["numeric"]:
95+
for col in fi["numeric"]:
96+
if col in df.columns:
97+
df[col] = df[col] / (df[col].max() + 1e-5)
98+
return df
99+
100+
def run(self):
101+
total_files = len(self.files)
102+
for i, path in enumerate(self.files):
103+
if not self._running:
104+
break
105+
try:
106+
for chunk in pd.read_csv(path, chunksize=self.chunk_size):
107+
if not self._running:
108+
break
109+
original_chunk = chunk.copy()
110+
chunk_processed = self._preprocess_chunk(chunk)
111+
missing_cols = set(self.feature_info["categorical"] + self.feature_info["numeric"]) - set(chunk_processed.columns)
112+
for col in missing_cols:
113+
chunk_processed[col] = 0
114+
preds = self.model.predict(chunk_processed[self.feature_info["categorical"] + self.feature_info["numeric"]])
115+
chunk["Approval_Prediction"] = self.feature_info["le_y"].inverse_transform(preds)
116+
if "found" in self.callbacks:
117+
self.callbacks["found"](path, original_chunk, chunk)
118+
except Exception as e:
119+
if "error" in self.callbacks:
120+
self.callbacks["error"](path, str(e))
121+
if "progress" in self.callbacks:
122+
self.callbacks["progress"](int((i + 1)/total_files * 100))
123+
if "finished" in self.callbacks:
124+
self.callbacks["finished"]()
125+
126+
# ---------------------- MAIN APP ----------------------
127+
class LoanGuardianApp:
128+
APP_NAME = "LoanGuardian"
129+
APP_VERSION = "2.1"
130+
SUPPORTED_EXT = (".csv",)
131+
132+
def __init__(self):
133+
if DND_ENABLED:
134+
self.root = TkinterDnD.Tk()
135+
else:
136+
self.root = tb.Window(themename="darkly")
137+
self.root.title(f"{self.APP_NAME} v{self.APP_VERSION}")
138+
self.root.minsize(1000, 600)
139+
140+
self.worker_obj = None
141+
self.progress_value = 0
142+
self.target_progress = 0
143+
self.file_set = set()
144+
self._build_ui()
145+
146+
# ---------------------- UI ----------------------
147+
def _build_ui(self):
148+
main = tb.Frame(self.root, padding=10)
149+
main.pack(fill=tk.BOTH, expand=True)
150+
tb.Label(main, text=f"🧠 {self.APP_NAME} - Enterprise Loan Predictor",
151+
font=("Segoe UI", 20, "bold")).pack(pady=(0,10))
152+
tb.Label(main, text="Auto-detect features, normalize numeric columns, process thousands of files",
153+
font=("Segoe UI", 10, "italic"), foreground="#9ca3af").pack(pady=(0,10))
154+
155+
row1 = tb.Frame(main)
156+
row1.pack(fill=tk.X, pady=(0,6))
157+
self.path_input = tb.Entry(row1, width=80)
158+
self.path_input.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,6))
159+
self.path_input.insert(0, "Drag & drop CSV files here…")
160+
browse_btn = tb.Button(row1, text="📂 Browse", bootstyle=INFO, command=self.browse)
161+
browse_btn.pack(side=tk.LEFT, padx=3)
162+
self.start_btn = tb.Button(row1, text="🚀 Start Prediction", bootstyle=SUCCESS, command=self.start)
163+
self.start_btn.pack(side=tk.LEFT, padx=3)
164+
self.cancel_btn = tb.Button(row1, text="⏹ Cancel", bootstyle=DANGER, command=self.cancel)
165+
self.cancel_btn.pack(side=tk.LEFT, padx=3)
166+
self.cancel_btn.config(state=tk.DISABLED)
167+
export_btn = tb.Button(row1, text="💾 Export Results", bootstyle=PRIMARY, command=self.export_results)
168+
export_btn.pack(side=tk.LEFT, padx=3)
169+
170+
self.progress = tb.Progressbar(main, bootstyle="success-striped", maximum=100)
171+
self.progress.pack(fill=tk.X, pady=(0,6))
172+
173+
columns = ("selected","filename","status")
174+
self.tree = ttk.Treeview(main, columns=columns, show="headings", selectmode="extended", height=20)
175+
self.tree.heading("selected", text="✅")
176+
self.tree.heading("filename", text="Filename", anchor=tk.W)
177+
self.tree.heading("status", text="Status", anchor=tk.W)
178+
self.tree.column("selected", width=50, anchor=tk.CENTER)
179+
self.tree.column("filename", width=600)
180+
self.tree.column("status", width=150)
181+
self.tree.pack(fill=tk.BOTH, expand=True, pady=(0,6))
182+
self.root.after(15, self.animate_progress)
183+
184+
if DND_ENABLED:
185+
self.tree.drop_target_register(DND_FILES)
186+
self.tree.dnd_bind("<<Drop>>", self.on_drop)
187+
188+
# ---------------------- File Handling ----------------------
189+
def browse(self):
190+
files = filedialog.askopenfilenames(filetypes=[("CSV Files","*.csv")])
191+
if files:
192+
self._queue_files(files)
193+
194+
def on_drop(self, event):
195+
paths = self.root.tk.splitlist(event.data)
196+
self._queue_files(paths)
197+
198+
def _queue_files(self, paths):
199+
for path in paths:
200+
ext = os.path.splitext(path)[1].lower()
201+
if ext in self.SUPPORTED_EXT and path not in self.file_set:
202+
self.file_set.add(path)
203+
self.tree.insert("", tk.END, values=("☑️", path, "Queued"))
204+
205+
# ---------------------- Actions ----------------------
206+
def start(self):
207+
selected_files = [self.tree.item(i)['values'][1] for i in self.tree.get_children()
208+
if self.tree.item(i)['values'][0]=="☑️"]
209+
if not selected_files:
210+
messagebox.showwarning("No Selection", "Select CSV files before starting prediction.")
211+
return
212+
self.progress["value"] = 0
213+
self.target_progress = 0
214+
self.start_btn.config(state=tk.DISABLED)
215+
self.cancel_btn.config(state=tk.NORMAL)
216+
threading.Thread(target=self._run_worker, args=(selected_files,), daemon=True).start()
217+
218+
def _run_worker(self, files):
219+
self.worker_obj = LoanWorker(
220+
files,
221+
callbacks={
222+
"found": self.add_result,
223+
"error": self.add_error,
224+
"progress": self.set_target,
225+
"finished": self.finish
226+
}
227+
)
228+
self.worker_obj.run()
229+
230+
def add_result(self, file, original_df, result_df):
231+
for i in self.tree.get_children():
232+
if self.tree.item(i)['values'][1]==file:
233+
self.tree.item(i, values=("☑️", file, "Predicted"))
234+
break
235+
236+
def add_error(self, file, error_msg):
237+
for i in self.tree.get_children():
238+
if self.tree.item(i)['values'][1]==file:
239+
self.tree.item(i, values=("☑️", file, f"Error: {error_msg}"))
240+
break
241+
242+
def set_target(self, v):
243+
self.target_progress = v
244+
245+
def animate_progress(self):
246+
if self.progress_value < self.target_progress:
247+
self.progress_value += 1
248+
self.progress["value"] = self.progress_value
249+
self.root.after(15, self.animate_progress)
250+
251+
def cancel(self):
252+
if self.worker_obj:
253+
self.worker_obj.stop()
254+
self.finish()
255+
256+
def finish(self):
257+
self.start_btn.config(state=tk.NORMAL)
258+
self.cancel_btn.config(state=tk.DISABLED)
259+
self.progress["value"] = 100
260+
261+
# ---------------------- Export ----------------------
262+
def export_results(self):
263+
selected_files = [self.tree.item(i)['values'] for i in self.tree.get_children()
264+
if self.tree.item(i)['values'][0]=="☑️"]
265+
if not selected_files:
266+
messagebox.showwarning("Export", "No files to export")
267+
return
268+
path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text Files","*.txt")])
269+
if path:
270+
with open(path,"w",encoding="utf-8") as f:
271+
for s in selected_files:
272+
f.write(f"{s[1]} | {s[2]}\n")
273+
messagebox.showinfo("Export", "Export completed")
274+
275+
# ---------------------- Run ----------------------
276+
def run(self):
277+
self.root.mainloop()
278+
279+
# ---------------------- RUN ----------------------
280+
if __name__ == "__main__":
281+
app = LoanGuardianApp()
282+
app.run()

0 commit comments

Comments
 (0)