Mailchimp

Les alternatives à Mailchimp – Mai 2019

Maintenant que Mailchimp a annoncé la fin du lien avec Shopify, ainsi que son pivot stratégique vers une plate-forme complète de CRM, plusieurs PME se demandent s’il est pertinent de trouver un nouveau logiciel de newsletter, après avoir passé tout ce temps à apprendre comment utiliser Mailchimp. Évidemment, ces derniers comptent sur l’inertie de leurs utilisateurs actuels. Toutefois, malgré mon amour profond pour Mailchimp après l’avoir utilisé des années, il est pertinent de réévaluer s’il est toujours le bon outil pour nous, considérant les changements fondamentaux de la plate-forme.

Je commencerai d’abord par expliquer en détails les changements sur la plateforme Mailchimp. Puis, je proposerai différentes options pour vous aider à prendre la meilleure décision pour votre situation. Puis, je terminerai par mon choix chouchou. Afin de faire des comparaisons de prix, je prendrai l’exemple d’une entreprise qui possède une liste de 10 000 abonnés, et qui envoie un courriel par semaine à sa liste. Les conversions $USDà$CAD sont en date du 27 mai 2019.

Les changements à la plateforme Mailchimp

Est-ce la réaction des gens chez Mailchimp?

Le premier changement a été la fin de la connexion native entre Mailchimp et Shopify afin de synchroniser les informations d’achats des clients pour analyser la performance, d’envoyer des courriels aux clients qui abandonnent leur panier, d’envoyer des notifications de suivi de commande par courriel, et d’avoir accès aux produits de notre boutique dans l’éditeur de courriels. La raison de cette séparation provient du refus de Mailchimp à la demande de Shopify de partager les informations de sa plateforme vers la leur. Difficile de dire qui est fautif dans cette histoire : je vous laisse juger par vous-même avec la déclaration de Mailchimp et la déclaration de Shopify. Malgré cette guéguerre, il y a heureusement une solution gratuite à ce problème avec ShopSync, qui permet de conserver l’intégration. Problème réglé !

Le deuxième changement, plus majeur, est le pivot de Mailchimp d’une plateforme d’envoi de courriels à une plateforme de CRM complète. La première étape de ce pivot est la restructuration de leurs échelles de prix : un contact est maintenant compté, qu’il soit abonné ou non à l’envoi de courriels. Cela implique qu’il faut additionner votre liste de clients non abonnés, à votre liste de courriels abonnés, à votre liste de courriels désinscrits. Une liste de 10 000 abonnés peut ainsi gonfler de façon importante (j’en ai vu quadrupler !), surtout si vous avez beaucoup d’activité d’acquisition de courriels, ce qui implique un plus gros churn qu’une acquisition organique. La logique derrière ce changement est qu’il est possible de communiquer avec les membres d’une audience par d’autres moyens via Mailchimp, comme les audiences de retargeting Google et Facebook, par exemple. Toutefois, il s’agissait de fonctionnalités qui existaient déjà auparavant. Il est donc un peu étrange, à mon avis, de changer complètement sa structure de prix sans créer de nouvelles fonctionnalités. 

Heureusement, pour les clients existants, la structure de prix reste la même selon un système de « Legacy Pricing ». La nouvelle structure de prix, beaucoup plus complexe qu’avant, s’applique à tous les nouveaux comptes et à tous les comptes inactifs depuis plus de 90 jours. En plus du gonflement dû à la nouvelle manière de compter un contact, le nombre de courriels envoyés par mois n’est plus illimitée non plus – il est maintenant ajusté en fonction du plan choisi. La limite est de 10 courriels par mois par contact pour le plan « Essentials », 12 courriels par mois par contact pour le plan « Standard » et 15 courriels par mois par contact pour le plan « Premium ». Quelqu’un qui roule beaucoup d’automatisations pourrait ainsi avoir à payer encore plus que le montant mensuel prévu, car le dépassement de la limite implique des frais supplémentaires. Les fonctionnalités sont aussi maintenant restreintes en fonction du niveau de service choisi : il n’est même plus possible de créer un template personnalisé ou des automatisations avec le plan « Essentials ».

À mon avis, considérant tous ces changements, Mailchimp n’est plus une plateforme attrayante pour un nouveau compte. Heureusement, le « Legacy Pricing » fait en sorte que les clients existants n’ont pas besoin de fuir au plus vite, mais qui sait si ce plan sera changé ou retiré dans le futur ? Je vous présente donc maintenant les différentes options disponibles, en fonction de différentes situations.

Option 1 : Statu quo en ajoutant ShopSync pour Mailchimp

ShopSync et Shopify

Quelqu’un ayant investi beaucoup de temps dans son workflow Mailchimp, qui n’a pas les resources humaines ou monétaires pour changer de plateforme, et qui est satisfait des fonctionnalités existantes, pourrait décider de rester sur Mailchimp. L’intégration avec ShopSync fonctionne bien, et le prix reste le même. Parfois, la réalité est qu’on a bien d’autres chats à fouetter : don’t fix what’s not broken. Toutefois, je conseillerais à une entreprise qui fait ce choix de rester à l’affût des « vraies » solutions de CRM comme Hubspot, et de planifier une intégration complète de ses activités de marketing dans ce genre de plateforme dans le futur, pour améliorer ses capacités et son efficacité.

Avantages

  • Nécessite peu d’efforts (15 minutes de réintégration)
  • Pas besoin d’apprendre un nouveau logiciel
  • La configuration existante est conservée

Inconvénients

  • Rester à la merci d’un futur changement aux tarifs
  • Pas d’amélioration des capacités de marketing
  • Pas de diminution des coûts

Prix

Pour une liste de 10 000 abonnés et un envoi par semaine

  • Legacy Pricing : 101$CAD/mois
  • Standard Plan : 133$CAD/mois

Option 2 : Diminuer les coûts avec Sendy et BeeFree

Interface purement utilitaire! Le composeur visuel est très peu sophistiqué, mais BeeFree est à la rescousse.

Sendy et son complément BeeFree sont les outils parfaits pour une entreprise qui désire avoir des coûts minuscules, qui a la capacité technique d’installer une application auto-hébergée, et qui a seulement besoin d’une application d’envoi de courriels plutôt qu’un CRM complet. À la place d’un modèle SaaS (Software-as-a-service), Sendy vends une licence d’utilisation pour une installation sur un serveur au coût unique de 59$USD, et agit ensuite comme back-end vers le service Amazon SES (Simple Email Service) qui a une structure tarifaire par courriel envoyé : 1$USD par 10 000 courriels envoyés. La faiblesse de Sendy est que son éditeur visuel n’est pas très performant, ce qui est réglé avec BeeFree, la meilleure plateforme de création de courriels sur le marché. En plus, il existe une intégration gratuite afin de lier les deux logiciels. Enfin, il faut lier Sendy et Shopify, ce qu’il est possible de faire avec Zapier (payant si gros volume) ou avec un script gratuit qu’on héberge soit-même – assez simple à faire sur Google App Engine par exemple.

Avantages

  • Prix très, très bas
  • Possibilité de personnaliser le back-end selon ses besoins
  • Prix très bas (est-ce que j’ai dis que c’était pas cher ?)

Inconvénients

  • Interface un peu vieillotte
  • Pas de A/B testing
  • Automatisation limitée (Courriels de bienvenue seulement)
  • Il faut gérer notre hébergement pour le back-end

Prix

Pour une liste de 10 000 abonnés et un envoi par semaine

  • Frais unique de 79$CAD
  • 6$CAD/mois (No joke!!!)

Option 3 : Augmenter la sophistication de nos campagnes courriel avec Omnisend

L’interface d’Omnisend pour choisir l’objectif de notre automatisation

Évidemment, l’autre solution est de migrer vers une nouvelle plateforme hébergée dans le nuage. Il y en a des dizaines, en voici une liste très incomplète :

Toutefois, mon choix personnel pour un marchand qui serait sur Shopify et qui cherche à migrer de Mailchimp vers un autre service serait Omnisend. C’est clairement la plateforme qui s’intègre le plus facilement avec Shopify : on aime les one-click install qui importe le opt-in de courriel et de SMS! Dans sa version « Lite », à un prix similaire à Mailchimp, elle offre plus de fonctionnalités que Mailchimp comme l’envoi de SMS et un constructeur d’automatisations visuel. Elle offre aussi des plans plus avancés qui permettent de faire du marketing par SMS, Messenger, WhatsApp ou push notifications, et de créer des audiences personalisées Facebook et Google Ads. En plus, plusieurs template d’automatisations sont déjà batis en fonction de l’objectif qu’on désire atteindre, par exemple, créer une série de courriels de bienvenue, une série de courriels de paniers abandonnés, faire du cross-selling, ou réactiver des abonnés. Encore mieux, elle offre un service de migration gratuit pour les marchands et un rabais de 50% pendant 3 mois et 20% pendant un an. Très agressif comme offre ! Sinon, si vous voulez le faire vous-même, c’est très simple de mettre votre clé API Mailchimp et d’importer vos listes. Maintenant, si vous décidez de prendre cette option, vous devriez quand même jeter un œil aux autres logiciels avant de faire votre choix, mais le miens s’est arrêté sur Omnisend. J’ai été particulièrement impressionné par son deliverability : j’utilise Zoho qui est très très agressif dans le classement automatique des infolettres dans des dossiers, et les courriels provenant d’Omnisend atterrissent dans mon Inbox !

Avantages

  • Sophistiqué mais simple d’utilisation
  • Intégration parfaite avec Shopify, autant au niveau des clients que des produits
  • Permet d’ajouter des fonctionnalités en fonction de nos besoins
  • Les triggers dans les automatisations peuvent être en fonction du comportement des contacts sur notre site, par pages ou produits vus, et même par custom event !

Inconvénients

  • Les fonctionnalités supplémentaires comme les push notifications, l’intégration à Messenger et les audiences Facebook et Google Ads sont à frais fixe, ce qui les rends un peu chères pour une petite liste.
  • Pas de workflow conditionnel (Si c’est un must, regardez du côté de Klaviyo ou ActiveCampaign)

Prix

Pour une liste de 10 000 abonnés, un envoi par semaine, un paiement annuel et le forfait « Lite »

  • 55$CAD/mois les trois premiers mois
  • 88$CAD/mois les neuf mois suivants
  • 110$CAD/mois par la suite

Voilà, j’espère que ce petit tour d’horizon vous sera utile ! Et vous, quelle est votre solution d’automatisation de marketing par courriel ?

TL :DR

  Mailchimp Sendy + BeeFree Omnisend
Sophistication 6/10 4/10 9/10
Facilité d’utilisation 8/10 5/10 10/10
Prix (10k user) 101-133$CAD 6$/mois (!!!) 110$
Intégrations 200 et + via API Via API seulement 30 et + via API
Qualité de l’éditeur visuel 6/10 10/10 (BeeFree) 0/10 (Sendy) 10/10
Deliverability 9/10 9/10 10/10
Automatisations 7/10 4/10 9/10
Intégration native avec Shopify Oui avec ShopSync (Contacts et product views) Oui (Contacts seulement) Oui (Contacts et comportement des visiteurs)

Exemple de tableau de bord automatisé

Jean-Philippe Allard

6 avril 2019

Bienvenue sur cet article avec les détails techniques de ma conférence du Web à Québec. Nous allons coder ensemble afin d’éviter à Bob de passer des heures à chaque mois à faire les mêmes opérations dans Excel, afin de le libérer à faire des activités plus productives.

Contexte de l’exemple de code

Cet exemple est basé sur les données réelles du Google Merchandise Store Demo Account de 2017 ainsi que d’un fichier lourdement modifié provenant de Kaggle. Toutes les manipulations pour créer ces fichiers sont détaillées dans mon autre article sur la préparation des données dans R. Malheureusement, confidentialité oblige, c’était la seule manière de créer un exemple accessible au public, car il y a très peu de données transactionnelles réelles disponibles au public.

Rappel du mandat

Bob est un analyste d’affaire très travaillant. Outre ses activités régulières, il passe une journée par mois à recueillir des données, les nettoyer, les analyser et les mettre en page, afin de faire une présentation de 15 minutes aux dirigeants de l’entreprise pour laquelle il travaille, sur la performance de leur boutique en ligne et de leur boutique physique.

Cela fait deux ans qu’il présente les mêmes données de la même façon, ce qui implique qu’il a passé 192 heures (24 jours de 8h) à refaire les mêmes tâches répétitives afin de présenter des données durant 6 heures (24 présentations de 15 minutes). Bob pense qu’il doit y avoir un meilleur moyen d’utiliser son temps. Il a raison!

Solution

Bob décide qu’il va apprendre à utiliser le langage de programmation R sur Datacamp. Il planifie apprendre ce langage 1h par jour en se réveillant 1h plus tôt chaque matin. Il se dit qu’en un an au maximum, avec 250h de pratique (1h/jour, 5 jours par semaine, 50 semaines), il sera capable d’utiliser ce langage pour libérer son temps. Et il a raison.

Voici ce qu’il peut faire après 250h de pratique. Nous sommes le 1ier janvier 2018 et Bob décide d’utiliser le temps qu’il mettait auparavant à produire son rapport à la main, afin de créer des visualisations selon les meilleurs principes de la communication des données.

Importation et nettoyage des données

#Bob commence par charger les librairies qu'il a besoin. 

#Librairie readr pour charger ses fichiers rapidement et facilement.
library(readr)

#Librairie tidyverse, une librairie de librairies, permettant de mieux travailler avec le tidy data. 
library(tidyverse)

#Librairie lubridate qui permet de facilement travailler avec les dates.
library(lubridate)

#Librairie ggthemes, qui offre des préconfigurations de style de tableaux
library(ggthemes)

#Librairie permettant de contrôler l'affichage des chiffres sur une visualisation
library(scales)

#Librairie pour gérer les fontes
library(extrafont)

#Bob va aussi déterminer la localisation des dates, pour affichage en français. 
Sys.setlocale("LC_TIME", "French")

Voici les cours Datacamp pour ces différentes librairies.

readr

tidyverse

lubridate

Maintenant, Bob lit ses fichiers et les transforme en objet dans R. Dans un contexte de production, ceux-ci seraient chargés via API ou par un connecteur de données. Mais pour garder cet exemple simple, Bob lit un fichier local sur son disque dur.

Si vous voulez avoir les fichiers de cet exemple, ils sont disponibles sur Kaggle.

#Lecture des fichiers
Online <- read_csv("Online.csv", col_types = cols(Date = col_date(format = "%Y%m%d")))
Retail <- read_csv("Retail.csv", col_types = cols(InvoiceDate = col_date(format = "%Y-%m-%d")))

#Bob vérifie la structure des fichiers afin de mieux orienter ses prochaines manipulations
glimpse(Online)
## Observations: 54,144
## Variables: 10
## $ `Transaction ID`                         <dbl> 48497, 48496, 48495, ...
## $ Date                                     <date> 2017-12-31, 2017-12-...
## $ `Product SKU`                            <chr> "GGOENEBQ079099", "GG...
## $ Product                                  <chr> "Nest® Protect Smoke ...
## $ `Product Category (Enhanced E-commerce)` <chr> "Nest-USA", "Nest-USA...
## $ Quantity                                 <dbl> 4, 5, 1, 1, 1, 3, 1, ...
## $ `Avg. Price`                             <dbl> 80.52, 80.52, 151.88,...
## $ Revenue                                  <dbl> 316.00, 395.00, 149.0...
## $ Tax                                      <dbl> 34.44, 33.14, 12.06, ...
## $ Delivery                                 <dbl> 19.99, 6.50, 6.50, 6....
glimpse(Retail)
## Observations: 181,247
## Variables: 4
## $ InvoiceNo   <dbl> 536598, 536598, 536598, 536599, 536599, 536600, 53...
## $ InvoiceDate <date> 2017-01-01, 2017-01-01, 2017-01-01, 2017-01-01, 2...
## $ StockCode   <dbl> 21421, 21422, 22178, 20749, 21056, 21730, 21871, 2...
## $ Quantity    <dbl> 1, 2, 26, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 60, 2, 1, ...

Bob remarque que son fichier Retail, montrant les ventes des magasins physiques, n’est pas très complet par rapport à son fichier Online. Il aimerait rajouter l’information sur les produits, leur prix de vente et leur catégorie à son fichier Retail. Mais les codes de produit sont aussi différents! Il va donc falloir créer une “clé” entre les “Product SKU” du fichier online et le “StockCode” du fichier retail.

Bob cherche donc à trouver une liste de StockCode – Nom de produit, mais malheureusement, son entreprise n’a jamais cru bon de créer cette liste (à se demander comment elle est toujours en affaires!). Comme Bob doit travailler avec ces contraintes, cela implique qu’il doit créer un fichier de “clé” faisant le lien entre le SKU et le StockCode. Bob demande donc à son stagiaire, Gérard, de créer la concordance entre les 1179 Product SKU et les 1179 StockCodes. Gérard, après avoir sombré dans l’alcoolisme, lui livre le fichier suivant:

KEY_SKU <- read_csv("KEY_SKU.csv")

glimpse(KEY_SKU)
## Observations: 1,178
## Variables: 2
## $ `Product SKU` <chr> "GGOENEBQ079099", "GGOENEBQ079199", "GGOENEBQ084...
## $ StockCode     <dbl> 21421, 21422, 22178, 20749, 21056, 21730, 21871,...

Il s’agit de la concordance entre chaque StockCode et son Product SKU. Avec cette concordance, il devient possible de prendre les informations du fichier Online et de les appliquer au fichier Retail.

Il faut d’abord créer un fichier avec la liste des informations voulues par SKU, c’est-à-dire le nom du produit, sa catégorie, et son prix.

#On commence par créer un vecteur logique des SKUS dupliqués, qu'on rajoute comme une nouvelle colonne dans notre fichier Online. 
Online$dups <- duplicated(Online$`Product SKU`)

#On conserve uniquement les lignes non dupliquées
Product_list <- Online %>% filter(dups == FALSE)

#On conserve seulement l'information pertinente
Product_list <- Product_list[c("Product SKU", "Product", "Product Category (Enhanced E-commerce)", "Avg. Price")]

#On rajoute le stock code à notre liste de produits
Product_list <- left_join(KEY_SKU, Product_list, by = "Product SKU")

#Notre liste de produits avec toutes les informations est prête! 
glimpse(Product_list)
## Observations: 1,178
## Variables: 5
## $ `Product SKU`                            <chr> "GGOENEBQ079099", "GG...
## $ StockCode                                <dbl> 21421, 21422, 22178, ...
## $ Product                                  <chr> "Nest® Protect Smoke ...
## $ `Product Category (Enhanced E-commerce)` <chr> "Nest-USA", "Nest-USA...
## $ `Avg. Price`                             <dbl> 80.52, 80.52, 151.88,...
#On peut enlever la colonne dups de notre fichier Online
Online$dups <- NULL

#Et ensuite, rajouter toute l'information à notre fichier Retail
Retail <- merge(Retail, Product_list, by = "StockCode", all.x = TRUE)
Retail <- arrange(Retail, InvoiceDate, InvoiceNo)

#Nous pouvons calculer le reste de l'information manquante: les revenus et les taxes. Nous calculons par ligne d'abord:
perLine <- Retail %>% mutate(RevenuePerLine = Quantity * `Avg. Price`, TaxPerLine = RevenuePerLine * 0.014975)

#Puis nous groupons les lignes par numéro de commande: 
perInvoiceNo <- perLine %>% group_by(InvoiceNo) %>% summarise(Revenue = sum(RevenuePerLine), Tax = sum(TaxPerLine))

#Puis nous joignons ces informations à nos données
Retail <- merge(Retail, perInvoiceNo, all.x = TRUE)

#Arrondissons le montant des taxes
Retail$Tax <- round(Retail$Tax, digits = 2)

#Et ajoutons des frais de livraison de 0
Retail <- Retail %>% mutate(Delivery = 0)

#On peut même enlever le StockCode et travailler uniquement avec le Product SKU
Retail$StockCode <- NULL

#Notre fichier Retail possède maintenant toutes les informations disponibles dans notre fichier Online! 
glimpse(Online)
## Observations: 54,144
## Variables: 10
## $ `Transaction ID`                         <dbl> 48497, 48496, 48495, ...
## $ Date                                     <date> 2017-12-31, 2017-12-...
## $ `Product SKU`                            <chr> "GGOENEBQ079099", "GG...
## $ Product                                  <chr> "Nest® Protect Smoke ...
## $ `Product Category (Enhanced E-commerce)` <chr> "Nest-USA", "Nest-USA...
## $ Quantity                                 <dbl> 4, 5, 1, 1, 1, 3, 1, ...
## $ `Avg. Price`                             <dbl> 80.52, 80.52, 151.88,...
## $ Revenue                                  <dbl> 316.00, 395.00, 149.0...
## $ Tax                                      <dbl> 34.44, 33.14, 12.06, ...
## $ Delivery                                 <dbl> 19.99, 6.50, 6.50, 6....
glimpse(Retail)
## Observations: 181,247
## Variables: 10
## $ InvoiceNo                                <dbl> 536598, 536598, 53659...
## $ InvoiceDate                              <date> 2017-01-01, 2017-01-...
## $ Quantity                                 <dbl> 1, 2, 26, 2, 2, 2, 1,...
## $ `Product SKU`                            <chr> "GGOENEBQ079099", "GG...
## $ Product                                  <chr> "Nest® Protect Smoke ...
## $ `Product Category (Enhanced E-commerce)` <chr> "Nest-USA", "Nest-USA...
## $ `Avg. Price`                             <dbl> 80.52, 80.52, 151.88,...
## $ Revenue                                  <dbl> 4190.44, 4190.44, 419...
## $ Tax                                      <dbl> 62.75, 62.75, 62.75, ...
## $ Delivery                                 <dbl> 0, 0, 0, 0, 0, 0, 0, ...

Dernière étape avant de faire nos analyses et nos visualisations: joindre les deux tableaux ensemble.

#Il faut d'abord renommer les colonnes pour qu'elles possèdent le même nom d'un côté comme de l'autre. Ça nous simplifiera la vie. 
Retail <- Retail %>% rename("Transaction ID" = "InvoiceNo", "Date" = "InvoiceDate")

#On rajoute un colonne catégorielle à chaque tableau
Retail$Channel <- "Retail"
Online$Channel <- "Online"

#On joint
Full <- bind_rows(Online, Retail)

#Notre tableau complet est prêt! 

Analyse des données

Maintenant que toutes nos données sont regroupées, il est très facile de faire des statistiques descriptives en utilisant les fonctions de base de R ainsi que les fonctions group_by(), filter() et summarize() de dplyr.

#Exemple: On peut calculer le revenu total annuel par Canal de vente (Online ou Retail)
Full_unique <- Full %>% 
  filter(duplicated(Full$`Transaction ID`) == FALSE)

Revenue_by_channel <- Full_unique %>% 
  group_by(Channel) %>% 
  summarize(sum(Revenue))

Revenue_by_channel
## # A tibble: 2 x 2
##   Channel `sum(Revenue)`
##   <chr>            <dbl>
## 1 Online        4743705.
## 2 Retail       19019265.

Rappelons les données que nous voulons obtenir et présenter pour notre rapport:

  • Revenus par catégorie par mois
  • Croissance par catégorie MoM
  • Estimation des ventes pour le mois suivant
  • Scénarios d’estimation des ventes en fonction du budget marketing

Nous allons produire et visualiser chacune de ces données. Débutons par créer un sommaire des revenus par canal, par catégorie, et par mois.

Revenus par mois par catégorie

#Créer la colonne "Mois-Année" qui va agir à titre de facteur de groupement
Full_unique$Mois_An <- format(as.Date(Full_unique$Date), "%Y-%m")

#Changer les vecteurs catégoriels en facteurs
Full_unique$`Product Category (Enhanced E-commerce)` <- as.factor(Full_unique$`Product Category (Enhanced E-commerce)`)
Full_unique$Mois_An <- as.factor(Full_unique$Mois_An)
Full_unique$Channel <- as.factor(Full_unique$Channel)

#Créer le sommaire des ventes par canal, mois et catégorie. 
Full_summary <- Full_unique %>%
  group_by(Channel, Mois_An, `Product Category (Enhanced E-commerce)`, .drop = FALSE) %>%
  summarize(sum(Revenue))

#On remplace les ventes non catégorisées par une catégorie "Autres"
Full_summary$`Product Category (Enhanced E-commerce)` <- as.character(Full_summary$`Product Category (Enhanced E-commerce)`)

Full_summary$`Product Category (Enhanced E-commerce)` <- Full_summary$`Product Category (Enhanced E-commerce)` %>% replace_na("Other")

Full_summary$`Product Category (Enhanced E-commerce)` <- as.factor(Full_summary$`Product Category (Enhanced E-commerce)`)

Maintenant que nous avons notre sommaire de ventes par canal, par catégorie et par mois, nous pouvons commençer à créer nos visualisations.

#Initialisation de la date. Dans un cadre réel, nous utiliserions Sys.Date(), mais ici, nous utilisons une date fixe pour notre exemple. 

sysdate <- as.Date(ymd_hms("2018-01-01 9:00:00"))

#Visualisation des ventes du dernier mois, par catégorie. 
ggplot(data = filter(Full_summary, Mois_An == format(as.Date(sysdate %m+% months(-1)), "%Y-%m")), aes(x = reorder(`Product Category (Enhanced E-commerce)`, `sum(Revenue)`), y = `sum(Revenue)`, fill = Channel)) +
  geom_col(position = "stack") +
  coord_flip() +
  theme_tufte() + 
  theme(legend.position = c(0.8, 0.15)) +
  scale_fill_manual(values = c("#2c7bb6", "#abd9e9")) +
  scale_y_continuous(labels = dollar) +
  theme(axis.title.x = element_text(family = "Arial", colour="#484848", size=12),
        axis.text.x  = element_text(family = "Arial", colour="#484848", size=12),
        axis.title.y = element_blank(),
        axis.text.y  = element_text(family = "Arial", colour="#484848", size=12), 
        legend.title = element_text(family = "Arial", colour="#484848", size=10), 
        legend.text  = element_text(family = "Arial", colour="#484848", size=9),
        plot.title   = element_text(family = "Arial", colour="#484848", size=16),
        plot.subtitle = element_text(family = "Arial", colour="#484848", size=10)) +
  labs(y = "Revenus (En $)",
       title = "Ventes par catégorie, par canal",
       subtitle = (paste("Mois de", format(as.Date(sysdate %m+% months(-1)), "%B %Y"), sep = " ")),
       fill = "Canal")

Maintenant, nous désirons obtenir la croissance des ventes par catégorie pour le dernier mois complet, comparé au mois précédent.

#Calcul de la croissance des ventes du dernier mois (Décembre) par rapport au mois précédent (Novembre)
Summary_current <- Full_summary %>% filter(Mois_An == format(as.Date(sysdate %m+% months(-1)), "%Y-%m"))
Summary_previous <- Full_summary %>% filter(Mois_An == format(as.Date(sysdate %m+% months(-2)), "%Y-%m"))
Summary_current$Growth <- (Summary_previous$`sum(Revenue)` - Summary_current$`sum(Revenue)`) / Summary_previous$`sum(Revenue)` * 100

#
Summary_current[is.na(Summary_current)] <- NA
Summary_current$Growth[is.infinite(Summary_current$Growth)] <- NA

#Visualisation de la croissance des ventes du dernier mois, en ligne, par catégorie. 
ggplot(data = filter(Summary_current, Channel == "Online" & !is.na(Growth)), aes(x = reorder(`Product Category (Enhanced E-commerce)`, Growth), y = Growth, fill = Growth > 0)) +
  geom_col() +
  coord_flip(ylim = c(-100, 100)) +
  theme_tufte() + 
  theme(axis.title.x = element_text(family = "Arial", colour="#484848", size=12),
        axis.text.x  = element_text(family = "Arial", colour="#484848", size=12),
        axis.title.y = element_blank(),
        axis.text.y  = element_text(family = "Arial", colour="#484848", size=12),
        plot.title   = element_text(family = "Arial", colour="#484848", size=16),
        plot.subtitle = element_text(family = "Arial", colour="#484848", size=10),
        legend.position = "none"
        ) +
  labs(y = "Variation MoM (en %)",
       title = "Variation des ventes mensuelles en ligne",
       subtitle = (paste("Mois de", format(as.Date(sysdate %m+% months(-1)), "%B %Y"), "par rapport à", format(as.Date(sysdate %m+% months(-2)), "%B %Y"), sep = " "))) +
       scale_fill_manual(values=c("FALSE"= "#fdae61", "TRUE" = "#abd9e9"))
#Visualisation de la croissance des ventes du dernier mois, en magasin, par catégorie. 
ggplot(data = filter(Summary_current, Channel == "Retail" & !is.na(Growth)), aes(x = reorder(`Product Category (Enhanced E-commerce)`, Growth), y = Growth, fill = Growth > 0)) +
  geom_col()+
  coord_flip(ylim = c(-100, 100)) +
  theme_tufte() + 
  theme(axis.title.x = element_text(family = "Arial", colour="#484848", size=12),
        axis.text.x  = element_text(family = "Arial", colour="#484848", size=12),
        axis.title.y = element_blank(),
        axis.text.y  = element_text(family = "Arial", colour="#484848", size=12),
        plot.title   = element_text(family = "Arial", colour="#484848", size=16),
        plot.subtitle = element_text(family = "Arial", colour="#484848", size=10),
        legend.position = "none"
        ) +
  labs(y = "Variation MoM (en %)",
       title = "Variation des ventes mensuelles en magasin",
       subtitle = (paste("Mois de", format(as.Date(sysdate %m+% months(-1)), "%B %Y"), "par rapport à", format(as.Date(sysdate %m+% months(-2)), "%B %Y"), sep = " "))) +
       scale_fill_manual(values=c("FALSE"= "#fdae61", "TRUE" = "#abd9e9"))

Maintenant que nous connaissons nos ventes et notre croissance par catégorie, nous aimerions faires des prévisions pour le mois prochain afin d’établir un objectif de ventes.

Si vous voulez apprendre à faire des modèles de séries temporelles, voici l’excellent cours sur Datacamp: Forecasting using R par Rob J. Hyndman, le créateur de la librairie forecast.

#Chargeons la librairie forecast qui permet de faire des modèles prédictifs de série temporelle, et la librairie xts qui permet de créer des objets de type séries de dates plus facilement
library(forecast)
library(xts)

#Créons les revenus journaliers totaux, incluant les ventes en ligne et en magasin
Revenue_daily <- Full_unique %>% group_by(Date) %>% summarize(sum(Revenue))

#Et une série de dates pour construire notre objet xts
dates <- seq(as.Date("2017-01-01"), length = 365, by = "days")

#Convertissons cet objet en format xts
Revenue_daily <- xts(Revenue_daily$`sum(Revenue)`, order.by = dates)

#Créons notre modèle prédictif de manière automatique
model <- auto.arima(Revenue_daily, stepwise = FALSE)

#Faisons une prédiction pour le nombre de jours dans le mois actuel
fcast <- forecast(model, h = days_in_month(sysdate))

#On jette un oeil rapide à la prédiction et aux valeurs prédites
plot(fcast)
#Et aux prévisions moyennes
fcast$mean
## Time Series:
## Start = 366 
## End = 396 
## Frequency = 1 
##  [1] 100409.10  95617.85  82661.92  95286.51  98492.79  85303.40  91042.45
##  [8]  98923.06  88758.51  88421.40  97584.02  91933.86  87538.74  95362.89
## [15]  94152.09  88053.42  93077.02  95182.81  89395.81  91298.83  95155.73
## [22]  90978.02  90295.77  94414.55  92341.67  90062.89  93365.82  93227.23
## [29]  90410.92  92360.72  93573.34
#On crée notre tableau de données à visualiser
df <- data.frame(Bas = sum(fcast$lower[,1]), Moyen = sum(fcast$mean), `Élevé` = sum(fcast$upper[,1]))
df_long <- gather(df, key = "Estimation", value = "value")

#On ajuste l'affichage des chiffres en $
dollar <- dollar_format(accuracy = 1, big.mark = " ", prefix = "", suffix = "$")

#On crée notre visualisation
ggplot(data = df_long, aes(x = Estimation, y = value, fill = Estimation, label = dollar(value))) +
  geom_bar(stat="identity", position = "dodge") +
  geom_text(hjust = 1.1, size = 5, color = "#ffffff") +
  coord_flip() +
  theme_tufte() +
  scale_y_continuous(labels = dollar) +
  scale_x_discrete(limits = c("Élevé", "Moyen", "Bas")) +
  scale_fill_manual(values = c("#abd9e9", "#abd9e9", "#2c7bb6")) +
  labs(title = (paste("Estimation des ventes pour le mois de", format(as.Date(sysdate), "%B %Y"), sep = " ")),
       subtitle = "Calculé à l'aide d'un modèle ARIMA automatique") +
  theme(
    axis.title.x = element_blank(),
    axis.text.x  = element_text(family = "Arial", colour="#484848", size=12),
    axis.title.y = element_blank(),
    axis.text.y  = element_text(family = c("Arial", "Arial Black", "Arial"), colour="#484848", size=12), 
    plot.title   = element_text(family = "Arial", colour="#484848", size=16),
    plot.subtitle = element_text(family = "Arial", colour="#484848", size=10),
    legend.position = "none"
) 

En utilisant uniquement un modèle de série temporelles, il y aura toujours une marge d’erreur assez importante. Toutefois, il est aussi possible de créer plutôt un modèle de régression linéaire avec des erreurs ARIMA. Comme nous désirons conaitre l’effet de la publicité sur nos ventes, nous allons créer ce type de modèle avec des données sur le budget publicitaire online et offline, et ensuite faire des prédictions par scénarios.

#Commençons par importer un fichier avec les dépenses marketing par jour pour l'année 2017. 
Marketing_spend <- read_csv("Marketing_Spend.csv", 
    col_types = cols(`Offline Spend` = col_double(), 
        `Online Spend` = col_double(), X1 = col_date(format = "%Y-%m-%d")))

#On crée trois modèles en ajoutant les budgets marketing comme régresseurs
model_spend_both <- auto.arima(Revenue_daily, stepwise = FALSE, xreg = cbind(Marketing_spend$`Online Spend`, Marketing_spend$`Offline Spend`))

model_spend_online <- auto.arima(Revenue_daily, stepwise = FALSE, xreg = Marketing_spend$`Online Spend`)

model_spend_offline <- auto.arima(Revenue_daily, stepwise = FALSE, xreg = Marketing_spend$`Offline Spend`)

#On observe le "fit" des modèles
summary(model_spend_both)
## Series: Revenue_daily 
## Regression with ARIMA(3,0,0) errors 
## 
## Coefficients:
##           ar1      ar2     ar3   intercept    xreg1   xreg2
##       -0.0879  -0.0553  0.0715  -24577.857  43.1001  2.6479
## s.e.   0.0523   0.0524  0.0529    2346.622   0.9302  0.7613
## 
## sigma^2 estimated as 190771248:  log likelihood=-3994.55
## AIC=8003.11   AICc=8003.42   BIC=8030.41
## 
## Training set error measures:
##                    ME  RMSE      MAE       MPE     MAPE      MASE
## Training set 8.370002 13698 11354.57 -12.31825 34.27963 0.3066989
##                      ACF1
## Training set -0.002404769
summary(model_spend_online)
## Series: Revenue_daily 
## Regression with ARIMA(3,0,0) errors 
## 
## Coefficients:
##           ar1      ar2     ar3   intercept     xreg
##       -0.0579  -0.0164  0.1133  -18797.216  44.0179
## s.e.   0.0527   0.0524  0.0531    1899.972   0.9144
## 
## sigma^2 estimated as 195942090:  log likelihood=-3999.95
## AIC=8011.9   AICc=8012.13   BIC=8035.3
## 
## Training set error measures:
##                   ME     RMSE      MAE       MPE     MAPE      MASE
## Training set 8.96493 13901.72 11577.88 -13.89588 34.22336 0.3127307
##                      ACF1
## Training set -0.008030993
summary(model_spend_offline)
## Series: Revenue_daily 
## Regression with ARIMA(0,1,4) errors 
## 
## Coefficients:
##           ma1      ma2     ma3     ma4     xreg
##       -0.9442  -0.1772  0.0047  0.1510  14.0375
## s.e.   0.0517   0.0723  0.0749  0.0534   1.6692
## 
## sigma^2 estimated as 1.224e+09:  log likelihood=-4323.92
## AIC=8659.84   AICc=8660.07   BIC=8683.22
## 
## Training set error measures:
##                    ME     RMSE      MAE       MPE     MAPE      MASE
## Training set 356.6522 34692.86 26926.26 -69.75817 96.22966 0.7273065
##                     ACF1
## Training set 0.005368986

Le modèle incluant les deux régresseurs est supérieur (AIC plus faible). On remarque aussi que le budget de marketing offline a peu d’effet sur les ventes, avec un coefficient de seulement 2.64, comparé à un coefficient de 43.10 pour le budget de marketing online. Une autre chose intéressante est que le AIC du modèle incluant les régresseurs est beaucoup plus faible que celui n’incluant pas les régresseurs, ce qui indique que nous devrions être en mesure de faire des prédictions plus précises.

Créons maintenant nos prédictions qui incluent le budget marketing prévu.

#Il faut d'abord créer des tableaux avec des scénarios d'augmentation de budgets en fonction des budgets du mois précédent.

Budget_same <- Marketing_spend %>% 
                filter(X1 <= (ceiling_date(as.Date(sysdate %m+% months(-1)), "month") - days(1)) & X1 >= as.Date(sysdate %m+% months(-1)))

Budget_higher_online <- Marketing_spend %>% 
                          filter(X1 <= (ceiling_date(as.Date(sysdate %m+% months(-1)), "month") - days(1)) & X1 >= as.Date(sysdate %m+% months(-1))) %>%
                          mutate(`Online Spend` = `Online Spend` * 1.25)

Budget_higher_offline <- Marketing_spend %>% 
                          filter(X1 <= (ceiling_date(as.Date(sysdate %m+% months(-1)), "month") - days(1)) & X1 >= as.Date(sysdate %m+% months(-1))) %>%
                          mutate(`Offline Spend` = `Offline Spend` * 1.25)

#On peut maintenant créer trois prédictions avec nos trois scénarios
fcast_same <- forecast(model_spend_both, xreg = cbind(Budget_same$`Online Spend`, Budget_same$`Offline Spend`), h = days_in_month(sysdate))

fcast_higher_online <- forecast(model_spend_both, xreg = cbind(Budget_higher_online$`Online Spend`, Budget_higher_online$`Offline Spend`), h = days_in_month(sysdate))

fcast_higher_offline <- forecast(model_spend_both, xreg = cbind(Budget_higher_offline$`Online Spend`, Budget_higher_offline$`Offline Spend`), h = days_in_month(sysdate))

#On jette un oeil à nos prédictions. Laquelle semble la plus intéressante? 
plot(fcast_same)
plot(fcast_higher_online)
plot(fcast_higher_offline)

Maintenant que nous avons nos prédictions avec nos trois scénarios, nous allons visualiser celles-ci. En premier lieu, nous allons refaire notre estimation précédente des ventes du mois avec notre nouveau modèle plus précis.

#On crée notre tableau de données à visualiser
df_same <- data.frame(Bas = sum(fcast_same$lower[,1]), Moyen = sum(fcast_same$mean), `Élevé` = sum(fcast_same$upper[,1]))
df_long_same <- gather(df_same, key = "Estimation", value = "value")

#On crée notre visualisation
ggplot(data = df_long_same, aes(x = Estimation, y = value, fill = Estimation, label = dollar(value))) +
  geom_bar(stat="identity", position = "dodge") +
  geom_text(hjust = 1.1, size = 5, color = "#ffffff") +
  coord_flip() +
  theme_tufte() +
  scale_y_continuous(labels = dollar) +
  scale_x_discrete(limits = c("Élevé", "Moyen", "Bas")) +
  scale_fill_manual(values = c("#abd9e9", "#abd9e9", "#2c7bb6")) +
  labs(title = (paste("Estimation des ventes pour le mois de", format(as.Date(sysdate), "%B %Y"), sep = " ")),
       subtitle = "Budget de marketing identique au mois précédent", 
       caption = "Régression linéaire du budget marketing sur les ventes avec erreurs ARIMA\nNiveau de confiance 80%") +
  theme(
    axis.title.x = element_blank(),
    axis.text.x  = element_text(family = "Arial", colour="#484848", size=12),
    axis.title.y = element_blank(),
    axis.text.y  = element_text(family = c("Arial", "Arial Black", "Arial"), colour="#484848", size=12), 
    plot.title   = element_text(family = "Arial", colour="#484848", size=16),
    plot.subtitle = element_text(family = "Arial", colour="#484848", size=10),
    plot.caption = element_text(family = "Arial", colour="#484848", size=8),
    legend.position = "none"
) 

Déjà beaucoup plus précis! Maintenant, nous allons créer une visualisation qu démontre les trois scénarios dans un seul graphique, afin qu’il soit évident de la décision à prendre.

#On crée notre tableau de données à visualiser
df_budget <- data.frame(Budget = c("Identique", "25% de plus en ligne", "25% de plus hors ligne"), 
                        Moyen = c(sum(fcast_same$mean), sum(fcast_higher_online$mean), sum(fcast_higher_offline$mean)))

#On crée notre visualisation
ggplot(data = df_budget, aes(x = Budget, y = Moyen, fill = Budget, label = dollar(Moyen))) +
  geom_bar(stat="identity", position = "dodge") +
  geom_text(hjust = 1.1, size = 5, color = "#ffffff") +
  coord_flip() +
  theme_tufte() +
  scale_y_continuous(labels = dollar) +
  scale_fill_manual(values = c("#2c7bb6", "#abd9e9", "#abd9e9")) +
  scale_x_discrete(limits = c("25% de plus en ligne", "25% de plus hors ligne", "Identique")) +
  labs(title = (paste("Estimation des ventes pour le mois de", format(as.Date(sysdate), "%B %Y"), sep = " ")),
       subtitle = "En fonction d'une augmentation du budget marketing en ligne ou hors ligne", 
       caption = "Prédictions calculées à partir de la moyenne du modèle de régression\n linéaire du budget marketing sur les ventes avec erreurs ARIMA") +
  theme(
    axis.title.x = element_blank(),
    axis.text.x  = element_text(family = "Arial", colour="#484848", size=12),
    axis.title.y = element_blank(),
    axis.text.y  = element_text(family = c("Arial Black", "Arial", "Arial"), colour="#484848", size=12), 
    plot.title   = element_text(family = "Arial", colour="#484848", size=16),
    plot.subtitle = element_text(family = "Arial", colour="#484848", size=10),
    plot.caption = element_text(family = "Arial", colour="#484848", size=8),
    legend.position = "none"
) 

Démarrage de l’instance Google Compute Engine

Maintenant que nous avons tous les rapports que nous désirons envoyer chaque mois, il nous reste seulement deux étapes. La première est de créer une instance de RStudio dans Google Compute Engine, et la deuxième est de créer un script dans cette instance qui enverra notre rapport chaque mois. Commençons par créer notre instance de RStudio dans Google Compute Engine avec la librairie googleComputeEngineR.

La première étape est de créer un compte Google Compute Engine. Il faut ajouter une carte de crédit, mais l’utilisation d’un serveur f1-micro est gratuit. C’est amplement assez performant pour la tâche qu’on va lui demander une fois par mois.

Ensuite, il faut créer notre projet, puis aller chercher les détails de notre “Service Account” en format JSON: API Manager -> Credentials -> Create credentials -> Service account key -> Key type = JSON. On ajoute ce fichier au dossier de travail de notre projet. À noter que c’est ce fichier JSON qui contient les informations d’accès au compte GCE: il convient donc de bien le protéger.

Ensuite, il faut créer un fichier .Renviron dans notre dossier de travail (avec le bloc-note) qui sera chargé automatiquement lorsque la librairie sera activée. Ce fichier doit contenir les éléments suivants:

GCE_AUTH_FILE = "C:/My First Project-168c949fa5ac.json" #L'emplaçement du fichier JSON créé précédemment. 
GCE_DEFAULT_PROJECT_ID = "mineral-highway-236314" #Le project ID du "My First Projet" créé par Google lors de la création du compte, ou un autre projet si vous préférez. 
GCE_DEFAULT_ZONE = "us-east1-b" #L'emplaçement du serveur, qui devrait être celui-ci si vous vous connectez à partir de la cote est. 

Ensuite, le code pour créer la nouvelle instance GCE est excessivement simple.

library(googleComputeEngineR)

vm <- gce_vm(template = "rstudio",
             name = "autoemailer", #Nom qu'on désire donner à l'instance
             username = "waq", #Nom d'utilisateur pour RStudio
             password = "qwertyiscool", #Mot de passe pour RStudio
             predefined_type = "n1-standard-1") #Type de serveur

Le f1-micro est le seul serveur gratuit, mais j’ai plutôt utilisé un n1-standard-1 à environ 25$/mois car les librairies utilisées pour l’exemple dépassaient la capacité de mémoire du petit f1-micro.

Préparation de l’instance de machine virtuelle

Il faut d’abord installer les librairies qui ne sont pas incluses dans l’image de base. Mais avant cela, il faut installer java ainsi que rJava via le terminal de notre instance (Linux Debian 8) car ils sont une dépendance de la librairie mailR que nous utiliserons pour l’envoi de courriel.

sudo apt-get install default-jdk
sudo apt-get install r-cran-rjava
sudo R CMD javareconf

Maintenant, nous pouvons installer les librairies:

install.packages("cronR")
install.packages("ggthemes")
install.packages("extrafont")
install.packages("miniUI")
install.packages("forecast")
install.packages("xts")
install.packages("mailR")
install.packages("rJava")
install.packages("shiny")
install.packages("shinyFiles")

Et les activer:

library(rJava)
library(cronR)
library(miniUI)
library(mailR)
library(readr)
library(tidyverse)
library(lubridate)
library(ggthemes)
library(scales)
library(extrafont)
library(forecast)
library(xts)
library(shiny)
library(shinyFiles)

Ensuite, si on veut que les dates soient formattées correctement, il faut créer la locale fr_FR.UTF-8, encore une fois dans le terminal, avec l’application à cet effet:

sudo dpkg-reconfigure locales

Et créer un fichier .Renviron avec les locales qui seront chargées au start-up de R:

LANG=en_US.UTF-8
LANGUAGE=
LC_CTYPE="fr_FR.UTF-8"
LC_NUMERIC="fr_FR.UTF-8"
LC_TIME="fr_FR.UTF-8"
LC_COLLATE="fr_FR.UTF-8"
LC_MONETARY="fr_FR.UTF-8"
LC_MESSAGES="fr_FR.UTF-8"
LC_PAPER="fr_FR.UTF-8"
LC_NAME="fr_FR.UTF-8"
LC_ADDRESS="fr_FR.UTF-8"
LC_TELEPHONE="fr_FR.UTF-8"
LC_MEASUREMENT="fr_FR.UTF-8"
LC_IDENTIFICATION="fr_FR.UTF-8"
LC_ALL=fr_FR.UTF-8

Et enfin, installer et activer le service cron dans le terminal

sudo apt-get install cron
sudo cron start

Création du script d’envoi de courriel automatique

J’ai d’abord créé une version abrégée du fichier actuel en format R Markdown, contenant uniquement les graphiques ainsi qu’une courte explication de ceux-ci. J’ai téléversé ce fichier sur mon instance GCE directement via RStudio. Ensuite, il faut téléverser tous les documents que nous avons utilisés jusqu’ici. Le plus simple est de les copier via un fichier .zip qui sera automatiquement décompressé à l’arrivée.

Une fois que c’est fait, voici le script à utiliser:

#Créer le rapport en html à partir du fichier .rmd
rapport <- rmarkdown::render("Rapport_Mensuel.Rmd")

#Envoyer le courriel
mailR::send.mail(from = "[email protected]",
          to = "[email protected]",
          subject = "Rapport de performance du mois",
          body = "Rapport_Mensuel.html",
          html = TRUE,
          inline = TRUE,
          smtp = list(host.name = "smtp.gmail.com", port = 465, user.name = "[email protected]", passwd = "xxxxxxxx", ssl = TRUE),
          authenticate = TRUE,
          send = TRUE)

La toute dernière étape est de sélectionner ce script dans l’onglet Tools –> Addins –> cronR afin de sélectionner la fréquence désirée!

Voila, vous reçevrez maintenant vos rapports dans votre boite courriel, toujours mis à jour, sans lever le petit doigt!