stephane09 commited on
Commit
6c8b83d
·
verified ·
1 Parent(s): 2f24bf5

Upload 41 files

Browse files
Files changed (42) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +43 -0
  3. app.R +639 -0
  4. cles_analyse_iramuteq.png +3 -0
  5. dictionnaires/lexique_fr.csv +0 -0
  6. gitattributes +43 -0
  7. gitignore +4 -0
  8. help/chd.md +101 -0
  9. help/chd_iramuteq.md +144 -0
  10. help/help.md +115 -0
  11. help/helpafc.md +61 -0
  12. help/helpchi2.md +28 -0
  13. help/ner.md +85 -0
  14. help/nettoyage.md +48 -0
  15. help/pos_spacy.md +82 -0
  16. help/rapport_audit_iramuteq_like.md +156 -0
  17. help/rapport_dendrogramme_iramuteq.md +22 -0
  18. help/rapport_mapping_chd_iramuteq_like.md +112 -0
  19. help/rapport_tokenisation_iramuteq_clone_v3.md +148 -0
  20. iramuteq-like/CHD.R +407 -0
  21. iramuteq-like/afc_helpers_iramuteq.R +39 -0
  22. iramuteq-like/afc_iramuteq.R +672 -0
  23. iramuteq-like/affichage_iramuteq-like.R +42 -0
  24. iramuteq-like/anacor.R +114 -0
  25. iramuteq-like/chd_afc_pipeline_iramuteq.R +316 -0
  26. iramuteq-like/chd_engine_iramuteq.R +117 -0
  27. iramuteq-like/chd_iramuteq.R +892 -0
  28. iramuteq-like/chdtxt.R +724 -0
  29. iramuteq-like/concordancier-iramuteq.R +182 -0
  30. iramuteq-like/cooccurrences_iramuteq.R +76 -0
  31. iramuteq-like/dendogramme_iramuteq.R +45 -0
  32. iramuteq-like/nettoyage_iramuteq.R +37 -0
  33. iramuteq-like/nlp_lexique_iramuteq.R +200 -0
  34. iramuteq-like/pipeline_iramuteq_analysis_iramuteq.R +33 -0
  35. iramuteq-like/pipeline_lexique_analysis_iramuteq.R +91 -0
  36. iramuteq-like/server_events_lancer_iramuteq.R +1133 -0
  37. iramuteq-like/stats_chd.R +136 -0
  38. iramuteq-like/textprepa_iramuteq.py +137 -0
  39. iramuteq-like/ui_options_iramuteq.R +36 -0
  40. iramuteq-like/wordcloud_iramuteq.R +69 -0
  41. penguins.csv +345 -0
  42. ui.R +421 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ cles_analyse_iramuteq.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: Dockerfile porte une partie du pipeline d'analyse Rainette.
2
+ # Ce script centralise une responsabilité métier/technique utilisée par l'application.
3
+
4
+ FROM rocker/r2u:22.04
5
+
6
+ ENV LANG=C.UTF-8
7
+ ENV LC_ALL=C.UTF-8
8
+ ENV DEBIAN_FRONTEND=noninteractive
9
+
10
+ # Python pour spaCy
11
+ RUN apt-get update && \
12
+ apt-get install -y --no-install-recommends \
13
+ ca-certificates \
14
+ python3 \
15
+ python3-pip \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Paquets R (r2u installe en binaires via apt/bspm, donc très rapide)
19
+ RUN install.r shiny quanteda wordcloud RColorBrewer igraph dplyr htmltools remotes irlba
20
+
21
+ # FactoMineR depuis GitHub (sans tirer les Suggests)
22
+ RUN R -q -e "options(repos=c(CRAN='https://cloud.r-project.org')); remotes::install_github('husson/FactoMineR', dependencies=NA, upgrade='never')"
23
+
24
+ # Utilisateur non-root compatible Hugging Face
25
+ RUN set -eux; \
26
+ if ! id -u user >/dev/null 2>&1; then \
27
+ if getent passwd 1000 >/dev/null 2>&1; then \
28
+ useradd -m -u 1001 user; \
29
+ else \
30
+ useradd -m -u 1000 user; \
31
+ fi; \
32
+ fi
33
+
34
+ ENV HOME=/home/user
35
+ WORKDIR /home/user/app
36
+
37
+ COPY . /home/user/app
38
+
39
+ RUN chown -R user:user /home/user/app
40
+
41
+ USER user
42
+ EXPOSE 7860
43
+ CMD ["Rscript", "/home/user/app/rainette/start.R"]
app.R ADDED
@@ -0,0 +1,639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: app.R porte une partie du pipeline d'analyse Rainette.
2
+ # Ce script centralise une responsabilité métier/technique utilisée par l'application.
3
+
4
+ ###############################################################################
5
+ # Script CHD - version beta 0.4 - 18-02-2026 #
6
+ # A partir d'un corpus texte formaté aux exigences IRAMUTEQ #
7
+ # Stéphane Meurisse #
8
+ # wwww.codeandcortex.fr #
9
+ # #
10
+ # 1.Réalise la CHD sur le corpus, sans rainette_explor #
11
+ # 2.Extrait chi2, lr, freq, docprop dans un CSV #
12
+ # 3.AFC #
13
+ # 4.Recherche de NER avec Spacy (md) #
14
+ # 5.Génère nuages de mots et graphes de cooccurrences par classe #
15
+ # 6.Exporte les segments de texte par classe au format text #
16
+ # 7.Creation d'un concordancier au format html #
17
+ # 8.Recherche de coocurrences #
18
+ ###############################################################################
19
+
20
+ library(shiny)
21
+ library(quanteda)
22
+ library(wordcloud)
23
+ library(RColorBrewer)
24
+ library(igraph)
25
+ library(dplyr)
26
+ library(htmltools)
27
+
28
+ options(shiny.maxRequestSize = 300 * 1024^2)
29
+ options(shinygadgets.viewer = shiny::browserViewer())
30
+ options(bspm.sudo = TRUE)
31
+
32
+ if (file.exists("help.md")) {
33
+ ui_aide_huggingface <- function() {
34
+ tagList(
35
+ tags$h2("Aide"),
36
+ includeMarkdown("help.md")
37
+ )
38
+ }
39
+ } else {
40
+ ui_aide_huggingface <- function() {
41
+ tagList(
42
+ tags$h2("Aide"),
43
+ tags$p("Le fichier help.md est introuvable. Ajoute help.md à la racine du projet.")
44
+ )
45
+ }
46
+ }
47
+
48
+
49
+ source("iramuteq-like/nettoyage_iramuteq.R", encoding = "UTF-8", local = TRUE)
50
+ source("iramuteq-like/concordancier-iramuteq.R", encoding = "UTF-8", local = TRUE)
51
+ source("spacy_ner/concordancier_ner.R", encoding = "UTF-8", local = TRUE)
52
+ source("iramuteq-like/afc_iramuteq.R", encoding = "UTF-8", local = TRUE)
53
+ source("iramuteq-like/ui_options_iramuteq.R", encoding = "UTF-8", local = TRUE)
54
+ source("iramuteq-like/affichage_iramuteq-like.R", encoding = "UTF-8", local = TRUE)
55
+ source("iramuteq-like/wordcloud_iramuteq.R", encoding = "UTF-8", local = TRUE)
56
+ source("ui.R", encoding = "UTF-8", local = TRUE)
57
+
58
+ source("iramuteq-like/chd_iramuteq.R", encoding = "UTF-8", local = TRUE)
59
+ source("iramuteq-like/dendogramme_iramuteq.R", encoding = "UTF-8", local = TRUE)
60
+ source("iramuteq-like/stats_chd.R", encoding = "UTF-8", local = TRUE)
61
+ source("iramuteq-like/chd_engine_iramuteq.R", encoding = "UTF-8", local = TRUE)
62
+
63
+ server <- function(input, output, session) {
64
+
65
+ rv <- reactiveValues(
66
+ logs = "",
67
+ statut = "En attente.",
68
+ progression = 0,
69
+
70
+ base_dir = NULL,
71
+ export_dir = NULL,
72
+ segments_file = NULL,
73
+ stats_file = NULL,
74
+ html_file = NULL,
75
+ ner_file = NULL,
76
+ zip_file = NULL,
77
+
78
+ res = NULL,
79
+ res_chd = NULL,
80
+ dfm_chd = NULL,
81
+ dfm = NULL,
82
+ filtered_corpus = NULL,
83
+ res_stats_df = NULL,
84
+ clusters = NULL,
85
+ max_n_groups = NULL,
86
+ max_n_groups_chd = NULL,
87
+
88
+ res_type = "simple",
89
+
90
+ exports_prefix = paste0("exports_", session$token),
91
+
92
+ spacy_tokens_df = NULL,
93
+ lexique_fr_df = NULL,
94
+ textes_indexation = NULL,
95
+
96
+ ner_df = NULL,
97
+ ner_nb_segments = NA_integer_,
98
+
99
+ afc_obj = NULL,
100
+ afc_erreur = NULL,
101
+
102
+ afc_vars_obj = NULL,
103
+ afc_vars_erreur = NULL,
104
+
105
+ afc_dir = NULL,
106
+ afc_table_mots = NULL,
107
+ afc_table_vars = NULL,
108
+ afc_plot_classes = NULL,
109
+ afc_plot_termes = NULL,
110
+ afc_plot_vars = NULL,
111
+
112
+ explor_assets = NULL,
113
+ stats_corpus_df = NULL,
114
+ stats_zipf_df = NULL
115
+ )
116
+
117
+ register_outputs_status(input, output, session, rv)
118
+
119
+ output$ui_afc_statut <- renderUI({
120
+ if (!is.null(rv$afc_erreur) && nzchar(rv$afc_erreur)) {
121
+ return(tags$p("AFC : erreur (voir ci-dessous)."))
122
+ }
123
+ if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) {
124
+ return(tags$p("AFC non calculée. Lance une analyse pour calculer l'AFC classes × termes."))
125
+ }
126
+ ncl <- nrow(rv$afc_obj$table)
127
+ nt <- ncol(rv$afc_obj$table)
128
+ tags$p(paste0("AFC calculée sur ", ncl, " classes et ", nt, " termes (table Classes × Termes)."))
129
+ })
130
+
131
+ output$ui_afc_erreurs <- renderUI({
132
+ messages <- Filter(
133
+ nzchar,
134
+ list(
135
+ rv$afc_erreur,
136
+ rv$afc_vars_erreur
137
+ )
138
+ )
139
+
140
+ if (length(messages) == 0) {
141
+ return(NULL)
142
+ }
143
+
144
+ tags$div(
145
+ style = "display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;",
146
+ lapply(messages, function(msg) {
147
+ tags$div(
148
+ style = "border: 1px solid #f5c2c7; background: #f8d7da; color: #842029; border-radius: 4px; padding: 10px; white-space: pre-wrap;",
149
+ msg
150
+ )
151
+ })
152
+ )
153
+ })
154
+
155
+ output$ui_spacy_langue_detection <- renderUI({
156
+ if (identical(input$source_dictionnaire, "lexique_fr")) {
157
+ return(NULL)
158
+ }
159
+
160
+ output$ui_ner_lexique_incompatibilite <- renderUI({
161
+ if (!isTRUE(input$activer_ner) || !identical(input$source_dictionnaire, "lexique_fr")) {
162
+ return(NULL)
163
+ }
164
+
165
+ output$ui_corpus_preview <- renderUI({
166
+ fichier <- input$fichier_corpus
167
+ if (is.null(fichier) || is.null(fichier$datapath) || !file.exists(fichier$datapath)) {
168
+ return(tags$p("Aucun corpus importé pour le moment."))
169
+ }
170
+
171
+ lignes <- tryCatch(
172
+ readLines(fichier$datapath, encoding = "UTF-8", warn = FALSE),
173
+ error = function(e) NULL
174
+ )
175
+
176
+ if (is.null(lignes) || length(lignes) == 0) {
177
+ return(tags$p("Le corpus importé est vide ou illisible."))
178
+ }
179
+
180
+ max_lignes <- 250
181
+ extrait <- lignes[seq_len(min(length(lignes), max_lignes))]
182
+ texte <- paste(extrait, collapse = "\n")
183
+
184
+ if (length(lignes) > max_lignes) {
185
+ texte <- paste0(
186
+ texte,
187
+ "\n\n… Aperçu limité aux ", max_lignes,
188
+ " premières lignes (", length(lignes), " lignes au total)."
189
+ )
190
+ }
191
+
192
+ tags$div(
193
+ tags$p(
194
+ style = "margin-bottom: 8px;",
195
+ paste0("Fichier : ", fichier$name)
196
+ ),
197
+ tags$pre(
198
+ style = "white-space: pre-wrap; max-height: 70vh; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background: #fafafa;",
199
+ texte
200
+ )
201
+ )
202
+ })
203
+
204
+ output$ui_table_stats_corpus <- renderUI({
205
+ req(rv$stats_corpus_df)
206
+
207
+ definitions <- c(
208
+ "Nom du corpus" = "Nom du fichier corpus importé.",
209
+ "Nombre de textes" = "Nombre d'unités de texte détectées dans le corpus.",
210
+ "Nombre de mots dans le corpus" = "Total des occurrences de mots (tokens).",
211
+ "Nombre de formes" = "Nombre de formes lexicales distinctes (types), différent des hapax.",
212
+ "Nombre de segments de texte" = "Nombre de segments après découpage pour l'analyse.",
213
+ "Nombre d'Hapax" = "Nombre de formes apparaissant une seule fois dans le corpus.",
214
+ "Loi de Zpif" = "Indicateur de conformité approximative à la loi de Zipf."
215
+ )
216
+
217
+ lignes <- lapply(seq_len(nrow(rv$stats_corpus_df)), function(i) {
218
+ metrique <- as.character(rv$stats_corpus_df$Metrique[i])
219
+ valeur <- as.character(rv$stats_corpus_df$Valeur[i])
220
+ definition <- unname(definitions[[metrique]])
221
+ if (is.null(definition) || !nzchar(definition)) definition <- ""
222
+
223
+ tags$tr(
224
+ tags$td(
225
+ tags$div(metrique),
226
+ if (nzchar(definition)) tags$div(
227
+ style = "font-size: 0.85em; color: #c62828; margin-top: 2px;",
228
+ definition
229
+ )
230
+ ),
231
+ tags$td(valeur)
232
+ )
233
+ })
234
+
235
+ tags$table(
236
+ class = "table table-striped table-condensed",
237
+ tags$thead(
238
+ tags$tr(
239
+ tags$th("Metrique"),
240
+ tags$th("Valeur")
241
+ )
242
+ ),
243
+ tags$tbody(lignes)
244
+ )
245
+ })
246
+
247
+
248
+ output$plot_stats_zipf <- renderPlot({
249
+ req(rv$stats_zipf_df)
250
+ df <- rv$stats_zipf_df
251
+ if (is.null(df) || nrow(df) < 2) {
252
+ plot.new()
253
+ text(0.5, 0.5, "Données insuffisantes pour tracer la loi de Zpif.", cex = 1.1)
254
+ return(invisible(NULL))
255
+ }
256
+
257
+ x_lim <- range(df$log_rang, na.rm = TRUE)
258
+ y_lim <- range(c(df$log_frequence, df$log_pred), na.rm = TRUE)
259
+
260
+ plot(
261
+ x = df$log_rang,
262
+ y = df$log_frequence,
263
+ pch = 16,
264
+ cex = 0.8,
265
+ col = grDevices::adjustcolor("#2C7FB8", alpha.f = 0.7),
266
+ xlab = "log(rang)",
267
+ ylab = "log(fréquence)",
268
+ main = "Loi de Zpif",
269
+ xlim = x_lim,
270
+ ylim = y_lim,
271
+ asp = 1
272
+ )
273
+ grid(col = "#E6E6E6", lty = "dotted")
274
+
275
+ ord <- order(df$log_rang)
276
+ lines(df$log_rang[ord], df$log_pred[ord], col = "#D7301F", lwd = 2.5)
277
+
278
+ legend(
279
+ "topright",
280
+ legend = c("Données", "Régression log-log"),
281
+ col = c("#2C7FB8", "#D7301F"),
282
+ pch = c(16, NA),
283
+ lty = c(NA, 1),
284
+ lwd = c(NA, 2),
285
+ bty = "n"
286
+ )
287
+ })
288
+
289
+ output$ui_chd_statut <- renderUI({
290
+ if (is.null(rv$res)) {
291
+ return(tags$p("CHD non disponible. Lance une analyse."))
292
+ }
293
+
294
+ nb_classes <- NA_integer_
295
+ if (!is.null(rv$clusters)) nb_classes <- length(rv$clusters)
296
+
297
+ if (identical(rv$res_type, "iramuteq")) {
298
+ return(tags$p(paste0("CHD disponible (moteur IRaMuTeQ-like) - classes détectées : ", nb_classes, ".")))
299
+ }
300
+
301
+ if (identical(rv$res_type, "double")) {
302
+ return(tags$p("CHD disponible (classification double rainette2)."))
303
+ }
304
+
305
+ tags$p(paste0("CHD disponible (classification simple rainette) - classes détectées : ", nb_classes, "."))
306
+ })
307
+
308
+ register_events_lancer(input, output, session, rv)
309
+ register_rainette_explor_affichage(input, output, session, rv)
310
+
311
+ output$plot_afc_classes <- renderPlot({
312
+ if (!is.null(rv$afc_erreur) && nzchar(rv$afc_erreur)) {
313
+ plot.new()
314
+ text(0.5, 0.5, "AFC indisponible (erreur).", cex = 1.1)
315
+ return(invisible(NULL))
316
+ }
317
+ if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) {
318
+ plot.new()
319
+ text(0.5, 0.5, "AFC non disponible. Lance une analyse.", cex = 1.1)
320
+ return(invisible(NULL))
321
+ }
322
+ tracer_afc_classes_seules(rv$afc_obj, axes = c(1, 2), cex_labels = 1.05)
323
+ })
324
+
325
+ panneaux <- lapply(classes, function(cl) {
326
+ output_id <- paste0("table_stats_chd_iramuteq_cl_", cl)
327
+
328
+ output[[output_id]] <- renderTable({
329
+ extraire_stats_chd_classe(
330
+ rv$res_stats_df,
331
+ classe = cl,
332
+ n_max = 100,
333
+ show_negative = FALSE,
334
+ max_p = if (isTRUE(input$filtrer_affichage_pvalue)) input$max_p else 1,
335
+ seuil_p_significativite = input$max_p,
336
+ style = "iramuteq_clone"
337
+ )
338
+ }, rownames = FALSE, sanitize.text.function = function(x) x)
339
+
340
+ tabPanel(
341
+ title = paste0("Classe ", cl),
342
+ tableOutput(output_id)
343
+ )
344
+ })
345
+
346
+ do.call(tabsetPanel, c(id = "tabs_stats_chd_iramuteq", panneaux))
347
+ })
348
+
349
+
350
+ if (is.null(rv$exports_prefix) || !nzchar(rv$exports_prefix)) {
351
+ return(tags$div(
352
+ style = "padding: 12px;",
353
+ tags$p("Préfixe de ressources invalide."),
354
+ tags$p("Relance l'analyse pour régénérer les exports.")
355
+ ))
356
+ }
357
+
358
+ if (!(rv$exports_prefix %in% names(shiny::resourcePaths()))) {
359
+ shiny::addResourcePath(rv$exports_prefix, rv$export_dir)
360
+ }
361
+
362
+ candidats_html <- c(
363
+ rv$html_file,
364
+ file.path(rv$export_dir, "segments_par_classe.html"),
365
+ file.path(rv$export_dir, "concordancier.html")
366
+ )
367
+ candidats_dyn <- list.files(
368
+ rv$export_dir,
369
+ pattern = "(segments.*classe|concord).*\\.html$",
370
+ ignore.case = TRUE,
371
+ full.names = TRUE
372
+ )
373
+ candidats_html <- c(candidats_html, candidats_dyn)
374
+ candidats_html <- unique(candidats_html[!is.na(candidats_html) & nzchar(candidats_html)])
375
+ html_existant <- candidats_html[file.exists(candidats_html)]
376
+
377
+ if (length(html_existant) == 0) {
378
+ return(tags$div(
379
+ style = "padding: 12px;",
380
+ tags$p("Le fichier du concordancier HTML n'est pas disponible pour cette analyse."),
381
+ tags$p("Relance l'analyse puis vérifie les logs si le problème persiste.")
382
+ ))
383
+ }
384
+
385
+ src_html <- html_existant[[1]]
386
+ nom_html <- basename(src_html)
387
+ src_dans_exports <- file.path(rv$export_dir, nom_html)
388
+
389
+ if (!isTRUE(file.exists(src_dans_exports))) {
390
+ ok_copy <- tryCatch(file.copy(src_html, src_dans_exports, overwrite = TRUE), error = function(e) FALSE)
391
+ if (isTRUE(ok_copy)) src_html <- src_dans_exports
392
+ } else {
393
+ src_html <- src_dans_exports
394
+ }
395
+
396
+ tags$iframe(
397
+ src = paste0("/", rv$exports_prefix, "/", basename(src_html)),
398
+ style = "width: 100%; height: 70vh; border: 1px solid #999;"
399
+ )
400
+ })
401
+
402
+ tags$div(
403
+ style = "text-align: center;",
404
+ tags$img(
405
+ src = paste0("/", rv$exports_prefix, "/", src_rel),
406
+ style = "max-width: 100%; height: auto; border: 1px solid #999; display: inline-block;"
407
+ )
408
+ )
409
+ })
410
+
411
+ output$table_stats_classe <- renderTable({
412
+ req(input$classe_viz, rv$res_stats_df)
413
+ classe_norm <- normaliser_id_classe_ui(input$classe_viz)
414
+ classe_stats <- if (is.na(classe_norm)) input$classe_viz else classe_norm
415
+
416
+ extraire_stats_chd_classe(
417
+ rv$res_stats_df,
418
+ classe = classe_stats,
419
+ n_max = 50,
420
+ max_p = if (isTRUE(input$filtrer_affichage_pvalue)) input$max_p else 1,
421
+ seuil_p_significativite = input$max_p,
422
+ style = "iramuteq_clone"
423
+ )
424
+ }, rownames = FALSE, sanitize.text.function = function(x) x)
425
+
426
+ output$plot_afc <- renderPlot({
427
+ if (!is.null(rv$afc_erreur) && nzchar(rv$afc_erreur)) {
428
+ plot.new()
429
+ text(0.5, 0.5, "AFC indisponible (erreur).", cex = 1.1)
430
+ return(invisible(NULL))
431
+ }
432
+ if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) {
433
+ plot.new()
434
+ text(0.5, 0.5, "AFC non disponible. Lance une analyse.", cex = 1.1)
435
+ return(invisible(NULL))
436
+ }
437
+
438
+ activer_repel <- TRUE
439
+ if (!is.null(input$afc_reduire_chevauchement)) activer_repel <- isTRUE(input$afc_reduire_chevauchement)
440
+
441
+ taille_sel <- "frequency"
442
+ if (!is.null(input$afc_taille_mots) && nzchar(as.character(input$afc_taille_mots))) {
443
+ taille_sel <- as.character(input$afc_taille_mots)
444
+ }
445
+ if (!taille_sel %in% c("frequency", "chi2")) taille_sel <- "frequency"
446
+
447
+ top_termes <- 120
448
+ if (!is.null(input$afc_top_termes) && is.finite(input$afc_top_termes)) top_termes <- as.integer(input$afc_top_termes)
449
+
450
+ tracer_afc_classes_termes(rv$afc_obj, axes = c(1, 2), top_termes = top_termes, taille_sel = taille_sel, activer_repel = activer_repel)
451
+ })
452
+
453
+ output$ui_table_afc_mots_par_classe <- renderUI({
454
+ if (is.null(rv$afc_table_mots)) {
455
+ output$table_afc_mots_message <- renderTable({
456
+ data.frame(Message = "AFC mots : non disponible.", stringsAsFactors = FALSE)
457
+ }, rownames = FALSE)
458
+ return(tableOutput("table_afc_mots_message"))
459
+ }
460
+
461
+ df <- rv$afc_table_mots
462
+ colonnes <- intersect(c("Terme", "Classe_max", "frequency", "chi2", "p_value", "Segment_texte"), names(df))
463
+ df <- df[, colonnes, drop = FALSE]
464
+ if ("p_value" %in% names(df)) {
465
+ df$p_value <- ifelse(
466
+ is.na(df$p_value),
467
+ NA_character_,
468
+ formatC(df$p_value, format = "f", digits = 6)
469
+ )
470
+ }
471
+
472
+ classes <- unique(as.character(df$Classe_max))
473
+ classes <- classes[!is.na(classes) & nzchar(classes)]
474
+ classes <- sort(classes)
475
+
476
+ if (length(classes) == 0) {
477
+ output$table_afc_mots_message <- renderTable({
478
+ data.frame(Message = "AFC mots : aucune classe disponible.", stringsAsFactors = FALSE)
479
+ }, rownames = FALSE)
480
+ return(tableOutput("table_afc_mots_message"))
481
+ }
482
+
483
+ ui_tables <- lapply(seq_along(classes), function(i) {
484
+ cl <- classes[[i]]
485
+ id <- paste0("table_afc_mots_", i)
486
+
487
+ output[[id]] <- renderUI({
488
+ sous_df <- df[df$Classe_max == cl, , drop = FALSE]
489
+ colonnes <- intersect(c("Terme", "frequency", "chi2", "p_value", "Segment_texte"), names(sous_df))
490
+ sous_df <- sous_df[, colonnes, drop = FALSE]
491
+
492
+ if ("p_value" %in% names(sous_df)) {
493
+ sous_df$p_value <- ifelse(
494
+ is.na(sous_df$p_value),
495
+ NA_character_,
496
+ formatC(sous_df$p_value, format = "f", digits = 6)
497
+ )
498
+ }
499
+
500
+ if ("chi2" %in% names(sous_df)) {
501
+ sous_df <- sous_df[order(-sous_df$chi2), , drop = FALSE]
502
+ sous_df$chi2 <- ifelse(
503
+ is.na(sous_df$chi2),
504
+ NA_character_,
505
+ formatC(sous_df$chi2, format = "f", digits = 6)
506
+ )
507
+ }
508
+
509
+ sous_df <- head(sous_df, 100)
510
+ generer_table_html_afc_mots(sous_df)
511
+ })
512
+
513
+ tagList(
514
+ tags$h5(cl),
515
+ uiOutput(id)
516
+ )
517
+ })
518
+
519
+ do.call(tagList, ui_tables)
520
+ })
521
+
522
+ output$plot_afc_vars <- renderPlot({
523
+ if (!is.null(rv$afc_vars_erreur) && nzchar(rv$afc_vars_erreur)) {
524
+ plot.new()
525
+ text(0.5, 0.5, "AFC variables étoilées indisponible (erreur).", cex = 1.1)
526
+ return(invisible(NULL))
527
+ }
528
+ if (is.null(rv$afc_vars_obj) || is.null(rv$afc_vars_obj$ca)) {
529
+ plot.new()
530
+ text(0.5, 0.5, "AFC variables étoilées non disponible. Lance une analyse.", cex = 1.1)
531
+ return(invisible(NULL))
532
+ }
533
+
534
+ activer_repel <- TRUE
535
+ if (!is.null(input$afc_reduire_chevauchement)) activer_repel <- isTRUE(input$afc_reduire_chevauchement)
536
+
537
+ top_mod <- 120
538
+ if (!is.null(input$afc_top_modalites) && is.finite(input$afc_top_modalites)) top_mod <- as.integer(input$afc_top_modalites)
539
+
540
+ tracer_afc_variables_etoilees(rv$afc_vars_obj, axes = c(1, 2), top_modalites = top_mod, activer_repel = activer_repel)
541
+ })
542
+
543
+ output$table_afc_vars <- renderTable({
544
+ if (is.null(rv$afc_table_vars)) {
545
+ return(data.frame(Message = "AFC variables étoilées : non disponible.", stringsAsFactors = FALSE))
546
+ }
547
+ df <- rv$afc_table_vars
548
+ colonnes <- intersect(c("Modalite", "Classe_max", "frequency", "chi2", "p_value"), names(df))
549
+ df <- df[, colonnes, drop = FALSE]
550
+ if ("p_value" %in% names(df)) {
551
+ p_values <- df$p_value
552
+ df$p_value <- ifelse(
553
+ is.na(p_values),
554
+ NA_character_,
555
+ ifelse(
556
+ p_values > 0.05,
557
+ sprintf("<span style='color:#d97706;font-weight:600;'>%s</span>", formatC(p_values, format = "f", digits = 6)),
558
+ formatC(p_values, format = "f", digits = 6)
559
+ )
560
+ )
561
+ }
562
+ if ("chi2" %in% names(df)) df <- df[order(-df$chi2), , drop = FALSE]
563
+ if ("chi2" %in% names(df)) {
564
+ df$chi2 <- ifelse(
565
+ is.na(df$chi2),
566
+ NA_character_,
567
+ formatC(df$chi2, format = "f", digits = 6)
568
+ )
569
+ }
570
+ head(df, 200)
571
+ }, rownames = FALSE, sanitize.text.function = function(x) x)
572
+
573
+ output$table_afc_eig <- renderTable({
574
+ if (!is.null(rv$afc_erreur) && nzchar(rv$afc_erreur)) {
575
+ return(data.frame(Message = "AFC indisponible (erreur).", stringsAsFactors = FALSE))
576
+ }
577
+ if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) {
578
+ return(data.frame(Message = "AFC non disponible.", stringsAsFactors = FALSE))
579
+ }
580
+ eig <- rv$afc_obj$ca$eig
581
+ if (is.null(eig)) return(data.frame(Message = "Valeurs propres indisponibles.", stringsAsFactors = FALSE))
582
+ df <- as.data.frame(eig)
583
+ df$Dim <- rownames(df)
584
+ rownames(df) <- NULL
585
+ df <- df[, c("Dim", names(df)[1], names(df)[2], names(df)[3]), drop = FALSE]
586
+ names(df) <- c("Dim", "Valeur_propre", "Pourcentage_inertie", "Pourcentage_cumule")
587
+ df
588
+ }, rownames = FALSE)
589
+
590
+ output$dl_segments <- downloadHandler(
591
+ filename = function() "segments_par_classe.txt",
592
+ content = function(file) {
593
+ req(rv$segments_file)
594
+ file.copy(rv$segments_file, file, overwrite = TRUE)
595
+ }
596
+ )
597
+
598
+ output$dl_stats <- downloadHandler(
599
+ filename = function() "stats_par_classe.csv",
600
+ content = function(file) {
601
+ req(rv$stats_file)
602
+ file.copy(rv$stats_file, file, overwrite = TRUE)
603
+ }
604
+ )
605
+
606
+ output$dl_html <- downloadHandler(
607
+ filename = function() "segments_par_classe.html",
608
+ content = function(file) {
609
+ req(rv$html_file)
610
+ file.copy(rv$html_file, file, overwrite = TRUE)
611
+ }
612
+ )
613
+
614
+ output$dl_zip <- downloadHandler(
615
+ filename = function() "exports_rainette.zip",
616
+ content = function(file) {
617
+ req(rv$zip_file)
618
+ file.copy(rv$zip_file, file, overwrite = TRUE)
619
+ }
620
+ )
621
+
622
+ output$dl_afc_zip <- downloadHandler(
623
+ filename = function() "afc_exports.zip",
624
+ content = function(file) {
625
+ req(rv$afc_dir)
626
+ zip_tmp <- tempfile(fileext = ".zip")
627
+ ancien <- getwd()
628
+ on.exit(setwd(ancien), add = TRUE)
629
+ setwd(dirname(rv$afc_dir))
630
+ if (file.exists(zip_tmp)) unlink(zip_tmp)
631
+ utils::zip(zipfile = zip_tmp, files = basename(rv$afc_dir))
632
+ file.copy(zip_tmp, file, overwrite = TRUE)
633
+ }
634
+ )
635
+
636
+ }
637
+
638
+ app <- shinyApp(ui = ui, server = server)
639
+ app
cles_analyse_iramuteq.png ADDED

Git LFS Details

  • SHA256: 41b2399f94ac0b428e45752498688db8a64fcc4ab4409f05606addaa87be95ff
  • Pointer size: 131 Bytes
  • Size of remote file: 262 kB
dictionnaires/lexique_fr.csv ADDED
The diff for this file is too large to render. See raw diff
 
gitattributes ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: gitattributes définit les règles Git appliquées au dépôt.
2
+ # Ce fichier contrôle le suivi des contenus et la normalisation associée.
3
+ # Il évite les écarts de versionnement et garantit une base de travail stable.
4
+ *.7z filter=lfs diff=lfs merge=lfs -text
5
+ *.arrow filter=lfs diff=lfs merge=lfs -text
6
+ *.bin filter=lfs diff=lfs merge=lfs -text
7
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
8
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
9
+ *.ftz filter=lfs diff=lfs merge=lfs -text
10
+ *.gz filter=lfs diff=lfs merge=lfs -text
11
+ *.h5 filter=lfs diff=lfs merge=lfs -text
12
+ *.joblib filter=lfs diff=lfs merge=lfs -text
13
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
14
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
15
+ *.model filter=lfs diff=lfs merge=lfs -text
16
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
17
+ *.npy filter=lfs diff=lfs merge=lfs -text
18
+ *.npz filter=lfs diff=lfs merge=lfs -text
19
+ *.onnx filter=lfs diff=lfs merge=lfs -text
20
+ *.ot filter=lfs diff=lfs merge=lfs -text
21
+ *.parquet filter=lfs diff=lfs merge=lfs -text
22
+ *.pb filter=lfs diff=lfs merge=lfs -text
23
+ *.pickle filter=lfs diff=lfs merge=lfs -text
24
+ *.pkl filter=lfs diff=lfs merge=lfs -text
25
+ *.pt filter=lfs diff=lfs merge=lfs -text
26
+ *.pth filter=lfs diff=lfs merge=lfs -text
27
+ *.rar filter=lfs diff=lfs merge=lfs -text
28
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
29
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
30
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
31
+ *.tflite filter=lfs diff=lfs merge=lfs -text
32
+ *.tgz filter=lfs diff=lfs merge=lfs -text
33
+ *.wasm filter=lfs diff=lfs merge=lfs -text
34
+ *.xz filter=lfs diff=lfs merge=lfs -text
35
+ *.zip filter=lfs diff=lfs merge=lfs -text
36
+ *.zst filter=lfs diff=lfs merge=lfs -text
37
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
38
+ packages/udpipe/data/brussels_listings.RData filter=lfs diff=lfs merge=lfs -text
39
+ packages/udpipe/data/brussels_reviews_anno.RData filter=lfs diff=lfs merge=lfs -text
40
+ packages/udpipe/data/brussels_reviews_w2v_embeddings_lemma_nl.RData filter=lfs diff=lfs merge=lfs -text
41
+ packages/udpipe/data/brussels_reviews.RData filter=lfs diff=lfs merge=lfs -text
42
+ packages/udpipe/inst/dummydata/toymodel.udpipe filter=lfs diff=lfs merge=lfs -text
43
+ packages/udpipe/french-gsd-ud-2.5-191206.udpipe filter=lfs diff=lfs merge=lfs -text
gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Rôle du fichier: gitignore définit les règles Git appliquées au dépôt.
2
+ # Ce fichier contrôle le suivi des contenus et la normalisation associée.
3
+ # Il évite les écarts de versionnement et garantit une base de travail stable.
4
+ .DS_Store
help/chd.md ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CHD — Clarification de `mincl` et de la sélection des classes terminales
2
+
3
+ ## 1) À quoi sert `mincl` ?
4
+
5
+ `mincl` est le **seuil minimal d'effectif d'une classe** (en pratique : nombre d'UCE/segments) utilisé au moment de la **sélection finale des classes terminales**.
6
+
7
+ - Si une classe terminale a un effectif `< mincl`, elle peut être écartée telle quelle.
8
+ - Dans la logique IRaMuTeQ, l'algorithme peut alors remonter dans l'arbre vers une classe mère pour garder une partition interprétable.
9
+
10
+ Autrement dit, `mincl` ne règle pas directement la segmentation du texte : il intervient surtout dans le **post-traitement de l'arbre CHD**.
11
+
12
+ ---
13
+
14
+ ## 2) Comment IRaMuTeQ applique `mincl` ?
15
+
16
+ Dans les scripts historiques d'IRaMuTeQ :
17
+
18
+ - `find.terminales(...)` conserve d'abord les classes terminales dont l'effectif est `>= mincl`.
19
+ - Pour les classes trop petites, il existe une logique de remontée vers la classe mère (via les liens mère/filles) avant de figer la solution.
20
+ - `make.classes(...)` reconstruit ensuite les classes finales et l'arbre filtré.
21
+
22
+ Cette étape explique pourquoi, à corpus identique, les classes finales peuvent différer d'une implémentation CHD qui n'a pas ce post-traitement.
23
+
24
+ ---
25
+
26
+ ## 3) Valeur "auto" de `mincl` dans IRaMuTeQ
27
+
28
+ ### 3.1 CHD texte (`Rchdtxt`)
29
+
30
+ Convention utilisée :
31
+
32
+ - `mincl = 0`
33
+ → mode automatique
34
+
35
+ Formule auto :
36
+
37
+ \[
38
+ mincl = round(nrow(classeuce1) / ind)
39
+ \]
40
+
41
+ avec :
42
+
43
+ - `ind = nbcl * 2` si `classif_mode == 0` (double classification)
44
+ - sinon `ind = nbcl`
45
+
46
+ et `nbcl = nbt + 1`.
47
+
48
+ ### 3.2 CHD questionnaire (`Rchdquest`)
49
+
50
+ Convention différente :
51
+
52
+ - `mincl = 2`
53
+ → mode automatique
54
+
55
+ Formule auto :
56
+
57
+ \[
58
+ mincl = round(nrow(classeuce1) / (nbt + 1))
59
+ \]
60
+
61
+ Puis un plancher est imposé :
62
+
63
+ - si `mincl < 3` alors `mincl = 3`.
64
+
65
+ ---
66
+
67
+ ## 4) Différence avec le script Rainette de ce projet
68
+
69
+ Dans ce projet, la classification est obtenue via `rainette(...)` / `rainette2(...)`, puis les groupes sont utilisés directement comme classes.
70
+
71
+ - Le paramètre `min_split_members` sert principalement à contraindre `k` (nombre de classes faisable).
72
+ - Ce n'est **pas** l'équivalent exact de `mincl` d'IRaMuTeQ.
73
+
74
+ Conséquence : sans post-traitement "classes terminales" à la manière IRaMuTeQ, le nombre et l'identité des classes finales peuvent varier.
75
+
76
+ ---
77
+
78
+ ## 5) Recommandation d'implémentation (optionnelle)
79
+
80
+ Pour rapprocher le comportement d'IRaMuTeQ sans casser l'existant :
81
+
82
+ 1. Ajouter un réglage `mode_mincl` :
83
+ - `manuel`
84
+ - `auto_iramuteq`
85
+ 2. Ajouter `mincl_manuel` (actif seulement en mode manuel).
86
+ 3. En mode `auto_iramuteq`, calculer `mincl` avec la formule texte ci-dessus.
87
+ 4. Appliquer ensuite un post-traitement de classes terminales (inspiré de `find.terminales`/`make.classes`).
88
+
89
+ Ainsi, l'utilisateur peut choisir entre :
90
+
91
+ - un mode Rainette "direct" (plus simple),
92
+ - un mode "IRa-like" (plus proche des sorties IRaMuTeQ).
93
+
94
+ ---
95
+
96
+ ## 6) Vocabulaire rapide
97
+
98
+ - **UCE / segment** : unité de texte classée.
99
+ - **Classe terminale** : classe feuille dans l'arbre CHD.
100
+ - **Classe mère / filles** : relation hiérarchique dans l'arbre de partition.
101
+ - **`mincl`** : effectif minimal exigé pour conserver une classe terminale telle quelle.
help/chd_iramuteq.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CHD IRaMuTeQ-like — fonctionnement et paramètres utilisateur
2
+
3
+ Ce document explique le fonctionnement du mode **CHD IRaMuTeQ-like** dans l’application, les paramètres disponibles, et l’interprétation des sorties (dendrogramme et tableaux de statistiques).
4
+
5
+ ---
6
+
7
+ ## 1) Objectif de la CHD IRaMuTeQ-like
8
+
9
+ La CHD (Classification Hiérarchique Descendante) cherche à partitionner les segments de texte en classes lexicalement homogènes et distinctes.
10
+
11
+ Dans ce mode IRaMuTeQ-like, l’algorithme suit la logique historique IRaMuTeQ :
12
+ - découpage hiérarchique en classes,
13
+ - sélection de classes terminales,
14
+ - calcul des termes caractéristiques par classe (chi2 signé, p-value, fréquence, proportion documentaire),
15
+ - visualisation via un dendrogramme de style phylogramme.
16
+
17
+ ---
18
+
19
+ ## 2) Pipeline simplifié
20
+
21
+ 1. **Préparation du DFM** (matrice documents/termes) à partir des segments.
22
+ 2. **Lancement du moteur CHD** avec un nombre de classes cible (paramètre `k`).
23
+ 3. **Reconstruction des classes terminales** avec un seuil `mincl` (auto ou manuel).
24
+ 4. **Calcul des statistiques par classe**:
25
+ - chi2 signé,
26
+ - p-value,
27
+ - fréquence (`frequency`),
28
+ - proportion documentaire (`docprop`),
29
+ - ratio de vraisemblance (`lr`).
30
+ 5. **Rendu visuel**:
31
+ - dendrogramme CHD IRaMuTeQ-like,
32
+ - tableaux de stats par classe,
33
+ - vues AFC (si activées).
34
+
35
+ ---
36
+
37
+ ## 3) Paramètres utilisateur (côté interface)
38
+
39
+ > Les noms affichés peuvent légèrement varier selon l’écran, mais la logique fonctionnelle est la suivante.
40
+
41
+ ### 3.1 Nombre de classes (`k`)
42
+ - **Libellé dans l’interface**: « Nombre de classes terminales de la phase 1 ».
43
+ - **Valeur par défaut**: `10`.
44
+ - **Rôle**: fixe le niveau de découpage attendu.
45
+ - **Effet**: plus `k` est élevé, plus la partition est fine (classes plus nombreuses, parfois moins stables).
46
+ - **Valeurs recommandées**:
47
+ - petits corpus: 3 à 6,
48
+ - corpus moyens/grands: 4 à 10.
49
+
50
+ ### 3.2 Mode de classification (`classif_mode`)
51
+ - **Valeurs**: `simple` ou `double`.
52
+ - **Rôle**:
53
+ - `simple`: classification standard,
54
+ - `double`: logique de classification croisée (plus stricte, selon configuration).
55
+
56
+ ### 3.3 Seuil minimal de classe (`mincl`) + mode auto/manuel (`mincl_mode`)
57
+ - **`mincl_mode = auto`**: le système calcule automatiquement un minimum d’effectif par classe.
58
+ - **`mincl_mode = manuel`**: l’utilisateur impose `mincl`.
59
+ - **Effet**:
60
+ - `mincl` plus grand = classes plus robustes, mais potentiellement moins nombreuses,
61
+ - `mincl` plus petit = classes plus fines, mais plus sensibles au bruit.
62
+
63
+ ### 3.4 Seuil de p-value (`max_p`)
64
+ - **Rôle**: filtre des termes dans certaines vues statistiques.
65
+ - **Exemples**:
66
+ - `0.05` pour les termes statistiquement marqués,
67
+ - `1` pour ne pas filtrer (vue exhaustive).
68
+
69
+ ### 3.5 Binarisation (`binariser`)
70
+ - **Rôle**: transforme les fréquences en présence/absence avant CHD.
71
+ - **Usage**: en général activée pour rester proche des pratiques historiques CHD lexicales.
72
+
73
+ ### 3.6 Méthode SVD (`svd_method`)
74
+ - **Valeurs**: `irlba`, `svdR`.
75
+ - **Rôle**: méthode de décomposition utilisée dans l’étape factorielle interne.
76
+ - **Interprétation des méthodes**:
77
+ - `irlba` : appelle `irlba::irlba(...)`, une SVD tronquée itérative adaptée aux matrices creuses/volumineuses ; souvent plus rapide et plus robuste en mémoire pour la CHD,
78
+ - `svdR` : appelle `svd(...)` de base R ; calcul exact complet, utile comme référence de reproductibilité mais potentiellement plus coûteux.
79
+ - **Par défaut**: `irlba`.
80
+
81
+ ### 3.7 Mode patate (`mode_patate`)
82
+ - **Rôle**: active/désactive l’étape de reclassement itératif des individus selon le moteur historique.
83
+ - **Effet**:
84
+ - désactivé (`FALSE`) = reclassement plus complet,
85
+ - activé (`TRUE`) = comportement simplifié/rapide.
86
+
87
+ ---
88
+
89
+ ## 4) Sorties et interprétation
90
+
91
+ ### 4.1 Dendrogramme CHD (IRaMuTeQ-like)
92
+ - Représentation en **phylogramme**:
93
+ - axe x: profondeur de partition,
94
+ - axe y: ordre des classes terminales.
95
+ - Les classes terminales peuvent être colorées différemment.
96
+ - Si la structure est incomplète (données trop pauvres ou sortie CHD invalide), un message explicite est affiché.
97
+
98
+ ### 4.2 Tableau statistique par classe
99
+ Colonnes principales:
100
+ - **Terme**: forme lexicale.
101
+ - **chi2**: chi2 signé (positif = sur-représenté dans la classe; négatif = sous-représenté).
102
+ - **p**: p-value associée.
103
+ - **frequency**: fréquence du terme dans la classe.
104
+ - **docprop**: proportion de segments de la classe contenant le terme.
105
+ - **lr**: ratio de vraisemblance (indicateur complémentaire).
106
+
107
+ ---
108
+
109
+ ## 5) Bonnes pratiques pour l’utilisateur
110
+
111
+ 1. Commencer avec un `k` modéré (4–6).
112
+ 2. Vérifier l’équilibre des classes (éviter des classes trop petites).
113
+ 3. Ajuster `mincl` si beaucoup de classes instables apparaissent.
114
+ 4. Utiliser `max_p = 0.05` pour une lecture interprétative, puis `max_p = 1` pour audit complet.
115
+
116
+ ---
117
+
118
+ ## 6) Dépannage rapide
119
+
120
+ ### Problème: pas de dendrogramme / message d’erreur
121
+ - Vérifier que le corpus contient suffisamment de segments et de vocabulaire.
122
+ - Réduire `k`.
123
+ - Revenir à `mincl_mode = auto`.
124
+
125
+ ### Problème: tableau de stats vide
126
+ - Vérifier les filtres (`max_p` trop strict).
127
+ - Vérifier que les classes reconstruites ne sont pas nulles.
128
+
129
+ ### Problème: résultats instables entre exécutions
130
+ - Éviter de changer simultanément plusieurs paramètres (`k`, `mincl`, `svd_method`).
131
+
132
+ ---
133
+
134
+ ## 7) Paramètres “utilisateur” recommandés (profil de départ)
135
+
136
+ - `k = 5`
137
+ - `classif_mode = simple`
138
+ - `mincl_mode = auto`
139
+ - `max_p = 0.05`
140
+ - `binariser = TRUE`
141
+ - `svd_method = irlba`
142
+ - `mode_patate = FALSE`
143
+
144
+ Ce profil donne généralement un bon compromis entre lisibilité des classes, stabilité et interprétabilité.
help/help.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [//]: # (Rôle du fichier: help.md documente une partie de l'application Rainette.)
2
+ [//]: # (Ce document sert de référence fonctionnelle/technique pour l'équipe.)
3
+ [//]: # (Il décrit le comportement attendu afin de sécuriser maintenance et diagnostics.)
4
+ ### codeandcortex.fr - Stéphane Meurisse - version beta 0.4 - 18-02-2026
5
+ - <a href="https://www.codeandcortex.fr" target="_blank" rel="noopener noreferrer">codeandcortex.fr</a>
6
+ - <a href="https://www.codeandcortex.fr/comprendre-chd-methode-reinert/" target="_blank" rel="noopener noreferrer">Comprendre la CHD</a>
7
+
8
+
9
+ ### IRaMuTeQ
10
+ IRaMuTeQ, développé par Pierre Ratinaud, est un logiciel libre devenu une référence pour l’analyse textuelle en sciences humaines et sociales. Il met en œuvre la méthode de Reinert (CHD), l’AFC, ainsi que l’analyse de similitudes de Vergès, et propose de nombreux traitements complémentaires pour explorer la structure lexicale d’un corpus. Un atout est son dictionnaire de lemmes, plus précis et performant que beaucoup d’alternatives, ce qui améliore la stabilité des classes. Depuis la version 0.4 vous avez le choix avec le dictionnaire NLP de spaCy et celui de **IRaMuTeQ - lexique_fr** (uniquement fr)
11
+ Ce qui change à partir de la version O.4 c'est l'utilisation du **dictionnaire** utilisé par **IRaMuTeQ** (uniquement fr). **Ce dictionnaire est plus précis que spaCy**.
12
+
13
+ - <a href="https://pratinaud.gitpages.huma-num.fr/iramuteq-website/" target="_blank" rel="noopener noreferrer">IRaMuTeQ</a>
14
+
15
+
16
+ ### Méthode Reinert - CHD
17
+
18
+ La méthode de Reinert est une approche statistique d’analyse lexicale conçue pour dégager des « mondes lexicaux » dans un corpus.
19
+ L’idée est de repérer des ensembles de segments de texte qui partagent des vocabulaires proches.
20
+
21
+ La CHD, pour "classification hiérarchique descendante", est l’algorithme de partitionnement associé à cette méthode.
22
+ Il procède par divisions successives : on prend l’ensemble des segments, puis on le coupe en deux groupes maximisant leur différenciation lexicale.
23
+ Ensuite, chaque groupe peut être à nouveau subdivisé, et ainsi de suite, jusqu’à obtenir un nombre de classes jugé pertinent ou une limite imposée par les paramètres.
24
+
25
+
26
+ ### Rainette développé par Julien Barnier
27
+
28
+ Rainette est un package R qui réalise une CHD selon la méthode Reinert.
29
+ - <a href="https://github.com/juba/rainette/blob/main/vignettes/introduction_usage.Rmd" target="_blank" rel="noopener noreferrer">Doc Rainette</a>
30
+ - <a href="https://cran.r-project.org/web/packages/rainette/vignettes/introduction_usage.html" target="_blank" rel="noopener noreferrer">Utilisation de rainette</a>
31
+ - <a href="https://juba.r-universe.dev/builds" target="_blank" rel="noopener noreferrer">Builds r-universe</a>
32
+
33
+
34
+ ### Pourquoi vos fichiers peuvent disparaître sur Hugging Face
35
+
36
+ Sur Hugging Face Spaces, le stockage local de ce conteneur est temporaire : si le serveur redémarre, ou si la page est rechargée après une déconnexion, les fichiers générés pendant une analyse précédente peuvent ne plus être disponibles.
37
+
38
+ Conseil : télécharge l’archive ZIP des exports juste après la fin de l’analyse.
39
+
40
+
41
+ # Logique générale de l’application
42
+
43
+ Uploadez un fichier texte au format IRaMuTeQ. L’app segmente, construit une matrice termes-documents (DTM), lance la CHD avec rainette, calcule les statistiques, génère un HTML surligné (concordancier), puis produit la CHD, AFC, NER, nuages de mots et réseaux de cooccurrences. L’onglet d’exploration (Explore_rainette) permet de visualiser la CHD.
44
+
45
+ ### Choix de la langue du dictionnaire spaCy
46
+
47
+ Vous avez le choix entre 4 langues spaCy préinstallées : français, anglais, espagnol et allemand (modèles "large", lg). D’autres langues peuvent être ajoutées ensuite selon les besoins. Il existe quatre tailles de modèles : "sm", "md", "lg" et "trf" (basé sur la technologie "transformer"). Le script détecte la cohérence entre le choix du dictionnaire et votre corpus importé, sur la base des stopwords.
48
+
49
+ ### Paramètres de l’analyse
50
+
51
+ - **segment_size** : taille des segments lors du découpage du corpus. Plus petit donne plus de segments, plus grand donne des segments plus longs.
52
+ - **k (nombre de classes)** : nombre de classes demandé pour la CHD.
53
+ - Nombre minimal de termes par segment : `min_segment_size` : Lors de la tokenisation et du calcul de la dtm, certaines formes (mots-outils, mots trop peu fréquents) ont été supprimées, les segments peuvent donc varier en taille.
54
+ Avec `min_segment_size = 10`, les segments comportant moins de 10 formes sont regroupés avec le segment suivant ou précédent du même document jusqu'à atteindre la taille minimale souhaitée.
55
+ - Effectif minimal pour scinder une classe : **min_split_members**. Nombre minimal de documents pour qu'une classe soit scindée en deux à l'étape suivante de la classification.
56
+ - Fréquence minimale des termes : `dfm_trim min_docfreq` : fréquence minimale en nombre de segments pour conserver un terme dans le DFM. Plus "haut" enlève les termes rares. Par exemple si vous `dfm_trim = 3` cela supprime de la matrice les termes apparaissant dans moins de 3 segments.
57
+ - **max_p (p-value)** : seuil de p-value pour filtrer les termes mis en avant dans les statistiques.
58
+ - **top_n (wordcloud)** : nombre de termes affichés dans chaque nuage de mots.
59
+ - **window (cooccurrences)** : taille de la fenêtre glissante pour calculer les cooccurrences.
60
+ - **top_feat (cooccurrences)** : nombre de termes retenus pour construire le réseau de cooccurrences.
61
+
62
+ ### Options de nettoyage du texte
63
+
64
+ Ces options agissent surtout sur la **préparation linguistique** (tokenisation, DFM, CHD, stats), pas sur l’affichage "brut" des segments.
65
+
66
+ - **Nettoyage caractères (regex)** (`nettoyage_caracteres`) : supprime les caractères non autorisés par la regex interne (ex : @).
67
+ - **Supprimer la ponctuation** (`supprimer_ponctuation`) : active `remove_punct` lors de la tokenisation quanteda. La ponctuation est retirée des tokens utilisés pour les analyses (CHD, stats).
68
+ - **Supprimer les chiffres (0-9)** (`supprimer_chiffres`) : supprime les chiffres avant tokenisation.
69
+ - **Traiter les élisions FR** (`supprimer_apostrophes`) : enlève les élisions en début de mot (`c'`, `j'`, `l'`, `m'`, `n'`, `s'`, `t'`, `d'`, `qu'`) pour ramener par ex. `c'est` vers `est`.
70
+ - **Forcer en minuscules avant analyse** (`forcer_minuscules_avant`) : convertit le texte en minuscules avant la construction des tokens/termes.
71
+
72
+ #### Stopwords en mode IRaMuTeQ-like
73
+
74
+ - En mode **IRaMuTeQ-like**, la source de lemmatisation est forcée sur **Lexique (fr)**.
75
+ - Donc, quand l'option **Retirer les stopwords** est activée, le filtrage se fait avec les stopwords **français de quanteda** (et non avec spaCy).
76
+ - Le filtrage stopwords via **spaCy** n'est utilisé que lorsque la source de dictionnaire est **spaCy** (mode Rainette).
77
+
78
+ #### Effet sur le concordancier HTML
79
+
80
+ - Quand **Supprimer la ponctuation** est cochée, la ponctuation est bien retirée dans les **données d’analyse**.
81
+ - Le **concordancier HTML** continue d’afficher les segments issus du corpus, donc vous pouvez encore voir de la ponctuation dans le texte affiché.
82
+
83
+ ### Classification double (rainette2)
84
+
85
+ - **Classification double** : l’application combine deux classifications rainette (res1 et res2) via rainette2, puis découpe l’arbre final avec k.
86
+
87
+ ### Lemmatisation (option)
88
+
89
+ - **Lemmatisation** : si activée, le texte est **lemmatisé avec Spacy ou le dictionnaire de lemme provenant du logiciel IRaMuTeQ - lexique_fr**. La lemmatisation semble (beaucoup) plus efficace avec le dictionnaire IRaMuTeQ provenant de **OpenLexicon (modifié)**.
90
+
91
+ - <a href="https://openlexicon.fr/" target="_blank" rel="noopener noreferrer">OpenLexicon</a>
92
+
93
+ ### Filtrage Morphosyntaxique
94
+ - **Tokens à conserver** : filtre les tokens conservés selon leur catégorie grammaticale (ex : NOUN, ADJ, VERB, PROPN, ADV...).
95
+
96
+ ### Paramètres SpaCy/NER
97
+ - Activer NER (spaCy) => Détections des entités nommées (NER) par spaCy (ex : "Paris" = "LOC"). Le modele spaCy "md" est un peu léger... pour cette tâche.
98
+
99
+ ### Exploration "Explore_rainette"
100
+
101
+ - **Classe** : sélection de la classe pour afficher les images et la table de statistiques associées.
102
+ - **CHD** : affichage graphique de la CHD.
103
+ - **Type** : bar (barres) ou cloud (nuage) pour l’affichage des termes par classe.
104
+ - **Statistiques** : chi2, lr, frequency, selon le critère utilisé pour classer les termes.
105
+ - Dans les exports CSV de type (`measure = "chi2"`), les colonnes suivantes sont importantes :
106
+ - **`n_target`** : nombre d’occurrences du terme dans la classe/cluster analysé.
107
+ - **`n_reference`** : nombre d’occurrences du même terme dans (tout) le corpus de référence (le reste des classes).
108
+ - **`chi2`** et **`p`** : test d’association entre cible et référence ; plus `chi2` est élevé et `p` petite, plus le terme est spécifiquement lié à la classe.
109
+ - **Nombre de termes** : nombre de termes affichés par classe dans la visualisation.
110
+ - **Afficher les valeurs négatives** : inclut les termes négativement associés à une classe.
111
+
112
+ ### Démarrage
113
+
114
+ - L’application ne fait plus de mise à jour automatique de `rainette` au lancement.
115
+ - Si vous voyez un ancien message `AUTO_UPDATE_RAINETTE=true -> tentative de mise à jour`, vérifiez que l’image/conteneur a bien été reconstruit avec la dernière version du dépôt.
help/helpafc.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [//]: # (Rôle du fichier: helpafc.md documente une partie de l'application Rainette.)
2
+ [//]: # (Ce document sert de référence fonctionnelle/technique pour l'équipe.)
3
+ [//]: # (Il décrit le comportement attendu afin de sécuriser maintenance et diagnostics.)
4
+ ## Aide AFC : calcul, affichage des termes, rôle de `top_termes`, calcul du `residu de Pearson`
5
+
6
+ ### 1) Comment l’AFC est calculée dans le script
7
+
8
+ L’AFC classes × termes est calculée en 3 étapes :
9
+
10
+ 1. Construction de la table de contingence **Classes × Termes** depuis le DFM.
11
+ 2. Exécution de l’AFC avec `FactoMineR::CA(tab, graph = FALSE)`.
12
+ 3. Récupération des coordonnées des classes (`rowcoord`) et des termes (`colcoord`) pour le tracé.
13
+
14
+ ### 2) Qu’est-ce que `top_termes` ?
15
+
16
+ `top_termes` est **une limite d’affichage graphique** des mots sur le plan AFC. Par défaut : `top_termes = 120`
17
+
18
+ ### 3) Sur quoi `top_termes` filtre ?
19
+
20
+ Le filtrage est fait dans la fonction de tracé des termes :
21
+
22
+ 1. on part de `termes_stats`,
23
+ 2. on enlève les termes vides,
24
+ 3. on trie par `frequency` décroissante,
25
+ 4. on garde les `top_termes` premiers.
26
+
27
+ Pourquoi top_termes est en fréquence et pas en p-value ?
28
+
29
+ Parce que top_termes est une contrainte de lisibilité graphique.
30
+ Le rôle de top_termes est de limiter le nombre de labels affichés dans `tracer_afc_classes_termes`, sinon le plot devient illisible (chevauchements).
31
+ Le code trie les termes par `frequency` qui détermine ensuite le `top_termes`.
32
+ En amont, dans le pipeline serveur, on construit termes_signif avec `p <= input$max_p`, puis on passe ces termes à `executer_afc_classes`, `termes_cibles` = termes_significatifs). **Donc la p-value réduit le périmètre des termes de l'AFC**.
33
+
34
+ ### 4) Le CSV contient-il seulement `top_termes` ?
35
+
36
+ Le CSV `stats_termes.csv` exporte la table `rv$afc_obj$termes_stats` (jeu complet de stats AFC disponible), sans appliquer la réduction `top_termes`.
37
+
38
+ ### 5) Note sur l'afc chi2/résidu de Pearson
39
+
40
+ - Les **positions AFC** viennent de `FactoMineR::CA`.
41
+ - Les **résidus/chi2** sont des statistiques d’association utilisés pour l’interprétation.
42
+
43
+ ### 6) Comment le résidu de Pearson est calculé
44
+
45
+ Pour chaque mot et chaque classe :
46
+
47
+ - On regarde combien de fois le mot apparaît réellement dans cette classe.
48
+ - On calcule ensuite combien on aurait “normalement” attendu pour ce mot dans cette classe, si la répartition était neutre.
49
+ - On compare les deux : réel vs attendu, et transforme cet écart en une valeur (le résidu).
50
+
51
+ Le résidu ne sert pas à recalculer l’AFC.
52
+
53
+ - Le code extrait, pour chaque mot, la classe où la surreprésentation est la plus forte et sa valeur (`resid_max`).
54
+ - `Classe_max` = la classe où le mot apparaît le plus en quantité brute (le plus d’occurrences observées).
55
+ - `resid_max` = la valeur qui mesure à quel point ce mot est plus (ou moins) présent que prévu dans la classe (sur/sous-représentation)
56
+
57
+ ### 7) Résidu positif / résidu négatif
58
+
59
+ - Résidu positif => le mot est plus présent que prévu dans cette classe (**surreprésenté**).
60
+ - Résidu négatif => le mot est moins présent que prévu dans cette classe (**sous-représenté**).
61
+ - Résidu proche de 0 => présence “normale”, pas d’écart fort.
help/helpchi2.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [//]: # (Rôle du fichier: helpchi2.md documente une partie de l'application Rainette.)
2
+ [//]: # (Ce document sert de référence fonctionnelle/technique pour l'équipe.)
3
+ [//]: # (Il décrit le comportement attendu afin de sécuriser maintenance et diagnostics.)
4
+ ## Aide : interprétation du chi2 et de `Classe_max`
5
+
6
+ Le code calcule un chi2 global par terme sur l’ensemble des classes :
7
+
8
+ \[
9
+ \chi_j^2 = \sum \frac{(O - E)^2}{E}
10
+ \]
11
+
12
+ puis une p-value.
13
+
14
+ En plus, il calcule les résidus standardisés cellule par cellule :
15
+
16
+ \[
17
+ \frac{O - E}{E}
18
+ \]
19
+
20
+ - `Classe_max` = la classe où ce résidu est le plus grand pour le terme considéré (`which.max`).
21
+ - C’est la classe de surreprésentation la plus forte pour aider la lecture des résultats.
22
+ - Les classes elles-mêmes viennent du regroupement (`dfm_group`) et sont renommées « Classe X » pour affichage.
23
+ - L’interface confirme : couleur des mots selon la classe de plus forte surreprésentation (résidus), taille selon fréquence ou chi2.
24
+
25
+ ## En une phrase
26
+
27
+ - `chi2` = « est-ce que la distribution du terme varie globalement selon les classes ? »
28
+ - `Classe_max` = « si oui, dans quelle classe l’excès est le plus marqué ? »
help/ner.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Named Entity Recognition NER (spaCy + règles JSON)
2
+
3
+ ## Fonctionnement
4
+ 1. spaCy détecte des entités (PER, ORG, LOC, etc...) mais le résultat nécessite bien souvent des corrections
5
+ 2. Dans le script un mini-filtrage a été ajouté pour supprimer des faux positifs (ponctuation seule, cas bruités, etc...)
6
+ 3. Vous pouvez ajouter un fichier au format **.json**, ses règles seront appliquées : exclusions et ajouts
7
+
8
+
9
+ ![Import NER](import_ner.png)
10
+
11
+
12
+ ## Format attendu du fichier JSON
13
+ - Le fichier doit être au **format `.json`**.
14
+ Exemple totalement farfellu montrant que vous pouvze exclure et inclure des mots, créer un nouveau label
15
+
16
+ ```json
17
+ {
18
+ "exclude_texts": ["ça", "«", "»"],
19
+ "exclude_labels": ["MISC"],
20
+ "include": [
21
+ {"text": "OpenAI", "label": "ORG"},
22
+ {"text": "ChatGPT", "label": "PRODUCT"},
23
+ {"text": "regarder", "label": "VERBE"},
24
+ {"text": "commencer", "label": "VERBE"}
25
+ ]
26
+ }
27
+ ```
28
+
29
+
30
+ ## Peut-on créer ses propres labels ?
31
+ Oui. Il faut écrire les **LABELS en MAJUSCULES**
32
+
33
+ - Les entités détectées *nativement* par spaCy gardent les labels du modèle (`PER`, `ORG`, `LOC`, etc.).
34
+ - Les entités ajoutées via `include` peuvent utiliser **n'importe quel label** (ex: `VOTRE_LABEL_1`, `VOTRE_LABEL_2`,...).
35
+ - Ces labels personnalisés apparaissent ensuite dans la sortie NER (`ent_label`).
36
+
37
+ Exemple: `{"text": "commencer", "label": "ACTION"}` forcera la présence de `commencer` avec le label `ACTION` si le mot est trouvé dans le texte.
38
+
39
+ ## Labels spaCy déjà existants
40
+ Les labels disponibles dépendent du **modèle spaCy chargé**.
41
+
42
+ ### Labels du modèle FR utilisé dans ce projet (`fr_core_news_md`)
43
+ - `PER` : personne
44
+ - `ORG` : organisation
45
+ - `LOC` : lieu
46
+ - `MISC` : catégorie diverse (autres entités)
47
+
48
+ ### Labels NER officiels spaCy (Si je ne me trompe pas avec des modeles "lg" on bénéficie de catégories étendus)
49
+ - `PERSON`: People, including fictional.
50
+ - `NORP`: Nationalities or religious or political groups.
51
+ - `FAC`: Buildings, airports, highways, bridges, etc.
52
+ - `ORG`: Companies, agencies, institutions, etc.
53
+ - `GPE`: Countries, cities, states.
54
+ - `LOC`: Non-GPE locations, mountain ranges, bodies of water.
55
+ - `PRODUCT`: Objects, vehicles, foods, etc. (Not services.)
56
+ - `EVENT`: Named hurricanes, battles, wars, sports events, etc.
57
+ - `WORK_OF_ART`: Titles of books, songs, etc.
58
+ - `LAW`: Named documents made into laws.
59
+ - `LANGUAGE`: Any named language.
60
+ - `DATE`: Absolute or relative dates or periods.
61
+ - `TIME`: Times smaller than a day.
62
+ - `PERCENT`: Percentage, including ”%“.
63
+ - `MONEY`: Monetary values, including unit.
64
+ - `QUANTITY`: Measurements, as of weight or distance.
65
+ - `ORDINAL`: “first”, “second”, etc.
66
+ - `CARDINAL`: Numerals that do not fall under another type.
67
+
68
+ ## Signification des champs JSON
69
+ - `exclude_texts` : liste de textes d'entité à **rejeter** (insensible à la casse).
70
+ - `exclude_labels` : liste de labels d'entité à **rejeter** (ex. `MISC`).
71
+ - `include` : liste d'entités à **forcer**.
72
+ - `text` : texte recherché dans le document.
73
+ - `label` : label assigné à l'entité ajoutée.
74
+
75
+ ## Expressions utilisées (important)
76
+ Pour `include`, le script utilise une regex Python de la forme :
77
+
78
+ - `\b<text>\b` avec `re.IGNORECASE`.
79
+
80
+ Cela veut dire :
81
+ - recherche **insensible à la casse** ;
82
+ - correspondance sur des **bornes de mot** (`\b`) ;
83
+ - évite de matcher au milieu d'un mot.
84
+
85
+ Exemple : `"text": "Paris"` matche `Paris` mais pas `parisien`.
help/nettoyage.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Audit des options de nettoyage (spaCy vs `lexique_fr`)
2
+
3
+ Contexte analysé : pipeline Rainette (`R/server_events_lancer.R`, `nettoyage.R`, `R/pipeline_spacy_analysis.R`, `R/pipeline_lexique_analysis.R`, `R/nlp_spacy.R`, `rainette/spacy_preprocess.py`, `R/chd_afc_pipeline.R`, `R/nlp_language.R`).
4
+
5
+ ## Résumé rapide
6
+
7
+ - Toutes les options UI sont bien **lues** par le pipeline.
8
+ - Mais en mode **spaCy**, certaines options sont **redondantes** ou **sans effet réel** sur la sortie finale.
9
+ - Ton log debug (`Diagnostic pipeline: ...`) est normal : il n'affiche qu'un sous-ensemble des options.
10
+
11
+ ## Vérification option par option
12
+
13
+ | Option UI | `lexique_fr` | `spaCy` | Verdict |
14
+ |---|---|---|---|
15
+ | `nettoyage_caracteres` | Appliquée avant pipeline via `appliquer_nettoyage_et_minuscules()` | Appliquée avant pipeline via `appliquer_nettoyage_et_minuscules()` | ✅ OK des deux côtés |
16
+ | `forcer_minuscules_avant` | Appliquée dans `appliquer_nettoyage_et_minuscules()` | Appliquée dans `appliquer_nettoyage_et_minuscules()` + passée à Python (`--lower_input`) | ⚠️ Redondante en spaCy |
17
+ | `supprimer_chiffres` | Appliquée dans `appliquer_nettoyage_et_minuscules()` + `tokens(..., remove_numbers=TRUE/FALSE)` | Appliquée dans `appliquer_nettoyage_et_minuscules()` + spaCy Python (`--remove_numbers`) + `tokens(..., remove_numbers=...)` | ⚠️ Très redondante en spaCy |
18
+ | `supprimer_apostrophes` | Appliquée dans `appliquer_nettoyage_et_minuscules()` | Appliquée dans `appliquer_nettoyage_et_minuscules()` + spaCy Python (`--strip_fr_elisions`) | ⚠️ Redondante en spaCy |
19
+ | `supprimer_ponctuation` | Appliquée à la tokenisation quanteda (`tokens(remove_punct=...)`) | **Déjà supprimée** dans `rainette/spacy_preprocess.py` (`tok.is_punct` ignoré), puis retokenisation quanteda | ⚠️ En spaCy, case quasi sans effet |
20
+ | `retirer_stopwords` | Oui (`obtenir_stopwords_analyse(..., source_dictionnaire='lexique_fr')` → quanteda FR) | Oui (`source_dictionnaire='spacy'` → stopwords spaCy) | ✅ OK des deux côtés |
21
+ | `filtrage_morpho` | Oui via `filtrer_textes_lexique_par_cgram()` (`c_morpho`) | Oui via `--pos_keep` et filtrage POS dans Python | ✅ OK des deux côtés |
22
+
23
+ ## Point important pour ton test (`lexique_fr` + pas de case minuscules)
24
+
25
+ Même si tu ne coches pas « Passage en minuscule », la chaîne de traitement fait ensuite un `tokens_tolower(...)` lors de la construction du DFM (avec ou sans stopwords). Donc la représentation finale utilisée pour l'analyse passe en minuscules dans tous les cas.
26
+
27
+ ➡️ En clair : pour l'analyse statistique, l'absence de coche « minuscules » n'empêche pas une normalisation en minuscules plus tard.
28
+
29
+ ## Pourquoi ton log debug ne montre pas tout
30
+
31
+ La ligne :
32
+
33
+ `Diagnostic pipeline: dictionnaire=... | langue UI=... | filtrage_morpho=... | retirer_stopwords=...`
34
+
35
+ est volontairement courte et n'inclut pas `forcer_minuscules_avant`, `supprimer_ponctuation`, `supprimer_chiffres`, `supprimer_apostrophes`, `nettoyage_caracteres`.
36
+
37
+ ## Conclusion
38
+
39
+ - ✅ **Oui**, les options principales sont prises en compte pour `lexique_fr` et spaCy.
40
+ - ⚠️ **Non**, elles ne sont pas toutes discriminantes en spaCy (certaines sont doublées, et `supprimer_ponctuation` n'a presque pas d'effet car spaCy retire déjà la ponctuation en amont).
41
+ - ⚠️ La case « Passage en minuscule » n'est pas un indicateur fiable du casing final du DFM, car le pipeline applique `tokens_tolower(...)` ensuite.
42
+
43
+ ## Recommandations (si tu veux un comportement plus lisible)
44
+
45
+ 1. Ajouter dans le log debug l'état de toutes les cases de nettoyage.
46
+ 2. En mode spaCy, clarifier dans l'UI que la ponctuation est déjà retirée côté Python.
47
+ 3. Décider d'un **seul** niveau pour la mise en minuscules (avant spaCy OU juste avant DFM), pour éviter les ambiguïtés de debug.
48
+ 4. Éviter les doubles suppressions chiffres/apostrophes (R + Python) sauf besoin explicite.
help/pos_spacy.md ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [//]: # (Rôle du fichier: pos_spacy.md documente une partie de l'application Rainette.)
2
+ [//]: # (Ce document sert de référence fonctionnelle/technique pour l'équipe.)
3
+ [//]: # (Il décrit le comportement attendu afin de sécuriser maintenance et diagnostics.)
4
+ ### Analyse morphosyntaxique avec spaCy et Lexique (fr)
5
+
6
+ - Documentation principale spaCy : <https://spacy.io/usage>
7
+ - Linguistic Features (POS, morphology) : <a href="https://spacy.io/usage/linguistic-feature/" target="_blank" rel="noopener noreferrer">Lexique POS spaCy</a>
8
+ - Documentation OpenLexicon : <a href="https://openlexicon.fr/" target="_blank" rel="noopener noreferrer">OpenLexicon</a>
9
+
10
+ ### Traduction FR des POS (spaCy)
11
+
12
+ - **ADJ** : adjectif
13
+ - **ADP** : adposition (préposition)
14
+ - **ADV** : adverbe
15
+ - **AUX** : auxiliaire
16
+ - **CCONJ** : conjonction de coordination
17
+ - **DET** : déterminant
18
+ - **INTJ** : interjection
19
+ - **NOUN** : nom
20
+ - **NUM** : numéral
21
+ - **PART** : particule
22
+ - **PRON** : pronom
23
+ - **PROPN** : nom propre
24
+ - **PUNCT** : ponctuation
25
+ - **SCONJ** : conjonction de subordination
26
+ - **SYM** : symbole
27
+ - **VERB** : verbe
28
+ - **X** : autre / catégorie inconnue
29
+
30
+ ### Filtrage morphosyntaxique spécifique lexique_fr
31
+
32
+ Le dictionnaire **lexique_fr** utilisé ici est celui d’**IRaMuTeQ**, et il semble lui-même issu d’**OpenLexicon**.
33
+
34
+ > Contrairement au logiciel IRaMuTeQ (où les niveaux sont généralement interprétés comme `1 = active` et `2 = supplémentaire`), le filtrage proposé ici est **binaire** et configurable de plusieurs façons.
35
+
36
+ ![Exemple : clés d'analyse logiciel IRaMuTeQ](cles_analyse_iramuteq.png)
37
+
38
+ Deux configurations principales dans l'interface sont possibles :
39
+ 1. Si vous **ne cochez pas** le filtrage morphosyntaxique, **tout le corpus** est pris en compte.
40
+ 2. Si vous **filtrez** sur des catégories morphosyntaxiques (voir la liste ci-dessous), l’analyse porte sur le **corpus filtré** par les catégories sélectionnées.
41
+
42
+ Noms des catégories de Lexique_fr
43
+
44
+ - **NOM** : nom commun
45
+ - **NOM_SUP** : nom
46
+ - **VER** : verbe
47
+ - **VER_SUP** : verbe
48
+ - **AUX** : auxiliaire
49
+ - **ADJ** : adjectif
50
+ - **ADJ_SUP** : adjectif
51
+ - **ADJ_DEM** : adjectif démonstratif
52
+ - **ADJ_IND** : adjectif indéfini
53
+ - **ADJ_INT** : adjectif interrogatif
54
+ - **ADJ_NUM** : adjectif numéral
55
+ - **ADJ_POS** : adjectif possessif
56
+ - **ADV** : adverbe
57
+ - **ADV_SUP** : adverbe
58
+ - **PRE** : préposition
59
+ - **CON** : conjonction
60
+ - **ART_DEF** : article défini
61
+ - **ART_IND** : article indéfini
62
+ - **PRO_DEM** : pronom démonstratif
63
+ - **PRO_IND** : pronom indéfini
64
+ - **PRO_PER** : pronom personnel
65
+ - **PRO_POS** : pronom possessif
66
+ - **PRO_REL** : pronom relatif
67
+ - **ONO** : onomatopée
68
+
69
+
70
+ Flux technique (mode "Lexique_fr"):
71
+ 1. tokenisation locale (quanteda),
72
+ 2. filtrage des tokens par présence dans lexique_fr avec les catégories `c_morpho` sélectionnées,
73
+ 3. lemmatisation (si activée) directement via lexique_fr (forme -> lemme).
74
+
75
+ > Le filtrage morphosyntaxique lexique_fr est donc indépendant de spaCy.
76
+
77
+ ### Activation de filtrage morphosyntaxique
78
+
79
+ - de choisir la langue spaCy (`fr`, `en`, `es`, `it`, `de`) quand la source est **spaCy**,
80
+ - de sélectionner les POS à conserver parmi la liste POS quand la source est **spaCy**,
81
+ - de sélectionner directement les catégories `c_morpho` à conserver quand la source est **Lexique (fr)**,
82
+ - de combiner ce filtrage avec la lemmatisation selon les besoins analytiques.
help/rapport_audit_iramuteq_like.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Audit technique détaillé — pipeline IRaMuTeQ-like (tokenisation + découpage)
2
+
3
+ ## 1) Ce que fait exactement le pipeline actuel
4
+
5
+ ### 1.1 Entrée corpus et segmentation initiale
6
+ - Le serveur lance d'abord l'import du corpus via `import_corpus_iramuteq(chemin_fichier)`, puis applique la segmentation via `split_segments(corpus, segment_size = input$segment_size)`.
7
+ - Le nombre de segments affiché juste après cette étape est `ndoc(corpus)` après `split_segments`.
8
+ - **Important**: les fonctions `import_corpus_iramuteq` et `split_segments` ne sont pas redéfinies dans ce dépôt (elles viennent d'une dépendance, typiquement `rainette`), donc leur comportement exact dépend de la version installée.
9
+
10
+ ### 1.2 Pré-nettoyage texte (avant tokenisation)
11
+ - Le texte segmenté passe ensuite par `appliquer_nettoyage_et_minuscules(...)`.
12
+ - Transformations possibles:
13
+ - normalisation espace insécable `\u00A0` -> espace,
14
+ - suppression des chiffres,
15
+ - suppression des préfixes d'élision FR (`c'`, `d'`, `l'`, `n'`, `t'`, `s'`, `j'`, `qu'`),
16
+ - suppression de caractères hors regex autorisée,
17
+ - normalisation des espaces,
18
+ - passage en minuscules.
19
+
20
+ ### 1.3 Tokenisation réellement utilisée
21
+
22
+ #### Chemin Lexique FR
23
+ - La tokenisation est faite avec `quanteda::tokens(...)` sur les textes prétraités.
24
+ - Les options `remove_punct` et `remove_numbers` dépendent de l'UI.
25
+ - Ensuite, stopwords optionnels puis `dfm` + `dfm_trim(min_docfreq=...)`.
26
+
27
+ #### Chemin spaCy
28
+ - Le script Python `rainette/spacy_preprocess.py` lit `doc_id,text`, puis pour chaque token spaCy:
29
+ - ignore espaces et ponctuation,
30
+ - optionnellement ignore nombres (`tok.like_num`),
31
+ - optionnellement retire préfixes d'élision FR,
32
+ - filtre POS (si demandé),
33
+ - prend surface ou lemme, en minuscules,
34
+ - recompose le document par `" ".join(tokens_sortie)`.
35
+ - Ensuite côté R, **il y a une seconde tokenisation** quanteda sur ce texte reconstruit.
36
+
37
+ ### 1.4 CHD IRaMuTeQ-like
38
+ - Quand `modele_chd == "iramuteq"`, le pipeline force `source_dictionnaire = "lexique_fr"`.
39
+ - Les stats classes sont calculées via `construire_stats_classes_iramuteq(...)` (contingence présence/absence doc-terme), avec exclusion des classes 0.
40
+ - Le moteur IRaMuTeQ-like (`lancer_moteur_chd_iramuteq`) s'appuie sur les scripts historiques `CHD.R`, `anacor.R`, `chdtxt.R`.
41
+
42
+ ---
43
+
44
+ ## 2) Où une divergence de nombre de segments peut apparaître
45
+
46
+ ## 2.1 Niveau segmentation primaire (le plus probable)
47
+ 1. **Version de dépendance différente** pour `split_segments`.
48
+ 2. **Normalisation des retours ligne** avant import non identique (`CRLF` vs `LF`) si la dépendance n'harmonise pas strictement.
49
+ 3. **Docnames invalides/dupliqués**: le pipeline renomme avec `make.unique`, ce qui peut modifier les correspondances ultérieures.
50
+
51
+ ## 2.2 Niveau filtrage post-segmentation
52
+ Même si la segmentation est identique au départ, le nombre final peut diverger car:
53
+ 1. `dfm_trim(min_docfreq)` peut supprimer des termes et rendre certains documents vides.
54
+ 2. `supprimer_docs_vides_dfm(...)` retire explicitement les segments à somme nulle.
55
+ 3. En CHD, les segments non assignés (`classe 0/NA`) sont exclus des stats et de l'AFC.
56
+
57
+ ## 2.3 Niveau tokenisation
58
+ 1. **Différence quanteda vs spaCy**: la définition d'un token n'est pas identique.
59
+ 2. **Double tokenisation en chemin spaCy** (spaCy puis quanteda) -> pertes supplémentaires possibles.
60
+ 3. **Option élisions FR** activée à un endroit et pas l'autre (R nettoyage vs Python spaCy).
61
+ 4. **Suppression des chiffres** appliquée à deux niveaux possibles.
62
+
63
+ ---
64
+
65
+ ## 3) Fonctions IRaMuTeQ-like auditées et risques par fonction
66
+
67
+ ### 3.1 `preparer_entrees_chd_iramuteq(...)`
68
+ - Fait: nettoyage + tokenisation quanteda + stopwords + DFM.
69
+ - Risques:
70
+ - incohérence si ce prétraitement n'est pas exactement celui utilisé en pipeline principal,
71
+ - variation stopwords selon langue/source.
72
+
73
+ ### 3.2 `calculer_chd_iramuteq(...)`
74
+ - Fait: conversion DFM -> matrice, binarisation optionnelle, appel `CHD(...)` historique.
75
+ - Risques:
76
+ - DFM trop pauvre (<2 lignes/colonnes),
77
+ - effet de la binarisation sur la stabilité des classes.
78
+
79
+ ### 3.3 `reconstruire_classes_terminales_iramuteq(...)`
80
+ - Fait: reconstruit classes terminales avec `find.terminales` + propagation descendants.
81
+ - Risques:
82
+ - classes 0 (non assignées) nombreuses si `mincl` trop strict,
83
+ - conflit entre `nb_classes_cible` et terminales reconstruites.
84
+
85
+ ### 3.4 `construire_stats_classes_iramuteq(...)`
86
+ - Fait: chi² signé doc-terme, exclut explicitement classes 0.
87
+ - Risques:
88
+ - impression de "perte" de segments côté utilisateur (en réalité exclus post-classement).
89
+
90
+ ---
91
+
92
+ ## 4) Réponse précise à "comment est tokenisé ?"
93
+
94
+ ### En mode Lexique FR
95
+ - `quanteda::tokens(textes_lexique, remove_punct=..., remove_numbers=...)`.
96
+ - Les formes sont ensuite potentiellement abaissées en minuscules, stopwords retirés, puis DFM.
97
+
98
+ ### En mode spaCy
99
+ 1. spaCy segmente selon son tokenizer modèle langue.
100
+ 2. Filtrage optionnel POS + nombres + élisions.
101
+ 3. Lemme (ou surface) en minuscules.
102
+ 4. Reconstruction du texte par espaces.
103
+ 5. Nouvelle tokenisation quanteda pour DFM.
104
+
105
+ **Conséquence**: le mode spaCy n'est pas équivalent byte-à-byte au mode lexique, même avec "mêmes paramètres UI".
106
+
107
+ ---
108
+
109
+ ## 5) Réponse précise à "comment est découpé le texte ?"
110
+
111
+ 1. Découpage en segments d'abord par `split_segments(corpus, segment_size=...)` (fonction externe).
112
+ 2. Ensuite, il existe un **découpage analytique secondaire**: les segments peuvent être retirés du jeu analysé si vidés par les filtres (`dfm_trim`, suppression docs vides, classes 0).
113
+
114
+ Donc il faut distinguer:
115
+ - **segments bruts après split**,
116
+ - **segments conservés pour CHD**,
117
+ - **segments classés (hors 0/NA)**.
118
+
119
+ ---
120
+
121
+ ## 6) Erreurs possibles (checklist de diagnostic)
122
+
123
+ 1. Version package `rainette` différente -> comportement `split_segments` différent.
124
+ 2. `segment_size` identique mais options de nettoyage non alignées.
125
+ 3. `supprimer_apostrophes` active d'un côté, inactive de l'autre.
126
+ 4. `supprimer_chiffres` active à la fois en nettoyage R et en spaCy.
127
+ 5. `remove_punct`/`remove_numbers` quanteda différents.
128
+ 6. `min_docfreq` trop élevé -> segments vidés.
129
+ 7. POS trop restrictif en spaCy -> segments vidés.
130
+ 8. Stopwords retirés puis fallback partiel.
131
+ 9. Docnames renommés (`make.unique`) perturbant certains alignements aval.
132
+ 10. Exclusion classes 0 interprétée comme erreur de découpage.
133
+
134
+ ---
135
+
136
+ ## 7) Recommandations immédiates (très concrètes)
137
+
138
+ 1. Logger séparément 3 compteurs à chaque run:
139
+ - `N_split = ndoc(corpus)` après `split_segments`,
140
+ - `N_non_vide = ndoc(dfm_obj)` après `supprimer_docs_vides_dfm`,
141
+ - `N_classes = ndoc(filtered_corpus_ok)` après exclusion classes 0/NA.
142
+ 2. Afficher ces 3 nombres dans l'UI, pas un seul "nombre de segments".
143
+ 3. Figer version `rainette` dans l'environnement pour reproductibilité.
144
+ 4. Pour audit comparatif, désactiver temporairement: POS filter, stopwords, `min_docfreq > 1`, suppression apostrophes/chiffres.
145
+
146
+ ---
147
+
148
+ ## 8) Conclusion opérationnelle
149
+
150
+ Le script ne fait pas qu'un découpage unique. Il y a:
151
+ 1. une segmentation primaire (`split_segments`),
152
+ 2. un prétraitement/tokenisation,
153
+ 3. des filtrages qui peuvent éliminer des segments,
154
+ 4. une exclusion finale des segments non classés.
155
+
156
+ Si vous observez un "nombre de segments" différent malgré corpus + segment_size identiques, l'écart provient très probablement de (a) version de `split_segments` ou (b) filtrages post-segmentation, et non du seul paramètre de segmentation UI.
help/rapport_dendrogramme_iramuteq.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rapport — construction du dendrogramme IRaMuTeQ-like
2
+
3
+ ## Constat
4
+ Le dendrogramme pouvait afficher plus de classes que les classes finales réellement exploitées (ex: 16 affichées vs 6 classes finales).
5
+
6
+ ## Vérification de `iramuteq_clone_v3`
7
+ Dans `iramuteq_clone_v3/tabchddist.py`, la découpe de l'arbre est pilotée explicitement par le nombre de classes cible (`clnb`) via :
8
+
9
+ - `classes<-as.data.frame(cutree(as.hclust(chd), k=clnb))[,1]`
10
+
11
+ Cette logique borne donc l'affichage/les statistiques à un nombre de classes final choisi.
12
+
13
+ ## Correctif appliqué
14
+ Dans `tracer_dendrogramme_chd_iramuteq` :
15
+
16
+ 1. Source de vérité prioritaire = classes présentes dans `res_stats_df$Classe` (résultat final affiché côté UI).
17
+ 2. Sinon repli sur `classes` documentaires.
18
+ 3. Projection sur `terminales` par index de classe (classe `i` -> `terminales[i]`) avec filtrage des indices invalides.
19
+ 4. Pas de repli "toutes les feuilles" tant qu'une liste de classes finales utiles existe.
20
+
21
+ ## Effet attendu
22
+ Le dendrogramme affiche uniquement les classes finales réellement présentes dans les résultats CHD (et donc alignées avec l'analyse affichée).
help/rapport_mapping_chd_iramuteq_like.md ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rapport de mapping IRaMuTeQ-like (CHD)
2
+
3
+ ## 1) État de la branche demandée
4
+
5
+ - La branche Git locale `iramuteq-like` n'existe pas dans ce dépôt au moment de l'audit (`git branch -a` ne retourne que `work`).
6
+ - L'analyse a donc été réalisée sur le **module fonctionnel `iramuteq-like/`** présent dans la branche courante.
7
+
8
+ ## 2) Mapping bout-en-bout: du concordancier au dendrogramme, à l'AFC (nuage de points) et à l'UI
9
+
10
+ ### 2.1 Concordancier
11
+
12
+ - **Calcul des termes par classe**: `iramuteq-like/concordancier-iramuteq.R`
13
+ - `.generer_concordancier_iramuteq_termes()`:
14
+ - filtre par classe,
15
+ - filtre p-value (`p` / `p_value`) selon `max_p`,
16
+ - conserve chi2 positifs,
17
+ - trie par chi2 décroissant.
18
+ - **Génération HTML**: `generer_concordancier_iramuteq_html()`:
19
+ - prend `segments_by_class`, `res_stats_df`, `textes_indexation`,
20
+ - applique fallback top chi2 si aucun terme filtré,
21
+ - détecte segments contenant les termes,
22
+ - surligne et écrit le HTML final.
23
+
24
+ ### 2.2 Calcul CHD IRaMuTeQ-like
25
+
26
+ - **Moteur CHD**: `iramuteq-like/chd_iramuteq.R`
27
+ - `calculer_chd_iramuteq()`:
28
+ - charge scripts historiques (`anacor.R`, `CHD.R`, `chdtxt.R`),
29
+ - binarise la matrice documentaire,
30
+ - lance `CHD(...)`.
31
+ - `reconstruire_classes_terminales_iramuteq()`:
32
+ - reconstruit classes documentaires finales depuis `n1`, `list_mere`, `list_fille`,
33
+ - applique logique `mincl` auto/manuel,
34
+ - mappe docs vers classes terminales.
35
+ - `construire_stats_classes_iramuteq()`:
36
+ - calcule stats par classe/terme,
37
+ - chi2 signé et p-value (présence/absence doc),
38
+ - structure compatible avec le concordancier et les sorties UI.
39
+
40
+ ### 2.3 Dendrogramme CHD
41
+
42
+ - **Entrée UI du dendrogramme**: `iramuteq-like/dendogramme_iramuteq.R`
43
+ - `tracer_dendogramme_iramuteq_ui()` récupère l'objet CHD (`rv$res$chd`, `rv$res_chd`, fallback) et appelle le traceur principal.
44
+ - **Traceur principal**: `iramuteq-like/chd_iramuteq.R`
45
+ - `tracer_dendrogramme_chd_iramuteq()`:
46
+ - reconstruit la topologie depuis `list_fille`,
47
+ - identifie racine/feuilles terminales,
48
+ - calcule layout arborescent,
49
+ - annote classes + pourcentages + termes top chi2,
50
+ - gère orientation `vertical` / `horizontal`.
51
+
52
+ ### 2.4 Nuage de points (AFC classes × termes / variables)
53
+
54
+ - **Calcul AFC**: `iramuteq-like/afc_iramuteq.R`
55
+ - `calculer_afc_classes_termes()` construit la table classes × termes,
56
+ - `executer_afc_classes_termes()` exécute l'AFC,
57
+ - `tracer_afc_classes_seules()` affiche classes,
58
+ - `tracer_afc_classes_termes()` affiche classes + termes.
59
+ - **AFC variables étoilées**: même fichier
60
+ - `calculer_afc_classes_variables()`, `executer_afc_classes_variables()`, `tracer_afc_classes_variables()`.
61
+
62
+ ### 2.5 Nuage de mots par classe
63
+
64
+ - **Génération assets PNG**: `iramuteq-like/wordcloud_iramuteq.R`
65
+ - **Exposition UI (iframe/img)**: `iramuteq-like/server_events_lancer_iramuteq.R` via `output$ui_wordcloud_iramuteq`.
66
+
67
+ ### 2.6 Orchestration serveur (pipeline)
68
+
69
+ - **Pipeline principal au clic Lancer**: `iramuteq-like/server_events_lancer_iramuteq.R`
70
+ - préparation corpus,
71
+ - exécution CHD,
72
+ - calcul stats,
73
+ - calcul AFC,
74
+ - génération wordclouds,
75
+ - génération concordancier HTML,
76
+ - alimentation de `rv$*` pour l'UI.
77
+
78
+ ### 2.7 Couches UI
79
+
80
+ - **Panneau résultats IRaMuTeQ-like**: `iramuteq-like/affichage_iramuteq-like.R`
81
+ - sous-onglets Dendrogramme / Stats CHD / Concordancier / Nuage de mots.
82
+ - **Rendu serveur principal Shiny**: `app.R`
83
+ - `output$plot_chd_iramuteq_dendro`,
84
+ - `output$ui_tables_stats_chd_iramuteq`,
85
+ - `output$plot_afc_classes`, `output$plot_afc`, `output$plot_afc_vars`,
86
+ - intègre `register_events_lancer(...)`.
87
+
88
+ ## 3) Changement réalisé: méthode d'affichage du dendrogramme
89
+
90
+ ### 3.1 Objectif
91
+
92
+ Réduire la surcharge visuelle quand les annotations de termes par classe se chevauchent.
93
+
94
+ ### 3.2 Implémentation
95
+
96
+ - **Nouvelle option UI** dans l'onglet Dendrogramme:
97
+ - `Méthode d'affichage`
98
+ - `standard` (comportement historique: termes près des classes)
99
+ - `compact` (nouveau défaut: termes regroupés en légende).
100
+ - **Propagation des paramètres**:
101
+ - `app.R` transmet `input$iramuteq_dendro_display_method` au traceur.
102
+ - `tracer_dendogramme_iramuteq_ui()` accepte `display_method` et le passe à `tracer_dendrogramme_chd_iramuteq()`.
103
+ - **Rendu compact dans le traceur**:
104
+ - en `vertical`: légende textuelle regroupée en bas (`mtext`).
105
+ - en `horizontal`: légende compacte en bas à droite (`legend`).
106
+ - titres ajustés pour expliciter le mode `compact`.
107
+
108
+ ### 3.3 Impact
109
+
110
+ - Pas de changement du calcul CHD ni des classes.
111
+ - Changement purement visuel du dendrogramme côté affichage.
112
+ - Le mode `standard` reste disponible.
help/rapport_tokenisation_iramuteq_clone_v3.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rapport ciblé — chaîne de tokenisation `iramuteq_clone_v3` et pipeline `iramuteq-like`
2
+
3
+ ## Contexte
4
+
5
+ Ce rapport répond à deux points :
6
+ 1. Auditer **la chaîne de tokenisation** du dossier `iramuteq_clone_v3`.
7
+ 2. Vérifier, côté application actuelle, le mode **`iramuteq-like`** (qui force `lexique_fr`) sans analyse spaCy.
8
+
9
+ ---
10
+
11
+ ## 1) Résultat principal sur `iramuteq_clone_v3`
12
+
13
+ ### 1.1 Limite structurelle du dépôt cloné
14
+
15
+ Dans ce dépôt, le cœur de préparation du corpus texte (module `corpus`) est référencé mais **n'est pas présent** dans l'arborescence locale fournie.
16
+
17
+ - `iramuteq.py` importe `from corpus import Builder, SubBuilder, MergeClusters`. Cela indique que la chaîne de tokenisation principale est attendue dans `corpus.py`/module `corpus`.【F:iramuteq_clone_v3/iramuteq.py†L43-L44】
18
+ - `textcheckcorpus.py` importe aussi `Corpus` depuis `corpus`, confirmant la dépendance centrale à ce module absent localement.【F:iramuteq_clone_v3/textcheckcorpus.py†L16-L17】
19
+
20
+ 👉 Conséquence : on peut auditer les options visibles en amont/aval, mais **pas reconstruire à 100% la tokenisation interne historique** sans ce module.
21
+
22
+ ### 1.2 Ce qui est explicitement vérifiable dans `iramuteq_clone_v3`
23
+
24
+ #### a) Contrôle du format corpus (pas de tokenisation lexicale ici)
25
+ `textcheckcorpus.py` vérifie surtout la syntaxe des lignes/étoiles (`****`, variables `*`, thématiques `-*`, espaces interdits, etc.), mais pas un découpage lexical type tokenizer moderne.【F:iramuteq_clone_v3/textcheckcorpus.py†L19-L39】
26
+
27
+ #### b) Options de CHD (aval de la tokenisation)
28
+ `textreinert.py` expose des paramètres de classification (pas la tokenisation elle-même), notamment :
29
+ - `classif_mode` (simple/double/uci),
30
+ - `tailleuc1`, `tailleuc2` (taille des unités de contexte),
31
+ - `mincl`, `minforme`,
32
+ - `nbcl_p1`,
33
+ - `max_actives`,
34
+ - `svdmethod`,
35
+ - `mode.patate`.【F:iramuteq_clone_v3/textreinert.py†L70-L81】
36
+
37
+ Il génère ensuite des matrices clairsemées à partir des UC/UCE/UCI via des méthodes de `corpus` (`make_and_write_sparse_matrix_from_uc`, `..._from_uces`, `..._from_uci`).【F:iramuteq_clone_v3/textreinert.py†L33-L41】
38
+
39
+ #### c) Règle mincl auto côté scripts R historiques
40
+ Dans `Rscripts/chdtxt.R`, si `mincl == 0`, la formule automatique est :
41
+ - `ind = nbcl * 2` en mode double, sinon `ind = nbcl` ;
42
+ - `mincl = round(nrow(classeuce1)/ind)`.【F:iramuteq_clone_v3/Rscripts/chdtxt.R†L278-L281】
43
+
44
+ ---
45
+
46
+ ## 2) Pipeline **réel** utilisé par ton application `iramuteq-like` (sans spaCy)
47
+
48
+ Tu as raison : en mode `iramuteq-like`, la source est forcée sur `lexique_fr`.
49
+
50
+ ### 2.1 Forçage `lexique_fr`
51
+ Dans `server_events_lancer.R`, si `modele_chd == "iramuteq"`, la source dictionnaire est forcée à `lexique_fr` avec log explicite.【F:R/server_events_lancer.R†L314-L317】
52
+
53
+ ### 2.2 Chaîne de prétraitement effectivement appliquée
54
+ Avant tokenisation, la pipeline applique :
55
+ - nettoyage caractères,
56
+ - minuscules (option),
57
+ - suppression chiffres (option),
58
+ - suppression apostrophes/élisions (option).【F:R/server_events_lancer.R†L295-L301】
59
+
60
+ Ensuite, en branche `lexique_fr`, la pipeline passe par `executer_pipeline_lexique(...)`.【F:R/server_events_lancer.R†L342-L347】
61
+
62
+ ### 2.3 Tokenisation et options actives en branche `lexique_fr`
63
+ Dans `executer_pipeline_lexique` :
64
+ - tokenisation `quanteda::tokens(...)` sur `textes_lexique`,
65
+ - options actives : `remove_punct = input$supprimer_ponctuation`, `remove_numbers = input$supprimer_chiffres`.
66
+ - puis construction DFM avec logique stopwords/min_docfreq via `construire_dfm_avec_fallback_stopwords(...)`.【F:R/pipeline_lexique_analysis.R†L67-L82】
67
+
68
+ ### 2.4 Option de normalisation en minuscules au niveau DFM
69
+ La fonction `construire_dfm_avec_fallback_stopwords(...)` applique `tokens_tolower(...)` (avec ou sans retrait stopwords), donc la représentation finale est normalisée en minuscules côté DFM.【F:R/chd_afc_pipeline.R†L63-L70】
70
+
71
+ ### 2.5 Option de préparation spécifique `iramuteq-like`
72
+ Le module `iramuteq-like/chd_iramuteq.R` normalise explicitement les options de nettoyage suivantes :
73
+ - `nettoyage_caracteres`,
74
+ - `forcer_minuscules_avant`,
75
+ - `supprimer_chiffres`,
76
+ - `supprimer_apostrophes`,
77
+ - `supprimer_ponctuation`,
78
+ - `retirer_stopwords`.
79
+ Puis tokenise avec `quanteda::tokens(remove_punct, remove_numbers)` et option stopwords quanteda selon langue.【F:iramuteq-like/chd_iramuteq.R†L26-L33】【F:iramuteq-like/chd_iramuteq.R†L80-L89】
80
+
81
+ ---
82
+
83
+ ## 3) Rapport des options actives (mode `iramuteq-like`)
84
+
85
+ ### 3.1 Options effectivement actives dans ta chaîne (et où)
86
+
87
+ - `nettoyage_caracteres` : actif avant tokenisation via `appliquer_nettoyage_et_minuscules(...)`.【F:R/server_events_lancer.R†L295-L299】
88
+ - `forcer_minuscules_avant` : actif avant tokenisation via `appliquer_nettoyage_et_minuscules(...)`.【F:R/server_events_lancer.R†L298-L299】
89
+ - `supprimer_chiffres` : actif avant tokenisation + actif dans `tokens(remove_numbers=...)`.【F:R/server_events_lancer.R†L299-L300】【F:R/pipeline_lexique_analysis.R†L69-L71】
90
+ - `supprimer_apostrophes` : actif avant tokenisation via nettoyage texte.【F:R/server_events_lancer.R†L300-L301】
91
+ - `supprimer_ponctuation` : actif dans `tokens(remove_punct=...)` en branche `lexique_fr`.【F:R/pipeline_lexique_analysis.R†L69-L70】
92
+ - `retirer_stopwords` : actif dans `construire_dfm_avec_fallback_stopwords(...)` (avec fallback si DFM trop pauvre).【F:R/chd_afc_pipeline.R†L55-L66】【F:R/chd_afc_pipeline.R†L87-L93】
93
+ - `min_docfreq` : actif au `dfm_trim(min_docfreq=...)`.【F:R/chd_afc_pipeline.R†L77-L77】
94
+ - `filtrage_morpho` + `lexique_utiliser_lemmes` : actifs en branche lexique via filtrage cgram/lemmatisation forme→lemme si demandé.【F:R/pipeline_lexique_analysis.R†L31-L36】【F:R/pipeline_lexique_analysis.R†L48-L61】
95
+
96
+ ### 3.2 Options non pertinentes pour ce mode
97
+ - Les options spaCy ne sont pas utilisées quand `modele_chd="iramuteq"` puisque la source est forcée `lexique_fr`.【F:R/server_events_lancer.R†L314-L317】
98
+
99
+ ---
100
+
101
+ ## 4) Pourquoi l'écart de formes peut persister malgré “même paramétrage”
102
+
103
+ Même en excluant spaCy, un écart peut persister entre IRaMuTeQ desktop et ton app si la tokenisation historique interne d'IRaMuTeQ (module `corpus` absent ici) diffère de ta chaîne `quanteda + lexique_fr + trim`.
104
+
105
+ En pratique, les points sensibles sont :
106
+ - règles de segmentation des unités de contexte (UC/UCE/UCI) et calcul d'actives/supplémentaires dans `corpus` historique (non auditable dans ce clone incomplet),
107
+ - ordre exact nettoyage → lemmatisation → tokenisation,
108
+ - traitement des élisions/apostrophes,
109
+ - seuil `min_docfreq`, suppression stopwords, fallback, et suppression segments vides avant CHD.
110
+
111
+ ---
112
+
113
+ ## 5) Recommandation opérationnelle
114
+
115
+ Pour une comparaison “iso-IRaMuTeQ” stricte, je recommande un mode audit en 2 passes :
116
+
117
+ 1. **Pass A (référence max proche IRaMuTeQ)**
118
+ - `retirer_stopwords = FALSE`
119
+ - `min_docfreq = 1`
120
+ - pas de filtrage morpho
121
+ - pas de suppression apostrophes/chiffres supplémentaire
122
+
123
+ 2. **Pass B (tes réglages actuels)**
124
+ - comparer : nb tokens bruts, nb formes DFM, nb hapax, nb segments non vides.
125
+
126
+ Et ajouter un export intermédiaire (tokens par segment) pour expliquer où apparaissent les +194 formes.
127
+
128
+
129
+ ---
130
+
131
+ ## 6) Corrections concrètes recommandées pour rapprocher les scores
132
+
133
+ 1. **Figer une prépa corpus unique avant `quanteda::tokens`**
134
+ - Utiliser `iramuteq-like/textprepa_iramuteq.py` pour générer un texte préparé stable et auditable (mêmes règles à chaque run), puis tokeniser ce texte côté R.
135
+
136
+ 2. **Éviter les doubles effets de nettoyage**
137
+ - Si `iramuteq-like/textprepa_iramuteq.py` est activé, neutraliser le nettoyage redondant en amont pour ne pas supprimer deux fois chiffres/élisions.
138
+
139
+ 3. **Comparer les formes à 3 niveaux**
140
+ - Niveau A: formes post-prépa (sortie `output_tokens` de `iramuteq-like/textprepa_iramuteq.py`),
141
+ - Niveau B: formes post-`quanteda::tokens`,
142
+ - Niveau C: formes post-`dfm_trim`.
143
+
144
+ 4. **Conserver un mode “audit IRaMuTeQ”**
145
+ - `min_docfreq=1`, `retirer_stopwords=FALSE`, pas de filtrage morpho, pour isoler l'écart de tokenisation.
146
+
147
+ 5. **Comparer sur un sous-corpus fixe**
148
+ - Exporter 50–100 segments identiques entre IRaMuTeQ desktop et app, comparer les listes de formes exactes segment par segment.
iramuteq-like/CHD.R ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Author: Pierre Ratinaud
2
+ #Copyright (c) 2008-2020 Pierre Ratinaud
3
+ #License: GNU/GPL
4
+
5
+ pp<-function(txt,val) {
6
+ d<-paste(txt,' : ')
7
+ print(paste(d,val))
8
+ }
9
+ MyChiSq<-function(x,sc,n){
10
+ sr<-rowSums(x)
11
+ E <- outer(sr, sc, "*")/n
12
+ STAT<-sum(((x - E)^2)/E)
13
+ STAT
14
+ }
15
+
16
+ MySpeedChi <- function(x,sc) {
17
+ sr <-rowSums(x)
18
+ E <- outer(sr, sc, "*")
19
+ STAT<-sum((x - E)^2/E)
20
+ STAT
21
+ }
22
+
23
+ find.max <- function(dtable, chitable, compte, rmax, maxinter, sc, TT) {
24
+ ln <- which(dtable==1, arr.ind=TRUE)
25
+ lo <- list()
26
+ lo[1:nrow(dtable)] <- 0
27
+ for (k in 1:nrow(ln)) {lo[[ln[k,1]]]<-append(lo[[ln[k,1]]],ln[k,2])}
28
+ for (k in 1:nrow(dtable)) {lo[[k]] <- lo[[k]][-1]}
29
+ ## lo<-lo[-c(1,length(lo))]
30
+ ## for (l in lo) {
31
+ ## compte <- compte + 1
32
+ ## chitable[1,l]<-chitable[1,l]+1
33
+ ## chitable[2,l]<-chitable[2,l]-1
34
+ ## chi<-MyChiSq(chitable,sc,TT)
35
+ ## if (chi>maxinter) {
36
+ ## maxinter<-chi
37
+ ## rmax<-compte
38
+ ## }
39
+ #}
40
+ lo<-lo[-c(1)]
41
+ for (l in lo) {
42
+ chi<-MyChiSq(chitable,sc,TT)
43
+ if (chi>maxinter) {
44
+ maxinter<-chi
45
+ rmax<-compte
46
+ }
47
+ compte <- compte + 1
48
+ chitable[1,l]<-chitable[1,l]+1
49
+ chitable[2,l]<-chitable[2,l]-1
50
+ }
51
+ res <- list(maxinter=maxinter, rmax=rmax)
52
+ res
53
+ }
54
+
55
+
56
+
57
+
58
+
59
+ CHD<-function(data.in, x=9, mode.patate = FALSE, svd.method, libsvdc.path=NULL){
60
+ # sink('/home/pierre/workspace/iramuteq/dev/findchi2.txt')
61
+ dataori <- data.in
62
+ row.names(dataori) <- rownames(data.in)
63
+ dtable <- data.in
64
+ colnames(dtable) <- 1:ncol(dtable)
65
+ dout <- NULL
66
+ rowelim<-NULL
67
+ pp('ncol entree : ',ncol(dtable))
68
+ pp('nrow entree',nrow(dtable))
69
+ listcol <- list()
70
+ listmere <- list()
71
+ list_fille <- list()
72
+ print('vire colonnes vides en entree')#FIXME : il ne doit pas y avoir de colonnes vides en entree !!
73
+ sdt<-colSums(dtable)
74
+ if (min(sdt)==0)
75
+ dtable<-dtable[,-which(sdt==0)]
76
+ print('vire lignes vides en entree')
77
+ sdt<-rowSums(dtable)
78
+ if (min(sdt)==0) {
79
+ rowelim<-as.integer(rownames(dtable)[which(sdt==0)])
80
+ print('&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&')
81
+ print(rowelim)
82
+ print('&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&')
83
+ dtable<-dtable[-which(sdt==0),]
84
+ }
85
+ mere<-1
86
+ for (i in 1:x) {
87
+ clnb<-(i*2)
88
+ listmere[[clnb]]<-mere
89
+ listmere[[clnb+1]]<-mere
90
+ list_fille[[mere]] <- c(clnb,clnb+1)
91
+ listcol[[clnb]]<-vector()
92
+ listcol[[clnb+1]]<-vector()
93
+ #extraction du premier facteur de l'afc
94
+ print('afc')
95
+ pp('taille dtable dans boucle (col/row)',c(ncol(dtable),nrow(dtable)))
96
+ afc<-boostana(dtable, nd=1, svd.method = svd.method, libsvdc.path=libsvdc.path)
97
+ pp('SV',afc$singular.values)
98
+ pp('V.P.', afc$eigen.values)
99
+ coordrow <- as.matrix(afc$row.scores[,1])
100
+ coordrowori<-coordrow
101
+ row.names(coordrow)<-rownames(dtable)
102
+ coordrow <- cbind(coordrow,1:nrow(dtable))
103
+ print('deb recherche meilleur partition')
104
+ ordert <- as.matrix(coordrow[order(coordrow[,1]),])
105
+ ordert <- cbind(ordert, 1:nrow(ordert))
106
+ ordert <- ordert[order(ordert[,2]),]
107
+
108
+ listinter<-vector()
109
+ listlim<-vector()
110
+ dtable <- dtable[order(ordert[,3]),]
111
+ sc <- colSums(dtable)
112
+ TT <- sum(sc)
113
+ sc1 <- dtable[1,]
114
+ sc2 <- colSums(dtable) - sc1
115
+ chitable <- rbind(sc1, sc2)
116
+ compte <- 1
117
+ maxinter <- 0
118
+ rmax <- NULL
119
+
120
+ inert <- find.max(dtable, chitable, compte, rmax, maxinter, sc, TT)
121
+ print('@@@@@@@@@@@@@@@@@@@@@@@@@@@@')
122
+ pp('max inter phase 1', inert$maxinter/TT)#max(listinter))
123
+ print('@@@@@@@@@@@@@@@@@@@@@@@@@@@@')
124
+ ordert <- ordert[order(ordert[,3]),]
125
+ listclasse<-ifelse(coordrowori<=ordert[(inert$rmax),1],clnb,clnb+1)
126
+ dtable <- dtable[order(ordert[,2]),]
127
+ cl<-listclasse
128
+ pp('TT',TT)
129
+ #dtable<-cbind(dtable,'cl'= as.vector(cl))
130
+
131
+ N1<-length(listclasse[listclasse==clnb])
132
+ N2<-length(listclasse[listclasse==clnb+1])
133
+ pp('N1',N1)
134
+ pp('N2',N2)
135
+ ###################################################################
136
+ # reclassement des individus #
137
+ ###################################################################
138
+ if (!mode.patate) {
139
+ malcl<-1000000000000
140
+ it<-0
141
+ listsub<-list()
142
+ #in boucle
143
+ ln <- which(dtable==1, arr.ind=TRUE)
144
+ lnz <- list()
145
+ lnz[1:nrow(dtable)] <- 0
146
+
147
+ for (k in 1:nrow(ln)) {lnz[[ln[k,1]]]<-append(lnz[[ln[k,1]]],ln[k,2])}
148
+ for (k in 1:nrow(dtable)) {lnz[[k]] <- lnz[[k]][-1]}
149
+ TT<-sum(dtable)
150
+
151
+ while (malcl!=0 & N1>=5 & N2>=5) {
152
+ it<-it+1
153
+ listsub[[it]]<-vector()
154
+ txt <- paste('nombre iteration', it)
155
+ #pp('nombre iteration',it)
156
+ vdelta<-vector()
157
+ #dtable[,'cl']<-cl
158
+ t1<-dtable[which(cl[,1]==clnb),]#[,-ncol(dtable)]
159
+ t2<-dtable[which(cl[,1]==clnb+1),]#[,-ncol(dtable)]
160
+ ncolt<-ncol(t1)
161
+ #pp('ncolt',ncolt)
162
+
163
+ if (N1 != 1) {
164
+ sc1<-colSums(t1)
165
+ } else {
166
+ sc1 <- t1
167
+ }
168
+ if (N2 != 1) {
169
+ sc2<-colSums(t2)
170
+ } else {
171
+ sc2 <- t2
172
+ }
173
+
174
+ sc<-sc1+sc2
175
+ chtableori<-rbind(sc1,sc2)
176
+ chtable<-chtableori
177
+ interori<-MyChiSq(chtableori,sc,TT)/TT#chisq.test(chtableori)$statistic#/TT
178
+ txt <- paste(txt, ' - interori : ',interori)
179
+ #pp('interori',interori)
180
+
181
+ N1<-nrow(t1)
182
+ N2<-nrow(t2)
183
+
184
+ #pp('N1',N1)
185
+ #pp('N2',N2)
186
+ txt <- paste(txt, 'N1:', N1,'-N2:',N2)
187
+ print(txt)
188
+ compte <- 0
189
+ for (l in lnz){
190
+ chi.in<-chtable
191
+ compte <- compte + 1
192
+ if(cl[compte]==clnb){
193
+ chtable[1,l]<-chtable[1,l]-1
194
+ chtable[2,l]<-chtable[2,l]+1
195
+ }else{
196
+ chtable[1,l]<-chtable[1,l]+1
197
+ chtable[2,l]<-chtable[2,l]-1
198
+ }
199
+ interswitch<-MyChiSq(chtable,sc,TT)/TT#chisq.test(chtable)$statistic/TT
200
+ ws<-interori-interswitch
201
+
202
+ if (ws<0){
203
+ interori<-interswitch
204
+ if(cl[compte]==clnb){
205
+ #sc1<-chtable[1,]
206
+ #sc2<-chtable[2,]
207
+ cl[compte]<-clnb+1
208
+ listsub[[it]]<-append(listsub[[it]],compte)
209
+ } else {
210
+ #sc1<-chtable[1,]
211
+ #sc2<-chtable[2,]
212
+ cl[compte]<-clnb
213
+ listsub[[it]]<-append(listsub[[it]],compte)
214
+ }
215
+ vdelta<-append(vdelta,compte)
216
+ } else {
217
+ chtable<-chi.in
218
+ }
219
+ }
220
+ # for (val in vdelta) {
221
+ # if (cl[val]==clnb) {
222
+ # cl[val]<-clnb+1
223
+ # listsub[[it]]<-append(listsub[[it]],val)
224
+ # }else {
225
+ # cl[val]<-clnb
226
+ # listsub[[it]]<-append(listsub[[it]],val)
227
+ # }
228
+ # }
229
+ print('###################################')
230
+ print('longueur < 0')
231
+ malcl<-length(vdelta)
232
+
233
+ if ((it>1)&&(!is.logical(listsub[[it]]))&&(!is.logical(listsub[[it-1]]))){
234
+ if (all(listsub[[it]]==listsub[[(it-1)]])){
235
+ malcl<-0
236
+ }
237
+ }
238
+ print(malcl)
239
+ print('###################################')
240
+ }
241
+ }
242
+ #dtable<-cbind(dtable,'cl'=as.vector(cl))
243
+ #dtable[,'cl'] <-as.vector(cl)
244
+ #######################################################################
245
+ # Fin reclassement des individus #
246
+ #######################################################################
247
+ # if (!(length(cl[cl==clnb])==1 || length(cl[cl==clnb+1])==1)) {
248
+ #t1<-dtable[dtable[,'cl']==clnb,][,-ncol(dtable)]
249
+ #t2<-dtable[dtable[,'cl']==clnb+1,][,-ncol(dtable)]
250
+ t1<-dtable[which(cl[,1]==clnb),]#[,-ncol(dtable)]
251
+ t2<-dtable[which(cl[,1]==clnb+1),]#[,-ncol(dtable)]
252
+ if (inherits(t1, "numeric")) {
253
+ sc1 <- as.vector(t1)
254
+ nrowt1 <- 1
255
+ } else {
256
+ sc1 <- colSums(t1)
257
+ nrowt1 <- nrow(t1)
258
+ }
259
+ if (inherits(t2, "numeric")) {
260
+ sc2 <- as.vector(t2)
261
+ nrowt2 <- 1
262
+ } else {
263
+ sc2 <- colSums(t2)
264
+ nrowt2 <- nrow(t2)
265
+ }
266
+ chtable<-rbind(sc1,sc2)
267
+ inter<-chisq.test(chtable)$statistic/TT
268
+ pp('last inter',inter)
269
+ print('=====================')
270
+ #calcul de la specificite des colonnes
271
+ mint<-min(nrowt1,nrowt2)
272
+ maxt<-max(nrowt1,nrowt2)
273
+ seuil<-round((1.9*(maxt/mint))+1.9,digit=6)
274
+ #sink('/home/pierre/workspace/iramuteq/dev/findchi2.txt')
275
+ # print('ATTENTION SEUIL 3,84')
276
+ # seuil<-3.84
277
+ pp('seuil',seuil)
278
+ sominf<-0
279
+ nv<-0
280
+ nz<-0
281
+ ncclnb<-0
282
+ ncclnbp<-0
283
+ NN1<-0
284
+ NN2<-0
285
+ maxchip<-0
286
+ nbzeroun<-0
287
+ res1<-0
288
+ res2<-0
289
+ nbseuil<-0
290
+ nbexe<-0
291
+ nbcontrib<-0
292
+ cn<-colnames(dtable)
293
+ #another try#########################################
294
+ one <- cbind(sc1,sc2)
295
+ cols <- c(length(which(cl==clnb)), length(which(cl==clnb+1)))
296
+ print(cols)
297
+ colss <- matrix(rep(cols,ncol(dtable)), ncol=2, byrow=TRUE)
298
+ zero <- colss - one
299
+ rows <- cbind(rowSums(zero), rowSums(one))
300
+ n <- sum(cols)
301
+ for (m in 1:nrow(rows)) {
302
+ obs <- t(matrix(c(zero[m,],one[m,]),2,2))
303
+ E <- outer(rows[m,],cols,'*')/n
304
+ if ((min(obs[2,])==0) & (min(obs[1,])!=0)) {
305
+ chi <- seuil + 1
306
+ } else if ((min(obs[1,])==0) & (min(obs[2,])!=0)) {
307
+ chi <- seuil - 1
308
+ } else if (any(obs < 10)) {
309
+ chi <- sum((abs(obs - E) - 0.5)^2 / E)
310
+ } else {
311
+ chi <- sum(((obs - E)^2)/E)
312
+ }
313
+ if (is.na(chi)) {
314
+ chi <- 0
315
+ }
316
+ if (chi > seuil) {
317
+ if (obs[2,1] < E[2,1]) {
318
+ listcol[[clnb]]<-append(listcol[[clnb]],cn[m])
319
+ ncclnb<-ncclnb+1
320
+ } else if (obs[2,2] < E[2,2]) {
321
+ listcol[[clnb+1]]<-append(listcol[[clnb+1]],cn[m])
322
+ ncclnbp<-ncclnbp+1
323
+ }
324
+ }
325
+ }
326
+ ######################################################
327
+ print('resultats elim item')
328
+ pp(clnb+1,length(listcol[[clnb+1]]))
329
+ pp(clnb,length(listcol[[clnb]]))
330
+ pp('ncclnb',ncclnb)
331
+ pp('ncclnbp',ncclnbp)
332
+ listrownamedtable<-rownames(dtable)
333
+ listrownamedtable<-as.integer(listrownamedtable)
334
+ newcol<-vector(length=nrow(dataori))
335
+ #remplissage de la nouvelle colonne avec les nouvelles classes
336
+ print('remplissage')
337
+ # num<-0
338
+ newcol[listrownamedtable] <- cl[,1]
339
+ #recuperation de la classe precedante pour les cases vides
340
+ print('recuperation classes precedentes')
341
+ if (i!=1) {
342
+ newcol[which(newcol==0)] <- dout[,ncol(dout)][which(newcol==0)]
343
+ }
344
+ if(!is.null(rowelim)) {
345
+ newcol[rowelim] <- 0
346
+ }
347
+ tailleclasse<-as.matrix(summary(as.factor(as.character(newcol))))
348
+ print('tailleclasse')
349
+ print(tailleclasse)
350
+ tailleclasse<-as.matrix(tailleclasse[!(rownames(tailleclasse)==0),])
351
+ plusgrand<-which.max(tailleclasse)
352
+ #???????????????????????????????????
353
+ #Si 2 classes ont des effectifs egaux, on prend la premiere de la liste...
354
+ if (length(plusgrand)>1) {
355
+ plusgrand<-plusgrand[1]
356
+ }
357
+ #????????????????????????????????????
358
+
359
+ #constuction du prochain tableau a analyser
360
+ print('construction tableau suivant')
361
+ dout<-cbind(dout,newcol)
362
+ classe<-as.integer(rownames(tailleclasse)[plusgrand])
363
+ dtable<-dataori[which(newcol==classe),]
364
+ row.names(dtable)<-rownames(dataori)[which(newcol==classe)]
365
+ colnames(dtable) <- 1:ncol(dtable)
366
+ mere<-classe
367
+ listcolelim<-listcol[[as.integer(classe)]]
368
+ mother<-listmere[[as.integer(classe)]]
369
+ while (mother!=1) {
370
+ listcolelim<-append(listcolelim,listcol[[mother]])
371
+ mother<-listmere[[mother]]
372
+ }
373
+
374
+ listcolelim<-sort(unique(listcolelim))
375
+ pp('avant',ncol(dtable))
376
+ if (!is.logical(listcolelim)){
377
+ print('elimination colonne')
378
+ #dtable<-dtable[,-listcolelim]
379
+ dtable<-dtable[,!(colnames(dtable) %in% listcolelim)]
380
+ }
381
+ pp('apres',ncol(dtable))
382
+ #elimination des colonnes ne contenant que des 0
383
+ print('vire colonne inf 3 dans boucle')
384
+ sdt<-colSums(dtable)
385
+ if (min(sdt)<=3)
386
+ dtable<-dtable[,-which(sdt<=3)]
387
+
388
+ #elimination des lignes ne contenant que des 0
389
+ print('vire ligne vide dans boucle')
390
+ if (ncol(dtable)==1) {
391
+ sdt<-dtable[,1]
392
+ } else {
393
+ sdt<-rowSums(dtable)
394
+ }
395
+ if (min(sdt)==0) {
396
+ rowelim<-as.integer(rownames(dtable)[which(sdt==0)])
397
+ print('&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&')
398
+ print(rowelim)
399
+ print('&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&')
400
+ dtable<-dtable[-which(sdt==0),]
401
+ }
402
+ # }
403
+ }
404
+ # sink()
405
+ res <- list(n1 = dout, list_mere = listmere, list_fille = list_fille)
406
+ res
407
+ }
iramuteq-like/afc_helpers_iramuteq.R ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: afc_helpers.R porte une partie du pipeline d'analyse Rainette.
2
+ # Ce script centralise une responsabilité métier/technique utilisée par l'application.
3
+ # Il facilite la maintenance en explicitant le périmètre et les points d'intégration.
4
+ # Module AFC - helpers de contextualisation des termes
5
+ # Ce fichier regroupe des utilitaires AFC, notamment la construction de segments
6
+ # exemples associés aux termes caractéristiques de classes.
7
+
8
+ construire_segments_exemples_afc <- function(termes_stats, dfm_obj, corpus_obj, max_chars = 220) {
9
+ if (is.null(termes_stats) || nrow(termes_stats) == 0 || is.null(dfm_obj) || is.null(corpus_obj)) return(termes_stats)
10
+ if (!all(c("Terme", "Classe_max") %in% names(termes_stats))) return(termes_stats)
11
+
12
+ classes_docs <- normaliser_classes(docvars(corpus_obj)$Classes)
13
+ textes <- as.character(corpus_obj)
14
+
15
+ if (length(classes_docs) != ndoc(dfm_obj) || length(textes) != ndoc(dfm_obj)) return(termes_stats)
16
+
17
+ mat <- as.matrix(dfm_obj)
18
+ termes_stats$Segment_texte <- NA_character_
19
+
20
+ for (i in seq_len(nrow(termes_stats))) {
21
+ terme <- as.character(termes_stats$Terme[i])
22
+ classe_num <- suppressWarnings(as.numeric(gsub("^Classe\\s+", "", as.character(termes_stats$Classe_max[i]))))
23
+
24
+ if (is.na(classe_num) || !nzchar(terme) || !(terme %in% colnames(mat))) next
25
+
26
+ idx <- which(classes_docs == as.character(classe_num) & mat[, terme] > 0)
27
+ if (length(idx) == 0) next
28
+
29
+ # Segment le plus représentatif pour le terme dans la classe (fréquence max du terme)
30
+ i_best <- idx[which.max(mat[idx, terme])]
31
+ seg <- gsub("\\s+", " ", trimws(textes[i_best]), perl = TRUE)
32
+ if (!nzchar(seg)) next
33
+ if (nchar(seg) > max_chars) seg <- paste0(substr(seg, 1, max_chars - 1), "…")
34
+
35
+ termes_stats$Segment_texte[i] <- seg
36
+ }
37
+
38
+ termes_stats
39
+ }
iramuteq-like/afc_iramuteq.R ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: afc.R porte une partie du pipeline d'analyse Rainette.
2
+ # Ce script centralise une responsabilité métier/technique utilisée par l'application.
3
+ # Il facilite la maintenance en explicitant le périmètre et les points d'intégration.
4
+ # afc.R
5
+ # AFC (Analyse Factorielle des Correspondances) pour classes × termes et classes × variables étoilées
6
+ # Dépendance principale : FactoMineR (CA)
7
+ # Le fichier fournit les fonctions attendues par app.R, sans changer la logique globale :
8
+ # - calcul des tables de contingence
9
+ # - exécution de l'AFC
10
+ # - statistiques (fréquence, chi2, p-value)
11
+ # - tracés avec axes centrés (0 au centre) et limites symétriques
12
+ # - option anti-chevauchement des labels (placement en spirale + tests de collision)
13
+
14
+ verifier_factominer <- function() {
15
+ if (!requireNamespace("FactoMineR", quietly = TRUE)) {
16
+ stop("AFC : le package 'FactoMineR' n'est pas installé ou indisponible dans l'environnement.")
17
+ }
18
+ }
19
+
20
+ # Limites symétriques autour de 0 pour centrer le graphe (0,0) au centre
21
+ calculer_lim_sym <- function(x, y, marge = 0.08) {
22
+ x <- x[is.finite(x)]
23
+ y <- y[is.finite(y)]
24
+ if (length(x) == 0 || length(y) == 0) return(c(-1, 1))
25
+ m <- max(abs(c(x, y)))
26
+ if (!is.finite(m) || m == 0) m <- 1
27
+ m <- m * (1 + marge)
28
+ c(-m, m)
29
+ }
30
+
31
+ # Test de chevauchement rectangles
32
+ .rectangles_chevauchent <- function(r1, r2) {
33
+ !(r1$xmax < r2$xmin || r2$xmax < r1$xmin || r1$ymax < r2$ymin || r2$ymax < r1$ymin)
34
+ }
35
+
36
+ # Placement anti-chevauchement des labels
37
+ # Technique : on place les mots un par un. Si collision avec un label déjà placé, on déplace le label le long d'une spirale
38
+ # (rayon croissant + angle) jusqu'à trouver une position non collisionnante, ou jusqu'à max_iter.
39
+ placer_labels_sans_chevauchement_spirale <- function(x, y, labels, cex_vec, max_iter = 220) {
40
+ x <- as.numeric(x)
41
+ y <- as.numeric(y)
42
+ n <- length(labels)
43
+ if (n == 0) return(list(x = x, y = y))
44
+
45
+ if (length(cex_vec) == 1) cex_vec <- rep(cex_vec, n)
46
+ cex_vec <- as.numeric(cex_vec)
47
+ cex_vec[!is.finite(cex_vec)] <- 1
48
+
49
+ usr <- par("usr")
50
+ rx <- usr[2] - usr[1]
51
+ ry <- usr[4] - usr[3]
52
+ if (!is.finite(rx) || rx == 0) rx <- 1
53
+ if (!is.finite(ry) || ry == 0) ry <- 1
54
+
55
+ # Pas de déplacement relatif à l'étendue du graphe
56
+ pas <- 0.012 * max(rx, ry)
57
+
58
+ x2 <- x
59
+ y2 <- y
60
+
61
+ rects <- vector("list", n)
62
+
63
+ for (i in seq_len(n)) {
64
+ lab <- labels[i]
65
+ cexi <- cex_vec[i]
66
+
67
+ wi <- strwidth(lab, units = "user", cex = cexi)
68
+ hi <- strheight(lab, units = "user", cex = cexi)
69
+
70
+ if (!is.finite(wi) || wi == 0) wi <- 0.02 * rx
71
+ if (!is.finite(hi) || hi == 0) hi <- 0.02 * ry
72
+
73
+ xi <- x2[i]
74
+ yi <- y2[i]
75
+
76
+ ri <- list(
77
+ xmin = xi - wi / 2, xmax = xi + wi / 2,
78
+ ymin = yi - hi / 2, ymax = yi + hi / 2
79
+ )
80
+
81
+ collision <- FALSE
82
+ if (i > 1) {
83
+ for (j in seq_len(i - 1)) {
84
+ if (!is.null(rects[[j]]) && .rectangles_chevauchent(ri, rects[[j]])) {
85
+ collision <- TRUE
86
+ break
87
+ }
88
+ }
89
+ }
90
+
91
+ if (!collision) {
92
+ rects[[i]] <- ri
93
+ next
94
+ }
95
+
96
+ # Recherche en spirale
97
+ angle <- 0
98
+ rayon <- pas
99
+
100
+ trouve <- FALSE
101
+ for (k in seq_len(max_iter)) {
102
+ angle <- angle + 0.65
103
+ rayon <- rayon + pas * 0.15
104
+
105
+ xi_try <- x[i] + rayon * cos(angle)
106
+ yi_try <- y[i] + rayon * sin(angle)
107
+
108
+ ri_try <- list(
109
+ xmin = xi_try - wi / 2, xmax = xi_try + wi / 2,
110
+ ymin = yi_try - hi / 2, ymax = yi_try + hi / 2
111
+ )
112
+
113
+ collision2 <- FALSE
114
+ if (i > 1) {
115
+ for (j in seq_len(i - 1)) {
116
+ if (!is.null(rects[[j]]) && .rectangles_chevauchent(ri_try, rects[[j]])) {
117
+ collision2 <- TRUE
118
+ break
119
+ }
120
+ }
121
+ }
122
+
123
+ if (!collision2) {
124
+ x2[i] <- xi_try
125
+ y2[i] <- yi_try
126
+ rects[[i]] <- ri_try
127
+ trouve <- TRUE
128
+ break
129
+ }
130
+ }
131
+
132
+ if (!trouve) {
133
+ rects[[i]] <- ri
134
+ }
135
+ }
136
+
137
+ list(x = x2, y = y2)
138
+ }
139
+
140
+ # Calcul des résidus standardisés par colonne (terme/modalité) et par classe
141
+ .calculer_residus_standardises <- function(tab) {
142
+ tab <- as.matrix(tab)
143
+ if (any(tab < 0, na.rm = TRUE)) stop("AFC : table de contingence invalide (valeurs négatives).")
144
+ n <- sum(tab)
145
+ if (!is.finite(n) || n <= 0) stop("AFC : table de contingence vide (somme nulle).")
146
+
147
+ rs <- rowSums(tab)
148
+ cs <- colSums(tab)
149
+ attendu <- outer(rs, cs) / n
150
+
151
+ # Résidus standardisés : (O - E) / sqrt(E)
152
+ res <- (tab - attendu) / sqrt(attendu)
153
+ res[!is.finite(res)] <- 0
154
+ list(attendu = attendu, residus = res)
155
+ }
156
+
157
+ # Statistiques globales d'association d'une colonne (terme/modalité) avec les classes
158
+ # chi2 et p_value : test sur la distribution observée vs attendue sur les classes
159
+ .calculer_stats_colonnes <- function(tab, seuil_p = 0.05) {
160
+ tab <- as.matrix(tab)
161
+ n <- sum(tab)
162
+ rs <- rowSums(tab)
163
+ cs <- colSums(tab)
164
+ k <- nrow(tab)
165
+
166
+ exp_mat <- outer(rs, cs) / n
167
+ exp_mat[!is.finite(exp_mat)] <- 0
168
+
169
+ out <- data.frame(
170
+ feature = colnames(tab),
171
+ frequency = as.numeric(cs),
172
+ chi2 = NA_real_,
173
+ p_value = NA_real_,
174
+ Classe_max = NA_character_,
175
+ resid_max = NA_real_,
176
+ stringsAsFactors = FALSE
177
+ )
178
+
179
+ rr <- .calculer_residus_standardises(tab)$residus
180
+
181
+ for (j in seq_len(ncol(tab))) {
182
+ obs <- tab[, j]
183
+ expv <- exp_mat[, j]
184
+
185
+ # chi2 global sur classes
186
+ good <- expv > 0
187
+ if (sum(good) >= 2) {
188
+ chi2j <- sum((obs[good] - expv[good])^2 / expv[good])
189
+ df <- max(1, sum(good) - 1)
190
+ pv <- suppressWarnings(stats::pchisq(chi2j, df = df, lower.tail = FALSE))
191
+ } else {
192
+ chi2j <- NA_real_
193
+ pv <- NA_real_
194
+ }
195
+
196
+ out$chi2[j] <- as.numeric(chi2j)
197
+ out$p_value[j] <- as.numeric(pv)
198
+
199
+ # classe de surreprésentation : max résidu standardisé
200
+ rj <- rr[, j]
201
+ imax <- which.max(rj)
202
+ out$Classe_max[j] <- rownames(tab)[imax]
203
+ out$resid_max[j] <- rj[imax]
204
+
205
+ }
206
+
207
+ out
208
+ }
209
+
210
+ # Construit la table Classes × Termes à partir d'un dfm quanteda
211
+ calculer_table_classes_termes <- function(dfm_obj, groupes, termes_cibles = NULL, max_termes = 400) {
212
+ if (!inherits(dfm_obj, "dfm")) stop("AFC : dfm_obj doit être un objet quanteda::dfm.")
213
+ if (is.null(groupes) || length(groupes) != quanteda::ndoc(dfm_obj)) stop("AFC : 'groupes' doit avoir la même longueur que ndoc(dfm_obj).")
214
+
215
+ g <- suppressWarnings(as.integer(groupes))
216
+ g[!is.finite(g) | is.na(g) | g <= 0] <- NA_integer_
217
+
218
+ keep <- !is.na(g)
219
+ dfm2 <- dfm_obj[keep, ]
220
+ g2 <- as.character(g[keep])
221
+
222
+ if (quanteda::ndoc(dfm2) < 2) stop("AFC : pas assez de segments classés (hors NA).")
223
+
224
+ # Sélection des termes
225
+ if (!is.null(termes_cibles)) {
226
+ termes_cibles <- unique(as.character(termes_cibles))
227
+ termes_cibles <- termes_cibles[!is.na(termes_cibles) & nzchar(termes_cibles)]
228
+ termes_cibles <- intersect(termes_cibles, quanteda::featnames(dfm2))
229
+ if (length(termes_cibles) >= 2) {
230
+ dfm2 <- dfm2[, termes_cibles]
231
+ }
232
+ }
233
+
234
+ if (quanteda::nfeat(dfm2) > max_termes) {
235
+ top <- quanteda::topfeatures(dfm2, n = max_termes)
236
+ dfm2 <- dfm2[, names(top)]
237
+ }
238
+
239
+ if (quanteda::nfeat(dfm2) < 2) stop("AFC : moins de 2 termes disponibles pour l'AFC.")
240
+
241
+ dfm_g <- quanteda::dfm_group(dfm2, groups = g2)
242
+ mat <- as.matrix(dfm_g)
243
+ if (nrow(mat) < 2 || ncol(mat) < 2) stop("AFC : table Classes × Termes trop petite.")
244
+
245
+ # Noms des classes pour affichage
246
+ rn <- rownames(mat)
247
+ rn2 <- paste0("Classe ", rn)
248
+ rownames(mat) <- rn2
249
+
250
+ mat
251
+ }
252
+
253
+ # Exécution AFC classes × termes
254
+ executer_afc_classes <- function(dfm_obj, groupes, termes_cibles = NULL, max_termes = 400, seuil_p = 0.05, rv = NULL) {
255
+ verifier_factominer()
256
+
257
+ tab <- calculer_table_classes_termes(
258
+ dfm_obj = dfm_obj,
259
+ groupes = groupes,
260
+ termes_cibles = termes_cibles,
261
+ max_termes = max_termes
262
+ )
263
+
264
+ ca <- FactoMineR::CA(tab, graph = FALSE)
265
+
266
+ rowcoord <- ca$row$coord
267
+ colcoord <- ca$col$coord
268
+
269
+ # Stats des termes (globales sur la table AFC)
270
+ st <- .calculer_stats_colonnes(tab, seuil_p = seuil_p)
271
+ names(st)[names(st) == "feature"] <- "Terme"
272
+
273
+ # Harmonisation : noms de classes déjà "Classe X"
274
+ st$Classe_max <- as.character(st$Classe_max)
275
+ # Dans .calculer_stats_colonnes, les classes sont rownames(tab), donc déjà "Classe X"
276
+
277
+ list(
278
+ table = tab,
279
+ ca = ca,
280
+ rowcoord = rowcoord,
281
+ colcoord = colcoord,
282
+ termes_stats = st,
283
+ seuil_p = seuil_p
284
+ )
285
+ }
286
+
287
+ # Tracé AFC des classes uniquement
288
+ tracer_afc_classes_seules <- function(obj, axes = c(1, 2), cex_labels = 1.0) {
289
+ if (is.null(obj$ca) || is.null(obj$rowcoord)) stop("AFC classes : objet incomplet.")
290
+ ax1 <- axes[1]
291
+ ax2 <- axes[2]
292
+
293
+ rc <- obj$rowcoord
294
+ x_c <- rc[, ax1]
295
+ y_c <- rc[, ax2]
296
+
297
+ lim <- calculer_lim_sym(x_c, y_c)
298
+ plot(
299
+ 0, 0,
300
+ type = "n",
301
+ xlab = paste0("Axe ", ax1),
302
+ ylab = paste0("Axe ", ax2),
303
+ xlim = lim, ylim = lim
304
+ )
305
+ abline(h = 0, v = 0, col = "gray80")
306
+
307
+ points(x_c, y_c, pch = 19, cex = 1.3)
308
+ text(x_c, y_c, labels = rownames(rc), pos = 3, cex = cex_labels)
309
+ }
310
+
311
+ # Tracé AFC classes + termes
312
+ # - couleurs des mots : selon la classe où ils sont le plus surreprésentés (Classe_max)
313
+ # - taille des mots : au choix (frequency ou chi2)
314
+ # - anti-chevauchement : option activer_repel
315
+ tracer_afc_classes_termes <- function(
316
+ obj,
317
+ axes = c(1, 2),
318
+ top_termes = 120,
319
+ taille_sel = c("frequency", "chi2"),
320
+ activer_repel = TRUE,
321
+ cex_min = 0.8,
322
+ cex_max = 2.0,
323
+ repel_max_iter = 220
324
+ ) {
325
+ if (is.null(obj$ca) || is.null(obj$rowcoord) || is.null(obj$colcoord) || is.null(obj$termes_stats)) {
326
+ stop("AFC : objet incomplet (coordonnées / stats manquantes).")
327
+ }
328
+
329
+ taille_sel <- match.arg(taille_sel)
330
+ ax1 <- axes[1]
331
+ ax2 <- axes[2]
332
+
333
+ rc <- obj$rowcoord
334
+ cc <- obj$colcoord
335
+ st <- obj$termes_stats
336
+
337
+ st <- st[!is.na(st$Terme) & nzchar(st$Terme), , drop = FALSE]
338
+ st <- st[order(-st$frequency), , drop = FALSE]
339
+
340
+ if (!is.null(top_termes) && is.finite(top_termes) && nrow(st) > top_termes) {
341
+ st <- st[seq_len(top_termes), , drop = FALSE]
342
+ }
343
+
344
+ st <- st[st$Terme %in% rownames(cc), , drop = FALSE]
345
+ if (nrow(st) < 2) {
346
+ plot.new()
347
+ text(0.5, 0.5, "AFC : pas assez de termes à tracer.", cex = 1.1)
348
+ return(invisible(NULL))
349
+ }
350
+
351
+ mots <- st$Terme
352
+ xy_m <- cc[mots, , drop = FALSE]
353
+ x_m <- xy_m[, ax1]
354
+ y_m <- xy_m[, ax2]
355
+
356
+ x_c <- rc[, ax1]
357
+ y_c <- rc[, ax2]
358
+
359
+ lim <- calculer_lim_sym(c(x_m, x_c), c(y_m, y_c))
360
+ plot(
361
+ 0, 0,
362
+ type = "n",
363
+ xlab = paste0("Axe ", ax1),
364
+ ylab = paste0("Axe ", ax2),
365
+ xlim = lim, ylim = lim
366
+ )
367
+ abline(h = 0, v = 0, col = "gray80")
368
+
369
+ # Note sur la technique : si activer_repel=TRUE, on applique un placement itératif en spirale
370
+ # avec tests de collision rectangles afin de réduire le chevauchement des étiquettes.
371
+ # Cela ne garantit pas 0 collision dans tous les cas, mais réduit fortement les superpositions.
372
+ points(x_c, y_c, pch = 19, cex = 1.25)
373
+ text(x_c, y_c, labels = rownames(rc), pos = 3, cex = 1.0)
374
+
375
+ # Couleurs par classe
376
+ classes <- sort(unique(rownames(rc)))
377
+ ncl <- length(classes)
378
+ pal <- if (requireNamespace("RColorBrewer", quietly = TRUE) && ncl <= 8) {
379
+ RColorBrewer::brewer.pal(max(3, ncl), "Set2")[seq_len(ncl)]
380
+ } else {
381
+ grDevices::rainbow(ncl)
382
+ }
383
+ col_map <- setNames(pal, classes)
384
+ col_m <- col_map[st$Classe_max]
385
+ col_m[is.na(col_m)] <- "gray40"
386
+
387
+ # Tailles
388
+ poids <- if (taille_sel == "chi2") st$chi2 else st$frequency
389
+ poids <- suppressWarnings(as.numeric(poids))
390
+ poids[!is.finite(poids)] <- 0
391
+ poids <- pmax(0, poids)
392
+
393
+ if (max(poids) == min(poids)) {
394
+ cex_vec <- rep((cex_min + cex_max) / 2, length(poids))
395
+ } else {
396
+ v <- sqrt(poids)
397
+ v <- (v - min(v)) / (max(v) - min(v))
398
+ cex_vec <- cex_min + v * (cex_max - cex_min)
399
+ }
400
+
401
+ if (isTRUE(activer_repel)) {
402
+ coords <- placer_labels_sans_chevauchement_spirale(
403
+ x = x_m, y = y_m, labels = mots, cex_vec = cex_vec, max_iter = repel_max_iter
404
+ )
405
+ x_lab <- coords$x
406
+ y_lab <- coords$y
407
+ } else {
408
+ x_lab <- x_m
409
+ y_lab <- y_m
410
+ }
411
+
412
+ points(x_lab, y_lab, pch = 16, cex = 0.5, col = col_m)
413
+ text(x_lab, y_lab, labels = mots, cex = cex_vec, col = col_m)
414
+ }
415
+
416
+ # Fallback : extraction de modalités depuis une ligne IRaMuTeQ (si docvars vides)
417
+ .extraire_modalites_depuis_ligne_iramuteq <- function(textes) {
418
+ if (length(textes) == 0) return(vector("list", 0))
419
+ res <- vector("list", length(textes))
420
+ for (i in seq_along(textes)) {
421
+ tx <- textes[i]
422
+ if (is.na(tx) || !nzchar(tx)) {
423
+ res[[i]] <- character(0)
424
+ next
425
+ }
426
+ # On cherche des tokens commençant par * dans les 400 premiers caractères
427
+ head <- substr(tx, 1, 400)
428
+ mods <- unlist(regmatches(head, gregexpr("\\*[A-Za-z0-9_\\-]+", head, perl = TRUE)), use.names = FALSE)
429
+ mods <- unique(mods)
430
+ mods <- mods[!is.na(mods) & nzchar(mods)]
431
+ res[[i]] <- mods
432
+ }
433
+ res
434
+ }
435
+
436
+ # Construction table Classes × Modalités à partir des docvars (ou fallback texte)
437
+ calculer_table_classes_modalites <- function(corpus_aligne, groupes, max_modalites = 400) {
438
+ if (is.null(corpus_aligne)) stop("AFC variables étoilées : corpus_aligne requis.")
439
+ dv <- quanteda::docvars(corpus_aligne)
440
+
441
+ g <- suppressWarnings(as.integer(groupes))
442
+ g[!is.finite(g) | is.na(g) | g <= 0] <- NA_integer_
443
+ keep <- !is.na(g)
444
+ if (sum(keep) < 2) stop("AFC variables étoilées : pas assez de segments classés (hors NA).")
445
+
446
+ dv2 <- dv[keep, , drop = FALSE]
447
+ g2 <- as.character(g[keep])
448
+
449
+ # Colonnes candidates : on exclut les colonnes techniques
450
+ exclure <- c("segment_source", "Classes")
451
+ cols <- setdiff(colnames(dv2), exclure)
452
+ cols <- cols[!is.na(cols) & nzchar(cols)]
453
+
454
+ # Priorité aux variables étoilées importées depuis l'entête IRaMuTeQ.
455
+ # Certains imports exposent les variables avec un nom préfixé "*".
456
+ cols_etoilees <- cols[grepl("^\\*", cols)]
457
+ if (length(cols_etoilees) > 0) {
458
+ cols <- cols_etoilees
459
+ }
460
+
461
+ modalites_par_seg <- NULL
462
+
463
+ if (length(cols) > 0) {
464
+ modalites_par_seg <- vector("list", nrow(dv2))
465
+ for (i in seq_len(nrow(dv2))) {
466
+ mods <- character(0)
467
+ for (cn in cols) {
468
+ if (is.na(cn) || !nzchar(cn)) next
469
+ v <- dv2[i, cn]
470
+ v <- as.character(v)
471
+ v <- v[!is.na(v) & nzchar(v)]
472
+ if (length(v) == 0) next
473
+ v <- gsub("\\s+", " ", trimws(v), perl = TRUE)
474
+ if (!nzchar(v)) next
475
+ # Modalité de type "var=valeur"
476
+ # Si la colonne est déjà une variable étoilée, on garde une modalité
477
+ # compacte de type "*var_valeur" pour rester proche de la syntaxe IRaMuTeQ.
478
+ if (isTRUE(grepl("^\\*", cn))) {
479
+ val_norm <- gsub("\\s+", "_", v, perl = TRUE)
480
+ val_norm <- gsub("[^[:alnum:]_\\-]", "_", val_norm, perl = TRUE)
481
+ val_norm <- gsub("_+", "_", val_norm, perl = TRUE)
482
+ val_norm <- gsub("^_+|_+$", "", val_norm, perl = TRUE)
483
+ if (nzchar(val_norm)) {
484
+ mods <- c(mods, paste0(cn, "_", val_norm))
485
+ }
486
+ } else {
487
+ mods <- c(mods, paste0(cn, "=", v))
488
+ }
489
+ }
490
+ modalites_par_seg[[i]] <- unique(mods)
491
+ }
492
+ }
493
+
494
+ # Fallback si aucune modalité trouvée via docvars
495
+ if (is.null(modalites_par_seg) || all(vapply(modalites_par_seg, length, integer(1)) == 0)) {
496
+ textes <- as.character(corpus_aligne)[keep]
497
+ modalites_par_seg <- .extraire_modalites_depuis_ligne_iramuteq(textes)
498
+ }
499
+
500
+ all_mods <- unique(unlist(modalites_par_seg, use.names = FALSE))
501
+ all_mods <- all_mods[!is.na(all_mods) & nzchar(all_mods)]
502
+ if (length(all_mods) < 2) stop("AFC variables étoilées : aucune modalité détectée.")
503
+
504
+ # Comptages classes × modalités (présence/absence par segment)
505
+ classes <- unique(g2)
506
+ classes <- classes[!is.na(classes)]
507
+ classes <- sort(classes)
508
+
509
+ tab <- matrix(0, nrow = length(classes), ncol = length(all_mods))
510
+ rownames(tab) <- paste0("Classe ", classes)
511
+ colnames(tab) <- all_mods
512
+
513
+ # Remplissage
514
+ for (i in seq_along(g2)) {
515
+ cl <- g2[i]
516
+ if (is.na(cl)) next
517
+ mods <- modalites_par_seg[[i]]
518
+ if (length(mods) == 0) next
519
+ mods <- intersect(mods, all_mods)
520
+ if (length(mods) == 0) next
521
+ tab[paste0("Classe ", cl), mods] <- tab[paste0("Classe ", cl), mods] + 1L
522
+ }
523
+
524
+ # Filtrage éventuel top modalités par fréquence
525
+ freq <- colSums(tab)
526
+ keepm <- freq > 0
527
+ tab <- tab[, keepm, drop = FALSE]
528
+ if (ncol(tab) < 2) stop("AFC variables étoilées : table trop pauvre après filtrage.")
529
+
530
+ if (ncol(tab) > max_modalites) {
531
+ ord <- order(colSums(tab), decreasing = TRUE)
532
+ tab <- tab[, ord[seq_len(max_modalites)], drop = FALSE]
533
+ }
534
+
535
+ tab
536
+ }
537
+
538
+ # Exécution AFC classes × variables étoilées
539
+ executer_afc_variables_etoilees <- function(corpus_aligne, groupes, max_modalites = 400, seuil_p = 0.05, rv = NULL) {
540
+ verifier_factominer()
541
+
542
+ tab <- calculer_table_classes_modalites(
543
+ corpus_aligne = corpus_aligne,
544
+ groupes = groupes,
545
+ max_modalites = max_modalites
546
+ )
547
+
548
+ if (!is.null(rv)) {
549
+ ajouter_log(
550
+ rv,
551
+ paste0(
552
+ "AFC variables étoilées : table construite (",
553
+ nrow(tab),
554
+ " classes × ",
555
+ ncol(tab),
556
+ " modalités)."
557
+ )
558
+ )
559
+ }
560
+
561
+ ca <- FactoMineR::CA(tab, graph = FALSE)
562
+ rowcoord <- ca$row$coord
563
+ colcoord <- ca$col$coord
564
+
565
+ st <- .calculer_stats_colonnes(tab, seuil_p = seuil_p)
566
+ names(st)[names(st) == "feature"] <- "Modalite"
567
+
568
+ list(
569
+ table = tab,
570
+ ca = ca,
571
+ rowcoord = rowcoord,
572
+ colcoord = colcoord,
573
+ modalites_stats = st,
574
+ seuil_p = seuil_p
575
+ )
576
+ }
577
+
578
+ # Tracé AFC classes + variables étoilées
579
+ tracer_afc_variables_etoilees <- function(
580
+ obj,
581
+ axes = c(1, 2),
582
+ top_modalites = 120,
583
+ activer_repel = TRUE,
584
+ cex_min = 0.8,
585
+ cex_max = 2.0,
586
+ repel_max_iter = 220
587
+ ) {
588
+ if (is.null(obj$ca) || is.null(obj$rowcoord) || is.null(obj$colcoord) || is.null(obj$modalites_stats)) {
589
+ stop("AFC variables étoilées : objet incomplet (coordonnées / stats manquantes).")
590
+ }
591
+
592
+ ax1 <- axes[1]
593
+ ax2 <- axes[2]
594
+
595
+ rc <- obj$rowcoord
596
+ cc <- obj$colcoord
597
+ st <- obj$modalites_stats
598
+
599
+ st <- st[!is.na(st$Modalite) & nzchar(st$Modalite), , drop = FALSE]
600
+ st <- st[order(-st$frequency), , drop = FALSE]
601
+
602
+ if (!is.null(top_modalites) && is.finite(top_modalites) && nrow(st) > top_modalites) {
603
+ st <- st[seq_len(top_modalites), , drop = FALSE]
604
+ }
605
+
606
+ st <- st[st$Modalite %in% rownames(cc), , drop = FALSE]
607
+ if (nrow(st) < 2) {
608
+ plot.new()
609
+ text(0.5, 0.5, "AFC variables étoilées : pas assez de modalités à tracer.", cex = 1.1)
610
+ return(invisible(NULL))
611
+ }
612
+
613
+ mods <- st$Modalite
614
+ xy_m <- cc[mods, , drop = FALSE]
615
+ x_m <- xy_m[, ax1]
616
+ y_m <- xy_m[, ax2]
617
+
618
+ x_c <- rc[, ax1]
619
+ y_c <- rc[, ax2]
620
+
621
+ lim <- calculer_lim_sym(c(x_m, x_c), c(y_m, y_c))
622
+ plot(
623
+ 0, 0,
624
+ type = "n",
625
+ xlab = paste0("Axe ", ax1),
626
+ ylab = paste0("Axe ", ax2),
627
+ xlim = lim, ylim = lim
628
+ )
629
+ abline(h = 0, v = 0, col = "gray80")
630
+
631
+ points(x_c, y_c, pch = 19, cex = 1.25)
632
+ text(x_c, y_c, labels = rownames(rc), pos = 3, cex = 1.0)
633
+
634
+ # Couleurs par classe (classe max)
635
+ classes <- sort(unique(rownames(rc)))
636
+ ncl <- length(classes)
637
+ pal <- if (requireNamespace("RColorBrewer", quietly = TRUE) && ncl <= 8) {
638
+ RColorBrewer::brewer.pal(max(3, ncl), "Set2")[seq_len(ncl)]
639
+ } else {
640
+ grDevices::rainbow(ncl)
641
+ }
642
+ col_map <- setNames(pal, classes)
643
+ col_m <- col_map[st$Classe_max]
644
+ col_m[is.na(col_m)] <- "gray40"
645
+
646
+ # Tailles : fréquence globale des modalités
647
+ poids <- suppressWarnings(as.numeric(st$frequency))
648
+ poids[!is.finite(poids)] <- 0
649
+ poids <- pmax(0, poids)
650
+
651
+ if (max(poids) == min(poids)) {
652
+ cex_vec <- rep((cex_min + cex_max) / 2, length(poids))
653
+ } else {
654
+ v <- sqrt(poids)
655
+ v <- (v - min(v)) / (max(v) - min(v))
656
+ cex_vec <- cex_min + v * (cex_max - cex_min)
657
+ }
658
+
659
+ if (isTRUE(activer_repel)) {
660
+ coords <- placer_labels_sans_chevauchement_spirale(
661
+ x = x_m, y = y_m, labels = mods, cex_vec = cex_vec, max_iter = repel_max_iter
662
+ )
663
+ x_lab <- coords$x
664
+ y_lab <- coords$y
665
+ } else {
666
+ x_lab <- x_m
667
+ y_lab <- y_m
668
+ }
669
+
670
+ points(x_lab, y_lab, pch = 16, cex = 0.5, col = col_m)
671
+ text(x_lab, y_lab, labels = mods, cex = cex_vec, col = col_m)
672
+ }
iramuteq-like/affichage_iramuteq-like.R ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: centraliser l'affichage des résultats IRaMuTeQ-like avec des sous-onglets dédiés.
2
+
3
+ ui_resultats_chd_iramuteq <- function() {
4
+ tabPanel(
5
+ "Résultats CHD Iramuteq",
6
+ tabsetPanel(
7
+ id = "tabs_resultats_chd_iramuteq",
8
+ tabPanel(
9
+ "Dendrogramme",
10
+ tags$h3("Dendrogramme CHD (IRaMuTeQ-like)"),
11
+ radioButtons(
12
+ "iramuteq_dendro_display_method",
13
+ "Méthode d'affichage",
14
+ choices = c(
15
+ "Style IRaMuTeQ (barres + mots par classe)" = "iramuteq_blocks",
16
+ "Standard (labels près des classes)" = "standard",
17
+ "Compact (légende des termes en bas)" = "compact"
18
+ ),
19
+ selected = "iramuteq_blocks",
20
+ inline = FALSE
21
+ ),
22
+ plotOutput("plot_chd_iramuteq_dendro", height = "420px")
23
+ ),
24
+ tabPanel(
25
+ "Stats CHD",
26
+ tags$h3("Tableaux statistiques CHD par classe"),
27
+ uiOutput("ui_tables_stats_chd_iramuteq")
28
+ ),
29
+ tabPanel(
30
+ "Concordancier IRaMuTeQ-like",
31
+ tags$h3("Concordancier"),
32
+ uiOutput("ui_concordancier_iramuteq")
33
+ ),
34
+ tabPanel(
35
+ "Nuage de mots",
36
+ tags$h3("Nuage de mots par classe"),
37
+ selectInput("classe_viz_iramuteq", "Classe", choices = c("1"), selected = "1"),
38
+ uiOutput("ui_wordcloud_iramuteq")
39
+ )
40
+ )
41
+ )
42
+ }
iramuteq-like/anacor.R ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #################################################################################
2
+
3
+
4
+ write.sparse <- function (m, to) {
5
+ ## Writes in a format that SVDLIBC can read
6
+ stopifnot(inherits(m, "dgCMatrix"))
7
+ fh <- file(to, open="w")
8
+
9
+ wl <- function(...) cat(..., "\n", file=fh)
10
+
11
+ ## header
12
+ wl(dim(m), length(m@x))
13
+
14
+ globalCount <- 1
15
+ nper <- diff(m@p)
16
+ for(i in 1:ncol(m)) {
17
+ wl(nper[i]) ## Number of entries in this column
18
+ if (nper[i]==0) next
19
+ for(j in 1:nper[i]) {
20
+ wl(m@i[globalCount], m@x[m@p[i]+j])
21
+ globalCount <- globalCount+1
22
+ }
23
+ }
24
+ }
25
+
26
+ my.svd <- function(x, nu, nv, libsvdc.path=NULL, sparse.path=NULL) {
27
+ print('my.svd')
28
+ stopifnot(nu==nv)
29
+ outfile <- file.path(tempdir(),'sout')
30
+ cmd <- paste(libsvdc.path, '-o', outfile, '-d')
31
+ #rc <- system(paste("/usr/bin/svd -o /tmp/sout -d", nu, "/tmp/sparse.m"))
32
+ rc <- system(paste(cmd, nu, sparse.path))
33
+ if (rc != 0)
34
+ stop("Couldn't run external svd code")
35
+ res1 <- paste(outfile,'-S', sep='')
36
+ d <- scan(res1, skip=1)
37
+ #FIXME : sometimes, libsvdc doesn't find solution with 2 dimensions, but does with 3
38
+ if (length(d)==1) {
39
+ nu <- nu + 1
40
+ #rc <- system(paste("/usr/bin/svd -o /tmp/sout -d", nu, "/tmp/sparse.m"))
41
+ rc <- system(paste(cmd, nu, sparse.path))
42
+ d <- scan(res1, skip=1)
43
+ }
44
+ utfile <- paste(outfile, '-Ut', sep='')
45
+ ut <- matrix(scan(utfile,skip=1),nrow=nu,byrow=TRUE)
46
+ if (nrow(ut)==3) {
47
+ ut <- ut[-3,]
48
+ }
49
+ vt <- NULL
50
+ list(d=d, u=-t(ut), v=vt)
51
+ }
52
+ ###################################################################################
53
+
54
+ #from anacor package
55
+ boostana<-function (tab, ndim = 2, svd.method = 'svdR', libsvdc.path=NULL)
56
+ {
57
+ #tab <- as.matrix(tab)
58
+ if (ndim > min(dim(tab)) - 1)
59
+ stop("Too many dimensions!")
60
+ name <- deparse(substitute(tab))
61
+ if (any(is.na(tab)))
62
+ print('YA NA')
63
+ #tab <- reconstitute(tab, eps = eps)
64
+ n <- dim(tab)[1]
65
+ m <- dim(tab)[2]
66
+ N <- sum(tab)
67
+ #tab <- as.matrix(tab)
68
+ #prop <- as.vector(t(tab))/N
69
+ r <- rowSums(tab)
70
+ c <- colSums(tab)
71
+ qdim <- ndim + 1
72
+ r <- ifelse(r == 0, 1, r)
73
+ c <- ifelse(c == 0, 1, c)
74
+ print('make z')
75
+ z1 <- t(tab)/sqrt(c)
76
+ z2 <- tab/sqrt(r)
77
+ z <- t(z1) * z2
78
+ if (svd.method == 'svdlibc') {
79
+ #START NEW SVD
80
+ z <- as(z, "dgCMatrix")
81
+ tmpmat <- tempfile(pattern='sparse')
82
+ print('write sparse matrix')
83
+ write.sparse(z, tmpmat)
84
+ print('do svd')
85
+ sv <- my.svd(z, qdim, qdim, libsvdc.path=libsvdc.path, sparse.path=tmpmat)
86
+ #END NEW SVD
87
+ } else if (svd.method == 'svdR') {
88
+ print('start R svd')
89
+ sv <- svd(z, nu = qdim, nv = qdim)
90
+ print('end svd')
91
+ } else if (svd.method == 'irlba') {
92
+ if (!requireNamespace("irlba", quietly = TRUE)) {
93
+ warning("Package 'irlba' introuvable, bascule automatique vers 'svdR'.")
94
+ print('start R svd (fallback)')
95
+ sv <- svd(z, nu = qdim, nv = qdim)
96
+ print('end svd (fallback)')
97
+ } else {
98
+ print('irlba')
99
+ sv <- irlba::irlba(z, nv = qdim, nu = qdim)
100
+ print('end irlba')
101
+ }
102
+ }
103
+ sigmavec <- (sv$d)[2:qdim]
104
+ x <- ((sv$u)/sqrt(r))[, -1]
105
+ x <- x * sqrt(N)
106
+ x <- x * outer(rep(1, n), sigmavec)
107
+ dimlab <- paste("D", 1:ndim, sep = "")
108
+ colnames(x) <- dimlab# <- colnames(y) <- dimlab
109
+ rownames(x) <- rownames(tab)
110
+ result <- list(ndim = ndim, row.scores = x,
111
+ singular.values = sigmavec, eigen.values = sigmavec^2)
112
+ class(result) <- "boostanacor"
113
+ result
114
+ }
iramuteq-like/chd_afc_pipeline_iramuteq.R ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: chd_afc_pipeline.R porte une partie du pipeline d'analyse Rainette.
2
+ # Module CHD/AFC - préparation des données et génération des artefacts CHD
3
+ # Ce fichier centralise les fonctions de préparation DFM/docvars, l'ajustement de `k`,
4
+ # les utilitaires de graphes d'adjacence/cooccurrence, et la génération des exports
5
+ # CHD (PNG + HTML) afin d'alléger `app.R` sans modifier le comportement existant.
6
+
7
+ extraire_classes_alignees <- function(corpus_obj, doc_ids, nom_colonne = "Classes") {
8
+ dv <- docvars(corpus_obj)
9
+ if (!(nom_colonne %in% names(dv))) return(rep(NA_character_, length(doc_ids)))
10
+
11
+ doc_ids <- as.character(doc_ids)
12
+ dn_corpus <- as.character(docnames(corpus_obj))
13
+ idx <- match(doc_ids, dn_corpus)
14
+
15
+ out <- rep(NA_character_, length(doc_ids))
16
+ ok <- !is.na(idx)
17
+ if (any(ok)) {
18
+ out[ok] <- as.character(dv[[nom_colonne]][idx[ok]])
19
+ }
20
+
21
+ normaliser_classes(out)
22
+ }
23
+
24
+ construire_segment_source <- function(corpus_segmente) {
25
+ dv <- docvars(corpus_segmente)
26
+
27
+ if ("segment_source" %in% names(dv)) {
28
+ v <- as.character(dv$segment_source)
29
+ if (length(v) == ndoc(corpus_segmente)) return(v)
30
+ }
31
+
32
+ dn <- docnames(corpus_segmente)
33
+
34
+ if (any(grepl("_seg[0-9]+$", dn))) return(gsub("_seg[0-9]+$", "", dn))
35
+ if (any(grepl("_[0-9]+$", dn))) return(gsub("_[0-9]+$", "", dn))
36
+ if (any(grepl("-[0-9]+$", dn))) return(gsub("-[0-9]+$", "", dn))
37
+
38
+ dn
39
+ }
40
+
41
+ assurer_docvars_dfm_minimal <- function(dfm_obj, corpus_aligne) {
42
+ seg_source <- construire_segment_source(corpus_aligne)
43
+ dv <- data.frame(segment_source = seg_source, stringsAsFactors = FALSE)
44
+ rownames(dv) <- docnames(corpus_aligne)
45
+ docvars(dfm_obj) <- dv
46
+ dfm_obj
47
+ }
48
+
49
+ construire_dfm_avec_fallback_stopwords <- function(tok_base, min_docfreq, retirer_stopwords, langue_spacy, rv, libelle, source_dictionnaire = "spacy", lexique_source_stopwords = "quanteda") {
50
+ n_base <- compter_tokens(tok_base)
51
+ ajouter_log(rv, paste0(libelle, " : tokens (avant stopwords) = ", n_base))
52
+
53
+ if (isTRUE(retirer_stopwords)) {
54
+ stopwords_a_retirer <- obtenir_stopwords_analyse(
55
+ langue_spacy = langue_spacy,
56
+ source_dictionnaire = source_dictionnaire,
57
+ lexique_source_stopwords = lexique_source_stopwords,
58
+ rv = rv
59
+ )
60
+ tok_sw <- tokens_remove(tok_base, stopwords_a_retirer)
61
+ tok_sw <- tokens_tolower(tok_sw)
62
+ n_sw <- compter_tokens(tok_sw)
63
+ ajouter_log(rv, paste0(libelle, " : tokens (après stopwords) = ", n_sw))
64
+ tok_final <- tok_sw
65
+ } else {
66
+ ajouter_log(rv, paste0(libelle, " : suppression des stopwords désactivée."))
67
+ tok_final <- tokens_tolower(tok_base)
68
+ }
69
+
70
+ if (!isTRUE(retirer_stopwords) && isTRUE(min_docfreq > 1)) {
71
+ ajouter_log(rv, paste0(libelle, " : min_docfreq=", min_docfreq, " peut éliminer des nombres rares (ex: numéros de téléphone)."))
72
+ }
73
+
74
+ dfm_obj <- dfm(tok_final)
75
+ dfm_obj <- dfm_trim(dfm_obj, min_docfreq = min_docfreq)
76
+ ajouter_log(
77
+ rv,
78
+ paste0(
79
+ libelle,
80
+ " : DFM après trim = ", ndoc(dfm_obj), " docs ; ", nfeat(dfm_obj),
81
+ ifelse(isTRUE(retirer_stopwords), " termes (avec stopwords retirés)", " termes (sans suppression des stopwords)")
82
+ )
83
+ )
84
+
85
+ if (isTRUE(retirer_stopwords) && nfeat(dfm_obj) < 2) {
86
+ ajouter_log(rv, paste0(libelle, " : DFM trop pauvre avec stopwords retirés. Relance automatique sans suppression des stopwords."))
87
+ tok_final <- tokens_tolower(tok_base)
88
+ dfm_obj <- dfm(tok_final)
89
+ dfm_obj <- dfm_trim(dfm_obj, min_docfreq = min_docfreq)
90
+ ajouter_log(rv, paste0(libelle, " : DFM après trim = ", ndoc(dfm_obj), " docs ; ", nfeat(dfm_obj), " termes (sans stopwords)"))
91
+ }
92
+
93
+ list(tok = tok_final, dfm = dfm_obj)
94
+ }
95
+
96
+ supprimer_docs_vides_dfm <- function(dfm_obj, corpus_aligne, tok_aligne, rv) {
97
+ rs <- tryCatch(Matrix::rowSums(dfm_obj), error = function(e) NULL)
98
+
99
+ if (is.null(rs)) {
100
+ ajouter_log(rv, "Impossible de calculer rowSums(dfm). Aucune suppression de segments vides.")
101
+ return(list(dfm = dfm_obj, corpus = corpus_aligne, tok = tok_aligne))
102
+ }
103
+
104
+ n_vides <- sum(rs == 0)
105
+ if (n_vides > 0) {
106
+ ajouter_log(rv, paste0("Segments vides (aucun terme) détectés : ", n_vides, ". Suppression avant CHD."))
107
+ garder <- rs > 0
108
+ dfm_obj <- dfm_obj[garder, ]
109
+ noms <- docnames(dfm_obj)
110
+ corpus_aligne <- corpus_aligne[noms]
111
+ tok_aligne <- tok_aligne[noms]
112
+ }
113
+
114
+ list(dfm = dfm_obj, corpus = corpus_aligne, tok = tok_aligne)
115
+ }
116
+
117
+ calculer_k_effectif <- function(dfm_obj, k_demande, min_split_members, rv = NULL) {
118
+ n_docs <- ndoc(dfm_obj)
119
+ if (!is.finite(min_split_members) || is.na(min_split_members) || min_split_members < 1) {
120
+ min_split_members <- 1
121
+ }
122
+
123
+ k_max_theorique <- floor(n_docs / min_split_members)
124
+ if (!is.finite(k_max_theorique) || is.na(k_max_theorique)) k_max_theorique <- n_docs
125
+ k_max_theorique <- max(1, min(k_max_theorique, n_docs - 1))
126
+
127
+ k_effectif <- min(k_demande, k_max_theorique)
128
+
129
+ if (k_effectif < 2) {
130
+ stop(
131
+ "Paramètres incompatibles : min_split_members=", min_split_members,
132
+ " est trop élevé pour ", n_docs,
133
+ " segments. Réduis min_split_members ou augmente la taille du corpus segmenté."
134
+ )
135
+ }
136
+
137
+ if (!is.null(rv) && k_effectif < k_demande) {
138
+ ajouter_log(
139
+ rv,
140
+ paste0(
141
+ "k ajusté automatiquement de ", k_demande, " à ", k_effectif,
142
+ " pour respecter min_split_members=", min_split_members,
143
+ " (", n_docs, " segments disponibles)."
144
+ )
145
+ )
146
+ }
147
+
148
+ k_effectif
149
+ }
150
+
151
+ verifier_dfm_avant_rainette <- function(dfm_obj, input) {
152
+ if (ndoc(dfm_obj) < 2) {
153
+ stop("Après filtrages, il reste moins de 2 segments utilisables. Réduis les filtrages ou augmente segment_size.")
154
+ }
155
+ if (nfeat(dfm_obj) < 2) {
156
+ stop(
157
+ "Après filtrages, il reste moins de 2 termes dans le DFM. ",
158
+ "Même avec min_docfreq=1, cela arrive si le filtrage morphosyntaxique est trop strict et/ou si les stopwords retirent la majorité des formes. ",
159
+ "Élargis les catégories morphosyntaxiques ou augmente segment_size."
160
+ )
161
+ }
162
+ }
163
+
164
+ obtenir_objet_dendrogramme <- function(res) {
165
+ if (is.null(res)) return(NULL)
166
+
167
+ if (inherits(res, "hclust") || inherits(res, "dendrogram")) return(res)
168
+
169
+ if (is.list(res)) {
170
+ candidats <- c("tree", "hc", "hclust", "dendro", "dendrogram")
171
+ for (nm in candidats) {
172
+ if (!is.null(res[[nm]]) && (inherits(res[[nm]], "hclust") || inherits(res[[nm]], "dendrogram"))) {
173
+ return(res[[nm]])
174
+ }
175
+ }
176
+ }
177
+
178
+ NULL
179
+ }
180
+
181
+ construire_graphe_adjacence <- function(mat) {
182
+ if ("graph_from_adjacency_matrix" %in% getNamespaceExports("igraph")) {
183
+ igraph::graph_from_adjacency_matrix(mat, mode = "undirected", weighted = TRUE, diag = FALSE)
184
+ } else {
185
+ igraph::graph.adjacency(mat, mode = "undirected", weighted = TRUE, diag = FALSE)
186
+ }
187
+ }
188
+
189
+ generer_chd_explor_si_absente <- function(rv) {
190
+ if (is.null(rv$export_dir) || !nzchar(rv$export_dir)) return(FALSE)
191
+
192
+ explor_dir <- file.path(rv$export_dir, "explor")
193
+ dir.create(explor_dir, showWarnings = FALSE, recursive = TRUE)
194
+
195
+ chd_png <- file.path(explor_dir, "chd.png")
196
+ if (file.exists(chd_png)) return(TRUE)
197
+
198
+ chd_obj <- rv$res_chd
199
+ if (is.null(chd_obj)) chd_obj <- rv$res
200
+ if (is.null(chd_obj)) return(FALSE)
201
+
202
+ dfm_obj <- rv$dfm_chd
203
+ err_msg <- NULL
204
+
205
+ ecrire_png_secours <- function(message = NULL) {
206
+ grDevices::png(chd_png, width = 2000, height = 1500, res = 180)
207
+ tryCatch({
208
+ plot.new()
209
+ title("CHD (export)")
210
+ txt <- "CHD indisponible pour cet export."
211
+ if (!is.null(message) && nzchar(message)) {
212
+ txt <- paste0(txt, "\n", message)
213
+ }
214
+ text(0.5, 0.5, txt, cex = 1.1)
215
+ }, finally = {
216
+ try(grDevices::dev.off(), silent = TRUE)
217
+ })
218
+ file.exists(chd_png) && is.finite(file.info(chd_png)$size) && file.info(chd_png)$size > 0
219
+ }
220
+
221
+ dessiner_chd <- function(avec_dfm = FALSE) {
222
+ grDevices::png(chd_png, width = 2000, height = 1500, res = 180)
223
+ ok_plot <- FALSE
224
+
225
+ tryCatch({
226
+ if (isTRUE(avec_dfm) && !is.null(dfm_obj)) {
227
+ k_plot <- suppressWarnings(as.integer(rv$max_n_groups_chd))
228
+ if (!is.finite(k_plot) || is.na(k_plot) || k_plot < 2) {
229
+ if (!is.null(chd_obj$group)) {
230
+ k_plot <- suppressWarnings(max(as.integer(chd_obj$group), na.rm = TRUE))
231
+ }
232
+ }
233
+ if (!is.finite(k_plot) || is.na(k_plot) || k_plot < 2) k_plot <- 2L
234
+
235
+ args_plot <- list(
236
+ chd_obj,
237
+ dfm_obj,
238
+ k = k_plot,
239
+ measure = "chi2",
240
+ type = "bar",
241
+ n_terms = 20,
242
+ show_negative = FALSE,
243
+ text_size = 12
244
+ )
245
+
246
+ params_plot <- tryCatch(names(formals(rainette_plot)), error = function(e) character(0))
247
+ if ("same_scales" %in% params_plot) args_plot$same_scales <- TRUE
248
+ if ("free_scales" %in% params_plot) args_plot$free_scales <- FALSE
249
+
250
+ do.call(rainette_plot, args_plot)
251
+ } else {
252
+ rainette_plot(chd_obj)
253
+ }
254
+ ok_plot <- TRUE
255
+ }, error = function(e) {
256
+ err_msg <<- conditionMessage(e)
257
+ }, finally = {
258
+ try(grDevices::dev.off(), silent = TRUE)
259
+ })
260
+
261
+ isTRUE(ok_plot) && file.exists(chd_png) && is.finite(file.info(chd_png)$size) && file.info(chd_png)$size > 0
262
+ }
263
+
264
+ ok <- dessiner_chd(avec_dfm = FALSE)
265
+ if (!ok && !is.null(dfm_obj)) {
266
+ ok <- dessiner_chd(avec_dfm = TRUE)
267
+ }
268
+
269
+ if (!ok) {
270
+ if (!is.null(rv)) {
271
+ msg <- if (!is.null(err_msg) && nzchar(err_msg)) err_msg else "raison inconnue"
272
+ ajouter_log(rv, paste0("CHD PNG non généré (", msg, ")."))
273
+ }
274
+ if (file.exists(chd_png)) unlink(chd_png)
275
+
276
+ ok_fallback <- ecrire_png_secours(err_msg)
277
+ if (ok_fallback) {
278
+ if (!is.null(rv)) ajouter_log(rv, paste0("CHD PNG de secours généré : ", chd_png))
279
+ ok <- TRUE
280
+ }
281
+ } else if (!is.null(rv)) {
282
+ ajouter_log(rv, paste0("CHD PNG généré : ", chd_png))
283
+ }
284
+
285
+ ok
286
+ }
287
+
288
+ generer_chd_html_explor <- function(rv, chd_png_rel = NULL) {
289
+ if (is.null(rv$export_dir) || !nzchar(rv$export_dir)) return(NULL)
290
+
291
+ explor_dir <- file.path(rv$export_dir, "explor")
292
+ dir.create(explor_dir, showWarnings = FALSE, recursive = TRUE)
293
+
294
+ chd_html <- file.path(explor_dir, "chd.html")
295
+ img_part <- "<p><em>CHD non disponible dans l'export.</em></p>"
296
+ if (!is.null(chd_png_rel) && nzchar(chd_png_rel)) {
297
+ img_src <- basename(chd_png_rel)
298
+ img_part <- paste0("<p><img src='", img_src, "' style='max-width:100%;height:auto;border:1px solid #ddd;'/></p>")
299
+ }
300
+
301
+ con <- file(chd_html, open = "wt", encoding = "UTF-8")
302
+ on.exit(try(close(con), silent = TRUE), add = TRUE)
303
+
304
+ writeLines("<html><head><meta charset='utf-8'/><title>CHD Rainette</title>", con)
305
+ writeLines("<style>body{font-family:Arial,sans-serif;margin:20px;} h1{margin-top:0;}</style>", con)
306
+ writeLines("</head><body>", con)
307
+ writeLines("<h1>CHD (Rainette)</h1>", con)
308
+ writeLines(img_part, con)
309
+ writeLines("</body></html>", con)
310
+
311
+ if (!file.exists(chd_html)) return(NULL)
312
+ if (!is.finite(file.info(chd_html)$size) || file.info(chd_html)$size <= 0) return(NULL)
313
+
314
+ if (!is.null(rv)) ajouter_log(rv, paste0("CHD HTML généré : ", chd_html))
315
+ file.path("explor", "chd.html")
316
+ }
iramuteq-like/chd_engine_iramuteq.R ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: chd_engine_iramuteq.R encapsule le lancement du moteur CHD IRaMuTeQ-like.
2
+ # Ce module sert de point d'entrée dédié pour exécuter la CHD historique et reconstruire
3
+ # les classes terminales avec mincl (auto ou manuel).
4
+
5
+ .obtenir_fonction_iramuteq <- function(nom_fonction,
6
+ chemin_module = "R/chd_iramuteq.R",
7
+ env = parent.frame()) {
8
+ fn <- get0(nom_fonction, mode = "function", inherits = TRUE)
9
+ if (!is.null(fn)) return(fn)
10
+
11
+ # Répertoire du projet (quand l'app est lancée hors du dossier racine).
12
+ racine_projet <- tryCatch({
13
+ if (requireNamespace("shiny", quietly = TRUE)) {
14
+ shiny::getShinyOption("appDir")
15
+ } else {
16
+ NULL
17
+ }
18
+ }, error = function(...) NULL)
19
+
20
+ # Répertoire courant du fichier R/chd_engine_iramuteq.R, si disponible.
21
+ fichier_courant <- tryCatch({
22
+ frames <- rev(sys.frames())
23
+ ofiles <- vapply(
24
+ frames,
25
+ function(fr) {
26
+ of <- get0("ofile", envir = fr, inherits = FALSE)
27
+ if (is.null(of)) "" else as.character(of)
28
+ },
29
+ FUN.VALUE = character(1)
30
+ )
31
+ ofiles <- ofiles[nzchar(ofiles)]
32
+ if (length(ofiles)) ofiles[[1]] else ""
33
+ }, error = function(...) "")
34
+ dir_fichier_courant <- if (nzchar(fichier_courant)) dirname(normalizePath(fichier_courant, mustWork = FALSE)) else ""
35
+ racine_depuis_fichier <- if (nzchar(dir_fichier_courant)) normalizePath(file.path(dir_fichier_courant, ".."), mustWork = FALSE) else ""
36
+
37
+ candidats <- unique(c(
38
+ chemin_module,
39
+ file.path("R", "chd_iramuteq.R"),
40
+ file.path("R", "chd_iramuteq_like.R"),
41
+ if (nzchar(racine_depuis_fichier)) file.path(racine_depuis_fichier, "iramuteq-like", "chd_iramuteq_compat.R") else "",
42
+ if (nzchar(racine_depuis_fichier)) file.path(racine_depuis_fichier, "iramuteq-like", "chd_iramuteq.R") else "",
43
+ if (nzchar(racine_depuis_fichier)) file.path(racine_depuis_fichier, "R", "chd_iramuteq.R") else "",
44
+ if (!is.null(racine_projet) && nzchar(racine_projet)) file.path(racine_projet, "iramuteq-like", "chd_iramuteq_compat.R") else "",
45
+ if (!is.null(racine_projet) && nzchar(racine_projet)) file.path(racine_projet, "iramuteq-like", "chd_iramuteq.R") else "",
46
+ if (!is.null(racine_projet) && nzchar(racine_projet)) file.path(racine_projet, "R", "chd_iramuteq.R") else "",
47
+ file.path(".", "iramuteq-like", "chd_iramuteq_compat.R"),
48
+ file.path(".", "iramuteq-like", "chd_iramuteq.R"),
49
+ file.path(".", "R", "chd_iramuteq.R"),
50
+ file.path(getwd(), "iramuteq-like", "chd_iramuteq_compat.R"),
51
+ file.path(getwd(), "iramuteq-like", "chd_iramuteq.R"),
52
+ file.path(getwd(), "R", "chd_iramuteq.R")
53
+ ))
54
+ candidats <- candidats[!is.na(candidats) & nzchar(candidats)]
55
+
56
+ for (cand in candidats) {
57
+ if (file.exists(cand)) {
58
+ source(cand, encoding = "UTF-8", local = env)
59
+ fn <- get0(nom_fonction, mode = "function", inherits = TRUE)
60
+ if (!is.null(fn)) return(fn)
61
+ }
62
+ }
63
+
64
+ stop(
65
+ "Moteur CHD IRaMuTeQ-like indisponible: ", nom_fonction,
66
+ "() introuvable. Module recherché dans: ",
67
+ paste(candidats, collapse = ", "),
68
+ "."
69
+ )
70
+ }
71
+
72
+ lancer_moteur_chd_iramuteq <- function(
73
+ dfm_obj,
74
+ k,
75
+ mincl_mode = c("auto", "manuel"),
76
+ mincl = 0,
77
+ classif_mode = c("simple", "double"),
78
+ svd_method = c("irlba", "svdR"),
79
+ mode_patate = FALSE,
80
+ libsvdc_path = NULL,
81
+ binariser = TRUE,
82
+ rscripts_dir = NULL
83
+ ) {
84
+ mincl_mode <- match.arg(mincl_mode)
85
+ classif_mode <- match.arg(classif_mode)
86
+ svd_method <- match.arg(svd_method)
87
+
88
+ calculer_chd_iramuteq_fn <- .obtenir_fonction_iramuteq("calculer_chd_iramuteq", env = environment())
89
+ reconstruire_classes_terminales_iramuteq_fn <- .obtenir_fonction_iramuteq("reconstruire_classes_terminales_iramuteq", env = environment())
90
+
91
+ chd_obj <- calculer_chd_iramuteq_fn(
92
+ dfm_obj = dfm_obj,
93
+ k = k,
94
+ mode_patate = mode_patate,
95
+ svd_method = svd_method,
96
+ libsvdc_path = libsvdc_path,
97
+ binariser = binariser,
98
+ rscripts_dir = rscripts_dir
99
+ )
100
+
101
+ classes_obj <- reconstruire_classes_terminales_iramuteq_fn(
102
+ chd_obj = chd_obj,
103
+ mincl = mincl,
104
+ mincl_mode = mincl_mode,
105
+ classif_mode = classif_mode,
106
+ nb_classes_cible = NULL,
107
+ respecter_nb_classes = FALSE
108
+ )
109
+
110
+ list(
111
+ engine = "iramuteq-like",
112
+ chd = chd_obj,
113
+ classes = classes_obj$classes,
114
+ terminales = classes_obj$terminales,
115
+ mincl = classes_obj$mincl
116
+ )
117
+ }
iramuteq-like/chd_iramuteq.R ADDED
@@ -0,0 +1,892 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: chd_iramuteq.R introduit une base "IRaMuTeQ-like" pour la CHD.
2
+ # Ce module prépare les entrées de CHD en respectant les options de nettoyage de l'application,
3
+ # expose des utilitaires pour le calcul de mincl (convention IRaMuTeQ texte),
4
+ # et fournit un calcul CHD réel en s'appuyant sur les scripts R historiques d'IRaMuTeQ.
5
+
6
+ # Valeur mincl automatique (mode texte IRaMuTeQ):
7
+ # mincl = round(n_uce / ind), avec ind = nbcl * 2 (double) sinon nbcl.
8
+ calculer_mincl_auto_iramuteq <- function(n_uce, nbcl, classif_mode = c("double", "simple")) {
9
+ classif_mode <- match.arg(classif_mode)
10
+ n_uce <- as.integer(n_uce)
11
+ nbcl <- as.integer(nbcl)
12
+
13
+ if (!is.finite(n_uce) || is.na(n_uce) || n_uce < 1) {
14
+ stop("mincl auto IRaMuTeQ: n_uce invalide.")
15
+ }
16
+ if (!is.finite(nbcl) || is.na(nbcl) || nbcl < 1) {
17
+ stop("mincl auto IRaMuTeQ: nbcl invalide.")
18
+ }
19
+
20
+ ind <- if (identical(classif_mode, "double")) nbcl * 2L else nbcl
21
+ mincl <- round(n_uce / ind)
22
+ as.integer(max(1L, mincl))
23
+ }
24
+
25
+ # Normalise une liste d'options de nettoyage selon les clés utilisées dans l'UI.
26
+ normaliser_options_nettoyage_iramuteq <- function(options_nettoyage = list()) {
27
+ opts <- list(
28
+ nettoyage_caracteres = isTRUE(options_nettoyage$nettoyage_caracteres),
29
+ forcer_minuscules_avant = isTRUE(options_nettoyage$forcer_minuscules_avant),
30
+ supprimer_chiffres = isTRUE(options_nettoyage$supprimer_chiffres),
31
+ supprimer_apostrophes = isTRUE(options_nettoyage$supprimer_apostrophes),
32
+ supprimer_ponctuation = isTRUE(options_nettoyage$supprimer_ponctuation),
33
+ retirer_stopwords = isTRUE(options_nettoyage$retirer_stopwords)
34
+ )
35
+ opts
36
+ }
37
+
38
+ # Prépare textes/tokens/dfm en tenant compte des options de nettoyage existantes de l'application.
39
+ preparer_entrees_chd_iramuteq <- function(
40
+ textes,
41
+ langue = "fr",
42
+ options_nettoyage = list(),
43
+ appliquer_nettoyage_fun = NULL
44
+ ) {
45
+ if (!is.character(textes)) {
46
+ textes <- as.character(textes)
47
+ }
48
+ textes[is.na(textes)] <- ""
49
+
50
+ opts <- normaliser_options_nettoyage_iramuteq(options_nettoyage)
51
+
52
+ if (is.null(appliquer_nettoyage_fun)) {
53
+ if (exists("appliquer_nettoyage_et_minuscules", mode = "function", inherits = TRUE)) {
54
+ appliquer_nettoyage_fun <- get("appliquer_nettoyage_et_minuscules", mode = "function", inherits = TRUE)
55
+ } else {
56
+ op <- par(no.readonly = TRUE)
57
+ on.exit(par(op), add = TRUE)
58
+ par(mar = c(3.0, 2.6, 3.4, 8.5))
59
+
60
+ appliquer_nettoyage_fun <- function(textes,
61
+ activer_nettoyage = FALSE,
62
+ forcer_minuscules = FALSE,
63
+ supprimer_chiffres = FALSE,
64
+ supprimer_apostrophes = FALSE) {
65
+ x <- as.character(textes)
66
+ if (isTRUE(forcer_minuscules)) x <- tolower(x)
67
+ x
68
+ }
69
+ }
70
+ }
71
+
72
+ textes_prep <- appliquer_nettoyage_fun(
73
+ textes = textes,
74
+ activer_nettoyage = opts$nettoyage_caracteres,
75
+ forcer_minuscules = opts$forcer_minuscules_avant,
76
+ supprimer_chiffres = opts$supprimer_chiffres,
77
+ supprimer_apostrophes = opts$supprimer_apostrophes
78
+ )
79
+
80
+ if (!requireNamespace("quanteda", quietly = TRUE)) {
81
+ stop("CHD IRaMuTeQ-like: package quanteda requis pour préparer les entrées.")
82
+ }
83
+
84
+ tok <- quanteda::tokens(
85
+ textes_prep,
86
+ remove_punct = opts$supprimer_ponctuation,
87
+ remove_numbers = opts$supprimer_chiffres
88
+ )
89
+
90
+ if (opts$retirer_stopwords) {
91
+ sw <- quanteda::stopwords(language = langue)
92
+ tok <- quanteda::tokens_remove(tok, pattern = sw)
93
+ }
94
+
95
+ dfm_obj <- quanteda::dfm(tok)
96
+ list(textes = textes_prep, tok = tok, dfm = dfm_obj, options = opts)
97
+ }
98
+
99
+ .trouver_fichier_insensible_casse <- function(dir_path, filename) {
100
+ if (!dir.exists(dir_path)) return(NA_character_)
101
+ files <- list.files(dir_path, full.names = TRUE)
102
+ if (length(files) == 0) return(NA_character_)
103
+ bn <- basename(files)
104
+ idx <- which(tolower(bn) == tolower(filename))
105
+ if (length(idx) == 0) return(NA_character_)
106
+ files[idx[1]]
107
+ }
108
+
109
+ .trouver_rscripts_iramuteq <- function(base_dir = NULL) {
110
+ scripts <- c("anacor.R", "CHD.R", "chdtxt.R")
111
+ candidats <- unique(c(
112
+ base_dir,
113
+ "iramuteq-like",
114
+ "iramuteq-like/Rscripts",
115
+ "iramuteq_clone_v3/Rscripts"
116
+ ))
117
+ candidats <- candidats[!is.na(candidats) & nzchar(candidats)]
118
+
119
+ for (cand in candidats) {
120
+ paths <- vapply(scripts, function(sc) .trouver_fichier_insensible_casse(cand, sc), FUN.VALUE = character(1))
121
+ if (all(!is.na(paths))) {
122
+ return(unname(paths))
123
+ }
124
+ }
125
+
126
+ stop(
127
+ "CHD IRaMuTeQ-like: scripts R introuvables. Répertoires testés: ",
128
+ paste(candidats, collapse = ", "),
129
+ ". Fichiers attendus: ",
130
+ paste(scripts, collapse = ", "),
131
+ "."
132
+ )
133
+ }
134
+
135
+ .charger_scripts_iramuteq_chd <- function(base_dir = NULL) {
136
+ paths <- .trouver_rscripts_iramuteq(base_dir)
137
+ for (p in paths) {
138
+ source(p, encoding = "UTF-8", local = .GlobalEnv)
139
+ }
140
+ invisible(paths)
141
+ }
142
+
143
+ .normaliser_n1_chd <- function(n1) {
144
+ if (is.null(n1)) return(NULL)
145
+ if (is.data.frame(n1)) n1 <- as.matrix(n1)
146
+ if (is.vector(n1)) {
147
+ n1 <- matrix(as.integer(n1), ncol = 1)
148
+ }
149
+ if (!is.matrix(n1)) return(NULL)
150
+ if (nrow(n1) < 1 || ncol(n1) < 1) return(NULL)
151
+ n1
152
+ }
153
+
154
+ # Calcul CHD IRaMuTeQ-like (algorithme historique via scripts R IRaMuTeQ).
155
+ calculer_chd_iramuteq <- function(
156
+ dfm_obj,
157
+ k = 3,
158
+ mode_patate = FALSE,
159
+ svd_method = c("irlba", "svdR"),
160
+ libsvdc_path = NULL,
161
+ binariser = TRUE,
162
+ rscripts_dir = NULL
163
+ ) {
164
+ svd_method <- match.arg(svd_method)
165
+
166
+ if (is.null(dfm_obj)) stop("CHD IRaMuTeQ-like: dfm_obj manquant.")
167
+ if (!is.finite(k) || is.na(k) || as.integer(k) < 2) stop("CHD IRaMuTeQ-like: k doit être >= 2.")
168
+
169
+ .charger_scripts_iramuteq_chd(rscripts_dir)
170
+
171
+ mat <- as.matrix(dfm_obj)
172
+ if (nrow(mat) < 2 || ncol(mat) < 2) {
173
+ stop("CHD IRaMuTeQ-like: matrice trop pauvre (>=2 lignes et >=2 colonnes requises).")
174
+ }
175
+
176
+ if (isTRUE(binariser)) {
177
+ mat <- ifelse(mat > 0, 1, 0)
178
+ }
179
+
180
+ rownames(mat) <- as.character(seq_len(nrow(mat)))
181
+
182
+ nb_tours <- as.integer(k) - 1L
183
+ if (nb_tours < 1) nb_tours <- 1L
184
+
185
+ chd <- CHD(
186
+ data.in = mat,
187
+ x = nb_tours,
188
+ mode.patate = isTRUE(mode_patate),
189
+ svd.method = svd_method,
190
+ libsvdc.path = libsvdc_path
191
+ )
192
+
193
+ n1 <- .normaliser_n1_chd(chd$n1)
194
+ if (is.null(n1) || nrow(n1) != nrow(mat)) {
195
+ stop("CHD IRaMuTeQ-like: sortie CHD invalide.")
196
+ }
197
+
198
+ chd$n1 <- n1
199
+
200
+ chd
201
+ }
202
+
203
+ # Reconstitue des classes finales depuis la sortie CHD et le principe find.terminales.
204
+ reconstruire_classes_terminales_iramuteq <- function(
205
+ chd_obj,
206
+ mincl = 0,
207
+ mincl_mode = c("auto", "manuel"),
208
+ classif_mode = c("simple", "double"),
209
+ nb_classes_cible = NULL,
210
+ respecter_nb_classes = TRUE
211
+ ) {
212
+ mincl_mode <- match.arg(mincl_mode)
213
+ classif_mode <- match.arg(classif_mode)
214
+
215
+ n1 <- .normaliser_n1_chd(chd_obj$n1)
216
+ list_mere <- chd_obj$list_mere
217
+ list_fille <- chd_obj$list_fille
218
+
219
+ if (is.null(n1) || is.null(list_mere) || is.null(list_fille)) {
220
+ stop("CHD IRaMuTeQ-like: objet chd incomplet.")
221
+ }
222
+
223
+ nbcl <- length(unique(n1[, ncol(n1)]))
224
+ nbcl <- max(2L, as.integer(nbcl))
225
+
226
+ if (mincl_mode == "auto") {
227
+ mincl_use <- calculer_mincl_auto_iramuteq(
228
+ n_uce = nrow(n1),
229
+ nbcl = nbcl,
230
+ classif_mode = classif_mode
231
+ )
232
+ } else {
233
+ op <- par(no.readonly = TRUE)
234
+ on.exit(par(op), add = TRUE)
235
+ par(mar = c(3.0, 2.6, 3.4, 8.5))
236
+
237
+ mincl_use <- as.integer(mincl)
238
+ if (!is.finite(mincl_use) || is.na(mincl_use) || mincl_use < 1) mincl_use <- 1L
239
+ }
240
+
241
+ terminales <- find.terminales(n1, list_mere, list_fille, mincl = mincl_use)
242
+ if (is.character(terminales) && length(terminales) == 1 && terminales == "no clusters") {
243
+ stop("CHD IRaMuTeQ-like: aucune classe terminale retenue.")
244
+ }
245
+
246
+ feuilles <- unique(as.integer(n1[, ncol(n1)]))
247
+
248
+ if (isTRUE(respecter_nb_classes) && !is.null(nb_classes_cible) && is.finite(nb_classes_cible)) {
249
+ nb_classes_cible <- as.integer(nb_classes_cible)
250
+ if (nb_classes_cible >= 2 && length(feuilles) == nb_classes_cible && length(unique(terminales)) != nb_classes_cible) {
251
+ terminales <- sort(feuilles)
252
+ mincl_use <- 1L
253
+ }
254
+ }
255
+
256
+ classes_finales <- rep(0L, nrow(n1))
257
+ feuilles_docs <- suppressWarnings(as.integer(n1[, ncol(n1)]))
258
+
259
+ # Reproduction fidèle de iramuteq_clone_v3/Rscripts/chdtxt.R::make.classes
260
+ # (sans la partie manipulation du tree, non nécessaire au calcul des classes docs).
261
+ cl_names <- seq_along(terminales)
262
+ for (i in seq_along(terminales)) {
263
+ cl <- suppressWarnings(as.integer(terminales[[i]]))
264
+ if (!is.finite(cl)) next
265
+
266
+ if (cl %in% feuilles) {
267
+ classes_finales[which(feuilles_docs == cl)] <- cl_names[[i]]
268
+ } else {
269
+ op <- par(no.readonly = TRUE)
270
+ on.exit(par(op), add = TRUE)
271
+ par(mar = c(3.0, 2.6, 3.4, 8.5))
272
+
273
+ filles <- suppressWarnings(as.integer(getfille(list_fille, cl, NULL)))
274
+ tochange <- intersect(filles, feuilles)
275
+ for (cl_fille in tochange) {
276
+ classes_finales[which(feuilles_docs == cl_fille)] <- cl_names[[i]]
277
+ }
278
+ }
279
+ }
280
+
281
+ classes_finales[which(is.na(classes_finales))] <- 0L
282
+ list(
283
+ classes = classes_finales,
284
+ terminales = as.integer(terminales),
285
+ mincl = mincl_use
286
+ )
287
+ }
288
+
289
+ # Calcule une table de statistiques par classe dans l'esprit des sorties IRaMuTeQ.
290
+ construire_stats_classes_iramuteq <- function(dfm_obj, classes, max_p = 1) {
291
+ if (is.null(dfm_obj)) stop("Stats IRaMuTeQ-like: dfm_obj manquant.")
292
+ if (is.null(classes)) stop("Stats IRaMuTeQ-like: classes manquantes.")
293
+
294
+ mat <- as.matrix(dfm_obj)
295
+ if (nrow(mat) != length(classes)) {
296
+ stop("Stats IRaMuTeQ-like: longueur de classes incohérente avec le DFM.")
297
+ }
298
+
299
+ classes <- as.integer(classes)
300
+ # Alignement IRaMuTeQ clone (BuildProf/chdfunct.R):
301
+ # les UCE non classées (classe 0) sont exclues du comptage
302
+ # des effectifs et des tableaux de contingence.
303
+ ok_docs <- !is.na(classes) & classes > 0L
304
+ mat <- mat[ok_docs, , drop = FALSE]
305
+ classes <- classes[ok_docs]
306
+
307
+ if (nrow(mat) < 2 || ncol(mat) < 1) return(data.frame())
308
+
309
+ # Alignement avec l'approche IRaMuTeQ historique (BuildProf):
310
+ # - contingence documentaire (présence/absence terme)
311
+ # - chi2 signé (sur/sous-représentation)
312
+ # - p-value issue de chisq.test(..., correct = FALSE)
313
+ mat_bin <- ifelse(mat > 0, 1L, 0L)
314
+ total_docs <- nrow(mat_bin)
315
+ docs_par_terme <- colSums(mat_bin)
316
+ occ_par_terme <- colSums(mat)
317
+
318
+ calc_chi_sign <- function(a, b, c, d) {
319
+ tb <- matrix(c(a, b, c, d), nrow = 2, byrow = TRUE)
320
+ chi <- suppressWarnings(stats::chisq.test(tb, correct = FALSE))
321
+ stat <- suppressWarnings(as.numeric(chi$statistic))
322
+ pval <- suppressWarnings(as.numeric(chi$p.value))
323
+ exp11 <- suppressWarnings(as.numeric(chi$expected[1, 1]))
324
+
325
+ if (!is.finite(stat) || is.na(stat)) stat <- 0
326
+ if (!is.finite(pval) || is.na(pval)) pval <- 1
327
+ if (!is.finite(exp11) || is.na(exp11)) exp11 <- a
328
+
329
+ signe <- if (a >= exp11) 1 else -1
330
+ c(chi2 = stat * signe, p = pval)
331
+ }
332
+
333
+ classes_uniques <- sort(unique(classes))
334
+ sorties <- vector("list", length(classes_uniques))
335
+
336
+ for (i in seq_along(classes_uniques)) {
337
+ cl <- classes_uniques[[i]]
338
+ in_cl <- classes == cl
339
+
340
+ docs_cl <- sum(in_cl)
341
+ if (docs_cl < 1) next
342
+
343
+ docs_terme_cl <- colSums(mat_bin[in_cl, , drop = FALSE])
344
+ docs_terme_hors <- pmax(0, docs_par_terme - docs_terme_cl)
345
+
346
+ n11 <- as.numeric(docs_terme_cl)
347
+ n12 <- as.numeric(docs_terme_hors)
348
+ n21 <- as.numeric(pmax(0, docs_cl - docs_terme_cl))
349
+ n22 <- as.numeric(pmax(0, (total_docs - docs_cl) - docs_terme_hors))
350
+
351
+ chi_p <- t(mapply(calc_chi_sign, n11, n12, n21, n22))
352
+
353
+ freq_cl <- colSums(mat[in_cl, , drop = FALSE])
354
+ docprop_cl <- if (docs_cl > 0) docs_terme_cl / docs_cl else rep(0, ncol(mat))
355
+ lr <- mapply(function(a, b, c, d) {
356
+ n <- a + b + c + d
357
+ r1 <- a + b
358
+ r2 <- c + d
359
+ c1 <- a + c
360
+ c2 <- b + d
361
+ expected <- c(r1 * c1 / n, r1 * c2 / n, r2 * c1 / n, r2 * c2 / n)
362
+ observed <- c(a, b, c, d)
363
+ idx <- observed > 0 & expected > 0
364
+ if (!any(idx)) return(0)
365
+ 2 * sum(observed[idx] * log(observed[idx] / expected[idx]))
366
+ }, n11, n12, n21, n22)
367
+
368
+ df <- data.frame(
369
+ Terme = colnames(mat),
370
+ chi2 = as.numeric(chi_p[, "chi2"]),
371
+ lr = as.numeric(lr),
372
+ frequency = as.numeric(freq_cl),
373
+ docprop = as.numeric(docprop_cl),
374
+ # Alignement IRaMuTeQ: les colonnes d'effectifs affichées en table
375
+ # correspondent aux occurrences (et non au nombre de segments contenant le terme).
376
+ # Ex.: "23/46" signifie 23 occurrences dans la classe sur 46 occurrences au total.
377
+ eff_st = as.numeric(freq_cl),
378
+ eff_total = as.numeric(occ_par_terme),
379
+ pourcentage = as.numeric(ifelse(occ_par_terme > 0, 100 * freq_cl / occ_par_terme, 0)),
380
+ # Colonnes documentaires conservées pour diagnostic (chi2 calculé sur présence/absence doc).
381
+ eff_docs_st = as.numeric(docs_terme_cl),
382
+ eff_docs_total = as.numeric(docs_par_terme),
383
+ p = as.numeric(chi_p[, "p"]),
384
+ Classe = as.integer(cl),
385
+ stringsAsFactors = FALSE
386
+ )
387
+
388
+ df <- df[is.finite(df$chi2) & !is.na(df$chi2), , drop = FALSE]
389
+ if (is.finite(max_p) && !is.na(max_p) && max_p < 1) {
390
+ df <- df[df$p <= max_p, , drop = FALSE]
391
+ }
392
+ df <- df[order(-df$chi2, -df$frequency, -occ_par_terme[df$Terme]), , drop = FALSE]
393
+ sorties[[i]] <- df
394
+ }
395
+
396
+ out <- dplyr::bind_rows(sorties)
397
+ if (!nrow(out)) return(out)
398
+
399
+ out$Classe_brut <- as.character(out$Classe)
400
+ out$p_value <- out$p
401
+ out$p_value_filter <- ifelse(out$p <= max_p, paste0("≤ ", max_p), paste0("> ", max_p))
402
+ out
403
+ }
404
+
405
+ # Dendrogramme CHD basé sur la structure hiérarchique IRaMuTeQ (list_mere/list_fille).
406
+ tracer_dendrogramme_chd_iramuteq <- function(chd_obj,
407
+ terminales = NULL,
408
+ classes = NULL,
409
+ res_stats_df = NULL,
410
+ top_n_terms = 4,
411
+ orientation = c("vertical", "horizontal"),
412
+ display_method = c("standard", "compact", "iramuteq_blocks")) {
413
+ orientation <- match.arg(orientation)
414
+ display_method <- match.arg(display_method)
415
+
416
+ if (is.null(chd_obj)) {
417
+ plot.new()
418
+ text(0.5, 0.5, "Dendrogramme CHD indisponible.", cex = 1.1)
419
+ return(invisible(NULL))
420
+ }
421
+
422
+ n1 <- .normaliser_n1_chd(chd_obj$n1)
423
+ if (is.null(chd_obj$list_fille) || is.null(n1)) {
424
+ plot.new()
425
+ text(0.5, 0.5, "Dendrogramme CHD indisponible.", cex = 1.1)
426
+ return(invisible(NULL))
427
+ }
428
+
429
+ list_fille <- chd_obj$list_fille
430
+ if (!is.list(list_fille) || length(list_fille) == 0) {
431
+ plot.new()
432
+ text(0.5, 0.5, "Dendrogramme CHD indisponible (list_fille vide).", cex = 1.1)
433
+ return(invisible(NULL))
434
+ }
435
+
436
+ noms <- names(list_fille)
437
+ if (is.null(noms) || any(!nzchar(noms))) noms <- as.character(seq_along(list_fille))
438
+ map_filles <- stats::setNames(lapply(list_fille, function(x) as.integer(x)), noms)
439
+
440
+ meres <- suppressWarnings(as.integer(names(map_filles)))
441
+ meres <- meres[is.finite(meres)]
442
+ enfants <- unique(as.integer(unlist(map_filles, use.names = FALSE)))
443
+ enfants <- enfants[is.finite(enfants)]
444
+
445
+ racines <- setdiff(meres, enfants)
446
+ racine <- if (length(racines)) racines[[1]] else if (length(meres)) meres[[1]] else NA_integer_
447
+ if (!is.finite(racine)) {
448
+ feuilles_n1 <- suppressWarnings(as.integer(n1[, ncol(n1)]))
449
+ feuilles_n1 <- feuilles_n1[is.finite(feuilles_n1)]
450
+ if (length(feuilles_n1)) racine <- min(feuilles_n1, na.rm = TRUE)
451
+ }
452
+
453
+ if (!is.finite(racine)) {
454
+ plot.new()
455
+ text(0.5, 0.5, "Structure CHD invalide.", cex = 1.1)
456
+ return(invisible(NULL))
457
+ }
458
+
459
+ get_filles <- function(node) {
460
+ key <- as.character(node)
461
+ x <- map_filles[[key]]
462
+ x <- x[is.finite(x)]
463
+ if (is.null(x)) integer(0) else as.integer(x)
464
+ }
465
+
466
+ terminales <- suppressWarnings(as.integer(terminales))
467
+ terminales <- terminales[is.finite(terminales)]
468
+ terminales <- unique(terminales)
469
+
470
+ # Source de vérité pour le nombre de classes à afficher :
471
+ # 1) résultats statistiques CHD (si disponibles), sinon
472
+ # 2) vecteur des classes finales documentaires.
473
+ # Comme dans iramuteq_clone_v3 (cutree(..., k = clnb)), on borne l'affichage
474
+ # aux classes finales réellement exploitées.
475
+ classes_utiles <- integer(0)
476
+ if (!is.null(res_stats_df) && is.data.frame(res_stats_df) && "Classe" %in% names(res_stats_df)) {
477
+ classes_stats <- suppressWarnings(as.integer(res_stats_df$Classe))
478
+ classes_stats <- classes_stats[is.finite(classes_stats) & classes_stats > 0]
479
+ classes_utiles <- sort(unique(classes_stats))
480
+ }
481
+
482
+ if (!length(classes_utiles) && !is.null(classes)) {
483
+ classes_int <- suppressWarnings(as.integer(classes))
484
+ classes_int <- classes_int[is.finite(classes_int) & classes_int > 0]
485
+ classes_utiles <- sort(unique(classes_int))
486
+ }
487
+
488
+ if (length(classes_utiles) && length(terminales)) {
489
+ idx_valides <- classes_utiles[classes_utiles >= 1L & classes_utiles <= length(terminales)]
490
+ terminales <- terminales[idx_valides]
491
+ classes_utiles <- idx_valides
492
+ }
493
+
494
+ utiliser_terminales <- length(terminales) > 0
495
+
496
+ leaves <- integer(0)
497
+ visited <- integer(0)
498
+ walk_leaves <- function(node) {
499
+ if (node %in% visited) return(invisible(NULL))
500
+ visited <<- c(visited, node)
501
+
502
+ if (isTRUE(utiliser_terminales) && node %in% terminales) {
503
+ leaves <<- c(leaves, node)
504
+ return(invisible(NULL))
505
+ }
506
+
507
+ filles <- get_filles(node)
508
+ if (!length(filles)) {
509
+ if (!isTRUE(utiliser_terminales)) {
510
+ leaves <<- c(leaves, node)
511
+ }
512
+ return(invisible(NULL))
513
+ }
514
+ for (f in filles) walk_leaves(f)
515
+ }
516
+ walk_leaves(racine)
517
+
518
+ if (isTRUE(utiliser_terminales)) {
519
+ terminales_atteintes <- intersect(unique(terminales), unique(visited))
520
+ leaves <- unique(c(leaves, terminales_atteintes))
521
+ }
522
+
523
+ if (!length(leaves) && !length(classes_utiles)) {
524
+ leaves <- sort(unique(suppressWarnings(as.integer(n1[, ncol(n1)]))))
525
+ leaves <- leaves[is.finite(leaves)]
526
+ }
527
+ if (!length(leaves)) {
528
+ plot.new()
529
+ text(0.5, 0.5, "Aucune feuille exploitable pour le dendrogramme.", cex = 1.1)
530
+ return(invisible(NULL))
531
+ }
532
+
533
+ leaves <- unique(leaves)
534
+ y_map <- stats::setNames(seq_along(leaves), as.character(leaves))
535
+ pos <- list()
536
+ seen <- integer(0)
537
+
538
+ layout_phylo <- function(node, depth = 0L) {
539
+ if (node %in% seen) return(pos[[as.character(node)]])
540
+ seen <<- c(seen, node)
541
+
542
+ if (isTRUE(utiliser_terminales) && node %in% leaves) {
543
+ y <- unname(y_map[[as.character(node)]])
544
+ if (is.null(y) || !is.finite(y)) y <- max(unname(y_map)) + 1
545
+ pos[[as.character(node)]] <<- c(x = depth, y = y)
546
+ return(pos[[as.character(node)]])
547
+ }
548
+
549
+ filles <- get_filles(node)
550
+ if (!length(filles)) {
551
+ y <- unname(y_map[[as.character(node)]])
552
+ if (is.null(y) || !is.finite(y)) y <- max(unname(y_map)) + 1
553
+ pos[[as.character(node)]] <<- c(x = depth, y = y)
554
+ return(pos[[as.character(node)]])
555
+ }
556
+
557
+ child_pos <- lapply(filles, function(f) layout_phylo(f, depth + 1L))
558
+ ys <- vapply(child_pos, function(v) as.numeric(v[["y"]]), numeric(1))
559
+ pos[[as.character(node)]] <<- c(x = depth, y = mean(ys))
560
+ return(pos[[as.character(node)]])
561
+ }
562
+
563
+ layout_phylo(racine, 0L)
564
+
565
+ if (!length(pos)) {
566
+ plot.new()
567
+ text(0.5, 0.5, "Dendrogramme CHD indisponible (positions vides).", cex = 1.1)
568
+ return(invisible(NULL))
569
+ }
570
+
571
+ all_pos <- do.call(rbind, pos)
572
+ if (is.null(dim(all_pos))) {
573
+ all_pos <- matrix(all_pos, nrow = 1L, dimnames = list(names(pos)[1], names(all_pos)))
574
+ }
575
+ all_pos <- as.matrix(all_pos)
576
+ if (is.null(colnames(all_pos)) || !all(c("x", "y") %in% colnames(all_pos))) {
577
+ plot.new()
578
+ text(0.5, 0.5, "Dendrogramme CHD indisponible (positions invalides).", cex = 1.1)
579
+ return(invisible(NULL))
580
+ }
581
+ depth_max <- max(all_pos[, "x"], na.rm = TRUE)
582
+ order_max <- max(all_pos[, "y"], na.rm = TRUE)
583
+
584
+ node_ids <- suppressWarnings(as.integer(rownames(all_pos)))
585
+ node_ids[!is.finite(node_ids)] <- NA_integer_
586
+ tip_idx <- which(node_ids %in% leaves)
587
+
588
+ tip_cols <- rep("#5B8FF9", nrow(all_pos))
589
+ if (length(terminales)) tip_cols[which(node_ids %in% terminales)] <- "#d62728"
590
+
591
+ tip_labels <- paste0("Classe ", rownames(all_pos)[tip_idx])
592
+ tip_nodes_chr <- rownames(all_pos)[tip_idx]
593
+ classe_par_noeud <- stats::setNames(rep(NA_integer_, length(tip_nodes_chr)), tip_nodes_chr)
594
+
595
+ if (length(terminales)) {
596
+ for (i in seq_along(terminales)) {
597
+ node <- terminales[[i]]
598
+ idx_node <- which(tip_nodes_chr == as.character(node))
599
+ if (!length(idx_node)) next
600
+ tip_labels[idx_node] <- paste0("Classe ", i)
601
+ classe_par_noeud[as.character(node)] <- i
602
+ }
603
+ }
604
+
605
+ if (!is.null(classes)) {
606
+ classes <- suppressWarnings(as.integer(classes))
607
+ classes <- classes[is.finite(classes) & classes > 0]
608
+ if (length(classes) && length(terminales)) {
609
+ pct_par_classe <- prop.table(table(classes)) * 100
610
+ for (i in seq_along(terminales)) {
611
+ node <- terminales[[i]]
612
+ idx_node <- which(tip_nodes_chr == as.character(node))
613
+ if (!length(idx_node)) next
614
+ pct <- unname(pct_par_classe[as.character(i)])
615
+ if (!is.finite(pct) || is.na(pct)) pct <- 0
616
+ tip_labels[idx_node] <- paste0("Classe ", i, " (", format(round(pct, 1), nsmall = 1), " %)")
617
+ }
618
+ }
619
+ }
620
+
621
+ top_n_terms <- suppressWarnings(as.integer(top_n_terms))
622
+ if (!is.finite(top_n_terms) || is.na(top_n_terms) || top_n_terms < 1L) top_n_terms <- 1L
623
+ termes_par_classe <- list()
624
+ if (!is.null(res_stats_df) && is.data.frame(res_stats_df) && nrow(res_stats_df) > 0 && all(c("Classe", "Terme") %in% names(res_stats_df))) {
625
+ df_terms <- res_stats_df
626
+ classes_num <- suppressWarnings(as.integer(df_terms$Classe))
627
+ df_terms <- df_terms[is.finite(classes_num) & !is.na(df_terms$Terme) & nzchar(as.character(df_terms$Terme)), , drop = FALSE]
628
+ df_terms$Classe <- suppressWarnings(as.integer(df_terms$Classe))
629
+
630
+ for (i in seq_along(terminales)) {
631
+ sous <- df_terms[df_terms$Classe == i, , drop = FALSE]
632
+ if (!nrow(sous)) next
633
+ if ("chi2" %in% names(sous)) {
634
+ chi <- suppressWarnings(as.numeric(sous$chi2))
635
+ chi[!is.finite(chi)] <- -Inf
636
+ sous <- sous[order(chi, decreasing = TRUE), , drop = FALSE]
637
+ }
638
+ termes <- unique(as.character(sous$Terme))
639
+ termes <- termes[nzchar(termes)]
640
+ if (length(termes)) {
641
+ termes_par_classe[[as.character(i)]] <- paste(utils::head(termes, top_n_terms), collapse = ", ")
642
+ }
643
+ }
644
+ }
645
+
646
+ if (identical(orientation, "vertical")) {
647
+ if (identical(display_method, "iramuteq_blocks")) {
648
+ op <- par(no.readonly = TRUE)
649
+ on.exit(par(op), add = TRUE)
650
+ par(mar = c(1.4, 1.4, 2.6, 1.4), xpd = NA)
651
+
652
+ class_ids <- sort(unique(na.omit(as.integer(classe_par_noeud))))
653
+ if (!length(class_ids) && length(terminales)) class_ids <- seq_along(terminales)
654
+ if (!length(class_ids)) {
655
+ plot.new()
656
+ text(0.5, 0.5, "Aucune classe terminale disponible.", cex = 1.1)
657
+ return(invisible(NULL))
658
+ }
659
+
660
+ cols_palette <- c("#ff3300", "#00ff00", "#1f3bff", "#ff00a8", "#00d4ff", "#ffaa00")
661
+ class_cols <- stats::setNames(cols_palette[(seq_along(class_ids) - 1L) %% length(cols_palette) + 1L], as.character(class_ids))
662
+
663
+ pct_par_classe <- NULL
664
+ if (!is.null(classes)) {
665
+ classes <- suppressWarnings(as.integer(classes))
666
+ classes <- classes[is.finite(classes) & classes > 0]
667
+ if (length(classes)) pct_par_classe <- prop.table(table(classes)) * 100
668
+ }
669
+
670
+ order_nodes <- tip_nodes_chr
671
+ if (!length(order_nodes) && length(terminales)) order_nodes <- as.character(terminales)
672
+ classes_ord <- suppressWarnings(as.integer(classe_par_noeud[order_nodes]))
673
+ keep <- is.finite(classes_ord)
674
+ order_nodes <- order_nodes[keep]
675
+ classes_ord <- classes_ord[keep]
676
+ if (!length(classes_ord)) classes_ord <- class_ids
677
+
678
+ n <- length(classes_ord)
679
+ x_pos <- seq(0.12, 0.88, length.out = n)
680
+
681
+ plot.new()
682
+ plot.window(xlim = c(0, 1), ylim = c(0, 1))
683
+
684
+ y_top <- 0.90
685
+ y_box_top <- 0.78
686
+ y_box_bot <- 0.64
687
+ box_w <- min(0.28, if (n > 1) 0.72 / (n - 1) else 0.3)
688
+
689
+ if (n >= 2) {
690
+ mid <- mean(x_pos)
691
+ segments(mid, y_top, min(x_pos), y_box_top + 0.02, lwd = 2.5, col = "#111111")
692
+ segments(mid, y_top, max(x_pos), y_box_top + 0.02, lwd = 2.5, col = "#111111")
693
+ }
694
+
695
+ for (i in seq_len(n)) {
696
+ cl <- classes_ord[i]
697
+ col_cl <- unname(class_cols[as.character(cl)])
698
+ if (is.na(col_cl) || !nzchar(col_cl)) col_cl <- "#1f3bff"
699
+ x <- x_pos[i]
700
+
701
+ rect(x - box_w / 2, y_box_bot, x + box_w / 2, y_box_top, col = col_cl, border = NA)
702
+ segments(x, y_box_top + 0.02, x, y_box_top, lwd = 2.5, col = "#111111")
703
+
704
+ text(x, y_box_top + 0.05, labels = paste0("classe ", cl), col = col_cl, cex = 1.2, font = 2)
705
+ pct <- if (!is.null(pct_par_classe)) unname(pct_par_classe[as.character(cl)]) else NA_real_
706
+ pct_lbl <- if (is.finite(pct)) paste0(format(round(pct, 1), trim = TRUE), " %") else ""
707
+ if (nzchar(pct_lbl)) text(x, y_box_bot + 0.015, labels = pct_lbl, cex = 0.9, col = "#111111")
708
+
709
+ termes <- character(0)
710
+ if (!is.null(termes_par_classe[[as.character(cl)]])) {
711
+ termes <- strsplit(termes_par_classe[[as.character(cl)]], "\\s*,\\s*")[[1]]
712
+ termes <- termes[nzchar(termes)]
713
+ }
714
+
715
+ if (length(termes)) {
716
+ y0 <- 0.58
717
+ step <- 0.055
718
+ for (j in seq_len(min(length(termes), 9L))) {
719
+ text(x - box_w / 2, y0 - (j - 1) * step, labels = termes[j], adj = c(0, 1), col = col_cl, cex = 1.6, font = 2)
720
+ }
721
+ }
722
+ }
723
+
724
+ title(main = "Dendrogramme CHD IRaMuTeQ-like (style IRaMuTeQ)")
725
+ return(invisible(NULL))
726
+ }
727
+
728
+ op <- par(no.readonly = TRUE)
729
+ on.exit(par(op), add = TRUE)
730
+ par(mar = c(3.2, 2.8, 3.6, 1.5))
731
+
732
+ all_pos_plot <- cbind(x = all_pos[, "y", drop = TRUE], y = depth_max - all_pos[, "x", drop = TRUE])
733
+ all_pos_plot <- as.matrix(all_pos_plot)
734
+ x_max <- max(all_pos_plot[, "x"], na.rm = TRUE)
735
+ y_max <- max(all_pos_plot[, "y"], na.rm = TRUE)
736
+
737
+ plot(
738
+ NA,
739
+ xlim = c(0.5, x_max + 0.5),
740
+ ylim = c(if (length(termes_par_classe) && identical(display_method, "standard")) -1.6 else -0.8, y_max + 0.8),
741
+ axes = FALSE,
742
+ xlab = "",
743
+ ylab = "",
744
+ main = if (identical(display_method, "compact")) "Dendrogramme CHD IRaMuTeQ-like (compact)" else "Dendrogramme CHD IRaMuTeQ-like"
745
+ )
746
+
747
+ for (mere_name in names(map_filles)) {
748
+ mere <- suppressWarnings(as.integer(mere_name))
749
+ if (!is.finite(mere)) next
750
+ p_m <- pos[[as.character(mere)]]
751
+ if (is.null(p_m)) next
752
+ p_m_plot <- c(x = p_m[["y"]], y = depth_max - p_m[["x"]])
753
+
754
+ filles <- as.integer(map_filles[[mere_name]])
755
+ filles <- filles[is.finite(filles)]
756
+ if (!length(filles)) next
757
+
758
+ x_child <- numeric(0)
759
+ for (f in filles) {
760
+ p_f <- pos[[as.character(f)]]
761
+ if (!is.null(p_f)) x_child <- c(x_child, p_f[["y"]])
762
+ }
763
+ if (length(x_child) >= 2) {
764
+ segments(min(x_child), p_m_plot[["y"]], max(x_child), p_m_plot[["y"]], col = "#2f4f4f", lwd = 1.6)
765
+ }
766
+
767
+ for (f in filles) {
768
+ p_f <- pos[[as.character(f)]]
769
+ if (is.null(p_f)) next
770
+ p_f_plot <- c(x = p_f[["y"]], y = depth_max - p_f[["x"]])
771
+ segments(p_f_plot[["x"]], p_m_plot[["y"]], p_f_plot[["x"]], p_f_plot[["y"]], col = "#2f4f4f", lwd = 1.6)
772
+ }
773
+ }
774
+
775
+ if (length(tip_idx)) {
776
+ points(all_pos_plot[tip_idx, "x", drop = TRUE], all_pos_plot[tip_idx, "y", drop = TRUE], pch = 19, col = tip_cols[tip_idx], cex = 0.95)
777
+ text(x = all_pos_plot[tip_idx, "x", drop = TRUE], y = all_pos_plot[tip_idx, "y", drop = TRUE] - 0.18, labels = tip_labels, adj = c(0.5, 1), cex = 0.78)
778
+
779
+ if (length(termes_par_classe) && identical(display_method, "standard")) {
780
+ for (j in seq_along(tip_idx)) {
781
+ node_id <- tip_nodes_chr[j]
782
+ class_id <- suppressWarnings(as.integer(classe_par_noeud[[node_id]]))
783
+ if (!is.finite(class_id)) next
784
+ termes_lbl <- termes_par_classe[[as.character(class_id)]]
785
+ if (is.null(termes_lbl) || !nzchar(termes_lbl)) next
786
+ text(
787
+ x = all_pos_plot[tip_idx[j], "x", drop = TRUE],
788
+ y = all_pos_plot[tip_idx[j], "y", drop = TRUE] - 0.56,
789
+ labels = termes_lbl,
790
+ adj = c(0.5, 1),
791
+ cex = 0.66,
792
+ col = "#333333"
793
+ )
794
+ }
795
+ }
796
+
797
+ if (length(termes_par_classe) && identical(display_method, "compact")) {
798
+ legend_lines <- unlist(lapply(sort(names(termes_par_classe)), function(cl_id) {
799
+ paste0("Classe ", cl_id, " : ", termes_par_classe[[cl_id]])
800
+ }), use.names = FALSE)
801
+
802
+ if (length(legend_lines)) {
803
+ texte_legend <- paste(legend_lines, collapse = "\n")
804
+ mtext(texte_legend, side = 1, line = 0.4, adj = 0, cex = 0.62, col = "#333333")
805
+ }
806
+ }
807
+ }
808
+ } else {
809
+ op <- par(no.readonly = TRUE)
810
+ on.exit(par(op), add = TRUE)
811
+ par(mar = c(3.0, 2.6, 3.4, 8.5))
812
+
813
+ plot(
814
+ NA,
815
+ xlim = c(-0.2, depth_max + 1.4),
816
+ ylim = c(order_max + 0.6, 0.4),
817
+ axes = FALSE,
818
+ xlab = "",
819
+ ylab = "",
820
+ main = if (identical(display_method, "compact")) "Dendrogramme CHD IRaMuTeQ-like (phylogramme compact)" else "Dendrogramme CHD IRaMuTeQ-like (phylogramme)"
821
+ )
822
+
823
+ for (mere_name in names(map_filles)) {
824
+ mere <- suppressWarnings(as.integer(mere_name))
825
+ if (!is.finite(mere)) next
826
+ p_m <- pos[[as.character(mere)]]
827
+ if (is.null(p_m)) next
828
+
829
+ filles <- as.integer(map_filles[[mere_name]])
830
+ filles <- filles[is.finite(filles)]
831
+ if (!length(filles)) next
832
+
833
+ y_child <- numeric(0)
834
+ for (f in filles) {
835
+ p_f <- pos[[as.character(f)]]
836
+ if (!is.null(p_f)) y_child <- c(y_child, p_f[["y"]])
837
+ }
838
+ if (length(y_child) >= 2) {
839
+ segments(p_m[["x"]], min(y_child), p_m[["x"]], max(y_child), col = "#2f4f4f", lwd = 1.6)
840
+ }
841
+
842
+ for (f in filles) {
843
+ p_f <- pos[[as.character(f)]]
844
+ if (is.null(p_f)) next
845
+ segments(p_m[["x"]], p_f[["y"]], p_f[["x"]], p_f[["y"]], col = "#2f4f4f", lwd = 1.6)
846
+ }
847
+ }
848
+
849
+ if (length(tip_idx)) {
850
+ points(all_pos[tip_idx, "x", drop = TRUE], all_pos[tip_idx, "y", drop = TRUE], pch = 19, col = tip_cols[tip_idx], cex = 0.95)
851
+ text(x = all_pos[tip_idx, "x", drop = TRUE] + 0.12, y = all_pos[tip_idx, "y", drop = TRUE], labels = tip_labels, adj = c(0, 0.5), cex = 0.78)
852
+
853
+ if (length(termes_par_classe) && identical(display_method, "standard")) {
854
+ for (j in seq_along(tip_idx)) {
855
+ node_id <- tip_nodes_chr[j]
856
+ class_id <- suppressWarnings(as.integer(classe_par_noeud[[node_id]]))
857
+ if (!is.finite(class_id)) next
858
+ termes_lbl <- termes_par_classe[[as.character(class_id)]]
859
+ if (is.null(termes_lbl) || !nzchar(termes_lbl)) next
860
+ text(
861
+ x = all_pos[tip_idx[j], "x", drop = TRUE] + 0.12,
862
+ y = all_pos[tip_idx[j], "y", drop = TRUE] + 0.28,
863
+ labels = termes_lbl,
864
+ adj = c(0, 0.5),
865
+ cex = 0.66,
866
+ col = "#333333"
867
+ )
868
+ }
869
+ }
870
+
871
+ if (length(termes_par_classe) && identical(display_method, "compact")) {
872
+ legend_lines <- unlist(lapply(sort(names(termes_par_classe)), function(cl_id) {
873
+ paste0("Classe ", cl_id, " : ", termes_par_classe[[cl_id]])
874
+ }), use.names = FALSE)
875
+
876
+ if (length(legend_lines)) {
877
+ legend(
878
+ "bottomright",
879
+ legend = legend_lines,
880
+ bty = "n",
881
+ cex = 0.64,
882
+ text.col = "#333333",
883
+ inset = c(-0.02, -0.02),
884
+ xpd = NA
885
+ )
886
+ }
887
+ }
888
+ }
889
+ }
890
+
891
+ invisible(NULL)
892
+ }
iramuteq-like/chdtxt.R ADDED
@@ -0,0 +1,724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Author: Pierre Ratinaud
2
+ #Copyright (c) 2008-2020 Pierre Ratinaud
3
+ #License: GNU/GPL
4
+
5
+
6
+ #fonction pour la double classification
7
+ #cette fonction doit etre splitter en 4 ou 5 fonctions
8
+
9
+ AssignClasseToUce <- function(listuce, chd) {
10
+ print('assigne classe -> uce')
11
+ chd[listuce[,2]+1,]
12
+ }
13
+
14
+ fille<-function(classe,classeuce) {
15
+ listfm<-unique(unlist(classeuce[classeuce[,classe%/%2]==classe,]))
16
+ listf<-listfm[listfm>=classe]
17
+ listf<-unique(listf)
18
+ listf
19
+ }
20
+
21
+
22
+ croiseeff <- function(croise, classeuce1, classeuce2) {
23
+ cl1 <- 0
24
+ cl2 <- 1
25
+ for (i in 1:ncol(classeuce1)) {
26
+ cl1 <- cl1 + 2
27
+ cl2 <- cl2 + 2
28
+ clj1 <- 0
29
+ clj2 <- 1
30
+ for (j in 1:ncol(classeuce2)) {
31
+ clj1 <- clj1 + 2
32
+ clj2 <- clj2 + 2
33
+ croise[cl1 - 1, clj1 -1] <- length(which(classeuce1[,i] == cl1 & classeuce2[,j] == clj1))
34
+ croise[cl1 - 1, clj2 -1] <- length(which(classeuce1[,i] == cl1 & classeuce2[,j] == clj2))
35
+ croise[cl2 - 1, clj1 -1] <- length(which(classeuce1[,i] == cl2 & classeuce2[,j] == clj1))
36
+ croise[cl2 - 1, clj2 -1] <- length(which(classeuce1[,i] == cl2 & classeuce2[,j] == clj2))
37
+ }
38
+ }
39
+ croise
40
+ }
41
+
42
+ addallfille <- function(lf) {
43
+ nlf <- list()
44
+ for (i in 1:length(lf)) {
45
+ if (! is.null(lf[[i]])) {
46
+ nlf[[i]] <- lf[[i]]
47
+ filles <- lf[[i]]
48
+ f1 <- filles[1]
49
+ f2 <- filles[2]
50
+ if (f1 > length(lf)) {
51
+ for (j in (length(lf) + 1):f2) {
52
+ nlf[[j]] <- 0
53
+ }
54
+ }
55
+ } else {
56
+ nlf[[i]] <- 0
57
+ }
58
+ }
59
+ nlf
60
+ }
61
+
62
+ getfille <- function(nlf, classe, pf) {
63
+ if (!length(nlf[[classe]])) {
64
+ return(pf)
65
+ } else {
66
+ for (cl in nlf[[classe]]) {
67
+ pf <- c(pf, cl)
68
+ if (cl <= length(nlf)) {
69
+ pf <- getfille(nlf, cl, pf)
70
+ }
71
+ }
72
+ }
73
+ return(pf)
74
+ }
75
+
76
+ getmere <- function(list_mere, classe) {
77
+ i <- as.numeric(classe)
78
+ pf <- NULL
79
+ while (i != 1 ) {
80
+ pf <- c(pf, list_mere[[i]])
81
+ i <- list_mere[[i]]
82
+ }
83
+ pf
84
+ }
85
+
86
+ getfillemere <- function(list_fille, list_mere, classe) {
87
+ return(c(getfille(list_fille, classe, NULL), getmere(list_mere, classe)))
88
+ }
89
+
90
+ getlength <- function(n1, clnb) {
91
+ colnb <- (clnb %/%2)
92
+ tab <- table(n1[,colnb])
93
+ eff <- tab[which(names(tab) == as.character(clnb))]
94
+ return(eff)
95
+ }
96
+
97
+
98
+ find.terminales <- function(n1, list_mere, list_fille, mincl) {
99
+ tab <- table(n1[,ncol(n1)])
100
+ clnames <- rownames(tab)
101
+ terminales <- clnames[which(tab >= mincl)]
102
+ tocheck <- setdiff(clnames, terminales)
103
+ if ("0" %in% terminales) {
104
+ terminales <- terminales[which(terminales != 0)]
105
+ }
106
+ if (length(terminales) == 0) {
107
+ return('no clusters')
108
+ }
109
+ if ("0" %in% tocheck) {
110
+ tocheck <- tocheck[which(tocheck != "0")]
111
+ }
112
+ print(terminales)
113
+ print(tocheck)
114
+ while (length(tocheck)!=0) {
115
+ for (val in tocheck) {
116
+ print(val)
117
+ mere <- list_mere[[as.numeric(val)]]
118
+ print('mere')
119
+ print(mere)
120
+ if (mere != 1) {
121
+ ln.mere <- getlength(n1, mere)
122
+ print('ln.mere')
123
+ print(ln.mere)
124
+ filles.mere <- getfille(list_fille, mere, NULL)
125
+ print('fille mere')
126
+ print(filles.mere)
127
+ filles.mere <- filles.mere[which(filles.mere != val)]
128
+ print(filles.mere)
129
+ if ((ln.mere >= mincl) & (length(intersect(filles.mere, tocheck)) == 0) & (length(intersect(filles.mere, terminales)) == 0 )) {
130
+ print('mere ok')
131
+ terminales <- c(terminales, mere)
132
+ for (f in c(filles.mere, val, mere)) {
133
+ tocheck <- tocheck[which(tocheck != f)]
134
+ }
135
+ } else if ((ln.mere >= mincl) & (length(intersect(filles.mere, terminales)) == 0) & (length(intersect(filles.mere, tocheck))!=0)){
136
+ print('mere a checke cause fille ds tocheck')
137
+ tocheck <- tocheck[which(tocheck != val)]
138
+ tocheck <- c(mere, tocheck)
139
+
140
+ } else {
141
+ print('pas ok on vire du check')
142
+ tocheck <- tocheck[which(tocheck != val)]
143
+ }
144
+ } else {
145
+ print('mere == 1')
146
+ tocheck <- tocheck[which(tocheck != val)]
147
+ }
148
+ print('tocheck')
149
+ print(tocheck)
150
+ }
151
+ print(tocheck)
152
+ }
153
+ terminales
154
+ }
155
+
156
+ make.classes <- function(terminales, n1, tree, lf) {
157
+ term.n1 <- unique(n1[,ncol(n1)])
158
+ tree.tip <- tree$tip.label
159
+ cl.n1 <- n1[,ncol(n1)]
160
+ classes <- rep(NA, nrow(n1))
161
+ cl.names <- 1:length(terminales)
162
+ new.cl <- list()
163
+ for (i in 1:length(terminales)) {
164
+ if (terminales[i] %in% term.n1) {
165
+ classes[which(cl.n1==terminales[i])] <- cl.names[i]
166
+ new.cl[[terminales[i]]] <- cl.names[i]
167
+ tree.tip[which(tree.tip==terminales[i])] <- paste('a', cl.names[i], sep='')
168
+ } else {
169
+ filles <- getfille(lf, as.numeric(terminales[i]), NULL)
170
+ tochange <- intersect(filles, term.n1)
171
+ for (cl in tochange) {
172
+ classes[which(cl.n1==cl)] <- cl.names[i]
173
+ new.cl[[cl]] <- cl.names[i]
174
+ tree.tip[which(tree.tip==cl)] <- paste('a', cl.names[i], sep='')
175
+ }
176
+ }
177
+ }
178
+ make.tip <- function(x) {
179
+ if (substring(x,1,1)=='a') {
180
+ return(substring(x,2))
181
+ } else {
182
+ return(0)
183
+ }
184
+ }
185
+ tree$tip.label <- tree.tip
186
+ ntree <- tree
187
+ tree$tip.label <- sapply(tree.tip, make.tip)
188
+ tovire <- sapply(tree.tip, function(x) {substring(x,1,1)!='a'})
189
+ tovire <- which(tovire)
190
+ ntree <- drop.tip(ntree, tip=tovire)
191
+ en.double <- ntree$tip.label[duplicated(ntree$tip.label)]
192
+ en.double <- unique(en.double)
193
+ tovire <- sapply(en.double, function(x) {which(ntree$tip.label == x)[1]})
194
+ ntree <- drop.tip(ntree, tip=tovire)
195
+ ntree$tip.label <- sapply(ntree$tip.label, function(x) {substring(x,2)})
196
+ classes[which(is.na(classes))] <- 0
197
+ res <- list(dendro_tot_cl = tree, tree.cl = ntree, n1=as.matrix(classes))
198
+ res
199
+ }
200
+
201
+ #nbt nbcl = nbt+1 tcl=((nbt+1) *2) - 2 n1[,ncol(n1)], nchd1[,ncol(nchd1)]
202
+ Rchdtxt<-function(uceout, chd1, chd2 = NULL, mincl=0, classif_mode=0, nbt = 9) {
203
+ #FIXME: le nombre de classe peut etre inferieur
204
+ nbcl <- nbt + 1
205
+ tcl <- ((nbt+1) * 2) - 2
206
+ #Assignation des classes
207
+ classeuce1<-AssignClasseToUce(listuce1,chd1$n1)
208
+ if (classif_mode==0) {
209
+ classeuce2<-AssignClasseToUce(listuce2,chd2$n1)
210
+ }
211
+ #} else {
212
+ # classeuce2<-classeuce1
213
+ #}
214
+
215
+ #calcul des poids (effectifs)
216
+
217
+ makepoids <- function(classeuce, poids) {
218
+ cl1 <- 0
219
+ cl2 <- 1
220
+ for (i in 1:nbt) {
221
+ cl1 <- cl1 + 2
222
+ cl2 <- cl2 + 2
223
+ poids[cl1 - 1] <- length(which(classeuce[,i] == cl1))
224
+ poids[cl2 - 1] <- length(which(classeuce[,i] == cl2))
225
+ }
226
+ poids
227
+ }
228
+
229
+ # makepoids<-function(classeuce,poids) {
230
+ # for (classes in 2:(tcl + 1)){
231
+ # for (i in 1:ncol(classeuce)) {
232
+ # if (poids[(classes-1)]<length(classeuce[,i][classeuce[,i]==classes])) {
233
+ # poids[(classes-1)]<-length(classeuce[,i][classeuce[,i]==classes])
234
+ # }
235
+ # }
236
+ # }
237
+ # poids
238
+ # }
239
+ print('make poids')
240
+ poids1<-vector(mode='integer',length = tcl)
241
+ poids1<-makepoids(classeuce1,poids1)
242
+ if (classif_mode==0) {
243
+ poids2<-vector(mode='integer',length = tcl)
244
+ poids2<-makepoids(classeuce2,poids2)
245
+ }# else {
246
+ # poids2<-poids1
247
+ #}
248
+
249
+ print('croisement classif')
250
+
251
+ # croise=matrix(ncol=tcl,nrow=tcl)
252
+ #
253
+ # docroise <- function(croise, classeuce1, classeuce2) {
254
+ # #production du tableau de contingence
255
+ # for (i in 1:ncol(classeuce1)) {
256
+ # #poids[i]<-length(classeuce1[,i][x==classes])
257
+ # for (j in 1:ncol(classeuce2)) {
258
+ # tablecroise<-table(classeuce1[,i],classeuce2[,j])
259
+ # tabcolnames<-as.numeric(colnames(tablecroise))
260
+ # #tabcolnames<-c(tabcolnames[(length(tabcolnames)-1)],tabcolnames[length(tabcolnames)])
261
+ # tabrownames<-as.numeric(rownames(tablecroise))
262
+ # #tabrownames<-c(tabrownames[(length(tabrownames)-1)],tabrownames[length(tabrownames)])
263
+ # for (k in (ncol(tablecroise)-1):ncol(tablecroise)) {
264
+ # for (l in (nrow(tablecroise)-1):nrow(tablecroise)) {
265
+ # croise[(tabrownames[l]-1),(tabcolnames[k]-1)]<-tablecroise[l,k]
266
+ # }
267
+ # }
268
+ # }
269
+ # }
270
+ # croise
271
+ # }
272
+ if (classif_mode==0) {
273
+ croise <- croiseeff( matrix(ncol=tcl,nrow=tcl), classeuce1, classeuce2)
274
+ } else {
275
+ croise <- croiseeff( matrix(ncol=tcl,nrow=tcl), classeuce1, classeuce1)
276
+ }
277
+ #print(croise)
278
+ if (classif_mode == 0) {ind <- (nbcl * 2)} else {ind <- nbcl}
279
+ if (mincl==0){
280
+ mincl<-round(nrow(classeuce1)/ind)
281
+ }
282
+ #if (mincl<3){
283
+ # mincl<-3
284
+ #}
285
+ print(mincl)
286
+ #print('table1')
287
+ #print(croise)
288
+ #tableau des chi2 signes
289
+ print('croise chi2')
290
+ #chicroise<-croise
291
+
292
+ # nr <- nrow(classeuce1)
293
+ # newchicroise <- function(croise, mincl, nr, poids1, poids2) {
294
+ # chicroise <- croise
295
+ # chicroise[which(croise < mincl)] <- 0
296
+ # tocompute <- which(chicroise > 0, arr.ind = TRUE)
297
+ # for (i in 1:nrow(tocompute)) {
298
+ # chitable <- matrix(ncol=2,nrow=2)
299
+ # chitable[1,1] <- croise[tocompute[i,1], tocompute[i,2]]
300
+ # chitable[1,2] <- poids1[tocompute[i,1]] - chitable[1,1]
301
+ # chitable[2,1] <- poids2[tocompute[i,2]] - chitable[1,1]
302
+ # chitable[2,2] <- nr - poids2[tocompute[i,2]] - chitable[1,2]
303
+ # chitest<-chisq.test(chitable,correct=FALSE)
304
+ # chicroise[tocompute[i,1], tocompute[i,2]] <- ifelse(chitable[1,1] > chitest$expected[1,1], round(chitest$statistic,digits=7), -round(chitest$statistic,digits=7))
305
+ # }
306
+ # chicroise
307
+ # }
308
+ #
309
+
310
+
311
+ dochicroise <- function(croise, mincl) {
312
+ chicroise <- croise
313
+ for (i in 1:nrow(croise)) {
314
+ for (j in 1:ncol(croise)) {
315
+ if (croise[i,j]==0) {
316
+ chicroise[i,j]<-0
317
+ } else if (croise[i,j]<mincl) {
318
+ chicroise[i,j]<-0
319
+ } else {
320
+ chitable<-matrix(ncol=2,nrow=2)
321
+ chitable[1,1]<-croise[i,j]
322
+ chitable[1,2]<-poids1[i]-chitable[1,1]
323
+ chitable[2,1]<-poids2[j]-chitable[1,1]
324
+ chitable[2,2]<-nrow(classeuce1)-poids2[j]-chitable[1,2]
325
+ chitest<-chisq.test(chitable,correct=FALSE)
326
+ if ((chitable[1,1]-chitest$expected[1,1])<0) {
327
+ chicroise[i,j]<--round(chitest$statistic,digits=7)
328
+ } else {
329
+ chicroise[i,j]<-round(chitest$statistic,digits=7)
330
+ #print(chitest)
331
+ }
332
+ }
333
+ }
334
+ }
335
+ chicroise
336
+ }
337
+
338
+ dochicroisesimple <- function(croise, mincl) {
339
+ chicroise <- croise
340
+ for (i in 1:nrow(croise)) {
341
+ for (j in 1:ncol(croise)) {
342
+ if (croise[i,j]==0) {
343
+ chicroise[i,j]<-0
344
+ } else if (croise[i,j]<mincl) {
345
+ chicroise[i,j]<-0
346
+ } else {
347
+ chitable<-matrix(ncol=2,nrow=2)
348
+ chitable[1,1]<-croise[i,j]
349
+ chitable[1,2]<-poids1[i]-chitable[1,1]
350
+ chitable[2,1]<-poids1[j]-chitable[1,1]
351
+ chitable[2,2]<-nrow(classeuce1)-poids1[j]-chitable[1,2]
352
+ chitest<-chisq.test(chitable,correct=FALSE)
353
+ if ((chitable[1,1]-chitest$expected[1,1])<0) {
354
+ chicroise[i,j]<--round(chitest$statistic,digits=7)
355
+ } else {
356
+ chicroise[i,j]<-round(chitest$statistic,digits=7)
357
+ #print(chitest)
358
+ }
359
+ }
360
+ }
361
+ }
362
+ chicroise
363
+ }
364
+ if (classif_mode == 0) {
365
+ chicroise <- dochicroise(croise, mincl)
366
+ } else {
367
+ chicroise <- dochicroisesimple(croise, mincl)
368
+ }
369
+
370
+ print('fin croise')
371
+ #print(chicroise)
372
+ #determination des chi2 les plus fort
373
+ chicroiseori<-chicroise
374
+
375
+ doxy <- function(chicroise) {
376
+ listx <- NULL
377
+ listy <- NULL
378
+ listxy <- which(chicroise > 3.84, arr.ind = TRUE)
379
+ #print(listxy)
380
+ val <- chicroise[which(chicroise > 3.84)]
381
+ ord <- order(val, decreasing = TRUE)
382
+ listxy <- listxy[ord,]
383
+ #for (i in 1:nrow(listxy)) {
384
+ # if ((!listxy[,2][i] %in% listx) & (!listxy[,1][i] %in% listy)) {
385
+ # listx <- c(listx, listxy[,2][i])
386
+ # listy <- c(listy, listxy[,1][i])
387
+ # }
388
+ #}
389
+ xy <- list(x = listxy[,2], y = listxy[,1])
390
+ xy
391
+ }
392
+ xy <- doxy(chicroise)
393
+ listx <- xy$x
394
+ listy <- xy$y
395
+
396
+ # maxi<-vector()
397
+ # chimax<-vector()
398
+ # for (i in 1:tcl) {
399
+ # maxi[i]<-which.max(chicroise)
400
+ # chimax[i]<-chicroise[maxi[i]]
401
+ # chicroise[maxi[i]]<-0
402
+ # }
403
+ # testpres<-function(x,listcoord) {
404
+ # for (i in 1:length(listcoord)) {
405
+ # if (x==listcoord[i]) {
406
+ # return(-1)
407
+ # } else {
408
+ # a<-1
409
+ # }
410
+ # }
411
+ # a
412
+ # }
413
+ # c.len=nrow(chicroise)
414
+ # r.len=ncol(chicroise)
415
+ # listx<-c(0)
416
+ # listy<-c(0)
417
+ # rang<-0
418
+ # cons<-list()
419
+ # #on garde une valeur par ligne / colonne
420
+ # for (i in 1:length(maxi)) {
421
+ # #coordonnées de chi2 max
422
+ # #coord <- arrayInd(maxi[i], dim(chicroise))
423
+ # #x.co <- coord[1,2]
424
+ # #y.co <- coord[1,1]
425
+ # x.co<-ceiling(maxi[i]/c.len)
426
+ # y.co<-maxi[i]-(x.co-1)*c.len
427
+ # #print(x.co)
428
+ # #print(y.co)
429
+ # #print(arrayInd(maxi[i], dim(chicroise)))
430
+ # a<-testpres(x.co,listx)
431
+ # b<-testpres(y.co,listy)
432
+ #
433
+ # if (a==1) {
434
+ # if (b==1) {
435
+ # rang<-rang+1
436
+ # listx[rang]<-x.co
437
+ # listy[rang]<-y.co
438
+ # }
439
+ # }
440
+ # cons[[1]]<-listx
441
+ # cons[[2]]<-listy
442
+ # }
443
+ #pour ecrire les resultats
444
+ for (i in 1:length(listx)) {
445
+ txt<-paste(listx[i]+1,listy[i]+1,sep=' ')
446
+ txt<-paste(txt,croise[listy[i],listx[i]],sep=' ')
447
+ txt<-paste(txt,chicroiseori[listy[i],listx[i]],sep=' ')
448
+ #print(txt)
449
+ }
450
+ #colonne de la classe
451
+ #trouver les filles et les meres
452
+ trouvefillemere<-function(classe,chd) {
453
+ unique(unlist(chd[chd[,classe%/%2]==classe,]))
454
+ }
455
+
456
+
457
+ #----------------------------------------------------------------------
458
+ findbestcoord <- function(classeuce1, classeuce2, classif_mode, nbt) {
459
+ #fillemere1 <- NULL
460
+ #fillemere2 <- NULL
461
+
462
+ #fillemere1 <- unique(classeuce1)
463
+ #if (classif_mode == 0) {
464
+ # fillemere2 <- unique(classeuce2)
465
+ #} else {
466
+ # fillemere2 <- fillemere1
467
+ #}
468
+
469
+ #
470
+ listcoordok <- list()
471
+ maxcl <- 0
472
+ nb <- 0
473
+ lf1 <- addallfille(chd1$list_fille)
474
+ if (classif_mode == 0) {
475
+ lf2 <- addallfille(chd2$list_fille)
476
+ } else {
477
+ lf2 <- lf1
478
+ listx<-listx[1:((nbt+1)*2)]
479
+ listy<-listy[1:((nbt+1)*2)]
480
+ }
481
+ lme1 <- chd1$list_mere
482
+ if (classif_mode == 0) {
483
+ lme2 <- chd2$list_mere
484
+ } else {
485
+ lme2 <- lme1
486
+ }
487
+ print('length listx')
488
+ print(length(listx))
489
+ #if (classif_mode == 0) {
490
+ for (first in 1:length(listx)) {
491
+ coordok <- NULL
492
+ f1 <- NULL
493
+ f2 <- NULL
494
+ listxp<-listx
495
+ listyp<-listy
496
+
497
+ #listxp<-listx[first:length(listx)]
498
+ #listxp<-c(listxp,listx[1:(first-1)])
499
+ #listyp<-listy[first:length(listy)]
500
+ #listyp<-c(listyp,listy[1:(first-1)])
501
+ listxp <- listxp[order(listx, decreasing = TRUE)]
502
+ listyp <- listyp[order(listx, decreasing = TRUE)]
503
+ #listxp<-c(listxp[first:length(listx)], listx[1:(first-1)])
504
+ #listyp<-c(listyp[first:length(listy)], listy[1:(first-1)])
505
+ for (i in 1:length(listx)) {
506
+ if( (!(listxp[i]+1) %in% f1) & (!(listyp[i]+1) %in% f2) ) {
507
+ #print(listyp[i]+1)
508
+ #print('not in')
509
+ #print(f2)
510
+ coordok <- rbind(coordok, c(listyp[i] + 1,listxp[i] + 1))
511
+ #print(c(listyp[i] + 1,listxp[i] + 1))
512
+ un1 <- getfillemere(lf2, chd2$list_mere, listxp[i] + 1)
513
+ f1 <- c(f1, un1)
514
+ f1 <- c(f1, listxp[i] + 1)
515
+ un2 <- getfillemere(lf1, chd1$list_mere, listyp[i] + 1)
516
+ f2 <- c(f2, un2)
517
+ f2 <- c(f2, listyp[i] + 1)
518
+ }
519
+ #print(coordok)
520
+ }
521
+ #if (nrow(coordok) > maxcl) {
522
+ nb <- 1
523
+ # listcoordok <- list()
524
+ listcoordok[[nb]] <- coordok
525
+ # maxcl <- nrow(coordok)
526
+ #} else if (nrow(coordok) == maxcl) {
527
+ nb <- nb + 1
528
+ # listcoordok[[nb]] <- coordok
529
+ #}
530
+ }
531
+ #} else {
532
+ # stopid <- ((nbt+1) * 2) - 2
533
+ # for (first in 1:stopid) {
534
+ # coordok <- NULL
535
+ # f1 <- NULL
536
+ # f2 <- NULL
537
+ # listxp<-listx
538
+ # listyp<-listy
539
+ #
540
+ # #listxp<-listx[first:length(listx)]
541
+ # #listxp<-c(listxp,listx[1:(first-1)])
542
+ # #listyp<-listy[first:length(listy)]
543
+ # #listyp<-c(listyp,listy[1:(first-1)])
544
+ # listxp <- listxp[order(listx, decreasing = TRUE)]
545
+ # listyp <- listyp[order(listx, decreasing = TRUE)]
546
+ # #listxp<-c(listxp[first:length(listx)], listx[1:(first-1)])
547
+ # #listyp<-c(listyp[first:length(listy)], listy[1:(first-1)])
548
+ # for (i in 1:stopid) {
549
+ # if( (!(listxp[i]+1) %in% f1) & (!(listyp[i]+1) %in% f2) ) {
550
+ # #print(listyp[i]+1)
551
+ # #print('not in')
552
+ # #print(f2)
553
+ # coordok <- rbind(coordok, c(listyp[i] + 1,listxp[i] + 1))
554
+ # #print(c(listyp[i] + 1,listxp[i] + 1))
555
+ # un1 <- getfillemere(lf2, chd2$list_mere, listxp[i] + 1)
556
+ # f1 <- c(f1, un1)
557
+ # f1 <- c(f1, listxp[i] + 1)
558
+ # un2 <- getfillemere(lf1, chd1$list_mere, listyp[i] + 1)
559
+ # f2 <- c(f2, un2)
560
+ # f2 <- c(f2, listyp[i] + 1)
561
+ # }
562
+ # #print(coordok)
563
+ # }
564
+ # #if (nrow(coordok) > maxcl) {
565
+ # nb <- 1
566
+ # # listcoordok <- list()
567
+ # listcoordok[[nb]] <- coordok
568
+ # # maxcl <- nrow(coordok)
569
+ # #} else if (nrow(coordok) == maxcl) {
570
+ # nb <- nb + 1
571
+ # # listcoordok[[nb]] <- coordok
572
+ # #}
573
+ # }
574
+ # }
575
+ #print(listcoordok)
576
+ listcoordok <- unique(listcoordok)
577
+ print(listcoordok)
578
+ best <- 1
579
+ if (length(listcoordok) > 1) {
580
+ maxchi <- 0
581
+ for (i in 1:length(listcoordok)) {
582
+ chi <- NULL
583
+ uce <- NULL
584
+ for (j in 1:nrow(listcoordok[[i]])) {
585
+ chi<-c(chi,chicroiseori[(listcoordok[[i]][j,1]-1),(listcoordok[[i]][j,2]-1)])
586
+ uce<-c(uce,croise[(listcoordok[[i]][j,1]-1),(listcoordok[[i]][j,2]-1)])
587
+ }
588
+ if (maxchi < sum(chi)) {
589
+ maxchi <- sum(chi)
590
+ suce <- sum(uce)
591
+ best <- i
592
+ }
593
+ }
594
+ print(suce/nrow(classeuce1))
595
+ }
596
+ listcoordok[[best]]
597
+ }
598
+ #---------------------------------------------------------------------------------
599
+ #pour trouver une valeur dans une liste
600
+ #is.element(elem, list)
601
+ #== elem%in%list
602
+ oldfindbestcoord <- function(listx, listy) {
603
+ coordok<-NULL
604
+ trouvecoordok<-function(first) {
605
+ fillemere1<-NULL
606
+ fillemere2<-NULL
607
+ listxp<-listx
608
+ listyp<-listy
609
+ listxp<-listx[first:length(listx)]
610
+ listxp<-c(listxp,listx[1:(first-1)])
611
+ listyp<-listy[first:length(listy)]
612
+ listyp<-c(listyp,listy[1:(first-1)])
613
+ for (i in 1:length(listxp)) {
614
+ if (!(listxp[i]+1)%in%fillemere1) {
615
+ if (!(listyp[i]+1)%in%fillemere2) {
616
+ coordok<-rbind(coordok,c(listyp[i]+1,listxp[i]+1))
617
+ fillemere1<-c(fillemere1,trouvefillemere(listxp[i]+1,chd2$n1))
618
+ fillemere2<-c(fillemere2,trouvefillemere(listyp[i]+1,chd1$n1))
619
+ }
620
+ }
621
+ }
622
+ coordok
623
+ }
624
+ #fonction pour trouver le nombre maximum de classes
625
+ findmaxclasse<-function(listx,listy) {
626
+ listcoordok<-list()
627
+ maxcl<-0
628
+ nb<-1
629
+ for (i in 1:length(listy)) {
630
+ coordok<-trouvecoordok(i)
631
+ if (maxcl <= nrow(coordok)) {
632
+ maxcl<-nrow(coordok)
633
+ listcoordok[[nb]]<-coordok
634
+ nb<-nb+1
635
+ }
636
+ }
637
+ listcoordok<-unique(listcoordok)
638
+ #print(listcoordok)
639
+ #si plusieurs ensemble avec le meme nombre de classe, on conserve
640
+ #la liste avec le plus fort chi2
641
+ if (length(listcoordok)>1) {
642
+ maxchi<-0
643
+ best<-NULL
644
+ for (i in 1:length(listcoordok)) {
645
+ chi<-NULL
646
+ uce<-NULL
647
+ if (nrow(listcoordok[[i]])==maxcl) {
648
+ for (j in 1:nrow(listcoordok[[i]])) {
649
+ chi<-c(chi,croise[(listcoordok[[i]][j,1]-1),(listcoordok[[i]][j,2]-1)])
650
+ uce<-c(uce,chicroiseori[(listcoordok[[i]][j,1]-1),(listcoordok[[i]][j,2]-1)])
651
+ }
652
+ if (maxchi < sum(chi)) {
653
+ maxchi <- sum(chi)
654
+ suce <- sum(uce)
655
+ best <- i
656
+ }
657
+ }
658
+ }
659
+ }
660
+ print((maxchi/nrow(classeuce1)*100))
661
+ listcoordok[[best]]
662
+ }
663
+ print('cherche max')
664
+ coordok<-findmaxclasse(listx,listy)
665
+ coordok
666
+ }
667
+ #findmaxclasse(listx,listy)
668
+ #coordok<-trouvecoordok(1)
669
+ #coordok <- oldfindbestcoord(listx, listy)
670
+ print('begin bestcoord')
671
+ coordok <- findbestcoord(listx, listy, classif_mode, nbt)
672
+
673
+
674
+ lfilletot<-function(classeuce,x) {
675
+ listfille<-NULL
676
+ for (classe in 1:nrow(coordok)) {
677
+ listfille<-unique(c(listfille,fille(coordok[classe,x],classeuce)))
678
+ listfille
679
+ }
680
+ }
681
+ print('listfille')
682
+ listfille1<-lfilletot(classeuce1,1)
683
+ if (classif_mode == 0) {
684
+ listfille2<-lfilletot(classeuce2,2)
685
+ }
686
+
687
+ #utiliser rownames comme coordonnees dans un tableau de 0
688
+ Assignclasse<-function(classeuce,x) {
689
+ nchd<-matrix(0,ncol=ncol(classeuce),nrow=nrow(classeuce))
690
+ for (classe in 1:nrow(coordok)) {
691
+ clnb<-coordok[classe,x]
692
+ colnb<-clnb%/%2
693
+ nchd[which(classeuce[,colnb]==clnb), colnb:ncol(nchd)] <- classe
694
+ }
695
+ nchd
696
+ }
697
+ print('commence assigne new classe')
698
+ nchd1<-Assignclasse(classeuce1,1)
699
+ if (classif_mode==0) {
700
+ nchd2<-Assignclasse(classeuce2,2)
701
+ }
702
+ print('fini assign new classe')
703
+ #croisep<-matrix(ncol=nrow(coordok),nrow=nrow(coordok))
704
+ if (classif_mode==0) {
705
+ nchd2[which(nchd1[,ncol(nchd1)]==0),] <- 0
706
+ nchd2[which(nchd1[,ncol(nchd1)]!=nchd2[,ncol(nchd2)]),] <- 0
707
+ nchd1[which(nchd2[,ncol(nchd2)]==0),] <- 0
708
+ }
709
+
710
+ print('fini croise')
711
+ elim<-which(nchd1[,ncol(nchd1)]==0)
712
+ keep<-which(nchd1[,ncol(nchd1)]!=0)
713
+ n1<-nchd1[nchd1[,ncol(nchd1)]!=0,]
714
+ if (classif_mode==0) {
715
+ n2<-nchd2[nchd2[,ncol(nchd2)]!=0,]
716
+ } else {
717
+ classeuce2 <- NULL
718
+ }
719
+ #clnb<-nrow(coordok)
720
+ print('fini')
721
+ write.csv2(nchd1[,ncol(nchd1)],uceout)
722
+ res <- list(n1 = nchd1, coord_ok = coordok, cuce1 = classeuce1, cuce2 = classeuce2)
723
+ res
724
+ }
iramuteq-like/concordancier-iramuteq.R ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: concordancier-iramuteq.R génère un concordancier HTML dédié au mode IRaMuTeQ-like.
2
+ # Le rendu suit le style Rainette (segments par classe + surlignage),
3
+ # avec une sélection des termes alignée sur les filtres statistiques IRaMuTeQ-like.
4
+
5
+ .generer_concordancier_iramuteq_termes <- function(res_stats_df, classe, max_p = 1, filtrer_pvalue = TRUE) {
6
+ if (is.null(res_stats_df) || nrow(res_stats_df) == 0) return(character(0))
7
+ if (!all(c("Classe", "Terme") %in% names(res_stats_df))) return(character(0))
8
+
9
+ df <- res_stats_df
10
+ cl <- suppressWarnings(as.numeric(classe))
11
+ if (!is.na(cl)) {
12
+ classes_num <- suppressWarnings(as.numeric(df$Classe))
13
+ df <- df[!is.na(classes_num) & classes_num == cl, , drop = FALSE]
14
+ }
15
+
16
+ if (nrow(df) == 0) return(character(0))
17
+
18
+ # Filtres IRaMuTeQ-like: p <= max_p et, par défaut, uniquement les chi2 positifs.
19
+ if (isTRUE(filtrer_pvalue)) {
20
+ if ("p" %in% names(df) && is.finite(max_p) && !is.na(max_p)) {
21
+ p_vals <- suppressWarnings(as.numeric(df$p))
22
+ df <- df[!is.na(p_vals) & p_vals <= max_p, , drop = FALSE]
23
+ } else if ("p_value" %in% names(df) && is.finite(max_p) && !is.na(max_p)) {
24
+ p_vals <- suppressWarnings(as.numeric(df$p_value))
25
+ df <- df[!is.na(p_vals) & p_vals <= max_p, , drop = FALSE]
26
+ }
27
+ }
28
+
29
+ if ("chi2" %in% names(df)) {
30
+ chi2_vals <- suppressWarnings(as.numeric(df$chi2))
31
+ df <- df[!is.na(chi2_vals) & chi2_vals > 0, , drop = FALSE]
32
+ chi2_vals <- suppressWarnings(as.numeric(df$chi2))
33
+ df <- df[order(-chi2_vals), , drop = FALSE]
34
+ }
35
+
36
+ termes <- unique(as.character(df$Terme))
37
+ termes <- termes[!is.na(termes) & nzchar(trimws(termes))]
38
+ termes
39
+ }
40
+
41
+ generer_concordancier_iramuteq_html <- function(
42
+ chemin_sortie,
43
+ segments_by_class,
44
+ res_stats_df,
45
+ max_p,
46
+ filtrer_pvalue = TRUE,
47
+ textes_indexation,
48
+ avancer = NULL,
49
+ rv = NULL,
50
+ ...
51
+ ) {
52
+ if (!is.null(rv)) ajouter_log(rv, "Concordancier IRaMuTeQ-like : génération HTML (filtres IRaMuTeQ + surlignage Unicode).")
53
+
54
+ con <- file(chemin_sortie, open = "wt", encoding = "UTF-8")
55
+ on.exit(try(close(con), silent = TRUE), add = TRUE)
56
+
57
+ writeLines("<html><head><meta charset='utf-8'/>", con)
58
+ writeLines("<style>body{font-family:Arial,sans-serif;line-height:1.45;} span.highlight{background-color:yellow;} p.segment{margin:0 0 .45rem 0;} .classe-bloc{margin-bottom:1.25rem;padding-bottom:.8rem;border-bottom:1px solid #eee;}</style>", con)
59
+ writeLines("</head><body>", con)
60
+ writeLines("<h1>Concordancier IRaMuTeQ-like</h1>", con)
61
+ writeLines("<h2>Segments par classe</h2>", con)
62
+ writeLines(if (isTRUE(filtrer_pvalue)) "<h3>Filtrage: p ≤ seuil + χ² positif (puis fallback top χ²)</h3>" else "<h3>Filtrage: χ² positif (sans filtre p-value)</h3>", con)
63
+
64
+ noms_classes <- names(segments_by_class)
65
+ n_classes <- length(noms_classes)
66
+ if (n_classes == 0) n_classes <- 1
67
+
68
+ for (i in seq_along(noms_classes)) {
69
+ cl <- noms_classes[[i]]
70
+ if (!is.null(avancer)) avancer(0.75 + (i / n_classes) * 0.08, paste0("HTML IRaMuTeQ : classe ", cl))
71
+
72
+ writeLines("<div class='classe-bloc'>", con)
73
+ writeLines(paste0("<h2>Classe ", cl, "</h2>"), con)
74
+
75
+ segments <- segments_by_class[[cl]]
76
+ ids_cl <- names(segments)
77
+ if (length(ids_cl) == 0) {
78
+ writeLines("<p><em>Aucun segment.</em></p>", con)
79
+ writeLines("</div>", con)
80
+ next
81
+ }
82
+
83
+ textes_filtrage <- unname(segments)
84
+ if (!is.null(textes_indexation) && length(textes_indexation) > 0) {
85
+ tx <- textes_indexation[ids_cl]
86
+ ok_tx <- !is.na(tx) & nzchar(tx)
87
+ if (any(ok_tx)) textes_filtrage[ok_tx] <- tx[ok_tx]
88
+ }
89
+
90
+ termes_cl <- .generer_concordancier_iramuteq_termes(res_stats_df, cl, max_p = max_p, filtrer_pvalue = filtrer_pvalue)
91
+
92
+ if (length(termes_cl) == 0 && !is.null(res_stats_df) && nrow(res_stats_df) > 0 && "Classe" %in% names(res_stats_df)) {
93
+ df_cl <- res_stats_df[suppressWarnings(as.numeric(res_stats_df$Classe)) == suppressWarnings(as.numeric(cl)), , drop = FALSE]
94
+ if (nrow(df_cl) > 0 && "chi2" %in% names(df_cl) && "Terme" %in% names(df_cl)) {
95
+ chi2_vals <- suppressWarnings(as.numeric(df_cl$chi2))
96
+ idx <- !is.na(chi2_vals) & !is.na(df_cl$Terme) & nzchar(as.character(df_cl$Terme))
97
+ if (any(idx)) {
98
+ df_cl <- df_cl[idx, , drop = FALSE]
99
+ df_cl <- df_cl[order(-suppressWarnings(as.numeric(df_cl$chi2))), , drop = FALSE]
100
+ termes_cl <- unique(head(as.character(df_cl$Terme), 20))
101
+ }
102
+ }
103
+ }
104
+
105
+ termes_cl <- expandir_variantes_termes(termes_cl)
106
+ keep <- detecter_segments_contenant_termes_unicode(textes_filtrage, termes_cl)
107
+ keep[is.na(keep)] <- FALSE
108
+
109
+ segments_keep <- segments[keep]
110
+ if (length(segments_keep) == 0 && length(segments) > 0) {
111
+ segments_keep <- segments
112
+ if (!is.null(rv)) {
113
+ ajouter_log(rv, paste0(
114
+ "Concordancier IRaMuTeQ-like : classe ", cl,
115
+ " sans segment après filtrage, fallback sur tous les segments."
116
+ ))
117
+ }
118
+ }
119
+
120
+ writeLines(paste0("<p><em>Segments conservés : ", length(segments_keep), " / ", length(segments), "</em></p>"), con)
121
+
122
+ if (length(segments_keep) == 0) {
123
+ writeLines("<p><em>Aucun segment.</em></p>", con)
124
+ writeLines("</div>", con)
125
+ next
126
+ }
127
+
128
+ if (length(termes_cl) == 0) {
129
+ for (seg in echapper_segments_en_preservant_surlignage(unname(segments_keep), "<span class='highlight'>", "</span>")) {
130
+ writeLines(paste0("<p class='segment'>", seg, "</p>"), con)
131
+ }
132
+ writeLines("</div>", con)
133
+ next
134
+ }
135
+
136
+ motifs <- preparer_motifs_surlignage_nfd(termes_cl, taille_lot = 80)
137
+ segments_hl <- surligner_vecteur_html_unicode(
138
+ unname(segments_keep),
139
+ motifs,
140
+ "<span class='highlight'>",
141
+ "</span>",
142
+ on_error = function(e, pat) {
143
+ if (!is.null(rv)) {
144
+ ajouter_log(rv, paste0("Concordancier IRaMuTeQ-like : erreur regex [", pat, "] - ", conditionMessage(e)))
145
+ }
146
+ }
147
+ )
148
+
149
+ has_hl <- any(grepl("<span class='highlight'>", segments_hl, fixed = TRUE))
150
+ if (!has_hl) {
151
+ textes_keep_idx <- textes_filtrage[keep]
152
+ segments_hl_idx <- surligner_vecteur_html_unicode(
153
+ unname(textes_keep_idx),
154
+ motifs,
155
+ "<span class='highlight'>",
156
+ "</span>",
157
+ on_error = function(e, pat) {
158
+ if (!is.null(rv)) {
159
+ ajouter_log(rv, paste0("Concordancier IRaMuTeQ-like : erreur regex index [", pat, "] - ", conditionMessage(e)))
160
+ }
161
+ }
162
+ )
163
+ if (any(grepl("<span class='highlight'>", segments_hl_idx, fixed = TRUE))) {
164
+ segments_hl <- segments_hl_idx
165
+ }
166
+ }
167
+
168
+ if (length(segments_hl) == 0 && length(segments_keep) > 0) {
169
+ segments_hl <- unname(segments_keep)
170
+ }
171
+
172
+ for (seg in echapper_segments_en_preservant_surlignage(segments_hl, "<span class='highlight'>", "</span>")) {
173
+ writeLines(paste0("<p class='segment'>", seg, "</p>"), con)
174
+ }
175
+ writeLines("</div>", con)
176
+ }
177
+
178
+ writeLines("</body></html>", con)
179
+ close(con)
180
+ if (!is.null(rv)) ajouter_log(rv, paste0("Concordancier IRaMuTeQ-like : HTML écrit dans : ", chemin_sortie))
181
+ chemin_sortie
182
+ }
iramuteq-like/cooccurrences_iramuteq.R ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: cooccurrences.R regroupe l'analyse des cooccurrences par classe.
2
+
3
+ generer_cooccurrences_par_classe <- function(tok_ok,
4
+ filtered_corpus_ok,
5
+ classes_uniques,
6
+ cooc_dir,
7
+ top_n,
8
+ top_feat,
9
+ window_cooc) {
10
+ top_n_demande <- suppressWarnings(as.integer(top_n))
11
+ if (!is.finite(top_n_demande) || is.na(top_n_demande)) top_n_demande <- 20L
12
+ top_n_demande <- max(5L, top_n_demande)
13
+
14
+ top_feat_demande <- suppressWarnings(as.integer(top_feat))
15
+ if (!is.finite(top_feat_demande) || is.na(top_feat_demande)) top_feat_demande <- 20L
16
+ top_feat_demande <- max(5L, top_feat_demande)
17
+
18
+ window_effectif <- suppressWarnings(as.integer(window_cooc))
19
+ if (!is.finite(window_effectif) || is.na(window_effectif)) window_effectif <- 5L
20
+ window_effectif <- max(1L, window_effectif)
21
+
22
+ for (cl in classes_uniques) {
23
+ tok_cl <- tok_ok[docvars(filtered_corpus_ok)$Classes == cl]
24
+ cooc_png <- file.path(cooc_dir, paste0("cluster_", cl, "_fcm_network.png"))
25
+
26
+ try({
27
+ if (length(tok_cl) > 0) {
28
+ fcm_cl <- fcm(tok_cl, context = "window", window = window_effectif, tri = FALSE)
29
+ term_freq <- sort(colSums(fcm_cl), decreasing = TRUE)
30
+
31
+ # On borne aussi par top_n pour garder une cohérence entre nuage de mots et graphe de cooccurrences.
32
+ top_feat_effectif <- min(top_feat_demande, top_n_demande)
33
+ feat_sel <- names(term_freq)[seq_len(min(top_feat_effectif, length(term_freq)))]
34
+ fcm_cl <- fcm_select(fcm_cl, feat_sel, selection = "keep")
35
+
36
+ adj <- as.matrix(fcm_cl)
37
+ g <- graph_from_adjacency_matrix(adj, mode = "undirected", weighted = TRUE, diag = FALSE)
38
+
39
+ num_nodes <- length(V(g))
40
+ palette_colors <- brewer.pal(min(8, num_nodes), "Set3")
41
+ V(g)$color <- palette_colors[seq_along(V(g))]
42
+
43
+ png(cooc_png, width = 1600, height = 1200)
44
+ plot(
45
+ g,
46
+ layout = layout_with_fr(g),
47
+ main = paste("Cooccurrences - Classe", cl),
48
+ vertex.size = 16,
49
+ vertex.color = V(g)$color,
50
+ vertex.label = V(g)$name,
51
+ vertex.label.cex = 1,
52
+ edge.width = E(g)$weight / 2,
53
+ edge.color = "gray80"
54
+ )
55
+ dev.off()
56
+ }
57
+ }, silent = TRUE)
58
+ }
59
+ }
60
+
61
+ construire_table_cooccurrences <- function(cooc_dir) {
62
+ cooc_files <- list.files(cooc_dir, pattern = "\\.png$", full.names = FALSE)
63
+ if (length(cooc_files) > 0) {
64
+ cooc_classes <- gsub("^cluster_([0-9]+)_fcm_network\\.png$", "\\1", cooc_files)
65
+ coocs_df <- data.frame(
66
+ classe = cooc_classes,
67
+ src = file.path("cooccurrences", cooc_files),
68
+ stringsAsFactors = FALSE
69
+ )
70
+ coocs_df <- coocs_df[order(suppressWarnings(as.integer(coocs_df$classe))), , drop = FALSE]
71
+ } else {
72
+ coocs_df <- data.frame(classe = character(0), src = character(0), stringsAsFactors = FALSE)
73
+ }
74
+
75
+ coocs_df
76
+ }
iramuteq-like/dendogramme_iramuteq.R ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: point d'entrée UI pour le tracé du dendrogramme IRaMuTeQ-like.
2
+
3
+ tracer_dendogramme_iramuteq_ui <- function(rv,
4
+ top_n_terms = 4,
5
+ orientation = "vertical",
6
+ display_method = "compact") {
7
+ if (is.null(rv$res) && is.null(rv$res_chd)) {
8
+ plot.new()
9
+ text(0.5, 0.5, "Dendrogramme CHD indisponible.", cex = 1.1)
10
+ return(invisible(NULL))
11
+ }
12
+
13
+ chd_obj <- NULL
14
+ if (!is.null(rv$res$chd)) {
15
+ chd_obj <- rv$res$chd
16
+ } else if (!is.null(rv$res_chd)) {
17
+ chd_obj <- rv$res_chd
18
+ } else if (exists("obtenir_objet_dendrogramme", mode = "function", inherits = TRUE)) {
19
+ chd_obj <- obtenir_objet_dendrogramme(rv$res)
20
+ }
21
+
22
+ terminales <- if (!is.null(rv$res$terminales)) rv$res$terminales else NULL
23
+ classes <- if (!is.null(rv$res$classes)) rv$res$classes else NULL
24
+ if (is.null(classes) && !is.null(rv$filtered_corpus) && "Classes" %in% names(docvars(rv$filtered_corpus))) {
25
+ classes <- docvars(rv$filtered_corpus)$Classes
26
+ }
27
+
28
+ if (is.null(chd_obj)) {
29
+ plot.new()
30
+ text(0.5, 0.5, "Dendrogramme CHD indisponible (objet CHD introuvable).", cex = 1.1)
31
+ return(invisible(NULL))
32
+ }
33
+
34
+ tracer_dendrogramme_chd_iramuteq(
35
+ chd_obj = chd_obj,
36
+ terminales = terminales,
37
+ classes = classes,
38
+ res_stats_df = rv$res_stats_df,
39
+ top_n_terms = top_n_terms,
40
+ orientation = orientation,
41
+ display_method = display_method
42
+ )
43
+
44
+ invisible(NULL)
45
+ }
iramuteq-like/nettoyage_iramuteq.R ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: nettoyage_iramuteq.R isole la préparation texte du mode IRaMuTeQ-like.
2
+ # Cette logique est volontairement séparée de Rainette car les conventions de préparation
3
+ # ne sont pas identiques (script textprepa Python et dictionnaire lexique_fr imposé).
4
+
5
+ appliquer_nettoyage_iramuteq <- function(textes,
6
+ activer_nettoyage = FALSE,
7
+ forcer_minuscules = FALSE,
8
+ supprimer_chiffres = FALSE,
9
+ supprimer_apostrophes = FALSE) {
10
+ x <- as.character(textes)
11
+ if (length(x) == 0) return(character(0))
12
+
13
+ x <- gsub("\u00A0", " ", x, fixed = TRUE)
14
+
15
+ if (isTRUE(supprimer_chiffres)) {
16
+ x <- gsub("[0-9]+", " ", x, perl = TRUE)
17
+ }
18
+
19
+ if (isTRUE(supprimer_apostrophes)) {
20
+ x <- gsub("(?i)\\b(?:[cdjlmnst]|qu)['’`´ʼʹ](?=\\p{L})", "", x, perl = TRUE)
21
+ }
22
+
23
+ if (isTRUE(activer_nettoyage)) {
24
+ regex_autorises <- "a-zA-Z0-9àÀâÂäÄáÁåÅãéÉèÈêÊëËìÌîÎïÏíÍóÓòÒôÔöÖõÕøØùÙûÛüÜúÚçÇßœŒ’ñÑ\\.:,;!\\?'"
25
+ regex_a_supprimer <- paste0("[^", regex_autorises, "]")
26
+ x <- gsub(regex_a_supprimer, " ", x, perl = TRUE)
27
+ }
28
+
29
+ x <- gsub("\\s+", " ", x, perl = TRUE)
30
+ x <- trimws(x)
31
+
32
+ if (isTRUE(forcer_minuscules)) {
33
+ x <- tolower(x)
34
+ }
35
+
36
+ x
37
+ }
iramuteq-like/nlp_lexique_iramuteq.R ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: nlp_lexique.R porte une partie du pipeline d'analyse Rainette.
2
+ # Ce script centralise une responsabilité métier/technique utilisée par l'application.
3
+ # Il facilite la maintenance en explicitant le périmètre et les points d'intégration.
4
+ # Module NLP - lemmatisation via lexique externe (dictionnaires/lexique_fr.csv)
5
+ # Ce module charge un lexique 3 colonnes au format canonique
6
+ # (c_mot, c_lemme, c_morpho).
7
+ # et applique une lemmatisation explicite sans fallback silencieux vers spaCy.
8
+
9
+ charger_lexique_fr <- function(chemin = "dictionnaires/lexique_fr.csv") {
10
+ fichier <- tryCatch(normalizePath(chemin, mustWork = TRUE), error = function(e) NA_character_)
11
+ if (is.na(fichier) || !file.exists(fichier)) {
12
+ stop(
13
+ paste0(
14
+ "Lexique (fr) introuvable : fichier '", chemin,
15
+ "' absent. Ajoute dictionnaires/lexique_fr.csv (format lexique_fr ou IRaMuTeQ)."
16
+ )
17
+ )
18
+ }
19
+
20
+ lire_tsv <- function() {
21
+ read.delim(
22
+ fichier,
23
+ sep = "\t",
24
+ header = TRUE,
25
+ stringsAsFactors = FALSE,
26
+ fileEncoding = "UTF-8",
27
+ check.names = FALSE
28
+ )
29
+ }
30
+
31
+ lire_csv <- function() {
32
+ read.csv(
33
+ fichier,
34
+ sep = ",",
35
+ header = TRUE,
36
+ stringsAsFactors = FALSE,
37
+ fileEncoding = "UTF-8",
38
+ check.names = FALSE
39
+ )
40
+ }
41
+
42
+ lire_csv_point_virgule <- function() {
43
+ read.csv(
44
+ fichier,
45
+ sep = ";",
46
+ header = TRUE,
47
+ stringsAsFactors = FALSE,
48
+ fileEncoding = "UTF-8",
49
+ check.names = FALSE
50
+ )
51
+ }
52
+
53
+ lexique <- tryCatch(lire_tsv(), error = function(e) NULL)
54
+ if (is.null(lexique) || ncol(lexique) <= 1) {
55
+ lexique <- tryCatch(lire_csv(), error = function(e) NULL)
56
+ }
57
+ if (is.null(lexique) || ncol(lexique) <= 1) {
58
+ lexique <- tryCatch(lire_csv_point_virgule(), error = function(e) NULL)
59
+ }
60
+
61
+ if (is.null(lexique) || nrow(lexique) == 0) {
62
+ stop("Lexique (fr) invalide : fichier vide ou illisible.")
63
+ }
64
+
65
+ names(lexique) <- trimws(sub("^\ufeff", "", names(lexique)))
66
+
67
+ colonnes_attendues <- c("c_mot", "c_lemme", "c_morpho")
68
+ manquantes <- setdiff(colonnes_attendues, names(lexique))
69
+ if (length(manquantes) > 0) {
70
+ stop(
71
+ paste0(
72
+ "Lexique (fr) mal configuré : colonnes manquantes [",
73
+ paste(manquantes, collapse = ", "),
74
+ "]. Format attendu : c_mot, c_lemme, c_morpho."
75
+ )
76
+ )
77
+ }
78
+
79
+ lexique$c_mot <- tolower(trimws(as.character(lexique$c_mot)))
80
+ lexique$c_lemme <- tolower(trimws(as.character(lexique$c_lemme)))
81
+ lexique$c_morpho <- toupper(trimws(as.character(lexique$c_morpho)))
82
+
83
+ lexique <- lexique[
84
+ nzchar(lexique$c_mot) &
85
+ nzchar(lexique$c_lemme) &
86
+ nzchar(lexique$c_morpho),
87
+ ,
88
+ drop = FALSE
89
+ ]
90
+
91
+ if (nrow(lexique) == 0) {
92
+ stop("Lexique (fr) mal configuré : aucune entrée exploitable après nettoyage.")
93
+ }
94
+
95
+ lexique
96
+ }
97
+
98
+ construire_type_lexique_fr <- function(termes, lexique) {
99
+ if (is.null(termes)) return(character(0))
100
+ x <- tolower(trimws(as.character(termes)))
101
+ x[is.na(x)] <- ""
102
+
103
+ if (is.null(lexique) || !is.data.frame(lexique) || nrow(lexique) < 1) {
104
+ return(rep("", length(x)))
105
+ }
106
+
107
+ cols_ok <- all(c("c_mot", "c_lemme", "c_morpho") %in% names(lexique))
108
+ if (!cols_ok) {
109
+ return(rep("", length(x)))
110
+ }
111
+
112
+ key_mot <- tolower(trimws(as.character(lexique$c_mot)))
113
+ key_lemme <- tolower(trimws(as.character(lexique$c_lemme)))
114
+ val_type <- tolower(trimws(as.character(lexique$c_morpho)))
115
+
116
+ map_lemme <- tapply(val_type, key_lemme, function(v) unique(v)[1])
117
+ map_mot <- tapply(val_type, key_mot, function(v) unique(v)[1])
118
+
119
+ out <- unname(map_lemme[x])
120
+ idx_na <- is.na(out) | !nzchar(out)
121
+ if (any(idx_na)) {
122
+ out[idx_na] <- unname(map_mot[x[idx_na]])
123
+ }
124
+
125
+ out[is.na(out)] <- ""
126
+ out
127
+ }
128
+
129
+
130
+ lemmatiser_textes_lexique <- function(textes, lexique, rv = NULL) {
131
+ tok <- quanteda::tokens(
132
+ textes,
133
+ remove_punct = FALSE,
134
+ remove_numbers = FALSE
135
+ )
136
+
137
+ map_forme_lemme <- tapply(
138
+ lexique$c_lemme,
139
+ lexique$c_mot,
140
+ function(x) unique(x)[1]
141
+ )
142
+
143
+ liste_tok <- as.list(tok)
144
+ textes_lem <- vapply(liste_tok, function(v) {
145
+ if (length(v) == 0) return("")
146
+ v_low <- tolower(as.character(v))
147
+ lem <- unname(map_forme_lemme[v_low])
148
+ lem[is.na(lem) | !nzchar(lem)] <- v_low[is.na(lem) | !nzchar(lem)]
149
+ paste(lem, collapse = " ")
150
+ }, FUN.VALUE = character(1))
151
+
152
+ if (!is.null(rv)) {
153
+ ajouter_log(rv, "Lexique (fr) : lemmatisation forme->lemme appliquée sans spaCy.")
154
+ }
155
+
156
+ textes_lem
157
+ }
158
+
159
+
160
+
161
+ filtrer_textes_lexique_par_cgram <- function(textes, lexique, cgram_a_conserver, rv = NULL) {
162
+ cgram_keep <- unique(toupper(trimws(as.character(cgram_a_conserver))))
163
+ cgram_keep <- cgram_keep[nzchar(cgram_keep)]
164
+ if (length(cgram_keep) == 0) return(textes)
165
+
166
+ formes_keep <- unique(lexique$c_mot[lexique$c_morpho %in% cgram_keep])
167
+ formes_keep <- formes_keep[nzchar(formes_keep)]
168
+
169
+ tok <- quanteda::tokens(
170
+ textes,
171
+ remove_punct = FALSE,
172
+ remove_numbers = FALSE
173
+ )
174
+
175
+ liste_tok <- as.list(tok)
176
+ total_tokens <- 0L
177
+ total_conserves <- 0L
178
+
179
+ textes_filtres <- vapply(liste_tok, function(v) {
180
+ if (length(v) == 0) return("")
181
+ v_low <- tolower(trimws(as.character(v)))
182
+ total_tokens <<- total_tokens + length(v_low)
183
+ garder <- v_low %in% formes_keep
184
+ total_conserves <<- total_conserves + sum(garder)
185
+ paste(v_low[garder], collapse = " ")
186
+ }, FUN.VALUE = character(1))
187
+
188
+ if (!is.null(rv)) {
189
+ ajouter_log(
190
+ rv,
191
+ paste0(
192
+ "Lexique (fr) : filtrage c_morpho [", paste(cgram_keep, collapse = ", "),
193
+ "] => ", total_conserves, "/", total_tokens, " token(s) conservé(s)."
194
+ )
195
+ )
196
+ }
197
+
198
+ names(textes_filtres) <- names(textes)
199
+ textes_filtres
200
+ }
iramuteq-like/pipeline_iramuteq_analysis_iramuteq.R ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: pipeline_iramuteq_analysis.R porte le pipeline dédié au mode IRaMuTeQ-like.
2
+ # Ce flux est volontairement séparé de Rainette/spaCy pour éviter tout mélange de logique.
3
+
4
+ executer_pipeline_iramuteq <- function(input, rv, textes_chd) {
5
+ ajouter_log(rv, "IRaMuTeQ-like : pipeline lexical dédié (lexique_fr uniquement).")
6
+
7
+ tok_base <- tokens(
8
+ textes_chd,
9
+ remove_punct = isTRUE(input$supprimer_ponctuation),
10
+ remove_numbers = isTRUE(input$supprimer_chiffres)
11
+ )
12
+
13
+ res_dfm <- construire_dfm_avec_fallback_stopwords(
14
+ tok_base = tok_base,
15
+ min_docfreq = input$min_docfreq,
16
+ retirer_stopwords = isTRUE(input$retirer_stopwords),
17
+ langue_spacy = "fr",
18
+ rv = rv,
19
+ libelle = "IRaMuTeQ-like",
20
+ source_dictionnaire = "lexique_fr",
21
+ lexique_source_stopwords = "quanteda"
22
+ )
23
+
24
+ rv$spacy_tokens_df <- NULL
25
+ rv$lexique_fr_df <- NULL
26
+
27
+ list(
28
+ tok = res_dfm$tok,
29
+ dfm_obj = res_dfm$dfm,
30
+ langue_reference = "fr",
31
+ source_dictionnaire = "lexique_fr"
32
+ )
33
+ }
iramuteq-like/pipeline_lexique_analysis_iramuteq.R ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: pipeline_lexique_analysis.R porte une partie du pipeline d'analyse Rainette.
2
+ # Ce script centralise une responsabilité métier/technique utilisée par l'application.
3
+ # Il facilite la maintenance en explicitant le périmètre et les points d'intégration.
4
+ executer_pipeline_lexique <- function(input, rv, textes_chd) {
5
+ filtrage_morpho <- isTRUE(input$filtrage_morpho)
6
+ utiliser_lemmes_lexique <- isTRUE(input$lexique_utiliser_lemmes)
7
+ langue_reference <- "fr"
8
+ lexique_source_stopwords <- "quanteda"
9
+
10
+ if (isTRUE(utiliser_lemmes_lexique) || isTRUE(filtrage_morpho)) {
11
+ lexique_fr <- charger_lexique_fr("dictionnaires/lexique_fr.csv")
12
+ ajouter_log(rv, paste0("Lexique (fr) chargé : ", nrow(lexique_fr), " entrées."))
13
+ rv$lexique_fr_df <- lexique_fr
14
+ } else {
15
+ lexique_fr <- NULL
16
+ rv$lexique_fr_df <- NULL
17
+ ajouter_log(rv, "Lexique (fr) sans lemmatisation/filtrage : conservation des formes d'origine.")
18
+ }
19
+
20
+ rv$spacy_tokens_df <- NULL
21
+
22
+ if (isTRUE(utiliser_lemmes_lexique) && !isTRUE(filtrage_morpho)) {
23
+ ajouter_log(rv, "Lexique (fr) sans filtrage morphosyntaxique : lemmatisation directe forme->lemme.")
24
+
25
+ textes_lexique <- lemmatiser_textes_lexique(
26
+ textes = textes_chd,
27
+ lexique = lexique_fr,
28
+ rv = rv
29
+ )
30
+
31
+ } else if (isTRUE(filtrage_morpho)) {
32
+ cgram_lexique_a_conserver <- toupper(trimws(as.character(input$pos_lexique_a_conserver)))
33
+ cgram_lexique_a_conserver <- unique(cgram_lexique_a_conserver[nzchar(cgram_lexique_a_conserver)])
34
+ if (is.null(cgram_lexique_a_conserver) || length(cgram_lexique_a_conserver) == 0) {
35
+ cgram_lexique_a_conserver <- c("NOM", "ADJ", "VER")
36
+ }
37
+
38
+ ajouter_log(
39
+ rv,
40
+ paste0(
41
+ "lexique_fr | filtrage morpho=1 (c_morpho: ",
42
+ paste(cgram_lexique_a_conserver, collapse = ", "),
43
+ ") | lemmes=", ifelse(utiliser_lemmes_lexique, "1", "0"),
44
+ " | stopwords: quanteda (Lexique fr)"
45
+ )
46
+ )
47
+
48
+ textes_lexique <- filtrer_textes_lexique_par_cgram(
49
+ textes = textes_chd,
50
+ lexique = lexique_fr,
51
+ cgram_a_conserver = cgram_lexique_a_conserver,
52
+ rv = rv
53
+ )
54
+
55
+ if (isTRUE(utiliser_lemmes_lexique)) {
56
+ textes_lexique <- lemmatiser_textes_lexique(
57
+ textes = textes_lexique,
58
+ lexique = lexique_fr,
59
+ rv = rv
60
+ )
61
+ }
62
+ } else {
63
+ ajouter_log(rv, "Lexique (fr) sans filtrage morphosyntaxique ni lemmatisation : pipeline standard.")
64
+ textes_lexique <- textes_chd
65
+ }
66
+
67
+ tok_base <- tokens(
68
+ textes_lexique,
69
+ remove_punct = isTRUE(input$supprimer_ponctuation),
70
+ remove_numbers = isTRUE(input$supprimer_chiffres)
71
+ )
72
+
73
+ res_dfm <- construire_dfm_avec_fallback_stopwords(
74
+ tok_base = tok_base,
75
+ min_docfreq = input$min_docfreq,
76
+ retirer_stopwords = isTRUE(input$retirer_stopwords),
77
+ langue_spacy = langue_reference,
78
+ rv = rv,
79
+ libelle = ifelse(utiliser_lemmes_lexique, "Lexique (fr)", "lexique_fr"),
80
+ source_dictionnaire = "lexique_fr",
81
+ lexique_source_stopwords = lexique_source_stopwords
82
+ )
83
+
84
+ list(
85
+ tok = res_dfm$tok,
86
+ dfm_obj = res_dfm$dfm,
87
+ langue_reference = langue_reference,
88
+ source_dictionnaire = "lexique_fr",
89
+ lexique_source_stopwords = lexique_source_stopwords
90
+ )
91
+ }
iramuteq-like/server_events_lancer_iramuteq.R ADDED
@@ -0,0 +1,1133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: server_events_lancer_iramuteq.R porte le pipeline d'analyse IRaMuTeQ-like.
2
+ # Ce script centralise une responsabilité métier/technique utilisée par l'application.
3
+ # Module server - événement principal `input$lancer`
4
+ # Ce fichier encapsule le pipeline principal lancé au clic sur "Lancer l'analyse"
5
+ # (préparation, CHD/AFC/NER, exports) pour alléger `app.R` à comportement constant.
6
+
7
+ register_events_lancer <- function(input, output, session, rv) {
8
+ app_dir <- tryCatch(shiny::getShinyOption("appDir"), error = function(e) NULL)
9
+ if (is.null(app_dir) || !nzchar(app_dir)) app_dir <- getwd()
10
+ env_modules <- environment()
11
+
12
+ charger_module_langue <- function() {
13
+ candidats_langue <- unique(c(
14
+ file.path(app_dir, "iramuteq-like", "nlp_lexique_iramuteq.R"),
15
+ file.path(getwd(), "iramuteq-like", "nlp_lexique_iramuteq.R"),
16
+ file.path("iramuteq-like", "nlp_lexique_iramuteq.R"),
17
+ file.path(app_dir, "iramuteq-like", "nlp_lexique_iramuteq.R"),
18
+ file.path(getwd(), "iramuteq-like", "nlp_lexique_iramuteq.R"),
19
+ file.path("iramuteq-like", "nlp_lexique_iramuteq.R")
20
+ ))
21
+
22
+ dernier_chemin <- candidats_langue[[1]]
23
+ derniere_raison <- "fonction verifier_coherence_dictionnaire_langue absente après source"
24
+
25
+ for (chemin_langue in candidats_langue) {
26
+ dernier_chemin <- chemin_langue
27
+ if (!file.exists(chemin_langue)) next
28
+
29
+ source_res <- tryCatch({
30
+ source(chemin_langue, encoding = "UTF-8", local = env_modules)
31
+ NULL
32
+ }, error = function(e) e)
33
+
34
+ if (inherits(source_res, "error")) {
35
+ derniere_raison <- paste0("échec source: ", conditionMessage(source_res))
36
+ next
37
+ }
38
+
39
+ if (exists("verifier_coherence_dictionnaire_langue", mode = "function", envir = env_modules, inherits = TRUE)) {
40
+ return(list(ok = TRUE, chemin = chemin_langue, raison = ""))
41
+ }
42
+
43
+ derniere_raison <- "fonction verifier_coherence_dictionnaire_langue absente après source"
44
+ }
45
+
46
+ list(ok = FALSE, chemin = dernier_chemin, raison = derniere_raison)
47
+ }
48
+
49
+ if (!exists("appliquer_nettoyage_iramuteq", mode = "function", inherits = TRUE)) {
50
+ chemin_nettoyage_iramuteq <- file.path(app_dir, "iramuteq-like", "nettoyage_iramuteq.R")
51
+ if (file.exists(chemin_nettoyage_iramuteq)) {
52
+ source(chemin_nettoyage_iramuteq, encoding = "UTF-8", local = TRUE)
53
+ }
54
+ }
55
+
56
+ if (!exists("appliquer_nettoyage_rainette", mode = "function", inherits = TRUE)) {
57
+ appliquer_nettoyage_rainette <- function(textes,
58
+ activer_nettoyage = FALSE,
59
+ forcer_minuscules = FALSE,
60
+ supprimer_chiffres = FALSE,
61
+ supprimer_apostrophes = FALSE) {
62
+ ajouter_log(rv, "Avertissement: appliquer_nettoyage_rainette indisponible; nettoyage contourné pour préserver l'exécution.")
63
+ if (is.null(textes)) return(character(0))
64
+ x <- as.character(textes)
65
+ if (isTRUE(forcer_minuscules)) x <- tolower(x)
66
+ x
67
+ }
68
+ }
69
+
70
+ if (!exists("appliquer_nettoyage_iramuteq", mode = "function", inherits = TRUE)) {
71
+ appliquer_nettoyage_iramuteq <- appliquer_nettoyage_rainette
72
+ }
73
+
74
+
75
+ executer_textprepa_iramuteq <- function(ids, textes, input, rv) {
76
+ candidats_script <- unique(c(
77
+ file.path(app_dir, "iramuteq-like", "textprepa_iramuteq.py"),
78
+ file.path(getwd(), "iramuteq-like", "textprepa_iramuteq.py"),
79
+ file.path("iramuteq-like", "textprepa_iramuteq.py")
80
+ ))
81
+ script_path <- candidats_script[file.exists(candidats_script)][1]
82
+ if (is.na(script_path) || !nzchar(script_path)) {
83
+ stop("IRaMuTeQ-like: script textprepa_iramuteq.py introuvable.")
84
+ }
85
+
86
+ py_bin <- Sys.which("python3")
87
+ if (!nzchar(py_bin)) py_bin <- Sys.which("python")
88
+ if (!nzchar(py_bin)) {
89
+ stop("IRaMuTeQ-like: Python introuvable (python3/python).")
90
+ }
91
+
92
+ in_tsv <- tempfile(pattern = "iramuteq_prepa_in_", fileext = ".tsv")
93
+ out_tsv <- tempfile(pattern = "iramuteq_prepa_out_", fileext = ".tsv")
94
+
95
+ df_in <- data.frame(
96
+ doc_id = as.character(ids),
97
+ text = as.character(textes),
98
+ stringsAsFactors = FALSE
99
+ )
100
+ write.table(df_in, file = in_tsv, sep = " ", row.names = FALSE, col.names = TRUE, quote = TRUE, fileEncoding = "UTF-8")
101
+
102
+ args <- c(
103
+ script_path,
104
+ "--input", in_tsv,
105
+ "--output", out_tsv,
106
+ "--nettoyage_caracteres", ifelse(isTRUE(input$nettoyage_caracteres), "1", "0"),
107
+ "--forcer_minuscules_avant", ifelse(isTRUE(input$forcer_minuscules_avant), "1", "0"),
108
+ "--supprimer_chiffres", ifelse(isTRUE(input$supprimer_chiffres), "1", "0"),
109
+ "--supprimer_apostrophes", ifelse(isTRUE(input$supprimer_apostrophes), "1", "0")
110
+ )
111
+
112
+ res <- tryCatch(
113
+ system2(py_bin, args = args, stdout = TRUE, stderr = TRUE),
114
+ error = function(e) structure(conditionMessage(e), status = 1L)
115
+ )
116
+ status <- attr(res, "status")
117
+ if (is.null(status)) status <- 0L
118
+ if (!identical(as.integer(status), 0L) || !file.exists(out_tsv)) {
119
+ out_msg <- if (length(res)) paste(res, collapse = " | ") else "(aucun message)"
120
+ stop(paste0("IRaMuTeQ-like: échec textprepa_iramuteq.py (code ", status, ") : ", out_msg))
121
+ }
122
+
123
+ df_out <- read.delim(out_tsv, sep = " ", header = TRUE, stringsAsFactors = FALSE, quote = '"', encoding = "UTF-8")
124
+ if (!all(c("doc_id", "text") %in% names(df_out))) {
125
+ stop("IRaMuTeQ-like: sortie textprepa invalide (colonnes doc_id/text manquantes).")
126
+ }
127
+
128
+ ids_chr <- as.character(ids)
129
+ idx <- match(ids_chr, as.character(df_out$doc_id))
130
+ if (any(is.na(idx))) {
131
+ stop("IRaMuTeQ-like: alignement doc_id invalide après textprepa.")
132
+ }
133
+
134
+ textes_prep <- as.character(df_out$text[idx])
135
+ names(textes_prep) <- ids_chr
136
+ ajouter_log(rv, "IRaMuTeQ-like: préparation texte exécutée via iramuteq-like/textprepa_iramuteq.py")
137
+ textes_prep
138
+ }
139
+
140
+ formater_df_csv_6_decimales <- function(df) {
141
+ if (is.null(df)) return(df)
142
+ df_out <- df
143
+ for (nm in names(df_out)) {
144
+ col <- df_out[[nm]]
145
+ if (is.numeric(col)) {
146
+ df_out[[nm]] <- ifelse(
147
+ is.na(col),
148
+ NA_character_,
149
+ formatC(col, format = "f", digits = 6)
150
+ )
151
+ }
152
+ }
153
+ df_out
154
+ }
155
+
156
+ ecrire_csv_6_decimales <- function(df, chemin, row.names = FALSE) {
157
+ write.csv(formater_df_csv_6_decimales(df), chemin, row.names = row.names)
158
+ }
159
+
160
+ observeEvent(input$modele_chd, {
161
+ if (identical(as.character(input$modele_chd), "iramuteq")) {
162
+ updateRadioButtons(
163
+ session,
164
+ "source_dictionnaire",
165
+ choices = c("Lexique (fr)" = "lexique_fr"),
166
+ selected = "lexique_fr"
167
+ )
168
+ if (isTRUE(input$activer_ner)) {
169
+ updateCheckboxInput(session, "activer_ner", value = FALSE)
170
+ ajouter_log(rv, "Mode IRaMuTeQ-like : NER spaCy automatiquement désactivé (mode uniquement Lexique fr).")
171
+ }
172
+ } else {
173
+ updateRadioButtons(
174
+ session,
175
+ "source_dictionnaire",
176
+ choices = c("spaCy" = "spacy", "Lexique (fr)" = "lexique_fr"),
177
+ selected = if (identical(as.character(input$source_dictionnaire), "lexique_fr")) "lexique_fr" else "spacy"
178
+ )
179
+ }
180
+ }, ignoreInit = FALSE)
181
+
182
+ output$ui_concordancier_iramuteq <- renderUI({
183
+ req(rv$export_dir)
184
+
185
+ if (!identical(rv$res_type, "iramuteq")) {
186
+ return(tags$p("Concordancier IRaMuTeQ-like indisponible (mode Rainette actif)."))
187
+ }
188
+
189
+ if (is.null(rv$exports_prefix) || !nzchar(rv$exports_prefix)) {
190
+ return(tags$div(
191
+ style = "padding: 12px;",
192
+ tags$p("Préfixe de ressources invalide."),
193
+ tags$p("Relance l'analyse pour régénérer les exports.")
194
+ ))
195
+ }
196
+
197
+ if (!(rv$exports_prefix %in% names(shiny::resourcePaths()))) {
198
+ shiny::addResourcePath(rv$exports_prefix, rv$export_dir)
199
+ }
200
+
201
+ candidats_html <- c(
202
+ rv$html_file,
203
+ file.path(rv$export_dir, "segments_par_classe.html"),
204
+ file.path(rv$export_dir, "concordancier.html")
205
+ )
206
+ candidats_dyn <- list.files(
207
+ rv$export_dir,
208
+ pattern = "(segments.*classe|concord).*\\.html$",
209
+ ignore.case = TRUE,
210
+ full.names = TRUE
211
+ )
212
+ candidats_html <- c(candidats_html, candidats_dyn)
213
+ candidats_html <- unique(candidats_html[!is.na(candidats_html) & nzchar(candidats_html)])
214
+ html_existant <- candidats_html[file.exists(candidats_html)]
215
+
216
+ if (length(html_existant) == 0) {
217
+ return(tags$div(
218
+ style = "padding: 12px;",
219
+ tags$p("Le fichier du concordancier HTML n'est pas disponible pour cette analyse."),
220
+ tags$p("Relance l'analyse puis vérifie les logs si le problème persiste.")
221
+ ))
222
+ }
223
+
224
+ src_html <- html_existant[[1]]
225
+ nom_html <- basename(src_html)
226
+ src_dans_exports <- file.path(rv$export_dir, nom_html)
227
+
228
+ if (!isTRUE(file.exists(src_dans_exports))) {
229
+ ok_copy <- tryCatch(file.copy(src_html, src_dans_exports, overwrite = TRUE), error = function(e) FALSE)
230
+ if (isTRUE(ok_copy)) src_html <- src_dans_exports
231
+ } else {
232
+ src_html <- src_dans_exports
233
+ }
234
+
235
+ tags$iframe(
236
+ src = paste0("/", rv$exports_prefix, "/", basename(src_html)),
237
+ style = "width: 100%; height: 70vh; border: 1px solid #999;"
238
+ )
239
+ })
240
+
241
+ output$ui_wordcloud_iramuteq <- renderUI({
242
+ req(rv$export_dir, rv$exports_prefix)
243
+
244
+ if (!identical(rv$res_type, "iramuteq")) {
245
+ return(tags$p("Nuage de mots IRaMuTeQ-like indisponible (mode Rainette actif)."))
246
+ }
247
+
248
+ classe_sel <- as.character(input$classe_viz_iramuteq)
249
+ if (length(classe_sel) != 1 || is.na(classe_sel) || !nzchar(classe_sel)) {
250
+ return(tags$p("Sélectionne une classe pour afficher le nuage de mots."))
251
+ }
252
+
253
+ src_rel <- file.path("wordclouds", paste0("cluster_", classe_sel, "_wordcloud.png"))
254
+ if (!file.exists(file.path(rv$export_dir, src_rel))) {
255
+ return(tags$p("Aucun nuage de mots disponible pour cette classe."))
256
+ }
257
+
258
+ tags$div(
259
+ style = "text-align: center;",
260
+ tags$img(
261
+ src = paste0("/", rv$exports_prefix, "/", src_rel),
262
+ style = "max-width: 100%; height: auto; border: 1px solid #999; display: inline-block;"
263
+ )
264
+ )
265
+ })
266
+
267
+ normaliser_id_classe_local <- function(x) {
268
+ x_chr <- as.character(x)
269
+ x_chr <- trimws(x_chr)
270
+
271
+ x_num <- suppressWarnings(as.numeric(x_chr))
272
+ need_extract <- is.na(x_num) & !is.na(x_chr) & nzchar(x_chr)
273
+
274
+ if (any(need_extract)) {
275
+ extrait <- sub("^.*?(\\d+).*$", "\\1", x_chr[need_extract])
276
+ extrait[!grepl("\\d", x_chr[need_extract])] <- NA_character_
277
+ x_num[need_extract] <- suppressWarnings(as.numeric(extrait))
278
+ }
279
+
280
+ x_num
281
+ }
282
+
283
+ observeEvent(input$lancer, {
284
+ rv$logs <- ""
285
+ rv$statut <- "Vérification du fichier..."
286
+ rv$progression <- 0
287
+
288
+ rv$spacy_tokens_df <- NULL
289
+ rv$lexique_fr_df <- NULL
290
+ rv$textes_indexation <- NULL
291
+ rv$ner_df <- NULL
292
+ rv$ner_nb_segments <- NA_integer_
293
+ rv$afc_obj <- NULL
294
+ rv$afc_erreur <- NULL
295
+ rv$afc_vars_obj <- NULL
296
+ rv$afc_vars_erreur <- NULL
297
+
298
+ rv$afc_dir <- NULL
299
+ rv$afc_table_mots <- NULL
300
+ rv$afc_table_vars <- NULL
301
+ rv$afc_plot_classes <- NULL
302
+ rv$afc_plot_termes <- NULL
303
+ rv$afc_plot_vars <- NULL
304
+
305
+ rv$segments_file <- NULL
306
+ rv$stats_file <- NULL
307
+ rv$html_file <- NULL
308
+ rv$ner_file <- NULL
309
+ rv$zip_file <- NULL
310
+
311
+ rv$res <- NULL
312
+ rv$res_chd <- NULL
313
+ rv$dfm_chd <- NULL
314
+ rv$res_type <- "simple"
315
+ rv$max_n_groups <- NULL
316
+ rv$max_n_groups_chd <- NULL
317
+ rv$explor_assets <- NULL
318
+ rv$stats_corpus_df <- NULL
319
+ rv$stats_zipf_df <- NULL
320
+
321
+ ajouter_log(rv, "Clic sur 'Lancer l'analyse' reçu.")
322
+
323
+ modele_chd <- "iramuteq"
324
+ mode_iramuteq <- TRUE
325
+ source_dictionnaire <- "lexique_fr"
326
+ updateRadioButtons(
327
+ session,
328
+ "source_dictionnaire",
329
+ choices = c("Lexique (fr)" = "lexique_fr"),
330
+ selected = "lexique_fr"
331
+ )
332
+ updateRadioButtons(
333
+ session,
334
+ "modele_chd",
335
+ selected = "iramuteq"
336
+ )
337
+
338
+ if (is.null(input$fichier_corpus) || is.null(input$fichier_corpus$datapath) || !file.exists(input$fichier_corpus$datapath)) {
339
+ rv$statut <- "Aucun fichier uploadé."
340
+ rv$progression <- 0
341
+ ajouter_log(rv, "Aucun fichier uploadé côté serveur. Sélectionne un .txt puis relance.")
342
+ showNotification("Aucun fichier uploadé. Choisis un .txt.", type = "error", duration = 6)
343
+ return(invisible(NULL))
344
+ }
345
+
346
+ if (isTRUE(input$activer_ner) && mode_iramuteq) {
347
+ rv$statut <- "Configuration invalide : NER indisponible en mode IRaMuTeQ-like."
348
+ rv$progression <- 0
349
+ ajouter_log(rv, "Blocage de l'analyse : NER activé en mode IRaMuTeQ-like (mode uniquement Lexique fr).")
350
+ showNotification(
351
+ "Analyse bloquée : le mode IRaMuTeQ-like fonctionne uniquement avec Lexique (fr) et sans NER spaCy.",
352
+ type = "error",
353
+ duration = 8
354
+ )
355
+ return(invisible(NULL))
356
+ }
357
+
358
+ if (isTRUE(input$activer_ner) && !is.null(input$fichier_ner_json) && !is.null(input$fichier_ner_json$datapath) && file.exists(input$fichier_ner_json$datapath)) {
359
+ rv$ner_file <- input$fichier_ner_json$datapath
360
+ ajouter_log(rv, paste0("NER : dictionnaire JSON importé via l'UI : ", input$fichier_ner_json$name))
361
+ }
362
+
363
+ p <- Progress$new(session, min = 0, max = 1)
364
+ on.exit(try(p$close(), silent = TRUE), add = TRUE)
365
+
366
+ avancer <- function(valeur, detail) {
367
+ valeur <- max(0, min(1, valeur))
368
+ p$set(value = valeur, message = "Calculs CHD en cours", detail = detail)
369
+ rv$progression <- round(valeur * 100)
370
+ }
371
+
372
+ tryCatch({
373
+
374
+ avancer(0.02, "Préparation des répertoires")
375
+ rv$statut <- "Préparation des répertoires..."
376
+
377
+ rv$base_dir <- file.path(tempdir(), paste0("rainette_", session$token))
378
+ rv$export_dir <- file.path(rv$base_dir, "exports")
379
+ dir.create(rv$export_dir, showWarnings = FALSE, recursive = TRUE)
380
+ ajouter_log(rv, paste0("export_dir = ", rv$export_dir))
381
+
382
+ avancer(0.08, "Import du corpus")
383
+ rv$statut <- "Import du corpus..."
384
+ chemin_fichier <- input$fichier_corpus$datapath
385
+ md5 <- md5_fichier(chemin_fichier)
386
+ ajouter_log(rv, paste0("MD5 fichier = ", md5))
387
+
388
+ corpus <- import_corpus_iramuteq(chemin_fichier)
389
+ ajouter_log(rv, paste0("Nombre de documents importés : ", ndoc(corpus)))
390
+
391
+ avancer(0.14, "Segmentation")
392
+ rv$statut <- "Segmentation..."
393
+ segment_size <- input$segment_size
394
+ corpus <- split_segments(corpus, segment_size = segment_size)
395
+ ajouter_log(rv, paste0("Nombre de segments après découpage : ", ndoc(corpus)))
396
+
397
+ stats_corpus <- calculer_stats_corpus(
398
+ chemin_fichier = chemin_fichier,
399
+ corpus_segments = corpus,
400
+ nom_corpus = input$fichier_corpus$name
401
+ )
402
+ if (is.null(stats_corpus)) {
403
+ rv$stats_corpus_df <- NULL
404
+ rv$stats_zipf_df <- NULL
405
+ } else {
406
+ rv$stats_corpus_df <- stats_corpus$table
407
+ rv$stats_zipf_df <- stats_corpus$zipf
408
+ }
409
+
410
+ ids_orig <- as.character(docnames(corpus))
411
+ ids_corpus <- ids_orig
412
+ invalides <- is.na(ids_corpus) | !nzchar(trimws(ids_corpus))
413
+ if (any(invalides)) {
414
+ ids_corpus[invalides] <- paste0("doc_", which(invalides))
415
+ }
416
+
417
+ ids_uniques <- make.unique(ids_corpus, sep = "_dup")
418
+ modif_ids <- any(ids_uniques != ids_orig)
419
+ if (isTRUE(modif_ids)) {
420
+ n_problemes <- sum(invalides) + sum(duplicated(ids_corpus))
421
+ ajouter_log(rv, paste0("Docnames invalides/dupliqués détectés après segmentation : ", n_problemes, ". Renommage automatique via make.unique()."))
422
+ }
423
+
424
+ docnames(corpus) <- ids_uniques
425
+ ids_corpus <- as.character(docnames(corpus))
426
+
427
+ textes_orig <- as.character(corpus)
428
+
429
+ avancer(0.18, "Préparation texte (nettoyage / minuscules)")
430
+ rv$statut <- "Préparation texte..."
431
+
432
+ if (mode_iramuteq) {
433
+ textes_nettoyes <- appliquer_nettoyage_iramuteq(
434
+ textes = textes_orig,
435
+ activer_nettoyage = isTRUE(input$nettoyage_caracteres),
436
+ forcer_minuscules = isTRUE(input$forcer_minuscules_avant),
437
+ supprimer_chiffres = isTRUE(input$supprimer_chiffres),
438
+ supprimer_apostrophes = isTRUE(input$supprimer_apostrophes)
439
+ )
440
+
441
+ textes_chd <- executer_textprepa_iramuteq(
442
+ ids = ids_corpus,
443
+ textes = textes_nettoyes,
444
+ input = input,
445
+ rv = rv
446
+ )
447
+ } else {
448
+ textes_chd <- appliquer_nettoyage_rainette(
449
+ textes = textes_orig,
450
+ activer_nettoyage = isTRUE(input$nettoyage_caracteres),
451
+ forcer_minuscules = isTRUE(input$forcer_minuscules_avant),
452
+ supprimer_chiffres = isTRUE(input$supprimer_chiffres),
453
+ supprimer_apostrophes = isTRUE(input$supprimer_apostrophes)
454
+ )
455
+ names(textes_chd) <- ids_corpus
456
+ }
457
+
458
+ if (!exists("verifier_coherence_dictionnaire_langue", mode = "function", inherits = TRUE)) {
459
+ charge_langue <- charger_module_langue()
460
+ if (!isTRUE(charge_langue$ok)) {
461
+ stop(paste0("Module langue indisponible (", charge_langue$raison, ") : ", charge_langue$chemin))
462
+ }
463
+ ajouter_log(rv, paste0("Diagnostic langue: module chargé depuis ", charge_langue$chemin, "."))
464
+ }
465
+
466
+ source_dictionnaire <- "lexique_fr"
467
+
468
+ verifier_coherence_dictionnaire_langue(
469
+ textes_chd,
470
+ if (identical(source_dictionnaire, "lexique_fr")) "fr" else as.character(input$spacy_langue),
471
+ rv = rv
472
+ )
473
+
474
+ avancer(0.22, "Prétraitement + DFM")
475
+ rv$statut <- "Prétraitement et DFM..."
476
+
477
+ ajouter_log(
478
+ rv,
479
+ paste0(
480
+ "Diagnostic pipeline: dictionnaire=", source_dictionnaire,
481
+ " | langue UI=", as.character(input$spacy_langue),
482
+ " | filtrage_morpho=", ifelse(isTRUE(input$filtrage_morpho), "1", "0"),
483
+ " | retirer_stopwords=", ifelse(isTRUE(input$retirer_stopwords), "1", "0"),
484
+ " | supprimer_ponctuation=", ifelse(isTRUE(input$supprimer_ponctuation), "1", "0"),
485
+ " | supprimer_chiffres=", ifelse(isTRUE(input$supprimer_chiffres), "1", "0"),
486
+ " | supprimer_apostrophes=", ifelse(isTRUE(input$supprimer_apostrophes), "1", "0"),
487
+ " | nettoyage_caracteres=", ifelse(isTRUE(input$nettoyage_caracteres), "1", "0")
488
+ )
489
+ )
490
+
491
+ sortie_pipeline <- executer_pipeline_iramuteq(
492
+ input = input,
493
+ rv = rv,
494
+ textes_chd = textes_chd
495
+ )
496
+
497
+ tok <- sortie_pipeline$tok
498
+ dfm_obj <- sortie_pipeline$dfm_obj
499
+ langue_reference <- sortie_pipeline$langue_reference
500
+ source_dictionnaire <- sortie_pipeline$source_dictionnaire
501
+
502
+ if (anyDuplicated(docnames(dfm_obj)) > 0) {
503
+ dups_dfm <- sum(duplicated(as.character(docnames(dfm_obj))))
504
+ docnames(dfm_obj) <- make.unique(as.character(docnames(dfm_obj)), sep = "_dup")
505
+ ajouter_log(rv, paste0("DFM : docnames dupliqués détectés (", dups_dfm, "). Renommage automatique."))
506
+ }
507
+
508
+ included_segments <- as.character(docnames(dfm_obj))
509
+ included_segments <- included_segments[!is.na(included_segments) & nzchar(included_segments)]
510
+ included_segments <- unique(included_segments)
511
+
512
+ filtered_corpus <- corpus[included_segments]
513
+ if (anyDuplicated(docnames(filtered_corpus)) > 0) {
514
+ dups_corpus <- sum(duplicated(as.character(docnames(filtered_corpus))))
515
+ docnames(filtered_corpus) <- make.unique(as.character(docnames(filtered_corpus)), sep = "_dup")
516
+ ajouter_log(rv, paste0("Corpus filtré : docnames dupliqués détectés (", dups_corpus, "). Renommage automatique."))
517
+ }
518
+
519
+ tok <- tok[included_segments]
520
+ if (anyDuplicated(docnames(tok)) > 0) {
521
+ dups_tok <- sum(duplicated(as.character(docnames(tok))))
522
+ docnames(tok) <- make.unique(as.character(docnames(tok)), sep = "_dup")
523
+ ajouter_log(rv, paste0("Tokens : docnames dupliqués détectés (", dups_tok, "). Renommage automatique."))
524
+ }
525
+
526
+ dfm_obj <- assurer_docvars_dfm_minimal(dfm_obj, filtered_corpus)
527
+
528
+ tmp <- supprimer_docs_vides_dfm(dfm_obj, filtered_corpus, tok, rv)
529
+ dfm_obj <- tmp$dfm
530
+ filtered_corpus <- tmp$corpus
531
+ tok <- tmp$tok
532
+
533
+ ajouter_log(rv, paste0("Après suppression segments vides : ", ndoc(dfm_obj), " docs ; ", nfeat(dfm_obj), " termes."))
534
+ verifier_dfm_avant_rainette(dfm_obj, input)
535
+
536
+ rv$textes_indexation <- vapply(as.list(tok), function(x) paste(x, collapse = " "), FUN.VALUE = character(1))
537
+ names(rv$textes_indexation) <- docnames(dfm_obj)
538
+
539
+ avancer(0.52, "Classification (rainette / rainette2)")
540
+ rv$statut <- "Classification en cours..."
541
+
542
+ modele_chd <- "iramuteq"
543
+
544
+ type_classif <- as.character(input$type_classification)
545
+ if (!type_classif %in% c("simple", "double")) type_classif <- "simple"
546
+
547
+ groupes <- NULL
548
+ res_final <- NULL
549
+
550
+ if (identical(modele_chd, "iramuteq")) {
551
+
552
+ rv$res_type <- "iramuteq"
553
+ ajouter_log(rv, "Mode : classification IRaMuTeQ-like.")
554
+
555
+ k_iramuteq <- suppressWarnings(as.integer(input$k_iramuteq))
556
+ if (is.na(k_iramuteq) || k_iramuteq < 2L) k_iramuteq <- 10L
557
+
558
+ mincl_mode_iramuteq <- as.character(input$iramuteq_mincl_mode)
559
+ if (!mincl_mode_iramuteq %in% c("auto", "manuel")) mincl_mode_iramuteq <- "auto"
560
+
561
+ mincl_iramuteq <- suppressWarnings(as.integer(input$iramuteq_mincl))
562
+ if (is.na(mincl_iramuteq) || mincl_iramuteq < 1L) mincl_iramuteq <- 1L
563
+
564
+ classif_mode_iramuteq <- as.character(input$iramuteq_classif_mode)
565
+ if (!classif_mode_iramuteq %in% c("simple", "double")) classif_mode_iramuteq <- "simple"
566
+
567
+ svd_method_iramuteq <- as.character(input$iramuteq_svd_method)
568
+ if (!svd_method_iramuteq %in% c("irlba", "svdR")) svd_method_iramuteq <- "irlba"
569
+
570
+ ajouter_log(
571
+ rv,
572
+ paste0(
573
+ "Paramètres IRaMuTeQ-like : k=", k_iramuteq,
574
+ " | mincl_mode=", mincl_mode_iramuteq,
575
+ if (identical(mincl_mode_iramuteq, "manuel")) paste0(" | mincl=", mincl_iramuteq) else "",
576
+ " | classif_mode=", classif_mode_iramuteq,
577
+ " | svd_method=", svd_method_iramuteq,
578
+ " | mode_patate=", ifelse(isTRUE(input$iramuteq_mode_patate), "1", "0")
579
+ )
580
+ )
581
+
582
+ res_ira <- lancer_moteur_chd_iramuteq(
583
+ dfm_obj = dfm_obj,
584
+ k = k_iramuteq,
585
+ mincl_mode = mincl_mode_iramuteq,
586
+ mincl = mincl_iramuteq,
587
+ classif_mode = classif_mode_iramuteq,
588
+ svd_method = svd_method_iramuteq,
589
+ mode_patate = isTRUE(input$iramuteq_mode_patate),
590
+ binariser = TRUE
591
+ )
592
+
593
+ groupes <- as.integer(res_ira$classes)
594
+ if (all(is.na(groupes)) || length(unique(groupes[groupes > 0])) < 2) {
595
+ stop("IRaMuTeQ-like n'a pas pu produire au moins 2 classes exploitables.")
596
+ }
597
+
598
+ res_final <- res_ira
599
+ rv$res_chd <- NULL
600
+ rv$dfm_chd <- NULL
601
+ rv$max_n_groups <- length(unique(groupes[groupes > 0]))
602
+ rv$max_n_groups_chd <- rv$max_n_groups
603
+
604
+ } else if (type_classif == "simple") {
605
+
606
+ rv$res_type <- "simple"
607
+ ajouter_log(rv, "Mode : classification simple (rainette).")
608
+
609
+ k_effectif <- calculer_k_effectif(dfm_obj, input$k, input$min_split_members, rv)
610
+
611
+ res <- rainette(
612
+ dfm_obj,
613
+ k = k_effectif,
614
+ min_segment_size = input$min_segment_size,
615
+ min_split_members = input$min_split_members,
616
+ doc_id = "segment_source"
617
+ )
618
+
619
+ if (is.null(res) || is.null(res$group) || length(res$group) == 0) stop("Rainette n'a pas pu calculer de clusters. Diminue les filtrages, augmente segment_size, ou réduis k.")
620
+
621
+ groupes <- res$group
622
+ res_final <- res
623
+ rv$res_chd <- res
624
+ rv$dfm_chd <- dfm_obj
625
+ rv$max_n_groups <- max(res$group, na.rm = TRUE)
626
+ rv$max_n_groups_chd <- rv$max_n_groups
627
+
628
+ } else {
629
+
630
+ rv$res_type <- "double"
631
+ ajouter_log(rv, "Mode : classification double (rainette2).")
632
+
633
+ k_effectif <- calculer_k_effectif(dfm_obj, input$k, input$min_split_members, rv)
634
+
635
+ res1 <- rainette(dfm_obj, k = k_effectif, min_segment_size = input$min_segment_size, min_split_members = input$min_split_members, doc_id = "segment_source")
636
+ if (is.null(res1) || is.null(res1$group) || length(res1$group) == 0) stop("Classification 1 (rainette) impossible.")
637
+
638
+ res2 <- rainette(dfm_obj, k = k_effectif, min_segment_size = input$min_segment_size2, min_split_members = input$min_split_members, doc_id = "segment_source")
639
+ if (is.null(res2) || is.null(res2$group) || length(res2$group) == 0) stop("Classification 2 (rainette) impossible.")
640
+
641
+ res_d <- rainette2(res1, res2, max_k = input$max_k_double)
642
+ groupes <- cutree(res_d, k = k_effectif)
643
+
644
+ res_final <- res_d
645
+ rv$res_chd <- res1
646
+ rv$dfm_chd <- dfm_obj
647
+ rv$max_n_groups <- input$max_k_double
648
+ rv$max_n_groups_chd <- max(res1$group, na.rm = TRUE)
649
+ }
650
+
651
+ docvars(filtered_corpus)$Classes <- groupes
652
+
653
+ classes_calculees <- suppressWarnings(as.integer(docvars(filtered_corpus)$Classes))
654
+ idx_ok <- !is.na(classes_calculees) & classes_calculees > 0
655
+
656
+ nb_non_assignes <- sum(!idx_ok)
657
+ if (nb_non_assignes > 0) {
658
+ ajouter_log(
659
+ rv,
660
+ paste0(
661
+ "Segments non assignés à une classe terminale (Classe 0 / NA) : ",
662
+ nb_non_assignes,
663
+ ". Exclusion des calculs CHD/AFC."
664
+ )
665
+ )
666
+ }
667
+ filtered_corpus_ok <- filtered_corpus[idx_ok]
668
+ dfm_ok <- dfm_obj[idx_ok, ]
669
+ tok_ok <- tok[idx_ok]
670
+
671
+ if (ndoc(dfm_ok) < 2) stop("Après classification, il reste moins de 2 segments classés (hors NA).")
672
+ if (nfeat(dfm_ok) < 2) stop("Après classification, le DFM classé est trop pauvre (moins de 2 termes).")
673
+
674
+ rv$clusters <- sort(unique(docvars(filtered_corpus_ok)$Classes))
675
+ rv$res <- res_final
676
+ rv$dfm <- dfm_ok
677
+ rv$filtered_corpus <- filtered_corpus_ok
678
+ rv$res_stats_df <- NULL
679
+
680
+ avancer(0.58, "NER (si activé)")
681
+ rv$statut <- "NER (si activé)..."
682
+
683
+ if (isTRUE(input$activer_ner)) {
684
+ ajouter_log(rv, "NER ignoré : cette branche IRaMuTeQ-like est strictement sans spaCy.")
685
+ }
686
+
687
+ avancer(0.62, "Exports + stats")
688
+ rv$statut <- "Exports et statistiques..."
689
+
690
+ segments_vec <- as.character(filtered_corpus_ok)
691
+ names(segments_vec) <- docnames(filtered_corpus_ok)
692
+ segments_by_class <- split(segments_vec, docvars(filtered_corpus_ok)$Classes)
693
+
694
+ segments_file <- file.path(rv$export_dir, "segments_par_classe.txt")
695
+ writeLines(unlist(lapply(names(segments_by_class), function(cl) c(paste0("Classe ", cl, ":"), unname(segments_by_class[[cl]]), ""))), segments_file)
696
+
697
+ if (identical(rv$res_type, "iramuteq")) {
698
+ ajouter_log(rv, "Statistiques CHD : calcul IRaMuTeQ-like (contingence classe × terme).")
699
+ res_stats_df <- construire_stats_classes_iramuteq(
700
+ dfm_obj = dfm_ok,
701
+ classes = docvars(filtered_corpus_ok)$Classes,
702
+ max_p = 1
703
+ ) %>%
704
+ mutate(Classe = normaliser_id_classe_local(Classe)) %>%
705
+ arrange(Classe, desc(chi2))
706
+ } else {
707
+ res_stats_list <- rainette_stats(
708
+ dtm = dfm_ok,
709
+ groups = docvars(filtered_corpus_ok)$Classes,
710
+ measure = c("chi2", "lr", "frequency", "docprop"),
711
+ n_terms = 9999,
712
+ # Harmonisation avec le graphe CHD :
713
+ # - pas de chi2 négatifs dans l'onglet Statistiques
714
+ # - pas de coupe préalable sur p-value pour conserver le même
715
+ # vivier de termes entre les vues (la colonne p_value_filter
716
+ # reste disponible pour distinguer les termes significatifs).
717
+ show_negative = FALSE,
718
+ max_p = 1
719
+ )
720
+
721
+ labels_stats <- names(res_stats_list)
722
+ labels_groupes <- as.character(sort(unique(docvars(filtered_corpus_ok)$Classes)))
723
+
724
+ if (is.null(labels_stats) || length(labels_stats) != length(res_stats_list) || any(!nzchar(labels_stats))) {
725
+ labels_stats <- labels_groupes
726
+ }
727
+
728
+ if (length(labels_stats) != length(res_stats_list)) {
729
+ labels_stats <- as.character(seq_along(res_stats_list))
730
+ }
731
+
732
+ tailles_stats <- vapply(res_stats_list, nrow, integer(1))
733
+
734
+ res_stats_df <- bind_rows(res_stats_list) %>%
735
+ mutate(ClusterID = rep(labels_stats, times = tailles_stats)) %>%
736
+ rename(Terme = feature, Classe = ClusterID) %>%
737
+ mutate(
738
+ p_value = p,
739
+ Classe_brut = as.character(Classe),
740
+ Classe = normaliser_id_classe_local(Classe),
741
+ p_value_filter = ifelse(p <= input$max_p, paste0("≤ ", input$max_p), paste0("> ", input$max_p))
742
+ ) %>%
743
+ arrange(Classe, desc(chi2))
744
+ }
745
+
746
+ if (identical(source_dictionnaire, "lexique_fr") &&
747
+ !is.null(rv$lexique_fr_df) &&
748
+ is.data.frame(rv$lexique_fr_df) &&
749
+ nrow(rv$lexique_fr_df) > 0 &&
750
+ "Terme" %in% names(res_stats_df) &&
751
+ exists("construire_type_lexique_fr", mode = "function", inherits = TRUE)) {
752
+ res_stats_df$Type <- construire_type_lexique_fr(res_stats_df$Terme, rv$lexique_fr_df)
753
+ }
754
+
755
+ stats_file <- file.path(rv$export_dir, "stats_par_classe.csv")
756
+ ecrire_csv_6_decimales(res_stats_df, stats_file, row.names = FALSE)
757
+
758
+ rv$segments_file <- segments_file
759
+ rv$stats_file <- stats_file
760
+ rv$res_stats_df <- res_stats_df
761
+
762
+ avancer(0.72, "AFC (classes × termes)")
763
+ rv$statut <- "Calcul AFC classes × termes..."
764
+
765
+ rv$afc_obj <- NULL
766
+ rv$afc_erreur <- NULL
767
+ rv$afc_vars_obj <- NULL
768
+ rv$afc_vars_erreur <- NULL
769
+ rv$afc_dir <- file.path(rv$export_dir, "afc")
770
+ dir.create(rv$afc_dir, showWarnings = FALSE, recursive = TRUE)
771
+
772
+ filtrer_affichage_pvalue <- isTRUE(input$filtrer_affichage_pvalue)
773
+
774
+ termes_signif <- NULL
775
+ if (isTRUE(filtrer_affichage_pvalue)) {
776
+ termes_signif <- unique(subset(res_stats_df, p <= input$max_p)$Terme)
777
+ termes_signif <- termes_signif[!is.na(termes_signif) & nzchar(termes_signif)]
778
+ if (length(termes_signif) < 2) termes_signif <- NULL
779
+ }
780
+
781
+ tryCatch({
782
+ groupes_docs <- docvars(filtered_corpus_ok)$Classes
783
+
784
+ obj <- executer_afc_classes(
785
+ dfm_obj = dfm_ok,
786
+ groupes = groupes_docs,
787
+ termes_cibles = termes_signif,
788
+ max_termes = 400,
789
+ seuil_p = if (isTRUE(filtrer_affichage_pvalue)) input$max_p else 1,
790
+ rv = rv
791
+ )
792
+
793
+ if (!is.null(obj$termes_stats) && !is.null(rv$res_stats_df)) {
794
+ df_m <- obj$termes_stats
795
+ df_m$Classe_num <- suppressWarnings(as.numeric(gsub("^Classe\\s+", "", as.character(df_m$Classe_max))))
796
+ rs <- rv$res_stats_df
797
+
798
+ rs2 <- rs[, intersect(c("Terme", "Classe", "chi2", "p", "frequency", "docprop", "lr"), names(rs)), drop = FALSE]
799
+ rs2$Classe <- as.numeric(rs2$Classe)
800
+
801
+ m <- merge(
802
+ df_m,
803
+ rs2,
804
+ by.x = c("Terme", "Classe_num"),
805
+ by.y = c("Terme", "Classe"),
806
+ all.x = TRUE,
807
+ suffixes = c("_global", "_rainette")
808
+ )
809
+
810
+ if ("chi2" %in% names(m)) {
811
+ df_m$chi2 <- ifelse(is.na(m$chi2), df_m$chi2, m$chi2)
812
+ }
813
+ if ("p" %in% names(m)) {
814
+ df_m$p_value <- ifelse(is.na(m$p), df_m$p_value, m$p)
815
+ }
816
+
817
+ df_m$Classe_num <- NULL
818
+ obj$termes_stats <- df_m
819
+ }
820
+
821
+ obj$termes_stats <- construire_segments_exemples_afc(
822
+ termes_stats = obj$termes_stats,
823
+ dfm_obj = dfm_ok,
824
+ corpus_obj = filtered_corpus_ok
825
+ )
826
+
827
+ rv$afc_obj <- obj
828
+ ajouter_log(rv, "AFC classes × termes : calcul terminé.")
829
+
830
+ }, error = function(e) {
831
+ rv$afc_erreur <- paste0("AFC classes × termes : ", e$message)
832
+ ajouter_log(rv, rv$afc_erreur)
833
+ showNotification(rv$afc_erreur, type = "error", duration = 8)
834
+ })
835
+
836
+ avancer(0.74, "AFC (variables étoilées)")
837
+ rv$statut <- "Calcul AFC variables étoilées..."
838
+
839
+ tryCatch({
840
+ if (!is.null(docvars(filtered_corpus_ok)$Classes)) {
841
+ objv <- executer_afc_variables_etoilees(
842
+ corpus_aligne = filtered_corpus_ok,
843
+ groupes = docvars(filtered_corpus_ok)$Classes,
844
+ max_modalites = 400,
845
+ seuil_p = if (isTRUE(filtrer_affichage_pvalue)) input$max_p else 1,
846
+ rv = rv
847
+ )
848
+ rv$afc_vars_obj <- objv
849
+ ajouter_log(rv, "AFC variables étoilées : calcul terminé.")
850
+ }
851
+ }, error = function(e) {
852
+ rv$afc_vars_erreur <- paste0("AFC variables étoilées : ", e$message)
853
+ ajouter_log(rv, rv$afc_vars_erreur)
854
+ })
855
+
856
+ if (!is.null(rv$afc_obj) && !is.null(rv$afc_obj$ca)) {
857
+
858
+ afc_classes_png <- file.path(rv$afc_dir, "afc_classes.png")
859
+ afc_termes_png <- file.path(rv$afc_dir, "afc_termes.png")
860
+
861
+ activer_repel <- TRUE
862
+ if (!is.null(input$afc_reduire_chevauchement)) activer_repel <- isTRUE(input$afc_reduire_chevauchement)
863
+
864
+ taille_sel <- "frequency"
865
+ if (!is.null(input$afc_taille_mots) && nzchar(as.character(input$afc_taille_mots))) {
866
+ taille_sel <- as.character(input$afc_taille_mots)
867
+ }
868
+ if (!taille_sel %in% c("frequency", "chi2")) taille_sel <- "frequency"
869
+
870
+ top_termes <- 120
871
+ if (!is.null(input$afc_top_termes) && is.finite(input$afc_top_termes)) top_termes <- as.integer(input$afc_top_termes)
872
+
873
+ png(afc_classes_png, width = 1800, height = 1400, res = 180)
874
+ try(tracer_afc_classes_seules(rv$afc_obj, axes = c(1, 2), cex_labels = 1.05), silent = TRUE)
875
+ dev.off()
876
+
877
+ png(afc_termes_png, width = 2000, height = 1600, res = 180)
878
+ try(tracer_afc_classes_termes(rv$afc_obj, axes = c(1, 2), top_termes = top_termes, taille_sel = taille_sel, activer_repel = activer_repel), silent = TRUE)
879
+ dev.off()
880
+
881
+ rv$afc_plot_classes <- afc_classes_png
882
+ rv$afc_plot_termes <- afc_termes_png
883
+
884
+ ecrire_csv_6_decimales(rv$afc_obj$table, file.path(rv$afc_dir, "table_classes_termes.csv"), row.names = TRUE)
885
+ ecrire_csv_6_decimales(rv$afc_obj$rowcoord, file.path(rv$afc_dir, "coords_classes.csv"), row.names = TRUE)
886
+ ecrire_csv_6_decimales(rv$afc_obj$colcoord, file.path(rv$afc_dir, "coords_termes.csv"), row.names = TRUE)
887
+ ecrire_csv_6_decimales(rv$afc_obj$termes_stats, file.path(rv$afc_dir, "stats_termes.csv"), row.names = FALSE)
888
+
889
+ if (!is.null(rv$afc_obj$ca$eig)) {
890
+ ecrire_csv_6_decimales(as.data.frame(rv$afc_obj$ca$eig), file.path(rv$afc_dir, "valeurs_propres.csv"), row.names = TRUE)
891
+ }
892
+
893
+ rv$afc_table_mots <- rv$afc_obj$termes_stats
894
+ }
895
+
896
+ if (!is.null(rv$afc_vars_obj) && !is.null(rv$afc_vars_obj$ca)) {
897
+
898
+ afc_vars_png <- file.path(rv$afc_dir, "afc_variables_etoilees.png")
899
+
900
+ activer_repel2 <- TRUE
901
+ if (!is.null(input$afc_reduire_chevauchement)) activer_repel2 <- isTRUE(input$afc_reduire_chevauchement)
902
+
903
+ top_mod <- 120
904
+ if (!is.null(input$afc_top_modalites) && is.finite(input$afc_top_modalites)) top_mod <- as.integer(input$afc_top_modalites)
905
+
906
+ png(afc_vars_png, width = 2000, height = 1600, res = 180)
907
+ try(tracer_afc_variables_etoilees(rv$afc_vars_obj, axes = c(1, 2), top_modalites = top_mod, activer_repel = activer_repel2), silent = TRUE)
908
+ dev.off()
909
+
910
+ rv$afc_plot_vars <- afc_vars_png
911
+
912
+ ecrire_csv_6_decimales(rv$afc_vars_obj$table, file.path(rv$afc_dir, "table_classes_variables.csv"), row.names = TRUE)
913
+ ecrire_csv_6_decimales(rv$afc_vars_obj$rowcoord, file.path(rv$afc_dir, "coords_classes_vars.csv"), row.names = TRUE)
914
+ ecrire_csv_6_decimales(rv$afc_vars_obj$colcoord, file.path(rv$afc_dir, "coords_modalites.csv"), row.names = TRUE)
915
+ ecrire_csv_6_decimales(rv$afc_vars_obj$modalites_stats, file.path(rv$afc_dir, "stats_modalites.csv"), row.names = FALSE)
916
+
917
+ if (!is.null(rv$afc_vars_obj$ca$eig)) {
918
+ ecrire_csv_6_decimales(as.data.frame(rv$afc_vars_obj$ca$eig), file.path(rv$afc_dir, "valeurs_propres_vars.csv"), row.names = TRUE)
919
+ }
920
+
921
+ rv$afc_table_vars <- rv$afc_vars_obj$modalites_stats
922
+ }
923
+
924
+ avancer(0.76, "Concordancier HTML")
925
+ rv$statut <- "Concordancier..."
926
+
927
+ html_file <- file.path(rv$export_dir, "segments_par_classe.html")
928
+ textes_index_ok <- rv$textes_indexation[docnames(dfm_ok)]
929
+ names(textes_index_ok) <- docnames(dfm_ok)
930
+
931
+ wordcloud_dir <- file.path(rv$export_dir, "wordclouds")
932
+ dir.create(wordcloud_dir, showWarnings = FALSE, recursive = TRUE)
933
+ cooc_dir <- file.path(rv$export_dir, "cooccurrences")
934
+ dir.create(cooc_dir, showWarnings = FALSE, recursive = TRUE)
935
+
936
+ classes_uniques <- sort(unique(as.integer(docvars(filtered_corpus_ok)$Classes)))
937
+ classes_uniques <- classes_uniques[is.finite(classes_uniques)]
938
+
939
+ if (length(classes_uniques) > 0) {
940
+ classes_choices <- as.character(classes_uniques)
941
+ updateSelectInput(
942
+ session,
943
+ "classe_viz_iramuteq",
944
+ choices = classes_choices,
945
+ selected = classes_choices[[1]]
946
+ )
947
+ }
948
+
949
+ if (!identical(rv$res_type, "iramuteq")) {
950
+ for (cl in classes_uniques) {
951
+ top_n_demande <- suppressWarnings(as.integer(input$top_n))
952
+ if (!is.finite(top_n_demande) || is.na(top_n_demande)) top_n_demande <- 20L
953
+ top_n_demande <- max(5L, top_n_demande)
954
+
955
+ if (isTRUE(input$filtrer_affichage_pvalue)) {
956
+ df_stats_cl <- subset(res_stats_df, Classe == cl & p <= input$max_p)
957
+ } else {
958
+ df_stats_cl <- subset(res_stats_df, Classe == cl)
959
+ }
960
+ if (nrow(df_stats_cl) > 0) {
961
+ df_stats_cl <- df_stats_cl[order(-df_stats_cl$chi2), , drop = FALSE]
962
+ df_stats_cl <- head(df_stats_cl, top_n_demande)
963
+
964
+ wc_png <- file.path(wordcloud_dir, paste0("cluster_", cl, "_wordcloud.png"))
965
+ try({
966
+ png(wc_png, width = 800, height = 600)
967
+ suppressWarnings(wordcloud(
968
+ words = df_stats_cl$Terme,
969
+ freq = df_stats_cl$chi2,
970
+ scale = c(10, 0.5),
971
+ min.freq = 0,
972
+ max.words = nrow(df_stats_cl),
973
+ colors = brewer.pal(8, "Dark2")
974
+ ))
975
+ dev.off()
976
+ }, silent = TRUE)
977
+ }
978
+
979
+ }
980
+
981
+ generer_cooccurrences_par_classe(
982
+ tok_ok = tok_ok,
983
+ filtered_corpus_ok = filtered_corpus_ok,
984
+ classes_uniques = classes_uniques,
985
+ cooc_dir = cooc_dir,
986
+ top_n = input$top_n,
987
+ top_feat = input$top_feat,
988
+ window_cooc = input$window_cooc
989
+ )
990
+ } else {
991
+ generer_wordclouds_iramuteq(
992
+ res_stats_df = res_stats_df,
993
+ classes_uniques = classes_uniques,
994
+ wordcloud_dir = wordcloud_dir,
995
+ top_n = input$top_n,
996
+ filtrer_pvalue = isTRUE(input$filtrer_affichage_pvalue),
997
+ max_p = input$max_p
998
+ )
999
+ ajouter_log(rv, "Mode IRaMuTeQ-like : nuages de mots générés via wordcloud_iramuteq.R (cooccurrences Explore rainette désactivées).")
1000
+ }
1001
+
1002
+ explor_assets <- NULL
1003
+ ok_chd_png <- FALSE
1004
+ if (!identical(rv$res_type, "iramuteq")) {
1005
+ ok_chd_png <- generer_chd_explor_si_absente(rv)
1006
+ }
1007
+
1008
+ chd_png_rel <- NULL
1009
+ if (isTRUE(ok_chd_png) && file.exists(file.path(rv$export_dir, "explor", "chd.png"))) {
1010
+ chd_png_rel <- file.path("explor", "chd.png")
1011
+ }
1012
+ chd_html_rel <- generer_chd_html_explor(rv, chd_png_rel)
1013
+
1014
+ wc_files <- list.files(wordcloud_dir, pattern = "\\.png$", full.names = FALSE)
1015
+ if (length(wc_files) > 0) {
1016
+ wc_classes <- gsub("^cluster_([0-9]+)_wordcloud\\.png$", "\\1", wc_files)
1017
+ wordclouds_df <- data.frame(
1018
+ classe = wc_classes,
1019
+ src = file.path("wordclouds", wc_files),
1020
+ stringsAsFactors = FALSE
1021
+ )
1022
+ wordclouds_df <- wordclouds_df[order(suppressWarnings(as.integer(wordclouds_df$classe))), , drop = FALSE]
1023
+ } else {
1024
+ wordclouds_df <- data.frame(classe = character(0), src = character(0), stringsAsFactors = FALSE)
1025
+ }
1026
+
1027
+ coocs_df <- construire_table_cooccurrences(cooc_dir)
1028
+
1029
+ explor_assets <- list(
1030
+ chd = chd_png_rel,
1031
+ chd_html = chd_html_rel,
1032
+ wordclouds = wordclouds_df,
1033
+ coocs = coocs_df
1034
+ )
1035
+ rv$explor_assets <- explor_assets
1036
+
1037
+ args_concordancier <- list(
1038
+ chemin_sortie = html_file,
1039
+ segments_by_class = segments_by_class,
1040
+ res_stats_df = res_stats_df,
1041
+ max_p = if (isTRUE(input$filtrer_affichage_pvalue)) input$max_p else 1,
1042
+ filtrer_pvalue = isTRUE(input$filtrer_affichage_pvalue),
1043
+ textes_indexation = textes_index_ok,
1044
+ spacy_tokens_df = rv$spacy_tokens_df,
1045
+ lexique_fr_df = rv$lexique_fr_df,
1046
+ source_dictionnaire = source_dictionnaire,
1047
+ avancer = avancer,
1048
+ rv = rv
1049
+ )
1050
+
1051
+ # Priorité explicite au concordancier IRaMuTeQ-like lorsque le mode
1052
+ # IRaMuTeQ est sélectionné dans l'UI (même si rv$res_type est désynchronisé).
1053
+ mode_iramuteq_actif <- identical(as.character(input$modele_chd), "iramuteq") ||
1054
+ identical(rv$res_type, "iramuteq")
1055
+
1056
+ fonction_concordancier <- if (isTRUE(mode_iramuteq_actif)) {
1057
+ generer_concordancier_iramuteq_html
1058
+ } else if (identical(source_dictionnaire, "lexique_fr")) {
1059
+ generer_concordancier_lexique_html
1060
+ } else {
1061
+ generer_concordancier_spacy_html
1062
+ }
1063
+
1064
+ html_genere <- do.call(fonction_concordancier, args_concordancier)
1065
+
1066
+ candidats_html <- unique(c(
1067
+ html_genere,
1068
+ html_file,
1069
+ file.path(rv$export_dir, "concordancier.html")
1070
+ ))
1071
+ candidats_html <- candidats_html[is.character(candidats_html) & !is.na(candidats_html) & nzchar(candidats_html)]
1072
+ html_existants <- candidats_html[file.exists(candidats_html)]
1073
+
1074
+ if (length(html_existants) == 0) {
1075
+ html_fallback <- file.path(rv$export_dir, "concordancier.html")
1076
+ args_concordancier$chemin_sortie <- html_fallback
1077
+ ajouter_log(rv, "Concordancier HTML introuvable après la première génération. Nouvelle tentative vers exports/concordancier.html.")
1078
+ html_retry <- tryCatch(
1079
+ do.call(fonction_concordancier, args_concordancier),
1080
+ error = function(e) {
1081
+ ajouter_log(rv, paste0("Concordancier HTML : échec de la relance - ", e$message))
1082
+ NA_character_
1083
+ }
1084
+ )
1085
+
1086
+ candidats_retry <- unique(c(html_retry, html_fallback, html_genere, html_file))
1087
+ candidats_retry <- candidats_retry[is.character(candidats_retry) & !is.na(candidats_retry) & nzchar(candidats_retry)]
1088
+ html_existants <- candidats_retry[file.exists(candidats_retry)]
1089
+ }
1090
+
1091
+ if (length(html_existants) > 0) {
1092
+ rv$html_file <- html_existants[[1]]
1093
+ ajouter_log(rv, paste0("Concordancier HTML validé : ", rv$html_file))
1094
+ } else {
1095
+ rv$html_file <- html_file
1096
+ ajouter_log(rv, "Concordancier HTML introuvable après relance. Vérifier les logs de génération du concordancier.")
1097
+ }
1098
+
1099
+ avancer(0.96, "ZIP")
1100
+ rv$statut <- "Création ZIP..."
1101
+ rv$zip_file <- file.path(rv$base_dir, "exports_rainette.zip")
1102
+ if (file.exists(rv$zip_file)) unlink(rv$zip_file)
1103
+
1104
+ ancien_wd <- getwd()
1105
+ setwd(rv$base_dir)
1106
+ utils::zip(zipfile = rv$zip_file, files = "exports")
1107
+ setwd(ancien_wd)
1108
+
1109
+ exports_prefix <- as.character(rv$exports_prefix)
1110
+ if (length(exports_prefix) > 1) exports_prefix <- exports_prefix[[1]]
1111
+ if (!length(exports_prefix) || is.na(exports_prefix) || !nzchar(exports_prefix)) {
1112
+ # Fallback robuste: évite une erreur "missing value where TRUE/FALSE needed"
1113
+ # lorsque le token de session est indisponible.
1114
+ exports_prefix <- paste0("exports_", format(Sys.time(), "%Y%m%d%H%M%S"))
1115
+ rv$exports_prefix <- exports_prefix
1116
+ }
1117
+
1118
+ if (!(exports_prefix %in% names(shiny::resourcePaths()))) {
1119
+ shiny::addResourcePath(exports_prefix, rv$export_dir)
1120
+ }
1121
+
1122
+ rv$statut <- "Analyse terminée."
1123
+ rv$progression <- 100
1124
+ ajouter_log(rv, "Analyse terminée.")
1125
+ showNotification("Analyse terminée.", type = "message", duration = 5)
1126
+
1127
+ }, error = function(e) {
1128
+ rv$statut <- paste0("Erreur : ", e$message)
1129
+ ajouter_log(rv, paste0("ERREUR : ", e$message))
1130
+ showNotification(e$message, type = "error", duration = 8)
1131
+ })
1132
+ })
1133
+ }
iramuteq-like/stats_chd.R ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: stats_chd.R centralise la table des statistiques CHD pour le mode IRaMuTeQ-like.
2
+
3
+ formatter_6_decimales_chd <- function(x) {
4
+ ifelse(is.na(x), NA_character_, formatC(as.numeric(x), format = "f", digits = 6))
5
+ }
6
+
7
+ .inferer_type_terme_iramuteq <- function(termes) {
8
+ x <- tolower(as.character(termes))
9
+ out <- rep("", length(x))
10
+
11
+ extraire_tag <- function(pattern, value) {
12
+ idx <- grepl(pattern, x)
13
+ out[idx] <<- value
14
+ }
15
+
16
+ extraire_tag("(^|[_/])nom$", "nom")
17
+ extraire_tag("(^|[_/])adj$", "adj")
18
+ extraire_tag("(^|[_/])ver$", "ver")
19
+ extraire_tag("(^|[_/])adv$", "adv")
20
+ extraire_tag("(^|[_/])nr$", "nr")
21
+
22
+ out
23
+ }
24
+
25
+ .normaliser_type_terme_iramuteq <- function(type_vals, termes) {
26
+ types <- tolower(trimws(as.character(type_vals)))
27
+ types[is.na(types)] <- ""
28
+ types[types %in% c("", "na", "nan", "null")] <- ""
29
+
30
+ types_inf <- .inferer_type_terme_iramuteq(termes)
31
+ idx_manquant <- !nzchar(types)
32
+ types[idx_manquant] <- types_inf[idx_manquant]
33
+ types
34
+ }
35
+
36
+ extraire_stats_chd_classe <- function(res_stats_df,
37
+ classe,
38
+ n_max = 50,
39
+ show_negative = FALSE,
40
+ max_p = 1,
41
+ seuil_p_significativite = 0.05,
42
+ style = c("iramuteq_clone", "legacy")) {
43
+ style <- match.arg(style)
44
+
45
+ if (is.null(res_stats_df) || nrow(res_stats_df) == 0) {
46
+ return(data.frame(Message = "Statistiques indisponibles.", stringsAsFactors = FALSE))
47
+ }
48
+
49
+ cl <- suppressWarnings(as.numeric(classe))
50
+ df <- res_stats_df
51
+ if (is.finite(cl) && !is.na(cl) && "Classe" %in% names(df)) {
52
+ df <- df[suppressWarnings(as.numeric(df$Classe)) == cl, , drop = FALSE]
53
+ }
54
+
55
+ colonnes_possibles <- intersect(
56
+ c("Terme", "chi2", "lr", "frequency", "docprop", "eff_st", "eff_total", "pourcentage", "p", "p_value", "p_value_filter", "Type", "type", "pos", "POS"),
57
+ names(df)
58
+ )
59
+ df <- df[, colonnes_possibles, drop = FALSE]
60
+
61
+ if ("p" %in% names(df) && is.finite(max_p) && !is.na(max_p) && max_p < 1) {
62
+ df <- df[suppressWarnings(as.numeric(df$p)) <= max_p, , drop = FALSE]
63
+ }
64
+
65
+ if ("chi2" %in% names(df)) {
66
+ chi2_vals <- suppressWarnings(as.numeric(df$chi2))
67
+ if (!isTRUE(show_negative)) {
68
+ df <- df[is.finite(chi2_vals) & chi2_vals > 0, , drop = FALSE]
69
+ chi2_vals <- suppressWarnings(as.numeric(df$chi2))
70
+ }
71
+
72
+ frequency_vals <- rep(-Inf, nrow(df))
73
+ if ("frequency" %in% names(df)) {
74
+ frequency_vals <- suppressWarnings(as.numeric(df$frequency))
75
+ frequency_vals[!is.finite(frequency_vals)] <- -Inf
76
+ }
77
+
78
+ chi2_sort <- chi2_vals
79
+ chi2_sort[!is.finite(chi2_sort)] <- -Inf
80
+ df <- df[order(-chi2_sort, -frequency_vals), , drop = FALSE]
81
+ }
82
+ df <- utils::head(df, n_max)
83
+
84
+ if (identical(style, "iramuteq_clone")) {
85
+ eff_st <- if ("eff_st" %in% names(df)) suppressWarnings(as.numeric(df$eff_st)) else round(suppressWarnings(as.numeric(df$docprop)) * suppressWarnings(as.numeric(df$eff_total)))
86
+ eff_total <- if ("eff_total" %in% names(df)) suppressWarnings(as.numeric(df$eff_total)) else suppressWarnings(as.numeric(df$frequency))
87
+ pourcentage <- if ("pourcentage" %in% names(df)) suppressWarnings(as.numeric(df$pourcentage)) else ifelse(eff_total > 0, 100 * eff_st / eff_total, NA_real_)
88
+ chi2_vals <- if ("chi2" %in% names(df)) suppressWarnings(as.numeric(df$chi2)) else NA_real_
89
+ p_vals <- if ("p" %in% names(df)) suppressWarnings(as.numeric(df$p)) else if ("p_value" %in% names(df)) suppressWarnings(as.numeric(df$p_value)) else NA_real_
90
+ formes <- if ("Terme" %in% names(df)) as.character(df$Terme) else rep("", nrow(df))
91
+ type_source <- if ("Type" %in% names(df)) df$Type else if ("type" %in% names(df)) df$type else if ("pos" %in% names(df)) df$pos else if ("POS" %in% names(df)) df$POS else rep("", nrow(df))
92
+ types <- .normaliser_type_terme_iramuteq(type_source, formes)
93
+
94
+ out <- data.frame(
95
+ num = seq_len(nrow(df)) - 1L,
96
+ forme = formes,
97
+ `eff. s.t.` = as.integer(round(eff_st)),
98
+ `eff. total` = as.integer(round(eff_total)),
99
+ pourcentage = ifelse(is.na(pourcentage), NA_character_, formatC(pourcentage, format = "f", digits = 2)),
100
+ chi2 = ifelse(is.na(chi2_vals), NA_character_, formatC(chi2_vals, format = "f", digits = 3)),
101
+ `p.value` = ifelse(is.na(p_vals), NA_character_, formatC(p_vals, format = "f", digits = 6)),
102
+ `p.value < 0.01` = ifelse(!is.na(p_vals) & p_vals < 0.01, "Oui", ""),
103
+ Type = types,
104
+ check.names = FALSE,
105
+ stringsAsFactors = FALSE
106
+ )
107
+
108
+ seuil_sig <- suppressWarnings(as.numeric(seuil_p_significativite))
109
+ if (is.finite(seuil_sig) && !is.na(seuil_sig) && nrow(out) > 0) {
110
+ idx_non_signif <- !is.na(p_vals) & p_vals > seuil_sig
111
+ if (any(idx_non_signif)) {
112
+ colonnes_colorables <- names(out)
113
+ out[idx_non_signif, colonnes_colorables] <- lapply(out[idx_non_signif, colonnes_colorables, drop = FALSE], function(col_vals) {
114
+ ifelse(
115
+ is.na(col_vals),
116
+ NA_character_,
117
+ sprintf("<span style='color:#842029;'>%s</span>", as.character(col_vals))
118
+ )
119
+ })
120
+ }
121
+ }
122
+
123
+ return(out)
124
+ }
125
+
126
+ colonnes_num <- intersect(c("chi2", "lr", "docprop", "p", "p_value"), names(df))
127
+ for (col in colonnes_num) {
128
+ df[[col]] <- formatter_6_decimales_chd(df[[col]])
129
+ }
130
+
131
+ if ("frequency" %in% names(df)) {
132
+ df$frequency <- ifelse(is.na(df$frequency), NA_character_, formatC(as.numeric(df$frequency), format = "f", digits = 6))
133
+ }
134
+
135
+ df
136
+ }
iramuteq-like/textprepa_iramuteq.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Préparation de corpus IRaMuTeQ-like (branche lexique_fr) pour audit de reproductibilité.
4
+
5
+ Entrée : TSV (doc_id, text)
6
+ Sortie : TSV (doc_id, text_prepared)
7
+ Optionnel: TSV de tokens (doc_id, token)
8
+
9
+ Objectif:
10
+ - Reproduire au plus près le pré-nettoyage de `nettoyage.R`
11
+ - Offrir un point d'audit unique pour comparer les formes avec IRaMuTeQ desktop
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import csv
18
+ import re
19
+ from typing import Iterable, List, Tuple
20
+
21
+ ALLOWED_CHARS = "a-zA-Z0-9àÀâÂäÄáÁåÅãéÉèÈêÊëËìÌîÎïÏíÍóÓòÒôÔöÖõÕøØùÙûÛüÜúÚçÇßœŒ’ñÑ\\.:,;!\\?'"
22
+ RE_REMOVE_DISALLOWED = re.compile(rf"[^{ALLOWED_CHARS}]")
23
+ RE_MULTI_SPACES = re.compile(r"\s+")
24
+ RE_NUMBERS = re.compile(r"[0-9]+")
25
+ # Équivalent pratique de (?i)\b(?:[cdjlmnst]|qu)['’`´ʼʹ](?=\p{L})
26
+ RE_FR_ELISIONS = re.compile(r"(?i)\b(?:[cdjlmnst]|qu)['’`´ʼʹ](?=[A-Za-zÀ-ÖØ-öø-ÿŒœ])")
27
+ # Token "mot" avec apostrophe interne possible
28
+ RE_WORD = re.compile(r"[A-Za-zÀ-ÖØ-öø-ÿŒœ0-9]+(?:['’][A-Za-zÀ-ÖØ-öø-ÿŒœ0-9]+)*")
29
+
30
+
31
+ def read_tsv(path: str) -> Tuple[List[str], List[str]]:
32
+ ids: List[str] = []
33
+ texts: List[str] = []
34
+ with open(path, "r", encoding="utf-8", newline="") as f:
35
+ reader = csv.DictReader(f, delimiter="\t")
36
+ if reader.fieldnames is None or "doc_id" not in reader.fieldnames or "text" not in reader.fieldnames:
37
+ raise ValueError("TSV invalide: colonnes attendues 'doc_id' et 'text'.")
38
+ for row in reader:
39
+ ids.append((row.get("doc_id") or "").strip())
40
+ texts.append(row.get("text") or "")
41
+ return ids, texts
42
+
43
+
44
+ def write_tsv(path: str, ids: Iterable[str], texts: Iterable[str]) -> None:
45
+ with open(path, "w", encoding="utf-8", newline="") as f:
46
+ writer = csv.DictWriter(f, fieldnames=["doc_id", "text"], delimiter="\t")
47
+ writer.writeheader()
48
+ for did, txt in zip(ids, texts):
49
+ writer.writerow({"doc_id": did, "text": txt})
50
+
51
+
52
+ def write_tokens(path: str, rows: List[Tuple[str, str]]) -> None:
53
+ with open(path, "w", encoding="utf-8", newline="") as f:
54
+ writer = csv.DictWriter(f, fieldnames=["doc_id", "token"], delimiter="\t")
55
+ writer.writeheader()
56
+ for did, tok in rows:
57
+ writer.writerow({"doc_id": did, "token": tok})
58
+
59
+
60
+ def prepare_text(
61
+ text: str,
62
+ nettoyer_caracteres: bool,
63
+ lower: bool,
64
+ remove_numbers: bool,
65
+ strip_fr_elisions: bool,
66
+ ) -> str:
67
+ out = text.replace("\u00A0", " ")
68
+
69
+ if remove_numbers:
70
+ out = RE_NUMBERS.sub(" ", out)
71
+
72
+ if strip_fr_elisions:
73
+ out = RE_FR_ELISIONS.sub("", out)
74
+
75
+ if nettoyer_caracteres:
76
+ out = RE_REMOVE_DISALLOWED.sub(" ", out)
77
+
78
+ out = RE_MULTI_SPACES.sub(" ", out).strip()
79
+
80
+ if lower:
81
+ out = out.lower()
82
+
83
+ return out
84
+
85
+
86
+ def tokenize(prepared: str, remove_numbers: bool, lower_tokens: bool) -> List[str]:
87
+ tokens = RE_WORD.findall(prepared)
88
+ if remove_numbers:
89
+ tokens = [t for t in tokens if not t.isdigit()]
90
+ if lower_tokens:
91
+ tokens = [t.lower() for t in tokens]
92
+ return tokens
93
+
94
+
95
+ def main() -> int:
96
+ p = argparse.ArgumentParser()
97
+ p.add_argument("--input", required=True, help="TSV entrée (doc_id, text)")
98
+ p.add_argument("--output", required=True, help="TSV sortie (doc_id, text préparé)")
99
+ p.add_argument("--nettoyage_caracteres", default="0", help="1 pour activer le nettoyage des caractères")
100
+ p.add_argument("--forcer_minuscules_avant", default="0", help="1 pour forcer les minuscules")
101
+ p.add_argument("--supprimer_chiffres", default="0", help="1 pour supprimer les chiffres")
102
+ p.add_argument("--supprimer_apostrophes", default="0", help="1 pour retirer les élisions FR (c', d', l', qu', ...)")
103
+ p.add_argument("--output_tokens", default="", help="TSV optionnel de tokens (doc_id, token)")
104
+ args = p.parse_args()
105
+
106
+ nettoyer_caracteres = str(args.nettoyage_caracteres).strip() == "1"
107
+ lower = str(args.forcer_minuscules_avant).strip() == "1"
108
+ remove_numbers = str(args.supprimer_chiffres).strip() == "1"
109
+ strip_fr_elisions = str(args.supprimer_apostrophes).strip() == "1"
110
+
111
+ ids, texts = read_tsv(args.input)
112
+
113
+ prepared = [
114
+ prepare_text(
115
+ t,
116
+ nettoyer_caracteres=nettoyer_caracteres,
117
+ lower=lower,
118
+ remove_numbers=remove_numbers,
119
+ strip_fr_elisions=strip_fr_elisions,
120
+ )
121
+ for t in texts
122
+ ]
123
+
124
+ write_tsv(args.output, ids, prepared)
125
+
126
+ if args.output_tokens:
127
+ rows: List[Tuple[str, str]] = []
128
+ for did, txt in zip(ids, prepared):
129
+ for tok in tokenize(txt, remove_numbers=remove_numbers, lower_tokens=True):
130
+ rows.append((did, tok))
131
+ write_tokens(args.output_tokens, rows)
132
+
133
+ return 0
134
+
135
+
136
+ if __name__ == "__main__":
137
+ raise SystemExit(main())
iramuteq-like/ui_options_iramuteq.R ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: iramuteq-like/ui_options_iramuteq.R définit les options UI
2
+ # spécifiques au mode IRaMuTeQ-like.
3
+
4
+ library(shiny)
5
+
6
+ ui_options_iramuteq <- function() {
7
+ tagList(
8
+ tags$div(class = "sidebar-section-title", "Paramètres CHD (IRaMuTeQ-like)"),
9
+ numericInput("k_iramuteq", "Nombre de classes terminales de la phase 1", value = 10, min = 2, step = 1),
10
+ radioButtons(
11
+ "iramuteq_mincl_mode",
12
+ "Nombre minimum d'UCE par classe terminale (mincl)",
13
+ choices = c("Automatique" = "auto", "Manuel" = "manuel"),
14
+ selected = "auto",
15
+ inline = FALSE
16
+ ),
17
+ conditionalPanel(
18
+ condition = "input.iramuteq_mincl_mode == 'manuel'",
19
+ numericInput("iramuteq_mincl", "mincl (manuel)", value = 5, min = 1, step = 1)
20
+ ),
21
+ radioButtons(
22
+ "iramuteq_classif_mode",
23
+ "Type de classification terminale",
24
+ choices = c("Simple" = "simple", "Double" = "double"),
25
+ selected = "simple",
26
+ inline = FALSE
27
+ ),
28
+ selectInput(
29
+ "iramuteq_svd_method",
30
+ "Méthode SVD",
31
+ choices = c("irlba" = "irlba", "svdR" = "svdR"),
32
+ selected = "irlba"
33
+ ),
34
+ checkboxInput("iramuteq_mode_patate", "Mode patate (moins précis, plus rapide)", value = FALSE)
35
+ )
36
+ }
iramuteq-like/wordcloud_iramuteq.R ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: générer des nuages de mots dédiés au mode IRaMuTeQ-like.
2
+
3
+ generer_wordclouds_iramuteq <- function(res_stats_df,
4
+ classes_uniques,
5
+ wordcloud_dir,
6
+ top_n = 20L,
7
+ filtrer_pvalue = FALSE,
8
+ max_p = 1) {
9
+ if (is.null(res_stats_df) || !is.data.frame(res_stats_df) || nrow(res_stats_df) == 0) {
10
+ return(invisible(NULL))
11
+ }
12
+
13
+ dir.create(wordcloud_dir, showWarnings = FALSE, recursive = TRUE)
14
+
15
+ top_n <- suppressWarnings(as.integer(top_n))
16
+ if (!is.finite(top_n) || is.na(top_n)) top_n <- 20L
17
+ top_n <- max(5L, top_n)
18
+
19
+ classe_col <- if ("Classe" %in% names(res_stats_df)) "Classe" else NULL
20
+ terme_col <- if ("Terme" %in% names(res_stats_df)) "Terme" else if ("forme" %in% names(res_stats_df)) "forme" else NULL
21
+ chi2_col <- if ("chi2" %in% names(res_stats_df)) "chi2" else NULL
22
+ p_col <- if ("p" %in% names(res_stats_df)) "p" else if ("p_value" %in% names(res_stats_df)) "p_value" else NULL
23
+
24
+ if (is.null(classe_col) || is.null(terme_col) || is.null(chi2_col)) {
25
+ return(invisible(NULL))
26
+ }
27
+
28
+ classes_num <- suppressWarnings(as.numeric(res_stats_df[[classe_col]]))
29
+
30
+ for (cl in classes_uniques) {
31
+ cl_num <- suppressWarnings(as.numeric(cl))
32
+ if (!is.finite(cl_num) || is.na(cl_num)) next
33
+
34
+ df_stats_cl <- res_stats_df[is.finite(classes_num) & !is.na(classes_num) & classes_num == cl_num, , drop = FALSE]
35
+ if (nrow(df_stats_cl) == 0) next
36
+
37
+ if (isTRUE(filtrer_pvalue) && !is.null(p_col) && is.finite(max_p) && !is.na(max_p)) {
38
+ p_vals <- suppressWarnings(as.numeric(df_stats_cl[[p_col]]))
39
+ df_stats_cl <- df_stats_cl[is.finite(p_vals) & !is.na(p_vals) & p_vals <= max_p, , drop = FALSE]
40
+ }
41
+ if (nrow(df_stats_cl) == 0) next
42
+
43
+ chi2_vals <- suppressWarnings(as.numeric(df_stats_cl[[chi2_col]]))
44
+ df_stats_cl <- df_stats_cl[is.finite(chi2_vals) & !is.na(chi2_vals), , drop = FALSE]
45
+ if (nrow(df_stats_cl) == 0) next
46
+
47
+ chi2_vals <- suppressWarnings(as.numeric(df_stats_cl[[chi2_col]]))
48
+ df_stats_cl <- df_stats_cl[order(-chi2_vals), , drop = FALSE]
49
+ df_stats_cl <- head(df_stats_cl, top_n)
50
+
51
+ wc_png <- file.path(wordcloud_dir, paste0("cluster_", cl, "_wordcloud.png"))
52
+ try({
53
+ png(wc_png, width = 800, height = 600)
54
+ chi2_vals <- suppressWarnings(as.numeric(df_stats_cl[[chi2_col]]))
55
+ suppressWarnings(wordcloud::wordcloud(
56
+ words = as.character(df_stats_cl[[terme_col]]),
57
+ freq = pmax(chi2_vals, 0),
58
+ scale = c(8, 0.8),
59
+ min.freq = 0,
60
+ random.order = FALSE,
61
+ max.words = nrow(df_stats_cl),
62
+ colors = RColorBrewer::brewer.pal(8, "Dark2")
63
+ ))
64
+ dev.off()
65
+ }, silent = TRUE)
66
+ }
67
+
68
+ invisible(NULL)
69
+ }
penguins.csv ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Species,Island,Bill Length (mm),Bill Depth (mm),Flipper Length (mm),Body Mass (g),Sex,Year
2
+ Adelie,Torgersen,39.1,18.7,181,3750,male,2007
3
+ Adelie,Torgersen,39.5,17.4,186,3800,female,2007
4
+ Adelie,Torgersen,40.3,18,195,3250,female,2007
5
+ Adelie,Torgersen,NA,NA,NA,NA,NA,2007
6
+ Adelie,Torgersen,36.7,19.3,193,3450,female,2007
7
+ Adelie,Torgersen,39.3,20.6,190,3650,male,2007
8
+ Adelie,Torgersen,38.9,17.8,181,3625,female,2007
9
+ Adelie,Torgersen,39.2,19.6,195,4675,male,2007
10
+ Adelie,Torgersen,34.1,18.1,193,3475,NA,2007
11
+ Adelie,Torgersen,42,20.2,190,4250,NA,2007
12
+ Adelie,Torgersen,37.8,17.1,186,3300,NA,2007
13
+ Adelie,Torgersen,37.8,17.3,180,3700,NA,2007
14
+ Adelie,Torgersen,41.1,17.6,182,3200,female,2007
15
+ Adelie,Torgersen,38.6,21.2,191,3800,male,2007
16
+ Adelie,Torgersen,34.6,21.1,198,4400,male,2007
17
+ Adelie,Torgersen,36.6,17.8,185,3700,female,2007
18
+ Adelie,Torgersen,38.7,19,195,3450,female,2007
19
+ Adelie,Torgersen,42.5,20.7,197,4500,male,2007
20
+ Adelie,Torgersen,34.4,18.4,184,3325,female,2007
21
+ Adelie,Torgersen,46,21.5,194,4200,male,2007
22
+ Adelie,Biscoe,37.8,18.3,174,3400,female,2007
23
+ Adelie,Biscoe,37.7,18.7,180,3600,male,2007
24
+ Adelie,Biscoe,35.9,19.2,189,3800,female,2007
25
+ Adelie,Biscoe,38.2,18.1,185,3950,male,2007
26
+ Adelie,Biscoe,38.8,17.2,180,3800,male,2007
27
+ Adelie,Biscoe,35.3,18.9,187,3800,female,2007
28
+ Adelie,Biscoe,40.6,18.6,183,3550,male,2007
29
+ Adelie,Biscoe,40.5,17.9,187,3200,female,2007
30
+ Adelie,Biscoe,37.9,18.6,172,3150,female,2007
31
+ Adelie,Biscoe,40.5,18.9,180,3950,male,2007
32
+ Adelie,Dream,39.5,16.7,178,3250,female,2007
33
+ Adelie,Dream,37.2,18.1,178,3900,male,2007
34
+ Adelie,Dream,39.5,17.8,188,3300,female,2007
35
+ Adelie,Dream,40.9,18.9,184,3900,male,2007
36
+ Adelie,Dream,36.4,17,195,3325,female,2007
37
+ Adelie,Dream,39.2,21.1,196,4150,male,2007
38
+ Adelie,Dream,38.8,20,190,3950,male,2007
39
+ Adelie,Dream,42.2,18.5,180,3550,female,2007
40
+ Adelie,Dream,37.6,19.3,181,3300,female,2007
41
+ Adelie,Dream,39.8,19.1,184,4650,male,2007
42
+ Adelie,Dream,36.5,18,182,3150,female,2007
43
+ Adelie,Dream,40.8,18.4,195,3900,male,2007
44
+ Adelie,Dream,36,18.5,186,3100,female,2007
45
+ Adelie,Dream,44.1,19.7,196,4400,male,2007
46
+ Adelie,Dream,37,16.9,185,3000,female,2007
47
+ Adelie,Dream,39.6,18.8,190,4600,male,2007
48
+ Adelie,Dream,41.1,19,182,3425,male,2007
49
+ Adelie,Dream,37.5,18.9,179,2975,NA,2007
50
+ Adelie,Dream,36,17.9,190,3450,female,2007
51
+ Adelie,Dream,42.3,21.2,191,4150,male,2007
52
+ Adelie,Biscoe,39.6,17.7,186,3500,female,2008
53
+ Adelie,Biscoe,40.1,18.9,188,4300,male,2008
54
+ Adelie,Biscoe,35,17.9,190,3450,female,2008
55
+ Adelie,Biscoe,42,19.5,200,4050,male,2008
56
+ Adelie,Biscoe,34.5,18.1,187,2900,female,2008
57
+ Adelie,Biscoe,41.4,18.6,191,3700,male,2008
58
+ Adelie,Biscoe,39,17.5,186,3550,female,2008
59
+ Adelie,Biscoe,40.6,18.8,193,3800,male,2008
60
+ Adelie,Biscoe,36.5,16.6,181,2850,female,2008
61
+ Adelie,Biscoe,37.6,19.1,194,3750,male,2008
62
+ Adelie,Biscoe,35.7,16.9,185,3150,female,2008
63
+ Adelie,Biscoe,41.3,21.1,195,4400,male,2008
64
+ Adelie,Biscoe,37.6,17,185,3600,female,2008
65
+ Adelie,Biscoe,41.1,18.2,192,4050,male,2008
66
+ Adelie,Biscoe,36.4,17.1,184,2850,female,2008
67
+ Adelie,Biscoe,41.6,18,192,3950,male,2008
68
+ Adelie,Biscoe,35.5,16.2,195,3350,female,2008
69
+ Adelie,Biscoe,41.1,19.1,188,4100,male,2008
70
+ Adelie,Torgersen,35.9,16.6,190,3050,female,2008
71
+ Adelie,Torgersen,41.8,19.4,198,4450,male,2008
72
+ Adelie,Torgersen,33.5,19,190,3600,female,2008
73
+ Adelie,Torgersen,39.7,18.4,190,3900,male,2008
74
+ Adelie,Torgersen,39.6,17.2,196,3550,female,2008
75
+ Adelie,Torgersen,45.8,18.9,197,4150,male,2008
76
+ Adelie,Torgersen,35.5,17.5,190,3700,female,2008
77
+ Adelie,Torgersen,42.8,18.5,195,4250,male,2008
78
+ Adelie,Torgersen,40.9,16.8,191,3700,female,2008
79
+ Adelie,Torgersen,37.2,19.4,184,3900,male,2008
80
+ Adelie,Torgersen,36.2,16.1,187,3550,female,2008
81
+ Adelie,Torgersen,42.1,19.1,195,4000,male,2008
82
+ Adelie,Torgersen,34.6,17.2,189,3200,female,2008
83
+ Adelie,Torgersen,42.9,17.6,196,4700,male,2008
84
+ Adelie,Torgersen,36.7,18.8,187,3800,female,2008
85
+ Adelie,Torgersen,35.1,19.4,193,4200,male,2008
86
+ Adelie,Dream,37.3,17.8,191,3350,female,2008
87
+ Adelie,Dream,41.3,20.3,194,3550,male,2008
88
+ Adelie,Dream,36.3,19.5,190,3800,male,2008
89
+ Adelie,Dream,36.9,18.6,189,3500,female,2008
90
+ Adelie,Dream,38.3,19.2,189,3950,male,2008
91
+ Adelie,Dream,38.9,18.8,190,3600,female,2008
92
+ Adelie,Dream,35.7,18,202,3550,female,2008
93
+ Adelie,Dream,41.1,18.1,205,4300,male,2008
94
+ Adelie,Dream,34,17.1,185,3400,female,2008
95
+ Adelie,Dream,39.6,18.1,186,4450,male,2008
96
+ Adelie,Dream,36.2,17.3,187,3300,female,2008
97
+ Adelie,Dream,40.8,18.9,208,4300,male,2008
98
+ Adelie,Dream,38.1,18.6,190,3700,female,2008
99
+ Adelie,Dream,40.3,18.5,196,4350,male,2008
100
+ Adelie,Dream,33.1,16.1,178,2900,female,2008
101
+ Adelie,Dream,43.2,18.5,192,4100,male,2008
102
+ Adelie,Biscoe,35,17.9,192,3725,female,2009
103
+ Adelie,Biscoe,41,20,203,4725,male,2009
104
+ Adelie,Biscoe,37.7,16,183,3075,female,2009
105
+ Adelie,Biscoe,37.8,20,190,4250,male,2009
106
+ Adelie,Biscoe,37.9,18.6,193,2925,female,2009
107
+ Adelie,Biscoe,39.7,18.9,184,3550,male,2009
108
+ Adelie,Biscoe,38.6,17.2,199,3750,female,2009
109
+ Adelie,Biscoe,38.2,20,190,3900,male,2009
110
+ Adelie,Biscoe,38.1,17,181,3175,female,2009
111
+ Adelie,Biscoe,43.2,19,197,4775,male,2009
112
+ Adelie,Biscoe,38.1,16.5,198,3825,female,2009
113
+ Adelie,Biscoe,45.6,20.3,191,4600,male,2009
114
+ Adelie,Biscoe,39.7,17.7,193,3200,female,2009
115
+ Adelie,Biscoe,42.2,19.5,197,4275,male,2009
116
+ Adelie,Biscoe,39.6,20.7,191,3900,female,2009
117
+ Adelie,Biscoe,42.7,18.3,196,4075,male,2009
118
+ Adelie,Torgersen,38.6,17,188,2900,female,2009
119
+ Adelie,Torgersen,37.3,20.5,199,3775,male,2009
120
+ Adelie,Torgersen,35.7,17,189,3350,female,2009
121
+ Adelie,Torgersen,41.1,18.6,189,3325,male,2009
122
+ Adelie,Torgersen,36.2,17.2,187,3150,female,2009
123
+ Adelie,Torgersen,37.7,19.8,198,3500,male,2009
124
+ Adelie,Torgersen,40.2,17,176,3450,female,2009
125
+ Adelie,Torgersen,41.4,18.5,202,3875,male,2009
126
+ Adelie,Torgersen,35.2,15.9,186,3050,female,2009
127
+ Adelie,Torgersen,40.6,19,199,4000,male,2009
128
+ Adelie,Torgersen,38.8,17.6,191,3275,female,2009
129
+ Adelie,Torgersen,41.5,18.3,195,4300,male,2009
130
+ Adelie,Torgersen,39,17.1,191,3050,female,2009
131
+ Adelie,Torgersen,44.1,18,210,4000,male,2009
132
+ Adelie,Torgersen,38.5,17.9,190,3325,female,2009
133
+ Adelie,Torgersen,43.1,19.2,197,3500,male,2009
134
+ Adelie,Dream,36.8,18.5,193,3500,female,2009
135
+ Adelie,Dream,37.5,18.5,199,4475,male,2009
136
+ Adelie,Dream,38.1,17.6,187,3425,female,2009
137
+ Adelie,Dream,41.1,17.5,190,3900,male,2009
138
+ Adelie,Dream,35.6,17.5,191,3175,female,2009
139
+ Adelie,Dream,40.2,20.1,200,3975,male,2009
140
+ Adelie,Dream,37,16.5,185,3400,female,2009
141
+ Adelie,Dream,39.7,17.9,193,4250,male,2009
142
+ Adelie,Dream,40.2,17.1,193,3400,female,2009
143
+ Adelie,Dream,40.6,17.2,187,3475,male,2009
144
+ Adelie,Dream,32.1,15.5,188,3050,female,2009
145
+ Adelie,Dream,40.7,17,190,3725,male,2009
146
+ Adelie,Dream,37.3,16.8,192,3000,female,2009
147
+ Adelie,Dream,39,18.7,185,3650,male,2009
148
+ Adelie,Dream,39.2,18.6,190,4250,male,2009
149
+ Adelie,Dream,36.6,18.4,184,3475,female,2009
150
+ Adelie,Dream,36,17.8,195,3450,female,2009
151
+ Adelie,Dream,37.8,18.1,193,3750,male,2009
152
+ Adelie,Dream,36,17.1,187,3700,female,2009
153
+ Adelie,Dream,41.5,18.5,201,4000,male,2009
154
+ Gentoo,Biscoe,46.1,13.2,211,4500,female,2007
155
+ Gentoo,Biscoe,50,16.3,230,5700,male,2007
156
+ Gentoo,Biscoe,48.7,14.1,210,4450,female,2007
157
+ Gentoo,Biscoe,50,15.2,218,5700,male,2007
158
+ Gentoo,Biscoe,47.6,14.5,215,5400,male,2007
159
+ Gentoo,Biscoe,46.5,13.5,210,4550,female,2007
160
+ Gentoo,Biscoe,45.4,14.6,211,4800,female,2007
161
+ Gentoo,Biscoe,46.7,15.3,219,5200,male,2007
162
+ Gentoo,Biscoe,43.3,13.4,209,4400,female,2007
163
+ Gentoo,Biscoe,46.8,15.4,215,5150,male,2007
164
+ Gentoo,Biscoe,40.9,13.7,214,4650,female,2007
165
+ Gentoo,Biscoe,49,16.1,216,5550,male,2007
166
+ Gentoo,Biscoe,45.5,13.7,214,4650,female,2007
167
+ Gentoo,Biscoe,48.4,14.6,213,5850,male,2007
168
+ Gentoo,Biscoe,45.8,14.6,210,4200,female,2007
169
+ Gentoo,Biscoe,49.3,15.7,217,5850,male,2007
170
+ Gentoo,Biscoe,42,13.5,210,4150,female,2007
171
+ Gentoo,Biscoe,49.2,15.2,221,6300,male,2007
172
+ Gentoo,Biscoe,46.2,14.5,209,4800,female,2007
173
+ Gentoo,Biscoe,48.7,15.1,222,5350,male,2007
174
+ Gentoo,Biscoe,50.2,14.3,218,5700,male,2007
175
+ Gentoo,Biscoe,45.1,14.5,215,5000,female,2007
176
+ Gentoo,Biscoe,46.5,14.5,213,4400,female,2007
177
+ Gentoo,Biscoe,46.3,15.8,215,5050,male,2007
178
+ Gentoo,Biscoe,42.9,13.1,215,5000,female,2007
179
+ Gentoo,Biscoe,46.1,15.1,215,5100,male,2007
180
+ Gentoo,Biscoe,44.5,14.3,216,4100,NA,2007
181
+ Gentoo,Biscoe,47.8,15,215,5650,male,2007
182
+ Gentoo,Biscoe,48.2,14.3,210,4600,female,2007
183
+ Gentoo,Biscoe,50,15.3,220,5550,male,2007
184
+ Gentoo,Biscoe,47.3,15.3,222,5250,male,2007
185
+ Gentoo,Biscoe,42.8,14.2,209,4700,female,2007
186
+ Gentoo,Biscoe,45.1,14.5,207,5050,female,2007
187
+ Gentoo,Biscoe,59.6,17,230,6050,male,2007
188
+ Gentoo,Biscoe,49.1,14.8,220,5150,female,2008
189
+ Gentoo,Biscoe,48.4,16.3,220,5400,male,2008
190
+ Gentoo,Biscoe,42.6,13.7,213,4950,female,2008
191
+ Gentoo,Biscoe,44.4,17.3,219,5250,male,2008
192
+ Gentoo,Biscoe,44,13.6,208,4350,female,2008
193
+ Gentoo,Biscoe,48.7,15.7,208,5350,male,2008
194
+ Gentoo,Biscoe,42.7,13.7,208,3950,female,2008
195
+ Gentoo,Biscoe,49.6,16,225,5700,male,2008
196
+ Gentoo,Biscoe,45.3,13.7,210,4300,female,2008
197
+ Gentoo,Biscoe,49.6,15,216,4750,male,2008
198
+ Gentoo,Biscoe,50.5,15.9,222,5550,male,2008
199
+ Gentoo,Biscoe,43.6,13.9,217,4900,female,2008
200
+ Gentoo,Biscoe,45.5,13.9,210,4200,female,2008
201
+ Gentoo,Biscoe,50.5,15.9,225,5400,male,2008
202
+ Gentoo,Biscoe,44.9,13.3,213,5100,female,2008
203
+ Gentoo,Biscoe,45.2,15.8,215,5300,male,2008
204
+ Gentoo,Biscoe,46.6,14.2,210,4850,female,2008
205
+ Gentoo,Biscoe,48.5,14.1,220,5300,male,2008
206
+ Gentoo,Biscoe,45.1,14.4,210,4400,female,2008
207
+ Gentoo,Biscoe,50.1,15,225,5000,male,2008
208
+ Gentoo,Biscoe,46.5,14.4,217,4900,female,2008
209
+ Gentoo,Biscoe,45,15.4,220,5050,male,2008
210
+ Gentoo,Biscoe,43.8,13.9,208,4300,female,2008
211
+ Gentoo,Biscoe,45.5,15,220,5000,male,2008
212
+ Gentoo,Biscoe,43.2,14.5,208,4450,female,2008
213
+ Gentoo,Biscoe,50.4,15.3,224,5550,male,2008
214
+ Gentoo,Biscoe,45.3,13.8,208,4200,female,2008
215
+ Gentoo,Biscoe,46.2,14.9,221,5300,male,2008
216
+ Gentoo,Biscoe,45.7,13.9,214,4400,female,2008
217
+ Gentoo,Biscoe,54.3,15.7,231,5650,male,2008
218
+ Gentoo,Biscoe,45.8,14.2,219,4700,female,2008
219
+ Gentoo,Biscoe,49.8,16.8,230,5700,male,2008
220
+ Gentoo,Biscoe,46.2,14.4,214,4650,NA,2008
221
+ Gentoo,Biscoe,49.5,16.2,229,5800,male,2008
222
+ Gentoo,Biscoe,43.5,14.2,220,4700,female,2008
223
+ Gentoo,Biscoe,50.7,15,223,5550,male,2008
224
+ Gentoo,Biscoe,47.7,15,216,4750,female,2008
225
+ Gentoo,Biscoe,46.4,15.6,221,5000,male,2008
226
+ Gentoo,Biscoe,48.2,15.6,221,5100,male,2008
227
+ Gentoo,Biscoe,46.5,14.8,217,5200,female,2008
228
+ Gentoo,Biscoe,46.4,15,216,4700,female,2008
229
+ Gentoo,Biscoe,48.6,16,230,5800,male,2008
230
+ Gentoo,Biscoe,47.5,14.2,209,4600,female,2008
231
+ Gentoo,Biscoe,51.1,16.3,220,6000,male,2008
232
+ Gentoo,Biscoe,45.2,13.8,215,4750,female,2008
233
+ Gentoo,Biscoe,45.2,16.4,223,5950,male,2008
234
+ Gentoo,Biscoe,49.1,14.5,212,4625,female,2009
235
+ Gentoo,Biscoe,52.5,15.6,221,5450,male,2009
236
+ Gentoo,Biscoe,47.4,14.6,212,4725,female,2009
237
+ Gentoo,Biscoe,50,15.9,224,5350,male,2009
238
+ Gentoo,Biscoe,44.9,13.8,212,4750,female,2009
239
+ Gentoo,Biscoe,50.8,17.3,228,5600,male,2009
240
+ Gentoo,Biscoe,43.4,14.4,218,4600,female,2009
241
+ Gentoo,Biscoe,51.3,14.2,218,5300,male,2009
242
+ Gentoo,Biscoe,47.5,14,212,4875,female,2009
243
+ Gentoo,Biscoe,52.1,17,230,5550,male,2009
244
+ Gentoo,Biscoe,47.5,15,218,4950,female,2009
245
+ Gentoo,Biscoe,52.2,17.1,228,5400,male,2009
246
+ Gentoo,Biscoe,45.5,14.5,212,4750,female,2009
247
+ Gentoo,Biscoe,49.5,16.1,224,5650,male,2009
248
+ Gentoo,Biscoe,44.5,14.7,214,4850,female,2009
249
+ Gentoo,Biscoe,50.8,15.7,226,5200,male,2009
250
+ Gentoo,Biscoe,49.4,15.8,216,4925,male,2009
251
+ Gentoo,Biscoe,46.9,14.6,222,4875,female,2009
252
+ Gentoo,Biscoe,48.4,14.4,203,4625,female,2009
253
+ Gentoo,Biscoe,51.1,16.5,225,5250,male,2009
254
+ Gentoo,Biscoe,48.5,15,219,4850,female,2009
255
+ Gentoo,Biscoe,55.9,17,228,5600,male,2009
256
+ Gentoo,Biscoe,47.2,15.5,215,4975,female,2009
257
+ Gentoo,Biscoe,49.1,15,228,5500,male,2009
258
+ Gentoo,Biscoe,47.3,13.8,216,4725,NA,2009
259
+ Gentoo,Biscoe,46.8,16.1,215,5500,male,2009
260
+ Gentoo,Biscoe,41.7,14.7,210,4700,female,2009
261
+ Gentoo,Biscoe,53.4,15.8,219,5500,male,2009
262
+ Gentoo,Biscoe,43.3,14,208,4575,female,2009
263
+ Gentoo,Biscoe,48.1,15.1,209,5500,male,2009
264
+ Gentoo,Biscoe,50.5,15.2,216,5000,female,2009
265
+ Gentoo,Biscoe,49.8,15.9,229,5950,male,2009
266
+ Gentoo,Biscoe,43.5,15.2,213,4650,female,2009
267
+ Gentoo,Biscoe,51.5,16.3,230,5500,male,2009
268
+ Gentoo,Biscoe,46.2,14.1,217,4375,female,2009
269
+ Gentoo,Biscoe,55.1,16,230,5850,male,2009
270
+ Gentoo,Biscoe,44.5,15.7,217,4875,NA,2009
271
+ Gentoo,Biscoe,48.8,16.2,222,6000,male,2009
272
+ Gentoo,Biscoe,47.2,13.7,214,4925,female,2009
273
+ Gentoo,Biscoe,NA,NA,NA,NA,NA,2009
274
+ Gentoo,Biscoe,46.8,14.3,215,4850,female,2009
275
+ Gentoo,Biscoe,50.4,15.7,222,5750,male,2009
276
+ Gentoo,Biscoe,45.2,14.8,212,5200,female,2009
277
+ Gentoo,Biscoe,49.9,16.1,213,5400,male,2009
278
+ Chinstrap,Dream,46.5,17.9,192,3500,female,2007
279
+ Chinstrap,Dream,50,19.5,196,3900,male,2007
280
+ Chinstrap,Dream,51.3,19.2,193,3650,male,2007
281
+ Chinstrap,Dream,45.4,18.7,188,3525,female,2007
282
+ Chinstrap,Dream,52.7,19.8,197,3725,male,2007
283
+ Chinstrap,Dream,45.2,17.8,198,3950,female,2007
284
+ Chinstrap,Dream,46.1,18.2,178,3250,female,2007
285
+ Chinstrap,Dream,51.3,18.2,197,3750,male,2007
286
+ Chinstrap,Dream,46,18.9,195,4150,female,2007
287
+ Chinstrap,Dream,51.3,19.9,198,3700,male,2007
288
+ Chinstrap,Dream,46.6,17.8,193,3800,female,2007
289
+ Chinstrap,Dream,51.7,20.3,194,3775,male,2007
290
+ Chinstrap,Dream,47,17.3,185,3700,female,2007
291
+ Chinstrap,Dream,52,18.1,201,4050,male,2007
292
+ Chinstrap,Dream,45.9,17.1,190,3575,female,2007
293
+ Chinstrap,Dream,50.5,19.6,201,4050,male,2007
294
+ Chinstrap,Dream,50.3,20,197,3300,male,2007
295
+ Chinstrap,Dream,58,17.8,181,3700,female,2007
296
+ Chinstrap,Dream,46.4,18.6,190,3450,female,2007
297
+ Chinstrap,Dream,49.2,18.2,195,4400,male,2007
298
+ Chinstrap,Dream,42.4,17.3,181,3600,female,2007
299
+ Chinstrap,Dream,48.5,17.5,191,3400,male,2007
300
+ Chinstrap,Dream,43.2,16.6,187,2900,female,2007
301
+ Chinstrap,Dream,50.6,19.4,193,3800,male,2007
302
+ Chinstrap,Dream,46.7,17.9,195,3300,female,2007
303
+ Chinstrap,Dream,52,19,197,4150,male,2007
304
+ Chinstrap,Dream,50.5,18.4,200,3400,female,2008
305
+ Chinstrap,Dream,49.5,19,200,3800,male,2008
306
+ Chinstrap,Dream,46.4,17.8,191,3700,female,2008
307
+ Chinstrap,Dream,52.8,20,205,4550,male,2008
308
+ Chinstrap,Dream,40.9,16.6,187,3200,female,2008
309
+ Chinstrap,Dream,54.2,20.8,201,4300,male,2008
310
+ Chinstrap,Dream,42.5,16.7,187,3350,female,2008
311
+ Chinstrap,Dream,51,18.8,203,4100,male,2008
312
+ Chinstrap,Dream,49.7,18.6,195,3600,male,2008
313
+ Chinstrap,Dream,47.5,16.8,199,3900,female,2008
314
+ Chinstrap,Dream,47.6,18.3,195,3850,female,2008
315
+ Chinstrap,Dream,52,20.7,210,4800,male,2008
316
+ Chinstrap,Dream,46.9,16.6,192,2700,female,2008
317
+ Chinstrap,Dream,53.5,19.9,205,4500,male,2008
318
+ Chinstrap,Dream,49,19.5,210,3950,male,2008
319
+ Chinstrap,Dream,46.2,17.5,187,3650,female,2008
320
+ Chinstrap,Dream,50.9,19.1,196,3550,male,2008
321
+ Chinstrap,Dream,45.5,17,196,3500,female,2008
322
+ Chinstrap,Dream,50.9,17.9,196,3675,female,2009
323
+ Chinstrap,Dream,50.8,18.5,201,4450,male,2009
324
+ Chinstrap,Dream,50.1,17.9,190,3400,female,2009
325
+ Chinstrap,Dream,49,19.6,212,4300,male,2009
326
+ Chinstrap,Dream,51.5,18.7,187,3250,male,2009
327
+ Chinstrap,Dream,49.8,17.3,198,3675,female,2009
328
+ Chinstrap,Dream,48.1,16.4,199,3325,female,2009
329
+ Chinstrap,Dream,51.4,19,201,3950,male,2009
330
+ Chinstrap,Dream,45.7,17.3,193,3600,female,2009
331
+ Chinstrap,Dream,50.7,19.7,203,4050,male,2009
332
+ Chinstrap,Dream,42.5,17.3,187,3350,female,2009
333
+ Chinstrap,Dream,52.2,18.8,197,3450,male,2009
334
+ Chinstrap,Dream,45.2,16.6,191,3250,female,2009
335
+ Chinstrap,Dream,49.3,19.9,203,4050,male,2009
336
+ Chinstrap,Dream,50.2,18.8,202,3800,male,2009
337
+ Chinstrap,Dream,45.6,19.4,194,3525,female,2009
338
+ Chinstrap,Dream,51.9,19.5,206,3950,male,2009
339
+ Chinstrap,Dream,46.8,16.5,189,3650,female,2009
340
+ Chinstrap,Dream,45.7,17,195,3650,female,2009
341
+ Chinstrap,Dream,55.8,19.8,207,4000,male,2009
342
+ Chinstrap,Dream,43.5,18.1,202,3400,female,2009
343
+ Chinstrap,Dream,49.6,18.2,193,3775,male,2009
344
+ Chinstrap,Dream,50.8,19,210,4100,male,2009
345
+ Chinstrap,Dream,50.2,18.7,198,3775,female,2009
ui.R ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rôle du fichier: ui.R porte une partie du pipeline d'analyse Rainette.
2
+ # ui.R
3
+
4
+ library(shiny)
5
+ library(htmltools)
6
+
7
+
8
+ if (!exists("ui_options_iramuteq", mode = "function", inherits = TRUE)) {
9
+ app_dir <- tryCatch(shiny::getShinyOption("appDir"), error = function(e) NULL)
10
+ if (is.null(app_dir) || !nzchar(app_dir)) app_dir <- getwd()
11
+ chemin_options_iramuteq <- file.path(app_dir, "iramuteq-like", "ui_options_iramuteq.R")
12
+
13
+ if (file.exists(chemin_options_iramuteq)) {
14
+ source(chemin_options_iramuteq, encoding = "UTF-8", local = TRUE)
15
+ }
16
+ }
17
+
18
+
19
+ if (!exists("ui_resultats_chd_iramuteq", mode = "function", inherits = TRUE)) {
20
+ app_dir <- tryCatch(shiny::getShinyOption("appDir"), error = function(e) NULL)
21
+ if (is.null(app_dir) || !nzchar(app_dir)) app_dir <- getwd()
22
+ chemin_affichage_iramuteq <- file.path(app_dir, "iramuteq-like", "affichage_iramuteq-like.R")
23
+
24
+ if (file.exists(chemin_affichage_iramuteq)) {
25
+ source(chemin_affichage_iramuteq, encoding = "UTF-8", local = TRUE)
26
+ }
27
+ }
28
+
29
+ if (!exists("ui_aide_huggingface", mode = "function")) {
30
+ if (file.exists("help.md")) {
31
+ ui_aide_huggingface <- function() {
32
+ tagList(
33
+ tags$h2("Aide"),
34
+ includeMarkdown("help/help.md")
35
+ )
36
+ }
37
+ } else {
38
+ ui_aide_huggingface <- function() {
39
+ tagList(
40
+ tags$h2("Aide"),
41
+ tags$p("Le fichier help.md est introuvable. Ajoute help.md à la racine du projet.")
42
+ )
43
+ }
44
+ }
45
+ }
46
+
47
+ if (!exists("REGEX_CARACTERES_A_SUPPRIMER", inherits = TRUE)) {
48
+ app_dir <- tryCatch(shiny::getShinyOption("appDir"), error = function(e) NULL)
49
+ if (is.null(app_dir) || !nzchar(app_dir)) app_dir <- getwd()
50
+ chemin_nettoyage <- file.path(app_dir, "rainette", "nettoyage_rainette.R")
51
+
52
+ if (file.exists(chemin_nettoyage)) {
53
+ source(chemin_nettoyage, encoding = "UTF-8", local = TRUE)
54
+ }
55
+ }
56
+
57
+ if (!exists("REGEX_CARACTERES_A_SUPPRIMER", inherits = TRUE)) {
58
+ # Fallback explicite : évite d'afficher un message d'erreur permanent dans l'UI
59
+ # quand le fichier rainette/nettoyage_rainette.R n'a pas pu être sourcé dans cet environnement.
60
+ REGEX_CARACTERES_AUTORISES <- "a-zA-Z0-9àÀâÂäÄáÁåÅãéÉèÈêÊëËìÌîÎïÏíÍóÓòÒôÔöÖõÕøØùÙûÛüÜúÚçÇßœŒ’ñÑ\\.:,;!\\?'"
61
+ REGEX_CARACTERES_A_SUPPRIMER <- paste0("[^", REGEX_CARACTERES_AUTORISES, "]")
62
+ }
63
+
64
+ ui <- fluidPage(
65
+ tags$head(
66
+ tags$style(HTML("
67
+ #shiny-modal .modal-dialog {
68
+ width: 96vw !important;
69
+ max-width: 96vw !important;
70
+ }
71
+ #shiny-modal .modal-body {
72
+ max-height: 88vh !important;
73
+ overflow-y: auto !important;
74
+ }
75
+ .sidebar-section-title {
76
+ font-weight: 700;
77
+ font-size: 18px !important;
78
+ color: #1e5aa8 !important;
79
+ margin-top: 12px;
80
+ margin-bottom: 6px;
81
+ }
82
+ small {
83
+ color: #842029 !important;
84
+ }
85
+ "))
86
+ ),
87
+
88
+ tags$h2(
89
+ style = "color: #1e5aa8;",
90
+ "IRaMuTeQ-Lite"
91
+ ),
92
+ tags$p(
93
+ style = "font-size: 14px;",
94
+ "Tentaive de reproduction de la CHD (Méthode Reinert) du logiciel IRaMuTeQ",
95
+ tags$br(),
96
+ "En test j'ai également expérimenté la recherche de NER dans le corpus s'appuyant sur la librairie Spacy (modele \"md\").",
97
+ tags$br(),
98
+ "Pour plus d’informations, vous pouvez consulter mon site : www.codeandcortex.fr",
99
+ tags$br(),
100
+ "version beta 0.4 - 18-02-2026"
101
+ ),
102
+
103
+ sidebarLayout(
104
+ sidebarPanel(
105
+ fileInput("fichier_corpus", "Uploader un corpus IRaMuTeQ (.txt)", accept = c(".txt")),
106
+
107
+ radioButtons(
108
+ "modele_chd",
109
+ "Méthode Iramuteq-like",
110
+ choices = c(
111
+ "IRaMuTeQ-like" = "iramuteq"
112
+ ),
113
+ selected = "iramuteq",
114
+ inline = FALSE
115
+ ),
116
+
117
+ tags$div(class = "sidebar-section-title", "Paramètres communs CHD"),
118
+ numericInput("segment_size", "segment_size", value = 40, min = 5, step = 1),
119
+ numericInput("min_docfreq", "Fréquence minimale des termes (min_docfreq)", value = 3, min = 1, step = 1),
120
+ numericInput("max_p", "max_p (p-value)", value = 0.05, min = 0, max = 1, step = 0.01),
121
+ checkboxInput(
122
+ "filtrer_affichage_pvalue",
123
+ "Filtrer l'affichage des résultats par p-value (p ≤ max_p)",
124
+ value = TRUE
125
+ ),
126
+
127
+ conditionalPanel(
128
+ condition = "input.modele_chd == 'rainette'",
129
+ ui_options_rainette()
130
+ ),
131
+
132
+ conditionalPanel(
133
+ condition = "input.modele_chd == 'iramuteq'",
134
+ ui_options_iramuteq()
135
+ ),
136
+
137
+ tags$div(class = "sidebar-section-title", "Dictionnaire"),
138
+ radioButtons(
139
+ "source_dictionnaire",
140
+ "Source de lemmatisation",
141
+ choices = c("spaCy" = "spacy", "Lexique (fr)" = "lexique_fr"),
142
+ selected = "spacy",
143
+ inline = FALSE
144
+ ),
145
+ conditionalPanel(
146
+ condition = "input.modele_chd == 'iramuteq'",
147
+ tags$small("En mode IRaMuTeQ-like, seul le dictionnaire Lexique (fr) est utilisé automatiquement."),
148
+ tags$small("Dans ce mode, le filtrage des stopwords utilise la liste française de quanteda (pas spaCy).")
149
+ ),
150
+ conditionalPanel(
151
+ condition = "input.source_dictionnaire == 'spacy'",
152
+ selectInput(
153
+ "spacy_langue",
154
+ "Langue spaCy",
155
+ choices = c("Français" = "fr", "Anglais" = "en", "Espagnol" = "es", "Italien" = "it", "Allemand" = "de", "Portugais" = "pt", "Catalan" = "ca"),
156
+ selected = "fr"
157
+ )
158
+ ),
159
+ conditionalPanel(
160
+ condition = "input.source_dictionnaire == 'spacy'",
161
+ checkboxInput("spacy_utiliser_lemmes", "Lemmatisation via spaCy uniquement", value = FALSE)
162
+ ),
163
+ conditionalPanel(
164
+ condition = "input.source_dictionnaire == 'lexique_fr'",
165
+ checkboxInput("lexique_utiliser_lemmes", "Lemmatisation via les lemmes de lexique_fr (forme → c_lemme)", value = TRUE)
166
+ ),
167
+ uiOutput("ui_spacy_langue_detection"),
168
+
169
+
170
+ tags$div(class = "sidebar-section-title", "Nettoyage"),
171
+
172
+ conditionalPanel(
173
+ condition = "input.modele_chd == 'iramuteq'",
174
+ tags$div(
175
+ style = "margin: 0 0 8px 0; padding: 8px; background: #f7fbff; border-left: 3px solid #1e5aa8;",
176
+ tags$strong("Options IRaMuTeQ-like (iramuteq-like/textprepa_iramuteq.py)"),
177
+ tags$br(),
178
+ tags$small("Ces options pilotent la préparation du texte avant la tokenisation en mode IRaMuTeQ-like.")
179
+ )
180
+ ),
181
+
182
+ checkboxInput("nettoyage_caracteres", "Nettoyage caractères (regex)", value = FALSE),
183
+ checkboxInput("forcer_minuscules_avant", "Passage en minuscules avant tokenisation", value = FALSE),
184
+ checkboxInput("supprimer_ponctuation", "Supprimer la ponctuation", value = FALSE),
185
+ tags$small("Supprime la ponctuation à la tokenisation quanteda (remove_punct), pour les deux sources (spaCy et lexique_fr), par ex. . , ; : ! ? ' ’ \" - ( ) [ ] …"),
186
+ checkboxInput("supprimer_chiffres", "Supprimer les chiffres (0-9)", value = FALSE),
187
+ checkboxInput("supprimer_apostrophes", "Traiter les élisions FR (c'est→est, m'écrire→écrire)", value = FALSE),
188
+ checkboxInput("retirer_stopwords", "Retirer les stopwords (spaCy si source spaCy, quanteda si source Lexique fr)", value = FALSE),
189
+ tags$small("La normalisation en minuscules est appliquée automatiquement avant la construction du DFM."),
190
+ checkboxInput("filtrage_morpho", "Filtrage morphosyntaxique", value = FALSE),
191
+ tags$small("Le filtrage morphosyntaxique s'applique à spaCy ou lexique_fr selon la source sélectionnée."),
192
+ conditionalPanel(
193
+ condition = "input.filtrage_morpho == true",
194
+ conditionalPanel(
195
+ condition = "input.source_dictionnaire == 'spacy'",
196
+ selectizeInput(
197
+ "pos_spacy_a_conserver",
198
+ "POS à conserver (spaCy)",
199
+ choices = c(
200
+ "ADJ", "ADP", "ADV", "AUX", "CCONJ", "DET", "INTJ", "NOUN",
201
+ "NUM", "PART", "PRON", "PROPN", "PUNCT", "SCONJ", "SYM", "VERB", "X"
202
+ ),
203
+ selected = c("NOUN", "VERB"),
204
+ multiple = TRUE,
205
+ options = list(plugins = list("remove_button"))
206
+ )
207
+ ),
208
+ conditionalPanel(
209
+ condition = "input.source_dictionnaire == 'lexique_fr'",
210
+ selectizeInput(
211
+ "pos_lexique_a_conserver",
212
+ "Catégories c_morpho à conserver (lexique_fr)",
213
+ choices = c(
214
+ "NOM", "VER", "AUX", "ADJ", "ADV", "PRE", "CON", "ONO",
215
+ "ADJ:NUM", "ADJ:POS", "ADJ:IND", "ADJ:INT", "ADJ:DEM",
216
+ "PRO:PER", "PRO:POS", "PRO:DEM", "PRO:IND", "PRO:REL", "PRO:INT",
217
+ "ART:DEF", "ART:IND"
218
+ ),
219
+ selected = c("NOM", "VER", "ADJ"),
220
+ multiple = TRUE,
221
+ options = list(plugins = list("remove_button"))
222
+ )
223
+ )
224
+ ),
225
+ tags$small("Regex appliquée quand “Nettoyage caractères (regex)” est activé :"),
226
+ tags$pre(
227
+ style = "white-space: pre-wrap; font-size: 11px; border: 1px solid #ddd; padding: 6px;",
228
+ REGEX_CARACTERES_A_SUPPRIMER
229
+ ),
230
+ tags$small("Les caractères présents dans la liste entre crochets sont conservés ; tous les autres (ex. @ # & / emoji) sont remplacés par des espaces."),
231
+ tags$small("L'option “Supprimer la ponctuation” pilote remove_punct, même si elle est autorisée par la regex ci-dessus."),
232
+ tags$small("Cette option conserve les apostrophes lexicales (ex. aujourd'hui) et ne traite que les élisions en début de mot."),
233
+
234
+ tags$div(class = "sidebar-section-title", "Paramètres SpaCy/NER"),
235
+
236
+ checkboxInput("activer_ner", "Activer NER (spaCy)", value = FALSE),
237
+ uiOutput("ui_ner_lexique_incompatibilite"),
238
+ conditionalPanel(
239
+ condition = "input.activer_ner == true",
240
+ fileInput(
241
+ "fichier_ner_json",
242
+ "Importer un dictionnaire NER (.json)",
243
+ accept = c(".json", "application/json")
244
+ ),
245
+ tags$small("Optionnel : importez un dictionnaire NER JSON si vous voulez personnaliser les entités. Si vous ne fournissez pas de fichier, l'analyse utilise le NER spaCy classique.")
246
+ ),
247
+
248
+ tags$hr(),
249
+
250
+ tags$div(class = "sidebar-section-title", "Paramètres AFC"),
251
+
252
+ checkboxInput("afc_reduire_chevauchement", "Réduire les chevauchements des mots (AFC)", value = FALSE),
253
+
254
+ radioButtons(
255
+ "afc_taille_mots",
256
+ "Taille des mots (AFC termes)",
257
+ choices = c("Fréquence" = "frequency", "Chi2" = "chi2"),
258
+ selected = "frequency",
259
+ inline = FALSE
260
+ ),
261
+
262
+ tags$hr(),
263
+
264
+ tags$div(
265
+ style = "display: flex; gap: 8px; flex-wrap: wrap; align-items: center;",
266
+ actionButton("lancer", "Lancer l'analyse"),
267
+ actionButton("explor", "Explor rainette", class = "btn-primary")
268
+ ),
269
+
270
+ tags$hr(),
271
+
272
+ downloadButton("dl_zip", "Télécharger exports (zip)"),
273
+ downloadButton("dl_afc_zip", "Télécharger AFC (zip)")
274
+ ),
275
+
276
+ mainPanel(
277
+ tabsetPanel(
278
+ id = "onglets_principaux",
279
+
280
+ tabPanel(
281
+ "Analyse",
282
+ tags$h3("Statut"),
283
+ textOutput("statut"),
284
+ tags$h3("Journal"),
285
+ tags$pre(style = "white-space: pre-wrap;", textOutput("logs")),
286
+ tags$h3("Analyse du corpus (mode debug)"),
287
+ uiOutput("ui_table_stats_corpus"),
288
+ tags$div(
289
+ style = "width: 600px;",
290
+ plotOutput("plot_stats_zipf", height = "600px", width = "600px")
291
+ ),
292
+ tags$h3("Répartition des classes"),
293
+ tableOutput("table_classes")
294
+ ),
295
+
296
+ tabPanel(
297
+ "Explore rainette",
298
+ tags$h3("Explore_rainette"),
299
+ selectInput("classe_viz", "Classe", choices = c("1"), selected = "1"),
300
+ tabsetPanel(
301
+ tabPanel(
302
+ "CHD",
303
+ tags$h4("Dendrogramme CHD (Rainette)"),
304
+ plotOutput("plot_chd_rainette_dendro", height = "360px"),
305
+ tags$hr(),
306
+ fluidRow(
307
+ column(
308
+ 4,
309
+ sliderInput("k_plot", "Nombre de classes (k)", min = 2, max = 2, value = 2, step = 1),
310
+ selectInput(
311
+ "measure_plot", "Statistiques",
312
+ choices = c(
313
+ "Frequency - Terms" = "frequency",
314
+ "Keyness - Chi-squared" = "chi2",
315
+ "Keyness - Likelihood ratio" = "lr",
316
+ "Frequency - Documents proportion" = "docprop"
317
+ ),
318
+ selected = "frequency"
319
+ ),
320
+ selectInput("type_plot", "Type", choices = c("bar", "cloud"), selected = "bar"),
321
+ numericInput("n_terms_plot", "Nombre de termes", value = 20, min = 5, max = 1000, step = 1),
322
+ conditionalPanel(
323
+ "input.measure_plot != 'docprop'",
324
+ checkboxInput("same_scales_plot", "Forcer les mêmes échelles", value = TRUE)
325
+ ),
326
+ checkboxInput("show_negative_plot", "Afficher les valeurs négatives", value = FALSE),
327
+ numericInput("text_size_plot", "Taille du texte", value = 12, min = 6, max = 30, step = 1)
328
+ ),
329
+ column(
330
+ 8,
331
+ plotOutput("plot_chd", height = "70vh")
332
+ )
333
+ )
334
+ ),
335
+ tabPanel("Concordancier HTML", uiOutput("ui_concordancier_explore")),
336
+ tabPanel("Nuage de mots", uiOutput("ui_wordcloud")),
337
+ tabPanel("Statistiques", tableOutput("table_stats_classe"))
338
+ )
339
+ ),
340
+
341
+
342
+
343
+ ui_resultats_chd_iramuteq(),
344
+
345
+ tabPanel(
346
+ "Prévisu corpus",
347
+ tags$h3("Corpus importé"),
348
+ uiOutput("ui_corpus_preview")
349
+ ),
350
+
351
+
352
+ tabPanel(
353
+ "AFC",
354
+ tags$h3("AFC"),
355
+ uiOutput("ui_afc_statut"),
356
+ uiOutput("ui_afc_erreurs"),
357
+
358
+ tags$h4("AFC des classes (Représentation des classes)"),
359
+ plotOutput("plot_afc_classes", height = "620px"),
360
+
361
+ tags$h4("AFC des termes"),
362
+ tags$p("Les mots sont colorés selon la classe où ils sont le plus surreprésentés (résidus standardisés) et leur taille est proportionnelle à leur fréquence globale ou chi2 (selon le choix)."),
363
+ plotOutput("plot_afc", height = "720px"),
364
+ tags$h4("Table des mots projetés (fréquence, chi2, p-value, segment exemple)"),
365
+ uiOutput("ui_table_afc_mots_par_classe"),
366
+
367
+ tags$h4("AFC des variables étoilées"),
368
+ plotOutput("plot_afc_vars", height = "720px"),
369
+ tags$h4("Table des modalités projetées"),
370
+ tableOutput("table_afc_vars"),
371
+
372
+ tags$h4("Valeurs propres"),
373
+ tableOutput("table_afc_eig")
374
+ ),
375
+
376
+ tabPanel(
377
+ "NER (beta)",
378
+ tags$h3("Détection d'entités nommées (spaCy)"),
379
+ uiOutput("ui_ner_statut"),
380
+ tags$h3("Résumé"),
381
+ tableOutput("table_ner_resume"),
382
+ tags$h3("Détails"),
383
+ tableOutput("table_ner_details"),
384
+ tags$h3("Nuage de mots (entités)"),
385
+ plotOutput("plot_ner_wordcloud", height = "520px"),
386
+ tags$h3("Nuages par classe"),
387
+ uiOutput("ui_ner_wordcloud_par_classe")
388
+ ),
389
+
390
+ tabPanel(
391
+ "Aide",
392
+ ui_aide_huggingface()
393
+ ),
394
+
395
+ tabPanel(
396
+ "Aide POS/Spacy",
397
+ tags$div(
398
+ style = "padding: 12px;",
399
+ if (file.exists("pos_spacy.md")) {
400
+ includeMarkdown("pos_spacy.md")
401
+ } else {
402
+ tags$p("Le fichier pos_spacy.md est introuvable à la racine du projet.")
403
+ }
404
+ )
405
+ ),
406
+
407
+ tabPanel(
408
+ "Aide NER",
409
+ tags$div(
410
+ style = "padding: 12px;",
411
+ if (file.exists("spacy_ner/ner.md")) {
412
+ includeMarkdown("spacy_ner/ner.md")
413
+ } else {
414
+ tags$p("Le fichier spacy_ner/ner.md est introuvable.")
415
+ }
416
+ )
417
+ )
418
+ )
419
+ )
420
+ )
421
+ )