# Extracción de datos estructurados desde texto usando IA

> Podemos usar la IA para transformar textos de cualquier tipo en datos estructurados. Esto significa que puedes convertir entrevistas, reseñas, opiniones, correos, noticias y más en bases de datos con la información que necesitas debidamente ordenada en filas y columnas. En ese tutorial usaremos modelos de lenguaje para extraer datos estructurados a partir de noticias reales.

- URL: https://bastianolea.rbind.io/blog/datos_estructurados_llm/
- Fecha: 2026-03-07
- Autor: Bastián Olea Herrera
- Etiquetas: inteligencia artificial, texto, análisis de texto, datos
- Recursos: [Workshop](https://thisisnic.github.io/rainbowrworkshop/) [Código](https://gist.github.com/bastianolea/8bc40e3acbb0716fbf607b6488886274)

---

> **Aviso:** Este tutorial lo hice con los aprendizajes que obtuve del excelente [workshop _LLMs in R for Data Analysis_](https://thisisnic.github.io/rainbowrworkshop/code.html) impartido por [Nic Crane](https://niccrane.com) en la fabulosa [conferencia de RainbowR 2026](https://conference.rainbowr.org)

Los modelos de lenguaje (LLMs, también llamados coloquialmente *inteligencia artificial*) nos pueden ayudar a **transformar textos de cualquier tipo en datos estructurados**. Esto significa que puedes convertir **entrevistas, reseñas, opiniones, correos, noticias y más** en **bases de datos** con la información que necesitas debidamente ordenada en filas y columnas.

***Ejemplos de uso:***
- Puedes pasar de un conjunto de entrevistas a una tabla con las respuestas a preguntas específicas sobre los entrevistados, incluyendo información como edad, género, profesión, opiniones, etc.
- Puedes convertir reseñas de productos en una tabla con la opinión general, los aspectos positivos y negativos, la puntuación, etc.
- Puedes tomar conjuntos de noticias o de artículos y obtener tablas que indican los actores principales, ubicaciones clave, fechas, resúmenes, análisis de sentimiento, etc.

En ese tutorial usaremos modelos de lenguaje para **extraer datos estructurados** a partir de noticias reales.

El requisito para poder realizar esto es tener acceso a una IA o modelo de lenguaje (LLM), ya sea local o en la nube (ChatGPT, Claude, Gemini, etc.). Puedes seguir este tutorial para [configurar el uso de inteligencia artificial con R.](https://bastianolea.rbind.io/blog/ellmer/)

> **Aviso:** **Requisito:** si necesitas configurar tu proveedor de IA con R, [sigue este tutorial](https://bastianolea.rbind.io/blog/ellmer/) para empezar a analizar datos con IA!

## Datos de texto de ejemplo

Los datos que usaremos para el tutorial será una base de **10.000 noticias reales** obtenidas desde medios digitales de Chile durante el año 2025. Las noticias son obtenidas por *web scraping*, y el código de este proyecto está disponible [en este repositorio](https://github.com/bastianolea/prensa_chile).

[Descargar datos (clic derecho y _guardar como_)](https://bastianolea.rbind.io/blog/datos_estructurados_llm/prensa_datos_muestra_2025.csv)

También puedes descargar los datos desde R:

``` r
# dirección de los datos
url = "https://github.com/bastianolea/prensa_chile/raw/main/prensa_datos_muestra_2025.csv"
```

``` r
# descargar el archivo con R
download.file(url,
              destfile = "prensa_datos_muestra_2025.csv")
```

O bien, cargar los datos directamente desde internet sin necesidad de descargar el archivo:

``` r
# cargar datos directamente desde internet
library(readr)
noticias <- read_csv2(url)
```

Veamos rápidamente la tabla de datos con la función `glimpse()`:

``` r
library(dplyr)
glimpse(noticias)
```

    Rows: 10,000
    Columns: 4
    $ titulo <chr> "Acusa falta de imparcialidad: Defensa de Vivanco pide excluir …
    $ cuerpo <chr> "La estrategia judicial de la exministra de la Corte Suprema, Á…
    $ fuente <chr> "CNN Chile", "The Clinic", "El Ciudadano", "La Tercera", "El Ci…
    $ fecha  <date> 2025-12-31, 2025-12-31, 2025-12-31, 2025-12-31, 2025-12-31, 20…

Vemos que tenemos una columna con los títulos, otra con el texto o *cuerpo* de las noticias, la fuente y la fecha de cada noticia.

## Extraer datos desde un solo texto

Empecemos extrayendo una sola noticia como ejemplo, usando `slice()` para elegir una fila de la tabla, y `pull()` para extraer la columna `cuerpo`, que contiene el texto de la noticia.

``` r
library(dplyr)

noticia <- noticias |> 
  slice(6) |> 
  pull(cuerpo)
```

Veamos un poco de qué se trata la noticia:

> El subprefecto Adolfo Domínguez, jefe de la Brigada Contra el Crimen Organizado, señaló que "se pudo indagar en diferentes comunas de Santiago y se logró dar con el paradero del sujeto, que mantiene múltiples antecedentes". La Policía de Investigaciones (PDI) recapturó a Matías Jesús Fuentes Roja...

Puedes [leer la noticia completa en este enlace](https://www.cooperativa.cl/noticias/pais/judicial/gendarmeria/pdi-recapturo-a-sujeto-al-que-gendarmeria-libero-por-error/2025-12-31/073547.html), pero la idea general es que contamos con el cuerpo entero de la noticia en el objeto `noticia`.

### Pregunta simple al modelo

Veamos la forma más simple de extraer datos estructurados desde información no estructurada: preguntarle al modelo de IA y cruzar los dedos 🤞🏼

**Creamos el *chat* de IA** que usaremos para **extraer los datos**. En mi caso, voy a usar modelos de *Anthropic*, por lo que uso la función `chat_anthropic()`, pero si usas otro modelo, puedes usar la función correspondiente de `{ellmer}` para crear tu chat, como se explica [en este tutorial](https://bastianolea.rbind.io/blog/ellmer/).

``` r
library(ellmer)

# crear un chat
chat <- chat_anthropic(model = "claude-haiku-4-5")
```

> **Nota:** Si necesitas configurar tu proveedor de IA con R, o configurar modelos locales de IA con R, [sigue este tutorial](https://bastianolea.rbind.io/blog/ellmer/)

Ahora hagamos una prueba/ejemplo: usemos la **noticia** que elegimos para entregarla al modelo de lenguaje, junto a una **breve instrucción**:

``` r
# crear el prompt con la instrucción y el texto
prompt <- paste("En qué lugar se desarrolla la siguiente noticia:",
                noticia)

# enviar el prompt al modelo
chat$chat(prompt)
```

    # Lugar donde se desarrolla la noticia

    La noticia se desarrolla en **Santiago de Chile**, específicamente en la 
    **comuna de Cerrillos**.

    Según el texto, la recaptura de Matías Jesús Fuentes Rojas fue concretada por 
    la PDI "durante la madrugada de este miércoles, en la comuna de Cerrillos". 
    Además, se menciona que las indagatorias se realizaron "en diferentes comunas 
    de Santiago".

El modelo de lenguaje *trata* de ser útil, y por eso entrega mucha más información que la que necesitamos! No podemos poner todo ese texto en una tabla de datos. Habría que limpiarlo muchísimo y dejaría de ser útil 🙄

Podemos **corregir el comportamiento del modelo** especificando una instrucción más clara en el *system prompt*, que es la **instrucción general** que le damos al modelo al crear el chat:

``` r
# definir instrucción general para el modelo
instruccion <- "Eres un experto en noticias de Chile.
                Entrega la información lo más breve posible,
                especificando en una o dos palabras y sin
                comentarios extra. Por ejemplo: {Puente Alto, Santiago}."

# crear chat con el modelo y con system prompt
chat <- chat_anthropic(model = "claude-haiku-4-5",
                       system_prompt = instruccion)

# escribir prompt o instrucción particular
prompt <- paste("En qué lugar se desarrolla la siguiente noticia:",
                noticia)

# enviar el prompt al modelo
chat$chat(prompt)
```

    Cerrillos, Santiago.

Podemos confirmar en la [noticia completa](https://www.cooperativa.cl/noticias/pais/judicial/gendarmeria/pdi-recapturo-a-sujeto-al-que-gendarmeria-libero-por-error/2025-12-31/073547.html) que efectivamente los hechos tuvieron lugar en la comuna de Cerrillos.

Ahora sí, el modelo de lenguaje responde tal como esperamos. Pero esta forma de hacerlo aún deja mucho al **azar**: la especificidad del *system prompt* que escribas, y la aleatoriedad del modelo que puede responder algo distinto sin previo aviso.

### Especificación de datos estructurados

Ahora veremos una una **forma más consistente de extraer datos a partir de textos con IA**, para que los modelos de lenguaje respondan específicamente lo que necesitamos.

Para ello, vamos a **especificar las respuestas que queremos recibir** del modelo, usando las funciones `type_{x}` de `{ellmer}`:

| Función | Tipo de respuesta | Descripción |
|------------------------|------------------------|------------------------|
| `type_string()` | Texto abierto | Para respuestas en texto libre, como nombres, conceptos, explicaciones, etc. |
| `type_enum()` | Variable categórica | Para respuestas que asuman un valor de un conjunto de valores predefinidos. |
| `type_number()` | Numérica | Para respuestas que son cifras (números enteros, decimales, etc.) |
| `type_boolean()` | Variable lógica | Para respuestas de verdadero/falso (`TRUE` / `FALSE`) |

Siguiendo con el ejemplo, si queremos extraer la **ubicación principal** mencionada en la noticia, usamos `type_string()` para pedirle una respuesta de texto al modelo, junto a una breve descripción de lo que queremos:

``` r
tipo_lugar <- type_string("Ubicación principal de la noticia")
```

Ahora, al momento de enviar el *prompt* al modelo (el objeto `noticia` que contiene el texto), usamos `chat$structured()` en vez de `chat$chat()` para recibir datos estructurados en vez de la respuesta libre del modelo, y le indicamos que queremos que la respuesta sea del tipo `tipo_lugar`:

``` r
# crear chat con el modelo
chat <- chat_anthropic(model = "claude-haiku-4-5")

# empezar chat estructurado especificando el tipo de respuesta
resultado <- chat$chat_structured(noticia, 
                                  type = tipo_lugar)

resultado
```

    [1] "Santiago, Chile"

El modelo de lenguaje responde con la ubicación principal de la noticia, sin ningún comentario extra, y sin necesidad de hacer un procesamiento posterior para limpiar la respuesta.

### Extraer variables categóricas

Si queremos extraer desde los datos no estructurados una variable categórica, es decir, que sólo pueda tomar un valor entre varias opciones predefinidas, podemos usar `type_enum()`, donde especificamos un vector con los valores posibles.

Por ejemplo, preguntemos qué tipo de noticia se trata, entre categorías como *social, economía, política,* etc.

``` r
# definir tipo de respuesta categórica
tipo_clasificacion <- type_enum(description = "Clasificación de la noticia según su enfoque temático principal",
                                values = c("social", "económico", "político", 
                                           "judicial", "delincuencia", "otros"))

# crear chat con el modelo
chat <- chat_anthropic(model = "claude-haiku-4-5")

# empezar chat estructurado especificando el tipo de respuesta
resultado <- chat$chat_structured(noticia, 
                                  type = tipo_clasificacion)

resultado
```

    [1] "delincuencia"

De esta forma, el modelo se limita a responder solamente entre las opciones que tú le especifiques, delimitando mucho más la calidad de la respuesta.

## Extracción datos estructurados desde varios textos

Hasta ahora hemos extraído datos estructurados a partir de un solo elemento de texto no estructurado.

Realicemos la extracción de datos estructurados de varias unidades de información al mismo tiempo. Como ejemplo, probemos con una muestra de 5 noticias:

``` r
library(dplyr)

muestra <- noticias |> 
  slice(20:25)
```

``` r
muestra |> 
  select(titulo)
```

    # A tibble: 6 × 1
      titulo                                                                        
      <chr>                                                                         
    1 Nuevo error en Gendarmería: imputado queda en libertad pese a que debía cumpl…
    2 Andes Iron acusa ofensiva política contra Dominga tras conocerse que enfrenta…
    3 Fiscalía acusó a Cathy Barriga por cuatro delitos y pide más de 23 años de cá…
    4 No fue Patricia Muñoz: Boric nombra a Macarena Cortés como la primera directo…
    5 “Ya tomaron las huellas”: dueño de tienda asaltada durante incendio en Mall P…
    6 Presidente Boric recibe cartas credenciales de cinco embajadores y cierra pol…

Siguiendo la definición de *tipos* de resultado que aprendimos más arriba, podemos **definir de una vez las distintas variables que queremos extraer desde nuestros datos no estructurados**: en el caso de las noticias, queremos extraer el lugar, los personajes principales, el tipo de personaje, la clasificación de las noticias, y el sentimiento general de la noticia.

Estas variables serán generalmente de tipo texto, pero varias de ellas queremos que sean variables categóricas con valores específicos.

``` r
tipo_lugar <- type_string("Ubicación principal de la noticia")

tipo_actor <- type_string("Actor, personaje o institución principal de la noticia")

tipo_grupo <- type_enum(description = "Tipo de entidad del actor, personaje o institución principal de la noticia",
                        values = c("institución pública", "político", "empresa", "empresario", "ciudadano", "otros"))

tipo_clasificacion <- type_enum(description = "Clasificación de la noticia según su enfoque temático principal",
                                values = c("social", "económico", "político", "judicial", "delincuencia", "otros"))

tipo_sentimiento <- type_enum(values = c("positivo", "neutro", "negativo"), 
                              description = "Sentimiento general de la noticia según su contenido")
```

Ahora podemos usar la función `parallel_chat_structured()` para enviar varios textos al modelo de lenguaje, y recibir datos estructurados para cada uno de ellos. Esta función recibe como argumentos el **chat** (que además tiene un *system prompt* que especifica el actuar del modelo), una lista con los **textos** que queremos procesar (en este caso, la columna `cuerpo` de la tabla `muestra`), y los **tipos de respuesta** que queremos recibir (al ser varias, van en dentro de `type_object()`).

``` r
instruccion <- "Eres un clasificador de noticias experto en el acontecer nacional de Chile"

resultado <- parallel_chat_structured(
  chat = chat_anthropic(model = "claude-haiku-4-5",
                        system_prompt = instruccion),
  prompts = as.list(muestra$cuerpo),
  type = type_object(
    lugar = tipo_lugar,
    actor = tipo_actor,
    actor_grupo = tipo_grupo,
    sentimiento = tipo_sentimiento,
    clasificacion = tipo_clasificacion
  )
)
```

Al solicitar varias variables al modelo, retorna una tabla o *tibble* con los datos estructurados de cada noticia, con una fila por cada noticia, en el orden que se entregaron las noticias.

| lugar | actor | actor_grupo | sentimiento | clasificacion |
|:--------------|:--------------------------------|:----------|:------|:-------|
| Santiago | Gendarmería de Chile | institución pública | negativo | judicial |
| Chile | Andes Iron | empresa | negativo | judicial |
| Maipú, Región Metropolitana | Cathy Barriga, exalcaldesa de Maipú | político | negativo | judicial |
| Chile | Gabriel Boric | político | positivo | político |
| Mall Plaza Norte, Chile | Propietario de tienda Chenza / Tres mujeres involucradas en el robo | ciudadano | negativo | delincuencia |
| Chile (La Moneda, Santiago) | Presidente Gabriel Boric | político | negativo | político |

Como vienen en el mismo orden que las noticias, podemos unir las columnas obtenidas a los datos originales:

``` r
tabla <- muestra |> 
  select(titulo, fecha) |> 
  bind_cols(resultado)
```

| titulo | fecha | lugar | actor | actor_grupo | sentimiento | clasificacion |
|:------------------------------|:---|:-------|:----------------|:-----|:---|:----|
| Nuevo error en Gendarmería: imputado queda en libertad pese a que debía cumplir prisión preventiva | 2025-12-30 | Santiago | Gendarmería de Chile | institución pública | negativo | judicial |
| Andes Iron acusa ofensiva política contra Dominga tras conocerse que enfrenta indagatoria penal | 2025-12-30 | Chile | Andes Iron | empresa | negativo | judicial |
| Fiscalía acusó a Cathy Barriga por cuatro delitos y pide más de 23 años de cárcel | 2025-12-30 | Maipú, Región Metropolitana | Cathy Barriga, exalcaldesa de Maipú | político | negativo | judicial |
| No fue Patricia Muñoz: Boric nombra a Macarena Cortés como la primera directora de la Defensoría de Víctimas | 2025-12-30 | Chile | Gabriel Boric | político | positivo | político |
| "Ya tomaron las huellas": dueño de tienda asaltada durante incendio en Mall Plaza Norte afirma que responsables fueron identificadas | 2025-12-30 | Mall Plaza Norte, Chile | Propietario de tienda Chenza / Tres mujeres involucradas en el robo | ciudadano | negativo | delincuencia |
| Presidente Boric recibe cartas credenciales de cinco embajadores y cierra polémica con EEUU e Israel | 2025-12-30 | Chile (La Moneda, Santiago) | Presidente Gabriel Boric | político | negativo | político |

Si te interesa profundizar en este tema, [Nic Crane ofrece un curso específico sobre el uso de LLMs con R](https://www.aifordatapeople.com/courses/llms-in-r)

## Referencias

-   [RainbowR Workshop: LLMs in R for Data Analysis](https://thisisnic.github.io/rainbowrworkshop/code.html) por [Nic Crane](https://niccrane.com)
-   [Learn to work with LLMs in R](https://www.aifordatapeople.com/courses/llms-in-r), curso impartido por [Nic Crane](https://niccrane.com)
-   [Documentación del paquete `{ellmer}`](https://ellmer.tidyverse.org/articles/structured-data.html) sobre datos estructurados
