make-mp-data 3.0.3 → 3.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dungeons/array-of-object-lookup-schema.json +327 -0
- package/dungeons/array-of-object-lookup.js +29 -9
- package/dungeons/capstone/capstone-ic3.js +291 -0
- package/dungeons/capstone/capstone-ic4.js +598 -0
- package/dungeons/capstone/capstone-ic5.js +668 -0
- package/dungeons/capstone/generate-product-lookup.js +309 -0
- package/dungeons/ecommerce-schema.json +462 -0
- package/dungeons/{copilot.js → ecommerce.js} +79 -17
- package/dungeons/education-schema.json +2409 -0
- package/dungeons/education.js +226 -462
- package/dungeons/fintech-schema.json +14034 -0
- package/dungeons/fintech.js +134 -413
- package/dungeons/foobar-schema.json +403 -0
- package/dungeons/foobar.js +27 -4
- package/dungeons/food-delivery-schema.json +192 -0
- package/dungeons/food-delivery.js +602 -0
- package/dungeons/food-schema.json +1152 -0
- package/dungeons/food.js +173 -406
- package/dungeons/gaming-schema.json +1270 -0
- package/dungeons/gaming.js +182 -42
- package/dungeons/insurance-application-schema.json +204 -0
- package/dungeons/insurance-application.js +605 -0
- package/dungeons/media-schema.json +906 -0
- package/dungeons/media.js +250 -420
- package/dungeons/retention-cadence-schema.json +78 -0
- package/dungeons/retention-cadence.js +35 -1
- package/dungeons/rpg-schema.json +4526 -0
- package/dungeons/rpg.js +171 -429
- package/dungeons/sanity-schema.json +255 -0
- package/dungeons/sanity.js +21 -10
- package/dungeons/sass-schema.json +1291 -0
- package/dungeons/sass.js +241 -368
- package/dungeons/scd-schema.json +919 -0
- package/dungeons/scd.js +41 -13
- package/dungeons/simple-schema.json +608 -0
- package/dungeons/simple.js +52 -15
- package/dungeons/simplest-schema.json +1418 -0
- package/dungeons/simplest.js +392 -0
- package/dungeons/social-schema.json +1118 -0
- package/dungeons/social.js +150 -391
- package/dungeons/text-generation-schema.json +3096 -0
- package/dungeons/text-generation.js +71 -0
- package/index.js +8 -6
- package/lib/core/config-validator.js +28 -8
- package/lib/core/storage.js +5 -5
- package/lib/generators/events.js +4 -4
- package/lib/orchestrators/mixpanel-sender.js +16 -13
- package/lib/orchestrators/user-loop.js +14 -6
- package/lib/templates/soup-presets.js +188 -0
- package/lib/utils/utils.js +52 -6
- package/package.json +1 -1
- package/types.d.ts +20 -3
- package/dungeons/adspend.js +0 -130
- package/dungeons/anon.js +0 -128
- package/dungeons/benchmark-heavy.js +0 -240
- package/dungeons/benchmark-light.js +0 -140
- package/dungeons/big.js +0 -226
- package/dungeons/business.js +0 -391
- package/dungeons/complex.js +0 -428
- package/dungeons/experiments.js +0 -137
- package/dungeons/funnels.js +0 -309
- package/dungeons/mil.js +0 -323
- package/dungeons/mirror.js +0 -161
- package/dungeons/soup-test.js +0 -52
- package/dungeons/streaming.js +0 -372
- package/dungeons/strict-event-test.js +0 -30
- package/dungeons/student-teacher.js +0 -438
- package/dungeons/too-big-events.js +0 -203
- package/dungeons/user-agent.js +0 -209
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import utc from "dayjs/plugin/utc.js";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import * as u from "../lib/utils/utils.js";
|
|
5
|
+
import * as v from "ak-tools";
|
|
6
|
+
|
|
7
|
+
const SEED = "dm4-food-delivery";
|
|
8
|
+
dayjs.extend(utc);
|
|
9
|
+
const chance = u.initChance(SEED);
|
|
10
|
+
const num_users = 5_000;
|
|
11
|
+
const days = 100;
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../types.d.ts").Dungeon} Config */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ===============================================================================
|
|
17
|
+
* DATASET OVERVIEW
|
|
18
|
+
* ===============================================================================
|
|
19
|
+
*
|
|
20
|
+
* QuickBite Delivery - A food delivery platform modeled after DoorDash/UberEats,
|
|
21
|
+
* focused on the discovery-to-loyalty pipeline: users browse restaurants, discover
|
|
22
|
+
* menu items, build favorites lists, and place orders.
|
|
23
|
+
*
|
|
24
|
+
* CORE USER LOOP:
|
|
25
|
+
* Sign up -> browse restaurants -> view menu items -> favorite items -> build cart
|
|
26
|
+
* -> checkout -> order -> delivery -> rate -> reorder
|
|
27
|
+
*
|
|
28
|
+
* The central mechanic is item discovery and favoriting. Users who curate a focused
|
|
29
|
+
* favorites list (exactly 3 items) have the highest order values and most orders.
|
|
30
|
+
* Over-favoriters (4+) are impulsive and actually perform worse. Users who never
|
|
31
|
+
* favorite anything churn at high rates.
|
|
32
|
+
*
|
|
33
|
+
* SCALE:
|
|
34
|
+
* - 5,000 users over 100 days (~600K events)
|
|
35
|
+
* - 18 event types, 4 funnels (onboarding, order, discovery, reorder)
|
|
36
|
+
* - 200 restaurants (group analytics)
|
|
37
|
+
* - Subscription tiers: Free, QuickBite+
|
|
38
|
+
* - Session tracking enabled
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ===============================================================================
|
|
43
|
+
* ANALYTICS HOOKS
|
|
44
|
+
* ===============================================================================
|
|
45
|
+
*
|
|
46
|
+
* 4 deliberately architected hooks centered on the discovery-to-loyalty pipeline.
|
|
47
|
+
* The hooks create a cascading behavioral pattern: views -> favorites -> orders -> retention.
|
|
48
|
+
*
|
|
49
|
+
* ---------------------------------------------------------------------------
|
|
50
|
+
* 1. BELL CURVE: VIEW -> FAVORITE RELATIONSHIP (everything hook)
|
|
51
|
+
* ---------------------------------------------------------------------------
|
|
52
|
+
*
|
|
53
|
+
* PATTERN: Bell-shaped relationship between menu item views and favorites.
|
|
54
|
+
* Gaussian peaks at ~25 views -> 5 favorites. Users with <=3 views get 0 favorites.
|
|
55
|
+
* Formula: targetFav = round(5 * exp(-((views-25)^2) / (2*12^2)))
|
|
56
|
+
*
|
|
57
|
+
* MIXPANEL REPORT:
|
|
58
|
+
* 1. Insights > Segmentation
|
|
59
|
+
* 2. Count "view menu item" per user -> bucket into ranges
|
|
60
|
+
* 3. Count "favorite item" per user
|
|
61
|
+
* 4. Plot favorites vs. views -- clear bell curve peaking at ~25 views
|
|
62
|
+
* 5. Bucket users by view count, compute avg favorites per bucket
|
|
63
|
+
*
|
|
64
|
+
* EXPECTED: Inverted-U peaking at ~5 favorites for 25 views, 0 at both extremes.
|
|
65
|
+
*
|
|
66
|
+
* ---------------------------------------------------------------------------
|
|
67
|
+
* 2. MAGIC NUMBER: 3 FAVORITES = PEAK ORDER PERFORMANCE (everything hook)
|
|
68
|
+
* ---------------------------------------------------------------------------
|
|
69
|
+
*
|
|
70
|
+
* PATTERN: Exactly 3 favorites is the sweet spot.
|
|
71
|
+
* - 3 favorites: 1.6x order values, ~35% extra order duplication
|
|
72
|
+
* - 1-2 favorites: 1.15x small boost
|
|
73
|
+
* - 4+ favorites: PENALIZED -- 0.7x order values, ~30% orders removed, views removed
|
|
74
|
+
*
|
|
75
|
+
* MIXPANEL REPORT:
|
|
76
|
+
* 1. Insights > Segmentation
|
|
77
|
+
* 2. Segment users by favorite count: 0, 1-2, 3, 4+
|
|
78
|
+
* 3. Compare avg order_total and order count per segment
|
|
79
|
+
* 4. Filter: magic_number_customer = true to isolate the effect
|
|
80
|
+
* 5. Verify 4+ favorites has LOWEST order values (even below baseline)
|
|
81
|
+
*
|
|
82
|
+
* EXPECTED: 3 favorites dominates on order value and count. Over-favoriters
|
|
83
|
+
* are impulsive and indecisive -- they perform worse than focused curators.
|
|
84
|
+
* Real-world analogue: Facebook's "7 friends in 10 days" activation threshold.
|
|
85
|
+
*
|
|
86
|
+
* ---------------------------------------------------------------------------
|
|
87
|
+
* 3. SESSION AMPLIFICATION (everything hook)
|
|
88
|
+
* ---------------------------------------------------------------------------
|
|
89
|
+
*
|
|
90
|
+
* PATTERN: Magic-number boost is amplified for users with 10+ distinct sessions.
|
|
91
|
+
* These users get 2.2x order value (vs 1.6x) and 60% order duplication (vs 35%).
|
|
92
|
+
* Only applies to exactly-3-favorites users.
|
|
93
|
+
*
|
|
94
|
+
* MIXPANEL REPORT:
|
|
95
|
+
* 1. Insights > Segmentation
|
|
96
|
+
* 2. Count distinct session_id per user
|
|
97
|
+
* 3. Segment: 3 favorites AND 10+ sessions vs 3 favorites AND <10 sessions
|
|
98
|
+
* 4. Compare order_total and order count between segments
|
|
99
|
+
* 5. Filter: session_amplified = true
|
|
100
|
+
*
|
|
101
|
+
* EXPECTED: Compound effect of magic number + session depth creates the
|
|
102
|
+
* highest-value "power user" segment.
|
|
103
|
+
*
|
|
104
|
+
* ---------------------------------------------------------------------------
|
|
105
|
+
* 4. NO FAVORITES -> CHURN (everything hook)
|
|
106
|
+
* ---------------------------------------------------------------------------
|
|
107
|
+
*
|
|
108
|
+
* PATTERN: Users who never favorite any item lose ~60% of their events from
|
|
109
|
+
* the second half of their timeline, simulating disengagement and abandonment.
|
|
110
|
+
*
|
|
111
|
+
* MIXPANEL REPORT:
|
|
112
|
+
* 1. Insights > Segmentation
|
|
113
|
+
* 2. Segment users: any "favorite item" event vs none
|
|
114
|
+
* 3. Compare median total event count per user (use median, not mean)
|
|
115
|
+
* 4. Look for ~60% fewer events in the 0-favorites group
|
|
116
|
+
*
|
|
117
|
+
* EXPECTED: 0-favorites users have ~60% fewer events. They fade out.
|
|
118
|
+
*
|
|
119
|
+
* ---------------------------------------------------------------------------
|
|
120
|
+
* ADVANCED ANALYSIS IDEAS
|
|
121
|
+
* ---------------------------------------------------------------------------
|
|
122
|
+
*
|
|
123
|
+
* CROSS-HOOK PATTERNS:
|
|
124
|
+
* - Full Cascade: views -> favorites -> orders -> retention (Hooks 1-4 chain)
|
|
125
|
+
* - Over-Favoriting Paradox: more favorites = WORSE performance past 3
|
|
126
|
+
* - Magic Number Discovery: plot order value by favorite count, peak at 3
|
|
127
|
+
* - Session-to-Activation: how many sessions before hitting magic number?
|
|
128
|
+
*
|
|
129
|
+
* COHORT ANALYSIS:
|
|
130
|
+
* - By favorites count: 0, 1-2, 3 (magic), 4+
|
|
131
|
+
* - By session depth: <10, 10+
|
|
132
|
+
* - By view count bucket: 0-5, 6-10, 11-15, 16-25, 25+
|
|
133
|
+
* - Cross-cohort: favorites x sessions matrix
|
|
134
|
+
*
|
|
135
|
+
* ---------------------------------------------------------------------------
|
|
136
|
+
* EXPECTED METRICS SUMMARY
|
|
137
|
+
* ---------------------------------------------------------------------------
|
|
138
|
+
*
|
|
139
|
+
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
140
|
+
* ------------------------|-------------------------|----------|--------------|------
|
|
141
|
+
* Bell Curve | Favorites at 25 views | random | ~5 | peak
|
|
142
|
+
* Bell Curve | Favorites at 50+ views | random | ~0-1 | zero
|
|
143
|
+
* Magic Number (3 fav) | Order total | ~$70 | ~$143 | 2.0x
|
|
144
|
+
* Magic Number (3 fav) | Orders per user | ~4 | ~9 | 2.3x
|
|
145
|
+
* Over-Favoriter (4+ fav) | Order total | ~$70 | ~$48 | 0.7x
|
|
146
|
+
* Over-Favoriter (4+ fav) | Avg views | ~16 | ~13 | lower
|
|
147
|
+
* Session Amplification | Order total (10+ sess) | ~$143 | ~$153 | 2.2x
|
|
148
|
+
* No Favorites -> Churn | Median events (0 fav) | ~78 | ~27 | 0.35x
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
// Generate consistent IDs for entity references
|
|
152
|
+
const restaurantIds = v.range(1, 201).map(() => `rest_${v.uid(6)}`);
|
|
153
|
+
const itemIds = v.range(1, 401).map(() => `item_${v.uid(7)}`);
|
|
154
|
+
const orderIds = v.range(1, 5001).map(() => `order_${v.uid(8)}`);
|
|
155
|
+
const couponCodes = v.range(1, 51).map(() => `QUICK${v.uid(5).toUpperCase()}`);
|
|
156
|
+
|
|
157
|
+
const cuisineTypes = ["American", "Italian", "Chinese", "Japanese", "Mexican", "Indian", "Thai", "Mediterranean"];
|
|
158
|
+
const itemCategories = ["entree", "appetizer", "drink", "dessert", "side"];
|
|
159
|
+
|
|
160
|
+
/** @type {Config} */
|
|
161
|
+
const config = {
|
|
162
|
+
token: "dbdf44f9b8f6527c71262030119de387",
|
|
163
|
+
seed: SEED,
|
|
164
|
+
numDays: days,
|
|
165
|
+
numEvents: num_users * 120,
|
|
166
|
+
numUsers: num_users,
|
|
167
|
+
hasAnonIds: false,
|
|
168
|
+
hasSessionIds: true,
|
|
169
|
+
format: "json",
|
|
170
|
+
gzip: true,
|
|
171
|
+
alsoInferFunnels: false,
|
|
172
|
+
hasLocation: true,
|
|
173
|
+
hasAndroidDevices: true,
|
|
174
|
+
hasIOSDevices: true,
|
|
175
|
+
hasDesktopDevices: true,
|
|
176
|
+
hasBrowser: false,
|
|
177
|
+
hasCampaigns: false,
|
|
178
|
+
isAnonymous: false,
|
|
179
|
+
hasAdSpend: false,
|
|
180
|
+
percentUsersBornInDataset: 35,
|
|
181
|
+
hasAvatar: true,
|
|
182
|
+
batchSize: 2_500_000,
|
|
183
|
+
concurrency: 1,
|
|
184
|
+
writeToDisk: false,
|
|
185
|
+
scdProps: {},
|
|
186
|
+
mirrorProps: {},
|
|
187
|
+
lookupTables: [],
|
|
188
|
+
|
|
189
|
+
events: [
|
|
190
|
+
{
|
|
191
|
+
event: "account created",
|
|
192
|
+
weight: 1,
|
|
193
|
+
isFirstEvent: true,
|
|
194
|
+
properties: {
|
|
195
|
+
signup_method: ["email", "google", "apple", "facebook"],
|
|
196
|
+
referral_source: ["organic", "referral", "paid_ad", "social_media"],
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
event: "app opened",
|
|
201
|
+
weight: 2,
|
|
202
|
+
isSessionStartEvent: true,
|
|
203
|
+
properties: {
|
|
204
|
+
open_source: ["direct", "push_notification", "deeplink", "widget"],
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
event: "browse restaurants",
|
|
209
|
+
weight: 15,
|
|
210
|
+
properties: {
|
|
211
|
+
cuisine_filter: cuisineTypes,
|
|
212
|
+
sort_by: ["recommended", "distance", "rating", "price", "delivery_time"],
|
|
213
|
+
price_filter: ["any", "$", "$$", "$$$", "$$$$"],
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
event: "search",
|
|
218
|
+
weight: 10,
|
|
219
|
+
properties: {
|
|
220
|
+
search_query: () => chance.pickone([
|
|
221
|
+
"pizza", "sushi", "burgers", "tacos", "pad thai",
|
|
222
|
+
"chicken wings", "salad", "ramen", "pasta", "sandwiches",
|
|
223
|
+
"burritos", "curry", "pho", "steak", "dumplings",
|
|
224
|
+
"acai bowl", "falafel", "poke", "fried rice", "soup"
|
|
225
|
+
]),
|
|
226
|
+
results_count: u.weighNumRange(0, 50, 0.8, 30),
|
|
227
|
+
search_type: ["dish", "restaurant", "cuisine"],
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
event: "restaurant viewed",
|
|
232
|
+
weight: 14,
|
|
233
|
+
properties: {
|
|
234
|
+
restaurant_id: u.pickAWinner(restaurantIds),
|
|
235
|
+
cuisine_type: cuisineTypes,
|
|
236
|
+
avg_rating: u.weighNumRange(1, 5, 0.8, 30),
|
|
237
|
+
delivery_time_est: u.weighNumRange(15, 75, 1.0, 35),
|
|
238
|
+
price_tier: ["$", "$$", "$$$", "$$$$"],
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
event: "view menu item",
|
|
243
|
+
weight: 16,
|
|
244
|
+
properties: {
|
|
245
|
+
item_id: u.pickAWinner(itemIds),
|
|
246
|
+
item_category: itemCategories,
|
|
247
|
+
item_price: u.weighNumRange(3, 55, 1.0, 25),
|
|
248
|
+
restaurant_id: u.pickAWinner(restaurantIds),
|
|
249
|
+
has_photo: u.pickAWinner([true, true, true, false]),
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
event: "favorite item",
|
|
254
|
+
weight: 5,
|
|
255
|
+
properties: {
|
|
256
|
+
item_id: u.pickAWinner(itemIds),
|
|
257
|
+
item_category: itemCategories,
|
|
258
|
+
item_price: u.weighNumRange(3, 55, 1.0, 25),
|
|
259
|
+
restaurant_id: u.pickAWinner(restaurantIds),
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
event: "add to cart",
|
|
264
|
+
weight: 12,
|
|
265
|
+
properties: {
|
|
266
|
+
item_id: u.pickAWinner(itemIds),
|
|
267
|
+
item_price: u.weighNumRange(3, 55, 1.0, 25),
|
|
268
|
+
quantity: u.weighNumRange(1, 5, 2.0, 10),
|
|
269
|
+
customization_count: u.weighNumRange(0, 4, 1.5, 15),
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
event: "remove from cart",
|
|
274
|
+
weight: 3,
|
|
275
|
+
properties: {
|
|
276
|
+
item_id: u.pickAWinner(itemIds),
|
|
277
|
+
removal_reason: ["changed_mind", "too_expensive", "substitution"],
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
event: "checkout started",
|
|
282
|
+
weight: 8,
|
|
283
|
+
properties: {
|
|
284
|
+
cart_total: u.weighNumRange(10, 120, 0.8, 35),
|
|
285
|
+
items_count: u.weighNumRange(1, 8, 1.2, 15),
|
|
286
|
+
delivery_address_saved: u.pickAWinner([true, true, true, false]),
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
event: "order placed",
|
|
291
|
+
weight: 7,
|
|
292
|
+
properties: {
|
|
293
|
+
order_id: u.pickAWinner(orderIds),
|
|
294
|
+
payment_method: ["credit_card", "apple_pay", "google_pay", "debit_card", "paypal"],
|
|
295
|
+
order_total: u.weighNumRange(12, 150, 0.8, 35),
|
|
296
|
+
tip_amount: u.weighNumRange(0, 25, 1.5, 15),
|
|
297
|
+
delivery_fee: u.weighNumRange(0, 10, 1.0, 15),
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
event: "order tracked",
|
|
302
|
+
weight: 9,
|
|
303
|
+
properties: {
|
|
304
|
+
order_id: u.pickAWinner(orderIds),
|
|
305
|
+
tracking_status: ["confirmed", "preparing", "picked_up", "en_route", "delivered"],
|
|
306
|
+
eta_mins: u.weighNumRange(5, 60, 1.0, 25),
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
event: "order delivered",
|
|
311
|
+
weight: 6,
|
|
312
|
+
properties: {
|
|
313
|
+
order_id: u.pickAWinner(orderIds),
|
|
314
|
+
delivery_time_mins: u.weighNumRange(15, 80, 1.0, 35),
|
|
315
|
+
on_time: u.pickAWinner([true, true, true, false]),
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
event: "order rated",
|
|
320
|
+
weight: 5,
|
|
321
|
+
properties: {
|
|
322
|
+
order_id: u.pickAWinner(orderIds),
|
|
323
|
+
food_rating: u.weighNumRange(1, 5, 0.8, 30),
|
|
324
|
+
delivery_rating: u.weighNumRange(1, 5, 0.8, 30),
|
|
325
|
+
would_reorder: u.pickAWinner([true, true, false]),
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
event: "promotion viewed",
|
|
330
|
+
weight: 6,
|
|
331
|
+
properties: {
|
|
332
|
+
promo_type: ["banner", "push_notification", "in_feed", "email"],
|
|
333
|
+
promo_value: ["10%", "15%", "20%", "25%", "$5 off", "$10 off", "free delivery"],
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
event: "coupon applied",
|
|
338
|
+
weight: 3,
|
|
339
|
+
properties: {
|
|
340
|
+
coupon_code: u.pickAWinner(couponCodes),
|
|
341
|
+
discount_type: ["percent", "flat", "free_delivery"],
|
|
342
|
+
discount_value: u.weighNumRange(5, 40, 1.2, 15),
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
event: "reorder initiated",
|
|
347
|
+
weight: 4,
|
|
348
|
+
properties: {
|
|
349
|
+
original_order_id: u.pickAWinner(orderIds),
|
|
350
|
+
days_since_original: u.weighNumRange(1, 45, 1.5, 20),
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
event: "support contacted",
|
|
355
|
+
weight: 2,
|
|
356
|
+
properties: {
|
|
357
|
+
issue_type: ["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request", "app_bug"],
|
|
358
|
+
order_id: u.pickAWinner(orderIds),
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
|
|
363
|
+
funnels: [
|
|
364
|
+
{
|
|
365
|
+
sequence: ["account created", "browse restaurants", "restaurant viewed"],
|
|
366
|
+
isFirstFunnel: true,
|
|
367
|
+
conversionRate: 75,
|
|
368
|
+
timeToConvert: 0.5,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
sequence: ["view menu item", "add to cart", "checkout started", "order placed"],
|
|
372
|
+
conversionRate: 50,
|
|
373
|
+
timeToConvert: 1,
|
|
374
|
+
weight: 5,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
sequence: ["browse restaurants", "restaurant viewed", "view menu item", "favorite item"],
|
|
378
|
+
conversionRate: 35,
|
|
379
|
+
timeToConvert: 2,
|
|
380
|
+
weight: 3,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
sequence: ["order delivered", "order rated", "reorder initiated"],
|
|
384
|
+
conversionRate: 40,
|
|
385
|
+
timeToConvert: 24,
|
|
386
|
+
weight: 2,
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
|
|
390
|
+
superProps: {
|
|
391
|
+
platform: ["iOS", "Android", "Web"],
|
|
392
|
+
subscription_tier: u.pickAWinner(["Free", "Free", "Free", "Free", "QuickBite+"]),
|
|
393
|
+
city: ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco", "Seattle", "Miami"],
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
userProps: {
|
|
397
|
+
preferred_cuisine: cuisineTypes,
|
|
398
|
+
avg_order_value: u.weighNumRange(15, 80, 0.8, 35),
|
|
399
|
+
orders_per_month: u.weighNumRange(1, 15, 1.5, 8),
|
|
400
|
+
account_age_days: u.weighNumRange(1, 365, 0.5, 60),
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
groupKeys: [
|
|
404
|
+
["restaurant_id", 200, ["restaurant viewed", "order placed", "order rated"]],
|
|
405
|
+
],
|
|
406
|
+
|
|
407
|
+
groupProps: {
|
|
408
|
+
restaurant_id: {
|
|
409
|
+
name: () => `${chance.pickone(["The", "Big", "Lucky", "Golden", "Fresh", "Urban", "Tasty", "Royal"])} ${chance.pickone(["Kitchen", "Grill", "Bowl", "Wok", "Bistro", "Plate", "Table", "Fork"])}`,
|
|
410
|
+
cuisine: cuisineTypes,
|
|
411
|
+
avg_rating: u.weighNumRange(1, 5, 0.8, 30),
|
|
412
|
+
delivery_radius_mi: u.weighNumRange(1, 12, 1.0, 8),
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* ARCHITECTED ANALYTICS HOOKS
|
|
418
|
+
*
|
|
419
|
+
* This hook function creates 4 deliberate patterns in the data:
|
|
420
|
+
*
|
|
421
|
+
* 1. BELL CURVE: VIEW → FAVORITE RELATIONSHIP
|
|
422
|
+
* Users who view ~25 menu items favorite ~5 (peak). Few views or 55+ views → 0.
|
|
423
|
+
* The curve produces a range of 0-5 favorites across the user population.
|
|
424
|
+
*
|
|
425
|
+
* 2. MAGIC NUMBER: 3 FAVORITES = PEAK ORDER PERFORMANCE
|
|
426
|
+
* Exactly 3 favorites is the sweet spot. These users get the highest order
|
|
427
|
+
* values (1.6x) and extra orders. Users with 1-2 favorites get a small boost (1.15x).
|
|
428
|
+
* Users with 4+ favorites (over-favoriters) are PENALIZED: lower order values (0.7x),
|
|
429
|
+
* fewer orders, and fewer view events — they favorite impulsively without engaging deeply.
|
|
430
|
+
*
|
|
431
|
+
* 3. SESSION AMPLIFICATION
|
|
432
|
+
* The magic-number boost is amplified for users with 10+ sessions (2.2x instead of 1.6x).
|
|
433
|
+
*
|
|
434
|
+
* 4. NO FAVORITES → CHURN
|
|
435
|
+
* Users who never favorite an item churn significantly — 60% of their late events are removed.
|
|
436
|
+
*/
|
|
437
|
+
hook: function (record, type, meta) {
|
|
438
|
+
|
|
439
|
+
if (type === "everything") {
|
|
440
|
+
const userEvents = record;
|
|
441
|
+
if (!userEvents || userEvents.length === 0) return record;
|
|
442
|
+
|
|
443
|
+
// ═══════════════════════════════════════════════════════════════
|
|
444
|
+
// HOOK 1: BELL CURVE — VIEWS → FAVORITES
|
|
445
|
+
// Gaussian: peak at 25 views → 5 favorites. Sigma=12.
|
|
446
|
+
// Users with ≤3 views are forced to 0 favorites.
|
|
447
|
+
// Range: 0-5 favorites across the population.
|
|
448
|
+
// ═══════════════════════════════════════════════════════════════
|
|
449
|
+
|
|
450
|
+
// Count view menu item events
|
|
451
|
+
let viewCount = 0;
|
|
452
|
+
userEvents.forEach(e => {
|
|
453
|
+
if (e.event === "view menu item") viewCount++;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Gaussian: peak at 25 views → 5 favorites, sigma=12
|
|
457
|
+
// Users with very few views (≤3) get forced to 0
|
|
458
|
+
const rawTarget = Math.round(5 * Math.exp(-((viewCount - 25) ** 2) / (2 * 12 ** 2)));
|
|
459
|
+
const targetFav = viewCount <= 3 ? 0 : rawTarget;
|
|
460
|
+
|
|
461
|
+
// Count current favorites
|
|
462
|
+
let currentFav = 0;
|
|
463
|
+
userEvents.forEach(e => {
|
|
464
|
+
if (e.event === "favorite item") currentFav++;
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Adjust favorites to match bell curve target
|
|
468
|
+
if (currentFav > targetFav) {
|
|
469
|
+
// Remove excess favorites (backwards to avoid index issues)
|
|
470
|
+
let toRemove = currentFav - targetFav;
|
|
471
|
+
for (let i = userEvents.length - 1; i >= 0 && toRemove > 0; i--) {
|
|
472
|
+
if (userEvents[i].event === "favorite item") {
|
|
473
|
+
userEvents.splice(i, 1);
|
|
474
|
+
toRemove--;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} else if (currentFav < targetFav) {
|
|
478
|
+
// Add favorites near existing view events
|
|
479
|
+
const viewEvents = userEvents.filter(e => e.event === "view menu item");
|
|
480
|
+
const toAdd = targetFav - currentFav;
|
|
481
|
+
for (let j = 0; j < toAdd && j < viewEvents.length; j++) {
|
|
482
|
+
const sourceView = viewEvents[j];
|
|
483
|
+
userEvents.push({
|
|
484
|
+
event: "favorite item",
|
|
485
|
+
time: dayjs(sourceView.time).add(chance.integer({ min: 10, max: 120 }), 'seconds').toISOString(),
|
|
486
|
+
user_id: sourceView.user_id,
|
|
487
|
+
item_id: sourceView.item_id,
|
|
488
|
+
item_category: sourceView.item_category,
|
|
489
|
+
item_price: sourceView.item_price,
|
|
490
|
+
restaurant_id: sourceView.restaurant_id,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ═══════════════════════════════════════════════════════════════
|
|
496
|
+
// Recount favorites after adjustment
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════
|
|
498
|
+
let favCount = 0;
|
|
499
|
+
userEvents.forEach(e => {
|
|
500
|
+
if (e.event === "favorite item") favCount++;
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ═══════════════════════════════════════════════════════════════
|
|
504
|
+
// Count distinct sessions (for Hook 3)
|
|
505
|
+
// ═══════════════════════════════════════════════════════════════
|
|
506
|
+
const sessionSet = new Set();
|
|
507
|
+
userEvents.forEach(e => {
|
|
508
|
+
if (e.session_id) sessionSet.add(e.session_id);
|
|
509
|
+
});
|
|
510
|
+
const sessionCount = sessionSet.size;
|
|
511
|
+
|
|
512
|
+
// ═══════════════════════════════════════════════════════════════
|
|
513
|
+
// HOOK 2 + 3: MAGIC NUMBER (3 FAVORITES) + SESSION AMPLIFICATION
|
|
514
|
+
//
|
|
515
|
+
// Exactly 3 favorites = peak order performance (the sweet spot).
|
|
516
|
+
// 1-2 favorites = small boost (engaged but haven't found the groove).
|
|
517
|
+
// 4+ favorites = over-favoriters: penalized with lower order values,
|
|
518
|
+
// fewer orders, and fewer view events.
|
|
519
|
+
// Session amplification only applies at the magic number.
|
|
520
|
+
// ═══════════════════════════════════════════════════════════════
|
|
521
|
+
if (favCount === 3) {
|
|
522
|
+
// MAGIC NUMBER: maximum order boost
|
|
523
|
+
const amplified = sessionCount > 10;
|
|
524
|
+
const valueMultiplier = amplified ? 2.2 : 1.6;
|
|
525
|
+
const dupeChance = amplified ? 60 : 35;
|
|
526
|
+
|
|
527
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
528
|
+
const event = userEvents[i];
|
|
529
|
+
if (event.event === "order placed") {
|
|
530
|
+
event.order_total = Math.round((event.order_total || 30) * valueMultiplier * 100) / 100;
|
|
531
|
+
event.magic_number_customer = true;
|
|
532
|
+
if (amplified) event.session_amplified = true;
|
|
533
|
+
|
|
534
|
+
if (chance.bool({ likelihood: dupeChance })) {
|
|
535
|
+
const extraOrder = {
|
|
536
|
+
event: "order placed",
|
|
537
|
+
time: dayjs(event.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
|
|
538
|
+
user_id: event.user_id,
|
|
539
|
+
order_id: chance.pickone(orderIds),
|
|
540
|
+
payment_method: chance.pickone(["credit_card", "apple_pay", "google_pay", "debit_card"]),
|
|
541
|
+
order_total: Math.round(chance.integer({ min: 25, max: 120 }) * valueMultiplier * 100) / 100,
|
|
542
|
+
tip_amount: chance.integer({ min: 3, max: 20 }),
|
|
543
|
+
delivery_fee: chance.integer({ min: 0, max: 8 }),
|
|
544
|
+
magic_number_customer: true,
|
|
545
|
+
bonus_order: true,
|
|
546
|
+
};
|
|
547
|
+
if (amplified) extraOrder.session_amplified = true;
|
|
548
|
+
userEvents.splice(i + 1, 0, extraOrder);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
} else if (favCount > 0 && favCount < 3) {
|
|
553
|
+
// Below magic number: small boost (engaged but not at sweet spot)
|
|
554
|
+
for (let i = 0; i < userEvents.length; i++) {
|
|
555
|
+
if (userEvents[i].event === "order placed") {
|
|
556
|
+
userEvents[i].order_total = Math.round((userEvents[i].order_total || 30) * 1.15 * 100) / 100;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} else if (favCount > 3) {
|
|
560
|
+
// Over-favoriters: PENALTY
|
|
561
|
+
// Lower order values (0.7x) and remove ~30% of orders
|
|
562
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
563
|
+
if (userEvents[i].event === "order placed") {
|
|
564
|
+
userEvents[i].order_total = Math.round((userEvents[i].order_total || 30) * 0.7 * 100) / 100;
|
|
565
|
+
if (chance.bool({ likelihood: 30 })) {
|
|
566
|
+
userEvents.splice(i, 1);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Remove view events — scale with distance from magic number
|
|
571
|
+
const excessFavs = favCount - 3;
|
|
572
|
+
let viewsToRemove = Math.ceil(viewCount * Math.min(0.6, excessFavs * 0.25));
|
|
573
|
+
for (let i = userEvents.length - 1; i >= 0 && viewsToRemove > 0; i--) {
|
|
574
|
+
if (userEvents[i].event === "view menu item") {
|
|
575
|
+
userEvents.splice(i, 1);
|
|
576
|
+
viewsToRemove--;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ═══════════════════════════════════════════════════════════════
|
|
582
|
+
// HOOK 4: NO FAVORITES → CHURN
|
|
583
|
+
// Users who never favorite any item lose ~60% of their events
|
|
584
|
+
// from the second half of their timeline.
|
|
585
|
+
// ═══════════════════════════════════════════════════════════════
|
|
586
|
+
if (favCount === 0) {
|
|
587
|
+
const midIdx = Math.floor(userEvents.length / 2);
|
|
588
|
+
for (let i = userEvents.length - 1; i >= midIdx; i--) {
|
|
589
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
590
|
+
userEvents.splice(i, 1);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return record;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return record;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
export default config;
|