File size: 9,890 Bytes
0ac9264
 
 
768bcdc
bb50616
 
0ac9264
 
 
 
 
bb50616
b968a85
bb50616
 
a8cc4f6
 
 
e94c73c
 
 
0ac9264
e94c73c
0ac9264
e94c73c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ac9264
768bcdc
b968a85
 
0ac9264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb50616
 
 
 
 
 
 
 
 
 
 
 
 
b968a85
 
768bcdc
 
 
 
 
 
 
 
 
 
b968a85
 
768bcdc
 
0ac9264
 
4349862
0ac9264
 
 
 
e94c73c
0ac9264
 
 
 
a8cc4f6
 
 
4349862
0ac9264
b968a85
e94c73c
 
 
 
0ac9264
 
 
 
 
 
 
a8cc4f6
 
e94c73c
 
768bcdc
 
 
0ac9264
 
b968a85
768bcdc
e94c73c
0ac9264
 
e94c73c
 
a8cc4f6
 
 
0ac9264
b968a85
bb50616
 
b968a85
0ac9264
 
 
 
 
 
 
 
 
 
 
 
 
 
768bcdc
 
 
 
b968a85
768bcdc
 
 
b968a85
 
bb50616
 
 
0ac9264
 
 
 
 
 
 
768bcdc
0ac9264
 
2fd93e8
0ac9264
 
2fd93e8
0ac9264
 
 
b968a85
 
a8cc4f6
0ac9264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb50616
e94c73c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ac9264
a8cc4f6
0ac9264
a8cc4f6
0ac9264
bb50616
e94c73c
bb50616
 
0ac9264
 
bb50616
 
 
 
e94c73c
bb50616
 
 
 
 
e94c73c
a8cc4f6
0ac9264
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
import streamlit as st
import pandas as pd
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from transformers import pipeline
from fuzzywuzzy import fuzz
from sklearn.feature_extraction.text import TfidfVectorizer
import torch.nn.functional as F
import torch
import io
import base64
from stqdm import stqdm
import nltk

from nltk.corpus import stopwords
nltk.download('stopwords')
import matplotlib.pyplot as plt
import numpy as np

from lime.lime_text import LimeTextExplainer
from lime import lime_text


stopwords_list = stopwords.words('english') + ['your_additional_stopwords_here']
st.set_page_config(layout="wide")
@st.cache_resource
def load_model_and_tokenizer(model_name):
    model = AutoModelForSequenceClassification.from_pretrained(model_name)
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    return model, tokenizer

model, tokenizer = load_model_and_tokenizer('nlptown/bert-base-multilingual-uncased-sentiment')

@st.cache_resource
def load_pipeline():
    classifier = pipeline("zero-shot-classification", model="facebook/bart-large-mnli")
    return classifier

classifier = load_pipeline()





#defs
def classify_reviews(reviews):
    inputs = tokenizer(reviews, return_tensors='pt', truncation=True, padding=True, max_length=512)
    outputs = model(**inputs)
    probabilities = F.softmax(outputs.logits, dim=1).tolist()  
    return probabilities

def top_rating(scores):
    return scores.index(max(scores)) + 1  

def top_prob(scores):
    return max(scores)

def get_table_download_link(df):
    csv = df.to_csv(index=False)
    b64 = base64.b64encode(csv.encode()).decode()
    return f'<a href="data:file/csv;base64,{b64}" download="data.csv">Download csv file</a>'

def filter_dataframe(df, review_column, filter_words):
    # Return full DataFrame if filter_words is empty or contains only spaces
    if not filter_words or all(word.isspace() for word in filter_words):
        return df
    filter_scores = df[review_column].apply(lambda x: max([fuzz.token_set_ratio(x, word) for word in filter_words]))
    return df[filter_scores > 70]  # Adjust this threshold as necessary



def process_filter_words(filter_words_input):
    filter_words = [word.strip() for word in filter_words_input.split(',')]
    return filter_words


# Function for classifying with the new model
def classify_with_new_classes(reviews, class_names):
    class_scores = []

    for review in reviews:
        result = classifier(review, class_names)
        scores_dict = dict(zip(result['labels'], result['scores']))
        # Reorder scores to match the original class_names order
        scores = [scores_dict[name] for name in class_names]
        class_scores.append(scores)

    return class_scores



def main():
    st.title('Sentiment Analysis')
    st.markdown('Upload an Excel file to get sentiment analytics')

    file = st.file_uploader("Upload an excel file", type=['xlsx'])
    review_column = None
    df = None
    class_names = None  

    if file is not None:
        try:
            df = pd.read_excel(file)
            df = df.dropna(how='all')
            df = df.replace(r'^\s*$', np.nan, regex=True)
            df = df.dropna(how='all')
            review_column = st.selectbox('Select the column from your excel file containing text', df.columns)
            df[review_column] = df[review_column].astype(str)

            filter_words_input = st.text_input('Enter words to filter the data by, separated by comma (or leave empty)')
            filter_words = [] if filter_words_input.strip() == "" else process_filter_words(filter_words_input)
            class_names = st.text_input('Enter the possible class names separated by comma')
            df = filter_dataframe(df, review_column, filter_words)
        except Exception as e:
            st.write("An error occurred while reading the uploaded file. Please make sure it's a valid Excel file.")
            return

    start_button = st.button('Start Analysis')

    if start_button and df is not None:
        df = df[df[review_column].notna()]
        df = df[df[review_column].str.strip() != '']
        class_names = [name.strip() for name in class_names.split(',')]  
        for name in class_names:  
            if name not in df.columns:
                df[name] = 0.0
    
        if review_column in df.columns:
            with st.spinner('Performing sentiment analysis...'):
                df, df_display = process_reviews(df, review_column, class_names)
    
            display_ratings(df, review_column) 
            display_dataframe(df, df_display)
        else:
            st.write("The selected review column doesn't exist in the dataframe")








def process_reviews(df, review_column, class_names):
    with st.spinner('Classifying reviews...'):
        progress_bar = st.progress(0)
        total_reviews = len(df[review_column].tolist())
        review_counter = 0

        batch_size = 50
        raw_scores = []
        reviews = df[review_column].tolist()
        for i in range(0, len(reviews), batch_size):
            batch_reviews = reviews[i:i+batch_size]
            batch_scores = classify_reviews(batch_reviews)
            raw_scores.extend(batch_scores)
            review_counter += len(batch_reviews)
            progress_bar.progress(review_counter / total_reviews)

    with st.spinner('Generating classes...'):
        class_scores = classify_with_new_classes(df[review_column].tolist(), class_names)
    
    class_scores_dict = {}  # New dictionary to store class scores
    for i, name in enumerate(class_names):
        df[name] = [score[i] for score in class_scores]
        class_scores_dict[name] = [score[i] for score in class_scores]

    # Add a new column with the class that has the highest score
    if class_names and not all(name.isspace() for name in class_names):
        df['Highest Class'] = df[class_names].idxmax(axis=1)


    df_new = df.copy()
    df_new['raw_scores'] = raw_scores
    scores_to_df(df_new)
    df_display = scores_to_percent(df_new.copy())

    # Get all columns excluding the created ones and the review_column
    remaining_columns = [col for col in df.columns if col not in [review_column, 'raw_scores', 'Weighted Rating', 'Rating', 'Probability', '1 Star', '2 Star', '3 Star', '4 Star', '5 Star', 'Highest Class'] + class_names]

    # Reorder the dataframe with selected columns first, created columns next, then the remaining columns
    df_new = df_new[[review_column, 'Weighted Rating', 'Rating', 'Probability', '1 Star', '2 Star', '3 Star', '4 Star', '5 Star'] + class_names + ['Highest Class'] + remaining_columns]

    # Reorder df_display as well
    df_display = df_display[[review_column, 'Weighted Rating', 'Rating', 'Probability', '1 Star', '2 Star', '3 Star', '4 Star', '5 Star'] + class_names + ['Highest Class'] + remaining_columns]

    return df_new, df_display




def scores_to_df(df):
    for i in range(1, 6):
        df[f'{i} Star'] = df['raw_scores'].apply(lambda scores: scores[i-1]).round(2)

    df['Rating'] = df['raw_scores'].apply(top_rating)
    df['Probability'] = df['raw_scores'].apply(top_prob).round(2)
    # Compute the Weighted Rating
    df['Weighted Rating'] = sum(df[f'{i} Star']*i for i in range(1, 6))
    
    df.drop(columns=['raw_scores'], inplace=True)

def scores_to_percent(df):
    for i in range(1, 6):
        df[f'{i} Star'] = df[f'{i} Star'].apply(lambda x: f'{x*100:.0f}%')

    df['Probability'] = df['Probability'].apply(lambda x: f'{x*100:.0f}%')

    return df

def convert_df_to_csv(df):
   return df.to_csv(index=False).encode('utf-8')

def display_dataframe(df, df_display):
    csv = convert_df_to_csv(df)

    col1, col2, col3, col4, col5, col6, col7, col8, col9 = st.columns(9)

    with col1:
        st.download_button(
            "Download CSV",
            csv,
            "data.csv",
            "text/csv",
            key='download-csv'
        )

    st.dataframe(df_display)
    
def important_words(reviews, model, num_words=5):
    # Create a LimeTextExplainer
    explainer = LimeTextExplainer(class_names=[str(i) for i in range(1, 6)])

    # Define a prediction function that takes a list of texts and outputs a prediction matrix
    def predict_proba(texts):
        inputs = tokenizer(texts, return_tensors='pt', truncation=True, padding=True, max_length=512)
        outputs = model(**inputs)
        probabilities = F.softmax(outputs.logits, dim=1).detach().numpy()
        return probabilities

    important_words_per_rating = {}

    for rating in range(1, 6):
        important_words_per_rating[rating] = []
        for review in reviews:
            # Get the explanation for the review
            explanation = explainer.explain_instance(review, predict_proba, num_features=num_words, labels=[rating - 1])

            # Get the list of important words
            words = [feature[0] for feature in explanation.as_list(rating - 1)]
            important_words_per_rating[rating].extend(words)

        # Keep only unique words
        important_words_per_rating[rating] = list(set(important_words_per_rating[rating]))

    return important_words_per_rating


def display_ratings(df, review_column):
    cols = st.columns(5)
    
    for i in range(1, 6):
        rating_reviews = df[df['Rating'] == i][review_column]
        top_words = important_words(rating_reviews, model)

        rating_counts = rating_reviews.shape[0]
        cols[i-1].markdown(f"### {rating_counts}")
        cols[i-1].markdown(f"{'⭐' * i}")

        # Display the most important words for each rating
        cols[i-1].markdown(f"#### Most Important Words:")
        if top_words: 
            for word in top_words[i]:
                cols[i-1].markdown(f"**{word}**")
        else: 
            cols[i-1].markdown("No important words to display")



        


if __name__ == "__main__":
    main()