1. Gestion de données avec `dplyr` et `(My)SQL`

    2016-05-31
    Source

    Cet article fournit des exemples de gestion de données dans R avec la librairie dplyr, et les commandes équivalentes avec MySQL.

    Commandes simples

    Les exemples seront appliqués au tableau de données suivant :

    1. dat1 <- data.frame(
    2. Name = c("Joe", "Joe", "Bill", "Jim", "Kate"),
    3. Value = c(10.1, 13, 9, 7.7, -3),
    4. Indic = c(0L, 1L, 1L, 2L, 2L),
    5. Status = c(TRUE, TRUE, TRUE, FALSE, FALSE),
    6. stringsAsFactors = FALSE
    7. )
    Name Value Indic Status
    Joe 10.1 0 TRUE
    Joe 13.0 1 TRUE
    Bill 9.0 1 TRUE
    Jim 7.7 2 FALSE
    Kate -3.0 2 FALSE

    On peut le créer ainsi dans MySQL :

    1. CREATE TABLE `dat1` (
    2. `Name` varchar(15),
    3. `Value` double,
    4. `Indic` int(11),
    5. `Status` tinyint(1)
    6. );
    7. INSERT INTO `dat1` VALUES
    8. ('Joe',10.1,0,1),('Joe',13,1,1),('Bill',9,1,1),('Jim',7.7,2,0),('Kate',-3,2,0);

    Sélectionner des colonnes

    R: MySQL:
    1. select(dat1, Name, Value)
    1. SELECT `Name`, `Value` FROM dat1;
    Name Value
    Joe 10.1
    Joe 13.0
    Bill 9.0
    Jim 7.7
    Kate -3.0

    Supprimer des colonnes

    R: MySQL:
    1. select(dat1, -Name, -Value)
    1. ALTER TABLE `dat1`
    2. DROP COLUMN `Name`, DROP COLUMN `Value`;
    Indic Status
    0 TRUE
    1 TRUE
    1 TRUE
    2 FALSE
    2 FALSE

    Plus d’infos pour SQL :

    Supprimer des lignes

    R: MySQL:
    1. dat1 <- dat1[-which(dat1$Name=="Joe"),]
    1. DELETE FROM `dat1`
    2. WHERE `Name`='Joe';
    Name Value Indic Status
    3 Bill 9.0 1 TRUE
    4 Jim 7.7 2 FALSE
    5 Kate -3.0 2 FALSE

    Renommer une colonne

    R: MySQL:
    1. rename(dat1, Score = Value)
    1. SELECT `Name`, `Value` AS `Score`, `Indic`, `Status`
    2. FROM `dat1`;
    Name Score Indic Status
    Joe 10.1 0 TRUE
    Joe 13.0 1 TRUE
    Bill 9.0 1 TRUE
    Jim 7.7 2 FALSE
    Kate -3.0 2 FALSE

    Dans MySQL, on peut aussi utiliser la méthode suivante qui est plus directe, avec elle on n’a pas besoin de taper les noms des autres colonnes :

    1. ALTER TABLE `dat1`
    2. CHANGE `Value` `Score` double NOT NULL;

    Notez qu’on dû spécifier le type de la colonne (double).

    Plus d’infos :

    Filtrer

    R: MySQL:
    1. filter(dat1, Value > 8)
    1. SELECT * FROM dat1
    2. WHERE `Value` > 8;
    Name Value Indic Status
    Joe 10.1 0 TRUE
    Joe 13.0 1 TRUE
    Bill 9.0 1 TRUE
    R: MySQL:
    1. filter(dat1, Value > 8, Indic==1)
    1. SELECT * FROM dat1
    2. WHERE `Value` > 8 AND Indic = 1;
    Name Value Indic Status
    Joe 13 1 TRUE
    Bill 9 1 TRUE

    Voici des exemples de conditions sur les chaînes de caractères :

    R: MySQL:
    1. library(stringr)
    2. # noms contenant "J"
    3. filter(dat1, str_detect(Name, "J"))
    4. # noms commençant par "J"
    5. filter(dat1, str_detect(Name, "^J"))
    6. # noms terminant par "e"
    7. filter(dat1, str_detect(Name, "e$"))
    8. # noms contenant "o" ou "K"
    9. filter(dat1, str_detect(Name, "[oK]"))
    1. /* noms contenant "J" */
    2. SELECT * FROM `dat1`
    3. WHERE `Name` LIKE '%J%';
    4. /* noms commençant par "J" */
    5. SELECT * FROM `dat1`
    6. WHERE `Name` LIKE 'J%';
    7. /* noms terminant par "e" */
    8. SELECT * FROM `dat1`
    9. WHERE `Name` LIKE '%e';
    10. /* noms contenant "o" ou "K" */
    11. SELECT * FROM `dat1`
    12. WHERE `Name` LIKE '%o%' OR Name LIKE '%K%';

    Transformer une colonne

    R: MySQL:
    1. mutate(dat1, Value=Value*2)
    1. UPDATE `dat1` SET `Value` = `Value`*2;
    Name Value Indic Status
    Joe 20.2 0 TRUE
    Joe 26.0 1 TRUE
    Bill 18.0 1 TRUE
    Jim 15.4 2 FALSE
    Kate -6.0 2 FALSE

    La fonction mutate permet aussi de mettre le résultat dans une nouvelle colonne :

    R: MySQL:
    1. mutate(dat1, x=Value*2)
    1. SELECT `Name`, `Value`, `Indic`, `Value` * 2 AS `x`
    2. FROM `dat1`;
    Name Value Indic Status x
    Joe 10.1 0 TRUE 20.2
    Joe 13.0 1 TRUE 26.0
    Bill 9.0 1 TRUE 18.0
    Jim 7.7 2 FALSE 15.4
    Kate -3.0 2 FALSE -6.0

    Pour éviter de sélectionner toutes les colonnes dans SQL, on peut d’abord créer une nouvelle colonne puis utiliser UPDATE comme précédemment :

    1. ALTER TABLE `dat1` ADD `x` double;
    2. UPDATE `dat1` SET `x` = `Value`*2;

    On peut facilement ajouter une colonne constante au tableau :

    R: MySQL:
    1. mutate(dat1, x='Hello')
    1. SELECT *, 'Hello' AS x
    2. FROM `dat1`;
    Name Value Indic Status x
    Joe 10.1 0 TRUE Hello
    Joe 13.0 1 TRUE Hello
    Bill 9.0 1 TRUE Hello
    Jim 7.7 2 FALSE Hello
    Kate -3.0 2 FALSE Hello

    Avec la fonction transmute, le résultat ne contient que les nouvelles colonnes :

    R: MySQL:
    1. transmute(dat1, x=Value*2, y=Indic+1)
    1. SELECT `Value` * 2 AS `x`, `Indic` + 1 AS `y`
    2. FROM `dat1`;
    x y
    20.2 1
    26.0 2
    18.0 2
    15.4 3
    -6.0 3

    Modifier la valeur dans une cellule

    R: MySQL:
    1. dat1$Value[which(dat1$Name=="Bill")] <- 8.5
    1. UPDATE `dat1`
    2. SET `Value` = 8.5
    3. WHERE `Name` = 'Bill';
    Name Value Indic Status
    Joe 10.1 0 TRUE
    Joe 13.0 1 TRUE
    Bill 8.5 1 TRUE
    Jim 7.7 2 FALSE
    Kate -3.0 2 FALSE

    Trier selon une colonne

    • dans l’ordre croissant :
    R: MySQL:
    1. arrange(dat1, Value)
    1. SELECT * FROM `dat1`
    2. ORDER BY `Value`;
    Name Value Indic Status
    Kate -3.0 2 FALSE
    Jim 7.7 2 FALSE
    Bill 9.0 1 TRUE
    Joe 10.1 0 TRUE
    Joe 13.0 1 TRUE
    • dans l’ordre décroissant :
    R: MySQL:
    1. arrange(dat1, desc(Value))
    1. SELECT * FROM `dat1`
    2. ORDER BY `Value` DESC;
    Name Value Indic Status
    Joe 13.0 1 TRUE
    Joe 10.1 0 TRUE
    Bill 9.0 1 TRUE
    Jim 7.7 2 FALSE
    Kate -3.0 2 FALSE

    Aggrégation

    Moyennes

    On obtient aussi la moyenne de Values pour chaque valeur de Name :

    R: MySQL:
    1. dat1 %>% group_by(Name) %>% summarise(m=mean(Value))
    1. SELECT `Name`, AVG(`Value`) AS `m`
    2. FROM `dat1`
    3. GROUP BY `Name`;
    Name m
    Bill 9.00
    Jim 7.70
    Joe 11.55
    Kate -3.00

    Si on veut obtenir les moyennes puis appliquer une autre requête, il suffit d’enchaîner les commandes avec dplyr ; par contre, avec SQL, il faut encapsuler le premier tableau avant d’appliquer la seconde requête :

    R: MySQL:
    1. dat1 %>% group_by(Name) %>% summarise(m=mean(Value)) %>% filter(m>8)
    1. SELECT `Name`, `m`
    2. FROM (SELECT `Name`, AVG(`Value`) AS `m`
    3. FROM `dat1`
    4. GROUP BY `Name`)
    5. AS `temp`
    6. WHERE `m`>8;
    Name m
    Bill 9.00
    Joe 11.55

    Comptages

    R: MySQL:
    1. dat1 %>% group_by(Name) %>% summarise(Count=n())
    1. SELECT `Name`, count(*) AS `Count`
    2. FROM `dat1`
    3. GROUP BY `Name`;
    Name Count
    Bill 1
    Jim 1
    Joe 2
    Kate 1

    Jonction de deux tableaux

    Dans les exemples qui suivent, nous utilisons ce deuxième tableau de données :

    1. dat2 <- data.frame(
    2. Name = c("Bill", "Brian"),
    3. Country = c("USA", "Scotland"),
    4. stringsAsFactors = FALSE
    5. )
    Name Country
    Bill USA
    Brian Scotland

    Jonction interne

    La commande R classique (qui n’utilise pas dplyr) est merge(dat1, dat2, by="Name").

    R: MySQL:
    1. inner_join(dat1, dat2, by="Name")
    1. SELECT * FROM
    2. `dat1` INNER JOIN `dat2`
    3. USING (`Name`);
    Name Value Indic Status Country
    Bill 9 1 TRUE USA

    Jonction à gauche

    commande R classique : merge(dat1, dat2, by="Name", all.x=TRUE)

    R: MySQL:
    1. left_join(dat1, dat2, by="Name")
    1. SELECT * FROM
    2. `dat1` LEFT JOIN `dat2`
    3. USING (`Name`);
    Name Value Indic Status Country
    Joe 10.1 0 TRUE NA
    Joe 13.0 1 TRUE NA
    Bill 9.0 1 TRUE USA
    Jim 7.7 2 FALSE NA
    Kate -3.0 2 FALSE NA

    Jonction à droite

    commande R classique : merge(dat1, dat2, by="Name", all.y=TRUE)

    R: MySQL:
    1. right_join(dat1, dat2, by="Name")
    1. SELECT * FROM
    2. `dat1` RIGHT JOIN `dat2`
    3. USING (`Name`);
    Name Value Indic Status Country
    Bill 9 1 TRUE USA
    Brian NA NA NA Scotland

    Jonction complète

    commande R classique : merge(dat1, dat2, all=TRUE)

    R: MySQL:
    1. full_join(dat1, dat2)
    1. (SELECT * FROM
    2. `dat1` LEFT JOIN `dat2`
    3. USING (`Name`))
    4. UNION
    5. (SELECT * FROM
    6. `dat1` RIGHT JOIN `dat2`
    7. USING (`Name`)
    8. WHERE `dat1`.`Name` IS NULL);
    Name Value Indic Status Country
    Joe 10.1 0 TRUE NA
    Joe 13.0 1 TRUE NA
    Bill 9.0 1 TRUE USA
    Jim 7.7 2 FALSE NA
    Kate -3.0 2 FALSE NA
    Brian NA NA NA Scotland

    Autres types de jonction (semi_join, anti_join):

    Du format large au format long (“unpivot”)

    Nous utilisons ici le tableau de données suivant :

    1. longdata <- data.frame(
    2. Name = c("Joe", "Bill"),
    3. Age = c(19L, 21L),
    4. Score1 = c(5, 9),
    5. Score2 = c(4, 8)
    6. )
    Name Age Score1 Score2
    Joe 19 5 4
    Bill 21 9 8

    Pour transformer ce tableau en un tableau au format long, avec Name et Age comme variables identifiantes, on peut utiliser la fonction melt de la librairie reshape2 :

    1. reshape2::melt(longdata, id=c("Name", "Age"), value.name="Value")

    La librairie tidyr est le successeur de la librairie reshape2.

    R: MySQL:
    1. library(tidyr)
    2. gather(longdata, Score, Value, -c(Name,Age))
    1. SELECT `Name`, `Age`, 'Score1' AS `Variable`, `Score1` AS `Value`
    2. FROM `longdata`
    3. UNION ALL
    4. SELECT `Name`, `Age`, 'Score2' AS `Variable`, `Score2` AS `Value`
    5. FROM `longdata`;
    Name Age Score Value
    Joe 19 Score1 5
    Bill 21 Score1 9
    Joe 19 Score2 4
    Bill 21 Score2 8

    Avec MySQL, nous avons simplement créé un tableau pour chaque valeur de la variable Score puis nous avons empilé les deux tableaux avec UNION ALL.

    1. mysql> SELECT `Name`, `Age`, 'Score1' AS `Score`, `Score1` AS `Value`
    2. -> FROM `longdata`;
    3. +------+------+--------+-------+
    4. | Name | Age | Score | Value |
    5. +------+------+--------+-------+
    6. | Joe | 19 | Score1 | 5 |
    7. | Bill | 21 | Score1 | 9 |
    8. +------+------+--------+-------+
    9. 2 rows in set (0.00 sec)

    Du format large au format long (“pivot”)

    Nous utilisons ici le tableau créé précedemment à partir du tableau longdata, et nous allons reconstruire longdata à partir de celui-ci.

    R: MySQL:
    1. widedata <- gather(longdata, Score, Value, -c(Name,Age))
    1. CREATE TABLE `widedata` AS
    2. SELECT `Name`, `Age`, 'Score1' AS `Score`, `Score1` AS `Value`
    3. FROM `longdata`
    4. UNION ALL
    5. SELECT `Name`, `Age`, 'Score2' AS `Score`, `Score2` AS `Value`
    6. FROM `longdata`;

    Dans R, on peut utiliser la commande dcast de la librairie reshape2 :

    1. reshape2::dcast(widedata, Name + Age ~ Score, value.var="Value")
    R: MySQL:
    1. library(tidyr)
    2. spread(widedata, Score, Value)
    1. SELECT Name, Age,
    2. MAX( IF( Score='Score1', `Value`, NULL ) ) AS `Score1`,
    3. MAX( IF( Score='Score2', `Value`, NULL ) ) AS `Score2`
    4. FROM widedata
    5. GROUP BY `Name`, `Age`;
    Name Age Score1 Score2
    Bill 21 9 8
    Joe 19 5 4

    Détaillons la procédure MySQL. Si on retire l’opérateur MAX et la commande GROUP BY, on obtient :

    1. mysql> SELECT Name, Age,
    2. -> IF( Score='Score1', `Value`, NULL ) AS `Score1`,
    3. -> IF( Score='Score2', `Value`, NULL ) AS `Score2`
    4. -> FROM widedata;
    5. +------+------+--------+--------+
    6. | Name | Age | Score1 | Score2 |
    7. +------+------+--------+--------+
    8. | Joe | 19 | 5 | NULL |
    9. | Bill | 21 | 9 | NULL |
    10. | Joe | 19 | NULL | 4 |
    11. | Bill | 21 | NULL | 8 |
    12. +------+------+--------+--------+
    13. 4 rows in set (0.00 sec)

    (on aurait le même résultat si on utilisait CASE WHEN `Score`='Score1' THEN `Value` END à la place de IF(...).)

    L’idée consiste alors à manipuler le tableau en regroupant par Name et Age, et à utiliser MAX pour garder la valeur numérique (par exemple, MAX appliqué à 5 et NULL retourne 5).