Jadjoueidi commited on
Commit
1eeb708
·
verified ·
1 Parent(s): 2407c79

Upload 5 files

Browse files
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: StayWise AI
3
+ emoji: 🏠
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 4.44.1
8
+ app_file: app.py
9
+ pinned: false
10
+ python_version: 3.10
11
+ ---
12
+
13
+ # StayWise AI
14
+
15
+ AI-powered pricing and performance optimization for short-term rentals.
16
+
17
+ ## What the app does
18
+
19
+ - Loads the Airbnb project datasets.
20
+ - Lets the user enter property information.
21
+ - Finds comparable listings.
22
+ - Estimates occupancy, revenue, demand score, opportunity score, and pricing recommendation.
23
+ - Optionally sends the pipeline output to n8n through a webhook.
24
+ - Displays the n8n response inside the app.
25
+
26
+ ## n8n integration
27
+
28
+ Create a Space secret called:
29
+
30
+ `N8N_WEBHOOK_URL`
31
+
32
+ The n8n workflow should use:
33
+
34
+ Webhook Trigger → Set/Edit Fields → Google Sheets or Email → Respond to Webhook
35
+
36
+ Recommended JSON response:
37
+
38
+ ```json
39
+ {
40
+ "status": "success",
41
+ "insight": "This listing has high demand potential but is underpriced.",
42
+ "next_step": "Consider increasing price by 8% and improving review quality.",
43
+ "log": "Saved to database and flagged for monitoring."
44
+ }
45
+ ```
airbnb_recommendation_output.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:18cce6473679041361120d96e14af3742f994175b8387c9a90b5a65328f3fa14
3
+ size 10814149
app.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import json
4
+ import math
5
+ import requests
6
+ import pandas as pd
7
+ import gradio as gr
8
+
9
+ APP_NAME = "StayWise AI"
10
+
11
+ DATA_FILE = "synthetic_airbnb_project_data.csv"
12
+ OUTPUT_FILE = "airbnb_recommendation_output.csv"
13
+
14
+ # Optional: set this in Hugging Face Space Secrets as N8N_WEBHOOK_URL
15
+ N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL", "").strip()
16
+
17
+ def load_data():
18
+ data = pd.read_csv(DATA_FILE)
19
+ output = pd.read_csv(OUTPUT_FILE) if os.path.exists(OUTPUT_FILE) else data.copy()
20
+ return data, output
21
+
22
+ df, output_df = load_data()
23
+
24
+ def safe_mean(series, default=0):
25
+ series = pd.to_numeric(series, errors="coerce").dropna()
26
+ if len(series) == 0:
27
+ return default
28
+ return float(series.mean())
29
+
30
+ def get_choices(column):
31
+ if column in df.columns:
32
+ return sorted([str(x) for x in df[column].dropna().unique().tolist()])
33
+ return []
34
+
35
+ neighbourhood_groups = get_choices("neighbourhood_group")
36
+ room_types = get_choices("room_type")
37
+ neighbourhoods = get_choices("neighbourhood")
38
+ seasons = get_choices("season") if "season" in df.columns else ["Low Season", "Shoulder Season", "High Season"]
39
+
40
+ def estimate_pipeline(neighbourhood_group, neighbourhood, room_type, price, availability_365, season, local_event_score, synthetic_rating, customer_sentiment_score, send_to_n8n):
41
+ price = float(price)
42
+ availability_365 = float(availability_365)
43
+ local_event_score = float(local_event_score)
44
+ synthetic_rating = float(synthetic_rating)
45
+ customer_sentiment_score = float(customer_sentiment_score)
46
+
47
+ comparable = df.copy()
48
+
49
+ if "neighbourhood_group" in comparable.columns:
50
+ comparable = comparable[comparable["neighbourhood_group"].astype(str) == str(neighbourhood_group)]
51
+
52
+ if "room_type" in comparable.columns:
53
+ comparable = comparable[comparable["room_type"].astype(str) == str(room_type)]
54
+
55
+ if "neighbourhood" in comparable.columns and neighbourhood:
56
+ local_comp = comparable[comparable["neighbourhood"].astype(str) == str(neighbourhood)]
57
+ if len(local_comp) >= 5:
58
+ comparable = local_comp
59
+
60
+ if len(comparable) < 5:
61
+ comparable = df[df["room_type"].astype(str) == str(room_type)] if "room_type" in df.columns else df.copy()
62
+
63
+ competitor_avg_price = safe_mean(comparable.get("price", pd.Series(dtype=float)), default=price)
64
+ avg_occupancy = safe_mean(comparable.get("occupancy_rate", pd.Series(dtype=float)), default=0.50)
65
+ avg_demand = safe_mean(comparable.get("demand_score", pd.Series(dtype=float)), default=50)
66
+ avg_revenue = safe_mean(comparable.get("monthly_revenue", pd.Series(dtype=float)), default=price * 15)
67
+
68
+ if competitor_avg_price == 0:
69
+ price_vs_competitor_pct = 0
70
+ else:
71
+ price_vs_competitor_pct = ((price - competitor_avg_price) / competitor_avg_price) * 100
72
+
73
+ # Occupancy estimate: base from comparable listings, adjusted for price gap, events, season, rating and sentiment
74
+ season_boost = {
75
+ "Low": -0.08,
76
+ "Low Season": -0.08,
77
+ "Medium": 0.00,
78
+ "Shoulder Season": 0.00,
79
+ "High": 0.08,
80
+ "High Season": 0.08,
81
+ "Peak": 0.12,
82
+ "Peak Season": 0.12
83
+ }.get(str(season), 0.00)
84
+
85
+ price_penalty = max(min(price_vs_competitor_pct / 100, 0.35), -0.35) * 0.30
86
+ event_boost = (local_event_score / 100) * 0.12
87
+ rating_boost = (synthetic_rating - 4.0) * 0.06
88
+ sentiment_boost = customer_sentiment_score * 0.08
89
+
90
+ occupancy_estimate = avg_occupancy + season_boost + event_boost + rating_boost + sentiment_boost - price_penalty
91
+ occupancy_estimate = max(0.05, min(0.95, occupancy_estimate))
92
+
93
+ booked_nights_month = round(occupancy_estimate * 30)
94
+ monthly_revenue = round(price * booked_nights_month, 2)
95
+
96
+ demand_score = (
97
+ 0.45 * avg_demand
98
+ + 0.25 * (occupancy_estimate * 100)
99
+ + 0.15 * local_event_score
100
+ + 0.10 * ((synthetic_rating / 5) * 100)
101
+ + 0.05 * ((customer_sentiment_score + 1) / 2 * 100)
102
+ )
103
+ demand_score = round(max(0, min(100, demand_score)), 1)
104
+
105
+ if demand_score >= 70:
106
+ demand_level = "High"
107
+ elif demand_score >= 45:
108
+ demand_level = "Medium"
109
+ else:
110
+ demand_level = "Low"
111
+
112
+ if price_vs_competitor_pct > 15 and demand_level != "High":
113
+ pricing_recommendation = "Consider lowering price"
114
+ suggested_price = round(competitor_avg_price * 1.05, 2)
115
+ elif price_vs_competitor_pct < -10 and demand_level in ["Medium", "High"]:
116
+ pricing_recommendation = "Consider raising price"
117
+ suggested_price = round(min(competitor_avg_price * 0.98, price * 1.12), 2)
118
+ else:
119
+ pricing_recommendation = "Keep price stable"
120
+ suggested_price = round(price, 2)
121
+
122
+ opportunity_score = round(
123
+ (demand_score * 0.45)
124
+ + (occupancy_estimate * 100 * 0.25)
125
+ + (synthetic_rating / 5 * 100 * 0.15)
126
+ + ((customer_sentiment_score + 1) / 2 * 100 * 0.15),
127
+ 2
128
+ )
129
+
130
+ if pricing_recommendation == "Consider lowering price":
131
+ insight = "The listing appears overpriced compared with similar properties, which may limit occupancy."
132
+ next_step = f"Test a lower price around ${suggested_price} and monitor occupancy changes."
133
+ elif pricing_recommendation == "Consider raising price":
134
+ insight = "The listing appears underpriced relative to demand and comparable properties."
135
+ next_step = f"Consider increasing the price toward ${suggested_price} while maintaining review quality."
136
+ else:
137
+ insight = "The current price is broadly aligned with the comparable market."
138
+ next_step = "Keep the price stable and focus on improving visibility, reviews, and conversion."
139
+
140
+ final_recommendation = f"{pricing_recommendation}. {next_step}"
141
+
142
+ result = {
143
+ "app_name": APP_NAME,
144
+ "neighbourhood_group": neighbourhood_group,
145
+ "neighbourhood": neighbourhood,
146
+ "room_type": room_type,
147
+ "current_price": price,
148
+ "suggested_price": suggested_price,
149
+ "competitor_avg_price": round(competitor_avg_price, 2),
150
+ "price_vs_competitor_pct": round(price_vs_competitor_pct, 2),
151
+ "occupancy_estimate": round(occupancy_estimate, 3),
152
+ "booked_nights_month": booked_nights_month,
153
+ "monthly_revenue": monthly_revenue,
154
+ "demand_score": demand_score,
155
+ "demand_level": demand_level,
156
+ "opportunity_score": opportunity_score,
157
+ "pricing_recommendation": pricing_recommendation,
158
+ "insight": insight,
159
+ "next_step": next_step,
160
+ "final_recommendation": final_recommendation
161
+ }
162
+
163
+ n8n_result = call_n8n(result) if send_to_n8n else {
164
+ "status": "not_sent",
165
+ "insight": "n8n automation was not triggered.",
166
+ "next_step": "Turn on the checkbox and add your N8N_WEBHOOK_URL secret to activate automation.",
167
+ "log": "Local pipeline only."
168
+ }
169
+
170
+ summary_md = f"""
171
+ # {APP_NAME} — Pipeline Result
172
+
173
+ ## Pricing recommendation
174
+ **{pricing_recommendation}**
175
+
176
+ | Metric | Result |
177
+ |---|---:|
178
+ | Current price | ${price:,.2f} |
179
+ | Suggested price | ${suggested_price:,.2f} |
180
+ | Comparable average price | ${competitor_avg_price:,.2f} |
181
+ | Price vs competitors | {price_vs_competitor_pct:.2f}% |
182
+ | Estimated occupancy | {occupancy_estimate * 100:.1f}% |
183
+ | Estimated booked nights/month | {booked_nights_month} |
184
+ | Estimated monthly revenue | ${monthly_revenue:,.2f} |
185
+ | Demand score | {demand_score}/100 |
186
+ | Demand level | {demand_level} |
187
+ | Opportunity score | {opportunity_score}/100 |
188
+
189
+ ## Business insight
190
+ {insight}
191
+
192
+ ## Next step
193
+ {next_step}
194
+ """
195
+
196
+ n8n_md = f"""
197
+ # Automation Output
198
+
199
+ **Status:** {n8n_result.get("status", "unknown")}
200
+
201
+ **Insight:** {n8n_result.get("insight", "No insight returned.")}
202
+
203
+ **Next step:** {n8n_result.get("next_step", "No next step returned.")}
204
+
205
+ **Log:** {n8n_result.get("log", "No log returned.")}
206
+ """
207
+
208
+ comp_cols = [c for c in ["id", "name", "neighbourhood_group", "neighbourhood", "room_type", "price", "occupancy_rate", "monthly_revenue", "demand_score", "demand_level", "pricing_recommendation"] if c in comparable.columns]
209
+ comparable_preview = comparable[comp_cols].head(10).copy() if comp_cols else comparable.head(10).copy()
210
+
211
+ return summary_md, n8n_md, comparable_preview, json.dumps(result, indent=2)
212
+
213
+ def call_n8n(payload):
214
+ if not N8N_WEBHOOK_URL:
215
+ return {
216
+ "status": "not_configured",
217
+ "insight": "No n8n webhook URL has been configured in the Space secrets.",
218
+ "next_step": "Add N8N_WEBHOOK_URL in Hugging Face Space Settings → Secrets.",
219
+ "log": "Pipeline calculated locally, but automation was not sent."
220
+ }
221
+
222
+ try:
223
+ response = requests.post(N8N_WEBHOOK_URL, json=payload, timeout=15)
224
+ if response.status_code >= 200 and response.status_code < 300:
225
+ try:
226
+ data = response.json()
227
+ return {
228
+ "status": data.get("status", "success"),
229
+ "insight": data.get("insight", "n8n received and processed the recommendation."),
230
+ "next_step": data.get("next_step", "Review the stored record or report generated by n8n."),
231
+ "log": data.get("log", "Automation completed.")
232
+ }
233
+ except Exception:
234
+ return {
235
+ "status": "success",
236
+ "insight": "n8n received the recommendation.",
237
+ "next_step": "Check the connected output in n8n.",
238
+ "log": response.text[:500]
239
+ }
240
+
241
+ return {
242
+ "status": "error",
243
+ "insight": f"n8n returned HTTP {response.status_code}.",
244
+ "next_step": "Check the webhook URL and the Respond to Webhook node.",
245
+ "log": response.text[:500]
246
+ }
247
+
248
+ except Exception as e:
249
+ return {
250
+ "status": "error",
251
+ "insight": "The app could not reach the n8n workflow.",
252
+ "next_step": "Verify that the production webhook URL is active and public.",
253
+ "log": str(e)
254
+ }
255
+
256
+ custom_css = """
257
+ .gradio-container {
258
+ max-width: 1200px !important;
259
+ margin: auto !important;
260
+ }
261
+ #hero {
262
+ background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 55%, #0284c7 100%);
263
+ padding: 28px;
264
+ border-radius: 22px;
265
+ color: white;
266
+ margin-bottom: 20px;
267
+ }
268
+ #hero h1 {
269
+ font-size: 42px;
270
+ margin-bottom: 6px;
271
+ }
272
+ #hero p {
273
+ font-size: 17px;
274
+ opacity: 0.92;
275
+ }
276
+ .card {
277
+ border-radius: 18px;
278
+ }
279
+ """
280
+
281
+ with gr.Blocks(css=custom_css, title=APP_NAME, theme=gr.themes.Soft()) as demo:
282
+ gr.HTML(f"""
283
+ <div id="hero">
284
+ <h1>{APP_NAME}</h1>
285
+ <p>AI-powered pricing and performance optimization for short-term rentals.</p>
286
+ </div>
287
+ """)
288
+
289
+ with gr.Row():
290
+ with gr.Column(scale=1):
291
+ gr.Markdown("## Property Inputs")
292
+ neighbourhood_group = gr.Dropdown(neighbourhood_groups, label="Neighbourhood Group", value=neighbourhood_groups[0] if neighbourhood_groups else None)
293
+ neighbourhood = gr.Dropdown(neighbourhoods, label="Neighbourhood", value=neighbourhoods[0] if neighbourhoods else None)
294
+ room_type = gr.Dropdown(room_types, label="Room Type", value=room_types[0] if room_types else None)
295
+ price = gr.Slider(20, 1000, value=150, step=5, label="Current Nightly Price ($)")
296
+ availability_365 = gr.Slider(0, 365, value=180, step=1, label="Availability per Year")
297
+ season = gr.Dropdown(seasons, label="Season", value=seasons[0] if seasons else None)
298
+ local_event_score = gr.Slider(0, 100, value=50, step=1, label="Local Event Demand Score")
299
+ synthetic_rating = gr.Slider(1, 5, value=4.4, step=0.1, label="Guest Rating")
300
+ customer_sentiment_score = gr.Slider(-1, 1, value=0.2, step=0.05, label="Customer Sentiment Score")
301
+ send_to_n8n = gr.Checkbox(label="Send pipeline output to n8n", value=False)
302
+ run_btn = gr.Button("Run Full Pipeline", variant="primary")
303
+
304
+ with gr.Column(scale=2):
305
+ pipeline_output = gr.Markdown(label="Pipeline Result")
306
+ n8n_output = gr.Markdown(label="Automation Output")
307
+ comparable_table = gr.Dataframe(label="Comparable Listings", interactive=False)
308
+ json_output = gr.Code(label="Pipeline JSON Output", language="json")
309
+
310
+ run_btn.click(
311
+ estimate_pipeline,
312
+ inputs=[
313
+ neighbourhood_group,
314
+ neighbourhood,
315
+ room_type,
316
+ price,
317
+ availability_365,
318
+ season,
319
+ local_event_score,
320
+ synthetic_rating,
321
+ customer_sentiment_score,
322
+ send_to_n8n
323
+ ],
324
+ outputs=[pipeline_output, n8n_output, comparable_table, json_output]
325
+ )
326
+
327
+ if __name__ == "__main__":
328
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio==4.44.1
2
+ pandas==2.2.3
3
+ requests==2.32.3
synthetic_airbnb_project_data.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e63f75596406a07ad35fb85e63a803708244e352792c6dcdf323bf788c00b19
3
+ size 17968855