File size: 6,823 Bytes
9d263bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import pandas as pd
import re
from sentence_transformers import SentenceTransformer, util
import streamlit as st
from nltk.corpus import stopwords
import nltk
import numpy as np
from datasets import load_dataset

# Download necessary NLTK data
nltk.download('stopwords')
nltk.download('punkt')

# Load the dataset from Hugging Face
try:
    dataset = load_dataset("Sasivar/Analytics_Vidhya_Free_Course_Data")
    df = pd.DataFrame(dataset['train'])  # Assuming the data is in the 'train' split
except Exception as e:
    st.error(f"Error loading dataset: {e}")

# Clean and preprocess text
stop_words = set(stopwords.words('english'))

# Abbreviation to full form mapping
abbreviation_map = {
    "ml": "machine learning",
    "dl": "deep learning",
    "llm": "large language model",
    "ai": "artificial intelligence",
    "nlp": "natural language processing",
    "cv": "computer vision",
    "knn": "k nearest neighbors",
    "regression": "regression analysis"
}

def expand_abbreviation(query):
    """Expand abbreviations to their full forms."""
    query = query.lower()  # Convert to lowercase for consistency
    for abbreviation, full_form in abbreviation_map.items():
        query = re.sub(rf"\b{abbreviation}\b", full_form, query)  # Word boundary match to avoid partial matches
    return query

def clean_text(text):
    """Clean and preprocess text (remove non-word characters and stopwords)."""
    text = text.lower()  # Convert to lowercase for consistency
    text = re.sub(r'\W', ' ', text)  # Remove non-word characters (punctuation, etc.)
    tokens = text.split()
    tokens = [word for word in tokens if word not in stop_words]  # Remove stopwords
    return ' '.join(tokens)

# Handle missing ratings
def handle_missing_ratings(df, strategy="average"):
    """Fill missing ratings based on the chosen strategy."""
    df['Rating'] = df['Rating'].replace('Unknown', np.nan)  # Replace 'Unknown' with NaN
    if strategy == "average":
        # Fill with average rating
        average_rating = df['Rating'].astype(float).mean()
        df['Rating'] = df['Rating'].fillna(average_rating)
    elif strategy == "mode":
        # Fill with mode rating
        mode_rating = df['Rating'].mode()[0]
        df['Rating'] = df['Rating'].fillna(mode_rating)
    elif strategy == "random":
        # Fill with random rating in the range of existing ratings
        min_rating = df['Rating'].min()
        max_rating = df['Rating'].max()
        random_ratings = np.random.randint(min_rating, max_rating + 1, size=df['Rating'].isna().sum())
        df.loc[df['Rating'].isna(), 'Rating'] = random_ratings
    return df

# Add 'Course Info' column by combining key information fields
df = handle_missing_ratings(df, strategy="average")  # Choose your strategy here: "average", "mode", "random"
df['Course Info'] = (
        df['Course Title'] + ' ' + df['Description'].fillna('') + ' ' + df['Level'].fillna('Unknown')
).apply(lambda x: expand_abbreviation(clean_text(x)))  # Clean and expand abbreviations

# Load and cache model
@st.cache_resource
def load_model():
    return SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

# Cache course embeddings
@st.cache_data
def get_course_embeddings(_model):
    return _model.encode(df['Course Info'].tolist(), convert_to_tensor=True)

# Load model and encode courses
model = load_model()
course_embeddings = get_course_embeddings(model)

# Define search function with improved processing
def search_courses(query, level_filter="All", rating_filter=0.0, threshold=0.5):
    """Search for the most relevant courses based on the user's query and filters."""
    try:
        # Expand abbreviations in the query
        query = expand_abbreviation(query)

        # Clean the expanded query text and compute its embedding
        query = clean_text(query)
        query_embedding = model.encode(query, convert_to_tensor=True)

        # Calculate similarity scores using cosine similarity
        similarities = util.pytorch_cos_sim(query_embedding, course_embeddings)[0]
        df['Score'] = similarities.cpu().numpy()

        # Apply threshold and filter by score
        sorted_df = df[df['Score'] > threshold].sort_values(by='Score', ascending=False)

        # Apply level filter if a specific level is selected, include 'Unknown' in the filtering process
        if level_filter != "All":
            sorted_df = sorted_df[(sorted_df['Level'] == level_filter) | (sorted_df['Level'] == "Unknown")]
        else:
            sorted_df = sorted_df[sorted_df['Level'] != "Unknown"]  # Exclude 'Unknown' if 'All' is selected

        # Apply rating filter
        sorted_df = sorted_df[sorted_df['Rating'] >= rating_filter]

        # Remove duplicates based on the course title and score
        sorted_df = sorted_df.drop_duplicates(subset=['Course Title'])

        # Select top results
        top_courses = sorted_df.head(5)[['Course Title', 'Duration', 'Course Link', 'Description', 'Rating', 'Level']]

        # Round the rating to 2 decimal places for cleaner display
        top_courses['Rating'] = top_courses['Rating'].round(2)

        # Prepare results for display
        results = [{
            "Title": row['Course Title'],
            "Duration": row['Duration'],
            "Link": row['Course Link'],
            "Description": row['Description'],
            "Rating": row['Rating'],
            "Level": row['Level']
        } for _, row in top_courses.iterrows()]

        return results
    except Exception as e:
        st.error(f"Error in search_courses function: {e}")
        return []

# Streamlit app interface
def run_app():
    st.title("Smart Course Search")
    st.write("Find the best free courses tailored to your search query!")

    # Input form for user query and filters
    query = st.text_input("Enter your search query:")
    level_filter = st.selectbox("Select Course Level", options=["All", "Beginner", "Intermediate", "Advanced"])
    rating_filter = st.slider("Minimum Rating", 0.0, 5.0, 0.0)

    if query:
        results = search_courses(query, level_filter, rating_filter)

        # Display results with enhanced formatting
        if results:
            st.subheader("Search Results:")
            for result in results:
                st.markdown(f"### {result['Title']}")
                st.write(
                    f"**Duration:** {result['Duration']} | **Rating:** {result['Rating']} ⭐ | **Level:** {result['Level']}")
                st.markdown(f"**Description:** {result['Description']}")
                st.markdown(f"[Course Link]({result['Link']})")
                st.write("---")
        else:
            st.write("No results found.")
    else:
        st.write("Please enter a query to search for courses.")

# Run the Streamlit app
if __name__ == "__main__":
    run_app()