Spaces:
Runtime error
Runtime error
| import pandas as pd | |
| import gradio as gr | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| from sklearn.preprocessing import MinMaxScaler | |
| import difflib | |
| import numpy as np | |
| import os | |
| import time | |
| # Load dataset with proper error handling | |
| def load_data(file_path='steam.csv', max_rows=10000): | |
| try: | |
| data = pd.read_csv(file_path, quotechar='"', on_bad_lines='skip', nrows=max_rows) | |
| print(f"Successfully loaded {len(data)} games from {file_path}") | |
| return data | |
| except Exception as e: | |
| print(f"Error loading data: {e}") | |
| # Return empty DataFrame with expected columns to avoid crashing | |
| return pd.DataFrame(columns=['name', 'genres', 'categories', 'steamspy_tags', 'platforms', 'positive_ratings', 'price']) | |
| # Load and preprocess data | |
| data = load_data() | |
| # Only proceed if we have data | |
| if len(data) > 0: | |
| # Handle missing values more carefully | |
| for feature in ['genres', 'categories', 'steamspy_tags', 'platforms', 'positive_ratings', 'price']: | |
| if feature not in data.columns: | |
| data[feature] = '' | |
| elif data[feature].dtype == object: # Only fill string columns with empty strings | |
| data[feature] = data[feature].fillna('') | |
| else: | |
| data[feature] = data[feature].fillna(0) # Fill numeric columns with 0 | |
| # Combine features - now including price | |
| data['combined_features'] = ( | |
| data['genres'].astype(str) + ' ' + | |
| data['categories'].astype(str) + ' ' + | |
| data['steamspy_tags'].astype(str) + ' ' + | |
| data['platforms'].astype(str) + ' ' + | |
| data['price'].astype(str) # Add price as a feature | |
| ) | |
| # Vectorize with error handling | |
| try: | |
| vectorizer = TfidfVectorizer(stop_words='english', ngram_range=(1, 2), max_features=8000) | |
| feature_vectors = vectorizer.fit_transform(data['combined_features']) | |
| print(f"Vectorization complete. Shape: {feature_vectors.shape}") | |
| except Exception as e: | |
| print(f"Vectorization error: {e}") | |
| # Create empty feature vectors to avoid crashing | |
| feature_vectors = np.zeros((len(data), 1)) | |
| # Normalize positive ratings safely | |
| if 'positive_ratings' in data.columns and len(data) > 0: | |
| scaler = MinMaxScaler() | |
| data['positive_ratings_scaled'] = scaler.fit_transform( | |
| data[['positive_ratings']].clip(lower=0) # Ensure no negative ratings | |
| ) | |
| else: | |
| data['positive_ratings_scaled'] = 0 | |
| # Compute similarity matrix (only if we have enough data) | |
| if feature_vectors.shape[0] > 1: | |
| try: | |
| # Use batched processing for large datasets to reduce memory usage | |
| if len(data) > 5000: | |
| print("Large dataset detected. Using batched similarity calculation.") | |
| batch_size = 1000 | |
| similarity_matrix = np.zeros((len(data), len(data))) | |
| for i in range(0, len(data), batch_size): | |
| end = min(i + batch_size, len(data)) | |
| batch = feature_vectors[i:end] | |
| similarity_matrix[i:end] = cosine_similarity(batch, feature_vectors) | |
| game_similarity = similarity_matrix | |
| else: | |
| game_similarity = cosine_similarity(feature_vectors) | |
| print(f"Similarity matrix created. Shape: {game_similarity.shape}") | |
| except Exception as e: | |
| print(f"Similarity calculation error: {e}") | |
| # Create identity matrix as fallback | |
| game_similarity = np.eye(len(data)) | |
| else: | |
| game_similarity = np.eye(len(data)) | |
| list_of_all_titles = data['name'].tolist() | |
| else: | |
| # Fallbacks for empty data | |
| feature_vectors = np.zeros((0, 0)) | |
| game_similarity = np.zeros((0, 0)) | |
| list_of_all_titles = [] | |
| # Cache for storing recommendation results to improve performance | |
| recommendation_cache = {} | |
| # Platform detection function with improved logic | |
| def detect_platforms(platforms_str): | |
| platforms_str = str(platforms_str).lower() | |
| os_icons = [] | |
| # More reliable platform detection | |
| if 'windows' in platforms_str: | |
| os_icons.append("🖥️ Windows") | |
| if any(mac_term in platforms_str for mac_term in ['mac', 'macos', 'osx']): | |
| os_icons.append("🍎 macOS") | |
| if 'linux' in platforms_str: | |
| os_icons.append("🐧 Linux") | |
| return os_icons if os_icons else ["❓ Unknown"] | |
| # Get game details for display | |
| def get_game_details(game_name): | |
| if not game_name or game_name not in list_of_all_titles: | |
| return "Game not found in database." | |
| try: | |
| game_data = data.loc[data['name'] == game_name].iloc[0] | |
| # Get genres and format them | |
| genres = str(game_data.get('genres', 'Unknown')) | |
| genres_list = [g.strip() for g in genres.split(';') if g.strip()] | |
| genres_display = ", ".join(genres_list) if genres_list else "Unknown" | |
| # Get price and format it | |
| price = game_data.get('price', 0) | |
| if isinstance(price, (int, float)): | |
| price_display = f"${price:.2f}" if price > 0 else "Free to Play" | |
| else: | |
| price_display = "Price unknown" | |
| # Get platforms | |
| platforms = game_data.get('platforms', '') | |
| os_list = detect_platforms(platforms) | |
| platforms_display = " | ".join(os_list) | |
| # Format the details | |
| details = f"## {game_name}\n\n" | |
| details += f"**Price:** {price_display}\n\n" | |
| details += f"**Genres:** {genres_display}\n\n" | |
| details += f"**Platforms:** {platforms_display}\n\n" | |
| # Add rating information if available | |
| if 'positive_ratings' in game_data: | |
| pos_ratings = int(game_data.get('positive_ratings', 0)) | |
| details += f"**Positive Ratings:** {pos_ratings:,}\n\n" | |
| if 'negative_ratings' in game_data: | |
| neg_ratings = int(game_data.get('negative_ratings', 0)) | |
| details += f"**Negative Ratings:** {neg_ratings:,}\n\n" | |
| # Calculate approval percentage if both values exist | |
| if pos_ratings + neg_ratings > 0: | |
| approval_percent = (pos_ratings / (pos_ratings + neg_ratings)) * 100 | |
| details += f"**Approval Rate:** {approval_percent:.1f}%\n\n" | |
| # Add release date if available | |
| if 'release_date' in game_data: | |
| release_date = game_data.get('release_date', 'Unknown') | |
| details += f"**Release Date:** {release_date}\n\n" | |
| # Add developer/publisher if available | |
| if 'developer' in game_data: | |
| developer = game_data.get('developer', 'Unknown') | |
| details += f"**Developer:** {developer}\n\n" | |
| if 'publisher' in game_data: | |
| publisher = game_data.get('publisher', 'Unknown') | |
| details += f"**Publisher:** {publisher}\n\n" | |
| return details | |
| except Exception as e: | |
| return f"Error retrieving game details: {str(e)}" | |
| # Generate a radar chart for game comparison | |
| def generate_game_comparison_chart(game_name): | |
| if not game_name or game_name not in list_of_all_titles: | |
| return None | |
| try: | |
| # Get the game index | |
| game_idx = data.loc[data['name'] == game_name].index[0] | |
| # Get top 3 similar games | |
| similarity_scores = list(enumerate(game_similarity[game_idx])) | |
| sorted_similar = sorted(similarity_scores, key=lambda x: x[1], reverse=True)[1:4] # Skip the first one (the game itself) | |
| similar_games = [data.iloc[idx]['name'] for idx, _ in sorted_similar] | |
| # Create feature vectors for radar chart (using genres as features) | |
| features = ['Action', 'Adventure', 'RPG', 'Strategy', 'Simulation', 'Sports', 'Racing'] | |
| chart_data = [] | |
| # Add main game | |
| main_game_data = data.iloc[game_idx] | |
| main_genres = str(main_game_data.get('genres', '')).split(';') | |
| main_values = [1 if genre in main_genres else 0.2 for genre in features] | |
| chart_data.append(go.Scatterpolar( | |
| r=main_values, | |
| theta=features, | |
| fill='toself', | |
| name=game_name | |
| )) | |
| # Add similar games | |
| for idx, score in sorted_similar: | |
| sim_game = data.iloc[idx] | |
| sim_genres = str(sim_game.get('genres', '')).split(';') | |
| sim_values = [1 if genre in sim_genres else 0.2 for genre in features] | |
| chart_data.append(go.Scatterpolar( | |
| r=sim_values, | |
| theta=features, | |
| fill='toself', | |
| name=sim_game['name'] | |
| )) | |
| fig = go.Figure(data=chart_data) | |
| fig.update_layout( | |
| polar=dict( | |
| radialaxis=dict( | |
| visible=True, | |
| range=[0, 1] | |
| ) | |
| ), | |
| showlegend=True, | |
| title=f"Genre Comparison: {game_name} vs Similar Games" | |
| ) | |
| return fig | |
| except Exception as e: | |
| print(f"Error generating comparison chart: {e}") | |
| return None | |
| # Recommend function with improved error handling and consistent return structure | |
| def recommend_games(user_game_name_input): | |
| # Check cache first | |
| if user_game_name_input in recommendation_cache: | |
| return recommendation_cache[user_game_name_input] | |
| if not user_game_name_input or not list_of_all_titles: | |
| return "Please enter a game name and ensure the dataset is loaded.", [] | |
| # Normalize input for better matching | |
| user_input_cleaned = user_game_name_input.strip().lower() | |
| # First try exact match | |
| exact_matches = [title for title in list_of_all_titles if title.lower() == user_input_cleaned] | |
| if exact_matches: | |
| closest_match = exact_matches[0] | |
| else: | |
| # Try fuzzy matching if no exact match | |
| find_close_match = difflib.get_close_matches(user_game_name_input, list_of_all_titles, n=1, cutoff=0.6) | |
| if not find_close_match: | |
| return f"No match found for '{user_game_name_input}'. Please try another game name.", [] | |
| closest_match = find_close_match[0] | |
| try: | |
| index_of_the_game = data.loc[data['name'] == closest_match].index[0] | |
| # Check for valid index | |
| if index_of_the_game >= len(game_similarity): | |
| return f"Found match '{closest_match}' but encountered an indexing error.", [] | |
| similarity_scores = list(enumerate(game_similarity[index_of_the_game])) | |
| # Sort by similarity and then by ratings as a tiebreaker | |
| sorted_similar_games = sorted( | |
| similarity_scores, | |
| key=lambda x: (x[1], data.iloc[x[0]]['positive_ratings_scaled']), | |
| reverse=True | |
| ) | |
| recommendations = [] | |
| game_list = [] | |
| # Add the searched game as the first entry | |
| recommendations.append(f"✓ You searched for: {closest_match}") | |
| game_list.append(closest_match) # Include the searched game in the list | |
| # Process recommendations | |
| for i, (index, score) in enumerate(sorted_similar_games[1:21]): | |
| if score < 0.2: # Higher threshold for better quality | |
| continue | |
| game_name = data.iloc[index]['name'] | |
| # Get platform info | |
| platforms = data.iloc[index].get('platforms', '') | |
| os_list = detect_platforms(platforms) | |
| os_display = " | ".join(os_list) | |
| # Get price info | |
| price = data.iloc[index].get('price', 0) | |
| price_display = f"${price:.2f}" if isinstance(price, (int, float)) and price > 0 else "Free" if price == 0 else "N/A" | |
| # Get genre info for additional context | |
| genres = str(data.iloc[index].get('genres', '')).split(';') | |
| genres_display = ", ".join(genres[:2]) if len(genres) > 0 and genres[0] else "" | |
| # Format recommendation with emoji and more details | |
| recommendation = f"{i+1}. {game_name} ({price_display}) - {score*100:.1f}% similar" | |
| if genres_display: | |
| recommendation += f" [{genres_display}]" | |
| recommendation += f" {os_display}" | |
| recommendations.append(recommendation) | |
| # Add to game list | |
| game_list.append(game_name) | |
| if len(recommendations) >= 11: # 10 recommendations + original search | |
| break | |
| result = ("\n".join(recommendations), game_list) | |
| recommendation_cache[user_game_name_input] = result # Cache the result | |
| return result | |
| except Exception as e: | |
| return f"Error while finding recommendations: {str(e)}", [] | |
| # Improved precision calculation | |
| def evaluate_precision(user_game_name_input): | |
| if not user_game_name_input or not list_of_all_titles: | |
| return 0.0 | |
| try: | |
| find_close_match = difflib.get_close_matches(user_game_name_input, list_of_all_titles, n=1, cutoff=0.6) | |
| if not find_close_match: | |
| return 0.0 | |
| closest_match = find_close_match[0] | |
| index_of_the_game = data.loc[data['name'] == closest_match].index[0] | |
| if index_of_the_game >= len(game_similarity): | |
| return 0.0 | |
| similarity_scores = list(enumerate(game_similarity[index_of_the_game])) | |
| sorted_similar_games = sorted( | |
| similarity_scores, | |
| key=lambda x: (x[1], data.iloc[x[0]]['positive_ratings_scaled']), | |
| reverse=True | |
| ) | |
| # Calculate precision based on genre overlap rather than exact match | |
| top_5_indices = [idx for idx, _ in sorted_similar_games[1:6]] | |
| original_genres = set(data.iloc[index_of_the_game]['genres'].split(';')) | |
| hits = 0 | |
| for idx in top_5_indices: | |
| rec_genres = set(data.iloc[idx]['genres'].split(';')) | |
| # Count as a hit if there's any genre overlap | |
| if original_genres.intersection(rec_genres): | |
| hits += 1 | |
| return round(hits / 5, 2) if top_5_indices else 0.0 | |
| except Exception as e: | |
| print(f"Precision calculation error: {str(e)}") | |
| return 0.0 | |
| # Function to generate price distribution chart | |
| def generate_price_chart(): | |
| try: | |
| # Filter for reasonable prices (exclude outliers) | |
| price_data = data[data['price'] < 100].copy() | |
| # Create price bins | |
| price_bins = [0, 5, 10, 15, 20, 30, 50, 100] | |
| price_data['price_category'] = pd.cut(price_data['price'], bins=price_bins, right=False) | |
| # Count games in each price bin | |
| price_counts = price_data['price_category'].value_counts().sort_index() | |
| # Create bar chart | |
| fig = px.bar( | |
| x=[str(cat) for cat in price_counts.index], | |
| y=price_counts.values, | |
| labels={'x': 'Price Range ($)', 'y': 'Number of Games'}, | |
| title='Price Distribution of Steam Games', | |
| color_discrete_sequence=['#1DB954'] # Steam-like green | |
| ) | |
| # Update layout | |
| fig.update_layout( | |
| xaxis_title='Price Range ($)', | |
| yaxis_title='Number of Games', | |
| template='plotly_white' | |
| ) | |
| return fig | |
| except Exception as e: | |
| print(f"Error generating price chart: {e}") | |
| return None | |
| # Combined function with progress updates | |
| def recommend_and_visualize(user_input): | |
| if not user_input or user_input.strip() == "": | |
| return "Please enter a game name", [] | |
| # Get recommendations | |
| recommendations, game_list = recommend_games(user_input) | |
| # Calculate precision | |
| precision = evaluate_precision(user_input) | |
| # Add platform legend and precision info | |
| footer = "\n\n📊 **Recommendation Quality**: " | |
| footer += f"Precision@5: {precision*100:.0f}%" if precision > 0 else "Unable to calculate precision" | |
| footer += "\n\n**Platform Legend**:\n" | |
| footer += "🖥️ Windows | 🍎 macOS | 🐧 Linux | ❓ Unknown" | |
| return recommendations + footer, game_list | |
| # Get details for selected game | |
| def display_game_details(game_name): | |
| if not game_name: | |
| return "Please select a game to view details." | |
| return get_game_details(game_name) | |
| # Function to create genre distribution chart | |
| def create_genre_chart(): | |
| try: | |
| # Extract all genres | |
| all_genres = [] | |
| for genres in data['genres'].dropna(): | |
| all_genres.extend([g.strip() for g in str(genres).split(';') if g.strip()]) | |
| # Get counts | |
| genre_counts = pd.Series(all_genres).value_counts().nlargest(10) | |
| # Create bar chart | |
| fig = px.bar( | |
| x=genre_counts.index, | |
| y=genre_counts.values, | |
| labels={'x': 'Genre', 'y': 'Number of Games'}, | |
| title='Top 10 Game Genres on Steam', | |
| color_discrete_sequence=['#66c0f4'] # Steam blue | |
| ) | |
| fig.update_layout( | |
| xaxis_title='Genre', | |
| yaxis_title='Number of Games', | |
| template='plotly_white' | |
| ) | |
| return fig | |
| except Exception as e: | |
| print(f"Error creating genre chart: {e}") | |
| return None | |
| # Improved Gradio UI with added features | |
| with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
| gr.Markdown("# 🎮 Steam Game Recommender") | |
| gr.Markdown("Enter the name of a game you like and get recommendations based on similarity!") | |
| with gr.Tab("Find Recommendations"): | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| input_box = gr.Textbox( | |
| label="Your Favorite Game", | |
| placeholder="e.g., Portal 2, Half-Life 2, Skyrim", | |
| info="Type a game name that exists in the Steam dataset" | |
| ) | |
| with gr.Column(scale=1): | |
| run_button = gr.Button("Find Recommendations", variant="primary") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| output_text = gr.Textbox( | |
| label="Recommendations", | |
| lines=15, | |
| interactive=False | |
| ) | |
| dropdown = gr.Dropdown( | |
| label="Select a Game to View Details", | |
| choices=[], | |
| interactive=True, | |
| info="Choose a game to see its details" | |
| ) | |
| with gr.Column(scale=1): | |
| game_details = gr.Markdown( | |
| label="Game Details", | |
| value="Select a game from the dropdown to view details." | |
| ) | |
| with gr.Tab("Statistics"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("## Game Price Distribution") | |
| price_chart = gr.Plot(value=generate_price_chart()) | |
| with gr.Column(): | |
| gr.Markdown("## Top Game Genres") | |
| genre_chart = gr.Plot(value=create_genre_chart()) | |
| with gr.Row(): | |
| refresh_stats_button = gr.Button("Refresh Statistics") | |
| # Add a tab for help/about | |
| with gr.Tab("About"): | |
| gr.Markdown(""" | |
| ## About This Recommender | |
| This Steam game recommender uses **TF-IDF vectorization** and **cosine similarity** to find games similar to your favorites. The recommendation engine analyzes: | |
| - Game genres | |
| - Categories | |
| - User-defined tags | |
| - Platforms | |
| - Price points | |
| The system then ranks games by similarity score and refines results using positive user ratings. | |
| ### How to Use | |
| 1. Enter the name of a game you enjoy in the search box | |
| 2. Click "Find Recommendations" to see similar games | |
| 3. Select any game from the dropdown to view detailed information | |
| 4. Explore the Statistics tab to see distributions of game prices and genres | |
| ### Dataset | |
| This system uses a dataset of Steam games with features like: | |
| - Game title | |
| - Genres | |
| - Categories | |
| - User tags | |
| - Price | |
| - Platform compatibility | |
| - User ratings | |
| ### Limitations | |
| - Recommendations depend on data quality and completeness | |
| - The system works best with popular titles that have detailed metadata | |
| - Very niche or new games may have fewer accurate recommendations | |
| """) | |
| # Add a search history tab | |
| with gr.Tab("Search History"): | |
| search_history = gr.Dataframe( | |
| headers=["Time", "Search Query", "Top Recommendation"], | |
| datatype=["str", "str", "str"], | |
| row_count=10, | |
| col_count=(3, "fixed"), | |
| value=[] | |
| ) | |
| clear_history_button = gr.Button("Clear History") | |
| # Register events | |
| search_history_data = [] | |
| def update_search_history(user_input): | |
| if not user_input or user_input.strip() == "": | |
| return search_history_data | |
| recommendations, game_list = recommend_games(user_input) | |
| # Format timestamp | |
| timestamp = time.strftime("%Y-%m-%d %H:%M:%S") | |
| # Get top recommendation (if any) | |
| top_rec = game_list[1] if len(game_list) > 1 else "No recommendation found" | |
| # Add to history | |
| search_history_data.append([timestamp, user_input, top_rec]) | |
| # Keep only the most recent 10 entries | |
| return search_history_data[-10:] | |
| def clear_history(): | |
| search_history_data.clear() | |
| return [] | |
| # Combined function to update recommendations and history | |
| def recommend_and_update_history(user_input): | |
| rec_text, game_list = recommend_and_visualize(user_input) | |
| history = update_search_history(user_input) | |
| return rec_text, game_list, history | |
| run_button.click( | |
| fn=recommend_and_update_history, | |
| inputs=input_box, | |
| outputs=[output_text, dropdown, search_history], | |
| show_progress=True | |
| ) | |
| # Also trigger on Enter key | |
| input_box.submit( | |
| fn=recommend_and_update_history, | |
| inputs=input_box, | |
| outputs=[output_text, dropdown, search_history], | |
| show_progress=True | |
| ) | |
| # Display game details when a game is selected | |
| dropdown.change( | |
| fn=display_game_details, | |
| inputs=dropdown, | |
| outputs=game_details | |
| ) | |
| # Clear history button | |
| clear_history_button.click( | |
| fn=clear_history, | |
| inputs=[], | |
| outputs=[search_history] | |
| ) | |
| # Refresh statistics | |
| refresh_stats_button.click( | |
| fn=lambda: (generate_price_chart(), create_genre_chart()), | |
| inputs=[], | |
| outputs=[price_chart, genre_chart] | |
| ) | |
| # Launch the Gradio app | |
| if __name__ == "__main__": | |
| demo.launch() |