Skip to content
This repository was archived by the owner on Jun 25, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
default:
just --list

# ENV = {doc | dev | prod}
# ENV = {doc | data | dev | prod}
up ENV:
#!/bin/bash
if [ "{{ENV}}" = "doc" ]; then
Expand All @@ -10,11 +10,13 @@ up ENV:
docker compose -f compose.dev.yml up --detach
elif [ "{{ENV}}" = "prod" ]; then
docker compose -f compose.prod.yml up --detach --build
elif [ "{{ENV}}" = "data" ]; then
docker compose -f docker/{{ENV}}/compose.yml up --detach
else
echo "{{ENV}}: Accepted values are: 'doc|dev|prod'." >&2
echo "{{ENV}}: Accepted values are: 'doc|data|dev|prod'." >&2
fi

# ENV = {doc | dev | prod}
# ENV = {doc | data | dev | prod}
down ENV:
#!/bin/bash
if [ "{{ENV}}" = "doc" ]; then
Expand All @@ -23,11 +25,13 @@ down ENV:
docker compose -f compose.dev.yml down
elif [ "{{ENV}}" = "prod" ]; then
docker compose -f compose.prod.yml down
elif [ "{{ENV}}" = "data" ]; then
docker compose -f docker/{{ENV}}/compose.yml down
else
echo "{{ENV}}: Accepted values are: 'doc|dev|prod'." >&2
echo "{{ENV}}: Accepted values are: 'doc|data|dev|prod'." >&2
fi

# ENV = {doc | dev | prod}
# ENV = {doc | data | dev | prod}
logs ENV:
#!/bin/bash
if [ "{{ENV}}" = "doc" ]; then
Expand All @@ -36,8 +40,10 @@ logs ENV:
docker compose -f compose.dev.yml logs --follow
elif [ "{{ENV}}" = "prod" ]; then
docker compose -f compose.prod.yml logs --follow
elif [ "{{ENV}}" = "data" ]; then
docker compose -f docker/{{ENV}}/compose.yml logs --follow
else
echo "{{ENV}}: Accepted values are: 'doc|dev|prod'." >&2
echo "{{ENV}}: Accepted values are: 'doc|data|dev|prod'." >&2
fi


38 changes: 38 additions & 0 deletions docker/data/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
services:
# Stockage objet S3-compatible pour la zone bronze du datalake (dev local).
minio:
image: minio/minio:latest
container_name: homepedia-minio
command: server /data --console-address ":9001"
ports:
- "9000:9000" # API S3 (endpoint utilisé par le pipeline)
- "9001:9001" # Console web (http://localhost:9001)
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
volumes:
- minio-data:/data
restart: unless-stopped

# Crée le bucket bronze au démarrage (idempotent) puis se termine.
# Attend que MinIO réponde (boucle de retry) — pas besoin de `mc` en local.
minio-init:
image: minio/mc:latest
container_name: homepedia-minio-init
depends_on:
- minio
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
HOMEPEDIA_S3_BUCKET: ${HOMEPEDIA_S3_BUCKET:-homepedia}
entrypoint: >
/bin/sh -c "
until mc alias set local http://minio:9000 \"$$MINIO_ROOT_USER\" \"$$MINIO_ROOT_PASSWORD\" >/dev/null 2>&1; do
echo 'minio-init: en attente de MinIO...'; sleep 1;
done;
mc mb --ignore-existing \"local/$$HOMEPEDIA_S3_BUCKET\";
echo \"minio-init: bucket prêt -> $$HOMEPEDIA_S3_BUCKET\";
"

volumes:
minio-data:
86 changes: 86 additions & 0 deletions documentation/src/dataset_communes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Dataset : Communes de France 2025

**Source :** [data.gouv.fr](https://www.data.gouv.fr/datasets/communes-et-villes-de-france-en-csv-excel-json-parquet-et-feather/)
**Resource ID :** `f5df602b-3800-44d7-b2df-fa40a0350325`
**Format :** CSV (16.3 MB, UTF-8, séparateur `,`)
**Millésime :** 2025, basé sur la géographie au 1er janvier 2024
**Licence :** Licence Ouverte v2 (LO v2)

---

## Colonnes retenues

Sur les 46 colonnes disponibles, 18 ont été sélectionnées pour leur pertinence dans le contexte d'une plateforme d'analyse du marché immobilier français.

### Identification géographique

| Colonne | Type | Description |
|---|---|---|
| `code_insee` | `string` | Code commune assigné par l'INSEE. Identifiant principal de la commune, utilisé comme clé de jointure avec les données DVF (Demandes de Valeurs Foncières) et la majorité des jeux de données open data. Format : 5 caractères (ex : `75056` pour Paris, `69123` pour Lyon). |
| `code_postal` | `string` | Code postal principal de la commune. Utilisé pour la recherche par les utilisateurs. Une commune peut avoir plusieurs codes postaux (voir `codes_postaux`). |
| `codes_postaux` | `string` | Liste de tous les codes postaux rattachés à la commune, séparés par des virgules. Utile pour la recherche et la correspondance avec des données issues d'autres sources. |

### Noms de la commune

| Colonne | Type | Description |
|---|---|---|
| `nom_standard` | `string` | Nom normalisé de la commune, avec article si applicable (ex : `Le Havre`, `Les Sables-d'Olonne`). Utilisé pour l'affichage dans l'interface. |
| `nom_sans_accent` | `string` | Nom de la commune sans accents, caractères spéciaux ni espaces. Utilisé pour la recherche et l'autocomplétion insensible aux accents (ex : `le-havre`, `les-sables-dolonne`). |

### Hiérarchie administrative

| Colonne | Type | Description |
|---|---|---|
| `dep_code` | `string` | Code du département de la commune, assigné par l'INSEE (ex : `75`, `69`, `2A`). Utilisé pour les filtres et regroupements par département. |
| `dep_nom` | `string` | Nom du département (ex : `Paris`, `Rhône`, `Corse-du-Sud`). Affiché dans les filtres et les résultats de recherche. |
| `reg_code` | `string` | Code de la région de la commune, assigné par l'INSEE (ex : `11` pour Île-de-France). Utilisé pour les filtres et les visualisations à l'échelle régionale. |
| `reg_nom` | `string` | Nom de la région (ex : `Île-de-France`, `Auvergne-Rhône-Alpes`). Affiché dans les filtres et les regroupements géographiques. |
| `typecom` | `string` | Type de la commune en version abrégée. Valeurs possibles : `COM` (commune standard), `COMA` (commune associée), `COMD` (commune déléguée), `ARM` (arrondissement municipal, ex : Paris, Lyon, Marseille). Permet de distinguer les entités géographiques dans les visualisations. |

### Coordonnées géographiques

| Colonne | Type | Description |
|---|---|---|
| `latitude_centre` | `float` | Latitude du centroïde géographique du territoire communal (WGS84). Utilisé pour le positionnement des marqueurs et la clustering sur la carte MapLibre. |
| `longitude_centre` | `float` | Longitude du centroïde géographique du territoire communal (WGS84). Utilisé conjointement avec `latitude_centre` pour le positionnement cartographique. |

### Données démographiques et territoriales

| Colonne | Type | Description |
|---|---|---|
| `population` | `float` | Population municipale de la commune (recensement INSEE). Fournit le contexte du marché local : distinguer les petites communes rurales des grandes agglomérations. |
| `superficie_km2` | `float` | Superficie de la commune en kilomètres carrés. Permet de calculer ou de vérifier la densité de population. |
| `densite` | `float` | Densité de population en habitants par km². Pré-calculé par la source. Utile pour les overlays de carte et les analyses de marché par type de territoire. |

### Classification du territoire

| Colonne | Type | Description |
|---|---|---|
| `grille_densite` | `string` | Grille communale de densité à 7 niveaux selon la classification INSEE (ex : `1`, `2`, ..., `7`). |
| `grille_densite_texte` | `string` | Libellé de la grille de densité (ex : `Commune densément peuplée`, `Commune rurale peu dense`, `Bourg rural`). Permet de catégoriser les communes pour filtres et analyses dans l'interface. |

### Contexte économique et urbain (optionnel)

| Colonne | Type | Description |
|---|---|---|
| `epci_code` | `string` | Code de l'EPCI (Établissement Public de Coopération Intercommunale) auquel appartient la commune. Permet les regroupements à l'échelle des communautés de communes ou d'agglomération. |
| `epci_nom` | `string` | Nom de l'EPCI (ex : `Métropole du Grand Paris`, `Communauté de communes du Pays de Gex`). Affiché comme niveau intermédiaire entre la commune et le département. |

---

## Colonnes exclues

Les 28 colonnes suivantes ont été écartées car non pertinentes pour un usage immobilier :

| Colonnes exclues | Raison |
|---|---|
| `nom_sans_pronom`, `nom_a`, `nom_de`, `nom_standard_majuscule` | Variantes de noms inutiles — `nom_standard` et `nom_sans_accent` suffisent |
| `typecom_texte` | Redondant avec `typecom` |
| `canton_code`, `canton_nom` | Découpage électoral, non pertinent pour l'immobilier |
| `academie_code`, `academie_nom` | Découpage scolaire hors scope |
| `zone_emploi`, `code_insee_centre_zone_emploi` | Trop spécifique pour le MVP |
| `code_unite_urbaine`, `nom_unite_urbaine`, `taille_unite_urbain`, `type_commune_unite_urbain`, `statut_commune_unite_urbain` | Redondant avec `grille_densite` |
| `altitude_moyenne`, `altitude_minimale`, `altitude_maximale` | Non pertinent pour l'immobilier |
| `latitude_mairie`, `longitude_mairie` | `latitude_centre` / `longitude_centre` sont plus représentatifs |
| `gentile` | Non pertinent |
| `url_wikipedia`, `url_villedereve` | Liens externes hors scope |
15 changes: 15 additions & 0 deletions pipeline/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Configuration de l'ingestion bronze -> S3.
# Copier en `.env` et adapter, puis charger les variables avant de lancer un script :
# set -a; source .env; set +a
# uv run python scripts/ingest_transport.py

# --- Cible S3 (commun MinIO local et AWS réel) ---
HOMEPEDIA_S3_BUCKET=homepedia
HOMEPEDIA_S3_PREFIX=bronze

# --- MinIO local (via `just up data`) ---
# Laisser AWS_ENDPOINT_URL vide / commenté pour viser AWS réel.
AWS_ENDPOINT_URL=http://localhost:9000
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
AWS_DEFAULT_REGION=us-east-1
1 change: 1 addition & 0 deletions pipeline/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
16 changes: 16 additions & 0 deletions pipeline/homepedia_ingest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Pipeline d'ingestion bronze Homepedia (data.gouv.fr -> S3)."""

from .bronze import ingest_datagouv_to_s3
from .datagouv import DownloadResult, ResourceMeta, download, get_resource_meta
from .s3 import make_client, s3_bucket, s3_prefix

__all__ = [
"ingest_datagouv_to_s3",
"get_resource_meta",
"download",
"ResourceMeta",
"DownloadResult",
"make_client",
"s3_bucket",
"s3_prefix",
]
36 changes: 36 additions & 0 deletions pipeline/homepedia_ingest/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""CLI générique d'ingestion bronze vers S3.

Ingère n'importe quelle resource data.gouv.fr (CSV ou autre) dans la zone
bronze, sans écrire de script dédié :

uv run python -m homepedia_ingest <resource_id> <dataset>

Variables d'environnement : HOMEPEDIA_S3_BUCKET (obligatoire), HOMEPEDIA_S3_PREFIX
(défaut "bronze"), AWS_ENDPOINT_URL (optionnel, MinIO/LocalStack) + credentials
AWS standard.
"""

from __future__ import annotations

import argparse

from .bronze import ingest_datagouv_to_s3


def main() -> int:
parser = argparse.ArgumentParser(prog="homepedia_ingest", description=__doc__)
parser.add_argument("resource_id", help="ID de la resource data.gouv.fr")
parser.add_argument("dataset", help="Nom logique du dataset (clé de partition)")
args = parser.parse_args()

res = ingest_datagouv_to_s3(args.resource_id, args.dataset)
if res["status"] == "skipped":
print(f"[bronze] SKIP (déjà présent, même sha256) : s3://{res['bucket']}/{res['key']}")
else:
print(f"[bronze] OK upload : s3://{res['bucket']}/{res['key']}")
print(f"[bronze] {res['size_bytes']} octets, sha256 {res['sha256']}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
96 changes: 96 additions & 0 deletions pipeline/homepedia_ingest/bronze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Ingestion de la couche bronze Homepedia vers S3.

Principe bronze : on dépose le fichier source **tel quel** dans S3, sans aucune
transformation ni typage (fidélité totale à la source). Le nettoyage et le
typage se feront en couche silver ; la mise en forme API en couche gold.

Clé S3 (style médaillon, partitionnée par date d'ingestion, compatible
Athena/BigQuery « Hive partitioning ») :

{prefix}/source=datagouv/dataset=<dataset>/ingestion_date=YYYY-MM-DD/<resource_id>__<fichier>

Des métadonnées de lignage sont attachées à l'objet S3 (source-url, resource-id,
dataset-id, sha256, ingested-at), relisibles via head_object.
"""

from __future__ import annotations

import tempfile
from datetime import datetime, timezone
from pathlib import Path

from .datagouv import download, get_resource_meta
from .s3 import head_metadata, make_client, put_file, s3_bucket, s3_prefix


def _object_key(
prefix: str, dataset: str, ingestion_date: str, resource_id: str, filename: str
) -> str:
return (
f"{prefix}/source=datagouv/dataset={dataset}"
f"/ingestion_date={ingestion_date}/{resource_id}__{filename}"
)


def ingest_datagouv_to_s3(resource_id: str, dataset: str) -> dict:
"""Télécharge une resource data.gouv.fr et la dépose brute dans S3 (bronze).

Idempotent : si l'objet existe déjà à la même clé avec le même SHA-256,
l'upload est sauté.

Args:
resource_id: identifiant de la resource data.gouv.fr.
dataset: nom logique du jeu de données (clé de partition, ex. "transport").

Returns:
Dictionnaire récapitulatif : status (uploaded|skipped), bucket, key,
sha256, size_bytes, source_url, ingested_at.
"""
# Config S3 d'abord : fail-fast si HOMEPEDIA_S3_BUCKET manque (avant tout réseau).
bucket = s3_bucket()
prefix = s3_prefix()
client = make_client()

meta = get_resource_meta(resource_id)
now = datetime.now(timezone.utc)
ingestion_date = now.strftime("%Y-%m-%d")
ingested_at = now.isoformat()

with tempfile.TemporaryDirectory() as tmp:
dl = download(meta.url, Path(tmp))
key = _object_key(prefix, dataset, ingestion_date, resource_id, dl.path.name)

existing = head_metadata(client, bucket, key)
if existing is not None and existing.get("sha256") == dl.sha256:
return {
"status": "skipped",
"bucket": bucket,
"key": key,
"sha256": dl.sha256,
"size_bytes": dl.size_bytes,
"source_url": meta.url,
"ingested_at": ingested_at,
}

# Métadonnées de lignage attachées à l'objet S3 (clés en tirets pour
# rester portables AWS / MinIO ; valeurs forcément des chaînes).
object_metadata = {
"source": "datagouv",
"dataset": dataset,
"resource-id": resource_id,
"dataset-id": meta.dataset_id,
"source-url": meta.url,
"sha256": dl.sha256,
"ingested-at": ingested_at,
}
put_file(client, bucket, key, dl.path, metadata=object_metadata)

return {
"status": "uploaded",
"bucket": bucket,
"key": key,
"sha256": dl.sha256,
"size_bytes": dl.size_bytes,
"source_url": meta.url,
"ingested_at": ingested_at,
}
Loading
Loading