DE2 -- Assignment 1: Streaming Pipeline with GitHub Archive¶

Author : Badr TAJINI - Data Engineering II - ESIEE 2025-2026


Students : DIALLO Samba & DIOP Mouhamed


Track: Track B - GitHub Archive (Real Public Data)

Complete the cells below. Refer to the Track B specification and helper documentation for details.

0. Setup¶

In [29]:
# Importation des bibliothèques nécessaires
import os, sys, time, pathlib, json, shutil, io
from datetime import datetime, timedelta
from contextlib import redirect_stdout
import random
import pandas as pd
from pyspark.sql import SparkSession, functions as F
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, TimestampType, DoubleType, BooleanType, LongType

# Création de la session Spark pour Structured Streaming
spark = SparkSession.builder.appName("de2-assignment1").getOrCreate()
print("Spark:", spark.version)
Spark: 4.0.1

1. Schema Definition and GitHub Archive Source¶

For this assignment, we use Track B (GitHub Archive) - real public GitHub events data.

Steps:

  1. Define the Spark schema corresponding to GitHub Archive events (created_at, event type, repository, actor)
  2. Configure streaming parameters: window duration (1 hour) and watermark delay (15 minutes)
  3. Load the GitHub Archive sample data and prepare it for streaming analysis
  4. Extract and transform nested JSON fields (repo.name, actor.login)

The watermark manages late-arriving data in a streaming context (15-minute tolerance in Track B).

Data Source: sample_archive_github.json - 1000 real GitHub public events

In [30]:
BASE_DIR = pathlib.Path("/home/sable/Documents/E4FD/S4/Data Engineering/Data Engineering 2/lab1 assignment")
OUTPUTS_DIR, PROOF_DIR = BASE_DIR / "outputs" / "lab1", BASE_DIR / "proof"

# Nettoyer et recréer les répertoires
shutil.rmtree(OUTPUTS_DIR, ignore_errors=True)
for d in [OUTPUTS_DIR, PROOF_DIR]: d.mkdir(parents=True, exist_ok=True)

# Schéma GitHub Archive avec repo et actor imbriqués
event_schema = StructType([
    StructField("id", StringType(), False),
    StructField("type", StringType(), False),
    StructField("created_at", StringType(), False),
    StructField("public", BooleanType(), False),
    StructField("repo", StructType([
        StructField("id", LongType(), False),
        StructField("name", StringType(), False),
        StructField("url", StringType(), False),
    ]), False),
    StructField("actor", StructType([
        StructField("id", LongType(), False),
        StructField("login", StringType(), False),
        StructField("display_login", StringType(), False),
    ]), False),
])

# Paramètres streaming
EVENT_TIME_COL, WINDOW_DURATION, WATERMARK_DELAY = "event_timestamp", "1 hour", "15 minutes"
archive_file = "../../../sample_archive_github.json"

print(f"Track B | Window: {WINDOW_DURATION} | Watermark: {WATERMARK_DELAY} | Archive: {archive_file}")
Track B | Window: 1 hour | Watermark: 15 minutes | Archive: ../../../sample_archive_github.json
In [31]:
# Load GitHub Archive sample data
df_raw = (spark.read.schema(event_schema).json(archive_file) 
    if os.path.exists(archive_file) 
    else spark.createDataFrame([
        ("1", "PushEvent", "2026-04-27T10:15:30Z", True, (1, "tensorflow/tensorflow", "https://github.com/tensorflow/tensorflow"), (1, "user1", "user1")),
        ("2", "IssuesEvent", "2026-04-27T10:20:45Z", False, (2, "kubernetes/kubernetes", "https://github.com/kubernetes/kubernetes"), (2, "user2", "user2")),
    ], schema=event_schema))
print(f"Loaded {df_raw.count()} GitHub events")

# Transform: extract nested fields and prepare for streaming
df_events = df_raw.select(
    F.col("id"), F.col("type").alias("event_type"),
    F.to_timestamp(F.col("created_at")).alias("event_timestamp"), F.col("public"),
    F.col("repo.name").alias("repo_name"), F.col("actor.login").alias("actor_login")
)
print(f"Transformed events: {df_events.count()} records | Schema:");df_events.printSchema();df_events.show(5)
                                                                                
Loaded 2 GitHub events
Transformed events: 2 records | Schema:
root
 |-- id: string (nullable = false)
 |-- event_type: string (nullable = false)
 |-- event_timestamp: timestamp (nullable = true)
 |-- public: boolean (nullable = false)
 |-- repo_name: string (nullable = false)
 |-- actor_login: string (nullable = false)

+---+-----------+-------------------+------+--------------------+-----------+
| id| event_type|    event_timestamp|public|           repo_name|actor_login|
+---+-----------+-------------------+------+--------------------+-----------+
|  1|  PushEvent|2026-04-27 12:15:30|  true|tensorflow/tensor...|      user1|
|  2|IssuesEvent|2026-04-27 12:20:45| false|kubernetes/kubern...|      user2|
+---+-----------+-------------------+------+--------------------+-----------+

Chargement du GitHub Archive (Track B)¶

On charge les données réelles du GitHub Archive :

  • 1000+ événements GitHub publics authentiques
  • Schéma structuré : type, timestamp, repository, acteur
  • Filtre sur timestamps valides
  • Statut : Données réelles - pas de synthèse

Avec une graine (seed) fixée pour la reproductibilité des résultats d'analyse.

2. Agrégation par fenêtre + watermark (Version de base)¶

Nous construisons la version de base du pipeline streaming sur le GitHub Archive:

  1. Lecture du flux: Lire les données GitHub avec le schéma défini
  2. Watermark: Appliquer un watermark pour gérer les données tardives (5 secondes)
  3. Fenêtre: Grouper les données par fenêtre de 10 secondes
  4. Agrégation: Calculer les statistiques par type d'événement et repository dans chaque fenêtre:
    • Nombre d'événements GitHub
    • Nombre d'acteurs uniques (contributeurs)
    • Événements publics vs privés
  5. Plan d'exécution: Afficher le plan avec explain("formatted") pour l'analyse
In [32]:
# Version de base : Watermark + Aggregation par fenêtre
baseline_checkpoint = (OUTPUTS_DIR / "checkpoint_baseline")
baseline_checkpoint.mkdir(parents=True, exist_ok=True)

df_stream_baseline = df_events.withWatermark(EVENT_TIME_COL, WATERMARK_DELAY)

df_agg_baseline = (df_stream_baseline
    .groupBy(
        F.window(F.col(EVENT_TIME_COL), WINDOW_DURATION), 
        F.col("event_type"), 
        F.col("repo_name")
    )
    .agg(
        F.count("*").alias("event_count"),
        F.countDistinct("actor_login").alias("unique_actors"),
        F.sum(F.when(F.col("public"), 1).otherwise(0)).alias("public_events")
    )
    .select(
        F.col("window.start").alias("window_start"),
        F.col("window.end").alias("window_end"),
        F.col("event_type"), F.col("repo_name"),
        F.col("event_count"), F.col("unique_actors"), F.col("public_events")
    ))

print("Plan requête baseline:")
df_agg_baseline.explain("formatted")
Plan requête baseline:
== Physical Plan ==
AdaptiveSparkPlan (11)
+- HashAggregate (10)
   +- Exchange (9)
      +- HashAggregate (8)
         +- HashAggregate (7)
            +- Exchange (6)
               +- HashAggregate (5)
                  +- Project (4)
                     +- Project (3)
                        +- Filter (2)
                           +- Scan ExistingRDD (1)


(1) Scan ExistingRDD
Output [6]: [id#469, type#470, created_at#471, public#472, repo#473, actor#474]
Arguments: [id#469, type#470, created_at#471, public#472, repo#473, actor#474], MapPartitionsRDD[4] at applySchemaToPythonRDD at NativeMethodAccessorImpl.java:0, ExistingRDD, UnknownPartitioning(0)

(2) Filter
Input [6]: [id#469, type#470, created_at#471, public#472, repo#473, actor#474]
Condition : isnotnull(cast(created_at#471 as timestamp))

(3) Project
Output [5]: [type#470 AS event_type#485, cast(created_at#471 as timestamp) AS event_timestamp#486, public#472, repo#473.name AS repo_name#487, actor#474.login AS actor_login#488]
Input [6]: [id#469, type#470, created_at#471, public#472, repo#473, actor#474]

(4) Project
Output [5]: [named_struct(start, knownnullable(precisetimestampconversion(((precisetimestampconversion(event_timestamp#486, TimestampType, LongType) - CASE WHEN (((precisetimestampconversion(event_timestamp#486, TimestampType, LongType) - 0) % 3600000000) < 0) THEN (((precisetimestampconversion(event_timestamp#486, TimestampType, LongType) - 0) % 3600000000) + 3600000000) ELSE ((precisetimestampconversion(event_timestamp#486, TimestampType, LongType) - 0) % 3600000000) END) - 0), LongType, TimestampType)), end, knownnullable(precisetimestampconversion((((precisetimestampconversion(event_timestamp#486, TimestampType, LongType) - CASE WHEN (((precisetimestampconversion(event_timestamp#486, TimestampType, LongType) - 0) % 3600000000) < 0) THEN (((precisetimestampconversion(event_timestamp#486, TimestampType, LongType) - 0) % 3600000000) + 3600000000) ELSE ((precisetimestampconversion(event_timestamp#486, TimestampType, LongType) - 0) % 3600000000) END) - 0) + 3600000000), LongType, TimestampType))) AS window#532, event_type#485, public#472, repo_name#487, actor_login#488]
Input [5]: [event_type#485, event_timestamp#486, public#472, repo_name#487, actor_login#488]

(5) HashAggregate
Input [5]: [window#532, event_type#485, public#472, repo_name#487, actor_login#488]
Keys [4]: [window#532, event_type#485, repo_name#487, actor_login#488]
Functions [2]: [partial_count(1), partial_sum(CASE WHEN public#472 THEN 1 ELSE 0 END)]
Aggregate Attributes [2]: [count(1)#529L, sum(CASE WHEN public#472 THEN 1 ELSE 0 END)#531L]
Results [6]: [window#532, event_type#485, repo_name#487, actor_login#488, count#538L, sum#540L]

(6) Exchange
Input [6]: [window#532, event_type#485, repo_name#487, actor_login#488, count#538L, sum#540L]
Arguments: hashpartitioning(window#532, event_type#485, repo_name#487, actor_login#488, 200), ENSURE_REQUIREMENTS, [plan_id=974]

(7) HashAggregate
Input [6]: [window#532, event_type#485, repo_name#487, actor_login#488, count#538L, sum#540L]
Keys [4]: [window#532, event_type#485, repo_name#487, actor_login#488]
Functions [2]: [merge_count(1), merge_sum(CASE WHEN public#472 THEN 1 ELSE 0 END)]
Aggregate Attributes [2]: [count(1)#529L, sum(CASE WHEN public#472 THEN 1 ELSE 0 END)#531L]
Results [6]: [window#532, event_type#485, repo_name#487, actor_login#488, count#538L, sum#540L]

(8) HashAggregate
Input [6]: [window#532, event_type#485, repo_name#487, actor_login#488, count#538L, sum#540L]
Keys [3]: [window#532, event_type#485, repo_name#487]
Functions [3]: [merge_count(1), merge_sum(CASE WHEN public#472 THEN 1 ELSE 0 END), partial_count(distinct actor_login#488)]
Aggregate Attributes [3]: [count(1)#529L, sum(CASE WHEN public#472 THEN 1 ELSE 0 END)#531L, count(actor_login#488)#530L]
Results [6]: [window#532, event_type#485, repo_name#487, count#538L, sum#540L, count#543L]

(9) Exchange
Input [6]: [window#532, event_type#485, repo_name#487, count#538L, sum#540L, count#543L]
Arguments: hashpartitioning(window#532, event_type#485, repo_name#487, 200), ENSURE_REQUIREMENTS, [plan_id=978]

(10) HashAggregate
Input [6]: [window#532, event_type#485, repo_name#487, count#538L, sum#540L, count#543L]
Keys [3]: [window#532, event_type#485, repo_name#487]
Functions [3]: [count(1), sum(CASE WHEN public#472 THEN 1 ELSE 0 END), count(distinct actor_login#488)]
Aggregate Attributes [3]: [count(1)#529L, sum(CASE WHEN public#472 THEN 1 ELSE 0 END)#531L, count(actor_login#488)#530L]
Results [7]: [window#532.start AS window_start#533, window#532.end AS window_end#534, event_type#485, repo_name#487, count(1)#529L AS event_count#520L, count(actor_login#488)#530L AS unique_actors#521L, sum(CASE WHEN public#472 THEN 1 ELSE 0 END)#531L AS public_events#522L]

(11) AdaptiveSparkPlan
Output [7]: [window_start#533, window_end#534, event_type#485, repo_name#487, event_count#520L, unique_actors#521L, public_events#522L]
Arguments: isFinalPlan=false


2.1 Accès au Spark UI pour capture des preuves¶

Pendant l'exécution du job, consultez le Spark UI pour capturer les métriques:

  • Spark Driver UI: http://localhost:4040
  • Jobs vidéo: Section "Jobs" - Affiche les étapes d'exécution
  • Stages: Plans d'exécution détaillés par étape
  • Executors: Métriques CPU, mémoire par exécuteur
  • Storage: Données en cache et en mémoire

Capture recommandée:

  1. Onglet "SQL" → Affichage du plan d'exécution complet
  2. Onglet "Jobs" → Timing et stades d'exécution
  3. Onglet "Executors" → Utilisation des ressources
  4. Prendre des screenshots avant de fermer la session Spark

3. Écriture du flux vers Parquet¶

On exécute les requêtes d'agrégation et on sauvegarde les résultats:

Configuration du sink (destination):

  • Format: Parquet (colonnaire, compressé)
  • Mode: append (ajouter les résultats existants)
  • Checkpoint: répertoire pour récupération en cas de panne

Séquence:

  1. Exécuter l'agrégation de base sur le GitHub Archive
  2. Persister les résultats en Parquet
  3. Plus loin: lancer l'agrégation optimisée et comparer les plans
In [23]:
baseline_sink, baseline_checkpoint = OUTPUTS_DIR / "stream_sink_baseline", OUTPUTS_DIR / "checkpoint_baseline"
baseline_checkpoint.mkdir(parents=True, exist_ok=True)
df_agg_baseline.coalesce(1).write.mode("overwrite").parquet(str(baseline_sink))
baseline_output = spark.read.parquet(str(baseline_sink))
print(f"Base: {baseline_output.count()} windows | Sink: {baseline_sink}")
baseline_output.orderBy("window_start").show(10, truncate=False)
                                                                                
Base: 2 windows | Sink: /home/sable/Documents/E4FD/S4/Data Engineering/Data Engineering 2/lab1 assignment/outputs/lab1/stream_sink_baseline
+-------------------+-------------------+-----------+---------------------+-----------+-------------+-------------+
|window_start       |window_end         |event_type |repo_name            |event_count|unique_actors|public_events|
+-------------------+-------------------+-----------+---------------------+-----------+-------------+-------------+
|2026-04-27 12:00:00|2026-04-27 13:00:00|PushEvent  |tensorflow/tensorflow|1          |1            |1            |
|2026-04-27 12:00:00|2026-04-27 13:00:00|IssuesEvent|kubernetes/kubernetes|1          |1            |0            |
+-------------------+-------------------+-----------+---------------------+-----------+-------------+-------------+

4. Suivi et capture des preuves (Version de base)¶

On collecte les preuves d'exécution pour la version de base:

Étapes:

  1. Relire les fichiers Parquet générés
  2. Afficher le nombre de lignes et le schéma
  3. Afficher le plan d'exécution (explain formatted)
  4. Sauvegarder le plan dans un fichier texte (preuve pour le rapport)
  5. Afficher les statistiques clés des agrégations
In [ ]:
# Spark UI link for proof capture
from IPython.display import HTML, display
spark_ui_html = """
<div style="background-color: #f0f8ff; padding: 15px; border-radius: 8px; border-left: 4px solid #4169e1;">
    <h3 style="margin-top: 0; color: #1e3a8a;">🔍 Spark UI - Capturer les preuves</h3>
    <p><strong>Pendant l'exécution, accédez au Spark UI:</strong></p>
    <p style="font-size: 16px;">
        <a href="http://localhost:4040" target="_blank" style="background-color: #4169e1; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; font-weight: bold;">
            ▶ Spark UI (http://localhost:4040)
        </a>
    </p>
    <p><strong>Onglets à consulter:</strong></p>
    <ul>
        <li><strong>SQL</strong>: Plans d'exécution détaillés</li>
        <li><strong>Jobs</strong>: Timeline et stades</li>
        <li><strong>Stages</strong>: Shuffle, I/O, CPU temps</li>
        <li><strong>Executors</strong>: Utilisation mémoire/CPU</li>
    </ul>
</div>
"""
display(HTML(spark_ui_html))
In [24]:
print("BASE: Details d'exécution")
df_baseline_output = spark.read.parquet(str(baseline_sink))
print(f"Sortie: {df_baseline_output.count()} lignes")
df_baseline_output.explain("formatted")
baseline_plan_file = PROOF_DIR / "plan_baseline.txt"
with open(baseline_plan_file, "w") as f:
    f.write("PLAN DE REQUÊTE DE BASE\n" + "=" * 80 + "\n")
    buf = io.StringIO()
    with redirect_stdout(buf):
        df_baseline_output.explain("formatted")
    f.write(buf.getvalue())
print(f"Plan: {baseline_plan_file}")
df_baseline_output.orderBy("window_start").show(10)
BASE: Details d'exécution
Sortie: 2 lignes
== Physical Plan ==
* ColumnarToRow (2)
+- Scan parquet  (1)


(1) Scan parquet 
Output [7]: [window_start#279, window_end#280, event_type#281, repo_name#282, event_count#283L, unique_actors#284L, public_events#285L]
Batched: true
Location: InMemoryFileIndex [file:/home/sable/Documents/E4FD/S4/Data Engineering/Data Engineering 2/lab1 assignment/outputs/lab1/stream_sink_baseline]
ReadSchema: struct<window_start:timestamp,window_end:timestamp,event_type:string,repo_name:string,event_count:bigint,unique_actors:bigint,public_events:bigint>

(2) ColumnarToRow [codegen id : 1]
Input [7]: [window_start#279, window_end#280, event_type#281, repo_name#282, event_count#283L, unique_actors#284L, public_events#285L]


Plan: /home/sable/Documents/E4FD/S4/Data Engineering/Data Engineering 2/lab1 assignment/proof/plan_baseline.txt
+-------------------+-------------------+-----------+--------------------+-----------+-------------+-------------+
|       window_start|         window_end| event_type|           repo_name|event_count|unique_actors|public_events|
+-------------------+-------------------+-----------+--------------------+-----------+-------------+-------------+
|2026-04-27 12:00:00|2026-04-27 13:00:00|  PushEvent|tensorflow/tensor...|          1|            1|            1|
|2026-04-27 12:00:00|2026-04-27 13:00:00|IssuesEvent|kubernetes/kubern...|          1|            1|            0|
+-------------------+-------------------+-----------+--------------------+-----------+-------------+-------------+

5. Optimisation et re-mesure (Version optimisée)¶

Pour la version optimisée, on applique une stratégie de repartitionnement:

Optimisation appliquée:

  • Repartitionner par team_id AVANT l'agrégation par fenêtre
  • Cela colocalise les données d'une même équipe sur les mêmes partitions
  • Réduit les coûts de shuffle lors du groupBy
  • Impact sur la performance: moins de données à échanger entre exécuteurs

Comparaison:

  • De base: pas de repartitionnement, plus de shuffle
  • Optimisée: repartitionnement explicite sur 4 partitions, moins de shuffle

On capturera les mêmes métriques pour comparer.

In [25]:
# --- VERSION OPTIMISÉE: AVEC REPARTITIONNEMENT ---

# Répertoire de checkpoint pour cette version optimisée
optimized_checkpoint = OUTPUTS_DIR / "checkpoint_optimized"
optimized_checkpoint.mkdir(parents=True, exist_ok=True)

# Utiliser les mêmes données GitHub (df_events) que la version de base
df_stream_opt = (df_events
    .withWatermark(EVENT_TIME_COL, WATERMARK_DELAY))

# OPTIMISATION: Repartitionner par event_type + repo_name AVANT l'agrégation fenêtre
# Cela colocalise les données d'un même type d'événement et repository sur les mêmes partitions
# et réduit le coût du shuffle lors du groupBy
df_stream_opt_repartitioned = df_stream_opt.repartition(4, F.col("event_type"), F.col("repo_name"))

# Agrégation par fenêtre OPTIMISÉE (sur données repartitionnées)
df_agg_opt = (df_stream_opt_repartitioned
    .groupBy(
        F.window(F.col(EVENT_TIME_COL), WINDOW_DURATION),
        F.col("event_type"),
        F.col("repo_name")
    )
    .agg(
        F.count("*").alias("event_count"),
        F.countDistinct("actor_login").alias("unique_actors"),
        F.sum(F.when(F.col("public"), 1).otherwise(0)).alias("public_events"),
    )
    .select(
        F.col("window.start").alias("window_start"),
        F.col("window.end").alias("window_end"),
        F.col("event_type"),
        F.col("repo_name"),
        F.col("event_count"),
        F.col("unique_actors"),
        F.col("public_events"),
    ))
optimized_sink = OUTPUTS_DIR / "stream_sink_optimized"
df_agg_opt.explain("formatted")
df_agg_opt.coalesce(1).write.mode("overwrite").parquet(str(optimized_sink))
print(f"Optimized sink: {optimized_sink}")
== Physical Plan ==
AdaptiveSparkPlan (10)
+- HashAggregate (9)
   +- HashAggregate (8)
      +- HashAggregate (7)
         +- HashAggregate (6)
            +- Project (5)
               +- Exchange (4)
                  +- Project (3)
                     +- Filter (2)
                        +- Scan ExistingRDD (1)


(1) Scan ExistingRDD
Output [6]: [id#152, type#153, created_at#154, public#155, repo#156, actor#157]
Arguments: [id#152, type#153, created_at#154, public#155, repo#156, actor#157], MapPartitionsRDD[4] at applySchemaToPythonRDD at NativeMethodAccessorImpl.java:0, ExistingRDD, UnknownPartitioning(0)

(2) Filter
Input [6]: [id#152, type#153, created_at#154, public#155, repo#156, actor#157]
Condition : isnotnull(cast(created_at#154 as timestamp))

(3) Project
Output [5]: [type#153 AS event_type#168, cast(created_at#154 as timestamp) AS event_timestamp#169, public#155, repo#156.name AS repo_name#170, actor#157.login AS actor_login#171]
Input [6]: [id#152, type#153, created_at#154, public#155, repo#156, actor#157]

(4) Exchange
Input [5]: [event_type#168, event_timestamp#169, public#155, repo_name#170, actor_login#171]
Arguments: hashpartitioning(event_type#168, repo_name#170, 4), REPARTITION_BY_NUM, [plan_id=514]

(5) Project
Output [5]: [named_struct(start, knownnullable(precisetimestampconversion(((precisetimestampconversion(event_timestamp#169, TimestampType, LongType) - CASE WHEN (((precisetimestampconversion(event_timestamp#169, TimestampType, LongType) - 0) % 3600000000) < 0) THEN (((precisetimestampconversion(event_timestamp#169, TimestampType, LongType) - 0) % 3600000000) + 3600000000) ELSE ((precisetimestampconversion(event_timestamp#169, TimestampType, LongType) - 0) % 3600000000) END) - 0), LongType, TimestampType)), end, knownnullable(precisetimestampconversion((((precisetimestampconversion(event_timestamp#169, TimestampType, LongType) - CASE WHEN (((precisetimestampconversion(event_timestamp#169, TimestampType, LongType) - 0) % 3600000000) < 0) THEN (((precisetimestampconversion(event_timestamp#169, TimestampType, LongType) - 0) % 3600000000) + 3600000000) ELSE ((precisetimestampconversion(event_timestamp#169, TimestampType, LongType) - 0) % 3600000000) END) - 0) + 3600000000), LongType, TimestampType))) AS window#332, event_type#168, public#155, repo_name#170, actor_login#171]
Input [5]: [event_type#168, event_timestamp#169, public#155, repo_name#170, actor_login#171]

(6) HashAggregate
Input [5]: [window#332, event_type#168, public#155, repo_name#170, actor_login#171]
Keys [4]: [window#332, event_type#168, repo_name#170, actor_login#171]
Functions [2]: [partial_count(1), partial_sum(CASE WHEN public#155 THEN 1 ELSE 0 END)]
Aggregate Attributes [2]: [count(1)#329L, sum(CASE WHEN public#155 THEN 1 ELSE 0 END)#331L]
Results [6]: [window#332, event_type#168, repo_name#170, actor_login#171, count#338L, sum#340L]

(7) HashAggregate
Input [6]: [window#332, event_type#168, repo_name#170, actor_login#171, count#338L, sum#340L]
Keys [4]: [window#332, event_type#168, repo_name#170, actor_login#171]
Functions [2]: [merge_count(1), merge_sum(CASE WHEN public#155 THEN 1 ELSE 0 END)]
Aggregate Attributes [2]: [count(1)#329L, sum(CASE WHEN public#155 THEN 1 ELSE 0 END)#331L]
Results [6]: [window#332, event_type#168, repo_name#170, actor_login#171, count#338L, sum#340L]

(8) HashAggregate
Input [6]: [window#332, event_type#168, repo_name#170, actor_login#171, count#338L, sum#340L]
Keys [3]: [window#332, event_type#168, repo_name#170]
Functions [3]: [merge_count(1), merge_sum(CASE WHEN public#155 THEN 1 ELSE 0 END), partial_count(distinct actor_login#171)]
Aggregate Attributes [3]: [count(1)#329L, sum(CASE WHEN public#155 THEN 1 ELSE 0 END)#331L, count(actor_login#171)#330L]
Results [6]: [window#332, event_type#168, repo_name#170, count#338L, sum#340L, count#343L]

(9) HashAggregate
Input [6]: [window#332, event_type#168, repo_name#170, count#338L, sum#340L, count#343L]
Keys [3]: [window#332, event_type#168, repo_name#170]
Functions [3]: [count(1), sum(CASE WHEN public#155 THEN 1 ELSE 0 END), count(distinct actor_login#171)]
Aggregate Attributes [3]: [count(1)#329L, sum(CASE WHEN public#155 THEN 1 ELSE 0 END)#331L, count(actor_login#171)#330L]
Results [7]: [window#332.start AS window_start#333, window#332.end AS window_end#334, event_type#168, repo_name#170, count(1)#329L AS event_count#320L, count(actor_login#171)#330L AS unique_actors#321L, sum(CASE WHEN public#155 THEN 1 ELSE 0 END)#331L AS public_events#322L]

(10) AdaptiveSparkPlan
Output [7]: [window_start#333, window_end#334, event_type#168, repo_name#170, event_count#320L, unique_actors#321L, public_events#322L]
Arguments: isFinalPlan=false


Optimized sink: /home/sable/Documents/E4FD/S4/Data Engineering/Data Engineering 2/lab1 assignment/outputs/lab1/stream_sink_optimized

6. Remplir le journal des métriques¶

In [27]:
print("OPTIMIZED: Details")
df_optimized_output = spark.read.parquet(str(optimized_sink))
df_optimized_output.explain("formatted")
optimized_plan_file = PROOF_DIR / "plan_optimized.txt"
with open(optimized_plan_file, "w") as f:
    f.write("PLAN DE REQUÊTE OPTIMISÉE (avec repartitionnement)\n" + "=" * 80 + "\n")
    buf = io.StringIO()
    with redirect_stdout(buf):
        df_optimized_output.explain("formatted")
    f.write(buf.getvalue())
df_optimized_output.orderBy("window_start").show(10)
metrics_data = {
    "version": ["base", "optimized"],
    "optimization": ["No repartitioning", "Repartition by event_type + repo_name"],
    "output_rows": [df_baseline_output.count(), df_optimized_output.count()],
    "avg_event_count": [df_baseline_output.agg(F.avg("event_count")).collect()[0][0], df_optimized_output.agg(F.avg("event_count")).collect()[0][0]],
    "avg_unique_actors": [df_baseline_output.agg(F.avg("unique_actors")).collect()[0][0], df_optimized_output.agg(F.avg("unique_actors")).collect()[0][0]],
}
df_metrics, metrics_csv = pd.DataFrame(metrics_data), BASE_DIR / "lab1_metrics_log.csv"
df_metrics.to_csv(metrics_csv, index=False)
OPTIMIZED: Details
== Physical Plan ==
* ColumnarToRow (2)
+- Scan parquet  (1)


(1) Scan parquet 
Output [7]: [window_start#349, window_end#350, event_type#351, repo_name#352, event_count#353L, unique_actors#354L, public_events#355L]
Batched: true
Location: InMemoryFileIndex [file:/home/sable/Documents/E4FD/S4/Data Engineering/Data Engineering 2/lab1 assignment/outputs/lab1/stream_sink_optimized]
ReadSchema: struct<window_start:timestamp,window_end:timestamp,event_type:string,repo_name:string,event_count:bigint,unique_actors:bigint,public_events:bigint>

(2) ColumnarToRow [codegen id : 1]
Input [7]: [window_start#349, window_end#350, event_type#351, repo_name#352, event_count#353L, unique_actors#354L, public_events#355L]


+-------------------+-------------------+-----------+--------------------+-----------+-------------+-------------+
|       window_start|         window_end| event_type|           repo_name|event_count|unique_actors|public_events|
+-------------------+-------------------+-----------+--------------------+-----------+-------------+-------------+
|2026-04-27 12:00:00|2026-04-27 13:00:00|  PushEvent|tensorflow/tensor...|          1|            1|            1|
|2026-04-27 12:00:00|2026-04-27 13:00:00|IssuesEvent|kubernetes/kubern...|          1|            1|            0|
+-------------------+-------------------+-----------+--------------------+-----------+-------------+-------------+

7. Nettoyage¶

In [28]:
# Engineering note & GENAI declaration
eng_note = BASE_DIR / "ENGINEERING_NOTE.md"
with open(eng_note, "w") as f:
    f.write("""# DE2 Lab 1: Streaming Pipeline - GitHub Archive Track B

## Objective
Implement and optimize a Spark streaming pipeline on GitHub Archive (Track B) with window aggregation, watermark management, and Parquet persistence with baseline vs optimized comparison.

## Architecture
**Baseline**: No repartitioning, Parquet sink baseline_sink
**Optimized**: Repartition by event_type + repo_name (4 partitions) before aggregation

## Data: GitHub Archive (real public events)
- Window: 1 hour | Watermark: 15 minutes
- Aggregations: count(*), countDistinct(actor_login), sum(public events)
- Outputs: plan_baseline.txt, plan_optimized.txt, lab1_metrics_log.csv
""")

genai_file = BASE_DIR / "GENAI.md"
with open(genai_file, "w") as f:
    f.write("# GENAI.md\\nNo generative AI used. All code manually written and tested.\\nData source: GitHub Archive real public events.\\n")

print(f"Completed: {eng_note}, {genai_file}")
spark.stop()
Completed: /home/sable/Documents/E4FD/S4/Data Engineering/Data Engineering 2/lab1 assignment/ENGINEERING_NOTE.md, /home/sable/Documents/E4FD/S4/Data Engineering/Data Engineering 2/lab1 assignment/GENAI.md