割り当てられた訪問者ごとに結果を定義する

PythonIntermediate
オンラインで実践に進む

はじめに

地域のワークショップには、参加登録ページの A 版と B 版があります。比較する前に、分析担当者は何を 1 つの観測とするかを決めなければなりません。ある人はページを 3 回訪れたり、登録を 2 回クリックしたりする一方、別の人はイベントをまったく残さないかもしれません。これらのイベント行を別々の人として数えると、比較がゆがみます。

まず 6 人の例を読み、その後、より大きな固定された実験について、割り当てられた訪問者ごとに 7 日間の二値結果を作成します。この教材独自の合成データはランダム割り当てを表しており、実際の訪問者の追跡記録や事業に関する証拠ではありません。独立した WebIDE 環境には Python、pandas、NumPy、SciPy、Matplotlib が用意されています。基本的なターミナル操作、Python 関数、pandas による表の選択を前提とします。必要な結合と時刻の操作はここで説明します。統計的推論の知識は不要です。55 分は、読解と一般的な誤りの修正を含む著者の見積もりです。

クリックではなく人を比較する

このステップでは、6 人の訪問者カードを読み、割り当てられた人数を分母として登録率を計算します。

WebIDE で Terminal → New Terminal を開きます。ターミナルとファイルエクスプローラーは同じファイルを共有します。cd は、このターミナルの作業ディレクトリを変更します。別のターミナルを開いたときは、再び実行してください。

cd /home/labex/project/signup-experiment

cat はテキストファイルを変更せずに表示します。小さな計算例を読みましょう。

cat warmup-visitors.csv

各行は、割り当てられた訪問者 1 人です。最初の 3 人は A、最後の 3 人は B に属します。この例の qualifying_signup_events は、すでに各人の 7 日間の時間枠内にあるものとして分類されています。signed_up は対応する二値結果で、有効な登録が少なくとも 1 回あれば 1、なければ 0 です。

P-8 には 3 回の訪問と 2 回の登録イベントがありますが、人数としては 1 人、正の結果としては 1 件だけ寄与します。Q-3 には訪問も登録イベントもありませんが、結果 0 の割り当て済みの人として残ります。計算する前に 6 枚のカードを読みましょう。A は割り当てられた 3 人中 1 人、B は 3 人中 2 人が登録しています。

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

完全な表には、各群 6 人、計 12 人の割り当て済みの人がいます。visitor_id は安定した文字列の識別子であり、行の位置ではありません。イベント表には同じ人の繰り返しと、無関係な訪問者 OUTSIDE があるため、2 つの表を行番号で対応させることはできません。この教材のログは完全です。有効なイベントがないことは、結果が不明という意味ではなく、登録がないことを意味します。

各人には、タイムゾーン付きの割り当て時刻 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 人、各群少なくとも 2 人です。群の矛盾を含む割り当て ID の重複は無効です。見栄えのよい結果になる行を選ぶことは修正ではありません。

左結合(left merge)は、すべての割り当てを出発点として、一致するイベントを付け加えます。how="left" はイベントがない人も残します。validate="one_to_many" は、割り当て表に各 ID が高々 1 回だけ出現することを要求しつつ、複数のイベント行を許します。無関係なイベント ID によって割り当て集団に人が追加されることはありません。

比較はブールマスクを作ります。pandas の & は条件を組み合わせ、| はどちらかの条件を意味します。各比較を括弧で囲んでください。次に groupby(...).any() が、各人に有効なイベントが 1 つでもあるかを調べます。そのフラグを ID で対応付け直すと、正当な 0 も含めて割り当てごとに 1 行が残ります。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(有効なイベントは 2 回でも結果は 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 実装も歓迎します。チェックを繰り返しても、既存の結果を作り直したり進捗を変えたりしません。

まとめ

6 枚のカードを比較し、再利用可能な率の関数を書き、生のイベントから完全な 7 日間の結果を構築しました。安定した ID、明示的な UTC の時間枠、割り当て人数を用いた分母によって、繰り返しクリックやイベントの欠如によるゆがみを防ぎました。保存した個人表と集計を使い、次はランダムな変動を正直に調べられます。