diff --git a/irve_bornes_recharge/index.py b/irve_bornes_recharge/index.py index 084a29c3..4bcf8066 100644 --- a/irve_bornes_recharge/index.py +++ b/irve_bornes_recharge/index.py @@ -1,68 +1,160 @@ -import geojson -import pandas as pd -import dash -import dash_table -import requests -from dash.dash_table.Format import Group -import dash_html_components as html -import os - -# Vérifier si le fichier local existe -local_file = "irve.geojson" -if not os.path.exists(local_file): - # Télécharger les données geojson si le fichier local n'existe pas - url = "https://www.data.gouv.fr/fr/datasets/r/7eee8f09-5d1b-4f48-a304-5e99e8da1e26" - response = requests.get(url) - with open(local_file, 'w') as f: - f.write(response.text) - -# Ouvrir le fichier local pour la suite du script -with open(local_file, 'r') as f: - data = geojson.load(f) - -# Convertir les données geojson en DataFrame pandas -df = pd.DataFrame(data['features']) - - -# Calcul des mauvaises qualités -def calcul_mauvaise_qualite(df): - df['mauvaise_qualite'] = 0 - - # Une ligne n'ayant rien dans une des colonnes "id_pdc_itinerance", "telephone_operateur" fait gagner 1 point - df.loc[(df['properties']['id_pdc_itinerance'] == '') | ( - df['properties']['telephone_operateur'] == ''), 'mauvaise_qualite'] += 1 - - # Un texte dans "telephone_operateur" fait gagner 2 points - df.loc[df['properties']['telephone_operateur'].str.contains(r'\D'), 'mauvaise_qualite'] += 2 - - # Si une ligne a les mêmes coordonnées géographiques qu'une autre, on gagne 3 points pour l'opérateur - df['coordinates'] = df['geometry']['coordinates'].apply(lambda x: tuple(x)) - df['duplicated'] = df.duplicated(subset='coordinates', keep=False) - df.loc[df['duplicated'], 'mauvaise_qualite'] += 3 +# url = "https://www.data.gouv.fr/fr/datasets/r/7eee8f09-5d1b-4f48-a304-5e99e8da1e26" +import pandas_geojson as pdg +from pandas import DataFrame +from pandas_geojson import GeoJSON +import re +def load_small_dataset(): + geojson: GeoJSON = pdg.read_geojson('small.geojson') + df: DataFrame = geojson.to_dataframe() + # df = clean_geojson_properties(df) return df -df = calcul_mauvaise_qualite(df) +def clean_geojson_properties(gdf): + """Convertit en True et False les valeurs de propriétés qui valent des string "True" ou "False" avec une case mixte. + Applique la conversion uniquement aux colonnes textuelles.""" -# Compter les mauvaises qualités par opérateur -operator_counts = df.groupby(df['properties']['nom_amenageur'])['mauvaise_qualite'].sum().reset_index() -operator_counts.columns = ['nom_amenageur', 'nbre_mauvaise_qualite'] - -# Ajouter le nombre de points de recharge par opérateur -operator_counts['nbre_pdc'] = df.groupby('properties')['nom_amenageur'].size().values - -# Créer le tableau de bord -app = dash.Dash(__name__) - -app.layout = html.Div([ - html.H1('Trophées de bras cassés de l\'open data'), - dash_table.DataTable( - id='table', - columns=[{'name': i, 'id': i} for i in operator_counts.columns], - data=operator_counts.to_dict('records') + PROPERTY_NAMES = ( + "prise_type_ef", + "prise_type_2", + "prise_type_combo_ccs", + "prise_type_chademo", + "prise_type_autre", + "gratuit", + "paiement_acte", + "paiement_cb", + "paiement_autre", + "station_deux_roues", + "consolidated_is_lon_lat_correct", + "consolidated_is_code_insee_verified", ) -]) + + for col_name in PROPERTY_NAMES: + pattern = r"^(?:true|false)$", re.IGNORECASE + gdf[col_name] = gdf[col_name].str.match(pattern).replace({True: True, False: False}).astype(bool) + + return gdf + +### +# les propriétés de chaque point de charge sont: +# ['nom_amenageur', 'iren_amenageur', 'contact_amenageur', 'properties.nom_operateur', 'contact_operateur', 'telephone_operateur', 'nom_enseigne', 'id_station_itinerance', 'id_station_local', 'nom_station', 'implantation_station', 'adresse_station', 'code_insee_commune', 'coordonneesXY', 'nbre_pdc', 'id_pdc_itinerance', 'id_pdc_local', 'puissance_nominale', 'prise_type_ef', 'prise_type_2', 'prise_type_combo_ccs', 'prise_type_chademo', 'prise_type_autre', 'gratuit', 'paiement_acte', 'paiement_cb', 'paiement_autre', 'tarification', 'condition_acces', 'eservation', 'horaires', 'accessibilite_pmr', 'estriction_gabarit', 'tation_deux_roues', 'raccordement', 'num_pdl', 'date_mise_en_service', 'observations', 'date_maj', 'cable_t2_attache', 'last_modified', 'datagouv_dataset_id', 'datagouv_resource_id', 'datagouv_organization_or_owner', 'created_at', 'consolidated_longitude', 'consolidated_latitude', 'consolidated_code_postal', 'consolidated_commune', 'consolidated_is_lon_lat_correct', 'consolidated_is_code_insee_verified'] +# +### +def evaluate_score_of_operators(df): + operateurs: int = df['properties.nom_amenageur'].value_counts() + print('score de chaque opérateur:') + for operateur, count in operateurs.items(): + print(f'- {operateur}: {count} points') + + +properties_quality_check = [ + 'missing__phone', + 'missing__id_pdc_itinerance', + 'bad_format__telephone_operateur', + 'duplicate__coordinates', + 'missing__puissance_nominale', + 'missing__cable_attached', + 'bad__lon_lat' +] + + +# on crée des colonnes supplémentaires pour stocker les erreurs de chaque type, +# puis on utilise la fonction groupby pour stocker les erreurs par opérateur. +# Le résultat est un DataFrame errors_by_operator qui contient +# le nombre d'erreurs de chaque type pour chaque opérateur. +def calculate_bad_quality(df): + df['bad_quality'] = 0 + + # Une ligne n'ayant rien dans une des colonnes "id_pdc_itinerance", "telephone_operateur" fait gagner 1 point. + df['missing__phone'] = df['properties.telephone_operateur'].isnull().astype(int) + df['missing__id_pdc_itinerance'] = df['properties.id_pdc_itinerance'].isnull().astype(int) + df['missing__puissance_nominale'] = df['properties.puissance_nominale'].isnull().astype(int) + df['missing__cable_attached'] = df['properties.cable_t2_attache'].isnull().astype(int) + # print(df['properties.consolidated_is_lon_lat_correct']) + df['bad__lon_lat'] = df['properties.consolidated_is_lon_lat_correct'] == False + + # Un texte dans "telephone_operateur" fait gagner 2 points. + df['bad_format__telephone_operateur'] = df['properties.telephone_operateur'].apply( + lambda x: isinstance(x, str)).astype(int) + + # Si une ligne a les mêmes coordonnées géographiques qu'une autre, on gagne 3 points pour l'opérateur. + df['coordinates'] = df['geometry.coordinates'].apply(lambda x: tuple(x)) + duplicate_coordinates = df[df.duplicated(subset='coordinates', keep=False)] + df['duplicate__coordinates'] = df['coordinates'].isin(duplicate_coordinates['coordinates']).astype(int) + + # Calcul du score de mauvaise qualité + df['bad_quality'] = df['missing__phone'] + df['missing__id_pdc_itinerance'] + 2 * df[ + 'bad_format__telephone_operateur'] + 3 * df['duplicate__coordinates'] + + # Stockage des erreurs par opérateur + errors_by_operator = df.groupby('properties.nom_amenageur')[ + properties_quality_check + ].sum() + + return df, errors_by_operator + + +def render_champions_of_bad_data(df): + df, errors_by_operator = calculate_bad_quality(df) + + print('* Nombre d\'opérateurs: ', len(df.groupby('properties.nom_amenageur'))) + operateurs = df.groupby('properties.nom_amenageur')['bad_quality'].sum().sort_values(ascending=False) + champions = operateurs.nlargest(3) + print('\n* Les champions des bras cassés de l\'open data:') + for i, (operateur, score) in enumerate(champions.items()): + if i == 0: + print(f'** Médaille d\'or: {operateur} avec {score} points de mauvaise qualité') + elif i == 1: + print(f'** Médaille d\'argent: {operateur} avec {score} points de mauvaise qualité') + elif i == 2: + print(f'** Médaille de bronze: {operateur} avec {score} points de mauvaise qualité') + + +def render_all_operator(df): + df, errors_by_operator = calculate_bad_quality(df) + operateurs = df.groupby('properties.nom_amenageur')['bad_quality'].sum().sort_values(ascending=False) + champions = operateurs.nlargest(500) + + print(errors_by_operator) + print('\n* Les opérateurs:') + for i, (operateur, score) in enumerate(champions.items()): + print('\n** ', operateur, ', ', score, ' points') + + search_bad_property_for_operator(df, errors_by_operator, 'bad_format__telephone_operateur', + 'mauvais numéros de téléphone', operateur) + search_bad_property_for_operator(df, errors_by_operator, 'duplicate__coordinates', 'coordonnées GPS dupliquées', + operateur) + search_bad_property_for_operator(df, errors_by_operator, 'missing__id_pdc_itinerance', + 'Identifiant en itinérance manquant', operateur) + search_bad_property_for_operator(df, errors_by_operator, 'missing__phone', 'numéro de téléphone manquant', + operateur) + search_bad_property_for_operator(df, errors_by_operator, 'missing__puissance_nominale', + 'puissance nominale manquante', operateur) + search_bad_property_for_operator(df, errors_by_operator, 'bad__lon_lat', + 'coordonnées éronnées', operateur) + + +def search_bad_property_for_operator(gdf, errors_by_operator, property_name, error_description, operator_name): + """Recherche les occurrences de la mauvaise propriété pour un opérateur donné + :type operator_name: str + """ + + counter_bad_lines = errors_by_operator.loc[operator_name, property_name] + if counter_bad_lines: + + total_lines = len(gdf.loc[gdf['properties.nom_amenageur'] == operator_name]) + + if total_lines != 0: + proportion = (counter_bad_lines / total_lines) * 100 + else: + proportion = 0 + + print(f'- {error_description}:\t{counter_bad_lines}/{total_lines}\t ({proportion:.2f}%)') + if __name__ == '__main__': - app.run_server(debug=True) \ No newline at end of file + df = load_small_dataset() + # evaluate_score_of_operators(df) + render_champions_of_bad_data(df) + render_all_operator(df)