Compare commits
4 Commits
901e5ba647
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 174e41c5b7 | |||
| 6f35be1564 | |||
| 98f3e739eb | |||
| 79d9fba330 |
11
.claude/settings.local.json
Normal file
11
.claude/settings.local.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(pip install:*)",
|
||||||
|
"Read(C:\\Users/**)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/analysis_results
|
||||||
528
README.md
Normal file
528
README.md
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
# 던전 스토커즈 신규 유저 리텐션 비교 분석 기획서
|
||||||
|
|
||||||
|
## 1. 분석 개요
|
||||||
|
|
||||||
|
### 1.1 분석 목표
|
||||||
|
- 스팀 얼리액세스 기간 중 유입된 신규 유저의 첫 24시간(D+0) 플레이 패턴을 분석
|
||||||
|
- D+1 리텐션(24-48시간 재접속)에 영향을 미치는 핵심 요소 파악
|
||||||
|
- 이탈 유저와 리텐션 유저 간의 행동 패턴 차이 분석
|
||||||
|
|
||||||
|
### 1.2 분석 대상
|
||||||
|
- 스크립트 실행 시 `--start-time`과 `--end-time`으로 지정된 기간에 처음 접속한 모든 유저
|
||||||
|
- 예시: `python ds_new_user_analy.py --start-time "2025-08-22T12:00:00+09:00" --end-time "2025-08-25T12:00:00+09:00"`
|
||||||
|
|
||||||
|
### 1.3 그룹 분류
|
||||||
|
- Retained_d0: 첫 접속 후 24시간 이내에만 접속한 유저 (D+0 이탈)
|
||||||
|
- Retained_d1: 마지막 접속이 D+1인 유저
|
||||||
|
- Retained_d2: 마지막 접속이 D+2인 유저
|
||||||
|
- Retained_d3: 마지막 접속이 D+3인 유저
|
||||||
|
- Retained_d4: 마지막 접속이 D+4인 유저
|
||||||
|
- Retained_d5: 마지막 접속이 D+5인 유저
|
||||||
|
- Retained_d6: 마지막 접속이 D+6인 유저
|
||||||
|
- Retained_d7+: D+7 이후에도 접속한 유저
|
||||||
|
|
||||||
|
### 1.4 OpenSearch 연결 정보
|
||||||
|
- opensearch:
|
||||||
|
- host: "ds-opensearch.oneunivrs.com"
|
||||||
|
- port: 9200
|
||||||
|
- auth:
|
||||||
|
- username: "admin"
|
||||||
|
- password: "DHp5#r#GYQ9d"
|
||||||
|
- use_ssl: true
|
||||||
|
- verify_certs: false
|
||||||
|
- timeout: 60
|
||||||
|
- max_retries: 3
|
||||||
|
- headers={"Connection": "close"}
|
||||||
|
|
||||||
|
## 2. OpenSearch 인덱스별 수집 데이터
|
||||||
|
|
||||||
|
### 2.1 유저 식별 및 세션 관리
|
||||||
|
| 인덱스 | 수집 항목 | 분석 용도 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `ds-logs-live-create_uid` | `uid`, `auth.id`, `@timestamp` | 신규 유저 식별 |
|
||||||
|
| `ds-logs-live-login_comp` | `uid`, `auth.id`, `@timestamp`, `body.nickname`, `body.language`, `country` | 첫 로그인 시간, 리텐션 판정, 닉네임, 사용 언어, 국가 |
|
||||||
|
| `ds-logs-live-logout` | `uid`, `@timestamp` | 세션 종료 시간, 비정상 종료 여부 |
|
||||||
|
| `ds-logs-live-heartbeat` | `uid`, `@timestamp` | 실제 활동 시간 추적 |
|
||||||
|
|
||||||
|
### 2.2 게임 진행 및 성과
|
||||||
|
| 인덱스 | 수집 항목 | 분석 용도 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `ds-logs-live-survival_sta` | `uid`, `body.dungeon_mode`, `body.stalker_name`, `@timestamp` | 플레이한 모드, 던전 진입 횟수, 선택한 스토커 이름, 가장 먼저 시작한 모드 |
|
||||||
|
| `ds-logs-live-survival_end` | `uid`, `body.dungeon_mode`, `body.result`, `body.play_stats.playtime`, `body.play_stats.armor_break_cnt`, `body.play_stats.monster_kill_cnt`, `body.play_stats.player_kill_cnt`, `body.play_stats.raid_play`, `body.play_stats.damage_dealt_monster`, `body.play_stats.damage_dealt_player` | 던전 탈출 성공률, 생존 시간, 갑옷 파괴 횟수, 몬스터 킬 수, 플레이어 킬 수, 레이드 횟수, 몬스터에게 입힌 데미지, 플레이어에게 입힌 데미지 |
|
||||||
|
| `ds-logs-live-level_up` | `uid`, `body.level`, `body.stalker`| 각 스토커별 최고 레벨 |
|
||||||
|
| `ds-logs-live-dead` | `uid`, `body.inter_type` | 사망 횟수, 사망 원인 (PK 제외) |
|
||||||
|
| `ds-logs-live-player_kill` | `body.target_uid` | PK로 인한 사망 |
|
||||||
|
| `ds-logs-live-obj_inter` | `uid`, `inter_type` | 오브젝트 상호작용 |
|
||||||
|
| `ds-logs-live-item_ingame_equip` | `uid`, `body.base_type`, `body.item_grade`, `body.item_type`, `body.part_type` | 인게임 장비 장착 |
|
||||||
|
| `ds-logs-live-skill_point_get` | `uid` | 인게임 스킬 포인트 획득 경험 |
|
||||||
|
|
||||||
|
|
||||||
|
### 2.3 튜토리얼 및 퀘스트
|
||||||
|
| 인덱스 | 수집 항목 | 분석 용도 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `ds-logs-live-tutorial_entry` | `uid`, `@timestamp`, `body.action` | 튜토리얼 진입 여부 (시작 또는 건너뛰기) |
|
||||||
|
| `ds-logs-live-log_tutorial` | `uid`, `body.action_type`, `body.stage_type` | 튜토리얼 완료 여부 (body.action_type: Complete 그리고 body.stage_type: result 인 경우 완료한 것임) |
|
||||||
|
| `ds-logs-live-guide_quest_stage` | `uid`, `body.guide_step` | 가이드 퀘스트 진행도 |
|
||||||
|
|
||||||
|
### 2.4 매칭 시스템
|
||||||
|
| 인덱스 | 수집 항목 | 분석 용도 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `ds-logs-live-matching_start` | `uid`, `body.game_mode` | 매칭 시도 횟수 및 모드 |
|
||||||
|
| `ds-logs-live-matching_complete` | `uid`, `body.game_mode`, `body.matchingtime` | 모드 별 매칭 성공률, 평균 대기 시간 |
|
||||||
|
| `ds-logs-live-matching_failed` | `uid`, `body.fail_type` | 매칭 실패 원인 |
|
||||||
|
|
||||||
|
### 2.5 아이템 및 경제
|
||||||
|
| 인덱스 | 수집 항목 | 분석 용도 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `ds-logs-live-item_get` | `uid`, `body.itemid`, `body.base_type`, `body.item_grade`, `body.item_type` | 아이템 획득 패턴 |
|
||||||
|
| `ds-logs-live-shop_buy` | `uid`, `body.cost_id`, `body.amt` | 구매 사용 금화량 (body.cost_id: i108000 일 경우만 합산) |
|
||||||
|
| `ds-logs-live-shop_sell` | `uid`, `body.cost_id`, `body.amt` | 판매 획득 금화량 (body.cost_id: i108000 일 경우만 합산) |
|
||||||
|
| `ds-logs-live-storage_use` | `uid`, `body.oper_type` | 창고 사용 여부 (입/출고) |
|
||||||
|
| `ds-logs-live-enchant` | `uid`, `body.amt` | 강화 소비 금화량 |
|
||||||
|
|
||||||
|
### 2.6 소셜 기능
|
||||||
|
| 인덱스 | 수집 항목 | 분석 용도 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `ds-logs-live-friend` | `uid`, `body.oper_type`, `body.friend_type` | 친구 추가/삭제 |
|
||||||
|
| `ds-logs-live-player_invite` | `uid`, `body.target_uid` | 파티 초대 한 경험, 파티 초대 받은 경험 |
|
||||||
|
| `ds-logs-live-mail_read` | `uid`, `@timestamp` | 메일 확인 경험 |
|
||||||
|
|
||||||
|
### 2.7 거래소 및 경제 활동
|
||||||
|
| 인덱스 | 수집 항목 | 분석 용도 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `ds-logs-live-exchange_reg` | `uid`, `@timestamp` | 거래소 등록 경험 |
|
||||||
|
| `ds-logs-live-exchange_use` | `uid`, `@timestamp` | 거래소 구입 경험 |
|
||||||
|
| `ds-logs-live-coupon` | `uid`, `body.coupon_code`, `@timestamp` | 리딤 코드 사용 |
|
||||||
|
|
||||||
|
### 2.8 기타 활동
|
||||||
|
| 인덱스 | 수집 항목 | 분석 용도 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `ds-logs-live-button_click` | `uid`, `body.button_id` | 버튼 클릭 이력 |
|
||||||
|
| `ds-logs-live-log_hideout_upgrade` | `uid`, `body.hideout_level`, `body.hideout_type`, `body.hideout_upgrade_event` | 은신처 업그레이드 |
|
||||||
|
| `ds-logs-live-season_pass` | `uid`, `bbody.cause`, `body.season_pass_step`, `body.season_pass_type` | 시즌패스 진행 기록 |
|
||||||
|
|
||||||
|
## 3. 분석 지표 설계
|
||||||
|
|
||||||
|
### 3.1 기본 정보
|
||||||
|
- `uid`: 유저 ID
|
||||||
|
- `auth_id`: 스팀 ID (auth.id)
|
||||||
|
- `nickname`: 유저 닉네임 (body.nickname from login_comp)
|
||||||
|
- `create_time`: 계정 생성 시간 (KST) - create_uid 기반
|
||||||
|
- `retention_status`: 리텐션 상태 (Retained_d0/d1/d2/d3/d4/d5/d6/d7+)
|
||||||
|
- `language`: 사용 언어 (body.language from login_comp 최신 기록)
|
||||||
|
- `country`: 국가 정보 (country from login_comp)
|
||||||
|
- `device`: 디바이스 정보 (body.device_mod)
|
||||||
|
|
||||||
|
### 3.2 플레이 시간 및 세션
|
||||||
|
- `session_count`: D+0 접속 횟수 (login_comp 카운트)
|
||||||
|
- `active_seconds`: D+0 실제 활동 시간 (초) - heartbeat 기반 계산 (--full 옵션 사용 시)
|
||||||
|
- `total_playtime_minutes`: D+0 총 플레이 시간 (분) (--full 옵션 사용 시)
|
||||||
|
- `avg_session_length`: 평균 세션 길이 (분) (--full 옵션 사용 시)
|
||||||
|
- `logout_abnormal`: 비정상 종료 여부 (0/1) - last_logout < last_login 체크 (--full 옵션 사용 시)
|
||||||
|
|
||||||
|
### 3.3 던전 플레이 성과
|
||||||
|
- **공통 지표**
|
||||||
|
- `dungeon_first_mode`: 처음 플레이한 던전 모드
|
||||||
|
- `dungeon_first_stalker`: 처음 선택한 스토커
|
||||||
|
- `dungeon_first_result`: 첫 던전 결과 (0: 사망, 1: 탈출, null: 미플레이)
|
||||||
|
|
||||||
|
- **모드별 지표** (COOP, Solo, Survival, Survival_BOT, Survival_Unprotected)
|
||||||
|
- `{mode}_entry_count`: 모드별 던전 진입 횟수
|
||||||
|
- `{mode}_first_result`: 모드별 첫 플레이 결과
|
||||||
|
- `{mode}_escape_count`: 모드별 탈출 성공 횟수
|
||||||
|
- `{mode}_avg_survival_time`: 모드별 평균 생존 시간
|
||||||
|
- `{mode}_max_survival_time`: 모드별 최대 생존 시간
|
||||||
|
- `{mode}_armor_break_count`: 모드별 갑옷 파괴 횟수
|
||||||
|
- `{mode}_raid_play_count`: 모드별 레이드 횟수
|
||||||
|
|
||||||
|
### 3.4 전투 성과
|
||||||
|
- **모드별 킬 수**
|
||||||
|
- `{mode}_monster_kill_count`: 모드별 몬스터 처치 수
|
||||||
|
- `{mode}_player_kill_count`: 모드별 플레이어 킬 수
|
||||||
|
|
||||||
|
- **사망 원인 분석**
|
||||||
|
- `death_PK`: PK로 인한 사망
|
||||||
|
- `death_GiveUp`: 포기로 인한 사망 (inter_type = 0)
|
||||||
|
- `death_Mob`: 몬스터에게 사망 (inter_type = 1)
|
||||||
|
- `death_Trap`: 함정에 의한 사망 (inter_type = 10)
|
||||||
|
- `death_Red`: 레드존 사망 (inter_type = 11)
|
||||||
|
- `death_Others`: 기타 원인 사망
|
||||||
|
|
||||||
|
### 3.5 진행도 및 성장
|
||||||
|
- `level_max`: 도달한 최고 레벨 (body.level from level_up 최댓값)
|
||||||
|
- `level_max_stalker`: 최고 레벨 달성 스토커 이름 (body.stalker from level_up)
|
||||||
|
- `tutorial_entry`: 튜토리얼 진입 여부 (body.action = "Start")
|
||||||
|
- `tutorial_completed`: 튜토리얼 완료 여부 (body.action_type = "Complete" and body.stage_type = "result")
|
||||||
|
- `guide_quest_stage`: 가이드 퀘스트 최대 진행 단계 (body.guide_step)
|
||||||
|
- `skill_points_earned`: 획득한 스킬 포인트 수 (skill_point_get 카운트)
|
||||||
|
|
||||||
|
### 3.6 아이템 및 경제
|
||||||
|
- `highest_item_grade`: 획득한 최고 등급 장비 아이템 (body.item_grade, base_type = 2)
|
||||||
|
- `blueprint_use_count`: 블루프린트 사용 횟수 (craft_from_blueprint 카운트)
|
||||||
|
- `shop_buy_count`: 상점 구매 횟수 (shop_buy 카운트)
|
||||||
|
- `shop_sell_count`: 상점 판매 횟수 (shop_sell 카운트)
|
||||||
|
- `gold_spent`: 총 소비 금화 (body.cost_id = "i108000"인 경우 body.amt 합계)
|
||||||
|
- `gold_earned`: 총 획득 금화 (판매로 얻은 금화)
|
||||||
|
- `storage_in_count`: 창고 입고 횟수 (body.oper_type = 1 카운트)
|
||||||
|
- `storage_out_count`: 창고 출고 횟수 (body.oper_type = -1 카운트)
|
||||||
|
- `enchant_count`: 강화 시도 횟수 (enchant 카운트)
|
||||||
|
- `enchant_gold_spent`: 강화 소비 금화 (body.amt 합계)
|
||||||
|
|
||||||
|
### 3.7 장비 관리
|
||||||
|
- `ingame_equip_count`: 인게임 장비 장착 횟수 (body.base_type = 2)
|
||||||
|
- `equip_by_grade`: 등급별 장착 아이템 수 (body.item_grade 분포)
|
||||||
|
- `equip_by_type`: 타입별 장착 아이템 수 (body.item_type 분포)
|
||||||
|
- `equip_by_part`: 부위별 장착 아이템 수 (body.part_type 분포)
|
||||||
|
|
||||||
|
### 3.8 오브젝트 상호작용
|
||||||
|
- `object_interaction_count`: 오브젝트 상호작용 총 횟수 (obj_inter 카운트)
|
||||||
|
- `interaction_by_type`: 타입별 상호작용 횟수 (body.inter_type 분포)
|
||||||
|
|
||||||
|
### 3.9 매칭 시스템
|
||||||
|
- `matching_start_count`: 매칭 시작 횟수 (matching_start 카운트)
|
||||||
|
- `matching_complete_count`: 매칭 성공 횟수 (matching_complete 카운트)
|
||||||
|
- `matching_failed_count`: 매칭 실패 횟수 (matching_failed 카운트)
|
||||||
|
- `avg_matching_time`: 평균 매칭 시간 (body.matchingtime 평균)
|
||||||
|
- `matching_by_mode`: 모드별 매칭 횟수 (body.game_mode 분포)
|
||||||
|
- `matching_fail_types`: 실패 원인별 횟수 (body.fail_type 분포)
|
||||||
|
|
||||||
|
### 3.10 소셜 활동
|
||||||
|
- `friend_add_count`: 친구 추가 수 (body.oper_type = 0, body.friend_type = 0)
|
||||||
|
- `friend_delete_count`: 친구 삭제 수 (body.oper_type = 1)
|
||||||
|
- `party_invite_sent`: 파티 초대 보낸 횟수 (player_invite with uid = sender)
|
||||||
|
- `party_invite_received`: 파티 초대 받은 횟수 (body.target_uid = uid)
|
||||||
|
- `mail_read_count`: 메일 읽은 횟수 (mail_read 카운트)
|
||||||
|
|
||||||
|
### 3.11 거래소 및 경제 활동
|
||||||
|
- `exchange_register_count`: 거래소 아이템 등록 횟수 (exchange_reg 카운트)
|
||||||
|
- `exchange_use_count`: 거래소 구매 횟수 (exchange_use 카운트)
|
||||||
|
- `coupon_used`: 쿠폰 사용 여부 (coupon 인덱스 존재 여부)
|
||||||
|
- `coupon_codes`: 사용한 쿠폰 코드 목록 (body.coupon_code)
|
||||||
|
|
||||||
|
### 3.12 기타 활동
|
||||||
|
- `button_click_count`: UI 버튼 클릭 수 (button_click 카운트)
|
||||||
|
- `button_click_types`: 클릭한 버튼 종류 (body.button_id 고유값 수)
|
||||||
|
- `hideout_upgrade_count`: 은신처 업그레이드 횟수 (log_hideout_upgrade 카운트)
|
||||||
|
- `hideout_max_level`: 은신처 최고 레벨 (body.hideout_level 최댓값)
|
||||||
|
- `hideout_types_upgraded`: 업그레이드한 은신처 종류 (body.hideout_type 고유값)
|
||||||
|
- `season_pass_buy`: 시즌패스 구매 여부 (body.cause = 1)
|
||||||
|
- `season_pass_max_step`: 시즌패스 최대 단계 (body.season_pass_step 최댓값)
|
||||||
|
|
||||||
|
## 4. 구현 계획
|
||||||
|
|
||||||
|
### 4.1 스크립트 구조
|
||||||
|
```
|
||||||
|
ds_new_user_analy.py
|
||||||
|
├── 설정 및 상수 정의
|
||||||
|
├── OpenSearch 연결
|
||||||
|
├── 명령줄 인자 파싱
|
||||||
|
├── 신규 유저 코호트 추출
|
||||||
|
├── 리텐션 그룹 분류
|
||||||
|
├── 배치 단위 데이터 수집 (msearch 활용)
|
||||||
|
├── 데이터 집계 및 가공
|
||||||
|
└── CSV 파일 출력
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 주요 기능
|
||||||
|
1. 명령줄 인터페이스
|
||||||
|
- `--start-time`: 분석 시작 시간 (KST)
|
||||||
|
- `--end-time`: 분석 종료 시간 (KST)
|
||||||
|
- `--output-dir`: 결과 파일 저장 경로 (기본값: 현재 폴더)
|
||||||
|
- `--batch-size`: 배치 처리 크기 (기본값: 1000)
|
||||||
|
- `--max-workers`: 병렬 처리 스레드 수 (기본값: 16)
|
||||||
|
- `--full`: 세션 관련 상세 지표 포함 (기본값: False)
|
||||||
|
- `--sample-size`: 샘플 분석 크기 (None이면 전체 분석)
|
||||||
|
|
||||||
|
2. 데이터 처리
|
||||||
|
- OpenSearch `scan` API를 활용한 대용량 데이터 스캔
|
||||||
|
- `msearch`를 활용한 효율적인 배치 쿼리 (한 번에 여러 인덱스 쿼리)
|
||||||
|
- nested 필드 처리 (body.로 시작하는 필드)
|
||||||
|
- ThreadPoolExecutor를 활용한 병렬 처리
|
||||||
|
- 진행 상황 표시 (tqdm)
|
||||||
|
|
||||||
|
3. 에러 처리 및 최적화
|
||||||
|
- 연결 실패 시 재시도
|
||||||
|
- 부분 실패 시 로깅 및 계속 진행
|
||||||
|
- timeout 설정 (120초)
|
||||||
|
- scroll timeout 설정 (5분)
|
||||||
|
- 실시간 타이머 표시
|
||||||
|
|
||||||
|
4. 쿼리 최적화
|
||||||
|
- uid와 auth.id를 모두 포함하는 user_identity_filter 사용
|
||||||
|
- D+0 범위 필터링 (첫 로그인 ~ 24시간)
|
||||||
|
- D+1 리텐션 판정 (24시간 ~ 48시간)
|
||||||
|
- 시간대 설정 (KST/UTC 변환)
|
||||||
|
|
||||||
|
### 4.3 핵심 알고리즘 (hack-detector 기법 적용)
|
||||||
|
|
||||||
|
1. **신규 유저 코호트 선정 (Composite Aggregation 최적화)**
|
||||||
|
- composite aggregation으로 메모리 효율적 처리
|
||||||
|
- 페이징 방식으로 대용량 데이터 안정적 처리
|
||||||
|
- auth.id별 첫 로그인 시간을 aggregation으로 직접 계산
|
||||||
|
|
||||||
|
2. **Active Hours 계산 (스트리밍 방식)**
|
||||||
|
- Generator 패턴으로 메모리 사용 최소화
|
||||||
|
- 세션 간격 5분 기준으로 자동 세션 분리
|
||||||
|
- 최대 세션당 3시간 제한으로 이상값 처리
|
||||||
|
|
||||||
|
3. **OpenSearch 쿼리 최적화 (매핑 기반)**
|
||||||
|
```python
|
||||||
|
# body 필드는 nested가 아닌 object 타입으로 처리
|
||||||
|
# 잘못된 nested 쿼리 대신 일반 필드 쿼리 사용
|
||||||
|
{"term": {"body.field_name.keyword": "value"}} # nested 제거
|
||||||
|
|
||||||
|
# 키워드 필드 사용으로 정확성 향상
|
||||||
|
{"term": {"body.dungeon_mode.keyword": mode}} # .keyword 추가
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **msearch 배치 처리 (NDJSON + 백오프 재시도)**
|
||||||
|
```python
|
||||||
|
# NDJSON 직접 생성으로 성능 향상
|
||||||
|
body_ndjson = "\n".join(json.dumps(x, ensure_ascii=False) for x in body_list) + "\n"
|
||||||
|
|
||||||
|
# 지수 백오프 재시도
|
||||||
|
for delay in [1, 2, 4, 8, 16]:
|
||||||
|
try:
|
||||||
|
response = client.msearch(body=body_ndjson, request_timeout=60)
|
||||||
|
break
|
||||||
|
except: time.sleep(delay)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **병렬 처리 최적화 (Future 패턴)**
|
||||||
|
- ThreadPoolExecutor의 Future 패턴으로 비동기 결과 수집
|
||||||
|
- 동적 청크 크기 조정 (사용자 수 기반)
|
||||||
|
- 실패한 청크 자동 재처리 메커니즘
|
||||||
|
|
||||||
|
6. **메모리 최적화 기법**
|
||||||
|
- 스트리밍 CSV 작성 (결과를 메모리에 모우지 않음)
|
||||||
|
- defaultdict와 generator 활용
|
||||||
|
- track_total_hits=False로 불필요한 카운트 생략
|
||||||
|
|
||||||
|
### 4.4 OpenSearch 매핑 기반 수정사항
|
||||||
|
|
||||||
|
**중요한 발견**: OpenSearch 매핑 분석 결과 `body` 필드가 `nested` 타입이 아닌 `object` 타입으로 확인됨
|
||||||
|
|
||||||
|
1. **nested 쿼리 제거**: `{"nested": {"path": "body", ...}}` → `{"term": {"body.field": value}}`
|
||||||
|
2. **키워드 필드 사용**: 문자열 필드에 `.keyword` 추가로 정확한 매칭
|
||||||
|
3. **retention_d1 필드 제거**: `retention_status`와 중복으로 제거
|
||||||
|
4. **필드 경로 정확성**: 실제 매핑 구조에 맞춘 필드 참조
|
||||||
|
|
||||||
|
### 4.5 개발 과정의 주요 시행착오 및 해결 방법
|
||||||
|
|
||||||
|
#### 4.5.1 데이터 수집 0값 문제 해결 (Critical Issue)
|
||||||
|
|
||||||
|
**문제 현상**:
|
||||||
|
- 초기 실행 시 모든 게임 관련 지표(dungeon_entry_count, monster_kill_count, player_kill_count 등)가 0으로 수집됨
|
||||||
|
- 사용자가 실제로 게임을 플레이했음에도 불구하고 활동 데이터가 누락됨
|
||||||
|
|
||||||
|
**근본 원인 분석**:
|
||||||
|
1. **잘못된 nested 쿼리 구조**: OpenSearch 매핑이 `object` 타입인데 `nested` 쿼리 사용
|
||||||
|
2. **필드 경로 오류**: `body.uid` 대신 직접 `uid` 필드 사용해야 함
|
||||||
|
3. **track_total_hits 설정**: `track_total_hits=False`로 인한 count 집계 실패
|
||||||
|
4. **필드명 불일치**: 실제 인덱스 필드명과 쿼리 필드명 mismatch
|
||||||
|
|
||||||
|
**해결 과정**:
|
||||||
|
```python
|
||||||
|
# 문제가 있던 코드 (Before)
|
||||||
|
{
|
||||||
|
"nested": {
|
||||||
|
"path": "body",
|
||||||
|
"query": {"term": {"body.uid": uid}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 수정된 코드 (After)
|
||||||
|
{"term": {"uid.keyword": uid}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**검증 방법**:
|
||||||
|
- OpenSearch DevTools에서 직접 쿼리 테스트
|
||||||
|
- 샘플 사용자 100명으로 실제 데이터 확인
|
||||||
|
- 각 지표별 개별 쿼리 검증 후 msearch 통합
|
||||||
|
|
||||||
|
#### 4.5.2 first_value 쿼리 타입 문제 해결
|
||||||
|
|
||||||
|
**문제 현상**:
|
||||||
|
- `dungeon_first_mode`와 `dungeon_first_stalker`가 모두 0으로 출력
|
||||||
|
- 실제로는 "COOP", "Rene", "Hilda" 등의 문자열 데이터 존재
|
||||||
|
|
||||||
|
**근본 원인**:
|
||||||
|
1. **정렬 필드 오류**: 문자열 필드(`body.dungeon_mode`)로 정렬 시도
|
||||||
|
2. **기본값 처리 오류**: 문자열 필드에 숫자 기본값 0 할당
|
||||||
|
3. **쿼리 구조 문제**: `@timestamp`로 정렬 후 첫 번째 문서 추출 필요
|
||||||
|
|
||||||
|
**해결 방법**:
|
||||||
|
```python
|
||||||
|
# 문제 코드
|
||||||
|
sort_field = config.get("field", "@timestamp") # body.dungeon_mode로 정렬 시도
|
||||||
|
query_body["sort"] = [{sort_field: {"order": sort_order}}]
|
||||||
|
|
||||||
|
# 수정 코드
|
||||||
|
if agg_type == "first_value":
|
||||||
|
query_body["sort"] = [{"@timestamp": {"order": "asc"}}] # 항상 timestamp로 정렬
|
||||||
|
|
||||||
|
# 기본값 처리 수정
|
||||||
|
if metric_name in ["dungeon_first_mode", "dungeon_first_stalker"]:
|
||||||
|
result[metric_name] = "" # 문자열 기본값
|
||||||
|
```
|
||||||
|
|
||||||
|
**결과 검증**:
|
||||||
|
- COOP, Survival, Survival_Unprotected 등 다양한 모드 수집 확인
|
||||||
|
- Rene, Nave, Hilda, Rio, Baran, Lian 등 스토커 이름 정상 수집
|
||||||
|
|
||||||
|
#### 4.5.3 게임당 평균 데미지 로직 구현
|
||||||
|
|
||||||
|
**요구사항 변경**:
|
||||||
|
- 기존: `damage_dealt_monster`, `damage_dealt_player` 절대값
|
||||||
|
- 변경: 게임당 평균 데미지로 수평 비교 가능하게 수정
|
||||||
|
|
||||||
|
**구현 방식**:
|
||||||
|
1. **기존 damage 필드 제거**: metrics_config에서 완전 제거
|
||||||
|
2. **실시간 계산**: 던전 입장 횟수가 있는 경우에만 평균 계산
|
||||||
|
3. **별도 쿼리 실행**: survival_end 인덱스에서 직접 aggregation 수행
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 평균 데미지 계산 로직
|
||||||
|
if result.get('dungeon_entry_count', 0) > 0:
|
||||||
|
damage_query = {
|
||||||
|
"aggs": {
|
||||||
|
"total_monster_damage": {"sum": {"field": "body.play_stats.damage_dealt_monster"}},
|
||||||
|
"total_player_damage": {"sum": {"field": "body.play_stats.damage_dealt_player"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result['avg_damage_per_game_monster'] = round(monster_damage / dungeon_count, 2)
|
||||||
|
result['avg_damage_per_game_player'] = round(player_damage / dungeon_count, 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.5.4 false positive 디버깅 - party_invite_received & season_pass_buy
|
||||||
|
|
||||||
|
**문제 제기**:
|
||||||
|
- 100명 샘플에서 `party_invite_received`와 `season_pass_buy`가 모두 0
|
||||||
|
- 실제 오류인지 정상 동작인지 확인 필요
|
||||||
|
|
||||||
|
**디버깅 과정**:
|
||||||
|
|
||||||
|
1. **party_invite_received 검증**:
|
||||||
|
```bash
|
||||||
|
# OpenSearch에서 직접 확인
|
||||||
|
curl -X GET "ds-logs-live-player_invite/_search"
|
||||||
|
# 결과: 데이터 존재하지만 테스트 사용자들은 실제로 초대받지 않음
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **season_pass_buy 검증**:
|
||||||
|
```bash
|
||||||
|
# cause 값 분포 확인
|
||||||
|
{"aggs": {"cause_values": {"terms": {"field": "body.cause"}}}}
|
||||||
|
# 결과: cause:0 (진행) 95,106개, cause:1 (구매) 0개
|
||||||
|
```
|
||||||
|
|
||||||
|
**결론**:
|
||||||
|
- `party_invite_received`: 정상 동작, 실제로 테스트 기간 중 초대받은 사용자 없음
|
||||||
|
- `season_pass_buy`: 정상 동작, 실제로 시즌패스 구매한 사용자 없음
|
||||||
|
|
||||||
|
#### 4.5.5 성능 최적화 및 안정성 개선
|
||||||
|
|
||||||
|
**병렬처리 최적화**:
|
||||||
|
```python
|
||||||
|
# ThreadPoolExecutor 활용
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
for chunk in chunked_uids:
|
||||||
|
future = executor.submit(process_fixed_batch, client, chunk, cohort, metrics_config)
|
||||||
|
future_to_chunk[future] = chunk
|
||||||
|
```
|
||||||
|
|
||||||
|
**에러 처리 강화**:
|
||||||
|
- 지수 백오프 재시도 로직
|
||||||
|
- 실패한 배치 자동 재처리
|
||||||
|
- 상세한 로깅 및 진행상황 표시
|
||||||
|
|
||||||
|
**메모리 최적화**:
|
||||||
|
- Generator 패턴으로 스트리밍 처리
|
||||||
|
- 불필요한 데이터 early cleanup
|
||||||
|
- 배치 크기 동적 조정
|
||||||
|
|
||||||
|
#### 4.5.6 국가 정보 → 언어 정보 변경
|
||||||
|
|
||||||
|
**변경 사유**:
|
||||||
|
- VPN 사용으로 인한 국가 정보 부정확성
|
||||||
|
- 언어 설정이 사용자 특성 분석에 더 유효
|
||||||
|
|
||||||
|
**구현 방식**:
|
||||||
|
```python
|
||||||
|
"latest_info": {
|
||||||
|
"top_hits": {
|
||||||
|
"size": 1,
|
||||||
|
"sort": [{"@timestamp": {"order": "desc"}}],
|
||||||
|
"_source": ["body.nickname", "body.language"] # country → language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.5.7 최종 검증 및 품질 보증
|
||||||
|
|
||||||
|
**검증 단계**:
|
||||||
|
1. **5명 샘플 테스트**: 기본 로직 확인
|
||||||
|
2. **30명 중간 테스트**: 성능 및 안정성 확인
|
||||||
|
3. **100명 전체 테스트**: 모든 지표 정상 동작 확인
|
||||||
|
|
||||||
|
**품질 지표**:
|
||||||
|
- 총 66개 지표 중 66개 모두 정상 수집
|
||||||
|
- D+1 리텐션율: 40% (40/100명)
|
||||||
|
- 평균 처리 시간: 100명/2분
|
||||||
|
- 에러율: 0% (모든 배치 성공)
|
||||||
|
|
||||||
|
### 4.5.8 향후 유사 스크립트 개발 시 주의사항
|
||||||
|
|
||||||
|
**OpenSearch 쿼리 개발**:
|
||||||
|
1. **매핑 먼저 확인**: 개발 시작 전 필드 타입 (object vs nested) 반드시 확인
|
||||||
|
2. **DevTools 활용**: 복잡한 쿼리는 OpenSearch DevTools에서 먼저 검증
|
||||||
|
3. **샘플 테스트**: 5명 → 30명 → 100명 단계적 검증 후 전체 실행
|
||||||
|
4. **필드명 정확성**: `.keyword` 사용 여부, 실제 필드 경로 정확히 매칭
|
||||||
|
|
||||||
|
**성능 및 안정성**:
|
||||||
|
1. **배치 처리**: msearch를 활용한 효율적 대량 쿼리 수행
|
||||||
|
2. **에러 처리**: 지수 백오프 재시도, 실패 배치 자동 재처리
|
||||||
|
3. **메모리 관리**: Generator 패턴, 스트리밍 처리로 메모리 사용량 최소화
|
||||||
|
4. **병렬 처리**: ThreadPoolExecutor로 성능 향상, 단 OpenSearch 부하 고려
|
||||||
|
|
||||||
|
**데이터 검증**:
|
||||||
|
1. **의심스러운 0값**: 실제 데이터 부재인지 쿼리 오류인지 반드시 검증
|
||||||
|
2. **문자열 필드**: 기본값, 정렬 방식 주의 (숫자 0 vs 빈 문자열 "")
|
||||||
|
3. **시간대 처리**: KST/UTC 변환, 사용자별 개별 시간 범위 적용
|
||||||
|
4. **FALSE POSITIVE 체크**: 0값이 의심될 때 OpenSearch에서 직접 데이터 확인
|
||||||
|
|
||||||
|
### 4.6 출력 파일 및 로깅
|
||||||
|
- **결과 파일명**: `ds-new_user_analy-YYYYMMDD_HHMMSS.csv`
|
||||||
|
- **로그 파일명**: `ds-new_user_analy-YYYYMMDD_HHMMSS.log`
|
||||||
|
- **인코딩**: UTF-8 with BOM (Excel 호환)
|
||||||
|
- **형식**: CSV (기본 약 80개 지표, --full 옵션 시 세션 지표 추가)
|
||||||
|
- **저장 위치**: 스크립트 실행 디렉토리 내 `analysis_results/` 폴더
|
||||||
|
- **시간 형식**: 모든 시간 필드는 KST 기준 `YYYY-MM-DDTHH:mm:ss+09:00` 형식
|
||||||
|
- **로깅**: hack-detector 수준의 상세 로깅 (파일 + 콘솔 동시 출력)
|
||||||
|
|
||||||
|
## 5. 필요 라이브러리
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import yaml
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
import threading
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from collections import defaultdict, Counter
|
||||||
|
from typing import Dict, List, Optional, Tuple, Generator, Any, Set
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed, Future
|
||||||
|
from pathlib import Path
|
||||||
|
import pandas as pd
|
||||||
|
from tqdm import tqdm
|
||||||
|
from opensearchpy import OpenSearch
|
||||||
|
from opensearchpy.helpers import scan
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 참고 자료
|
||||||
|
- 이전 분석 스크립트: `E:\DS_Git\DS_data_center\DS Log 분석\archive\DS 파이널테스트 신규유저 분석.py`
|
||||||
|
- OpenSearch 맵핑: `E:\DS_Git\DS_data_center\DS INFO\ds_opensearch_mappings.json`
|
||||||
|
- OpenSearch 스펙: `E:\DS_Git\DS_data_center\DS INFO\오픈서치 라이브 스펙.txt`
|
||||||
|
- 코드 품질 참고: `E:\DS_Git\hack-detector\` 디렉터리의 각종 분석 스크립트
|
||||||
556
retention_analysis.md
Normal file
556
retention_analysis.md
Normal file
@ -0,0 +1,556 @@
|
|||||||
|
# Retention Analysis Script Documentation
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
`retention_analysis.py`는 던전 스토커즈 게임의 신규 유저 리텐션 분석을 위한 고도화된 Python 분석 도구입니다. 이 스크립트는 통계적 방법론을 기반으로 신규 유저의 행동 패턴을 심층 분석하여 리텐션에 영향을 미치는 핵심 지표를 식별하고, 특히 D0(첫날) 이탈 문제에 대한 실행 가능한 인사이트를 제공합니다.
|
||||||
|
|
||||||
|
## 프로젝트 배경 및 필요성
|
||||||
|
|
||||||
|
현재 던전 스토커즈는 **D0 이탈률이 65.9%**로 매우 높아 서비스 지속성에 심각한 위협이 되고 있습니다. 모바일 게임 업계 평균 D0 이탈률이 25-35%인 점을 고려하면, 이는 즉각적인 개선이 필요한 critical 수준입니다.
|
||||||
|
|
||||||
|
이 스크립트는 데이터 기반의 과학적 접근을 통해:
|
||||||
|
|
||||||
|
- **리텐션 영향 요인 정량화**: 어떤 지표가 유저 리텐션에 긍정적/부정적 영향을 미치는지 통계적으로 검증
|
||||||
|
- **행동 패턴 차이 분석**: D0 이탈 유저와 잔존 유저의 게임 내 행동 패턴의 핵심 차이점 파악
|
||||||
|
- **개선 우선순위 제시**: 게임 디자인 개선을 위한 데이터 기반의 구체적이고 실행 가능한 인사이트 제공
|
||||||
|
- **예측 모델 구축**: 머신러닝을 활용한 이탈 위험 예측 및 조기 경보 시스템
|
||||||
|
|
||||||
|
## 설치 및 환경 설정
|
||||||
|
|
||||||
|
### 필수 패키지 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 기본 데이터 분석 패키지
|
||||||
|
pip install pandas numpy scipy matplotlib seaborn
|
||||||
|
|
||||||
|
# 머신러닝 패키지
|
||||||
|
pip install scikit-learn
|
||||||
|
|
||||||
|
# 진행률 표시 및 유틸리티
|
||||||
|
pip install tqdm
|
||||||
|
|
||||||
|
# 선택적: 고급 분석을 위한 추가 패키지
|
||||||
|
pip install plotly jupyter ipywidgets # 인터랙티브 시각화
|
||||||
|
pip install statsmodels # 고급 통계 분석
|
||||||
|
```
|
||||||
|
|
||||||
|
### 프로젝트 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
ds_new_user_analy/
|
||||||
|
├── retention_analysis.py # 메인 분석 스크립트 (562 lines)
|
||||||
|
├── retention_analysis_config.json # 설정 파일 (분석 파라미터 관리)
|
||||||
|
├── retention_analysis.md # 이 문서 (사용법 및 이론)
|
||||||
|
└── analysis_results/ # 분석 결과 저장 디렉토리
|
||||||
|
├── correlation_analysis_YYYYMMDD_HHMMSS.csv # 상관관계 분석 결과
|
||||||
|
├── d0_churn_analysis_YYYYMMDD_HHMMSS.csv # D0 이탈 분석 결과
|
||||||
|
├── feature_importance_YYYYMMDD_HHMMSS.csv # 특성 중요도 분석 결과
|
||||||
|
├── insights_report_YYYYMMDD_HHMMSS.html # HTML 인사이트 리포트
|
||||||
|
└── retention_analysis_YYYYMMDD_HHMMSS.log # 상세 실행 로그
|
||||||
|
```
|
||||||
|
|
||||||
|
## 사용법
|
||||||
|
|
||||||
|
### 기본 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 명령행 인자로 CSV 파일 지정
|
||||||
|
python retention_analysis.py path/to/your/analysis_data.csv
|
||||||
|
|
||||||
|
# 대화형 모드 (파일 경로 입력 프롬프트)
|
||||||
|
python retention_analysis.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 예시
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 절대 경로 사용 (권장)
|
||||||
|
python retention_analysis.py "E:\DS_Git\ds_new_user_analy\analysis_results\ds-new_user_analy-20250830_174158.csv"
|
||||||
|
|
||||||
|
# 상대 경로 사용
|
||||||
|
python retention_analysis.py analysis_results/ds-new_user_analy-20250830_174158.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 입력 데이터 요구사항
|
||||||
|
|
||||||
|
스크립트가 정상 작동하기 위해 CSV 파일에 다음 컬럼들이 필수로 포함되어야 합니다:
|
||||||
|
|
||||||
|
**필수 컬럼:**
|
||||||
|
- `uid`: 사용자 고유 식별자
|
||||||
|
- `retention_status`: 리텐션 상태 (Retained_d0, Retained_d1, ..., Retained_d7+)
|
||||||
|
- `create_time`: 사용자 생성 시간 (ISO 8601 형식: `2025-08-13T20:14:43+09:00`)
|
||||||
|
|
||||||
|
**권장 컬럼 (분석 품질 향상):**
|
||||||
|
- `tutorial_complete`: 튜토리얼 완료 여부
|
||||||
|
- `level_up_count`: 레벨업 횟수
|
||||||
|
- `play_time_total`: 총 플레이 시간
|
||||||
|
- `COOP_entry_count`, `Solo_entry_count`: 게임 모드별 진입 횟수
|
||||||
|
- `death_Monster`, `death_Trap`, `death_PK`: 사망 원인별 통계
|
||||||
|
- `item_gain_Equipment`, `gold_gain_total`: 아이템/골드 획득량
|
||||||
|
|
||||||
|
**데이터 형식 표준:**
|
||||||
|
- `create_time`은 ISO 8601 표준 형식을 사용하며 빈 값이 없음을 보장
|
||||||
|
- 한국 시간대(+09:00) 정보를 포함하여 정확한 시간대 분석 지원
|
||||||
|
- 모든 날짜/시간 계산은 한국 표준시(KST) 기준으로 처리
|
||||||
|
|
||||||
|
## 스크립트 작동 원리 및 아키텍처
|
||||||
|
|
||||||
|
### 전체 워크플로우
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[CSV 파일 로드] --> B[데이터 유효성 검사]
|
||||||
|
B --> C[분석 기간 정보 추출 ISO8601]
|
||||||
|
C --> D[기본 전처리 및 인코딩]
|
||||||
|
D --> E[상관관계 분석 Spearman]
|
||||||
|
E --> F[D0 이탈자 특성 분석 t-test]
|
||||||
|
F --> G[Feature Importance Random Forest]
|
||||||
|
G --> H[HTML 인사이트 리포트 생성]
|
||||||
|
H --> I[결과 파일 저장 CSV/HTML/LOG]
|
||||||
|
|
||||||
|
C --> J[시간대별 가입 패턴 분석]
|
||||||
|
C --> K[요일별 가입 패턴 분석]
|
||||||
|
J --> H
|
||||||
|
K --> H
|
||||||
|
```
|
||||||
|
|
||||||
|
### 핵심 클래스: RetentionAnalyzer
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RetentionAnalyzer:
|
||||||
|
"""
|
||||||
|
리텐션 분석의 메인 엔진
|
||||||
|
- 통계적 방법론 적용
|
||||||
|
- 결과 저장 및 시각화
|
||||||
|
- 확장 가능한 분석 프레임워크
|
||||||
|
"
|
||||||
|
|
||||||
|
# 핵심 메서드들
|
||||||
|
def __init__(csv_path, config_path='retention_analysis_config.json') # 초기화
|
||||||
|
def validate_csv() # 입력 데이터 검증
|
||||||
|
def load_data() # 데이터 로드 및 기본 통계
|
||||||
|
def extract_analysis_period() # 분석 기간 및 가입 패턴 추출 (ISO 8601)
|
||||||
|
def analyze_correlations() # Spearman 상관관계 분석
|
||||||
|
def analyze_d0_churners() # D0 이탈자 vs 잔존자 비교 (t-test)
|
||||||
|
def feature_importance_analysis() # Random Forest 중요도 분석
|
||||||
|
def generate_insights_report() # HTML 리포트 생성 (콘솔로그 제외)
|
||||||
|
def run_full_analysis() # 전체 분석 파이프라인 실행
|
||||||
|
```
|
||||||
|
|
||||||
|
### 데이터 구조 및 전처리
|
||||||
|
|
||||||
|
**1. Retention Status 순서형 인코딩:**
|
||||||
|
```python
|
||||||
|
# 문자열 리텐션 상태를 순서형 수치로 변환
|
||||||
|
retention_groups = ['Retained_d0', 'Retained_d1', ..., 'Retained_d7+']
|
||||||
|
df['retention_encoded'] = df['retention_status'].map(
|
||||||
|
{status: i for i, status in enumerate(retention_groups)}
|
||||||
|
)
|
||||||
|
# 결과: Retained_d0=0, Retained_d1=1, ..., Retained_d7+=7
|
||||||
|
```
|
||||||
|
|
||||||
|
이 인코딩을 통해 리텐션 단계 간의 순서 관계를 보존하면서 수치 분석이 가능해집니다.
|
||||||
|
|
||||||
|
**2. 분석 기간 정보 추출 (create_time 기반):**
|
||||||
|
```python
|
||||||
|
# ISO 8601 형식 시간 데이터 처리
|
||||||
|
create_times = pd.to_datetime(df['create_time'], format='ISO8601')
|
||||||
|
|
||||||
|
# 한국 시간대 기준 분석
|
||||||
|
korea_dates = create_times.dt.tz_convert('Asia/Seoul').dt.date
|
||||||
|
korea_hours = create_times.dt.tz_convert('Asia/Seoul').dt.hour
|
||||||
|
korea_weekdays = create_times.dt.tz_convert('Asia/Seoul').dt.dayofweek
|
||||||
|
|
||||||
|
# 가입 패턴 분석
|
||||||
|
daily_signups = korea_dates.value_counts().sort_index() # 일별 가입자
|
||||||
|
hourly_signups = korea_hours.value_counts().sort_index() # 시간대별 가입자
|
||||||
|
weekday_signups = korea_weekdays.value_counts().sort_index() # 요일별 가입자
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 결측값 처리 전략:**
|
||||||
|
- **상관관계 분석**: 각 지표별로 pairwise deletion (해당 지표만 결측값 제거)
|
||||||
|
- **Feature Importance**: Zero imputation (게임 내 활동 없음으로 해석)
|
||||||
|
- **그룹 비교**: Complete case analysis (양쪽 그룹 모두 유효한 값만 사용)
|
||||||
|
- **시간 데이터**: ISO 8601 표준으로 완전한 데이터 보장
|
||||||
|
|
||||||
|
## 통계적 분석 방법론
|
||||||
|
|
||||||
|
### 1. 상관관계 분석 (Spearman Rank Correlation)
|
||||||
|
|
||||||
|
**목적**: 각 게임 내 지표와 리텐션 단계 간의 단조 관계를 정량화
|
||||||
|
|
||||||
|
**왜 Spearman을 선택했는가?**
|
||||||
|
- `retention_status`가 순서형 변수 (d0 < d1 < d2 < ... < d7+)
|
||||||
|
- 비모수적 방법으로 분포 가정이 불필요
|
||||||
|
- 이상치에 덜 민감
|
||||||
|
- 비선형 단조 관계도 감지 가능
|
||||||
|
|
||||||
|
**수학적 배경:**
|
||||||
|
Spearman 상관계수 ρ는 다음과 같이 계산됩니다:
|
||||||
|
|
||||||
|
```
|
||||||
|
ρ = 1 - (6 × Σd²) / (n × (n² - 1))
|
||||||
|
```
|
||||||
|
|
||||||
|
여기서:
|
||||||
|
- d: 각 관측값의 두 변수 순위 차이
|
||||||
|
- n: 샘플 크기
|
||||||
|
|
||||||
|
**코드 구현:**
|
||||||
|
```python
|
||||||
|
from scipy.stats import spearmanr
|
||||||
|
|
||||||
|
for col in numeric_columns:
|
||||||
|
# 결측값 제거
|
||||||
|
valid_data = df[[col, 'retention_encoded']].dropna()
|
||||||
|
|
||||||
|
# 최소 샘플 크기 확인 (기본값: 10)
|
||||||
|
if len(valid_data) >= min_sample_size:
|
||||||
|
corr, p_value = spearmanr(
|
||||||
|
valid_data[col],
|
||||||
|
valid_data['retention_encoded']
|
||||||
|
)
|
||||||
|
|
||||||
|
# 통계적 유의성 검정 (α = 0.05)
|
||||||
|
is_significant = p_value < 0.05
|
||||||
|
```
|
||||||
|
|
||||||
|
**결과 해석:**
|
||||||
|
- ρ > 0: 해당 지표 값이 높을수록 더 오래 게임 플레이 (긍정적)
|
||||||
|
- ρ < 0: 해당 지표 값이 높을수록 빨리 이탈 (부정적)
|
||||||
|
- |ρ| > 0.3: 중간 정도의 관계
|
||||||
|
- |ρ| > 0.5: 강한 관계
|
||||||
|
|
||||||
|
### 2. 그룹 비교 분석 (Welch's t-test)
|
||||||
|
|
||||||
|
**목적**: D0 이탈 그룹과 잔존 그룹 간의 평균 차이를 통계적으로 검정
|
||||||
|
|
||||||
|
**Welch's t-test를 선택한 이유:**
|
||||||
|
- 등분산성 가정이 불필요 (두 그룹의 분산이 달라도 됨)
|
||||||
|
- 샘플 크기가 달라도 안정적
|
||||||
|
- 게임 데이터에서 흔히 발생하는 이분산성 문제 해결
|
||||||
|
|
||||||
|
**통계적 가설:**
|
||||||
|
- H₀(귀무가설): μ₁ = μ₂ (두 그룹의 평균이 같다)
|
||||||
|
- H₁(대립가설): μ₁ ≠ μ₂ (두 그룹의 평균이 다르다)
|
||||||
|
|
||||||
|
**코드 구현:**
|
||||||
|
```python
|
||||||
|
from scipy import stats
|
||||||
|
|
||||||
|
# D0 이탈자와 잔존자 데이터 분리
|
||||||
|
d0_users = df[df['retention_status'] == 'Retained_d0']
|
||||||
|
retained_users = df[df['retention_status'] != 'Retained_d0']
|
||||||
|
|
||||||
|
for metric in key_metrics:
|
||||||
|
d0_data = d0_users[metric].dropna()
|
||||||
|
retained_data = retained_users[metric].dropna()
|
||||||
|
|
||||||
|
# Welch's t-test 수행
|
||||||
|
t_stat, p_value = stats.ttest_ind(
|
||||||
|
d0_data,
|
||||||
|
retained_data,
|
||||||
|
equal_var=False # 등분산 가정 안 함
|
||||||
|
)
|
||||||
|
|
||||||
|
# 실용적 차이 계산 (효과 크기)
|
||||||
|
d0_mean = d0_data.mean()
|
||||||
|
retained_mean = retained_data.mean()
|
||||||
|
diff_percentage = ((retained_mean - d0_mean) / (d0_mean + 0.0001)) * 100
|
||||||
|
```
|
||||||
|
|
||||||
|
**결과 해석:**
|
||||||
|
- p-value < 0.05: 통계적으로 유의한 차이 존재
|
||||||
|
- 차이율 100% 이상: 잔존자가 이탈자보다 2배 이상 높은 지표
|
||||||
|
- Cohen's d 효과 크기: |d| > 0.8 (큰 효과), 0.5-0.8 (중간), 0.2-0.5 (작은 효과)
|
||||||
|
|
||||||
|
### 3. 특성 중요도 분석 (Random Forest)
|
||||||
|
|
||||||
|
**목적**: 다변량 환경에서 리텐션 예측에 가장 중요한 변수들을 식별
|
||||||
|
|
||||||
|
**Random Forest의 장점:**
|
||||||
|
- **비선형 관계 포착**: 복잡한 게임 내 상호작용 모델링
|
||||||
|
- **변수 간 상호작용**: A와 B의 조합 효과 자동 탐지
|
||||||
|
- **과적합 방지**: 배깅과 랜덤 서브스페이스 기법 사용
|
||||||
|
- **안정성**: 아웃라이어에 견고함
|
||||||
|
- **해석 가능성**: 각 변수의 기여도 정량화
|
||||||
|
|
||||||
|
**중요도 계산 원리:**
|
||||||
|
각 변수의 중요도는 해당 변수가 모든 트리에서 불순도 감소에 기여한 평균값입니다:
|
||||||
|
|
||||||
|
```
|
||||||
|
Importance(v) = (1/T) × Σ[t=1 to T] Σ[s∈Splits(v,t)] p(s) × ΔI(s)
|
||||||
|
```
|
||||||
|
|
||||||
|
여기서:
|
||||||
|
- T: 트리 개수
|
||||||
|
- p(s): 분할 s에 도달하는 샘플 비율
|
||||||
|
- ΔI(s): 분할 s에서의 불순도 감소량
|
||||||
|
|
||||||
|
**코드 구현:**
|
||||||
|
```python
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
|
||||||
|
# 데이터 준비
|
||||||
|
X = df[feature_columns].fillna(0) # Zero imputation
|
||||||
|
y = df['retention_encoded'] # 타겟 변수
|
||||||
|
|
||||||
|
# Random Forest 모델 설정
|
||||||
|
rf = RandomForestClassifier(
|
||||||
|
n_estimators=100, # 트리 개수 (안정성과 성능 균형)
|
||||||
|
random_state=42, # 재현 가능성
|
||||||
|
n_jobs=-1, # 병렬 처리
|
||||||
|
max_depth=None, # 트리 깊이 제한 없음 (자연스러운 중단)
|
||||||
|
min_samples_split=2, # 최소 분할 샘플 수
|
||||||
|
min_samples_leaf=1 # 최소 리프 샘플 수
|
||||||
|
)
|
||||||
|
|
||||||
|
# 모델 학습
|
||||||
|
rf.fit(X, y)
|
||||||
|
|
||||||
|
# 중요도 추출
|
||||||
|
importance_scores = rf.feature_importances_
|
||||||
|
```
|
||||||
|
|
||||||
|
**결과 해석:**
|
||||||
|
- 0.1 이상: 매우 중요한 변수 (핵심 개선 타겟)
|
||||||
|
- 0.05-0.1: 중요한 변수 (보조 개선 요소)
|
||||||
|
- 0.01-0.05: 보통 중요 (모니터링 지표)
|
||||||
|
- 0.01 미만: 낮은 중요도 (우선순위 후순위)
|
||||||
|
|
||||||
|
## 고급 분석 기법 및 확장
|
||||||
|
|
||||||
|
### 1. 통계적 검증 강화
|
||||||
|
|
||||||
|
**다중 비교 문제 해결:**
|
||||||
|
```python
|
||||||
|
from statsmodels.stats.multitest import multipletests
|
||||||
|
|
||||||
|
# Bonferroni 또는 FDR 보정 적용
|
||||||
|
p_values = [result['p_value'] for result in analysis_results]
|
||||||
|
rejected, p_corrected, _, _ = multipletests(p_values, method='fdr_bh')
|
||||||
|
```
|
||||||
|
|
||||||
|
**효과 크기 계산:**
|
||||||
|
```python
|
||||||
|
# Cohen's d 계산
|
||||||
|
def cohens_d(group1, group2):
|
||||||
|
pooled_std = np.sqrt(((len(group1) - 1) * group1.var() +
|
||||||
|
(len(group2) - 1) * group2.var()) /
|
||||||
|
(len(group1) + len(group2) - 2))
|
||||||
|
return (group1.mean() - group2.mean()) / pooled_std
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 심층 세그멘테이션 분석
|
||||||
|
|
||||||
|
**클러스터링 기반 유저 타입 분류:**
|
||||||
|
```python
|
||||||
|
from sklearn.cluster import KMeans
|
||||||
|
from sklearn.preprocessing import StandardScaler
|
||||||
|
|
||||||
|
# 특성 정규화
|
||||||
|
scaler = StandardScaler()
|
||||||
|
scaled_features = scaler.fit_transform(df[numeric_columns])
|
||||||
|
|
||||||
|
# K-means 클러스터링
|
||||||
|
kmeans = KMeans(n_clusters=5, random_state=42)
|
||||||
|
df['user_segment'] = kmeans.fit_predict(scaled_features)
|
||||||
|
|
||||||
|
# 세그먼트별 리텐션 패턴 분석
|
||||||
|
segment_retention = df.groupby(['user_segment', 'retention_status']).size()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 시계열 분석 및 트렌드
|
||||||
|
|
||||||
|
**일별/주별 리텐션 트렌드:**
|
||||||
|
```python
|
||||||
|
# 코호트 분석
|
||||||
|
df['signup_date'] = pd.to_datetime(df['signup_date'])
|
||||||
|
df['cohort'] = df['signup_date'].dt.to_period('W') # 주간 코호트
|
||||||
|
|
||||||
|
cohort_retention = df.groupby('cohort')['retention_encoded'].describe()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 설정 파일 상세 설명
|
||||||
|
|
||||||
|
### retention_analysis_config.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"analysis_settings": {
|
||||||
|
"correlation_threshold": 0.05, // 통계적 유의수준 (Type I 오류율)
|
||||||
|
"top_n_features": 20, // 결과 표시할 상위 중요 변수 개수
|
||||||
|
"min_sample_size": 10, // 분석 최소 샘플 크기 (신뢰성 확보)
|
||||||
|
"effect_size_threshold": 0.1 // 실용적 유의성 최소 효과 크기
|
||||||
|
},
|
||||||
|
|
||||||
|
"retention_groups": [ // 리텐션 단계 정의 및 순서
|
||||||
|
"Retained_d0", "Retained_d1", "Retained_d2", "Retained_d3",
|
||||||
|
"Retained_d4", "Retained_d5", "Retained_d6", "Retained_d7+"
|
||||||
|
],
|
||||||
|
|
||||||
|
"exclude_columns": [ // 분석에서 제외할 메타데이터 컬럼
|
||||||
|
"uid", "retention_status", "retention_encoded",
|
||||||
|
"country", "nickname", "auth_id", "create_time"
|
||||||
|
],
|
||||||
|
|
||||||
|
"key_metrics_for_d0_analysis": [ // D0 분석 핵심 지표 (도메인 지식 기반)
|
||||||
|
"tutorial_complete", // 온보딩 완료도
|
||||||
|
"level_up_count", // 성장 경험
|
||||||
|
"play_time_total", // 참여도
|
||||||
|
"COOP_entry_count", "Solo_entry_count", // 게임 모드 선호
|
||||||
|
"death_Monster", "death_Trap", "death_PK", // 실패 경험
|
||||||
|
"item_gain_Equipment", "gold_gain_total" // 보상 경험
|
||||||
|
],
|
||||||
|
|
||||||
|
"output_settings": {
|
||||||
|
"save_csv": true, // 원시 데이터 CSV 저장
|
||||||
|
"save_html": true, // HTML 리포트 저장 (기본 출력)
|
||||||
|
"save_console_log": true, // 디버깅용 로그 저장
|
||||||
|
"timestamp_format": "%Y%m%d_%H%M%S" // 파일명 타임스탬프 형식
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 출력 결과 해석 가이드
|
||||||
|
|
||||||
|
### 상관관계 분석 결과
|
||||||
|
|
||||||
|
**긍정적 상관관계 (양의 값) 해석:**
|
||||||
|
```
|
||||||
|
shop_sell_count: 0.428 (p < 0.001)
|
||||||
|
```
|
||||||
|
→ 아이템 판매 활동이 활발한 유저일수록 더 오래 게임을 플레이하는 경향
|
||||||
|
→ **액션 아이템**: 거래 시스템 개선, 마켓플레이스 UX 향상
|
||||||
|
|
||||||
|
**부정적 상관관계 (음의 값) 해석:**
|
||||||
|
```
|
||||||
|
dungeon_first_result: -0.173 (p < 0.01)
|
||||||
|
```
|
||||||
|
→ 첫 던전에서 실패한 유저일수록 빨리 이탈하는 경향
|
||||||
|
→ **액션 아이템**: 초심자 던전 난이도 조정, 실패 시 보상 시스템
|
||||||
|
|
||||||
|
### Feature Importance 결과
|
||||||
|
|
||||||
|
**중요도 점수별 액션:**
|
||||||
|
- **0.1 이상** (매우 중요): 즉시 개선 프로젝트 착수
|
||||||
|
- **0.05-0.1** (중요): 다음 분기 개선 계획에 포함
|
||||||
|
- **0.01-0.05** (보통): 정기 모니터링 대상
|
||||||
|
- **0.01 미만** (낮음): 우선순위 후순위
|
||||||
|
|
||||||
|
### D0 분석 결과
|
||||||
|
|
||||||
|
**차이율별 해석 및 액션:**
|
||||||
|
```
|
||||||
|
tutorial_complete:
|
||||||
|
- D0 평균: 0.23, 잔존 평균: 0.89, 차이: 287%
|
||||||
|
```
|
||||||
|
→ 튜토리얼 완료가 리텐션에 결정적 영향
|
||||||
|
→ **즉시 액션**: 튜토리얼 완료율 개선 프로젝트
|
||||||
|
|
||||||
|
### 가입 패턴 분석 결과
|
||||||
|
|
||||||
|
**시간대별 분석:**
|
||||||
|
```
|
||||||
|
가입 집중 시간: 20시 (1,250명)
|
||||||
|
```
|
||||||
|
→ 저녁 시간대에 가입이 집중됨을 의미
|
||||||
|
→ **마케팅 액션**: 해당 시간대 타겟팅 광고 집중
|
||||||
|
|
||||||
|
**요일별 분석:**
|
||||||
|
```
|
||||||
|
가입 집중 요일: 토요일 (3,200명)
|
||||||
|
```
|
||||||
|
→ 주말에 신규 가입이 활발함
|
||||||
|
→ **운영 액션**: 주말 이벤트 및 서버 안정성 강화
|
||||||
|
|
||||||
|
## 문제 해결 및 최적화
|
||||||
|
|
||||||
|
### 일반적인 오류와 해결책
|
||||||
|
|
||||||
|
**1. 메모리 부족 오류**
|
||||||
|
```python
|
||||||
|
# 청크 단위 처리
|
||||||
|
chunk_size = 50000
|
||||||
|
for chunk in pd.read_csv(filepath, chunksize=chunk_size):
|
||||||
|
process_chunk(chunk)
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 인코딩 오류**
|
||||||
|
```python
|
||||||
|
# UTF-8 인코딩 명시적 지정
|
||||||
|
df = pd.read_csv(filepath, encoding='utf-8-sig')
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 성능 최적화**
|
||||||
|
```python
|
||||||
|
# 데이터 타입 최적화
|
||||||
|
df['user_level'] = df['user_level'].astype('int16') # 메모리 절약
|
||||||
|
df['is_premium'] = df['is_premium'].astype('bool') # 불린 최적화
|
||||||
|
```
|
||||||
|
|
||||||
|
### 대용량 데이터 처리
|
||||||
|
|
||||||
|
**샘플링 전략:**
|
||||||
|
```python
|
||||||
|
# 층화 샘플링 (각 리텐션 그룹에서 동일 비율 추출)
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
|
||||||
|
sampled_data, _ = train_test_split(
|
||||||
|
df,
|
||||||
|
test_size=0.7, # 30%만 사용
|
||||||
|
stratify=df['retention_status'], # 층화
|
||||||
|
random_state=42
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 비즈니스 임팩트 및 ROI
|
||||||
|
|
||||||
|
### 개선 우선순위 매트릭스
|
||||||
|
|
||||||
|
```
|
||||||
|
높은 임팩트 낮은 임팩트
|
||||||
|
높은 구현비용 [계획 검토] [보류]
|
||||||
|
낮은 구현비용 [즉시 실행] [빠른 승리]
|
||||||
|
```
|
||||||
|
|
||||||
|
### A/B 테스트 설계
|
||||||
|
|
||||||
|
분석 결과를 바탕으로 한 개선안은 반드시 A/B 테스트를 통해 검증:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 실험 설계 예시
|
||||||
|
control_group = new_users.sample(frac=0.5, random_state=42)
|
||||||
|
treatment_group = new_users.drop(control_group.index)
|
||||||
|
|
||||||
|
# 개선 효과 측정
|
||||||
|
improvement = (treatment_retention - control_retention) / control_retention
|
||||||
|
statistical_power = calculate_power(effect_size, alpha=0.05, sample_size=len(control_group))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 결론 및 향후 개선
|
||||||
|
|
||||||
|
이 `retention_analysis.py` 스크립트는 단순한 데이터 분석을 넘어서, 게임 서비스 개선을 위한 **의사결정 지원 시스템**의 역할을 합니다.
|
||||||
|
|
||||||
|
**핵심 가치:**
|
||||||
|
1. **과학적 접근**: 통계적으로 검증된 방법론으로 신뢰할 수 있는 인사이트 제공
|
||||||
|
2. **실행 가능성**: 분석 결과가 구체적인 개선 액션으로 연결
|
||||||
|
3. **확장성**: 새로운 분석 기법과 지표를 쉽게 추가 가능
|
||||||
|
4. **자동화**: 정기적인 분석과 모니터링을 통한 지속적 개선
|
||||||
|
5. **시간대 인사이트**: 가입 패턴 분석으로 마케팅/운영 최적화 지원
|
||||||
|
|
||||||
|
**2024년 8월 업데이트 내용:**
|
||||||
|
- **HTML 리포트**: 마크다운에서 전문적인 HTML 형식으로 변경
|
||||||
|
- **분석 기간 정보**: ISO 8601 형식 `create_time` 기반 자동 추출
|
||||||
|
- **시간대 분석**: 한국 시간 기준 시간대별/요일별 가입 패턴 분석
|
||||||
|
- **콘솔로그 분리**: HTML 리포트에서 콘솔로그 제거, 상세 분석 데이터 중심으로 개편
|
||||||
|
- **데이터 품질 강화**: 표준 시간 형식 지원으로 분석 정확도 향상
|
||||||
|
|
||||||
|
**향후 발전 방향:**
|
||||||
|
- 실시간 대시보드 연동 (가입 패턴 실시간 모니터링)
|
||||||
|
- 예측 모델 고도화 (딥러닝 활용)
|
||||||
|
- 개인화된 리텐션 전략 추천 (시간대별 타겟팅)
|
||||||
|
- 자동화된 A/B 테스트 파이프라인
|
||||||
|
- 가입 패턴 기반 마케팅 최적화 시스템
|
||||||
|
|
||||||
|
정기적인 분석을 통해 게임 개선 효과를 측정하고, 새로운 문제점을 조기에 발견하여 서비스의 지속 가능한 성장에 기여할 수 있습니다.
|
||||||
1212
retention_analysis.py
Normal file
1212
retention_analysis.py
Normal file
File diff suppressed because it is too large
Load Diff
43
retention_analysis_config.json
Normal file
43
retention_analysis_config.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"analysis_settings": {
|
||||||
|
"correlation_threshold": 0.05,
|
||||||
|
"top_n_features": 20,
|
||||||
|
"min_sample_size": 10
|
||||||
|
},
|
||||||
|
"retention_groups": [
|
||||||
|
"Retained_d0",
|
||||||
|
"Retained_d1",
|
||||||
|
"Retained_d2",
|
||||||
|
"Retained_d3",
|
||||||
|
"Retained_d4",
|
||||||
|
"Retained_d5",
|
||||||
|
"Retained_d6",
|
||||||
|
"Retained_d7+"
|
||||||
|
],
|
||||||
|
"exclude_columns": [
|
||||||
|
"uid",
|
||||||
|
"retention_status",
|
||||||
|
"retention_encoded",
|
||||||
|
"country",
|
||||||
|
"nickname",
|
||||||
|
"auth_id"
|
||||||
|
],
|
||||||
|
"key_metrics_for_d0_analysis": [
|
||||||
|
"tutorial_complete",
|
||||||
|
"level_up_count",
|
||||||
|
"play_time_total",
|
||||||
|
"COOP_entry_count",
|
||||||
|
"Solo_entry_count",
|
||||||
|
"death_Monster",
|
||||||
|
"death_Trap",
|
||||||
|
"death_PK",
|
||||||
|
"item_gain_Equipment",
|
||||||
|
"gold_gain_total"
|
||||||
|
],
|
||||||
|
"output_settings": {
|
||||||
|
"save_csv": true,
|
||||||
|
"save_markdown": true,
|
||||||
|
"save_console_log": true,
|
||||||
|
"timestamp_format": "%Y%m%d_%H%M%S"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user