▼오늘 배운 사항들
1. 이벤트 데이터
- 데이터레이어
- ga4 데이터 pandas 연동
2. 전처리 실습 : B2B 리드 퍼널 데이터 분석
- 전처리 : 유입채널 정제
- 영업 사이클 시각화
- 유입 채널별 리드 전환율 분석
지난주 크롤링 배우면서, pandas 까먹었을 것 같다고 다음 강의인 머신러닝 전에 전처리 내용 다시 진행!
Part 1. 이벤트 데이터
[정의]
- 본질 : '누가' '무엇을' 했다 기록하는 것
- 일반 데이터 vs 이벤트 데이터 비교시, 이벤트 데이터 자체가 더 중요
[그로스마케터에게 이벤트 데이터가 중요한 이유]
- 핵심 업무 : 전환율 향상, 이탈률 감소, 재구매 증가
- 이 모든 작업의 출발점이 "사용자가 실제로 어떻게 행동하는가" 파악하는 것이고
- 그 근거가 이벤트 데이터이기 때문
| 업무 | 질문 | 필요한 event data |
| 퍼널 분석 | 누가 어느 단계에서 전환이 안됐는지 | view → cart → purchase 이벤트 흐름 |
| 리타겟팅 광고 | 구매 의사가 있었으나 최종 결제 단계에서 망설이는 타겟은 누구인가? | cart 이벤트 발생 후 purchase 없는 사용자 |
| 개인화 추천 | 사용자가 현재 가장 관심을 두고 있는 카테고리나 취향은 무엇인가? | 특정 카테고리 view 이벤트가 많은 사용자 |
| 이탈 분석 | 어떤 시점에 사용자의 흥미가 떨어졌으며, 이탈 징후를 보이는 구간은 어디인가? | 마지막 이벤트 이후 접속 없는 사용자 |
| A/B 테스트 평가 | 실험군과 대조군 중 어떤 UI/UX가 유저의 유의미한 행동 변화를 이끌어냈는가? | 그룹별 이벤트 발생 횟수 및 전환율 비교 |
[datalayer 실제구조]
- push : data를 올리겠다
- 보통 gtm 태그만 설치하기도 하는데 중요한내용이나 누락방지를 위해서는 서버내용에 datalayer 설치하는게 좋다
Part 2. B2B 퍼널데이터 전처리하기
Step 1 데이터 파악
- 결측치 확인
→ 왜이렇게 많은가? = 선택으로 되어있는 항목은 잘 안할 것
→ 선택폼일 땐 대부분 쓰지 않아서 결측치가 이만큼 많이 나오는 것 - has_company 사업자등록여부
- has_gtin
- average_stock
쩐다 엄청나다 이제까지 가입자수 매월 똑같고 결측치 없던 데이터에서 이런 값보니까 진짜 데이터 같고 좋다
그래서 삭제하는 것이 아니라 비어있는게 정상이구나 하고 넘어가기
💡결측치가 많다고 다 문제는 아니다 !


Step 2 데이터 정제
- 날짜변환 : pd.to_datetime()
💡날짜변환의 목적 : 영업사이클 분석, 월별 추이 분석 등 날짜 기준으로 데이터 가공할 때 필요해서


- 수집기간 확인 : .min().date()
- 중괄호 2개 써서 날짜로 보여줘도 되고, 중괄호 하나만 넣고 안에 뺄셈 이용해서 days로 보이는 것도 가능
- 스터디 과제 1 재구매일 구하던 것처럼
# 수집 기간 : 컬럼.min().date() 적은 최소숫자 컬럼.max().date() 가장 최대숫자
#mql 총 수집기간
print(f" mql 수집기간 : {mql.first_contact_date.min().date()} - {mql.first_contact_date.max().date()}")
print(f" mql 수집기간 : {mql.first_contact_date.max().date() - mql.first_contact_date.min().date()}")
print("---------------------------------")
#closed 계약 성사 총 기간
print(f" closed 계약 성사 총 기간 : {closed.won_date.min().date()} - {closed.won_date.max().date()}")
print(f" closed 계약 성사 총 기간 : {closed.won_date.max().date() - closed.won_date.min().date()}")

- 연/월/분기별로 파생컬럼 생성하기
- 월별 리드 수 구할 때, 기간별 컬럼 만들어두면 groupby 사용해서 신속하게 활용 가능(분석할 때 자주사용하니 미리 빼놓기)
- 날짜 만드는 명령어 dt 사용
- dt.하고 나오는 리스트들 보면서 확인해보기
# 파생컬럼 생성 - 날짜
# 월별 리드 수 구할 때, 파생컬럼 만들어두면 groupby 사용해서 신속하게 활용 가능(분석할 때 자주사용하니 미리 빼놓기)
# 날짜 만드는 명령어 dt
mql['contact_year'] = mql['first_contact_date'].dt.year
mql['contact_month'] = mql['first_contact_date'].dt.month
mql['contact_quarter'] = mql['first_contact_date'].dt.quarter
closed['won_year'] = closed['won_date'].dt.year
closed['won_month'] = closed['won_date'].dt.month
closed['won_quarter'] = closed['won_date'].dt.quarter

.dt. 리스트
mql['first_contact_date'].dt.day_of_year #몇 번째 일자인지
mql['first_contact_date'].dt.unit
#Pandas에서 dt.unit은 해당 날짜 데이터(Series)가 어느 정도의 시간 단위(정밀도)로 저장되어 있는지를 나타내는 속성입니다.
#출력된 **'ns'**는 **Nanoseconds(나노초, $10^{-9}$초)**를 의미
#dt.unit이 데이터의 '정밀도'를 보여주는 눈금자였다면, asfreq는 데이터의 '주기(Frequency)'를 강제로 고정하는 도구
#.dt 뒤에 붙여 쓰기보다는, 날짜가 **인덱스(Index)**로 잡혀 있을 때 df.asfreq()
mql['first_contact_date'].dt.asfreq
# <예시>
# 1. 날짜를 인덱스로 설정 (asfreq를 쓰기 위한 필수 단계)
mql_daily = mql.set_index('first_contact_date')
# 2. 일 단위('D')로 주기를 맞춤
# 데이터가 없는 날은 NaN으로 생성됨
mql_asfreq = mql_daily.asfreq('D')
# 3. 비어있는(NaN) 날의 리드 수를 0으로 채우기
mql_filled = mql_daily.asfreq('D', fill_value=0)
# 4. 혹은 직전 데이터를 그대로 가져오기 (Forward Fill)
mql_ffill = mql_daily.asfreq('D', method='ffill')
참고) 주차 설정(외국은 주차가 중요함)
.dt.isocalendar().week.astype(int)
# .dt.isocalendar() calendar날짜로 변경할건데
# .week 주차만 빼오고
# .astype(int) 숫자로 표기해줘


[유입채널 정제하기]
유입채널(origin)을 정제해서 표준화하는 작업 진행
Step 1 데이터 파악
- 유입채널데이터 확인 및 파악 작업
# 1. 유입채널 어떻게 생긴지 확인
mql['origin']
# 2. 유입채널 결측치 확인
print(mql['origin'].isnull().sum())
# 3. 유입채널 값별 분포도 확인
mql['origin'].value_counts()


Step 2. 데이터 정제
- 고민할 사항
null값이랑 unknown, other 다 다른거잖아...null값은 unknow, other 조차 없는 건데..
| 정제 처리항목 | 내용 | 함수 | |
| 1 | 결측치 처리 | 결측치 변경 필요 -> 채워주기 | fillna |
| 2 | unknown 처리 | 표준화할수있도록 unknown 값을 not_identified로 변경하기 | replace |
| 3 | other 처리 | other, other_publicities 병합 후 이름 지정하기 | map 딕셔너리 |
# 1. fillna ( 결측치 값 not_identified )
mql['origin'] = mql['origin'].fillna('not_identified') # mql origin에 비어있는 값은 not identified로 채워라
# 2. unknown -> not_identified
mql['origin'] = mql['origin'].replace('unknown', 'not_identified' ) # replace 앞의 값을 뒤의 값으로 변경해라 : 앞에있는 값이 원래값 -> 뒤에값으로 변경해라
# 3. 채널 맵핑 : 자잘한 채널 -> 상위 채널로 묶기
# map = 맵핑 (한 번에 여러가지를 바꿔야할 때 사용)
# 맵핑은 꼭 {} 딕셔너리 값으로 지정
# 변경내용 : other, other_publicities -> other / direct_traffic -> direct
channel_map = {
'organic_search' : 'organic_search',
'paid_search' : 'paid_search',
'social' : 'social',
'not_identified': 'not_identified',
'direct_traffic' : 'direct',
'email' : 'email',
'other' : 'other',
' other_publicities' : 'other' } #map에 없으면 결측치 값으로 설정됨
mql['origin_clean'] = mql['origin'].map(channel_map).fillna('other')
# map{}안에 없는 referral , display 값은 아래 명령에서 other 로 fillna 하겠다
# (맵핑에 없는 값은 결측치 값으로 변환된다음에 fillna로 지정)

1. 결측치 처리 : fillna
2. 결측치 처리 확인 : isnull().sum()
3. unknown 값 처리 : replace('','')
4. unknown 값 처리 확인 : .value_counts()
5. 채널 맵핑 처리 : map{}
6. 채널 맵핑 처리 확인 : .value_counts()

Step 3. 데이터 가공
- 데이터 병합 : 데이터.merge
- 계약 성사 데이터 필터링 : 데이터['컬럼명'].notna()
[데이터 병합]
- mql과 closed를 합쳐서 8000개 잠재고객 중 계약 성사여부 확인하기
- merge : mql_id 기준으로 left join으로 붙이는 함수로, 8000개 행 유지하면서 오른쪽으로 계약성사된 데이터만 값이 붙는 것
# 퍼널 통합 테이블 >> MQL 테이블 + Closed 테이블
funnel = mql.merge(closed, on='mql_id', how='left') # mql에 합쳐줘(첫번째 값을)
[계약 성사 건 추출]
1) 계약 성사 여부 확인
- 계약이 성사된 것과 성사되지 않는 것을 분류하기
- 기준은 계약날짜를 확인하면 계약이 성사된 것을 알 수 있음
- notna() : 결측치가 없다면 True, 있다면 False
# 계약이 성사된 건만 있는 컬럼 생성 (계약날짜를 확인하면 계약이 성사된 것이겠죠)
# notna() : 결측치가 없다면 True, 있다면 False
funnel['is_won'] = funnel['won_date'].notna() #won_date의 결측치가 없다면 true, 실제 결측치라면 false 값
2) 계약 성사된 리드만 필터링
- 생성된 파생컬럼에서 계약성사된 것만 추출
- 추출한 데이터를 copy해서 새로운 변수에 담아서 데이터 활용
# 계약 성사된 건만 필터링
won = funnel[funnel['is_won'] == True].copy() #funnel의 is_won이 true만 모아서 필터링해서 새로운걸로 copy하고 won 변수에 넣어줘
won


3) 계약 성사 까지 기간 분석
- 계약 성사까지의 최장, 최단, 평균, 중앙값 추출
- 집계함수 4개나 쓸거면 agg로 묶으면 안되나?? =_=..groupby agg
# 계약 성사까지 얼마나 걸리는 지 알고 싶다!
won['sales_cycle_days'] = (won['won_date'] - won['first_contact_date']).dt.days
print(f"최장 계약 기간 : {won['sales_cycle_days'].max()}일")
print(f"최단 계약 기간 : {won['sales_cycle_days'].min()}일") #오류발생 오류발생! 음수라니!
print(f"평균 계약 기간 : {won['sales_cycle_days'].mean():.0f}일")
print(f"중앙값 계약 기간 : { won['sales_cycle_days'].median():.0f}일")

※ 응용 : 집계함수 groupby, agg써서 계약일 기간 분석하기
# 여러 집계 함수를 리스트 형태로 전달
stats = won['sales_cycle_days'].agg(['max', 'min', 'mean', 'median']).round(0)
print(stats)
# 채널별로도 비교 가능
# 유입 경로(origin)별로 세일즈 사이클 통계 내기
origin_stats = won.groupby('origin')['sales_cycle_days'].agg(['max', 'min', 'mean', 'median']).round(0)
print(origin_stats)
💥 💥 3-2) 데이터 오류 삭제 💥 💥
계약 성사일 최소값이 -2가 나왔기 때문에 확인 후 오류값은 삭제하도록 합니다.
1. 필터링 확인 : 0보다 작은 값이 있는지 변수에 담아서 확인
2. 삭제 : 0보다 크거나 같은 값으로 조건 설정
3. min()값 재확인
# 계약 성사일이 음수인 데이터 오류 삭제
# -1. 필터링하기
negative_cycle = won[ won['sales_cycle_days'] < 0]
negative_cycle
# 계약 성사일이 음수인 데이터 오류 삭제
# -2. won에 >= 0 으로 설정
won = won[won['sales_cycle_days'] >= 0]


Step 4. 데이터 시각화
[영업 사이클 데이터 시각화]
- 도화지 생성 : fig, ax
- axvline : 세로축 선 생성 함수
- 표에서 중앙값이 어느쯤인지 알고 싶어서 추가
- axvline의 label과 ax.legend()는 세트로 axvline에서 지정한 label은 ax.lengend()로 출력됨 - bins로 그래프의 빈도 구간을 좁게 설정해서 좀 더 가시적으로 출력하기
# 영업 사이클 분포
# -1. 도화지 생성
fig, ax = plt.subplots(figsize=(10,5) )
# -2. hist 차트 만들 것 - won['sales_cycle_days']
ax.hist( won['sales_cycle_days'] , bins=50) # bins로 구간나눠서 가시적으로
ax.axvline( won['sales_cycle_days'].median(), color='red', linestyle='--', label=f"중앙값 : {won['sales_cycle_days'].median():.0f}" ) # 세로축에 중앙값 넣고 싶음
ax.set_title("계약 성사까지 걸린 기간(일수)")
ax.set_xlabel("일수")
ax.set_ylabel("건수")
ax.legend()
plt.show()


Step 5. 데이터 분석 (마케팅 지표 집계)
[ 유입채널별 리드 전환율 분석 ]
1) 총 리드수, 성사된 리드수 추출
- 채널별 전환율 집계를 위해 전체 몇 명의 잠재고객(leads)가 있는지, 그 중 성사된 건은 몇 건인지 확인하기
- 추출 항목 : 총 리드수, 성사된 리드수
- 사용 함수 : group by, agg
- .reset_index()로 라벨 정리
# 1. 채널별 전환율 집계
# 1-1. 총 리드수와 그 중 실제 성사된 리드수만 추출하기
channel_funnel = funnel.groupby('origin_clean').agg( # 위에서 채널 맵핑해줬더 origin_clean 쓰기
total_leads = ('mql_id', 'count'), # 총 리드수 중에
won_leads = ('is_won', 'sum')) # 실제 성사된 수는 몇 개인지
channel_funnel = channel_funnel.reset_index() # 라벨정리
2) 전환율 파생컬럼 생성
- 전환율 공식 대입 = (성사된 리드수( won_leads ) / 총 리드수 ( total_leads) ) * 100
# 전환율 파생 컬럼 옆에다가 만들기
channel_funnel['conversion_rate'] = ((channel_funnel['won_leads'] / channel_funnel['total_leads']) * 100).round(2)
channel_funnel


3) 채널별 평균 영업 사이클 파생컬럼 생성
- 2가지 방법 : agg / 바로 컬럼명만 꺼내는 방법
- 컬럼에서 바로 꺼낼 땐, rename(columns={ : })으로 이름 지정하기
# 1. won.groupby : 성공한 테이블인 won에서,
# 2. ('origin_clean') : 채널인 origin_clean 기준으로,
# 3. avg_cycle_days = : 평균 영업 사이클 이름 지정하고
# 4. ('sales_cycle_days', 'mean') : sales_cycle_days의 중앙값mean을 넣어라
# 채널별 평균 영업 사이클 추가
# 성공한 테이블 won / sales_cycle_days / mean() / avg_cycle_days / cycle_by_channel 테이블 이름
# won : 성사된 채널로 묶기
# 영업 사이클 .mean 넣기
# 테이블명 : cycle_by_channel
cycle_by_channel = won.groupby('origin_clean').agg(
avg_cycle_days = ('sales_cycle_days', 'mean')
).round(0)
cycle_by_channel = cycle_by_channel.reset_index()
cycle_by_channel
# 강사님 해설 : agg로 해도 되는데 1개만 있으니까 이런식의 코드로 빠르게 확인 가능
#cycle_by_channel = won.groupby('origin_clean')['sales_cycle_days'].mean().round(0).reset_index().rename(columns={'sales_cycle_days' : 'avg_cycle_days'})

4) 1,2번의 채널퍼널과 3번의 채널별 영업 사이클 병합
- merge 사용해서 병합하고 on=, how= 잊지말기
# 데이터 병합 : chnnel_funnel = channel_funnel + cycle_by_channel left join merge
# on= 기준
# how= left 잊지말기
channel_funnel = channel_funnel.merge(cycle_by_channel, on='origin_clean', how='left')
# 전환율 기준으로 정렬하기
channel_funnel = channel_funnel.sort_values('conversion_rate', ascending=False)
channel_funnel
💡4번 테이블로 확인 가능한 내용
- organic search는 total leads는 많은데 전환이 낮다 = 광고태워서 리드 많이 가져왔는데 전환율이 낮다는 것
심지어 사이클도 길다. 이는, 질나쁜 리드들이 들어오는 것 같으므로 예산을 줄이는 게 나을 것 같다
- direct 채널은 mql리드의 수는 적은데 전환율이 높으면 질이 좋은 애들이니 조금 예산 높여서 진행해도 될듯 하다

5) 채널별 전환율 시각화
- 리드의 질이 낮은 채널 분석하기 = MQL 수가 높은데 전환율은 낮은 채널
- 시각화 주제 : MQL 수, 전환율
# 채널별 전환율 시각화 / MQL 수, 전환율
fig, axes = plt.subplots(1, 2, figsize =(14,5))
# 채널별 전환율 : y열 전환율 (가로막대는 y축 써주고, x축 써야함)
axes[0].barh( channel_funnel['origin_clean'], channel_funnel['conversion_rate'], color='coral')
axes[0].set_title("채널별 리드 전환율 %")
# MQL 수
axes[1].barh( channel_funnel['origin_clean'], channel_funnel['total_leads'])
axes[1].set_title("채널별 MQL 수")
plt.show()

[ SDR 담당자별 성과 분석 ]
담당자 그룹별로 집계해서 평균 영업성사일도 확인해보고 우수영업사원도 파악해서 영업전략 수립하기
- sdr_id = 실제 영업사원들의 ID
- 담당자가 얼마나 많은 mql만나서 성사시켰는지 : mql_id, count
- 담당자별 평균 영업 사이클 : sales_cycle_days, mean
#sdr_id = 실제 영업사원들의 ID
won.info()
# SDR 성과분석
# 담당자가 얼마나 많은 mql만나서 성사시켰는지 / 영업사이클일수 평균
# = mql_id, count / sales_cycle_days, mean
sdr_perf = won.groupby('sdr_id').agg(
won_count = ('mql_id', 'count'),
avg_cycle = ('sales_cycle_days', 'mean'),
median_cycle = ('sales_cycle_days', 'median')
).round(0).reset_index()
#숫자 정리
sdr_perf = sdr_perf.sort_values('won_count', ascending=False)

💡최종 전처리 결과 요약 !
순서 작업 내용 핵심 메서드 01 두 파일 기본 정보 및 결측치 구조 파악 .isnull().sum(), .describe() 02 날짜 컬럼 변환 및 파생 컬럼 생성 pd.to_datetime(), .dt.quarter 03 유입 채널(origin) 정제 및 표준화 .fillna(), .replace(), .map() 04 두 파일 LEFT JOIN 퍼널 통합 .merge(how='left'), notna() 05 영업 사이클 기간 계산 .dt.days, 음수 오류 탐지 06 채널별 전환율 분석 .groupby().agg(), 이중 축 시각화 07 업종 결측치 처리 및 분포 분석 .fillna(), value_counts() 08 리드 행동 프로파일 분석 조건 필터링, .notna() 11 SDR 담당자별 성과 분석 .groupby().agg(), 산점도
야간 스터디 과제하고나니까 본 수업의 이해도가 조금 올라간 것 같다.
똑같은 내용을 3번 보면 이해하기 시작한다는데
음 반복학습이 답이구나...
오늘 내용이 엄청 많은 느낌인데 막상 한 게 그렇게나 많은 것 같진 않고 (= 과제할 때랑 똑같네!)

'Growth Marketing > GM4' 카테고리의 다른 글
| [TIL-260422] 멋쟁이사자처럼 그로스마케팅 4기 - 데이터분석 개론 day30 | 머신러닝 기초 - 선형회귀분석모델 (0) | 2026.04.23 |
|---|---|
| [TIL-260421] 멋쟁이사자처럼 그로스마케팅 4기 - 데이터분석 개론 day29 | 전자상거래 퍼널분석 (0) | 2026.04.23 |
| [TIL-260417] 멋쟁이사자처럼 그로스마케팅 4기 - 데이터분석 개론 day27 | 동적 크롤링 (2) | 2026.04.17 |
| [TIL-260416] 멋쟁이사자처럼 그로스마케팅 4기 - 데이터분석 개론 day26 | 크롤링-xml, json (0) | 2026.04.16 |
| [TIL-260415] 멋쟁이사자처럼 그로스마케팅 4기 - 데이터분석 개론 day25 | 프롬프트 & RSS (1) | 2026.04.15 |