Définissez un résultat par visiteur affecté

PythonIntermediate
Pratiquer maintenant

Introduction

Un atelier associatif dispose de deux versions de sa page d’inscription, A et B. Avant de les comparer, son analyste doit définir ce qui constitue une observation. Une personne peut visiter trois fois la page ou cliquer deux fois pour s’inscrire, tandis qu’une autre ne génère aucun événement. Compter ces lignes d’événements comme des personnes distinctes fausserait la comparaison.

Vous lirez d’abord un exemple à six personnes, puis construirez un résultat binaire sur sept jours pour chaque visiteur affecté, dans une expérience fixe plus grande. Ces données pédagogiques synthétiques originales représentent une affectation aléatoire ; elles ne constituent ni un suivi réel de visiteurs ni des preuves concernant une activité réelle. Python, pandas, NumPy, SciPy et Matplotlib sont préparés dans un environnement WebIDE indépendant. Vous devez connaître la navigation élémentaire dans un terminal, les fonctions Python et la sélection de tables avec pandas ; les jointures et opérations temporelles nécessaires sont expliquées ici. L’inférence statistique n’est pas un prérequis. La durée de 55 minutes est une estimation de l’auteur incluant lecture et corrections ordinaires.

Comparez des personnes plutôt que des clics

Dans cette étape, vous lirez six fiches de visiteurs et calculerez les taux d’inscription avec les personnes affectées comme dénominateurs.

Dans WebIDE, ouvrez Terminal → New Terminal. Le terminal et l’explorateur partagent les mêmes fichiers. cd change le répertoire de travail de ce terminal ; répétez la commande dans un nouveau terminal.

cd /home/labex/project/signup-experiment

cat affiche un fichier texte sans le modifier. Lisez le petit exemple :

cat warmup-visitors.csv

Chaque ligne correspond à un visiteur affecté. Les trois premiers appartiennent à A, les trois derniers à B. Pour cet exemple, qualifying_signup_events est déjà classé dans la fenêtre de sept jours de chaque personne. signed_up est le résultat binaire correspondant : 1 s’il existe au moins une inscription admissible, 0 sinon.

P-8 compte trois visites et deux événements d’inscription, mais apporte une personne et un résultat positif. Q-3 n’a ni visite ni événement d’inscription, mais reste une personne affectée avec résultat 0. Lisez les six fiches avant le calcul : A compte une inscription parmi trois personnes affectées ; B en compte deux parmi trois.

Le taux de A est 1 / 3, soit environ 33.33%. Celui de B est 2 / 3, soit environ 66.67%. La différence est B moins A, environ +33.33 points de pourcentage. Les points de pourcentage soustraient directement deux pourcentages ; ce n’est pas une augmentation relative en pourcentage. Des visites supplémentaires n’agrandissent aucun dénominateur.

Écrivez une fonction de comparaison réutilisable. L’exécuteur transmet à votre fonction un DataFrame pandas nommé people, avec les colonnes visitor_id, group et signed_up. Un masque booléen tel que people["group"] == "A" sélectionne les lignes de A. len(a) compte les personnes affectées ; la somme d’une colonne binaire compte les personnes inscrites. int et float produisent des nombres Python ordinaires pour les fichiers JSON.

La redirection du shell > remplace le fichier nommé. Le here-document entre guillemets <<'PY' transmet tout le contenu au fichier jusqu’à une ligne contenant seulement PY, sans expansion du shell. Collez le bloc entier, y compris son PY final. Vous pouvez aussi modifier le même fichier dans WebIDE et utiliser 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 exécute Python. Le fichier préparé run.py appelle votre fonction et écrit sa valeur de retour réelle ; son argument warmup sélectionne cette entrée. Il ne complète pas les réponses absentes.

python3 run.py warmup

La commande enregistre output/warmup.json. JSON conserve des objets nommés et des nombres. Le lecteur préparé affiche uniquement les fichiers existants ; il ne relance jamais votre programme et ne répare pas les résultats manquants.

python3 show_results.py warmup

Confirmez les effectifs affectés 3 et 3, les positifs 1 et 2 et une différence proche de +33.33 points de pourcentage. Gardez les nombres non arrondis dans les résultats enregistrés ; les arrondis de cette explication facilitent seulement la lecture. Un taux observé supérieur ne suffit pas à établir que B a amélioré la page.

Seuls compare.py, puis outcomes.py, doivent changer dans ce laboratoire. Vous pouvez ajouter des fichiers auxiliaires Python ordinaires à côté. Conservez assignments.csv, events.csv, warmup-visitors.csv, config.json, data-dictionary.md, run.py et show_results.py. Les autres unités possèdent des copies indépendantes et ne dépendent pas des fichiers de cette VM.

Si un nom de fichier ou un répertoire est incorrect, utilisez pwd pour afficher le répertoire courant et ls pour lister ses fichiers, puis revenez avec le cd précédent. Recopier le bloc complet de la fonction corrige une erreur d’édition ordinaire ; inutile de relancer setup ou de réinitialiser l’environnement.

Construisez des résultats complets sur sept jours

Dans cette étape, vous transformerez les affectations et événements bruts en la table complète par personne dont la comparaison a besoin.

Continuez dans le même terminal WebIDE, à /home/labex/project/signup-experiment. Lisez les définitions des champs, puis les tables originales :

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

La table complète contient 12 personnes affectées, six par groupe. visitor_id est une identité textuelle stable, pas une position de ligne. La table d’événements répète des personnes et contient un visiteur sans rapport, OUTSIDE : les tables ne peuvent donc pas être appariées par numéro de ligne. Le journal est complet pour cet exemple pédagogique : aucun événement admissible signifie aucune inscription, et non un résultat inconnu.

Chaque personne a un instant d’affectation avec fuseau horaire, assigned_at. La fenêtre est [affectation, affectation + sept jours) : le début compte, la borne supérieure exacte non. UTC est une référence commune ; 12:00:00+02:00 et 10:00:00Z décrivent le même instant. pd.to_datetime(..., utc=True, format="ISO8601") convertit ces chaînes au format explicite en horodatages UTC comparables. Timedelta(days=7) représente sept jours écoulés.

Tous les suivis doivent être terminés à l’instant d’analyse fourni :

cat config.json

Dans cette unité, chaque fenêtre se termine avant analysis_at. Les autres paramètres serviront plus tard. Ne changez pas l’instant d’analyse pour faire paraître complet le suivi d’une personne encore inachevé. Toutes les tables du cours contiennent 4–14 personnes, au moins deux par groupe. Les ID d’affectation dupliqués, y compris les groupes contradictoires, sont invalides ; choisir la ligne donnant un résultat plus séduisant ne répare pas le problème.

Une jointure gauche (left merge) part de chaque affectation et lui associe les événements correspondants. how="left" conserve une personne sans événement. validate="one_to_many" exige au plus une occurrence de chaque ID dans la table d’affectations, tout en autorisant plusieurs événements. Un ID d’événement extérieur ne peut pas ajouter une personne à la population affectée.

Les comparaisons produisent des masques booléens. Avec pandas, & combine des conditions et | signifie l’une ou l’autre ; placez chaque comparaison entre parenthèses. groupby(...).any() demande ensuite s’il existe un événement admissible pour chaque personne. Reporter ces indicateurs par ID conserve une ligne par affectation, y compris les zéros légitimes. ValueError refuse explicitement une affectation invalide ou un suivi inachevé, au lieu de faire semblant que les données sont comparables.

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

Une ligne sans événement correspondant dans la jointure gauche a des champs d’événement manquants. Ses comparaisons d’admissibilité sont fausses ; cette personne reçoit donc 0. Cela ne remplace pas par zéro une affectation inconnue ou un suivi incomplet.

La prochaine commande contient volontairement une faute dans le nom d’étape. Elle doit afficher invalid choice et terminer avec le code 2. C’est une erreur d’argument de commande ; elle n’a ni invalidé ni modifié les données.

python3 run.py pepole

Corrigez le nom d’étape et enregistrez la table par personne :

python3 run.py people
python3 show_results.py people

Il doit y avoir 12 ID distincts. Vérifiez P-8 (deux événements admissibles, mais résultat 1), Q-3 (aucun événement, résultat 0), M-90 (inscription exactement à la borne supérieure exclue, résultat 0) et K-4 (une seconde avant cette borne, résultat 1). X-02 s’est inscrit avant l’affectation et reste à 0. OUTSIDE ne doit pas apparaître.

Réutilisez maintenant votre fonction de comparaison sur cette table réellement enregistrée :

python3 run.py summary
python3 show_results.py summary

L’expérience complète compte 1 personne inscrite sur 6 dans A et 5 sur 6 dans B, soit une différence proche de +66.67 points de pourcentage. Vous avez maintenant un dénominateur et une fenêtre justifiables, mais pas encore la preuve que la nouvelle page a causé une amélioration. Les prochains laboratoires examinent comment l’affectation aléatoire peut faire varier la différence observée.

Les vérifications lisent votre travail enregistré et appellent vos fonctions inchangées uniquement sur des copies isolées respectant le même contrat public. Elles couvrent des ordres indépendants des lignes d’entrée, d’autres ID, des entrées sans événements et des affectations/suivis invalides. Les implémentations pandas ou Python équivalentes sont acceptées si build_people renvoie les mêmes colonnes nommées du DataFrame et si summarize renvoie les objets documentés. Répéter les vérifications ne reconstruit pas les résultats existants et ne change pas votre progression.

Résumé

Vous avez comparé six fiches, écrit une fonction de taux réutilisable et construit des résultats complets sur sept jours à partir des événements bruts. Les ID stables, une fenêtre UTC explicite et les dénominateurs de personnes affectées ont empêché les clics répétés et l’absence d’événements de fausser le résultat. La table individuelle et le résumé enregistrés permettent désormais d’examiner honnêtement la variation aléatoire.