from nltk.sentiment import SentimentAnalyzer
from nltk.sentiment.vader import SentimentIntensityAnalyzer
from nltk.sentiment.util import *
from nltk import tokenize
from nltk.tokenize import sent_tokenize
from nltk.corpus import stopwords
import pandas as pd
import numpy as np
import nltk
from collections import Counter
import re
import math
import html
import sklearn
import sklearn.metrics as metrics
from sklearn.metrics import mutual_info_score
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer, ENGLISH_STOP_WORDS
import matplotlib.pyplot as plt
import seaborn as sns
from pprint import pprint
import contractions
from nltk.tokenize import word_tokenize
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix

%matplotlib inline


Extract the n-grams and pmi rescoring for uni-gram
The code defines a function, getPMI_ngram, which calculates Pointwise Mutual Information (PMI) for n-gram features in a DataFrame's 'Review Text' column. It uses a CountVectorizer to convert the text data into a count vector, computes the PMI values for each feature with respect to a specified ground truth class, and returns a DataFrame of PMI values sorted in descending order. The PMI reflects the strength of association between words and the ground truth class in the given text data.



# Function to calculate Pointwise Mutual Information (PMI) for n-grams
def getPMI_ngram(df, gt, ngram_range=(1, 1), max_features=1000):
    # Create a CountVectorizer with specified n-gram range, stop words, and maximum features
    vectorizer = CountVectorizer(ngram_range=ngram_range, stop_words="english", max_features=max_features)
    
    # Transform the 'Review Text' column into a count vector
    countvector = vectorizer.fit_transform(df['Review Text'])
    
    # Convert the count vector to a dense NumPy array
    densevector = np.array(countvector.todense())
    
    # Calculate the probability of the ground truth class
    px = sum(df['Recommended'] == gt) / len(df)
    
    # Initialize an empty list to store PMI values
    pmis = []
    
    # Loop over each feature (word) in the count vector
    for i in range(max_features):
        # Calculate the probability of the feature occurring in any review
        py = sum(densevector[:, i] == 1) / len(df)
        
        # Calculate the joint probability of the feature occurring with the ground truth class
        pxy = len(df[(df['Recommended'] == gt) & (densevector[:, i] == 1)]) / len(df)
        
        # Calculate PMI, handling the case when the joint probability is zero
        if pxy == 0:
            pmi = math.log10((pxy + 0.0001) / (px * py))
        else:
            pmi = math.log10(pxy / (px * py))
        
        # Append the calculated PMI to the list
        pmis.append(pmi)
    
    # Create a DataFrame with PMI values and feature names as index
    pmis = pd.DataFrame(data={'pmi' + gt: pmis}, index=vectorizer.get_feature_names_out())
    
    # Sort the DataFrame by PMI values in descending order
    return pmis.sort_values('pmi' + gt, ascending=False)
	
pmiPos_unigram = None
sid_pmi = None
sid = None



Experiment
Instantiate SentimentIntensityAnalyzer:

Two instances of the SentimentIntensityAnalyzer class are created: sid and sid_pmi.

Set Up Stopwords:

A set of English stopwords is defined using the NLTK library.

Text Preprocessing Functions:

Several functions are defined for text preprocessing:
removeContraction: Removes contractions from a given review.
removeStopwords: Removes stopwords from a given review.
removeSymbols: Removes specific symbols from a given review.

Sentiment Analysis Functions:

Two functions are defined for sentiment analysis using VADER:
findSentimentVADER: Calculates sentiment scores using VADER for a given review.
findSentimentPMI: Calculates sentiment scores using VADER with PMI for a given review.

Main Processing Function (process):

Reads a dataset from the CSV file "skytrax_reviews.csv."
Prints the number of records in the dataset.

Dataset Splitting (Optional):

If specified, the dataset is split based on recommendation and rating conditions.
Records meeting the conditions for recommendation are separated into "yes," and others into "no."
The final dataset is then printed.

Text Preprocessing:

Optionally removes contractions, symbols, and stopwords from the 'Review Text' column based on user preferences.

VADER Sentiment Analysis:

Calculates sentiment scores using default VADER and updates the dataset.
Prints confusion matrix and classification report for the VADER sentiment analysis.
Plots a histogram of VADER scores and saves it as "hist10bins_VADER.png."

Update VADER Scores with PMI:

If pmiPos_unigram is not None, it calculates PMI for positive unigrams and updates VADER scores.
Prints a message indicating the use of PMI.

Sentiment Analysis with PMI Updated VADER:

Calculates sentiment scores using VADER with PMI and updates the dataset.
Prints confusion matrix and classification report for the sentiment analysis with PMI.
Plots a histogram of PMI updated VADER scores and saves it as "hist10bins_stop_words_removed.png."

Return Processed Dataset:

Returns the processed dataset.

These steps collectively represent a sentiment analysis pipeline that includes text preprocessing, sentiment analysis using VADER, and enhancement of VADER scores using PMI for positive unigrams. The analysis results are printed, and visualizations are generated for both the default VADER and the PMI updated VADER sentiment analyses.


#Instantiate an instance to access SentimentIntensityAnalyzer class
sid = SentimentIntensityAnalyzer()
sid_pmi = SentimentIntensityAnalyzer()

# Set of English stopwords
stop_words = set(stopwords.words('english'))

# Function to remove contractions in a given review
def removeContraction(review):
    review = contractions.fix(review)
    return review

# Function to remove stopwords from a given review
def removeStopwords(review):
    tokens = word_tokenize(review.lower())
    filtered_text = [t for t in tokens if not t in stop_words]
    return " ".join(filtered_text)

# Function to remove specific symbols from a given review
def removeSymbols(review):
    review = review.replace('"',"")
    review = review.replace(')',"")
    review = review.replace('(',"")
    review = review.replace('-'," ")
    review = review.replace(','," ")
    return review

# Function to find sentiment using VADER for a given review
def findSentimentVADER(review):    
    vader_score = sid.polarity_scores(review)["compound"]
    return vader_score

# Function to find sentiment using VADER with PMI for a given review
def findSentimentPMI(review):    
    vader_score = sid_pmi.polarity_scores(review)["compound"]
    return vader_score

# Function to process the dataset and perform sentiment analysis
def process(pmiPos_unigram, split="NO", split_yes_condition=None, remove_stopword="Yes", remove_symbols="No", remove_shortform="Yes"):
    
    # Read the dataset from a CSV file
    airportDf = pd.read_csv(r'Data//skytrax_reviews.csv')
    print("Dataset Contains {} records".format(airportDf.shape[0]))
    
    # Split the dataset based on recommendation and rating conditions
    if split == "YES":
        yes = airportDf[(airportDf["Recommended"] == "yes")]
        yes = yes[yes["Review Rating"] >= split_yes_condition]
        print("Dataset Contains {} records (recommended)".format(yes.shape[0]))
        
        no = airportDf[(airportDf["Recommended"] == "no")]
        no = no[no["Review Rating"] < split_yes_condition]
        print("Dataset Contains {} records (Not recommended)".format(no.shape[0]))
        
        airportDf = pd.concat([yes, no])
        print("Final Dataset after spliting Contains {} records".format(airportDf.shape[0]))
    
    # Remove contractions from the reviews
    if remove_shortform == "Yes":
        airportDf['Review Text'] = airportDf['Review Text'].apply(removeContraction)
    
    # Remove symbols from the reviews
    if remove_symbols == "Yes":
        airportDf['Review Text'] = airportDf['Review Text'].apply(removeSymbols)
    
    # Remove stopwords from the reviews
    if remove_stopword == "Yes":
        airportDf['Review Text'] = airportDf['Review Text'].apply(removeStopwords)
    
    airportDf['vaderScore'] = airportDf['Review Text'].apply(findSentimentVADER)
    
    airportDf.loc[airportDf["vaderScore"] < 0, "vaderRecommendation"] = "no"
    airportDf.loc[airportDf["vaderScore"] >= 0, "vaderRecommendation"] = "yes"
    
    print("\n\nDefault VADER\n\n")
    print(confusion_matrix(airportDf['Recommended'], airportDf['vaderRecommendation']))
    print("--------------------")
    print(classification_report(airportDf['Recommended'], airportDf['vaderRecommendation']))
    
    plt.rcParams.update({'font.size': 18})
    plt.figure(figsize=[14, 10])
    plt.hist(airportDf['vaderScore'], bins=20)
    plt.ylabel("Frequency of Reviews")
    #plt.title("Sentiment Analysis with Default VADER")
    plt.savefig("hist10bins_VADER.png")
    plt.show()
    
    #Update VADER scores with PMI 
    if pmiPos_unigram is None:    
        pmiPos_unigram = getPMI_ngram(airportDf, 'yes')
        new_pos_words = pmiPos_unigram.to_dict('dict')["pmiyes"]
        sid_pmi.lexicon.update(new_pos_words)
        
    print("\n\PMI Updated VADER\n\n")
    
    airportDf['pmiScoreNoStops'] = airportDf['Review Text'].apply(findSentimentPMI)
    airportDf.loc[airportDf["pmiScoreNoStops"] < 0, "stopPMIRecommendation"] = "no"
    airportDf.loc[airportDf["pmiScoreNoStops"] >= 0, "stopPMIRecommendation"] = "yes"
    
    print(confusion_matrix(airportDf['Recommended'], airportDf['stopPMIRecommendation']))
    print("--------------------")
    print(classification_report(airportDf['Recommended'], airportDf['stopPMIRecommendation']))
    
    plt.rcParams.update({'font.size': 18})
    plt.figure(figsize=[14, 10])
    plt.hist(airportDf['pmiScoreNoStops'], bins=20)
    plt.ylabel("Frequency of Reviews")
    #plt.title("Sentiment Analysis with PMI Updated VADER")
    plt.savefig("hist10bins_stop_words_removed.png")
    plt.show()
    
    return airportDf
	
	
Experiment 1
Don't Split Data
Don't Remove Stopwords
Don't Remove Symols
Don't expand Shortforms

df = process(pmiPos_unigram, split="NO", split_yes_condition=None, remove_stopword="No", remove_symbols="No", remove_shortform="No")

Dataset Contains 38854 records


Default VADER


[[16632 12432]
 [ 1355  8435]]
--------------------
              precision    recall  f1-score   support

          no       0.92      0.57      0.71     29064
         yes       0.40      0.86      0.55      9790

    accuracy                           0.65     38854
   macro avg       0.66      0.72      0.63     38854
weighted avg       0.79      0.65      0.67     38854



\PMI Updated VADER


[[22464  6600]
 [ 3032  6758]]
--------------------
              precision    recall  f1-score   support

          no       0.88      0.77      0.82     29064
         yes       0.51      0.69      0.58      9790

    accuracy                           0.75     38854
   macro avg       0.69      0.73      0.70     38854
weighted avg       0.79      0.75      0.76     38854


Experiment 2
Split Data (condition if rating >= 5, then yes)
Dont Remove Stopwords
Dont Remove Symols
Dont Expand Shortforms

df = process(pmiPos_unigram, split="YES", split_yes_condition=5, remove_stopword="No", remove_symbols="No", remove_shortform="No")

Dataset Contains 38854 records
Dataset Contains 9186 records (recommended)
Dataset Contains 22765 records (Not recommended)
Final Dataset after spliting Contains 31951 records


Default VADER


[[14201  8564]
 [ 1070  8116]]
--------------------
              precision    recall  f1-score   support

          no       0.93      0.62      0.75     22765
         yes       0.49      0.88      0.63      9186

    accuracy                           0.70     31951
   macro avg       0.71      0.75      0.69     31951
weighted avg       0.80      0.70      0.71     31951



\PMI Updated VADER


[[18866  3899]
 [ 2567  6619]]
--------------------
              precision    recall  f1-score   support

          no       0.88      0.83      0.85     22765
         yes       0.63      0.72      0.67      9186

    accuracy                           0.80     31951
   macro avg       0.75      0.77      0.76     31951
weighted avg       0.81      0.80      0.80     31951



Experiment 3
Split Data (condition if rating >= 4, then yes)
Dont Remove Stopwords
Dont Remove Symols
Dont Expand Shortforms

df = process(pmiPos_unigram, split="YES", split_yes_condition=4, remove_stopword="No", remove_symbols="No", remove_shortform="No")

Dataset Contains 38854 records
Dataset Contains 9461 records (recommended)
Dataset Contains 19686 records (Not recommended)
Final Dataset after spliting Contains 29147 records


Default VADER


[[13255  6431]
 [ 1181  8280]]
--------------------
              precision    recall  f1-score   support

          no       0.92      0.67      0.78     19686
         yes       0.56      0.88      0.69      9461

    accuracy                           0.74     29147
   macro avg       0.74      0.77      0.73     29147
weighted avg       0.80      0.74      0.75     29147



\PMI Updated VADER


[[17294  2392]
 [ 2759  6702]]
--------------------
              precision    recall  f1-score   support

          no       0.86      0.88      0.87     19686
         yes       0.74      0.71      0.72      9461

    accuracy                           0.82     29147
   macro avg       0.80      0.79      0.80     29147
weighted avg       0.82      0.82      0.82     29147




Table 3 Generation

positives = ["breeze", "smooth", "Simple", "Spacious", "Choices","Organized","Ease","Quick","Polite","Quickly","Kind"]

negatives = ["joke", "Costs", "Late","waiting","Delays","Worst","Unhelpful","zero","Disorganised","Disgrace", "joke"]

def createTable3():
    if len(positives) != len(negatives):
        print("Please provide equal positive and negative words")
        return
    else:
        print("Table 3")
    
    result = []
    for i in range(len(positives)):
        vader_pos = sid.polarity_scores(positives[i].lower())["compound"]
        vader_neg = sid.polarity_scores(negatives[i].lower())["compound"]
        
        pmi_vader_pos = sid_pmi.polarity_scores(positives[i].lower())["compound"]
        pmi_vader_neg = sid_pmi.polarity_scores(negatives[i].lower())["compound"]
        
        result.append([positives[i], pmi_vader_pos, vader_pos, negatives[i], pmi_vader_neg, vader_neg])
    
    import pandas as pd
    df = pd.DataFrame(data=result, columns=["Positive Lexicons","PMI Score","Default Vader Score","Negative Lexicons","PMI Score","Default Vader Score"])
    
    return df

createTable3()

Table 3
Positive Lexicons	PMI Score	Default Vader Score	Negative Lexicons	PMI Score	Default Vader Score
0	breeze	0.0832	0.0000	joke	-0.1741	0.2960
1	smooth	0.0916	0.0000	Costs	-0.1137	0.0000
2	Simple	0.0291	0.0000	Late	-0.0311	0.0000
3	Spacious	0.0859	0.0000	waiting	-0.0131	0.0000
4	Choices	0.0480	0.0000	Delays	-0.0145	0.0000
5	Organized	0.0477	0.0000	Worst	-0.1867	-0.6249
6	Ease	0.3612	0.3612	Unhelpful	-0.1760	0.0000
7	Quick	0.0689	0.0000	zero	0.0000	0.0000
8	Polite	0.0662	0.0000	Disorganised	-0.1741	0.0000
9	Quickly	0.0512	0.0000	Disgrace	-0.1773	-0.4939
10	Kind	0.0183	0.5267	joke	-0.1741	0.2960


Table 4 Generation

sentences = ["Quick check-in and polite staff",
             "Very smooth departure from Addis Ababa Airport.",
             "I do not know if I have hometown bias, but in terms of overall ease of use to and cleanliness.",
             "Forty minutes for luggage to arrive on carousel.",
             "The toilets were spotless."]

columns = ["", "Random Sentences from Reviews","Proposed Lexicon’s Score","VADER Score"]

def createTable4():
    
    result = []
    
    count = 0
    
    for sent in sentences:
        count = count+1
        pmi_neg = sid_pmi.polarity_scores(sent.lower())['neg']
        pmi_neu = sid_pmi.polarity_scores(sent.lower())['neu']
        pmi_pos = sid_pmi.polarity_scores(sent.lower())['pos']
        pmi_compound = sid_pmi.polarity_scores(sent.lower())['compound']
        
        vader_neg = sid.polarity_scores(sent.lower())['neg']
        vader_neu = sid.polarity_scores(sent.lower())['neu']
        vader_pos = sid.polarity_scores(sent.lower())['pos']
        vader_compound = sid.polarity_scores(sent.lower())['compound']
        
        result.append([count, sent, "Negative:  "+str(pmi_neg), "Negative:  "+str(vader_neg)])
        result.append(["", "", "Neutral:   "+str(pmi_neu), "Neutral:   "+str(vader_neu)])
        result.append(["", "", "Positive:  "+str(pmi_pos), "Positive:  "+str(vader_pos)])
        result.append(["", "", "Compound:  "+str(pmi_compound), "Compound:  "+str(vader_compound)])
        
        
    import pandas as pd
    df = pd.DataFrame(data=result, columns=columns, index=None)
    return df
        
createTable4()


	Random Sentences from Reviews	Proposed Lexicon’s Score	VADER Score
0	1	Quick check-in and polite staff	Negative: 0.0	Negative: 0.0
1			Neutral: 0.361	Neutral: 1.0
2			Positive: 0.639	Positive: 0.0
3			Compound: 0.1392	Compound: 0.0
4	2	Very smooth departure from Addis Ababa Airport.	Negative: 0.0	Negative: 0.0
5			Neutral: 0.501	Neutral: 1.0
6			Positive: 0.499	Positive: 0.0
7			Compound: 0.2444	Compound: 0.0
8	3	I do not know if I have hometown bias, but in ...	Negative: 0.105	Negative: 0.059
9			Neutral: 0.517	Neutral: 0.782
10			Positive: 0.378	Positive: 0.159
11			Compound: 0.5857	Compound: 0.4678
12	4	Forty minutes for luggage to arrive on carousel.	Negative: 0.376	Negative: 0.0
13			Neutral: 0.497	Neutral: 1.0
14			Positive: 0.127	Positive: 0.0
15			Compound: -0.0002	Compound: 0.0
16	5	The toilets were spotless.	Negative: 0.0	Negative: 0.0
17			Neutral: 0.744	Neutral: 1.0
18			Positive: 0.256	Positive: 0.0
19			Compound: 0.0076	Compound: 0.0


Values for some terms (pos & neg) and senences

pmi_pos_lexicons = ["Plenty", "Breeze", "WiFi", "Spacious", "Bright", "Convenient", "Easy", "Quick", "Polite", "Surprised", "Beautiful"]
pmi_neg_lexicons = ["Money", "Costs", "Late", "Disgusting", "Shocking", "Worst", "Unhelpful", "Awful", "Disorganised", "Disgraced", "Hate"]

for w in pmi_pos_lexicons:
    print(w, "----->", sid.polarity_scores(w.lower())["compound"])
	
Plenty -----> 0.0
Breeze -----> 0.0
WiFi -----> 0.0
Spacious -----> 0.0
Bright -----> 0.4404
Convenient -----> 0.0
Easy -----> 0.4404
Quick -----> 0.0
Polite -----> 0.0
Surprised -----> 0.2263
Beautiful -----> 0.5994

for w in pmi_neg_lexicons:
    print(w, "----->", sid.polarity_scores(w.lower())["compound"])
	
Money -----> 0.0
Costs -----> 0.0
Late -----> 0.0
Disgusting -----> -0.5267
Shocking -----> -0.4019
Worst -----> -0.6249
Unhelpful -----> 0.0
Awful -----> -0.4588
Disorganised -----> 0.0
Disgraced -----> -0.4588
Hate -----> -0.5719

sentences = ["Worst and most chaotic airport transfer I have ever experienced.", \
             "This Airport is more like a bus station and really needs to be extended.", \
             "I wonder if anyone regulates those shops since a lot of people end up paying surprisingly more (up to 40%) than the advertised price.", \
             "Overall very bright free access to computers for checking mails etc while waiting.", \
             "The toilets were spotless."] 
for w in sentences:
    print(sid.polarity_scores(w.lower()))
	
{'neg': 0.52, 'neu': 0.48, 'pos': 0.0, 'compound': -0.8221}
{'neg': 0.0, 'neu': 0.811, 'pos': 0.189, 'compound': 0.4201}
{'neg': 0.0, 'neu': 0.905, 'pos': 0.095, 'compound': 0.296}
{'neg': 0.0, 'neu': 0.619, 'pos': 0.381, 'compound': 0.7764}
{'neg': 0.0, 'neu': 1.0, 'pos': 0.0, 'compound': 0.0}


Affin Sentiment Analysis

from afinn import Afinn

afn = Afinn(emoticons=True)
afn.score('I really hated the hero of this movie')
-1.0

airportDf = pd.read_csv(r'Data//skytrax_reviews.csv')
def findSentimentAFFIN(review):    
    affin_score = afn.score(review)
    return affin_score
	
airportDf['Affin_Score'] = airportDf['Review Text'].apply(findSentimentAFFIN)
airportDf.loc[airportDf["Affin_Score"] < 0, "Affin_Recommendation"] = "no"
airportDf.loc[airportDf["Affin_Score"] >= 0, "Affin_Recommendation"] = "yes"

print("\n\nDefault AFINN\n\n")
print(confusion_matrix(airportDf['Recommended'], airportDf['Affin_Recommendation']))
print("--------------------")
print(classification_report(airportDf['Recommended'], airportDf['Affin_Recommendation']))

Default AFINN


[[17923 11141]
 [ 1662  8128]]
--------------------
              precision    recall  f1-score   support

          no       0.92      0.62      0.74     29064
         yes       0.42      0.83      0.56      9790

    accuracy                           0.67     38854
   macro avg       0.67      0.72      0.65     38854
weighted avg       0.79      0.67      0.69     38854


afn.data_dir()
import glob
lexicon_data_files = glob.glob("D:\\Anaconda3\\lib\\site-packages\\afinn\\data\\*")
lexicon_data_files

Affin Experimentation

stop_words = set(stopwords.words('english'))

def removeContraction(review):
    review = contractions.fix(review)
    return review

def removeStopwords(review):
    tokens = word_tokenize(review.lower())
    filtered_text = [t for t in tokens if not t in stop_words]
    return " ".join(filtered_text)
    
def removeSymbols(review):
    review = review.replace('"',"")
    review = review.replace(')',"")
    review = review.replace('(',"")
    review = review.replace('-'," ")
    review = review.replace(','," ")
    return review

def process_affin(split="NO", split_yes_condition=None, remove_stopword="Yes", remove_symbols="No", remove_shortform="Yes"):
    
    airportDf = pd.read_csv(r'Data//skytrax_reviews.csv')
    
    print("Dataset Contains {} records".format(airportDf.shape[0]))
    
    if split == "YES":
        yes = airportDf[(airportDf["Recommended"] == "yes")]
        yes = yes[yes["Review Rating"] >= split_yes_condition]
        print("Dataset Contains {} records (recommended)".format(yes.shape[0]))
        
        no = airportDf[(airportDf["Recommended"] == "no")]
        no = no[no["Review Rating"] < split_yes_condition]
        print("Dataset Contains {} records (Not recommended)".format(no.shape[0]))
        
        airportDf = pd.concat([yes, no])
        print("Final Dataset after spliting Contains {} records".format(airportDf.shape[0]))
        
    if remove_shortform == "Yes":
        airportDf['Review Text'] = airportDf['Review Text'].apply(removeContraction)
    
    if remove_symbols == "Yes":
        airportDf['Review Text'] = airportDf['Review Text'].apply(removeSymbols)
        
    if remove_stopword == "Yes":
        airportDf['Review Text'] = airportDf['Review Text'].apply(removeStopwords)
    
    airportDf['vaderScore'] = airportDf['Review Text'].apply(findSentimentAFFIN)
    
    airportDf.loc[airportDf["vaderScore"] < 0, "vaderRecommendation"] = "no"
    airportDf.loc[airportDf["vaderScore"] >= 0, "vaderRecommendation"] = "yes"
    
    print("\n\nDefault AFFIN\n\n")
    print(confusion_matrix(airportDf['Recommended'], airportDf['vaderRecommendation']))
    print("--------------------")
    print(classification_report(airportDf['Recommended'], airportDf['vaderRecommendation']))
    
    plt.rcParams.update({'font.size': 18})
    plt.figure(figsize=[14, 10])
    plt.hist(airportDf['vaderScore'], bins=20)
    plt.ylabel("Frequency of Reviews")
    plt.title("Sentiment Analysis with Default AFFIN")
    plt.savefig("hist10bins_AFFIN.png")
    plt.show()
	
	
process_affin(split="YES", split_yes_condition=5, remove_stopword="No", remove_symbols="No", remove_shortform="No")

Dataset Contains 38854 records
Dataset Contains 9186 records (recommended)
Dataset Contains 22765 records (Not recommended)
Final Dataset after spliting Contains 31951 records


Default AFFIN


[[15291  7474]
 [ 1352  7834]]
--------------------
              precision    recall  f1-score   support

          no       0.92      0.67      0.78     22765
         yes       0.51      0.85      0.64      9186

    accuracy                           0.72     31951
   macro avg       0.72      0.76      0.71     31951
weighted avg       0.80      0.72      0.74     31951