import pandas as pd import networkx as nx from collections import Counter import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import io import base64 class RoutingEngine: def __init__(self, data_path): self.df = pd.read_csv(data_path) self.df.columns = self.df.columns.str.strip() self.G = nx.Graph() self.all_unique_skills = set() self.all_jobs = set() self._build_graph() def _build_graph(self): self.df['All_Skills'] = ( self.df['Skills'].fillna('') + "," + self.df.get('Programming Languages', pd.Series([''] * len(self.df))) + "," + self.df['Tools'].fillna('') ) # Split skills and explode df_exploded = self.df.assign(All_Skills=self.df['All_Skills'].str.split(',')).explode('All_Skills') df_exploded['All_Skills'] = df_exploded['All_Skills'].str.strip() # Remove empty strings df_exploded = df_exploded[df_exploded['All_Skills'] != ''] for _, row in df_exploded.iterrows(): job = row['Job roles'] skill = row['All_Skills'] if isinstance(skill, str) and len(skill) > 1: self.G.add_edge(job, skill) self.all_unique_skills.add(skill) self.all_jobs.add(job) def get_skill_list(self): return sorted([str(s) for s in self.all_unique_skills if isinstance(s, str)]) def get_job_list(self): return sorted(list(self.all_jobs)) def get_gap(self, target_job, user_skills): if target_job not in self.G.nodes: return [] required_skills = set(self.G.neighbors(target_job)) current_skills = set(user_skills) missing_skills = list(required_skills - current_skills) return missing_skills def recommend(self, user_skills): possible_jobs = [] for skill in user_skills: if skill in self.G.nodes: neighbors = list(self.G.neighbors(skill)) possible_jobs.extend(neighbors) if not possible_jobs: return None, [] top_jobs_counter = Counter(possible_jobs).most_common(1) best_job = top_jobs_counter[0][0] missing_skills = self.get_gap(best_job, user_skills) return best_job, missing_skills def get_career_transition_path(self, current_job, target_job): """Finds the shortest upskilling path between two roles via shared skills.""" if current_job not in self.G.nodes or target_job not in self.G.nodes: return None try: # NetworkX finds the shortest path alternating: Job -> Skill -> Job path = nx.shortest_path(self.G, source=current_job, target=target_job) skills_to_learn = [node for node in path if node in self.all_unique_skills] stepping_stones = [node for node in path if node in self.all_jobs and node not in (current_job, target_job)] return { "path": path, "skills_to_learn": skills_to_learn, "stepping_stones": stepping_stones } except Exception: return None def get_subgraph_figure_base64(self, center_node, user_skills, depth=1): try: if center_node not in self.G.nodes: return None skills = list(self.G.neighbors(center_node)) subG = self.G.subgraph([center_node] + skills) plt.figure(figsize=(8, 6), facecolor='#0f172a') pos = nx.spring_layout(subG, seed=42, k=0.5) nx.draw_networkx_edges(subG, pos, edge_color='#334155', alpha=0.5) node_colors = [] for n in subG.nodes(): if n == center_node: node_colors.append('#ffffff') elif n in user_skills: node_colors.append('#047857') else: node_colors.append('#dc2626') nx.draw_networkx_nodes(subG, pos, node_color=node_colors, node_size=600, alpha=0.9) labels = {n: n for n in subG.nodes()} nx.draw_networkx_labels(subG, pos, labels=labels, font_size=8, font_color='white', font_family='sans-serif') plt.title(f"Skill Gap Map: {center_node}", color='white') plt.axis('off') buf = io.BytesIO() plt.savefig(buf, format='png', bbox_inches='tight', facecolor='#0f172a') plt.close() buf.seek(0) img_base64 = base64.b64encode(buf.read()).decode('utf-8') return f"data:image/png;base64,{img_base64}" except Exception as e: print(f"Graph Error: {e}") return None