input: 
  def perform_sentiment_analysis(reviews: pd.DataFrame) -> str:
      """Performs sentiment analysis on product reviews using natural language processing techniques like tokenization, stopwords removal, and n-gram features. 
      It uses a Naive Bayes classifier to assign sentiments to the reviews and returns the classification report.
      >>> reviews = pd.DataFrame({
      ...     'review': [
      ...         "I love this product!",
      ...         "This is the worst product ever.",
      ...         "Could be better, but not bad."
      ...     ],
      ...     'sentiment': ['positive', 'negative', 'neutral']
      ... })
      >>> perform_sentiment_analysis(reviews)
      ('              precision    recall  f1-score   support\n\n'
       '    negative       1.00      1.00      1.00         1\n'
       '     neutral       1.00      1.00      1.00         1\n'
       '    positive       1.00      1.00      1.00         1\n\n'
       '    accuracy                           1.00         3\n'
       '   macro avg       1.00      1.00      1.00         3\n'
       'weighted avg       1.00      1.00      1.00         3\n')
      """
      # Tokenise the texts
      reviews['tokenised'] = reviews['review'].apply(word_tokenize)
  
      # Remove stopwords
      stop_words = set(stopwords.words('english'))
      reviews['filtered'] = reviews['tokenised'].apply(lambda x: [val for val in x if val not in stop_words])
  
      # Normalize words
      wordnet_lemmatizer = WordNetLemmatizer()
      reviews['normalised'] = reviews['filtered'].apply(lambda x: [wordnet_lemmatizer.lemmatize(val) for val in x])
  
      # Generate features using CountVectorizer
      cv = CountVectorizer()
      reviews['features'] = cv.fit_transform(reviews['normalised'].astype('str')).toarray()
  
      # Split into training and test sets
      X = reviews['features']
      y = reviews['sentiment']
      X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
  
      # Train model
      model = MultinomialNB(alpha=0.1)
      model.fit(X_train, y_train)
  
      # Predict using the test set
      predictions = model.predict(X_test)
  
      # Generate a classification report
      report = classification_report(y_test, predictions)
      return report
  
output:     
      # Tokenise the texts
      reviews['tokenised'] = reviews['review'].apply(word_tokenize)
  
      # Remove stopwords
      stop_words = set(stopwords.words('english'))
      reviews['filtered'] = reviews['tokenised'].apply(lambda x: [val for val in x if val not in stop_words])
  
      # Normalize words
      wordnet_lemmatizer = WordNetLemmatizer()
      reviews['normalised'] = reviews['filtered'].apply(lambda x: [wordnet_lemmatizer.lemmatize(val) for val in x])
  
      # Generate features using CountVectorizer
      cv = CountVectorizer()
      reviews['features'] = cv.fit_transform(reviews['normalised'].astype('str')).toarray()
  
      # Split into training and test sets
      X = reviews['features']
      y = reviews['sentiment']
      X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
  
      # Train model
      model = MultinomialNB(alpha=0.1)
      model.fit(X_train, y_train)
  
      # Predict using the test set
      predictions = model.predict(X_test)
  
      # Generate a classification report
      report = classification_report(y_test, predictions)
      return report
  