diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa9f5d9 --- /dev/null +++ b/README.md @@ -0,0 +1,514 @@ +# 던전 스토커즈 신규 유저 리텐션 비교 분석 기획서 + +## 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_d1): 첫 접속 후 24시간 ~ 48시간 사이에 접속 기록이 있는 유저 +- 이탈 유저 (Retained_d0): 첫 접속 후 24시간 ~ 48시간 사이에 접속 기록이 없는 유저 + +### 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` | 첫 로그인 시간, D+1 리텐션 판정, 닉네임, 사용 언어 | +| `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) +- `first_login_time`: 최초 로그인 시간 (KST) +- `retention_status`: 리텐션 상태 (Retained_d0/Retained_d1) +- `language`: 사용 언어 (body.language from login_comp 최신 기록) +- `device`: 디바이스 정보 (body.device_mod) + +### 3.2 플레이 시간 및 세션 +- `active_seconds`: D+0 실제 활동 시간 (초) - heartbeat 기반 계산 +- `total_playtime_minutes`: D+0 총 플레이 시간 (분) +- `session_count`: D+0 접속 횟수 (login_comp 카운트) +- `avg_session_length`: 평균 세션 길이 (분) +- `logout_abnormal`: 비정상 종료 여부 (0/1) - last_logout < last_login 체크 + +### 3.3 던전 플레이 성과 +- `dungeon_entry_count`: 던전 진입 횟수 (survival_sta 카운트) +- `dungeon_first_mode`: 처음 플레이한 던전 모드 (body.dungeon_mode) +- `dungeon_first_stalker`: 처음 선택한 스토커 (body.stalker_name) +- `dungeon_first_result`: 첫 던전 결과 (0: 사망, 1: 탈출, 2: 미플레이) +- `dungeon_escape_count`: 던전 탈출 성공 횟수 (body.result = 1) +- `dungeon_escape_rate`: 던전 탈출률 (%) +- `avg_survival_time`: 평균 생존 시간 (body.play_stats.playtime) +- `max_survival_time`: 최대 생존 시간 +- `total_armor_break`: 총 갑옷 파괴 횟수 (body.play_stats.armor_break_cnt) +- `raid_play_count`: 레이드 플레이 횟수 (body.play_stats.raid_play) +- `escape_count`: 던전 포기(탈주) 횟수 (dead with body.inter_type = 0) + +### 3.4 전투 성과 +- `monster_kill_count`: 몬스터 처치 수 (body.play_stats.monster_kill_cnt 합계) +- `player_kill_count`: 플레이어 킬 수 (body.instigator_uid = uid) +- `player_killed_count`: 플레이어에게 사망 수 (body.target_uid = uid) +- `death_count`: PK 이외 사망 횟수 (dead 인덱스 카운트, inter_type != 0) +- `avg_damage_per_game_monster`: 게임당 평균 몬스터 피해량 (총 피해량 ÷ 던전 입장 횟수) +- `avg_damage_per_game_player`: 게임당 평균 플레이어 피해량 (총 피해량 ÷ 던전 입장 횟수) + +### 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 아이템 및 경제 +- `items_obtained_count`: 획득한 아이템 수 (item_get 카운트) +- `highest_item_grade`: 획득한 최고 등급 아이템 (body.item_grade) +- `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`: 병렬 처리 스레드 수 (기본값: 6) + - `--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-fixed_YYYYMMDD_HHMMSS.csv` +- **로그 파일명**: `ds-new-user-fixed-analysis_YYYYMMDD_HHMMSS.log` +- **인코딩**: UTF-8 with BOM (Excel 호환) +- **형식**: CSV (66개 분석 지표) +- **저장 위치**: `E:\DS_Git\DS_data_center\DS Log 분석\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\` 디렉터리의 각종 분석 스크립트 \ No newline at end of file