#03 Cómo construyo un modelo de priorización de URLs basado en datos de crawl + GSC
Cómo construyo un modelo de priorización de URLs basado en datos de crawl + GSC
Una de las preguntas que más me hacen cuando audito un sitio grande es: ¿por dónde empezamos? Y es una buena pregunta. Cuando tienes 10.000 URLs, no puedes optimizarlas todas a la vez. Necesitas un criterio claro para decidir dónde poner el foco primero.
Durante mucho tiempo ese criterio era más intuitivo que sistemático. Revisaba las páginas con más tráfico, las que tenían errores obvios, las que el cliente mencionaba en la reunión. Funcionaba, pero no escalaba y, sobre todo, no era reproducible.
Ahora tengo un modelo. No es complejo, pero sí es consistente. Te lo cuento.
La lógica detrás del modelo
La idea central es simple: una URL merece atención prioritaria cuando tiene potencial real de mejora y cuando esa mejora tiene impacto de negocio. Para medir eso, necesito dos fuentes de datos combinadas:
- Datos de crawl: estado técnico de la URL (indexabilidad, velocidad, errores, estructura interna)
- Datos de GSC: rendimiento actual en búsqueda (impresiones, clics, posición media, CTR)
Por separado, cada fuente cuenta la mitad de la historia. Juntas, te dan una visión completa de dónde hay problemas y cuánto te cuesta tenerlos sin resolver.
Las fuentes de datos
Crawl con Screaming Frog
Exporto el crawl completo en CSV desde Screaming Frog. Las columnas que me interesan son:
Address: URL de la páginaStatus Code: para detectar errores 4xx, 5xx o redireccionesIndexability: si la URL es indexable o no (y el motivo)Word Count: cantidad de contenidoResponse Time: velocidad de respuestaInlinks: número de enlaces internos que recibe
Datos de GSC vía API
Para el lado del rendimiento, uso la API de Search Console y traigo los datos agrupados por URL (dimensión page):
from google.oauth2 import service_account
from googleapiclient.discovery import build
import pandas as pd
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
SERVICE_ACCOUNT_FILE = "credentials.json"
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
service = build("searchconsole", "v1", credentials=credentials)
response = service.searchanalytics().query(
siteUrl="https://tusitio.com",
body={
"startDate": "2025-12-01",
"endDate": "2026-05-30",
"dimensions": ["page"],
"rowLimit": 25000
}
).execute()
rows = response.get("rows", [])
gsc_df = pd.DataFrame([{
"url": r["keys"][0],
"clicks": r["clicks"],
"impressions": r["impressions"],
"ctr": r["ctr"],
"position": r["position"]
} for r in rows])
Construir el modelo paso a paso
1. Cargar y cruzar los datos
import pandas as pd
# Cargar crawl
crawl_df = pd.read_csv("screaming_frog_export.csv", low_memory=False)
crawl_df = crawl_df.rename(columns={
"Address": "url",
"Status Code": "status_code",
"Indexability": "indexability",
"Word Count": "word_count",
"Response Time": "response_time",
"Inlinks": "inlinks"
})
# Cruzar con GSC por URL
df = crawl_df.merge(gsc_df, on="url", how="left")
# Rellenar NaN en columnas de GSC (URLs sin datos de rendimiento)
df[["clicks", "impressions", "ctr", "position"]] = df[[
"clicks", "impressions", "ctr", "position"
]].fillna(0)
2. Crear señales de oportunidad
Aquí defino las señales que me indican que una URL tiene potencial de mejora. Cada señal es una columna binaria (0 o 1) que luego combino en una puntuación final.
# Señal 1: URL indexable con impresiones pero sin clics
df["s_impresiones_sin_clics"] = (
(df["indexability"] == "Indexable") &
(df["impressions"] > 50) &
(df["clicks"] == 0)
).astype(int)
# Señal 2: Posición entre 6 y 20 (cerca del top pero fuera)
df["s_posicion_near_top"] = (
(df["position"] >= 6) &
(df["position"] <= 20)
).astype(int)
# Señal 3: CTR por debajo del 2% en posiciones 1-5
df["s_ctr_bajo_top5"] = (
(df["position"] <= 5) &
(df["ctr"] < 0.02)
).astype(int)
# Señal 4: Contenido escaso (menos de 300 palabras)
df["s_contenido_escaso"] = (df["word_count"] < 300).astype(int)
# Señal 5: Pocos enlaces internos (URL poco reforzada)
df["s_pocos_inlinks"] = (df["inlinks"] < 3).astype(int)
# Señal 6: Tiempo de respuesta alto
df["s_lenta"] = (df["response_time"] > 2000).astype(int) # en ms
3. Calcular la puntuación de prioridad
Asigno un peso a cada señal según su impacto esperado. Estos pesos los ajusto proyecto a proyecto según el contexto del cliente, pero estos son mis valores de partida:
pesos = {
"s_impresiones_sin_clics": 3,
"s_posicion_near_top": 3,
"s_ctr_bajo_top5": 2,
"s_contenido_escaso": 2,
"s_pocos_inlinks": 1,
"s_lenta": 1
}
df["score"] = sum(df[señal] * peso for señal, peso in pesos.items())
Además, incorporo el volumen de impresiones como multiplicador. Una URL con score alto pero sin visibilidad es menos urgente que una con el mismo score y miles de impresiones.
import numpy as np
df["score_ponderado"] = df["score"] * np.log1p(df["impressions"])
4. Filtrar y ordenar el resultado
# Solo URLs indexables con alguna señal activa
resultado = df[
(df["indexability"] == "Indexable") &
(df["score"] > 0)
].sort_values("score_ponderado", ascending=False)
columnas_salida = [
"url", "clicks", "impressions", "ctr", "position",
"word_count", "inlinks", "response_time",
"score", "score_ponderado"
]
resultado[columnas_salida].to_csv("urls_priorizadas.csv", index=False)
El resultado es una lista ordenada de URLs donde la primera es, según el modelo, la que más impacto puede tener si la trabajas ahora. No es una verdad absoluta, pero sí un criterio objetivo y consistente para arrancar.
Cómo uso el output en el día a día
El CSV exportado lo abro en Google Sheets y lo comparto con el equipo o con el cliente. Las primeras 20-30 URLs del ranking suelen ser las que entran en el sprint de optimización del mes. Para cada una, las señales activas me dicen exactamente qué tipo de acción aplicar:
s_contenido_escasoactivado → ampliar y mejorar el contenidos_pocos_inlinksactivado → reforzar el enlazado internos_ctr_bajo_top5activado → reescribir título y metadescripcións_posicion_near_topactivado → revisar relevancia semántica del contenido
Lo que este modelo no resuelve
Seré honesto: el modelo no sabe nada de negocio. Una URL con score bajo puede ser estratégicamente crítica para el cliente aunque los datos no la destaquen. Y al revés: una URL en el top del ranking puede ser una página secundaria que no mueve la aguja comercialmente.
Por eso siempre presento el modelo como una herramienta de soporte, no como un oráculo. El criterio estratégico sigue siendo tuyo. El modelo solo te evita tomar decisiones a ciegas.
En el siguiente post voy un paso más allá y te cuento cómo detecto canibalización a escala usando pandas. Si este proceso te ha resultado útil, ese te va a gustar más todavía.