Repository: dataforgoodfr/13_potentiel_solaire
Pull request #178: [ALGO] Feature/data 177 ajout indicateurs fiches zone app
Author: login: machbry
Created: 2025-04-05T17:00:52Z | Merged: 2025-04-17T19:47:25Z

### Description
Github issues : https://github.com/dataforgoodfr/13_potentiel_solaire/issues/177 & https://github.com/dataforgoodfr/13_potentiel_solaire/issues/181

Tous les indicateurs des fiches zones sont présents sur les tables `communes `/ `departements `et `regions `et sont calculés en SQL depuis la table `etablissements`.

### Bilan des changements de schéma de la base de données : 
Table `ref_code_postal`
- `code_insee` (`VARCHAR`)
- `code_postal` (`VARCHAR`)

Table `etablissements`
- `surface_utile` -> `surface_exploitable_max` (`INTEGER`)
- `niveau_potentiel` (`VARCHAR`)

Table `communes`
- `surface_utile` -> `surface_exploitable_max_total` (`INTEGER`)
- `potentiel_solaire` -> `potentiel_solaire_total` (`BIGINT`)
- `count_etablissements` -> `nb_etablissements_total` (`INTEGER`)
- `count_etablissements_proteges` -> `nb_etablissements_proteges_total` (`INTEGER`)
- `nb_eleves_total` (`INTEGER`)
- `potentiel_nb_foyers_total` (`INTEGER`)
- `top_etablissements_total` (`JSON`)
- `nb_etablissements_par_niveau_potentiel_total` (`JSON`)
- `surface_exploitable_max_primaires` (`INTEGER`)
- `potentiel_solaire_primaires` (`BIGINT`)
- `nb_etablissements_primaires` (`INTEGER`)
- `nb_etablissements_proteges_primaires` (`INTEGER`)
- `nb_eleves_primaires` (`INTEGER`)
- `potentiel_nb_foyers_primaires` (`INTEGER`)
- `top_etablissements_primaires` (`JSON`)
- `nb_etablissements_par_niveau_potentiel_primaires` (`JSON`)
- `potentiel_solaire_colleges` (`BIGINT`)
- `potentiel_solaire_lycees` (`BIGINT`)
- `potentiel_nb_foyers` (`INTEGER`)

Table `departements`
- `surface_utile` -> `surface_exploitable_max_total` (`INTEGER`)
- `potentiel_solaire` -> `potentiel_solaire_total` (`BIGINT`)
- `count_etablissements` -> `nb_etablissements_total` (`INTEGER`)
- `count_etablissements_proteges` -> `nb_etablissements_proteges_total` (`INTEGER`)
- `nb_eleves_total` (`INTEGER`)
- `potentiel_nb_foyers_total` (`INTEGER`)
- `top_etablissements_total` (`JSON`)
- `nb_etablissements_par_niveau_potentiel_total` (`JSON`)
- `surface_exploitable_max_colleges` (`INTEGER`)
- `potentiel_solaire_colleges` (`BIGINT`)
- `nb_etablissements_colleges` (`INTEGER`)
- `nb_etablissements_proteges_colleges` (`INTEGER`)
- `nb_eleves_colleges` (`INTEGER`)
- `potentiel_nb_foyers_colleges` (`INTEGER`)
- `top_etablissements_colleges` (`JSON`)
- `nb_etablissements_par_niveau_potentiel_colleges` (`JSON`)
- `potentiel_solaire_primaires` (`BIGINT`)
- `potentiel_solaire_lycees` (`BIGINT`)

Table `regions`
- `surface_utile` -> `surface_exploitable_max_total` (`INTEGER`)
- `potentiel_solaire` -> `potentiel_solaire_total` (`BIGINT`)
- `count_etablissements` -> `nb_etablissements_total` (`INTEGER`)
- `count_etablissements_proteges` -> `nb_etablissements_proteges_total` (`INTEGER`)
- `nb_eleves_total` (`INTEGER`)
- `potentiel_nb_foyers_total` (`INTEGER`)
- `top_etablissements_total` (`JSON`)
- `nb_etablissements_par_niveau_potentiel_total` (`JSON`)
- `surface_exploitable_max_lycees` (`INTEGER`)
- `potentiel_solaire_lycees` (`BIGINT`)
- `nb_etablissements_lycees` (`INTEGER`)
- `nb_etablissements_proteges_lycees` (`INTEGER`)
- `nb_eleves_lycees` (`INTEGER`)
- `potentiel_nb_foyers_lycees` (`INTEGER`)
- `top_etablissements_lycees` (`JSON`)
- `nb_etablissements_par_niveau_potentiel_lycees` (`JSON`)
- `potentiel_solaire_primaires` (`BIGINT`)
- `potentiel_solaire_colleges` (`BIGINT`)

Table `seuils_niveaux_potentiels`
- `niveau_potentiel` (`VARCHAR`)
- `min_potentiel_solaire` (`BIGINT`)
- `max_potentiel_solaire` (`BIGINT`)

Trois niveaux :
- `1_HIGH` : > `250 000`
- `2_GOOD` : > `100 000` & < `250 000`
- `3_LIMITED` : < `100 00`

Ca permet de répartir en trois groupes de même tailles les établissements qui ont un `potentiel_solaire > 0`

Exemple de valeur pour le `nb_etablissements_par_niveau_potentiel_colleges` (pour le departement `013`) : 
`{"1_HIGH":124,"2_GOOD":5,"3_LIMITED":38}`

Les trois seuils sont toujours présents dans le JSON. Par exemple sur une commune sans aucun établissement on a : 
`{"1_HIGH": 0,"2_GOOD": 0,"3_LIMITED": 0}`

Exemple de valeur pour le `top_etablissements_primaires` (pour la commune `97101`) : 
`[
{"id":"9710648C","libelle":"Ecole primaire Grand Camp 1","potentiel_solaire":677957},
{"id":"9710514G","libelle":"Ecole primaire Maurice St-Pierre","potentiel_solaire":632018},
{"id":"9711292C","libelle":"Ecole élémentaire publique Germaine Petit-Devaed du Groupe Scolaire Guy Cornely","potentiel_solaire":511879}
]`
Ce JSON peut être une liste vide [ ] (ex : communes sans école primaire) ou n'avoir que 1 ou 2 éléments.
Format similaire pour les tops des departements et regions.

### Diagramme du schéma de la database
![image](https://github.com/user-attachments/assets/1a465695-0b1a-485c-ba42-5fe9b32da36b)

### Comment tester ?
Récupérer la base de données :
- `docker pull ghcr.io/dataforgoodfr/13_potentiel_solaire_db:0.1.20250417`
- ou télécharger directement [ici](https://drive.google.com/file/d/1wls1JF1bUYKKClfa_bgZ17GqiFyXEXLt/view?usp=sharing)

### Pour faciliter la validation de ma PR
- [x] J'ai pris connaissance et respecté les [règles de contribution](../CONTRIBUTING.md)
- [x] Les pre-commit passent
- [x] Les test unitaires passent
- [x] Le code modifié fonctionne en local
- [x] J'ai demandé une peer-review à un autre bénévole du projet
- [ ] J'ai ajouté / mis à jour de la documentation sur [outline](https://outline.services.dataforgood.fr/collection/13_potentiel_scolaire-qJFjGnz5Ec)



Diff:
diff --git a/algorithme/.gitignore b/algorithme/.gitignore
index 06433d77..277a2b6f 100644
--- a/algorithme/.gitignore
+++ b/algorithme/.gitignore
@@ -179,3 +179,4 @@ ENR*
 *.duckdb.wal
 *.json
 *.err
+*.parquet
diff --git a/algorithme/README.md b/algorithme/README.md
index f62cfecb..e7727013 100644
--- a/algorithme/README.md
+++ b/algorithme/README.md
@@ -1,13 +1,19 @@
 # Glossaire
 - [Installation](#installation)
-- [Mettre à jour ou modifier son environnement](#mettre-à-jour-ou-modifier-son-environnement)
-- [Recuperer les données](data/README.md)
+    - [Installer Poetry](#11-installer-poetry)
+    - [Installer les dépendances et la database](#12-installer-les-dépendances-et-la-database)
+- [Calculs de potentiel solaire](#2-calculs-de-potentiel-solaire)
+    - [Executer les calculs](#21-executer-les-calculs)
+    - [Récupérer les resultats](#22-récupérer-les-resultats)
 - [Tester & verifier son code](#tester--verifier-son-code)
+    - [Analyser les resultats sur un departement](#31-analyser-les-resultats-sur-un-departement)
+    - [Lancer les precommit-hook localement](#32-lancer-les-precommit-hook-localement)
+    - [Lancer les tests unitaires](#33-lancer-les-tests-unitaires)
 
 
-# Installation
+# 1. Installation
 
-## 1. Installer Poetry
+## 1.1 Installer Poetry
 
 Plusieurs [méthodes d'installation](https://python-poetry.org/docs/#installation) sont décrites dans la documentation de poetry dont:
 
@@ -20,7 +26,7 @@ L'avantage de pipx est que l'installation de pipx est documentée pour linux, wi
 
 Cependant, libre à toi d'utiliser la méthode qui te convient le mieux ! Quelque soit la méthode choisie, il est important de ne pas installer poetry dans l'environnement virtuel qui sera créé un peu plus tard dans ce README pour les dépendances de la base de code de ce repo git.
 
-### 1.1 Installation de Poetry avec pipx
+### Installation de Poetry avec pipx
 
 Suivre les instructions pour [installer pipx](https://pipx.pypa.io/stable/#install-pipx) selon ta plateforme (linux, windows, etc...)
 
@@ -32,76 +38,65 @@ Par exemple pour Ubuntu 23.04+:
 
 [Installer Poetry avec pipx](https://python-poetry.org/docs/#installing-with-pipx):
 
-    pipx install poetry==1.7
+    pipx install poetry
 
-### 1.2 Installation de Poetry avec l'installateur officiel
+### Installation de Poetry avec l'installateur officiel
 
 L'installation avec l'installateur officiel nécessitant quelques étapes supplémentaires,
 se référer à la [documentation officielle](https://python-poetry.org/docs/#installing-with-the-official-installer).
 
-### 1.3 Configurer poetry pour créer les environnements virtuels au sein du projet
+### Configurer poetry pour créer les environnements virtuels au sein du projet
 
     poetry config virtualenvs.in-project true
 
-## 2. Installer les dépendances et le package potentiel_solaire
+## 1.2 Installer les dépendances et la database
 
-### 2.1 Naviguer dans le dossier algorithme
+### Naviguer dans le dossier algorithme
 
     cd algorithme
 
-### 2.2 Installer les dépendances
+### Installer les dépendances
 
     poetry install --with dev
 
-### 2.3 Verifier le venv ainsi créé
+### Verifier le venv ainsi créé
 
     poetry env info
 
-### 2.4 Activer l'environnement
-
-    poetry shell
-
-
-# Modifier les dépendances
-
-### Ajouter une dépendance
-
-    poetry add pandas
-
-### Mettre à jour les dépendances
-
-    poetry update
-
-# Initialiser la base de données duckdb
+### Initialiser la database duckdb
 
     alembic upgrade head
 
-# Executer les calculs de potentiel solaire
+# 2. Calculs de potentiel solaire
+
+## 2.1 Executer les calculs
 
-* Sur un departement: `run-pipeline-algorithme -d 093`
+* Sur un departement: `poetry run run-pipeline-algorithme -d 093`
 
-* Sur une region: `run-pipeline-algorithme -r 11`
+* Sur une region: `poetry run run-pipeline-algorithme -r 11`
 
-* Sur toute la France: `run-pipeline-algorithme --all`
+* Sur toute la France: `poetry run run-pipeline-algorithme --all`
 
+* Mettre à jour les indicateurs dans la database duckdb: `poetry run update-database-indicators`
+ 
 Note : pour executer la pipeline sur toute la France, il faut avoir à minima 180 Go d'espace disponible et compter minimum 4H de temps d'execution.
 
-## Resultats
+## 2.2 Récupérer les resultats
 * un fichier .gpkg dans le dossier [results](data/results) pour les resultats pour chaque departement (ex: [D093_pipeline_results.gpkg](data/results/D093_pipeline_results.gpkg))
-* dans la [database duckdb](database/potentiel_solaire.duckdb)
 * un notebook resultats est aussi genere pour chaque departement dans le dossier [exports](notebooks/exports) (ex: [D093_pipeline_algorithme.ipynb](notebooks/exports/D093_pipeline_algorithme.ipynb))
+* dans la [database duckdb](database/potentiel_solaire.duckdb)
 
-# Tester & verifier son code
+# 3. Tester & verifier son code
 
-## Analyser les resultats sur un departement
+## 3.1 Analyser les resultats sur un departement
 Le notebook [analyse_qualite_resultats_pipeline.ipynb](notebooks/analyse_qualite_resultats_pipeline.ipynb) permet d'analyser les resulats sur le departement de son choix.
 
-## Lancer les precommit-hook localement
+## 3.2 Lancer les precommit-hook localement
 
 [Installer les precommit](https://pre-commit.com/)
     
-    pre-commit run --all-files
+    poetry run pre-commit run --all-files
 
-## Utiliser Tox pour tester votre code
+## 3.3 Lancer les tests unitaires
 
-    tox -vv
+    poetry run tox -vv
diff --git a/algorithme/data/README.md b/algorithme/data/README.md
deleted file mode 100644
index 4ed302c8..00000000
--- a/algorithme/data/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-## Recuperation des données
-
-Etre dans le dossier algorithme
-
-    cd algorithme
-
-Installer les dépendances & activer l'environnement
-
-    poetry install
-    poetry shell
-
-Initialiser la base de données duckdb
-
-    alembic upgrade head
-
-Lancer le script de recuperation sur un departement
-
-    run-pipeline-algorithme -d 093
-
-Une fois ce script réussi, les données et resultats devraient être visibles :
-* dans ce dossier pour les fichiers sources listes dans [sources.yaml](sources.yaml)
-* un fichier .gpkg dans le dossier [results](results) pour les resultats pour chaque departement (ex: [093_pipeline_results.gpkg](results/D093_pipeline_results.gpkg))
-* dans la [database duckdb](../database/potentiel_solaire.duckdb)
-* un notebook resultats est aussi genere pour chaque departement dans le dossier [exports](notebooks/exports) (ex: [D093_pipeline_algorithme.ipynb](notebooks/exports/D093_pipeline_algorithme.ipynb))
diff --git a/algorithme/data/sources.yaml b/algorithme/data/sources.yaml
index 83fc02f0..75273985 100644
--- a/algorithme/data/sources.yaml
+++ b/algorithme/data/sources.yaml
@@ -22,3 +22,8 @@ regions:
   url: "https://www.data.gouv.fr/fr/datasets/r/d993e112-848f-4446-b93b-0a9d5997c4a4"
   filename: "regions.json"
   description: "Geometries des regions francaises"
+
+codes_postaux:
+  url: "https://object.files.data.gouv.fr/hydra-parquet/hydra-parquet/54535896c301b2b3928f8db6305309ad.parquet"
+  filename: "codes_postaux.parquet"
+  description: "base officielle des codes postaux"
diff --git a/algorithme/database/versions/2025_04_05_1457_add_indicators_for_app_fiches_zone.py b/algorithme/database/versions/2025_04_05_1457_add_indicators_for_app_fiches_zone.py
new file mode 100644
index 00000000..cc76589a
--- /dev/null
+++ b/algorithme/database/versions/2025_04_05_1457_add_indicators_for_app_fiches_zone.py
@@ -0,0 +1,236 @@
+"""add indicators for app fiches zone
+
+Revision ID: b80cbbbf68d9
+Revises: 49d44d790ee6
+Create Date: 2025-04-05 14:57:19.102683
+
+"""
+from dataclasses import dataclass
+from typing import Optional, Sequence, Union
+
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'b80cbbbf68d9'
+down_revision: Union[str, None] = '49d44d790ee6'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+# schema
+etablissements_table = "etablissements"
+communes_table = "communes"
+departements_table = "departements"
+regions_table = "regions"
+seuils_niveaux_potentiels_table = "seuils_niveaux_potentiels"
+
+
+@dataclass
+class Aggregation:
+    table: str
+    suffix: str = ""
+
+@dataclass
+class Indicator:
+    name: str
+    data_type: str
+    default_value: Optional[str] = None
+
+@dataclass
+class ZoneAggregationIndicator:
+    aggregation: Aggregation
+    indicator: Indicator
+
+    @property
+    def table_name(self) -> str:
+        return self.aggregation.table
+    
+    @property
+    def column_name(self) -> str:
+        return f"{self.indicator.name}{self.aggregation.suffix}"
+    
+    @property
+    def column_type(self) -> str:
+        return f"{self.indicator.data_type} DEFAULT {self.indicator.default_value}" if self.indicator.default_value else self.indicator.data_type
+
+
+# aggregations to add
+new_aggregations = [
+    Aggregation(table=communes_table, suffix="_primaires"),
+    Aggregation(table=communes_table, suffix="_total"),
+    Aggregation(table=departements_table, suffix="_colleges"),
+    Aggregation(table=departements_table, suffix="_total"),
+    Aggregation(table=regions_table, suffix="_lycees"),
+    Aggregation(table=regions_table, suffix="_total"),
+]
+
+additional_aggregations_for_potentiel_solaire = [
+    Aggregation(table=communes_table, suffix="_colleges"),
+    Aggregation(table=communes_table, suffix="_lycees"),
+    Aggregation(table=departements_table, suffix="_primaires"),
+    Aggregation(table=departements_table, suffix="_lycees"),
+    Aggregation(table=regions_table, suffix="_primaires"),
+    Aggregation(table=regions_table, suffix="_colleges"),   
+]
+    
+
+# indicators to add
+new_indicators = [
+    Indicator(name="nb_eleves", data_type="INTEGER", default_value="0"),
+    Indicator(name="nb_etablissements", data_type="INTEGER", default_value="0"),
+    Indicator(name="nb_etablissements_proteges", data_type="INTEGER", default_value="0"),
+    Indicator(name="surface_exploitable_max", data_type="INTEGER", default_value="0"),
+    Indicator(name="potentiel_solaire", data_type="BIGINT", default_value="0"),
+    Indicator(name="potentiel_nb_foyers", data_type="INTEGER", default_value="0"),
+    Indicator(name="top_etablissements", data_type="JSON", default_value="'[]'"),
+    Indicator(name="nb_etablissements_par_niveau_potentiel", data_type="JSON", default_value="'{\"1_HIGH\": 0,\"2_GOOD\": 0,\"3_LIMITED\": 0}'"),
+]
+
+
+# new indicators x aggregations to add
+new_zone_aggregation_indicators = [
+    ZoneAggregationIndicator(
+        aggregation=aggregation,
+        indicator=indicator
+    )
+    for aggregation in new_aggregations
+    for indicator in new_indicators
+]
+
+potentiel_solaire_indicator = Indicator(name="potentiel_solaire", data_type="BIGINT", default_value="0")
+for aggregation in additional_aggregations_for_potentiel_solaire:
+    new_zone_aggregation_indicators.append(
+        ZoneAggregationIndicator(
+            aggregation=aggregation,
+            indicator=potentiel_solaire_indicator
+        )
+    )
+
+
+# indicators to delete
+deleted_indicators = [
+    Indicator(name="surface_utile", data_type="INTEGER", default_value="0"),
+    Indicator(name="potentiel_solaire", data_type="BIGINT", default_value="0"),
+    Indicator(name="count_etablissements", data_type="INTEGER", default_value="0"),
+    Indicator(name="count_etablissements_proteges", data_type="INTEGER", default_value="0"),
+]
+
+# aggregations to delete
+deleted_aggregations = [
+    Aggregation(table=communes_table),
+    Aggregation(table=departements_table),
+    Aggregation(table=regions_table),
+]
+
+# deleted indicators x aggregations to delete
+deleted_zone_aggregation_indicators = [
+    ZoneAggregationIndicator(
+        aggregation=aggregation,
+        indicator=indicator
+    )
+    for aggregation in deleted_aggregations
+    for indicator in deleted_indicators
+]
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    op.execute("""
+        INSTALL spatial;
+        LOAD spatial;
+    """)
+
+    # Rename column for surface_utile in etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        RENAME COLUMN surface_utile TO surface_exploitable_max
+    """)
+
+    # Create table with min values for each niveau_potentiel
+    op.execute(f"""
+        CREATE TABLE IF NOT EXISTS {seuils_niveaux_potentiels_table} (
+            niveau_potentiel VARCHAR UNIQUE NOT NULL,
+            min_potentiel_solaire BIGINT NOT NULL,
+            max_potentiel_solaire BIGINT NOT NULL,
+        )
+    """)
+
+    # Insert values into seuils_niveaux_potentiels table
+    op.execute(f"""
+        INSERT INTO {seuils_niveaux_potentiels_table} 
+            (niveau_potentiel, min_potentiel_solaire, max_potentiel_solaire)
+        VALUES
+            ('1_HIGH', 250000, 999999999999),
+            ('2_GOOD', 100000, 249999), 
+            ('3_LIMITED', 0, 99999)
+    """)
+
+    # Add niveau_potentiel column to etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        ADD COLUMN IF NOT EXISTS niveau_potentiel VARCHAR DEFAULT '3_LIMITED'
+    """)
+
+    # Add potentiel_solaire column to etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        ADD COLUMN IF NOT EXISTS potentiel_nb_foyers INTEGER DEFAULT 0
+    """)
+
+    # Add new columns to each table
+    for zone_agg_ind in new_zone_aggregation_indicators:
+        op.execute(f"""
+            ALTER TABLE {zone_agg_ind.table_name}
+            ADD COLUMN IF NOT EXISTS {zone_agg_ind.column_name} {zone_agg_ind.column_type}
+        """)
+
+    # Remove columns for each table
+    for zone_agg_ind in deleted_zone_aggregation_indicators:
+        op.execute(f"""
+            ALTER TABLE {zone_agg_ind.table_name}
+            DROP COLUMN IF EXISTS {zone_agg_ind.column_name}
+        """)
+
+def downgrade() -> None:
+    """Downgrade schema."""
+    op.execute("""
+        INSTALL spatial;
+        LOAD spatial;
+    """)
+
+    # Add new columns to each table
+    for zone_agg_ind in deleted_zone_aggregation_indicators:
+        op.execute(f"""
+            ALTER TABLE {zone_agg_ind.table_name}
+            ADD COLUMN IF NOT EXISTS {zone_agg_ind.column_name} {zone_agg_ind.column_type}
+        """)
+
+    # Remove columns for each table
+    for zone_agg_ind in new_zone_aggregation_indicators:
+        op.execute(f"""
+            ALTER TABLE {zone_agg_ind.table_name}
+            DROP COLUMN IF EXISTS {zone_agg_ind.column_name}
+        """)
+
+    # Remove potentiel_nb_foyers column from etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        DROP COLUMN IF EXISTS potentiel_nb_foyers
+    """)
+
+    # Remove niveau_potentiel column from etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        DROP COLUMN IF EXISTS niveau_potentiel
+    """)
+
+    # Remove seuils_niveaux_potentiels table
+    op.execute(f"""
+        DROP TABLE IF EXISTS {seuils_niveaux_potentiels_table}
+    """)
+    
+    # Rename column for surface_exploitable_max in etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        RENAME COLUMN surface_exploitable_max TO surface_utile
+    """)
diff --git a/algorithme/database/versions/2025_04_12_2250_create_code_postal_table.py b/algorithme/database/versions/2025_04_12_2250_create_code_postal_table.py
new file mode 100644
index 00000000..38137625
--- /dev/null
+++ b/algorithme/database/versions/2025_04_12_2250_create_code_postal_table.py
@@ -0,0 +1,44 @@
+"""create code postal table
+
+Revision ID: 5f96d921513d
+Revises: b80cbbbf68d9
+Create Date: 2025-04-12 22:50:48.145411
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+
+from potentiel_solaire.sources.extract import extract_sources
+
+
+# revision identifiers, used by Alembic.
+revision: str = '5f96d921513d'
+down_revision: Union[str, None] = 'b80cbbbf68d9'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+# schema
+codes_postaux_table = "ref_code_postal"
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    sources = extract_sources()
+    codes_postaux_path = sources["codes_postaux"].filepath
+
+    op.execute(f"""
+        CREATE OR REPLACE TABLE {codes_postaux_table} AS
+        SELECT 
+            DISTINCT ON(code_insee, code_postal) "#Code_commune_INSEE" AS code_insee, 
+            Code_postal AS code_postal
+        FROM read_parquet('{codes_postaux_path}')
+    """)    
+
+
+def downgrade() -> None:
+    """Downgrade schema."""
+    op.execute(f"""
+        DROP TABLE IF EXISTS {codes_postaux_table}
+    """)
diff --git a/algorithme/notebooks/pipeline_algorithme.ipynb b/algorithme/notebooks/pipeline_algorithme.ipynb
index d80b2d97..614a37cf 100644
--- a/algorithme/notebooks/pipeline_algorithme.ipynb
+++ b/algorithme/notebooks/pipeline_algorithme.ipynb
@@ -34,7 +34,7 @@
    },
    "outputs": [],
    "source": [
-    "code_departement = \"973\""
+    "code_departement = \"093\""
    ]
   },
   {
@@ -63,10 +63,7 @@
     "from potentiel_solaire.sources.protected_buildings import get_areas_with_protected_buildings\n",
     "from potentiel_solaire.features.solar_potential import calculate_solar_potential\n",
     "from potentiel_solaire.aggregate import aggregate_solar_potential_by_etablishment\n",
-    "from potentiel_solaire.duckdb_manager import save_solar_potential_by_school, \\\n",
-    "    save_solar_potential_by_commune, \\\n",
-    "    save_solar_potential_by_department\n",
-    "from potentiel_solaire.geojson_files_creator import export_to_geofile"
+    "from potentiel_solaire.database.queries import update_results_for_schools"
    ]
   },
   {
@@ -362,7 +359,7 @@
     "results_by_school = aggregate_solar_potential_by_etablishment(\n",
     "    schools_establishments=schools_establishments,\n",
     "    solar_potential_of_schools_buildings=solar_potential_of_schools_buildings\n",
-    ")\n"
+    ")"
    ]
   },
   {
@@ -388,14 +385,9 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "stats_schools= save_solar_potential_by_school(\n",
+    "update_results_for_schools(\n",
     "    results_by_school,\n",
-    "    code_departement\n",
-    ")\n",
-    "\n",
-    "assert stats_schools.query('surface_utile > 0').shape[0] > 0, \"Problème lors de la mise à jour des établissements\"\n",
-    "\n",
-    "stats_schools.head(3)"
+    ")"
    ]
   },
   {
@@ -406,22 +398,6 @@
     "<!-- #### Mise à jour des données des Communes du Département en Base de Données -->"
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "341619cc",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "stats_communes = save_solar_potential_by_commune(\n",
-    "    code_departement\n",
-    ")\n",
-    "\n",
-    "assert stats_communes.query('surface_utile > 0').shape[0] > 0, \"Problème lors de la mise à jour des communes\"\n",
-    "\n",
-    "stats_communes.head(3)"
-   ]
-  },
   {
    "cell_type": "markdown",
    "id": "aa3147ae",
@@ -430,22 +406,6 @@
     "<!-- #### Mise à jour des données du Département en Base de données -->"
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "7a46bd4a",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "stats_department = save_solar_potential_by_department(\n",
-    "    code_departement\n",
-    ")\n",
-    "\n",
-    "assert stats_department['surface_utile'].shape[0] > 0, f\"Problème lors de la mise à jour du département {code_departement}\"\n",
-    "\n",
-    "stats_department.head(3)"
-   ]
-  },
   {
    "cell_type": "markdown",
    "id": "2723a1b3",
@@ -464,13 +424,10 @@
     "layers = [\"schools_establishments\", \"educational_zones\", \"schools_buildings\", \"solar_potential_of_schools_buildings\", \"areas_with_protected_buildings\"]\n",
     "gdfs = [schools_establishments, educational_zones, schools_buildings, solar_potential_of_schools_buildings, areas_with_protected_buildings]\n",
     "output_gpkg = RESULTS_FOLDER / f\"D{code_departement}_pipeline_results.gpkg\"\n",
+    "output_gpkg.unlink(missing_ok=True) \n",
     "\n",
     "for layer, gdf in zip(layers, gdfs):\n",
-    "    gdf.to_file(output_gpkg, layer=layer, driver=\"GPKG\")\n",
-    "\n",
-    "export_to_geofile(stats_schools, output_gpkg, layer=\"etablissements\", driver=\"GPKG\")\n",
-    "export_to_geofile(stats_communes, output_gpkg, layer=\"communes\", driver=\"GPKG\")\n",
-    "export_to_geofile(stats_department, output_gpkg, layer=\"departements\", driver=\"GPKG\")"
+    "    gdf.to_file(output_gpkg, layer=layer, driver=\"GPKG\")"
    ]
   }
  ],
diff --git a/algorithme/potentiel_solaire/database/models.py b/algorithme/potentiel_solaire/database/models.py
new file mode 100644
index 00000000..36feed0e
--- /dev/null
+++ b/algorithme/potentiel_solaire/database/models.py
@@ -0,0 +1,29 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class Table:
+    name: str
+    pkey: str
+
+# schema
+etablissements_table = Table(
+    name="etablissements", 
+    pkey="identifiant_de_l_etablissement"
+)
+communes_table = Table(
+    name="communes", 
+    pkey="code_commune"
+)
+departements_table = Table(
+    name="departements", 
+    pkey="code_departement"
+)
+regions_table = Table(
+    name="regions", 
+    pkey="code_region"
+)
+seuils_niveaux_potentiels_table = Table(
+    name="seuils_niveaux_potentiels", 
+    pkey="niveau_potentiel"
+)
diff --git a/algorithme/potentiel_solaire/database/queries.py b/algorithme/potentiel_solaire/database/queries.py
new file mode 100644
index 00000000..6153ce91
--- /dev/null
+++ b/algorithme/potentiel_solaire/database/queries.py
@@ -0,0 +1,330 @@
+from importlib.metadata import version
+from typing import Literal, Optional
+
+import duckdb
+import pandas as pd
+
+from potentiel_solaire.constants import DUCK_DB_PATH
+from potentiel_solaire.database.models import (
+    Table,
+    etablissements_table,
+    communes_table,
+    departements_table,
+    regions_table,
+    seuils_niveaux_potentiels_table,
+)
+from potentiel_solaire.logger import get_logger
+
+logger = get_logger()
+
+
+def get_connection():
+    conn = duckdb.connect(DUCK_DB_PATH)
+
+    # Load Spatial extension
+    conn.execute("""
+        INSTALL spatial;
+        LOAD spatial;
+    """)
+
+    conn.commit()
+
+    return conn
+
+
+def get_departements():
+    with get_connection() as conn:
+        query = f"""
+        SELECT {departements_table.pkey} AS code_departement
+        FROM {departements_table.name} 
+        GROUP BY {departements_table.pkey}
+        """
+
+        return list(conn.query(query).df()["code_departement"].unique())
+
+
+def get_departements_for_region(code_region: str):
+    with get_connection() as conn:
+        query = f"""
+        SELECT {departements_table.pkey} AS code_departement
+        FROM {departements_table.name}  
+        WHERE {regions_table.pkey} = '{code_region}'
+        GROUP BY {departements_table.pkey}
+        """
+
+        return list(conn.query(query).df()["code_departement"].unique())
+
+
+def get_regions():
+    with get_connection() as conn:
+        query = f"""
+        SELECT {regions_table.pkey} AS code_region
+        FROM {regions_table.name} 
+        GROUP BY {regions_table.pkey}
+        """
+
+        return list(conn.query(query).df()["code_region"].unique())
+
+
+def update_results_for_schools(
+    results_by_school: pd.DataFrame,
+):
+    with get_connection() as conn:
+        conn.register("results_by_school", results_by_school)
+
+        code_version = version('potentiel_solaire')
+
+        update_query = f"""
+            UPDATE {etablissements_table.name}
+            SET 
+                surface_exploitable_max = results_by_school.surface_utile,
+                potentiel_solaire = results_by_school.potentiel_solaire,
+                protection = results_by_school.protection,
+                date_calcul = CURRENT_TIMESTAMP,
+                version = '{code_version}',
+                nb_batiments_associes = results_by_school.nb_batiments_associes
+            FROM
+                results_by_school
+            WHERE
+                {etablissements_table.name}.{etablissements_table.pkey} = results_by_school.identifiant_de_l_etablissement
+        """
+
+        conn.execute(update_query)
+
+
+def update_indicators_for_table(
+    table: Table,
+    suffix: Literal["_primaires", "_colleges", "_lycees", "_total"] = "_total",
+    type_etablissement: Optional[Literal["Ecole", "Collège", "Lycée"]] = None
+):
+    """Update des indicateurs pour une aggregation donnee
+    
+    Args:
+        table (Table): Table cible pour l aggregation
+        suffix (str): Suffixe a ajouter au nom des indicateurs
+        type_etablissement (str): Type d etablissement a filtrer, 
+            si aucun type n est indique, tous les etablissements sont pris en compte
+    """
+    # Filtre sur le type d'etablissement si indique
+    agg_filter = f"type_etablissement = '{type_etablissement}'" if type_etablissement else "1 = 1"
+
+    update_query = f"""
+    -- Update des indicateurs simples
+    WITH 
+        agg AS (
+            SELECT 
+                {table.pkey} AS code_zone,
+                SUM(COALESCE(nb_eleves, 0)) AS nb_eleves,
+                COUNT(*) AS nb_etablissements,
+                SUM(CASE WHEN protection THEN 1 ELSE 0 END) AS nb_etablissements_proteges,
+                SUM(surface_exploitable_max) AS surface_exploitable_max,
+                SUM(potentiel_solaire) AS potentiel_solaire,
+
+            FROM {etablissements_table.name}
+            WHERE {agg_filter}
+            GROUP BY code_zone
+        )
+
+
+    UPDATE {table.name}
+    SET 
+        nb_eleves{suffix} = agg.nb_eleves,
+        nb_etablissements{suffix} = agg.nb_etablissements,
+        nb_etablissements_proteges{suffix} = agg.nb_etablissements_proteges,
+        surface_exploitable_max{suffix} = agg.surface_exploitable_max,
+        potentiel_solaire{suffix} = agg.potentiel_solaire,
+        potentiel_nb_foyers{suffix} = FLOOR(agg.potentiel_solaire / 5000),
+    FROM agg
+    WHERE {table.name}.{table.pkey} = agg.code_zone;
+
+
+    -- Update du top 3 des etablissements
+    WITH
+        top_etablissements AS (
+            SELECT
+                {table.pkey} AS code_zone,
+                json_object(
+                    'id', {etablissements_table.pkey}, 
+                    'libelle', nom_etablissement, 
+                    'type_etablissement', type_etablissement,
+                    'potentiel_solaire', potentiel_solaire
+                ) AS etablissement_json,
+            FROM {etablissements_table.name}
+            WHERE {agg_filter} 
+                AND NOT protection 
+                AND potentiel_solaire > 0
+            QUALIFY ROW_NUMBER() OVER (PARTITION BY {table.pkey} ORDER BY potentiel_solaire DESC) <= 3
+            ORDER BY code_zone, potentiel_solaire DESC
+        ),
+                
+        agg AS (
+            SELECT
+                code_zone,
+                list(etablissement_json)::JSON AS top_etablissements
+            FROM top_etablissements
+            GROUP BY code_zone
+        )
+            
+    UPDATE {table.name}
+    SET
+        top_etablissements{suffix} = agg.top_etablissements
+    FROM agg
+    WHERE {table.name}.{table.pkey} = agg.code_zone;
+
+
+    -- Update du nombre d etablissements par niveau potentiel
+    WITH 
+        zone_et_niveaux AS (
+            SELECT 
+                DISTINCT e.{table.pkey} AS code_zone, 
+                s.{seuils_niveaux_potentiels_table.pkey} AS niveau_potentiel
+            FROM {etablissements_table.name} e
+            CROSS JOIN {seuils_niveaux_potentiels_table.name} s
+        ),
+        
+        agg_par_zone_et_niveau AS (
+            SELECT 
+                {table.pkey} AS code_zone,
+                {seuils_niveaux_potentiels_table.pkey} AS niveau_potentiel,
+                COUNT(*) AS nb_etablissements,
+            FROM {etablissements_table.name}
+            WHERE {agg_filter}
+            GROUP BY code_zone, niveau_potentiel
+        ),
+        
+        nb_etablissements_par_zone_et_niveau AS (
+            SELECT 
+                zn.code_zone,
+                zn.niveau_potentiel,
+                COALESCE(agg.nb_etablissements, 0) AS nb_etablissements
+            FROM zone_et_niveaux zn
+            LEFT JOIN agg_par_zone_et_niveau agg
+                ON zn.code_zone = agg.code_zone
+                AND zn.niveau_potentiel = agg.niveau_potentiel
+            ORDER BY zn.code_zone, zn.niveau_potentiel
+        ),
+
+        agg_par_zone AS (
+            SELECT 
+                code_zone,
+                json_group_object(
+                    niveau_potentiel, nb_etablissements
+                ) AS nb_etablissements_par_niveau_potentiel
+            FROM nb_etablissements_par_zone_et_niveau
+            GROUP BY code_zone
+        )
+    
+    UPDATE {table.name}
+    SET
+        nb_etablissements_par_niveau_potentiel{suffix} = agg_par_zone.nb_etablissements_par_niveau_potentiel
+    FROM agg_par_zone
+    WHERE {table.name}.{table.pkey} = agg_par_zone.code_zone;
+    """
+    
+    with get_connection() as conn:
+        if type_etablissement is None:
+            message = (
+                f"Update des indicateurs de la table {table.name} "
+                "pour tout type d etablissement"
+            )
+        else:
+            message = (
+                f"Update des indicateurs de la table {table.name} "
+                f"pour les etablissements de type {type_etablissement}"
+            )
+
+        logger.info(message)
+        conn.execute(update_query)
+
+def update_additional_map_indicators_for_table(table: Table):
+    """Update des indicateurs additionnels pour la carte"""
+    update_query = f"""
+    WITH 
+        agg AS (
+            SELECT 
+                {table.pkey} AS code_zone,
+                SUM(CASE WHEN type_etablissement = 'Ecole' THEN potentiel_solaire ELSE 0 END) AS potentiel_solaire_primaires,
+                SUM(CASE WHEN type_etablissement = 'Collège' THEN potentiel_solaire ELSE 0 END) AS potentiel_solaire_colleges,
+                SUM(CASE WHEN type_etablissement = 'Lycée' THEN potentiel_solaire ELSE 0 END) AS potentiel_solaire_lycees,
+            FROM {etablissements_table.name}
+            GROUP BY code_zone
+        )
+
+    UPDATE {table.name}
+    SET 
+        potentiel_solaire_primaires = agg.potentiel_solaire_primaires,
+        potentiel_solaire_colleges = agg.potentiel_solaire_colleges,
+        potentiel_solaire_lycees = agg.potentiel_solaire_lycees,
+    FROM agg
+    WHERE {table.name}.{table.pkey} = agg.code_zone
+    """
+
+    with get_connection() as conn:
+        logger.info(f"Update des indicateurs additionnels pour la carte de la table {table.name}")
+        conn.execute(update_query)
+    
+
+def update_indicators_for_schools():
+    """Update des indicateurs de la table etablissements"""
+    with get_connection() as conn:
+        logger.info("Update des indicateurs de la table etablissements")
+
+        update_query = f"""
+        UPDATE {etablissements_table.name}
+        SET 
+            niveau_potentiel = s.niveau_potentiel,
+            potentiel_nb_foyers = FLOOR(potentiel_solaire / 5000)
+        FROM {seuils_niveaux_potentiels_table.name} s
+        WHERE potentiel_solaire >= s.min_potentiel_solaire
+              AND potentiel_solaire <= s.max_potentiel_solaire
+        """
+
+        conn.execute(update_query)
+
+
+def update_indicators_for_communes():
+    """Update des indicateurs de la table communes"""
+    # Calcul des indicateurs sans filtre sur le type d etablissement
+    update_indicators_for_table(table=communes_table)
+
+    # Calcul des indicateurs en filtrant sur les ecoles primaires
+    update_indicators_for_table(
+        table=communes_table, 
+        suffix="_primaires", 
+        type_etablissement="Ecole"
+    )
+
+    # Calcul des indicateurs utiles pour la carte par commune
+    update_additional_map_indicators_for_table(table=communes_table)
+
+
+def update_indicators_for_departements():
+    """Update des indicateurs de la table departements"""
+    # Calcul des indicateurs sans filtre sur le type d etablissement
+    update_indicators_for_table(table=departements_table, suffix="_total")
+
+    # Calcul des indicateurs en filtrant sur les colleges
+    update_indicators_for_table(
+        table=departements_table, 
+        suffix="_colleges", 
+        type_etablissement="Collège"
+    )
+
+    # Calcul des indicateurs utiles pour la carte par departement
+    update_additional_map_indicators_for_table(table=departements_table)
+
+
+def update_indicators_for_regions():
+    """Update des indicateurs de la table regions"""
+    # Calcul des indicateurs sans filtre sur le type d etablissement
+    update_indicators_for_table(table=regions_table, suffix="_total")
+
+    # Calcul des indicateurs en filtrant sur les lycees
+    update_indicators_for_table(
+        table=regions_table, 
+        suffix="_lycees", 
+        type_etablissement="Lycée"
+    )
+
+    # Calcul des indicateurs utiles pour la carte par region
+    update_additional_map_indicators_for_table(table=regions_table)
diff --git a/algorithme/potentiel_solaire/duckdb_manager.py b/algorithme/potentiel_solaire/duckdb_manager.py
deleted file mode 100644
index 1254ee89..00000000
--- a/algorithme/potentiel_solaire/duckdb_manager.py
+++ /dev/null
@@ -1,212 +0,0 @@
-from importlib.metadata import version
-
-import duckdb
-import pandas as pd
-
-from potentiel_solaire.constants import DUCK_DB_PATH
-
-
-def get_connection():
-    conn = duckdb.connect(DUCK_DB_PATH)
-
-    # Load Spatial extension
-    conn.execute("""
-        INSTALL spatial;
-        LOAD spatial;
-    """)
-
-    conn.commit()
-
-    return conn
-
-
-def get_departements():
-    with get_connection() as conn:
-        query = """
-        SELECT code_departement
-        FROM departements 
-        GROUP BY code_departement
-        """
-
-        return list(conn.query(query).df()["code_departement"].unique())
-
-
-def get_departements_for_region(code_region: str):
-    with get_connection() as conn:
-        query = f"""
-        SELECT code_departement
-        FROM departements 
-        WHERE code_region = '{code_region}'
-        GROUP BY code_departement
-        """
-
-        return list(conn.query(query).df()["code_departement"].unique())
-
-
-def get_regions():
-    with get_connection() as conn:
-        query = """
-        SELECT code_region
-        FROM regions 
-        GROUP BY code_region
-        """
-
-        return list(conn.query(query).df()["code_region"].unique())
-
-
-def save_solar_potential_by_school(
-    results_by_school: pd.DataFrame,
-    code_departement: str
-):
-    with get_connection() as conn:
-        conn.register("results_by_school", results_by_school)
-
-        code_version = version('potentiel_solaire')
-
-        update_query = f"""
-            UPDATE etablissements
-            SET 
-                surface_utile = results_by_school.surface_utile,
-                potentiel_solaire = results_by_school.potentiel_solaire,
-                protection = results_by_school.protection,
-                date_calcul = CURRENT_TIMESTAMP,
-                version = '{code_version}',
-                nb_batiments_associes = results_by_school.nb_batiments_associes
-            FROM
-                results_by_school
-            WHERE
-                etablissements.identifiant_de_l_etablissement = results_by_school.identifiant_de_l_etablissement
-        """
-
-        conn.execute(update_query)
-
-        query = f"""
-            SELECT
-                *,
-                ST_AsGeoJSON(geom) as geometry
-            FROM
-                etablissements
-            WHERE
-            code_departement = '{code_departement}'
-            """
-
-        stats_schools = conn.query(query).df()
-
-        return stats_schools
-
-
-def save_solar_potential_by_commune(
-    code_departement: str
-):
-    with get_connection() as conn:
-        update_query = f"""
-            UPDATE communes
-            SET 
-                surface_utile = agg.surface_utile,
-                potentiel_solaire = agg.potentiel_solaire,
-                count_etablissements = agg.count_etablissements,
-                count_etablissements_proteges = agg.count_etablissements_proteges
-            FROM (
-                SELECT 
-                    code_commune,
-                    SUM(surface_utile) AS surface_utile,
-                    SUM(potentiel_solaire) AS potentiel_solaire,
-                    COUNT(*) AS count_etablissements,
-                    SUM(CASE WHEN protection THEN 1 ELSE 0 END) AS count_etablissements_proteges
-                FROM etablissements
-                GROUP BY code_commune
-            ) AS agg
-            WHERE
-            communes.code_commune = agg.code_commune
-            AND communes.code_departement = '{code_departement}'
-            """
-
-        conn.execute(update_query)
-
-        query = f"""
-        SELECT
-            *,
-            ST_AsGeoJSON(geom) as geometry
-        FROM
-            communes
-        WHERE
-            code_departement = '{code_departement}'
-                """
-
-        return conn.query(query).df()
-
-
-def save_solar_potential_by_department(
-    code_departement: str
-):
-    with get_connection() as conn:
-        update_query = f"""
-            UPDATE departements
-            SET 
-                surface_utile = agg.surface_utile,
-                potentiel_solaire = agg.potentiel_solaire,
-                count_etablissements = agg.count_etablissements,
-                count_etablissements_proteges = agg.count_etablissements_proteges
-            FROM (
-                SELECT 
-                    code_departement,
-                    SUM(surface_utile) AS surface_utile,
-                    SUM(potentiel_solaire) AS potentiel_solaire,
-                    COUNT(*) AS count_etablissements,
-                    SUM(CASE WHEN protection THEN 1 ELSE 0 END) AS count_etablissements_proteges
-                FROM etablissements
-                GROUP BY code_departement
-            ) AS agg
-            WHERE
-                departements.code_departement = agg.code_departement
-                AND departements.code_departement = '{code_departement}'
-            """
-
-        conn.execute(update_query)
-
-        query = f"""
-            SELECT
-                *,
-                ST_AsGeoJSON(geom) as geometry 
-            FROM
-                departements WHERE code_departement = '{code_departement}'
-            """
-        return conn.query(query).df()
-
-
-def save_solar_potential_by_region(
-    code_region: str
-):
-    with get_connection() as conn:
-        update_query = f"""
-            UPDATE regions
-            SET 
-                surface_utile = agg.surface_utile,
-                potentiel_solaire = agg.potentiel_solaire,
-                count_etablissements = agg.count_etablissements,
-                count_etablissements_proteges = agg.count_etablissements_proteges
-            FROM (
-                SELECT 
-                    code_region,
-                    SUM(surface_utile) AS surface_utile,
-                    SUM(potentiel_solaire) AS potentiel_solaire,
-                    COUNT(*) AS count_etablissements,
-                    SUM(CASE WHEN protection THEN 1 ELSE 0 END) AS count_etablissements_proteges
-                FROM etablissements
-                GROUP BY code_region
-            ) AS agg
-            WHERE
-                regions.code_region = agg.code_region
-                AND regions.code_region = '{code_region}'
-            """
-
-        conn.execute(update_query)
-
-        query = f"""
-            SELECT
-                *,
-                ST_AsGeoJSON(geom) as geometry 
-            FROM
-                regions WHERE code_region = '{code_region}'
-            """
-        return conn.query(query).df()
diff --git a/algorithme/potentiel_solaire/geojson_files_creator.py b/algorithme/potentiel_solaire/geojson_files_creator.py
deleted file mode 100644
index eafd2aa7..00000000
--- a/algorithme/potentiel_solaire/geojson_files_creator.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import pandas as pd
-import geopandas as gpd
-import shapely.geometry
-import json
-
-from potentiel_solaire.constants import DEFAULT_CRS
-from potentiel_solaire.logger import get_logger
-
-logger = get_logger()
-
-
-def export_to_geofile(
-    stats: pd.DataFrame,
-    output_filepath: str,
-    driver: str = 'GeoJSON',
-    layer: str = None,
-    crs: int = DEFAULT_CRS
-) -> bool:
-    """
-    Exporte un DataFrame Pandas contenant des géométries dans un fichier au format souhaite.
-
-    Args:
-        stats (pd.DataFrame): Le DataFrame contenant les statistiques, avec une colonne nommée 'geometry' qui contient des géométries 
-                               au format GeoJSON (sous forme de chaînes JSON).
-        output_filepath (str): Le chemin complet du fichier de sortie
-        driver (str): Le format du fichier de sortie (.geojson, .gpkg, ...)
-        layer (str): Le nom du layer si le format de sortie est GPKG
-        crs (int): Le crs du fichier de sortie
-
-    Returns:
-        bool: Retourne `True` si l'exportation a réussi, sinon lève une exception.
-
-    Raises:
-        ValueError: Si la colonne 'geometry' n'est pas présente dans le DataFrame ou si aucune géométrie valide n'a pu être convertie.
-        Exception: Si une erreur générale survient pendant la conversion ou l'exportation.
-
-    Example:
-        >>> import pandas as pd
-        >>> df = pd.DataFrame({
-        >>>     'surface_utile': [11120, 800000],
-        >>>     'geometry': ['{"type":"Polygon","coordinates":[[[2.319887543, 48.8588443]]]}', 
-        >>>                  '{"type":"Polygon","coordinates":[[[2.319888, 48.858845]]]}']
-        >>> })
-        >>> export_to_geofile(df, "output.geojson")
-        True
-
-    Notes:
-        - Les géométries dans la colonne 'geometry' doivent être des chaînes JSON valides au format GeoJSON.
-        - Si une géométrie est invalide ou ne peut être convertie en objet Shapely, elle sera ignorée.
-    """
-    try:
-        if 'geometry' not in stats.columns:
-            raise ValueError("La colonne 'geometry' n'existe pas dans le DataFrame.")
-        
-        # Convertir la colonne 'geometry' (qui est en format GeoJSON) en objets géométriques Shapely
-        def safe_geojson_to_geometry(geojson_value):
-            try:
-                if not isinstance(geojson_value, str) or not geojson_value.strip():
-                    return None  # Ignore les valeurs None ou vides
-                
-                geom_dict = json.loads(geojson_value)
-                return shapely.geometry.shape(geom_dict)
-            except Exception as e:
-                logger.error("Erreur de conversion pour la géométrie: %s", e)
-                return None
-
-        # Pour pouvoir ré-exécuter la fonction sans souci.
-        df = stats.copy()
-        df['geometry'] = df['geometry'].apply(safe_geojson_to_geometry)
-        df = df.dropna(subset=['geometry'])
-
-        if df['geometry'].empty:
-            raise ValueError("Aucune géométrie valide après conversion.")
-
-        gdf = gpd.GeoDataFrame(df, geometry='geometry', crs=crs)
-
-        if driver == 'GPKG':
-            gdf.to_file(output_filepath, layer=layer, driver=driver)
-        else:
-            gdf.to_file(output_filepath, driver=driver)
-
-        logger.info("Fichier '%s' créé avec succès ✅", output_filepath)
-
-        return True
-    except Exception as e:
-        logger.error(f"Erreur lors de l'exportation vers {driver}: {e}")
-        raise Exception(f"Erreur lors de l'exportation vers {driver}: {e}") from e
diff --git a/algorithme/potentiel_solaire/logger.py b/algorithme/potentiel_solaire/logger.py
index cfb4f9dc..df6346c1 100644
--- a/algorithme/potentiel_solaire/logger.py
+++ b/algorithme/potentiel_solaire/logger.py
@@ -8,7 +8,7 @@ def setup_logger():
     global logger_init
     logging.basicConfig(
         level=logging.INFO,
-        format='%(asctime)s - %(levelname)s - %(name)s - %(pathname)s - %(funcName)s - %(message)s',
+        format='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s',
         handlers=[logging.StreamHandler()]
     )
     logger_init = True
diff --git a/algorithme/potentiel_solaire/main.py b/algorithme/potentiel_solaire/main.py
index dd619471..72f3bd2a 100644
--- a/algorithme/potentiel_solaire/main.py
+++ b/algorithme/potentiel_solaire/main.py
@@ -5,10 +5,14 @@
 from papermill.exceptions import PapermillExecutionError
 
 from potentiel_solaire.constants import ALGORITHME_FOLDER, RESULTS_FOLDER
-from potentiel_solaire.duckdb_manager import (
-    get_departements, get_regions,
+from potentiel_solaire.database.queries import (
+    get_departements, 
+    get_regions,
     get_departements_for_region,
-    save_solar_potential_by_region,
+    update_indicators_for_communes,
+    update_indicators_for_departements,
+    update_indicators_for_regions, 
+    update_indicators_for_schools,
 )
 from potentiel_solaire.logger import get_logger
 
@@ -88,17 +92,10 @@ def run_pipeline_algorithme():
             with open(error_file_path, "w") as error_file:
                 error_file.write(traceback.format_exc())
 
-    # aggregation des resultats pour les regions
-    agg_on_regions = []
-    if run_on_france:
-        agg_on_regions = codes_regions
-    elif code_region:
-        agg_on_regions.append(code_region)
-
-    for region in agg_on_regions:
-        logger.info("Aggregation on region %s", region)
-        save_solar_potential_by_region(code_region=region)
-
 
-if __name__ == "__main__":
-    run_pipeline_algorithme()
+def update_database_indicators():
+    """Update indicators in database used by application"""
+    update_indicators_for_schools()
+    update_indicators_for_communes()
+    update_indicators_for_departements()
+    update_indicators_for_regions()
diff --git a/algorithme/pyproject.toml b/algorithme/pyproject.toml
index 4c4bbdfe..ef4f1902 100644
--- a/algorithme/pyproject.toml
+++ b/algorithme/pyproject.toml
@@ -43,6 +43,7 @@ tox = "^4.4.8"
 
 [tool.poetry.scripts]
 run-pipeline-algorithme = "potentiel_solaire.main:run_pipeline_algorithme"
+update-database-indicators = "potentiel_solaire.main:update_database_indicators"
 
 [tool.ruff]
 # Same as Black.


Reviews:
user: login: GaelleNS
body: <3
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T17:53:14Z
user: login: machbry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T18:11:35Z
user: login: jbuiquan
body: Il manque juste le potentiel_solaire pour chaque type d'etablissement + le total dans chaque table de zones aggrégés (regions, departements, communes) sinon ca me semble bon!
Ca nous servira pour les filtres de la carte.

ex : 
potentiel_solaire_total 
potentiel_solaire_lycees
potentiel_solaire_colleges
potentiel_solaire_primaires
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T15:10:50Z
user: login: jbuiquan
body: Il manque le `type_etablissement` pour `top_etablissements_total` des zones aggrégés mais c'est bon pour moi pour cette première version.
J'ai testé avec l'appli et je crois qu'il ne me manque que ça ! Merci pour le boulot :) 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-16T22:16:07Z

Review comments:
diff_hunk: @@ -0,0 +1,346 @@
+"""add indicators for app fiches zone
+
+Revision ID: b80cbbbf68d9
+Revises: 49d44d790ee6
+Create Date: 2025-04-05 14:57:19.102683
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'b80cbbbf68d9'
+down_revision: Union[str, None] = '49d44d790ee6'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+# schema
+etablissements_table = "etablissements"
+communes_table = "communes"
+departements_table = "departements"
+regions_table = "regions"
+
+new_columns_for_each_table = {
+    communes_table: {
+        "nb_eleves_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_primaires": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_primaires": "INTEGER DEFAULT 0",
+        "potentiel_solaire_primaires": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_primaires": "INTEGER DEFAULT 0",
+        "top_etablissements_primaires": "JSON",
+    },
+    departements_table: {
+        "nb_eleves_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_colleges": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_colleges": "INTEGER DEFAULT 0",
+        "potentiel_solaire_colleges": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_colleges": "INTEGER DEFAULT 0",
+        "top_etablissements_colleges": "JSON",
+    },
+    regions_table: {
+        "nb_eleves_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_lycees": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_lycees": "INTEGER DEFAULT 0",
+        "potentiel_solaire_lycees": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_lycees": "INTEGER DEFAULT 0",
+        "top_etablissements_lycees": "JSON",
+    },
+}
+
+
+delete_columns = {
+    "surface_utile": "INTEGER DEFAULT 0",
+    "potentiel_solaire": "BIGINT DEFAULT 0",
+    "count_etablissements": "INTEGER DEFAULT 0",
+    "count_etablissements_proteges": "INTEGER DEFAULT 0",
+}
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    op.execute("""
+        INSTALL spatial;
+        LOAD spatial;
+    """)
+
+    # Rename column for surface_utile in etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        RENAME COLUMN surface_utile TO surface_exploitable_max
+    """)
+
+    # Add new columns to each table
+    for table_name, new_columns in new_columns_for_each_table.items():
+        for column_name, column_type in new_columns.items():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                ADD COLUMN IF NOT EXISTS {column_name} {column_type}
+            """)
+
+    # Remove columns for each table
+    for table_name in new_columns_for_each_table.keys():
+        for column_name in delete_columns.keys():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                DROP COLUMN IF EXISTS {column_name}
+            """)
+    
+    # Update communes table data
+    op.execute(f"""
+        UPDATE {communes_table}
+        SET 
+            nb_eleves_primaires = agg.nb_eleves,
+            nb_etablissements_primaires = agg.nb_etablissements,
+            nb_etablissements_proteges_primaires = agg.nb_etablissements_proteges,
+            surface_exploitable_max_primaires = agg.surface_exploitable_max,
+            potentiel_solaire_primaires = agg.potentiel_solaire,
+            potentiel_nb_foyers_primaires = ROUND(agg.potentiel_solaire / 5000),
path: algorithme/database/versions/2025_04_05_1457_add_indicators_for_app_fiches_zone.py
user: login: GaelleNS
body: round va arrondir au dessus et dessous, j'aurais tendance à favoriser l'arrondissement au dessous donc FLOOR()
created_at: 2025-04-08T17:39:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,346 @@
+"""add indicators for app fiches zone
+
+Revision ID: b80cbbbf68d9
+Revises: 49d44d790ee6
+Create Date: 2025-04-05 14:57:19.102683
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'b80cbbbf68d9'
+down_revision: Union[str, None] = '49d44d790ee6'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+# schema
+etablissements_table = "etablissements"
+communes_table = "communes"
+departements_table = "departements"
+regions_table = "regions"
+
+new_columns_for_each_table = {
+    communes_table: {
+        "nb_eleves_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_primaires": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_primaires": "INTEGER DEFAULT 0",
+        "potentiel_solaire_primaires": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_primaires": "INTEGER DEFAULT 0",
+        "top_etablissements_primaires": "JSON",
+    },
+    departements_table: {
+        "nb_eleves_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_colleges": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_colleges": "INTEGER DEFAULT 0",
+        "potentiel_solaire_colleges": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_colleges": "INTEGER DEFAULT 0",
+        "top_etablissements_colleges": "JSON",
+    },
+    regions_table: {
+        "nb_eleves_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_lycees": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_lycees": "INTEGER DEFAULT 0",
+        "potentiel_solaire_lycees": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_lycees": "INTEGER DEFAULT 0",
+        "top_etablissements_lycees": "JSON",
+    },
+}
+
+
+delete_columns = {
+    "surface_utile": "INTEGER DEFAULT 0",
+    "potentiel_solaire": "BIGINT DEFAULT 0",
+    "count_etablissements": "INTEGER DEFAULT 0",
+    "count_etablissements_proteges": "INTEGER DEFAULT 0",
+}
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    op.execute("""
+        INSTALL spatial;
+        LOAD spatial;
+    """)
+
+    # Rename column for surface_utile in etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        RENAME COLUMN surface_utile TO surface_exploitable_max
+    """)
+
+    # Add new columns to each table
+    for table_name, new_columns in new_columns_for_each_table.items():
+        for column_name, column_type in new_columns.items():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                ADD COLUMN IF NOT EXISTS {column_name} {column_type}
+            """)
+
+    # Remove columns for each table
+    for table_name in new_columns_for_each_table.keys():
+        for column_name in delete_columns.keys():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                DROP COLUMN IF EXISTS {column_name}
+            """)
+    
+    # Update communes table data
+    op.execute(f"""
+        UPDATE {communes_table}
+        SET 
+            nb_eleves_primaires = agg.nb_eleves,
+            nb_etablissements_primaires = agg.nb_etablissements,
+            nb_etablissements_proteges_primaires = agg.nb_etablissements_proteges,
+            surface_exploitable_max_primaires = agg.surface_exploitable_max,
+            potentiel_solaire_primaires = agg.potentiel_solaire,
+            potentiel_nb_foyers_primaires = ROUND(agg.potentiel_solaire / 5000),
+        FROM (
+            SELECT 
+                code_commune,
+                SUM(nb_eleves) AS nb_eleves,
+                COUNT(*) AS nb_etablissements,
path: algorithme/database/versions/2025_04_05_1457_add_indicators_for_app_fiches_zone.py
user: login: GaelleNS
body: comment on gère les null ?
created_at: 2025-04-08T17:41:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,346 @@
+"""add indicators for app fiches zone
+
+Revision ID: b80cbbbf68d9
+Revises: 49d44d790ee6
+Create Date: 2025-04-05 14:57:19.102683
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'b80cbbbf68d9'
+down_revision: Union[str, None] = '49d44d790ee6'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+# schema
+etablissements_table = "etablissements"
+communes_table = "communes"
+departements_table = "departements"
+regions_table = "regions"
+
+new_columns_for_each_table = {
+    communes_table: {
+        "nb_eleves_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_primaires": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_primaires": "INTEGER DEFAULT 0",
+        "potentiel_solaire_primaires": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_primaires": "INTEGER DEFAULT 0",
+        "top_etablissements_primaires": "JSON",
+    },
+    departements_table: {
+        "nb_eleves_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_colleges": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_colleges": "INTEGER DEFAULT 0",
+        "potentiel_solaire_colleges": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_colleges": "INTEGER DEFAULT 0",
+        "top_etablissements_colleges": "JSON",
+    },
+    regions_table: {
+        "nb_eleves_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_lycees": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_lycees": "INTEGER DEFAULT 0",
+        "potentiel_solaire_lycees": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_lycees": "INTEGER DEFAULT 0",
+        "top_etablissements_lycees": "JSON",
+    },
+}
+
+
+delete_columns = {
+    "surface_utile": "INTEGER DEFAULT 0",
+    "potentiel_solaire": "BIGINT DEFAULT 0",
+    "count_etablissements": "INTEGER DEFAULT 0",
+    "count_etablissements_proteges": "INTEGER DEFAULT 0",
+}
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    op.execute("""
+        INSTALL spatial;
+        LOAD spatial;
+    """)
+
+    # Rename column for surface_utile in etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        RENAME COLUMN surface_utile TO surface_exploitable_max
+    """)
+
+    # Add new columns to each table
+    for table_name, new_columns in new_columns_for_each_table.items():
+        for column_name, column_type in new_columns.items():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                ADD COLUMN IF NOT EXISTS {column_name} {column_type}
+            """)
+
+    # Remove columns for each table
+    for table_name in new_columns_for_each_table.keys():
+        for column_name in delete_columns.keys():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                DROP COLUMN IF EXISTS {column_name}
+            """)
+    
+    # Update communes table data
+    op.execute(f"""
+        UPDATE {communes_table}
+        SET 
+            nb_eleves_primaires = agg.nb_eleves,
+            nb_etablissements_primaires = agg.nb_etablissements,
+            nb_etablissements_proteges_primaires = agg.nb_etablissements_proteges,
+            surface_exploitable_max_primaires = agg.surface_exploitable_max,
+            potentiel_solaire_primaires = agg.potentiel_solaire,
+            potentiel_nb_foyers_primaires = ROUND(agg.potentiel_solaire / 5000),
+        FROM (
+            SELECT 
+                code_commune,
+                SUM(nb_eleves) AS nb_eleves,
+                COUNT(*) AS nb_etablissements,
+                SUM(CASE WHEN protection THEN 1 ELSE 0 END) AS nb_etablissements_proteges,
+                SUM(surface_exploitable_max) AS surface_exploitable_max,
path: algorithme/database/versions/2025_04_05_1457_add_indicators_for_app_fiches_zone.py
user: login: GaelleNS
body: c'est forcement un int ? pas un float ? pareil pour la ligne suivante
created_at: 2025-04-08T17:43:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,346 @@
+"""add indicators for app fiches zone
+
+Revision ID: b80cbbbf68d9
+Revises: 49d44d790ee6
+Create Date: 2025-04-05 14:57:19.102683
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'b80cbbbf68d9'
+down_revision: Union[str, None] = '49d44d790ee6'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+# schema
+etablissements_table = "etablissements"
+communes_table = "communes"
+departements_table = "departements"
+regions_table = "regions"
+
+new_columns_for_each_table = {
+    communes_table: {
+        "nb_eleves_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_primaires": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_primaires": "INTEGER DEFAULT 0",
+        "potentiel_solaire_primaires": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_primaires": "INTEGER DEFAULT 0",
+        "top_etablissements_primaires": "JSON",
+    },
+    departements_table: {
+        "nb_eleves_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_colleges": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_colleges": "INTEGER DEFAULT 0",
+        "potentiel_solaire_colleges": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_colleges": "INTEGER DEFAULT 0",
+        "top_etablissements_colleges": "JSON",
+    },
+    regions_table: {
+        "nb_eleves_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_lycees": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_lycees": "INTEGER DEFAULT 0",
+        "potentiel_solaire_lycees": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_lycees": "INTEGER DEFAULT 0",
+        "top_etablissements_lycees": "JSON",
+    },
+}
+
+
+delete_columns = {
+    "surface_utile": "INTEGER DEFAULT 0",
+    "potentiel_solaire": "BIGINT DEFAULT 0",
+    "count_etablissements": "INTEGER DEFAULT 0",
+    "count_etablissements_proteges": "INTEGER DEFAULT 0",
+}
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    op.execute("""
+        INSTALL spatial;
+        LOAD spatial;
+    """)
+
+    # Rename column for surface_utile in etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        RENAME COLUMN surface_utile TO surface_exploitable_max
+    """)
+
+    # Add new columns to each table
+    for table_name, new_columns in new_columns_for_each_table.items():
+        for column_name, column_type in new_columns.items():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                ADD COLUMN IF NOT EXISTS {column_name} {column_type}
+            """)
+
+    # Remove columns for each table
+    for table_name in new_columns_for_each_table.keys():
+        for column_name in delete_columns.keys():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                DROP COLUMN IF EXISTS {column_name}
+            """)
+    
+    # Update communes table data
+    op.execute(f"""
+        UPDATE {communes_table}
+        SET 
+            nb_eleves_primaires = agg.nb_eleves,
+            nb_etablissements_primaires = agg.nb_etablissements,
+            nb_etablissements_proteges_primaires = agg.nb_etablissements_proteges,
+            surface_exploitable_max_primaires = agg.surface_exploitable_max,
+            potentiel_solaire_primaires = agg.potentiel_solaire,
+            potentiel_nb_foyers_primaires = ROUND(agg.potentiel_solaire / 5000),
+        FROM (
+            SELECT 
+                code_commune,
+                SUM(nb_eleves) AS nb_eleves,
+                COUNT(*) AS nb_etablissements,
+                SUM(CASE WHEN protection THEN 1 ELSE 0 END) AS nb_etablissements_proteges,
+                SUM(surface_exploitable_max) AS surface_exploitable_max,
+                SUM(potentiel_solaire) AS potentiel_solaire,
+            FROM {etablissements_table}
+            WHERE type_etablissement = 'Ecole'
+            GROUP BY code_commune
+        ) AS agg
+        WHERE {communes_table}.code_commune = agg.code_commune
+    """)
+
+    op.execute(f"""
+        UPDATE {communes_table}
+        SET 
+            top_etablissements_primaires = agg.top_etablissements_primaires
+        FROM (
+            SELECT 
+                code_commune,
+                list(etablissement_json)::JSON AS top_etablissements_primaires,
+            FROM (
+                SELECT 
+                    code_commune,
+                    ROW_NUMBER() OVER (PARTITION BY code_commune ORDER BY potentiel_solaire DESC) AS rank_commune,
+                    json_object(
+                        'id', identifiant_de_l_etablissement, 
+                        'libelle', nom_etablissement, 
+                        'potentiel_solaire', potentiel_solaire
+                    ) AS etablissement_json,
+                FROM {etablissements_table}
+                WHERE type_etablissement = 'Ecole' 
+                    AND NOT protection 
+                    AND potentiel_solaire > 0
+                ORDER BY code_commune, potentiel_solaire DESC
+            )
+            WHERE rank_commune <= 3
path: algorithme/database/versions/2025_04_05_1457_add_indicators_for_app_fiches_zone.py
user: login: GaelleNS
body: plutôt un qualify et enlève la sous requête dans ce cas
created_at: 2025-04-08T17:52:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,346 @@
+"""add indicators for app fiches zone
+
+Revision ID: b80cbbbf68d9
+Revises: 49d44d790ee6
+Create Date: 2025-04-05 14:57:19.102683
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'b80cbbbf68d9'
+down_revision: Union[str, None] = '49d44d790ee6'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+# schema
+etablissements_table = "etablissements"
+communes_table = "communes"
+departements_table = "departements"
+regions_table = "regions"
+
+new_columns_for_each_table = {
+    communes_table: {
+        "nb_eleves_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_primaires": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_primaires": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_primaires": "INTEGER DEFAULT 0",
+        "potentiel_solaire_primaires": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_primaires": "INTEGER DEFAULT 0",
+        "top_etablissements_primaires": "JSON",
+    },
+    departements_table: {
+        "nb_eleves_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_colleges": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_colleges": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_colleges": "INTEGER DEFAULT 0",
+        "potentiel_solaire_colleges": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_colleges": "INTEGER DEFAULT 0",
+        "top_etablissements_colleges": "JSON",
+    },
+    regions_table: {
+        "nb_eleves_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_lycees": "INTEGER DEFAULT 0",
+        "nb_etablissements_proteges_lycees": "INTEGER DEFAULT 0",
+        "surface_exploitable_max_lycees": "INTEGER DEFAULT 0",
+        "potentiel_solaire_lycees": "BIGINT DEFAULT 0",
+        "potentiel_nb_foyers_lycees": "INTEGER DEFAULT 0",
+        "top_etablissements_lycees": "JSON",
+    },
+}
+
+
+delete_columns = {
+    "surface_utile": "INTEGER DEFAULT 0",
+    "potentiel_solaire": "BIGINT DEFAULT 0",
+    "count_etablissements": "INTEGER DEFAULT 0",
+    "count_etablissements_proteges": "INTEGER DEFAULT 0",
+}
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    op.execute("""
+        INSTALL spatial;
+        LOAD spatial;
+    """)
+
+    # Rename column for surface_utile in etablissements table
+    op.execute(f"""
+        ALTER TABLE {etablissements_table}
+        RENAME COLUMN surface_utile TO surface_exploitable_max
+    """)
+
+    # Add new columns to each table
+    for table_name, new_columns in new_columns_for_each_table.items():
+        for column_name, column_type in new_columns.items():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                ADD COLUMN IF NOT EXISTS {column_name} {column_type}
+            """)
+
+    # Remove columns for each table
+    for table_name in new_columns_for_each_table.keys():
+        for column_name in delete_columns.keys():
+            op.execute(f"""
+                ALTER TABLE {table_name}
+                DROP COLUMN IF EXISTS {column_name}
+            """)
+    
+    # Update communes table data
+    op.execute(f"""
+        UPDATE {communes_table}
+        SET 
+            nb_eleves_primaires = agg.nb_eleves,
+            nb_etablissements_primaires = agg.nb_etablissements,
+            nb_etablissements_proteges_primaires = agg.nb_etablissements_proteges,
+            surface_exploitable_max_primaires = agg.surface_exploitable_max,
+            potentiel_solaire_primaires = agg.potentiel_solaire,
+            potentiel_nb_foyers_primaires = ROUND(agg.potentiel_solaire / 5000),
+        FROM (
+            SELECT 
+                code_commune,
+                SUM(nb_eleves) AS nb_eleves,
+                COUNT(*) AS nb_etablissements,
path: algorithme/database/versions/2025_04_05_1457_add_indicators_for_app_fiches_zone.py
user: login: machbry
body: -> ajout COLAESCE 
created_at: 2025-04-08T18:11:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: jbuiquan
created_at: 2025-04-06T19:08:36Z
body: Tout m'a l'air bon, pour l'histoire des potentiels avec filtres sur le type d'établissement, est ce que ça sera dans cette PR ?
author_association: COLLABORATOR
user: login: machbry
created_at: 2025-04-06T19:13:36Z
body: @jbuiquan Oui tout est filtré par type d'établissement.
C'est pour ca que dans les noms des colonnes il y a `_primaires`, `_colleges` ou `_lycees`.
author_association: CONTRIBUTOR
user: login: jbuiquan
created_at: 2025-04-06T20:14:00Z
body: @machbry sur la carte chaque zone affiche le potentiel tout type confondus, non ? @GaelleNS  
On rajoute des filtres permettant de n'afficher que le potentiel selon ces filtres.

De ce que j'avais compris on aurait donc pour chaque niveaux, le potentiel lycée + le potentiel college + le potentiel primaire.
Sur la fiche on n'affichera que les données du niveau associé (par ex, region -> lycées), mais sur la carte visualiserait tout type confondu. 
author_association: COLLABORATOR
user: login: machbry
created_at: 2025-04-08T18:06:26Z
body: TODO :
- ajout total de chaque indicateur à chaque niveau
- ajout colonne niveau_potentiel (`1_HIGH`  / `2_GOOD` / `3_LIMITED`) pour les établissements
- ajout colonne JSON avec nb d'établissements pour chaque niveau (pour les communes / départements / regions)
- ajout table code_postal
author_association: CONTRIBUTOR
user: login: machbry
created_at: 2025-04-12T21:16:03Z
body: @jbuiquan @GaelleNS 

J'ai ajouté tout ce qu'il manquait après notre discussion de mardi soir. 
Ainsi qu'une refacto pour éviter de la duplication au niveau du code, il y en avait beaucoup avant même les dernières modifications.

Pour récupérer la base de données :
- docker pull ghcr.io/dataforgoodfr/13_potentiel_solaire_db:0.1.20250412
- ou télécharger directement [ici](https://drive.google.com/file/d/1t5C-zY4GNu5S84QCvUOnsIJIXNwN_1OA/view?usp=sharing)

J'ai aussi mis à jour mon message initiale sur la PR pour détailler tous les changements sur la database.

N'hésitez pas à me poser des questions !
author_association: CONTRIBUTOR
user: login: jbuiquan
created_at: 2025-04-14T17:48:20Z
body: @machbry  je m'attendais à avoir `potentiel_nb_foyers` dans la table etablissement. On l'affiche sur la fiche etablissement.
En tout cas merci pour toute la description, c'est top !
author_association: COLLABORATOR
user: login: jbuiquan
created_at: 2025-04-14T18:36:54Z
body: @GaelleNS  qu'est ce qu'on fait finalement pour les seuils en fonction du niveau d'aggrégation ? Dans cette version j'ai des seuils au niveau établissement, mais qu'en est il des autres niveaux ? 
author_association: COLLABORATOR
user: login: GaelleNS
created_at: 2025-04-15T16:44:03Z
body: > @GaelleNS qu'est ce qu'on fait finalement pour les seuils en fonction du niveau d'aggrégation ? Dans cette version j'ai des seuils au niveau établissement, mais qu'en est il des autres niveaux ?

On garde les seuils "fait maison" sur les autres niveaux pour le moment.
author_association: COLLABORATOR
user: login: jbuiquan
created_at: 2025-04-16T15:12:18Z
body: > Il manque juste le potentiel_solaire pour chaque type d'etablissement + le total dans chaque table de zones aggrégés (regions, departements, communes) sinon ca me semble bon! Ca nous servira pour les filtres de la carte.

@GaelleNS  par contre j'ai vraiment du mal a voir comment on va reussir a adapter l'echelle/ la legende de la carte avec l'utilisation de ces filtres


author_association: COLLABORATOR
user: login: machbry
created_at: 2025-04-16T20:49:14Z
body: Bonsoir @jbuiquan @GaelleNS ,

Normalement c'est complet désormais. J'ai ajouté le potentiel_nb_foyers sur la table des établissements et les indicateurs manquants pour la carte. J'ai mis à jour mon premier message comme ca t'as l'exhaustivité des changements @jbuiquan.

Pour récupérer la base de données :
- docker pull ghcr.io/dataforgoodfr/13_potentiel_solaire_db:0.1.20250416
- ou télécharger directement [ici](https://drive.google.com/file/d/1GAK-t7lX2MvOkgCEbt1jPz0gBvPTurwY/view?usp=sharing)

Un de vous peut me valider la PR ? Merci
author_association: CONTRIBUTOR