Travaux pratiques - Visualisation de données volumineuses

Références externes utiles :

Objectif du TP

Ce TP aborde un problème concret rencontré dès que l’on travaille sur des données massives : comment visualiser plusieurs millions d’observations ? Un scatterplot naïf sur 3 millions de points produit une image illisible ; calculer un histogramme ou une heatmap avec Pandas sur un tel volume est lent et consommateur de mémoire.

Nous mettons en place une chaîne de traitement améliorée pour ce contexte :

  1. PySpark : chargement et préparation des données distribuées ;

  2. sparkpl : conversion de Spark vers Polars avec Apache Arrow, sans passer par Pandas ;

  3. Polars : filtrage, agrégation et transformations rapides en mémoire ;

  4. Altair : visualisations déclaratives (avec backend natif Polars) ;

  5. hvPlot + Datashader : rendu de millions de points sans overplotting.

Les données utilisées sont les trajets de taxis jaunes new-yorkais (NYC TLC, janvier 2024) : un fichier Parquet public d’environ 50 Mo contenant ~2,9 millions de courses.

Note

Choix des outils : pourquoi cette chaîne ?

La pratique courante dans les notebooks Spark consiste à appeler .toPandas() pour passer à la visualisation. Cette approche a deux inconvénients : elle introduit une dépendance à Pandas (lourd, mémoire moins bien gérée) et peut être lente sur de grands volumes. sparkpl utilise la conversion Arrow native de Spark (format colonnes en mémoire) pour produire directement un DataFrame Polars, plus rapide et sans surcoût mémoire intermédiaire. Polars est lui-même écrit en Rust avec un moteur d’exécution (locale) parallèle : les agrégations et filtres sont plus rapides que leurs équivalents Pandas sur des volumes similaires.

Altair (v5+) supporte Polars nativement sans conversion. C’est le backend de visualisation par défaut de Polars depuis la v 1.6. hvPlot accepte également Polars (conversion Arrow interne) et, couplé à Datashader, résout le problème de l”overplotting en agrégeant les points en pixels avant le rendu, ce qui permet d’afficher des millions de points en quelques secondes dans un notebook.

Partie 1 : Mise en place et chargement des données

Installation des bibliothèques

(vérifie d’abord si elles sont déjà présentes)

import sys
!{sys.executable} -m pip install sparkpl==2.0.1 polars altair hvplot datashader

Initialisation de Spark

import os
os.environ['PYSPARK_PYTHON'] = sys.executable
os.environ['PYSPARK_DRIVER_PYTHON'] = sys.executable
from pyspark.sql import SparkSession

spark = (SparkSession.builder
         .appName("tpVisualisationTaxisNYC")
         .config("spark.sql.execution.arrow.pyspark.enabled", "true")
         .getOrCreate())

Note

L’option spark.sql.execution.arrow.pyspark.enabled active le transfert Arrow entre Spark et Python. C’est ce qui permet à sparkpl d’éviter la sérialisation ligne par ligne et de produire un DataFrame Polars en quelques secondes plutôt qu’en plusieurs minutes.

Téléchargement du fichier Parquet

Le site NYC TLC publie les données mensuellement. Nous utilisons d’abord les courses de janvier 2024, soit ~2,9 millions de courses.

import os

dossier    = "tpVisualisationDonnees/nyc_taxi/"
fichier_pq = dossier + "yellow_tripdata_2024-01.parquet"
url        = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet"

os.makedirs(dossier, exist_ok=True)
if not os.path.exists(fichier_pq):
    print("Téléchargement en cours...")
    rc = os.system(f"wget -q -nc {url} -P {dossier}")
    print("OK" if rc == 0 else "Erreur de téléchargement")
else:
    print("Fichier déjà présent.")

Chargement dans Spark

sdf = spark.read.parquet(fichier_pq)
print(f"Lignes   : {sdf.count():,}")
print(f"Colonnes : {len(sdf.columns)}")
print(f"Schéma DataFrame :")
sdf.printSchema()

Parmi les colonnes on peut mentionner :

  • tpep_pickup_datetime, tpep_dropoff_datetime : horodatages de début et fin de course ;

  • passenger_count : nombre de passagers ;

  • trip_distance : distance en miles ;

  • PULocationID, DOLocationID : identifiants de zone TLC de départ et d’arrivée (265 zones couvrant les 5 boroughs de NYC) ;

  • payment_type : mode de paiement (1 = carte, 2 = espèces, …) ;

  • fare_amount, tip_amount, total_amount : montants en dollars.

Question

Affichez les 25 premières lignes avec sdf.show(25, truncate=False). Repérez les colonnes contenant des valeurs manquantes ou nulles.

Préparation dans Spark

Avant de transférer les données vers Polars, on effectue le nettoyage et les transformations coûteuses côté Spark, là où le calcul est distribué.

from pyspark.sql import functions as F

sdf_clean = (sdf
    # Garder uniquement les courses valides
    .filter(F.col("trip_distance")  >  0)
    .filter(F.col("trip_distance")  <= 100)          # éliminer les outliers
    .filter(F.col("fare_amount")    >  0)
    .filter(F.col("fare_amount")    <= 500)
    .filter(F.col("passenger_count").between(1, 6))
    .filter(F.col("tpep_pickup_datetime").isNotNull())
    # Dériver quelques colonnes utiles
    .withColumn("pickup_hour",
                F.hour("tpep_pickup_datetime"))
    .withColumn("pickup_dow",                         # jour de semaine (1 = Dimanche ?)
                F.dayofweek("tpep_pickup_datetime"))
    .withColumn("duration_min",
                (F.unix_timestamp("tpep_dropoff_datetime") -
                 F.unix_timestamp("tpep_pickup_datetime")) / 60)
    .withColumn("speed_mph",
                F.col("trip_distance") / (F.col("duration_min") / 60))
    .filter(F.col("duration_min") > 0)
    .filter(F.col("speed_mph") <= 80)                # vitesse physiquement plausible
    .select("pickup_hour", "pickup_dow", "passenger_count",
            "trip_distance", "fare_amount", "tip_amount", "total_amount",
            "duration_min", "speed_mph",
            "PULocationID", "DOLocationID", "payment_type")
)

print(f"Lignes après nettoyage : {sdf_clean.count():,}")

Question

Quel pourcentage de lignes ont été éliminées par le nettoyage ? Ces suppressions vous semblent-elles justifiées ?

Conversion de Spark vers Polars avec sparkpl

Note technique

A partir de sparkpl v2.0.0 (qui exige pyspark v > 4.0.0…), la conversion de Spark à Polars est directe : spark_to_polars appelle sdf_clean.toArrow() pour obtenir un pyarrow.Table en mémoire partagée, puis polars.from_arrow() pour construire le DataFrame Polars. Aucune copie de données n’a lieu, les deux représentations pointent vers le même buffer mémoire. C’est ce qu’on appelle une conversion zero-copy.

Mesurons le temps d’exécution de la conversion vers Polars et ensuite le temps de la conversion de Spark vers Pandas :

from sparkpl.converter import spark_to_polars
import time

sdf_clean.cache()
sdf_clean.count()   # déclenche le calcul et remplit le cache

t0 = time.time()
pdf2 = sdf_clean.toPandas()
print(f"Spark to Pandas : {time.time()-t0:.2f} s")

t0 = time.time()
pdf = spark_to_polars(sdf_clean)
print(f"Spark to Polars : {time.time()-t0:.2f} s")

sdf_clean.count() sert à charger le DataFrame Spark en mémoire avant de faire la conversion, sinon le temps de lecture depuis le disque risque d’être dominant dans les résultats.

Partie 2 : Le problème overplotting

Avant d’utiliser des outils spécialisés, illustrons d’abord le problème que pose la visualisation directe de millions de points.

import matplotlib.pyplot as plt

# Scatterplot direct : fare_amount vs trip_distance
# ATTENTION : cette cellule peut être lente et produire une image illisible
sample = pdf.sample(fraction=0.05, seed=42)   # 5 % = ~135 000 points
plt.figure(figsize=(7, 5))
plt.scatter(sample["trip_distance"].to_numpy(),
            sample["fare_amount"].to_numpy(),
            alpha=0.05, s=1, color="steelblue")
plt.xlabel("Distance (miles)")
plt.ylabel("Tarif (USD)")
plt.title(f"Scatterplot Matplotlib — {len(sample):,} points (5 % du corpus)")
plt.tight_layout()
plt.show()

Question

Que constatez-vous ? L’image vous semble-t-elle lisible ? Que se passerait-il si l’on utilisait la totalité des 2,7 millions de points ? Réessayez avec fraction=1.0 et observez le temps de rendu.

Partie 3 : Visualisation avec Altair

Altair adopte une approche déclarative : on décrit ce qu’on souhaite visualiser (quelles colonnes, quels encodages visuels) sans décrire comment le le dessiner. La bibliothèque génère une spécification Vega-Lite (JSON) que le navigateur interprète pour produire un graphique SVG interactif.

import altair as alt

# Altair accepte directement un DataFrame Polars (sans conversion)
alt.data_transformers.enable("default", max_rows=None)

Note

Altair a une limite en nombre de lignes, c’est pourquoi nous utilisons sample (les données sont embarquées en JSON dans le notebook). Pour les agrégations (histogrammes, heatmaps), on ne dépasse jamais cette limite car on travaille sur des résumés, pas sur les données brutes. Pour les scatterplots sur données brutes on doit échantillonner explicitement.

Distribution horaire des courses

chart_heures = (
    alt.Chart(sample)
    .mark_bar()
    .encode(
        x=alt.X("pickup_hour:O",
                title="Heure de départ",
                axis=alt.Axis(labelAngle=0)),
        y=alt.Y("count():Q",
                title="Nombre de courses"),
        color=alt.Color("pickup_hour:O",
                        scale=alt.Scale(scheme="blues"),
                        legend=None),
        tooltip=["pickup_hour:O", "count():Q"]
    )
    .properties(
        title="Distribution horaire des courses (janvier 2024)",
        width=600, height=300
    )
    .interactive()
)
chart_heures

Question

Identifiez les heures creuses et les heures de pointe. Y a-t-il un creux nocturne marqué ? Ces résultats correspondent-ils à votre intuition ?

Distribution par jour de la semaine

import polars as pl

jours = {1: "Dim", 2: "Lun", 3: "Mar", 4: "Mer",
         5: "Jeu", 6: "Ven", 7: "Sam"}

pdf_dow = sample.with_columns(
    pl.col("pickup_dow")
      .replace_strict(jours, return_dtype=pl.Utf8)
      .alias("jour")
)

chart_dow = (
    alt.Chart(pdf_dow)
    .mark_bar()
    .encode(
        x=alt.X("jour:O",
                sort=list(jours.values()),
                title="Jour de la semaine"),
        y=alt.Y("count():Q", title="Nombre de courses"),
        color=alt.Color("jour:O",
                        scale=alt.Scale(scheme="tableau10"),
                        legend=None),
        tooltip=["jour:O", "count():Q"]
    )
    .properties(title="Courses par jour de la semaine", width=500, height=280)
)
chart_dow

Question

Le volume de courses est-il uniforme sur la semaine ? Que constatez-vous pour le week-end ?

Heatmap heure × jour

import polars as pl

# Agrégation Polars : nombre de courses par (heure, jour)
agg_heure_jour = (
    pdf_dow
    .group_by(["pickup_hour", "jour"])
    .agg(pl.len().alias("nb_courses"))
)

chart_heatmap = (
    alt.Chart(agg_heure_jour)
    .mark_rect()
    .encode(
        x=alt.X("pickup_hour:O",
                title="Heure",
                axis=alt.Axis(labelAngle=0)),
        y=alt.Y("jour:O",
                sort=list(jours.values()),
                title="Jour"),
        color=alt.Color("nb_courses:Q",
                        scale=alt.Scale(scheme="orangered"),
                        title="Nb courses"),
        tooltip=["jour:O", "pickup_hour:O",
                 alt.Tooltip("nb_courses:Q", format=",")]
    )
    .properties(
        title="Activité par heure et jour de la semaine",
        width=600, height=250
    )
)
chart_heatmap

Question

Quelles sont les plages horaires les plus actives selon le jour de la semaine ? Les vendredis et samedis soir se distinguent-ils ?

Distribution des tarifs

# Histogramme de fare_amount (sur les données brutes, Altair calcule les bins)
chart_tarif = (
    alt.Chart(sample)
    .mark_bar(opacity=0.8)
    .encode(
        x=alt.X("fare_amount:Q",
                bin=alt.Bin(maxbins=60),
                title="Tarif de base (USD)"),
        y=alt.Y("count():Q", title="Nombre de courses"),
        tooltip=["count():Q"]
    )
    .properties(
        title="Distribution des tarifs — janvier 2024",
        width=600, height=280
    )
)
chart_tarif

Question

La distribution est-elle symétrique ? Que remarquez-vous autour des valeurs 3-4 USD, 18-20 USD et 65-70 USD ? Pouvez-vous expliquer ces pics ?

Tarif moyen selon le mode de paiement

import polars as pl

# Données initiales
df = pl.DataFrame({"statut": [1, 2, 3, 4]})

# Dictionnaire de correspondance
mapping = {1: "Actif", 2: "Inactif"}

# Remplacement des valeurs
df_res = df.with_columns(
    pl.col("statut")
      .replace_strict(mapping, default="Inconnu")
      .alias("statut_texte")
)

print(df_res)
modes_paiement = {1: "Carte", 2: "Espèces", 3: "Sans frais",
                  4: "Litige", 5: "Inconnu", 6: "Voyage annulé"}
pdf_pay = pdf.with_columns(
    pl.col("payment_type")
      .cast(pl.Int64)
      .replace_strict(modes_paiement)
      .alias("mode_paiement")
)

agg_pay = (
    pdf_pay
    .group_by("mode_paiement")
    .agg([
        pl.len().alias("nb_courses"),
        pl.col("fare_amount").mean().alias("tarif_moyen"),
        pl.col("tip_amount").mean().alias("pourboire_moyen"),
        pl.col("total_amount").mean().alias("total_moyen"),
    ])
    .sort("nb_courses", descending=True)
)

# pour voir les différentes valeurs passez la souris sur les barres
chart_pay = (
    alt.Chart(agg_pay)
    .mark_bar()
    .encode(
        x=alt.X("mode_paiement:N",
                sort="-y",
                title="Mode de paiement"),
        y=alt.Y("nb_courses:Q", title="Nombre de courses"),
        color=alt.Color("tarif_moyen:Q",
                        scale=alt.Scale(scheme="viridis"),
                        title="Tarif moyen (USD)"),
        tooltip=[
            "mode_paiement:N",
            alt.Tooltip("nb_courses:Q",    format=","),
            alt.Tooltip("tarif_moyen:Q",   format=".2f"),
            alt.Tooltip("pourboire_moyen:Q", format=".2f"),
        ]
    )
    .properties(
        title="Courses et tarif moyen par mode de paiement",
        width=500, height=300
    )
)
chart_pay

Question

Comparez le pourboire moyen pour les paiements par carte vs espèces. Qu’observez-vous ? Quelle en est l’explication probable ?

Graphique composé : tarif et pourboire par heure

Altair permet de combiner plusieurs graphiques avec des axes partagés (attention toutefois aux problèmes de lisibilité).

agg_heure = (
    pdf
    .group_by("pickup_hour")
    .agg([
        pl.col("fare_amount").mean().alias("tarif_moyen"),
        pl.col("tip_amount").mean().alias("pourboire_moyen"),
        pl.len().alias("nb_courses"),
    ])
    .sort("pickup_hour")
)

base = alt.Chart(agg_heure).encode(
    x=alt.X("pickup_hour:O",
            title="Heure de départ",
            axis=alt.Axis(labelAngle=0))
)

ligne_tarif = (base
    .mark_line(color="steelblue", strokeWidth=2)
    .encode(y=alt.Y("tarif_moyen:Q", title="USD (moyen)"),
            tooltip=["pickup_hour:O",
                     alt.Tooltip("tarif_moyen:Q", format=".2f")])
)
ligne_tip = (base
    .mark_line(color="darkorange", strokeDash=[4, 2], strokeWidth=2)
    .encode(y=alt.Y("pourboire_moyen:Q"),
            tooltip=["pickup_hour:O",
                     alt.Tooltip("pourboire_moyen:Q", format=".2f")])
)
barres_vol = (base
    .mark_bar(opacity=0.2, color="grey")
    .encode(y=alt.Y("nb_courses:Q", title="Nb courses"),
            tooltip=[alt.Tooltip("nb_courses:Q", format=",")])
)

chart_compose = (
    alt.layer(barres_vol, ligne_tarif, ligne_tip)
    .resolve_scale(y="independent")
    .properties(
        title="Tarif moyen (bleu), pourboire moyen (orange) "
              "et volume (gris) par heure",
        width=650, height=320
    )
)
chart_compose

Question

Le tarif moyen augmente-t-il aux heures de pointe ou aux heures creuses ? Comment l’expliquer (courses plus longues ? supplément nuit ? destinations plus lointaines ?) ?

Partie 4 : affichage de millions de points avec hvPlot et Datashader

Dans la section précédente nous avons visualisé des agrégations de données, Altair est parfaitement adaptée. Pour visualiser des données brutes à l’échelle de millions de points nous pouvons employer Datashader.

Principe de Datashader

Datashader ne dessine pas chaque point individuellement mais rasterise les données : il divise le plan en une grille de pixels, compte le nombre de points dans chaque pixel et applique une palette de couleurs à partir de ces valeurs. Le résultat est une image de résolution fixe (par ex. 800 × 600 pixels) quel que soit le nombre de points. Le rendu est alors :

  • rapide, car proportionnel à la résolution de l’image et non au nombre de points ;

  • plus interprétable, car la couleur de chaque pixel reflète la densité réelle dans cette zone.

Installation et imports

import hvplot.polars       # enregistre l'accesseur .hvplot sur Polars
import holoviews as hv
import datashader as ds
from holoviews.operation.datashader import datashade, dynspread
import panel as pn

hv.extension("bokeh")      # backend interactif dans Jupyter

Note

import hvplot.polars enregistre un « accesseur » .hvplot sur tous les DataFrames Polars de la session. En interne, hvPlot convertit les colonnes nécessaires en NumPy/Arrow avant de les passer à Bokeh ou Datashader. La conversion est paresseuse et ne porte que sur les colonnes du graphique.

Scatterplot avec Datashader : fare_amount vs trip_distance

# Datashader « rasterise » les 2,7 millions de points en une image 700 × 500
plot_scatter_ds = pdf.hvplot.scatter(
    x="trip_distance",
    y="fare_amount",
    datashade=True,               # active Datashader
    dynspread=True,               # élargit les points isolés pour la lisibilité
    cmap="fire",                  # palette (*fire* = du noir au blanc avec rouge/jaune)
    width=700, height=500,
    xlabel="Distance (miles)",
    ylabel="Tarif (USD)",
    title="Tarif vs distance pour 2,7 M courses (Datashader)"
)
plot_scatter_ds

Question

Comparez cette image au scatterplot Matplotlib de la partie 2. Que révèle Datashader que Matplotlib ne montrait pas ? Identifiez :

  • la relation linéaire principale (tarif ~ distance) ;

  • les bandes horizontales correspondant aux tarifs forfaitaires (JFK, Newark) ;

  • les concentrations de courses courtes et chères (Manhattan).

Densité par heure avec Datashader

# Distribution de la vitesse suivant l'heure de la journée
plot_speed = pdf.hvplot.scatter(
    x="pickup_hour",
    y="speed_mph",
    datashade=True,
    cmap="fire",
    width=700, height=400,
    xlabel="Heure de départ",
    ylabel="Vitesse moyenne (mph)",
    title="Vitesse des courses par heure de départ"
)
plot_speed

Question

À quelles heures les courses sont-elles les plus rapides ? Les plus lentes ? Cela correspond à ce que vous attendriez des embouteillages new-yorkais ?

Distribution 2D : durée vs distance

plot_duree_distance = pdf.hvplot.scatter(
    x="trip_distance",
    y="duration_min", ylim=(0, 1500),
    datashade=True,
    cmap="fire",
    width=700, height=500,
    xlabel="Distance (miles)",
    ylabel="Durée (minutes)",
    title="Durée vs distance pour toutes les courses"
)
plot_duree_distance

Histogramme 2D (heatmap de densité)

Pour des données continues, une heatmap de densité est souvent plus lisible qu’un scatterplot même avec Datashader.

plot_hexbin = pdf.hvplot.hexbin(
    x="trip_distance",
    y="fare_amount",
    gridsize=60,
    cmap="viridis",
    width=700, height=500,
    xlabel="Distance (miles)",
    ylabel="Tarif (USD)",
    title="Densité (hexbin) pour tarif vs distance"
)
plot_hexbin

Question

Comparez la heatmap hexagonale au scatterplot Datashader précédent. Quels sont les avantages et inconvénients de chaque représentation ?

Partie 5 : Agrégation de plusieurs mois dans Spark

La puissance de la chaîne Spark → sparkpl → Polars apparaît quand on travaille sur plusieurs mois de données (volume total de plusieurs dizaines de millions de lignes, ou plusieurs centaines de Mo).

# Téléchargement de 3 mois supplémentaires
mois = ["2024-02", "2024-03", "2024-04"]
for m in mois:
    f = f"{dossier}yellow_tripdata_{m}.parquet"
    if not os.path.exists(f):
        url_m = f"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_{m}.parquet"
        print(f"Téléchargement {m}...")
        os.system(f"wget -q -nc {url_m} -P {dossier}")
# Chargement de tous les fichiers disponibles en un seul DataFrame Spark
sdf_multi = spark.read.parquet(dossier + "yellow_tripdata_2024-*.parquet")
print(f"Total lignes : {sdf_multi.count():,}")
# Agrégation distribuée avec Spark : tarif moyen et volume par heure et mois
from pyspark.sql import functions as F

agg_spark = (
    sdf_multi
    .filter(F.col("fare_amount").between(0, 200))
    .filter(F.col("trip_distance").between(0, 50))
    .withColumn("mois",         F.month("tpep_pickup_datetime"))
    .withColumn("pickup_hour",  F.hour("tpep_pickup_datetime"))
    .groupBy("mois", "pickup_hour")
    .agg(
        F.count("*").alias("nb_courses"),
        F.mean("fare_amount").alias("tarif_moyen"),
        F.mean("trip_distance").alias("dist_moy"),
    )
    .orderBy("mois", "pickup_hour")
)

# Conversion du résumé (quelques centaines de lignes) vers Polars
pdf_multi = spark_to_polars(agg_spark)
print(pdf_multi)

Note

C’est ici qu’on peut voir l’importance de chaîne : Spark agrège des dizaines de millions de lignes distribuées en parallèle sur le cluster, et on ne transfère vers Polars que le résumé agrégé (quelques centaines de lignes ici). La visualisation avec Altair est alors instantanée.

pdf_multi_str = pdf_multi.with_columns(
    pl.col("mois").cast(pl.Utf8).alias("mois_str")
)

chart_multi = (
    alt.Chart(pdf_multi_str)
    .mark_line(point=True)
    .encode(
        x=alt.X("pickup_hour:O",
                title="Heure de départ",
                axis=alt.Axis(labelAngle=0)),
        y=alt.Y("nb_courses:Q", title="Nombre de courses"),
        color=alt.Color("mois_str:N",
                        title="Mois",
                        scale=alt.Scale(scheme="category10")),
        tooltip=["mois_str:N", "pickup_hour:O",
                 alt.Tooltip("nb_courses:Q", format=","),
                 alt.Tooltip("tarif_moyen:Q", format=".2f")]
    )
    .properties(
        title="Volume de courses par heure — comparaison mensuelle",
        width=650, height=320
    )
    .interactive()
)
chart_multi

Question

Les profils horaires sont-ils stables d’un mois à l’autre ? Observe-t-on des différences entre janvier (hiver) et avril (printemps) ?

Exercice

Construisez une heatmap Altair (axes : mois × heure, couleur : tarif_moyen) à partir de pdf_multi_str et discutez l’évolution du tarif moyen suivant l’heure et la période de l’année.

Synthèse

Dans ce TP vous avez évalué une chaîne complète pour la visualisation de données volumineuses dans un environnement Jupyter/Spark :

PySpark pour le chargement, le nettoyage et l’agrégation distribuée → sparkpl pour la conversion Arrow zero-copy vers Polars → Altair pour les visualisations déclaratives sur agrégats ou hvPlot + Datashader pour le rendu de données brutes à grande échelle sans overplotting.

Les points clés à retenir :

  • Le problème de l”overplotting est inévitable dès que l’on dépasse quelques milliers de points dans un scatterplot. Datashader le résout par rasterisation.

  • Effectuer les agrégations dans Spark avant de transférer les données vers Polars est la clé du passage à l’échelle car on ne transfère que ce qui est nécessaire à la visualisation.

  • sparkpl évite le détour par Pandas grâce à la conversion Arrow, plus rapide et sans copie de données.

  • Altair et hvPlot acceptent nativement les DataFrames Polars.

Références

NYC Taxi & Limousine Commission. TLC Trip Record Data. https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page

Boadzie, D. sparkpl: A lightweight, pandas-free Python package for seamless conversion between PySpark and Polars DataFrames. https://pypi.org/project/sparkpl/

HoloViz team. hvPlot: A high-level plotting API for data exploration. https://hvplot.holoviz.org/

Bednar, J. et al. Datashader: Accurately representing big data. https://datashader.org/

VanderPlas, J. and Granger, B. Altair: Interactive Statistical Visualizations for Python. Journal of Open Source Software, 3(32), 1057 (2018).