<jupyter_start><jupyter_text>Ratings Prediction Project  Introduction> This project is related a website where people write different reviews for technical products. Now a new feature has been added to website i.e., the reviewer will have to add stars(rating) as well with the review. The rating is out 5 stars and it only has 5 options available 1 star, 2 stars, 3 stars, 4 stars, 5 stars. Now the requirement is to predict ratings for the reviews which were written in the past for which website don’t have a rating. So, we have to build an application which can predict the rating by seeing the review.  About DatasetData has been collected from different e-commerce website(s) and stored in .CSV (Comma Seperated Values) format which consists of **31585 records with 3 features** as follow:1. **`Ratings:`** Rating of the product i.e., 1, 2, 3, 4, & 5.2. **`Review Title:`** Title for the review.3. **`Review Description:`** Description of review.  Required Libraries<jupyter_code>#importing required libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from nltk.corpus import stopwords
from wordcloud import WordCloud

#suppressing warnings
import warnings
warnings.filterwarnings('ignore')<jupyter_output><empty_output><jupyter_text>Load Dataset<jupyter_code>#loading dataset
df_rr = pd.read_csv('ratings-reviews-dataset.csv',index_col=0)

#interpreting dataset
df_rr<jupyter_output><empty_output><jupyter_text>Remarks:- There are **31585 records with 3 features** present.- Since, the feature `Review Titles` and `Review Descriptions` are denoted same thing i.e., `Review Texts` therefore, these features can be merged to a single feature.  Data Analysis: Cleaning & Preparation<jupyter_code>#copying dataframe to another dataframe object
df = df_rr.copy()

#Merging feature Review Titles and Review Descriptions to Review Titles
df['review_text'] = df['Review Titles'].apply(lambda x: str(x)+" ") + df['Review Descriptions']

#Rename column Ratings to rating
df.rename(columns={'Ratings':'rating'},inplace=True)

#Dropping feature Review Titles and Review Descriptions as these are already merged into review_text
df.drop(columns=['Review Titles','Review Descriptions'],inplace=True)

#interpreting first 5 rows of new dataset
df.head()
#checking general informations
df.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
Int64Index: 31585 entries, 0 to 31584
Data columns (total 2 columns):
 #   Column       Non-Null Count  Dtype 
---  ------       --------------  ----- 
 0   rating       31585 non-null  int64 
 1   review_text  31530 non-null  object
dtypes: int64(1), object(1)
memory usage: 740.3+ KB<jupyter_text>Remarks:- There are two features out of which one is of int type and other is object type.- Non-Null Count suggests there are some null values present in feature review_text.<jupyter_code>#checking null values
df.isnull().sum()
#dropping null values rows
df.dropna(inplace=True)
#checking shape of dataset
print(f"Original Shape: {df_rr.shape}")
print(f"New Shape: {df.shape}")
#converting all the review_text to lowercase
df.review_text = df.review_text.str.lower()

#interpreting first 5 rows
df.head()<jupyter_output><empty_output><jupyter_text>Remark:- All the review text has been converted to lowercase.  Exploratory Data Analysis (EDA) & Visualization  1. Univariate Analysis<jupyter_code>#checking feature rating using count plot
x = 'rating'
fig, ax = plt.subplots(1,1,figsize=(8,6))
sns.countplot(x=x,data=df,ax=ax)
p=0
for i in ax.patches:
    q = i.get_height()/2
    val = i.get_height()
    ratio = round(val*100/len(df),2)
    prn = f"{val}\n({ratio}%)"
    ax.text(p,q,prn,ha="center",color="white",rotation=0,fontweight="bold",fontsize="13")
    p += 1
    
plt.title("Count Plot: rating")
plt.show()<jupyter_output><empty_output><jupyter_text>Remarks:- There are almost equal number of records are availble for all ratings i.e. from 1 to 5.  Checking Distribution of Review Text length for each rating using distribution plot<jupyter_code>#adding a new column length for review text length
df['length'] = df.review_text.str.len()

#interpreting first 5 rows
df.head()
#checking review text length distribution for each rating
ratings = np.sort(df.rating.unique())
cols = 3
rows = len(ratings)//cols
if rows % cols != 0:
    rows += 1
    
fig = plt.figure(figsize=(15,6))
plt.subplots_adjust(hspace=0.6, wspace=0.5)
p = 1
colors = [(1,0,0,1),(0.6,0.2,0,1),(0.4,0.5,0,1),(0.2,0.7,0,1),(0,1,0.1,1)]
for i in ratings:
    axis = fig.add_subplot(rows,cols,p)
    sns.distplot(df.length[df.rating==i],ax=axis,label=f"For Rating: {i}",color=colors[i-1])
    axis.set_xlabel(f"Review Text Length")
    axis.legend()
    p += 1
    
plt.show()<jupyter_output><empty_output><jupyter_text>Remarks:- Rating 2, 3, 4 has almost similer review text length and higher than Rating 1 and 5.- Rating 5 has lowest review text lengths.  Removing and Replacing certain terms using `Regular Expression`<jupyter_code># Replace money sysmbols with 'dollars' Euro symbol: Alt + 156
df.review_text = df.review_text.str.replace(r'₹|\£|\$','dollars')

# Replace number with 'numbr'
df.review_text = df.review_text.str.replace(r'\d+(\.\d+)?','numbr')

# Remove punctuation
df.review_text = df.review_text.str.replace(r'[^\w\d\s]',' ')

# Replace white space between term with single space
df.review_text = df.review_text.str.replace(r'\s+',' ')

# Remove leading and trailing whitespaces
df.review_text = df.review_text.str.replace(r'^\s+|\s+?$','')
#interpreting first 5 rows
df.head()<jupyter_output><empty_output><jupyter_text>Remove Stop Words<jupyter_code>#setting stop words
stop_words = set(stopwords.words('english') + ['u','ur','ure','im','gd','dont','doin'])

#removing stop words
df.review_text = df.review_text.apply(lambda x: ' '.join(word for word in x.split() if word not in stop_words))
#Adding new column clean_length for review_text length after cleaning
df['clean_length'] = df.review_text.str.len()

#Interpreting first 5 rows
df.head()
#Checking the difference between original review text length and cleaned length
print(f"Orignal Review Text Length: {df.length.sum()}")
print(f"Cleaned Review Text Length: {df.clean_length.sum()}")
print(f"Difference: {df.length.sum() - df.clean_length.sum()}")<jupyter_output>Orignal Review Text Length: 5196793
Cleaned Review Text Length: 3686053
Difference: 1510740<jupyter_text>Remarks:- Review text length reduced by greater length after removal of stop words and punctuations.<jupyter_code>#checking review text length distribution for each rating after cleaning
ratings = np.sort(df.rating.unique())
cols = 3
rows = len(ratings)//cols
if len(ratings) % cols != 0:
    rows += 1
    
fig = plt.figure(figsize=(15,6))
plt.subplots_adjust(hspace=0.6, wspace=0.5)
p = 1
colors = [(1,0,0,1),(0.6,0.2,0,1),(0.4,0.5,0,1),(0.2,0.7,0,1),(0,1,0.1,1)]
for i in ratings:
    axis = fig.add_subplot(rows,cols,p)
    sns.distplot(df.clean_length[df.rating==i],ax=axis,label=f"For Rating: {i}",color=colors[i-1])
    axis.set_xlabel(f"Review Text Length")
    axis.legend()
    p += 1

plt.show()<jupyter_output><empty_output><jupyter_text>Remarks:- Review text length reduced by almost 1000 characters for Rating 1 to 4 while for Rating 5 it is almost 200 characters  Displaying with Word Cloud<jupyter_code># getting sense of loud word in each rating
cols = 2
rows = len(ratings)//2
if len(ratings) % cols != 0:
    rows += 1
    
fig = plt.figure(figsize=(15,20))
plt.subplots_adjust(hspace=0.3)
p = 1
for i in ratings:
    word_cloud = WordCloud(height=800, width=1000, background_color="white",max_words=30).generate(' '.join(df.review_text[df.rating==i]))
    axis = fig.add_subplot(rows,cols,p)
    axis.set_title(f"WordClound for Rating: {i}")
    axis.imshow(word_cloud)
    for spine in axis.spines.values():
        spine.set_edgecolor('r')
    axis.set_xticks([])
    axis.set_yticks([])
    
    plt.tight_layout(pad=5)
    p += 1
    
plt.show()<jupyter_output><empty_output><jupyter_text>Remarks:**`for Rating: 1`**- It mostly consists of words like laptop, waste, money, slow, worst, issue, horrible etc.**`for Rating: 2`**- It mostly consists of words like phone, good, printer, product, problem, issue, bad, poor, slow etc.**`for Rating: 3`**- It mostly consists of words like phone, good, laptop, problem, bad, issue, slow, life, average, nice etc.**`for Rating: 4`**- It mostly consists of words like laptop, good, value, money, nice, performance, great, better, wonderful etc.**`for Rating: 5`**- It mostly consists of words like laptop, excellent, must buy, great, perfect, super, awesome, mind blowing etc.  EDA Concluding Remarks:> Starting with univariate analysis, with the help of countplot, it was found that the data consists of almost in equal amount for each rating (i.e., from 1 to 5). Moving further with the removal and replacement of certain terms (like, punctuations, extra spaces, numbers, money symbols) as well as removal of stop words, it was evident that the length of review text decreases by a large amount. This was also dipcted by using distribution plot. With the help of wordcloud, it was found that the rating 1 consists of words like waste, money, slow, worst, issue, horrible etc, rating 2 consists of words like problem, issue, bad, poor, slow etc., rating 3 consists of word like problem, bad, issue, slow, life, average, nice etc, rating 4 consists of word like good, value, money, nice, performance, great, better, wonderful etc. and rating 5 consists of words like excellent, must buy, great, perfect, super, awesome, mind blowing etc.  Preparing Data for Model Training & Testing<jupyter_code>#importing required libraries
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import SGDClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
import tqdm.notebook as tqdm
import sys, timeit
from IPython.display import display<jupyter_output><empty_output><jupyter_text>1. Convert Text into Vectors<jupyter_code>#converting text into vectors using TF-IDF
tfidf = TfidfVectorizer()

#vectorizing text features
features = tfidf.fit_transform(df.review_text)<jupyter_output><empty_output><jupyter_text>2. Seperating Input and Output variables<jupyter_code>#Input Variable
X = features

#Output Variable
Y = df.rating<jupyter_output><empty_output><jupyter_text>3. Training & Testing Model<jupyter_code>#function to get best random state
def get_best_random_state(model,X,Y,t_size=0.25,rs_range=range(1,301,50)):
    best_rstate = 0
    best_accuracy_score = 0
    random_state_message = "\r"
    
    for i in tqdm.tqdm(rs_range,desc=f"Best_Random_State => {model}"):
        X_train, X_test, Y_train, Y_test = train_test_split(X,Y,test_size=t_size,random_state=i)
        model.fit(X_train, Y_train)
        y_pred = model.predict(X_test)
        a_score = accuracy_score(Y_test,y_pred)
        
        if a_score > best_accuracy_score:
            best_accuracy_score = a_score
            best_rstate = i
            
        random_state_message += f"[{i}: {round(a_score*100,2)}]<--->"
        sys.stdout.write(random_state_message)
        
    sys.stdout.write(f"\n\nBest Random State: {best_rstate} found with Accuracy: {best_accuracy_score}")
    return best_rstate, best_accuracy_score
#End of function


#function to get best cv score
def get_best_cv(model,X_train,Y_train,parameters,cv_range=range(5,25,5)):
    best_cv_score = 0
    best_cv = 0
    
    cv_message = "\r"
    for i in tqdm.tqdm(cv_range,desc=f"Best_CV => {model}"):
        gscv = GridSearchCV(model,parameters)
        gscv.fit(X_train,Y_train)
        
        cv_score = cross_val_score(gscv.best_estimator_,X_train,Y_train,cv=i).mean()
        
        if cv_score > best_cv_score:
            best_cv_score = cv_score
            best_cv = i
        
        cv_message += f"[{i}:{round(cv_score*100,2)}]<--->"
        sys.stdout.write(cv_message)
    
    sys.stdout.write(f"\n\nBest CV: {best_cv} found with Cross Val Score: {best_cv_score}")
    
    return best_cv, best_cv_score
#End of function

#function to build models
def build_models(models,X,Y,t_size=0.25,rs_range=range(1,301,50),cv_range=range(5,25,5)):
    for i in tqdm.tqdm(models,desc="Building Models"):
        sys.stdout.write("\n=======================================================================================\n")
        sys.stdout.write(f"Current Model in Progress: {i} ")
        sys.stdout.write("\n=======================================================================================\n")
        
        #start time
        start_time = timeit.default_timer()
        
        #Find the best random state
        best_random_state, best_accuracy_score = get_best_random_state(models[i]['name'],X,Y,t_size,rs_range)
        sys.stdout.write("\n")
        
        #Spliting train and test data using train_test_split method with best random state value
        X_train,X_test,Y_train,Y_test = train_test_split(X,Y,test_size=t_size,random_state=best_random_state)
        
        #Find the best CV
        best_cv, best_cv_score = get_best_cv(models[i]['name'],X_train,Y_train,models[i]['parameters'],cv_range)
        sys.stdout.write("\n\nBuilding Model...")
        
        #Training the model using best CV
        gscv = GridSearchCV(models[i]['name'],models[i]['parameters'],cv=best_cv)
        gscv.fit(X_train,Y_train)
        
        #Testing model
        y_pred = gscv.best_estimator_.predict(X_test)
        
        #Recording model performance
        model_accuracy_score = accuracy_score(Y_test,y_pred)
        model_confusion_matrix = confusion_matrix(Y_test,y_pred)
        model_classification_report = classification_report(Y_test,y_pred)
        
        #end time
        end_time = timeit.default_timer()
        sys.stdout.write(f"Completed in [{end_time-start_time} sec.]")
        
        #storing model specifications
        models[i]['initial_accuracy_score'] = best_accuracy_score
        models[i]['best_random_state'] = best_random_state
        models[i]['x_train'] = X_train
        models[i]['x_test'] = X_test
        models[i]['y_train'] = Y_train
        models[i]['y_test'] = Y_test
        models[i]['best_cv'] = best_cv
        models[i]['best_cv_score'] = best_cv_score
        models[i]['gscv'] = gscv
        models[i]['y_predict'] = y_pred
        models[i]['final_accuracy'] = model_accuracy_score
        models[i]['confusion_matrix'] = model_confusion_matrix
        models[i]['classification_report'] = model_classification_report
        models[i]['build_time'] = f"{end_time - start_time} (in sec.)"
        
        sys.stdout.write("\n=======================================================================================\n\n\n")
    
    return models
#End of function

#function to display model performance
def display_performance(models):
    model_names = []
    model_initial_score = []
    model_cross_val_score = []
    model_final_score = []
    model_build_time = []
    for i in models:
        model_names.append(i)
        model_initial_score.append(models[i]['initial_accuracy_score'])
        model_cross_val_score.append(models[i]['best_cv_score'])
        model_final_score.append(models[i]['final_accuracy'])
        model_build_time.append(models[i]['build_time'])
    
    model_df = pd.DataFrame({
        "Name": model_names,
        "Initial Score": model_initial_score,
        "Cross Val Score": model_cross_val_score,
        "Final Score": model_final_score,
        "Build Time": model_build_time,
    })
    
    model_df['Difference (Final Score - Cross Val Score)'] = model_df['Final Score'] - model_df['Cross Val Score']
    display(model_df)
    
    for i in models:
        print("========================================================")
        print(f"for model: {i}")
        print("========================================================")
        print("CLASSIFICATION REPORT")
        print(models[i]['classification_report'])
        print("CONFUSION MATRIX")
        print(models[i]['confusion_matrix'])
        print("=======================================================\n\n")
        
    return
#End of function
#List of models for training & testing
models = {
    "MultinomialNB":{
        "name": MultinomialNB(),
        "parameters":{
            "alpha": [1.0]                
        }
    },
    "SGDClassifier":{
        "name": SGDClassifier(),
        "parameters":{
            "loss":['hinge','modified_huber'],
            "alpha":[0.001,0.0001,0.00001],
            "n_jobs":[-1],
            "learning_rate":['optimal'],
            "max_iter":[100]
        }
    },
    "KNeighborsClassifier":{
        "name": KNeighborsClassifier(),
        "parameters":{
            "n_neighbors": [5,10],
            "weights": ['uniform','distance'],
            "n_jobs": [-1]
        }
    },
    "DecisionTreeClassifier":{
        "name": DecisionTreeClassifier(),
        "parameters":{
            "criterion": ['gini','entropy'],
            "splitter": ['best','random']
        }
    }
}
#building models
build_model = build_models(models,X,Y)
#displaying model performances
display_performance(models)<jupyter_output><empty_output><jupyter_text>Remarks:> from the above model comparision it is clear that **SGDClassifier** performs better with **Accuracy Score: 75.92% and Cross Val Score: 75.08%** than other models. Therefore, proceeding with SGDClassifier.  Model Saving or Serialization<jupyter_code>import joblib
#selecting best model
best_model = models['SGDClassifier']

#saving model
joblib.dump(best_model['gscv'].best_estimator_,open('ratings-reviews.obj','wb'))<jupyter_output><empty_output><jupyter_text>Conclusion<jupyter_code>model_conclusion = pd.DataFrame({
    "Original": best_model['y_test'],
    "Predicted": best_model['y_predict']
})
model_conclusion<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1414974968366617, 'fraction_numerical': 0.017274577763107223, 'mean_word_length': 4.2310791366906475, 'pattern_counts': {'":': 30, '<': 66, '<?xml version=': 0, '>': 73, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '46086', 'license': 'MIT', 'n_tokens_mistral': 6024, 'n_tokens_neox': 5740, 'n_words': 1524, 'path': 'ratings-project/ratings-reviews-project.ipynb', 'repo_name': 'ashok-kumar-sharma/ds-internship', 'size': '18177'}