Skip to content

Commit 0bfe355

Browse files
authored
Create SalaryPredictor.py
1 parent 0b254b0 commit 0bfe355

1 file changed

Lines changed: 331 additions & 0 deletions

File tree

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
"""
2+
SalaryPredictor v2.0 - Enterprise Edition
3+
Predict salaries from employee features using ML
4+
Supports batch predictions with real-time per-employee preview, filtering, and live search
5+
"""
6+
7+
import os, sys, threading
8+
import pandas as pd
9+
import joblib
10+
import tkinter as tk
11+
from tkinter import filedialog, messagebox, ttk
12+
import ttkbootstrap as tb
13+
from ttkbootstrap.constants import *
14+
15+
try:
16+
from tkinterdnd2 import TkinterDnD, DND_FILES
17+
DND_ENABLED = True
18+
except ImportError:
19+
DND_ENABLED = False
20+
print("Drag & Drop requires tkinterdnd2: pip install tkinterdnd2")
21+
22+
# ---------------------- UTIL ----------------------
23+
def resource_path(file_name):
24+
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
25+
return os.path.join(base_path, file_name)
26+
27+
# ---------------------- SALARY PREDICTION WORKER ----------------------
28+
class SalaryPredictWorker:
29+
def __init__(self, files, model_path, callbacks):
30+
self.files = files
31+
self.model = joblib.load(model_path)
32+
self.callbacks = callbacks
33+
self._running = True
34+
35+
def stop(self):
36+
self._running = False
37+
38+
def run(self):
39+
total_files = len(self.files)
40+
feature_cols = ["Age", "Experience", "EducationLevel"] # Only these are used by the model
41+
for i, path in enumerate(self.files):
42+
if not self._running:
43+
break
44+
try:
45+
df = pd.read_csv(path)
46+
# Select only the model features
47+
X = df[feature_cols]
48+
df['PredictedSalary'] = self.model.predict(X)
49+
50+
for idx, row in df.iterrows():
51+
if not self._running:
52+
break
53+
row_data = row.to_dict()
54+
row_data['_row_id'] = idx
55+
if "found" in self.callbacks:
56+
self.callbacks["found"](path, row_data)
57+
58+
except Exception as e:
59+
print(f"Error processing {path}: {e}")
60+
61+
if "progress" in self.callbacks:
62+
self.callbacks["progress"](int((i + 1) / total_files * 100))
63+
64+
if "finished" in self.callbacks:
65+
self.callbacks["finished"]()
66+
67+
# ---------------------- MAIN APP ----------------------
68+
class SalaryPredictApp:
69+
APP_NAME = "SalaryPredictor"
70+
APP_VERSION = "2.0"
71+
72+
def __init__(self):
73+
if DND_ENABLED:
74+
self.root = TkinterDnD.Tk()
75+
else:
76+
self.root = tb.Window(themename="darkly")
77+
78+
self.root.title(f"{self.APP_NAME} v{self.APP_VERSION}")
79+
self.root.minsize(1300, 750)
80+
81+
try:
82+
self.root.iconbitmap(resource_path("logo.ico"))
83+
except: pass
84+
85+
self.worker_obj = None
86+
self.smooth_value = 0
87+
self.target_progress = 0
88+
self.file_set = set()
89+
self.model_path = tk.StringVar()
90+
self.filter_min = tk.DoubleVar(value=0)
91+
self.filter_max = tk.DoubleVar(value=1e9)
92+
self.search_var = tk.StringVar()
93+
94+
# store all rows to enable filtering/search
95+
self.all_rows = {}
96+
97+
self._build_ui()
98+
self._apply_styles()
99+
100+
# ---------------------- UI ----------------------
101+
def _build_ui(self):
102+
main = tb.Frame(self.root, padding=10)
103+
main.pack(fill=BOTH, expand=True)
104+
105+
tb.Label(main, text=f"💰 {self.APP_NAME} - Salary Prediction System",
106+
font=("Segoe UI", 22, "bold")).pack(pady=(0, 4))
107+
tb.Label(main, text="Batch salary prediction with per-employee preview, filtering, and live search",
108+
font=("Segoe UI", 10, "italic"), foreground="#9ca3af").pack(pady=(0, 20))
109+
110+
# Row 1: File Selection
111+
row1 = tb.Frame(main)
112+
row1.pack(fill=X, pady=(0,6))
113+
self.path_input = tb.Entry(row1, width=80)
114+
self.path_input.pack(side=LEFT, fill=X, expand=True, padx=(0,6))
115+
self.path_input.insert(0, "Drag & drop CSV files or folders here…")
116+
117+
tb.Button(row1, text="📂 Browse", bootstyle=INFO, command=self.browse).pack(side=LEFT, padx=3)
118+
self.start_btn = tb.Button(row1, text="🚀 Start Prediction", bootstyle=SUCCESS, command=self.start)
119+
self.start_btn.pack(side=LEFT, padx=3)
120+
self.cancel_btn = tb.Button(row1, text="⏹ Cancel", bootstyle=DANGER, command=self.cancel)
121+
self.cancel_btn.pack(side=LEFT, padx=3)
122+
self.cancel_btn.config(state=DISABLED)
123+
tb.Button(row1, text="💾 Export Results", bootstyle=PRIMARY, command=self.export_results).pack(side=LEFT, padx=3)
124+
tb.Button(row1, text="ℹ️ About", bootstyle=INFO, command=self.show_about).pack(side=LEFT, padx=3)
125+
126+
# Row 2: Model selection + filters + search
127+
row2 = tb.Frame(main)
128+
row2.pack(fill=X, pady=(0,6))
129+
tb.Label(row2, text="ML Model Path").pack(side=LEFT, padx=3)
130+
self.model_input = tb.Entry(row2, width=50, textvariable=self.model_path)
131+
self.model_input.pack(side=LEFT, padx=3)
132+
tb.Button(row2, text="Browse", bootstyle=INFO, command=self.browse_model).pack(side=LEFT, padx=3)
133+
134+
# Salary filters
135+
tb.Label(row2, text="Min Salary").pack(side=LEFT, padx=3)
136+
tb.Entry(row2, width=10, textvariable=self.filter_min).pack(side=LEFT, padx=3)
137+
tb.Label(row2, text="Max Salary").pack(side=LEFT, padx=3)
138+
tb.Entry(row2, width=10, textvariable=self.filter_max).pack(side=LEFT, padx=3)
139+
tb.Label(row2, text="Search Employee").pack(side=LEFT, padx=3)
140+
search_entry = tb.Entry(row2, width=20, textvariable=self.search_var)
141+
search_entry.pack(side=LEFT, padx=3)
142+
self.search_var.trace_add("write", lambda *args: self.apply_filters())
143+
144+
# Progress bar
145+
self.progress = tb.Progressbar(main, bootstyle="success-striped", maximum=100)
146+
self.progress.pack(fill=X, pady=(0,6))
147+
148+
# Treeview for per-employee predictions
149+
columns = ("selected", "filename", "employee_name", "predicted_salary")
150+
self.tree = ttk.Treeview(main, columns=columns, show="headings", selectmode="extended", height=25)
151+
self.tree.heading("selected", text="✅")
152+
self.tree.heading("filename", text="Filename", anchor=W)
153+
self.tree.heading("employee_name", text="Employee", anchor=W)
154+
self.tree.heading("predicted_salary", text="Predicted Salary", anchor=W)
155+
self.tree.column("selected", width=50, anchor=CENTER)
156+
self.tree.column("filename", width=400)
157+
self.tree.column("employee_name", width=250)
158+
self.tree.column("predicted_salary", width=150)
159+
self.tree.pack(fill=BOTH, expand=True, pady=(0,6))
160+
161+
self.stats_label = tb.Label(main, text="EMPLOYEES PROCESSED: 0")
162+
self.stats_label.pack(anchor=E)
163+
164+
self.root.after(15, self.animate_progress)
165+
166+
if DND_ENABLED:
167+
self.tree.drop_target_register(DND_FILES)
168+
self.tree.dnd_bind("<<Drop>>", self.on_drop)
169+
170+
# ---------------------- Browse / DnD ----------------------
171+
def browse(self):
172+
folder = filedialog.askdirectory(title="Select Folder with CSV files")
173+
if folder:
174+
self.start_btn.config(state=DISABLED)
175+
self.cancel_btn.config(state=NORMAL)
176+
threading.Thread(target=self._scan_and_queue_files_thread, args=([folder],), daemon=True).start()
177+
178+
def browse_model(self):
179+
path = filedialog.askopenfilename(title="Select ML Model", filetypes=[("Joblib Files","*.joblib")])
180+
if path:
181+
self.model_path.set(path)
182+
183+
def on_drop(self, event):
184+
dropped_paths = self.root.tk.splitlist(event.data)
185+
self.start_btn.config(state=DISABLED)
186+
self.cancel_btn.config(state=NORMAL)
187+
threading.Thread(target=self._scan_and_queue_files_thread, args=(dropped_paths,), daemon=True).start()
188+
189+
def _scan_and_queue_files_thread(self, paths):
190+
all_files = []
191+
for p in paths:
192+
if os.path.isdir(p):
193+
for root_dir, _, fs in os.walk(p):
194+
for name in fs:
195+
all_files.append(os.path.join(root_dir, name))
196+
elif os.path.isfile(p):
197+
all_files.append(p)
198+
self._insert_files_chunked(all_files)
199+
200+
def _insert_files_chunked(self, paths, chunk_size=500):
201+
if not paths:
202+
self.start_btn.config(state=NORMAL)
203+
self.cancel_btn.config(state=DISABLED)
204+
self.path_input.delete(0, END)
205+
self.path_input.insert(0, f"{len(self.file_set)} files queued")
206+
return
207+
chunk = paths[:chunk_size]
208+
remaining = paths[chunk_size:]
209+
for path in chunk:
210+
ext = os.path.splitext(path)[1].lower()
211+
if ext == ".csv":
212+
if path not in self.file_set:
213+
self.file_set.add(path)
214+
self.tree.insert("", END, iid=f"{path}_placeholder",
215+
values=("☑️", path, "-", "-"))
216+
self.root.after(1, lambda: self._insert_files_chunked(remaining, chunk_size))
217+
218+
# ---------------------- Actions ----------------------
219+
def start(self):
220+
if not self.model_path.get():
221+
messagebox.showwarning("No Model", "Please select an ML model before starting.")
222+
return
223+
selected_files = [self.tree.item(i)['values'][1] for i in self.tree.get_children()
224+
if self.tree.item(i)['values'][0]=="☑️"]
225+
if not selected_files:
226+
messagebox.showwarning("No Selection", "Select CSV files using the checkboxes before predicting.")
227+
return
228+
self.progress["value"] = 0
229+
self.smooth_value = 0
230+
self.target_progress = 0
231+
self.start_btn.config(state=DISABLED)
232+
self.cancel_btn.config(state=NORMAL)
233+
self.all_rows = {}
234+
self.tree.delete(*self.tree.get_children())
235+
threading.Thread(target=self._run_worker, args=(selected_files,), daemon=True).start()
236+
237+
def _run_worker(self, files):
238+
self.worker_obj = SalaryPredictWorker(
239+
files,
240+
self.model_path.get(),
241+
callbacks={
242+
"found": self.add_result,
243+
"progress": self.set_target,
244+
"finished": self.finish
245+
}
246+
)
247+
self.worker_obj.run()
248+
249+
def add_result(self, file, row_data):
250+
row_key = f"{file}_{row_data.get('_row_id',0)}"
251+
self.all_rows[row_key] = {
252+
"selected": "☑️",
253+
"filename": file,
254+
"employee_name": row_data.get('Name',''),
255+
"predicted_salary": row_data['PredictedSalary']
256+
}
257+
self.apply_filters()
258+
259+
def apply_filters(self):
260+
min_salary = self.filter_min.get()
261+
max_salary = self.filter_max.get()
262+
search_text = self.search_var.get().lower()
263+
self.tree.delete(*self.tree.get_children())
264+
for key, row in self.all_rows.items():
265+
if min_salary <= row['predicted_salary'] <= max_salary:
266+
if search_text in row['employee_name'].lower():
267+
self.tree.insert("", END, iid=key,
268+
values=(row['selected'], row['filename'], row['employee_name'], f"${row['predicted_salary']:,.2f}"))
269+
self.stats_label.config(text=f"EMPLOYEES DISPLAYED: {len(self.tree.get_children())}")
270+
271+
def set_target(self, v):
272+
self.target_progress = v
273+
274+
def animate_progress(self):
275+
if self.smooth_value < self.target_progress:
276+
self.smooth_value += 1
277+
self.progress["value"] = self.smooth_value
278+
self.root.after(15, self.animate_progress)
279+
280+
def cancel(self):
281+
if self.worker_obj:
282+
self.worker_obj.stop()
283+
self.finish()
284+
285+
def finish(self):
286+
self.start_btn.config(state=NORMAL)
287+
self.cancel_btn.config(state=DISABLED)
288+
self.progress["value"] = 100
289+
290+
# ---------------------- Export ----------------------
291+
def export_results(self):
292+
selected_rows = [self.tree.item(i)['values'] for i in self.tree.get_children()
293+
if self.tree.item(i)['values'][0]=="☑️"]
294+
if not selected_rows:
295+
messagebox.showwarning("Export", "No selected rows to export")
296+
return
297+
path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV Files","*.csv")])
298+
if path:
299+
with open(path,"w",encoding="utf-8") as f:
300+
f.write("Filename,Employee,PredictedSalary\n")
301+
for s in selected_rows:
302+
f.write(f"{s[1]},{s[2]},{s[3]}\n")
303+
messagebox.showinfo("Export", "Export completed")
304+
305+
# ---------------------- About ----------------------
306+
def show_about(self):
307+
messagebox.showinfo(
308+
f"About {self.APP_NAME}",
309+
f"{self.APP_NAME} v{self.APP_VERSION}\n\n"
310+
"• Drag & drop CSV files or folders\n"
311+
"• Real-time per-employee salary prediction\n"
312+
"• Min/Max salary filtering and live employee search\n"
313+
"• Batch processing with smooth progress bar\n"
314+
"• Export results to CSV\n\n"
315+
"🏢 Built by Your Company\n"
316+
"🌐 https://yourwebsite.com"
317+
)
318+
319+
# ---------------------- Styles ----------------------
320+
def _apply_styles(self):
321+
self.root.style = tb.Style(theme="darkly")
322+
self.root.style.configure("TProgressbar", troughcolor="#1b1f3a", background="#7c3aed", thickness=14)
323+
324+
# ---------------------- Run ----------------------
325+
def run(self):
326+
self.root.mainloop()
327+
328+
# ---------------------- RUN ----------------------
329+
if __name__ == "__main__":
330+
app = SalaryPredictApp()
331+
app.run()

0 commit comments

Comments
 (0)