How to join assignment logs with outcomes in Python

Merge submission log entries with grading outcomes using left join and full outer join patterns in pure Python.

Medium Python 3.9+ Aug 9, 2026 A/B testing & experimentation 12 views 0 copies

Python code

53 lines
Python 3.9+
from datetime import datetime, timedelta

class AssignmentLog:
    def __init__(self):
        self.logs = [
            {"assignment_id": 101, "student_id": "S001", "submitted_at": "2024-03-01 10:30:00"},
            {"assignment_id": 101, "student_id": "S002", "submitted_at": "2024-03-02 14:15:00"},
            {"assignment_id": 102, "student_id": "S001", "submitted_at": "2024-03-05 09:45:00"},
        ]
        self.outcomes = [
            {"assignment_id": 101, "student_id": "S001", "grade": "A"},
            {"assignment_id": 101, "student_id": "S002", "grade": "B"},
            {"assignment_id": 102, "student_id": "S003", "grade": "C"},
        ]

    def join_outcomes_present(self):
        """Left join: all log entries, outcome grade if exists."""
        result = []
        for log in self.logs:
            matched = [o for o in self.outcomes 
                       if o["assignment_id"] == log["assignment_id"] 
                       and o["student_id"] == log["student_id"]]
            row = dict(log)
            row["grade"] = matched[0]["grade"] if matched else None
            result.append(row)
        return result

    def join_outcomes_all(self):
        """Full outer join mock: union of log keys and outcome keys."""
        keys = set((l["assignment_id"], l["student_id"]) for l in self.logs)
        keys |= set((o["assignment_id"], o["student_id"]) for o in self.outcomes)
        result = []
        for aid, sid in sorted(keys):
            log_match = next((l for l in self.logs 
                              if l["assignment_id"] == aid and l["student_id"] == sid), None)
            out_match = next((o for o in self.outcomes 
                              if o["assignment_id"] == aid and o["student_id"] == sid), None)
            result.append({
                "assignment_id": aid,
                "student_id": sid,
                "submitted_at": log_match["submitted_at"] if log_match else None,
                "grade": out_match["grade"] if out_match else None,
            })
        return result

if __name__ == "__main__":
    log = AssignmentLog()
    print("Left Join (log-driven):")
    for row in log.join_outcomes_present():
        print(row)
    print("\nFull Outer Join:")
    for row in log.join_outcomes_all():
        print(row)

Output

stdout
Left Join (log-driven):
{'assignment_id': 101, 'student_id': 'S001', 'submitted_at': '2024-03-01 10:30:00', 'grade': 'A'}
{'assignment_id': 101, 'student_id': 'S002', 'submitted_at': '2024-03-02 14:15:00', 'grade': 'B'}
{'assignment_id': 102, 'student_id': 'S001', 'submitted_at': '2024-03-05 09:45:00', 'grade': None}

Full Outer Join:
{'assignment_id': 101, 'student_id': 'S001', 'submitted_at': '2024-03-01 10:30:00', 'grade': 'A'}
{'assignment_id': 101, 'student_id': 'S002', 'submitted_at': '2024-03-02 14:15:00', 'grade': 'B'}
{'assignment_id': 102, 'student_id': 'S001', 'submitted_at': '2024-03-05 09:45:00', 'grade': None}
{'assignment_id': 102, 'student_id': 'S003', 'submitted_at': None, 'grade': 'C'}

How it works

This code simulates SQL-style joins with Python lists and dicts. The left join keeps every log entry and attaches a grade only when a matching outcome exists, using a list comprehension to find matches. The full outer join builds a union of all unique (assignment_id, student_id) pairs, then looks up each side independently. Both methods use dictionary key equality as the join condition, which mirrors how you'd combine experiment data tables. The dict(log) copy ensures the original logs stay unmodified when adding the grade field.

Common mistakes

  • Forgetting to match on both assignment_id and student_id, causing cross-joins.
  • Mutating the original log dictionaries instead of copying them with dict(log).
  • Assuming every log has an outcome, so accessing matched[0] without checking raises IndexError.

Variations

  1. Use pandas merge with how='left' or how='outer' on DataFrames for larger datasets.
  2. Use SQLite in-memory tables with two SELECT queries for declarative joins.

Real-world use cases

  • Merging event telemetry from an experiment with user bucketing data to compute per-variant metrics.
  • Reconciling survey responses against participant enrollment lists to identify missing respondent data.
  • Combining task logs with completion grades in an A/B testing platform to evaluate feature impact.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from A/B testing & experimentation

Related tutorials and quizzes for this topic.