Cuaderno de apoyo en Python para construir el corpus
Este cuaderno acompaña las ocho semanas. Está escrito para alguien que nunca ha programado: cada bloque lleva antes una explicación de qué hace y por qué, y se puede copiar tal cual en una celda del cuaderno propio. Los bloques usan el caso demostrativo del curso; para el corpus propio basta cambiar las rutas y los nombres. Todo el software es libre.
Regla del curso: antes de cada bloque de código, una celda de texto con sus palabras que diga qué decisión encarna. Después de cada sesión, reiniciar el núcleo y ejecutar todo de arriba abajo.
Preparación
Instale Python 3.11 o superior desde https://www.python.org/ y, en una terminal, las bibliotecas del curso:
pip install jupyterlab pandas requests beautifulsoup4
Abra el cuaderno con jupyter lab. Si la instalación no es posible, cualquier servicio gratuito de cuadernos en la nube sirve; suba la carpeta crudo y trabaje igual.
Estructura de carpetas del proyecto, que el cuaderno espera:
mi-corpus/
crudo/ archivos tal como llegaron; no se editan
limpio/ archivos en UTF-8, limpios
cuadernos/ los cuadernos del curso
decisiones.csv decisiones a mano, con fecha
catalogo.csv generado por el cuaderno
Semana 2. Leer archivos y escribir el primer registro
Una cadena es un texto en una variable. Se le pueden hacer preguntas que la máquina responde sin interpretar.
texto = "El día de ayer se reunió la junta en el salón de sesiones del concejo."
print(len(texto)) # cuántos caracteres
print(texto[:20]) # los primeros veinte
print("junta" in texto) # ¿contiene la palabra?
print(texto.lower()) # todo en minúsculas
print(texto.split()[:5]) # las primeras cinco palabras
Abrir un archivo exige declarar su codificación. El curso usa UTF-8 siempre.
from pathlib import Path
ruta = Path("crudo") / "P-0001.txt"
texto = ruta.read_text(encoding="utf-8")
print(ruta.name, len(texto), "caracteres")
print(texto.splitlines()[0]) # la primera línea
pathlib recorre una carpeta y devuelve sus archivos como una lista. Esa lista es la primera forma del corpus.
rutas = sorted(Path("crudo").glob("*.txt"))
print(len(rutas), "archivos")
for r in rutas[:3]:
print(r.name)
Un diccionario es un registro con campos nombrados. Una lista de diccionarios es un catálogo rudimentario. El bucle hace con cada archivo lo que el investigador haría con uno.
import datetime
registro = []
for r in rutas:
t = r.read_text(encoding="utf-8")
registro.append({
"archivo": r.name,
"bytes": r.stat().st_size,
"caracteres": len(t),
"primera_linea": t.splitlines()[0] if t.strip() else "",
"fecha_lectura": datetime.datetime.now().isoformat(timespec="minutes"),
})
registro[0]
Una función es un criterio escrito una vez. Esta encarna un criterio del protocolo preliminar: entra lo que tiene más de doscientos caracteres y contiene una fecha con cuatro dígitos. Cambie el criterio por el suyo y escriba arriba de dónde viene.
import re
def entra(texto):
"""Criterio 1 del protocolo: longitud mínima y fecha legible."""
tiene_fecha = re.search(r"\b1[89]\d\d\b|\b20\d\d\b", texto) is not None
return len(texto) > 200 and tiene_fecha
for fila, r in zip(registro, rutas):
fila["entra"] = entra(r.read_text(encoding="utf-8"))
El registro se escribe en un archivo CSV que cualquier hoja de cálculo abre. Es el primer producto del código.
import csv
campos = list(registro[0].keys())
with open("registro.csv", "w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fieldnames=campos)
w.writeheader()
w.writerows(registro)
print("registro.csv escrito con", len(registro), "filas")
Semana 3. Recolección reproducible
Antes de pedir nada, se lee lo que el sitio declara. Python trae un lector de robots.txt en la biblioteca estándar.
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://www.datos.gov.co/robots.txt")
rp.read()
print(rp.can_fetch("corpus-curso2", "https://www.datos.gov.co/resource/"))
La función de descarga encarna la cortesía técnica: se identifica, espera, guarda lo que llegó sin tocarlo, calcula el hash y anota la bitácora. Cambie el correo por el suyo. Si el sitio responde con error, la función lo anota y no insiste.
import requests, time, hashlib, json, datetime
from pathlib import Path
CABECERAS = {"User-Agent": "corpus-curso2 (investigacion; su-correo@ejemplo.org)"}
PAUSA = 1.0 # segundos entre peticiones
def sha256_bytes(b):
return hashlib.sha256(b).hexdigest()
def descargar(url, destino, bitacora="bitacora_descarga.jsonl"):
"""Pide url, guarda la respuesta intacta en destino y anota la bitácora."""
fila = {"url": url, "destino": str(destino),
"fecha_descarga": datetime.datetime.now().isoformat(timespec="minutes")}
try:
r = requests.get(url, headers=CABECERAS, timeout=30)
fila["estado"] = r.status_code
if r.ok:
Path(destino).write_bytes(r.content)
fila["hash_sha256"] = sha256_bytes(r.content)
fila["bytes"] = len(r.content)
except requests.RequestException as e:
fila["estado"] = "error"
fila["detalle"] = str(e)
with open(bitacora, "a", encoding="utf-8") as f:
f.write(json.dumps(fila, ensure_ascii=False) + "\n")
time.sleep(PAUSA)
return fila
La lista de lo que se pide se escribe antes y se revisa a mano. Aquí, una norma de la Secretaría del Senado y un conjunto de datos abiertos por su interfaz de programación. El protocolo dice qué va en esta lista; el programa solo la recorre.
Path("crudo").mkdir(exist_ok=True)
pedidos = [
("http://www.secretariasenado.gov.co/senado/basedoc/ley_0023_1982.html", "crudo/N-0001.html"),
("https://www.datos.gov.co/resource/XXXX-XXXX.csv?$limit=500", "crudo/D-0001.csv"),
]
for url, destino in pedidos:
print(descargar(url, destino))
Reemplace XXXX-XXXX por el identificador del conjunto de datos que eligió en datos.gov.co; aparece en la dirección de cada conjunto. Para extraer el texto de una página HTML se usa Beautiful Soup, pero eso ocurre en la semana 5, sobre una copia: crudo no se toca.
Comparar dos ejecuciones es comparar hash. Este bloque lee la bitácora y señala las direcciones cuyo contenido cambió.
import pandas as pd
b = pd.read_json("bitacora_descarga.jsonl", lines=True)
cambios = b.groupby("url")["hash_sha256"].nunique()
print(cambios[cambios > 1]) # vacío si nada cambió entre ejecuciones
Semana 4. El catálogo maestro con pandas
El hash de cada archivo en crudo es la base del catálogo. Se calcula sobre los bytes, no sobre el texto, para que no dependa de la codificación.
import hashlib
from pathlib import Path
import pandas as pd
def sha256_archivo(ruta):
return hashlib.sha256(Path(ruta).read_bytes()).hexdigest()
filas = []
for r in sorted(Path("crudo").iterdir()):
if r.is_file():
filas.append({"ruta_crudo": str(r).replace("\\", "/"),
"hash_sha256": sha256_archivo(r)})
hashes = pd.DataFrame(filas)
hashes.head()
La bitácora de descarga aporta la dirección y la fecha de descarga; se une por la ruta. Los identificadores se asignan en orden de descarga y no se reutilizan.
b = pd.read_json("bitacora_descarga.jsonl", lines=True)
b = b[b["estado"] == 200].rename(columns={"destino": "ruta_crudo"})
cat = hashes.merge(b[["ruta_crudo", "url", "fecha_descarga"]], on="ruta_crudo", how="left")
cat = cat.sort_values("fecha_descarga").reset_index(drop=True)
cat["id"] = ["P-%04d" % (i + 1) for i in range(len(cat))]
Los duplicados exactos comparten hash. El primero se conserva; los demás se anotan y se excluyen con el criterio E1, sin perder su identificador.
dup = cat.duplicated("hash_sha256", keep="first")
primero = cat.drop_duplicates("hash_sha256").set_index("hash_sha256")["id"]
cat["notas"] = ""
cat.loc[dup, "notas"] = "Duplicado exacto de " + cat.loc[dup, "hash_sha256"].map(primero) + "; excluido"
cat["criterio_inclusion"] = ""
cat.loc[dup, "criterio_inclusion"] = "E1"
print(dup.sum(), "duplicados exactos")
Las decisiones a mano viven en decisiones.csv, con fecha. El cuaderno las lee y las une por id. Así el catálogo sigue siendo regenerable aunque contenga decisiones que ningún programa toma.
dec = pd.read_csv("decisiones.csv", dtype=str).fillna("")
cat = cat.merge(dec, on="id", how="left", suffixes=("", "_dec"))
for col in ["titulo", "autor", "fuente", "fecha_documento", "escena", "derechos", "ruta_limpio"]:
if col not in cat.columns:
cat[col] = ""
# una decisión a mano sobre criterio o notas tiene prioridad sobre lo automático
for col in ["criterio_inclusion", "notas"]:
if col + "_dec" in cat.columns:
cat[col] = cat[col + "_dec"].where(cat[col + "_dec"] != "", cat[col])
cat = cat.drop(columns=[col + "_dec"])
Las cinco reglas de integridad en una función. Se ejecuta cada vez que el catálogo cambia; devuelve las filas que fallan y por qué.
import re
OBLIGATORIOS = ["id", "titulo", "fuente", "url", "fecha_documento", "fecha_descarga",
"escena", "derechos", "criterio_inclusion", "hash_sha256", "ruta_crudo"]
CRITERIOS = {"1", "2", "3", "4", "E1", "E2"} # los del protocolo
def validar(cat):
fallas = []
for i, f in cat.iterrows():
vacios = [c for c in OBLIGATORIOS if str(f.get(c, "")).strip() == ""]
if vacios:
fallas.append((f["id"], "regla 1: vacíos " + ", ".join(vacios)))
fd, fc = str(f["fecha_documento"]), str(f["fecha_descarga"])
if not re.fullmatch(r"\d{4}(-\d{2}(-\d{2})?)?", fd) or fc[:10] < fd[:10]:
fallas.append((f["id"], "regla 2: fechas"))
if Path(f["ruta_crudo"]).exists() and sha256_archivo(f["ruta_crudo"]) != f["hash_sha256"]:
fallas.append((f["id"], "regla 3: hash no coincide"))
if str(f["criterio_inclusion"]) not in CRITERIOS:
fallas.append((f["id"], "regla 5: criterio no está en el protocolo"))
repetidos = cat["id"][cat["id"].duplicated()].tolist()
for r in repetidos:
fallas.append((r, "regla 4: id repetido"))
return pd.DataFrame(fallas, columns=["id", "falla"])
fallas = validar(cat)
print(len(fallas), "fallas")
fallas
El catálogo se guarda con las columnas en el orden del esquema, y se producen los tres conteos que describen el corpus.
ORDEN = ["id", "titulo", "autor", "fuente", "url", "fecha_documento", "fecha_descarga", "escena",
"derechos", "criterio_inclusion", "hash_sha256", "ruta_crudo", "ruta_limpio", "notas"]
cat = cat.reindex(columns=ORDEN).fillna("")
cat.to_csv("catalogo.csv", index=False, encoding="utf-8")
print(cat.groupby("fuente").size())
print(cat.groupby(cat["fecha_documento"].str[:4]).size())
print(cat.groupby("criterio_inclusion").size())
Semana 5. Limpieza y normalización
La codificación se detecta probando: si UTF-8 falla, se intenta CP1252. Lo que se escribe en limpio es UTF-8 siempre. Después se busca el rastro del error.
from pathlib import Path
import unicodedata, re
def leer_con_deteccion(ruta):
b = Path(ruta).read_bytes()
for enc in ("utf-8", "cp1252", "latin-1"):
try:
return b.decode(enc), enc
except UnicodeDecodeError:
continue
raise ValueError("no se pudo decodificar " + str(ruta))
texto, enc = leer_con_deteccion("crudo/P-0001.txt")
print(enc, "|", texto.count("Ã"), "apariciones de Ã")
Si el archivo es HTML, el texto se extrae con Beautiful Soup sobre la copia leída, nunca sobre crudo.
from bs4 import BeautifulSoup
html, enc = leer_con_deteccion("crudo/N-0001.html")
soup = BeautifulSoup(html, "html.parser")
for etiqueta in soup(["script", "style", "nav", "header", "footer"]):
etiqueta.decompose()
texto = soup.get_text("\n")
print(texto[:300])
Cada operación de limpieza es un patrón con su antes y después. Este quita un encabezado de periódico y números de página; cambie el patrón por el que su fuente necesite y muestre siempre lo que quitó.
def quitar_encabezados(t):
antes = len(t)
t = re.sub(r"^\s*EL TIEMPO.*$", "", t, flags=re.MULTILINE) # encabezado
t = re.sub(r"^\s*p[áa]g\.?\s*\d+\s*$", "", t, flags=re.MULTILINE) # paginación
return t, antes - len(t)
limpio, quitados = quitar_encabezados(texto)
print(quitados, "caracteres quitados")
La normalización unifica la forma Unicode, los espacios, las comillas y los guiones. No toca mayúsculas ni tildes: esa es una decisión del curso, escrita aquí.
def normalizar(t):
t = unicodedata.normalize("NFC", t)
t = t.replace("“", '"').replace("”", '"').replace("«", '"').replace("»", '"')
t = t.replace("‘", "'").replace("’", "'")
t = re.sub(r"[‐-―]", "-", t) # guiones de distintas longitudes
t = re.sub(r"(\w)-\n(\w)", r"\1\2", t) # palabra cortada al final de línea
t = re.sub(r"[ \t]+", " ", t)
t = re.sub(r"\n{3,}", "\n\n", t)
return t.strip()
limpio = normalizar(limpio)
Path("limpio").mkdir(exist_ok=True)
Path("limpio/P-0001.txt").write_text(limpio, encoding="utf-8")
La segmentación produce la tabla de unidades: una fila por unidad con el id de la pieza y la posición. Aquí por oración, con un patrón simple que el estudiante puede ajustar; por párrafo bastaría dividir por línea en blanco.
import pandas as pd
def segmentar_oraciones(t):
partes = re.split(r"(?<=[.!?])\s+(?=[A-ZÁÉÍÓÚÑ¿¡\"'])", t)
return [p.strip() for p in partes if p.strip()]
unidades = []
for ruta in sorted(Path("limpio").glob("*.txt")):
pid = ruta.stem
for i, s in enumerate(segmentar_oraciones(ruta.read_text(encoding="utf-8")), start=1):
unidades.append({"id": pid, "posicion": i, "texto": s})
U = pd.DataFrame(unidades)
U.to_csv("unidades.csv", index=False, encoding="utf-8")
print(len(U), "unidades")
Estimar el error de OCR es leer cien palabras y contar. El programa solo ayuda a elegirlas al azar y a registrar el resultado en decisiones.csv o en notas.
import random
palabras = re.findall(r"\w+", Path("limpio/P-0001.txt").read_text(encoding="utf-8"))
random.seed(7)
inicio = random.randint(0, max(0, len(palabras) - 100))
print(" ".join(palabras[inicio:inicio + 100]))
# cuente a mano los errores y anote: error_ocr = errores / 100
Semana 6. Primera lectura computacional
Contar palabras exige decidir qué es una palabra. Aquí: secuencias de letras, en minúsculas, con tildes conservadas. La lista de palabras funcionales que se quita está escrita y se puede cambiar.
from collections import Counter
import pandas as pd, re
U = pd.read_csv("unidades.csv", dtype=str)
FUNCIONALES = set("""de la el y a en los las que del se por con su sus o al un una es no lo para
como más ni ante sin sobre son le les ha han e u cual entre todo toda todos todas esta este esto
estos estas ser será serán hay sea sean tiene tienen""".split())
def tokens(t):
return re.findall(r"[a-záéíóúüñ]+", t.lower())
todos = [w for t in U["texto"] for w in tokens(t)]
frec = Counter(todos)
print(len(todos), "palabras;", len(frec), "distintas")
print(frec.most_common(10))
contenido = Counter({w: c for w, c in frec.items() if w not in FUNCIONALES})
print(contenido.most_common(20))
La concordancia muestra la palabra nodo con su compañía, y devuelve el id y la posición para que la línea se pueda citar. La ventana se mide en palabras.
def kwic(U, nodo, ventana=5):
filas = []
patron = re.compile(r"\b" + re.escape(nodo) + r"\b", re.IGNORECASE)
for _, u in U.iterrows():
ws = u["texto"].split()
for i, w in enumerate(ws):
if patron.fullmatch(re.sub(r"^\W+|\W+$", "", w)):
izq = " ".join(ws[max(0, i - ventana):i])
der = " ".join(ws[i + 1:i + 1 + ventana])
filas.append({"id": u["id"], "posicion": u["posicion"], "izquierda": izq, "nodo": w, "derecha": der})
return pd.DataFrame(filas)
K = kwic(U, "orden", ventana=5)
print(len(K), "líneas")
for _, k in K.head(20).iterrows():
print("%-40s %-10s %s (%s, %s)" % (k["izquierda"][-40:], k["nodo"], k["derecha"][:40], k["id"], k["posicion"]))
Las colocaciones cuentan las palabras en la ventana y las comparan con su frecuencia total. Se descartan las que aparecen pocas veces en ventana, porque una razón alta con tres apariciones no vale nada.
def colocaciones(U, nodo, ventana=4, minimo=5):
en_ventana = Counter()
for _, k in kwic(U, nodo, ventana).iterrows():
for w in tokens(k["izquierda"]) + tokens(k["derecha"]):
if w not in FUNCIONALES:
en_ventana[w] += 1
filas = [{"palabra": w, "en_ventana": c, "total": frec[w], "razon": round(c / frec[w], 3)}
for w, c in en_ventana.items() if c >= minimo]
return pd.DataFrame(filas).sort_values(["en_ventana", "razon"], ascending=False)
colocaciones(U, "orden")
Un dominio léxico es una lista escrita por el investigador. Se cuenta por pieza y se compara por las columnas del catálogo como proporción por mil palabras.
DOMINIOS = {
"orden": ["orden", "policía", "autoridad", "ley", "decreto", "tribunal"],
"trabajo": ["trabajo", "obreros", "jornal", "salario", "huelga", "fábrica"],
}
cat = pd.read_csv("catalogo.csv", dtype=str)
def peso_dominios(texto):
ws = tokens(texto)
n = max(1, len(ws))
return {d: 1000 * sum(ws.count(p) for p in lista) / n for d, lista in DOMINIOS.items()}
por_pieza = U.groupby("id")["texto"].apply(" ".join).apply(peso_dominios).apply(pd.Series)
por_pieza = por_pieza.merge(cat[["id", "fuente", "fecha_documento"]], left_index=True, right_on="id")
por_pieza["anio"] = por_pieza["fecha_documento"].str[:4]
print(por_pieza.groupby("fuente")[list(DOMINIOS)].mean().round(2))
print(por_pieza.groupby("anio")[list(DOMINIOS)].mean().round(2))
Exportar a CSV lo que va al informe, con el nombre de la celda que lo produjo en el nombre del archivo.
Path("salidas").mkdir(exist_ok=True)
K.to_csv("salidas/kwic_orden_v5.csv", index=False, encoding="utf-8")
colocaciones(U, "orden").to_csv("salidas/colocaciones_orden.csv", index=False, encoding="utf-8")
Semana 7. Lectura asistida auditada
La instrucción se guarda como archivo con versión. El programa la lee, la envía con cada unidad y registra cada respuesta con fecha, modelo, versión e identificador. La biblioteca openai sirve para cualquier proveedor compatible con su interfaz; varios ofrecen cuota gratuita. Las credenciales van en un archivo .env que no se entrega.
pip install openai python-dotenv
import os, json, datetime, random
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
import pandas as pd
load_dotenv()
client = OpenAI(api_key=os.getenv("API_KEY"), base_url=os.getenv("BASE_URL"))
MODEL = os.getenv("MODEL")
VERSION = "instruccion_v1"
INSTRUCCION = Path(VERSION + ".md").read_text(encoding="utf-8")
def etiquetar(unidad_id, texto, registro="llamadas.jsonl"):
r = client.chat.completions.create(
model=MODEL, temperature=0,
messages=[{"role": "system", "content": INSTRUCCION},
{"role": "user", "content": texto}])
salida = r.choices[0].message.content.strip()
with open(registro, "a", encoding="utf-8") as f:
f.write(json.dumps({"fecha": datetime.datetime.now().isoformat(timespec="minutes"),
"modelo": MODEL, "version": VERSION, "id": unidad_id,
"respuesta": salida}, ensure_ascii=False) + "\n")
return salida
La muestra se elige al azar con semilla fija, para que la selección sea reproducible. El modelo etiqueta la muestra; el estudiante la etiqueta a ciegas en otro archivo, sin abrir el primero.
U = pd.read_csv("unidades.csv", dtype=str)
random.seed(2027)
muestra = U.sample(n=30, random_state=2027).copy()
muestra["etiqueta_modelo"] = [etiquetar(i, t) for i, t in zip(muestra["id"] + "-" + muestra["posicion"], muestra["texto"])]
muestra[["id", "posicion", "texto", "etiqueta_modelo"]].to_csv("muestra_modelo.csv", index=False, encoding="utf-8")
muestra[["id", "posicion", "texto"]].assign(mi_etiqueta="").to_csv("muestra_a_ciegas.csv", index=False, encoding="utf-8")
Cuando muestra_a_ciegas.csv esté llena, se comparan las dos tablas: proporción de acuerdo y kappa de Cohen, calculado en diez líneas sin bibliotecas.
m = pd.read_csv("muestra_modelo.csv", dtype=str)
h = pd.read_csv("muestra_a_ciegas.csv", dtype=str)
c = m.merge(h, on=["id", "posicion", "texto"])
a, b = c["etiqueta_modelo"].str.strip().str.lower(), c["mi_etiqueta"].str.strip().str.lower()
def kappa(a, b):
n = len(a)
po = sum(x == y for x, y in zip(a, b)) / n
cats = set(a) | set(b)
pe = sum((list(a).count(k) / n) * (list(b).count(k) / n) for k in cats)
return (po - pe) / (1 - pe) if pe < 1 else 1.0
print("acuerdo:", round((a == b).mean(), 3), " kappa:", round(kappa(a, b), 3))
desacuerdos = c[a != b][["id", "posicion", "texto", "etiqueta_modelo", "mi_etiqueta"]]
desacuerdos.assign(tipo="", quien_tenia_razon="").to_csv("desacuerdos.csv", index=False, encoding="utf-8")
print(len(desacuerdos), "desacuerdos para tipificar a mano")
El registro de frontera por programa: una fila por operación, que al final se convierte en la tabla de la plantilla.
def decidir(semana, operacion, propuesta, inferencia, decision, criterio, responde, archivo="frontera.jsonl"):
with open(archivo, "a", encoding="utf-8") as f:
f.write(json.dumps({"semana": semana, "operacion": operacion, "propuso_el_modelo": propuesta,
"inferencia": inferencia, "decidi": decision, "criterio": criterio,
"responde": responde}, ensure_ascii=False) + "\n")
decidir(7, "Etiquetas sobre la muestra", "30 etiquetas con instruccion_v1", "I",
"corregir la instrucción y repetir", "8 de 9 desacuerdos fuerzan la categoría 'conflicto' donde no aplica", "yo")
pd.read_json("frontera.jsonl", lines=True).to_csv("registro-de-frontera.csv", index=False)
Semana 8. Documentar para reconstruir
El archivo de versiones se produce desde el cuaderno. Sin él, el cuaderno que hoy corre puede dejar de correr.
import subprocess, sys
req = subprocess.run([sys.executable, "-m", "pip", "freeze"], capture_output=True, text=True).stdout
Path("requirements.txt").write_text(req, encoding="utf-8")
print(req[:300])
La comprobación final: el hash del catálogo. Quien reconstruya el corpus debe obtener el mismo.
print("catalogo.csv:", sha256_archivo("catalogo.csv"))
Nota sobre costos y privacidad
Use cuotas gratuitas y material sin datos personales de terceros. Si su proyecto incluye material sensible, haga la práctica de la semana 7 con una muestra anonimizada o con material público. No entregue el archivo .env. Exterioridad y gobernanza de la IA en ciencias sociales trata estos problemas en detalle: derechos sobre el material, datos personales y consentimiento, con una matriz de riesgos y derechos por tipo de pieza del corpus.
Práctica 3. Recolección reproducible
Duración estimada: 3 horas, de las cuales 1 en la sesión sincrónica.
Requisitos: el cuaderno de la semana 2 funcionando, las bibliotecas requests y pandas instaladas, la ficha de delimitación revisada y la plantilla plantillas/protocolo-de-corpus.md. Bloques de código en practicas/cuaderno-apoyo.md, sección "Semana 3".
Objetivo
Ejecutar una recolección pequeña sobre el caso demostrativo y sobre una fuente propia, de modo que otra persona pueda repetirla con el mismo cuaderno y obtener el mismo resultado, y producir con eso el anexo del protocolo de corpus.
Antes de la sesión
- Elija una fuente propia con no más de diez piezas para esta práctica. Puede ser la más fácil de sus fuentes; la práctica es sobre el método, no sobre el volumen.
- Abra en el navegador el
robots.txtde esa fuente (la dirección del sitio seguida de/robots.txt) y sus términos de uso. Anote qué dicen sobre descarga y reproducción. - Escriba a mano la lista de las piezas que va a pedir, con su dirección completa. Revísela. Esa lista es el insumo del programa, no su resultado.
- Cree la carpeta
crudovacía dentro de su proyecto.
Tres rondas durante la sesión
Ronda 1. Leer lo que el sitio declara
Con el bloque de robots.txt del cuaderno de apoyo, compruebe por programa lo que leyó a mano para datos.gov.co, para la Secretaría del Senado y para su fuente. Anote en el cuaderno, en una celda de texto, qué permite cada uno y qué ritmo de petición va a usar. Si su fuente no permite la descarga automática, la práctica continúa con recolección manual: descargue cada pieza con el navegador, guárdela en crudo y llene la bitácora a mano con los mismos campos.
Ronda 2. Descargar el caso demostrativo
Ejecute la función descargar sobre la norma de la Secretaría del Senado y sobre un conjunto de datos.gov.co. Abra la bitácora bitacora_descarga.jsonl y lea una fila: debe tener la dirección, el destino, la fecha, el estado, el hash y el tamaño. Abra el archivo de crudo sin editarlo y confirme que es lo que la fuente mostraba.
Preguntas que debe poder responder antes de seguir:
- ¿Cuánto esperó el programa entre las dos peticiones? ¿Dónde está escrito?
- ¿Con qué nombre y correo se identificó? ¿Dónde está escrito?
- Si la fuente hubiera respondido con error, ¿qué habría hecho el programa?
Ronda 3. Descargar la fuente propia
Pase su lista revisada al bloque pedidos y ejecute. Al terminar, reinicie el núcleo, borre nada, y ejecute el cuaderno completo de nuevo. Compare los hash de las dos ejecuciones con el bloque de comparación. Si coinciden, su recolección es reproducible hoy. Si no coinciden, averigüe por qué: la fuente cambió, la página incluye la fecha de consulta, o algo en el programa no es determinista. Las tres cosas se anotan.
Después de la sesión
- Llene en el protocolo la tabla de fuentes con lo aprendido: forma de entrega, robots.txt y términos, ritmo, método.
- Llene la tabla de lo que no se descarga. Debe tener al menos una fila; si no la tiene, no ha mirado con cuidado.
- Guarde el cuaderno de recolección como
cuadernos/01-recoleccion.ipynby adjúntelo al protocolo. - Anote en el registro de frontera qué código propuso el modelo, qué corrigió usted antes de ejecutarlo y por qué.
Qué se evalúa
Que el cuaderno corra de arriba abajo sobre una fuente propia con bitácora, pausa e identificación, y que el protocolo diga lo que no se descarga con su razón. Una recolección de tres piezas bien registrada vale más que una de trescientas sin bitácora.
Variante para grupos
Dos estudiantes intercambian cuadernos y listas, y cada uno ejecuta la recolección del otro. Las diferencias de hash entre lo que obtuvo el autor y lo que obtuvo el par son el mejor material de discusión de la sesión siguiente.
Práctica 6. Primera lectura computacional del corpus propio
Duración estimada: 4 horas, de las cuales 1 en la sesión sincrónica.
Requisitos: el corpus propio en limpio con la tabla unidades.csv de la semana 5, el catálogo maestro validado, y la mini app apps/concordancias.html para ensayar antes de programar. Bloques de código en practicas/cuaderno-apoyo.md, sección "Semana 6".
Objetivo
Aplicar los cuatro instrumentos de la semana (frecuencias, concordancia, colocaciones, dominios léxicos) al corpus propio, y producir para cada uno la tabla que lo documenta: qué produce, qué sugiere y qué líneas se leyeron para decirlo.
Antes de la sesión
- Elija desde el protocolo tres palabras nodo: palabras que su pregunta de investigación necesita entender en este corpus. No elija las más frecuentes; elija las que importan.
- Abra la mini app de concordancias y ensaye con el corpus de demostración: cambie la ventana, mire cómo cambian las colocaciones, lea las líneas. Anote qué espera encontrar en su propio corpus para cada palabra nodo.
- Escriba a mano una primera lista de palabras para dos dominios léxicos de su proyecto.
Cuatro instrumentos durante la sesión
Frecuencias
Ejecute el bloque de frecuencias con y sin palabras funcionales. Anote en una celda de texto la lista de funcionales que quitó y por qué esas. Mire las veinte palabras de contenido más frecuentes y responda: ¿cuántas esperaba? ¿Cuáles no? Las que no esperaba son candidatas a palabra nodo para una segunda vuelta.
Concordancia
Para cada palabra nodo, produzca la concordancia con ventana de cinco y lea veinte líneas, con su identificador y posición. Mientras lee, clasifique los usos: si "orden" aparece como "orden público" y como "orden del día", son dos palabras que el conteo fundió. Anote cuántos usos distintos encontró y cuál predomina.
Colocaciones
Produzca las colocaciones de cada nodo con ventana de cuatro y mínimo de cinco apariciones. Si el mínimo deja la lista vacía, su corpus es pequeño para este instrumento y eso se anota; no baje el mínimo para que aparezca algo. Para la colocación más alta de cada nodo, vuelva a la concordancia y lea las líneas donde ocurre: ¿la asociación significa lo que parece?
Dominios léxicos
Pida al modelo palabras para cada dominio dándole el nombre del dominio y diez líneas de concordancia. Acepte o rechace una por una; anote las rechazadas con la razón. Calcule el peso de cada dominio por fuente y por año como proporción por mil palabras. Mire la tabla y escriba una frase sobre lo que sugiere, y debajo, las líneas que leyó para escribirla.
Después de la sesión
- Llene la tabla de instrumentos:
| Instrumento | Qué produce | Qué sugiere | Qué líneas se leyeron |
|---|---|---|---|
| Frecuencias | |||
| Concordancia de … | |||
| Colocaciones de … | |||
| Dominio … por fuente |
- Exporte a
salidas/cada tabla con el nombre de la celda que la produjo. - Guarde el cuaderno como
cuadernos/04-lectura.ipynb, reinicie y ejecute de arriba abajo. - Anote en el registro de frontera las palabras que propuso el modelo y la interpretación que propuso de las colocaciones, con su decisión.
Qué se evalúa
La relación entre lo que el instrumento sugiere y lo que usted afirma. Una tabla de colocaciones seguida de "el corpus asocia orden con policía" sin líneas leídas no está en nivel Logrado. La misma tabla seguida de "en 14 de las 19 líneas la asociación es con la policía municipal; en las otras 5 'orden' es orden del día; se lee como X por Y" sí.
Variante para grupos
Dos estudiantes con corpus distintos eligen la misma palabra nodo y comparan concordancias. Lo que la palabra hace en un corpus y no en el otro es la mejor demostración de que la frecuencia no es el sentido.
Práctica 7. Auditoría de la lectura asistida
Duración estimada: 4 horas, de las cuales 1 en la sesión sincrónica.
Requisitos: la tabla unidades.csv del corpus propio, acceso a un modelo de lenguaje (versión web gratuita o interfaz de programación con cuota gratuita), la plantilla plantillas/registro-de-frontera-corpus.md. Bloques de código en practicas/cuaderno-apoyo.md, sección "Semana 7". La práctica se puede hacer sin programar, con la versión web del modelo y dos hojas de cálculo; el cuaderno solo la hace repetible.
Objetivo
Hacer que un modelo etiquete una muestra del corpus con una instrucción escrita y versionada, etiquetar la misma muestra a ciegas, medir el desacuerdo, tipificarlo, decidir qué se hace con las etiquetas del resto del corpus, y dejar todo en el registro de frontera.
Antes de la sesión
- Escriba la lista de categorías con que quiere etiquetar las unidades de su corpus. Entre tres y siete. Para cada una, una definición de dos líneas y un ejemplo de su corpus.
- Escriba la regla para la duda: qué debe hacer el etiquetador cuando ninguna categoría aplica o cuando aplican dos.
- Reúna todo en
instruccion_v1.md, con el formato de salida al final: una sola palabra, la categoría, sin explicación. - Elija la muestra con el bloque del cuaderno (treinta unidades al azar con semilla fija) o, sin programar, numerando las unidades y tomando treinta números de una tabla de azar. Anote cómo la eligió.
Tres rondas durante la sesión
Ronda 1. La máquina etiqueta
Envíe cada unidad de la muestra al modelo con la instrucción, por programa o pegando en la versión web la instrucción seguida de la unidad. Guarde cada respuesta con la fecha, el modelo y la versión de la instrucción. Cierre el archivo y no lo vuelva a abrir hasta la ronda 3.
Ronda 2. Usted etiqueta a ciegas
En una tabla aparte, con la misma instrucción delante, etiquete las treinta unidades. No mire lo que dijo el modelo. Si una unidad le hace dudar, aplique la regla para la duda y anote que dudó: esas unidades van a ser las interesantes.
Ronda 3. Comparar y tipificar
Una las dos tablas. Calcule la proporción de acuerdo y kappa con el bloque del cuaderno o a mano. Para cada desacuerdo, vuelva a leer la unidad y clasifíquelo:
| Tipo | Qué pasó |
|---|---|
| Generalización ajena | El modelo leyó algo como se lee en su entrenamiento, no como se lee en este corpus |
| Categoría forzada | Ninguna categoría aplicaba y el modelo eligió una en lugar de usar la regla para la duda |
| Contexto local | El modelo no sabía algo del periodo, del lugar o de la institución que usted sí sabe |
| Lectura literal | Ironía, fórmula de cortesía o cita que el modelo tomó al pie de la letra |
| Error propio | Usted se equivocó, y lo nota al releer |
| Lista defectuosa | Dos categorías se solapan y el desacuerdo mide la lista, no la lectura |
Cuente cuántos de cada tipo. Si predomina "lista defectuosa" o "categoría forzada", el problema está en la instrucción. Si predomina "contexto local", el problema está en lo que el modelo no puede saber. Si predomina "error propio", hay que revisar la lectura propia antes de culpar a nadie.
La decisión
Elija una de las tres y escriba la razón:
- Aceptar las etiquetas del modelo para el resto del corpus, con la tasa de acuerdo declarada en el informe y las categorías problemáticas marcadas.
- Corregir la instrucción (
instruccion_v2.md), etiquetar una muestra nueva y repetir la auditoría. - Rechazar la lectura asistida para esta tarea, porque los desacuerdos tocan lo que la pregunta de investigación necesita.
Ninguna es la respuesta correcta en general. La que elija tiene que seguirse de la tipología.
Después de la sesión
- Llene la sección de auditoría del registro de frontera y actualice las filas de las semanas anteriores.
- Si eligió corregir, haga la segunda vuelta antes de la semana 8 y reporte las dos auditorías.
- Escriba la sección 13 del informe de corpus con la plantilla.
Qué se evalúa
La precisión de la auditoría, no la tasa de acuerdo. Una auditoría con 60 % de acuerdo, desacuerdos bien tipificados y la decisión de corregir vale más que una con 95 % de acuerdo y la decisión de aceptar sin haber leído los desacuerdos.
Variante para grupos
Dos estudiantes etiquetan la misma muestra de uno de los dos corpus, a ciegas, con la misma instrucción. Ahora hay tres etiquetadores: el modelo y dos personas. El acuerdo entre las dos personas es la cifra de referencia contra la que se lee el acuerdo con el modelo.