# Project export: Muru | The Movie Guru

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: find your next favorite movie with personalized recommendations based on your likes and dislikes. Our app learns your preferences and suggests movies that match your taste! With Muru, it's up to youru
- Devpost: https://devpost.com/software/muru-the-movie-guru
- GitHub: https://github.com/ricardo-escalante06/Muru-The-Movie-Guru
- Team: 1 GitHub contributor(s) — Ricardo Escalante (8 commits)

## Devpost submission (written by the team)

### Inspiration

Looking for hours for a movie to watch with seemingly no end to the random trailers in sight.

### What it does

Muru curates a selection of movies based on your likes and dislikes based on an IMDb dataset that finds the cosine similarities between your inputs and other movies based on genres and keywords.

### How we built it

Our website's front and backend were created through Reflex and our dataset was collected through IMDb.

### Challenges we ran into

We had several issues connecting the front and back ends of our project together. Switching the webpage to the recommended movies page and printing out the five posters caused us the most trouble. We solved this issue by returning the movie poster and printing it in a grid. Figuring out how to create the recommendations was pretty difficult. None of us had any experience when it came to processing data. It required a lot of research to get started working on it. We ended up switching to a different project late Friday.

### Accomplishments we're proud of

We are proud that we were able to create a proper project. We are proud to create a really good algorithm that recommends movies We are proud that we were able to work together to combine all our work

### What we learned

We learned a lot about preparing ahead of time for projects. Some of us had to learn Python to use Reflex for our front and backend Learned how to utilize data. (extract/refine) Learned how to find similarities in datasets.

### What's next

We want to implement a few features. An evil mode Our original plan was to include this as a fun bit to find movies you wouldn't like An exit/match feature This would allow more similarities for the Tinder UI Change how the frontend loads the movie recommendations/images This would increase the speed of the website Currently, it updates the whole website but we would want it to redirect the user Finnaly, we would want to have it hosted and running

## README (from the GitHub repository)

# Muru-The-Movie-Guru 1.0
Cal Hack 11 Submission

**Tinder X Movies**
- Uses an imdb movie dataset
- The code computes cosine similarity between different movies based on their genres and keywords to recommend movies based on user preferences.
For liked and disliked movies, the system computes a similarity score, subtracting the disliked movie scores from the liked ones to generate a recommendation.


## Detected evidence (automated analysis)

Indexed codebase: 7 recognized source files, 24 KB.
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (9 of 9)

```
movies.csv
MovieTinder/MovieTinder/__init__.py
MovieTinder/MovieTinder/mainAI.py
MovieTinder/MovieTinder/movie_helper.py
MovieTinder/MovieTinder/MovieTinder.py
MovieTinder/MovieTinder/MovieTinderBackup02.py
MovieTinder/requirements.txt
MovieTinder/rxconfig.py
README.md
```

### Dependencies

- MovieTinder/requirements.txt: reflex@==0.6.3

### Recent commits (newest first)

- Update Dataset
- Delete MovieTinder/movies_with_encoded_keywords.csv
- Delete MovieTinder/movies_with_encoded_genres_keywords.csv
- Delete MovieTinder/movies_with_encoded_genres.csv
- Delete MovieTinder/movies.csv
- Update README.md
- Add files via upload
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### MovieTinder/requirements.txt

```
reflex==0.6.3

```

### MovieTinder/rxconfig.py

```python
import reflex as rx

config = rx.Config(
    app_name="MovieTinder",
)
```

### MovieTinder/MovieTinder/MovieTinderBackup02.py

```python
import reflex as rx
from sklearn.metrics.pairwise import cosine_similarity

from .movie_helper import find_row, get_random_movie_id, recommend_movies, movies
cosine_sim = cosine_similarity

config = rx.Config(
    app_name="Movie Tinder",
    frontend_port=3000,
)


class State(rx.State):
    current_title: str = ""
    current_movie_id: int = 0
    liked_movies: list = []
    disliked_movies: list = []
    poster_path: str = ""

    redirect_to_org: bool = False

    def randomize_option(self):
        width = 500
        new_movie_id = get_random_movie_id()
        current_movie_row = find_row(new_movie_id)
        self.current_title = current_movie_row["title"]
        self.current_movie_id = new_movie_id
        base_url = f"https://image.tmdb.org/t/p/w{width}"
        self.poster_path = f"{base_url}{current_movie_row['poster_path']}"
        print("generated a new image"),

    def curated_content(self):
        width = 300
        movie_rec_array = recommend_movies(self.liked_movies, self.disliked_movies, cosine_sim, movies, 5)
        new_movie_id = get_random_movie_id()
        current_movie_row = find_row(new_movie_id)
        self.current_title = current_movie_row["title"]
        self.current_movie_id = new_movie_id
        base_url = f"https://image.tmdb.org/t/p/w{width}"
        self.poster_path = f"{base_url}{current_movie_row['poster_path']}"
        print(f"R: {movie_rec_array}")
        
    def love_title(
        self,
    ):
        print(f"I love {self.current_title} with ID of: {self.current_movie_id}")
        self.liked_movies.append(self.current_title)
        print(self.liked_movies)
        self.randomize_option()
        if (len(self.liked_movies) + len(self.disliked_movies)) % 5 == 0:
            self.redirect_to_recs()  # Trigger redirect

    def hate_title(
        self,
    ):
        print(f"I hate {self.current_title} with ID of: {self.current_movie_id}")
        self.disliked_movies.append(self.current_title)
        print(self.disliked_movies)
        self.randomize_option()

    #def change_redirect(self):
    #    self.redirect_to_org = not self.redirect_to_org

    #@rx.var

    #def url(self) -> str:
    #    return (
            
    #    )
    
    def redirect_to_recs(self):
        print("Redirecting to recommendations page")
        return rx.redirect("/recs/")  # Redirect to recommendations page


def generate_image(
    top: int = 0,
    left: int = 0,
    width: int = 500,
):
    return rx.hstack(
        rx.image(
            src=State.poster_path,
            style={
                "position": "absolute",
                "top": f"{top}px",
                "left": f"{left}px",
                "width": f"{width}px",
            },
        )
    )


def page_content():
    return rx.hstack(
        generate_image(
            top=100,
            left=500,
            width=400,
        ),
        rx.button(
            "I LOVE THIS MOVIEEEE",
            color_scheme="grass",
            on_click=State.love_title,
        ),
        rx.button(
            "I HATE THIS MOVIE",
            color_scheme="ruby",
            on_click=State.hate_title,
        ),
        spacing="20",
    )


def index():
    return rx.vstack(
        page_content(),  # Generate the image and buttons based on the movie ID
        rx.text(
            f"Current Movie: {State.current_title} with ID: {State.current_movie_id}"
        ),  # Display the clicked movie title and ID
    )

def recs():
    return rx.container(
    #heading
    rx.heading(
        "Recommendations Based on Your Likes",
        font_family="Cooper Black",
        font_size = "8", 
        color = "blue", 
        align = "center"
    ),
    #keep going button
    rx.box( 
    rx.button(
        "Keep Going",
        color_scheme="green",
        position="absolute",
        bottom="1000px",
        left="400px",
    ),

    width="100%",
    height="100vh",
        #position="relative",
    ),
    #quit buttton
    rx.box(
    rx.button(
        "Quit",
        color_scheme="red",
        position="absolute",
        bottom="1000px",
        right="400px",
    ),
    
    width="100%",
    height="100vh",
    #position="relative",
)
)


# Initialize Reflex App
app = rx.App()
app.add_page(
    index,
    route="/",
    on_load=State.randomize_option,
)
app.add_page(
    recs,
    route="/recs/",
    on_load=State.curated_content,
)

```

### MovieTinder/MovieTinder/MovieTinder.py

```python
import reflex as rx
from sklearn.metrics.pairwise import cosine_similarity
from typing import List
from .movie_helper import find_row, get_random_movie_id, recommend_movies, movies
cosine_sim = cosine_similarity

config = rx.Config(
    app_name="Movie Tinder",
    frontend_port=3000,
)


class State(rx.State):
    current_title: str = ""
    current_movie_id: int = 0
    liked_movies: list = []
    disliked_movies: list = []
    poster_path: str = ""
    movie_rec_array: List[str] = []

    redirect_to_org: bool = False

    def randomize_option(self):
        width = 500
        new_movie_id = get_random_movie_id()
        current_movie_row = find_row(new_movie_id)
        self.current_title = current_movie_row["title"]
        self.current_movie_id = new_movie_id
        base_url = f"https://image.tmdb.org/t/p/w{width}"
        self.poster_path = f"{base_url}{current_movie_row['poster_path']}"
        print("generated a new image"),

    def curated_content(self):
    # Get a DataFrame from recommend_movies and then extract the 'poster_path' column as a list
        recommended_movies_df = recommend_movies(self.liked_movies, self.disliked_movies, cosine_sim, movies, 5)
        
        # Ensure you're getting the poster paths and converting it to a list
        self.movie_rec_array = recommended_movies_df['poster_path'].tolist()

    def love_title(
        self,
    ):
        print(f"I love {self.current_title} with ID of: {self.current_movie_id}")
        self.liked_movies.append(self.current_title)
        print(self.liked_movies)
        self.randomize_option()
        test = (len(self.liked_movies) + len(self.disliked_movies))
        if test % 5 == 0:
            self.redirect_to_org = True
            yield rx.redirect("/recs")

    def hate_title(
        self,
    ):
        print(f"I hate {self.current_title} with ID of: {self.current_movie_id}")
        self.disliked_movies.append(self.current_title)
        print(self.disliked_movies)
        self.randomize_option()
        test = (len(self.liked_movies) + len(self.disliked_movies))
        if test % 5 == 0:
            self.redirect_to_org = True
            yield rx.redirect("/recs")

    def redirect_to_recs(self):
        print("Redirecting to recommendations page")
        return rx.redirect("/recs")  # Redirect to recommendations page


def generate_image(
    top: int = 0,
    left: int = 0,
    width: int = 500,
):
    return rx.hstack(
        rx.image(
            src=State.poster_path,
            style={
                "position": "absolute",
                "top": f"{top}px",
                "left": f"{left}px",
                "width": f"{width}px",
            },
        )
    )

def page_content():
    return rx.hstack(
        generate_image(
            top=100,
            left=500,
            width=400,
        ),
        rx.box(
        rx.heading(
                "Muru | The Movie Guru",
                font_family="Cooper Black", 
                font_size = "9", 
                color = "#4817A4",
                position="absolute", 
                top="50px",
                left="550px"),
            width="100%",
            height="100vh",
            #position="relative",
        ),
        rx.box(
        rx.button(
            rx.icon(tag="heart"),
            "Like",
            color_scheme="green",
            position="absolute",
            bottom="300px",
            right="400px",
            on_click=State.love_title,
        
            ),
            width="100%",
            height="100vh",
            #position="relative",
        ),

        rx.box(
        rx.button(
            rx.icon(tag="thumbs_down"),
            "Dislike",
            color_scheme="crimson",
            position="absolute",
            bottom="300px",
            left="300px",
            on_click=State.hate_title,
        ),
            width="100%",
            height="100vh",
            #position="relative",
        ),
        spacing="20",
    )


def index():
    return rx.vstack(
        page_content(),  # Generate the image and buttons based on the movie ID
        rx.text(
            f"Current Movie: {State.current_title} with ID: {State.current_movie_id}"
        ),  # Display the clicked movie title and ID
    )

def recs():
    return rx.container(
    #heading
    foreach_poster(),
    rx.heading(
        "Recommendations Based on Your Likes",
        font_family="Cooper Black",
        font_size = "8", 
        color = "#4817A4", 
        align = "center"
    ),
    #keep going button
    rx.box( 
    rx.button(
        "Keep Going",
        color_scheme="violet",
        position="absolute",
        bottom="600px",
        left="700px",
        on_click=rx.redirect("/")
    ),

    width="100%",
    height="100vh",
        #position="relative",
    ),

    
)   


def poster_maker(poster_path: str):
    return rx.image(src=f"https://image.tmdb.org/t/p/w400{poster_path}")


def foreach_poster():
    return rx.grid(
        rx.foreach(State.movie_rec_array, poster_maker),
        columns="5",
    )
    

# Initialize Reflex App

style = {
    "background": "#d8c0ff",
}

app = rx.App(style=style)
app.add_page(
    index,
    route="/",
    on_load=State.randomize_option,
)
app.add_page(
    recs,
    route="/recs",
    on_load=State.curated_content,
)

```

### MovieTinder/MovieTinder/movie_helper.py

```python
import pandas as pd
from sklearn.preprocessing import MultiLabelBinarizer
import numpy as np
from scipy.sparse import hstack, csr_matrix
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics.pairwise import cosine_similarity

def find_row(movie_id):
    return movies[movies['id'] == movie_id].iloc[0]

# Assign weights to genres and keywords
genre_weight = 0.8  # 80% weight for genres
keyword_weight = 0.2  # 20% weight for keywords

# Load the dataset (replace with your file path)
file_path = "C:/Users/kaijn/Downloads/MovieTinder/movies.csv"
movies = pd.read_csv(file_path)

# Step 2: Handle NaN or unexpected float values in the 'genres' column
movies['genres'] = movies['genres'].apply(lambda x: x.split('-') if isinstance(x, str) else [])

# Step 3: One-hot encode the 'genres' column
mlb = MultiLabelBinarizer()
genre_matrix = mlb.fit_transform(movies['genres'])
genre_df = pd.DataFrame(genre_matrix, columns=mlb.classes_)

# Step 4: Handle NaN or unexpected float values in the 'keywords' column
movies['keywords'] = movies['keywords'].apply(lambda x: x.split('-') if isinstance(x, str) else [])

# One-hot encode the 'keywords' column
mlb = MultiLabelBinarizer()
keyword_matrix = mlb.fit_transform(movies['keywords'])
keyword_df = pd.DataFrame(keyword_matrix, columns=mlb.classes_)

# Combine one-hot encoded genres and keywords with the original dataset
movies.reset_index(drop=True, inplace=True)
movies = pd.concat([movies, genre_df, keyword_df], axis=1)

# Save the new dataset (optional)
movies.to_csv('movies_with_encoded_genres_keywords.csv', index=False)

# Ensure all matrices are sparse and 2-D
def to_sparse_if_needed(matrix):
    return csr_matrix(matrix) if not isinstance(matrix, csr_matrix) else matrix

# Convert genre and keyword matrices to sparse format
genre_matrix = to_sparse_if_needed(genre_matrix)
keyword_matrix = to_sparse_if_needed(keyword_matrix)

# Apply weights to genre and keyword matrices
weighted_genre_matrix = genre_matrix * genre_weight
weighted_keyword_matrix = keyword_matrix * keyword_weight

# Combine the weighted genre and keyword matrices
combined_features = hstack([weighted_genre_matrix, weighted_keyword_matrix]).tocsr()

# Normalize the features
scaler = MinMaxScaler()
normalized_features = scaler.fit_transform(combined_features.toarray())


# Function to recommend movies based on liked and disliked movies
def recommend_movies(liked_movies, disliked_movies=None, cosine = cosine_similarity, movies=movies, top_n=1):
    liked_sim_scores = np.zeros(len(movies))
    disliked_sim_scores = np.zeros(len(movies))
    cosine_sim = cosine_similarity(normalized_features)
    # Calculate similarity for liked movies
    for movie_title in liked_movies:
        if movie_title in movies['title'].values:
            idx = movies[movies['title'] == movie_title].index[0]
            sim_scores = cosine_sim[idx]
            liked_sim_scores += sim_scores
        else:
            print(f"Movie '{movie_title}' not found in dataset.")

    # Calculate similarity for disliked movies
    if disliked_movies:
        for movie_title in disliked_movies:
            if movie_title in movies['title'].values:
                idx = movies[movies['title'] == movie_title].index[0]
                sim_scores = cosine_sim[idx]
                disliked_sim_scores += sim_scores

    # Combine the liked and disliked similarity scores
    combined_sim_scores = liked_sim_scores - disliked_sim_scores

    # Sort the combined similarity scores in descending order
    sim_scores = list(enumerate(combined_sim_scores))
    sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)

    # Get the top N most similar movies, excluding those in liked or disliked movies
    movie_indices = [i[0] for i in sim_scores if movies.iloc[i[0]]['title'] not in liked_movies + (disliked_movies or [])][:top_n]

    # Return the top N most similar movies
    return movies.iloc[movie_indices][['poster_path']]

# Function to recommend movies based on disliked movies (alternative)
def recommend_movies_bad(disliked_movies, liked_movies=None, cosine_sim = cosine_similarity, movies=movies, top_n=1):
    liked_sim_scores = np.zeros(len(movies))
    disliked_sim_scores = np.zeros(len(movies))
    cosine_sim = cosine_similarity(normalized_features)

    # Calculate similarity for disliked movies
    for movie_title in disliked_movies:
        if movie_title in movies['title'].values:
            idx = movies[movies['title'] == movie_title].index[0]
            sim_scores = cosine_sim[idx]
            liked_sim_scores += sim_scores

    # Calculate similarity for liked movies (if any)
    if liked_movies:
        for movie_title in liked_movies:
            if movie_title in movies['title'].values:
                idx = movies[movies['title'] == movie_title].index[0]
                sim_scores = cosine_sim[idx]
                disliked_sim_scores += sim_scores

    # Combine the liked and disliked similarity scores
    combined_sim_scores = liked_sim_scores - disliked_sim_scores

    # Sort the combined similarity scores in descending order
    sim_scores = list(enumerate(combined_sim_scores))
    sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)

    # Get the top N most similar movies, excluding those in liked or disliked movies
    movie_indices = [i[0] for i in sim_scores if movies.iloc[i[0]]['title'] not in liked_movies + (disliked_movies or [])][:top_n]

    # Return the top N most similar movies
    return movies.iloc[movie_indices][['poster_path']]

# Function to get a random movie ID (exclude already displayed movies)
displayed_movies = set()
def get_random_movie_id():
    global displayed_movies
    available_movies = movies[~movies['id'].isin(displayed_movies)]
    if available_movies.empty:
        print("No more movies to display.")
        return None
    random_movie = available_movies.sample(n=1)
    movie_id = random_movie.iloc[0]['id']
    displayed_movies.add(movie_id)
    return mo
[truncated — 7 more characters]
```

### MovieTinder/MovieTinder/mainAI.py

```python
import pandas as pd
from sklearn.preprocessing import MultiLabelBinarizer
import numpy as np
from scipy.sparse import hstack, csr_matrix
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics.pairwise import cosine_similarity

# Assign weights to genres and keywords (you can tweak these values)
genre_weight = 0.8  # 70% weight for genres
keyword_weight = 0.2  # 30% weight for keywords



# Load the dataset (replace with the correct file path)
file_path = "C:/Users/kaijn/Downloads/MovieTinder/movies.csv"
movies = pd.read_csv(file_path)

# Step 2: Handle NaN or unexpected float values in the 'genres' column
movies['genres'] = movies['genres'].apply(lambda x: x.split('-') if isinstance(x, str) else [])



# Step 3: Use MultiLabelBinarizer to one-hot encode the split genres
mlb = MultiLabelBinarizer()
genre_matrix = mlb.fit_transform(movies['genres'])



# Step 4: Convert to DataFrame for easy viewing
genre_df = pd.DataFrame(genre_matrix, columns=mlb.classes_)




# Step 6: Print total counts of each genre
genre_totals = genre_df.sum()


# Step 7: Combine the one-hot encoded genres with the original dataset
# Resetting index of movies to avoid index conflicts
movies.reset_index(drop=True, inplace=True)
movies = pd.concat([movies, genre_df], axis=1)



# Step 8: Save the new dataset with one-hot encoded genres (optional)
movies.to_csv('movies_with_encoded_genres.csv', index=False)



# Step 2: Handle NaN or unexpected float values in the 'keywords' column
movies['keywords'] = movies['keywords'].apply(lambda x: x.split('-') if isinstance(x, str) else [])



mlb = MultiLabelBinarizer()
keyword_matrix = mlb.fit_transform(movies['keywords'])



# Step 4: Convert to DataFrame for easy viewing
keyword_df = pd.DataFrame(keyword_matrix, columns=mlb.classes_)


# Step 6: Print total counts of each keyword
keyword_totals = keyword_df.sum()

# Step 7: Combine the one-hot encoded keywords with the original dataset
# Resetting index of movies to avoid index conflicts
movies.reset_index(drop=True, inplace=True)
movies = pd.concat([movies, keyword_df], axis=1)


# Step 8: Save the new dataset with one-hot encoded keywords (optional)
movies.to_csv('movies_with_encoded_keywords.csv', index=False)

# Ensure all matrices are sparse and 2-D
def to_sparse_if_needed(matrix):
    return csr_matrix(matrix) if not isinstance(matrix, csr_matrix) else matrix

# Convert all matrices to sparse format if they aren't already
genre_matrix = to_sparse_if_needed(genre_matrix)
keywords_matrix = to_sparse_if_needed(keyword_matrix)


# Apply weights to genre and keyword matrices
weighted_genre_matrix = genre_matrix * genre_weight
weighted_keyword_matrix = keyword_matrix * keyword_weight


combined_features = hstack([
    weighted_genre_matrix, 
    weighted_keyword_matrix
]).tocsr()


scaler = MinMaxScaler()
normalized_features = scaler.fit_transform(combined_features.toarray())








# Calculate cosine similarity matrix
cosine_sim = cosine_similarity(combined_features)







def recommend_movies(liked_movies, disliked_movies=None, cosine_sim=cosine_sim, movies=movies, top_n=1):
    # Initialize similarity score arrays with zeros for both liked and disliked movies
    liked_sim_scores = np.zeros(len(movies))
    disliked_sim_scores = np.zeros(len(movies))

    # Calculate similarity for liked movies
    for movie_title in liked_movies:
        if movie_title in movies['title'].values:
            idx = movies[movies['title'] == movie_title].index[0]
            sim_scores = cosine_sim[idx]
            liked_sim_scores += sim_scores
        else:
            print(f"Movie '{movie_title}' not found in dataset.")

    # Calculate similarity for disliked movies (if any)
    if disliked_movies:
        for movie_title in disliked_movies:
            if movie_title in movies['title'].values:
                idx = movies[movies['title'] == movie_title].index[0]
                sim_scores = cosine_sim[idx]
                # Penalize similar movies by subtracting similarity scores
                disliked_sim_scores += sim_scores
            else:
                print(f"Movie '{movie_title}' not found in dataset.")
    
    # Combine the liked and disliked similarity scores
    combined_sim_scores = liked_sim_scores - disliked_sim_scores

    # Sort the combined similarity scores in descending order
    sim_scores = list(enumerate(combined_sim_scores))
    sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)

    # Get the top_n most similar movies, excluding those in the input lists
    movie_indices = [
        i[0] for i in sim_scores if movies.iloc[i[0]]['title'] not in liked_movies + (disliked_movies or [])
    ][:top_n]

    # Return the top_n most similar movies
    return movies.iloc[movie_indices][['id']]

# This is the EVIL input ------------------------------------------------------

def recommend_movies_bad(disliked_movies, liked_movies=None, cosine_sim=cosine_sim, movies=movies, top_n=1):
    # Initialize similarity score arrays with zeros for both liked and disliked movies
    liked_sim_scores = np.zeros(len(movies))
    disliked_sim_scores = np.zeros(len(movies))

    # Calculate similarity for disliked movies
    for movie_title in disliked_movies:
        if movie_title in movies['title'].values:
            idx = movies[movies['title'] == movie_title].index[0]
            sim_scores = cosine_sim[idx]
            liked_sim_scores += sim_scores
        else:
            print(f"Movie '{movie_title}' not found in dataset.")

    # Calculate similarity for disliked movies (if any)
    if liked_movies:
        for movie_title in liked_movies:
            if movie_title in movies['title'].values:
                idx = movies[movies['title'] == movie_title].index[0]
                sim_scores = cosine_sim[idx]
             
[truncated — 2045 more characters]
```