Skip to content

Commit 54217c9

Browse files
qloridantQuentin Loridant
andauthored
feat(ETL): Implementation des champs cantines analyses (#4538)
Co-authored-by: Quentin Loridant <q.loridant@gmail.com>
1 parent 59354d1 commit 54217c9

8 files changed

Lines changed: 334 additions & 57 deletions

File tree

data/schemas/schema_analysis_cantines.json

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,161 @@
22
"encoding": "utf-8",
33
"fields": [
44
{
5+
"example": "1143",
56
"name": "id",
6-
"type": "integer",
7-
"example": "1143"
7+
"type": "integer"
88
},
99
{
10+
"example": "Ecole Jean Jaurès",
1011
"name": "nom",
11-
"type": "string",
12-
"example": "Ecole Jean Jaurès"
12+
"type": "string"
1313
},
1414
{
15-
"name": "siret",
16-
"type": "string",
17-
"example": "11007001800012",
1815
"constraints": {
1916
"pattern": "^[0-9]{14}$",
2017
"required": true
21-
}
18+
},
19+
"example": "11007001800012",
20+
"name": "siret",
21+
"type": "string"
22+
},
23+
{
24+
"constraints": {
25+
"pattern": "^([013-9]\\d|2[AB1-9])\\d{3}$",
26+
"required": false
27+
},
28+
"description": "Le code INSEE de la commune dans laquelle la cantine se trouve.",
29+
"example": "751005",
30+
"name": "code_insee_commune",
31+
"title": "Code Insee Commune",
32+
"type": "string"
33+
},
34+
{
35+
"description": "Libellé de la cantine dans laquelle se trouve la cantine",
36+
"example": "Paris",
37+
"name": "libelle_commune",
38+
"title": "Libellé commune",
39+
"type": "string"
40+
},
41+
{
42+
"description": "Code Insee du département dans lequel se trouve la cantine",
43+
"example": "75",
44+
"name": "departement",
45+
"title": "Code Département",
46+
"type": "string"
47+
},
48+
{
49+
"description": "Libellé du département dans lequel se trouve la cantine",
50+
"example": "Paris",
51+
"name": "departement_lib",
52+
"title": "Département Libellé",
53+
"type": "string"
54+
},
55+
{
56+
"description": "Code Insee de la région dans lequel se trouve la cantine",
57+
"example": "11",
58+
"name": "region",
59+
"title": "Code Région",
60+
"type": "string"
2261
},
2362
{
63+
"description": "Libellé de la région dans lequel se trouve la cantine",
64+
"example": "Île-de-France",
65+
"name": "region_lib",
66+
"title": "Région libellé",
67+
"type": "string"
68+
},
69+
{
70+
"example": "2024-01-27",
2471
"name": "date_creation",
25-
"type": "date",
26-
"example": "2024-01-27"
72+
"title": "Date création",
73+
"type": "date"
2774
},
2875
{
76+
"example": "2024-02-17",
2977
"name": "date_modification",
30-
"type": "date",
31-
"example": "2024-02-17"
78+
"title": "Date modification",
79+
"type": "date"
80+
},
81+
{
82+
"description": "Nombre de repas servis, en moyenne, par jour dans la cantine",
83+
"example": "100",
84+
"name": "nbre_repas_jour",
85+
"title": "Nombre de repas par jour",
86+
"type": "integer"
87+
},
88+
{
89+
"description": "Nombre de repas servis, en moyenne, par an dans la cantine",
90+
"example": "20000",
91+
"name": "nbre_repas_an",
92+
"title": "Nombre de repas par an",
93+
"type": "integer"
94+
},
95+
{
96+
"description": "Modèle économique de la cantine. Il existe 2 types différents : public (cantine publique), private (cantine privée).",
97+
"example": "public",
98+
"name": "modele_economique",
99+
"title": "Modèle Economique",
100+
"type": "string"
101+
},
102+
{
103+
"description": "Gestionnaire de la cantine. La cantine peut-être gérée directement ou de manière concédée. Si la valeur est 'concédée', la délégation peut être totale ou partielle",
104+
"example": "conceded",
105+
"name": "type_gestion",
106+
"title": "Type de Gestion/Management",
107+
"type": "string"
108+
},
109+
{
110+
"description": "Lieu de production et de service des repas. Il existe 4 types différents : central (cuisine centrale sans lieu de consommation), central_serving (cuisine centrale qui accueille aussi des convives sur place), site (cantine qui produit les repas sur place), site_cooked_elsewhere (cantine qui sert des repas preparés par une cuisine centrale, appelé également satellite). Dans ce dernier cas, le champ central_producer_siret renseigne l'identifiant SIRET de la cuisine préparant les repas. Dans le cas d'une cantine qui cuisine pour d'autres cantines, le champ satellite_canteens_count renseigne le nombre de cantines satellites.",
111+
"example": "central",
112+
"name": "type_production",
113+
"title": "Type de Production",
114+
"type": "string"
115+
},
116+
{
117+
"description": "Nombre de cantines satellites pour une cantine qui produit les repas sur place",
118+
"example": "0",
119+
"name": "nombre_satellites",
120+
"title": "Nombre de cantines satellites",
121+
"type": "integer"
122+
},
123+
{
124+
"constraints": {
125+
"pattern": "^[0-9]{14}$"
126+
},
127+
"description": "Identifiant du [Système d'Identification du Répertoire des Etablissements](https://fr.wikipedia.org/wiki/Syst%C3%A8me_d%27identification_du_r%C3%A9pertoire_des_%C3%A9tablissements) (SIRET) qui identifie la cantine ayant produit les repas",
128+
"example": "34784885147362",
129+
"name": "siret_cuisine_centrale",
130+
"title": "SIRET de la cantine centrale",
131+
"type": "string"
132+
},
133+
{
134+
"description": "Ministère de tutelle de la cantine, s'il y en a un",
135+
"example": "Ministère de l'Education Nationale",
136+
"name": "ministere_tutelle",
137+
"title": "Ministère de tutelle",
138+
"type": "string"
139+
},
140+
{
141+
"description": "Secteur d'activité de l'établissement",
142+
"example": "Restaurants administratifs des collectivités territoriales",
143+
"name": "secteur",
144+
"title": "Secteur",
145+
"type": "string"
146+
},
147+
{
148+
"description": "Catégorie du secteur d'activité de l'établissement",
149+
"example": "Administration",
150+
"name": "categorie",
151+
"title": "Catégorie",
152+
"type": "string"
153+
},
154+
{
155+
"description": "Etablissement concerné par la politique prioritaire du gouvernement sur les services public écoresponsables",
156+
"example": "true",
157+
"name": "spe",
158+
"title": "Service Public Ecoresponsable",
159+
"type": "boolean"
32160
}
33161
],
34162
"format": "csv",

macantine/etl/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Général
2+
3+
L'ETL permet d'extraire des données issues de la plateforme vers :
4+
* data.gouv.fr : pour l'ouverture en open-data (cf `ETL_OPEN_DATA`)
5+
* un data wharehouse : pour l'analyse et la création d'indicateurs (cf `ETL_ANALYSIS`)
6+
7+
Pour chaque destination, deux jeux de données sont édités et publiés :
8+
9+
* cantines (cf cf `ETL_OPEN_DATA_CANTEENS` et `ETL_ANLYSIS_CANTEENS`)
10+
* télédéclarations (cf cf `ETL_OPEN_DATA_TD` et `ETL_ANLYSIS_TD`)
11+
12+
## Open Data
13+
14+
Les données sont stockées sur notre S3 puis référencées depuis l'intertace d'administration de data.gouv.fr (compte perso, affiliation nécessaire à l'organisme MASA).
15+
16+
L'API de data.gouv.fr est utilisée afin de "notifier" data.gouv.fr des mises à jour de fichier et ainsi permettre d'afficher les informatiojns correctes sur lma fréquence de mise à jour ainsi que la date de la dernière modification.
17+
18+
Nous validons les jeux de données en couplant l'outil validata avec le schéma de donnée de chaque jeu.
19+
20+
Les résutats des campagnes des TD sont millisimés (un fichier par année)
21+
22+
## Data Warehouse
23+
24+
Les données sont exportées dans un postgres.
25+
Parmis les transformations apportées, nous :
26+
27+
* traduisons les champs en français (car à destination des équipes métiers)
28+
* apportons plus d'informations géographiques (libellés des départemets/régions, epci...)
29+
30+
Nous connectons l'outil de dataviz à ce postgres afin de construire nos indicateurs.

macantine/etl/analysis.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,20 +136,6 @@ def get_ratio_egalim_sans_bio(row):
136136
return utils.get_ratio(row, "value_somme_egalim_hors_bio_ht", "value_total_ht")
137137

138138

139-
def format_sector_column(row: pd.Series, sector_col_name: str):
140-
"""
141-
Splitting sectors information into two new columns, one for the sector, one for the category
142-
If there are multiple sectors, we
143-
"""
144-
x = row[sector_col_name]
145-
if type(x) == list:
146-
if len(x) > 1:
147-
return "Secteurs multiples", "Catégories multiples"
148-
elif len(x) == 1:
149-
return x[0]["name"], x[0]["category"]
150-
return np.nan, np.nan
151-
152-
153139
def check_column_matches_substring(df, sub_categ: str):
154140
for substring in sub_categ:
155141
pattern = rf".*{re.escape(substring)}.*"
@@ -260,8 +246,8 @@ def transform_dataset(self):
260246

261247
# Extract the sector names and categories
262248
logger.info("Canteens : Extract sectors...")
263-
self.df[["secteur", "catégorie"]] = self.df["canteen.sectors"].apply(
264-
lambda x: format_sector_column(x, "canteen.sectors"), axis=1, result_type="expand"
249+
self.df[["secteur", "catégorie"]] = self.df.apply(
250+
lambda x: utils.format_td_sector_column(x, "canteen.sectors"), axis=1, result_type="expand"
265251
)
266252

267253
# Rename columns
@@ -308,24 +294,51 @@ class ETL_ANALYSIS_CANTEEN(ETL_ANALYSIS):
308294
* Extract data from prod
309295
* Run a SQL query using Metabase API to transform the dataset
310296
* Load the transformed data in a new table within the Data WareHouse
297+
298+
How to add a new field ? Add it to the corresponding schema : schema_analysis_cantines.json
299+
- If it's a extracted field : Add it to the columns_mapper with its translation
300+
- If it's a generated field : Add it in the transform_dataset()
311301
"""
312302

313303
def __init__(self):
314304
self.df = None
315305
self.extracted_table_name = "canteens_extracted"
316306
self.warehouse = DataWareHouse()
317307
self.schema = json.load(open("data/schemas/schema_analysis_cantines.json"))
308+
# The following mapper is used for renaming columns and for selecting the columns to extract from db
318309
self.columns_mapper = {
319310
"id": "id",
320311
"name": "nom",
321312
"siret": "siret",
313+
"city_insee_code": "code_insee_commune",
314+
"city": "libelle_commune",
315+
"department": "departement",
316+
"region": "region",
322317
"creation_date": "date_creation",
323318
"modification_date": "date_modification",
319+
"daily_meal_count": "nbre_repas_jour",
320+
"yearly_meal_count": "nbre_repas_an",
321+
"economic_model": "modele_economique",
322+
"management_type": "type_gestion",
323+
"production_type": "type_production",
324+
"satellite_canteens_count": "nombre_satellites",
325+
"central_producer_siret": "siret_cuisine_centrale",
326+
"line_ministry": "ministere_tutelle",
327+
"sectors": "secteur",
324328
}
325329

326330
def extract_dataset(self):
327331
self.df = utils.fetch_canteens(self.columns_mapper.keys())
328332

329333
def transform_dataset(self):
334+
logger.info("Filling geo names")
335+
self.fill_geo_names()
336+
self.columns_mapper["department_lib"] = "departement_lib"
337+
338+
# Extract the sector names and categories
339+
logger.info("Canteens : Extract sectors and SPE...")
340+
self.df = utils.extract_sectors(self.df, extract_spe=True, split_category_and_sector=True, only_one_value=True)
341+
self.df = self.df.rename(columns={"categories": "categorie"})
342+
330343
self.df = self.df.rename(columns=self.columns_mapper)
331344
self.df = utils.filter_dataframe_with_schema_cols(self.df, self.schema)

macantine/etl/open_data.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from data.models import Canteen
1414
from macantine.etl import etl
1515
from macantine.etl.etl import logger
16-
from macantine.etl.utils import fetch_canteens, fetch_teledeclarations
16+
from macantine.etl.utils import extract_sectors, fetch_canteens, fetch_teledeclarations
1717

1818

1919
class ETL_OPEN_DATA(etl.ETL):
@@ -83,15 +83,6 @@ def _clean_dataset(self):
8383
self.df[col_int["name"]] = self.df[col_int["name"]].round(decimals=4)
8484
self.df = self.df.replace("<NA>", "")
8585

86-
def _extract_sectors(self):
87-
# Fetching sectors information and aggreting in list in order to have only one row per canteen
88-
sectors = macantine.etl.utils.map_sectors()
89-
self.df["sectors"] = self.df["sectors"].apply(lambda x: macantine.etl.utils.fetch_sector(x, sectors))
90-
canteens_sectors = self.df.groupby("id")["sectors"].apply(list).apply(macantine.etl.utils.format_list_sectors)
91-
del self.df["sectors"]
92-
93-
return self.df.merge(canteens_sectors, on="id")
94-
9586
def get_schema(self):
9687
return self.schema
9788

@@ -240,7 +231,7 @@ def transform_dataset(self):
240231
self.df["active_on_ma_cantine"] = self.df["id"].apply(lambda x: x not in non_active_canteens)
241232

242233
logger.info("Canteens : Extract sectors...")
243-
self.df = self._extract_sectors()
234+
self.df = extract_sectors(self.df, extract_spe=False, split_category_and_sector=False, only_one_value=False)
244235

245236
bucket_url = os.environ.get("CELLAR_HOST")
246237
bucket_name = os.environ.get("CELLAR_BUCKET_NAME")
@@ -306,7 +297,7 @@ def transform_sectors(self) -> pd.Series:
306297
sectors = self.df["canteen_sectors"]
307298
if not sectors.isnull().all():
308299
sectors = sectors.apply(lambda x: list(map(lambda y: macantine.etl.utils.format_sector(y), x)))
309-
sectors = sectors.apply(macantine.etl.utils.format_list_sectors)
300+
sectors = sectors.apply(macantine.etl.utils.format_list)
310301
return sectors
311302

312303
def transform_dataset(self):

0 commit comments

Comments
 (0)