# --------------------------------
# Data Mining
# --------------------------------


# Agglomerative Hierarchical Clustering
def ac():
    print(r"""
  import numpy as np
  from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
  import matplotlib.pyplot as plt

  # Sample dataset
  X = np.array([
      [1, 2],
      [2, 3],
      [5, 6],
      [6, 7],
      [8, 9]
  ])

  # Step 1: Perform clustering
  Z = linkage(X, method='single')   # 'single', 'complete', 'average', 'ward'

  # Step 2: Plot dendrogram
  plt.figure()
  dendrogram(Z)
  plt.title("Dendrogram")
  plt.xlabel("Data Points")
  plt.ylabel("Distance")
  plt.show()

  # Step 3: Form clusters (k = 2)
  clusters = fcluster(Z, 2, criterion='maxclust')

  print("Cluster labels:", clusters)""")


##DBScan clustering
def db():
    print(r"""
  import numpy as np
  from sklearn.cluster import DBSCAN
  import matplotlib.pyplot as plt

  # Sample dataset
  X = np.array([
      [1, 2],
      [2, 2],
      [2, 3],
      [8, 7],
      [8, 8],
      [25, 80]   # noise
  ])

  # Apply DBSCAN
  db = DBSCAN(eps=2, min_samples=2)
  labels = db.fit_predict(X)

  print("Cluster labels:", labels)

  # Plot
  plt.scatter(X[:,0], X[:,1], c=labels)
  plt.title("DBSCAN Clustering")
  plt.show()""")


##Generalized Sequential Pattern (GSP) Algorithm
def gsp():
    print(r"""
  from collections import defaultdict

  # Sample sequence database
  D = [
      ['A', 'B', 'C'],
      ['A', 'C'],
      ['A', 'B', 'C'],
      ['B', 'C']
  ]

  min_sup = 2

  # Step 1: Find frequent 1-sequences
  def get_frequent_1(D, min_sup):
      count = defaultdict(int)
      
      for seq in D:
          for item in set(seq):
              count[item] += 1
      
      return { (item,): count[item] for item in count if count[item] >= min_sup }


  # Step 2: Generate candidates
  def generate_candidates(prev_freq):
      candidates = []
      keys = list(prev_freq.keys())
      
      for i in range(len(keys)):
          for j in range(len(keys)):
              if keys[i][1:] == keys[j][:-1]:
                  new_seq = keys[i] + (keys[j][-1],)
                  candidates.append(new_seq)
      
      return list(set(candidates))


  # Step 3: Count support
  def count_support(D, candidates):
      count = defaultdict(int)
      
      for seq in D:
          for cand in candidates:
              if is_subsequence(cand, seq):
                  count[cand] += 1
      
      return {c: count[c] for c in count}


  # Check subsequence
  def is_subsequence(subseq, seq):
      i = 0
      for item in seq:
          if i < len(subseq) and subseq[i] == item:
              i += 1
      return i == len(subseq)


  # GSP Main
  def GSP(D, min_sup):
      freq_patterns = []
      
      # Step 1
      L1 = get_frequent_1(D, min_sup)
      current = L1
      freq_patterns.extend(L1.keys())
      
      while current:
          # Step 2
          candidates = generate_candidates(current)
          
          # Step 3
          counted = count_support(D, candidates)
          
          # Prune
          current = {c: counted[c] for c in counted if counted[c] >= min_sup}
          
          freq_patterns.extend(current.keys())
      
      return freq_patterns


  # Run
  patterns = GSP(D, min_sup)

  print("Frequent Sequential Patterns:")
  for p in patterns:
      print(p)
  """)


##Problem statement 13
def ps13():
    print(r"""
  Consider Customer mall dataset with following details.
  1. CustomerID: An identifier for each customer.
  2. Gender: Indicates the gender of the customer (Male or Female).
  3. Age: Represents the age of the customer in years.
  4. Annual Income (k$): Denotes the annual income of the customer in thousands of dollars.
  5. Spending Score (1–100): A score ranging from 1 to 100 that quantifies the customer’s
  spending habits and preferences. A higher score indicates a higher tendency to spend.

  Do the following tasks
  • Remove CustomerID column .
  • Check for missing values.
  • Convert categorical variable value of gender to numerical (Male-1, Female-0).
  • Display male, female ratio as pie chart.
  • Display age, annual income as bar graph
  • Perform agglomerative clustering with ward algorithm as linkage.
  • Display dendrogram.
  

  import pandas as pd
  import matplotlib.pyplot as plt
  from scipy.cluster.hierarchy import linkage, dendrogram
  from sklearn.cluster import AgglomerativeClustering

  # Load dataset (update path if needed)
  df = pd.read_csv("Mall_Customers.csv")

  # -------------------------------
  # 1. Remove CustomerID column
  # -------------------------------
  df = df.drop("CustomerID", axis=1)

  # -------------------------------
  # 2. Check for missing values
  # -------------------------------
  print("Missing values:\n", df.isnull().sum())

  # -------------------------------
  # 3. Convert Gender to numerical
  # Male = 1, Female = 0
  # -------------------------------
  df["Gender"] = df["Gender"].map({"Male": 1, "Female": 0})

  # -------------------------------
  # 4. Pie chart (Male vs Female)
  # -------------------------------
  gender_counts = df["Gender"].value_counts()

  labels = ["Male", "Female"]
  values = [gender_counts.get(1, 0), gender_counts.get(0, 0)]

  plt.figure()
  plt.pie(values, labels=labels, autopct='%1.1f%%')
  plt.title("Male vs Female Ratio")
  plt.show()

  # -------------------------------
  # 5. Bar graph (Age & Income)
  # -------------------------------
  plt.figure()

  plt.bar(range(len(df)), df["Age"])
  plt.title("Age Distribution")
  plt.xlabel("Customers")
  plt.ylabel("Age")
  plt.show()

  plt.figure()

  plt.bar(range(len(df)), df["Annual Income (k$)"])
  plt.title("Annual Income Distribution")
  plt.xlabel("Customers")
  plt.ylabel("Income (k$)")
  plt.show()

  # -------------------------------
  # 6. Agglomerative Clustering (Ward)
  # -------------------------------
  # Use relevant features
  X = df[["Age", "Annual Income (k$)", "Spending Score (1-100)"]]

  model = AgglomerativeClustering(n_clusters=3, linkage='ward')
  labels = model.fit_predict(X)

  df["Cluster"] = labels
  print("Cluster labels:\n", df["Cluster"].head())

  # -------------------------------
  # 7. Dendrogram
  # -------------------------------
  plt.figure()
  Z = linkage(X, method='ward')
  dendrogram(Z)
  plt.title("Dendrogram (Ward Linkage)")
  plt.xlabel("Customers")
  plt.ylabel("Distance")
  plt.show()
  """)


##Problem Sheet 14
def ps14():
    print(r"""
  1. Consider dataset consisting of annual customer data for a wholesale distributor. The dataset
  contains 440 customers and has 8 attributes. Perform the following tasks.
  • Drop the columns channel and region and display first few records.
  • Consider Groceries and Milk attributes. Normalize these attribute values by scaling it from 0
  mean to unit variance. Visualize normalized dataset.
  • Write your own implementation of DBSCAN (no library) with Minpts = 15 and EPS=0.5.
  • Plot the cluster Results.

  2. Use the make_moons function from the sklearn.datasets module to generate a synthetic
  dataset that has a moon-shaped pattern. Give 2000 as value to sample parameter.
  Run built-in DBSCAN algorithm and visualize the result. Add some noise data and again
  visualize the results.
  

  import pandas as pd
  import numpy as np
  import matplotlib.pyplot as plt
  from sklearn.datasets import make_moons
  from sklearn.cluster import DBSCAN

  # =====================================
  # PART 1: WHOLESALE DATASET
  # =====================================

  # Load dataset
  df = pd.read_csv("Wholesale customers data.csv")

  # 1. Drop columns
  df = df.drop(["Channel", "Region"], axis=1)
  print("First few records:\n", df.head())

  # 2. Normalize Groceries & Milk
  X = df[["Groceries", "Milk"]].values

  mean = X.mean(axis=0)
  std = X.std(axis=0)
  X_norm = (X - mean) / std

  # Plot normalized data
  plt.figure()
  plt.scatter(X_norm[:, 0], X_norm[:, 1])
  plt.title("Normalized Data (Groceries vs Milk)")
  plt.xlabel("Groceries")
  plt.ylabel("Milk")
  plt.show()


  # =====================================
  # CUSTOM DBSCAN IMPLEMENTATION
  # =====================================

  def euclidean(p1, p2):
      return np.sqrt(np.sum((p1 - p2) ** 2))

  def region_query(X, point_idx, eps):
      neighbors = []
      for i in range(len(X)):
          if euclidean(X[point_idx], X[i]) <= eps:
              neighbors.append(i)
      return neighbors

  def expand_cluster(X, labels, point_idx, neighbors, cluster_id, eps, min_pts):
      labels[point_idx] = cluster_id
      
      i = 0
      while i < len(neighbors):
          n_point = neighbors[i]
          
          if labels[n_point] == -1:
              labels[n_point] = cluster_id
          
          if labels[n_point] == 0:
              labels[n_point] = cluster_id
              n_neighbors = region_query(X, n_point, eps)
              
              if len(n_neighbors) >= min_pts:
                  neighbors += n_neighbors
          
          i += 1

  def dbscan(X, eps, min_pts):
      labels = [0] * len(X)  # 0 = unvisited
      cluster_id = 0
      
      for i in range(len(X)):
          if labels[i] != 0:
              continue
          
          neighbors = region_query(X, i, eps)
          
          if len(neighbors) < min_pts:
              labels[i] = -1  # noise
          else:
              cluster_id += 1
              expand_cluster(X, labels, i, neighbors, cluster_id, eps, min_pts)
      
      return np.array(labels)

  # 3. Run custom DBSCAN
  labels_custom = dbscan(X_norm, eps=0.5, min_pts=15)

  # 4. Plot results
  plt.figure()
  plt.scatter(X_norm[:, 0], X_norm[:, 1], c=labels_custom)
  plt.title("Custom DBSCAN Clustering")
  plt.xlabel("Groceries")
  plt.ylabel("Milk")
  plt.show()


  # =====================================
  # PART 2: MOON DATASET
  # =====================================

  # Generate moon data
  X_moon, _ = make_moons(n_samples=2000, noise=0.05)

  plt.figure()
  plt.scatter(X_moon[:, 0], X_moon[:, 1])
  plt.title("Moon Dataset")
  plt.show()

  # Built-in DBSCAN
  model = DBSCAN(eps=0.2, min_samples=5)
  labels_moon = model.fit_predict(X_moon)

  plt.figure()
  plt.scatter(X_moon[:, 0], X_moon[:, 1], c=labels_moon)
  plt.title("DBSCAN on Moon Data")
  plt.show()

  # Add noise
  noise = np.random.uniform(low=-2, high=3, size=(200, 2))
  X_noisy = np.vstack((X_moon, noise))

  plt.figure()
  plt.scatter(X_noisy[:, 0], X_noisy[:, 1])
  plt.title("Moon Data with Noise")
  plt.show()

  # DBSCAN on noisy data
  labels_noisy = model.fit_predict(X_noisy)

  plt.figure()
  plt.scatter(X_noisy[:, 0], X_noisy[:, 1], c=labels_noisy)
  plt.title("DBSCAN with Noise")
  plt.show()
  """)


##Problem sheet 15
def ps15():
    print(r"""
  Implement Mining Sequential Patterns Based on GSP (Generalized Sequential Patterns) MS-GSP
  algorithm - Sequential pattern mining using multiple minimum supports with a support difference
  constraint.
  Input format:
  data.txt:Each line represents a Transaction Sequence and each set in a sequence represents a set of
  items.
  para.txt:Gives the minimum item support for each item as well as the support difference constraint
  Output format:
  Pattern :<{30,20}{70,80}{20,30,70}> count: 10

  import re
  from collections import defaultdict

  # ==========================================
  # 1. READ DATA
  # ==========================================

  def read_data(file):
      sequences = []
      with open(file, 'r') as f:
          for line in f:
              seq = []
              sets = re.findall(r'\{([^}]*)\}', line)
              for s in sets:
                  items = list(map(int, s.split(',')))
                  seq.append(items)
              sequences.append(seq)
      return sequences


  def read_params(file):
      MIS = {}
      SDC = 0
      
      with open(file, 'r') as f:
          for line in f:
              if "MIS" in line:
                  item = int(re.findall(r'\d+', line)[0])
                  value = float(re.findall(r'\d+\.\d+', line)[0])
                  MIS[item] = value
              elif "SDC" in line:
                  SDC = float(re.findall(r'\d+\.\d+', line)[0])
      
      return MIS, SDC


  # ==========================================
  # 2. SUPPORT COUNT
  # ==========================================

  def get_support(sequences):
      count = defaultdict(int)
      total = len(sequences)
      
      for seq in sequences:
          unique_items = set()
          for itemset in seq:
              for item in itemset:
                  unique_items.add(item)
          for item in unique_items:
              count[item] += 1
      
      support = {item: count[item]/total for item in count}
      return support, count


  # ==========================================
  # 3. CHECK SUBSEQUENCE
  # ==========================================

  def is_subsequence(candidate, sequence):
      i = 0
      for itemset in sequence:
          if all(item in itemset for item in candidate[i]):
              i += 1
              if i == len(candidate):
                  return True
      return False


  # ==========================================
  # 4. COUNT SUPPORT FOR SEQUENCE
  # ==========================================

  def count_sequence_support(sequences, candidates):
      count = defaultdict(int)
      
      for seq in sequences:
          for cand in candidates:
              if is_subsequence(cand, seq):
                  count[str(cand)] += 1
      
      return count


  # ==========================================
  # 5. JOIN STEP (CANDIDATE GENERATION)
  # ==========================================

  def join(L):
      candidates = []
      
      for i in range(len(L)):
          for j in range(len(L)):
              s1 = L[i]
              s2 = L[j]
              
              if s1[1:] == s2[:-1]:
                  new_seq = s1 + [s2[-1]]
                  if new_seq not in candidates:
                      candidates.append(new_seq)
      
      return candidates


  # ==========================================
  # 6. MS-GSP MAIN
  # ==========================================

  def msgsp(sequences, MIS, SDC):
      support, raw_count = get_support(sequences)
      N = len(sequences)
      
      # Sort items by MIS
      items = sorted(MIS.keys(), key=lambda x: MIS[x])
      
      # Init pass
      L = []
      for item in items:
          if support.get(item, 0) >= MIS[item]:
              L.append(item)
      
      # Frequent 1-sequences
      F = []
      F1 = []
      for item in L:
          if support[item] >= MIS[item]:
              F1.append([[item]])
      
      F.append(F1)
      
      k = 2
      while True:
          prev = F[k-2]
          if not prev:
              break
          
          # Join
          candidates = join(prev)
          
          # Count support
          counts = count_sequence_support(sequences, candidates)
          
          freq_k = []
          
          for cand in candidates:
              key = str(cand)
              sup = counts[key] / N if key in counts else 0
              
              # MIS check (first item)
              first_item = cand[0][0]
              
              # SDC check
              valid = True
              flat = [item for subset in cand for item in subset]
              for i in range(len(flat)):
                  for j in range(len(flat)):
                      if abs(support.get(flat[i],0) - support.get(flat[j],0)) > SDC:
                          valid = False
              
              if sup >= MIS[first_item] and valid:
                  freq_k.append(cand)
          
          if not freq_k:
              break
          
          F.append(freq_k)
          k += 1
      
      return F, sequences


  # ==========================================
  # 7. PRINT OUTPUT
  # ==========================================

  def print_patterns(F, sequences):
      counts = count_sequence_support(sequences, 
          [cand for level in F for cand in level])
      
      for level in F:
          for pattern in level:
              key = str(pattern)
              print(f"Pattern :<{pattern}> count: {counts.get(key,0)}")


  # ==========================================
  # RUN
  # ==========================================

  sequences = read_data("data.txt")
  MIS, SDC = read_params("para.txt")

  F, sequences = msgsp(sequences, MIS, SDC)

  print_patterns(F, sequences)  
  """)


# --------------------------------------
# IR
# --------------------------------------
# Collaborative Filtering (User-Based)
def cf():
    print(r"""
  import math
  # Sample user-item matrix (0 = not rated)
  R = {
      'A': {'item1': 5, 'item2': 3, 'item3': 0},
      'B': {'item1': 4, 'item2': 0, 'item3': 2},
      'C': {'item1': 0, 'item2': 4, 'item3': 5}
  }

  # Step 1: Cosine similarity
  def cosine_similarity(user1, user2):
      dot = 0
      norm1 = 0
      norm2 = 0
      
      for item in R[user1]:
          r1 = R[user1][item]
          r2 = R[user2][item]
          
          dot += r1 * r2
          norm1 += r1 ** 2
          norm2 += r2 ** 2
      
      if norm1 == 0 or norm2 == 0:
          return 0
      
      return dot / (math.sqrt(norm1) * math.sqrt(norm2))

  # Step 2: Get similar users
  def get_neighbors(target_user):
      similarities = []
      
      for user in R:
          if user != target_user:
              sim = cosine_similarity(target_user, user)
              similarities.append((user, sim))
      
      similarities.sort(key=lambda x: x[1], reverse=True)
      return similarities

  # Step 3: Predict rating
  def predict_rating(user, item):
      neighbors = get_neighbors(user)
      
      numerator = 0
      denominator = 0
      
      for neighbor, sim in neighbors:
          rating = R[neighbor][item]
          if rating != 0:
              numerator += sim * rating
              denominator += abs(sim)
      
      if denominator == 0:
          return 0
      
      return numerator / denominator

  # Example
  print("Predicted rating for A on item3:", predict_rating('A', 'item3'))""")


##Content-Based Recommendation
def cbr():
    print(r"""import math

  # Item feature vectors
  items = {
      'item1': [1, 0, 1],
      'item2': [0, 1, 1],
      'item3': [1, 1, 0]
  }

  # User liked items
  user_likes = ['item1', 'item2']

  # Step 1: Build user profile
  def build_user_profile(likes):
      profile = [0] * len(items['item1'])
      
      for item in likes:
          for i in range(len(profile)):
              profile[i] += items[item][i]
      
      # average
      for i in range(len(profile)):
          profile[i] /= len(likes)
      
      return profile

  # Step 2: Cosine similarity
  def cosine(v1, v2):
      dot = sum(v1[i] * v2[i] for i in range(len(v1)))
      norm1 = math.sqrt(sum(x*x for x in v1))
      norm2 = math.sqrt(sum(x*x for x in v2))
      
      if norm1 == 0 or norm2 == 0:
          return 0
      
      return dot / (norm1 * norm2)

  # Step 3: Recommend items
  def recommend():
      profile = build_user_profile(user_likes)
      scores = []
      
      for item in items:
          if item not in user_likes:
              sim = cosine(profile, items[item])
              scores.append((item, sim))
      
      scores.sort(key=lambda x: x[1], reverse=True)
      return scores

  # Example
  print("Recommendations:", recommend())""")


##Page rank


def pr():

    print(r"""
  import math
  def pagerank(graph, d=0.85, iterations=10):
      N = len(graph)
      
      # Step 1: Initialize PageRank
      pr = {}
      for page in graph:
          pr[page] = 1 / N
      
      # Step 2: Iterations
      for _ in range(iterations):
          new_pr = {}
          
          for page in graph:
              # Base value (random jump)
              new_pr[page] = (1 - d) / N
              
              for node in graph:
                  # Case 1: Normal link
                  if len(graph[node]) > 0:
                      if page in graph[node]:  # node -> page
                          new_pr[page] += d * (pr[node] / len(graph[node]))
                  
                  # Case 2: Dangling node (no outgoing links)
                  else:
                      # Distribute its rank equally to all pages
                      new_pr[page] += d * (pr[node] / N)
          
          pr = new_pr
      
      return pr


  # Example graph with dangling node
  graph = {
      'A': ['B'],
      'B': ['C'],
      'C': ['A'],
      'D': []   # Dangling node
  }

  # Run PageRank
  result = pagerank(graph)

  # Print results
  for page, rank in result.items():
      print(page, ":", round(rank, 4))""")


##Principal Component Analysis (PCA)
def pca():
    print(r"""
  import math

  # Example dataset (2D → reduce to 1D)
  X = [
      [2.5, 2.4],
      [0.5, 0.7],
      [2.2, 2.9],
      [1.9, 2.2],
      [3.1, 3.0]
  ]

  # Step 1: Mean Centering
  def mean_center(X):
      mean = [sum(col)/len(col) for col in zip(*X)]
      
      X_centered = []
      for row in X:
          X_centered.append([row[i] - mean[i] for i in range(len(row))])
      
      return X_centered, mean

  # Step 2: Covariance Matrix
  def covariance_matrix(X):
      n = len(X)
      m = len(X[0])
      
      cov = [[0]*m for _ in range(m)]
      
      for i in range(m):
          for j in range(m):
              for row in X:
                  cov[i][j] += row[i] * row[j]
              cov[i][j] /= n
      
      return cov

  # Step 3: Eigenvalues & Eigenvectors (2x2 only for lab simplicity)
  def eigen_2x2(matrix):
      a, b = matrix[0]
      c, d = matrix[1]
      
      # Eigenvalues
      trace = a + d
      det = a*d - b*c
      
      term = math.sqrt(trace**2 - 4*det)
      
      lambda1 = (trace + term)/2
      lambda2 = (trace - term)/2
      
      # Eigenvectors
      vec1 = [b, lambda1 - a] if b != 0 else [1, 0]
      vec2 = [b, lambda2 - a] if b != 0 else [0, 1]
      
      return [(lambda1, vec1), (lambda2, vec2)]

  # Step 4: Projection
  def project(X, vector):
      projected = []
      for row in X:
          val = sum(row[i] * vector[i] for i in range(len(vector)))
          projected.append(val)
      return projected


  # Run PCA
  X_centered, mean = mean_center(X)
  cov = covariance_matrix(X_centered)
  eigen_pairs = eigen_2x2(cov)

  # Sort by eigenvalue
  eigen_pairs.sort(key=lambda x: x[0], reverse=True)

  # Take top eigenvector
  top_vector = eigen_pairs[0][1]

  # Project data
  result = project(X_centered, top_vector)

  print("Reduced Data:", result)

  ##Feature Selection (Simple Reduction)

  def variance(feature):
      mean = sum(feature)/len(feature)
      return sum((x - mean)**2 for x in feature) / len(feature)

  def feature_selection(X, threshold=0.5):
      selected = []
      
      # Transpose dataset
      features = list(zip(*X))
      
      for i, feature in enumerate(features):
          var = variance(feature)
          if var >= threshold:
              selected.append(i)
      
      # Build reduced dataset
      reduced = []
      for row in X:
          reduced.append([row[i] for i in selected])
      
      return reduced


  # Example
  X = [
      [1, 100, 2],
      [2, 100, 3],
      [3, 100, 4]
  ]

  print("Reduced:", feature_selection(X))""")


def recommend():
    print(r"""
    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt


    # =========================
    # DATA PREPARATION
    def load_data():
        data = [
            [5, 3, 4, '?'],
            [3, 1, 2, 3],
            [4, 3, 4, 5],
            [3, 3, 1, 5]
        ]

        df = pd.DataFrame(
            data,
            index=['User1', 'User2', 'User3', 'User4'],
            columns=['Item1', 'Item2', 'Item3', 'Item4']
        )

        df = df.mask(df == '?', np.nan).astype(float)
        return df

    def mean_center(df):
        user_mean = df.mean(axis=1)
        df_centered = df.sub(user_mean, axis=0)
        return user_mean, df_centered

    # SIMILARITY FUNCTION
    def cosine_similarity(a, b):
        mask = ~np.isnan(a) & ~np.isnan(b)

        if np.sum(mask) == 0:
            return 0

        a, b = a[mask], b[mask]
        return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

    # USER-BASED CF
    def predict_user_based(df, user, item):
        target_vector = df.loc[user].values
        similarities = {}

        for other_user in df.index:
            if other_user == user:
                continue

            if not np.isnan(df.loc[other_user, item]):
                sim = cosine_similarity(
                    target_vector,
                    df.loc[other_user].values
                )
                similarities[other_user] = sim

        num, den = 0, 0
        for u, sim in similarities.items():
            num += sim * df.loc[u, item]
            den += abs(sim)

        return np.nan if den == 0 else num / den

    def predict_user_based_mean_centered(df, user, item):
        user_mean = df.mean(axis=1)
        target_vector = df.loc[user].values
        similarities = {}

        for other_user in df.index:
            if other_user == user:
                continue

            if not np.isnan(df.loc[other_user, item]):
                sim = cosine_similarity(
                    target_vector,
                    df.loc[other_user].values
                )
                similarities[other_user] = sim

        num, den = 0, 0

        for u, sim in similarities.items():
            ru_i = df.loc[u, item]
            ru_mean = user_mean[u]

            num += sim * (ru_i - ru_mean)
            den += abs(sim)

        if den == 0:
            return user_mean[user]

        return user_mean[user] + (num / den)

    def predict_user_based_topk(df, user, item, k=2):
        target_vector = df.loc[user].values
        similarities = []

        for other_user in df.index:
            if other_user == user:
                continue

            if not np.isnan(df.loc[other_user, item]):
                sim = cosine_similarity(
                    target_vector,
                    df.loc[other_user].values
                )
                similarities.append((other_user, sim))

        # Sort by similarity (descending)
        similarities.sort(key=lambda x: x[1], reverse=True)

        # Select Top-K
        top_k = similarities[:k]

        num, den = 0, 0
        for u, sim in top_k:
            num += sim * df.loc[u, item]
            den += abs(sim)

        return np.nan if den == 0 else num / den

    # ITEM-BASED CF
    def predict_item_based(df, user, item):
        target_vector = df[item].values
        similarities = {}

        for other_item in df.columns:
            if other_item == item:
                continue

            if not np.isnan(df.loc[user, other_item]):
                sim = cosine_similarity(
                    target_vector,
                    df[other_item].values
                )
                similarities[other_item] = sim

        num, den = 0, 0

        for i, sim in similarities.items():
            num += sim * df.loc[user, i]
            den += abs(sim)

        return np.nan if den == 0 else num / den

    def predict_item_based_topk(df, user, item, k=2):
        target_vector = df[item].values
        similarities = []

        for other_item in df.columns:
            if other_item == item:
                continue

            if not np.isnan(df.loc[user, other_item]):
                sim = cosine_similarity(
                    target_vector,
                    df[other_item].values
                )
                similarities.append((other_item, sim))

        # Sort & select Top-K
        similarities.sort(key=lambda x: x[1], reverse=True)
        top_k = similarities[:k]

        num, den = 0, 0
        for i, sim in top_k:
            num += sim * df.loc[user, i]
            den += abs(sim)

        return np.nan if den == 0 else num / den

    # EVALUATION
    def evaluate(df):
        actuals, preds = [], []

        for u in df.index:
            for i in df.columns:
                if not np.isnan(df.loc[u, i]):

                    temp = df.copy()
                    actual = temp.loc[u, i]
                    temp.loc[u, i] = np.nan

                    p = predict_user_based(temp, u, i)

                    if not np.isnan(p):
                        actuals.append(actual)
                        preds.append(p)

        actuals = np.array(actuals)
        preds = np.array(preds)

        rmse = np.sqrt(np.mean((actuals - preds) ** 2))
        mae = np.mean(np.abs(actuals - preds))

        return rmse, mae

    # VISUALIZATION
    def plot_matrix(matrix, title):
        plt.figure()
        plt.imshow(matrix, aspect='auto')
        plt.title(title)
        plt.colorbar()
        plt.show()

    # MAIN EXECUTION
    def main():
        df = load_data()

        # Mean Centering
        user_mean, df_centered = mean_center(df)
        print("\nMean Centered Matrix:\n", df_centered)

        
        #for u in df.index:
        #    for i in df.columns:
        #        if np.isnan(df.loc[u, i]):
        #            user_pred_matrix.loc[u, i] = predict_user_based(df, u, i)
        #            item_pred_matrix.loc[u, i] = predict_item_based(df, u, i)

        #print("\nUser-Based Prediction Matrix:\n", user_pred_matrix)
        #print("\nItem-Based Prediction Matrix:\n", item_pred_matrix)
        

        # Prediction
        user = "User1"
        item = "Item4"

        user_pred = predict_user_based(df, user, item)
        item_pred = predict_item_based(df, user, item)

        print("\nUser-Based Prediction:\n", user_pred)
        print("\nItem-Based Prediction:\n", item_pred)

        # Evaluation
        rmse, mae = evaluate(df)
        print("\nEvaluation Metrics:")
        print("RMSE =", rmse)
        print("MAE =", mae)

        # Fill NaNs for visualization
        user_pred_matrix = df.copy().fillna(df.mean().mean())
        item_pred_matrix = df.copy().fillna(df.mean().mean())

        # Plots
        plot_matrix(df.fillna(0), "Original Matrix")
        plot_matrix(df_centered.fillna(0), "Mean Centered Matrix")
        plot_matrix(user_pred_matrix, "User-Based Predicted Matrix")
        plot_matrix(item_pred_matrix, "Item-Based Predicted Matrix")


    if __name__ == "__main__":
        main()""")


recommend()
