Devin.KR

전후 비교와 원인 가설

160분 안팎

학습 목표

측정 조건 변화와 인과 해석의 한계를 기록합니다.

개념

같은 숫자 표에도 여러 원인이 있습니다

before 완료율보다 after 완료율이 높으면 개선 가설을 검토할 근거가 생깁니다. 하지만 화면 변경 뒤 행사 종류가 달라졌거나 사용자들이 같은 과업을 연습했다면 화면만의 효과라고 말할 수 없습니다. 제품 담당은 전후 결과와 비교 조건을 같이 전달합니다. 이번 자료는 가상 이벤트의 계산 연습이며 실제 효과 평가가 아닙니다. 앞 모듈의 관찰 O01·O02는 상태 의미 문제를, O04·O05는 경로 도움을 설명합니다. 수치와 행동을 연결하되 원인을 입증했다는 표현을 사용하지 않습니다.

두 구간을 같은 쿼리에서 집계합니다

기간별로 쿼리를 복사해 수정하면 after에 테스트 계정 제외를 빼먹기 쉽습니다. WITH periods로 before와 after의 시작·종료를 두 행에 적고 같은 JOIN 조건을 적용합니다. WITH는 쿼리 안에서 이름 붙인 임시 결과를 만드는 문법이며 별도 저장 파일을 생성하지 않습니다. periods에 ord도 넣어 결과를 before·after 순서로 고정합니다. T01·is_test=0·시작 이상·종료 미만은 양쪽에 똑같이 적용합니다. 기간 이름을 로그에 임의로 채우는 대신 시작 시각으로 구간 소속을 결정합니다.

빈 구간도 한 행을 보존합니다

LEFT JOIN은 기간을 먼저 남기고 맞는 시도가 없어도 해당 기간 행을 유지합니다. 이때 COUNT(*)는 빈 기간의 보존 행까지 1로 셀 수 있으므로 COUNT(a.attempt_id)를 사용합니다. attempt_id는 실제 시도에만 존재하는 기본 키입니다. 과업·테스트·시각 조건은 ON에 둡니다. WHERE a.is_test=0을 뒤에 쓰면 시도가 없는 행의 NULL이 제거되어 빈 기간이 사라질 수 있습니다. 두 기간 모두 자료가 없어도 before 0 0 0과 after 0 0 0을 출력하는 계약으로 경계를 확인합니다.

출력에는 고유 사용자 수도 둡니다

출력 열은 PERIOD ATTEMPTS SUCCESS USERS입니다. USERS는 COUNT(DISTINCT a.user_id)이며 기간 안 비테스트 T01 사용자의 수입니다. 같은 사용자 재시도가 두 번 있으면 시도는 늘고 사람 수는 그대로입니다. USERS는 독립적인 표본 규모를 해석하는 보조 정보이고 분모는 ATTEMPTS입니다. 전후 기간에 같은 가명 user_id가 나타날 수 있으므로 두 기간의 USERS를 더해 전체 참여자 수라고 하지 않습니다. 사용자별 변화 분석은 해당 연결과 반복 관찰 조건을 따로 정의한 뒤 진행합니다.

비율 차이와 상대 변화를 구분합니다

제공 미션 이벤트 before는 4시도 중 성공 2건이라 50.0%, after는 4시도 중 성공 3건이라 75.0%입니다. 차이는 25.0퍼센트포인트입니다. 상대 변화는 기존 50.0%를 기준으로 50.0% 증가이며 두 표현의 분모가 다릅니다. 이번 제출에서는 혼동을 줄이기 위해 퍼센트포인트 차이를 사용합니다. before 분모가 0이면 비율 차이도 산출할 수 없습니다. 건수와 비율을 함께 남기고 25% 개선이라고 모호하게 줄여 쓰지 않습니다.

관찰 전후는 별도의 표로 둡니다

m07 v1은 S01·S02·S03의 독립 성공 1/3이고 v2는 V01의 1/1입니다. 이 값은 가상 사례의 판정 요약이며 서로 다른 사람과 표본 크기입니다. 1/1을 전체 사용자 100% 해결이라고 발표하지 않습니다. 이벤트의 2/4·3/4와 합쳐 3/7·4/5를 만들면 출처·측정 방식이 다른 자료가 한 분모에 들어갑니다. metrics.json observationSummary에는 세션 ID와 재확인 ID를 그대로 연결하고 eventResults와 나눕니다. 읽는 동료가 어느 숫자가 어떤 근거인지 되찾을 수 있어야 합니다.

바뀐 조건을 빠짐없이 적습니다

비교 표 옆에 화면 버전, 참여자 모집 기준, 기기, 과업 문장, 도움 규칙, 시작 화면, 기간, 행사 조건을 적습니다. v1과 v2에서 안내 문구·행동 이름이 바뀐 것은 시험한 변경이며 다른 요소는 가능한 한 맞춥니다. 자료에 기기나 행사 난이도가 없으면 같았다고 채우지 않고 미기록이라고 적습니다. 같은 7일이라는 길이도 행사 일정과 요일별 이용량까지 같다는 뜻은 아닙니다. 정보를 모르는 항목은 한계와 추가 수집 계획으로 연결합니다.

학습 효과를 가설로 남깁니다

같은 사용자가 before와 after에 등장하면 이전 탐색 경험으로 빨라질 수 있습니다. 서로 다른 사용자를 관찰해도 경험 차이가 결과에 영향을 줄 수 있습니다. 이벤트는 사용자 반복을 보여 주지만 화면을 실제로 봤는지와 노출 순서까지 기록하지 않아 학습 정도를 산출할 수 없습니다. 다음 테스트는 경로를 모르는 새 참여자를 모집하거나 순서 영향을 고려한 배치를 검토합니다. 어떤 방법을 쓰든 참가 조건과 도움 규칙을 고정하고 학습 가능성을 없앴다고 성급히 단정하지 않습니다.

두 변경을 동시에 시험한 한계를 씁니다

R01 안내와 R02 행동 이름은 함께 적용한 v2 화면입니다. 성공이 늘었어도 어느 변경이 얼마나 기여했는지 분리할 수 없습니다. O01·O02와 O04·O05는 각각의 가설을 지지하는 행동 자료지만 단독 효과 크기는 아닙니다. 두 요소를 따로 판단해야 하는 결정이라면 변경을 나누어 같은 과업을 관찰하는 추가 계획을 세웁니다. 현 단계에서는 두 변경을 포함한 제안의 검증 후보로 남기고 R01만으로 25퍼센트포인트 올렸다는 문장을 쓰지 않습니다.

다음 행동은 근거와 위험을 연결합니다

이번 결정은 확대 적용 확정이 아니라 추가 검증입니다. 새 참여자의 T01 독립 수행을 같은 90초 기준으로 관찰하고 참가 확정 오해와 도움 여부를 함께 기록합니다. 구현 뒤에는 키보드 이동과 본인 데이터 권한을 회귀 확인합니다. 담당·수집 조건·판정·확인 시점을 nextValidation에 씁니다. 빈 내역 N01과 실제 앱 이동 N02는 기존 remaining에서 이어받습니다. 미결 D01 정원 정책과 보류 I02·I03을 지표 상승으로 자동 승인하지 않습니다. 검증 결과가 어떤 범위의 결정을 도울지 정합니다.

검사기는 계산을 보고 동료는 판단을 읽습니다

check.py는 이전 파일 해시·근거 참조·지표 계약·쿼리 재실행·기간별 결과·통계·관찰 요약을 검사합니다. FAIL eventResults는 쿼리 결과와 제출 건수를 대조하라는 뜻이며 FAIL references는 원본에 없는 근거 ID를 확인하라는 뜻입니다. 성공 수를 잘못 적어도 형식만 맞으면 통과하는 검사를 피하기 위해 기대 수치도 재계산합니다. 검사 통과가 문장의 타당성까지 증명하지는 않습니다. 동료는 비교 조건 누락, 가상 출처 표시, 인과 단정, 추가 검증의 실행 가능성을 읽습니다.

보고는 결과와 불확실성을 함께 전달합니다

보고 문장은 “가상 이벤트의 같은 제외 조건에서 기록 기준 완료율은 50.0%에서 75.0%로 25.0퍼센트포인트 높았습니다. 사용자 반복과 표본 수, 미기록 조건 때문에 화면 변경의 실제 효과로 해석하지 않습니다”처럼 씁니다. O03의 기존 화면 성공 반례도 보존합니다. 원하는 결론만 강조하면 어떤 사용자는 이미 가능했던 사실을 놓칩니다. 최종 제출에는 재현 쿼리·원자료·관찰 ID·한계·다음 결정이 이어져야 합니다. 동료가 숫자를 다시 만들고 남은 질문을 찾을 수 있으면 이 단계가 완료됩니다.

따라하기

같은 조건으로 두 구간을 집계합니다

sqlite3 -separator ' ' -nullvalue NULL :memory:로 연습 DB를 엽니다. 공백 구분과 NULL 표시를 맞춥니다. 아래 테이블 생성·삽입·조회를 순서대로 붙여 넣습니다. 다음 단계 쿼리는 이 DB에 그대로 입력합니다. 다시 시작하려면 SQLite를 종료하고 새 메모리 DB를 엽니다. 브라우저 실습은 테스트 입력이 초기 자료를 준비하므로 조회 쿼리만 제출합니다.

CREATE TABLE task_attempts(
 attempt_id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
 task_id TEXT NOT NULL, started_at TEXT NOT NULL,
 is_test INTEGER NOT NULL CHECK(is_test IN (0,1)),
 outcome TEXT, duration_seconds REAL);

INSERT INTO task_attempts VALUES
('B01','U01','T01','2026-10-01T00:00:00+09:00',0,'success',30),
('B02','U02','T01','2026-10-02T10:00:00+09:00',0,'failure',NULL),
('B03','U02','T01','2026-10-03T10:00:00+09:00',0,'success',70),
('B04','U03','T01','2026-10-07T23:59:59+09:00',0,NULL,NULL),
('A01','U01','T01','2026-10-08T00:00:00+09:00',0,'success',20),
('A02','U04','T01','2026-10-09T10:00:00+09:00',0,'success',30),
('A03','U04','T01','2026-10-10T10:00:00+09:00',0,'success',40),
('A04','U05','T01','2026-10-14T23:59:59+09:00',0,'assisted',NULL),
('X01','TEST','T01','2026-10-02T10:00:00+09:00',1,'success',10),
('X02','TEST','T01','2026-10-09T10:00:00+09:00',1,'success',10),
('X03','U06','T02','2026-10-02T10:00:00+09:00',0,'success',15),
('X04','U06','T01','2026-09-30T23:59:59+09:00',0,'success',15),
('X05','U06','T01','2026-10-15T00:00:00+09:00',0,'success',15);

WITH periods(ord, period, start_at, end_at) AS (
 VALUES (1, 'before', '2026-10-01T00:00:00+09:00', '2026-10-08T00:00:00+09:00'),
        (2, 'after', '2026-10-08T00:00:00+09:00', '2026-10-15T00:00:00+09:00')
)
SELECT p.period, COUNT(a.attempt_id),
       COALESCE(SUM(CASE WHEN a.outcome = 'success' THEN 1 ELSE 0 END), 0),
       COUNT(DISTINCT a.user_id)
FROM periods p
LEFT JOIN task_attempts a
 ON a.task_id = 'T01' AND a.is_test = 0
 AND a.started_at >= p.start_at AND a.started_at < p.end_at
GROUP BY p.ord, p.period
ORDER BY p.ord;

실행 결과

before 4 2 3
after 4 3 3

빈 기간의 LEFT JOIN을 확인합니다

앞에서 준비한 연습 DB에 다음 쿼리를 입력합니다. 빈 자료 확인 단계는 새 메모리 DB에서 같은 테이블만 만들고 INSERT 없이 실행합니다.

WITH periods(ord, period, start_at, end_at) AS (
 VALUES (1, 'before', '2026-10-01T00:00:00+09:00', '2026-10-08T00:00:00+09:00'),
        (2, 'after', '2026-10-08T00:00:00+09:00', '2026-10-15T00:00:00+09:00')
)
SELECT p.period, COUNT(a.attempt_id),
       COALESCE(SUM(CASE WHEN a.outcome = 'success' THEN 1 ELSE 0 END), 0),
       COUNT(DISTINCT a.user_id)
FROM periods p
LEFT JOIN task_attempts a
 ON a.task_id = 'T01' AND a.is_test = 0
 AND a.started_at >= p.start_at AND a.started_at < p.end_at
GROUP BY p.ord, p.period
ORDER BY p.ord;

실행 결과

before 0 0 0
after 0 0 0

퍼센트포인트 차이를 계산합니다

다음 독립 예제를 실행하고 결과의 대상과 단위를 확인합니다.

before = 100 * 2 / 4
after = 100 * 3 / 4
print(f'{before:.1f}% {after:.1f}% {(after-before):.1f}pp')

실행 결과

50.0% 75.0% 25.0pp

미션의 해석과 계획을 제출합니다

comparison.sql과 metrics.json을 완성합니다. observationSummary는 v1 1/3·v2 1/1을 보존하고 한계 세 가지 이상 및 nextValidation의 대상·과업·담당·확인 시점을 씁니다. python3 check.py --submission metrics.json으로 계산·참조를 검사한 뒤 동료가 인과 단정과 미결 정책을 읽습니다.

확인 문제

실습

두 구간의 T01·is_test=0 시도를 동일 조건으로 집계합니다. before는 2026-10-01T00:00:00+09:00 이상 2026-10-08T00:00:00+09:00 미만, after는 그 종료 시각 이상 2026-10-15T00:00:00+09:00 미만입니다. PERIOD ATTEMPTS SUCCESS USERS 순서로 before, after 두 행을 출력합니다. USERS는 고유 user_id 수이며 분모는 시도 수입니다. success만 분자이고 결과 누락도 분모에 남깁니다. 빈 기간은 이름 뒤 0 0 0을 출력합니다. 같은 ID 중복 전송은 이미 제거했으며 다른 시도 ID의 재시도는 모두 셉니다.

모범 답안
WITH periods(ord, period, start_at, end_at) AS (
 VALUES (1, 'before', '2026-10-01T00:00:00+09:00', '2026-10-08T00:00:00+09:00'),
        (2, 'after', '2026-10-08T00:00:00+09:00', '2026-10-15T00:00:00+09:00')
)
SELECT p.period, COUNT(a.attempt_id),
       COALESCE(SUM(CASE WHEN a.outcome = 'success' THEN 1 ELSE 0 END), 0),
       COUNT(DISTINCT a.user_id)
FROM periods p
LEFT JOIN task_attempts a
 ON a.task_id = 'T01' AND a.is_test = 0
 AND a.started_at >= p.start_at AND a.started_at < p.end_at
GROUP BY p.ord, p.period
ORDER BY p.ord;

더 읽기

면접 질문

  • 기능 개선 여부를 확인할 지표를 정하는 방법을 설명해 주시면 됩니다.