Le blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)
Suite de notre épopée :
A l’issue du précédent billet, depuis le WarpStudio et en stockant les données dans la sandbox, nous avons manipuler les données pour :
Cependant, cela n’est pas parfait :
Nous allons voir aujourd’hui comment rappatrier les données générées dans sa propre instance Warp 10.
Pour commencer du bon pied et être sur que tout le monde est au même niveau, nous allons regénérer les prévisions et chaque prévision sera alors stockée dans une sériée dédiée. Cela nous permet d’avoir notre jeu de données de départ. On doit pouvoir sauver directement nos données dans une base distante, mais pour simplifier le tutoriel, nous allons le faire en deux étapes.
'<read token>' 'readToken' STORE
'<write token>' 'writeToken' STORE
// Récupération des séries 2017 > 2020
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'result' STORE
// Prévision sur les 12 prochains mois avec AUTO
// On n'a pas besoin d'afficher les données
// Donc plus besoin de les stocker sous forme de variable avec utilisation de la fonction STORE
// Par contre, on veut perstister les données en base
// ce qui se fait avec UPDATE et l'utilsation d'un token en écriture
[ $result mapper.todouble 0 0 0 ] MAP
AUTO 12 FORECAST.ADDVALUES
"auto_result" RENAME
$writeToken UPDATE
[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO 12 FORECAST.ADDVALUES
"auto_revenue" RENAME
$writeToken UPDATE
[ $exp mapper.todouble 0 0 0 ] MAP
AUTO 12 FORECAST.ADDVALUES
"auto_expense" RENAME
$writeToken UPDATE
# Prévisions avec SAUTO
[ $result mapper.todouble 0 0 0 ] MAP
12 SAUTO 12 FORECAST.ADDVALUES
"sauto_result" RENAME
$writeToken UPDATE
[ $revenue mapper.todouble 0 0 0 ] MAP
12 SAUTO 12 FORECAST.ADDVALUES
"sauto_revenue" RENAME
$writeToken UPDATE
[ $exp mapper.todouble 0 0 0 ] MAP
12 SAUTO 12 FORECAST.ADDVALUES
"sauto_expense" RENAME
$writeToken UPDATE
Si nous vouulons vérifier que vos données sont bien en base, il faut utiliser FETCH :
// Ex de FETCH avec la série auto_revenue
'<read token>' 'readToken' STORE
[ $readToken 'auto_revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2022-01-01T00:00:00Z' ] FETCH
0 GET
Attaquons maintenant la grande traversée des données vers mon instance Warp 10.
Alors on pourrait simplement migrer les données à coup de “curl in/curl out” mais l’idée ici est plus d’illustrer les interactions possibles entre des instances Warp 10.
Pour exécuter du warpscrip sur une instance distante, il faut utiliser la fonction REXEC
Cette exécution distante est désactivée par défaut, il faut donc activer cette extension:
Dans /path/to/warp10/etc/conf.d/70--extensions.conf
, nous avons :
// REXEC
#warpscript.extension.rexec = io.warp10.script.ext.rexec.RexecWarpScriptExtension
#warpscript.rexec.endpoint.patterns = .*
// REXEC connect timeout in ms (default = 0, no timeout)
#warpscript.rexec.timeout.connect = 3600000
// REXEC read timeout in ms (default = 0, no timeout)
#warpscript.rexec.timeout.read = 3600000
Décommentons ces lignes et relançons warp 10.
Pour valider que REXEC est bien activé, depuis le studio, nous pouvons faire un test simple:
'2 2 +' 'https://warp10.url:port/api/v0/exec' REXEC
La réponse est 4.
Attention, il faut choisir votre instance comme endpoint dans la liste déroulante du studio. Si vous êtes sur le endpoint de la sandbox, vous auez le message d’erreur suivant :
https://sandbox.senx.io/api/v0/exec
line #2: Exception at ''2%202%20+' 'https://warp10.url:port/api/v0/exec' =>REXEC<=' in section [TOP] (REXEC encountered a forbidden URL 'http://warp10.url:port/api/v0/exec')
En effet, depuis la Sandbox, il n’est pas possible d’accéder à n’importe quelle machine par mesure de sécurité.
Dans le studio, à partir de maintenant, il doit être configuré pour utliiser votre instance Warp 10 comme endpoint.
Le test simple étant fonctionnel, passons à un test un peu plus compliqué, à savoir recupérer nos données hébergées depuis la sandbox en lecture pour le moment.
// tokens pour la sandbox
'<sandboxReadToken>' 'sandboxReadToken' STORE
'<sandboxWriteToken>' 'sandoxWriteToken' STORE
// Url de la sandbox
'https://sandbox.senx.io/api/v0/exec' 'url' STORE
// On introduit ici la notion de template - comme on va vouloir récupérer plusieurs séries avec les mêmes paramètres
// Autant automatiser un peu et s'appuyer sur une boucle ! :-)
// On crée donc un TEMPLATE pour la fonction FETCH qui va récupérer un token en écriture
// et un nom de classe permettant de récupérer nos GTS.
// Rappel le <' ... '> permet de faire des strings en multi-lignes
// On stocke le template sous la forme d'une variable fetchTpl.
<'
{
'token' '{{ remoteReadToken }}'
'class' '{{ remoteClass }}'
'labels' {}
'start' '2016-12-01T00:00:00Z'
'end' '2022-01-31T00:00:00Z'
} FETCH
'>
'fetchTpl' STORE
// Avec la fonction TEMPLATE, on remplace les clés par leurs valeurs en fournissant le template
// et un dictionnaire à la fonction.
$fetchTpl
{ 'remoteReadToken' $sandboxReadToken 'remoteClass' 'revenue' } TEMPLATE
// Execution de la requête distante avec REXECZ
// La différence avec REXEC est qu'une compression est appliquée sur la réponse à la requête
$url REXECZ
// La liste de GTS issue de FETCH ne contient qu'une liste, on prend donc la première
0 GET
// Stockage sous la forme d'une variable
'revenueGTS' STORE
// Affichage de la série
$revenueGTS
Si vous allez dans l’onglet dataviz, vous pouvez constater que vos données issues de la sandbox mais qui ont transité via votre instance sont bien disponibles.
Si nous commençons par une seule série :
// tokens de l'instance
'<instanceReadToken>' 'instanceReadToken' STORE
'<instanceWriteToken>' 'instanceWriteToken' STORE
// tokens pour la sandbox
'<sandboxReadToken>' 'sandboxReadToken' STORE
'<sandboxWriteToken>' 'sandoxWriteToken' STORE
// Url de la sandbox
'https://sandbox.senx.io/api/v0/exec' 'url' STORE
// Template de code warpscript
<'
{
'token' '{{ remoteReadToken }}'
'class' '{{ remoteClass }}'
'labels' {}
'start' '2016-12-01T00:00:00Z'
'end' '2022-01-31T00:00:00Z'
} FETCH
'>
'fetchTpl' STORE
// Substitution des variables
$fetchTpl
{ 'remoteReadToken' $sandboxReadToken 'remoteClass' 'revenue' } TEMPLATE
// Exécution de la requête
$url REXECZ
// La liste de GTS issue de FETCH ne contient qu'une liste, on prend donc la première
0 GET
// Il faut renommer "localement" la série avant de pouvoir la stocker dans l'instance
// Peut éviter de mauvaises manipulations que l'on pourrait regretter :-)
"revenue" RENAME
// Persistance des données
$instanceWriteToken UPDATE
Il y a quelques occurences de “revenue” en dur dans le code, il va falloir améliorer cela.
Et maintenant, traitons nos 9 series d’un coup
// tokens de l'instance
'<instanceReadToken>' 'instanceReadToken' STORE
'<instanceWriteToken>' 'instanceWriteToken' STORE
// tokens pour la sandbox
'<sandboxReadToken>' 'sandboxReadToken' STORE
'<sandboxWriteToken>' 'sandoxWriteToken' STORE
// Url de la sandbox
'https://sandbox.senx.io/api/v0/exec' 'url' STORE
// Template de code warpscript
<'
{
'token' '{{ remoteReadToken }}'
'class' '{{ remoteClass }}'
'labels' {}
'start' '2016-12-01T00:00:00Z'
'end' '2022-01-31T00:00:00Z'
} FETCH
'>
'fetchTpl' STORE
// Création d'une liste avec nos 9 séries
// C'est cette liste que nous allons passer ensuite dans une MACRO.
// Cette MACRO va être exécutée sur chaque élément de la liste via l'utilisation de la fonction LMAP
// https://www.warp10.io/doc/LMAP
[ 'revenue' 'exp' 'result' 'auto_revenue' 'auto_result' 'auto_expense' 'sauto_revenue' 'sauto_result' 'sauto_expense' ]
// Début de la MACRO
<%
// On récupère la valeur de la liste que l'on stocke sous la forme d'une variable
'remoteClass' STORE
// Substitution des valeurs de template
$fetchTpl
{ 'remoteReadToken' $sandboxReadToken 'remoteClass' $remoteClass } TEMPLATE
// Exécution distante de la requête
$url REXECZ
// On récupère ici une liste de GTS - plutôt que d'en extraire la GTS comme précédemment
// on va garder une liste de GTS à 1 élément, mais ce qui permet à nouveau d'utiliser la fonction LMAP
// Sur chaque entrée de la liste, une seconde macro est appliquée
// Le contenu de notre macro consiste à utliser la fonction RENAME
// '+' RENAME, cela revient à renommer la GTS en prenant le même nom que celui qui est fourni
// '+x' RENAME aurait ajouté un x au nom de la série
// Il reste l'index de la liste à traiter - soit on le supprime avec DROP
//<% DROP '+' RENAME %> LMAP
// Soit on passe F comme 3ème argument à LMAP - cela permet d'ignorer cet index
// <% '+' RENAME %> F LMAP
// Prenons la seconde forme :
<% '+' RENAME %> F LMAP
// Toutes nos séries ont été correctement renommées !
// On persiste la GTS dans la base locale
$instanceWriteToken UPDATE
%>
// Fin de la MACRO
// Application de la fonction LMAP pour que notre macro soit exécutée sur chaque élément de la liste.
// Comme on ne veut que les valeurs de la liste et pas les index, on positionne aussi F
// comme 3ème argument à LMAP
F LMAP
Et voilà, nos données ont été récupérées de la Sandbox et stockées dans notre instance locale.
Une version alternative - dans mes données, je peux tricher et ne filtrer que sur le label company
avec pour valeur cerenit
:
// tokens de l'instance
'<instanceReadToken>' 'instanceReadToken' STORE
'<instanceWriteToken>' 'instanceWriteToken' STORE
// tokens pour la sandbox
'<sandboxReadToken>' 'sandboxReadToken' STORE
'<sandboxWriteToken>' 'sandoxWriteToken' STORE
// Url de la sandbox
'https://sandbox.senx.io/api/v0/exec' 'url' STORE
// Warpscript template
<'
{
'token' '{{ remoteReadToken }}'
'class' '~.*'
'labels' { 'company' 'cerenit' }
'start' '2016-12-01T00:00:00Z'
'end' '2022-01-31T00:00:00Z'
} FETCH
'>
'fetchTpl' STORE
// Substitution dans le template
$fetchTpl
{ 'remoteReadToken' $sandboxReadToken } TEMPLATE
// Execution de la requête
$url REXECZ
// Renommage des séries
<% '+' RENAME %> F LMAP
// Presistence des données
$instanceWriteToken UPDATE
Bravo si vous m’avez suivi jusqu’ici, nous avons pu voir l’utilisation de :
REXEC
et REXECZ
LMAP
et une MACRO
TEMPLATE
Nous verrons dans un prochain épisode :
Suite de notre épopée :
L’année dernière, nous avions travaillé sur Warp 10 et mes données de comptabilité et jouer un peu avec les algo de prévision.
Les données comptables ayant été un peu ajustées entre temps et la librairie de prévision ayant aussi évolué coté SenX, les résultats ne sont plus tout à fait les mêmes. Nous allons donc reprendre tout ça.
En septembre dernier, nous avions ce code pour avoir les données jusqu’au mois de Mai 2020 et une prévision jusqu’à la fin d’année:
'<read token>' 'readToken' STORE
'<write token>' 'writeToken' STORE
// Récupération des données de dépenses / chiffre d'affaires / résult pour la période du 01/01/2017 -> 31/05/2020
// Chaque série est stockée dans une variable
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE
// On affiche les trois courbes
$revenue
$exp
$result
// On génère et affiche les prévisions - on renomme les séries pour mieux les différencier ensuite au niveau dataviz
[ $result mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"forecast_result" RENAME
[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"forecast_revenue" RENAME
[ $exp mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"forecast_expense" RENAME
Au global :
Focus 2020 avec la partie prévision à partir de juin :
Si on fait la même chose en prenant un algo incluant un effet de saisonnalité :
'<read token>' 'readToken' STORE
'<write token>' 'writeToken' STORE
// Récupération des données de dépenses / chiffre d'affaires / résult pour la période du 01/01/2017 -> 31/05/2020
// Chaque série est stockée dans une variable
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE
// On affiche les trois courbes
$revenue
$exp
$result
[ $result mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"forecast_result" RENAME
[ $revenue mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"forecast_revenue" RENAME
[ $exp mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"forecast_expense" RENAME
Au global :
Focus 2020 avec la partie prévision à partir de juin :
On a bien un petit écart de comportement sur la prévision entre les deux modèles (focus sur 2020 avec les différentes prévisions à partir de juin) :
'<read token>' 'readToken' STORE
'<write token>' 'writeToken' STORE
// Récupération des données de dépenses / chiffre d'affaires / résult pour la période du 01/01/2017 -> 31/05/2020
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE
[ $result mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"sauto_result" RENAME
[ $revenue mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"sauto_revenue" RENAME
[ $exp mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"sauto_expense" RENAME
// On génère et affiche les prévisions - on renomme les séries pour mieux les différencier ensuite au niveau dataviz
[ $result mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"auto_result" RENAME
[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"auto_revenue" RENAME
[ $exp mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"auto_expense" RENAME
Comparons maintenant les prévisions à la réalité - je vais rajouter les requêtes pour avoir la vue complète des données - pour éviter de trop surcharger le graphique, comme les séries forecast_*
reprennent les données sources et y ajoutent la prévision, je ne vais afficher que ces séries et les séries réelles :
'<read token>' 'readToken' STORE
'<write token>' 'writeToken' STORE
// Récupération des données de base qui serviront ensuite pour la prévision
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE
// Récupération des données réelles de la période 01/01/2017 > 31/12/2020
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_result' STORE
// Génération des prévisions
// Pour SAUTO, il faut définir en plus un cycle, ici 12 mois
[ $result mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"forecast_result" RENAME
[ $revenue mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"forecast_revenue" RENAME
[ $exp mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"forecast_expense" RENAME
$real_result
$real_revenue
$real_exp
Ce qui nous donne au global :
et avec le focus 2020 :
Si on fait la même chose avec SAUTO
'<read token>' 'readToken' STORE
'<write token>' 'writeToken' STORE
// Récupération des données de base qui serviront ensuite pour la prévision
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE
// Récupération des données réelles de la période 01/01/2017 > 31/12/2020
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_result' STORE
// Génération des prévisions
[ $result mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"forecast_result" RENAME
[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"forecast_revenue" RENAME
[ $exp mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"forecast_expense" RENAME
$real_result
$real_revenue
$real_exp
Au global :
Focus 2020 avec la partie prévision à partir de juin :
Essayons d’analyser tout ça (il faut regarder les fins de mois - les points sont en date du dernier jour du mois) :
La pertinence est prévisions est donc plutôt correct au global et les écarts sont expliquables.
Et au niveau annuel ? Est-ce que les prévisions de chiffres d’affaires / dépenses / résultats sont bonnes si on ne tient plus compte des petits écarts de temps ci-dessus ?
Voyons celà :
'<read token>' 'readToken' STORE
'<write token>' 'writeToken' STORE
// Récupération des différentes séries comme précédemment
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2020-06-01T00:00:00Z' ] FETCH
0 GET
'result' STORE
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'real_result' STORE
// Calcul des prévisions comme précédemment
// Petit ajout, on stocke le résultat sous la forme d'une variable pour être réutilisé ultérieurement
[ $result mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"auto_result" RENAME
'auto_result' STORE
[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"auto_revenue" RENAME
'auto_revenue' STORE
[ $exp mapper.todouble 0 0 0 ] MAP
AUTO 7 FORECAST.ADDVALUES
"auto_expense" RENAME
'auto_expense' STORE
// Aggrégation annuelle
// Utilisation de BUCKETIZE.CALENDAR et de la macro BUCKETIZE.byyear qui s'appuie dessus et qui permet de faire une aggrégation annuelle sur des données
// bucketizer.sum permet d'appliquer une somme sur les données regroupées par année
// UNBUCKETIZE.CALENDAR permet de retransformer l'indice issue de BUCKETIZE.CALENDAR en timestamp
[ $real_revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
[ $real_result bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
[ $real_exp bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
[ $auto_revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
[ $auto_result bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
[ $auto_expense bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
// Meme chose pour SAUTO
[ $result mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"sauto_result" RENAME
'sauto_result' STORE
[ $revenue mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"sauto_revenue" RENAME
'sauto_revenue' STORE
[ $exp mapper.todouble 0 0 0 ] MAP
12 SAUTO 7 FORECAST.ADDVALUES
"sauto_expense" RENAME
'sauto_expense' STORE
[ $sauto_revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
[ $sauto_result bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
[ $sauto_expense bucketizer.sum ] @senx/cal/BUCKETIZE.byyear UNBUCKETIZE.CALENDAR
Pour expliciter un peu au dessus :
On veut obtenir un résultat annuel couvant la période du 01/01 au 31/12 d’une année. Il faut donc prendre tous les points de l’année en question et en fait la somme.
Si on fait:
[ $real_revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear
On obtient :
[{"c":"revenue","l":{"company":"cerenit",".app":"52274aa9-8242-49ee-b3e8-dbc6f514999d",".uuid":"52274aa9-8242-49ee-b3e8-dbc6f514999d"},"a":{".buckettimezone":"UTC",".bucketduration":"P1Y",".bucketoffset":"0"},"la":1612528364518,"v":[[47,100850],[48,132473],[49,151714],[50,139146]]}]
Les valeus obtenues sont :
[[47,100850],[48,132473],[49,151714],[50,139146]]
Les indices 47, 48, 49, 50 sont en fait un delta par rapport au 01/01/70. En effet, 2020 = 1970 + 50
En appliquant UNBUCKETIZE.CALENDAR
, on retransforme ce 50 par ex en son équivalent sous la forme d’un timestamp : 1609459199999999
.
On peut aussi utiliser TIMESHIFT
de la façon suivante :
[ $real_revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
Pour obtenir pour la partie valeur :
[[2017,100850],[2018,132473],[2019,151714],[2020,139146]]
Pour en savoir plus sur BUCKETIZE.CALENDAR
et ses utilisations : Aggregate by calendar duration in WarpScript
Une fois qu’on reprend toutes ses données, on peut essayer de mesurer les écarts entre le réél et les prévisions des deux modèles :
AUTO | SAUTO | Réel | AUTO vs Réel | SAUTO vs Réel | |
---|---|---|---|---|---|
Chiffre d’affaires | 144.029 | 125.128 | 139.146 | -3,39% | +11,20% |
Dépénses | 117.701 | 113.765 | 129.464 | +9,99% | +13,80% |
Résultat | 14.754 | 16.893 | 9.682 | -34,38% | -42,69% |
Résultat corrigé | 26.328 | 11.363 | 9.682 | -63,23% | -14,79% |
Intéressant, la prévision de résultat n’est pas égale à la différence entre la prévision de chiffre d’affaires et la prévision des dépenses ! C’est la raison de la ligne “Résultat corrigé”.
A ce stade, il ne me semble pas possible de privilégier un modèle plus qu’un autre - même si du fait de la récurrence des vacances, on peut supposer que le modèle avec saisonnalité pourrait être plus pertinent.
Pour aller au bout de cet exerice, il ne reste plus qu’à voir ce que nos algoritmes prévoient pour 2021 :
'<read token>' 'readToken' STORE
'<write token>' 'writeToken' STORE
// Récupération des séries 2017 > 2020
[ $readToken 'expense' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'exp' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'revenue' STORE
[ $readToken 'result' { 'company' '=cerenit' } '2016-12-01T00:00:00Z' '2021-01-01T00:00:00Z' ] FETCH
0 GET
'result' STORE
// Prévision sur les 12 prochains mois
[ $result mapper.todouble 0 0 0 ] MAP
AUTO 12 FORECAST.ADDVALUES
"auto_result" RENAME
'auto_result' STORE
[ $revenue mapper.todouble 0 0 0 ] MAP
AUTO 12 FORECAST.ADDVALUES
"auto_revenue" RENAME
'auto_revenue' STORE
[ $exp mapper.todouble 0 0 0 ] MAP
AUTO 12 FORECAST.ADDVALUES
"auto_expense" RENAME
'auto_expense' STORE
// Consolidation annuelle avec AUTO
[ $auto_revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
[ $auto_result bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
[ $auto_expense bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
// Prévisions avec SAUTO
[ $result mapper.todouble 0 0 0 ] MAP
12 SAUTO 12 FORECAST.ADDVALUES
"sauto_result" RENAME
'sauto_result' STORE
[ $revenue mapper.todouble 0 0 0 ] MAP
12 SAUTO 12 FORECAST.ADDVALUES
"sauto_revenue" RENAME
'sauto_revenue' STORE
[ $exp mapper.todouble 0 0 0 ] MAP
12 SAUTO 12 FORECAST.ADDVALUES
"sauto_expense" RENAME
'sauto_expense' STORE
// Consolidation annuelle avec SAUTO
[ $sauto_revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
[ $sauto_result bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
[ $sauto_expense bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
On passe tout ça dans le shaker et on obtient :
Prévu avec AUTO | Prévu avec SAUTO | |
---|---|---|
Chiffre d’affaires | 78.230 | 129.465 |
Dépénses | 118.383 | 110.434 |
Résultat prévu | 5.730 | 4.049 |
Résultat corrigé | -40.153 | 19.031 |
Rendez-vous à la fin de l’année pour voir ce qu’il en est… et on peut espérer que la réalité sera proche du modèle avec saisonnalité !
Pour le moment, on travalle toujours dans le WarpStudio et on voudrait bien avoir des (jolis) dashboards qui font tout ça pour nous plutôt que de copier/coller du Warpscript. Ce sera le sujet de la partie 3.
libssh-dev(el)
suivant votre distribution pour pouvoir installer ansible-pylibssh
. Mes premiers essais ne notent pas une amélioration sensible des performances… à voir sur d’autres machines et dans la durée…sudoers
est présent sur le système (en général: /etc/sudoers
). Les versions 1.8.2 à 1.8.31 et 1.9.0 à 1.9.5-p1 sont impactées, il faut passer en version 1.9.5-p2.Routine habituelle de début d’année pour la clôture de ce 4ème exercice (déjà !).
Au global, une bonne année au regard des conditions - les objectifs sont remplis.
D’un point de vue comptable, cela donne :
2020 | 2019 | 2018 | 2017 | Variation n/n-1 | |
---|---|---|---|---|---|
Chiffre d’affaires | ~138 K€ | ~150 K€ | ~132 K€ | ~100 K€ | -8% |
Résultat après impôts | ~8 K€ | ~13.5 K€ | ~10 K€ | ~20 K€ | -41% |
Jours facturés | 175 | 197 | 178 | 160 | -11% |
TJM | 789€ | 761€ | 742€ | 625€ | +3.6% |
Contrairement aux autres années, les jours facturés ne prennent plus en compte des prestatations forfaitaires (comme l’infogérance, etc) pour lesquelles je faisais un équivalent jour. J’ai ajusté les valeurs de ce tableau mais je n’ai pas mis à jour les synthèses 2019, 2018 et 2017. Cela a pour conséquence d’améliorer sensiblement le TJM.
L’épisode COVID n’a pas eu d’impact direct sur mon activité et je fais un chiffre d’affaire conforme à ce que j’avais prévu en début d’année. Clairement, je mesure ma chance d’avoir passé cette année sans encombres professionnels. J’avais dit que je passerai à 4/5 sur l’année. Dès lors je ne pouvais envisager de factuer plus de 80% des jours ouvrés et et je parviens à en factuer 77% (toujours hors prestatations forfaitaires). En faisait un TJM de 700€ et 80% des jours ouvrés, cela me donnait un chiffre d’affaires à atteindre de 128 K€. J’atteins à peu près cet objectif avec les jours facturés et je le dépasse grâce aux prestations forfaitaires. Ces prestations forfaitaires ayant sensiblement augmenté en 2020 (passage de ~10K€ à ~13K€) et même si l’une d’entre elles a généré un investissement matériel important et qui sera compensé sur les prochaines années. Cela explique principalement la chute du résultat (si on prend 2018 comme année de comparaison, pour une chiffre d’affaire et un volume de jours facturés similaire, le résultat est 20% inférieur).
Comme chaque année, j’en profite pour remercier Fabrice pour son accompagnement en tant qu’expert-comptable. Je le dis et le répête, mais avoir confiance dans son expert comptable et pouvoir compter sur lui pour apporter de bons conseils aux bons moments et être serein sur la gestion de l’entreprise, c’est indispensable - surtout en cette période. Même si je n’en ai pas bénéficié directement, les informations transmises pendant cette période sur les aides et autres mécanismes mis en place ont été très utiles.
D’un point de vue activité, c’est une bonne année en termes de contenus de missions :
Pour le reste, j’ai le plaisir de :
Petite déception toutefois sur la partie développement, où je n’ai pas pu me mettre sérieusement à Go ou Rust.
Enfin, je m’étais posé la question du rôle social d’une entreprise dans notre société en temps de COVID. Ma contribution a certes été modeste dans la limite de ce qui était autorisé par la loi d’une part et ne sachant pas trop comment se finirait l’année d’autre part. Je pense que je vais continuer dans cette voie et voir quel(s) projet(s) je pourrai soutenir en 2021. Content d’avoir contribué au projet Makair et de voir comment il évolue en tous cas.
L’année commence bien avec la suite de la mission Warp 10/InfluxDB dans le monde nautique mentionnée précédemment. A celà s’ajoute une autre mission de conseil autour des usages de séries temporelles pour un autre acteur de l’énergie. J’ai du décliner un troisième appel d’offre sur un sujet similaire du fait de mes engagements actuels, mais j’espère qu’il y aura d’autres projets similaires.
Ayant aussi découvert le monde de l’impression 3D durant le premier confinement et plus récemment à jouer avec des cartes micro:bit (et peut être bientôt des ESP32), j’irais bien voir du coté de l’IoT et donner une dimension “plus industrielle” à mes usages de séries temporelles. Sortir des usages de monitoring serveur pour les séries temporelles et aller vers des usages plus industriels ou métiers est clairement intéressant. Osons le terme: direction l’industrie 4.0 !
Pour rebondir sur cette dimension usage, j’ambitionne pour le Paris Time Series Meetup d’avoir un focus usage plus important et avoir des retours d’expérience (et moins de présentation produit par des éditeurs).
Sur BigData Hebo, nous venons de lancer les brèves afin de mettre en avant les contributions des membres de la communauté. A suivre !
Pour le développement en Go et Rust, le premier devrait voir le jour dans l’année de façon assez certaine, c’est plus incertain pour le second.
Et enfin, pour le projet commencé en septembre et dont je ne peux pas encore parler, j’espère pouvoir lever le voile prochainement !
Si certains sujets vous interpellent ou si vous avez des contacts à me suggérer, n’hésitez pas à me contacter.
Un client m’a dit récemment : “Nicolas, tu es pénible - jusqu’à présent, je pensais que la solution X était bien, mais quand je vois les mises à jour à faire ici et là, ça devient pénible” et un de ces développeurs de me dire “Mais pourquoi mettre à jour, ça marche !”. Effectivement, ça marche jusqu’au jour où cela ne marche plus.
Souvent, on dépeint les développeurs comme voulant toujours des nouveautés et les ops comme voulant la stabilité et idéalement ne rien changer. Sauf que ne rien changer, ce n’est pas apporter de la stabilité mais juste de l’immobilisme. Darwin sait comment cela se finit, à la fin, le serveur ou l’application, ils meurent. Par ricochet, le projet et l’équipe projet peuvent aussi y passer.
Par ailleurs, nous sommes dans un monde qui évolue. Des nouveautés sont apportées, ainsi que des améliorations de performance mais aussi ne les oublions pas des failles. Dès lors, lorsque l’on fournit un service quelconque, la stabilité prend un autre sens à mon avis : il ne s’agit plus de figer le service dans un état donné mais plutôt de fournir un environnement qui permet au service de s’exécuter avec la même stabilité. Votre service vit dans un écosystème et celui-ci évolue. Dès lors, la stabilité de votre service implique d’être toujours en phase avec votre écosystème. Si votre service s’interface avec le fournisseur Y, vous devez toujours être en mesure de vous interfacer avec plutôt que de dire à votre client : “Le fournisseur Y a changé son API ou je ne sais quoi, vous ne pouvez plus utiliser le service”. Dans ce cas, ce n’est pas de la stabilité que vous apporter à votre client mais une régression. D’ailleurs, la littérature informatique a un terme pour cela “Le Maintien en Conditions Opérationnelles (MCO)” (à ne pas confondre avec “Le Maintien à bout de bras” ou “Le Maintien à flot”).
Et sinon, les projets d’upgrade tous les X ans qui prennent Y mois pendant lesquels on n’apporte aucune valeur au client le temps de faire cette fichue mise à jour, c’est d’un pénible et d’un frustrant. Et faut-il encore que ce budget d’upgrade - qui ne cesse de grossir par nombre de fois où il est reporté - soit un jour accordé.
Donc tout comme il est plus facile de ranger sa chambre chaque jour un petit peu plutôt qu’une fois par mois ou trimestre, la stabilité de votre plateforme, vous l’assurez en mettant à jour régulièrement vos composants. Il y a certes un petit effort régulier à faire. Certains pourront trouver ça pénible - mais vaut mieux ça que le “plus jamais ça” après un chantier d’un an de mise à jour. Les premiers rangements, vous allez les sentir passer car ils sont encore un peu gros. Mais au fur et à mesure, ils vont diminuer et être tout à fait acceptables. En faisant aussi des petites mises à jour incrémentales et pour les bugs que vous allez découvrir, il est aussi plus facile de voir ce qui a pu créer un bug dans le cadre de la mise à jour du fait de sa taille. Dans les grosses mises à jour, il y a tellement de coupables possibles que c’est une véritable partie de Cluedo que vous allez devoir mener pendant des semaines.
De plus, avoir des composants à jour vous permet d’être supporté par les éditeurs ou les communautés d’utilisateurs. Vous éviterez la réponse “Mettez à jour - passer à la version X pour être supporté - Je ferme le ticket”.
Donc oui, je vais fortement encourager mes clients à maintenir leur système à jour car je veux leur apporter la stabilité dont ils ont besoin. Au delà du fait aussi que je ne veux pas me retrouver dans une situation dantesque en cas de pépin majeur, c’est tout autant ma tranquillité que la leur dont je m’assure de façon régulière. Cerise sur le gâteau, ils bénéficient aussi des avancées des composants et peuvent à leur tour fournir un meilleur service à leurs clients et contribuer ainsi à la pérennité de leur entreprise.