Files
ds_new_user_analy/README.md

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\` 디렉터리의 각종 분석 스크립트