library(httr)
library(jsonlite)
library(tidyverse)
library(rvest)
<- function(x, API_key){
weather <- paste0("https://api.darksky.net/forecast/",API_key,
url "/", x[1], ",", x[2],
"?units=ca&exclude=hourly,alerts,flags")
<- GET(url)
rep
<- fromJSON(content(rep, "text"))
table
<- with(table,
current.weather.info data.frame(Air_temp = currently$temperature,
Weather = currently$summary,
Atm_pres = currently$pressure,
Wind_str = currently$windSpeed,
Wind_dir = currently$windBearing,
Cloud_cover = currently$cloudCover,
PrecipProb = currently$precipProbability,
PrecipInt = currently$precipIntensity,
Moon = daily$data$moonPhase[1]))
return(current.weather.info)
}
ℹ️ Note: Lorsque j’ai développé cette application, j’étais débutant en développement web et en gestion des données. J’ai choisi Shiny car c’était une solution simple pour moi à l’époque.
Avec le recul, si je devais refaire ce projet aujourd’hui, j’opterais plutôt pour une application Django avec une base de données dédiée, ce qui offrirait plus de flexibilité et de robustesse.
Mon passe-temps favori, en plus de R bien sûr, est la pêche. La plupart du temps, je pêche le bar (Dicentrarchus labrax) dans les estuaires. Le bar est un prédateur qui a un large éventail de proies : crabes, lançons, crevettes, gambas et autres poissons. Pour pêcher le bar, je n’utilise pas d’appâts vivants, je préfère utiliser des leurres artificiels qui imitent une proie spécifique.
En théorie, attraper un poisson est assez simple :
Utiliser un leurre qui imite la proie actuelle du bar.
Animer le leurre dans une zone où les poissons sont actifs.
Attraper un très gros poisson !
En pratique, c’est une autre histoire ! En effet, l’activité alimentaire, la position du bar dans l’estuaire et ses proies varient en fonction de plusieurs paramètres :
- Les caractéristiques du fond du fleuve, qui dépendent de l’endroit où je pêche.
- L’heure de la journée : le bar est plus actif à l’aube et au crépuscule.
- Le courant et le niveau d’eau associés à la marée. Le niveau d’eau dans les estuaires varie constamment en raison de l’influence des marées. Il est aussi influencé par le débit du fleuve, qui peut être plus élevé en cas de fortes pluies.
Comme vous l’avez compris, de nombreux paramètres peuvent potentiellement influencer les résultats de mes sessions de pêche. C’est pourquoi j’ai décidé de créer une application Shiny pour augmenter le nombre et la taille des poissons capturés durant mes sessions. Pour atteindre cet objectif, je dois mieux comprendre l’activité, la position et les proies du bar en fonction des paramètres décrits ci-dessus.
Exigences de mon application
- Elle doit stocker les données de mes sessions de pêche :
Informations nécessaires | Description des variables | Source des données |
---|---|---|
Temps | Heure à laquelle un poisson est capturé, durée écoulée depuis le début de la session | R |
Prise | Espèce et taille du poisson capturé | Géolocalisation via smartphone ? |
Leurres | Type, longueur, couleur du leurre utilisé | API météo |
- Elle doit enregistrer les données sur mes prises et les leurres artificiels utilisés :
Informations nécessaires | Description des variables | Source des données |
---|---|---|
Temps | Heure à laquelle un poisson est capturé, durée écoulée depuis le début de la session | R |
Prise | Espèce et taille du poisson capturé | Saisie utilisateur |
Leurres | Type, longueur, couleur du leurre utilisé | Saisie utilisateur |
Elle doit être adaptée aux petits écrans, car je l’utiliserai toujours sur mon téléphone.
Elle doit rester gratuite.
Collecte des données
Récupération de ma position GPS
Ma position GPS est collectée grâce à un peu de code Javascript intégré dans l’en-tête de l’application Shiny. Ce code a été développé par AugusT et est disponible sur son dépôt GitHub.
API météo
Pour les données météorologiques, j’ai trouvé une API gratuite appelée Dark Sky. J’ai développé une fonction qui prend en entrée les coordonnées d’un lieu ainsi que la clé utilisateur de l’API et retourne les conditions météorologiques actuelles sous forme de dataframe :
Web scraping des données de marée
J’ai créé une fonction pour récupérer des informations sur les marées à partir d’un site web français. La fonction suivante ne prend aucun argument et retourne le niveau d’eau actuel, l’état de la marée (montante ou descendante) ainsi que le temps écoulé depuis le dernier pic de marée pour le lieu où je pêche.
<- function(){
tide
# Set the current time and time zone
Sys.setenv(TZ="Europe/Paris")
<- as.POSIXct(Sys.time())
time <- "https://services.data.shom.fr/hdm/vignette/grande/BOUCAU-BAYONNE?locale=en"
url
# Read the web page that contains the tide data
<- url %>%
text read_html() %>%
html_text()
# Clean the html data to get a dataframe with two cols Time and water level:
<- as.character(sub(".*var data = *(.*?) *\\;.*", "\\1", text))
text <- unlist(str_split( substr(text, 1, nchar(text)-2), "\\],"))
text <- data.frame(hour=NA,Water=NA)
tidy_df
for(i in 1:length(text)){
<- unlist(str_split(text[i], '"'))[c(2,3)]
text_dat 1] <- substr(text_dat[1], 1, nchar(text_dat[1])-1)
text_dat[2] <- as.numeric(substr(text_dat[2], 2, nchar(text_dat[2])))
text_dat[<- text_dat
tidy_df[i,]
}
$hour <- as.POSIXct(paste(format(Sys.time(),"%Y-%m-%d"), tidy_df$hour))
tidy_df
# Some lines to get the tide status (going down or up) :
<- which(abs(tidy_df$hour - time) == min(abs(tidy_df$hour - time)))
n_closest
<- as.numeric(tidy_df[n_closest, 2])
water_level
<- all(tidy_df$Water[(n_closest-6):(n_closest+6)] ==
all_decrea cummin(tidy_df$Water[(n_closest-6):(n_closest+6)] ))
<- all(tidy_df$Water[(n_closest-6):(n_closest+6)] ==
all_increa cummax(tidy_df$Water[(n_closest-6):(n_closest+6)] ))
<- ifelse(all_decrea, "Down", ifelse(all_increa, "Up", "Dead"))
maree
# Compute time since the last peak :
<- max(cumsum(rle(diff(as.numeric(tidy_df$Water), lag = 2) > 0)$lengths)
last_peak cumsum(rle(diff(as.numeric(tidy_df$Water), lag = 2) >0)$lengths) < n_closest])
[
<- as.numeric(difftime(tidy_df$hour[n_closest], tidy_df$hour[last_peak], units = "mins"))
time_after
# Return the list with the results :
return(list(Water_level = water_level,
Maree = maree,
Time_peak = time_after))
}
L’application Shiny
Le principal problème que j’ai rencontré lors du développement de cette application était le stockage des données. Shinyapps.io héberge gratuitement votre application Shiny, mais j’ai rencontré des problèmes lorsque j’ai utilisé l’application pour modifier les fichiers CSV.
La solution que j’ai trouvée a été de stocker les données sur mon compte Dropbox. Vous pouvez trouver ici plus de détails sur le sujet ainsi que des solutions alternatives. J’ai utilisé le package rdrop2 pour accéder et modifier les données via l’application Shiny.
Voici les principales étapes de cette application :
Au démarrage de l’application, un fichier CSV stocké sur mon Dropbox est lu afin de vérifier si une session de pêche est en cours ou non. Si ce n’est pas le cas, l’utilisateur peut démarrer une session de pêche.
Lors du démarrage d’une nouvelle session, une ligne contenant les coordonnées, les conditions météorologiques et les conditions de marée est ajoutée au fichier CSV mentionné précédemment.
Si un poisson est pêché, l’utilisateur peut remplir un formulaire pour enregistrer les données dans un second fichier CSV. Ce fichier contient : l’heure, l’espèce et la longueur du poisson ainsi que des informations sur le leurre utilisé (type, couleur, longueur).
L’utilisateur peut mettre fin à la session de pêche en appuyant sur un bouton. Cela enregistre l’heure de fin, les conditions météorologiques et les conditions de marée dans le premier fichier CSV.
Un schéma simplifié est présenté ci-dessous :
Côté interface utilisateur (UI)
L’interface utilisateur de l’application est construite en utilisant le package miniUI. Ce package permet aux utilisateurs de R de développer des applications Shiny adaptées aux petits écrans.
# Load libraries
library(shiny)
library(shinyWidgets)
library(googlesheets)
library(miniUI)
library(leaflet)
library(rdrop2)
Sys.setenv(TZ="Europe/Paris")
#Import the functions for weather API and webscrapping
suppressMessages(source("api_functions.R"))
# Load the dropbox token :
<<- readRDS("token.rds")
token
# Minipage for small screens
<- miniPage(
ui # Javascript that give user location (input$lat,input$long)
$script('$(document).ready(function () {
tags navigator.geolocation.getCurrentPosition(onSuccess, onError);
function onError (err) {
Shiny.onInputChange("geolocation", false);
}
function onSuccess (position) {
setTimeout(function () {
var coords = position.coords;
console.log(coords.latitude + ", " + coords.longitude);
Shiny.onInputChange("geolocation", true);
Shiny.onInputChange("lat", coords.latitude);
Shiny.onInputChange("long", coords.longitude);
}, 1100)
}
});'),
gadgetTitleBar("Catch them all", left = NULL, right = NULL),
miniTabstripPanel(
#First panel depends if a fishing session is started or not
miniTabPanel("Session", icon = icon("sliders"),
miniContentPanel(uiOutput("UI_sess", align = "center"),
uiOutput("UI", align = "center"))
),# Second panel displays the location of the previous fishing session with the number of fish caught
miniTabPanel("Map", icon = icon("map-o"),
miniContentPanel(scrollable = FALSE,padding = 0,
div(style="text-align:center",
prettyRadioButtons("radio", inline = TRUE, label = "",
choices = list("3 dernières sessions" = 1,
"3 Meilleures Sessions" = 2,
"Tout afficher" = 3),
selected = 1)),
leafletOutput("map", height = "93%")
))
)
)
Côté serveur
Le côté serveur est principalement composé de fonctions observeEvent. L’utilité de chaque observeEvent est indiquée dans le script sous forme de commentaires.
<- function(input, output, session){
server source("api_functions.R")
# Read the csv file containing information about fishing session. If a session is running,
# display the UI that allows the user to input data about the fish caught. If a session is not started,
# display a button to start the session.
observeEvent(input$go ,{
<<- drop_read_csv("/app_peche/session.csv", header = T, stringsAsFactors = F, dtoken = token)
dat
$UI<- renderUI({
outputtagList(
if(rev(dat$Status)[1] == "end"){
actionButton("go","Start session")}
else{
actionButton("go","End session")
}
)
})
$UI_sess<- renderUI({
outputif(rev(dat$Status)[1] == "end"){
tagList(textInput("comments", label = h3("Commentaires"), value = "NA"))
else{
}$catch
input
tagList(
selectInput("species", label = h3("Espèces"),
choices = list("Bar" = "bar",
"Bar moucheté" = "bar_m",
"Alose" = "alose",
"Alose Feinte" = "alose_f",
"Maquereau" = "maquereau",
"Chinchard" = "chinchard"), selected = "bar"),
sliderInput("length",label = h3("Taille du poisson"),value=25,min=0,max=80, step=1),
selectInput("lure", label = h3("Type de leurre"),
choices = list("Shad" = "shad",
"Slug" = "slug",
"Jerkbait" = "jerkbait",
"Casting jig" = "jig",
"Topwater" = "topwater"), selectize = FALSE),
selectInput("color_lure", label = h3("Couleur du leurre"),
choices = list("Naturel" = "naturel",
"Sombre" = "sombre",
"Clair" = "clair",
"Flashy" = "flashy" ), selectize = FALSE),
selectInput("length_lure", label = h3("Taille du leurre"),
choices = list("Petit" = "petit",
"Moyen" = "moyen",
"Grand" = "grand"), selectize = FALSE),
actionButton("catch","Rajoutez cette capture aux stats!"),
textInput("comments1", label = h3("Commentaire avant la fin ?"), value = "NA")
)
}
})
ignoreNULL = F)
},
#If the button is pushed, create the line to be added in the csv file.
observeEvent(input$go,{
#Tide + geoloc + Weather
<- unlist(tide())
c_tide <- c(input$lat,input$long)
geoloc <- weather(geoloc)
current.weather.info
# Two outcomes depending if the session starts or ends. This gives the possibility
# to the user to add a comment before starting the session or after ending the session
if(rev(dat$Status)[1] == "end"){
<- c(rev(dat$Session)[1]+1)
n_ses <- c("beg")
stat_ses <- as.character(as.POSIXct(Sys.time()))
time_beg <- input$comments
comment <- data.frame(n_ses, stat_ses, time_beg ,geoloc[2], geoloc[1], current.weather.info, c_tide[1], c_tide[2], c_tide[3], comment)
dat.f names(dat.f)<-names(dat)
<- rbind(dat,dat.f)
a
else{
}
<- c(rev(dat$Session)[1])
n_ses <- c("end")
stat_ses <- as.character(as.POSIXct(Sys.time()))
time_beg <- input$comments1
comment1 <- data.frame(n_ses, stat_ses, time_beg ,geoloc[2], geoloc[1], current.weather.info, c_tide[1], c_tide[2], c_tide[3], comment1)
dat.fnames(dat.f)<-names(dat)
<- rbind(dat,dat.f)
a
}
# Write csv in temporary files of shiny server
write_csv(as.data.frame(a), "session.csv")
# Upload it to dropbox account
drop_upload("session.csv", path = "App_peche", mode = "overwrite", dtoken = token)
})
# Add a line to the catch csv file whenever a fish is caught
observeEvent(input$catch,{
<- drop_read_csv("/app_peche/catch.csv", header = T, stringsAsFactors = F, dtoken = token)
caugth
<- c(rev(dat$Session)[1])
n_ses <- as.POSIXct(Sys.time())
time <- round(as.numeric(difftime(time, rev(dat$Time)[1], units = "mins")), digits = 0)
time_after_beg
<- data.frame(n_ses,
catch time = as.character(time),
min_fishing = as.character(time_after_beg),
species = input$species,
length = input$length,
lure = input$lure,
colour = input$color_lure,
length_lure = input$length_lure)
<- rbind(caugth,catch)
b
# Write csv in temporary files of shiny server
write_csv(as.data.frame(b), "catch.csv")
# Upload it to dropbox account
drop_upload("catch.csv", path = "App_peche", mode = "overwrite", dtoken = token)
})
# Create the map with the results of previous session depending on the choice of the user :
observeEvent(input$radio,{
$map <- renderLeaflet({
output<- map_choice(input$radio)
map_data leaflet(map_data) %>% addTiles() %>%
addPopups(lng = ~Long,
lat = ~Lat,
with(map_data,
sprintf("<b>Session %.0f : %.1f h</b> <br/> %s <br/> %.0f poissons <br/> hauteur d'eau: %.0f m, %s, %.0f min après l'étal",
n_ses,
duration,
Time,
nb,
Water_level,
Tide_status,
Tide_time)),options = popupOptions(maxWidth = 100, minWidth = 50))
})
})
}
Conclusion et améliorations futures
Vous pouvez trouver un exemple de démonstration de cette application (non connectée au compte Dropbox)
ici.
J’utilise cette application depuis un an sans aucun problème ! Les données que j’ai collectées seront présentées dans le prochain article.
Dans les mois à venir, je dois trouver une nouvelle API gratuite pour remplacer l’actuelle. En effet, l’API météo que j’utilise a été rachetée par Apple et les requêtes gratuites seront arrêtées l’année prochaine.