Sitio web en mantenimiento
Saltar al contenido principal
SEO

Pipeline de keyword research automatizado: Python + LLM + clustering (caso real)

Tutorial paso a paso para construir un pipeline que automatiza tu keyword research diario con Python, LLMs y clustering. Caso real con 5.000 keywords.

EC
Equipo Codify Tech
SEO + Data
9 min lectura

Hacer keyword research manual cada 3 meses es como limpiar tu casa una vez al año. Se ensucia más rápido de lo que la mantienes. Un pipeline automatizado te da keyword research continuo sin esfuerzo humano.

En este artículo te explico cómo construir un pipeline de keyword research automatizado con Python, LLMs y clustering. Con código, paso a paso y caso real con 5.000 keywords.

Si quieres entender el contexto más amplio, lee nuestro pilar MLOps aplicado a SEO: automatiza el keyword research y el monitoreo de contenido con IA.

El keyword research tradicional no escala

El keyword research manual tiene 4 problemas graves:

  1. Es lento: 2-3 semanas para analizar 100 keywords decentes
  2. Es estático: lo haces una vez y queda desactualizado en 1 mes
  3. Es incompleto: no analizas todas las variantes, long-tails, preguntas
  4. Es reactivo: descubres oportunidades cuando ya es tarde

Insight: si haces keyword research cada 3-6 meses, estás 90 días atrasado respecto a tu competencia que lo hace continuamente.

Arquitectura del pipeline completo

INGESTA (diario)
  ├── Google Search Console API
  ├── Google Trends API
  ├── DataForSEO SERP API
  └── GPT-5 (expansión de keywords semilla)

PROCESAMIENTO
  ├── Embeddings (OpenAI text-embedding-3-small)
  ├── BERTopic clustering
  ├── Clasificación de intención (GPT-5)
  └── Scoring (volumen × competencia × relevancia)

OUTPUT (Looker Studio + Slack)
  ├── Dashboard de clusters temáticos
  ├── Top oportunidades semanales
  └── Alertas de nuevas keywords trending

Paso 1: Ingesta de datos

Necesitas 4 fuentes de datos:

Fuente 1: Google Search Console API

from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
import pandas as pd

# Autenticación
SCOPES = ['https://www.googleapis.com/auth/webmasters.readonly']
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
creds = flow.run_local_server()

service = build('searchconsole', 'v1', credentials=creds)

# Obtener datos de los últimos 30 días
response = service.searchanalytics().query(
    siteUrl='https://tudominio.com',
    body={
        'startDate': '2026-08-01',
        'endDate': '2026-09-01',
        'dimensions': ['query', 'page'],
        'rowLimit': 25000
    }
).execute()

# Convertir a DataFrame
gsc_data = pd.DataFrame(response['rows'])
gsc_data.columns = ['keyword', 'page', 'clicks', 'impressions', 'ctr', 'position']

Output: 25.000 filas con keywords, páginas, clics, impresiones, CTR, posición.

from pytrends.request import TrendReq
import pandas as pd

pytrends = TrendReq(hl='es-CO', tz=300)

# Para cada keyword semilla, obtener tendencia
seed_keywords = ['seo colombia', 'agencia seo', 'posicionamiento web']
trends_data = []

for kw in seed_keywords:
    pytrends.build_payload([kw], timeframe='today 3-m', geo='CO')
    interest = pytrends.interest_over_time()
    if not interest.empty:
        trends_data.append({
            'keyword': kw,
            'avg_interest': interest[kw].mean(),
            'trend_direction': 'up' if interest[kw].iloc[-1] > interest[kw].iloc[0] else 'down',
            'current_interest': interest[kw].iloc[-1]
        })

Fuente 3: DataForSEO SERP API (volumen + competencia)

import requests

API_LOGIN = 'tu_login'
API_PASSWORD = 'tu_password'

# Para cada keyword, obtener volumen y competencia
def get_keyword_data(keyword):
    response = requests.post(
        'https://api.dataforseo.com/v3/keywords_data/google/search_volume/live',
        auth=(API_LOGIN, API_PASSWORD),
        json=[{
            'keyword': keyword,
            'location_code': 2404,  # Colombia
            'language_code': 'es'
        }]
    )
    return response.json()

keyword_data = []
for kw in sample_keywords:
    data = get_keyword_data(kw)
    keyword_data.append(data['tasks'][0]['result'][0])

Fuente 4: Expansión con LLM (generar long-tails)

import openai

def expand_keywords(seed_keyword, count=50):
    prompt = f"""Eres un experto en SEO para el mercado colombiano.
Dado la keyword semilla "{seed_keyword}", genera {count} variaciones
relevantes que un usuario real buscaría en Google Colombia.

Incluye:
- Long-tails específicas (3-6 palabras)
- Preguntas (qué es, cómo, cuánto, dónde)
- Variaciones con ciudades colombianas (Bogotá, Medellín, Cali, etc.)
- Intención comercial, informativa y transaccional

Devuelve solo la lista, una por línea."""
    
    response = openai.chat.completions.create(
        model="gpt-5-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.7
    )
    
    return [kw.strip() for kw in response.choices[0].message.content.split('\n') if kw.strip()]

# Expandir tus top 100 keywords
expanded_keywords = []
for seed in top_100_seeds:
    expanded_keywords.extend(expand_keywords(seed, count=30))

print(f"Total keywords: {len(expanded_keywords)}")
# Output típico: 3.000 keywords

Paso 2: Embeddings + clustering

Una vez que tienes 3.000-5.000 keywords, las agrupas por temática usando embeddings.

Generar embeddings

import openai
import numpy as np

def get_embeddings(keywords):
    response = openai.embeddings.create(
        model="text-embedding-3-small",
        input=keywords
    )
    return np.array([d.embedding for d in response.data])

embeddings = get_embeddings(expanded_keywords)
print(f"Embeddings shape: {embeddings.shape}")
# Output: (3000, 1536)

Clusterizar con BERTopic

from bertopic import BERTopic
from umap import UMAP
from hdbscan import HDBSCAN

# Reducir dimensionalidad para clustering
umap_model = UMAP(n_neighbors=15, n_components=5, min_dist=0.0, metric='cosine')

# Clustering
hdbscan_model = HDBSCAN(min_cluster_size=10, min_samples=5, metric='euclidean')

# Modelo BERTopic
topic_model = BERTopic(
    umap_model=umap_model,
    hdbscan_model=hdbscan_model,
    verbose=True
)

# Fit
topics, probs = topic_model.fit_transform(expanded_keywords, embeddings)

# Ver clusters
topic_info = topic_model.get_topic_info()
print(topic_info.head(20))

Output típico: 20-40 clusters temáticos con sus keywords representativas.

Paso 3: Clasificar intención con LLM

Para cada cluster, clasifica la intención de búsqueda (informacional, comercial, transaccional, navegacional).

def classify_intent(keyword):
    prompt = f"""Clasifica la intención de búsqueda de esta keyword: "{keyword}"

Opciones:
- Informacional: el usuario quiere aprender
- Comercial: el usuario está investigando antes de comprar
- Transaccional: el usuario quiere comprar/contratar
- Navegacional: el usuario quiere llegar a un sitio específico

Responde solo con: INFORMACIONAL, COMERCIAL, TRANSACCIONAL o NAVEGACIONAL"""

    response = openai.chat.completions.create(
        model="gpt-5-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )
    return response.choices[0].message.content.strip()

# Clasificar todas las keywords
intents = [classify_intent(kw) for kw in expanded_keywords]

Paso 4: Scoring y priorización

Cada keyword se puntúa según:

def score_keyword(keyword, volume, competition, intent, business_relevance):
    # Score base
    volume_score = min(volume / 1000, 1)  # normalizar 0-1
    competition_score = 1 - competition  # menor competition = mejor
    
    # Bonus por intención
    intent_bonus = {
        'TRANSACCIONAL': 1.0,
        'COMERCIAL': 0.8,
        'INFORMACIONAL': 0.5,
        'NAVEGACIONAL': 0.3
    }.get(intent, 0.5)
    
    # Score final
    score = (volume_score * 0.4 + competition_score * 0.3) * intent_bonus * business_relevance
    return score

# Aplicar a todas las keywords
df = pd.DataFrame({
    'keyword': expanded_keywords,
    'volume': [...],  # desde DataForSEO
    'competition': [...],  # desde DataForSEO
    'intent': intents,
    'cluster': topics
})

df['score'] = df.apply(lambda row: score_keyword(
    row['keyword'], row['volume'], row['competition'], row['intent'], 1.0
), axis=1)

# Top 50 oportunidades
top_opportunities = df.nlargest(50, 'score')

Paso 5: Output y alertas

Dashboard en Looker Studio

Exporta los datos a BigQuery y conecta Looker Studio:

from google.cloud import bigquery

client = bigquery.Client()

# Subir datos a BigQuery
df.to_gbq('proyecto.dataset.keyword_research', if_exists='replace')

# Ahora conecta Looker Studio a esa tabla y crea visualizaciones:
# - Top clusters por score
# - Distribución de intención por cluster
# - Evolución temporal de nuevas keywords
# - Keywords trending vs declining

Alertas automáticas a Slack

import requests
import json

SLACK_WEBHOOK = 'https://hooks.slack.com/services/...'

def send_slack_alert(top_opportunities):
    message = {
        "text": f"🎯 *Nuevas oportunidades de keywords* (esta semana)\n\n"
    }
    
    for idx, row in top_opportunities.head(10).iterrows():
        message["text"] += f"• *{row['keyword']}* (vol: {row['volume']}, score: {row['score']:.2f})\n"
    
    requests.post(SLACK_WEBHOOK, json=message)

# Ejecutar semanalmente
send_slack_alert(top_opportunities)

Caso real: 5.000 keywords en 1 hora

Cliente: SaaS de gestión de proyectos en Colombia Objetivo: encontrar 500 oportunidades de keywords cada mes

Implementación:

  1. Pipeline completo (2 semanas de desarrollo):

    • Python + Airflow como orquestador
    • Google Search Console API (queries de 90 días)
    • GPT-5 para expansión
    • BERTopic para clustering
    • BigQuery para almacenar histórico
    • Looker Studio para dashboards
    • Slack para alertas semanales
  2. Pipeline en producción:

    • Corre semanal (cada lunes 6am)
    • Genera 3.000-5.000 keywords nuevas
    • Clusteriza en 30-40 temas
    • Prioriza top 50 oportunidades
    • Manda alerta a Slack
  3. Después de 6 meses:

    • 200 artículos nuevos creados (basados en oportunidades detectadas)
    • Tráfico orgánico +180%
    • Keywords en Top 10: de 80 a 240
    • Tiempo del equipo SEO: -40% (automatización)

Insight: el pipeline no reemplazó al equipo. Les dio tiempo para escribir mejor contenido mientras el pipeline hacía el trabajo mecánico.

Optimizaciones avanzadas

Una vez que el pipeline básico funciona, puedes agregar:

Detección de keyword gaps con competencia

# Comparar keywords que rankeas vs keywords que rankea tu competencia
def find_keyword_gaps(your_keywords, competitor_keywords):
    your_set = set(your_keywords)
    competitor_set = set(competitor_keywords)
    
    # Keywords donde rankeas pero no tu competencia (mantener)
    yours_only = your_set - competitor_set
    
    # Keywords donde rankea competencia pero no tú (gap)
    gaps = competitor_set - your_set
    
    # Keywords donde ambos rankean (oportunidad de superar)
    contested = your_set & competitor_set
    
    return gaps, yours_only, contested

gaps, yours_only, contested = find_keyword_gaps(
    your_keywords=df[df['position'] <= 20]['keyword'],
    competitor_keywords=competitor_data['keywords']
)

Detección de canibalización

# Keywords donde 2+ páginas tuyas rankean
def detect_cannibalization(gsc_data):
    kw_pages = gsc_data.groupby('keyword')['page'].nunique()
    cannibalized = kw_pages[kw_pages > 1].index
    
    return gsc_data[gsc_data['keyword'].isin(cannibalized)].sort_values('impressions', ascending=False)

cannibalized = detect_cannibalization(gsc_data)
# Output: keywords donde tienes 2+ páginas rankeando
# Acción: consolidar con 301 o diferenciar más

Predicción de tendencia

from prophet import Prophet
import pandas as pd

def predict_keyword_trend(keyword_history):
    """keyword_history: DataFrame con columnas 'ds' (fecha) y 'y' (búsquedas)"""
    
    model = Prophet(yearly_seasonality=True, weekly_seasonality=True)
    model.fit(keyword_history)
    
    future = model.make_future_dataframe(periods=90)  # 90 días
    forecast = model.predict(future)
    
    return forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail(90)

Los 5 errores comunes

Empezar con embeddings sin limpiar las keywords — si hay keywords duplicadas o mal formadas, el cluster falla

Usar solo GSC como fuente — pierdes keywords que NO te están dando tráfico pero podrían

No actualizar el pipeline — los datos de hace 6 meses son menos valiosos que los de hoy

Confiar 100% en el scoring del LLM — el humano debe revisar top oportunidades antes de actuar

Ignorar el costo — GPT-5 para 5.000 keywords cuesta ~$10 por ejecución. Si lo corres semanal, son $40/mes solo en LLM. Optimiza con batches o usa modelos más baratos para tareas simples.

Cuándo NO vale la pena este pipeline

  • < 500 keywords en tu nicho: keyword research manual es suficiente
  • Nicho ultra competido con miles de keywords: necesitas ML más serio (no BERTopic básico)
  • Presupuesto < $200/mes: los costos de API + herramientas se comen el ROI

Regla práctica: si tienes menos de 50 artículos en tu web, no necesitas este pipeline. Empieza con SEO básico.


Siguiente paso

¿Quieres implementar un pipeline de keyword research automatizado? En Codify Tech diseñamos pipelines a medida según tu tamaño y presupuesto. Hacemos una auditoría + prototipo donde te mostramos:

  • Qué fuentes de datos necesitas
  • Arquitectura técnica recomendada
  • Estimación de costos mensuales
  • Plan de implementación en 2-4 semanas

Sin compromiso. Sin venta forzada.

👉 Solicita tu auditoría de pipeline SEO o escríbenos a [email protected].

Otros artículos del cluster MLOps para SEO que te van a servir:

Etiquetas

#keyword research #pipeline seo #python seo #bertopic #clustering keywords