Bnava13's picture
Update app.py
aa642f2 verified
Raw
History Blame
23.5 kB
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()