배정된 방문자마다 결과 하나 정의하기

PythonIntermediate
지금 연습하기

소개

지역 워크숍에는 신청 페이지 A와 B가 있습니다. 비교하기 전에 분석자는 무엇을 하나의 관측으로 셀지 정의해야 합니다. 어떤 사람은 세 번 방문하거나 신청을 두 번 클릭하지만, 다른 사람은 이벤트를 전혀 남기지 않을 수 있습니다. 이런 이벤트 행을 서로 다른 사람으로 세면 비교가 왜곡됩니다.

먼저 여섯 사람의 예를 읽고, 더 큰 고정 실험에서 배정된 방문자마다 7일 동안의 이진 결과를 만듭니다. 직접 제작한 이 합성 교육 데이터는 무작위 배정을 나타내며, 실제 방문자 추적이나 실제 사업에 관한 증거가 아닙니다. 독립된 WebIDE 환경에 Python, pandas, NumPy, SciPy, Matplotlib이 준비되어 있습니다. 기본 터미널 탐색, Python 함수, pandas 표 선택을 알고 있어야 하며, 필요한 병합과 시간 연산은 여기서 설명합니다. 통계적 추론 지식은 필요하지 않습니다. 55분은 읽기와 일반적인 오류 수정을 포함한 저자의 예상 시간입니다.

클릭 대신 사람을 비교하기

이 단계에서는 방문자 카드 여섯 장을 읽고, 배정된 사람 수를 분모로 신청률을 계산합니다.

WebIDE에서 Terminal → New Terminal을 엽니다. 터미널과 파일 탐색기는 같은 파일을 공유합니다. cd는 이 터미널의 작업 디렉터리를 바꿉니다. 다른 터미널을 열면 다시 실행하세요.

cd /home/labex/project/signup-experiment

cat은 텍스트 파일을 바꾸지 않고 표시합니다. 작은 계산 예를 읽어 보세요.

cat warmup-visitors.csv

각 행은 배정된 방문자 한 명입니다. 처음 세 명은 A, 마지막 세 명은 B입니다. 이 예에서 qualifying_signup_events는 이미 각 사람의 7일 시간 창 안에 있는 이벤트로 분류되어 있습니다. signed_up은 해당 이진 결과로, 유효한 신청이 한 번 이상 있으면 1, 없으면 0입니다.

P-8은 방문 세 번과 신청 이벤트 두 번이 있지만 사람 한 명과 양성 결과 하나만 더합니다. Q-3은 방문도 신청 이벤트도 없지만 결과 0인 배정된 사람으로 남습니다. 계산 전에 여섯 카드를 읽으세요. A는 배정된 세 명 중 한 명, B는 세 명 중 두 명이 신청했습니다.

A의 신청률은 1 / 3, 약 33.33%입니다. B는 2 / 3, 약 66.67%입니다. 차이는 B 빼기 A, 약 +33.33퍼센트포인트입니다. 퍼센트포인트는 백분율을 직접 뺀 값이며 상대적인 백분율 증가가 아닙니다. 방문이 늘어도 어느 분모도 커지지 않습니다.

재사용 가능한 비교 함수를 작성합니다. 실행기는 visitor_id, group, signed_up 열이 있는 people이라는 pandas DataFrame을 전달합니다. people["group"] == "A" 같은 불리언 마스크는 A의 행을 선택합니다. len(a)는 배정된 사람 수를 세고, 이진 열의 합은 신청한 사람 수를 셉니다. int와 float는 JSON 파일에 쓸 일반 Python 숫자를 만듭니다.

셸 리디렉션 >는 지정된 파일을 교체합니다. 따옴표로 감싼 히어 도큐먼트 <<'PY'는 PY만 있는 줄까지의 내용을 셸 확장 없이 파일에 전달합니다. 마지막 PY를 포함한 전체 블록을 붙여 넣으세요. WebIDE에서 같은 파일을 편집하고 File → Save를 사용해도 됩니다.

cat > compare.py <<'PY'
def summarize(people):
    result = {}
    for group in ["A", "B"]:
        selected = people[people["group"] == group]
        assigned = len(selected)
        signed_up = int(selected["signed_up"].sum())
        result[group] = {
            "assigned": assigned,
            "signed_up": signed_up,
            "rate_pct": 100 * signed_up / assigned,
        }
    result["difference_pp"] = result["B"]["rate_pct"] - result["A"]["rate_pct"]
    return result
PY

python3는 Python을 실행합니다. 준비된 run.py는 함수를 호출하고 실제 반환값을 기록하며, warmup 인수는 이 입력을 선택합니다. 빠진 답을 채워 주지는 않습니다.

python3 run.py warmup

명령은 output/warmup.json을 저장합니다. JSON은 이름 있는 객체와 숫자를 저장합니다. 준비된 읽기 프로그램은 이미 있는 파일만 표시하며, 프로그램을 다시 실행하거나 빠진 결과를 복구하지 않습니다.

python3 show_results.py warmup

배정 인원이 3과 3, 양성 결과 수가 1과 2, 차이가 약 +33.33퍼센트포인트인지 확인하세요. 저장된 결과는 반올림하지 마세요. 설명의 반올림은 읽기 편하게 하기 위한 것입니다. 관측된 비율이 더 높다는 사실만으로 B가 페이지를 개선했다고 입증되지는 않습니다.

이 실습에서는 compare.py와 다음에 다룰 outcomes.py만 바꾸면 됩니다. 같은 위치에 일반 Python 보조 파일을 추가할 수 있습니다. assignments.csv, events.csv, warmup-visitors.csv, config.json, data-dictionary.md, run.py, show_results.py는 보존하세요. 다른 단원은 독립된 사본을 가지며 이 VM의 파일에 의존하지 않습니다.

파일 이름이나 디렉터리가 틀렸다면 pwd로 현재 위치를 보고 ls로 파일을 나열한 후 위의 cd로 돌아갑니다. 일반 편집 실수는 함수 블록 전체를 다시 복사해 고치세요. 설정을 다시 실행하거나 환경을 초기화할 필요는 없습니다.

완전한 7일 결과 만들기

이 단계에서는 원시 배정 기록과 이벤트 기록을 비교에 필요한 완전한 사람 단위 표로 바꿉니다.

같은 WebIDE 터미널의 /home/labex/project/signup-experiment에서 계속합니다. 필드 정의를 읽고 원본 표를 확인하세요.

cat data-dictionary.md
cat assignments.csv
cat events.csv

전체 표에는 각 집단 여섯 명씩 배정된 사람 12명이 있습니다. visitor_id는 안정적인 텍스트 식별자이며 행 위치가 아닙니다. 이벤트 표에는 같은 사람의 반복과 무관한 방문자 OUTSIDE가 있으므로 두 표를 행 번호로 짝지을 수 없습니다. 이 교육 예의 로그는 완전합니다. 유효한 이벤트가 없다는 것은 결과를 모른다는 뜻이 아니라 신청하지 않았다는 뜻입니다.

각 사람에게는 시간대가 있는 배정 시각 assigned_at이 있습니다. 시간 창은 **[배정 시각, 배정 시각 + 7일)**입니다. 시작은 포함하고 정확한 상한은 제외합니다. UTC는 공통 시간 기준입니다. 12:00:00+02:00와 10:00:00Z는 같은 순간입니다. pd.to_datetime(..., utc=True, format="ISO8601")는 이 명시적 형식의 문자열을 비교 가능한 UTC 타임스탬프로 바꿉니다. Timedelta(days=7)는 경과 시간 7일을 나타냅니다.

주어진 분석 시각까지 모든 추적 관찰이 끝나야 합니다.

cat config.json

이 단원의 모든 시간 창은 analysis_at 이전에 끝납니다. 다른 설정은 이후 실습에서 사용합니다. 아직 관찰이 끝나지 않은 사람을 완료된 것처럼 보이게 하려고 분석 시각을 바꾸지 마세요. 제공되는 코스의 표는 모두 총 4–14명이고 집단당 최소 두 명입니다. 집단 충돌을 포함한 중복 배정 ID는 잘못된 입력입니다. 더 좋은 결과가 나오는 행을 고르는 것은 수정이 아닙니다.

**왼쪽 병합(left merge)**은 모든 배정에서 시작하여 일치하는 이벤트를 붙입니다. how="left"는 이벤트가 없는 사람도 유지합니다. validate="one_to_many"는 배정 표에서 각 ID가 최대 한 번만 나오게 하면서 여러 이벤트 행은 허용합니다. 무관한 이벤트 ID가 배정 모집단에 사람을 추가할 수는 없습니다.

비교는 불리언 마스크를 만듭니다. pandas에서 &는 조건을 결합하고 |는 둘 중 하나를 뜻합니다. 각 비교를 괄호로 감싸세요. 그다음 groupby(...).any()는 각 사람에게 유효한 이벤트가 하나라도 있는지 확인합니다. 이 플래그를 ID로 다시 매핑하면 정당한 0도 포함해 배정당 한 행을 유지합니다. ValueError는 잘못된 배정이나 미완료 관찰을 명시적으로 거부하여 비교 가능한 데이터인 척하지 않도록 합니다.

cat > outcomes.py <<'PY'
import pandas as pd


def build_people(assignments, events, analysis_at):
    a = assignments.copy()
    if (a["visitor_id"].isna().any()
            or (a["visitor_id"] == "").any()
            or a["visitor_id"].duplicated().any()):
        raise ValueError("Each visitor needs one unique, nonempty assignment ID.")
    if not a["group"].isin(["A", "B"]).all():
        raise ValueError("Only groups A and B are valid.")
    a["assigned_at"] = pd.to_datetime(a["assigned_at"], utc=True, format="ISO8601")
    a["window_end"] = a["assigned_at"] + pd.Timedelta(days=7)
    if not (a["window_end"] <= pd.Timestamp(analysis_at)).all():
        raise ValueError("Every seven-day follow-up must be complete.")
    e = events.copy()
    e["occurred_at"] = pd.to_datetime(e["occurred_at"], utc=True, format="ISO8601")
    joined = a.merge(e, on="visitor_id", how="left", validate="one_to_many")
    eligible = ((joined["kind"] == "signup")
                & (joined["occurred_at"] >= joined["assigned_at"])
                & (joined["occurred_at"] < joined["window_end"]))
    flags = eligible.groupby(joined["visitor_id"]).any()
    a["signed_up"] = a["visitor_id"].map(flags).astype(int)
    return a[["visitor_id", "group", "signed_up"]]
PY

왼쪽 병합에서 일치하는 이벤트가 없으면 이벤트 필드가 결측입니다. 유효성 비교는 거짓이므로 그 사람의 결과는 0입니다. 알 수 없는 배정이나 끝나지 않은 관찰을 0으로 채우는 것은 아닙니다.

다음 명령은 의도적으로 단계 이름을 잘못 씁니다. invalid choice 메시지를 표시하고 종료 상태 2로 끝나야 합니다. 이는 명령 인수 오류이며, 데이터를 무효로 만들거나 바꾸지 않았습니다.

python3 run.py pepole

단계 이름을 고쳐 사람 단위 표를 저장합니다.

python3 run.py people
python3 show_results.py people

서로 다른 ID 12개가 있어야 합니다. P-8(유효한 이벤트 두 번이지만 결과 1), Q-3(이벤트가 없지만 결과 0), M-90(제외되는 상한과 정확히 같은 신청 시각이므로 결과 0), K-4(상한 1초 전이므로 결과 1)를 확인하세요. X-02는 배정 전에 신청해서 0으로 남습니다. OUTSIDE는 나타나면 안 됩니다.

실제로 저장된 이 표에 비교 함수를 다시 사용합니다.

python3 run.py summary
python3 show_results.py summary

전체 실험에서 A는 6명 중 1명, B는 6명 중 5명이 신청하여 차이가 약 +66.67퍼센트포인트입니다. 이제 분모와 시간 창을 설명할 수 있지만 새 페이지가 개선을 일으켰다고 주장할 수는 없습니다. 다음 실습은 무작위 배정이 관측 차이를 어떻게 바꿀 수 있는지 살펴봅니다.

검사는 저장된 작업을 읽고 같은 공개 계약 안의 격리된 사본에서만 변경하지 않은 함수를 호출합니다. 각 입력 표의 독립적인 행 순서 변경, 다른 ID, 이벤트 없는 입력, 잘못된 배정과 관찰을 다룹니다. build_people이 같은 이름의 DataFrame 열을, summarize가 문서화된 객체를 반환하면 동등한 pandas나 Python 구현도 환영합니다. 반복 검사는 기존 결과를 다시 만들거나 진도를 바꾸지 않습니다.

요약

여섯 카드를 비교하고 재사용 가능한 비율 함수를 작성했으며 원시 이벤트에서 완전한 7일 결과를 만들었습니다. 안정적인 ID, 명시적 UTC 시간 창, 배정된 사람을 세는 분모 덕분에 반복 클릭과 이벤트 부재가 결과를 왜곡하지 않았습니다. 저장된 사람 표와 요약을 바탕으로 이제 무작위 변동을 정직하게 살펴볼 수 있습니다.