Navya-Sree commited on
Commit
47c29ec
Β·
verified Β·
1 Parent(s): a0c8d46

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +559 -0
  2. packages.txt +1 -0
  3. requirements.txt +11 -0
app.py ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import requests
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+ from bs4 import BeautifulSoup
6
+ from transformers import pipeline
7
+ import streamlit as st
8
+ import torch
9
+ import spacy
10
+ from wordcloud import WordCloud
11
+ import pandas as pd
12
+ from collections import defaultdict
13
+
14
+ # --- Streamlit Page Config (MUST BE FIRST) ---
15
+ st.set_page_config(
16
+ page_title="National Park Review Analyzer",
17
+ page_icon="🏞️",
18
+ layout="wide"
19
+ )
20
+
21
+ # --- NLP Setup ---
22
+ @st.cache_resource
23
+ def load_nlp_models():
24
+ # Try to load spacy model, download if not available
25
+ try:
26
+ nlp = spacy.load("en_core_web_sm")
27
+ except OSError:
28
+ import subprocess
29
+ subprocess.run([sys.executable, "-m", "spacy", "download", "en_core_web_sm"])
30
+ nlp = spacy.load("en_core_web_sm")
31
+
32
+ sentiment_analyzer = pipeline(
33
+ "sentiment-analysis",
34
+ model="distilbert-base-uncased-finetuned-sst-2-english",
35
+ device=0 if torch.cuda.is_available() else -1
36
+ )
37
+ return nlp, sentiment_analyzer
38
+
39
+ try:
40
+ nlp, sentiment_analyzer = load_nlp_models()
41
+ except Exception as e:
42
+ st.error(f"Error loading NLP models: {str(e)}")
43
+ nlp, sentiment_analyzer = None, None
44
+
45
+ # --- Constants ---
46
+ ALLOWED_DOMAINS = ['recreation.gov', 'nps.gov', 'nationalparks.org']
47
+
48
+ # Define categories for analysis
49
+ CATEGORIES = {
50
+ 'hiking': ['hiking', 'trail', 'hike', 'trek', 'trekking', 'paths', 'walk', 'walking'],
51
+ 'fees': ['fee', 'price', 'cost', 'payment', 'dollar', 'money', 'expensive', 'cheap', 'affordable'],
52
+ 'equipment': ['equipment', 'gear', 'supplies', 'tent', 'backpack', 'boots', 'poles', 'shoes'],
53
+ 'water': ['water', 'lake', 'river', 'stream', 'pond', 'waterfall', 'creek', 'swimming'],
54
+ 'facilities': ['facilities', 'restroom', 'bathroom', 'shower', 'toilet', 'visitor center', 'parking']
55
+ }
56
+
57
+ class RecreationGovScraper:
58
+ def __init__(self):
59
+ self.session = requests.Session()
60
+ self.session.headers.update({
61
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
62
+ 'Accept': 'text/html,application/xhtml+xml,application/xml',
63
+ 'Accept-Language': 'en-US,en;q=0.9'
64
+ })
65
+ self.session.mount('https://', requests.adapters.HTTPAdapter(max_retries=3))
66
+
67
+ def validate_url(self, url):
68
+ return any(domain in url for domain in ALLOWED_DOMAINS)
69
+
70
+ def extract_content(self, url):
71
+ try:
72
+ if not self.validate_url(url):
73
+ return {'error': 'Domain not allowed. Please use a URL from recreation.gov, nps.gov, or nationalparks.org'}
74
+
75
+ response = self.session.get(url, timeout=15)
76
+ response.raise_for_status()
77
+ soup = BeautifulSoup(response.content, 'html.parser')
78
+
79
+ # Extract reviews specifically for Recreation.gov
80
+ reviews = []
81
+ review_elements = soup.select('.rec-reviews-card')
82
+
83
+ if not review_elements and 'recreation.gov' in url:
84
+ # Try alternative selectors for Recreation.gov
85
+ review_elements = soup.select('.review-content') or soup.select('[data-component="review"]')
86
+
87
+ for review_elem in review_elements:
88
+ review_text = review_elem.get_text(strip=True)
89
+ if review_text:
90
+ reviews.append(review_text)
91
+
92
+ # If no reviews found through specific selectors, fallback to paragraphs
93
+ if not reviews:
94
+ all_text = ' '.join(p.get_text(strip=True) for p in soup.find_all('p') if len(p.get_text(strip=True)) > 20)
95
+ reviews = [all_text]
96
+
97
+ return {
98
+ 'reviews': reviews,
99
+ 'fees': self._extract_fees(soup),
100
+ 'facilities': self._extract_facilities(soup),
101
+ 'activities': self._extract_activities(soup),
102
+ 'title': self._extract_title(soup)
103
+ }
104
+ except Exception as e:
105
+ return {'error': str(e)}
106
+
107
+ def _extract_title(self, soup):
108
+ title = soup.find('h1')
109
+ if title:
110
+ return title.get_text(strip=True)
111
+ return "Unknown Park"
112
+
113
+ def _extract_fees(self, soup):
114
+ fees = []
115
+ fee_patterns = [
116
+ r'\$\d+\.?\d*(?:\s*-\s*\$\d+\.?\d*)?(?:\s*per\s*(?:person|vehicle|night|day|site|entrance))?',
117
+ r'(?:Fee|Price|Cost):\s*\$\d+\.?\d*'
118
+ ]
119
+
120
+ for pattern in fee_patterns:
121
+ fees.extend(re.findall(pattern, soup.text))
122
+
123
+ return fees[:5] # Return up to 5 fee matches
124
+
125
+ def _extract_facilities(self, soup):
126
+ facilities = []
127
+ facility_keywords = ['restroom', 'shower', 'campsite', 'picnic', 'visitor center',
128
+ 'parking', 'trailhead', 'lodging', 'camping', 'cabin']
129
+
130
+ for keyword in facility_keywords:
131
+ if keyword.lower() in soup.text.lower():
132
+ facilities.append(keyword)
133
+
134
+ # Also look for lists that might contain facilities
135
+ for list_item in soup.find_all('li'):
136
+ item_text = list_item.get_text(strip=True).lower()
137
+ if any(keyword in item_text for keyword in facility_keywords):
138
+ facilities.append(item_text[:50] + "..." if len(item_text) > 50 else item_text)
139
+
140
+ return list(set(facilities))[:5] # Deduplicate and limit to 5
141
+
142
+ def _extract_activities(self, soup):
143
+ activities = []
144
+ activity_keywords = ['hiking', 'swimming', 'fishing', 'boating', 'camping',
145
+ 'wildlife viewing', 'biking', 'kayaking', 'canoeing', 'photography']
146
+
147
+ for keyword in activity_keywords:
148
+ if keyword.lower() in soup.text.lower():
149
+ activities.append(keyword)
150
+
151
+ return list(set(activities)) # Deduplicate
152
+
153
+ def map_sentiment_label(sentiment):
154
+ """Maps sentiment labels to standardized format"""
155
+ if sentiment == 'POSITIVE':
156
+ return 'positive'
157
+ elif sentiment == 'NEGATIVE':
158
+ return 'negative'
159
+ else:
160
+ return 'neutral'
161
+
162
+ def categorize_text(text):
163
+ """Identify which categories the text belongs to"""
164
+ text_lower = text.lower()
165
+ categories_found = []
166
+
167
+ for category, keywords in CATEGORIES.items():
168
+ if any(keyword in text_lower for keyword in keywords):
169
+ categories_found.append(category)
170
+
171
+ # If no categories found, mark as 'general'
172
+ if not categories_found:
173
+ categories_found.append('general')
174
+
175
+ return categories_found
176
+
177
+ def analyze_content(url):
178
+ scraper = RecreationGovScraper()
179
+ data = scraper.extract_content(url)
180
+
181
+ if 'error' in data:
182
+ return None, f"Error: {data['error']}"
183
+
184
+ if not data['reviews']:
185
+ return None, "Error: No review content found on the page."
186
+
187
+ # Prepare for analysis
188
+ all_sentiments = []
189
+ category_sentiments = defaultdict(list)
190
+ sentences = []
191
+
192
+ # Process each review
193
+ for review in data['reviews']:
194
+ # Split review into sentences for more granular analysis
195
+ review_sentences = re.split(r'(?<=[.!?])\s+', review)
196
+
197
+ for sentence in review_sentences:
198
+ if len(sentence.strip()) < 10: # Skip very short sentences
199
+ continue
200
+
201
+ sentences.append(sentence)
202
+
203
+ # Determine categories this sentence belongs to
204
+ sentence_categories = categorize_text(sentence)
205
+
206
+ # Break long sentences into chunks for the sentiment analyzer
207
+ text_chunks = [sentence[i:i+512] for i in range(0, len(sentence), 512)]
208
+
209
+ try:
210
+ for chunk in text_chunks:
211
+ sentiment_result = sentiment_analyzer(chunk)[0]
212
+ sentiment_label = sentiment_result['label']
213
+ confidence = sentiment_result['score']
214
+
215
+ # Add neutrality for mid-range confidence scores
216
+ if 0.55 <= confidence <= 0.70:
217
+ sentiment_label = 'NEUTRAL'
218
+ confidence = 0.5 + (confidence - 0.55) * 0.5
219
+
220
+ # Store the sentiment
221
+ standardized_label = map_sentiment_label(sentiment_label)
222
+ sentiment_entry = {
223
+ 'text': chunk,
224
+ 'sentiment': standardized_label,
225
+ 'confidence': confidence,
226
+ 'categories': sentence_categories
227
+ }
228
+
229
+ all_sentiments.append(sentiment_entry)
230
+
231
+ # Categorize by topics
232
+ for category in sentence_categories:
233
+ category_sentiments[category].append(sentiment_entry)
234
+
235
+ except Exception as e:
236
+ return None, f"Sentiment analysis failed: {str(e)}"
237
+
238
+ if not all_sentiments:
239
+ return None, "Error: Could not perform sentiment analysis."
240
+
241
+ # Create overall sentiment distribution
242
+ sentiment_df = pd.DataFrame([{'sentiment': s['sentiment']} for s in all_sentiments])
243
+ sentiment_counts = sentiment_df['sentiment'].value_counts()
244
+
245
+ # Add missing sentiment categories if any are absent
246
+ for sentiment in ['positive', 'negative', 'neutral']:
247
+ if sentiment not in sentiment_counts:
248
+ sentiment_counts[sentiment] = 0
249
+
250
+ # Create category sentiment distribution
251
+ category_data = []
252
+ for category, sentiments in category_sentiments.items():
253
+ # Count sentiment by category
254
+ cat_sentiment_counts = defaultdict(int)
255
+ for s in sentiments:
256
+ cat_sentiment_counts[s['sentiment']] += 1
257
+
258
+ # Ensure all sentiments are represented
259
+ for sentiment in ['positive', 'negative', 'neutral']:
260
+ if sentiment not in cat_sentiment_counts:
261
+ cat_sentiment_counts[sentiment] = 0
262
+
263
+ # Add to dataset for plotting
264
+ for sentiment, count in cat_sentiment_counts.items():
265
+ category_data.append({
266
+ 'category': category,
267
+ 'sentiment': sentiment,
268
+ 'count': count
269
+ })
270
+
271
+ category_df = pd.DataFrame(category_data)
272
+
273
+ # Create visualizations
274
+ # 1. Overall Sentiment Distribution
275
+ plt.figure(figsize=(10, 6))
276
+ colors = {'positive': 'green', 'neutral': 'gold', 'negative': 'red'}
277
+ overall_sentiment_fig = plt.figure(figsize=(10, 6))
278
+ ax = overall_sentiment_fig.add_subplot(111)
279
+ bars = ax.bar(sentiment_counts.index, sentiment_counts.values, color=[colors[s] for s in sentiment_counts.index])
280
+ ax.set_title('Overall Sentiment Distribution', fontsize=16)
281
+ ax.set_ylabel('Number of Reviews', fontsize=12)
282
+ ax.grid(axis='y', linestyle='--', alpha=0.7)
283
+
284
+ # Add counts as text on bars
285
+ for bar in bars:
286
+ height = bar.get_height()
287
+ ax.text(bar.get_x() + bar.get_width()/2., height + 0.1,
288
+ f'{int(height)}', ha='center', va='bottom')
289
+
290
+ plt.tight_layout()
291
+
292
+ # 2. Sentiment Distribution by Category
293
+ if category_sentiments:
294
+ # Pivot the data for easier plotting
295
+ pivot_df = category_df.pivot_table(index='category', columns='sentiment', values='count', fill_value=0)
296
+
297
+ cat_fig = plt.figure(figsize=(12, 7))
298
+ ax = cat_fig.add_subplot(111)
299
+
300
+ # Set width of bars
301
+ bar_width = 0.25
302
+ index = np.arange(len(pivot_df.index))
303
+
304
+ # Plot bars for each sentiment
305
+ for i, sentiment in enumerate(['positive', 'neutral', 'negative']):
306
+ if sentiment in pivot_df.columns:
307
+ bars = ax.bar(index + i*bar_width, pivot_df[sentiment], bar_width,
308
+ label=sentiment, color=colors[sentiment])
309
+
310
+ # Add count labels on bars
311
+ for bar in bars:
312
+ height = bar.get_height()
313
+ if height > 0:
314
+ ax.text(bar.get_x() + bar.get_width()/2., height + 0.1,
315
+ f'{int(height)}', ha='center', va='bottom', fontsize=9)
316
+
317
+ # Set plot attributes
318
+ ax.set_title('Sentiment Distribution by Category', fontsize=16)
319
+ ax.set_ylabel('Number of Mentions', fontsize=12)
320
+ ax.set_xticks(index + bar_width)
321
+ ax.set_xticklabels(pivot_df.index, rotation=30, ha='right')
322
+ ax.legend(title='Sentiment')
323
+ ax.grid(axis='y', linestyle='--', alpha=0.7)
324
+
325
+ plt.tight_layout()
326
+ else:
327
+ cat_fig = None
328
+
329
+ # 3. Sentiment Confidence Distribution
330
+ conf_fig = plt.figure(figsize=(10, 6))
331
+ ax = conf_fig.add_subplot(111)
332
+
333
+ # Get confidence values for each sentiment
334
+ pos_conf = [s['confidence'] for s in all_sentiments if s['sentiment'] == 'positive']
335
+ neu_conf = [s['confidence'] for s in all_sentiments if s['sentiment'] == 'neutral']
336
+ neg_conf = [s['confidence'] for s in all_sentiments if s['sentiment'] == 'negative']
337
+
338
+ # Create histogram
339
+ if pos_conf:
340
+ ax.hist(pos_conf, bins=10, alpha=0.7, label='Positive', color='green')
341
+ if neu_conf:
342
+ ax.hist(neu_conf, bins=10, alpha=0.7, label='Neutral', color='gold')
343
+ if neg_conf:
344
+ ax.hist(neg_conf, bins=10, alpha=0.7, label='Negative', color='red')
345
+
346
+ ax.set_title('Sentiment Confidence Distribution', fontsize=16)
347
+ ax.set_xlabel('Confidence Score', fontsize=12)
348
+ ax.set_ylabel('Frequency', fontsize=12)
349
+ ax.legend()
350
+ ax.grid(alpha=0.3)
351
+
352
+ plt.tight_layout()
353
+
354
+ # 4. Word Cloud
355
+ combined_text = ' '.join(data['reviews'])
356
+ if combined_text:
357
+ try:
358
+ wordcloud = WordCloud(width=800, height=400, background_color='white',
359
+ colormap='viridis', max_words=100,
360
+ contour_width=1).generate(combined_text)
361
+ wordcloud_fig = plt.figure(figsize=(10, 5))
362
+ ax = wordcloud_fig.add_subplot(111)
363
+ ax.imshow(wordcloud, interpolation='bilinear')
364
+ ax.set_title('Most Common Words in Reviews', fontsize=16)
365
+ ax.axis('off')
366
+ plt.tight_layout()
367
+ except Exception as e:
368
+ wordcloud_fig = None
369
+ else:
370
+ wordcloud_fig = None
371
+
372
+ # 5. Top Positive and Negative Sentences
373
+ # Sort by confidence
374
+ positive_sentences = sorted(
375
+ [s for s in all_sentiments if s['sentiment'] == 'positive'],
376
+ key=lambda x: x['confidence'],
377
+ reverse=True
378
+ )
379
+
380
+ negative_sentences = sorted(
381
+ [s for s in all_sentiments if s['sentiment'] == 'negative'],
382
+ key=lambda x: x['confidence'],
383
+ reverse=True
384
+ )
385
+
386
+ # Prepare report data
387
+ positive_count = sum(1 for s in all_sentiments if s['sentiment'] == 'positive')
388
+ negative_count = sum(1 for s in all_sentiments if s['sentiment'] == 'negative')
389
+ neutral_count = sum(1 for s in all_sentiments if s['sentiment'] == 'neutral')
390
+ total_count = len(all_sentiments)
391
+
392
+ # Calculate percentages
393
+ if total_count > 0:
394
+ positive_pct = (positive_count / total_count) * 100
395
+ negative_pct = (negative_count / total_count) * 100
396
+ neutral_pct = (neutral_count / total_count) * 100
397
+ else:
398
+ positive_pct = negative_pct = neutral_pct = 0
399
+
400
+ report = {
401
+ 'title': data['title'],
402
+ 'url': url,
403
+ 'positive_count': positive_count,
404
+ 'negative_count': negative_count,
405
+ 'neutral_count': neutral_count,
406
+ 'total_count': total_count,
407
+ 'positive_pct': positive_pct,
408
+ 'negative_pct': negative_pct,
409
+ 'neutral_pct': neutral_pct,
410
+ 'fees': data['fees'],
411
+ 'facilities': data['facilities'],
412
+ 'activities': data['activities'],
413
+ 'overall_sentiment_fig': overall_sentiment_fig,
414
+ 'category_sentiment_fig': cat_fig,
415
+ 'confidence_fig': conf_fig,
416
+ 'wordcloud': wordcloud_fig,
417
+ 'top_positive': positive_sentences[:5] if positive_sentences else [],
418
+ 'top_negative': negative_sentences[:5] if negative_sentences else [],
419
+ 'category_sentiments': category_sentiments
420
+ }
421
+
422
+ return report, None
423
+
424
+ # Streamlit Interface
425
+ st.title("🏞️ National Park Review Analyzer")
426
+ st.write("""
427
+ This tool analyzes reviews and information from national park websites.
428
+ Enter a URL from Recreation.gov, NPS.gov, or NationalParks.org to get started.
429
+ """)
430
+
431
+ url_input = st.text_input(
432
+ "Enter National Park URL",
433
+ placeholder="https://www.recreation.gov/gateways/2584"
434
+ )
435
+
436
+ if st.button("Analyze", type="primary"):
437
+ if not url_input:
438
+ st.error("Please enter a URL to analyze")
439
+ else:
440
+ with st.spinner("Analyzing... This may take a minute"):
441
+ report, error = analyze_content(url_input)
442
+
443
+ if error:
444
+ st.error(error)
445
+ elif report:
446
+ # Display report
447
+ st.header(f"Analysis Report: {report['title']}")
448
+
449
+ # Overall metrics
450
+ st.subheader("Overall Sentiment")
451
+ cols = st.columns(3)
452
+
453
+ with cols[0]:
454
+ st.metric("Positive", f"{report['positive_count']} ({report['positive_pct']:.1f}%)")
455
+
456
+ with cols[1]:
457
+ st.metric("Neutral", f"{report['neutral_count']} ({report['neutral_pct']:.1f}%)")
458
+
459
+ with cols[2]:
460
+ st.metric("Negative", f"{report['negative_count']} ({report['negative_pct']:.1f}%)")
461
+
462
+ # Display overall sentiment distribution
463
+ st.pyplot(report['overall_sentiment_fig'])
464
+
465
+ # Display category sentiment distribution if available
466
+ if report['category_sentiment_fig']:
467
+ st.subheader("Sentiment by Category")
468
+ st.pyplot(report['category_sentiment_fig'])
469
+
470
+ # Show detailed category breakdown
471
+ st.subheader("Category Details")
472
+
473
+ for category in CATEGORIES.keys():
474
+ if category in report['category_sentiments']:
475
+ with st.expander(f"{category.title()} - {len(report['category_sentiments'][category])} mentions"):
476
+ cat_sentiments = report['category_sentiments'][category]
477
+ pos = sum(1 for s in cat_sentiments if s['sentiment'] == 'positive')
478
+ neg = sum(1 for s in cat_sentiments if s['sentiment'] == 'negative')
479
+ neu = sum(1 for s in cat_sentiments if s['sentiment'] == 'neutral')
480
+
481
+ st.write(f"πŸ‘ Positive: {pos} ({pos/len(cat_sentiments)*100:.1f}%)")
482
+ st.write(f"πŸ‘Ž Negative: {neg} ({neg/len(cat_sentiments)*100:.1f}%)")
483
+ st.write(f"😐 Neutral: {neu} ({neu/len(cat_sentiments)*100:.1f}%)")
484
+
485
+ # Show top sentence for this category
486
+ if cat_sentiments:
487
+ top_positive = next((s for s in cat_sentiments if s['sentiment'] == 'positive'), None)
488
+ top_negative = next((s for s in cat_sentiments if s['sentiment'] == 'negative'), None)
489
+
490
+ if top_positive:
491
+ st.write("**Sample positive mention:**")
492
+ st.write(f"*\"{top_positive['text']}\"*")
493
+
494
+ if top_negative:
495
+ st.write("**Sample negative mention:**")
496
+ st.write(f"*\"{top_negative['text']}\"*")
497
+ else:
498
+ with st.expander(f"{category.title()} - 0 mentions"):
499
+ st.write("No mentions found for this category.")
500
+
501
+ # Display confidence distribution
502
+ st.subheader("Sentiment Confidence")
503
+ st.pyplot(report['confidence_fig'])
504
+
505
+ # Display word cloud
506
+ if report['wordcloud']:
507
+ st.subheader("Word Cloud")
508
+ st.pyplot(report['wordcloud'])
509
+
510
+ # Display top positive and negative sentences
511
+ col1, col2 = st.columns(2)
512
+
513
+ with col1:
514
+ st.subheader("Top Positive Mentions")
515
+ if report['top_positive']:
516
+ for i, sentence in enumerate(report['top_positive'], 1):
517
+ st.write(f"**{i}.** *\"{sentence['text']}\"* (Confidence: {sentence['confidence']:.2f})")
518
+ else:
519
+ st.write("No positive mentions found.")
520
+
521
+ with col2:
522
+ st.subheader("Top Negative Mentions")
523
+ if report['top_negative']:
524
+ for i, sentence in enumerate(report['top_negative'], 1):
525
+ st.write(f"**{i}.** *\"{sentence['text']}\"* (Confidence: {sentence['confidence']:.2f})")
526
+ else:
527
+ st.write("No negative mentions found.")
528
+
529
+ # Display extracted information
530
+ st.subheader("Park Information")
531
+
532
+ if report['fees']:
533
+ with st.expander("Fees Mentioned"):
534
+ for fee in report['fees']:
535
+ st.write(f"- {fee}")
536
+
537
+ if report['facilities']:
538
+ with st.expander("Facilities"):
539
+ for facility in report['facilities']:
540
+ st.write(f"- {facility}")
541
+
542
+ if report['activities']:
543
+ with st.expander("Activities"):
544
+ for activity in report['activities']:
545
+ st.write(f"- {activity}")
546
+
547
+ st.sidebar.header("About")
548
+ st.sidebar.write("""
549
+ This tool uses natural language processing to analyze reviews and content from national park websites.
550
+ It extracts information about fees, facilities, and visitor sentiments.
551
+ """)
552
+
553
+ st.sidebar.subheader("Categories Analyzed")
554
+ for category in CATEGORIES:
555
+ st.sidebar.write(f"- {category.title()}")
556
+
557
+ st.sidebar.subheader("Supported Websites")
558
+ for domain in ALLOWED_DOMAINS:
559
+ st.sidebar.write(f"- {domain}")
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ libgl1-mesa-glx
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.30.0
2
+ requests==2.31.0
3
+ matplotlib==3.8.2
4
+ numpy==1.25.2
5
+ beautifulsoup4==4.12.2
6
+ transformers==4.38.1
7
+ torch==2.1.2
8
+ spacy==3.7.2
9
+ wordcloud==1.9.3
10
+ pandas==2.1.4
11
+ en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.0/en_core_web_sm-3.7.0-py3-none-any.whl