Nothing
# =============================================================================
# statAfrikR - Module Tableaux
# Production de tableaux statistiques institutionnels INS
# Exports : Word (flextable) et Excel (openxlsx2)
# =============================================================================
utils::globalVariables(c(
"variable", "n", "moyenne", "mediane", "ecart_type",
"min", "max", "ic_bas", "ic_haut", "proportion",
"pourcentage", "effectif", "modalite", "total"
))
# Palette institutionnelle
.INS_NAVY <- "#0F2742"
.INS_PETRL <- "#1B4965"
.INS_GOLD <- "#B8872F"
.INS_GREY <- "#F4F7FA"
# =============================================================================
# 1. TABLEAU DESCRIPTIF
# =============================================================================
#' @title Tableau de statistiques descriptives institutionnel
#' @description Produit un tableau de statistiques descriptives pondere,
#' formate selon les conventions des INS africains. Exportable en
#' Word (flextable) ou Excel (openxlsx2).
#'
#' @param data data.frame, tibble ou objet \code{svydesign} -- Donnees
#' @param vars character -- Variables numeriques a analyser
#' @param poids character ou NULL -- Variable de ponderation. Defaut : NULL
#' @param par character ou NULL -- Variable de ventilation (groupe).
#' Defaut : NULL
#' @param stats character -- Statistiques parmi :
#' \code{"n"}, \code{"moyenne"}, \code{"mediane"}, \code{"ecart_type"},
#' \code{"min"}, \code{"max"}, \code{"ic"}. Defaut : toutes
#' @param format character -- \code{"tibble"}, \code{"flextable"} ou
#' \code{"excel"}. Defaut : "tibble"
#' @param chemin character ou NULL -- Chemin Excel. Defaut : NULL
#' @param titre character ou NULL -- Titre du tableau. Defaut : NULL
#' @param source character ou NULL -- Note source. Defaut : NULL
#'
#' @return Tibble, flextable ou chemin fichier Excel
#'
#' @examples
#' set.seed(42)
#' donnees <- data.frame(
#' age = sample(18:65, 200, replace = TRUE),
#' revenu = rexp(200, rate = 1/250000),
#' poids = runif(200, 0.8, 1.3),
#' milieu = sample(c("urbain", "rural"), 200, TRUE)
#' )
#' tableau_descriptif(donnees, vars = c("age", "revenu"), poids = "poids")
#'
#' @export
tableau_descriptif <- function(data,
vars,
poids = NULL,
par = NULL,
stats = c("n", "moyenne", "mediane",
"ecart_type", "min", "max",
"ic"),
format = c("tibble", "flextable", "excel"),
chemin = NULL,
titre = NULL,
source = NULL) {
format <- match.arg(format)
est_svy <- inherits(data, "survey.design")
data_brute <- if (est_svy) data$variables else data
# Validations
vars_abs <- setdiff(vars, names(data_brute))
if (length(vars_abs) > 0) {
rlang::abort(paste0("Variables introuvables : ",
paste(vars_abs, collapse = ", ")))
}
vars_num <- vars[sapply(data_brute[vars], is.numeric)]
vars_non <- setdiff(vars, vars_num)
if (length(vars_non) > 0) {
rlang::warn(paste0("Variables non numeriques ignorees : ",
paste(vars_non, collapse = ", ")))
vars <- vars_num
}
if (length(vars) == 0) rlang::abort("Aucune variable numerique valide.")
# Calcul
if (est_svy || (!is.null(poids) && poids %in% names(data_brute))) {
res <- .desc_pondere(data, data_brute, vars, poids, par, stats, est_svy)
} else {
res <- .desc_simple(data_brute, vars, par, stats)
}
if (format == "tibble") return(res)
if (format == "flextable") {
.verifier_package("flextable", "tableau_descriptif")
return(.ft_descriptif(res, titre, source))
}
if (format == "excel") {
.verifier_package("openxlsx2", "tableau_descriptif")
if (is.null(chemin))
chemin <- file.path(tempdir(), "tableau_descriptif.xlsx")
.excel_simple(res, chemin, titre, source, sheet = "Descriptif")
return(invisible(chemin))
}
}
# =============================================================================
# 2. TABLEAU CROISE INSTITUTIONNEL
# =============================================================================
#' @title Tableau croise pondere - format institutionnel INS
#' @description Produit un tableau de contingence pondere avec marges,
#' pourcentages et test du chi-deux.
#'
#' @param data data.frame, tibble ou \code{svydesign} -- Donnees
#' @param ligne character -- Variable en ligne
#' @param colonne character -- Variable en colonne
#' @param poids character ou NULL -- Variable de ponderation. Defaut : NULL
#' @param type_pct character -- \code{"colonne"}, \code{"ligne"} ou
#' \code{"total"}. Defaut : "colonne"
#' @param marges logical -- Afficher les totaux. Defaut : TRUE
#' @param chi2 logical -- Calculer le test du chi-deux. Defaut : TRUE
#' @param format character -- \code{"tibble"}, \code{"flextable"} ou
#' \code{"excel"}. Defaut : "tibble"
#' @param chemin character ou NULL -- Chemin Excel. Defaut : NULL
#' @param titre character ou NULL -- Titre. Defaut : NULL
#'
#' @return Tibble, flextable ou chemin Excel
#'
#' @examples
#' set.seed(42)
#' donnees <- data.frame(
#' region = sample(c("Nord", "Sud", "Est", "Ouest"), 300, TRUE),
#' milieu = sample(c("urbain", "rural"), 300, TRUE, prob = c(0.4, 0.6)),
#' poids = runif(300, 0.7, 1.4)
#' )
#' tableau_croise_ins(donnees, "region", "milieu", poids = "poids")
#'
#' @export
tableau_croise_ins <- function(data,
ligne,
colonne,
poids = NULL,
type_pct = c("colonne", "ligne", "total"),
marges = TRUE,
chi2 = TRUE,
format = c("tibble", "flextable", "excel"),
chemin = NULL,
titre = NULL) {
type_pct <- match.arg(type_pct)
format <- match.arg(format)
est_svy <- inherits(data, "survey.design")
data_brute <- if (est_svy) data$variables else data
for (v in c(ligne, colonne)) {
if (!v %in% names(data_brute)) {
rlang::abort(paste0("Variable introuvable : '", v, "'."))
}
}
w <- if (!is.null(poids) && poids %in% names(data_brute)) {
data_brute[[poids]]
} else {
rep(1, nrow(data_brute))
}
# Table ponderee
tab_pond <- stats::xtabs(w ~ data_brute[[ligne]] + data_brute[[colonne]])
names(dimnames(tab_pond)) <- c(ligne, colonne)
tab_pct <- switch(type_pct,
"colonne" = prop.table(tab_pond, margin = 2) * 100,
"ligne" = prop.table(tab_pond, margin = 1) * 100,
"total" = prop.table(tab_pond) * 100
)
# Tibble long
res_eff <- as.data.frame(tab_pond, stringsAsFactors = FALSE)
names(res_eff) <- c(ligne, colonne, "effectif")
res_pct <- as.data.frame(tab_pct, stringsAsFactors = FALSE)
names(res_pct) <- c(ligne, colonne, "pourcentage")
res_pct$pourcentage <- round(res_pct$pourcentage, 1)
res <- dplyr::left_join(res_eff, res_pct, by = c(ligne, colonne))
# Format large
res_large <- tidyr::pivot_wider(
res,
id_cols = dplyr::all_of(ligne),
names_from = dplyr::all_of(colonne),
values_from = "pourcentage"
)
eff_total <- dplyr::summarise(
dplyr::group_by(res, dplyr::across(dplyr::all_of(ligne))),
N = sum(.data$effectif), .groups = "drop"
)
res_large <- dplyr::left_join(res_large, eff_total, by = ligne)
# Marges
if (marges) {
total_row <- as.data.frame(t(colSums(res_large[sapply(res_large,
is.numeric)],
na.rm = TRUE)))
total_row[[ligne]] <- "Total"
res_large <- dplyr::bind_rows(res_large, total_row)
}
# Chi-deux
chi2_res <- NULL
if (chi2) {
tab_brute <- table(data_brute[[ligne]], data_brute[[colonne]])
chi2_res <- tryCatch(
stats::chisq.test(tab_brute),
warning = function(w) suppressWarnings(stats::chisq.test(tab_brute))
)
}
attr(res_large, "chi2") <- chi2_res
attr(res_large, "titre") <- titre
attr(res_large, "ligne") <- ligne
attr(res_large, "colonne") <- colonne
if (format == "tibble") return(res_large)
if (format == "flextable") {
.verifier_package("flextable", "tableau_croise_ins")
return(.ft_croise(res_large, chi2_res, titre, ligne, colonne))
}
if (format == "excel") {
.verifier_package("openxlsx2", "tableau_croise_ins")
if (is.null(chemin))
chemin <- file.path(tempdir(), "tableau_croise.xlsx")
note_chi2 <- if (!is.null(chi2_res)) {
paste0("Chi-deux = ", round(chi2_res$statistic, 3),
" p = ", round(chi2_res$p.value, 4))
} else NULL
.excel_simple(res_large, chemin, titre, note_chi2,
sheet = "Croisement")
return(invisible(chemin))
}
}
# =============================================================================
# 3. EXPORT EXCEL MULTI-FEUILLES
# =============================================================================
#' @title Export Excel institutionnel multi-feuilles
#' @description Exporte une liste de tableaux dans un classeur Excel
#' avec feuille de sommaire.
#'
#' @param tableaux list -- Liste nommee de data.frames a exporter
#' @param chemin character -- Chemin du fichier Excel
#' @param titre_classeur character -- Titre general.
#' Defaut : \code{"Statistiques INS"}
#' @param pays character ou NULL -- Pays/organisation. Defaut : NULL
#' @param annee integer ou NULL -- Annee de reference. Defaut : NULL
#' @param style character -- Style : \code{"ins_standard"} ou
#' \code{"minimal"}. Defaut : "ins_standard"
#'
#' @return Chemin du fichier cree (invisible)
#'
#' @examples
#' \dontrun{
#' tableaux <- list(
#' "Descriptif" = data.frame(
#' variable = c("age", "revenu"),
#' n = c(200L, 200L),
#' moyenne = c(38.2, 245000)
#' ),
#' "Croisement" = data.frame(
#' region = c("Nord", "Sud"),
#' urbain = c(45.2, 32.1),
#' rural = c(54.8, 67.9)
#' )
#' )
#' chemin <- file.path(tempdir(), "stats_ins.xlsx")
#' exporter_excel_ins(tableaux, chemin,
#' titre_classeur = "Enquete menages 2026",
#' pays = "Centrafrique", annee = 2026L)
#' }
#'
#' @export
exporter_excel_ins <- function(tableaux,
chemin,
titre_classeur = "Statistiques INS",
pays = NULL,
annee = NULL,
style = c("ins_standard",
"minimal")) {
style <- match.arg(style)
.verifier_package("openxlsx2", "exporter_excel_ins")
if (!is.list(tableaux) || length(tableaux) == 0) {
rlang::abort("`tableaux` doit etre une liste non vide.")
}
if (!is.character(chemin) || nchar(chemin) == 0) {
rlang::abort("`chemin` doit etre un chemin valide.")
}
if (!dir.exists(dirname(chemin))) {
rlang::abort(paste0("Repertoire inexistant : ", dirname(chemin)))
}
noms <- names(tableaux)
if (is.null(noms) || any(noms == "")) {
noms <- paste0("Tableau_", seq_along(tableaux))
names(tableaux) <- noms
}
wb <- openxlsx2::wb_workbook()
# Feuille Sommaire
wb <- openxlsx2::wb_add_worksheet(wb, sheet = "Sommaire")
meta_lines <- c(
titre_classeur,
if (!is.null(pays)) paste0("Pays / Organisation : ", pays),
if (!is.null(annee)) paste0("Annee de reference : ", annee),
paste0("Date de production : ", format(Sys.Date(), "%d/%m/%Y")),
"Produit avec statAfrikR -- cran.r-project.org"
)
meta_lines <- meta_lines[!sapply(meta_lines, is.null)]
for (i in seq_along(meta_lines)) {
wb <- openxlsx2::wb_add_data(wb, sheet = "Sommaire",
x = meta_lines[[i]],
start_row = i, start_col = 1,
col_names = FALSE)
}
debut_index <- length(meta_lines) + 2L
index_df <- data.frame(
Feuille = noms,
Lignes = sapply(tableaux, nrow),
Colonnes = sapply(tableaux, ncol)
)
wb <- openxlsx2::wb_add_data(wb, sheet = "Sommaire",
x = index_df,
start_row = debut_index)
# Une feuille par tableau
for (nm in noms) {
tab <- tableaux[[nm]]
sheet_nm <- substr(nm, 1, 31)
wb <- openxlsx2::wb_add_worksheet(wb, sheet = sheet_nm)
wb <- openxlsx2::wb_add_data(wb, sheet = sheet_nm,
x = nm, start_row = 1L, start_col = 1L,
col_names = FALSE)
wb <- openxlsx2::wb_add_data(wb, sheet = sheet_nm,
x = tab, start_row = 2L)
if (style == "ins_standard") {
n_cols <- ncol(tab)
wb <- openxlsx2::wb_add_fill(
wb, sheet = sheet_nm,
color = openxlsx2::wb_color(.INS_NAVY),
dims = openxlsx2::wb_dims(rows = 2L, cols = seq_len(n_cols))
)
wb <- openxlsx2::wb_add_font(
wb, sheet = sheet_nm,
bold = TRUE,
color = openxlsx2::wb_color("FFFFFF"),
dims = openxlsx2::wb_dims(rows = 2L, cols = seq_len(n_cols))
)
}
}
openxlsx2::wb_save(wb, file = chemin, overwrite = TRUE)
message("Classeur exporte : ", chemin,
" (", length(tableaux), " feuilles)")
invisible(chemin)
}
# =============================================================================
# FONCTIONS INTERNES
# =============================================================================
#' @keywords internal
.desc_simple <- function(data, vars, par, stats) {
calc <- function(df, v) {
x <- df[[v]][!is.na(df[[v]])]
n <- length(x)
res <- tibble::tibble(variable = v, n = n)
if ("moyenne" %in% stats) res$moyenne <- round(mean(x), 3)
if ("mediane" %in% stats) res$mediane <- round(stats::median(x), 3)
if ("ecart_type" %in% stats) res$ecart_type <- round(stats::sd(x), 3)
if ("min" %in% stats) res$min <- round(min(x), 3)
if ("max" %in% stats) res$max <- round(max(x), 3)
if ("ic" %in% stats) {
se <- stats::sd(x) / sqrt(n)
res$ic_bas <- round(mean(x) - 1.96 * se, 3)
res$ic_haut <- round(mean(x) + 1.96 * se, 3)
}
res
}
if (!is.null(par) && par %in% names(data)) {
groupes <- sort(unique(data[[par]][!is.na(data[[par]])]))
out <- lapply(groupes, function(g) {
df_g <- data[!is.na(data[[par]]) & data[[par]] == g, ]
res <- dplyr::bind_rows(lapply(vars, calc, df = df_g))
res[[par]] <- as.character(g)
dplyr::select(res, dplyr::all_of(par), dplyr::everything())
})
return(dplyr::bind_rows(out))
}
dplyr::bind_rows(lapply(vars, calc, df = data))
}
#' @keywords internal
.desc_pondere <- function(data, data_brute, vars, poids, par,
stats, est_svy) {
if (!est_svy) {
.verifier_package("survey", "tableau_descriptif (pondere)")
pw <- data_brute[[poids]]
data <- survey::svydesign(ids = ~1, weights = pw, data = data_brute)
est_svy <- TRUE
}
calc_svy <- function(svy, v) {
f <- as.formula(paste0("~", v))
n <- sum(!is.na(svy$variables[[v]]))
res <- tibble::tibble(variable = v, n = n)
if ("moyenne" %in% stats || "ic" %in% stats) {
m <- survey::svymean(f, svy, na.rm = TRUE)
moy <- as.numeric(m)
if ("moyenne" %in% stats) res$moyenne <- round(moy, 3)
if ("ic" %in% stats) {
se <- sqrt(as.numeric(attr(m, "var")))
res$ic_bas <- round(moy - 1.96 * se, 3)
res$ic_haut <- round(moy + 1.96 * se, 3)
}
}
if ("mediane" %in% stats) {
q <- survey::svyquantile(f, svy, quantiles = 0.5,
na.rm = TRUE)
res$mediane <- round(as.numeric(q[[1]])[1], 3)
}
if ("ecart_type" %in% stats) {
vr <- survey::svyvar(f, svy, na.rm = TRUE)
res$ecart_type <- round(sqrt(as.numeric(vr)), 3)
}
if ("min" %in% stats)
res$min <- round(min(svy$variables[[v]], na.rm = TRUE), 3)
if ("max" %in% stats)
res$max <- round(max(svy$variables[[v]], na.rm = TRUE), 3)
res
}
if (!is.null(par) && par %in% names(data_brute)) {
groupes <- sort(unique(data_brute[[par]][!is.na(data_brute[[par]])]))
out <- lapply(groupes, function(g) {
svy_g <- subset(data, data$variables[[par]] == g)
res <- dplyr::bind_rows(lapply(vars, calc_svy, svy = svy_g))
res[[par]] <- as.character(g)
dplyr::select(res, dplyr::all_of(par), dplyr::everything())
})
return(dplyr::bind_rows(out))
}
dplyr::bind_rows(lapply(vars, calc_svy, svy = data))
}
#' @keywords internal
.ft_descriptif <- function(tab, titre, source) {
n_rows <- nrow(tab)
idx_even <- if (n_rows >= 2) seq(2, n_rows, by = 2) else integer(0)
ft <- flextable::flextable(tab) |>
flextable::set_caption(
caption = if (!is.null(titre)) titre
else "Statistiques descriptives"
) |>
flextable::bg(bg = .INS_NAVY, part = "header") |>
flextable::color(color = "white", part = "header") |>
flextable::bold(part = "header") |>
flextable::bold(j = 1) |>
flextable::font(fontname = "Arial", part = "all") |>
flextable::fontsize(size = 10, part = "all") |>
flextable::fontsize(size = 11, part = "header") |>
flextable::autofit()
if (length(idx_even) > 0) {
ft <- flextable::bg(ft, i = idx_even, bg = .INS_GREY, part = "body")
}
if (!is.null(source)) {
ft <- flextable::add_footer_lines(ft,
values = paste0("Source : ", source))
ft <- flextable::color(ft, color = "#64748B", part = "footer")
ft <- flextable::fontsize(ft, size = 9, part = "footer")
}
ft
}
#' @keywords internal
.ft_croise <- function(tab, chi2_res, titre, ligne, colonne) {
ft <- flextable::flextable(tab) |>
flextable::set_caption(
caption = if (!is.null(titre)) titre
else paste0("Repartition de ", ligne,
" par ", colonne, " (%)")
) |>
flextable::bg(bg = .INS_NAVY, part = "header") |>
flextable::color(color = "white", part = "header") |>
flextable::bold(part = "header") |>
flextable::bold(j = 1) |>
flextable::font(fontname = "Arial", part = "all") |>
flextable::fontsize(size = 10, part = "all") |>
flextable::autofit()
if (!is.null(chi2_res)) {
note <- paste0(
"Chi-deux = ", round(chi2_res$statistic, 3),
", df = ", chi2_res$parameter,
", p = ", round(chi2_res$p.value, 4)
)
ft <- flextable::add_footer_lines(ft, values = note)
ft <- flextable::color(ft, color = "#64748B", part = "footer")
ft <- flextable::fontsize(ft, size = 9, part = "footer")
}
ft
}
#' @keywords internal
.excel_simple <- function(tab, chemin, titre, note, sheet = "Tableau") {
wb <- openxlsx2::wb_workbook()
wb <- openxlsx2::wb_add_worksheet(wb, sheet = sheet)
debut <- 1L
if (!is.null(titre)) {
wb <- openxlsx2::wb_add_data(wb, sheet = sheet,
x = titre, start_row = 1L,
start_col = 1L, col_names = FALSE)
debut <- 2L
}
wb <- openxlsx2::wb_add_data(wb, sheet = sheet,
x = tab, start_row = debut)
# Style header
wb <- openxlsx2::wb_add_fill(
wb, sheet = sheet,
color = openxlsx2::wb_color(.INS_NAVY),
dims = openxlsx2::wb_dims(rows = debut, cols = seq_len(ncol(tab)))
)
wb <- openxlsx2::wb_add_font(
wb, sheet = sheet,
bold = TRUE,
color = openxlsx2::wb_color("FFFFFF"),
dims = openxlsx2::wb_dims(rows = debut, cols = seq_len(ncol(tab)))
)
if (!is.null(note)) {
wb <- openxlsx2::wb_add_data(
wb, sheet = sheet,
x = note,
start_row = debut + nrow(tab) + 1L,
col_names = FALSE
)
}
openxlsx2::wb_save(wb, file = chemin, overwrite = TRUE)
message("Tableau exporte : ", chemin)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.