install.packages("ggplot2")Atelier : R pour la linguistique
Dans cet atelier, je vous présente une introduction au langage R pour l’étude de la linguistique. R est un langage de programmation libre avec le code source ouvert qui est particulièrement populaire dans les sciences naturelles et sociales. Nous commençons par une introduction générale aux fonctions permettant de télécharger, visualiser et manipuler des données structurées sous forme de tableau. Ensuite, nous continuons à étudier des modèles spécifiques pour l’étude du langage. Nous finissons avec l’application des interfaces de programmation d’application (API) pour l’usage des grands modèles de langage. Dans tous les exemples, nous employons les données qui proviennent de diverses parties de la linguistique.
Bien que nous présentions les principaux concepts dans les notes suivantes, nous vous suggérons de consulter les ressources supplémentaires suivantes pour approfondir le sujet :
- R for Data Science (2e) Livre disponible gratuitement en anglais qui approfondit les fonctions essentielles de visualisation, de manipulation et de programmation des données dans R présentées dans ces notes.
- Humanities Data in R (2e) Livre accèsible en anglais (nom d’utilisateur et mot de passe son “hdir”) qui donne les méthodes plus avancées pour traiter des données complexes et multimodales.
- Les aides-mémoirs : les graphiques, remaniement de données, et expressions régulières
Vous trouverez également une documentation complète sur les différentes fonctions et les différents «packages» sur le site de The Comprehensive R Archive Network (CRAN).
1. Philosophie
Ma philosophie en matière d’enseignement de la science des données est motivée par l’objectif de fournir les outils nécessaires à l’exploration libre des données, plutôt que de simplement encourager l’utilisation de chaînes de traitement et de modèles statiques existants. Pour atteindre cet objectif, je vous présente cet atelier organisé selon trois principes :
- des données tabulaires : Je privilégie les données tabulaires parce qu’elles nous permettent de baser nos analyses sur des structures théoriques. Ces théories proviennent de domaines tels que l’informatique, les mathématiques, les statistiques et la philosophie.
- des fonctions générales et basées sur les théories : Je privilégie les fonctions qui viennent des « packages » comme
dplyret ggplot2 au lieu des fonctions anciennes ou fonctions qui ne marchent qu’avec une seul type de donnée. Cela nous aide également à nous adapter vers d’autres langages de programmation comme Python, SQL ou JavaScript. - une base solide : Nous nous concentrerons sur les opérations de base avant de passer à des tâches plus complexes.
Ces principes nous obligent à nous concentrer fortement sur les fonctions générales pour visualiser et manipuler des données relativement simples dans les premières sections. Mais, comme vous pouvez le voir dans la table des matières à droite, ce travail nous aidera à progresser rapidement vers des sujets plus complexes… et à les comprendre une fois que nous y serons arrivés.
2. Configuration
Comme toutes les compétences, la seule façon d’apprendre le langage R est de pratiquer en produisant du code vous-même. Pour vous aider à le faire, je vous fournis des exercices qui correspondent à chaque section ci-dessous. Pour les suivre, vous pouvez soit exécuter le code dans Google Colab, soit télécharger R, RStudio, et les exemples et données ci-dessous dans votre propre ordinateur.
Les exercices appliquent et approfondissent les thèmes abordés à l’aide de données supplémentaires.
Il y a des collections de fonctions supplementaires dans les packages. Il existe des milliers de paquets open source. Au cours de cet atelier, nous en utiliserons plusieurs. La méthode la plus courante pour télécharger un package depuis R est donnée par le code suivant, dans lequel nous téléchargeons le package ggplot2.
Après avoir téléchargé le package, ce qui ne doit être fait qu’une seule fois, nous devons le charger à l’aide du code suivant, qui est exécuté à chaque fois que nous démarrons une nouvelle session dans R.
library(ggplot2)Le code permettant de télécharger tous les paquets nécessaires à l’atelier est fourni dans les documents liés ci-dessus. Nous utiliserons la fonction library pour charger chaque bibliothèque requise avant sa première utilisation dans notre code.
3. Données tabulaires
Nous commençons avec des données de la production des voyelles anglaises qui viennent d’une étude très bien connue de 139 personnes en 1995 (Hillenbrand et al.). Je les ai choisies parce qu’elles sont suffisamment petites pour une première application mais assez complexes pour être intéressantes.
J’ai préparé cette collection dans un fichier CSV. Vous pouvez également enregistrer vos données au format CSV en utilisant tous les tableurs comme LibreOffice, Google Sheets ou Excel. Afin de charger ces données dans R, on peut utiliser la fonction read_csv2 du package readr. Cette fonction nécessite un argument qui spécifie le chemin d’accès au fichier par rapport au code. Voici un exemple de charger les données de la production des voyelles anglaises.
library(readr)
read_csv2("donnees/hillenbrand_voyelle_eng.csv")# A tibble: 1,668 × 9
id groupe api xsampa dur f0 f1 f2 f3
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 1 fille æ { 257 238 630 2423 3166
2 1 fille ɑ A 212 241 831 1676 2602
3 1 fille ɔ O 242 247 725 1384 2642
4 1 fille ɛ E 184 214 713 2095 3129
5 1 fille e e 222 230 534 2690 3335
6 1 fille ɜ˞ 3' 227 240 608 1733 2159
7 1 fille ɪ I 197 263 551 2393 3324
8 1 fille i i 237 277 554 3022 3541
9 1 fille o o 267 250 693 1235 2850
10 1 fille ʊ U 184 247 553 1495 2868
# ℹ 1,658 more rows
Nous voyons que cette fonction renvoie un objet qui s’appelle un « tibble ». Dans R, un tibble est la structure dans laquelle on charge les données structurées sous forme de tableau. L’objet ci-dessus a 1668 lignes et 8 colonnes. Chaque ligne montre les métadonnées d’un locuteur et les mesures phonétiques d’une voyelle. Par défaut, nous ne voyons que les dix premières lignes. Pour chaque colonne, il y a un nom (id, groupe, etc.) et un type de données. Les colonnes numériques ont le type <dbl>, une abréviation de « double » dans l’expression « nombre flottante à virgule double précision ». Les colonnes qui contiennent des chaines de caractères ont le type <chr>, une abréviation de mot « caractère » en anglais. Nous allons voir que ces structures sont essentielles pour toutes les étapes de l’analyse de données en R.
Dans le code ci-dessus, nous avons créé et imprimé un tibble. Cependant, après l’exécution du code le tibble n’existe plus dans R. Afin de le sauvegarder, nous devons attribuer la sortie de la fonction à un nom à l’aide d’une flèche (signe inférieur et signe moins). Ici, nous sauvegardons le tibble dans l’objet phone.
phone <- read_csv2("donnees/hillenbrand_voyelle_eng.csv")Puis, après exécution, nous pouvons utiliser l’objet phone dans n’importe quelle autre tâche. Par exemple, dans la section suivante, nous allons visualiser ces données avec un langage adapté à l’exploration d’informations quantitatives.
4. Visualiser
La visualisation est une étape essentielle dans l’exploration de données. Nous appliquerons un langage général, qui s’appelle la « grammaire des graphiques », pour décrire et coder les visualisations dans R. Nous avons besoin d’un peu de la théorie avant de continuer, mais cette construction rend les visualisations complexes relativement faciles à réaliser.
Dans la grammaire des graphiques, une couche consiste de trois éléments :
- un tibble : L’objet qui contient l’information à visualiser.
- une géométrie : La forme qui correspond à chaque ligne du tibble. La visualisation consiste en une forme pour chaque ligne.
- des esthétiques : Les associations entre colonnes du tibble et les paramètres de chaque forme.
Une visualisation consiste d’une ou plusieurs couches. Un exemple sert à clarifier la manière dont ces parties peuvent correspondre aux éléments graphiques. Voici le code pour créer une visualisation de notre tibble phone qui a un point pour chaque ligne avec une position horizontale indiquée par la fréquence fondamentale (f0) et une position verticale indiquée par la durée de la voyelle (dur).
Dans cet exemple, nous commençons avec le nom d’un tibble (phone) suivi par le symbole |>. Ce symbole s’appelle un « pipe ». Il passe le tibble aux prochaines étapes. Cette construction nous permettre d’adapter l’order des fonctions et de les rendre plus facile à lire : p(h(g(f(x)))) devient x |> f() |> g() |> h() |> p().
Nous continuons avec la fonction ggplot() pour indiquer que nous voulons créer une visualisation. Dans la dernière ligne, nous appliquons la fonction geom_point pour spécifier la géométrie. À l’intérieur de la fonction, nous mettons la fonction aes() (pour aesthetic, esthétique en anglais) avec les associations entre les noms de colonnes et les positions horizontale et verticale. Par convention, la grammaire des graphiques utilise la lettre x pour la dimension horizontale et y pour la dimension verticale.
Comme l’exemple ci-dessus, la géométrie des points a deux esthétiques requises (x et y). Il y a d’autres esthétiques facultatives qu’on peut ajouter pour améliorer l’information portée par la visualisation. Par exemple, il y a une esthétique de la couleur qui permet de changer la couleur de chaque point en fonction d’une colonne du tibble. Voici un exemple où la couleur indique le groupe du locuteur ou de la locutrice.
Déjà, cette visualisation montre certaines correspondances entre les colonnes. Nous voyons que la différence de la fréquence fondamentale correspond à une séparation les hommes et les autres. Et, qu’il peut exister une correspondance où les hommes ont des durées légèrement plus petites que les femmes et les enfants.
Le lien entre les couleurs et les groupes n’est pas explicit dans notre code. Le système de la grammaire de graphiques peut choisir les couleurs automatiquement. C’était la même chose pour les positions horizontale et verticale. Mais, souvent on a besoin de les changer. Pour spécifier la relation entre les esthétiques et les éléments réels d’un graphique, on peut appliquer les échelles. Les échelles peuvent être ajoutées comme une autre ligne de notre code. Voici un exemple où nous modifions les couleurs à l’aide d’une palette adaptée aux personnes atteintes de daltonisme.
Voyons maintenant la visualisation la plus courante pour les données phonétiques qui fournit la relation entre les deux premiers formants F1 et F2. Ce graphique est au fond la même chose : les points avec les noms f2 et f1, respectivement, attribués aux esthétiques x et y. Mais, il y a une complexité. Afin de correspondre à la forme de la langue pour quelqu’un qui regarde vers sa droite, nous devons inverser le sens des axes. Cela nécessite l’application des échelles scale_x_reverse() et scale_y_reverse(). L’exemple suivant donne la forme correcte.
Cette visualisation montre la forme triangulaire classique de l’espace vocalique. Supposons qu’on veuille voir quelles voyelles correspondent à ces points. Comment pourrions-nous faire cela ? Nous devons appliquer une nouvelle géométrie : geom_text. Elle ne crée pas de points pour chaque ligne de données. En revanche, la géométrie de texte place directement des caractères dans le graphique. Nous devons donner une esthétique nouvelle pour indiquer la colonne qui correspond à ces caractères. Voici la façon d’avoir une visualisation avec les symboles API à la place des points.
Nous pouvons sauvgarder une visualisation avec la fonction ggsave. Par défaut, le dernier graphique affiché est enregistré.
ggsave("ma_visualisation.png", height=12, width=12, units="cm")Il existe beaucoup de géométries, d’esthétiques et d’échelles pour élargir les possibilités de visualisations dans la grammaire de graphiques. Vous pouvez consulter les références suivants : [1; 2].
Nous avons vu les éléments centraux de la grammaire de graphiques. Dans le but d’aller plus loin, nous continuons en étudiant les méthodes pour modifier les tibbles avec les fonctions liées aux bases de données.
5. Manipuler un tableau
Souvent, il faut transformer un tableau de données en un autre tableau avec des éléments différents ou réorganiser. Dans R, nous pouvons manipuler les tableaux avec une collection de fonctions qui s’appellent des « verbes ». Ces fonctions ont la même structure : on les donne un tibble puis on reçoit un nouveau tibble. Cette structure permet d’appliquer un nombre quelconque de fonctions à un tibble. Il existe environ 50 verbes. Heureusement, nous n’avons besoin d’en apprendre que 12 pour effectuer toutes les opérations possibles. Dans cette section, nous commençons avec les 8 premiers verbes.
Le verbe slice_head tranche les n premières lignes d’un tibble, où n est un argument dans la fonction. Comme tous les verbes, nous utilisons un pipe (|>) pour donner l’objet de données à la fonction.
library(dplyr)
phone |>
slice_head(n = 4)# A tibble: 4 × 9
id groupe api xsampa dur f0 f1 f2 f3
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 1 fille æ { 257 238 630 2423 3166
2 1 fille ɑ A 212 241 831 1676 2602
3 1 fille ɔ O 242 247 725 1384 2642
4 1 fille ɛ E 184 214 713 2095 3129
Le verbe filter retient les lignes selon une relation entre les valeurs. Pour appliquer cette fonction, on place une expression à l’intérieur avec les noms des colonnes. Les lignes où l’expression est vraie seront conservées. Voici un exemple pour trouver les lignes de la voyelle «u».
phone |>
filter(api == "u")# A tibble: 139 × 9
id groupe api xsampa dur f0 f1 f2 f3
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 1 fille u u 253 246 502 1540 3176
2 2 fille u u 327 287 559 1312 2870
3 3 fille u u 304 194 506 2378 2991
4 4 fille u u 292 258 503 1104 3346
5 5 fille u u 260 240 481 1226 3131
6 6 fille u u 303 330 609 1658 2644
7 7 fille u u 262 228 440 1124 3117
8 8 fille u u 211 255 503 1119 3100
9 9 fille u u 275 215 463 1676 2976
10 10 fille u u 246 197 430 1448 2896
# ℹ 129 more rows
Pour réorganiser des lignes par les valeurs dans une ou plusieurs colonnes, nous appliquons la fonction arrange. Nous indiquons simplement le nom de la colonne dans la fonction.
phone |>
arrange(dur)# A tibble: 1,668 × 9
id groupe api xsampa dur f0 f1 f2 f3
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 74 home ʊ U 111 156 463 1086 2453
2 71 home ɛ E 125 128 636 1893 2765
3 90 home u u 125 171 374 887 2478
4 74 home ʌ V 128 152 622 1285 2472
5 59 home ɪ I 134 145 424 1913 2556
6 74 home ɪ I 134 165 440 2119 2694
7 75 home ʊ U 134 160 500 1067 2435
8 7 fille ɛ E 135 205 605 2445 3265
9 22 fille ɪ I 137 229 447 2645 3302
10 64 home ɛ E 138 128 615 1624 2265
# ℹ 1,658 more rows
La réorganisation des lignes, par défaut, trie les lignes par ordre croissant. Pour un ordre décroissant, nous ajoutons la fonction desc.
phone |>
arrange(desc(dur))# A tibble: 1,668 × 9
id groupe api xsampa dur f0 f1 f2 f3
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 93 femme æ { 486 214 624 2442 3091
2 19 fille e e 465 232 463 2745 3155
3 111 femme ɔ O 465 226 698 1127 2886
4 93 femme ɔ O 464 211 760 1225 2796
5 115 femme æ { 461 220 646 2406 3283
6 31 garçon æ { 456 227 682 2638 3510
7 126 femme ɑ A 455 208 952 1676 2862
8 93 femme e e 453 220 477 2704 3102
9 24 fille æ { 451 220 643 2434 3326
10 93 femme ɑ A 443 209 883 1682 2962
# ℹ 1,658 more rows
Afin de démontrer l’application de plusieurs verbes, notons qu’il est souvent efficace d’appliquer arrange puis slice_head pour trouver les lignes des valeurs extrêmes. Nous voyons que chaque ligne, sauf la dernière, a un pipe à la fin.
phone |>
arrange(desc(f1)) |>
slice_head(n = 10)# A tibble: 10 × 9
id groupe api xsampa dur f0 f1 f2 f3
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 41 garçon ɑ A 299 233 1316 1752 3113
2 29 garçon ɑ A 298 208 1312 1820 3308
3 22 fille ɑ A 238 228 1205 1605 2708
4 35 garçon ɑ A 321 245 1197 1734 3187
5 123 femme ɑ A 283 241 1163 1685 3250
6 38 garçon ɑ A 317 200 1154 1932 3044
7 25 fille ɑ A 270 223 1147 1553 2877
8 45 garçon ɑ A 259 234 1145 1655 3062
9 138 femme ɑ A 326 224 1145 1455 3272
10 44 garçon ɑ A 220 235 1129 2005 2826
Le verbe mutate ajoute une colonne au tableau. Cela marche avec un nom de nouvelle colonne et la formule pour créer à partir d’autres colonnes. Par exemple, ci-dessous on a un exemple de la création d’une colonne dur_s (durée en secondes) qui est définie par la durée (en millisecondes) divisée par 1000.
phone |>
mutate(dur_s = dur / 1000)# A tibble: 1,668 × 10
id groupe api xsampa dur f0 f1 f2 f3 dur_s
<dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 1 fille æ { 257 238 630 2423 3166 0.257
2 1 fille ɑ A 212 241 831 1676 2602 0.212
3 1 fille ɔ O 242 247 725 1384 2642 0.242
4 1 fille ɛ E 184 214 713 2095 3129 0.184
5 1 fille e e 222 230 534 2690 3335 0.222
6 1 fille ɜ˞ 3' 227 240 608 1733 2159 0.227
7 1 fille ɪ I 197 263 551 2393 3324 0.197
8 1 fille i i 237 277 554 3022 3541 0.237
9 1 fille o o 267 250 693 1235 2850 0.267
10 1 fille ʊ U 184 247 553 1495 2868 0.184
# ℹ 1,658 more rows
Le verbe select fait une sélection de colonnes selon fonction de leur nom. Il est utile si la taille d’un tableau devient trop grande.
phone |>
select(f1, f2)# A tibble: 1,668 × 2
f1 f2
<dbl> <dbl>
1 630 2423
2 831 1676
3 725 1384
4 713 2095
5 534 2690
6 608 1733
7 551 2393
8 554 3022
9 693 1235
10 553 1495
# ℹ 1,658 more rows
Nous finissons cette section avec deux verbes qui fonctionnent souvent ensemble : group_by et summarise. Comme mutate, le verbe summarise permet de créer de nouvelles colonnes. Mais, il réduit les lignes à un sommaire avec l’application de fonctions comme mean (la moyenne) ou sd (l’écart-type). La fonction group_by indique par quelle colonne (ou colonnes) le sommaire est appliqué. Cet exemple peut clarifier la relation entre les deux. Voici le code pour calculer les moyennes de F1 et F2 selon la voyelle.
phone |>
group_by(api) |>
summarise(
f1_m = mean(f1),
f2_m = mean(f2)
)# A tibble: 12 × 3
api f1_m f2_m
<chr> <dbl> <dbl>
1 e 526. 2426.
2 i 412. 2725.
3 o 551. 1030.
4 u 445. 1167.
5 æ 663. 2257.
6 ɑ 891. 1508.
7 ɔ 767. 1173.
8 ɛ 686. 2050.
9 ɜ˞ 529. 1564.
10 ɪ 476. 2323.
11 ʊ 520. 1286.
12 ʌ 708. 1380.
Nous pouvons enregistrer le résultat d’une manipulation de données à l’aide d’une flèche (<-). Celui-ci peut ensuite être enregistré sous forme de fichier que nous pouvons ouvrir dans d’autres logiciels à l’aide de write_csv2. Voici un exemple
phone_moyenne <- phone |>
group_by(api) |>
summarise(
f1_m = mean(f1),
f2_m = mean(f2)
)
write_csv2(phone_moyenne, "donnees/phone_moyenne.csv")Le pouvoir de la fonction summarise devient plus clair en voyant l’application aux visualisations. Avec une combinaison des verbes et couches de graphiques, nous avons les outils pour visualiser les formants moyens de chaque voyelle dans les données.
phone |>
group_by(api) |>
summarise(
f1_m = mean(f1),
f2_m = mean(f2)
) |>
ggplot() +
geom_point(aes(x=f2_m, y=f1_m), size = 8) +
geom_text(aes(x=f2_m, y=f1_m, label = api), colour="#fff") +
scale_x_reverse() +
scale_y_reverse() Je tiens à mentionner ici une fonction R de base très efficace pour explorer les données : table. Elle peut compter le nombre de valeurs dans une colonne, par exemple (notez le symbole $ entre le nom de la table et le nom de la colonne) :
table(phone$api)
æ ɑ e ɛ ɜ˞ i ɪ o ɔ u ʊ ʌ
139 139 139 139 139 139 139 139 139 139 139 139
Ou bien, elle peut également compter les combinaisons de deux colonnes :
table(phone$api, phone$groupe)
femme fille garçon home
æ 48 27 19 45
ɑ 48 27 19 45
e 48 27 19 45
ɛ 48 27 19 45
ɜ˞ 48 27 19 45
i 48 27 19 45
ɪ 48 27 19 45
o 48 27 19 45
ɔ 48 27 19 45
u 48 27 19 45
ʊ 48 27 19 45
ʌ 48 27 19 45
Maintenant, nous avons une base solide de verbes et de fonctions de visualisation. Tous ces éléments sont au cœur de la science des données. Dans la section suivante, nous ajoutons des fonctions de modélisation.
6. Les modèles statistiques
Maintenant que nous savons comment manipuler les données, nous pouvons commencer à travailler avec de nouveaux ensembles de données. Ici, nous chargeons un ensemble de données de enregistreur de frappe dans lequel chaque saisi de texte effectuée lors d’une session d’écriture est enregistrée.
touches <- read_csv2("donnees/keylog-touches.csv.bz2", na="NA")
touches# A tibble: 1,145,051 × 7
id t0 t1 dur dur_apres touche code
<chr> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
1 R_00RbUqO7jXLDItP 20914. 20978. 64.4 80.1 "I" KeyI
2 R_00RbUqO7jXLDItP 21146. 21226. 80.1 55.8 "f" KeyF
3 R_00RbUqO7jXLDItP 21234. 21290. 55.8 80.2 "" Space
4 R_00RbUqO7jXLDItP 22074. 22154. 80.2 88.2 "I" KeyI
5 R_00RbUqO7jXLDItP 22306. 22394. 88.2 64.3 "" Space
6 R_00RbUqO7jXLDItP 23674. 23739. 64.3 56.1 "c" KeyC
7 R_00RbUqO7jXLDItP 23818. 23874. 56.1 46.6 "o" KeyO
8 R_00RbUqO7jXLDItP 24044. 24090. 46.6 64 "u" KeyU
9 R_00RbUqO7jXLDItP 25066. 25130. 64 79.8 "l" KeyL
10 R_00RbUqO7jXLDItP 25170. 25250 79.8 72.3 "d" KeyD
# ℹ 1,145,041 more rows
Une fois les données chargées, le bloc suivant réalise une étape de préparation où nous filtrons les observations pour ne retenir que deux types précis de touches, ici l’espace et le point. Cette sélection permet de comparer plus facilement la durée de frappe de ces deux catégories.
touches_sub <- touches |>
filter(code %in% c("Space", "Period"))Après cette préparation, nous appliquons un test statistique de comparaison de moyennes. Le « test de Student » permet d’évaluer si les durées de frappe mesurées diffèrent de manière significative entre les deux types de touches retenus. L’idée est de vérifier si l’espace et le point présentent des temps d’appui systématiquement différents, ce qui pourrait refléter des habitudes de frappe ou des contraintes mécaniques spécifiques.
t.test(dur ~ code, data = touches_sub)
Welch Two Sample t-test
data: dur by code
t = 21.903, df = 11523, p-value < 2.2e-16
alternative hypothesis: true difference in means between group Period and group Space is not equal to 0
95 percent confidence interval:
6.915632 8.275116
sample estimates:
mean in group Period mean in group Space
86.04275 78.44737
Les résultats montrent qu’il existe une différence significative, et que la durée moyenne d’un point (86.04 ms) est plus grande que celle d’un espace (78.45 ms).
Le bloc suivant étend l’analyse à un autre angle. Cette fois, il s’agit d’un test ANOVA à un facteur qui utilise l’identifiant de la session comme variable explicative. L’objectif est d’estimer si la durée de frappe varie beaucoup d’un individu à l’autre. Si la variabilité est importante, cela peut indiquer que les différences interpersonnelles jouent un rôle déterminant dans la vitesse ou le style de frappe.
oneway.test(dur ~ id, data = touches)
One-way analysis of means (not assuming equal variances)
data: dur and id
F = 320.45, num df = 822, denom df = 349612, p-value < 2.2e-16
Là encore, nous voyons qu’il existe une différence de la moyenne de la durée selon la session.
Enfin, le dernier bloc propose une analyse par régression linéaire. Il s’agit ici de comprendre comment la durée de frappe d’une touche pourrait être associée à la durée qui suit immédiatement cette frappe. Le modèle construit cherche à déterminer si, lorsque l’on appuie plus ou moins longtemps sur une touche, cela influence la rapidité avec laquelle on enchaîne la frappe suivante.
summary(lm(dur_apres ~ dur, data = touches))
Call:
lm(formula = dur_apres ~ dur, data = touches)
Residuals:
Min 1Q Median 3Q Max
-768972 -17 1 20 9042
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 71.33665 1.64052 43.484 < 2e-16 ***
dur 0.06564 0.01909 3.438 0.000586 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 765.1 on 1145049 degrees of freedom
Multiple R-squared: 1.032e-05, Adjusted R-squared: 9.45e-06
F-statistic: 11.82 on 1 and 1145049 DF, p-value: 0.0005857
Les résultats montrent qu’il existe une relation positive entre la durée d’une touche et la durée de la prochaine touche. Mais, le coefficient de détermination linéaire (également connu sous le nom de « pourcentage de variance expliquée » ou, en anglais, « Multiple R-squared ») est très petit, ce qui indique que cette relation n’est pas trop forte.
7. Plusieurs tableaux
Dans cette section, nous apprendrons les derniers verbes dont nous avons besoin. Ces verbes nous aident à combiner l’information dans plusieurs tableaux et complètent l’essentiel pour notre boîte à outils des verbes.
Nous commençons par charger un second tableau de données contenant des informations descriptives sur les utilisateurs dans les sessions de frappe. Ce fichier regroupe généralement des métadonnées telles que la langue, le niveau déclaré ou d’autres caractéristiques permettant de mieux contextualiser les mesures.
meta <- read_csv2("donnees/keylog-meta.csv.bz2")
meta# A tibble: 823 × 4
id age lang cefr
<chr> <dbl> <chr> <chr>
1 R_2EGIsZARLydD3Uc 25 Italian C1/C2
2 R_1obCaysaZCWZXoG 22 Spanish B1/B2
3 R_3fqTek829k38iCk 22 Polish B1/B2
4 R_brxD7Q5ZnPW8Gn7 43 English C1/C2
5 R_1k1RE78cBbZyZMA 23 Polish B1/B2
6 R_1NwuZMzRkVIR0WT 32 English C1/C2
7 R_2t8LOS9nQDBQPA8 24 Spanish C1/C2
8 R_239Q0X5YLwB7U6Z 28 English C1/C2
9 R_10xbkjEmnsusfb1 32 Polish B1/B2
10 R_10CbLBzAnYKgWxB 21 Polish B1/B2
# ℹ 813 more rows
Nous importons ensuite un troisième tableau, cette fois centré sur des mesures liées aux mots eux-mêmes. Il peut s’agir, par exemple, de durées de frappe associées à chaque mot tapé, ce qui fournit une information plus synthétique que l’analyse touche par touche.
mots <- read_csv2("donnees/keylog-mots.csv.bz2")
mots# A tibble: 210,337 × 6
id mot char_mot dur_mot d1 d2
<chr> <chr> <dbl> <dbl> <dbl> <dbl>
1 R_00RbUqO7jXLDItP If 2 312. 928. 848.
2 R_00RbUqO7jXLDItP I 1 80.2 1600. 1520
3 R_00RbUqO7jXLDItP could 5 1576. 2168. 2088.
4 R_00RbUqO7jXLDItP choose 6 945. 976. 904.
5 R_00RbUqO7jXLDItP to 2 200. 930. 849.
6 R_00RbUqO7jXLDItP be 2 151 528. 456.
7 R_00RbUqO7jXLDItP any 3 440. 264 168.
8 R_00RbUqO7jXLDItP animal 6 1560 976. 888
9 R_00RbUqO7jXLDItP for 3 264. 248 176.
10 R_00RbUqO7jXLDItP one 3 312 1120. 1048.
# ℹ 210,327 more rows
Une fois ces deux tableaux disponibles, nous effectuons une jonction entre eux à partir d’un identifiant commun. Cette opération permet de combiner les caractéristiques présentes dans les métadonnées avec les observations relatives aux mots, enrichissant ainsi chaque ligne d’information contextuelle supplémentaire. Le résultat est une table fusionnée où les durées ou autres mesures des mots peuvent être interprétées en fonction des profils utilisateurs. Voici un exemple où nous utilisons l’identifiant du participant (id) pour les fusionner.
mots |>
left_join(meta, by = "id")# A tibble: 210,337 × 9
id mot char_mot dur_mot d1 d2 age lang cefr
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
1 R_00RbUqO7jXLDItP If 2 312. 928. 848. 28 Italian C1/C2
2 R_00RbUqO7jXLDItP I 1 80.2 1600. 1520 28 Italian C1/C2
3 R_00RbUqO7jXLDItP could 5 1576. 2168. 2088. 28 Italian C1/C2
4 R_00RbUqO7jXLDItP choose 6 945. 976. 904. 28 Italian C1/C2
5 R_00RbUqO7jXLDItP to 2 200. 930. 849. 28 Italian C1/C2
6 R_00RbUqO7jXLDItP be 2 151 528. 456. 28 Italian C1/C2
7 R_00RbUqO7jXLDItP any 3 440. 264 168. 28 Italian C1/C2
8 R_00RbUqO7jXLDItP animal 6 1560 976. 888 28 Italian C1/C2
9 R_00RbUqO7jXLDItP for 3 264. 248 176. 28 Italian C1/C2
10 R_00RbUqO7jXLDItP one 3 312 1120. 1048. 28 Italian C1/C2
# ℹ 210,327 more rows
Le bloc suivant exploite cette jonction pour procéder à un regroupement selon une variable décrivant le niveau de compétence linguistique. Après avoir regroupé les observations selon ce critère, on calcule la médiane d’une mesure temporelle spécifique, ce qui permet d’obtenir un indicateur robuste de la performance dactylographique pour chaque niveau. Le tri final met en évidence les niveaux pour lesquels la médiane est la plus élevée, facilitant la comparaison globale.
mots |>
left_join(meta, by = "id") |>
group_by(cefr) |>
summarise(mu = median(d1)) |>
arrange(desc(mu))# A tibble: 3 × 2
cefr mu
<chr> <dbl>
1 A1/A2 789.
2 B1/B2 592
3 C1/C2 448
Enfin, sur un principe similaire, l’analyse est répétée en regroupant cette fois les données selon la langue déclarée. L’objectif est d’examiner si la performance sur les mots varie sensiblement d’une langue maternelle à l’autre.
mots |>
left_join(meta, by = "id") |>
group_by(lang) |>
summarise(mu = median(d1)) |>
arrange(desc(mu))# A tibble: 8 × 2
lang mu
<chr> <dbl>
1 Greek 608.
2 Polish 520
3 Portuguese 503.
4 Spanish 499
5 Italian 486.
6 French 447.
7 German 441
8 English 435
Nous voyons que les étudiants anglais ont tapé le plus vite.
8. Fenêtres glissantes
Nous avons traité les analyses horizontales dans lesquelles nous ne voyons que les relations entres les valeurs d’une ligne. Avec summarise et les fonctions pour les modèles statistiques, nous avons egalement traité les analyses verticales dans lesquelles nous traitons toutes les valeurs d’une colonne sur toutes les lignes, ou tous les groupes de lignes. Dans cette section, nous allons plus loin en utilisant ce que l’on appelle des fonctions à fenêtre glissante. Ces fonctions nous aident à trouver des relations entre les observations en fonction de l’ordre des lignes dans nos données. Des fonctions à fenêtre glissante sont très utiles lorsque l’on travaille avec des séries chronologiques, comme c’est souvent le cas en linguistique.
Nous commençons par créer un tableau simplifié contenant uniquement les variables essentielles pour illustrer l’usage des fonctions de fenêtre.
touches_min <- select(touches, id, t0, t1, dur, touche, code)
touches_min# A tibble: 1,145,051 × 6
id t0 t1 dur touche code
<chr> <dbl> <dbl> <dbl> <chr> <chr>
1 R_00RbUqO7jXLDItP 20914. 20978. 64.4 "I" KeyI
2 R_00RbUqO7jXLDItP 21146. 21226. 80.1 "f" KeyF
3 R_00RbUqO7jXLDItP 21234. 21290. 55.8 "" Space
4 R_00RbUqO7jXLDItP 22074. 22154. 80.2 "I" KeyI
5 R_00RbUqO7jXLDItP 22306. 22394. 88.2 "" Space
6 R_00RbUqO7jXLDItP 23674. 23739. 64.3 "c" KeyC
7 R_00RbUqO7jXLDItP 23818. 23874. 56.1 "o" KeyO
8 R_00RbUqO7jXLDItP 24044. 24090. 46.6 "u" KeyU
9 R_00RbUqO7jXLDItP 25066. 25130. 64 "l" KeyL
10 R_00RbUqO7jXLDItP 25170. 25250 79.8 "d" KeyD
# ℹ 1,145,041 more rows
Le bloc suivant illustre l’usage de plusieurs fonctions de fenêtre classiques comme lag et lead, fréquemment utilisées dans l’analyse de séries temporelles ou de séquences ordonnées. Elles nous permettent de « voir » les lignes avant et après. L’argument n donne le nombre de lignes auxquelles accéder.
touches_min |>
arrange(id, t0) |>
group_by(id) |>
mutate(
diff_avant = t0 - lag(t0, n=1),
diff_apres = lead(t0, n=1) - t0,
gap_avant = t0 - lag(t1, n=1),
gap_apres = lead(t0, n=1) - t1,
)# A tibble: 1,145,051 × 10
# Groups: id [823]
id t0 t1 dur touche code diff_avant diff_apres gap_avant
<chr> <dbl> <dbl> <dbl> <chr> <chr> <dbl> <dbl> <dbl>
1 R_00RbUqO7j… 20914. 20978. 64.4 "I" KeyI NA 232. NA
2 R_00RbUqO7j… 21146. 21226. 80.1 "f" KeyF 232. 88.1 168.
3 R_00RbUqO7j… 21234. 21290. 55.8 "" Space 88.1 840. 8
4 R_00RbUqO7j… 22074. 22154. 80.2 "I" KeyI 840. 232. 784
5 R_00RbUqO7j… 22306. 22394. 88.2 "" Space 232. 1368. 152.
6 R_00RbUqO7j… 23674. 23739. 64.3 "c" KeyC 1368. 144. 1280.
7 R_00RbUqO7j… 23818. 23874. 56.1 "o" KeyO 144. 225. 79.8
8 R_00RbUqO7j… 24044. 24090. 46.6 "u" KeyU 225. 1023. 169.
9 R_00RbUqO7j… 25066. 25130. 64 "l" KeyL 1023. 104. 976
10 R_00RbUqO7j… 25170. 25250 79.8 "d" KeyD 104. 120 39.9
# ℹ 1,145,041 more rows
# ℹ 1 more variable: gap_apres <dbl>
Il y a d’autres fonctions de fenêtre dans le package slider qui nous aident à calculer des résumés plus complexes. Par exemple, slide_mean donne la moyenne d’une fenêtre autour de chaque ligne d’une taille spécifiée.
library(slider)
touches_min |>
filter(id == "R_00RbUqO7jXLDItP") |>
arrange(t0) |>
mutate(
dur_moyenne10 = slide_mean(dur, before=10),
dur_moyenne100 = slide_mean(dur, before=100),
dur_moyenne1000 = slide_mean(dur, before=1000)
) |>
ggplot() +
geom_line(aes(x=t0, y=dur_moyenne10), colour = "#fa8072") +
geom_line(aes(x=t0, y=dur_moyenne100), colour = "#808000") +
geom_line(aes(x=t0, y=dur_moyenne1000), colour = "#6fa8dc") Enfin, le dernier bloc explore l’utilisation de fenêtres glissantes plus larges permettant de calculer des moyennes mobiles sur différents horizons. Après avoir filtré un utilisateur particulier, nous ordonnons ses frappes puis appliquons plusieurs tailles de fenêtre afin de lisser progressivement les durées observées. Cela donne des courbes plus ou moins sensible aux variations locales : une petite fenêtre suit de près les changements instantanés, tandis qu’une grande fenêtre met en évidence des tendances plus globales. Le graphique obtenu superpose ces différentes moyennes, offrant une visualisation intuitive de l’évolution du rythme de frappe au cours du temps.
9. Chaînes de caractères
Dans cette section, nous introduisons l’usage des fonctions provenant du package stringi pour manipuler les données textuelles . Ces fonctions ont la même forme : le nom commence avec stri_ et le premier argument est une séquence de caractères. Par exemple, voici la fonction stri_length qui compte le nombre de caractères dans une séquence.
library(stringi)
mots |>
mutate(nchar = stri_length(mot))# A tibble: 210,337 × 7
id mot char_mot dur_mot d1 d2 nchar
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <int>
1 R_00RbUqO7jXLDItP If 2 312. 928. 848. 2
2 R_00RbUqO7jXLDItP I 1 80.2 1600. 1520 1
3 R_00RbUqO7jXLDItP could 5 1576. 2168. 2088. 5
4 R_00RbUqO7jXLDItP choose 6 945. 976. 904. 6
5 R_00RbUqO7jXLDItP to 2 200. 930. 849. 2
6 R_00RbUqO7jXLDItP be 2 151 528. 456. 2
7 R_00RbUqO7jXLDItP any 3 440. 264 168. 3
8 R_00RbUqO7jXLDItP animal 6 1560 976. 888 6
9 R_00RbUqO7jXLDItP for 3 264. 248 176. 3
10 R_00RbUqO7jXLDItP one 3 312 1120. 1048. 3
# ℹ 210,327 more rows
Le bloc suivant combine cette nouvelle information à celle provenant du tableau des métadonnées. Après avoir joint les deux tables selon l’identifiant utilisateur, nous regroupons les mots par langue, puis calculons la longueur moyenne des mots pour chaque groupe. Ce type de résumé aide à comparer les différentes langues présentes dans le corpus, en examinant si certaines présentent systématiquement des mots plus longs, ce qui pourrait influencer la dynamique de frappe observée.
mots |>
mutate(nchar = stri_length(mot)) |>
left_join(meta, by = "id") |>
group_by(lang) |>
summarise(mu = mean(nchar, na.rm=TRUE)) |>
arrange(desc(mu))# A tibble: 8 × 2
lang mu
<chr> <dbl>
1 German 4.47
2 Greek 4.47
3 French 4.45
4 Portuguese 4.43
5 Italian 4.42
6 Polish 4.39
7 Spanish 4.38
8 English 4.38
La majorité des fonctions de stringi a besoin d’un argument figé pour spécifier ce qu’elles font. Par exemple, la fonction stri_detect indique la présence d’une séquence de caractères. Ici nous l’appliquons pour indiquer quels mots ont le caractère <a> en utilisant l’argument fixed.
mots |>
mutate(nombre_a = stri_detect(mot, fixed = "a"))# A tibble: 210,337 × 7
id mot char_mot dur_mot d1 d2 nombre_a
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <lgl>
1 R_00RbUqO7jXLDItP If 2 312. 928. 848. FALSE
2 R_00RbUqO7jXLDItP I 1 80.2 1600. 1520 FALSE
3 R_00RbUqO7jXLDItP could 5 1576. 2168. 2088. FALSE
4 R_00RbUqO7jXLDItP choose 6 945. 976. 904. FALSE
5 R_00RbUqO7jXLDItP to 2 200. 930. 849. FALSE
6 R_00RbUqO7jXLDItP be 2 151 528. 456. FALSE
7 R_00RbUqO7jXLDItP any 3 440. 264 168. TRUE
8 R_00RbUqO7jXLDItP animal 6 1560 976. 888 TRUE
9 R_00RbUqO7jXLDItP for 3 264. 248 176. FALSE
10 R_00RbUqO7jXLDItP one 3 312 1120. 1048. FALSE
# ℹ 210,327 more rows
Souvent, nous ne voulons pas seulement chercher un sequence donné. En revanche, nous avons besoin de trouver les lignes qui correspondent à un ensemble des caractères. Pour cela, nous appliquons les « expressions régulières », langage pour décrire les motifs dans les séquences de caractères. Par exemple, l’expression [A-Z] indique les lettres latines majuscules.
mots |>
mutate(nombre_maj = stri_detect(mot, regex = "[A-Z]"))# A tibble: 210,337 × 7
id mot char_mot dur_mot d1 d2 nombre_maj
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <lgl>
1 R_00RbUqO7jXLDItP If 2 312. 928. 848. TRUE
2 R_00RbUqO7jXLDItP I 1 80.2 1600. 1520 TRUE
3 R_00RbUqO7jXLDItP could 5 1576. 2168. 2088. FALSE
4 R_00RbUqO7jXLDItP choose 6 945. 976. 904. FALSE
5 R_00RbUqO7jXLDItP to 2 200. 930. 849. FALSE
6 R_00RbUqO7jXLDItP be 2 151 528. 456. FALSE
7 R_00RbUqO7jXLDItP any 3 440. 264 168. FALSE
8 R_00RbUqO7jXLDItP animal 6 1560 976. 888 FALSE
9 R_00RbUqO7jXLDItP for 3 264. 248 176. FALSE
10 R_00RbUqO7jXLDItP one 3 312 1120. 1048. FALSE
# ℹ 210,327 more rows
Après avoir détecté les majuscules et recalculé la longueur du mot, nous filtrons les cas les plus courts afin de faciliter la visualisation. Les données sont ensuite regroupées par longueur et par présence ou absence de majuscule, avant de calculer une médiane des durées associées à la frappe du mot. Le graphique produit illustre ces tendances en représentant, pour chaque longueur, les différences éventuelles entre les deux types de mots, ce qui fournit un aperçu visuel clair des variations liées à la mise en forme du texte.
mots |>
mutate(has_maj = stri_detect(mot, regex = "[A-Z]")) |>
mutate(nchar = stri_length(mot)) |>
filter(nchar < 10) |>
group_by(nchar, has_maj) |>
summarise(mu = median(dur_mot)) |>
ggplot() +
geom_point(aes(x=factor(nchar), y=mu, colour = has_maj))Il y a beaucoup d’autres fonctions de stringi qui ont la même forme avec le choix d’une phrase figée ou une expression régulière. Par exemple, j’utilise souvent stri_count, stri_replace, stri_extract et stri_match.
Nous n’avons pas le temps pour une introduction complète aux expressions régulières. Voici les symboles les plus courants pour la linguistique :
\w: n’importe quel caractère d’un mot (a-z, 0-9, à, è, é, etc.)\W: l’inverse, c’est-à-dire les espaces, virgules, points etc.\A: indique le début d’une phrase\Z: indique la fit d’une phrase- ‘+’ : indique une ou plusieurs occurrences du symbole précédent
Pour une référence de toutes les options, je vous recommande le « Cheatsheet » de Mozilla, qui est accessible en français ou en anglais.
10. TextGrid
Dans cet section, nous travaillons avec des fichiers au format TextGrid, un format couramment utilisé en phonétique pour annoter des enregistrements audio qui provient du logiciel Pratt [3]. Nous allons étudier les données d’un corpus qui s’appelle Rhapsodie. Rhapsodie fournit une base de données syntaxique et prosodique pour le français parlé [4]. Voici un exemple d’un TextGrid du corpus Rhapsodie dans Pratt.

Ce fichier TextGrid contenant plusieurs niveaux d’annotation, chacun représentant des informations différentes comme des segments phonétiques, des contours prosodiques ou d’autres repères temporels. Les niveaux d’annotation se sont appellés les «tiers». Dans Praat, les annotations sont organisées en différentes couches alignées selon leur temporalité.
Voici nous téléchargeons un fichier du corpus Rhapsodie dans R en utilisons le package readtextgrid [5].
library(readtextgrid)
tg <- read_textgrid("donnees/rhapsodie/tg/Rhap-M0018-Pro.TextGrid")
tg# A tibble: 2,731 × 10
file tier_num tier_name tier_type tier_xmin tier_xmax xmin xmax text
<chr> <int> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
1 Rhap-M001… 1 phone Interval… 0 89.7 0 0.695 _
2 Rhap-M001… 1 phone Interval… 0 89.7 0.695 0.735 e
3 Rhap-M001… 1 phone Interval… 0 89.7 0.735 0.845 s
4 Rhap-M001… 1 phone Interval… 0 89.7 0.845 0.875 a
5 Rhap-M001… 1 phone Interval… 0 89.7 0.875 0.935 a~
6 Rhap-M001… 1 phone Interval… 0 89.7 0.935 1.01 S
7 Rhap-M001… 1 phone Interval… 0 89.7 1.01 1.06 E
8 Rhap-M001… 1 phone Interval… 0 89.7 1.06 1.10 n
9 Rhap-M001… 1 phone Interval… 0 89.7 1.10 1.16 s
10 Rhap-M001… 1 phone Interval… 0 89.7 1.16 1.22 y
# ℹ 2,721 more rows
# ℹ 1 more variable: annotation_num <int>
Le format dans R est très différent du format dans Praat. Dans R, les dimensions temporelles des niveaux ne sont pas directement reliées entre elles. Toutes les données d’un niveau (ici phone) sont fournies, suivies du deuxième niveau, et ainsi de suite. Ce format n’est pas aussi bien adapté à la consultation directe des données, mais il est plutôt optimisé pour la programmation.
Le bloc suivant extrait spécifiquement le tier correspondant aux phonèmes, appelé « phone » dans le fichier. Ce niveau contient une segmentation fine de la parole où chaque entrée représente un segment phonétique délimité dans le temps.
tg_phone <- tg |>
filter(tier_name == "phone")
tg_phone# A tibble: 734 × 10
file tier_num tier_name tier_type tier_xmin tier_xmax xmin xmax text
<chr> <int> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
1 Rhap-M001… 1 phone Interval… 0 89.7 0 0.695 _
2 Rhap-M001… 1 phone Interval… 0 89.7 0.695 0.735 e
3 Rhap-M001… 1 phone Interval… 0 89.7 0.735 0.845 s
4 Rhap-M001… 1 phone Interval… 0 89.7 0.845 0.875 a
5 Rhap-M001… 1 phone Interval… 0 89.7 0.875 0.935 a~
6 Rhap-M001… 1 phone Interval… 0 89.7 0.935 1.01 S
7 Rhap-M001… 1 phone Interval… 0 89.7 1.01 1.06 E
8 Rhap-M001… 1 phone Interval… 0 89.7 1.06 1.10 n
9 Rhap-M001… 1 phone Interval… 0 89.7 1.10 1.16 s
10 Rhap-M001… 1 phone Interval… 0 89.7 1.16 1.22 y
# ℹ 724 more rows
# ℹ 1 more variable: annotation_num <int>
Nous procédons ensuite à une analyse descriptive des durées phonémiques. En éliminant les segments marqués par un symbole de remplissage, nous regroupons les phonèmes par type et calculons la durée moyenne correspondante. Cela permet de visualiser, sous forme de nuage de points, les phonèmes les plus courts et les plus longs, révélant ainsi des tendances phonétiques naturelles comme la brièveté des voyelles réduites ou la relative lenteur de certaines consonnes.
tg_phone |>
filter(text != "_") |>
group_by(text) |>
summarise(dur_moyenne = mean(xmax - xmin)) |>
arrange(dur_moyenne) |>
ggplot() +
geom_point(aes(x=fct_inorder(text), y=dur_moyenne))Le bloc suivant extrait un autre tier, ici appelé contour, qui peut représenter des catégories prosodiques ou des niveaux mélodiques associés à la parole. En filtrant ce tier pour ne conserver que certains symboles choisis, on se concentre sur les principales catégories d’annotation nécessaires à l’analyse. Afficher le tableau obtenu permet de vérifier que les entrées retenues sont bien celles attendues avant de les utiliser comme éléments de référence pour une fusion avec les phonèmes.
tg_contour <- tg |>
filter(tier_name == "contour") |>
filter(text %in% c("M", "C", "H", "L"))
tg_contour# A tibble: 191 × 10
file tier_num tier_name tier_type tier_xmin tier_xmax xmin xmax text
<chr> <int> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
1 Rhap-M001… 5 contour Interval… 0 89.8 0.695 0.735 L
2 Rhap-M001… 5 contour Interval… 0 89.8 0.735 0.875 L
3 Rhap-M001… 5 contour Interval… 0 89.8 0.875 0.935 L
4 Rhap-M001… 5 contour Interval… 0 89.8 0.935 1.10 H
5 Rhap-M001… 5 contour Interval… 0 89.8 1.22 1.36 L
6 Rhap-M001… 5 contour Interval… 0 89.8 1.36 1.52 H
7 Rhap-M001… 5 contour Interval… 0 89.8 4.08 4.49 C
8 Rhap-M001… 5 contour Interval… 0 89.8 6.04 6.14 C
9 Rhap-M001… 5 contour Interval… 0 89.8 6.79 7.43 C
10 Rhap-M001… 5 contour Interval… 0 89.8 8.99 9.14 H
# ℹ 181 more rows
# ℹ 1 more variable: annotation_num <int>
Le bloc suivant illustre une opération plus avancée : il s’agit de joindre les informations phonémiques et prosodiques en fonction de leurs chevauchements temporels en utilisant la fonction join_by. Cette jonction exploite une condition où un phonème est associé à une catégorie prosodique si ses bornes temporelles se trouvent incluses dans l’intervalle correspondant du contour. Le résultat est un tableau enrichi où chaque phonème porte, en plus de son étiquette propre, l’annotation prosodique qui lui correspond. Ce type de fusion temporelle est courant en traitement de la parole pour relier différents niveaux d’analyse.
tg_join <- tg_phone |>
left_join(
select(tg_contour, xmin, xmax, text),
by = join_by(
xmin >= xmin,
xmax <= xmax
),
suffix = c("", "_contour")
)
tg_join# A tibble: 734 × 13
file tier_num tier_name tier_type tier_xmin tier_xmax xmin xmax text
<chr> <int> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
1 Rhap-M001… 1 phone Interval… 0 89.7 0 0.695 _
2 Rhap-M001… 1 phone Interval… 0 89.7 0.695 0.735 e
3 Rhap-M001… 1 phone Interval… 0 89.7 0.735 0.845 s
4 Rhap-M001… 1 phone Interval… 0 89.7 0.845 0.875 a
5 Rhap-M001… 1 phone Interval… 0 89.7 0.875 0.935 a~
6 Rhap-M001… 1 phone Interval… 0 89.7 0.935 1.01 S
7 Rhap-M001… 1 phone Interval… 0 89.7 1.01 1.06 E
8 Rhap-M001… 1 phone Interval… 0 89.7 1.06 1.10 n
9 Rhap-M001… 1 phone Interval… 0 89.7 1.10 1.16 s
10 Rhap-M001… 1 phone Interval… 0 89.7 1.16 1.22 y
# ℹ 724 more rows
# ℹ 4 more variables: annotation_num <int>, xmin_contour <dbl>,
# xmax_contour <dbl>, text_contour <chr>
Enfin, nous terminons par un tableau croisé qui récapitule la distribution des phonèmes selon les catégories prosodiques associées.
tg_join |>
filter(text != "_") |>
group_by(text) |>
summarise(
moyenne_l = mean(text_contour == "L", na.rm=TRUE),
n = n()
) |>
arrange(moyenne_l) |>
filter(n > 20) |>
print(n = Inf)# A tibble: 14 × 3
text moyenne_l n
<chr> <dbl> <int>
1 t 0.263 28
2 p 0.278 29
3 k 0.333 22
4 a~ 0.389 30
5 i 0.417 28
6 R 0.44 49
7 @ 0.455 30
8 d 0.467 27
9 s 0.517 39
10 u 0.545 22
11 E 0.583 23
12 a 0.6 73
13 l 0.625 47
14 e 0.696 35
Ce tableau permet de repérer rapidement quelles combinaisons apparaissent fréquemment et lesquelles sont rares ou absentes. Une telle vue d’ensemble peut aider à détecter des structures prosodiques typiques ou à identifier d’éventuelles incohérences dans l’annotation.
11. Programmer
L’analyse dans la dernière section n’utilise qu’un seul fichier. Le corpus Rhapsodie se compose d’environ 60 fichiers. Ce n’est pas pratique de créer le code pour les charger un par un. En revanche, on a besoin de fonctions de programmation en R pour appliquer notre analyse à chaque fichier.
Voici un exemple de code qui (1) trouve les fichiers au format TextGrid (avec dir), (2) applique une boucle sur chaque fichier et (3) combine tous les résultats dans un grand tableau (avec bind_rows).
dir_nom <- "donnees/rhapsodie/tg/"
d <- dir(dir_nom, pattern="TextGrid$")
df <- list("vector", length(d))
for (j in seq_along(d)) {
tg <- read_textgrid(file.path(dir_nom, d[j]), encoding="UTF-8")
tg$id <- j
# ↓↓↓ cette partie ci-dessous fonctionne de traiter une seule
# ↓↓↓ fiche ; vous pouvez la changer
tg_phone <- tg |>
filter(tier_name == "phone")
tg_contour <- tg |>
filter(tier_name == "contour") |>
filter(text %in% c("M", "C", "H", "L"))
res <- tg_phone |>
left_join(
select(tg_contour, xmin, xmax, text),
by = join_by(
xmin >= xmin,
xmax <= xmax
),
suffix = c("", "_contour")
)
# ↑↑↑
df[[j]] <- res
}
df <- bind_rows(df)
df# A tibble: 104,695 × 14
file tier_num tier_name tier_type tier_xmin tier_xmax xmin xmax text
<chr> <int> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
1 Rhap-D000… 1 phone Interval… 0 330. 0 2.23 _
2 Rhap-D000… 1 phone Interval… 0 330. 2.23 2.27 e
3 Rhap-D000… 1 phone Interval… 0 330. 2.27 2.42 s
4 Rhap-D000… 1 phone Interval… 0 330. 2.42 2.45 k
5 Rhap-D000… 1 phone Interval… 0 330. 2.45 2.48 @
6 Rhap-D000… 1 phone Interval… 0 330. 2.48 2.54 v
7 Rhap-D000… 1 phone Interval… 0 330. 2.54 2.65 u
8 Rhap-D000… 1 phone Interval… 0 330. 2.65 2.68 p
9 Rhap-D000… 1 phone Interval… 0 330. 2.68 2.74 u
10 Rhap-D000… 1 phone Interval… 0 330. 2.74 2.82 R
# ℹ 104,685 more rows
# ℹ 5 more variables: annotation_num <int>, id <int>, xmin_contour <dbl>,
# xmax_contour <dbl>, text_contour <chr>
La partie entre les flèches indique où j’ai ajouté du code spécifique à chaque fichier.
Après avoir obtenu le tableau df, nous pouvons appliquer les mêmes verbes que nous avons utilisé dans la section précédente.
df |>
filter(text != "_") |>
group_by(text) |>
summarise(
moyenne_l = mean(text_contour == "L", na.rm = TRUE),
n = n()
) |>
arrange(moyenne_l) |>
filter(n > 20) |>
print(n = Inf)# A tibble: 38 × 3
text moyenne_l n
<chr> <dbl> <int>
1 m= 0.25 50
2 ? 0.364 23
3 H 0.382 426
4 f 0.411 1369
5 w 0.418 1194
6 S 0.450 511
7 j 0.451 1811
8 i 0.464 5412
9 s 0.467 5828
10 p 0.468 3608
11 t 0.468 5069
12 o~ 0.488 2178
13 o 0.503 1284
14 k 0.504 4208
15 u 0.514 2160
16 b 0.517 1212
17 y 0.518 1978
18 O 0.533 2021
19 R 0.535 7418
20 9 0.548 883
21 a~ 0.552 3425
22 Z 0.552 1710
23 E 0.552 4393
24 2 0.556 967
25 J 0.562 27
26 g 0.571 636
27 e~ 0.574 759
28 e 0.575 6287
29 n 0.576 2724
30 z 0.576 1451
31 m 0.579 3202
32 v 0.603 2721
33 a 0.609 8589
34 l 0.611 6144
35 9~ 0.640 860
36 d 0.642 4413
37 @ 0.642 3647
38 % NaN 127
Nous voyons que les résultats deviennent plus stables avec tous les fichiers.
12. Données audio
Il est possible d’analyser les fichiers audio (.mp3, .wav, etc.) directement dans R. Voici nous télécharger un fichier qui contient la prononciation de la voyelle /i/ en français avec les fonctions readWave et makesound, respectivement du package tuneR et de package phonTools [6; 7].
library(tuneR)
library(phonTools)
w <- readWave("donnees/voyelles/i.wav")
snd <- makesound(w@left, fs = w@samp.rate)
snd
Sound Object
Read from file: w@left.wav
Sampling frequency: 44100 Hz
Duration: 2340 ms
Number of Samples: 103194
Le résultat donne un objet spécifique (« Sound Object ») que nous pouvons étudier avec les fonctions provenant du package phonTools. Par exemple, nous pouvons calculer l’intensité du son avec powertrak. Par défaut, la fonction crée une visualisation en même temps.
Comme toujours, nous voudrions organiser l’information dans un tableau puis appliquer toutes les fonctions générales pour la visualisation, la modification et les modèles.
power <- as_tibble(power)
power# A tibble: 467 × 2
time power
<dbl> <dbl>
1 7.53 -70.1
2 12.5 -70.3
3 17.5 -70.2
4 22.5 -70.1
5 27.5 -69.5
6 32.5 -69.5
7 37.5 -69.5
8 42.4 -70.2
9 47.4 -69.9
10 52.4 -70.5
# ℹ 457 more rows
Nous pouvons faire la même chose avec les formants en utilisant la fonction formanttrack.
Et aussi, nous pouvons créer des données tabulaires.
formants <- as_tibble(formants)
formants# A tibble: 90 × 6
time f1 f2 f3 f4 f5
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 840. 1078. 2087. 4162. 0 0
2 895. 899. 3212. 4513. 0 0
3 965. 1122. 2078. 2718. 3430. 4287.
4 1010. 1075. 3405. 4099. 0 0
5 1050. 593. 1722. 2551. 3468. 4290.
6 1075. 294. 2447. 3636. 4406. 0
7 1080. 280. 2459. 3713. 4322. 0
8 1085. 262. 2498. 3640. 4177. 0
9 1090. 245. 2493. 3597. 4118. 0
10 1095. 235. 2459. 3575. 4124. 0
# ℹ 80 more rows
Pour aller plus loin, il faut appliquer les fonctions de programmation pour calculer la courbe de l’intensité et les formants pour chaque voyelle (qui sont dans leurs propres fichiers). Nous pouvons appliquer et modifier l’exemple dans la Section 11. Voici le code qui sauvegarde les formants au point de l’intensité maximale pour chaque voyelle.
dir_nom <- "donnees/voyelles"
d <- dir(dir_nom, pattern="wav$")
df <- list("vector", length(d))
for (j in seq_along(d)) {
w <- readWave(file.path(dir_nom, d[j]))
snd <- makesound(w@left, fs = w@samp.rate)
# ↓↓↓ cette partie ci-dessous fonctionne de traiter une seule
# ↓↓↓ fiche ; vous pouvez la changer
power <- powertrack(snd, fs = fs, show=FALSE)
power <- as_tibble(power)
formants <- formanttrack(snd, fs = fs, show=FALSE)
formants <- as_tibble(formants)
t_haut <- arrange(power, desc(power))$time[1]
res <- arrange(formants, abs(time - t_haut))[1,]
res$fname <- d[j]
# ↑↑↑
df[[j]] <- res
}
df <- bind_rows(df)
df# A tibble: 11 × 7
time f1 f2 f3 f4 f5 fname
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
1 965. 760. 1173. 2480. 3453. 3662. a.wav
2 1380. 351. 2404. 2919. 4144. 0 e.wav
3 650. 537. 1460. 2581. 3471. 4365. ə.wav
4 520. 663. 1626. 2459. 3512. 3794. ɛ.wav
5 1155. 216. 2437. 3622. 4101. 0 i.wav
6 975. 352. 874. 2494. 3698. 4329. o.wav
7 1460. 250 1421. 2595. 3453. 4174. ø.wav
8 795. 501. 1398. 2581. 3386. 0 œ.wav
9 955. 619. 1115. 2610. 3524. 4188. ɔ.wav
10 1050. 257. 580. 1923. 2626. 3842. u.wav
11 975. 239. 2119. 2307. 3138. 4085. y.wav
Nous pouvons récréer la visualisation comme dans la Section 4 avec les formants de ces exemples.
df |>
mutate(api = stri_replace(fname, "", fixed = ".wav")) |>
ggplot() +
geom_text(aes(x=f2, y=f1, label = api)) +
scale_x_reverse() +
scale_y_reverse()Pour finir, je note qu’il est aussi possible d’utiliser la fonction pitchtrack de la même façon pour calculer la fréquence fondamentalle d’un fichier sonore.
13. Analyse grammaticale
Dans cette section, nous explorons l’analyse grammaticale automatique à l’aide de udpipe, outil qui permet d’annoter automatiquement du texte de manière détaillée : identification des mots, lemmatisation, catégorisation grammaticale et détection de traits morphologiques [8].
Le premier bloc charge un modèle de langue pré-entraîné pour le français. Ce modèle est dérivé du corpus GSD [9]. Si l’on ne spécifie pas de modèle, celui-ci sera téléchargé automatiquement.
library(udpipe)
udmodel_fr <- udpipe_load_model(file = "donnees/french-gsd-ud-2.5-191206.udpipe")Le bloc suivant applique ce modèle à un texte littéraire, la « tirade du nez » [10]. Le texte est dans un format brut puis passé au moteur d’annotation qui segmente les mots, identifie leur catégorie grammaticale, leur lemme et leurs traits morphologiques. Le résultat, converti en tibble, permet d’inspecter les annotations ligne par ligne.
txt_tirade <- read_lines("donnees/tirade_du_nez.txt")
df_tirade <- as_tibble(udpipe_annotate(
udmodel_fr, x = txt_tirade
))
df_tirade# A tibble: 625 × 14
doc_id paragraph_id sentence_id sentence token_id token lemma upos xpos
<chr> <int> <int> <chr> <chr> <chr> <chr> <chr> <chr>
1 doc1 1 1 C'est tout ? 1 C' ce PRON <NA>
2 doc1 1 1 C'est tout ? 2 est être AUX <NA>
3 doc1 1 1 C'est tout ? 3 tout tout ADJ <NA>
4 doc1 1 1 C'est tout ? 4 ? ? PUNCT <NA>
5 doc2 1 1 Ah ! non ! 1 Ah ah INTJ <NA>
6 doc2 1 1 Ah ! non ! 2 ! ! PUNCT <NA>
7 doc2 1 1 Ah ! non ! 3 non non ADV <NA>
8 doc2 1 1 Ah ! non ! 4 ! ! PUNCT <NA>
9 doc2 1 2 c'est un pe… 1 c' ce PRON <NA>
10 doc2 1 2 c'est un pe… 2 est être AUX <NA>
# ℹ 615 more rows
# ℹ 5 more variables: feats <chr>, head_token_id <chr>, dep_rel <chr>,
# deps <chr>, misc <chr>
L’application de udpipe_annotate peut prendre plusieurs heures pour les grandes données. Heureusement, comme les résultats sont un tableau, nous ne pouvons l’appliquer qu’une seule fois, sauvegarder dans un fichier, puis charger pour réanalyser.
Dans le bloc suivant, nous chargeons un corpus tiré de Wikipédia (3 pour cent de tous les articles), déjà prétraité pour inclure des annotations grammaticales. Ce corpus volumineux permet d’étudier les habitudes grammaticales de milliers de phrases, ce qui ouvre la voie à des analyses quantitatives plus ambitieuses.
wikifr <- read_csv2("donnees/wiki_parsed.csv.bz2")
wikifr# A tibble: 2,431,081 × 11
doc_id sid tid token lemma pos xpos dep dep_head head morph
<chr> <dbl> <dbl> <chr> <chr> <chr> <chr> <chr> <dbl> <chr> <chr>
1 Arabie saoudi… 1 1 L l NOUN NOUN nsubj 10 mona… <NA>
2 Arabie saoudi… 1 2 , , PUNCT PUNCT punct 1 L <NA>
3 Arabie saoudi… 1 3 en en ADP ADP case 4 forme <NA>
4 Arabie saoudi… 1 4 forme forme NOUN NOUN nmod 1 L Gend…
5 Arabie saoudi… 1 5 long… long ADJ ADJ amod 4 forme Gend…
6 Arabie saoudi… 1 6 le le DET DET det 10 mona… Defi…
7 Arabie saoudi… 1 7 , , PUNCT PUNCT punct 10 mona… <NA>
8 Arabie saoudi… 1 8 est être AUX AUX cop 10 mona… Mood…
9 Arabie saoudi… 1 9 une un DET DET det 10 mona… Defi…
10 Arabie saoudi… 1 10 mona… mona… NOUN NOUN ROOT 0 ROOT Gend…
# ℹ 2,431,071 more rows
Le bloc suivant effectue une analyse centrée sur les verbes et leurs temps conjugués en utilisant les fonctions provenant de stringi. Nous filtrons d’abord les entrées pour ne conserver que les verbes qui portent un trait morphologique indiquant un temps verbal. Ensuite, nous détectons la présence de plusieurs temps (présent, imparfait et passé) en comptant combien de fois chaque trait apparaît dans les annotations. En regroupant les occurrences par lemme, nous obtenons une estimation de la fréquence moyenne des temps verbaux utilisés pour chaque verbe. Après filtrage des lemmes suffisamment fréquents, nous trions les résultats pour mettre en avant ceux dont le présent apparaît le moins souvent. Cette approche montre comment extraire des tendances grammaticales générales à partir d’un grand corpus.
wikifr |>
filter(pos == "VERB") |>
filter(stri_detect(morph, fixed = "Tense=")) |>
mutate(
pres = stri_count(morph, fixed = "Tense=Pres"),
imp = stri_count(morph, fixed = "Tense=Imp"),
passe = stri_count(morph, fixed = "Tense=Past")
) |>
group_by(lemma) |>
summarise(
pres_moyenne = mean(pres),
imp_moyenne = mean(imp),
passe_moyenne = mean(passe),
n = n()
) |>
filter(n > 800) |>
arrange(pres_moyenne) |>
print(n = Inf)# A tibble: 21 × 5
lemma pres_moyenne imp_moyenne passe_moyenne n
<chr> <dbl> <dbl> <dbl> <int>
1 situer 0.191 0.0237 0.783 1012
2 utiliser 0.220 0.0637 0.705 1020
3 appeler 0.273 0.0317 0.688 882
4 créer 0.275 0.00581 0.713 861
5 connaître 0.280 0.0201 0.695 995
6 réaliser 0.304 0.0242 0.669 869
7 considérer 0.328 0.0536 0.614 839
8 mettre 0.399 0.0134 0.584 1415
9 dire 0.427 0.0239 0.535 836
10 faire 0.608 0.0563 0.308 2893
11 voir 0.660 0.0317 0.304 1041
12 passer 0.661 0.0289 0.305 935
13 avoir 0.677 0.127 0.171 3391
14 devoir 0.682 0.177 0.111 1716
15 prendre 0.686 0.0163 0.287 1351
16 devenir 0.687 0.0166 0.289 1445
17 permettre 0.767 0.0509 0.171 1532
18 trouver 0.789 0.0699 0.135 1316
19 pouvoir 0.834 0.0537 0.0939 3484
20 aller 0.885 0.0680 0.0403 868
21 être 0.928 0.0261 0.0301 1228
Il est possible aussi d’entraîner les modèles nous-mêmes. Pour cela, nous chargeons un corpus au format CoNLL-U, structure standard utilisée dans le projet Universal Dependencies pour représenter des annotations linguistiques complètes. Ce fichier contient des exemples annotés manuellement, ce qui en fait une ressource précieuse pour entraîner ou évaluer des modèles. La conversion en tibble permet d’en inspecter facilement les colonnes, notamment les mots, les lemmes, les étiquettes grammaticales et les dépendances syntaxiques.
ud <- as_tibble(udpipe_read_conllu("donnees/fr_sequoia-ud-train.conllu"))
ud# A tibble: 51,862 × 14
doc_id paragraph_id sentence_id sentence token_id token lemma upos xpos
<chr> <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 <NA> 0 annodis.er_000… Gutenbe… 1 Gute… Gute… PROPN <NA>
2 <NA> 0 annodis.er_000… Cette e… 1 Cette ce DET <NA>
3 <NA> 0 annodis.er_000… Cette e… 2 expo… expo… NOUN <NA>
4 <NA> 0 annodis.er_000… Cette e… 3 nous nous PRON <NA>
5 <NA> 0 annodis.er_000… Cette e… 4 appr… appr… VERB <NA>
6 <NA> 0 annodis.er_000… Cette e… 5 que que SCONJ <NA>
7 <NA> 0 annodis.er_000… Cette e… 6 dès dès ADP <NA>
8 <NA> 0 annodis.er_000… Cette e… 7 le le DET <NA>
9 <NA> 0 annodis.er_000… Cette e… 8 XIIe XIIe ADJ <NA>
10 <NA> 0 annodis.er_000… Cette e… 9 sièc… sièc… NOUN <NA>
# ℹ 51,852 more rows
# ℹ 5 more variables: feats <chr>, head_token_id <chr>, dep_rel <chr>,
# deps <chr>, misc <chr>
Le bloc suivant montre comment entraîner son propre modèle UDPipe à partir d’un corpus annoté. L’entraînement consiste à apprendre un tokenizer, un étiqueteur morphosyntaxique et un analyseur en dépendances. Les résultats sont sauvegardés dans le fichier exemple_fr.udpipe.
m <- udpipe_train(
file = "donnees/exemple_fr.udpipe",
files_conllu_training = "donnees/fr_sequoia-ud-train.conllu",
annotation_tokenizer = "default",
annotation_tagger = "default",
annotation_parser = "default"
)Enfin, le dernier bloc charge ce modèle nouvellement entraîné, puis l’applique au texte de la tirade afin de comparer les résultats avec ceux du modèle standard. Cette étape permet d’évaluer les différences d’annotation, d’observer les éventuelles améliorations ou divergences, et d’illustrer concrètement l’importance du choix du modèle dans une chaîne d’analyse grammaticale automatisée.
udmodel_fr_nouv <- udpipe_load_model(
file = "donnees/exemple_fr.udpipe"
)
df_tirade_nouv <- as_tibble(udpipe_annotate(
udmodel_fr_nouv, x = txt_tirade
))
df_tirade_nouv# A tibble: 620 × 14
doc_id paragraph_id sentence_id sentence token_id token lemma upos xpos
<chr> <int> <int> <chr> <chr> <chr> <chr> <chr> <chr>
1 doc1 1 1 C'est tout ? 1 C' ce PRON <NA>
2 doc1 1 1 C'est tout ? 2 est être AUX <NA>
3 doc1 1 1 C'est tout ? 3 tout tout ADJ <NA>
4 doc1 1 1 C'est tout ? 4 ? ? PUNCT <NA>
5 doc2 1 1 Ah ! 1 A à ADP <NA>
6 doc2 1 1 Ah ! 2 h h NOUN <NA>
7 doc2 1 1 Ah ! 3 ! ! PUNCT <NA>
8 doc2 1 2 non ! 1 non non ADV <NA>
9 doc2 1 2 non ! 2 ! ! PUNCT <NA>
10 doc2 1 3 c'est un pe… 1 c' ce PRON <NA>
# ℹ 610 more rows
# ℹ 5 more variables: feats <chr>, head_token_id <chr>, dep_rel <chr>,
# deps <chr>, misc <chr>
Nous allons analyser les différences entre ces résultats et ceux du modèle standard dans une section suivante.
14. ACP + UMAP
Dans cette section, nous explorons deux méthodes très répandues de réduction de dimensionnalité, la ACP (analyse en composantes principales) et UMAP, qui permettent de représenter des données complexes dans un espace de faible dimension tout en conservant autant que possible leur structure.
Le contexte ici est l’analyse de représentations vectorielles de mots obtenues par un modèle de type fastText, qui génère pour chaque mot un vecteur de grande dimension reflétant ses similarités sémantiques dans de vastes corpus textuels [11]. Réduire ces vecteurs à deux dimensions permet de visualiser les relations entre mots d’un simple coup d’œil. fasttext. Nous allons voir d’autres applications de ces méthodes dans les sections suivantes.
fl <- read_csv2("donnees/fruitlegumes.csv")
fl# A tibble: 119 × 2
nom type
<chr> <chr>
1 abricot fruit
2 açaï fruit
3 agrumes fruit
4 amande fruit
5 ananas fruit
6 argousier fruit
7 avocat fruit
8 banane fruit
9 bergamote fruit
10 bigarreau fruit
# ℹ 109 more rows
Le premier bloc charge un tableau contenant une liste de fruits et légumes ainsi qu’une indication de leur catégorie. Ce tableau servira de référence pour associer chaque mot (par exemple « pomme », « carotte ») à sa classe (« fruit » ou « légume »), ce qui facilitera la visualisation et l’interprétation des résultats produits par les méthodes de réduction de dimensionnalité.
Nous chargeons ensuite une matrice de vecteurs d’embedding, c’est-à-dire une représentation numérique du sens des mots. Chaque mot correspond à une ligne de la matrice et chaque colonne à une dimension latente. En faisant correspondre les noms du tableau de fruits/légumes aux lignes de cette matrice, nous extrayons les vecteurs utiles pour notre analyse.
embed <- read_rds("donnees/fasttext_embed.rds")
idx <- match(fl$nom, rownames(embed))
X <- embed[idx, ]
dim(X)[1] 119 300
Le bloc suivant applique la ACP aux vecteurs. Cette méthode linéaire vise à projeter les données dans un espace de plus faible dimension en conservant la direction de variance maximale.
apc <- prcomp(X, center = TRUE, scale. = TRUE)
fl$apc1 <- apc$x[, 1]
fl$apc2 <- apc$x[, 2]
fl# A tibble: 119 × 4
nom type apc1 apc2
<chr> <chr> <dbl> <dbl>
1 abricot fruit 7.13 -4.45
2 açaï fruit 3.51 8.55
3 agrumes fruit 3.90 -0.761
4 amande fruit 3.89 -5.05
5 ananas fruit 2.72 -0.415
6 argousier fruit 2.94 3.95
7 avocat fruit -0.166 -1.45
8 banane fruit 4.12 -3.87
9 bergamote fruit 6.16 -1.93
10 bigarreau fruit 3.78 0.289
# ℹ 109 more rows
Nous retenons ici les deux premières composantes principales et les ajoutons au tableau initial afin de pouvoir les utiliser directement pour l’affichage. Cela permet de visualiser, de manière simplifiée, comment les représentations des mots se distribuent dans l’espace.
Une fois les deux composantes extraites, nous les représentons graphiquement. Chaque point correspond à un fruit ou un légume, la couleur désigne la catégorie, et les étiquettes textuelles facilitent l’identification individuelle. Cette visualisation met généralement en évidence des regroupements naturels : par exemple, les fruits tendent à se rassembler dans une zone du plan, les légumes dans une autre, ce qui illustre la capacité des embeddings à capturer des dimensions sémantiques pertinentes.
fl |>
ggplot() +
geom_point(aes(x=apc1, y=apc2, colour = type)) +
geom_text(
aes(x=apc1, y=apc2, label = nom, colour = type),
size = 2,
nudge_y = -0.25
)Le bloc suivant applique UMAP, une méthode non linéaire beaucoup plus flexible que la ACP. UMAP cherche à préserver la structure locale des données et est particulièrement efficace pour faire apparaître des groupes compacts ainsi que des relations sémantiques fines. Nous utilisons le package umap et la fonction umap [12].
library(umap)
obj <- umap(X)
fl$umap1 <- obj$layout[, 1]
fl$umap2 <- obj$layout[, 2]Les coordonnées résultantes sont ajoutées au tableau principal afin de pouvoir les visualiser de la même manière que les composantes APC.
fl |>
ggplot() +
geom_point(aes(x=umap1, y=umap2, colour = type)) +
geom_text(
aes(x=umap1, y=umap2, label = nom, colour = type),
size = 2,
nudge_y = -0.1
)La dernière visualisation montre le résultat d’UMAP dans un plan à deux dimensions. Ce type de projection peut révéler des structures que la APC ne met pas en évidence, comme des sous-groupes plus subtils ou des distances sémantiques plus cohérentes à petite échelle. La combinaison des couleurs et des étiquettes rend la lecture intuitive et facilite la comparaison des deux approches de réduction de dimensionnalité.
15. Modèles prédictifs
Dans cette section, nous abordons la construction d’un modèle prédictif à partir des données textuelles issues des mots tapés par les utilisateurs. L’objectif est d’illustrer comment transformer des données brutes en un ensemble de caractéristiques exploitables par un algorithme d’apprentissage supervisé. Nous utilison les données des critique du film du site Allociné.fr. Nous l’utilisons pour une analyse de sentiment.
Nous commençons par importer les données d’analyse linguistique. Ce fichier contient les mots tokenisés et lemmatisés de chaque document.
parse <- read_csv2("donnees/polarity_parse.csv.bz2")
parse# A tibble: 17,431,814 × 5
doc_id sid token lemma pos
<dbl> <dbl> <chr> <chr> <chr>
1 0 0 Si si SCONJ
2 0 0 vous vous PRON
3 0 0 cherchez chercher VERB
4 0 0 du de ADP
5 0 0 cinéma cinéma NOUN
6 0 0 abrutissant abrutissant ADV
7 0 0 à à ADP
8 0 0 tous tout ADJ
9 0 0 les le DET
10 0 0 étages étage NOUN
# ℹ 17,431,804 more rows
Nous chargeons ensuite les métadonnées qui contiennent notamment la variable cible (polarity) que nous cherchons à prédire.
meta <- read_csv2("donnees/polarity_meta.csv.bz2")
meta# A tibble: 160,000 × 2
doc_id polarity
<dbl> <dbl>
1 0 0
2 1 0
3 2 0
4 3 0
5 4 1
6 5 0
7 6 1
8 7 1
9 8 1
10 9 0
# ℹ 159,990 more rows
Cette étape transforme le texte en matrice sparse avec to_sparse. Nous normalisons les lemmes en minuscules avec stri_trans_tolower, puis filtrons pour ne garder que les mots apparaissant plus de 200 fois. Chaque ligne représente un document et chaque colonne un lemme.
X <- parse |>
mutate(lemma = stri_trans_tolower(lemma)) |>
group_by(lemma) |>
filter(n() > 200) |>
to_sparse(doc_id, lemma)
dim(X)[1] 159961 4639
Nous créons un tibble qui associe chaque identifiant de document aux métadonnées correspondantes avec left_join, assurant ainsi la correspondance entre X et les étiquettes.
df_meta <- tibble(doc_id = as.numeric(rownames(X))) |>
left_join(meta, by = "doc_id")
df_meta# A tibble: 159,961 × 2
doc_id polarity
<dbl> <dbl>
1 0 0
2 1 0
3 2 0
4 3 0
5 4 1
6 5 0
7 6 1
8 7 1
9 8 1
10 9 0
# ℹ 159,951 more rows
Nous séparons aléatoirement les données en un ensemble d’apprentissage (10 000 observations) et un ensemble de validation. Cette division permet d’évaluer la performance du modèle sur des données non vues.
ind_train <- sort(sample(seq_len(nrow(X)), 10000))
ind_valid <- setdiff(seq_len(nrow(X)), ind_train)Nous entraînons une régression logistique régularisée avec cv.glmnet du package glmnet [13]. L’option family="binomial" spécifie un problème de classification binaire et effectue automatiquement une validation croisée pour sélectionner le paramètre de régularisation optimal.
library(glmnet)
model <- cv.glmnet(
X[ind_train,], df_meta$polarity[ind_train], family="binomial"
)Nous générons des prédictions sur l’ensemble de validation avec predict et calculons le taux de précision global en comparant les prédictions aux vraies valeurs.
pred <- predict(model, newx=X[ind_valid,], type = "class")
mean(pred == df_meta$polarity[ind_valid])[1] 0.8867772
La fonction table crée une matrice de confusion qui détaille les vrais positifs, vrais négatifs, faux positifs et faux négatifs, offrant une vue plus complète de la performance du modèle.
table(pred=pred, y=df_meta$polarity[ind_valid]) y
pred 0 1
0 66131 8656
1 8323 66851
Nous extrayons les coefficients non nuls du modèle avec coef() pour identifier les lemmes les plus influents. Les coefficients positifs indiquent une association avec une classe, les négatifs avec l’autre.
cf <- coef(model, s = model$lambda[5])
cf <- cf[cf[,1] != 0,,drop=FALSE][-1,,drop=FALSE]
cf[order(cf[,1]),,drop=FALSE]9 x 1 sparse Matrix of class "dgCMatrix"
s=0.07176278
intérêt -0.116654918
rien -0.096866697
pas -0.036332424
mal -0.017120113
mauvais -0.007037554
ridicule -0.004267539
très 0.000301326
magnifique 0.224069435
excellent 0.374677833
Nous voyons plusieurs mots associés à chaque polarité.
16. LLM
Les grands modèles de langage (abrégé LLM, de l’anglais large language model) donnent aux agents conversationnels la capacité de comprendre et de répondre à des entrées en texte libre. Les LLM ne peuvent pas être exécutés directement dans R, mais il est possible d’y accéder au moyen d’une API. Une API permet d’utiliser un LLM à partir de divers logiciels, que ce soit localement ou via un service externe payant. Deux logiciels courants pour utiliser les LLM localement sont ollama et LM Studio.
Voici un exemple d’utilisation d’un LLM avec la fonction oai_chat, LM Studio et un modèle source ouvert de OpenAI. Nous pourrions utiliser un autre service en modifiant le paramètre base_url ou un autre modèle en modifiant le paramètre model.
msg <- "Combien de personnes vivent dans le 11e arrondissement de Paris ?"
res <- oai_chat(
base_url = "http://localhost:1234",
model = "openai/gpt-oss-20b",
msg = msg,
temperature = 0.7,
)
resEnviron **110 000 personnes** habitent le 11ᵉ arrondissement de Paris (données du recensement officiel d’INSEE, année 2019). Le chiffre exact varie légèrement selon les mises à jour annuelles (par exemple, l’estimation 2021 se situe autour de 112 k habitants), mais on peut dire qu’il y a un peu plus d’un centième de million de résidents dans ce quartier.
Les LLM ouvrent de nombreuses possibilités pour étendre l’analyse des données textuelles. Par exemple, au lieu d’entraîner un modèle supervisé à partir d’annotations humaines, nous pouvons utiliser un LLM pour générer des annotations à grande échelle. Essayons maintenant avec un sous-ensemble de nos données de critiques de films.
Vous trouverez ci-dessous le code qui demande à un modèle LLM d’évaluer la polarité, la certitude et la conviction d’un critiques. Le résultat est renvoyé au format JSON, couramment utilisé pour transmettre des données via une API.
msg <- c(
'Évaluer le degré de conviction, le degré de certitude et le caractère',
'positif de ce texte sur une échelle de 1 (peu convaincant/certain/positif)',
'à 10 (très convaincant/certain/positif). Indiquez votre évaluation dans une',
'forme pur JSON. Par exemple :',
'{"conviction": 4, "certain": 8, "positif": 2}.\n\n'
)
res <- oai_chat(
base_url = "http://localhost:1234/v1/chat/completions",
model = "openai/gpt-oss-20b",
msg = paste(msg, document, collapse=" "),
temperature = 0.7,
)
res{"conviction": 7, "certain": 8, "positif": 5}
Les données au format JSON peuvent être facilement analysées dans R à l’aide de la fonction fromJSON du package jsonlite [14].
library(jsonlite)
as_tibble(fromJSON(res))# A tibble: 1 × 3
conviction certain positif
<int> <int> <int>
1 7 8 5
Nous importons les résultats d’annotation produits par le LLM pour l’ensemble des documents. Ces annotations serviront de nouvelles étiquettes pour entraîner un modèle.
llm <- read_csv2("donnees/polarity_llm.csv.bz2")
llm# A tibble: 953 × 7
doc_id input polarity raw conviction certain positif
<dbl> <chr> <dbl> <chr> <dbl> <dbl> <dbl>
1 39 "Petit film amateur réalisé… 0 "{\"… 6 7 5
2 132 "Une comédie romantique bie… 1 "{\"… 7 8 9
3 618 "Le sujet est d'une sensibi… 1 "{\"… 9 9 10
4 720 "Structure narrative classi… 0 "{\"… 8 7 2
5 859 "Un film énigmatique dans s… 1 "{\"… 5 4 7
6 989 "Fortunata n'a pas la vie f… 0 "{\"… 6 8 2
7 1781 "j'ai vu les deux films et … 1 "{\"… 8 7 9
8 1817 "Très déçue. Comme trop sou… 0 "{\"… 8 7 2
9 1852 "Antonio Banderas s'investi… 0 "{\"… 9 9 1
10 1982 "Mais qu est ce que c est n… 0 "{\"… 6 8 2
# ℹ 943 more rows
La fonction table crée un tableau croisé comparant la polarité originale avec le score de positivité généré par le LLM, permettant d’évaluer la cohérence entre les deux mesures.
table(llm$polarity, llm$positif)
1 2 3 4 5 6 7 8 9 10
0 129 256 57 15 16 9 9 2 2 1
1 2 10 9 7 11 17 32 86 216 67
Nous entraînons un nouveau modèle glmnet en utilisant les annotations de polarité générées par le LLM comme variable cible. La matrice X est d’abord filtrée pour ne garder que les documents annotés.
X_nouv <- X[as.character(llm$doc_id),]
model <- cv.glmnet(X_nouv, llm$positif)Nous extrayons et trions les coefficients non nuls pour identifier quels lemmes sont les plus associés aux jugements de polarité du LLM, révélant potentiellement des patterns différents de ceux du modèle précédent.
cf <- coef(model, s = model$lambda[12])
cf <- cf[cf[,1] != 0,,drop=FALSE][-1,,drop=FALSE]
cf[order(cf[,1]),,drop=FALSE]9 x 1 sparse Matrix of class "dgCMatrix"
s=0.5341057
mal -0.30335297
mauvais -0.19382625
ne -0.18204717
pire -0.04267963
ridicule -0.02476939
très 0.04357302
magnifique 0.14690575
bel 0.20887087
excellent 1.05102845
Cette section démontre une stratégie hybride où un LLM sert d’annotateur automatique pour créer des données d’entraînement. En comparant les coefficients du modèle entraîné sur les annotations LLM avec ceux du modèle basé sur les annotations originales, nous pouvons identifier les différences dans les patterns linguistiques capturés par chaque approche. Cette méthode est particulièrement utile lorsque l’annotation humaine est coûteuse ou lorsque nous souhaitons explorer rapidement différentes dimensions d’analyse textuelle. Cependant, il est important de valider la qualité des annotations LLM et de comprendre que le modèle résultant reflète les biais et les capacités du LLM utilisé pour l’annotation.
17. Whisper
Whisper est un modèle de reconnaissance vocale développé par OpenAI capable de transcrire automatiquement de l’audio en texte avec un haut niveau de précision. Cette section montre comment utiliser l’API Whisper pour transcrire un fichier audio en français et obtenir non seulement le texte complet, mais aussi le temps précis de chaque mot. Cette capacité de segmentation temporelle est particulièrement utile pour l’analyse linguistique, l’alignement texte-audio, ou la création de sous-titres automatiques.
Nous envoyons un fichier audio MP3 à l’API Whisper avec la fonction oai_transcriptions. Les paramètres spécifient le modèle à utiliser (“whisper-1”) et la langue source (“fr”), et la granularité de temps (segments et mots individuels).
Sys.setenv(OPENAI_API_KEY = "AJOUTEZ_ICI")
trans <- oai_transcriptions(
file = "donnees/rhapsodie/mp3/Rhap-D0001.mp3",
model_name = "whisper-1",
language = "fr"
)
whisper_mots <- as_tibble(trans$words)
whisper_mots# A tibble: 905 × 3
word start end
<chr> <dbl> <dbl>
1 C 0 0.32
2 est 0.32 0.32
3 pas 0.32 1.52
4 mal 1.52 1.74
5 disons 1.74 2.2
6 est 2.4 2.46
7 ce 2.46 2.46
8 que 2.46 2.48
9 vous 2.48 2.66
10 pourriez 2.66 3.34
# ℹ 895 more rows
L’accès programmatique à l’API Whisper via R facilite l’intégration de la reconnaissance vocale dans des chaînes de traitement d’analyse de données plus larges, ouvrant la voie à l’étude systématique de grandes quantités de données audio.
18. Plongement de textes
Les plongements de textes (text embeddings) sont des représentations vectorielles denses qui capturent le sens sémantique des documents dans un espace de dimension réduite. Contrairement aux représentations sac-de-mots qui créent des vecteurs creux de haute dimension, les embeddings produits par des modèles comme OpenAI’s plongement de textes génèrent des vecteurs denses de quelques milliers de dimensions où des textes similaires en sens sont proches dans l’espace vectoriel. Cette section montre comment obtenir des embeddings via l’API OpenAI, les utiliser pour entraîner un modèle prédictif, et visualiser la structure sémantique des documents en deux dimensions avec UMAP.
Pour accéder aux plongements basées sur LLM, nous utilisons encore une API, cette fois avec la fonction oai_embeddings. Dans cet exemple, nous utiliserons un modèle propriétaire d’OpenAI, mais il est également possible de modifier l’URL pour utiliser un modèle local.
Sys.setenv(OPENAI_API_KEY = "AJOUTEZ_ICI")
res <- oai_embeddings(
input = "Que sais-je?",
model = "text-embedding-3-large"
)
dim(res)[1] 3072
Nous voyons que ce plongement de texte a une dimension de 3072. Notez que la dimension ne change pas en fonction de la taille du texte.
Nous chargeons un ensemble de données précalculées d’intégrations provenant d’un échantillon de critiques de films.
X <- read_rds("donnees/polarity_embed.rds")
dim(X)[1] 953 3072
Nous créons un ensemble d’entraînement réduit (700 observations) et un ensemble de validation avec sample. Les embeddings étant des représentations riches, même un petit ensemble d’entraînement peut produire de bons résultats.
ind_train <- sort(sample(seq_len(nrow(X)), 700))
ind_valid <- setdiff(seq_len(nrow(X)), ind_train)Nous entraînons une régression logistique avec régularisation ridge (alpha=0) via cv.glmnet. La régularisation L2 est particulièrement adaptée aux embeddings car elle pénalise uniformément toutes les dimensions sans en éliminer complètement.
model <- cv.glmnet(
X[ind_train,], llm$polarity[ind_train], family="binomial", alpha=0
)Nous générons des prédictions sur l’ensemble de validation avec predict et calculons la précision globale. Les embeddings capturant la sémantique profonde, la performance peut être élevée même avec peu de données d’entraînement.
pred <- predict(model, newx=X[ind_valid,], type = "class")
mean(pred == llm$polarity[ind_valid])[1] 0.9644269
La fonction table produit une matrice de confusion détaillant la répartition des prédictions correctes et incorrectes, permettant d’identifier d’éventuels biais du modèle.
table(pred=pred, y=llm$polarity[ind_valid]) y
pred 0 1
0 135 2
1 7 109
Nous appliquons UMAP avec umap pour projeter les embeddings de haute dimension en deux dimensions visualisables. Cette technique de réduction dimensionnelle préserve la structure locale et révèle les regroupements sémantiques dans les données.
obj <- umap(X)
llm$umap1 <- obj$layout[, 1]
llm$umap2 <- obj$layout[, 2]Nous créons un graphique de dispersion avec ggplot() montrant la projection UMAP colorée par le score de positivité. Cette visualisation révèle si les documents de positivité similaire forment des clusters distincts dans l’espace sémantique.
llm |>
mutate(positif = factor(positif)) |>
ggplot() +
geom_point(aes(x=umap1, y=umap2, colour = positif), size=6, alpha=0.4) +
scale_colour_viridis_d()Cette section illustre la puissance des embeddings de textes pour l’analyse et la classification. Comparés aux représentations sac-de-mots, les embeddings capturent des relations sémantiques complexes et permettent d’obtenir de bonnes performances avec moins de données d’entraînement. La visualisation UMAP révèle la structure sémantique sous-jacente des documents, montrant comment le modèle d’embedding organise les textes selon leur sens. Cette approche est particulièrement efficace pour des tâches nécessitant une compréhension nuancée du contenu textuel, et peut facilement être intégrée dans des chaînes de traitement d’apprentissage automatique plus complexes.
19. Alignement des textes
Lorsque nous analysons le même texte avec différents modèles ou outils d’annotation linguistique, il est essentiel de pouvoir comparer systématiquement leurs sorties. Cette section présente l’alignement de deux annotations du même texte produites par des modèles différents. En alignant les tokens correspondants, nous pouvons identifier les divergences dans les analyses (étiquettes morphosyntaxiques, lemmes, relations de dépendance) et évaluer la cohérence ou les différences entre les modèles. Cette approche est cruciale pour la validation de modèles, l’amélioration de chaînes de traitement d’analyse, ou la compréhension des forces et faiblesses de différents systèmes d’annotation.
Nous utilisons la fonction alignement_des_textes() pour apparier les tokens des deux annotations en se basant sur la colonne token. Le paramètre suffix_y = “_nouv” ajoute un suffixe aux colonnes de la seconde annotation pour les distinguer, créant ainsi un tibble combiné où chaque ligne contient les informations des deux modèles pour le même token.
df_comb <- alignement_des_textes(
df_tirade,
df_tirade_nouv,
col1 = token,
col2 = token,
suffix_y = "_nouv"
)
df_comb# A tibble: 613 × 30
id id_y doc_id paragraph_id sentence_id sentence token_id token lemma
<dbl> <dbl> <chr> <int> <int> <chr> <chr> <chr> <chr>
1 1 1 doc1 1 1 C'est tout ? 1 C' ce
2 2 2 doc1 1 1 C'est tout ? 2 est être
3 3 3 doc1 1 1 C'est tout ? 3 tout tout
4 4 4 doc1 1 1 C'est tout ? 4 ? ?
5 6 7 doc2 1 1 Ah ! non ! 2 ! !
6 7 8 doc2 1 1 Ah ! non ! 3 non non
7 8 9 doc2 1 1 Ah ! non ! 4 ! !
8 9 10 doc2 1 2 c'est un pe… 1 c' ce
9 10 11 doc2 1 2 c'est un pe… 2 est être
10 11 12 doc2 1 2 c'est un pe… 3 un un
# ℹ 603 more rows
# ℹ 21 more variables: upos <chr>, xpos <chr>, feats <chr>,
# head_token_id <chr>, dep_rel <chr>, deps <chr>, misc <chr>,
# doc_id_nouv <chr>, paragraph_id_nouv <int>, sentence_id_nouv <int>,
# sentence_nouv <chr>, token_id_nouv <chr>, token_nouv <chr>,
# lemma_nouv <chr>, upos_nouv <chr>, xpos_nouv <chr>, feats_nouv <chr>,
# head_token_id_nouv <chr>, dep_rel_nouv <chr>, deps_nouv <chr>, …
Nous filtrons les lignes où les étiquettes morphosyntaxiques diffèrent entre les deux modèles avec filter(upos != upos_nouv). La fonction select() permet de visualiser côte à côte les tokens, lemmes et étiquettes des deux annotations, révélant précisément où et comment les modèles divergent dans leur analyse.
df_comb |>
filter(upos != upos_nouv) |>
select(token, lemma, token_nouv, lemma_nouv, upos, upos_nouv)# A tibble: 63 × 6
token lemma token_nouv lemma_nouv upos upos_nouv
<chr> <chr> <chr> <chr> <chr> <chr>
1 Dieu Dieu Dieu Dieu PROPN ADV
2 bien bien bien bien ADV NOUN
3 boire boire boire boire NOUN VERB
4 Curieux curieux Curieux curieux ADJ ADV
5 oblongue oblongue oblongue oblongue ADJ NOUN
6 capsule capsule capsule capsule NOUN ADJ
7 Truculent Truculent Truculent Truculer ADJ VERB
8 Çà Çà Çà çà PROPN NOUN
9 sort sortir sort sort VERB PROPN
10 Prévenant prévenant Prévenant Prévenant PROPN ADV
# ℹ 53 more rows
Cette section démontre l’importance de l’alignement pour la comparaison systématique d’annotations linguistiques. En appariant les sorties de différents modèles, nous pouvons quantifier leur accord, identifier les cas problématiques, et mieux comprendre les choix d’annotation de chaque système. Cette approche est particulièrement utile pour l’évaluation de nouveaux modèles, le diagnostic d’erreurs, ou le choix du meilleur outil pour une tâche spécifique. L’alignement constitue ainsi une étape fondamentale dans tout workflow d’analyse textuelle rigoureux impliquant plusieurs sources d’annotation.
20. Conclusions
Ce parcours à travers diverses techniques de traitement et d’analyse des données textuelles et temporelles a montré la richesse des outils disponibles en R pour explorer des corpus linguistiques, qu’ils proviennent de frappes clavier, d’annotations phonétiques, de textes littéraires ou de grands ensembles issus du web. En partant de transformations simples de tableaux et de statistiques descriptives, nous avons progressivement étendu la palette d’analyses vers des modèles plus sophistiqués : fonctions de fenêtre pour décrire des dynamiques, expressions régulières pour extraire de l’information fine au sein des mots, annotation grammaticale automatique pour comprendre la structure des phrases, réduction de dimensionnalité pour visualiser des représentations sémantiques, et enfin modèles prédictifs pour relier des caractéristiques textuelles à des propriétés externes.
L’ensemble de ces approches montre comment des données très hétérogènes — temps d’appui sur les touches, phonèmes, traits morphologiques, vecteurs d’embedding ou encore fréquences lexicales — peuvent être articulées pour éclairer des questions linguistiques ou comportementales. Les exemples présentés ne constituent qu’un point de départ : ils ouvrent la voie à des analyses plus fines, des comparaisons plus larges et des modèles plus ambitieux. L’essentiel est de comprendre que chaque étape, du prétraitement aux modèles prédictifs, contribue à transformer des données brutes en connaissances interprétables. Ces techniques offrent ainsi un ensemble puissant et flexible pour explorer empiriquement la langue et les usages qu’en font les locuteurs.














