import pandas as pd import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans import streamlit as st import re def clean_salary(salary_str): """Extract and clean salary information, including ranges and hourly rates.""" if pd.isna(salary_str): return 0 salary_str = str(salary_str).lower().replace(',', '') # Convert hourly rate to annual salary (assuming 40 hours/week, 52 weeks/year) HOURS_PER_YEAR = 40 * 52 # 2080 hours per year # Handle hourly rates with ranges (e.g., "50-54 an hour" or "$50-$54/hr") hourly_range_match = re.search(r'(\d+\.?\d*)\s*[-to]+\s*(\d+\.?\d*)\s*(?:an hour|per hour|\/hour|\/hr|\$?\/h|\$?ph|hr)', salary_str) if hourly_range_match: low, high = map(float, hourly_range_match.groups()) avg_hourly = (low + high) / 2 return int(avg_hourly * HOURS_PER_YEAR) # Handle single hourly rate (e.g., "50 an hour" or "$50/hr") hourly_match = re.search(r'(\d+\.?\d*)\s*(?:an hour|per hour|\/hour|\/hr|\$?\/h|\$?ph|hr)', salary_str) if hourly_match: hourly_rate = float(hourly_match.group(1)) return int(hourly_rate * HOURS_PER_YEAR) # Handle annual salary ranges (e.g., "100,440-223,680 a year" or "100,440−100,440−223,680 a year") # First, clean up multiple dashes and convert them to a single dash cleaned_str = re.sub(r'−+', '-', salary_str) # Replace multiple em-dashes with hyphen cleaned_str = re.sub(r'-+', '-', cleaned_str) # Replace multiple hyphens with single hyphen # Look for salary range pattern range_match = re.search(r'(\d+\.?\d*)\s*k?\s*-\s*(\d+\.?\d*)\s*k?', cleaned_str) if range_match: nums = [float(x) for x in range_match.groups()] # Convert if 'k' is in the string if 'k' in salary_str: nums = [x * 1000 for x in nums] low = min(nums) high = max(nums) return int((low + high) // 2) # Handle single values with 'k' (e.g., "150k" or "150K") k_match = re.search(r'(\d+\.?\d*)\s*k', salary_str) if k_match: return int(float(k_match.group(1)) * 1000) # Extract plain numbers numbers = re.findall(r'\d+\.?\d*', salary_str) if numbers: nums = [float(x) for x in numbers] # If multiple numbers found, assume it's a range if len(nums) >= 2: low = min(nums) high = max(nums) # If the numbers are small (likely in thousands), multiply by 1000 if high < 1000: low *= 1000 high *= 1000 return int((low + high) // 2) else: # Single number found salary = nums[0] # If the number is small (likely in thousands), multiply by 1000 if salary < 1000: salary *= 1000 return int(salary) return 0 def load_and_preprocess_data(url): """Load and preprocess the dataset.""" try: df = pd.read_csv(url) df = df[['Title', 'Location', 'Salary', 'Description']] df.dropna(subset=['Title', 'Location', 'Description'], inplace=True) # Clean salary column df['clean_salary'] = df['Salary'].apply(clean_salary) # Add debug print for salary cleaning st.write("Sample of cleaned salaries:") sample_df = df[['Salary', 'clean_salary']].head() st.write(sample_df) # Combine title and description for better clustering df['combined_text'] = df['Title'].str.lower() + ' ' + df['Description'].str.lower() return df except Exception as e: st.error(f"Error loading data: {str(e)}") return None def find_matching_jobs(df, job_title, location, min_salary, keywords, debug=True): """Find matching jobs using simple filtering first.""" matches = df.copy() # Debug information if debug: st.write("### Debug Information") st.write(f"Initial number of jobs: {len(matches)}") # Apply location filter if provided if location: matches = matches[matches['Location'].str.contains(location, case=False, na=False)] if debug: st.write(f"After location filter: {len(matches)} jobs") # Apply salary filter if min_salary > 0: matches = matches[matches['clean_salary'] >= min_salary] if debug: st.write(f"After salary filter: {len(matches)} jobs") # Apply keyword filter if provided if keywords: for keyword in keywords.split(','): keyword = keyword.strip() if keyword: matches = matches[ matches['Description'].str.contains(keyword, case=False, na=False) | matches['Title'].str.contains(keyword, case=False, na=False) ] if debug: st.write(f"After keyword '{keyword}' filter: {len(matches)} jobs") # Check if we have any matches before applying K-means if len(matches) == 0: if debug: st.warning("No jobs match the basic filters. Try relaxing your search criteria.") return matches # Only apply title and K-means clustering if we have matches if job_title: # Create TF-IDF vectors vectorizer = TfidfVectorizer( stop_words='english', max_features=1000, ngram_range=(1, 2) ) # Fit vectorizer on the remaining matches tfidf_matrix = vectorizer.fit_transform(matches['combined_text']) # Create clusters num_clusters = min(5, len(matches)) # Adjust number of clusters based on matches kmeans = KMeans(n_clusters=num_clusters, random_state=42, n_init=10) clusters = kmeans.fit_predict(tfidf_matrix) # Transform search query search_query = job_title.lower() query_vector = vectorizer.transform([search_query]) query_cluster = kmeans.predict(query_vector)[0] # Assign clusters and filter matches['cluster'] = clusters matches = matches[matches['cluster'] == query_cluster] if debug: st.write(f"After clustering filter: {len(matches)} jobs") st.write(f"Query assigned to cluster: {query_cluster}") return matches def main(): st.set_page_config(page_title="Job Search Application", layout="wide") st.title("Job Search Application") st.write("Find jobs that match your preferences!") # Load data url = "https://huggingface.co/datasets/burcuonel/datajob_indeed_usa/raw/main/datajob_indeed_usa.csv" jobs_df = load_and_preprocess_data(url) if jobs_df is None: return # Initialize session state if 'search_performed' not in st.session_state: st.session_state.search_performed = False if 'filtered_jobs' not in st.session_state: st.session_state.filtered_jobs = pd.DataFrame() if 'expanded_descriptions' not in st.session_state: st.session_state.expanded_descriptions = set() # Sidebar filters with st.sidebar: st.header("Search Filters") with st.form(key='search_form'): job_title = st.text_input("Job Title:", "") location = st.text_input("Location:", "") min_salary = st.slider("Minimum Salary:", 0, 300000, 50000, step=5000) keywords = st.text_area( "Keywords (comma-separated):", placeholder="e.g., Python, SQL, machine learning", help="Enter keywords to find in job descriptions" ) # Add debug mode checkbox debug_mode = st.checkbox("Show debug information", value=True) search_button = st.form_submit_button( "Search Jobs", use_container_width=True, type="primary" ) if search_button: st.session_state.filtered_jobs = find_matching_jobs( jobs_df, job_title, location, min_salary, keywords, debug=debug_mode ) st.session_state.search_performed = True # Reset expanded descriptions when new search is performed st.session_state.expanded_descriptions = set() if st.button("Reset Filters", use_container_width=True): st.session_state.search_performed = False st.session_state.filtered_jobs = pd.DataFrame() st.session_state.expanded_descriptions = set() st.rerun() # Display results if st.session_state.search_performed: filtered_jobs = st.session_state.filtered_jobs st.subheader(f"Found {len(filtered_jobs)} Matching Jobs") if not filtered_jobs.empty: for index, row in filtered_jobs.iterrows(): with st.expander(f"{row['Title']} - {row['Location']}"): st.write(f"**Salary:** {row['Salary']}") st.write(f"**Cleaned Salary:** {row['clean_salary']}") if 'cluster' in row: st.write(f"**Cluster:** {row['cluster']}") st.write("**Description:**") description = row['Description'] # Highlight keywords if keywords: keyword_list = [k.strip() for k in keywords.split(',')] for keyword in keyword_list: if keyword: pattern = re.compile(f'({re.escape(keyword)})', re.IGNORECASE) description = pattern.sub(r'**\1**', description) # Check if this description is expanded is_expanded = index in st.session_state.expanded_descriptions # Show full or truncated description based on state if len(description) > 500 and not is_expanded: st.write(description[:500] + "...") if st.button("See more", key=f"expand_{index}"): st.session_state.expanded_descriptions.add(index) st.rerun() else: st.write(description) if len(description) > 500 and st.button("See less", key=f"collapse_{index}"): st.session_state.expanded_descriptions.remove(index) st.rerun() else: st.warning(""" No matching jobs found. Try: 1. Using fewer filters 2. Using more general keywords 3. Reducing the minimum salary 4. Checking different locations """) else: st.info("Use the filters in the sidebar and click 'Search Jobs' to find matching positions.") if __name__ == "__main__": main()