528 lines
24 KiB
Markdown
528 lines
24 KiB
Markdown
# 던전 스토커즈 신규 유저 리텐션 비교 분석 기획서
|
|
|
|
## 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\` 디렉터리의 각종 분석 스크립트 |