markobinario commited on
Commit
beb1862
Β·
verified Β·
1 Parent(s): 6adf0f1

Create recommender.py

Browse files
Files changed (1) hide show
  1. recommender.py +336 -0
recommender.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from sklearn.neighbors import KNeighborsClassifier
4
+ from sklearn.preprocessing import LabelEncoder, StandardScaler
5
+ import joblib
6
+ import json
7
+ import os
8
+ import requests
9
+
10
+ class CourseRecommender:
11
+ def __init__(self):
12
+ self.model = None
13
+ self.label_encoders = {}
14
+ self.scaler = StandardScaler()
15
+ self.courses = self.get_courses()
16
+ self.training_data = self.get_training_data()
17
+ self.train_model()
18
+
19
+ def get_courses(self):
20
+ """Get static course data"""
21
+ return {
22
+ 'BSCS': 'Bachelor of Science in Computer Science',
23
+ 'BSIT': 'Bachelor of Science in Information Technology',
24
+ 'BSBA': 'Bachelor of Science in Business Administration',
25
+ 'BSED': 'Bachelor of Science in Education',
26
+ 'BSN': 'Bachelor of Science in Nursing',
27
+ 'BSArch': 'Bachelor of Science in Architecture',
28
+ 'BSIE': 'Bachelor of Science in Industrial Engineering',
29
+ 'BSHM': 'Bachelor of Science in Hospitality Management',
30
+ 'BSA': 'Bachelor of Science in Accountancy',
31
+ 'BSPsych': 'Bachelor of Science in Psychology',
32
+ 'BSAgri': 'Bachelor of Science in Agriculture'
33
+ }
34
+
35
+ def save_student_data(self, stanine, gwa, strand, course, rating, hobbies=None):
36
+ """Save student feedback to in-memory storage (for demonstration purposes)"""
37
+ try:
38
+ # In a real implementation, you could save this to a file or external storage
39
+ print(f"Student feedback saved: Stanine={stanine}, GWA={gwa}, Strand={strand}, Course={course}, Rating={rating}, Hobbies={hobbies}")
40
+ return True
41
+ except Exception as e:
42
+ print(f"Error saving student feedback: {e}")
43
+ return False
44
+
45
+ def get_training_data(self):
46
+ """Get static training data for demonstration purposes"""
47
+ # Sample training data to demonstrate the recommender system
48
+ training_data = [
49
+ # STEM students
50
+ (8, 95, 'STEM', 'BSCS', 5, 'programming, gaming, technology'),
51
+ (7, 90, 'STEM', 'BSIT', 4, 'computers, software, coding'),
52
+ (9, 98, 'STEM', 'BSCS', 5, 'programming, algorithms, math'),
53
+ (6, 85, 'STEM', 'BSIT', 3, 'technology, computers'),
54
+ (8, 92, 'STEM', 'BSArch', 4, 'design, drawing, creativity'),
55
+ (7, 88, 'STEM', 'BSIE', 4, 'engineering, problem solving'),
56
+
57
+ # ABM students
58
+ (8, 90, 'ABM', 'BSBA', 5, 'business, management, leadership'),
59
+ (7, 85, 'ABM', 'BSA', 4, 'accounting, numbers, finance'),
60
+ (6, 82, 'ABM', 'BSBA', 3, 'business, marketing'),
61
+ (9, 95, 'ABM', 'BSA', 5, 'accounting, finance, analysis'),
62
+
63
+ # HUMSS students
64
+ (8, 88, 'HUMSS', 'BSED', 5, 'teaching, helping, education'),
65
+ (7, 85, 'HUMSS', 'BSPsych', 4, 'psychology, helping, people'),
66
+ (6, 80, 'HUMSS', 'BSED', 3, 'teaching, children'),
67
+ (9, 92, 'HUMSS', 'BSPsych', 5, 'psychology, counseling, people'),
68
+
69
+ # General interests
70
+ (7, 87, 'STEM', 'BSN', 4, 'helping, healthcare, caring'),
71
+ (8, 89, 'ABM', 'BSHM', 4, 'hospitality, service, management'),
72
+ (6, 83, 'HUMSS', 'BSAgri', 3, 'agriculture, environment, nature'),
73
+ ]
74
+
75
+ return pd.DataFrame(training_data, columns=['stanine', 'gwa', 'strand', 'course', 'rating', 'hobbies'])
76
+
77
+ def train_model(self):
78
+ """Train the recommendation model using the training data"""
79
+ try:
80
+ training_data = self.get_training_data()
81
+
82
+ if training_data.empty:
83
+ print("No training data available - using default recommendations")
84
+ return
85
+
86
+ # Prepare features (hobbies required)
87
+ feature_columns = ['stanine', 'gwa', 'strand', 'hobbies']
88
+
89
+ # Create feature matrix
90
+ X = training_data[feature_columns].copy()
91
+ y = training_data['course']
92
+
93
+ # Handle categorical variables
94
+ categorical_columns = ['strand', 'hobbies']
95
+
96
+ # Refit encoders every training to incorporate new categories
97
+ for col in categorical_columns:
98
+ if col in X.columns:
99
+ X[col] = X[col].fillna('unknown')
100
+ self.label_encoders[col] = LabelEncoder()
101
+ X[col] = self.label_encoders[col].fit_transform(X[col])
102
+
103
+ # Scale numerical features
104
+ numerical_columns = ['stanine', 'gwa']
105
+ if not X[numerical_columns].empty:
106
+ X[numerical_columns] = self.scaler.fit_transform(X[numerical_columns])
107
+
108
+ # Train KNN model
109
+ self.model = KNeighborsClassifier(n_neighbors=3, weights='distance')
110
+ self.model.fit(X, y)
111
+
112
+ print("βœ… Model trained successfully (hobbies required and encoded)")
113
+
114
+ except Exception as e:
115
+ print(f"Error training model: {e}")
116
+ self.model = None
117
+
118
+ def get_default_recommendations(self, stanine, gwa, strand):
119
+ """Provide default recommendations based on basic rules when no training data is available"""
120
+ courses = self.courses
121
+ recommendations = []
122
+
123
+ # Basic rules for recommendations
124
+ if strand == 'STEM':
125
+ if stanine >= 8 and gwa >= 90:
126
+ priority_courses = ['BSCS', 'BSIT']
127
+ else:
128
+ priority_courses = ['BSIT', 'BSCS']
129
+ elif strand == 'ABM':
130
+ priority_courses = ['BSBA']
131
+ elif strand == 'HUMSS':
132
+ priority_courses = ['BSED']
133
+ else:
134
+ priority_courses = list(courses.keys())
135
+
136
+ # Add courses with default probabilities
137
+ for i, course in enumerate(priority_courses[:2]): # Only take top 2
138
+ if course in courses:
139
+ recommendations.append({
140
+ 'code': course,
141
+ 'name': courses[course],
142
+ 'probability': 1.0 - (i * 0.2) # Decreasing probability for each course
143
+ })
144
+
145
+ return recommendations
146
+
147
+ def recommend_courses(self, stanine, gwa, strand, hobbies=None, top_n=5):
148
+ """Recommend courses based on student profile (hobbies required)"""
149
+ try:
150
+ if self.model is None:
151
+ return self.get_default_recommendations(stanine, gwa, strand)
152
+
153
+ # Prepare input features
154
+ input_data = pd.DataFrame([{
155
+ 'stanine': stanine,
156
+ 'gwa': gwa,
157
+ 'strand': strand,
158
+ 'hobbies': (hobbies or '').strip()
159
+ }])
160
+ # Validate hobbies
161
+ if not input_data['hobbies'].iloc[0]:
162
+ raise ValueError('hobbies is required for recommendations')
163
+
164
+ # Encode categorical variables
165
+ for col in ['strand', 'hobbies']:
166
+ if col in input_data.columns and col in self.label_encoders:
167
+ value = input_data[col].iloc[0]
168
+ if value not in self.label_encoders[col].classes_:
169
+ # Extend encoder classes to include unseen value at inference
170
+ self.label_encoders[col].classes_ = np.append(self.label_encoders[col].classes_, value)
171
+ input_data[col] = self.label_encoders[col].transform(input_data[col])
172
+
173
+ # Scale numerical features
174
+ numerical_columns = ['stanine', 'gwa']
175
+ if not input_data[numerical_columns].empty:
176
+ input_data[numerical_columns] = self.scaler.transform(input_data[numerical_columns])
177
+
178
+ # Get predictions
179
+ predictions = self.model.predict_proba(input_data)
180
+ courses = self.model.classes_
181
+
182
+ # Get top recommendations
183
+ top_indices = np.argsort(predictions[0])[-top_n:][::-1]
184
+ recommendations = []
185
+
186
+ course_map = self.courses
187
+ for idx in top_indices:
188
+ code = courses[idx]
189
+ confidence = predictions[0][idx]
190
+ recommendations.append({
191
+ 'code': code,
192
+ 'name': course_map.get(code, code),
193
+ 'rating': round(confidence * 100, 1)
194
+ })
195
+
196
+ return recommendations
197
+
198
+ except Exception as e:
199
+ print(f"Error recommending courses: {e}")
200
+ return self.get_default_recommendations(stanine, gwa, strand)
201
+
202
+ def _get_recommendation_reason(self, course, stanine, gwa, strand, hobbies, interests, personality_type, learning_style, career_goals):
203
+ """Generate personalized reason for recommendation"""
204
+ reasons = []
205
+
206
+ # Academic performance reasons
207
+ if stanine >= 8:
208
+ reasons.append("Excellent academic performance")
209
+ elif stanine >= 6:
210
+ reasons.append("Good academic foundation")
211
+
212
+ if gwa >= 85:
213
+ reasons.append("High academic achievement")
214
+ elif gwa >= 80:
215
+ reasons.append("Strong academic record")
216
+
217
+ # Strand alignment
218
+ if strand == "STEM" and course in ["BSCS", "BSIT", "BSArch", "BSIE", "BSN"]:
219
+ reasons.append("Perfect match with your STEM background")
220
+ elif strand == "ABM" and course in ["BSBA", "BSA"]:
221
+ reasons.append("Excellent alignment with your ABM strand")
222
+ elif strand == "HUMSS" and course in ["BSED", "BSPsych"]:
223
+ reasons.append("Great fit with your HUMSS background")
224
+
225
+ # Hobbies and interests alignment
226
+ if hobbies and any(hobby in hobbies.lower() for hobby in ["gaming", "programming", "technology", "computers"]):
227
+ if course in ["BSCS", "BSIT"]:
228
+ reasons.append("Matches your technology interests")
229
+
230
+ if hobbies and any(hobby in hobbies.lower() for hobby in ["business", "leadership", "management"]):
231
+ if course in ["BSBA", "BSA"]:
232
+ reasons.append("Aligns with your business interests")
233
+
234
+ if hobbies and any(hobby in hobbies.lower() for hobby in ["helping", "teaching", "caring"]):
235
+ if course in ["BSED", "BSN", "BSPsych"]:
236
+ reasons.append("Perfect for your helping nature")
237
+
238
+ # Personality type alignment
239
+ if personality_type == "introvert" and course in ["BSCS", "BSA", "BSArch"]:
240
+ reasons.append("Suits your introverted personality")
241
+ elif personality_type == "extrovert" and course in ["BSBA", "BSED", "BSHM"]:
242
+ reasons.append("Great for your outgoing personality")
243
+
244
+ # Learning style alignment
245
+ if learning_style == "hands-on" and course in ["BSIT", "BSHM", "BSAgri"]:
246
+ reasons.append("Matches your hands-on learning preference")
247
+ elif learning_style == "visual" and course in ["BSArch", "BSCS"]:
248
+ reasons.append("Perfect for your visual learning style")
249
+
250
+ # Career goals alignment
251
+ if career_goals and any(goal in career_goals.lower() for goal in ["developer", "programmer", "software"]):
252
+ if course in ["BSCS", "BSIT"]:
253
+ reasons.append("Direct path to your career goals")
254
+
255
+ if career_goals and any(goal in career_goals.lower() for goal in ["business", "entrepreneur", "manager"]):
256
+ if course in ["BSBA", "BSA"]:
257
+ reasons.append("Direct path to your business goals")
258
+
259
+ # Default reason if no specific matches
260
+ if not reasons:
261
+ reasons.append("Good academic and personal fit")
262
+
263
+ return " β€’ ".join(reasons[:3]) # Limit to top 3 reasons
264
+
265
+ def save_model(self, model_path='course_recommender_model.joblib'):
266
+ """Save the trained model"""
267
+ if self.model is None:
268
+ raise Exception("No model to save!")
269
+
270
+ model_data = {
271
+ 'model': self.model,
272
+ 'scaler': self.scaler,
273
+ 'label_encoders': self.label_encoders
274
+ }
275
+ joblib.dump(model_data, model_path)
276
+
277
+ def load_model(self, model_path='course_recommender_model.joblib'):
278
+ """Load a trained model"""
279
+ model_data = joblib.load(model_path)
280
+ self.model = model_data['model']
281
+ self.scaler = model_data['scaler']
282
+ self.label_encoders = model_data['label_encoders']
283
+
284
+
285
+ # ===== UI helper for Hugging Face integration =====
286
+ def get_course_recommendations_ui(recommender: "CourseRecommender", stanine, gwa, strand, hobbies) -> str:
287
+ if recommender is None:
288
+ return "Sorry, the recommendation system is not available at the moment. Please try again later."
289
+ try:
290
+ try:
291
+ stanine = int(stanine.strip()) if isinstance(stanine, str) else int(stanine)
292
+ except (ValueError, TypeError, AttributeError):
293
+ return "❌ Stanine score must be a valid number between 1 and 9"
294
+ try:
295
+ gwa = float(gwa.strip()) if isinstance(gwa, str) else float(gwa)
296
+ except (ValueError, TypeError, AttributeError):
297
+ return "❌ GWA must be a valid number between 75 and 100"
298
+ if not (1 <= stanine <= 9):
299
+ return "❌ Stanine score must be between 1 and 9"
300
+ if not (75 <= gwa <= 100):
301
+ return "❌ GWA must be between 75 and 100"
302
+ if not strand:
303
+ return "❌ Please select a strand"
304
+ if not hobbies or not str(hobbies).strip():
305
+ return "❌ Please enter your hobbies/interests"
306
+
307
+ recommendations = recommender.recommend_courses(
308
+ stanine=stanine,
309
+ gwa=gwa,
310
+ strand=strand,
311
+ hobbies=str(hobbies)
312
+ )
313
+ if not recommendations:
314
+ return "No recommendations available at the moment."
315
+ response = f"## 🎯 Course Recommendations for You\n\n"
316
+ response += f"**Profile:** Stanine {stanine}, GWA {gwa}, {strand} Strand\n"
317
+ response += f"**Interests:** {hobbies}\n\n"
318
+ for i, rec in enumerate(recommendations, 1):
319
+ response += f"### {i}. {rec['code']} - {rec['name']}\n"
320
+ response += f"**Match Score:** {rec.get('rating', rec.get('probability', 0)):.1f}%\n\n"
321
+ return response
322
+ except Exception as e:
323
+ return f"❌ Error getting recommendations: {str(e)}"
324
+
325
+ # Example usage
326
+ if __name__ == "__main__":
327
+ recommender = CourseRecommender()
328
+
329
+ # Example recommendation
330
+ recommendations = recommender.recommend_courses(
331
+ stanine=8,
332
+ gwa=95,
333
+ strand='STEM',
334
+ hobbies='programming, gaming, technology'
335
+ )
336
+ print("Recommended courses:", json.dumps(recommendations, indent=2))