#!/usr/bin/env python3 """单元挑战漏斗:按用户交叉匹配,不依赖ex3区分路径""" import json, urllib.request, base64, ssl ES_HOST = "es-7vd7jcu9.public.tencentelasticsearch.com" ES_PORT = 9200 ES_USER = "elastic" ES_PASS = "F%?QDcWes7N2WTuiYD11" START_TS = 1779465600 END_TS = 1782057599 ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE def es_query(body): url = f"https://{ES_HOST}:{ES_PORT}/user_behavior_buried_points/_search" auth = base64.b64encode(f"{ES_USER}:{ES_PASS}".encode()).decode() req = urllib.request.Request(url, data=json.dumps(body).encode(), headers={ "Content-Type": "application/json", "Authorization": f"Basic {auth}" }) resp = urllib.request.urlopen(req, context=ctx) return json.loads(resp.read()) def fetch_users(sub_id): """获取某个 subId 的所有用户""" body = { "size": 10000, "query": { "bool": { "must": [ {"term": {"buryingPointId": 1300}}, {"term": {"buryingPointSubId": sub_id}}, {"range": {"activeTime": {"gte": START_TS, "lte": END_TS}}} ] } }, "_source": ["accountId", "courseLevel"] } result = es_query(body) hits = result.get("hits", {}).get("hits", []) a1 = set() a2 = set() for hit in hits: src = hit["_source"] aid = src.get("accountId", 0) level = src.get("courseLevel", "") if aid <= 0: continue if level == "A1": a1.add(aid) elif level == "A2": a2.add(aid) return a1, a2 # 1. 开始挑战 (subId=25) start_a1, start_a2 = fetch_users(25) # 2. 再次挑战 (subId=26) restart_a1, restart_a2 = fetch_users(26) # 3. 第一题曝光 (subId=32) - 全部 first_a1, first_a2 = fetch_users(32) # 4. 结算页曝光 (subId=27) - 全部 settle_a1, settle_a2 = fetch_users(27) # 漏斗:按用户交叉匹配 # 开始挑战漏斗 = 开始挑战用户 ∩ 第一题用户 ∩ 结算用户 start_first_a1 = start_a1 & first_a1 start_settle_a1 = start_a1 & settle_a1 start_first_a2 = start_a2 & first_a2 start_settle_a2 = start_a2 & settle_a2 # 再次挑战漏斗 = 再次挑战用户 ∩ 第一题用户 ∩ 结算用户 restart_first_a1 = restart_a1 & first_a1 restart_settle_a1 = restart_a1 & settle_a1 restart_first_a2 = restart_a2 & first_a2 restart_settle_a2 = restart_a2 & settle_a2 def pct(part, base): if base == 0: return "N/A" return f"{part/base*100:.1f}%" print("单元挑战漏斗 | 2026-05-23 ~ 2026-06-21") print("方法:按用户交叉匹配,不依赖ex3区分路径") print("=" * 60) print(f"\n📌 A1 开始挑战漏斗:") print(f" 开始挑战: {len(start_a1)}人") print(f" → 第一题曝光: {len(start_first_a1)}人 ({pct(len(start_first_a1), len(start_a1))})") print(f" → 结算页曝光: {len(start_settle_a1)}人 ({pct(len(start_settle_a1), len(start_a1))})") print(f"\n📌 A1 再次挑战漏斗:") print(f" 再次挑战: {len(restart_a1)}人") print(f" → 第一题曝光: {len(restart_first_a1)}人 ({pct(len(restart_first_a1), len(restart_a1))})") print(f" → 结算页曝光: {len(restart_settle_a1)}人 ({pct(len(restart_settle_a1), len(restart_a1))})") print(f"\n📌 A2 开始挑战漏斗:") print(f" 开始挑战: {len(start_a2)}人") print(f" → 第一题曝光: {len(start_first_a2)}人 ({pct(len(start_first_a2), len(start_a2))})") print(f" → 结算页曝光: {len(start_settle_a2)}人 ({pct(len(start_settle_a2), len(start_a2))})") print(f"\n📌 A2 再次挑战漏斗:") print(f" 再次挑战: {len(restart_a2)}人") print(f" → 第一题曝光: {len(restart_first_a2)}人 ({pct(len(restart_first_a2), len(restart_a2))})") print(f" → 结算页曝光: {len(restart_settle_a2)}人 ({pct(len(restart_settle_a2), len(restart_a2))})") # 诊断:结算页有但第一题没有的用户 print(f"\n--- 诊断 ---") settle_only_a1 = start_settle_a1 - start_first_a1 settle_only_a2 = start_settle_a2 - start_first_a2 print(f"A1 开始挑战→结算页有但第一题没有: {len(settle_only_a1)}人") print(f"A2 开始挑战→结算页有但第一题没有: {len(settle_only_a2)}人")