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",
|
"agg_type": "count",
|
||||||
"filters": [
|
"filters": [
|
||||||
{"term": {"body.action_type.keyword": "Complete"}},
|
{"term": {"body.action_type.keyword": "Complete"}},
|
||||||
{"term": {"body.stage_type.keyword": "result"}}
|
{"term": {"body.stage_type.keyword": "tutorial_escape_portal"}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"guide_quest_stage": {
|
"guide_quest_stage": {
|
||||||
@ -334,7 +334,8 @@ def get_fixed_metrics_config() -> Dict[str, Dict]:
|
|||||||
"index": "ds-logs-live-item_get",
|
"index": "ds-logs-live-item_get",
|
||||||
"time_range": "d0",
|
"time_range": "d0",
|
||||||
"agg_type": "max",
|
"agg_type": "max",
|
||||||
"field": "body.item_grade"
|
"field": "body.item_grade",
|
||||||
|
"filters": [{"term": {"body.base_type": 2}}]
|
||||||
},
|
},
|
||||||
"blueprint_use_count": {
|
"blueprint_use_count": {
|
||||||
"index": "ds-logs-live-craft_from_blueprint",
|
"index": "ds-logs-live-craft_from_blueprint",
|
||||||
@ -509,11 +510,21 @@ def get_fixed_metrics_config() -> Dict[str, Dict]:
|
|||||||
"field": "body.season_pass_step"
|
"field": "body.season_pass_step"
|
||||||
},
|
},
|
||||||
|
|
||||||
# ==================== 리텐션 판정 (retention_d1 삭제됨) ====================
|
# ==================== 리텐션 판정 ====================
|
||||||
"retention_check": {
|
"retention_check_d1": {
|
||||||
"index": "ds-logs-live-login_comp",
|
"index": "ds-logs-live-heartbeat",
|
||||||
"time_range": "d1",
|
"time_range": "d1",
|
||||||
"agg_type": "exists"
|
"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_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')),
|
'create_time_dt': datetime.fromisoformat(new_user_map[uid]["create_time"].replace('Z', '+00:00')),
|
||||||
'language': 'N/A',
|
'language': 'N/A',
|
||||||
|
'country': 'N/A',
|
||||||
'device': 'N/A',
|
'device': 'N/A',
|
||||||
'nickname': 'N/A'
|
'nickname': 'N/A'
|
||||||
}
|
}
|
||||||
@ -665,14 +677,14 @@ def get_new_user_cohort_optimized(
|
|||||||
"top_hits": {
|
"top_hits": {
|
||||||
"size": 1,
|
"size": 1,
|
||||||
"sort": [{"@timestamp": {"order": "asc"}}],
|
"sort": [{"@timestamp": {"order": "asc"}}],
|
||||||
"_source": ["body.device_mod", "body.nickname", "auth.id"]
|
"_source": ["body.device_mod", "body.nickname", "auth.id", "country"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"latest_info": {
|
"latest_info": {
|
||||||
"top_hits": {
|
"top_hits": {
|
||||||
"size": 1,
|
"size": 1,
|
||||||
"sort": [{"@timestamp": {"order": "desc"}}],
|
"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]['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')
|
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 수집 (1순위)
|
||||||
auth_id = latest_info_hit.get('auth', {}).get('id') or user_hit.get('auth', {}).get('id')
|
auth_id = latest_info_hit.get('auth', {}).get('id') or user_hit.get('auth', {}).get('id')
|
||||||
if auth_id:
|
if auth_id:
|
||||||
@ -713,9 +730,9 @@ def get_new_user_cohort_optimized(
|
|||||||
|
|
||||||
logger.info(f"login_comp에서 {len(login_comp_collected)}명의 정보 수집 완료")
|
logger.info(f"login_comp에서 {len(login_comp_collected)}명의 정보 수집 완료")
|
||||||
|
|
||||||
# Step 4: log_return_to_lobby 인덱스에서 차선 정보 수집 (auth.id 2순위, language fallback)
|
# Step 4: log_return_to_lobby 인덱스에서 차선 정보 수집 (auth.id 2순위)
|
||||||
# login_comp에서 수집되지 않은 유저 + language가 N/A인 유저들 처리
|
# login_comp에서 수집되지 않은 유저들 처리
|
||||||
missing_uids = [uid for uid in uid_list if uid not in login_comp_collected or cohort[uid]['language'] == 'N/A']
|
missing_uids = [uid for uid in uid_list if uid not in login_comp_collected]
|
||||||
|
|
||||||
if missing_uids:
|
if missing_uids:
|
||||||
logger.info(f"log_return_to_lobby 인덱스에서 {len(missing_uids)}명의 차선 정보 수집 중 (auth.id 2순위)...")
|
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':
|
if cohort[uid]['nickname'] == 'N/A':
|
||||||
cohort[uid]['nickname'] = info_hit.get('body', {}).get('nickname', 'N/A')
|
cohort[uid]['nickname'] = info_hit.get('body', {}).get('nickname', 'N/A')
|
||||||
|
|
||||||
# language fallback: country 값 사용 (language가 N/A인 경우에만)
|
# country 수집 (없는 경우에만)
|
||||||
if cohort[uid]['language'] == 'N/A':
|
if cohort[uid]['country'] == 'N/A':
|
||||||
country = info_hit.get('country')
|
country = info_hit.get('country')
|
||||||
if country:
|
if country:
|
||||||
cohort[uid]['language'] = f"country-{country}"
|
cohort[uid]['country'] = country
|
||||||
|
|
||||||
# auth.id 수집 (2순위, 없는 경우에만)
|
# auth.id 수집 (2순위, 없는 경우에만)
|
||||||
if cohort[uid]['auth_id'] == 'N/A':
|
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')
|
d0_end = (create_time_dt + timedelta(hours=24)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
d1_start = d0_end
|
d1_start = d0_end
|
||||||
d1_end = (create_time_dt + timedelta(hours=48)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
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():
|
for metric_name, config in metrics_config.items():
|
||||||
# 시간 범위 선택
|
# 시간 범위 선택
|
||||||
if config["time_range"] == "d0":
|
if config["time_range"] == "d0":
|
||||||
time_filter = {"range": {"@timestamp": {"gte": d0_start, "lt": d0_end}}}
|
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}}}
|
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:
|
if "target_field" in config:
|
||||||
@ -1085,7 +1111,9 @@ def process_fixed_batch(
|
|||||||
'nickname': user_data['nickname'],
|
'nickname': user_data['nickname'],
|
||||||
'create_time': user_data.get('create_time_kst', 'N/A'),
|
'create_time': user_data.get('create_time_kst', 'N/A'),
|
||||||
'retention_status': 'Retained_d0', # 기본값, 나중에 업데이트
|
'retention_status': 'Retained_d0', # 기본값, 나중에 업데이트
|
||||||
|
'last_active_day': 0, # 마지막 접속일 추가
|
||||||
'language': user_data['language'],
|
'language': user_data['language'],
|
||||||
|
'country': user_data.get('country', 'N/A'),
|
||||||
'device': user_data['device'],
|
'device': user_data['device'],
|
||||||
'active_seconds': user_session_metrics.get('active_seconds', 0),
|
'active_seconds': user_session_metrics.get('active_seconds', 0),
|
||||||
'total_playtime_minutes': user_session_metrics.get('total_playtime_minutes', 0),
|
'total_playtime_minutes': user_session_metrics.get('total_playtime_minutes', 0),
|
||||||
@ -1205,9 +1233,21 @@ def process_fixed_batch(
|
|||||||
else:
|
else:
|
||||||
result[metric_name] = 0
|
result[metric_name] = 0
|
||||||
|
|
||||||
# 리텐션 상태 업데이트
|
# 모든 메트릭 처리 후 리텐션 상태 판정
|
||||||
if metric_name == "retention_check" and result[metric_name] > 0:
|
# 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['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:
|
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("저장할 결과 데이터가 없습니다.")
|
logger.error("저장할 결과 데이터가 없습니다.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 수정된 헤더 (first_login_time 제거, create_time만 사용)
|
# 수정된 헤더 (first_login_time 제거, create_time만 사용, country 및 last_active_day 추가)
|
||||||
headers = [
|
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',
|
'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_entry_count', 'dungeon_first_mode', 'dungeon_first_stalker', 'dungeon_first_result',
|
||||||
'dungeon_escape_count', 'dungeon_escape_rate', 'avg_survival_time', 'max_survival_time',
|
'dungeon_escape_count', 'dungeon_escape_rate', 'avg_survival_time', 'max_survival_time',
|
||||||
@ -1464,14 +1504,19 @@ def main():
|
|||||||
# 요약
|
# 요약
|
||||||
if results:
|
if results:
|
||||||
total_users = len(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')
|
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("=" * 80)
|
||||||
logger.info("분석 요약")
|
logger.info("분석 요약")
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
logger.info(f"총 신규 유저: {total_users:,}명")
|
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}분")
|
logger.info(f"평균 활동 시간: {sum(r.get('active_seconds', 0) for r in results) / total_users / 60:.1f}분")
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
Reference in New Issue
Block a user