mtoft20 commited on
Commit
927aefb
Β·
verified Β·
1 Parent(s): 933f085

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +135 -46
src/streamlit_app.py CHANGED
@@ -251,17 +251,29 @@ def extract_unique_names(content_list, field):
251
  unique_names.add(cleaned_name)
252
  return sorted(list(unique_names))
253
 
254
- def prepare_movie_features(content_list):
255
- """Prepare movie features for clustering"""
 
256
  # Convert to DataFrame for easier processing
257
- df = pd.DataFrame(content_list)
258
 
259
- # Create TF-IDF features from genres and description
260
- tfidf = TfidfVectorizer(stop_words='english')
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
- # Combine genres and description for text features
263
- df['text_features'] = df['listed_in'].fillna('') + ' ' + df['description'].fillna('')
264
- text_features = tfidf.fit_transform(df['text_features']).toarray()
265
 
266
  # Prepare numerical features
267
  df['release_year'] = pd.to_numeric(df['release_year'], errors='coerce').fillna(0)
@@ -281,37 +293,87 @@ def prepare_movie_features(content_list):
281
  scaler = StandardScaler()
282
  numerical_features = scaler.fit_transform(df[['release_year', 'duration_minutes']])
283
 
284
- # Combine all features
285
- all_features = np.hstack([text_features, numerical_features])
286
-
287
- return all_features, df
288
-
289
- def find_similar_content(content_list, selected_content, n_clusters=20, n_recommendations=5):
290
- """Find similar content using k-means clustering"""
291
- # Prepare features
292
- features, df = prepare_movie_features(content_list)
293
-
294
- # Perform k-means clustering
295
- kmeans = KMeans(n_clusters=n_clusters, random_state=42)
296
- cluster_labels = kmeans.fit_predict(features)
297
-
298
- # Find the cluster of the selected content
299
- selected_idx = df[df['title'] == selected_content['title']].index[0]
300
- selected_cluster = cluster_labels[selected_idx]
301
 
302
- # Get indices of content in the same cluster
303
- cluster_indices = np.where(cluster_labels == selected_cluster)[0]
 
 
 
304
 
305
- # Calculate distances to all points in the same cluster
306
- cluster_features = features[cluster_indices]
307
- selected_features = features[selected_idx].reshape(1, -1)
308
- distances = np.linalg.norm(cluster_features - selected_features, axis=1)
309
-
310
- # Get the indices of the most similar content (excluding the selected content)
311
- similar_indices = cluster_indices[np.argsort(distances)][1:n_recommendations+1]
312
-
313
- # Return the similar content
314
- return [content_list[idx] for idx in similar_indices]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
  # =============================================================================
317
  # MAIN APP
@@ -564,18 +626,45 @@ def main():
564
  st.write(f"**πŸ“– Description:**")
565
  st.write(content.get('description', 'N/A'))
566
 
567
- # Add Find Similar button
568
- if st.button(f"πŸ” Find Similar Content", key=f"similar_{i}"):
 
569
  with st.spinner("Finding similar content..."):
570
- similar_content = find_similar_content(all_content, content)
571
 
572
  if similar_content:
573
- st.write("**πŸ‘₯ Similar Content You Might Like:**")
574
- for sim_content in similar_content:
575
- with st.container():
576
- st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
577
- st.write(f"*Genres:* {sim_content.get('listed_in')}")
578
- st.write("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  else:
580
  st.info("No similar content found.")
581
 
 
251
  unique_names.add(cleaned_name)
252
  return sorted(list(unique_names))
253
 
254
+ @st.cache_data(ttl=3600) # Cache feature vectors for 1 hour
255
+ def prepare_content_features():
256
+ """Prepare and cache content features for similarity matching"""
257
  # Convert to DataFrame for easier processing
258
+ df = pd.DataFrame(all_content)
259
 
260
+ # Create TF-IDF vectorizer for text features
261
+ tfidf = TfidfVectorizer(
262
+ stop_words='english',
263
+ max_features=1000, # Limit features to most important ones
264
+ ngram_range=(1, 2) # Include bigrams for better context
265
+ )
266
+
267
+ # Combine relevant text fields with weights
268
+ df['text_features'] = (
269
+ df['listed_in'].fillna('') + ' ' + # Genres
270
+ df['description'].fillna('') + ' ' + # Plot
271
+ df['cast'].fillna('').apply(lambda x: ' '.join(x.split(',')[:3])) + ' ' + # Top 3 cast members
272
+ df['director'].fillna('') # Director
273
+ )
274
 
275
+ # Get text features
276
+ text_features = tfidf.fit_transform(df['text_features'])
 
277
 
278
  # Prepare numerical features
279
  df['release_year'] = pd.to_numeric(df['release_year'], errors='coerce').fillna(0)
 
293
  scaler = StandardScaler()
294
  numerical_features = scaler.fit_transform(df[['release_year', 'duration_minutes']])
295
 
296
+ # Convert sparse matrix to dense for easier calculations
297
+ text_features_dense = text_features.toarray()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
+ # Combine features with appropriate weights
300
+ combined_features = np.hstack([
301
+ text_features_dense * 0.8, # 80% weight to text features
302
+ numerical_features * 0.2 # 20% weight to numerical features
303
+ ])
304
 
305
+ return combined_features, df
306
+
307
+ @st.cache_resource # Cache the KMeans model
308
+ def get_kmeans_model(n_clusters=50):
309
+ """Get or create cached KMeans model"""
310
+ return KMeans(
311
+ n_clusters=n_clusters,
312
+ random_state=42,
313
+ n_init=10, # Number of times to run with different centroid seeds
314
+ )
315
+
316
+ @st.cache_data(ttl=3600) # Cache cluster assignments for 1 hour
317
+ def get_content_clusters(features, n_clusters=50):
318
+ """Get cluster assignments for all content"""
319
+ kmeans = get_kmeans_model(n_clusters)
320
+ return kmeans.fit_predict(features)
321
+
322
+ def find_similar_content(content, n_clusters=50, n_recommendations=5):
323
+ """Find similar content using optimized k-means clustering"""
324
+ try:
325
+ # Get cached features and cluster assignments
326
+ features, df = prepare_content_features()
327
+ cluster_labels = get_content_clusters(features, n_clusters)
328
+
329
+ # Find the index of the selected content
330
+ selected_idx = df[df['title'] == content['title']].index[0]
331
+ selected_cluster = cluster_labels[selected_idx]
332
+
333
+ # Get indices of content in the same cluster
334
+ cluster_indices = np.where(cluster_labels == selected_cluster)[0]
335
+
336
+ # If cluster is too small, adjust n_clusters and retry
337
+ if len(cluster_indices) < n_recommendations + 1:
338
+ cluster_labels = get_content_clusters(features, n_clusters // 2)
339
+ selected_cluster = cluster_labels[selected_idx]
340
+ cluster_indices = np.where(cluster_labels == selected_cluster)[0]
341
+
342
+ # Calculate distances to all points in the same cluster
343
+ cluster_features = features[cluster_indices]
344
+ selected_features = features[selected_idx].reshape(1, -1)
345
+
346
+ # Use euclidean distance for similarity
347
+ distances = np.linalg.norm(cluster_features - selected_features, axis=1)
348
+
349
+ # Get indices of most similar content (excluding self)
350
+ similar_indices = cluster_indices[np.argsort(distances)][1:n_recommendations+1]
351
+
352
+ # Convert to list of content dictionaries
353
+ similar_content = []
354
+ for idx in similar_indices:
355
+ row = df.iloc[idx]
356
+ similarity_score = 1 / (1 + distances[np.where(cluster_indices == idx)[0][0]])
357
+
358
+ content_dict = {
359
+ 'title': row['title'],
360
+ 'type': row['type'],
361
+ 'release_year': row['release_year'],
362
+ 'listed_in': row['listed_in'],
363
+ 'description': row['description'],
364
+ 'streaming_service': row['streaming_service'],
365
+ 'rating': row['rating'],
366
+ 'duration': row['duration'],
367
+ 'cast': row['cast'],
368
+ 'director': row['director'],
369
+ 'similarity': f"{similarity_score:.2%}"
370
+ }
371
+ similar_content.append(content_dict)
372
+
373
+ return similar_content
374
+ except Exception as e:
375
+ st.error(f"Error finding similar content: {str(e)}")
376
+ return []
377
 
378
  # =============================================================================
379
  # MAIN APP
 
626
  st.write(f"**πŸ“– Description:**")
627
  st.write(content.get('description', 'N/A'))
628
 
629
+ # Add Find Similar button with loading state
630
+ similar_button = st.button(f"πŸ” Find Similar Content", key=f"similar_{i}")
631
+ if similar_button:
632
  with st.spinner("Finding similar content..."):
633
+ similar_content = find_similar_content(content)
634
 
635
  if similar_content:
636
+ # Create tabs for different aspects of recommendations
637
+ sim_tab1, sim_tab2 = st.tabs(["πŸ“Ί Similar Titles", "πŸ” Why These Recommendations"])
638
+
639
+ with sim_tab1:
640
+ for sim_content in similar_content:
641
+ with st.container():
642
+ col1, col2 = st.columns([3, 1])
643
+ with col1:
644
+ st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
645
+ st.write(f"*Available on:* {sim_content.get('streaming_service')}")
646
+ st.write(f"*Genres:* {sim_content.get('listed_in')}")
647
+ with col2:
648
+ st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}")
649
+
650
+ with st.expander("See more details"):
651
+ st.write(f"**Cast:** {sim_content.get('cast', 'N/A')}")
652
+ st.write(f"**Director:** {sim_content.get('director', 'N/A')}")
653
+ st.write(f"**Description:** {sim_content.get('description', 'N/A')}")
654
+ st.write("---")
655
+
656
+ with sim_tab2:
657
+ st.write("**Why these recommendations?**")
658
+ st.write("""
659
+ These titles were selected based on our k-means clustering algorithm which groups similar content together using:
660
+ - Genre matching and themes (80% weight)
661
+ - Release year proximity (10% weight)
662
+ - Duration similarity (10% weight)
663
+ - Cast and director overlap
664
+ - Content description similarity
665
+
666
+ The percentage match indicates how close each title is to your selected content within its cluster.
667
+ """)
668
  else:
669
  st.info("No similar content found.")
670