Wheel Python et Azure feed : centralisez et distribuez vos packages

Assurer la qualité d’un développement Python passe par le fait de packager du code (des classes, des fonctions…) dans des modules que l’on pourra ensuite simplement installer dans un nouvel environnement (avec un classique pip install...) et importer dans des scripts à l’aide des syntaxes habituelles que sont import package ou from package import function.

Créer le package Wheel

C’est bien sûr la toute première étape une fois que notre code a été écrit. Et bien écrit, c’est-à-dire en respectant par exemple la norme PEP8 (nous en reparlerons) et en intégrant des docstrings dans les fonctions.

Nous aurons besoin de quelques librairies, dont bien évidemment wheel et nous profiterons d’un environnement virtuel pour les installer (commandes Windows ci-dessous pour créer, activer et configurer cet environnement).

python -m venv env
env\bin\activate.bat
python -m pip install -U setuptools wheel twine

Notre exemple se basera sur du code simple générant un pandas dataframe avec des nombres aléatoires. Notons au passage (pour les puristes :)) que ce code poserait problème dès que nb_col dépasserait la valeur 26.

import pandas as pd
import numpy as np
import string


def generate_df(nb_col, nb_row):
     """
         Generate a pandas DataFrame
         with nb_col columns and nb_row rows
     """
     alphabet_string = string.ascii_uppercase
     columns_string = alphabet_string[:nb_col]
     columns_list = list(columns_string)
     df = pd.DataFrame(np.random.randint(0, 100, size=(nb_row, nb_col)), columns=columns_list)
 return df

Le script, nommé ici my_function.py, doit se trouver dans une arborescence de fichiers définie comme suit :

my_package
├── LICENSE
├── README.md
├── my_pkg
│   └── __init__.py
│   └── my_function.py
├── setup.py
└── tests

Le fichier __init__.py est tout simplement un fichier vide, seul le nom est obligatoire. Ce fichier doit être dans le répertoire dédié aux fichiers développés (sous-répertoire du répertoire principal). Il peut toutefois contenir également des fonctions qui seront chargées à l’appel du module. Pour réaliser des tests simples, voici ce qu’il contient.

def function_init():
     print('Successfully imported Init.py')


 def print_hello_iam(name):
     print(f'Hello, I am {name}')

Nous complétons avec un fichier d’informations README au format Markdown, un fichier texte contenant la licence (ci-dessous) et un répertoire, éventuellement vide dans un premier temps, qui contiendra des tests.

Copyright (c) 2018 The Python Packaging Authority
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.

Le dernier élément nécessaire est le fichier setup.py dont un modèle de contenu peut être retrouver sur cette page. Il faudra en particulier y préciser le nom souhaité pour le package.

Pour générer l’archive de distribution, nous lançons la commande suivante, au niveau du répertoire contenant le fichier setup.py :

python setup.py sdist bdist_wheel

Un répertoire dist est alors créé et contient deux fichiers : le fichier .whl et une archive .tar.gz.

Le nom du package wheel est normalisé de la sorte :

{dist}-{version}(-{build})?-{python}-{abi}-{platform}.whl

Réalisons tout de suite un premier test pour valider que notre package est bien construit. Au niveau du dossier contenant le fichier .whl, nous lançons dans un terminal la commande suivante, depuis le répertoire dist :

python -m pip install my_pkg_byPaulPETON-0.0.1-py3-none-any.whl

Puis dans un prompt Python, nous vérifions que l’import des nos méthodes est bien reconnu :

from my_pkg.__init__ import *
from my_pkg.my_function import *

Tester le package depuis un cluster Azure Databricks

Nous allons maintenant installer manuellement le fichier .whl disponible dans le répertoire dist sur un cluster Databricks. Nous commençons par le télécharger sur un cluster interactif démarré.

Dès lors, les fonctions deviennent disponibles dans un notebook.

Notez au passage le “dark mode” des notebooks Databricks 🙂

Charger le package sur un feed Azure DevOps

Nous n’allons bien sûr pas utiliser le fichier .whl localement, celui-ci doit être hébergé sur une plateforme accessible de tous les développeurs de l’équipe.

Tout comme le code est placé dans un dépôt (repository) d’un gestionnaire de versions comme Azure DevOps, nous allons placer le package, ici nommé artefact, dans un feed, accessible aux personnes autorisées.

Nous réalisons tout d’abord la création du feed. Il sera ici public, donc accessible au travers d’Internet. Pour une pratique en entreprise, un projet privé est bien évidemment recommandé.

Le feed est maintenant bien actif.

Attention, il est préférable de vérifier vos options de facturation (dans le menu “Organization Settings”). Un compte gratuit présentera des limites de taille pour l’usage des artefacts stockés dans le feed.

Vérifions également le stockage associé aux artefacts (ici au niveau projet, puisque c’est le périmètre qui a été défini).

Il s’agit ensuite d’automatiser la création du package wheel par un pipeline d’intégration continue. Nous démarrons à partir d’un “starter pipeline”.

Le code complet de ce pipeline, enregistré automatiquement dans un fichier azure-pipelines.yml, ajouté au repository, est disponible par exemple sur ce GitHub (veillez à adapter si besoin la version de Python attendue ainsi que le nom de la branche – main pour master – si vous souhaitez un lancement automatique de la pipeline à chaque commit).

Ce script YAML reprend l’exécution du fichier setup.py pour créer l’artefact dans un conteneur dédié.

Nous avons ensuite besoin d’un pipeline de release qui déposera l’artefact dans le feed qui servira de point de distribution.

A partir d’un modèle vide de pipeline de release (“empty job“), nous attachons le résultat du pipeline de build réalisé précédemment.

Nous ajoutons trois activités qui sont :

  • Python twine upload authenticate
  • Command line pour l’installation de Twine
  • Command line pour la publication de l’artefact

L’étape 1 s’authentifie auprès du feed, dont il faut saisir le nom.

L’étape 2 réalise l’installation du package Twine par la commande pip install.

L’étape 3 utilise Twine pour télécharger le package.

Voici la ligne de code nécessaire :

twine upload -r <feed_name> --config-file $(PYPIRC_PATH) d:\a\r1\a\_<project_name>\dist\dist*

Cette étape est sans doute la plus délicate. En cas d’erreur pour localiser ce répertoire, je vous conseille de regarder les logs de la première étape du pipeline de build afin de visualiser l’artefact dans son arborescence.

Utiliser le package depuis un nouveau script

Nous allons avoir besoin d’une nouvelle librairie : artifacts-keyring..

pip install artifacts-keyring

Ensuite, nous pouvons faire appel à la commande classique “pip install” pointant vers notre feed.

pip install packageName --index-url https://pkgs.dev.azure.com/<organization_name>/<project_name>/_packaging/<feed_name>/pypi/simple/

Le “project_name” est facultatif si le feed est déclaré au niveau de l’organisation. Il sera possible de remplacer par l’option –index-url par –extra-index-url comme indiqué dans cet article mais ceci est à réaliser seulement si l’on utilise la méthode proposée dans le portail Azure DevOps : la création d’un fichier pip.ini.

En cas de project privé, une authentification sera alors demandée, de manière interactive, au travers d’un navigateur.

Ce mode n’est bien sûr pas envisageable pour une installation devant s’exécuter de manière autonome. Nous allons donc générer un Personal Access Token (PAT) qui permettra de nous authentifier. Ce jeton s’obtient dans Azure DevOps.

Nous donnons les droits au niveau “packaging“.

Dans l’environnement d’exécution, une variable d’environnement sera nécessaire pour désactiver l’authentification interactive et tenir compte du PAT. Cela se fait par exemple dans Windows par le menu “Modifier les variables d’environnement système” ou dans un dockerfile avec la ligne ci-dessous.

VAR ARTIFACTS_KEYRING_NONINTERACTIVE_MODE=true

Il sera alors possible d’appeler le package avec la syntaxe suivante :

pip install packageName --extra-index-url=https://<PAT>@pkgs.dev.azure.com/<organization_name>/<project_name>/_packaging/<feed_name>/pypi/simple/

Ca y est ! Le package est maintenant installé et nous pouvons nous appuyer sur les fonctions qu’il contient.

from my_pkg.my_function import *

En conclusion

Nous avons mis en place ici une approche dédiée à l’industrialisation d’un développement, accompagné d’un processus CI/CD. Cela demande un investissement en temps et en prise en main de cette procédure mais c’est une garantie de stabilité et de non régression sur le livrable en production.

Interagir avec Azure ML depuis Visual Studio Code

Travailler avec une ressource Azure Machine Learning peut se faire de plusieurs façons : approche “no code” depuis le Concepteur, Automated ML ou encore par scripts R ou Python exploitant le SDK azureml qui permet d’interagir avec tous les composants du studio.

C’est bien sûr cette dernière approche “full code” que nous allons privilégier. Il est possible de lancer les notebooks natifs du studio mais ceux-ci ne donnent pas à ce jour (avril 2021) la même expérience que les notebooks Jupyter. Nous pouvons alors créer une instance de calcul qui se déploiera avec un serveur JupyterLab (ainsi que RStudio qu’il n’est pas impossible d’utiliser… avec Python !).

Plusieurs limites apparaissent alors pour un usage professionnel, c’est-à-dire vérifiant un niveau de sécurité au travers de l’isolement des développements, de leur versionning et de leur déploiement en production. Si les instances de calcul sont personnelles (attribuées à un et un seul utilisateur, nominativement), les scripts enregistrés sont visibles par toutes les personnes disposant d’une ressource. De plus, il n’y a pas de possibilité de lier les notebooks et autres fichiers à un dépôt (repository) de type Git. Enfin, si l’on pousse la réflexion sur le passage à l’échelle, il faudra envisager d’exécuter les scripts sur un cluster de calcul plutôt que sur l’instance de calcul elle-même.

Nous allons répondre à toutes ces problématiques par l’utilisation de l’IDE de Microsoft : Visual Studio Code. Au préalable, il faudra installer les extensions suivantes :

  • Python
  • Jupyter
  • Azure Account
  • Azure Machine Learning

En appuyant sur F1, nous pouvons nous authentifier sur Azure.

L’icône Azure donne alors la visibilité des ressources Azure Machine Learning et de tous les éléments qui les composent.

Nous souhaiterons en particulier utiliser un “compute cluster” pour exécuter un script Python. Nous allons pour cela créer un notebook d’interaction avec Azure Machine Learning qui pilotera l’exécution du script.

Pour assurer la gestion “Git” de nos fichiers, nous avons maintenant tous les outils à disposition. Il suffira d’utiliser les fonctions intégrées, par le menu ou en lignes de code, ce que nous développerons dans un autre article.

Les notebooks sont “suspects” pour VSC et il faudra déclarer qu’ils sont fiables (“trust“) à chaque ouverture. Pour simplifier ce processus, il est possible de modifier les paramètres pour accepter par défaut tous les notebooks.

Nous allons utiliser un serveur local Jupyter pour exécuter le code Python (et c’est une source d’économies !).

Les librairies spécifiques à Azure Machine Learning doivent bien sûr être installées dans cet environnement local.

!python -m pip install azureml-sdk --upgrade --user
!python -m pip install azureml-core --upgrade --user

Le package azureml-core évolue souvent, pensez à le mettre régulièrement à jour car les composants du studio Azure ML évolueront automatiquement.

Il faut maintenant nous authentifier vis à vis de ce service. Les informations nécessaires sont accessibles dans le fichier “config.json” téléchargeable depuis le portail Azure.

Nous donnons ces informations dans le code ci-dessous.

A l’exécution du code, une fenêtre va s’ouvrir dans un navigateur demandant de s’authentifier avec le compte Azure autorisé sur la ressource Azure ML. Une fois cette opération réalisée, la cellule du notebook est validée.

Ceci ne sera bien sûr pas envisageable dans une scénario tout automatisé et nous privilégierons alors l’authentification au travers d’un principal de service.

Nous pouvons vérifier la bonne connexion à la ressource avec l’instruction suivante :

print(ws.name, ws.location, ws.resource_group, ws.location, sep='\t')

Nous allons maintenant mettre en place les éléments qui permettront d’exécuter un script Python.

Ce schéma décrit le fonctionnement global :

  • VSC lance l’exécution (run) d’un script Python
  • exécuté sur un cluster de calcul
  • accompagné d’un environnement (les dépendances de packages)
  • loggé dans une expérience

Les éléments de script ci-dessous ont été piochés dans différents tutoriels disponibles sur le Web. Nous commençons par la cellule qui déclarer le cluster de calcul ou bien le crée à la volée s’il n’existe pas de cluster nommé de la sorte.

Les packages nécessaires au script Python sont déclarés dans un environnement.

Notez la dernière commande qui permet de sauvegarder cet environnement dans un fichier YAML.

env.python.conda_dependencies.save_to_file(".", "my_remote_env.yml")

Il sera alors possible de réutiliser directement ce fichier pour définir de nouveaux environnements. Voici son contenu.

Enfin, l’expérience s’instancie tout simplement, en relation avec l’espace de travail Azure ML.

Le script Python peut être écrit dans une cellule du notebook. Il pourra alors être donné en paramètre de l’objet ScriptRunConfig.

A l’intérieur du script Python, il sera certainement utile de retrouver la référence à l’exécution réalisée au sein de l’espace de travail. Ceci se fait en enchainant ces trois lignes de code, précédées de l’import de la librairie azureml-core.

from azureml.core.run import Run


run = Run.get_context()
exp = run.experiment
ws = run.experiment.workspace

La soumission du script au sein de l’expérience se fait alors par la commande ci-dessous.

run = exp.submit(config=src, tags=runtags)

Nous pouvons dès lors suivre l’évolution dans le studio Azure ML.

A noter que les widgets qui permettent de suivre l’exécution dans un notebook semblent ne pas être compatibles avec VSC.

Mais ce n’est là qu’un détail au regard de ce que nous gagnons à utiliser VSC : versionning du code, approche DevOps, diminution des coûts de développement.

Le notebook utilisé dans cet article est disponible sur ce repository.

Comment documenter un rapport Power BI

Développer un rapport Power BI peut-il s’apparenter à un développement « classique » au sens du code ? Pas entièrement sans doute mais des bonnes pratiques sont à mettre en œuvre et la documentation en fait sans nul doute partie.

Pourtant, car comme nous savons tous que ce n’est pas l’aspect le plus intéressant d’un développement, nous souhaiterons le faire :

  • Sans y passer trop de temps et de la manière la plus automatisée possible
  • En produisant un contenu utile pour un autre développeur qui n’aurait pas connaissance du projet initial
  • En sécurisant et versionnant les contenus les plus précieux (M, DAX)

En effet, nous ne sommes jamais à l’abri d’une corruption de fichier et conserver toute « l’intelligence » du développement permettrait de ne pas tout refaire de zéro.

A faire au cours développement

Les pratiques qui seront présentées ici visent bien sûr à améliorer la qualité du rapport Power BI mais aussi à faciliter la documentation qui pourra être produite par la suite.

Réduire les étapes de transformation

Par exemple, il est possible de “typer une nouvelle colonne à la création.

= Table.AddColumn(#"Previous step", "Test approved = 0", each [#"Approved amount (local currency)"] * [Approved quantity] = 0, type logical)

Une bonne approche pour réduire les étapes consiste à privilégier les transformations réalisées au niveau de la source de données, par exemple en langage SQL.

Nommer certaines étapes de transformation

Les étapes d’une requête Power Query peuvent être renommées. Il est recommandé de privilégier le nommage automatique en anglais et de préciser certaines étapes.

Du point de vue de la relecture du script, il n’est pas optimal de disposer de plusieurs étapes similaires (par exemple : Change Type ou Renamed Columns) mais cela est parfois inévitable comme pour l’action Replaced Value. On précisera ainsi la valeur remplacée par cette étape. De même pour des opérations comme Added Custom, Merge Queries, Filtered Rows, etc.

Ajouter si besoin une description plus détaillée au moyen d’un commentaire dans le script M entre /*…*/ ou sur une seule ligne à l’aide de //.

Comme dans tout développement, un commentaire vient expliquer la logique adoptée par le développeur lorsque celle-ci est plus complexe que l’usage nominal de la formule.

Déterminer la bonne stratégie de filtres

Il y a tellement d’endroits où l’on peut filtrer les données lors d’un développement Power BI ! Dans une ordre « chronologique » au sens des phases de développement, nous pouvons identifier :

  • La requête vers la source (par exemple avec une clause WHERE dans un script SQL)
  • Une étape de transformation Power Query
  • Un contexte dans une mesure DAX avec des fonctions comme CALCULATE() ou FILTER()
  • Un rôle de sécurité
  • Le volet de filtre latéral :
    • Au niveau du rapport
    • Au niveau de la page
    • Au niveau du visuel

D’emblée, précisons tout de suite que si vous êtes tentés par les filtres de rapport, c’est très vraisemblablement une mauvaise idée et il était sûrement possible de le faire dans une étape précédente ! Une règle (non absolue) est de réaliser l’opération le plus tôt possible, dans la liste précédente.

Ainsi, on privilégiera la requête SQL, ou le query folding, qui délégueront toute la préparation de données au serveur, qui sera vraisemblablement plus puissant que le service Power BI.

Appliquer une nomenclature aux noms de tables, champs et mesures

Il n’est pas pertinent de préfixer les noms de tables par DIM_ ou FACT_, ceci étant des notions propres aux développeurs décisionnels mais ne parlant pas aux utilisateurs métiers.

Les noms de tables peuvent comporter des espaces, accents ou caractères spéciaux mais l’usage de ces caractères complexifiera l’utilisation de l’auto-complétion des formules. L’utilisation d’émoticônes peut entrainer une corruption du fichier.

Il vaut mieux utiliser des noms de tables courts pour limiter la taille du volet latéral Fields.

Les noms des champs et des mesures doivent être parlants pour un être humain, on bannira donc les _ ou l’écriture CamelCase.

Il est possible d’utiliser des symboles tels que % ou pour donner une information sur le calcul réalisé.

Afin de profiter de l’écriture dynamique des titres, les mesures peuvent être écrites avec une majuscule et les autres noms de champs en minuscules uniquement. Cela donnera par exemple : « Nb commandes par catégorie » à partir d’une mesure « Nb commandes » et d’un champ nommé « catégorie ».

Mettre en place une stratégie d’imbrication de mesures

Il est recommandé de produire des mesures avancées en se basant sur des mesures simples (et toujours explicites !), quitte à masquer celles-ci.

Par exemple :

# ventes = COUNTROWS(VENTES)
# ventes en ligne = CALCULATE([# ventes], VENTES[Canal] = "en ligne")

Les mesures appelées dans une autre mesure ne seront pas préfixées par un nome de table à l’inverse des noms de colonnes.

Décrire les mesures

La description fonctionnelle des mesures se fait (et ce n’est pas très intuitif…) dans l’affichage de modèle.

Cet affichage permet également de regrouper les mesures (d’une même table uniquement) dans un dossier ou plusieurs dossiers (« display folder »).

Nous utiliserons ici la propriété « is hidden » pour masquer les colonnes qui ne sont pas utiles à un utilisateur : identifiants, clés techniques, valeurs numériques reprises dans des mesures explicites.

Commenter les formules DAX

Comme dans tout  langage de programmation, le commentaire ne doit intervenir que lorsque la stratégie mise en place par le code n’est pas intuitive.

Il est recommandé d’utiliser les séparateurs virgule (paramètres) et point (décimales) dans les formules DAX. Ceci est paramétrable dans les options globales.

Penser également à mettre en forme systématiquement les champs DAX à l’aide de l’outil en ligne DAX formatter.

Nommer les composants visuels

Pour nommer un composant, on utilise le champ « Title », que celui-ci soit activé ou désactivé. C’est pourquoi il faut activer le titre pour le renseigner, quitte ensuite à le désactiver. Les objets peuvent donc avoir un nom qui est un titre utilisé de manière visuelle.

Le nom de l’objet est ensuite visible dans le volet de sélection.

Les objets ont un identifiant et un nom qui sont utilisés dans les fichiers JSON constituant le contenu du fichier .pbix.

Documenter à l’aide d’outils externes

Nous allons mettre en place une approche pragmatique, à partir d’outils tiers, qui ne sont ni développés ni maintenus par Microsoft mais intégrés maintenant au sein du menu « External Tools ».

Suite à l’installation de ces outils, des raccourcis seront disponibles dans le nouveau menu.

Les différents exécutables sont simples à trouver au travers d’un moteur de recherche.

Power BI Helper

Nous utilisons ici la version de décembre 2020.

Ouvrir le rapport dans Power BI Desktop.

Lancer Power BI Helper. Depuis l’onglet Model Analysis, cliquer sur Connect to Model.

Le nom du fichier doit apparaître dans la liste déroulante « Choose the Power BI file ».

Créer le fichier HTML en cliquant sur « export to document » ou aller sur l’onglet « Document » du menu horizontal.

Choisir les éléments voulus dans la liste « Model Analysis and Visualisation » puis cliquer sur « Create Power BI file’s document ». Nous obtenons un fichier au format .htm contenant des tableaux.

Voici pourquoi il est important de renseigner la description dans le rapport Power BI.

Tiens, pourquoi pas brancher un Power BI sur ce fichier HTML et ainsi charger les tableaux dans un « méta-rapport » ?

Le code M est visible dans l’onglet du menu « M script ».

Le bouton d’export semble avoir disparu mais il est possible de faire un simple copier-coller de la zone de texte grisée.

Sans même utiliser un outil externe, il était déjà possible de copier une requête dans l’éditeur (clic droit, copier), puis de coller dans un bloc-notes pour obtenir le script, qu’on enregistre par convention dans un fichier d’extension .M.

Power BI Field Finder

Télécharger la dernière version du modèle .pbit depuis le dépôt GitHub : https://github.com/stephbruno/Power-BI-Field-Finder

Ouvrir le modèle .pbit et renseigner comme valeur de paramètre le chemin du fichier .pbix que l’on souhaite documenter.

Les données vont se mettre à jour et il sera possible d’enregistrer le rapport au format .pbix, puis de le mettre à jour en cas de modification dans le fichier d’origine.

Je tiens à signaler que je suis vraiment bluffé par la qualité de cet outil qui rend d’immenses services au quotidien. Bravo, Stéphanie BRUNO ! L’outil est présenté plus en détail sur ce site.

Cliquer sur un nom de champ pour identifier la ou les pages où il est utilisé.

La page détaillée indique ensuite le(s) visuel(s) utilisant ce champ.

DAX Studio

Quelques requêtes permettent de synthétiser le code DAX écrit dans le fichier .pbix. Conservez-les précieusement, par exemple dans un fichier texte d’extension .dax. Le résultat pourra ensuite être enregistré dans un format CSV ou Excel en paramétrant l’output.

Liste de toutes les colonnes calculées

  
 select 
    TableID,
    ExplicitName, 
    Expression
 from $SYSTEM.TMSCHEMA_COLUMNS
 where [Type] = 2
 order by TableID 

Liste de toutes les mesures

 
  select 
   MEASUREGROUP_NAME, 
   MEASURE_NAME, 
   EXPRESSION
from $SYSTEM.MDSCHEMA_MEASURES
where MEASURE_AGGREGATOR = 0
order by MEASUREGROUP_NAME 

Dépendances entre les formules DAX

Puisque je vous ai encouragés à utiliser les mesures imbriquant d’autres mesures, vous chercherez certainement les dépendances créés. Celles-ci peuvent être obtenues par la requête suivante, où l’on précisera le nom de la mesure entre simples cotes.

Cette démarche est expliquée en détail dans cet article de Chris Webb.

 
select referenced_object_type, referenced_table, referenced_object, referenced_expression
from $system.discover_calc_dependency
where [object]='Nom de la mesure' 

Ainsi, pour la mesure simple (mais explicite !) ‘Nb commandes’, nous obtenons :

Puis, pour une mesure ‘CA A-1’ basé sur une autre mesure :

Vous avez quelques belles cartes en main pour créer et maintenir une documentation robuste autour de vos rapports Power BI. Maintenant, à vous de jouer ! Les personnes qui prendront votre relais sur ces développements vous remercient déjà par avance !

Recueillir le besoin pour la création d’un rapport Power BI

Le chemin est long et semé d’embuches jusqu’à l’obtention d’un rapport Power BI pertinent et exploitable ! La démarche se doit d’être progressive, il ne faut pas sauter d’étapes, au risque de manquer l’adéquation du rapport avec le besoin de ses utilisateurs.

Etape 0 : Définir la thématique, cadrer le besoin

Le besoin émane souvent (et ce devrait même être toujours le cas !) des futurs utilisateurs du rapport. Mais ceux-ci peuvent être différents (niveau hiérarchique, responsabilités, appétence aux chiffres, etc.) et il faut déterminer des « personnes génériques », que l’on nomme personae.

Nous définissons ensuite, de manière propre à chaque persona, les « parcours utilisateurs » qui assisteront une prise de décision. Il est donc ici fondamental de connaître les prérogatives des différents personae afin de déterminer le bon degré d’informations dont ils doivent disposer.

Un bon test consiste à déterminer un titre explicite au rapport ou à l’application (groupe de rapports). Si ce titre n’est pas suffisamment précis et évocateur, le cadre a peut-être été défini de manière trop large.

Vous aussi, comptez le nombre de rapports “test” dans votre organisation !

Les étapes suivantes de collecte et encore plus de modélisation ne se prêtent pas à un élargissement du besoin a posteriori, c’est pourquoi il est fondamental que le cadre soit bien défini et arrêté, quitte à prendre le temps nécessaire pour cela.

Étape 1 : Collecter les données sources

Établir le « dictionnaire de données », liste exhaustive des informations attendues, organisées par dans des entités traduisant des notions métiers (clients, factures, produits, ventes, stock, etc.). Puis faire la correspondance avec les données stockées ou à stocker dans le Système d’Information (SI).

Selon l’origine des données (fichiers, bases de données, Web, etc.), des opérations de nettoyage pourront être nécessaires.

Pour chaque source de données, il faudra préciser la fréquence de rafraichissement attendue. Sauf à utiliser les dataflows, les sources de données dans un dataset Power BI sont à ce jour toutes actualisées simultanément.

D’un point de vue technique, il faut anticiper le fait qu’un persona puisse être soumis à une stratégie de sécurité à la ligne (« Row Level Security »), c’est-à-dire être limité à un périmètre de données. Pour limiter l’accès à des indicateurs, il fallait jusqu’à présent définir un autre rapport mais l’Object Level Security arrive dans Power BI en ce mois de février 2021.

Étape 2 : Modéliser

Afin de fonctionner au mieux (simplicité des calculs, passage à l’échelle), le respect de la modélisation en étoile(s) est fortement conseillé. Il est donc impératif de disposer de clés primaires (champ exprimant une notion d’unicité) dans les tables de dimensions.

https://docs.microsoft.com/fr-fr/power-bi/guidance/star-schema

Il faut ici anticiper les croisements demandés et donc l’étape suivante de la définition des indicateurs. Faire préciser sur quel(s) axe(s) d’analyse un indicateur devra être représenté. L’axe du temps demande une attention toute particulière car il déterminera la relation principale (active) de la table de dates (le calendrier, à la granularité journalière) avec la table de faits. Si plusieurs notions de temps sont demandées (ex. : date de commande, date d’expédition, date de livraison), on définira des relations inactives et les mesures DAX utiliseront le pattern suivant :

= CALCULATE([measure], USERELATIONSHIP(FAITS[date2], CALENDAR[date])

Étape 3 : Définir les indicateurs

Au-delà des agrégations simples (somme, moyenne, min-max, etc.) qui devront être programmés comme des mesures explicites (faîtes vraiment une mesure, ne vous contentez pas de l’agrégat automatique !), les indicateurs plus complexes doivent être définis par les règles métiers qu’ils traduisent.

Par exemple, le montant total des ventes s’exprime TTC, en retirant les remboursements, indiqués par un code spécifique.

Ces règles devront être accessibles à tous et remporter l’unanimité. En effet, rien de pire qu’un indicateur dont la formule varie selon les interlocuteurs ! Chacun disposerait alors de sa propre version, sans doute correcte, mais qui ne permettrait pas un usage commun et raisonné.

Étape 4 : Définir les sous-thèmes du rapport

Nous recommandons de ne présenter sur une page de rapport qu’un seul thème, quitte à dédier plusieurs pages à celui-ci.

Chaque thème doit être mis en regard des personae qui vont le consulter.

Toutefois, un rapport présentant un trop grand nombre de pages (disons 10 arbitrairement) témoigne sans doute d’un cadrage trop large et il serait sûrement possible de réaliser plusieurs rapports, quitte à regrouper ceux-ci au sein d’une application.

Étape 5 : Définir les visuels et les filtres

Il faut distinguer les visuels portant un message (la conclusion est connue et doit être communiquée) et ceux permettant de réaliser une analyse (l’interprétation est à construire, éventuellement en manipulant des filtres ou des paramètres).

Les principaux visuels d’analyse natifs sont :

  • Le nuage de points ou de bulles (scatterplot)
  • Les influenceurs clés (key influencers)
  • L’arbre de décomposition (decomposition tree)
  • La cascade (waterfall)
  • Les courbes associées à la fonctionnalité « expliquer la hausse / la baisse »

Les autres visuels doivent être utilisés pour porter un et un seul message. Pour passer plusieurs messages, il faut faire plusieurs visuels. Le message, s’il est constant, peut être exprimé au travers du titre du visuel.

De nombreux chart choosers permettent de choisir le bon visuel adapté à la comparaison que l’on souhaite présenter. Il existe une version spécifique à Power BI, incluant de nombreux custom visuals : Power BI Visuals Reference – SQLBI

Les principales comparaisons, et quelques graphiques associés, sont :

  • La décomposition (les parties d’un tout)
    • La position ou le classement (selon un indicateur)
    • L’évolution (dans le temps)
    • Le flux (entre deux ou plusieurs étapes)
    • La corrélation (entre deux indicateurs numériques)
    • La géolocalisation

Le choix par défaut d’un visuel croisant un axe d’analyse et un indicateur se portera judicieusement sur le diagramme en barres. On facilitera la lecture par le tri, souvent décroissant, sur l’indicateur et la position horizontale des barres, aidant à la lecture des légendes, sans avoir à pencher la tête.

Fin du parcours ?

Voilà, nous disposons enfin d’un rapport concret, contenant des données, représentées sous forme visuelle ! Le (long) chemin ne s’arrête pas là puisqu’il faudra maintenant itérer, c’est-à-dire discuter avec les utilisateurs du rapport pour améliorer, par petites touches, le rendu afin de coller au mieux aux usages. L’étape 5 va donc être remise en cause, jusqu’à se stabiliser autour d’un rendu final. De temps en temps, il pourra être nécessaire de revenir à l’étape 4, voire 3. Analyser des données procure de nouvelles idées… En revanche, si l’étape 2 de modélisation doit être remise en cause, c’est que le processus de recueil de besoin a sans doute été incomplet ou trop rapide. N’hésitez pas alors à faire table rase et à repartir de (presque) zéro pour un nouveau projet. Vous verrez alors que les différentes s’enchaineront beaucoup plus vite et avec plus de fluidité.

Déployer Azure Databricks avec Terraform

Déployer une infrastructure cloud est un projet qui peut (et doit !) se penser comme du code. En effet, une rigueur dans le nommage des ressources est particulièrement importante et nous aurons certainement plusieurs environnements à créer (développement, qualification, production…). Nous allons donc éviter des tâches répétitives et sécuriser ce processus. En cas de perte d’une partie ou de tout un environnement, il sera également simple de le recréer à l’identique.

Nous allons pour cela utiliser Terraform qui présente l’avantage d’être commun à différents fournisseurs de cloud.

Azure Databricks ne fait pas exception à cette démarche et nous allons illustrer ici le déploiement d’un espace de travail au travers d’un script Terraform.

Installer Terraform dans un environnement Windows

  • Télécharger la dernière version de Terraform via le lien suivant : Terraform Versions | HashiCorp Releases
  • Créer un dossier : “.terraform” dans le répertoire : C:\Users\username\.terraform
  • Dézipper le fichier téléchargé et le déplacer dans le dossier “.terraform”
  • Ajouter le chemin: C:\Users\username\.terraform dans la variable d’environnement Path
  • Depuis un terminal, tester la commande : terraform –version

Ecrire le fichier main.tf

Le début du fichier va référencer la version de Terraform utilisée.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=2.47.0"
      skip_credentials_validation = false
    }
  }
}

Pour autoriser le script à prendre la main sur notre souscription Azure, un principal de service sera nécessaire et celui-ci doit disposer des droits de niveau gestion des ressources sur la souscription. Dans un premier temps, une authentification par Azure CLI est aussi envisageable. Les différents modes sont décrits dans la documentation de Terraform.

Depuis un environnement local, nous écrivons la valeur des variables sous la forme nomVar = “valeur” dans un fichier au nom réservé terraform.tfvars.

Les variables sont ensuite déclarées et appelées dans le code à l’aide de la syntaxe suivante :

# VARIABLES
variable "subscription_id" {}
variable "client_id" {}
variable "client_secret" {}
variable "tenant_id" {}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
}

Un groupe de ressource sera nécessaire et il se crée avec la syntaxe suivante :

resource "azurerm_resource_group" "rg" {
  name     = "rg-analytics"  
  location = "West Europe"

  tags = {
        environment = "dev"
        project = "unified analytics"
    }
}

Passons maintenant à la création de la ressource Databricks en elle-même, nous ferons référence à l’alias du groupe de ressources défini dans Terraform (ici, “rg”).

resource "azurerm_databricks_workspace" "dbx" {
  name                = "dbx-workspace"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  sku                 = "standard"

  tags = {
    environment = "dev"
    project = "unified analytics"
  }
}

La valeur définie dans “sku” correspond à la licence choisie parmi les valeurs standard, premium ou trial. Cette dernière permet de tester la licence Premium pendant 14 jours.

La création d’une ressource Databricks entraine la création conjointe d’un groupe de ressources managé contenant un compte de stockage, un réseau virtuel et un groupe de sécurité réseau.

Si nous avons besoin d’un cluster interactif pour utiliser cette ressource (attention, pourquoi ne pas passer par des automated clusters lancés par Azure Data Factory comme expliqué dans cet article ?).

Les propriétés utilisées sont celles visibles dans la description au format JSON du cluster, depuis l’espace de travail Databricks.

resource "databricks_cluster" "cluster" {
  cluster_name  = "myInteractiveCluster"
  spark_version = "7.3.x-scala2.12"
  node_type_id  = "Standard_F4s"
  num_workers = 2
  autotermination_minutes = 60
  library {
    pypi {
      package = "azureml-core"
    }
  }
}

Lors de la création de ce cluster, nous installons automatiquement la librairie azureml-core, qui correspond au SDK pilotant les ressources d’Azure Machine Learning, souvent évoqué sur ce blog.

Quelques commandes Terraform

Pour initialiser le répertoire contenant les différents fichiers, nous lançons la commande suivante :

terraform init

Un sous-répertoire .terraform se crée alors automatiquement.

Pour analyser le script Terraform et visualiser les éléments qui seront ajoutés ou modifiés :

terraform plan

Par défaut et sans préciser l’argument -var-file, le fichier de secrets terraform.tfvars est utilisé.

Pour appliquer ces transformations :

terraform apply

Une validation sera demandée.

Pour supprimer les ressources définies dans le main.tf :

terraform destroy

A nouveau (et heureusement !), une validation est requise.

Voici donc nos premiers pas sous Terraform et la simplicité d’utilisation vous fera vite oublier les cases à remplir et à cocher de l’interface utilisateur du portail Azure !

Il ne reste plus qu’à intégrer ce script dans un pipeline d’intégration continue, par exemple sous Azure DevOps.

Exploiter l’API Azure Databricks en PowerShell

L’API d’Azure Databricks permet de réaliser de nombreuses actions au moyen de commandes émises au travers d’une URL, de type GET ou POST. La documentation complète est disponible sur le site de Microsoft ou bien sur celui de Databricks.

Un premier exemple prend la forme ci-dessous et permet d’obtenir des informations détaillées sur un cluster :

GET https://<databricks-instance>/api/2.0/clusters/get?cluster_id=<cluster-id>

Les identifiants nécessaires de l’espace de travail et du cluster Databricks peuvent être obtenues en se rendant sur la page Web du cluster.

Bien sûr, Databricks ne se limite pas à des clusters, il faut des notebooks contenant du code et ceux-ci sont pilotés par des jobs. Pour imaginer un scénario paramétrable, nous définissons des widgets dans le notebook, ce qui permettra de passer les valeurs de ces paramètres aux jobs.

parquet_file = dbutils.widgets.get("pParquetFile")
limit = int(dbutils.widgets.get("pLimit")

La définition du job se fait dans l’interface dédiée et les paramètres peuvent y être déclarés. Il faut noter ici l’identifiant du job, nous en aurons besoin par la suite.

Sauf à planifier le job, ces étapes resteront manuelles et les valeurs des paramètres seront à préciser à chaque exécution.

En intégrant différentes instructions dans un script PowerShell, nous pouvons élaborer le scénario suivant :

Le code correspondant est détaillé ci-dessous :

$param1 = "abc"
$param2 = 123
$limitRows = 100

$dbxPAT = "dapixxxxxxxxxxxxxxxxxxxxx"
$dbxBearerToken = "Bearer " + $dbxPAT
$dbxWorkspaceURL = "https://adb-00000000000000000.0.azuredatabricks.net/"
$clusterID = "0000-000000-xxxxx000"
$jobID = 42
$localPath = "C:\temp\"
$filePath = $localPath+$fileName

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", $dbxBearerToken)

 Write-Output "Cluster status"
 $URL = $dbxWorkspaceURL + "api/2.0/clusters/get?cluster_id="+$($clusterID)
 $responseCluster = Invoke-RestMethod $URL -Method 'GET' -Headers $headers
 $clusterState = $responseCluster.state
 Write-Output "Cluster state :" $clusterState

 If($clusterState -ne "RUNNING")
 {
     Write-Output "Start cluster"
     $URL = $dbxWorkspaceURL + "api/2.0/clusters/start"
     $body = @{"cluster_id"=$clusterID} | ConvertTo-Json
     $responseStart = Invoke-RestMethod $URL -Method 'POST' -Headers $headers -Body $body
     $clusterState = $responseStart.state
 
    While($clusterState -ne "RUNNING")
  {
     Write-Output "Please wait one minute more...the cluster is starting"
     Start-Sleep -Seconds 60
     $URL = $dbxWorkspaceURL + "api/2.0/clusters/get?cluster_id="+$($clusterID)
     $responseStart = Invoke-RestMethod $URL -Method 'GET' -Headers $headers
     $clusterState = $responseStart.state
     Write-Output $clusterState
  }
 }

Write-Output "Run job"
$body = @{
   "job_id"= $jobID
   "notebook_params"= @{"P1"= $param1 ;"P2"= $param2 ;"limit"= $limitRows }
 } | ConvertTo-Json
$URL = $dbxWorkspaceURL + "api/2.0/jobs/run-now"
$responseJob = Invoke-RestMethod $URL -Method 'POST' -Headers $headers -Body $body
$runId = $responseJob.run_id
Write-Output "run ID : "$runId

$URL = $dbxWorkspaceURL + "api/2.0/jobs/runs/get-output?run_id="+$($runId)
Write-Output "Run status and Get output"
$runStatus = "PENDING"
Do
{
 Write-Output "Please wait one minute more...the job is running"
 Start-Sleep -Seconds 60
 $responseRun = Invoke-RestMethod $URL -Method 'GET' -Headers $headers
 $responseun | ConvertTo-Json
 $runStatus = $responseRun.metadata.state.life_cycle_state
 Write-Output $runStatus
 }
Until($runStatus -eq "TERMINATED")

Write-Output "Check run result state"
$runResultState = $responseRun.metadata.state.result_state
Write-Output $runResultState

If($runResultState -ne "SUCCESS")
{
    exit;
}

Write-Output "Export result to file"
$result = $responseRun.notebook_output.result
$isTruncated = $responseRun.notebook_output.truncated
if($isTruncated -eq "False")
{
    $result | Out-File -FilePath $filePath -Force
    Write-Output "Export done"
}

Dans ce code, nous nous appuyons sur la fonction Invoke-RestMethod suivie de l’URL de l’API Databricks. La réponse sera ensuite exploitée pour continuer le programme.

L’instruction api/2.0/jobs/runs/get-output?run_id= permet de retourner un texte passé en paramètre de la commande Databricks qui viendra conclure le notebook (aucune autre cellule ne sera ensuite exécutée) :

dbutils.notebook.exit(textObject)

Le contenu de la variable textObject se retrouve alors au niveau metadata.state.result_state du résultat de l’instruction. La sortie ne peut dépasser un volume de plus de 5Mo. Nous pouvons vérifier que le résultat n’est pas tronqué à l’aide de la valeur de l’élément notebook_output.truncated à false.

En mettant en œuvre ce code au sein d’une ressource comme Azure Function (les paramètres définis au début du code intégrant alors la route de la fonction), nous avons obtenu une “meta API” paramétrable, restituant un résultat sous forme d’export de données !

Créer un custom role pour sécuriser les accès au portail Azure Machine Learning

Le service Azure Machine Learning offre de nombreuses fonctionnalités au travers de son portail accessible par l’URL https://ml.azure.com:

  • Notebooks Jupyter
  • Interface d’automated Machine Learning
  • Interface de conception visuelle de pipelines de Machine Learning
  • Gestion de ressources de calcul
  • Gestion des sources et des jeux de données
  • etc.

Dans une optique d’utilisation en entreprise, et donc en équipe, tout le monde ne bénéficiera pas de toutes ces fonctionnalités. En effet, il faut poser a minima des garde-fous concernant la gestion des ressources de calcul, car la facturation du service en dépend directement !

On pourra également souhaiter que seuls certains administrateurs soient en capacité de créer ou supprimer des sources de données. Bref, il faudra établir des profils types (“personae“) des utilisateurs et leur accorder le bon périmètre d’actions autorisées.

La gestion dite “Role Based Access Control” (RBAC) des ressources Azure permet de répondre à ce besoin. Dans le cas d’Azure Machine Learning, nous disposons de trois rôles prédéfinis, décrits dans la documentation officielle.

Extrait de la documentation Microsoft https://docs.microsoft.com/fr-fr/azure/machine-learning/how-to-assign-roles#default-roles

L’affectation d’un rôle se fait dans le menu “Access control (IAM)” de la ressource, depuis le portail Azure.

Mais prenons par exemple le scénario suivant. Une organisation fait travailler des Data Scientists dans un espace de travail Azure Machine Learning et souhaite qu’ils puissent :

  • Créer des jeux de données à partir de sources de données définies (et ne pas supprimer ces dernières)
  • Créer des expériences de Machine Learning par le biais du Concepteur, de l’Automated ML ou de notebooks
  • Solliciter des ressources de calculs de type “compute instance” ou “compute cluster” mais ne pas en créer de nouvelles
  • Publier des pipelines d’inférence sous la forme de points de terminaison (endpoint) temps réel ou par lot (batch)

Cette description ne correspond à aucun rôle prédéfini, nous allons donc passer par la création d’un rôle personnalisé ou “custom role“.

Ce rôle ne peut être créé que par le propriétaire de la ressource Azure Machine Learning et se fait au niveau de l’espace de travail contenant cette ressource.

Pour débuter plus simplement, nous commençons par cloner un des trois rôles existants :

Le menu suivant permet d’ajouter ou de retirer des permissions. La liste de celles-ci s’obtient en recherchant le terme “machine learning”.

Les (très nombreuses !) actions sont alors détaillées et souvent déclinées sous le forme : read / write / delete / other.

Veillez à bien préciser le périmètre d’application du rôle en notant l’identifiant de souscription suivi du nom du groupe de ressources où se trouve le service Azure Machine Learning.

Nous allons ensuite utiliser le langage JSON pour définir précisément chaque action.

{
"properties": {
"roleName": "Data Scientist",
"description": "Create and run experiments, publish endpoints, can't create or delete compute ressources and data sources",
"assignableScopes": [
"/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/rg-sandbox"
],
"permissions": [
{
"Actions": [
            "Microsoft.MachineLearningServices/workspaces/*/read",
           ],
"NotActions": [
            "Microsoft.MachineLearningServices/workspaces/*/write",
            "Microsoft.MachineLearningServices/workspaces/*/delete",
              ],
"dataActions": [],
"notDataActions": []
}
]
}
}

Il faut comprendre que certaines actions sont “imbriquées” et il sera possible d’autoriser un niveau supérieur pour sous-entendre l’autorisation sur les niveaux inférieurs. C’est ainsi le rôle du caractère “*” dans la ligne ci-dessous.

 "Microsoft.MachineLearningServices/workspaces/*/read"

Voici le JSON complet répondant à notre scénario.

Ainsi, dans la partie Clusters de calcul, le bouton “Nouveau” a disparu mais l’utilisateur pourra toujours affecter un cluster à une expérience lancée par le Concepteur ou par le biais de l’Automated ML.

Mais nous verrons que ce rôle interdit l’utilisation d’instance de calcul, c’est-à-dire des machines virtuelles qui sont le support de l’usage de notebooks. Autant dire que les Data Scientists n’iront pas très loin sans cela ! (Sauf peut-être à travailler depuis Visual Studio Code et nous en reparlerons dans un prochain article…).

Nous cherchons donc à ajouter ces ressources de calcul, à l’aide des permissions cochées dans l’image ci-dessous.

Malheureusement il n’existe pas de droits différenciant les instances de calcul des clusters. Nous ne pouvons donc pas aboutir à un scénario où l’utilisateur pourrait créer son instance personnelle sans pour autant créer un grappe d’une dizaine de machines en GPU ! Une instance de calcul est dédiée à un et un seul utilisateur mais il est possible pour un administrateur de la ressource (profil Owner) de créer des VMs et de les affecter à des utilisateurs. Ce processus se fait au travers du déploiement d’un template ARM et est documenté sur ce lien.

Attention, même si l’utilisateur ne peut pas accéder à la VM, il pourrait tout de même avec ces autorisations la démarrer… et oublier de l’éteindre (pas d’arrêt planifié)… et donc faire augmenter la facturation !

EDIT : Les “notebook VM” ont été remplacées par les instances de calcul mais les droits associés figurent toujours dans la liste des permissions.

Utiliser Databricks CLI

Jusqu’à présent, nous avons l’habitude d’utiliser un espace de travail Azure Databricks en nous connectant au portail, l’authentification étant réalisée par un couple login / password déclaré dans l’annuaire Azure Active Directory.

Dans un but d’automatisation des tâches, il est intéressant de se pencher sur les commandes en lignes ou CLI. Celles-ci sont documentées sur le site de Databricks. Les commandes peuvent être vues comme une surcouche de l’API REST de Databricks.

Installation du CLI Databricks

Un environnement Python est nécessaire. Nous pouvons ensuite lancer le téléchargement du package dédié avec la commande ci-dessous, depuis un terminal.

pip install databricks-cli

Nous vérifions dans la foulée que l’affichage de l’aide d’une commande, sur laquelle nous reviendrons plus tard, est fonctionnel.

databricks fs -h

La version installée peut être retrouvée par la commande :

databricks --version

C’est un produit qui évolue rapidement et il conviendra de le mettre à jour fréquemment, afin de bénéficier d’un maximum de fonctionnalités.

Authentification

Nous allons utiliser un Personal Access Token pour nous authentifier auprès du service. Ce jeton de sécurité est obtenu sur le portail Databricks, dans le menu User Settings.

Attention à bien noter (dans un coffre-fort électronique !) le jeton obtenu, il ne sera plus possible d’afficher sa valeur par la suite.

Nous démarrons la configuration par la commande :

databricks configure --token

Deux valeurs seront attendues : l’URL de l’espace de travail, puis le token précédemment généré. L’URL pour une ressource Azure est dorénavant de la forme https://adb-XXXXXXXXXXXXXXXX.XX.azuredatabricks.net/.

Un fichier local s’écrit alors et contient les informations renseignées.

Vérifions maintenant que l’authentification est effective en lançant une commande interagissant avec l’espace de travail :

databricks workspace ls

Si nous obtenons un message d’erreur Error: b’Bad Request’, la cause peut être un mauvais enregistrement du token dans le fichier de configuration. En ouvrant celui-ci dans un éditeur de texte, nous visualisons le résultat suivant :

Remplacer les caractères SYN par la valeur du token permettra de résoudre ce problème mais demande de stocker ce secret de manière non sécurisée.

Nous pouvons maintenant lister le contenu de l’espace de travail.

Copier un fichier depuis le FileStore

Le FileStore est une zone de stockage spécifique (un dossier) du DataBricks File System (DBFS). Celui-ci nous permet de réaliser des échanges avec l’extérieur : copie de fichiers vers ou depuis le DBFS. Une documentation complète est disponible ici.

A l’aide des commandes du CLI, nous identifions un fichier que nous pouvons recopier localement.

Comme le CLI est une surcouche de l’API REST de Databricks, il est intéressant de retrouver la commande initiale émise vers l’espace de travail. Nous pouvons l’obtenir en ajoutant –debug après la commande de copie.

Le premier appel vérifie le statut du fichier. Il est possible d’exécuter la requête dans Postman (coller un token dans la partie Authorization).

Nous trouvons ici la taille du fichier. Il faut savoir que le contenu du fichier est converti en base 64 et nous allons ensuite mieux comprendre pourquoi.

Un second appel à l’API se fait avec la méthode read et deux options &offset=0&length=1048576. Si le fichier dépasse 1 méga-octet, celui-ci est découpé en morceaux (chunks) et plusieurs appels seront nécessaires. La commande reconstitue toutefois le fichier et le décode automatiquement. Tout cela est donc transparent pour l’utilisateur !

Premiers pas avec l’API Text Analytics sous Postman

Les services cognitifs Azure permettent de bénéficier d’algorithmes déjà entrainés pour répondre à des analyses de type « cognitives », c’est-à-dire simulant le fonctionnement du cerveau humain (raisonnement) et des sens comme la vue ou l’ouïe.

Plusieurs domaines sont couverts par ces services :

  • La décision
  • Le langage
  • La parole (« speech »)
  • La vision

Dans les domaines du texte et du langage, nous bénéficions, outre la traduction, de 4 principales fonctionnalités ;

  • La détection de la langue
  • La reconnaissance d’entités nommées
  • L’extraction de phrases clés
  • L’analyse de sentiments

Mais avant de nous lancer, il nous faut créer une ressource Azure qui fournira une clé d’authentification auprès de l’API. Cette ressource est aujourd’hui (janvier 2021) commune à la plupart des APIs des services cognitifs Azure, ce qui simplifie son usage. On veillera toutefois, dans un environnement de production, à segmenter les ressources selon les différents usages. QnA Maker, Speech Services, Translator et Custom Vision ne sont actuellement disponibles que sous forme d’API individuelles.

Nous pouvons bénéficier de la tarification Standard S0, décrite sur ce lien.

Cette tarification se base sur des tranches de 1000 enregistrements de texte, avec un tarif dégressif selon des paliers d’enregistrements.

A la page de la ressource, nous trouvons deux informations qui seront ensuite indispensables : point de terminaison ou endpoint (celui-ci dépend de la région Azure préalablement sélectionnée) et deux clés d’authentification. Disposer de deux clés permet d’assurer une rotation lors du renouvellement des clés.

De nombreux langages de programmation permettent d’appeler les APIs des services cognitifs (C#, Python, node.js, Go, Ruby…) mais pour simplifier le propos et aller directement à l’essentiel, nous utiliserons ici le client Postman, téléchargeable sur ce lien, qui permettra d’interroger le point de terminaison et de bien détailler le rôle de chaque élément.

La page de documentation la plus utile sera certainement celle-ci. Nous y trouvons les différents endpoints régionaux, et dans le menu de gauche, les quatre actions possibles à partir du service cognitif Text Analytics.

Interrogation sous Postman

Le premier élément nécessaire sera l’URL de l’API, de la forme :

https://{endpoint}/text/analytics/vX.X/languages[?showStats]

Le endpoint, visible dans le portail Azure, s’écrit :

<nom-de-la -ressource>.cognitiveservices.azure.com

Nous adaptons cette URL à notre région, en retirant le paramètres ?showStats et en positionnant la boîte de dialogue de Postman sur POST. La version de l’API doit être précisée. A ce jour, nous utilisons la version 2.1 ou 3.0 voire la préversion 3.1 qui apporte de nouvelles fonctionnalités. Celles-ci sont détaillées dans cette documentation officielle.

Deux informations d’entêtes sont requises, sous forme de couple clé-valeur et nous les renseignons dans le menu Headers :

  • Le type de contenu
  • La clé d’authentification

En basculant sur le contenu brut (bouton « bulk edit » de l’interface), nous obtenons le script suivant :

Content-Type:application/json
Ocp-Apim-Subscription-Key:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Pour renseigner le corps de la requête (« Body »), il est également possible de basculer sur l’affichage « raw », pour un contenu de type JSON.

Le schéma attendu nous est à nouveau donné par la page de documentation de l’API.

Pour la détection du langage, chaque document est attendu, au sein d’une liste marquée par des crochets, sous la forme de trois éléments :

  • countryHint (facultatif)
  • id
  • text

Pour une seule phrase, le contenu minimal de ce corps est le suivant :

{documents:[{"id":0, "text": "dans quelle langue cette phrase est-elle écrite ?"}]}

La réponse obtenue, de statut 200 OK, prend alors la forme ci-dessous :

Le score donne un indicateur de confiance, entre 0 et 1, de la prévision réalisée. En pratique, un doute sera émis dès que le score sera inférieur à 1.

L’information countryHint peut être renseignée par un code sur deux lettres représentant le pays d’où est issu le texte, ce qui permet de retirer d’éventuelles ambiguités lorsqu’un même mot est utilisé dans plusieurs langues. Le champ peut être soit omis, soit renseigné comme une chaîne vide countryHint = “”.

Lorsqu’il n’est pas possible d’identifier un langage, la réponse otenue sera le mot « (Unknown) ».

Le schéma ci-dessous résume les principales syntaxes d’interrogation de l’API Text Analytics.

L’API Translator

Dans la foulée de la reconnaissance de la langue, il est logique de rechercher à traduire le texte. Nous appelons ici une autre API : Translator, réalisant ci-dessous une traduction en français.

https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=fr

Le body de la requête s’écrit de la sorte :

[{"Text":"Azure Cognitive Services are cloud-based services with REST APIs and client library SDKs available
to help you build cognitive intelligence into your applications.
You can add cognitive features to your applications without having artificial intelligence (AI) or data science skills."}]

Nous obtenons la sortie suivante.

Si besoin, la langue d’origine peut être précisée et ce n’est pas le mécanisme d’auto-détection qui est mis en œuvre.

https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from=en&to=fr

La documentation détaillée de l’API est disponible sur ce GitHub et un extrait est illustré ci-dessous.

Un niveau de tarification supérieur à S0 est nécessaire, sinon le message suivant apparaît :

The Translate Operation under Translator Text API v3.0 is not supported with the current subscription key
and pricing tier CognitiveServices.S0.

Les méthodes l’API Text Analytics

Voyons maintenant les trois autres méthodes disponibles avec l’API Text Analytics.

Celles-ci ne seront pas toujours disponibles en français, ou dans d’autres langues que l’anglais. La vérification peut être faite, en fonction de la version de l’API, sur cette page.

L’analyse de sentiments évalue un texte dans sa totalité en se basant sur le poids des mots utilisés, pour attribuer trois scores : positif, neutre, négatif. Un sentiment global est donné sous forme de texte : positive, mixed, negative ou neutral si aucun des trois cas précédents n’est vérifié.

https:// <nom-de-la-ressouce>.cognitiveservices.azure.com/text/analytics/v3.1-preview.3/sentiment

Pour obtenir l’exploration des opinions, uniquement en anglais à ce jour, nous ajoutons à la fin de l’URL : ?opinionMining=true

{documents:[{"language": "en", "id":0, "text": "Very welcome and advice from the guests. Very clean accommodation. We recommend it!"}]}

Nous obtenons alors, lorsqu’une relation de type opinion est détectée, un groupe d’informations supplémentaires intitulé « opinions », disposant de deux scores, positif et négatif.

L’extraction de phrases clés permet d’identifier des éléments (mots ou groupes de mots) les plus importants au sein d’un texte (phrase ou ensemble de phrases). On pourrait voir cela comme une extension d’un preprocessing visant à retirer les mots outils (« stop words »).

https://< nom-de-la-ressouce>.cognitiveservices.azure.com/text/analytics/v3.0/keyPhrases

Nous obtenons le résultat suivant avec la première phrase du roman de Marcel Proust, A la Recherche du Temps Perdu.

La reconnaissance d’entités nommées se fait avec la syntaxe :

recognition/general

Nous retrouvons certains éléments ressortant dans les phrases clés, pour lesquels une catégorie et sous-catégorie sont données, associées à un score de confiance. A ce jour (février 2021), c’est la version 2.1 de l’API qui est utilisée en langue française, même si la version 3.0 est indiquée dans l’URL.

La liaison d’entités (non disponible pour l’instant en français) ajoute une information supplémentaire à la reconnaissance générale d’entité en spécifiant l’URL d’une page Wikipedia dédiée à cette entité.

https://<nom-de-la -ressource>.cognitiveservices.azure.com/text/analytics/v3.0/entities/linking

Les Informations d’identification personnelle (PII) sont des patterns correspondant à différents éléments : numéro de téléphone, adresse e-mail, adresse postale, numéro de passeport.

https://<nom-de-la -ressource>.cognitiveservices.azure.com/text/analytics/ v3.1-preview.3/entities/recognition/pii

Nous remarquons que la sortie donne un élément redactedText qui remplace les éléments reconnus comme informations personnelles par des étoiles (*).

Un domaine (optionnel) peut être précisé pour obtenir les informations (personnelles) médicales mais il n’existe à ce jour pas de documentation explicitant les éléments pouvant être identifiés.

https://<nom-de-la -ressource>.cognitiveservices.azure.com/text/analytics/v3.1-preview.3/entities/recognition/pii?domain=phi

Text Analytics avec le concepteur (Designer) dans Azure Machine Learning

Les citizen data scientists disposent d’un outil graphique dans le portail Azure Machine Learning nommé Concepteur (ou Designer en anglais) permettant de réaliser des pipelines de Machine Learning, pour l’apprentissage comme pour l’inférence (c’est-à-dire le fonctionnement prédictif).

Depuis la première version de l’outil, anciennement nommé Azure Machine Learning Studio, et dit maintenant “classique”, des modules pour l’analyse textuelle étaient déjà présents mais l’offre s’est aujourd’hui, en ce début 2021, agrandie.

L’objectif de cet article est de faire un survol des méthodes disponibles, dans cette approche “low code“, pour un jeu de données contenant un champ de “texte libre”, et dans le but de réaliser une méthode d’apprentissage supervisé, et plus précisément de classification.

En prérequis, rappelons les notions indispensables avant de s’attaquer à notre problématique :

– disposer d’une ressource de calcul dite “compute cluster” qui servira à exécuter les différentes itérations du pipeline (succession de modules)

– créer une nouvelle expérience, qui permettra de suivre le résultat des exécutions ou runs (outputs, logs, artefacts, métriques…)

– connaître la structure classique d’un pipeline d’entrainement supervisé (s’inspirer des exemples disponibles, en particulier “Text Classification – Wikipedia SP 500 Dataset” qui est décrit sur ce site GitHub.

Vue d’ensemble des fonctionnalités de Microsoft Machine Learning Studio (classique)
Image reprise de la documentation officielle de Microsoft et téléchargeable ici : https://download.microsoft.com/download/C/4/6/C4606116-522F-428A-BE04-B6D3213E9E52/ml_studio_overview_v1.1.pdf

Nous allons nous concentrer ici sur deux phases qui auront leurs spécificités pour le traitement de données textuelles :

– le preprocessing

– le choix du modèle d’apprentissage pour une classification

Preprocessing des données textuelles

Nous allons utiliser ici le module Preprocess text qui réalisera de nombreuses actions de nettoyage et de préparation de nos données textuelles, en une seule étape.

Ce module est très puissant et basé sur la librairie de référence SpaCy mais ne supporte pour l’instant malheureusement que la langue anglaise. Nous pourrons contourner cela par un script R ou Python pour le cas de corpus en français. Mais à l’exception de la lemmatisation, les opérations pourront toutefois s’appliquer à un corpus d’une autre langue que l’anglais.

La case à cocher “Expand verb contractions” est toutefois spécifique à des formulations anglaises comme don’t, isn’t, I’ve, you’ll, etc.

Détaillons maintenant les autres options, avec quelques exemples simples si nécessaire.

– Retrait des mots outils (stop words) : a, about, all, any, etc.

Cette opération est paramétrable au moyen de la seconde entrée du module. Nous pouvons donc utiliser un fichier, d’une colonne, contenant les mots en français. Ce site propose une liste de mots outils dans de nombreuses langues.

Lemmatisation (remplacement du mot par sa forme canonique qui est “l’entrée du dictionnaire”, c’est-à-dire, l’infinitif ou le masculin singulier lorsqu’il existe) : la phrase “The babies are walking on their feet” donnera “The baby be walk on their foot” puis sans mots outils “baby walk foot“.

– Détection des phrases (séparées alors par |||, à choisir si l’on souhaite un traitement spécifique par phrase plutôt que par ligne du jeu de données)

– Normalisation en minuscules (sinon les mots sont considérés comme différents)

Suppression des nombres, caractères spéciaux (non alphanumériques), caractères dupliqués plus de deux fois, adresses email de la forme <string>@<string>, URLs reconnues par les préfixes httphttpsftpwww

– Remplacement des backslashes \ en slashes / : l’usage ne sera pas fréquent, on passera plutôt par la suppression de ces caractères spéciaux.

Séparation des tokens (ici, les mots) sur la base de caractères spéciaux comme & (pour l’esperluette, on préfèrera à nouveau la citer dans les caractères spéciaux à supprimer) ou - : un “pense-bête” deviendra “penser bête” suite à séparation et lemmatisation en français.

En pratique, il est important de maîtriser l’ordre dans lequel ces opérations s’appliquent. Ainsi, on réalisera plutôt les opérations de nettoyage (suppressions, minuscules, mots outils…) avant les opérations de lemmatisation.

Importer la librairie NLTK et les mots outils en français

Nous disposons d’un module Python permettant d’exécuter un script, sur un ou deux dataframes en entrée de ce module. Le code suivant nous permettra d’importer la librairie NLTK et d’utiliser la liste des mots outils ou bien la lemmatisation en français.

import pandas as pd

def azureml_main(dataframe1 = None, dataframe2 = None):
    import importlib.util
    package_name = 'nltk'
    spec = importlib.util.find_spec(package_name)
    logging.debug(spec)
    if spec is None:
        import os
        os.system(f"pip install nltk")

    import nltk
    from nltk.corpus import stopwords
    stopwords = set(stopwords.words('french'))

    def preprocess(text):
        clean_data = []
        for x in (text[:]):
            print(x)
            new_text = re.sub('<.*?>', '', x)   # remove HTML tags
            new_text = re.sub(r'[^\w\s]', '', new_text) # remove punc.
            new_text = re.sub(r'\d+','', new_text)# remove numbers
            new_text = new_text.lower() # lower case, .upper() for upper          
            if new_text != '':
                clean_data.append(new_text)
        return clean_data    

    dataframe1['preprocess'] = preprocess(dataframe1['Text'])

    dataframe1['preprocess_without_stop'] = dataframe1['preprocess'].apply(lambda x: [word for word in x if word not in stopwords])

    #On peut observer la liste des mots outils de NLTK dans la seconde sortie
    dataframe2 = pd.DataFrame(list(stopwords))

    return dataframe1, dataframe2,

Apprentissage supervisé sur données textuelles

Dans le groupe Text Analytics, nous disposons des modules ci-dessous.

Vowpall Wabbit

Nous écartons d’emblée l’approche Vowpal Wabbit, framework de Machine / Online Learning initialement développé par la société Yahoo !. Celui-ci nécessite en effet d’obtenir en entrée des données préprocessées mais surtout présentées dans un format spécifique. Vous pouvez toutefois vous référer à ce blog si vous souhaitez préparer vos données en ce sens. Il faudra ensuite utiliser des lignes de commandes au sein du module Train Vowpal Wabbit Model.

Toutefois, nous allons pouvoir remplacer ce framework par une approche relativement similaire réalisée par le module Feature Hashing.

Feature Hashing

Cette méthode consiste à créer une table de hash, c’est-à-dire de valeurs numériques, construites à partir du dictionnaire des mots.

Nous prendrons comme entrée le texte préprocessé. Nous avons alors deux paramètres à renseigner dans la boîte de dialogue du module.

Une taille de 10 bits correspondra à 2^10, soit 1024, nouvelles colonnes dans le jeu de données en sortie. Ces colonnes contiendront le poids de la feature dans le texte. Une valeur de 10 se montre généralement suffisante et attention à l’explosion de la taille du jeu de données (2^20 correspondrait à 1048576 colonnes !).

Un N-gram est une suite de n mots consécutifs, considérée comme une seule unité. Dans la phrase “le ciel est bleu”, nous avons quatre unigrams, trois bigrams (“le ciel”, “ciel est”, “est bleu”) et deux trigrams (“le ciel est”, “ciel est bleu”). Le paramètre attendu est la valeur maximale, ainsi si nous choisissons la valeur 3, nous prenons en compte les unigrams, bigrams et trigrams.

Nous obtenons le résultat suivant.

Seules les colonnes numériques seront ensuite soumises à l’entrainement du modèle.

Extract N-gram feature

Nous retrouvons ici la notion de N-gram (suite de n mots consécutifs, considérée comme une seule unité). Les paramètres disponibles nous permettent de spécifier :

– la taille maximale des N-grams considérés dans le dictionnaire. Ce paramètre conditionnera la taille du jeu de données, qui peut devenir très grand.

– les longueurs minimale et maximale des mots, en nombre de lettres

– la fréquence minimale de présence d’un N-gram dans le document (ensemble des lignes du jeu de données)

– le pourcentage maximum de présence d’un N-gram par ligne sur l’ensemble des lignes. La valeur 1 (100%) correspond à retirer un N-gram présent dans chaque ligne, et donc considéré comme un bruit qu’il faut supprimer.

– la métrique de pondération calculée par ligne, pour chaque feature créée.

La métrique TF-IDF est un standard de la discipline est correspond au ratio de la fréquence d’un N-gram dans la ligne (TF) par le log d’un autre ratio dit”inversé” : le nombre de lignes du jeu de données sur la fréquence du terme dans le jeu de données. Les valeurs seront normalisées selon une norme L2 si la case correspondante est cochée dans la boîte de dialogue.

En créant le vocabulaire, nous obtenons un jeu de données en sortie tel que représenté ci-dessous. Celui-ci contient la liste des N-grams, ainsi que les métriques DF et IDF associées.

Un pronom est ici identifié dans le bigram.

C’est ce vocabulaire établi sur le jeu d’entrainement qui devra saisir à la phase de validation sur les nouvelles données. Il ne faudrait pas créer un nouveau vocabulaire sur la seconde partie des données ! Mais les métriques doivent être calculées. Nous utilisons donc la combinaison ci-dessous des modules.

Le paramètre “Vocabulary mode” est alors positionné sur la valeur ReadOnly.

Convert word to vector

Nous retrouvons ici une méthode dite de plongement lexical (word embedding) qui a connu un fort engouement ces dernières années, et plus communément appelée word2vec. Il s’agit d’utiliser des modèles pré-entrainés sur des milliards de documents, issus par exemple de Wikipedia ou de Google News.

A ce jour (janvier 2021), il ne semble pas y avoir de module permettant de tirer parti du vocabulaire obtenu en sortie. Nous privilégierons donc une approche par script Python, au moyen de librairies comme Gensim (voir le site officiel).

Latent Dirichlet Allocation

Nous sortons ici du cadre supervisé pour obtenir une méthode non supervisée et donc dédiée à rassembler des textes similaires, dans un nombre prédéfini (“Number of topics to model”) de catégories, sans que celles-ci ne soient explicitement définies.

Comme nous disposons déjà des catégories, cette méthode ne se prête pas à l’objectif mais pourrait être intéressante en amont, dans un but exploratoire (nos catégories a priori ne sont peut-être pas si pertinentes…).

Un mode d’options avancées donnera la main sur tous les hyperparamètres du module.

Chaque ligne est évaluée sur les nombre prédéfini de sujets (topics).

A partir de la sortie, il sera nécessaire de réaliser une lecture et une interprétation “humaine” sur les sur les groupes afin de les nommer. Dans la feature matrix topic, nous pouvons visualiser les N-grams les plus contributeurs de chaque sujet.

Publier le pipeline d’entrainement

Publier un pipeline permet de bénéficier de l’ordonnancement de celui-ci, par exemple avec un outil comme Azure Data Factory. Nous obtiendrons ainsi un identifiant (ID) qui devra être renseignée dans l’activité.

Les différents paramètres sélectionnés dans les modules peuvent être ajoutés comme paramètres du pipeline et ainsi renseignés lors de nouveaux lancements.

Déploiement du pipeline d’inférence

Une fois le pipeline d’entrainement soumis et correctement exécuté (tous les modules sont en vert), nous pouvons choisir entre un service Web prédictif, soit en real-time (prévision par valeur), soit en batch (prévision par lot).

Si nous avons comparé plusieurs modèles au sein du pipeline d’entrainement, il faut cliquer sur le module “Train model” correspond à celui que l’on veut déployer (en pratique, celui ayant les meilleures métriques d’évaluation).

Le pipeline graphique se simplifie alors et se voit enrichi d’une entrée et d’une sortie pour le service Web qui sera créé.

Attention, pour aller plus loin, il est nécessaire de supprimer le module d’évaluation ou sinon, vous rencontrerez le message d’erreur suivant.

Un nouveau run du pipeline doit être soumis avant de pouvoir cliquer sur le bouton de déploiement du service web.

Le déploiement se fait au choix, sur une ressource ACI (plutôt pour le test) ou AKS (plutôt pour la production).

En conclusion, même s’il paraît difficile d’aller jusqu’en production avec cet outil, il reste néanmoins très rapide pour mettre en œuvre différentes méthodes reconnues dans le Traitement Automatique du Langage (TAL) et pourra apporter des résultats pertinents dans des cas relativement simples de classification.