<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Loko Labs]]></title><description><![CDATA[Crash tests d'outils data, DataOps industriel et architecture durable. Des verdicts honnêtes basés sur des tests réels, pas du marketing.]]></description><link>https://lokolabs.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/68b76b1986cb88ebbc317df2/d067358f-2def-4736-b4e0-c55e8bb27750.png</url><title>Loko Labs</title><link>https://lokolabs.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 17 Sep 2026 09:09:16 GMT</lastBuildDate><atom:link href="https://lokolabs.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Ma stack data locale en 2026 et pourquoi chaque outil a gagné sa place]]></title><description><![CDATA[Le principe de base : reproduire la prod en local
La question que je me pose avant d'ajouter un outil : est-ce que ce setup me permettrait de bosser sur un vrai pipeline de prod sans rien réapprendre ]]></description><link>https://lokolabs.hashnode.dev/ma-stack-data-locale-en-2026-et-pourquoi-chaque-outil-a-gagne-sa-place</link><guid isPermaLink="true">https://lokolabs.hashnode.dev/ma-stack-data-locale-en-2026-et-pourquoi-chaque-outil-a-gagne-sa-place</guid><category><![CDATA[duckDB]]></category><category><![CDATA[stack]]></category><category><![CDATA[dbt-core]]></category><category><![CDATA[dbt]]></category><category><![CDATA[Prefect]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Sèmèvo SALAKO]]></dc:creator><pubDate>Wed, 25 Mar 2026 09:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68b76b1986cb88ebbc317df2/337bdc03-8546-4647-aecd-283abcc4261b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<h2>Le principe de base : reproduire la prod en local</h2>
<p>La question que je me pose avant d'ajouter un outil : <em><strong>est-ce que ce setup me permettrait de bosser sur un vrai pipeline de prod sans rien réapprendre ?</strong></em></p>
<p>Ce n'est pas du perfectionnisme. C'est du pragmatisme. Quand tu arrives en stage ou en poste, la douleur vient rarement du code, elle vient de l'environnement. Docker qui se comporte différemment en local, des dépendances qui divergent, un pipeline qui "marche sur ma machine". Autant l'éviter dès le départ.</p>
<hr />
<h2>DuckDB : la base de données qui n'a pas besoin de serveur</h2>
<p><strong>Ce que c'est</strong> : un moteur OLAP embarqué, qui tourne directement dans le process Python. Pas de daemon, pas de connexion réseau, pas de configuration.</p>
<p><strong>Pourquoi il est là</strong> : J'ai découvert DuckDB en cherchant une alternative à Pandas sur des gros volumes. Mais ce qui m'a convaincu de l'intégrer dans mon stack local, c'est la friction quasi-nulle. <code>pip install duckdb</code>, et tu requêtes du Parquet, du CSV, ou de la mémoire en SQL. C'est ça.</p>
<pre><code class="language-python">import duckdb
conn = duckdb.connect()
result = conn.execute(
        """ SELECT
                pickup_location_id,
                COUNT(*) AS nb_courses,
                AVG(fare_amount) AS tarif_moyen
            FROM 'data/yellow_taxi_2023.parquet'
            GROUP BY 1
            ORDER BY 2 DESC
            LIMIT 10
        """
).df()
</code></pre>
<p><strong>Ce qu'il remplace</strong> : Pandas pour les explorations sur des fichiers &gt; 1M lignes. SQLite pour les petites bases locales. Un Postgres local pour les développements légers.</p>
<p><strong>Limite honnête</strong> : DuckDB n'est pas fait pour de la donnée transactionnelle. Et sur des jointures massives avec spill sur disque, il peut surprendre. Mais pour du local analytique, il fait le job mieux que tout le reste.</p>
<hr />
<h2>dbt-core : la transformation comme du vrai code</h2>
<p><strong>Ce que c'est</strong> : un framework de transformation SQL, avec gestion de dépendances, tests, documentation et lineage.</p>
<p><strong>Pourquoi il est là</strong> : Parce qu'un pipeline de transformation qui vit dans un notebook n'existe pas vraiment. Il ne peut pas être revu, testé, versionné proprement, ou repris par quelqu'un d'autre. dbt force une structure : des modèles SQL, un DAG de dépendances, des tests sur les données.</p>
<pre><code class="language-python">models/
├── staging/ 
        └── stg_taxi_trips.sql
└── marts/
        └── mart_revenue_by_zone.sql
</code></pre>
<pre><code class="language-sql">-- models/marts/mart_revenue_by_zone.sql
SELECT
    pickup_location_id,
    DATE_TRUNC('month', pickup_datetime) AS mois,
    COUNT(*) AS nb_courses,
    SUM(total_amount) AS revenu_total
FROM {{ ref('stg_taxi_trips') }}
GROUP BY 1, 2
</code></pre>
<p><strong>Ce qu'il apporte concrètement</strong> : La commande <code>dbt test</code> qui plante avant que les données corrompues remontent en prod. Le <code>dbt docs generate</code> qui produit une documentation vivante. Et surtout, la discipline de traiter une transformation SQL comme un artefact de code — pas comme un bout de requête oublié dans un coin.</p>
<p><strong>Avec DuckDB</strong> : dbt-core + dbt-duckdb, c'est le combo local parfait. Aucun serveur, tout en fichiers, reproductible en 30 secondes sur une nouvelle machine.</p>
<hr />
<h2>Prefect — l'orchestration sans la douleur d'Airflow</h2>
<p><strong>Ce que c'est</strong> : un orchestrateur de workflows Python. Tu décores tes fonctions, tu définis un flow, tu l'exécutes localement ou tu le déploies.</p>
<p><strong>Pourquoi il est là</strong> : Airflow est puissant. Airflow est aussi un enfer à installer localement. Prefect a fait un choix différent : le code d'abord, l'infrastructure ensuite.</p>
<pre><code class="language-python">from prefect import flow, task

@task
def extraire_donnees(source: str) -&gt; pd.DataFrame: 
    return pd.read_parquet(source)

@task
def charger_donnees(df: pd.DataFrame, destination: str):
    df.to_parquet(destination)

@flow(name="pipeline-taxi")
def pipeline(source: str, destination: str):
    df = extraire_donnees(source)
    charger_donnees(df, destination)

if name == "main":
    pipeline(source="data/raw/", destination="data/processed/")
</code></pre>
<p><strong>Ce qu'il apporte</strong> : La visibilité sur les runs, la gestion des retries, les logs structurés — sans avoir à déployer un cluster. En local, <code>prefect server start</code> lance une UI complète en une commande.</p>
<p><strong>Limite honnête</strong> : Si ton pipeline tient en un script Python de 50 lignes, Prefect est overkill. Je l'utilise dès que j'ai des dépendances entre tâches, des retries nécessaires, ou que je veux tracer l'exécution.</p>
<hr />
<h2>Docker : la frontière entre "ça marche" et "ça marchera"</h2>
<p><strong>Ce que c'est</strong> : la containerisation. Tu packages ton environnement, et il tourne pareil partout.</p>
<p><strong>Pourquoi il est là</strong> : Je ne l'utilise pas pour chaque script. Je l'utilise pour tout ce qui a vocation à sortir de mon laptop : un pipeline Prefect à déployer, un service qui expose une API, un benchmark reproductible.</p>
<pre><code class="language-dockerfile">FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
CMD ["python", "pipeline.py"]
</code></pre>
<p><strong>Ce qu'il règle</strong> : Le fameux <strong>ça marche chez moi</strong>. Si ton pipeline tourne dans un container, il tourne en CI, en prod, sur la machine d'un collègue. La reproductibilité n'est plus un espoir, c'est une propriété.</p>
<hr />
<h2>Ce qui n'est pas dans la stack et pourquoi</h2>
<p><strong>Jupyter Notebooks</strong> : Je les utilise encore pour l'exploration initiale. Mais dès que quelque chose doit être reproductible ou partagé, ça sort du notebook et ça entre dans la stack.</p>
<p><strong>Spark en local</strong> : J'y viendrai. Mais Spark local demande des ressources, une config, et a une courbe d'apprentissage. DuckDB couvre 95% de mes besoins analytiques locaux — Spark, c'est pour les volumes où DuckDB atteint ses limites (&gt; 100M lignes avec des transformations complexes).</p>
<p><strong>Airflow</strong> : Prefect fait le job en local. Si un projet exige Airflow, j'apprends Airflow sur ce projet.</p>
<hr />
<h2>Le schéma complet</h2>
<pre><code class="language-plaintext">Données brutes (CSV, Parquet, JSON)
                ↓
DuckDB (requêtage, exploration)
                ↓
dbt-core (transformations SQL, tests)
                ↓
Prefect (orchestration, scheduling, retries)
                ↓
Docker (packaging, reproductibilité)
                ↓
CI/CD (GitHub Actions — article à venir)
</code></pre>
<hr />
<h2>Ce que cette stack m'a appris</h2>
<p>Le bon environnement local n'est pas celui qui a le plus d'outils. C'est celui qui crée le moins de friction entre une idée et un pipeline qui tourne. Chaque outil dans cette stack a une raison d'être précise, et aucun ne se chevauche avec un autre.</p>
<p>La prochaine étape logique : ajouter GitHub Actions pour que chaque push déclenche les tests dbt et valide le pipeline. Ce sera peut-être le prochain article pour la data, sans les hacks.</p>
]]></content:encoded></item><item><title><![CDATA[dbt + DuckDB : monte ton entrepôt local de zéro]]></title><description><![CDATA[Pourquoi DuckDB comme backend dbt ?
dbt a besoin d'un moteur SQL pour exécuter les transformations. En général c'est Snowflake, BigQuery, Redshift. Ces outils sont excellents en prod — mais lourds à c]]></description><link>https://lokolabs.hashnode.dev/dbt-duckdb-monte-ton-entrepot-local-de-zero</link><guid isPermaLink="true">https://lokolabs.hashnode.dev/dbt-duckdb-monte-ton-entrepot-local-de-zero</guid><category><![CDATA[dataops]]></category><category><![CDATA[dbt]]></category><category><![CDATA[duckDB]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Sèmèvo SALAKO]]></dc:creator><pubDate>Mon, 16 Mar 2026 17:41:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68b76b1986cb88ebbc317df2/4589f8b4-d2e4-42bc-810a-0ee746f26490.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<h2>Pourquoi DuckDB comme backend dbt ?</h2>
<p>dbt a besoin d'un moteur SQL pour exécuter les transformations. En général c'est Snowflake, BigQuery, Redshift. Ces outils sont excellents en prod — mais lourds à configurer pour du dev local.</p>
<p>DuckDB, c'est un moteur SQL analytique embarqué. Il tourne dans un fichier <code>.duckdb</code> sur ton laptop. Pas de daemon, pas de port à ouvrir, pas de credentials à gérer.</p>
<p>Ce qu'on gagne concrètement :</p>
<ul>
<li><p><strong>Itération rapide</strong> : pas de round-trip réseau, les requêtes répondent en millisecondes</p>
</li>
<li><p><strong>Environnement reproductible</strong> : le fichier <code>.duckdb</code> est portable</p>
</li>
<li><p><strong>Zéro coût</strong> : pas de compute cloud qui tourne pendant que tu développes</p>
</li>
<li><p><strong>Idéal pour apprendre</strong> : tu te concentres sur dbt, pas sur la config infra</p>
</li>
</ul>
<hr />
<h2>Ce qu'on va construire</h2>
<p>Un projet dbt complet branché sur DuckDB, avec un vrai dataset e-commerce Olist (~100k lignes) :</p>
<pre><code class="language-shell">seeds (CSV Olist)
    └── models/staging (4 vues — nettoyage, typage)
            └── models/marts (1 table — agrégation métier)
</code></pre>
<hr />
<h2>Prérequis</h2>
<ul>
<li><p><strong>Python 3.11 ou 3.12</strong> — pas 3.13+, pas 3.14 (voir ci-dessous)</p>
</li>
<li><p><code>pip</code></p>
</li>
<li><p>Connaissance basique de dbt (<code>ref()</code>, <code>dbt run</code>)</p>
</li>
</ul>
<hr />
<h2>⚠️ Le crash qu'on a eu - Python 3.14</h2>
<p>Avant de commencer, un point important.</p>
<p>Si tu as Python 3.14 installé sur ta machine, <code>dbt --version</code> va planter avec cette erreur :</p>
<pre><code class="language-shell">TypeError: Metaclasses with custom tp_new are not supported.
</code></pre>
<p>Ce n'est pas un bug dbt — c'est une incompatibilité entre Python 3.14 (encore en beta) et <code>protobuf</code>, une dépendance interne de dbt. Le fix : <strong>utiliser Python 3.11</strong>.</p>
<pre><code class="language-shell"># Vérifie ta version
python --version

# Si tu as 3.14, télécharge Python 3.11.9 sur python.org
# Puis crée ton venv en ciblant explicitement 3.11
py -3.11 -m venv .venv
</code></pre>
<p>On a perdu du temps là-dessus. C'est documenté ici pour que tu n'en perdes pas.</p>
<hr />
<h2>Étape 1 — Setup de l'environnement</h2>
<pre><code class="language-shell">mkdir dbt-duckdb-demo &amp;&amp; cd dbt-duckdb-demo
py -3.11 -m venv .venv
.venv\Scripts\activate

pip install "dbt-core==1.8.9" "dbt-duckdb==1.8.4"
dbt --version
</code></pre>
<p>Output attendu :</p>
<pre><code class="language-shell">Core:
  - installed: 1.8.9
Plugins:
  - duckdb: 1.8.4
</code></pre>
<hr />
<h2>Étape 2 — Initialiser le projet</h2>
<pre><code class="language-shell">dbt init dbt_duckdb_demo
cd dbt-duckdb-demo
</code></pre>
<p>dbt te demande quel adaptateur utiliser — choisis <strong>duckdb</strong>.</p>
<p>Le <code>profiles.yml</code> est créé automatiquement dans <code>~/.dbt/</code>. Il ressemble à ça :</p>
<pre><code class="language-yaml">dbt_duckdb_demo:
  outputs:
    dev:
      type: duckdb
      path: dev.duckdb
      threads: 1
    prod:
      type: duckdb
      path: prod.duckdb
      threads: 4
  target: dev
</code></pre>
<p>Trois lignes qui remplacent un bloc de credentials Snowflake. Vérifie la connexion :</p>
<pre><code class="language-powershell">dbt debug
</code></pre>
<p><code>Connection test: OK</code> — on continue.</p>
<hr />
<h2>Étape 3 — Le dataset Olist</h2>
<p>On utilise le <strong>Brazilian E-Commerce Dataset</strong> d'Olist, disponible sur Kaggle. Un vrai dataset de production : commandes, clients, paiements d'un e-commerce brésilien.</p>
<p>🔗 <a href="https://www.kaggle.com/datasets/olistbr/brazilian-ecommerce">kaggle.com/datasets/olistbr/brazilian-ecommerce</a></p>
<p>On garde 4 fichiers sur les 9 disponibles :</p>
<table>
<thead>
<tr>
<th>Fichier</th>
<th>Lignes</th>
</tr>
</thead>
<tbody><tr>
<td><code>olist_customers_dataset.csv</code></td>
<td>99 441</td>
</tr>
<tr>
<td><code>olist_orders_dataset.csv</code></td>
<td>99 441</td>
</tr>
<tr>
<td><code>olist_order_items_dataset.csv</code></td>
<td>112 650</td>
</tr>
<tr>
<td><code>olist_order_payments_dataset.csv</code></td>
<td>103 886</td>
</tr>
</tbody></table>
<p>Copie-les dans <code>seeds/</code>, puis supprime les modèles d'exemple :</p>
<pre><code class="language-shell">Remove-Item -Recurse -Force models\example
mkdir models\staging
mkdir models\marts
</code></pre>
<p>Charge les seeds :</p>
<pre><code class="language-shell">dbt seed
</code></pre>
<pre><code class="language-shell">1 of 4 OK loaded seed file main.olist_customers_dataset    [INSERT 99441 in 1.07s]
2 of 4 OK loaded seed file main.olist_order_items_dataset  [INSERT 112650 in 3.51s]
3 of 4 OK loaded seed file main.olist_order_payments_dataset [INSERT 103886 in 1.88s]
4 of 4 OK loaded seed file main.olist_orders_dataset       [INSERT 99441 in 6.14s]

Finished running 4 seeds in 12.83s.
</code></pre>
<p>415 418 lignes chargées. On a de quoi travailler.</p>
<hr />
<h2>Étape 4 — Les modèles staging</h2>
<p>Le staging nettoie et type les données brutes. Une source = un modèle staging.</p>
<p><code>models/staging/stg_customers.sql</code></p>
<pre><code class="language-sql">with customers as (
    select * from {{ ref('olist_customers_dataset') }}
)

select
    customer_id,
    customer_unique_id,
    customer_zip_code_prefix    as zip_code,
    customer_city               as city,
    customer_state              as state
from customers
where customer_id is not null
</code></pre>
<p><code>models/staging/stg_orders.sql</code></p>
<pre><code class="language-sql">with orders as (
    select * from {{ ref('olist_orders_dataset') }}
)

select
    order_id,
    customer_id,
    order_status,
    order_purchase_timestamp::timestamp         as purchase_at,
    order_approved_at::timestamp                as approved_at,
    order_delivered_carrier_date::timestamp     as delivered_carrier_date,
    order_delivered_customer_date::timestamp    as delivered_customer_date,
    order_estimated_delivery_date::timestamp    as estimated_delivery_date
from orders
where order_id is not null
</code></pre>
<p><code>models/staging/stg_order_items.sql</code></p>
<pre><code class="language-sql">with order_items as (
    select * from {{ ref('olist_order_items_dataset') }}
)

select
    order_id,
    order_item_id,
    product_id,
    seller_id,
    price::decimal(10,2)            as price,
    freight_value::decimal(10,2)    as freight_value
from order_items
where order_id is not null
</code></pre>
<p><code>models/staging/stg_order_payments.sql</code></p>
<pre><code class="language-sql">with stg_order_payments as (
    select * from {{ ref('olist_order_payments_dataset') }}
)

select
    order_id,
    payment_sequential,
    payment_type,
    payment_installments,
    cast(payment_value as decimal(10,2))    as amount
from stg_order_payments
where order_id is not null
</code></pre>
<p>Lance le staging complet :</p>
<pre><code class="language-shell">dbt run --select staging
</code></pre>
<pre><code class="language-shell">Finished running 4 view models in 0.60s.
Completed successfully
</code></pre>
<p><strong>4 modèles. 0.60 secondes. Zéro cloud.</strong></p>
<hr />
<h2>Étape 5 — Le modèle mart</h2>
<p>Le mart agrège les données staging pour répondre à une question métier : <em>quel est le comportement d'achat par client ?</em></p>
<p><code>models/marts/mart_orders_summary.sql</code></p>
<pre><code class="language-sql">with stg_customers as (
    select * from {{ ref('stg_customers') }}
),

stg_orders as (
    select * from {{ ref('stg_orders') }}
),

stg_order_payments as (
    select * from {{ ref('stg_order_payments') }}
)

select
    c.customer_id,
    c.customer_unique_id,
    c.zip_code,
    c.city,
    c.state,
    count(distinct o.order_id)                                          as total_orders,
    sum(p.amount)                                                       as total_amount,
    sum(
        case when o.order_status = 'delivered'
        then p.amount else 0 end
    )                                                                   as delivered_amount,
    round(
        count(distinct case when o.order_status = 'delivered'
              then o.order_id end) * 100.0
        / nullif(count(distinct o.order_id), 0)
    )                                                                   as delivery_rate_pct
from stg_customers c
left join stg_orders o on c.customer_id = o.customer_id
left join stg_order_payments p on o.order_id = p.order_id
group by c.customer_id, c.customer_unique_id, c.zip_code, c.city, c.state
</code></pre>
<pre><code class="language-shell">dbt run --select mart_orders_summary
</code></pre>
<pre><code class="language-shell">Finished running 1 table model in 7.18s.
Completed successfully
</code></pre>
<p>7 secondes pour joindre et agréger 3 tables sur ~100k lignes chacune. Sur un laptop, sans cloud.</p>
<hr />
<h2>Étape 6 — Les tests</h2>
<p>Crée <code>models/staging/schema.yml</code> :</p>
<pre><code class="language-yaml">version: 2

models:
  - name: stg_customers
    columns:
      - name: customer_id
        tests:
          - unique
          - not_null

  - name: stg_orders
    columns:
      - name: order_id
        tests:
          - unique
          - not_null
      - name: order_status
        tests:
          - accepted_values:
              values: ['delivered', 'shipped', 'canceled', 'unavailable',
                       'invoiced', 'processing', 'created', 'approved']

  - name: stg_order_items
    columns:
      - name: order_id
        tests:
          - not_null

  - name: stg_order_payments
    columns:
      - name: order_id
        tests:
          - not_null
      - name: amount
        tests:
          - not_null
</code></pre>
<pre><code class="language-shell">dbt test
</code></pre>
<pre><code class="language-shell">Finished running 7 tests in 1.23s.
Completed successfully — PASS=7 WARN=0 ERROR=0
</code></pre>
<hr />
<h2>Étape 7 — Le lineage</h2>
<pre><code class="language-powershell">dbt docs generate
dbt docs serve
</code></pre>
<p>dbt ouvre un navigateur avec le DAG complet du projet :</p>
<img src="https://cdn.hashnode.com/uploads/covers/68b76b1986cb88ebbc317df2/4182f245-4b29-410f-9c07-e8af09f5ff19.png" alt="Lineage Graph — seeds → staging → mart" style="display:block;margin:0 auto" />

<p>Le lineage te montre visuellement que chaque modèle ne dépend que de ce qu'il doit dépendre. C'est la traçabilité de ta transformation, documentée automatiquement.</p>
<hr />
<h2>Ce qu'on a construit</h2>
<blockquote>
<p>Note : l'architecture complète Medallion inclut une couche intermediate entre staging et marts. On l'a volontairement omise ici pour garder le projet lisible. Elle fera l'objet d'un article dédié.</p>
</blockquote>
<table>
<thead>
<tr>
<th>Étape</th>
<th>Résultat</th>
<th>Temps</th>
</tr>
</thead>
<tbody><tr>
<td><code>dbt seed</code></td>
<td>415 418 lignes chargées</td>
<td>12.83s</td>
</tr>
<tr>
<td><code>dbt run --select staging</code></td>
<td>4 vues créées</td>
<td>0.60s</td>
</tr>
<tr>
<td><code>dbt run --select mart_orders_summary</code></td>
<td>1 table, 3 joins sur 100k lignes</td>
<td>7.18s</td>
</tr>
<tr>
<td><code>dbt test</code></td>
<td>7 tests passés</td>
<td>1.23s</td>
</tr>
</tbody></table>
<hr />
<h2>Ce que DuckDB ne remplace pas</h2>
<p>DuckDB est excellent pour le dev local et les volumes jusqu'à quelques dizaines de millions de lignes. Ce n'est pas un entrepôt cloud donc pas de multi-utilisateurs, pas de contrôle d'accès granulaire, pas de haute disponibilité.</p>
<p>Pour de la prod partagée, tu brancheras dbt sur Snowflake ou BigQuery. Les modèles qu'on vient d'écrire fonctionneront sans modification — c'est exactement l'intérêt de cette stack.</p>
<hr />
<h2>Prochaine étape</h2>
<p>Tu as une stack dbt + DuckDB qui tourne en local avec de vraies données. L'étape suivante : <strong>automatiser tout ça avec GitHub Actions</strong> pour que chaque modification de modèle soit validée et testée automatiquement avant de merger.</p>
<p>C'est l'article #004.</p>
<hr />
<p><em>Solide comme un Loko.</em></p>
]]></content:encoded></item><item><title><![CDATA[DuckDB vs Pandas : j'ai testé les deux sur 54 millions de lignes]]></title><description><![CDATA[Le contexte
Pandas est l'outil par défaut de tout Data Engineer Python. Fiable, connu, documenté. Mais il a une limite fondamentale : il charge tout le dataset en RAM. Chaque opération commence par ma]]></description><link>https://lokolabs.hashnode.dev/duckdb-vs-pandas-tested-on-54-millions-lines</link><guid isPermaLink="true">https://lokolabs.hashnode.dev/duckdb-vs-pandas-tested-on-54-millions-lines</guid><category><![CDATA[duckDB]]></category><category><![CDATA[pandas]]></category><category><![CDATA[Python]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[SQL]]></category><category><![CDATA[dataops]]></category><category><![CDATA[Benchmark]]></category><dc:creator><![CDATA[Sèmèvo SALAKO]]></dc:creator><pubDate>Fri, 13 Mar 2026 08:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68b76b1986cb88ebbc317df2/49a4e440-e541-4cf0-80f3-34761aa221db.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<h2>Le contexte</h2>
<p>Pandas est l'outil par défaut de tout Data Engineer Python. Fiable, connu, documenté. Mais il a une limite fondamentale : <strong>il charge tout le dataset en RAM</strong>. Chaque opération commence par matérialiser l'intégralité des données en mémoire.</p>
<p>DuckDB prend le contre-pied total. Il scanne les fichiers Parquet directement sur disque, colonne par colonne, sans jamais tout charger. C'est ce qu'on appelle un <strong>moteur de requêtes en place</strong>.</p>
<p>J'ai voulu mesurer la différence de façon rigoureuse, pas juste lire la doc. Voici les résultats bruts.</p>
<h2>Le setup</h2>
<p><strong>Dataset :</strong> NYC Yellow Taxi Trip Data (Kaggle)</p>
<p><strong>2 tailles testées :</strong></p>
<ul>
<li><p><strong>Small :</strong> 10,9M lignes - 229 MB Parquet</p>
</li>
<li><p><strong>Medium :</strong> 54M lignes - 1138 MB Parquet</p>
</li>
</ul>
<p><strong>7 opérations testées :</strong> Nous avons testés 7 opérations, lesquelles sont les suivantes :</p>
<ul>
<li><p>Chargement</p>
</li>
<li><p>COUNT(*)</p>
</li>
<li><p>GROUP BY simple &amp; GROUP BY multi</p>
</li>
<li><p>Filtre + agrégation</p>
</li>
<li><p>Window function</p>
</li>
<li><p>JOIN</p>
</li>
</ul>
<p><strong>Métriques capturées :</strong> Notre analyse s'est basé sur 3 indicateurs clés à savoir :</p>
<ul>
<li><p>le Temps d'exécution</p>
</li>
<li><p>la RAM consommée</p>
</li>
<li><p>et la consommation en terme de CPU</p>
</li>
</ul>
<p><strong>Environnement :</strong> <code>Python 3.11 · DuckDB 1.x · Pandas 2.x · 15 Go RAM</code></p>
<hr />
<h2>Les Résultats</h2>
<ol>
<li>Sur le <strong>dataset</strong> Small de 10,9M de lignes</li>
</ol>
<table>
<thead>
<tr>
<th>Opération</th>
<th>Pandas</th>
<th>DuckDB</th>
<th>Ratio</th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Chargement</strong></td>
<td>2.528s / +3334 MB</td>
<td>0.013s / +2MB</td>
<td>194x DuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>COUNT(*)</strong></td>
<td>0.002s</td>
<td>0.002s</td>
<td>1x</td>
<td>égalité</td>
</tr>
<tr>
<td><strong>GROUP BY simple</strong></td>
<td>0.282s</td>
<td>0.053s</td>
<td>5.3x DuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>GROUP BY multiple</strong></td>
<td>0.704s</td>
<td>0.047s</td>
<td>15x DuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>Filtre + aggrégation</strong></td>
<td>1.152s</td>
<td>0.061s</td>
<td>18.9x DuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>Window function</strong></td>
<td>9.373s</td>
<td>6.734s</td>
<td>1.4xDuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>JOIN</strong></td>
<td>ERREUR</td>
<td>ERREUR</td>
<td>-</td>
<td></td>
</tr>
</tbody></table>
<div>
<div>💡</div>
<div><em>Le chiffre le plus frappant : Pandas charge 229 MB de Parquet → 3,3 Go en RAM. DuckDB charge le même fichier en 0,013s et consomme 2 MB. Ratio : 194x.</em></div>
</div>

<ol>
<li>Sur le dataset <strong>Medium</strong> de 54 M de lignes</li>
</ol>
<table>
<thead>
<tr>
<th><strong>Operation</strong></th>
<th><strong>Pandas</strong></th>
<th><strong>DuckDB</strong></th>
<th><strong>Ratio</strong></th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Chargement</strong></td>
<td>101.6s / +300 MB</td>
<td>0.041s / +3 MB</td>
<td>2478x DuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>COUNT(*)</strong></td>
<td>0.006s</td>
<td>0.008s</td>
<td>0.75x Pandas</td>
<td>🐼</td>
</tr>
<tr>
<td><strong>GROUP BY simple</strong></td>
<td>11.1s</td>
<td>0.365s</td>
<td>30x DuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>GROUP BY multi</strong></td>
<td>13.4s</td>
<td>0.404s</td>
<td>33x DuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>Filtre + agg</strong></td>
<td>76.8s / +3167 MB</td>
<td>0.496s / +7 MB</td>
<td>154x DuckDB</td>
<td>🦆</td>
</tr>
<tr>
<td><strong>Window function</strong></td>
<td>88.5s</td>
<td>ERREUR disk</td>
<td>—</td>
<td></td>
</tr>
<tr>
<td><strong>JOIN</strong></td>
<td>ERREUR</td>
<td>ERREUR</td>
<td>—</td>
<td></td>
</tr>
</tbody></table>
<div>
<div>💡</div>
<div><em>À 54M lignes, un simple filtre + agrégation prend 76 secondes avec Pandas. DuckDB fait la même chose en 0,5 seconde. 154x plus rapide.</em></div>
</div>

<hr />
<h2>Les crashs : des observations légitimes</h2>
<p>Deux opérations ont crashé des deux côtés. Ce n'est pas un échec de test. C'est le résultat le plus instructif.</p>
<details>
<summary><strong>JOIN - MemoryError sur les deux</strong></summary>
<p><strong>Pandas : </strong>tente de charger l'intégralité du DataFrame en RAM avant de merger. Sur 10M+ lignes, c'est systématiquement fatal.</p><p><strong>DuckDB : </strong>sature l'espace disque temporaire sur les très gros volumes. Il peut spiller sur disque, mais la partition /tmp de Kaggle est trop petite.</p><p><strong>Conclusion : </strong>pour un JOIN à grande échelle, ni l'un ni l'autre ne fonctionne sur une machine de développement standard. Il faut un cluster Spark ou Databricks avec suffisamment de ressources.</p><p></p>
</details><details>
<summary><strong>Window function DuckDB - ERREUR disk sur Medium</strong></summary>
<p>DuckDB a tenté de spiller (débordé) sur disque pour la window function sur 54M lignes, mais a manqué d'espace temporaire. C'est ce qui a entrainé l'erreur : <code>Out of Memory Error: failed to offload data block</code>.</p><p><strong>La correction :</strong> limiter la window function à un échantillon représentatif, ou augmenter le temp directory. Sur un environnement de production avec plus de disque, ce n'est pas un problème.</p><p></p>
</details>

<hr />
<h2>Le bon pattern : DuckDB + Pandas ensemble</h2>
<p>DuckDB ne remplace pas Pandas, il le complète. Le bon workflow serait :</p>
<ol>
<li><p><strong>DuckDB pour l'agrégation lourde</strong> : lecture Parquet, filtres, GROUP BY, agrégations</p>
</li>
<li><p><strong>Pandas pour la manipulation fine</strong> : transformations Python custom, ML, visualisation</p>
</li>
<li><p><strong>DuckDB retourne directement un DataFrame Pandas</strong> via <code>.df()</code></p>
</li>
</ol>
<pre><code class="language-python"># Le pattern recommande
# DuckDB fait le travail lourd, Pandas reçoit un DataFrame propre

import duckdb

result = duckdb.sql("""
    SELECT passenger_count,
           AVG(fare_amount)  AS avg_fare,
           COUNT(*)          AS nb_trips
    FROM read_parquet('data/*.parquet')
    WHERE trip_distance &gt; 5
    GROUP BY passenger_count
    ORDER BY nb_trips DESC
""").df()  # &lt;- retourne un DataFrame Pandas

# Ensuite tu travailles normalement avec Pandas
print(result.head())
</code></pre>
<hr />
<h2>Le verdict</h2>
<table style="min-width:449px"><colgroup><col style="min-width:25px"></col><col style="width:424px"></col></colgroup><tbody><tr><td><p><strong>Volume</strong></p></td><td><p><strong>Recommandation</strong></p></td></tr><tr><td><p><strong>&lt; 1M lignes</strong></p></td><td><p>Pandas suffit, pas besoin de changer</p></td></tr><tr><td><p><strong>1M — 10M lignes</strong></p></td><td><p>DuckDB commence à gagner sur les agrégations</p></td></tr><tr><td><p><strong>&gt; 10M lignes</strong></p></td><td><p>DuckDB est recommandé, Pandas devient lent</p></td></tr><tr><td><p><strong>&gt; 50M lignes</strong></p></td><td><p>Pandas est inutilisable sur les opérations complexes</p></td></tr></tbody></table>

<div>
<div>💡</div>
<div><strong><em>PRODUCTION READY</em></strong><em> : DuckDB pour les </em><strong><em>datasets &gt; 10M</em></strong><em> lignes. Associé à Pandas pour la manipulation fine, c'est le </em><strong><em>duo gagnant en Data Engineering Python</em></strong><em>.</em></div>
</div>]]></content:encoded></item><item><title><![CDATA[Pourquoi j'ai choisi un fichier HTML plutôt que Next.js pour ma landing page]]></title><description><![CDATA[Il y a un anti-pattern que je vois partout dans le monde de la data : utiliser Apache Spark pour traiter 10 000 lignes de CSV. Le framework impressionne sur le papier, il est dans le CV, alors on l'ut]]></description><link>https://lokolabs.hashnode.dev/html-vs-nextjs-landing-page-data-engineer</link><guid isPermaLink="true">https://lokolabs.hashnode.dev/html-vs-nextjs-landing-page-data-engineer</guid><category><![CDATA[dataops]]></category><category><![CDATA[architecture]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Hashnode]]></category><category><![CDATA[GitHubPages]]></category><category><![CDATA[bonnes pratiques]]></category><dc:creator><![CDATA[Sèmèvo SALAKO]]></dc:creator><pubDate>Mon, 09 Mar 2026 09:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68b76b1986cb88ebbc317df2/29ee41a0-1a47-492c-801d-ce397a398e51.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>Il y a un anti-pattern que je vois partout dans le monde de la data : utiliser <strong>Apache Spark pour traiter 10 000 lignes de CSV</strong>. Le framework impressionne sur le papier, il est dans le CV, alors on l'utilise — même quand un simple script pandas ferait le travail en 2 secondes avec zéro infrastructure.</p>
<p>J'ai failli faire la même erreur en construisant ce blog. Réflexe de dev : <code>npx create-next-app@latest loko-labs</code>.</p>
<p>Un framework React, un pipeline de build, des dépendances à maintenir. Tout ça pour... afficher du texte et appeler une API.</p>
<p>J'ai pris du recul. Et j'ai appliqué à mon propre projet le même principe que je défends pour les pipelines de données : <strong>la bonne complexité pour le bon problème.</strong></p>
<hr />
<h3>Ce que la landing page devait faire</h3>
<p>Avant de choisir un outil, un ingénieur pose la question du besoin réel. Voici la spec complète :</p>
<ul>
<li><p>Présenter Loko Labs et son positionnement</p>
</li>
<li><p>Afficher les derniers articles publiés sur Hashnode</p>
</li>
<li><p>Être hébergée gratuitement</p>
</li>
<li><p>Se mettre à jour automatiquement à chaque publication</p>
</li>
<li><p>Zéro maintenance dans le temps</p>
</li>
</ul>
<div>
<div>💡</div>
<div><strong>VERDICT</strong> — C'est une page statique avec une seule source de données externe. La complexité d'un framework est injustifiée.</div>
</div>

<h3>L'arbitrage : Next.js vs HTML statique</h3>
<p>Voici les trade-offs concrets que j'ai évalués :</p>
<table>
<thead>
<tr>
<th>Critères</th>
<th>Next.js/Framework</th>
<th>HTML Statique</th>
</tr>
</thead>
<tbody><tr>
<td>Complexité</td>
<td>Build pipeline, webpack, 200+ packages</td>
<td><mark class="bg-yellow-200 dark:bg-yellow-500/30">Zéro, un seul fichier</mark></td>
</tr>
<tr>
<td>Dépendances</td>
<td>Vulnérabilités à surveiller en continu</td>
<td><mark class="bg-yellow-200 dark:bg-yellow-500/30">Aucune</mark></td>
</tr>
<tr>
<td>Déploiement</td>
<td>Vercel, CI/CD, variables d'environnement</td>
<td><mark class="bg-yellow-200 dark:bg-yellow-500/30">Github Pages, drag &amp; drop</mark></td>
</tr>
<tr>
<td>Maintenance</td>
<td>MàJ régulières de packages</td>
<td><mark class="bg-yellow-200 dark:bg-yellow-500/30">Aucune</mark></td>
</tr>
<tr>
<td>Performance</td>
<td>Bundle JS chargé avant le 1er pixel</td>
<td><mark class="bg-yellow-200 dark:bg-yellow-500/30">HTML rendu immédiatement</mark></td>
</tr>
<tr>
<td>Données</td>
<td>SSG ou ISR à configurer</td>
<td><mark class="bg-yellow-200 dark:bg-yellow-500/30">Fetch API, toujours frais</mark></td>
</tr>
<tr>
<td>Portabilité</td>
<td>Vendor lock-in Vercel/framework</td>
<td><mark class="bg-yellow-200 dark:bg-yellow-500/30">Fichier HTML portable partout</mark></td>
</tr>
</tbody></table>
<p>Next.js aurait été le bon choix avec un routing complexe ou du rendu serveur. Mais pour une landing page qui appelle une API ? C'est ce qu'on appelle du <strong>over-engineering</strong>.</p>
<h3>La stack retenue : simple, solide, portable</h3>
<p>Voici exactement ce que j'ai utilisé - et c'est très exactement uniquement ça :</p>
<pre><code class="language-shell">loko-labs/
    index.html    &lt;- tout le code (HTML, CSS, JS en un seul fichier)

# Hébergement : GitHub Pages (gratuit, illimité pour les repos publics)
# Données : API GraphQL Hashnode (appelée côté client au chargement de la page)
# Domaine :  à venir, mais un fichier CNAME dans le repo suffit à le gérer
</code></pre>
<h3>L'intégration Hashnode en 15 lignes</h3>
<p>L'API GraphQL de Hashnode est publique. On a pas besoin de clé API pour lire les articles :</p>
<pre><code class="language-javascript">const query = `
    query {
        publication(host: "votreusername.hashnode.dev") {
            posts(first: 6) {
                edges {
                  node { title brief slug url publishedAt tags { name } }
                }
            }
        }
    }
`;

const res = await fetch('https://gql.hashnode.com/', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query })
});

const { data } = await res.json();
// -&gt; articles frais a chaque visite, sans redéploiement
</code></pre>
<p><strong>Résultat :</strong> à chaque visite, les articles sont <strong>toujours à jour</strong> sans aucun redéploiement. Pas de webhook, pas de GitHub Action, juste un <strong>fetch</strong>.</p>
<hr />
<h3>Le vrai sujet : l'ingénierie, c'est choisir</h3>
<p>Dans le monde de la data, on souffre d'un biais systématique vers la complexité. On déploie Kubernetes parce que Netflix le fait. On adopte des frameworks parce qu'ils sont populaires, pas parce qu'ils résolvent notre problème.</p>
<p>Un bon ingénieur data ne se demande pas <em>"quel outil impressionnant puis-je utiliser ?"</em> Il se demande <strong>"quel est le minimum viable pour que ça tienne en production ?"</strong></p>
<div>
<div>⚡</div>
<div><strong>PRINCIPE LOKO LABS </strong>— La complexité est un coût. Chaque dépendance, chaque service supplémentaire est une surface de panne potentielle. Sois capable de justifier chaque couche de ta stack.</div>
</div>

<p>Ce principe s'applique exactement à un pipeline de données :</p>
<ul>
<li><p><strong>Airflow</strong> pour un job cron quotidien ? → Un simple script Python suffit.</p>
</li>
<li><p><strong>Kafka</strong> pour 100 événements/jour ? → Une table PostgreSQL avec polling suffit.</p>
</li>
<li><p><strong>dbt</strong> pour 3 transformations simples ? → Des vues SQL directement dans la base suffisent.</p>
</li>
<li><p><strong>Next.js</strong> pour une landing page statique ? → Un fichier HTML suffit.</p>
</li>
</ul>
<blockquote>
<p>La bonne stack n'est pas celle qui impressionne — c'est celle qui <strong>résiste dans le temps</strong> avec le moins d'efforts de maintenance possible.</p>
</blockquote>
<hr />
<h3>Et la suite ?</h3>
<p>Cette landing page va évoluer avec Loko Labs :</p>
<ul>
<li><p>Domaine custom <code>lokolabs.dev</code> → un fichier <code>CNAME</code> dans le repo</p>
</li>
<li><p>Compteur d'articles dynamique dans le hero</p>
</li>
<li><p>Score Lighthouse target : <strong>100/100</strong></p>
</li>
</ul>
<div>
<div>💡</div>
<div><strong>NOTE</strong> — Quand le blog grandira et que le besoin de pages multiples ou de SEO avancé se fera sentir, là je migrerai vers Astro ou Next.js. Pas avant. C'est ça, l'ingénierie.</div>
</div>

<hr />
<h3>En résumé</h3>
<blockquote>
<p><strong>La complexité est une dette.</strong></p>
</blockquote>
<p>Chaque framework, chaque service que tu ajoutes est une obligation de maintenance future. La vraie expertise d'un ingénieur, ce n'est pas de savoir utiliser les outils complexes — c'est de savoir <strong>quand ne pas les utiliser.</strong></p>
<p>La landing page de Loko Labs a été construit avec un fichier HTML, l'API Hashnode, et GitHub Pages. Ça tourne, c'est gratuit, ça ne tombera pas en panne à 2h du matin. <strong>Solide comme un Loko.</strong></p>
<hr />
<p>Tags : #DataOps #Architecture #WebDev #Hashnode #GitHubPages #BonnesPratiques</p>
]]></content:encoded></item></channel></rss>