Bnava13 commited on
Commit
d37e92f
ยท
verified ยท
1 Parent(s): abaa7f5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +334 -130
app.py CHANGED
@@ -5,163 +5,367 @@ from sklearn.feature_extraction.text import TfidfVectorizer
5
  from sklearn.metrics.pairwise import cosine_similarity
6
  from sklearn.preprocessing import MinMaxScaler
7
  import difflib
 
8
 
9
- # Load dataset
10
- data = pd.read_csv('steam.csv', quotechar='"', on_bad_lines='skip', nrows=10000)
11
- data.fillna('', inplace=True)
 
 
 
 
 
 
 
12
 
13
- # Combine features
14
- selected_features = ['genres', 'categories', 'steamspy_tags', 'platforms']
15
- for feature in selected_features:
16
- if feature not in data.columns:
17
- data[feature] = ''
18
- data['combined_features'] = data['genres'] + ' ' + data['categories'] + ' ' + data['steamspy_tags'] + ' ' + data['platforms']
19
 
20
- # Vectorize
21
- vectorizer = TfidfVectorizer(stop_words='english', ngram_range=(1, 2), max_features=8000)
22
- feature_vectors = vectorizer.fit_transform(data['combined_features'])
23
-
24
- # Normalize positive ratings
25
- scaler = MinMaxScaler()
26
- data['positive_ratings_scaled'] = scaler.fit_transform(data[['positive_ratings']])
27
-
28
- # Similarity matrix
29
- game_similarity = cosine_similarity(feature_vectors)
30
- list_of_all_titles = data['name'].tolist()
31
-
32
- # Recommend function
33
- def recommend_games(user_game_name_input):
34
- find_close_match = difflib.get_close_matches(user_game_name_input, list_of_all_titles, n=1)
35
- if not find_close_match:
36
- return "No close match found. Please try another game name.", None, [], {}
37
-
38
- closest_match = find_close_match[0]
39
- index_of_the_game = data.loc[data['name'] == closest_match].index[0]
40
-
41
- similarity_scores = list(enumerate(game_similarity[index_of_the_game]))
42
- sorted_similar_games = sorted(
43
- similarity_scores,
44
- key=lambda x: (x[1], data.iloc[x[0]]['positive_ratings_scaled']),
45
- reverse=True
46
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- recommendations = []
49
- chart_data = []
50
- radar_data = {}
51
- for i, (index, score) in enumerate(sorted_similar_games[1:21]):
52
- if score < 0.3:
53
- continue
54
- game_name = data.iloc[index]['name']
55
-
56
- # Get OS support info
57
- platforms = data.iloc[index].get('platforms', '').lower()
58
- os_icons = []
59
- if 'windows' in platforms:
60
- os_icons.append("๐ŸชŸ")
61
- if 'mac' in platforms or 'macos' in platforms:
62
- os_icons.append("๐ŸŽ")
63
- if 'linux' in platforms or 'steam' in platforms:
64
- os_icons.append("๐Ÿง")
65
-
66
- os_display = ' '.join(os_icons) if os_icons else "โ“"
67
-
68
- recommendations.append(f"{i+1}. {game_name} {os_display} (Similarity: {score*100:.1f}%)")
69
- chart_data.append({'Game': game_name, 'Similarity': score * 100})
70
- radar_data[game_name] = {
71
- "genres": data.iloc[index]['genres'],
72
- "categories": data.iloc[index]['categories']
73
- }
74
-
75
- if len(recommendations) >= 10:
76
- break
77
 
78
- chart_df = pd.DataFrame(chart_data)
79
- return "\n".join(recommendations), chart_df, list(radar_data.keys()), radar_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- # Precision@5
82
  def evaluate_precision(user_game_name_input):
83
- find_close_match = difflib.get_close_matches(user_game_name_input, list_of_all_titles, n=1)
84
- if not find_close_match:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  return 0.0
86
- closest_match = find_close_match[0]
87
- index_of_the_game = data.loc[data['name'] == closest_match].index[0]
88
- similarity_scores = list(enumerate(game_similarity[index_of_the_game]))
89
- sorted_similar_games = sorted(
90
- similarity_scores,
91
- key=lambda x: (x[1], data.iloc[x[0]]['positive_ratings_scaled']),
92
- reverse=True
93
- )
94
- top_5 = [data.iloc[idx]['genres'] for idx, _ in sorted_similar_games[1:6]]
95
- original_genre = data.iloc[index_of_the_game]['genres']
96
- hits = sum(1 for genre in top_5 if genre == original_genre)
97
- return round(hits / 5, 2)
98
 
99
- # Radar plot function
100
  def plot_game_features(game_name, radar_data):
101
- if game_name not in radar_data:
102
  return None
103
 
104
- genres = radar_data[game_name]["genres"].split(';')
105
- categories = radar_data[game_name]["categories"].split(';')
106
- features = list(set([g.strip() for g in genres + categories if g.strip()]))
107
-
108
- if not features:
109
- return None
 
 
 
 
 
 
 
 
110
 
111
- values = [1] * len(features)
112
- radar_df = pd.DataFrame(dict(
113
- Feature=features,
114
- Presence=values
115
- ))
116
 
117
- fig = px.line_polar(radar_df, r='Presence', theta='Feature', line_close=True,
118
- title=f"Feature Radar: {game_name}", range_r=[0, 1])
119
- fig.update_traces(fill='toself')
120
- return fig
 
 
 
 
121
 
122
- # Combined Gradio function
123
  def recommend_and_visualize(user_input):
 
 
 
 
124
  recommendations, chart_df, game_names, radar_data = recommend_games(user_input)
 
 
125
  precision = evaluate_precision(user_input)
 
 
126
  chart = None
127
-
128
  if chart_df is not None and not chart_df.empty:
129
- chart = px.bar(chart_df, x="Game", y="Similarity", title="Top Game Recommendations",
130
- labels={"Similarity": "Similarity (%)"}, height=400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- legend = "\n\n๐Ÿ“Œ **Platform Legend**\n๐ŸชŸ = Windows\n๐ŸŽ = macOS\n๐Ÿง = Linux\nโ“ = Unknown"
133
- return recommendations + f"\n\nPrecision@5 (approx): {precision}" + legend, chart, game_names, radar_data
134
 
135
- # Trigger radar chart
136
  def show_selected_game_radar(game_name, radar_data):
 
 
137
  return plot_game_features(game_name, radar_data)
138
 
139
- # Gradio UI
140
- with gr.Blocks() as demo:
141
- gr.Markdown("## ๐ŸŽฎ Steam Game Recommender")
142
  gr.Markdown("Enter the name of a game you like and get recommendations based on similarity!")
143
-
144
- with gr.Row():
145
- input_box = gr.Textbox(label="Your Favorite Game", placeholder="e.g., Portal 2")
146
-
147
- with gr.Row():
148
- output_text = gr.Textbox(label="Recommendations", lines=12, interactive=False)
149
- output_chart = gr.Plot(label="Recommendation Chart")
150
-
151
- run_button = gr.Button("Recommend")
152
-
153
  with gr.Row():
154
- dropdown = gr.Dropdown(label="Inspect a Recommended Game", choices=[], interactive=True)
155
- radar_chart = gr.Plot(label="Genre/Category Radar")
156
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  radar_data_state = gr.State()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- run_button.click(fn=recommend_and_visualize,
160
- inputs=input_box,
161
- outputs=[output_text, output_chart, dropdown, radar_data_state])
162
-
163
- dropdown.change(fn=show_selected_game_radar,
164
- inputs=[dropdown, radar_data_state],
165
- outputs=radar_chart)
166
-
167
- demo.launch()
 
5
  from sklearn.metrics.pairwise import cosine_similarity
6
  from sklearn.preprocessing import MinMaxScaler
7
  import difflib
8
+ import numpy as np
9
 
10
+ # Load dataset with proper error handling
11
+ def load_data(file_path='steam.csv', max_rows=10000):
12
+ try:
13
+ data = pd.read_csv(file_path, quotechar='"', on_bad_lines='skip', nrows=max_rows)
14
+ print(f"Successfully loaded {len(data)} games from {file_path}")
15
+ return data
16
+ except Exception as e:
17
+ print(f"Error loading data: {e}")
18
+ # Return empty DataFrame with expected columns to avoid crashing
19
+ return pd.DataFrame(columns=['name', 'genres', 'categories', 'steamspy_tags', 'platforms', 'positive_ratings'])
20
 
21
+ # Load and preprocess data
22
+ data = load_data()
 
 
 
 
23
 
24
+ # Only proceed if we have data
25
+ if len(data) > 0:
26
+ # Handle missing values more carefully
27
+ for feature in ['genres', 'categories', 'steamspy_tags', 'platforms', 'positive_ratings']:
28
+ if feature not in data.columns:
29
+ data[feature] = ''
30
+ elif data[feature].dtype == object: # Only fill string columns with empty strings
31
+ data[feature] = data[feature].fillna('')
32
+ else:
33
+ data[feature] = data[feature].fillna(0) # Fill numeric columns with 0
34
+
35
+ # Combine features
36
+ data['combined_features'] = (
37
+ data['genres'].astype(str) + ' ' +
38
+ data['categories'].astype(str) + ' ' +
39
+ data['steamspy_tags'].astype(str) + ' ' +
40
+ data['platforms'].astype(str)
 
 
 
 
 
 
 
 
 
41
  )
42
+
43
+ # Vectorize with error handling
44
+ try:
45
+ vectorizer = TfidfVectorizer(stop_words='english', ngram_range=(1, 2), max_features=8000)
46
+ feature_vectors = vectorizer.fit_transform(data['combined_features'])
47
+ print(f"Vectorization complete. Shape: {feature_vectors.shape}")
48
+ except Exception as e:
49
+ print(f"Vectorization error: {e}")
50
+ # Create empty feature vectors to avoid crashing
51
+ feature_vectors = np.zeros((len(data), 1))
52
+
53
+ # Normalize positive ratings safely
54
+ if 'positive_ratings' in data.columns and len(data) > 0:
55
+ scaler = MinMaxScaler()
56
+ data['positive_ratings_scaled'] = scaler.fit_transform(
57
+ data[['positive_ratings']].clip(lower=0) # Ensure no negative ratings
58
+ )
59
+ else:
60
+ data['positive_ratings_scaled'] = 0
61
+
62
+ # Compute similarity matrix (only if we have enough data)
63
+ if feature_vectors.shape[0] > 1:
64
+ try:
65
+ # Use batched processing for large datasets to reduce memory usage
66
+ if len(data) > 5000:
67
+ print("Large dataset detected. Using batched similarity calculation.")
68
+ batch_size = 1000
69
+ similarity_matrix = np.zeros((len(data), len(data)))
70
+
71
+ for i in range(0, len(data), batch_size):
72
+ end = min(i + batch_size, len(data))
73
+ batch = feature_vectors[i:end]
74
+ similarity_matrix[i:end] = cosine_similarity(batch, feature_vectors)
75
+
76
+ game_similarity = similarity_matrix
77
+ else:
78
+ game_similarity = cosine_similarity(feature_vectors)
79
+
80
+ print(f"Similarity matrix created. Shape: {game_similarity.shape}")
81
+ except Exception as e:
82
+ print(f"Similarity calculation error: {e}")
83
+ # Create identity matrix as fallback
84
+ game_similarity = np.eye(len(data))
85
+ else:
86
+ game_similarity = np.eye(len(data))
87
+
88
+ list_of_all_titles = data['name'].tolist()
89
+ else:
90
+ # Fallbacks for empty data
91
+ feature_vectors = np.zeros((0, 0))
92
+ game_similarity = np.zeros((0, 0))
93
+ list_of_all_titles = []
94
 
95
+ # Platform detection function with improved logic
96
+ def detect_platforms(platforms_str):
97
+ platforms_str = str(platforms_str).lower()
98
+ os_icons = []
99
+
100
+ # More reliable platform detection
101
+ if 'windows' in platforms_str:
102
+ os_icons.append("๐Ÿ–ฅ๏ธ Windows")
103
+ if any(mac_term in platforms_str for mac_term in ['mac', 'macos', 'osx']):
104
+ os_icons.append("๐ŸŽ macOS")
105
+ if 'linux' in platforms_str:
106
+ os_icons.append("๐Ÿง Linux")
107
+
108
+ return os_icons if os_icons else ["โ“ Unknown"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ # Recommend function with improved error handling and consistent return structure
111
+ def recommend_games(user_game_name_input):
112
+ if not user_game_name_input or not list_of_all_titles:
113
+ return "Please enter a game name and ensure the dataset is loaded.", None, [], {}
114
+
115
+ # Normalize input for better matching
116
+ user_input_cleaned = user_game_name_input.strip().lower()
117
+
118
+ # First try exact match
119
+ exact_matches = [title for title in list_of_all_titles if title.lower() == user_input_cleaned]
120
+
121
+ if exact_matches:
122
+ closest_match = exact_matches[0]
123
+ else:
124
+ # Try fuzzy matching if no exact match
125
+ find_close_match = difflib.get_close_matches(user_game_name_input, list_of_all_titles, n=1, cutoff=0.6)
126
+ if not find_close_match:
127
+ return f"No match found for '{user_game_name_input}'. Please try another game name.", None, [], {}
128
+ closest_match = find_close_match[0]
129
+
130
+ try:
131
+ index_of_the_game = data.loc[data['name'] == closest_match].index[0]
132
+
133
+ # Check for valid index
134
+ if index_of_the_game >= len(game_similarity):
135
+ return f"Found match '{closest_match}' but encountered an indexing error.", None, [], {}
136
+
137
+ similarity_scores = list(enumerate(game_similarity[index_of_the_game]))
138
+
139
+ # Sort by similarity and then by ratings as a tiebreaker
140
+ sorted_similar_games = sorted(
141
+ similarity_scores,
142
+ key=lambda x: (x[1], data.iloc[x[0]]['positive_ratings_scaled']),
143
+ reverse=True
144
+ )
145
+
146
+ recommendations = []
147
+ chart_data = []
148
+ radar_data = {}
149
+
150
+ # Add the searched game as the first entry
151
+ recommendations.append(f"โœ“ You searched for: {closest_match}")
152
+
153
+ # Process recommendations
154
+ for i, (index, score) in enumerate(sorted_similar_games[1:21]):
155
+ if score < 0.2: # Higher threshold for better quality
156
+ continue
157
+
158
+ game_name = data.iloc[index]['name']
159
+
160
+ # Get platform info
161
+ platforms = data.iloc[index].get('platforms', '')
162
+ os_list = detect_platforms(platforms)
163
+ os_display = " | ".join(os_list)
164
+
165
+ # Format recommendation with emoji
166
+ recommendations.append(f"{i+1}. {game_name} ({os_display}) - {score*100:.1f}% similar")
167
+
168
+ # Store data for charts
169
+ chart_data.append({'Game': game_name, 'Similarity': score * 100})
170
+
171
+ # Store data for radar chart
172
+ genres = data.iloc[index].get('genres', '')
173
+ categories = data.iloc[index].get('categories', '')
174
+ radar_data[game_name] = {
175
+ "genres": genres,
176
+ "categories": categories
177
+ }
178
+
179
+ if len(recommendations) >= 11: # 10 recommendations + original search
180
+ break
181
+
182
+ chart_df = pd.DataFrame(chart_data) if chart_data else None
183
+ return "\n".join(recommendations), chart_df, list(radar_data.keys()), radar_data
184
+
185
+ except Exception as e:
186
+ return f"Error while finding recommendations: {str(e)}", None, [], {}
187
 
188
+ # Improved precision calculation
189
  def evaluate_precision(user_game_name_input):
190
+ if not user_game_name_input or not list_of_all_titles:
191
+ return 0.0
192
+
193
+ try:
194
+ find_close_match = difflib.get_close_matches(user_game_name_input, list_of_all_titles, n=1, cutoff=0.6)
195
+ if not find_close_match:
196
+ return 0.0
197
+
198
+ closest_match = find_close_match[0]
199
+ index_of_the_game = data.loc[data['name'] == closest_match].index[0]
200
+
201
+ if index_of_the_game >= len(game_similarity):
202
+ return 0.0
203
+
204
+ similarity_scores = list(enumerate(game_similarity[index_of_the_game]))
205
+ sorted_similar_games = sorted(
206
+ similarity_scores,
207
+ key=lambda x: (x[1], data.iloc[x[0]]['positive_ratings_scaled']),
208
+ reverse=True
209
+ )
210
+
211
+ # Calculate precision based on genre overlap rather than exact match
212
+ top_5_indices = [idx for idx, _ in sorted_similar_games[1:6]]
213
+ original_genres = set(data.iloc[index_of_the_game]['genres'].split(';'))
214
+
215
+ hits = 0
216
+ for idx in top_5_indices:
217
+ rec_genres = set(data.iloc[idx]['genres'].split(';'))
218
+ # Count as a hit if there's any genre overlap
219
+ if original_genres.intersection(rec_genres):
220
+ hits += 1
221
+
222
+ return round(hits / 5, 2) if top_5_indices else 0.0
223
+
224
+ except Exception as e:
225
+ print(f"Precision calculation error: {str(e)}")
226
  return 0.0
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
+ # Improved radar plot function
229
  def plot_game_features(game_name, radar_data):
230
+ if not game_name or game_name not in radar_data:
231
  return None
232
 
233
+ try:
234
+ # Extract features
235
+ genres = str(radar_data[game_name]["genres"]).split(';') if radar_data[game_name]["genres"] else []
236
+ categories = str(radar_data[game_name]["categories"]).split(';') if radar_data[game_name]["categories"] else []
237
+
238
+ # Clean and deduplicate features
239
+ features = list(set([g.strip() for g in genres + categories if g.strip()]))
240
+
241
+ # Limit to top features for readability
242
+ if len(features) > 10:
243
+ features = features[:10]
244
+
245
+ if not features:
246
+ return None
247
 
248
+ values = [1] * len(features)
249
+ radar_df = pd.DataFrame({
250
+ 'Feature': features,
251
+ 'Presence': values
252
+ })
253
 
254
+ fig = px.line_polar(radar_df, r='Presence', theta='Feature', line_close=True,
255
+ title=f"Feature Radar: {game_name}", range_r=[0, 1])
256
+ fig.update_traces(fill='toself')
257
+ return fig
258
+
259
+ except Exception as e:
260
+ print(f"Radar chart error: {str(e)}")
261
+ return None
262
 
263
+ # Combined function with progress updates
264
  def recommend_and_visualize(user_input):
265
+ if not user_input or user_input.strip() == "":
266
+ return "Please enter a game name", None, [], {}
267
+
268
+ # Get recommendations
269
  recommendations, chart_df, game_names, radar_data = recommend_games(user_input)
270
+
271
+ # Calculate precision
272
  precision = evaluate_precision(user_input)
273
+
274
+ # Create chart if data is available
275
  chart = None
 
276
  if chart_df is not None and not chart_df.empty:
277
+ try:
278
+ chart = px.bar(chart_df, x="Game", y="Similarity",
279
+ title="Top Game Recommendations",
280
+ labels={"Similarity": "Similarity (%)"},
281
+ height=400)
282
+ # Improve readability of labels
283
+ chart.update_layout(xaxis_tickangle=-45)
284
+ except Exception as e:
285
+ print(f"Chart creation error: {str(e)}")
286
+
287
+ # Add platform legend and precision info
288
+ footer = "\n\n๐Ÿ“Š **Recommendation Quality**: "
289
+ footer += f"Precision@5: {precision*100:.0f}%" if precision > 0 else "Unable to calculate precision"
290
+
291
+ footer += "\n\n**Platform Legend**:\n"
292
+ footer += "๐Ÿ–ฅ๏ธ Windows | ๐ŸŽ macOS | ๐Ÿง Linux | โ“ Unknown"
293
 
294
+ return recommendations + footer, chart, game_names, radar_data
 
295
 
296
+ # Trigger radar chart display
297
  def show_selected_game_radar(game_name, radar_data):
298
+ if not game_name:
299
+ return None
300
  return plot_game_features(game_name, radar_data)
301
 
302
+ # Improved Gradio UI
303
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
304
+ gr.Markdown("# ๐ŸŽฎ Steam Game Recommender")
305
  gr.Markdown("Enter the name of a game you like and get recommendations based on similarity!")
306
+
 
 
 
 
 
 
 
 
 
307
  with gr.Row():
308
+ with gr.Column(scale=4):
309
+ input_box = gr.Textbox(
310
+ label="Your Favorite Game",
311
+ placeholder="e.g., Portal 2, Half-Life 2, Skyrim",
312
+ info="Type a game name that exists in the Steam dataset"
313
+ )
314
+ with gr.Column(scale=1):
315
+ run_button = gr.Button("Find Recommendations", variant="primary")
316
+
317
+ with gr.Accordion("Status", open=False):
318
+ status_text = gr.Markdown(f"Dataset loaded: {len(data)} games")
319
+
320
+ with gr.Tabs():
321
+ with gr.TabItem("Recommendations"):
322
+ with gr.Row():
323
+ with gr.Column(scale=2):
324
+ output_text = gr.Textbox(
325
+ label="Recommendations",
326
+ lines=15,
327
+ interactive=False
328
+ )
329
+ with gr.Column(scale=3):
330
+ output_chart = gr.Plot(label="Similarity Chart")
331
+
332
+ with gr.TabItem("Game Features"):
333
+ with gr.Row():
334
+ with gr.Column(scale=1):
335
+ dropdown = gr.Dropdown(
336
+ label="Select a Recommended Game to Analyze",
337
+ choices=[],
338
+ interactive=True,
339
+ info="Choose a game to see its features"
340
+ )
341
+ with gr.Column(scale=3):
342
+ radar_chart = gr.Plot(label="Genre & Category Radar")
343
+
344
+ # Store radar data between function calls
345
  radar_data_state = gr.State()
346
+
347
+ # Register events
348
+ run_button.click(
349
+ fn=recommend_and_visualize,
350
+ inputs=input_box,
351
+ outputs=[output_text, output_chart, dropdown, radar_data_state],
352
+ show_progress=True
353
+ )
354
+
355
+ # Also trigger on Enter key
356
+ input_box.submit(
357
+ fn=recommend_and_visualize,
358
+ inputs=input_box,
359
+ outputs=[output_text, output_chart, dropdown, radar_data_state],
360
+ show_progress=True
361
+ )
362
+
363
+ dropdown.change(
364
+ fn=show_selected_game_radar,
365
+ inputs=[dropdown, radar_data_state],
366
+ outputs=radar_chart
367
+ )
368
 
369
+ # Launch the app
370
+ if __name__ == "__main__":
371
+ demo.launch()