| import gradio as gr |
| import pandas as pd |
| import numpy as np |
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
| import seaborn as sns |
| from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer |
| from sklearn.ensemble import RandomForestClassifier |
| from sklearn.model_selection import train_test_split |
| from sklearn.preprocessing import LabelEncoder |
| from statsmodels.tsa.arima.model import ARIMA |
| import warnings |
| warnings.filterwarnings('ignore') |
|
|
| |
| np.random.seed(42) |
|
|
| n_listings = 500 |
| neighbourhoods = ['Le Marais', 'Montmartre', 'Latin Quarter', 'Bastille', |
| 'Belleville', 'Oberkampf', 'Saint-Germain', 'Pigalle', |
| 'Batignolles', 'Menilmontant', 'Republique', 'Nation'] |
| room_types = ['Entire home/apt', 'Private room', 'Shared room'] |
|
|
| listings = pd.DataFrame({ |
| 'listing_id': range(1, n_listings + 1), |
| 'neighbourhood': np.random.choice(neighbourhoods, n_listings), |
| 'room_type': np.random.choice(room_types, n_listings, p=[0.55, 0.38, 0.07]), |
| 'accommodates': np.random.choice([1,2,3,4,5,6], n_listings, p=[0.1,0.3,0.25,0.2,0.1,0.05]), |
| 'bedrooms': np.random.choice([0,1,2,3], n_listings, p=[0.15,0.5,0.25,0.1]), |
| 'minimum_nights': np.random.choice([1,2,3,5,7,30], n_listings, p=[0.3,0.25,0.2,0.1,0.1,0.05]), |
| 'number_of_reviews': np.random.poisson(40, n_listings), |
| 'reviews_per_month': np.round(np.random.exponential(2.5, n_listings), 2), |
| 'host_is_superhost': np.random.choice([0, 1], n_listings, p=[0.7, 0.3]), |
| 'instant_bookable': np.random.choice([0, 1], n_listings, p=[0.4, 0.6]), |
| }) |
|
|
| base_prices = {'Entire home/apt': 120, 'Private room': 55, 'Shared room': 25} |
| premium = ['Le Marais', 'Saint-Germain', 'Latin Quarter', 'Montmartre'] |
| listings['price'] = listings.apply( |
| lambda r: base_prices[r['room_type']] * (1.3 if r['neighbourhood'] in premium else 1.0) |
| * np.random.uniform(0.6, 1.6), axis=1).round(2) |
| listings['review_scores_rating'] = np.clip( |
| np.where(listings['host_is_superhost'] == 1, |
| np.random.normal(4.7, 0.2, n_listings), |
| np.random.normal(4.3, 0.4, n_listings)), 3.0, 5.0).round(2) |
|
|
| |
| review_templates = { |
| 'positive': ["Amazing location, very clean and the host was super responsive!", |
| "Perfect apartment for our stay. Walking distance to everything.", |
| "Loved the cozy atmosphere. Would definitely come back!", |
| "Great value for money. The neighborhood is lovely.", |
| "Exceeded expectations! Beautiful decor and comfortable bed."], |
| 'neutral': ["Decent place, a bit noisy at night but overall okay.", |
| "Good location but the apartment was smaller than expected.", |
| "It was fine for the price. Nothing special but clean enough."], |
| 'negative': ["Disappointed. The photos were misleading and it was dirty.", |
| "Would not recommend. Noisy neighbors and broken appliances.", |
| "Not worth the price at all. Bed was uncomfortable."] |
| } |
|
|
| reviews_list = [] |
| for _ in range(5000): |
| lid = np.random.choice(listings['listing_id']) |
| rating = listings.loc[listings['listing_id'] == lid, 'review_scores_rating'].values[0] |
| probs = [0.75, 0.2, 0.05] if rating >= 4.5 else ([0.5, 0.35, 0.15] if rating >= 4.0 else [0.25, 0.35, 0.4]) |
| cat = np.random.choice(['positive', 'neutral', 'negative'], p=probs) |
| reviews_list.append({'listing_id': lid, 'comments': np.random.choice(review_templates[cat])}) |
| reviews = pd.DataFrame(reviews_list) |
|
|
| |
| analyzer = SentimentIntensityAnalyzer() |
| reviews['sentiment'] = reviews['comments'].apply(lambda x: analyzer.polarity_scores(str(x))['compound']) |
| listing_sent = reviews.groupby('listing_id')['sentiment'].mean().reset_index() |
| listing_sent.columns = ['listing_id', 'avg_sentiment'] |
| listings = listings.merge(listing_sent, on='listing_id', how='left').fillna(0) |
|
|
| |
| le_room = LabelEncoder() |
| le_hood = LabelEncoder() |
| listings['room_enc'] = le_room.fit_transform(listings['room_type']) |
| listings['hood_enc'] = le_hood.fit_transform(listings['neighbourhood']) |
| med_rating = listings['review_scores_rating'].median() |
| med_reviews = listings['reviews_per_month'].median() |
| listings['HighPerformer'] = ((listings['review_scores_rating'] >= med_rating) & |
| (listings['reviews_per_month'] >= med_reviews)).astype(int) |
|
|
| features = ['price','accommodates','bedrooms','minimum_nights','number_of_reviews', |
| 'host_is_superhost','instant_bookable','avg_sentiment','room_enc','hood_enc'] |
| X = listings[features] |
| y = listings['HighPerformer'] |
| X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) |
| rf = RandomForestClassifier(n_estimators=100, random_state=42, max_depth=10) |
| rf.fit(X_train, y_train) |
|
|
| |
|
|
| def neighbourhood_dashboard(neighbourhood): |
| subset = listings[listings['neighbourhood'] == neighbourhood] |
| n = len(subset) |
| avg_price = subset['price'].mean() |
| avg_sent = subset['avg_sentiment'].mean() |
| avg_rating = subset['review_scores_rating'].mean() |
| pct_superhost = subset['host_is_superhost'].mean() * 100 |
| hp_pct = subset['HighPerformer'].mean() * 100 |
|
|
| summary = f"""## {neighbourhood} Dashboard |
| | Metric | Value | |
| |--------|-------| |
| | Total Listings | {n} | |
| | Average Price | β¬{avg_price:.0f}/night | |
| | Average Sentiment | {avg_sent:.3f} | |
| | Average Rating | {avg_rating:.2f}/5.0 | |
| | Superhost % | {pct_superhost:.0f}% | |
| | High Performers | {hp_pct:.0f}% | |
| |
| ### Recommendation |
| {"**Premium neighbourhood** β prices above city average. Focus on maintaining quality to justify pricing." if avg_price > listings['price'].mean() else "**Value neighbourhood** β room to increase prices if sentiment stays positive."} |
| """ |
| |
| fig, axes = plt.subplots(1, 2, figsize=(12, 4)) |
| subset['room_type'].value_counts().plot(kind='bar', ax=axes[0], color=['#3498db','#e67e22','#27ae60']) |
| axes[0].set_title(f'Room Types in {neighbourhood}') |
| axes[0].set_xticklabels(axes[0].get_xticklabels(), rotation=30) |
| axes[1].hist(subset['price'], bins=15, color='#3498db', edgecolor='white') |
| axes[1].axvline(listings['price'].mean(), color='red', linestyle='--', label='City avg') |
| axes[1].set_title(f'Price Distribution in {neighbourhood}') |
| axes[1].set_xlabel('Price (β¬)') |
| axes[1].legend() |
| plt.tight_layout() |
| return summary, fig |
|
|
| def feature_importance_chart(): |
| importances = pd.Series(rf.feature_importances_, index=features).sort_values(ascending=True) |
| fig, ax = plt.subplots(figsize=(10, 6)) |
| importances.plot(kind='barh', ax=ax, color='#3498db', edgecolor='white') |
| ax.set_xlabel('Importance Score') |
| ax.set_title('What Makes an Airbnb Listing a High Performer?', fontsize=14, fontweight='bold') |
| plt.tight_layout() |
| accuracy = rf.score(X_test, y_test) |
| report = f"**Model Accuracy: {accuracy*100:.1f}%**\n\nTop 3 features: {', '.join(importances.tail(3).index.tolist()[::-1])}" |
| return report, fig |
|
|
| def price_forecast(neighbourhood): |
| subset = listings[listings['neighbourhood'] == neighbourhood] |
| base = subset['price'].mean() |
| months = pd.date_range('2023-01-01', periods=24, freq='MS') |
| trend = np.linspace(0, base * 0.15, 24) |
| seasonality = base * 0.1 * np.sin(np.linspace(0, 4*np.pi, 24)) |
| noise = np.random.normal(0, base * 0.03, 24) |
| ts = pd.Series(base + trend + seasonality + noise, index=months) |
|
|
| model = ARIMA(ts, order=(1,1,1)) |
| fitted = model.fit() |
| forecast = fitted.forecast(steps=6) |
| forecast_idx = pd.date_range(months[-1] + pd.DateOffset(months=1), periods=6, freq='MS') |
|
|
| fig, ax = plt.subplots(figsize=(12, 5)) |
| ax.plot(ts.index, ts.values, 'b-o', markersize=4, label='Historical', linewidth=1.5) |
| ax.plot(forecast_idx, forecast.values, 'r--o', markersize=4, label='Forecast', linewidth=1.5) |
| ax.fill_between(forecast_idx, forecast.values*0.92, forecast.values*1.08, alpha=0.2, color='red') |
| ax.set_title(f'{neighbourhood} β 6-Month Price Forecast (ARIMA)', fontsize=14, fontweight='bold') |
| ax.set_ylabel('Average Price (β¬)') |
| ax.legend() |
| ax.grid(True, alpha=0.3) |
| plt.tight_layout() |
|
|
| change = ((forecast.values[-1] - ts.values[-1]) / ts.values[-1]) * 100 |
| info = f"**Current avg: β¬{ts.values[-1]:.0f}** β **Forecasted: β¬{forecast.values[-1]:.0f}** ({change:+.1f}% over 6 months)" |
| return info, fig |
|
|
| def sentiment_overview(): |
| fig, axes = plt.subplots(1, 2, figsize=(14, 5)) |
| hood_sent = listings.groupby('neighbourhood')['avg_sentiment'].mean().sort_values() |
| colors = ['#e74c3c' if v < 0.3 else '#f39c12' if v < 0.37 else '#27ae60' for v in hood_sent] |
| hood_sent.plot(kind='barh', ax=axes[0], color=colors) |
| axes[0].set_title('Sentiment by Neighbourhood') |
| axes[0].set_xlabel('Average Sentiment') |
| sh = listings.groupby('host_is_superhost')['avg_sentiment'].mean() |
| sh.index = ['Regular', 'Superhost'] |
| sh.plot(kind='bar', ax=axes[1], color=['#3498db', '#e67e22']) |
| axes[1].set_title('Superhost Effect on Sentiment') |
| axes[1].set_xticklabels(axes[1].get_xticklabels(), rotation=0) |
| plt.tight_layout() |
| best = hood_sent.idxmax() |
| worst = hood_sent.idxmin() |
| info = f"**Best:** {best} ({hood_sent.max():.3f}) | **Worst:** {worst} ({hood_sent.min():.3f}) | **Superhost boost:** +{sh['Superhost']-sh['Regular']:.3f}" |
| return info, fig |
|
|
| |
| with gr.Blocks(title="Airbnb Pricing & Satisfaction Optimizer", theme=gr.themes.Soft()) as app: |
| gr.Markdown("""# π Airbnb Pricing & Guest Satisfaction Optimizer |
| *AI for Big Data Management β Group Project | ESCP Business School* |
| |
| Analyze listing performance across Paris neighbourhoods using sentiment analysis, |
| random forest classification, and ARIMA price forecasting. |
| """) |
|
|
| with gr.Tab("π Neighbourhood Dashboard"): |
| gr.Markdown("Select a neighbourhood to see key metrics and pricing insights.") |
| hood_input = gr.Dropdown(choices=sorted(neighbourhoods), value='Le Marais', label="Neighbourhood") |
| hood_btn = gr.Button("Analyze", variant="primary") |
| hood_summary = gr.Markdown() |
| hood_chart = gr.Plot() |
| hood_btn.click(neighbourhood_dashboard, inputs=hood_input, outputs=[hood_summary, hood_chart]) |
|
|
| with gr.Tab("π² Feature Importance"): |
| gr.Markdown("Which features most predict whether a listing will be a high performer?") |
| fi_btn = gr.Button("Show Feature Importance", variant="primary") |
| fi_text = gr.Markdown() |
| fi_chart = gr.Plot() |
| fi_btn.click(feature_importance_chart, outputs=[fi_text, fi_chart]) |
|
|
| with gr.Tab("π Price Forecast"): |
| gr.Markdown("ARIMA(1,1,1) forecast of average listing prices for the next 6 months.") |
| fc_input = gr.Dropdown(choices=sorted(neighbourhoods), value='Le Marais', label="Neighbourhood") |
| fc_btn = gr.Button("Forecast", variant="primary") |
| fc_text = gr.Markdown() |
| fc_chart = gr.Plot() |
| fc_btn.click(price_forecast, inputs=fc_input, outputs=[fc_text, fc_chart]) |
|
|
| with gr.Tab("π¬ Sentiment Analysis"): |
| gr.Markdown("Overview of guest sentiment across all neighbourhoods and host types.") |
| sa_btn = gr.Button("Show Sentiment Overview", variant="primary") |
| sa_text = gr.Markdown() |
| sa_chart = gr.Plot() |
| sa_btn.click(sentiment_overview, outputs=[sa_text, sa_chart]) |
|
|
| if __name__ == "__main__": |
| app.launch() |
|
|