<aside> 📌 Task


큐에르

# exit_position_numeric 75 이상이면 완독한 것으로 간주
df['is_completed'] = df['exit_position_numeric'] >= 75

# 유입 경로별 완독률
completion_by_channel = df.groupby('entry_channel')['is_completed'].mean().sort_values(ascending=False)

# 추천 클릭 + 자발적 이탈 + 세부 이탈 사유 필터링
filtered = df[
    (df['recommendation_clicked'] == 1) &
    (df['dropout_reason_category'] == '자발적') 
    # & (df['dropout_reason_detail'].isin(['지루함', '너무 김']))
]

reason_ratio = filtered['dropout_reason_detail'].value_counts(normalize=True)

# 이탈 기준: exit_position_numeric < 75 (완독 실패)
df['is_dropout'] = df['exit_position_numeric'] < 75

# 추천 or 홈메인배너 유입자만 필터
subset = df[df['entry_channel'].isin(['추천', '홈메인배너'])]

# 미리보기 사용 여부별 이탈률 비교
dropout_by_preview = subset.groupby('quick_preview_used')['is_dropout'].mean()

석미느

# theme_mode 입력값 중 오타 정제
merged_df["theme_mode"] = merged_df["theme_mode"].replace("customized", "custom")
merged_df["theme_mode"] = merged_df["theme_mode"].replace("dark_mode", "dark")
merged_df["theme_mode"] = merged_df["theme_mode"].replace("Light", "light")

# birthday 컬럼으로 age / age_group 파생 변수 생성
merged_df["birth_year"] = pd.to_numeric(merged_df["birthday"].str[:4], errors="coerce")
current_year = pd.Timestamp.now().year
merged_df['age'] = current_year - merged_df['birth_year']

bins = [0, 19, 29, 39, 49, 200]
labels = ['10대 이하', '20대', '30대', '40대', '50대 이상']
merged_df['age_group'] = pd.cut(merged_df['age'], bins=bins, labels=labels, right=True)

재웅티비

여기에 입력하세욝!


정무느

birthday 990 non-null object

last_access_timestamp 1000 non-null object

⇒ datetime으로 변환 필요

기준일과 6개월 이전 날짜 정의 cutoff_date = pd.Timestamp("2024-01-01") - pd.DateOffset(months=6)


이녕으


</aside>

<aside> 📌 실행 및 진행 사항 정리


큐에르

# exit_position_numeric 95 이상이면 완독한 것으로 간주
df['is_completed'] = df['exit_position_numeric'] >= 95

# 유입 경로별 완독률
completion_by_channel = df.groupby('entry_channel')['is_completed'].mean().sort_values(ascending=False)

# 추천 클릭 + 자발적 이탈 + 세부 이탈 사유 필터링
filtered = df[
    (df['recommendation_clicked'] == 1) &
    (df['dropout_reason_category'] == '자발적') 
    # & (df['dropout_reason_detail'].isin(['지루함', '너무 김']))
]

reason_ratio = filtered['dropout_reason_detail'].value_counts(normalize=True)

# 이탈 기준: exit_position_numeric < 80 (완독 실패)
df['is_dropout'] = df['exit_position_numeric'] < 95

# 추천 or 홈메인배너 유입자만 필터
subset = df[df['entry_channel'].isin(['추천', '홈메인배너'])]

# 미리보기 사용 여부별 이탈률 비교
dropout_by_preview = subset.groupby('quick_preview_used')['is_dropout'].mean()

석미느

# theme_mode 입력값 중 오타 정제
merged_df["theme_mode"] = merged_df["theme_mode"].replace("customized", "custom")
merged_df["theme_mode"] = merged_df["theme_mode"].replace("dark_mode", "dark")
merged_df["theme_mode"] = merged_df["theme_mode"].replace("Light", "light")

# birthday 컬럼으로 age / age_group 파생 변수 생성
merged_df["birth_year"] = pd.to_numeric(merged_df["birthday"].str[:4], errors="coerce")
current_year = pd.Timestamp.now().year
merged_df['age'] = current_year - merged_df['birth_year']

bins = [0, 19, 29, 39, 49, 200]
labels = ['10대 이하', '20대', '30대', '40대', '50대 이상']
merged_df['age_group'] = pd.cut(merged_df['age'], bins=bins, labels=labels, right=True)

재웅티비



정무느

*데이터 정제 및 변환 
# 1. last_access_timestamp datetime 변환
df['last_access_timestamp'] = pd.to_datetime(df['last_access_timestamp'])

# 2. birthday datetime 변환
df['birthday'] = pd.to_datetime(df['birthday'], errors='coerce')

#요일별평균 exit_position_numeric

#1 이상치 날짜 빼기 
df = df.dropna(subset=["last_access_timestamp"])

# 2 weekday 컬럼 생성
df["weekday"] = df["last_access_timestamp"].dt.dayofweek

# 3 요일별 평균 exit_position_numeric 계산
weekday_avg_exit = (
    filtered_df.groupby("weekday")["exit_position_numeric"]
    .mean()
    .reset_index())

# 4 요일 숫자를 이름으로 매핑
weekday_avg_exit["weekday_name"] = weekday_avg_exit["weekday"].map({
    0: "월요일", 1: "화요일", 2: "수요일", 3: "목요일",
    4: "금요일", 5: "토요일", 6: "일요일"})

# 5 평균 이탈 위치 기준 내림차순 정렬
weekday_avg_exit_sorted = weekday_avg_exit.sort_values(by="exit_position_numeric", ascending=False)

# 6 출력
print(weekday_avg_exit_sorted)

=> 주말완독율이 더 높지 않음, 틀린 가설

# 6개월 이상 미접속자의 이탈 사유 분석

# 1. 기준일과 6개월 이전 날짜 정의
cutoff_date = pd.Timestamp("2024-01-01") - pd.DateOffset(months=6)

# 2. 6개월 이상 접속하지 않은 유저 필터링
inactive_users = df[df["last_access_timestamp"] < cutoff_date]

# 3. dropout_reason_category 비율 계산
dropout_reason_pct = (
    inactive_users["dropout_reason_category"]
    .value_counts(normalize=True) * 100
).reset_index()

# 4. 컬럼명 정리
dropout_reason_pct.columns = ["dropout_reason_category", "percentage"]

# 5. 출력
print(dropout_reason_pct)

#자발적 이탈유저 상세사유 

# 3. 자발적 이탈한 유저 중 상세 사유 존재하는 데이터만 필터링
voluntary_dropouts = inactive_users[
    (inactive_users["dropout_reason_category"] == "자발적") &
    (inactive_users["dropout_reason_detail"].notna())]

# 4. 상세 사유 비율 계산
detail_reason_pct = (
    voluntary_dropouts["dropout_reason_detail"]
    .value_counts(normalize=True) * 100).reset_index()

# 5. 컬럼명 정리
detail_reason_pct.columns = ["dropout_reason_detail", "percentage"]

# 6. 출력
print(detail_reason_pct)


이녕으


</aside>

<aside> 📌 결과


큐에르

user_id gender birthday device_type subscription_plan theme_mode \ 0 user_0001 male 1998-06-17 mobile monthly dark

1 user_0002 male 2010-04-15 mobile monthly dark

2 user_0003 male 1985-02-13 mobile monthly dark

3 user_0004 female 1974-11-21 eReader monthly light

4 user_0005 male 1970-11-01 mobile free_trial dark

.. ... ... ... ... ... ...

995 user_0996 male 2008-08-11 tablet monthly light

996 user_0997 male 1981-01-23 tablet monthly light

997 user_0998 male 2009-07-08 tablet monthly light

998 user_0999 male 2015-07-05 tablet monthly light

999 user_1000 male 1998-10-06 tablet monthly light

entry_channel quick_preview_used  recommendation_clicked  \\\\

0 추천 No True

1 추천 No True

2 추천 No True

3 추천 Yes True

4 검색 Yes False

.. ... ... ...

995 추천 Yes True

996 추천 Yes True

997 추천 Yes True

998 추천 Yes True

999 추천 Yes True

last_access_timestamp book_id  genre  exit_position_numeric  \\\\

0 2023-05-08 14:13:00 E01 경제/시사 68

1 2023-01-13 00:54:00 E01 경제/시사 44

2 2023-07-12 09:13:03 E01 경제/시사 59

3 2023-11-20 21:24:55 N05 소설 97

4 2023-11-01 05:55:04 S02 자기계발 51

.. ... ... ... ...

995 2023-09-03 17:44:46 S01 자기계발 39

996 2023-04-06 07:03:38 S01 자기계발 71

997 2023-12-26 13:44:38 S01 자기계발 10

998 2023-06-26 22:39:14 S01 자기계발 15

999 2023-01-01 20:58:12 S01 자기계발 71

dropout_reason_category dropout_reason_detail  is_completed  is_dropout

0 자발적 지루함 False True

1 자발적 추천 실패 False True

2 자발적 너무 김 False True

3 UX 불편 NaN True False

4 UX 불편 NaN False True

.. ... ... ... ...

995 자발적 추천 실패 False True

996 자발적 너무 김 False True

997 UX 불편 NaN False True

998 자발적 금한일 False True

999 자발적 지루함 False True

</aside>