Определите один исход на назначенного посетителя

PythonIntermediate
Практиковаться сейчас

Введение

У общественной мастерской есть две версии страницы регистрации: A и B. Прежде чем сравнивать их, аналитику нужно определить, что считать одним наблюдением. Один человек может посетить страницу трижды или дважды нажать кнопку регистрации, а другой не создаст ни одного события. Если считать строки событий отдельными людьми, сравнение будет искажено.

Сначала вы прочитаете пример с шестью людьми, затем построите бинарный семидневный исход для каждого назначенного посетителя в более крупном фиксированном эксперименте. Эти оригинальные синтетические учебные данные представляют случайное распределение; это не отслеживание реальных посетителей и не свидетельства о настоящем бизнесе. Python, pandas, NumPy, SciPy и Matplotlib уже подготовлены в независимой среде WebIDE. Предполагается знание базовой навигации в терминале, функций Python и выбора данных в pandas; необходимые объединения и операции со временем объясняются здесь. Знания статистического вывода не нужны. 55 минут — оценка автора с учётом чтения и исправления обычных ошибок.

Сравните людей вместо кликов

На этом шаге вы прочитаете шесть карточек посетителей и рассчитаете доли регистраций, используя назначенных людей как знаменатели.

Начните в WebIDE и откройте Terminal → New Terminal. Терминал и проводник используют одни и те же файлы. cd меняет рабочий каталог этого терминала; повторите команду при открытии другого терминала.

cd /home/labex/project/signup-experiment

cat показывает текстовый файл, не изменяя его. Прочитайте небольшой разобранный пример:

cat warmup-visitors.csv

Каждая строка — один назначенный посетитель. Первые трое принадлежат A, последние трое — B. В этом примере qualifying_signup_events уже классифицированы внутри семидневного окна каждого человека. 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 процентного пункта. Процентные пункты получаются прямым вычитанием процентов; это не относительный процентный рост. Дополнительные посещения не увеличивают ни один знаменатель.

Напишите многократно используемую функцию сравнения. Программа запуска передаёт pandas DataFrame с именем people и столбцами visitor_id, group, signed_up. Булева маска, например people["group"] == "A", выбирает строки A. len(a) считает назначенных людей; сумма бинарного столбца считает зарегистрировавшихся людей. int и float создают обычные числа Python для JSON-файлов.

Перенаправление оболочки > заменяет указанный файл. Here-document с кавычками <<'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. Повторное копирование полного блока функции исправит обычную ошибку редактирования; повторять setup или сбрасывать среду не нужно.

Постройте полные семидневные исходы

На этом шаге вы превратите исходные записи распределения и событий в полную таблицу на уровне людей, необходимую для сравнения.

Продолжайте в том же терминале WebIDE, в /home/labex/project/signup-experiment. Прочитайте определения полей, затем исходные таблицы:

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

В полной таблице 12 назначенных людей, по шесть в каждой группе. visitor_id — устойчивый текстовый идентификатор, а не позиция строки. В таблице событий люди повторяются, а также есть посторонний посетитель OUTSIDE, поэтому сопоставлять таблицы по номерам строк нельзя. Журнал полон для этого учебного примера: отсутствие подходящего события означает отсутствие регистрации, а не неизвестный исход.

У каждого человека есть момент назначения с часовым поясом — assigned_at. Окно имеет вид [назначение, назначение + семь дней): начало включается, точная верхняя граница — нет. UTC — общая временная система отсчёта; 12:00:00+02:00 и 10:00:00Z обозначают один момент. pd.to_datetime(..., utc=True, format="ISO8601") преобразует эти строки явного формата в сопоставимые временные метки UTC. Timedelta(days=7) представляет семь прошедших суток.

К заданному моменту анализа все периоды наблюдения должны завершиться:

cat config.json

В этом модуле каждое окно заканчивается до analysis_at. Другие настройки понадобятся в следующих работах. Не меняйте момент анализа, чтобы представить незавершённое наблюдение за человеком как завершённое. Все предоставленные таблицы курса содержат 4–14 людей, не менее двух в каждой группе. Дублирующиеся ID распределения, включая противоречащие группы, недопустимы; выбор строки с более красивым результатом не является исправлением.

Левое объединение (left merge) начинает с каждого назначения и присоединяет подходящие события. how="left" сохраняет человека без событий. validate="one_to_many" требует, чтобы каждый ID встречался в таблице распределения не более одного раза, но допускает несколько строк событий. Посторонний ID события не может добавить человека в назначенную совокупность.

Сравнения создают булевы маски. В pandas & объединяет условия, а | означает хотя бы одно из условий; каждое сравнение заключайте в скобки. Затем groupby(...).any() проверяет, существует ли хотя бы одно подходящее событие для каждого человека. Обратное сопоставление этих флагов по ID сохраняет одну строку на назначение, включая обоснованные нули. 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. Это не заполнение нулём неизвестного назначения или незавершённого наблюдения.

Следующая команда намеренно содержит опечатку в имени этапа. Она должна вывести invalid choice и завершиться со статусом 2. Это ошибка аргумента команды; данные не стали недопустимыми и не изменились.

python3 run.py pepole

Исправьте имя этапа и сохраните таблицу на уровне людей:

python3 run.py people
python3 show_results.py people

Должно быть 12 различных ID. Проверьте P-8 (два подходящих события, но исход 1), Q-3 (нет событий, но исход 0), M-90 (регистрация точно на исключённой верхней границе, исход 0) и K-4 (за секунду до границы, исход 1). X-02 зарегистрировался до назначения и остаётся 0. OUTSIDE не должен появиться.

Теперь повторно используйте функцию сравнения для этой действительно сохранённой таблицы:

python3 run.py summary
python3 show_results.py summary

В полном эксперименте зарегистрировался 1 из 6 людей A и 5 из 6 в B, разность около +66.67 процентного пункта. Теперь у вас есть обоснованные знаменатель и окно, но ещё нет утверждения, что новая страница вызвала улучшение. Следующие работы исследуют, как случайное распределение меняет наблюдаемую разность.

Проверки читают сохранённую работу и вызывают неизменённые функции только на изолированных копиях в рамках того же открытого контракта. Они охватывают независимо изменённый порядок строк входных таблиц, другие ID, входы без событий и недопустимое распределение/наблюдение. Эквивалентные реализации pandas или Python приветствуются, если build_people возвращает те же именованные столбцы DataFrame, а summarize — документированные объекты. Повторные проверки не создают текущие результаты заново и не меняют прогресс.

Резюме

Вы сравнили шесть карточек, написали многократно используемую функцию долей и построили полные семидневные исходы из необработанных событий. Устойчивые ID, явное окно UTC и знаменатели из назначенных людей не позволили повторным кликам и отсутствию событий исказить результат. Сохранённые таблица людей и сводка теперь позволяют честно исследовать случайную вариацию.