|
import streamlit as st |
|
import pandas as pd |
|
from transformers import pipeline |
|
import torch |
|
import numpy as np |
|
from groq import Client as Groq |
|
import os |
|
from dotenv import load_dotenv |
|
import time |
|
load_dotenv() |
|
|
|
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" |
|
|
|
st.set_page_config( |
|
page_title="Restaurant Review Analyzer π½οΈ", |
|
page_icon="π", |
|
layout="wide" |
|
) |
|
|
|
|
|
st.markdown(""" |
|
<style> |
|
.main { |
|
padding: 2rem; |
|
} |
|
.stProgress > div > div > div { |
|
background-color: #1f77b4; |
|
} |
|
.metric-card { |
|
background-color: #f8f9fa; |
|
padding: 1rem; |
|
border-radius: 0.5rem; |
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
def setup_classifier(): |
|
"""Initialize the zero-shot classification pipeline with GPU support if available""" |
|
with st.spinner('Loading classification model... βοΈ'): |
|
device = 0 if torch.cuda.is_available() else -1 |
|
return pipeline( |
|
"zero-shot-classification", |
|
model="joeddav/xlm-roberta-large-xnli", |
|
device=device |
|
) |
|
|
|
|
|
def create_aspect_labels(): |
|
"""Create labels for all aspects with positive/negative sentiment""" |
|
aspects = [ |
|
"food quality", |
|
"service", |
|
"ambiance", |
|
"price", |
|
"cleanliness", |
|
"portion size", |
|
"wait time", |
|
"menu variety" |
|
] |
|
|
|
sentiment_labels = [] |
|
for aspect in aspects: |
|
sentiment_labels.extend([ |
|
f"positive {aspect}", |
|
f"negative {aspect}" |
|
]) |
|
|
|
return aspects, sentiment_labels |
|
|
|
|
|
def classify_review(classifier, review, sentiment_labels): |
|
"""Classify a single review across all aspects and sentiments""" |
|
if pd.isna(review) or not isinstance(review, str): |
|
return {label: 0 for label in sentiment_labels} |
|
|
|
try: |
|
result = classifier( |
|
review, |
|
sentiment_labels, |
|
multi_label=True |
|
) |
|
return dict(zip(result['labels'], result['scores'])) |
|
except Exception as e: |
|
st.error(f"Error processing review: {e}") |
|
return {label: 0 for label in sentiment_labels} |
|
|
|
def format_summary_for_llm(summary_df): |
|
"""Format the classification summary into a clear text prompt""" |
|
summary_text = "Restaurant Reviews Analysis Summary:\n\n" |
|
|
|
sentiment_analysis = {} |
|
for aspect in summary_df.index: |
|
pos = summary_df.loc[aspect, 'positive_mentions'] |
|
neg = summary_df.loc[aspect, 'negative_mentions'] |
|
total = pos + neg |
|
if total > 0: |
|
pos_percent = (pos / total) * 100 |
|
neg_percent = (neg / total) * 100 |
|
difference = pos_percent - neg_percent |
|
sentiment_analysis[aspect] = { |
|
'difference': difference, |
|
'positive_percent': pos_percent, |
|
'negative_percent': neg_percent, |
|
'total_mentions': total |
|
} |
|
|
|
for aspect, metrics in sentiment_analysis.items(): |
|
summary_text += f"{aspect}:\n" |
|
summary_text += f"- Total Mentions: {metrics['total_mentions']}\n" |
|
summary_text += f"- Positive Mentions: {metrics['positive_percent']:.1f}%\n" |
|
summary_text += f"- Negative Mentions: {metrics['negative_percent']:.1f}%\n" |
|
summary_text += f"- Sentiment Difference: {metrics['difference']:.1f}%\n" |
|
summary_text += "\n" |
|
|
|
return summary_text |
|
|
|
def generate_insights(groq_client, summary_text): |
|
"""Generate insights using Groq API""" |
|
prompt = f"""You are an expert restaurant consultant analyzing customer feedback data. |
|
Based on the following customer review analysis summary, provide actionable insights |
|
and recommendations for the restaurant owner. When analyzing the data: |
|
|
|
- If an aspect has a positive difference of 0.5% or more, consider it a strength |
|
- If an aspect has a negative difference of 0.5% or more, consider it an area for improvement |
|
- For differences smaller than 0.5%, consider the aspect neutral or mixed |
|
- Pay special attention to aspects with high total mentions as they represent stronger customer sentiment |
|
|
|
Analysis Data: |
|
{summary_text} |
|
|
|
Please provide: |
|
1. Key Strengths: What's working well (aspects with >0.5% positive difference) |
|
2. Areas for Improvement: What needs attention (aspects with >0.5% negative difference) |
|
3. Mixed Reception Areas: Aspects with minimal difference (<0.5%) between positive and negative |
|
4. Actionable Recommendations: Specific steps based on the analysis |
|
5. Priority Actions: What should be addressed first, considering both sentiment differences and total mention count |
|
|
|
Format your response in clear sections with bullet points where appropriate. |
|
Add relevant emojis to make the response more engaging. |
|
""" |
|
|
|
try: |
|
with st.spinner('Generating insights... π€'): |
|
chat_completion = groq_client.chat.completions.create( |
|
messages=[{"role": "user", "content": prompt}], |
|
model="mixtral-8x7b-32768", |
|
temperature=0.7, |
|
max_tokens=1500, |
|
) |
|
return chat_completion.choices[0].message.content |
|
except Exception as e: |
|
st.error(f"Error generating insights: {str(e)}") |
|
return None |
|
|
|
def main(): |
|
|
|
st.title("π½οΈ Restaurant Review Analyzer") |
|
st.markdown("### Transform your customer feedback into actionable insights! π") |
|
|
|
|
|
st.sidebar.header("π Configuration") |
|
|
|
|
|
uploaded_file = st.sidebar.file_uploader("Upload your CSV file", type=['csv']) |
|
|
|
if uploaded_file is not None: |
|
|
|
try: |
|
df = pd.read_csv(uploaded_file) |
|
df.columns = df.columns.str.strip() |
|
|
|
|
|
if 'Review' not in df.columns: |
|
st.error("β 'Review' column not found in the CSV file!") |
|
return |
|
|
|
|
|
st.subheader("π Sample Reviews") |
|
st.dataframe(df[['Review']].head(5), use_container_width=True) |
|
|
|
|
|
if st.button("π Analyze Reviews"): |
|
|
|
classifier = setup_classifier() |
|
aspects, sentiment_labels = create_aspect_labels() |
|
|
|
|
|
results = [] |
|
progress_bar = st.progress(0) |
|
status_text = st.empty() |
|
|
|
for idx, review in enumerate(df['Review'].head(30)): |
|
status_text.text(f"Processing review {idx + 1}/30...") |
|
scores = classify_review(classifier, review, sentiment_labels) |
|
results.append(scores) |
|
progress_bar.progress((idx + 1) / 30) |
|
|
|
results_df = pd.DataFrame(results) |
|
|
|
|
|
summary = pd.DataFrame() |
|
for aspect in aspects: |
|
pos_col = f"positive {aspect}" |
|
neg_col = f"negative {aspect}" |
|
|
|
summary.loc[aspect, 'positive_mentions'] = (results_df[pos_col] > 0.5).sum() |
|
summary.loc[aspect, 'negative_mentions'] = (results_df[neg_col] > 0.5).sum() |
|
summary.loc[aspect, 'avg_positive_score'] = results_df[pos_col].mean() |
|
summary.loc[aspect, 'avg_negative_score'] = results_df[neg_col].mean() |
|
|
|
|
|
st.subheader("π Analysis Summary") |
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
st.markdown("#### π Positive Mentions") |
|
for aspect in aspects: |
|
st.metric( |
|
label=aspect.title(), |
|
value=f"{summary.loc[aspect, 'positive_mentions']} reviews", |
|
delta=f"{summary.loc[aspect, 'avg_positive_score']:.2%} avg. confidence" |
|
) |
|
|
|
with col2: |
|
st.markdown("#### π Negative Mentions") |
|
for aspect in aspects: |
|
st.metric( |
|
label=aspect.title(), |
|
value=f"{summary.loc[aspect, 'negative_mentions']} reviews", |
|
delta=f"{summary.loc[aspect, 'avg_negative_score']:.2%} avg. confidence", |
|
delta_color="inverse" |
|
) |
|
|
|
|
|
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY")) |
|
|
|
summary_text = format_summary_for_llm(summary) |
|
insights = generate_insights(groq_client, summary_text) |
|
|
|
if insights: |
|
st.subheader("π‘ Key Insights and Recommendations") |
|
st.markdown(insights) |
|
|
|
except Exception as e: |
|
st.error(f"Error processing file: {str(e)}") |
|
|
|
else: |
|
|
|
st.markdown(""" |
|
### π Welcome to the Restaurant Review Analyzer! |
|
|
|
To get started: |
|
1. π Upload your CSV file containing customer reviews |
|
2. π Make sure your file has a 'Review' column |
|
3. π Click 'Analyze Reviews' to process the data |
|
4. π Get detailed insights and recommendations |
|
|
|
The analyzer will process the reviews to provide quick insights! |
|
""") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
|
|