Refactor new user retention metrics: update filters, add country data, and enhance retention checks. 튜토 완료 수집 조건 변경, 득템 기준 변경 (장비만)
This commit is contained in:
@ -309,7 +309,7 @@ def get_fixed_metrics_config() -> Dict[str, Dict]:
|
||||
"agg_type": "count",
|
||||
"filters": [
|
||||
{"term": {"body.action_type.keyword": "Complete"}},
|
||||
{"term": {"body.stage_type.keyword": "result"}}
|
||||
{"term": {"body.stage_type.keyword": "tutorial_escape_portal"}}
|
||||
]
|
||||
},
|
||||
"guide_quest_stage": {
|
||||
@ -334,7 +334,8 @@ def get_fixed_metrics_config() -> Dict[str, Dict]:
|
||||
"index": "ds-logs-live-item_get",
|
||||
"time_range": "d0",
|
||||
"agg_type": "max",
|
||||
"field": "body.item_grade"
|
||||
"field": "body.item_grade",
|
||||
"filters": [{"term": {"body.base_type": 2}}]
|
||||
},
|
||||
"blueprint_use_count": {
|
||||
"index": "ds-logs-live-craft_from_blueprint",
|
||||
@ -509,11 +510,21 @@ def get_fixed_metrics_config() -> Dict[str, Dict]:
|
||||
"field": "body.season_pass_step"
|
||||
},
|
||||
|
||||
# ==================== 리텐션 판정 (retention_d1 삭제됨) ====================
|
||||
"retention_check": {
|
||||
"index": "ds-logs-live-login_comp",
|
||||
# ==================== 리텐션 판정 ====================
|
||||
"retention_check_d1": {
|
||||
"index": "ds-logs-live-heartbeat",
|
||||
"time_range": "d1",
|
||||
"agg_type": "exists"
|
||||
},
|
||||
"retention_check_d2_6": {
|
||||
"index": "ds-logs-live-heartbeat",
|
||||
"time_range": "d2_6",
|
||||
"agg_type": "exists"
|
||||
},
|
||||
"retention_check_d7_plus": {
|
||||
"index": "ds-logs-live-heartbeat",
|
||||
"time_range": "d7_plus",
|
||||
"agg_type": "exists"
|
||||
}
|
||||
}
|
||||
|
||||
@ -631,6 +642,7 @@ def get_new_user_cohort_optimized(
|
||||
'create_time_kst': format_kst_time(new_user_map[uid]["create_time"]),
|
||||
'create_time_dt': datetime.fromisoformat(new_user_map[uid]["create_time"].replace('Z', '+00:00')),
|
||||
'language': 'N/A',
|
||||
'country': 'N/A',
|
||||
'device': 'N/A',
|
||||
'nickname': 'N/A'
|
||||
}
|
||||
@ -665,14 +677,14 @@ def get_new_user_cohort_optimized(
|
||||
"top_hits": {
|
||||
"size": 1,
|
||||
"sort": [{"@timestamp": {"order": "asc"}}],
|
||||
"_source": ["body.device_mod", "body.nickname", "auth.id"]
|
||||
"_source": ["body.device_mod", "body.nickname", "auth.id", "country"]
|
||||
}
|
||||
},
|
||||
"latest_info": {
|
||||
"top_hits": {
|
||||
"size": 1,
|
||||
"sort": [{"@timestamp": {"order": "desc"}}],
|
||||
"_source": ["body.nickname", "body.language", "auth.id"]
|
||||
"_source": ["body.nickname", "body.language", "auth.id", "country"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -701,6 +713,11 @@ def get_new_user_cohort_optimized(
|
||||
cohort[uid]['device'] = user_hit.get('body', {}).get('device_mod', 'N/A')
|
||||
cohort[uid]['nickname'] = latest_info_hit.get('body', {}).get('nickname') or user_hit.get('body', {}).get('nickname', 'N/A')
|
||||
|
||||
# country 수집 (login_comp에서)
|
||||
country = latest_info_hit.get('country') or user_hit.get('country')
|
||||
if country:
|
||||
cohort[uid]['country'] = country
|
||||
|
||||
# auth.id 수집 (1순위)
|
||||
auth_id = latest_info_hit.get('auth', {}).get('id') or user_hit.get('auth', {}).get('id')
|
||||
if auth_id:
|
||||
@ -713,9 +730,9 @@ def get_new_user_cohort_optimized(
|
||||
|
||||
logger.info(f"login_comp에서 {len(login_comp_collected)}명의 정보 수집 완료")
|
||||
|
||||
# Step 4: log_return_to_lobby 인덱스에서 차선 정보 수집 (auth.id 2순위, language fallback)
|
||||
# login_comp에서 수집되지 않은 유저 + language가 N/A인 유저들 처리
|
||||
missing_uids = [uid for uid in uid_list if uid not in login_comp_collected or cohort[uid]['language'] == 'N/A']
|
||||
# Step 4: log_return_to_lobby 인덱스에서 차선 정보 수집 (auth.id 2순위)
|
||||
# login_comp에서 수집되지 않은 유저들 처리
|
||||
missing_uids = [uid for uid in uid_list if uid not in login_comp_collected]
|
||||
|
||||
if missing_uids:
|
||||
logger.info(f"log_return_to_lobby 인덱스에서 {len(missing_uids)}명의 차선 정보 수집 중 (auth.id 2순위)...")
|
||||
@ -772,11 +789,11 @@ def get_new_user_cohort_optimized(
|
||||
if cohort[uid]['nickname'] == 'N/A':
|
||||
cohort[uid]['nickname'] = info_hit.get('body', {}).get('nickname', 'N/A')
|
||||
|
||||
# language fallback: country 값 사용 (language가 N/A인 경우에만)
|
||||
if cohort[uid]['language'] == 'N/A':
|
||||
# country 수집 (없는 경우에만)
|
||||
if cohort[uid]['country'] == 'N/A':
|
||||
country = info_hit.get('country')
|
||||
if country:
|
||||
cohort[uid]['language'] = f"country-{country}"
|
||||
cohort[uid]['country'] = country
|
||||
|
||||
# auth.id 수집 (2순위, 없는 경우에만)
|
||||
if cohort[uid]['auth_id'] == 'N/A':
|
||||
@ -822,13 +839,22 @@ def build_fixed_msearch_queries(
|
||||
d0_end = (create_time_dt + timedelta(hours=24)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
d1_start = d0_end
|
||||
d1_end = (create_time_dt + timedelta(hours=48)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
d2_start = d1_end
|
||||
d6_end = (create_time_dt + timedelta(hours=168)).strftime('%Y-%m-%dT%H:%M:%SZ') # 168시간 = 7일
|
||||
d7_plus_start = d6_end
|
||||
|
||||
for metric_name, config in metrics_config.items():
|
||||
# 시간 범위 선택
|
||||
if config["time_range"] == "d0":
|
||||
time_filter = {"range": {"@timestamp": {"gte": d0_start, "lt": d0_end}}}
|
||||
else: # d1
|
||||
elif config["time_range"] == "d1":
|
||||
time_filter = {"range": {"@timestamp": {"gte": d1_start, "lt": d1_end}}}
|
||||
elif config["time_range"] == "d2_6":
|
||||
time_filter = {"range": {"@timestamp": {"gte": d2_start, "lt": d6_end}}}
|
||||
elif config["time_range"] == "d7_plus":
|
||||
time_filter = {"range": {"@timestamp": {"gte": d7_plus_start}}}
|
||||
else: # 기본값
|
||||
time_filter = {"range": {"@timestamp": {"gte": d0_start, "lt": d0_end}}}
|
||||
|
||||
# 사용자 식별 필터
|
||||
if "target_field" in config:
|
||||
@ -1085,7 +1111,9 @@ def process_fixed_batch(
|
||||
'nickname': user_data['nickname'],
|
||||
'create_time': user_data.get('create_time_kst', 'N/A'),
|
||||
'retention_status': 'Retained_d0', # 기본값, 나중에 업데이트
|
||||
'last_active_day': 0, # 마지막 접속일 추가
|
||||
'language': user_data['language'],
|
||||
'country': user_data.get('country', 'N/A'),
|
||||
'device': user_data['device'],
|
||||
'active_seconds': user_session_metrics.get('active_seconds', 0),
|
||||
'total_playtime_minutes': user_session_metrics.get('total_playtime_minutes', 0),
|
||||
@ -1205,9 +1233,21 @@ def process_fixed_batch(
|
||||
else:
|
||||
result[metric_name] = 0
|
||||
|
||||
# 리텐션 상태 업데이트
|
||||
if metric_name == "retention_check" and result[metric_name] > 0:
|
||||
result['retention_status'] = 'Retained_d1'
|
||||
# 모든 메트릭 처리 후 리텐션 상태 판정
|
||||
# D7+ > D2~6 > D1 > D0 순서로 판정 (마지막 접속일 기준)
|
||||
if result.get('retention_check_d7_plus', 0) > 0:
|
||||
result['retention_status'] = 'Retained_d7+'
|
||||
result['last_active_day'] = 7 # 7+ 표시
|
||||
elif result.get('retention_check_d2_6', 0) > 0:
|
||||
result['retention_status'] = 'Retained_d2~6'
|
||||
result['last_active_day'] = 2 # 정확한 날짜는 알 수 없으므로 2로 표시
|
||||
elif result.get('retention_check_d1', 0) > 0:
|
||||
result['retention_status'] = 'Retained_d1'
|
||||
result['last_active_day'] = 1
|
||||
else:
|
||||
# D0에만 접속 (기본값 유지)
|
||||
result['retention_status'] = 'Retained_d0'
|
||||
result['last_active_day'] = 0
|
||||
|
||||
# 계산된 지표
|
||||
if result.get('dungeon_entry_count', 0) > 0:
|
||||
@ -1346,9 +1386,9 @@ def write_fixed_results(results: List[Dict], output_path: str) -> None:
|
||||
logger.error("저장할 결과 데이터가 없습니다.")
|
||||
return
|
||||
|
||||
# 수정된 헤더 (first_login_time 제거, create_time만 사용)
|
||||
# 수정된 헤더 (first_login_time 제거, create_time만 사용, country 및 last_active_day 추가)
|
||||
headers = [
|
||||
'uid', 'auth_id', 'nickname', 'create_time', 'retention_status', 'language', 'device',
|
||||
'uid', 'auth_id', 'nickname', 'create_time', 'retention_status', 'last_active_day', 'language', 'country', 'device',
|
||||
'active_seconds', 'total_playtime_minutes', 'session_count', 'avg_session_length', 'logout_abnormal',
|
||||
'dungeon_entry_count', 'dungeon_first_mode', 'dungeon_first_stalker', 'dungeon_first_result',
|
||||
'dungeon_escape_count', 'dungeon_escape_rate', 'avg_survival_time', 'max_survival_time',
|
||||
@ -1464,14 +1504,19 @@ def main():
|
||||
# 요약
|
||||
if results:
|
||||
total_users = len(results)
|
||||
retained_d0 = sum(1 for r in results if r.get('retention_status') == 'Retained_d0')
|
||||
retained_d1 = sum(1 for r in results if r.get('retention_status') == 'Retained_d1')
|
||||
retention_rate = (retained_d1 / total_users) * 100
|
||||
retained_d2_6 = sum(1 for r in results if r.get('retention_status') == 'Retained_d2~6')
|
||||
retained_d7_plus = sum(1 for r in results if r.get('retention_status') == 'Retained_d7+')
|
||||
|
||||
logger.info("=" * 80)
|
||||
logger.info("분석 요약")
|
||||
logger.info("=" * 80)
|
||||
logger.info(f"총 신규 유저: {total_users:,}명")
|
||||
logger.info(f"D+1 리텐션: {retained_d1:,}명 ({retention_rate:.1f}%)")
|
||||
logger.info(f"D+0 리텐션: {retained_d0:,}명 ({(retained_d0/total_users)*100:.1f}%)")
|
||||
logger.info(f"D+1 리텐션: {retained_d1:,}명 ({(retained_d1/total_users)*100:.1f}%)")
|
||||
logger.info(f"D+2~6 리텐션: {retained_d2_6:,}명 ({(retained_d2_6/total_users)*100:.1f}%)")
|
||||
logger.info(f"D+7+ 리텐션: {retained_d7_plus:,}명 ({(retained_d7_plus/total_users)*100:.1f}%)")
|
||||
logger.info(f"평균 활동 시간: {sum(r.get('active_seconds', 0) for r in results) / total_users / 60:.1f}분")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
|
||||
Reference in New Issue
Block a user