부트캠프

멋쟁이사자처럼 부트캠프 그로스마케팅 4기 29일차_260420

Yuuma 2026. 4. 20. 17:17

 

이벤트 데이터 수집과 전처리 ─
dataLayer부터 B2B 퍼널 분석까지

1. 이벤트 데이터란 무엇인가

1-1. 이벤트 데이터 vs 일반 데이터

일반 데이터(예: 회원 테이블)는 현재 상태를 저장한다. 반면 이벤트 데이터는 "누가 무엇을 언제 했는가"를 기록한다. 이 차이가 그로스마케터에게 이벤트 데이터가 중요한 이유다.

구분 일반 데이터 이벤트 데이터
저장 방식 현재 상태 (스냅샷) 행동 로그 (타임라인)
질문 "누가 있다" "누가 무엇을 했다"
예시 user_id, name, age event_time, event_type, user_id

1-2. 이벤트 데이터의 3요소

이벤트 데이터는 Who / What / When 세 가지 요소로 구성된다. 이 구조를 이해해야 퍼널 분석, 리타겟팅, A/B 테스트 평가 등 실무 작업을 제대로 수행할 수 있다.

이벤트 데이터 3요소 인포그래픽

1-3. 이벤트 타입 상세

이번 실습 데이터셋에는 4가지 이벤트가 있으며, 각각 실무에서의 의미가 다르다.

view

상품 상세 페이지 조회. 가장 많이 발생하며, 리타겟팅·개인화 추천의 핵심 신호다.

cart

장바구니 담기. 구매 의도 신호. view 대비 cart 비율 = 장바구니 전환율.

remove_from_cart

장바구니에서 상품 제거. 구매 이탈 신호. 이 이벤트가 많은 상품은 가격·UX 문제 점검 필요.

purchase

결제 완료. 최종 목표 이벤트. cart 대비 purchase 비율 = 결제 전환율.

1-4. 그로스마케터가 이벤트 데이터로 하는 일

업무 필요한 이벤트 데이터
퍼널 분석 view → cart → purchase 이벤트 흐름
리타겟팅 광고 cart 이벤트 발생 후 purchase 없는 사용자
개인화 추천 특정 카테고리 view 이벤트가 많은 사용자
이탈 분석 마지막 이벤트 이후 접속 없는 사용자
A/B 테스트 평가 그룹별 이벤트 발생 횟수 및 전환율 비교

2. dataLayer와 GTM이란 무엇인가

2-1. 왜 dataLayer가 필요했나

디지털 마케팅 초창기에는 사용자 행동을 수집하려면 개발자가 각 페이지 코드를 직접 수정해야 했다. 마케팅 도구가 바뀔 때마다 코드를 전부 다시 짜야 하는 문제가 있었다.

💡
dataLayer는 웹사이트와 마케팅 도구 사이의 중간 데이터 저장소다. 개발자는 dataLayer에 한 번만 기록하면, GA4·메타픽셀·크리테오 등 어떤 마케팅 도구든 그 데이터를 가져갈 수 있다.

2-2. dataLayer → GTM → GA4 전체 흐름

dataLayer GTM GA4 데이터 수집 흐름

2-3. dataLayer 실제 코드 구조

datalayer_examples.js JavaScript
// ① 상품 상세 페이지 조회 (view_item)
window.dataLayer = window.dataLayer || [];
dataLayer.push({
  'event': 'view_item',
  'ecommerce': {
    'items': [{
      'item_id': '5300797',
      'item_name': 'Samsung Galaxy S10',
      'item_category': 'electronics.smartphone',
      'item_brand': 'samsung',
      'price': 559.00
    }]
  }
});

// ② 장바구니 담기 (add_to_cart)
dataLayer.push({
  'event': 'add_to_cart',
  'ecommerce': {
    'items': [{ 'item_id': '5300797', 'price': 559.00, 'quantity': 1 }]
  }
});

// ③ 구매 완료 (purchase)
dataLayer.push({
  'event': 'purchase',
  'ecommerce': {
    'transaction_id': 'TXN-20191101-8821',
    'value': 559.00,
    'currency': 'USD',
    'items': [{ 'item_id': '5300797', 'price': 559.00, 'quantity': 1 }]
  }
});
🔍 코드 해설
window.dataLayer || [] dataLayer가 이미 있으면 그걸 쓰고, 없으면 빈 배열을 새로 만든다. 충돌 방지용 안전장치.
dataLayer.push() 배열 맨 뒤에 데이터를 추가하는 함수. 엑셀에서 마지막 행에 새 데이터를 입력하는 것과 같다. GTM이 이 변화를 감지해서 GA4 등으로 자동 전송한다.
ecommerce.items[{...}] 이커머스 표준 데이터 구조. 상품 1개를 객체(중괄호)로 표현하고, 여러 상품은 배열(대괄호) 안에 넣는다.

2-4. dataLayer 키 → CSV 컬럼 매핑

dataLayer 키 CSV 컬럼 예시 값
event event_type view, cart, purchase
items[0].item_id product_id 5300797
items[0].item_category category_code electronics.smartphone
items[0].price price 559.00
사용자 식별 시스템 user_id 512428238
서버 타임스탬프 event_time 2019-11-01 00:00:00 UTC
🔑
GTM은 "dataLayer에 purchase 이벤트가 들어오면 GA4와 메타픽셀에 동시에 보내라"는 규칙을 코딩 없이 설정할 수 있게 해준다. 마케팅 도구가 바뀌어도 개발 코드는 그대로, 설정만 변경하면 된다.

3. GA4에서 데이터를 확인하고 추출하는 법

GA4 데모 계정 접속 → 탐색 분석 → CSV 내보내기

1
구글 계정으로 로그인 후 GA4 데모 계정 접속 링크 클릭
2
"데모 계정 액세스" 클릭 → Google Merch Shop 속성 선택
3
보고서 → 참여도 → 이벤트에서 purchase, add_to_cart 등 이벤트 확인
4
탐색 → 빈 양식 선택 → 측정기준(이벤트 이름, 날짜) + 측정항목(이벤트 수) 배치
5
우측 상단 다운로드 아이콘 → CSV 내보내기

GA4 CSV를 pandas로 불러오기

ga4_load.py Python
import pandas as pd

# GA4 CSV는 상단 8행이 메타 정보 → skiprows=8 필요
ga4_df = pd.read_csv('Analytics 탐색 분석 데이터.csv', skiprows=8)

print(ga4_df.shape)
print(ga4_df.head())
print(ga4_df.dtypes)
🔍 함수 설명
pd.read_csv() CSV 파일을 읽어서 pandas의 표(DataFrame) 형태로 변환한다. 엑셀을 파이썬으로 열어주는 함수라고 생각하면 된다. skiprows=8은 "앞 8줄은 건너뛰고 읽어라"는 뜻 — GA4 CSV 특유의 메타 정보 영역을 제외하기 위함이다.
.shape 데이터의 (행 수, 열 수)를 튜플로 반환한다. 예: (1000, 9)면 1,000행 9열짜리 표.
.head() 데이터의 첫 5행을 보여준다. 데이터가 제대로 들어왔는지 확인할 때 가장 먼저 쓰는 함수. 괄호 안에 숫자를 넣으면 원하는 행 수만큼 확인 가능 (예: .head(10)).
.dtypes 각 컬럼의 데이터 타입을 보여준다. 숫자인데 문자(object)로 저장됐거나, 날짜인데 문자열로 저장된 경우를 발견할 때 쓴다.

4. 실습 데이터셋 소개 : Olist Marketing Funnel

4-1. 왜 B2B 리드 퍼널 데이터인가

이번 실습은 B2B 리드 퍼널로 관점을 바꿔본다. 그로스마케터는 SaaS, 에듀테크, 부동산 플랫폼 등 B2B 구조의 서비스를 다룰 일도 많기 때문이다.

B2B B2C 퍼널 비교 인포그래픽

4-2. Olist 데이터셋 구성

파일명 설명 행 수
olist_marketing_qualified_leads_dataset.csv MQL (마케팅 자격 리드) 목록 8,000건
olist_closed_deals_dataset.csv 실제 계약이 성사된 딜 정보 842건
📊
8,000명 중 842명만 실제 계약으로 이어졌다. 전환율 약 10.5%. "관심을 보인 리드" 전체와 "실제로 계약한 리드"가 별개 파일로 분리 관리되는 것이 B2B 데이터의 특징이다.

5. 전처리 실습 : B2B 리드 퍼널 데이터

전처리 7단계 파이프라인 인포그래픽

환경 세팅: 라이브러리 임포트 및 데이터 불러오기

setup.py Python
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

pd.set_option('display.max_columns', None)
pd.set_option('display.float_format', '{:.2f}'.format)

# MQL 파일 : 리드 획득 정보
mql = pd.read_csv('olist_marketing_qualified_leads_dataset.csv')

# Closed Deals 파일 : 계약 성사 정보
closed = pd.read_csv('olist_closed_deals_dataset.csv')

print(f"MQL 행 수: {len(mql):,} / 열 수: {mql.shape[1]}")
print(f"Closed Deals 행 수: {len(closed):,} / 열 수: {closed.shape[1]}")
🔍 함수 설명
import pandas as pd pandas 라이브러리를 불러온다. pandas는 파이썬에서 엑셀처럼 표 형태의 데이터를 다루는 도구다. as pd는 "앞으로 pandas를 pd라는 별명으로 부르겠다"는 뜻.
import matplotlib.pyplot as plt 그래프를 그리는 라이브러리. as plt로 별명을 붙여 편하게 호출한다.
warnings.filterwarnings('ignore') 실행 중 나타나는 경고 메시지를 숨긴다. 분석 결과에는 영향 없이 화면을 깔끔하게 유지할 때 쓴다.
pd.set_option() pandas의 출력 설정을 변경한다. display.max_columns=None은 "컬럼이 아무리 많아도 전부 보여줘", display.float_format은 "소수점은 2자리까지만 표시해줘"라는 의미.
실습 01 두 파일 기본 정보 확인

목적: 컬럼 타입, 결측치, 데이터 분포를 파악해 전처리 방향 결정

01_missing_check.py Python
# MQL 결측치 현황
missing_mql = mql.isnull().sum()
missing_mql_pct = (missing_mql / len(mql) * 100).round(1)
print(pd.DataFrame({
    '결측치 수': missing_mql,
    '결측치 비율(%)': missing_mql_pct
}))

# Closed Deals 결측치 현황
missing_cd = closed.isnull().sum()
missing_cd_pct = (missing_cd / len(closed) * 100).round(1)
print(pd.DataFrame({
    '결측치 수': missing_cd,
    '결측치 비율(%)': missing_cd_pct
}))
🔍 함수 설명
.isnull() 각 셀이 비어있는지(결측치인지) 확인해서 True/False로 표시한다. 엑셀에서 빈 셀을 표시하는 것과 같다.
.sum() .isnull()과 함께 쓰면 True(=결측치)의 개수를 컬럼별로 합산한다. True는 1, False는 0으로 계산되기 때문에 합계 = 결측치 수가 된다.
len(mql) mql 데이터의 전체 행 수를 반환한다. 결측치 수를 전체 행 수로 나누면 결측치 비율을 계산할 수 있다.
.round(1) 소수점 첫째 자리까지 반올림한다. .round(2)면 둘째 자리, .round(0)이면 정수로 반올림.
pd.DataFrame({...}) 딕셔너리(중괄호로 된 데이터)를 표 형태로 변환한다. 여러 Series를 한 눈에 보기 좋게 정리할 때 쓴다.
⚠️
Closed Deals의 has_company, declared_monthly_revenue는 결측치 90% 이상이다. 오류가 아니라 리드가 폼에 선택적으로 입력한 항목들이기 때문. 결측치 비율을 보고 어떤 컬럼을 분석에 쓸지 판단해야 한다.
실습 02 날짜 컬럼 datetime 변환 및 파생 컬럼 생성

목적: 날짜 문자열을 datetime으로 변환하고 분석용 파생 컬럼(년/월/분기/주차) 생성

02_datetime.py Python
# 날짜 타입 변환 (문자열 → datetime)
mql['first_contact_date'] = pd.to_datetime(mql['first_contact_date'])
closed['won_date'] = pd.to_datetime(closed['won_date'])

# 수집 기간 확인
print(f"MQL 수집 기간: {mql['first_contact_date'].min().date()} ~ {mql['first_contact_date'].max().date()}")

# MQL 파생 컬럼 생성
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
mql['contact_week']    = mql['first_contact_date'].dt.isocalendar().week.astype(int)

# Closed Deals 파생 컬럼
closed['won_year']    = closed['won_date'].dt.year
closed['won_month']   = closed['won_date'].dt.month
closed['won_quarter'] = closed['won_date'].dt.quarter
🔍 함수 설명
pd.to_datetime() "2019-11-01"처럼 문자열로 저장된 날짜를 파이썬이 날짜로 인식하는 형태(datetime)로 변환한다. 변환 전에는 단순 문자열이라 날짜 계산이 안 되지만, 변환 후에는 "며칠 차이인지", "몇 분기인지" 등을 바로 계산할 수 있다.
.dt.year / .dt.month / .dt.quarter datetime 컬럼에서 년도 / 월 / 분기만 따로 추출한다. .dt는 날짜 관련 속성에 접근하는 접두어다. 이렇게 추출한 값으로 "월별 리드 유입 추이", "분기별 계약 성사 추이" 같은 분석이 가능해진다.
.dt.isocalendar().week ISO 표준 기준으로 해당 날짜가 연간 몇 번째 주인지 반환한다. 주차별 리드 유입량 분석에 활용한다.
.astype(int) 데이터 타입을 정수(int)로 변환한다. isocalendar().week는 특수한 타입으로 반환되기 때문에 연산을 쉽게 하려면 정수로 바꿔주는 것이 좋다.
.min() / .max() 컬럼의 최솟값 / 최댓값을 반환한다. 날짜에 쓰면 가장 이른 날짜 / 가장 늦은 날짜를 알 수 있어 데이터 수집 기간 확인에 유용하다.
실습 03 유입 채널(origin) 정제

목적: 불규칙하게 입력된 origin 값을 분석 가능한 형태로 표준화

03_origin_clean.py Python
# 현황 파악 — 값별 분포 확인
print(mql['origin'].value_counts())
print(f"결측치: {mql['origin'].isnull().sum()}건")

# 결측치와 'unknown' → 'not_identified'로 통일
mql['origin'] = mql['origin'].fillna('not_identified')
mql['origin'] = mql['origin'].replace('unknown', 'not_identified')

# 세부 채널 → 상위 채널로 매핑
channel_map = {
    'organic_search':    'organic_search',
    'paid_search':       'paid_search',
    'social':            'social',
    'email':             'email',
    'direct_traffic':   'direct',
    'other_publicities': 'other',
    'other':             'other',
    'not_identified':   'not_identified'
}
mql['origin_clean'] = mql['origin'].map(channel_map).fillna('other')
🔍 함수 설명
.value_counts() 해당 컬럼에 어떤 값이 몇 번 등장하는지 빈도수를 내림차순으로 보여준다. 채널별 리드 수를 파악하거나 예상치 못한 이상 값이 있는지 확인할 때 가장 먼저 쓰는 함수.
.fillna('값') 결측치(빈 값)를 지정한 값으로 채운다. fill(채우다) + NA(결측치)의 합성어. 여기서는 출처를 알 수 없는 빈 값을 'not_identified'로 채워 분석에서 제외되지 않게 한다.
.replace('이전값', '새값') 특정 값을 다른 값으로 교체한다. 엑셀의 찾아 바꾸기 기능과 동일. 딕셔너리를 넣으면 여러 값을 한 번에 교체할 수도 있다.
.map(딕셔너리) 딕셔너리(대응표)를 기준으로 값을 변환한다. "organic_search → organic_search, direct_traffic → direct"처럼 변환 규칙을 표로 만들어 일괄 적용하는 방식. 딕셔너리에 없는 값은 NaN이 되므로 뒤에 .fillna('other')로 처리한다.
origin은 GTM의 utm_source 값과 동일한 개념이다. 어떤 채널에서 유입된 리드가 가장 잘 전환되는지 파악하는 것이 그로스마케터의 핵심 업무다.
실습 04 두 파일 LEFT JOIN : 퍼널 통합 테이블 구성

목적: 8,000개 리드 전체에 계약 성사 여부를 붙여 퍼널 전체 뷰 생성

04_left_join.py Python
# LEFT JOIN: mql_id 기준으로 두 파일 결합
# MQL 전체 기준, 계약이 성사된 경우에만 closed 정보가 추가됨
funnel = mql.merge(closed, on='mql_id', how='left')

print(f"결합 후 전체 행 수: {len(funnel):,}")

# 계약 성사 여부 컬럼 생성
# won_date가 있으면(not null이면) True, 없으면 False
funnel['is_won'] = funnel['won_date'].notna()

print(funnel['is_won'].value_counts())
print(f"전체 전환율: {funnel['is_won'].mean()*100:.1f}%")

# 출력 결과:
# False    7158
# True      842
# 전체 전환율: 10.5%
🔍 함수 설명
.merge(df, on='키', how='left') 두 표를 공통 컬럼을 기준으로 합친다. 엑셀의 VLOOKUP과 비슷하지만 훨씬 강력하다. how='left'는 "왼쪽 표(mql) 행은 전부 유지하고, 오른쪽(closed)에서 일치하는 것만 붙여라"는 의미. 계약이 안 된 7,158개 리드는 closed 컬럼이 NaN으로 채워진다.
.notna() 값이 존재하면 True, 결측치(NaN)이면 False를 반환한다. .isnull()의 반대. 여기서는 won_date가 있으면(= 계약이 성사됐으면) True로 표시해 is_won 컬럼을 만든다.
.mean() 평균을 계산한다. True/False 컬럼에 쓰면 True의 비율을 반환한다 (True=1, False=0으로 계산되므로). 여기서 is_won.mean() = 전환율이 된다.
실습 05 영업 사이클 기간(Sales Cycle) 계산

목적: 리드별 영업 사이클 일수 계산, 이상값(음수) 제거, 분포 시각화

05_sales_cycle.py Python
# 성사된 리드만 필터링
won = funnel[funnel['is_won'] == True].copy()

# 영업 사이클 = 계약 성사일 - 최초 컨택일
won['sales_cycle_days'] = (won['won_date'] - won['first_contact_date']).dt.days

print(won['sales_cycle_days'].describe())

# 음수 값 확인 (데이터 오류) 및 제거
negative = won[won['sales_cycle_days'] < 0]
print(f"음수 오류: {len(negative)}건 → 제거")
won = won[won['sales_cycle_days'] >= 0]

# 시각화: 영업 사이클 분포
fig, ax = plt.subplots(figsize=(10, 5))
ax.hist(won['sales_cycle_days'], bins=50, color='steelblue', alpha=0.8)
ax.axvline(won['sales_cycle_days'].median(), color='red', linestyle='--',
           label=f"중앙값: {won['sales_cycle_days'].median():.0f}일")
ax.set_title('영업 사이클 분포')
ax.legend()
plt.show()
🔍 함수 설명
df[조건].copy() 조건에 맞는 행만 필터링해서 독립적인 새 데이터프레임을 복사한다. .copy()를 붙이지 않으면 원본 데이터와 연결된 뷰(view)가 만들어져, 나중에 값을 수정할 때 경고가 발생한다.
(날짜A - 날짜B).dt.days 두 날짜의 차이를 일(day) 단위 정수로 변환한다. datetime끼리 빼면 "timedelta"라는 특수 타입이 나오는데, .dt.days를 붙여야 우리가 쓰기 편한 숫자(일수)가 된다.
.describe() 컬럼의 기초 통계량(개수, 평균, 표준편차, 최솟값, 사분위수, 최댓값)을 한 번에 보여준다. 데이터 분포를 빠르게 파악할 때 필수.
plt.subplots(figsize=()) 그래프를 그릴 캔버스(fig)와 좌표 영역(ax)을 생성한다. figsize=(10, 5)는 가로 10인치, 세로 5인치 크기의 그래프를 만들라는 뜻.
ax.hist(data, bins=50) 히스토그램(막대 분포 그래프)을 그린다. bins=50은 전체 범위를 50개 구간으로 나눠 표시하라는 뜻. bins 값이 클수록 막대가 촘촘해진다.
ax.axvline(값, color, linestyle) 그래프에 수직선(세로 기준선)을 추가한다. 중앙값이나 평균을 표시해 분포의 중심을 시각적으로 보여줄 때 쓴다.
실습 06 유입 채널별 리드 전환율 분석

목적: 채널별 MQL 수·성사 수·전환율·평균 영업 사이클을 비교해 마케팅 예산 배분 근거 도출

06_channel_analysis.py Python
# 채널별 전환율 집계
channel_funnel = (
    funnel.groupby('origin_clean')
    .agg(
        total_leads=('mql_id', 'count'),
        won_leads=('is_won', 'sum')
    )
    .reset_index()
)
channel_funnel['conversion_rate'] = (
    channel_funnel['won_leads'] / channel_funnel['total_leads'] * 100
).round(2)

# 채널별 평균 영업 사이클 추가
channel_cycle = (
    won.groupby('origin_clean')['sales_cycle_days']
    .mean().round(0).reset_index()
    .rename(columns={'sales_cycle_days': 'avg_cycle_days'})
)
channel_funnel = channel_funnel.merge(channel_cycle, on='origin_clean', how='left')
channel_funnel = channel_funnel.sort_values('conversion_rate', ascending=False)

# 시각화: 채널별 전환율 vs MQL 수
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].barh(channel_funnel['origin_clean'], channel_funnel['conversion_rate'], color="#CF1441")
axes[0].set_title('채널별 리드 전환율 (%)')
axes[1].barh(channel_funnel['origin_clean'], channel_funnel['total_leads'], color="#BDC334")
axes[1].set_title('채널별 MQL 수')
plt.tight_layout()
plt.show()
🔍 함수 설명
.groupby('컬럼') 지정한 컬럼을 기준으로 데이터를 그룹으로 묶는다. 엑셀의 피벗 테이블에서 행 기준을 설정하는 것과 같다. 여기서는 유입 채널(origin_clean)별로 묶었다.
.agg(새컬럼=(기준컬럼, 집계함수)) 그룹별로 집계 연산을 한다. Named Aggregation 방식으로, "새 컬럼 이름 = (집계할 컬럼, 집계 방법)"으로 작성한다. 'count'는 개수, 'sum'은 합계, 'mean'은 평균.
.reset_index() groupby 연산 후 인덱스(행 번호)가 변수명으로 올라가는데, 이를 다시 일반 컬럼으로 내려준다. 결과를 깔끔한 표 형태로 만들기 위해 거의 항상 함께 쓴다.
.rename(columns={'이전':'새이름'}) 컬럼 이름을 변경한다. 엑셀에서 열 헤더 이름을 수정하는 것과 같다.
.sort_values('컬럼', ascending=False) 지정한 컬럼 기준으로 데이터를 정렬한다. ascending=False는 내림차순(큰 값부터), True면 오름차순(작은 값부터).
ax.barh(y, x) 가로 막대 그래프를 그린다. 일반 bar()는 세로 막대, barh()는 수평 막대. 채널 이름처럼 텍스트가 긴 항목을 y축에 놓을 때 가독성이 좋다.
plt.tight_layout() 여러 그래프를 나란히 그릴 때 제목이나 축 레이블이 겹치지 않도록 간격을 자동으로 조정한다. 그래프 마지막에 습관적으로 붙이면 좋다.
⚠️
MQL 수가 많다고 좋은 채널이 아니다. 전환율과 영업 사이클을 함께 봐야 한다. MQL은 많지만 전환율이 낮은 채널 → 리드의 질이 낮음. MQL은 적지만 전환율이 높은 채널 → 집중 투자할 만한 채널.
실습 07 SDR 담당자별 성과 분석

목적: SDR별 담당 건수·평균 사이클을 비교해 우수 SDR 행동 패턴 파악

07_sdr_analysis.py Python
# SDR 성과 집계 (성사 리드 기준)
sdr_perf = (
    won.groupby('sdr_id')
    .agg(
        total_leads=('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('total_leads', ascending=False)
print(sdr_perf.head(10).to_string(index=False))
🔍 함수 설명
'median' 집계 중앙값을 구한다. 평균(mean)은 극단값(아주 긴 계약 등)에 크게 흔들리지만, 중앙값은 데이터를 크기순으로 나열했을 때 정중앙에 위치한 값이라 이상치에 강하다. SDR 성과를 볼 때 평균과 중앙값을 같이 보면 더 정확하다.
.to_string(index=False) DataFrame을 텍스트로 변환해서 출력한다. index=False는 왼쪽에 표시되는 행 번호(0, 1, 2...)를 숨긴다는 뜻. 깔끔한 결과 출력에 유용하다.
성사 건수가 많고 사이클이 짧은 SDR이 우수 담당자다. 이들의 첫 컨택 방식, 사용 스크립트, 후속 연락 타이밍을 분석해 전체 팀에 적용하면 팀 전환율을 높일 수 있다.

6. 전처리 완료 데이터 저장 및 정리

6-1. 전처리 결과 요약

번호 작업 내용 핵심 메서드
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(), .barh()
07 SDR 담당자별 성과 분석 .groupby().agg(), .sort_values()

6-2. 최종 데이터 저장

save_final.py Python
# 전처리 완료된 통합 퍼널 데이터 저장
funnel.to_csv('olist_funnel_clean.csv', index=False)

print(f"저장 완료: olist_funnel_clean.csv")
print(f"최종 행 수: {len(funnel):,}")
print(f"최종 컬럼: {list(funnel.columns)}")
🔍 함수 설명
.to_csv('파일명', index=False) DataFrame을 CSV 파일로 저장한다. index=False를 붙이지 않으면 행 번호(0, 1, 2...)가 첫 번째 컬럼으로 함께 저장된다. 불필요한 컬럼이 생기지 않도록 거의 항상 index=False를 붙인다.
f"{len(funnel):,}" f-string 안의 :,는 숫자에 천 단위 쉼표를 붙여 표시한다. 8000 대신 8,000으로 보여주는 방식.
list(funnel.columns) DataFrame의 모든 컬럼 이름을 리스트로 반환한다. 전처리 후 최종적으로 어떤 컬럼들이 남았는지 확인할 때 쓴다.

🎯
오늘의 핵심 인사이트

이벤트 데이터는 "누가 있다"가 아니라 "누가 무엇을 했다"를 기록하는 데이터다. dataLayer → GTM → GA4로 이어지는 수집 구조를 이해하면 데이터가 어디서, 어떻게 만들어지는지 알 수 있다.

B2B 퍼널 분석의 핵심은 두 파일의 LEFT JOIN이다. MQL 전체를 기준으로 계약 성사 여부를 붙인 다음, 채널별 전환율과 영업 사이클을 같이 봐야 진짜 인사이트가 나온다. MQL 수가 많다고 좋은 채널이 아니다.