Estadística descriptiva con R
Análisis de la base PhiUSIIL Phishing URL (Website)
Ovaldino Palacio · Grupo:
Universidad de Panamá · Facultad de Informática · Licenciatura en Desarrollo de Aplicaciones
Asignatura: Estadística Descriptiva · Profesor: Abraham de Sedas
Herramienta: RStudio (R 4.6.1) · 15 de noviembre de 2026
UNIVERSIDAD DE PANAMÁ
FACULTAD DE INFORMÁTICA
Licenciatura en Desarrollo de Aplicaciones
Asignatura: Estadística Descriptiva
Parcial: análisis descriptivo de la base PhiUSIIL Phishing URL (Website)
Estudiante: Ovaldino Palacio
Cédula:
Grupo:
Profesor: Abraham de Sedas
Herramienta utilizada: RStudio (lenguaje R, versión 4.6.1)
15 de noviembre de 2026
El phishing es una forma de fraude en la que una página web imita a un sitio de confianza para obtener datos de las personas. Este informe describe la base pública PhiUSIIL Phishing URL (Website), que reúne 235,795 direcciones URL clasificadas como legítimas o phishing (Prasad y Chandra, 2024a). El trabajo organiza, resume y representa un grupo de variables de esa base mediante tablas de frecuencia, medidas descriptivas y gráficos. El análisis es únicamente descriptivo: no predice, no prueba hipótesis y no establece causas.
Describir la distribución, el comportamiento y la variabilidad de variables seleccionadas de la base PhiUSIIL mediante tablas, medidas y gráficos elaborados en RStudio.
Verificar la estructura, los tipos de variables, la integridad y el tamaño de la base.
Construir tablas de frecuencia simples y agrupadas, con frecuencias absolutas, relativas y acumuladas cuando corresponda.
Calcular medidas de tendencia central, posición y dispersión según el tipo de variable.
Seleccionar gráficos adecuados e interpretar los resultados en el contexto de la ciberseguridad.
Se trabajó con la base completa, sin muestreo y sin eliminar
registros. El archivo PhiUSIIL_Phishing_URL_Dataset.csv se importó en
RStudio, con el lenguaje R (R Core Team, 2026), mediante la función
read_csv() del paquete readr. Las tablas y las medidas se
calcularon con dplyr y tidyr, y los gráficos se elaboraron con ggplot2
(Wickham, 2016). El mismo procedimiento está en el script
Parcial_Estadistica_Completo.R, que regenera las tablas,
las figuras y este informe a partir del CSV original.
Las convenciones de cálculo fueron las siguientes. Los cuartiles se
obtuvieron con la función quantile() en su método por
defecto (tipo 7). La varianza y la desviación estándar se calcularon con
var() y sd(), que usan el divisor n − 1; con
235,795 registros, la diferencia con el divisor n es despreciable. El
coeficiente de variación (CV) se calculó como 100 × desviación estándar
/ media, solo para variables en escala de razón. Se consideró atípico
todo valor fuera del intervalo [Q1 − 1.5 RIC, Q3 + 1.5 RIC]. Las
frecuencias relativas se redondearon a tres decimales y los porcentajes
a dos. En las comparaciones por clase, los porcentajes se calcularon
dentro de cada clase, y así se indica en cada tabla.
La variable URL se conservó solo como identificador y la columna FILENAME se excluyó del análisis, como permite la ficha de UCI. La variable label se trató como cualitativa: sus códigos 1 y 0 son etiquetas y no cantidades.
paquetes <- c("readr", "dplyr", "tidyr", "ggplot2", "scales", "stringr",
"knitr", "rmarkdown")
faltantes <- paquetes[!vapply(paquetes, requireNamespace, logical(1),
quietly = TRUE)]
if (length(faltantes) > 0) {
message("Instalando paquetes faltantes: ", paste(faltantes, collapse = ", "))
install.packages(faltantes)
}
suppressPackageStartupMessages({
library(readr)
library(dplyr)
library(tidyr)
library(ggplot2)
library(scales)
library(stringr)
})
message("[0] Paquetes cargados correctamente.")# Se busca la carpeta raíz del proyecto a partir del directorio de trabajo.
archivo_proyecto <- "Parcial_Estadistica_Descriptiva.Rproj"
if (file.exists(archivo_proyecto)) {
raiz <- "."
} else if (file.exists(file.path("..", archivo_proyecto))) {
raiz <- ".." # caso: se ejecuta desde /scripts o /informe
} else {
raiz <- "." # caso: se ejecuta fuera del proyecto
}
dir_datos <- file.path(raiz, "datos")
dir_tablas <- file.path(raiz, "tablas")
dir_graficos <- file.path(raiz, "graficos")
dir_informe <- file.path(raiz, "informe")
for (carpeta in c(dir_datos, dir_tablas, dir_graficos, dir_informe)) {
dir.create(carpeta, showWarnings = FALSE, recursive = TRUE)
}nombre_csv <- "PhiUSIIL_Phishing_URL_Dataset.csv"
candidatos <- c(file.path(dir_datos, nombre_csv), file.path(raiz, nombre_csv))
ruta_csv <- candidatos[file.exists(candidatos)][1]
if (is.na(ruta_csv)) {
if (interactive()) {
message("No se encontró el CSV en la carpeta /datos. Selecciónelo manualmente.")
ruta_csv <- file.choose()
} else {
stop("No se encontró '", nombre_csv, "'. Cópielo en la carpeta 'datos' ",
"del proyecto y vuelva a ejecutar.")
}
}
datos <- tryCatch(
read_csv(ruta_csv, show_col_types = FALSE, progress = FALSE),
error = function(e) stop("Error al importar el CSV: ", conditionMessage(e))
)
problemas_importacion <- nrow(problems(datos))
message("[2] Base importada: ", format(nrow(datos), big.mark = ","), " filas y ",
ncol(datos), " columnas.")
dim(datos) # filas y columnas#> [1] 235795 56
#> [1] "FILENAME" "URL" "URLLength"
#> [4] "Domain" "DomainLength" "IsDomainIP"
#> [7] "TLD" "URLSimilarityIndex" "CharContinuationRate"
#> [10] "TLDLegitimateProb" "URLCharProb" "TLDLength"
#> [13] "NoOfSubDomain" "HasObfuscation" "NoOfObfuscatedChar"
#> [16] "ObfuscationRatio" "NoOfLettersInURL" "LetterRatioInURL"
#> [19] "NoOfDegitsInURL" "DegitRatioInURL" "NoOfEqualsInURL"
#> [22] "NoOfQMarkInURL" "NoOfAmpersandInURL" "NoOfOtherSpecialCharsInURL"
#> [25] "SpacialCharRatioInURL" "IsHTTPS" "LineOfCode"
#> [28] "LargestLineLength" "HasTitle" "Title"
#> [31] "DomainTitleMatchScore" "URLTitleMatchScore" "HasFavicon"
#> [34] "Robots" "IsResponsive" "NoOfURLRedirect"
#> [37] "NoOfSelfRedirect" "HasDescription" "NoOfPopup"
#> [40] "NoOfiFrame" "HasExternalFormSubmit" "HasSocialNet"
#> [43] "HasSubmitButton" "HasHiddenFields" "HasPasswordField"
#> [46] "Bank" "Pay" "Crypto"
#> [49] "HasCopyrightInfo" "NoOfImage" "NoOfCSS"
#> [52] "NoOfJS" "NoOfSelfRef" "NoOfEmptyRef"
#> [55] "NoOfExternalRef" "label"
n_total <- nrow(datos)
n_columnas <- ncol(datos)
# Tipos de dato de las 56 columnas
tipo_columna <- vapply(datos, function(x) {
if (is.character(x)) "Texto"
else if (all(x == round(x))) "Numérica entera"
else "Numérica decimal"
}, character(1))
tabla_tipos <- as.data.frame(table(tipo_columna), stringsAsFactors = FALSE)
names(tabla_tipos) <- c("Tipo de dato", "Columnas")
# Columnas binarias (solo toman los valores 0 y 1)
columnas_binarias <- names(datos)[vapply(datos, function(x)
is.numeric(x) && all(x %in% c(0, 1)), logical(1))]
tabla_tipos#> Tipo de dato Columnas
#> 1 Numérica decimal 10
#> 2 Numérica entera 41
#> 3 Texto 5
La fuente es el UCI Machine Learning Repository. El archivo importado tiene 235,795 filas y 56 columnas: la columna FILENAME, las 54 características que reporta UCI y la variable label. La unidad de observación es una dirección URL junto con la página web a la que corresponde. La variable label indica la clasificación: 1 = URL legítima y 0 = URL phishing. De las 56 columnas, 41 son numéricas enteras, 10 son numéricas decimales y 5 son de texto; 20 de las numéricas solo toman los valores 0 y 1.
# Variables exigidas por el profesor y variables adicionales seleccionadas.
vars_obligatorias <- c("label", "URLLength", "DomainLength",
"URLSimilarityIndex", "TLDLegitimateProb", "WebTraffic")
vars_adicionales <- c("URL", "Domain", "TLD", "IsHTTPS", "IsDomainIP",
"HasTitle", "NoOfSubDomain", "NoOfLettersInURL")
# Búsqueda sin distinguir mayúsculas, por si el archivo cambia el formato.
buscar_columna <- function(nombre, columnas) {
coincide <- columnas[tolower(columnas) == tolower(nombre)]
if (length(coincide) == 0) NA_character_ else coincide[1]
}
equivalencias <- tibble(
variable_solicitada = c(vars_obligatorias, vars_adicionales),
columna_en_csv = vapply(c(vars_obligatorias, vars_adicionales),
buscar_columna, character(1), columnas = names(datos))
)
vars_ausentes <- equivalencias$variable_solicitada[is.na(equivalencias$columna_en_csv)]
# WebTraffic figura en las instrucciones, pero NO existe en el CSV de UCI.
# Se documenta la ausencia; no se inventa ni se sustituye por otra variable.
existe_webtraffic <- !("WebTraffic" %in% vars_ausentes)
columnas_similares_trafico <- grep("traffic|rank|visit|popular", names(datos),
ignore.case = TRUE, value = TRUE)
if (!existe_webtraffic) {
message("[3] AVISO: la variable 'WebTraffic' no existe en el archivo. ",
"Se documenta en la auditoría y se continúa con las demás.")
}
imprescindibles <- setdiff(c(vars_obligatorias, vars_adicionales), "WebTraffic")
if (any(imprescindibles %in% vars_ausentes)) {
stop("Faltan columnas imprescindibles: ",
paste(intersect(imprescindibles, vars_ausentes), collapse = ", "))
}
print(equivalencias, n = Inf)#> # A tibble: 14 × 2
#> variable_solicitada columna_en_csv
#> <chr> <chr>
#> 1 label label
#> 2 URLLength URLLength
#> 3 DomainLength DomainLength
#> 4 URLSimilarityIndex URLSimilarityIndex
#> 5 TLDLegitimateProb TLDLegitimateProb
#> 6 WebTraffic <NA>
#> 7 URL URL
#> 8 Domain Domain
#> 9 TLD TLD
#> 10 IsHTTPS IsHTTPS
#> 11 IsDomainIP IsDomainIP
#> 12 HasTitle HasTitle
#> 13 NoOfSubDomain NoOfSubDomain
#> 14 NoOfLettersInURL NoOfLettersInURL
#> character(0)
Las instrucciones piden incluir obligatoriamente la variable WebTraffic. Ese requisito no se puede cumplir con el archivo oficial: la columna no existe en el CSV descargado de UCI. Se revisaron los 56 nombres de columna, sin distinguir mayúsculas, y ninguno coincide con WebTraffic ni contiene un término equivalente (traffic, rank, visit o popular). La tabla de variables de la ficha de UCI tiene 56 entradas, el mismo número de columnas que el archivo. Por lo tanto no hay equivalencia que documentar y la variable no se pudo analizar. No se sustituyó por otra ni se simularon valores. Las otras cuatro variables cuantitativas obligatorias (URLLength, DomainLength, URLSimilarityIndex y TLDLegitimateProb) sí están en el archivo con el mismo nombre.
# Diccionario de las variables seleccionadas
# Nota: la ficha de UCI no trae descripciones; se redactaron a partir del
# nombre de cada variable y del artículo de Prasad y Chandra (2024).
diccionario <- tribble(
~Variable, ~Descripción, ~Tipo, ~Escala, ~`Valores o unidad`,
"URL", "Dirección web completa; se usa solo como identificador", "Cualitativa", "Nominal", "Texto",
"label", "Clasificación de la URL", "Cualitativa binaria", "Nominal", "1 = legítima; 0 = phishing",
"IsHTTPS", "Indica si la URL usa el protocolo HTTPS", "Cualitativa binaria", "Nominal", "1 = sí; 0 = no",
"IsDomainIP", "Indica si el dominio es una dirección IP", "Cualitativa binaria", "Nominal", "1 = sí; 0 = no",
"HasTitle", "Indica si la página web tiene título", "Cualitativa binaria", "Nominal", "1 = sí; 0 = no",
"TLD", "Dominio de nivel superior (com, org, net, ...)", "Cualitativa politómica", "Nominal",
paste(n_distinct(datos$TLD), "categorías"),
"URLLength", "Longitud de la URL", "Cuantitativa discreta", "Razón", "Caracteres",
"DomainLength", "Longitud del dominio", "Cuantitativa discreta", "Razón", "Caracteres",
"URLSimilarityIndex", "Índice de similitud de la URL calculado por los autores", "Cuantitativa continua", "Razón", "Puntos, de 0 a 100",
"TLDLegitimateProb", "Probabilidad de legitimidad asociada al TLD, derivada por los autores", "Cuantitativa continua", "Razón", "Proporción, de 0 a 1",
"NoOfLettersInURL", "Cantidad de letras en la URL", "Cuantitativa discreta", "Razón", "Letras",
"NoOfSubDomain", "Cantidad de subdominios de la URL", "Cuantitativa discreta", "Razón", "Subdominios"
)Se seleccionaron 11 variables de análisis, además de URL como identificador (Tabla 1): la variable de clase, cuatro cualitativas y seis cuantitativas. Así se cumple el mínimo de ocho variables, con al menos tres categóricas o binarias y al menos cuatro cuantitativas.
Tabla 1
Diccionario de las variables seleccionadas
| Variable | Descripción | Tipo | Escala | Valores o unidad |
|---|---|---|---|---|
| URL | Dirección web completa; se usa solo como identificador | Cualitativa | Nominal | Texto |
| label | Clasificación de la URL | Cualitativa binaria | Nominal | 1 = legítima; 0 = phishing |
| IsHTTPS | Indica si la URL usa el protocolo HTTPS | Cualitativa binaria | Nominal | 1 = sí; 0 = no |
| IsDomainIP | Indica si el dominio es una dirección IP | Cualitativa binaria | Nominal | 1 = sí; 0 = no |
| HasTitle | Indica si la página web tiene título | Cualitativa binaria | Nominal | 1 = sí; 0 = no |
| TLD | Dominio de nivel superior (com, org, net, …) | Cualitativa politómica | Nominal | 695 categorías |
| URLLength | Longitud de la URL | Cuantitativa discreta | Razón | Caracteres |
| DomainLength | Longitud del dominio | Cuantitativa discreta | Razón | Caracteres |
| URLSimilarityIndex | Índice de similitud de la URL calculado por los autores | Cuantitativa continua | Razón | Puntos, de 0 a 100 |
| TLDLegitimateProb | Probabilidad de legitimidad asociada al TLD, derivada por los autores | Cuantitativa continua | Razón | Proporción, de 0 a 1 |
| NoOfLettersInURL | Cantidad de letras en la URL | Cuantitativa discreta | Razón | Letras |
| NoOfSubDomain | Cantidad de subdominios de la URL | Cuantitativa discreta | Razón | Subdominios |
Nota. La ficha de UCI no incluye descripciones; se redactaron a partir del nombre de cada variable y del artículo de los autores (Prasad y Chandra, 2024b). Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# Calidad: faltantes, duplicados, valores imposibles e inconsistencias
columnas_numericas <- names(datos)[vapply(datos, is.numeric, logical(1))]
url_repetida <- duplicated(datos$URL) | duplicated(datos$URL, fromLast = TRUE)
diferencia_longitud <- datos$URLLength - nchar(datos$URL)
calidad <- tibble(
Verificación = c(
"Filas importadas",
"Columnas importadas",
"Problemas de lectura reportados por readr",
"Valores faltantes (NA) en toda la base",
"Filas duplicadas exactas (sin FILENAME)",
"URL repetidas (registros adicionales con la misma URL)",
"Registros involucrados en URL repetidas",
"Valores negativos en columnas numéricas",
"label con valores distintos de 0 y 1",
"URLSimilarityIndex fuera del rango 0-100",
"TLDLegitimateProb fuera del rango 0-1",
"URLLength menor o igual que 0",
"DomainLength mayor que URLLength",
"DomainLength distinto del número de caracteres de Domain",
"URLLength igual al número de caracteres de URL",
"URLLength un carácter menor que el texto de URL",
"URLLength con otra diferencia respecto al texto de URL",
"URL legítimas con URLLength = DomainLength + 7"
),
Resultado = c(
n_total,
n_columnas,
problemas_importacion,
sum(is.na(datos)),
sum(duplicated(select(datos, -FILENAME))),
sum(duplicated(datos$URL)),
sum(url_repetida),
sum(vapply(datos[columnas_numericas], function(x) sum(x < 0), numeric(1))),
sum(!datos$label %in% c(0, 1)),
sum(datos$URLSimilarityIndex < 0 | datos$URLSimilarityIndex > 100),
sum(datos$TLDLegitimateProb < 0 | datos$TLDLegitimateProb > 1),
sum(datos$URLLength <= 0),
sum(datos$DomainLength > datos$URLLength),
sum(datos$DomainLength != nchar(datos$Domain)),
sum(diferencia_longitud == 0),
sum(diferencia_longitud == -1),
sum(!diferencia_longitud %in% c(0, -1)),
sum(datos$label == 1 & datos$URLLength == datos$DomainLength + 7)
)
)
# Clase de los registros con URL repetida (para describirlos, no para borrarlos)
url_repetidas_por_clase <- table(datos$label[url_repetida])# Contraste con la ficha de UCI
contraste_uci <- tibble(
Aspecto = c("Registros", "URL legítimas (label = 1)",
"URL phishing (label = 0)", "Valores faltantes"),
`Ficha UCI` = c(235795, 134850, 100945, 0),
`CSV importado` = c(n_total, sum(datos$label == 1), sum(datos$label == 0),
sum(is.na(datos)))
) |>
mutate(Coincide = ifelse(`Ficha UCI` == `CSV importado`, "Sí", "No"))
contraste_uci#> # A tibble: 4 × 4
#> Aspecto `Ficha UCI` `CSV importado` Coincide
#> <chr> <dbl> <int> <chr>
#> 1 Registros 235795 235795 Sí
#> 2 URL legítimas (label = 1) 134850 134850 Sí
#> 3 URL phishing (label = 0) 100945 100945 Sí
#> 4 Valores faltantes 0 0 Sí
Los totales coinciden con la ficha de UCI: 134,850 URL legítimas, 100,945 URL phishing y 0 valores faltantes. Se encontró una diferencia de tipo: UCI lista URLSimilarityIndex como variable entera, pero en el archivo toma valores decimales entre 0.156 y 100, por lo que se trató como cuantitativa continua.
La revisión de calidad se resume en la Tabla 2. No hay valores faltantes, filas duplicadas exactas, valores negativos ni valores fuera de los rangos posibles. Hay 425 URL que aparecen más de una vez (850 registros involucrados, todos de la clase phishing). Esos registros no son duplicados exactos, porque difieren en otras columnas, así que se conservaron. También se observó una inconsistencia de formato: en 187,149 registros (79.37 %) el valor de URLLength es un carácter menor que el texto guardado en la columna URL. Se usó URLLength tal como viene en la base y la diferencia se reporta como limitación. No se eliminó ni se modificó ningún registro.
Tabla 2
Revisión de la calidad de los datos
| Verificación | Resultado |
|---|---|
| Filas importadas | 235,795 |
| Columnas importadas | 56 |
| Problemas de lectura reportados por readr | 0 |
| Valores faltantes (NA) en toda la base | 0 |
| Filas duplicadas exactas (sin FILENAME) | 0 |
| URL repetidas (registros adicionales con la misma URL) | 425 |
| Registros involucrados en URL repetidas | 850 |
| Valores negativos en columnas numéricas | 0 |
| label con valores distintos de 0 y 1 | 0 |
| URLSimilarityIndex fuera del rango 0-100 | 0 |
| TLDLegitimateProb fuera del rango 0-1 | 0 |
| URLLength menor o igual que 0 | 0 |
| DomainLength mayor que URLLength | 0 |
| DomainLength distinto del número de caracteres de Domain | 0 |
| URLLength igual al número de caracteres de URL | 48,644 |
| URLLength un carácter menor que el texto de URL | 187,149 |
| URLLength con otra diferencia respecto al texto de URL | 2 |
| URL legítimas con URLLength = DomainLength + 7 | 134,850 |
Nota. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# Base de análisis: se conserva `datos` intacto y se agregan etiquetas
datos_analisis <- datos |>
mutate(
clase = factor(label, levels = c(1, 0),
labels = c("Legítima", "Phishing")),
https = factor(IsHTTPS, levels = c(1, 0), labels = c("Sí", "No")),
dominio_ip = factor(IsDomainIP, levels = c(1, 0), labels = c("Sí", "No")),
tiene_titulo = factor(HasTitle, levels = c(1, 0), labels = c("Sí", "No"))
)
stopifnot(nrow(datos_analisis) == n_total) # no se eliminó ningún registro
message("[4] Auditoría completada: ", sum(is.na(datos)), " faltantes, ",
calidad$Resultado[5], " filas duplicadas exactas.")En todas las tablas, fi es la frecuencia absoluta, hi la frecuencia relativa, Fi la frecuencia absoluta acumulada y Hi la frecuencia relativa acumulada. Las acumuladas se presentan solo cuando la variable tiene un orden. Todas las tablas suman 235,795 registros.
# Notación: fi = frecuencia absoluta; hi = frecuencia relativa;
# Fi = absoluta acumulada; Hi = relativa acumulada.
# Tabla Tipo I: una fila por categoría o valor observado.
# Las acumuladas solo se agregan si la variable tiene orden (acumular = TRUE).
tabla_tipo1 <- function(x, nombre, acumular = FALSE) {
t <- as.data.frame(table(x), stringsAsFactors = FALSE)
names(t) <- c(nombre, "fi")
t$hi <- t$fi / sum(t$fi)
t$`%` <- 100 * t$hi
if (acumular) {
t$Fi <- cumsum(t$fi)
t$Hi <- cumsum(t$hi)
}
t
}
# Tabla con clases (Tipo II y Tipo III). `cortes` son los límites inferiores
# de cada clase más el límite superior de la última; clases [Li, Ls).
tabla_clases <- function(x, cortes, etiquetas, limite_superior_final = NULL) {
clase <- cut(x, breaks = cortes, right = FALSE, labels = etiquetas)
fi <- as.vector(table(clase))
li <- head(cortes, -1)
ls <- tail(cortes, -1)
if (!is.null(limite_superior_final)) ls[length(ls)] <- limite_superior_final
tibble(
Clase = etiquetas, Li = li, Ls = ls,
Amplitud = ls - li, `Marca de clase` = (li + ls) / 2,
fi = fi, hi = fi / sum(fi), `%` = 100 * fi / sum(fi),
Fi = cumsum(fi), Hi = cumsum(fi) / sum(fi)
)
}# --- Tabla 3 (Tipo I): label ---------------------------------------------------
tabla_label <- tabla_tipo1(datos_analisis$clase, "Clasificación")La Tabla 3 presenta la variable de clase. La base tiene 134,850 URL legítimas (57.19 %) y 100,945 URL phishing (42.81 %). La variable es nominal, por lo que no se acumula.
Tabla 3
Tabla Tipo I: distribución de las URL según su clasificación (label)
| Clasificación | fi | hi | % |
|---|---|---|---|
| Legítima | 134,850 | 0.572 | 57.19 |
| Phishing | 100,945 | 0.428 | 42.81 |
| Total | 235,795 | 1.000 | 100.00 |
Nota. label = 1 corresponde a legítima y label = 0 a phishing. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# --- Tabla 4 (Tipo I): IsHTTPS -------------------------------------------------
tabla_https <- tabla_tipo1(datos_analisis$https, "Usa HTTPS")La Tabla 4 muestra que 184,539 URL (78.26 %) usan el protocolo HTTPS y 51,256 (21.74 %) no lo usan.
Tabla 4
Tabla Tipo I: distribución de las URL según el uso del protocolo HTTPS (IsHTTPS)
| Usa HTTPS | fi | hi | % |
|---|---|---|---|
| Sí | 184,539 | 0.783 | 78.26 |
| No | 51,256 | 0.217 | 21.74 |
| Total | 235,795 | 1.000 | 100.00 |
Nota. Sí corresponde a IsHTTPS = 1 y No a IsHTTPS = 0. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# --- Tabla 5 (Tipo I): TLD, 10 más frecuentes y el resto agrupado ----------------
# TLD tiene cientos de categorías; el profesor pide agrupar las menos frecuentes.
n_tld_top <- 10
conteo_tld <- sort(table(datos$TLD), decreasing = TRUE)
tld_top <- names(conteo_tld)[seq_len(n_tld_top)]
n_tld_otros <- length(conteo_tld) - n_tld_top
etiqueta_otros <- paste0("Otros (", n_tld_otros, " TLD)")
tabla_tld <- tibble(
TLD = c(paste0(".", tld_top), etiqueta_otros),
fi = c(as.vector(conteo_tld[tld_top]), sum(conteo_tld[-seq_len(n_tld_top)]))
) |>
mutate(hi = fi / sum(fi), `%` = 100 * hi)La variable TLD tiene 695 categorías distintas. Como indica el profesor para variables con demasiadas categorías, la Tabla 5 presenta las 10 más frecuentes y agrupa las 685 restantes en la categoría “Otros”. El TLD más frecuente es .com, con 112,554 URL (47.73 %).
Tabla 5
Tabla Tipo I: distribución de las URL según el dominio de nivel superior (TLD)
| TLD | fi | hi | % |
|---|---|---|---|
| .com | 112,554 | 0.477 | 47.73 |
| .org | 18,793 | 0.080 | 7.97 |
| .net | 7,097 | 0.030 | 3.01 |
| .app | 6,508 | 0.028 | 2.76 |
| .uk | 6,395 | 0.027 | 2.71 |
| .co | 5,422 | 0.023 | 2.30 |
| .io | 4,201 | 0.018 | 1.78 |
| .de | 3,996 | 0.017 | 1.69 |
| .ru | 3,875 | 0.016 | 1.64 |
| .au | 2,979 | 0.013 | 1.26 |
| Otros (685 TLD) | 63,975 | 0.271 | 27.13 |
| Total | 235,795 | 1.000 | 100.00 |
Nota. Las categorías menos frecuentes se agruparon en Otros. Variable nominal: no se acumula. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# --- Tabla 6 (Tipo I con acumuladas): NoOfSubDomain ------------------------------
# Variable discreta con pocos valores y con orden: admite Fi y Hi.
tabla_subdominios <- tabla_tipo1(datos$NoOfSubDomain, "Subdominios",
acumular = TRUE)La Tabla 6 presenta la cantidad de subdominios. Es una variable discreta con pocos valores y con orden, por lo que admite frecuencias acumuladas. El valor más frecuente es 1 subdominio, con 178,406 URL (75.66 %); el 97.01 % de las URL tiene 2 subdominios o menos.
Tabla 6
Tabla Tipo I con frecuencias acumuladas: cantidad de subdominios de la URL (NoOfSubDomain)
| Subdominios | fi | hi | % | Fi | Hi |
|---|---|---|---|---|---|
| 0 | 13,983 | 0.059 | 5.93 | 13,983 | 0.059 |
| 1 | 178,406 | 0.757 | 75.66 | 192,389 | 0.816 |
| 2 | 36,353 | 0.154 | 15.42 | 228,742 | 0.970 |
| 3 | 5,119 | 0.022 | 2.17 | 233,861 | 0.992 |
| 4 | 1,563 | 0.007 | 0.66 | 235,424 | 0.998 |
| 5 | 336 | 0.001 | 0.14 | 235,760 | 1.000 |
| 6 | 21 | 0.000 | 0.01 | 235,781 | 1.000 |
| 7 | 5 | 0.000 | 0.00 | 235,786 | 1.000 |
| 8 | 4 | 0.000 | 0.00 | 235,790 | 1.000 |
| 10 | 5 | 0.000 | 0.00 | 235,795 | 1.000 |
Nota. En la base no hay URL con 9 subdominios. Un valor de 0.000 indica una proporción menor que 0.0005. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# --- Tabla 7 (Tipo II): NoOfLettersInURL en clases discretas ---------------------
# Clases de 10 valores (0-9, 10-19, ...) y una clase final que reúne la cola.
max_letras <- max(datos$NoOfLettersInURL)
cortes_letras <- c(seq(0, 60, by = 10), max_letras + 1)
etiquetas_letras <- c(paste(seq(0, 50, 10), "-", seq(9, 59, 10)),
paste("60 -", max_letras))
tabla_letras <- tabla_clases(datos$NoOfLettersInURL, cortes_letras,
etiquetas_letras) |>
mutate(Ls = Ls - 1, # límites discretos: 0-9, 10-19...
Amplitud = Ls - Li + 1, # cantidad de valores por clase
`Marca de clase` = (Li + Ls) / 2)La cantidad de letras de la URL es una variable discreta que toma 421 valores distintos, entre 0 y 5,191. Se agrupó en clases de 10 valores consecutivos (0 a 9, 10 a 19, y así sucesivamente). Como los valores de 60 o más son poco frecuentes y muy dispersos, se reunieron en una clase final. La clase con mayor frecuencia es la de 10 - 19 letras, con 125,614 URL (53.27 %) (Tabla 7).
Tabla 7
Tabla Tipo II: cantidad de letras de la URL agrupada en clases discretas (NoOfLettersInURL)
| Clase (letras) | Li | Ls | Amplitud | fi | hi | % | Fi | Hi |
|---|---|---|---|---|---|---|---|---|
| 0 - 9 | 0 | 9 | 10 | 46,763 | 0.198 | 19.83 | 46,763 | 0.198 |
| 10 - 19 | 10 | 19 | 10 | 125,614 | 0.533 | 53.27 | 172,377 | 0.731 |
| 20 - 29 | 20 | 29 | 10 | 35,366 | 0.150 | 15.00 | 207,743 | 0.881 |
| 30 - 39 | 30 | 39 | 10 | 11,345 | 0.048 | 4.81 | 219,088 | 0.929 |
| 40 - 49 | 40 | 49 | 10 | 4,708 | 0.020 | 2.00 | 223,796 | 0.949 |
| 50 - 59 | 50 | 59 | 10 | 3,413 | 0.014 | 1.45 | 227,209 | 0.964 |
| 60 - 5191 | 60 | 5,191 | 5,132 | 8,586 | 0.036 | 3.64 | 235,795 | 1.000 |
Nota. Li y Ls son el menor y el mayor valor de cada clase. La amplitud es la cantidad de valores enteros de la clase; la última clase reúne la cola de la distribución. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# --- Tabla 8 (Tipo III): URLLength ----------------------------------------------
# Regla de Sturges como referencia: k = 1 + 3.322 log10(n)
k_sturges <- ceiling(1 + 3.322 * log10(n_total))
rango_url <- max(datos$URLLength) - min(datos$URLLength)
amplitud_sturges_url <- ceiling(rango_url / k_sturges)
# Con esa amplitud casi toda la base cae en la primera clase (se calcula abajo),
# por lo que se usan clases de 10 caracteres y una clase final para la cola.
pct_primera_clase_sturges <- 100 * mean(
datos$URLLength < min(datos$URLLength) + amplitud_sturges_url)
max_url <- max(datos$URLLength)
cortes_url <- c(seq(10, 100, by = 10), max_url + 1)
etiquetas_url <- c(paste0("[", seq(10, 90, 10), ", ", seq(20, 100, 10), ")"),
paste0("[100, ", max_url, "]"))
tabla_urllength <- tabla_clases(datos$URLLength, cortes_url, etiquetas_url,
limite_superior_final = max_url)
c(k_sturges = k_sturges, rango = rango_url, amplitud_sturges = amplitud_sturges_url,
pct_primera_clase = round(pct_primera_clase_sturges, 2))#> k_sturges rango amplitud_sturges pct_primera_clase
#> 19.00 6084.00 321.00 99.79
Para la longitud de la URL se tomó como referencia la regla de Sturges, k = 1 + 3.322 log(n), que da 19 clases. El rango es de 6,084 caracteres, así que la amplitud sería de 321 caracteres y el 99.79 % de las URL quedaría en la primera clase, lo que no describe la distribución. Por esa razón se usaron clases de 10 caracteres entre 10 y 100, donde se concentra la mayor parte de los datos, y una clase final de 100 caracteres o más. Las clases son cerradas a la izquierda y abiertas a la derecha, de modo que no se superponen. La clase modal es [20, 30), con 129,295 URL (54.83 %) (Tabla 8).
Tabla 8
Tabla Tipo III: longitud de la URL en intervalos (URLLength, en caracteres)
| Clase | Li | Ls | Amplitud | Marca de clase | fi | hi | Fi | Hi |
|---|---|---|---|---|---|---|---|---|
| [10, 20) | 10 | 20 | 10 | 15.0 | 12,961 | 0.055 | 12,961 | 0.055 |
| [20, 30) | 20 | 30 | 10 | 25.0 | 129,295 | 0.548 | 142,256 | 0.603 |
| [30, 40) | 30 | 40 | 10 | 35.0 | 54,560 | 0.231 | 196,816 | 0.835 |
| [40, 50) | 40 | 50 | 10 | 45.0 | 15,272 | 0.065 | 212,088 | 0.899 |
| [50, 60) | 50 | 60 | 10 | 55.0 | 7,460 | 0.032 | 219,548 | 0.931 |
| [60, 70) | 60 | 70 | 10 | 65.0 | 3,342 | 0.014 | 222,890 | 0.945 |
| [70, 80) | 70 | 80 | 10 | 75.0 | 2,605 | 0.011 | 225,495 | 0.956 |
| [80, 90) | 80 | 90 | 10 | 85.0 | 3,092 | 0.013 | 228,587 | 0.969 |
| [90, 100) | 90 | 100 | 10 | 95.0 | 2,232 | 0.009 | 230,819 | 0.979 |
| [100, 6097] | 100 | 6,097 | 5,997 | 3,098.5 | 4,976 | 0.021 | 235,795 | 1.000 |
Nota. La última clase es cerrada en ambos extremos y llega hasta el valor máximo observado (6,097 caracteres). Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# --- Tabla 9 (Tipo III): DomainLength con la regla de Sturges --------------------
min_dom <- min(datos$DomainLength)
max_dom <- max(datos$DomainLength)
rango_dom <- max_dom - min_dom
amplitud_dom <- ceiling(rango_dom / k_sturges)
# Con la amplitud redondeada hacia arriba pueden sobrar clases vacías al final;
# se conservan solo las necesarias para cubrir hasta el valor máximo.
n_clases_dom <- ceiling((rango_dom + 1) / amplitud_dom)
cortes_dom <- min_dom + amplitud_dom * (0:n_clases_dom)
etiquetas_dom <- paste0("[", head(cortes_dom, -1), ", ", tail(cortes_dom, -1), ")")
tabla_domainlength <- tabla_clases(datos$DomainLength, cortes_dom, etiquetas_dom)
c(rango = rango_dom, amplitud = amplitud_dom, clases = n_clases_dom)#> rango amplitud clases
#> 106 6 18
Para la longitud del dominio sí se aplicó la regla de Sturges. Con k = 19 y un rango de 106 caracteres (de 4 a 110), la amplitud es 106 / 19 = 5.58, que se redondeó hacia arriba a 6 caracteres. Con esa amplitud bastan 18 clases para cubrir todos los valores. La clase modal es [16, 22), con 92,217 URL (39.11 %) (Tabla 9).
Tabla 9
Tabla Tipo III: longitud del dominio en intervalos (DomainLength, en caracteres)
| Clase | Li | Ls | Amplitud | Marca de clase | fi | hi | Fi | Hi |
|---|---|---|---|---|---|---|---|---|
| [4, 10) | 4 | 10 | 6 | 7 | 3,529 | 0.015 | 3,529 | 0.015 |
| [10, 16) | 10 | 16 | 6 | 13 | 48,773 | 0.207 | 52,302 | 0.222 |
| [16, 22) | 16 | 22 | 6 | 19 | 92,217 | 0.391 | 144,519 | 0.613 |
| [22, 28) | 22 | 28 | 6 | 25 | 53,932 | 0.229 | 198,451 | 0.842 |
| [28, 34) | 28 | 34 | 6 | 31 | 19,245 | 0.082 | 217,696 | 0.923 |
| [34, 40) | 34 | 40 | 6 | 37 | 8,017 | 0.034 | 225,713 | 0.957 |
| [40, 46) | 40 | 46 | 6 | 43 | 4,442 | 0.019 | 230,155 | 0.976 |
| [46, 52) | 46 | 52 | 6 | 49 | 2,378 | 0.010 | 232,533 | 0.986 |
| [52, 58) | 52 | 58 | 6 | 55 | 914 | 0.004 | 233,447 | 0.990 |
| [58, 64) | 58 | 64 | 6 | 61 | 259 | 0.001 | 233,706 | 0.991 |
| [64, 70) | 64 | 70 | 6 | 67 | 124 | 0.001 | 233,830 | 0.992 |
| [70, 76) | 70 | 76 | 6 | 73 | 1,249 | 0.005 | 235,079 | 0.997 |
| [76, 82) | 76 | 82 | 6 | 79 | 602 | 0.003 | 235,681 | 1.000 |
| [82, 88) | 82 | 88 | 6 | 85 | 65 | 0.000 | 235,746 | 1.000 |
| [88, 94) | 88 | 94 | 6 | 91 | 33 | 0.000 | 235,779 | 1.000 |
| [94, 100) | 94 | 100 | 6 | 97 | 6 | 0.000 | 235,785 | 1.000 |
| [100, 106) | 100 | 106 | 6 | 103 | 9 | 0.000 | 235,794 | 1.000 |
| [106, 112) | 106 | 112 | 6 | 109 | 1 | 0.000 | 235,795 | 1.000 |
Nota. Clases cerradas a la izquierda y abiertas a la derecha. Un valor de 0.000 indica una proporción menor que 0.0005. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# --- Comprobaciones de coherencia -------------------------------------------------
tablas_frecuencia <- list(label = tabla_label, https = tabla_https,
tld = tabla_tld, subdominios = tabla_subdominios,
letras = tabla_letras, urllength = tabla_urllength,
domainlength = tabla_domainlength)
comprobacion_tablas <- tibble(
Tabla = names(tablas_frecuencia),
`Suma de fi` = vapply(tablas_frecuencia, function(t) sum(t$fi), numeric(1)),
`Suma de hi` = vapply(tablas_frecuencia, function(t) sum(t$hi), numeric(1))
) |>
mutate(Correcta = `Suma de fi` == n_total & abs(`Suma de hi` - 1) < 1e-9)
stopifnot(all(comprobacion_tablas$Correcta))
message("[5] Tablas de frecuencia construidas: todas suman ",
format(n_total, big.mark = ","), " registros.")
comprobacion_tablas#> # A tibble: 7 × 4
#> Tabla `Suma de fi` `Suma de hi` Correcta
#> <chr> <dbl> <dbl> <lgl>
#> 1 label 235795 1 TRUE
#> 2 https 235795 1 TRUE
#> 3 tld 235795 1 TRUE
#> 4 subdominios 235795 1 TRUE
#> 5 letras 235795 1 TRUE
#> 6 urllength 235795 1 TRUE
#> 7 domainlength 235795 1 TRUE
Se comprobó que la suma de las frecuencias absolutas de cada tabla es 235,795 y que la suma de las frecuencias relativas es 1. Las 7 tablas cumplen ambas condiciones.
# Convenciones:
# - Cuartiles: quantile() tipo 7 (método por defecto de R).
# - Varianza y desviación estándar: var() y sd(), con divisor n - 1.
# - CV = 100 * desviación estándar / media (solo variables de razón).
# - Atípico: valor fuera de [Q1 - 1.5 RIC, Q3 + 1.5 RIC] (criterio de Tukey).
moda <- function(x) {
t <- table(x)
as.numeric(names(t)[which.max(t)])
}
medidas <- function(x) {
q <- quantile(x, c(.25, .50, .75), names = FALSE)
ric <- q[3] - q[1]
c(`n válido` = sum(!is.na(x)),
Media = mean(x), Mediana = median(x), Moda = moda(x),
Mínimo = min(x), Máximo = max(x), Rango = max(x) - min(x),
Q1 = q[1], Q2 = q[2], Q3 = q[3], RIC = ric,
Varianza = var(x), `Desviación estándar` = sd(x),
`CV (%)` = 100 * sd(x) / mean(x),
# Si RIC = 0 el criterio de Tukey no es informativo: se deja en blanco (NA)
`Atípicos superiores` = if (ric > 0) sum(x > q[3] + 1.5 * ric) else NA,
`Atípicos inferiores` = if (ric > 0) sum(x < q[1] - 1.5 * ric) else NA)
}vars_cuantitativas <- c("URLLength", "DomainLength", "URLSimilarityIndex",
"TLDLegitimateProb", "NoOfLettersInURL", "NoOfSubDomain")
if (existe_webtraffic) vars_cuantitativas <- c(vars_cuantitativas, "WebTraffic")
# Tabla 10: medidas de toda la base (una columna por variable)
matriz_medidas <- sapply(datos[vars_cuantitativas], medidas)
tabla_medidas <- tibble(Medida = rownames(matriz_medidas)) |>
bind_cols(as_tibble(matriz_medidas))La Tabla 10 reúne las medidas de las 6 variables cuantitativas para toda la base. Todas están en escala de razón, con un cero que significa ausencia, por lo que el coeficiente de variación es interpretable. En URLSimilarityIndex y TLDLegitimateProb el CV debe leerse con cautela, porque sus valores están acotados.
Tabla 10
Medidas descriptivas de las variables cuantitativas (base completa)
| Medida | URL Length | Domain Length | URL Similarity Index | TLD Legitimate Prob | NoOf Letters InURL | NoOf Sub Domain |
|---|---|---|---|---|---|---|
| n válido | 235,795 | 235,795 | 235,795 | 235,795 | 235,795 | 235,795 |
| Media | 34.573 | 21.470 | 78.431 | 0.260 | 19.429 | 1.165 |
| Mediana | 27.000 | 20.000 | 100.000 | 0.080 | 14.000 | 1.000 |
| Moda | 26.000 | 18.000 | 100.000 | 0.523 | 12.000 | 1.000 |
| Mínimo | 13.000 | 4.000 | 0.156 | 0.000 | 0.000 | 0.000 |
| Máximo | 6,097.000 | 110.000 | 100.000 | 0.523 | 5,191.000 | 10.000 |
| Rango | 6,084.000 | 106.000 | 99.844 | 0.523 | 5,191.000 | 10.000 |
| Q1 | 23.000 | 16.000 | 57.025 | 0.006 | 10.000 | 1.000 |
| Q2 | 27.000 | 20.000 | 100.000 | 0.080 | 14.000 | 1.000 |
| Q3 | 34.000 | 24.000 | 100.000 | 0.523 | 20.000 | 1.000 |
| RIC | 11.000 | 8.000 | 42.975 | 0.517 | 10.000 | 0.000 |
| Varianza | 1,706.859 | 83.737 | 839.612 | 0.063 | 846.247 | 0.361 |
| Desviación estándar | 41.314 | 9.151 | 28.976 | 0.252 | 29.090 | 0.601 |
| CV (%) | 119.498 | 42.621 | 36.945 | 96.623 | 149.727 | 51.596 |
| Atípicos superiores | 22,493 | 13,474 | 0 | 0 | 19,662 | — |
| Atípicos inferiores | 0 | 0 | 0 | 0 | 0 | — |
Nota. Los nombres de las variables se muestran partidos en varias líneas. Q2 coincide con la mediana. RIC = Q3 − Q1. Varianza y desviación estándar con divisor n − 1. Atípicos según el criterio de Tukey; en NoOfSubDomain el RIC es 0 y el criterio no es informativo. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
En la longitud de la URL, la media (34.57 caracteres) es mayor que la mediana (27) y que la moda (26), lo que indica asimetría hacia la derecha. La mitad central de las URL mide entre 23 y 34 caracteres (RIC = 11), pero el máximo llega a 6,097; hay 22,493 valores atípicos superiores (9.54 % de la base). Esos valores extremos elevan la desviación estándar a 41.31 caracteres y el CV a 119.50 %, por lo que la mediana y el RIC resumen mejor esta variable que la media y la desviación estándar. La longitud del dominio es más homogénea: su media es 21.47, su mediana 20 y su CV 42.62 %.
En URLSimilarityIndex la mediana y el tercer cuartil valen 100, es decir, al menos la mitad de las URL tiene el valor máximo del índice; la media (78.43) queda por debajo, lo que señala asimetría hacia la izquierda. En TLDLegitimateProb la moda (0.523) coincide con el máximo y corresponde al TLD .com, mientras que la mediana es 0.080.
# Tabla 11: variables categóricas (moda, frecuencia y porcentaje de la moda)
resumen_categorica <- function(x, nombre) {
t <- sort(table(x), decreasing = TRUE)
tibble(Variable = nombre, Categorías = length(t),
Moda = names(t)[1], `fi de la moda` = as.vector(t[1]),
`% de la moda` = 100 * as.vector(t[1]) / sum(t))
}
tabla_categoricas <- bind_rows(
resumen_categorica(datos_analisis$clase, "label"),
resumen_categorica(datos_analisis$https, "IsHTTPS"),
resumen_categorica(datos_analisis$dominio_ip, "IsDomainIP"),
resumen_categorica(datos_analisis$tiene_titulo, "HasTitle"),
resumen_categorica(paste0(".", datos$TLD), "TLD")
)Para las variables categóricas se presenta la moda con su frecuencia y su porcentaje (Tabla 11). No se calcularon medias ni varianzas de estas variables, porque sus códigos son etiquetas.
Tabla 11
Moda de las variables categóricas
| Variable | Categorías | Moda | fi de la moda | % de la moda |
|---|---|---|---|---|
| label | 2 | Legítima | 134,850 | 57.19 |
| IsHTTPS | 2 | Sí | 184,539 | 78.26 |
| IsDomainIP | 2 | No | 235,157 | 99.73 |
| HasTitle | 2 | Sí | 203,081 | 86.13 |
| TLD | 695 | .com | 112,554 | 47.73 |
Nota. En las variables binarias, Sí corresponde al código 1 y No al código 0. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
# Comparación por clase (legítima / phishing)
vars_comparacion <- c("URLLength", "DomainLength", "URLSimilarityIndex",
"TLDLegitimateProb")
tabla_comparacion <- bind_rows(lapply(vars_comparacion, function(v) {
bind_rows(lapply(levels(datos_analisis$clase), function(g) {
m <- medidas(datos_analisis[[v]][datos_analisis$clase == g])
tibble(Variable = v, Clase = g, n = m["n válido"], Media = m["Media"],
Mediana = m["Mediana"], Q1 = m["Q1"], Q3 = m["Q3"], RIC = m["RIC"],
`Desv. est.` = m["Desviación estándar"], `CV (%)` = m["CV (%)"],
Mínimo = m["Mínimo"], Máximo = m["Máximo"])
}))
}))
# Para que quepa en la página, la comparación se presenta en dos tablas:
# Tabla 12 (centro y posición) y Tabla 13 (dispersión).
tabla_comparacion_centro <- tabla_comparacion |>
select(Variable, Clase, n, Media, Mediana, Q1, Q3)
tabla_comparacion_dispersion <- tabla_comparacion |>
select(Variable, Clase, Mínimo, Máximo, RIC, `Desv. est.`, `CV (%)`)
# Dato de apoyo para la interpretación del índice de similitud
phishing_indice_100 <- sum(datos$label == 0 & datos$URLSimilarityIndex == 100)
# Dato de apoyo para las limitaciones: URL phishing formadas solo por
# protocolo y dominio (misma estructura que todas las legítimas)
phishing_solo_dominio <- sum(datos$label == 0 &
datos$URLLength == datos$DomainLength + 7)# Tabla 14: variables binarias según la clase (porcentaje DENTRO de cada clase)
tabla_binarias_clase <- datos_analisis |>
select(clase, IsHTTPS, IsDomainIP, HasTitle) |>
pivot_longer(-clase, names_to = "Variable", values_to = "valor") |>
group_by(Variable, Clase = clase) |>
summarise(n = n(), `fi (valor = 1)` = sum(valor),
`% dentro de la clase` = 100 * mean(valor), .groups = "drop") |>
mutate(Variable = factor(Variable, c("IsHTTPS", "HasTitle", "IsDomainIP"))) |>
arrange(Variable, Clase) |>
mutate(Variable = as.character(Variable))
message("[6] Medidas descriptivas calculadas para ",
length(vars_cuantitativas), " variables cuantitativas.")Las Tablas 12 y 13 comparan cuatro variables cuantitativas entre las dos clases. La Tabla 14 hace lo mismo con las variables binarias, con porcentajes calculados dentro de cada clase.
Tabla 12
Medidas de centro y posición por clasificación de la URL
| Variable | Clase | n | Media | Mediana | Q1 | Q3 |
|---|---|---|---|---|---|---|
| URLLength | Legítima | 134,850 | 26.229 | 26.000 | 23.000 | 29.000 |
| URLLength | Phishing | 100,945 | 45.720 | 34.000 | 26.000 | 48.000 |
| DomainLength | Legítima | 134,850 | 19.229 | 19.000 | 16.000 | 22.000 |
| DomainLength | Phishing | 100,945 | 24.465 | 22.000 | 16.000 | 29.000 |
| URLSimilarityIndex | Legítima | 134,850 | 100.000 | 100.000 | 100.000 | 100.000 |
| URLSimilarityIndex | Phishing | 100,945 | 49.617 | 51.419 | 32.121 | 67.251 |
| TLDLegitimateProb | Legítima | 134,850 | 0.282 | 0.523 | 0.018 | 0.523 |
| TLDLegitimateProb | Phishing | 100,945 | 0.232 | 0.018 | 0.002 | 0.523 |
Nota. URLLength y DomainLength en caracteres; URLSimilarityIndex en puntos de 0 a 100; TLDLegitimateProb en proporción. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
Tabla 13
Medidas de dispersión por clasificación de la URL
| Variable | Clase | Mínimo | Máximo | RIC | Desv. est. | CV (%) |
|---|---|---|---|---|---|---|
| URLLength | Legítima | 15.000 | 57.000 | 6.000 | 4.816 | 18.360 |
| URLLength | Phishing | 13.000 | 6,097.000 | 22.000 | 61.146 | 133.738 |
| DomainLength | Legítima | 8.000 | 50.000 | 6.000 | 4.816 | 25.044 |
| DomainLength | Phishing | 4.000 | 110.000 | 13.000 | 12.204 | 49.883 |
| URLSimilarityIndex | Legítima | 100.000 | 100.000 | 0.000 | 0.000 | 0.000 |
| URLSimilarityIndex | Phishing | 0.156 | 100.000 | 35.131 | 22.572 | 45.493 |
| TLDLegitimateProb | Legítima | 0.000 | 0.523 | 0.505 | 0.247 | 87.752 |
| TLDLegitimateProb | Phishing | 0.000 | 0.523 | 0.521 | 0.255 | 109.770 |
Nota. RIC = rango intercuartílico; Desv. est. = desviación estándar; CV = coeficiente de variación. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
Tabla 14
Variables binarias según la clasificación de la URL (porcentaje dentro de cada clase)
| Variable | Clase | n | fi (valor = 1) | % dentro de la clase |
|---|---|---|---|---|
| IsHTTPS | Legítima | 134,850 | 134,850 | 100.00 |
| IsHTTPS | Phishing | 100,945 | 49,689 | 49.22 |
| HasTitle | Legítima | 134,850 | 134,682 | 99.88 |
| HasTitle | Phishing | 100,945 | 68,399 | 67.76 |
| IsDomainIP | Legítima | 134,850 | 0 | 0.00 |
| IsDomainIP | Phishing | 100,945 | 638 | 0.63 |
Nota. n es el total de URL de la clase; el porcentaje se calcula sobre ese total y no sobre toda la base. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
En las URL legítimas la longitud tiene una mediana de 26 caracteres y un RIC de 6; en las phishing la mediana es 34 y el RIC 22. La desviación estándar es de 4.82 caracteres en las legítimas y de 61.15 en las phishing. En esta base, entonces, las URL phishing son más largas en el centro de la distribución y mucho más variables. En URLSimilarityIndex todas las URL legítimas tienen el valor 100 (desviación estándar 0), mientras que las phishing tienen una mediana de 51.42 y un RIC de 35.13 puntos. Estas son diferencias observadas en los registros; el análisis no permite afirmar a qué se deben.
Todas las figuras se elaboraron con ggplot2, con el mismo tema y la misma paleta: azul para las URL legítimas y rojo para las phishing. Cada figura se guarda en la carpeta graficos a 300 ppp.
# Identidad visual única para todas las figuras.
color_legitima <- "#1F4E79" # azul sobrio
color_phishing <- "#B03A2E" # rojo ladrillo
color_neutro <- "#3E5C76" # azul grisáceo para una sola serie
color_linea <- "#1B1B1B"
colores_clase <- c("Legítima" = color_legitima, "Phishing" = color_phishing)
fuente_grafico <- "Fuente: elaboración propia en RStudio con datos de Prasad y Chandra (2024), UCI Machine Learning Repository."
tema_parcial <- function() {
theme_minimal(base_size = 12) +
theme(
text = element_text(colour = "black"),
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 11, margin = margin(b = 10)),
plot.caption = element_text(size = 8.5, hjust = 0, margin = margin(t = 10)),
plot.title.position = "plot",
plot.caption.position = "plot",
axis.title = element_text(size = 11),
axis.text = element_text(colour = "black"),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank(),
legend.position = "top",
legend.title = element_text(size = 10.5),
plot.margin = margin(12, 16, 10, 12)
)
}
miles <- function(x) format(x, big.mark = ",", trim = TRUE)
# Función para guardar cada figura con el mismo tamaño y resolución
guardar_figura <- function(figura, nombre) {
ggsave(file.path(dir_graficos, paste0(nombre, ".png")), figura,
width = 8, height = 5.2, dpi = 300, bg = "white")
}# --- Figura 1: barras de label ----------------------------------------------------
fig1 <- ggplot(tabla_label, aes(x = Clasificación, y = fi, fill = Clasificación)) +
geom_col(width = 0.55, show.legend = FALSE) +
geom_text(aes(label = paste0(miles(fi), "\n(", sprintf("%.2f", `%`), " %)")),
vjust = -0.25, size = 3.9, lineheight = 0.95) +
scale_fill_manual(values = colores_clase) +
scale_y_continuous(labels = label_comma(), expand = expansion(mult = c(0, 0.18))) +
labs(title = "Distribución de las URL según su clasificación",
subtitle = paste0("Base completa PhiUSIIL, n = ", miles(n_total), " URL"),
x = "Clasificación de la URL (label)", y = "Frecuencia absoluta (URL)",
caption = fuente_grafico) +
tema_parcial()
guardar_figura(fig1, "Figura_1_barras_label")Figura 1
Gráfico de barras de las URL según su clasificación (label)
Nota. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
La Figura 1 representa la frecuencia de cada clase. Hay 134,850 URL legítimas (57.19 %) y 100,945 URL phishing (42.81 %), una diferencia de 33,905 registros. Las dos clases tienen un tamaño suficiente para describirlas por separado. Para un equipo de seguridad, esta proporción describe la composición de la base y no debe leerse como la frecuencia del phishing en internet.
# --- Figura 2: diagrama de Pareto de TLD ------------------------------------------
pareto_tld <- tabla_tld |>
mutate(TLD = factor(TLD, levels = TLD), acumulado = cumsum(`%`))
escala_pareto <- n_total / 100 # 1 % equivale a n_total / 100 URL
fig2 <- ggplot(pareto_tld, aes(x = TLD)) +
geom_col(aes(y = fi), fill = color_neutro, width = 0.7) +
geom_text(aes(y = fi, label = sprintf("%.1f %%", `%`)), vjust = -0.5, size = 3.1) +
geom_line(aes(y = acumulado * escala_pareto, group = 1),
colour = color_phishing, linewidth = 0.8) +
geom_point(aes(y = acumulado * escala_pareto), colour = color_phishing, size = 2) +
scale_y_continuous(
labels = label_comma(), limits = c(0, n_total * 1.03),
expand = expansion(mult = c(0, 0.02)),
sec.axis = sec_axis(~ . / escala_pareto, name = "Porcentaje acumulado (%)",
breaks = seq(0, 100, 20))) +
labs(title = "Diagrama de Pareto del dominio de nivel superior (TLD)",
subtitle = paste0("Los ", n_tld_top, " TLD más frecuentes y el resto agrupado; ",
"barras = frecuencia, línea = porcentaje acumulado"),
x = "Dominio de nivel superior (TLD)", y = "Frecuencia absoluta (URL)",
caption = fuente_grafico) +
tema_parcial() +
theme(axis.text.x = element_text(angle = 35, hjust = 1))
guardar_figura(fig2, "Figura_2_pareto_TLD")Figura 2
Diagrama de Pareto del dominio de nivel superior (TLD)
Nota. Las barras muestran la frecuencia absoluta y la línea el porcentaje acumulado. La categoría Otros se ubica al final por convención. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
La Figura 2 ordena los TLD de mayor a menor frecuencia. El TLD .com reúne el 47.73 % de las URL y los 10 TLD más frecuentes acumulan el 72.87 %; los otros 685 TLD se reparten el 27.13 % restante. La variable está muy concentrada en pocas categorías, lo que justifica agrupar las demás al resumirla.
# --- Figura 3: histograma y polígono de frecuencias de URLLength -------------------
# Se grafican las nueve clases de amplitud 10; la clase final [100, máx] se
# informa en una nota porque su amplitud impide dibujarla a la misma escala.
clases_url_10 <- tabla_urllength[-nrow(tabla_urllength), ]
cola_url <- tabla_urllength[nrow(tabla_urllength), ]
poligono_url <- tibble(x = c(5, clases_url_10$`Marca de clase`),
y = c(0, clases_url_10$fi))
nota_cola_url <- paste0("Clase final ", cola_url$Clase, ": ", miles(cola_url$fi),
" URL (", sprintf("%.2f", cola_url$`%`),
" %),\nno dibujada por su amplitud")
fig3 <- ggplot() +
geom_histogram(data = filter(datos, URLLength < 100), aes(x = URLLength),
breaks = seq(10, 100, 10), closed = "left",
fill = color_neutro, colour = "white") +
geom_line(data = poligono_url, aes(x, y), colour = color_phishing, linewidth = 0.8) +
geom_point(data = poligono_url, aes(x, y), colour = color_phishing, size = 2) +
annotate("label", x = 98, y = max(clases_url_10$fi) * 0.62, label = nota_cola_url,
hjust = 1, size = 3.3, label.size = 0.25, lineheight = 1) +
scale_x_continuous(breaks = seq(0, 100, 10), limits = c(0, 100)) +
scale_y_continuous(labels = label_comma(), expand = expansion(mult = c(0, 0.06))) +
labs(title = "Histograma y polígono de frecuencias de la longitud de la URL",
subtitle = "Clases de 10 caracteres (Tabla 8); la línea une las marcas de clase",
x = "Longitud de la URL (caracteres)", y = "Frecuencia absoluta (URL)",
caption = fuente_grafico) +
tema_parcial()
guardar_figura(fig3, "Figura_3_histograma_URLLength")Figura 3
Histograma y polígono de frecuencias de la longitud de la URL (URLLength)
Nota. Se dibujan las nueve clases de 10 caracteres de la Tabla 8. La clase final, de 100 a 6,097 caracteres, se informa en el recuadro porque su amplitud impide representarla a la misma escala; ningún registro fue eliminado. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
La Figura 3 muestra una distribución con un solo pico y cola hacia la derecha. La clase [20, 30) concentra 129,295 URL (54.83 %) y el 83.47 % de las URL mide menos de 40 caracteres. Solo 4,976 URL (2.11 %) tienen 100 caracteres o más, pero esa cola llega hasta 6,097. En la práctica, las URL muy largas son poco frecuentes en esta base y conviene revisarlas aparte.
# --- Figura 4: histograma de DomainLength ------------------------------------------
fig4 <- ggplot(datos, aes(x = DomainLength)) +
geom_histogram(breaks = cortes_dom, closed = "left",
fill = color_neutro, colour = "white") +
scale_x_continuous(breaks = cortes_dom[seq(1, length(cortes_dom), by = 2)]) +
scale_y_continuous(labels = label_comma(), expand = expansion(mult = c(0, 0.06))) +
labs(title = "Histograma de la longitud del dominio",
subtitle = paste0(n_clases_dom, " clases de ", amplitud_dom,
" caracteres según la regla de Sturges (Tabla 9)"),
x = "Longitud del dominio (caracteres)", y = "Frecuencia absoluta (URL)",
caption = fuente_grafico) +
tema_parcial()
guardar_figura(fig4, "Figura_4_histograma_DomainLength")Figura 4
Histograma de la longitud del dominio (DomainLength)
Nota. Clases de la Tabla 9. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
La Figura 4 muestra que la longitud del dominio se concentra entre 10 y 28 caracteres: esas tres clases suman 194,922 URL (82.67 %). La clase modal es [16, 22), con 92,217 URL. La distribución tiene asimetría hacia la derecha y un grupo pequeño separado entre 70 y 82 caracteres (1,851 URL, 0.79 %).
# --- Figura 5: ojiva de DomainLength -----------------------------------------------
ojiva_dom <- tibble(x = c(min_dom, tabla_domainlength$Ls),
y = c(0, 100 * tabla_domainlength$Hi))
fig5 <- ggplot(ojiva_dom, aes(x, y)) +
geom_hline(yintercept = 50, linetype = "dashed", colour = "grey45") +
geom_line(colour = color_legitima, linewidth = 0.9) +
geom_point(colour = color_legitima, size = 2) +
annotate("text", x = max(cortes_dom), y = 46, label = "50 % de las URL",
hjust = 1, size = 3.4, colour = "grey25") +
scale_x_continuous(breaks = cortes_dom[seq(1, length(cortes_dom), by = 2)]) +
scale_y_continuous(breaks = seq(0, 100, 20), limits = c(0, 100),
labels = function(v) paste0(v, " %")) +
labs(title = "Ojiva de la longitud del dominio",
subtitle = "Frecuencia relativa acumulada (Hi) en el límite superior de cada clase (Tabla 9)",
x = "Longitud del dominio (caracteres)",
y = "Porcentaje acumulado de URL",
caption = fuente_grafico) +
tema_parcial() +
theme(panel.grid.major.x = element_line(colour = "grey92"))
guardar_figura(fig5, "Figura_5_ojiva_DomainLength")Figura 5
Ojiva de la longitud del dominio (DomainLength)
Nota. Cada punto es la frecuencia relativa acumulada (Hi) en el límite superior de una clase de la Tabla 9. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
La ojiva de la Figura 5 muestra el porcentaje acumulado de URL hasta cada longitud de dominio. El 22.18 % de los dominios mide menos de 16 caracteres, el 61.29 % menos de 22 y el 84.16 % menos de 28. La curva cruza el 50 % dentro de la clase [16, 22), lo que concuerda con la mediana de 20 caracteres. A partir de 52 caracteres la curva es casi plana: solo el 1.38 % de los dominios mide 52 caracteres o más.
# --- Figura 6: diagrama de cajas de URLLength por clase ----------------------------
# Escala logarítmica en el eje vertical: los valores no se transforman ni se
# eliminan; solo cambia la separación del eje para que la cola sea legible.
fig6 <- ggplot(datos_analisis, aes(x = clase, y = URLLength, fill = clase)) +
geom_boxplot(width = 0.5, outlier.size = 0.6, outlier.alpha = 0.25,
show.legend = FALSE, colour = color_linea, linewidth = 0.4) +
scale_fill_manual(values = alpha(colores_clase, 0.85)) +
scale_y_log10(breaks = c(10, 20, 50, 100, 200, 500, 1000, 2000, 5000),
labels = label_comma()) +
labs(title = "Diagrama de cajas de la longitud de la URL según la clasificación",
subtitle = "Eje vertical en escala logarítmica; los puntos son valores atípicos (criterio de Tukey)",
x = "Clasificación de la URL (label)",
y = "Longitud de la URL (caracteres, escala log)",
caption = fuente_grafico) +
tema_parcial()
guardar_figura(fig6, "Figura_6_cajas_URLLength_label")Figura 6
Diagrama de cajas de la longitud de la URL según la clasificación (label)
Nota. El eje vertical está en escala logarítmica para que la caja y los valores extremos sean legibles en la misma figura; los datos no se transformaron. Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
La Figura 6 compara la longitud de la URL entre las dos clases. En las legítimas la caja va de 23 a 29 caracteres, con mediana 26 y máximo 57. En las phishing la caja va de 26 a 48, con mediana 34 y máximo 6,097. Todos los valores de 100 caracteres o más pertenecen a la clase phishing.
# --- Figura 7: barras agrupadas de variables binarias por clase --------------------
etiquetas_binarias <- c(IsHTTPS = "Usa HTTPS\n(IsHTTPS = 1)",
HasTitle = "Tiene título\n(HasTitle = 1)",
IsDomainIP = "Dominio es una IP\n(IsDomainIP = 1)")
datos_fig7 <- tabla_binarias_clase |>
mutate(Variable = factor(etiquetas_binarias[Variable], levels = etiquetas_binarias))
fig7 <- ggplot(datos_fig7, aes(x = Variable, y = `% dentro de la clase`, fill = Clase)) +
geom_col(position = position_dodge(width = 0.7), width = 0.62) +
geom_text(aes(label = sprintf("%.2f %%", `% dentro de la clase`)),
position = position_dodge(width = 0.7), vjust = -0.5, size = 3.5) +
scale_fill_manual(values = colores_clase, name = "Clasificación (label)") +
scale_y_continuous(limits = c(0, 100), breaks = seq(0, 100, 20),
labels = function(v) paste0(v, " %"),
expand = expansion(mult = c(0, 0.1))) +
labs(title = "Características binarias de las URL según la clasificación",
subtitle = "Porcentaje calculado dentro de cada clase (legítimas y phishing por separado)",
x = "Característica observada", y = "Porcentaje de URL de la clase",
caption = fuente_grafico) +
tema_parcial()
guardar_figura(fig7, "Figura_7_barras_binarias_label")Figura 7
Barras agrupadas de tres características binarias según la clasificación (label)
Nota. Porcentajes calculados dentro de cada clase (Tabla 14). Elaboración propia en RStudio con datos de Prasad y Chandra (2024a).
La Figura 7 compara el porcentaje de URL que presenta cada característica dentro de su clase. El 100.00 % de las legítimas usa HTTPS, frente al 49.22 % de las phishing. El 99.88 % de las legítimas tiene título, frente al 67.76 % de las phishing. El uso de una dirección IP como dominio es poco frecuente: 0.63 % en las phishing y 0.00 % en las legítimas. Cerca de la mitad de las URL phishing de la base usa HTTPS, de modo que en estos registros el protocolo no separa por sí solo a las dos clases.
Composición de la base. La variable label registra 134,850 URL legítimas (57.19 %) y 100,945 URL phishing (42.81 %), para un total de 235,795 registros (Tabla 3, Figura 1). La clase modal es la legítima.
Longitud de la URL por clase. La mediana de URLLength es de 26 caracteres en las legítimas y de 34 en las phishing; el RIC es de 6 y 22 caracteres, y el máximo de 57 y 6,097 (Tablas 12 y 13, Figura 6). Las URL phishing de la base son más largas y más dispersas.
Asimetría de la longitud de la URL. En toda la base, la media de URLLength (34.57) supera a la mediana (27), el CV es de 119.50 % y hay 22,493 valores atípicos superiores (9.54 %) (Tabla 10). El 2.11 % de las URL tiene 100 caracteres o más (Tabla 8, Figura 3).
Índice de similitud. Las 134,850 URL legítimas tienen URLSimilarityIndex igual a 100, con desviación estándar 0. En las phishing la mediana es 51.42, el primer cuartil 32.12 y el tercero 67.25 (Tablas 12 y 13); solo 786 URL phishing (0.78 % de su clase) alcanzan el valor 100.
Uso de HTTPS y presencia de título. Dentro de cada clase, el 100.00 % de las legítimas y el 49.22 % de las phishing usan HTTPS; el 99.88 % y el 67.76 %, respectivamente, tienen título (Tabla 14, Figura 7).
Concentración del TLD. De 695 TLD distintos, .com es la moda con el 47.73 % de las URL y los 10 más frecuentes acumulan el 72.87 % (Tabla 5, Figura 2).
Longitud del dominio. La mediana de DomainLength es de 20 caracteres y el 61.29 % de los dominios mide menos de 22 (Tabla 9, Figura 5). Por clase, la mediana es 19 en las legítimas y 22 en las phishing, con desviaciones estándar de 4.82 y 12.20 caracteres (Tablas 12 y 13).
Estas diferencias son observaciones sobre los registros de la base. Una explicación posible de las diferencias de longitud es la forma en que se recolectaron las URL de cada clase, que se describe en las limitaciones; el análisis descriptivo no permite confirmar esa ni otra explicación.
La base cumple lo que reporta la ficha de UCI en tamaño, composición por clase y ausencia de valores faltantes: 235,795 registros, 57.19 % legítimos y 42.81 % phishing, sin filas duplicadas exactas.
Las variables de longitud tienen distribuciones asimétricas hacia la derecha, con valores extremos concentrados en la clase phishing. Por eso la mediana y el rango intercuartílico las describen mejor que la media y la desviación estándar: en URLLength la media es 34.57 y la mediana 27.
En las variables analizadas, las URL legítimas de la base forman un grupo muy homogéneo (URLSimilarityIndex constante en 100, HTTPS en el 100.00 %, CV de URLLength de 18.36 %), mientras que las phishing son heterogéneas (CV de URLLength de 133.74 %, HTTPS en el 49.22 %).
Acompañar la base con un diccionario de datos completo. La ficha de UCI no describe las variables ni sus unidades, lista URLSimilarityIndex como entera aunque es decimal y no explica por qué URLLength difiere en un carácter del texto de la URL en el 79.37 % de los registros. Un diccionario evitaría interpretaciones erróneas.
Al resumir las variables de longitud, reportar la mediana y los cuartiles junto con la media, y presentar los valores extremos en una clase o en una escala aparte, como se hizo en la Tabla 8 y en la Figura 6. Así los 4,976 registros de 100 caracteres o más no distorsionan la lectura del resto.
La principal limitación es la forma de las URL legítimas. En las 134,850 URL legítimas (el 100 % de la clase) se cumple que URLLength = DomainLength + 7, es decir, todas constan solo del protocolo y el dominio, sin ruta ni parámetros. En las URL phishing esa igualdad se cumple solo en 11,166 registros (11.06 % de la clase); el resto incluye ruta o parámetros. Por lo tanto, parte de las diferencias de longitud entre clases puede reflejar cómo se armó la base y no una característica general de los sitios legítimos.
Un requisito de las instrucciones no se pudo cumplir: el análisis de la variable WebTraffic, que no existe en el archivo oficial. Los demás requisitos se desarrollaron con las variables disponibles. Además, hay 850 registros con URL repetida que se conservaron, y los resultados describen únicamente los 235,795 registros de la base, por lo que no deben generalizarse a todas las URL de internet.
Prasad, A., & Chandra, S. (2024a). PhiUSIIL Phishing URL (Website) [Conjunto de datos]. UCI Machine Learning Repository. https://doi.org/10.1016/j.cose.2023.103545
Prasad, A., & Chandra, S. (2024b). PhiUSIIL: A diverse security profile empowered phishing URL detection framework based on similarity index and incremental learning. Computers & Security, 136, Artículo 103545. https://doi.org/10.1016/j.cose.2023.103545
R Core Team. (2026). R: A language and environment for statistical computing (Versión 4.6.1) [Software]. R Foundation for Statistical Computing. https://www.R-project.org/
Wickham, H. (2016). ggplot2: Elegant graphics for data analysis (2.ª ed.). Springer. https://ggplot2.tidyverse.org
figuras <- list(
Figura_1_barras_label = fig1,
Figura_2_pareto_TLD = fig2,
Figura_3_histograma_URLLength = fig3,
Figura_4_histograma_DomainLength = fig4,
Figura_5_ojiva_DomainLength = fig5,
Figura_6_cajas_URLLength_label = fig6,
Figura_7_barras_binarias_label = fig7
)
redondear <- function(t, decimales = 3) {
mutate(t, across(where(is.double), ~ round(.x, decimales)))
}
tablas_exportar <- list(
Tabla_01_diccionario = diccionario,
Tabla_02_calidad_de_datos = calidad,
Tabla_03_TipoI_label = tabla_label,
Tabla_04_TipoI_IsHTTPS = tabla_https,
Tabla_05_TipoI_TLD = tabla_tld,
Tabla_06_TipoI_NoOfSubDomain = tabla_subdominios,
Tabla_07_TipoII_NoOfLettersInURL = tabla_letras,
Tabla_08_TipoIII_URLLength = tabla_urllength,
Tabla_09_TipoIII_DomainLength = tabla_domainlength,
Tabla_10_medidas_descriptivas = tabla_medidas,
Tabla_11_variables_categoricas = tabla_categoricas,
Tabla_12_comparacion_centro_posicion = tabla_comparacion_centro,
Tabla_13_comparacion_dispersion = tabla_comparacion_dispersion,
Tabla_14_binarias_por_label = tabla_binarias_clase,
Anexo_contraste_UCI = contraste_uci,
Anexo_tipos_de_dato = tabla_tipos,
Anexo_comprobacion_de_sumas = comprobacion_tablas
)
for (nombre in names(tablas_exportar)) {
write_excel_csv(redondear(as_tibble(tablas_exportar[[nombre]])),
file.path(dir_tablas, paste0(nombre, ".csv")))
}
for (nombre in names(figuras)) {
ggsave(file.path(dir_graficos, paste0(nombre, ".png")), figuras[[nombre]],
width = 8, height = 5.2, dpi = 300, bg = "white")
}
message("[8] Exportadas ", length(tablas_exportar), " tablas a /tablas y ",
length(figuras), " figuras a /graficos.")El proyecto se entrega con los siguientes archivos: el script
Parcial_Estadistica_Completo.R (carpeta scripts), este
documento Informe_Estadistica_Descriptiva.Rmd (carpeta
informe), las 17 tablas en formato CSV (carpeta tablas) y las 7 figuras
en formato PNG a 300 ppp (carpeta graficos). Para reproducir todo, se
abre el archivo Parcial_Estadistica_Descriptiva.Rproj en
RStudio y se ejecuta el script completo, que también genera este informe
en HTML y en Word.
# Versión de R y de los paquetes con los que se generó el informe
sessionInfo()$R.version$version.string#> [1] "R version 4.6.1 (2026-06-24 ucrt)"
sapply(c("readr", "dplyr", "tidyr", "ggplot2", "scales", "stringr", "knitr",
"rmarkdown"), function(p) as.character(packageVersion(p)))#> readr dplyr tidyr ggplot2 scales stringr knitr rmarkdown
#> "2.2.0" "1.2.1" "1.3.2" "4.0.3" "1.4.0" "1.6.0" "1.52" "2.32"