Agentes de IA para procesar series temporales y grandes marcos de datos

Construya desde cero usando solo Python y Ollama (sin GPU, sin clave API)

una introducción

Los agentes son sistemas de IA impulsados ​​por grandes modelos de lenguaje (LLM), capaces de razonar sobre sus objetivos y tomar acciones para alcanzar un objetivo final. Está diseñado no sólo para responder consultas, sino para organizar una serie de operaciones, incluido el procesamiento de datos (como marcos de datos y series de tiempo). Esta capacidad permite que muchas aplicaciones del mundo real democraticen el acceso al análisis de datos, como la automatización de informes, consultas sin código y soporte para la limpieza y el procesamiento de datos.

Los agentes pueden interactuar con los marcos de datos de dos maneras diferentes:

  • Por lenguaje natural – El modelo de lenguaje grande (LLM) lee la tabla como una cadena de texto e intenta comprenderla basándose en su base de conocimientos.
  • Por Crear y ejecutar código – El agente activa herramientas para procesar el conjunto de datos como un objeto.

Al combinar el poder del procesamiento del lenguaje natural (PLN) con la precisión de la ejecución del código, los agentes de IA permiten que una gama más amplia de usuarios interactúen con conjuntos de datos complejos y extraigan información valiosa.

En este tutorial, demostraré cómo manipular dataframes y series temporales utilizando agentes de IA . Proporcionaré algunos fragmentos de código Python útiles que se pueden aplicar fácilmente en otros casos similares (solo hay que copiar, pegar y ejecutar), y explicaré cada línea de código con comentarios para que puedas replicar este ejemplo (encontrarás el enlace al código completo al final del artículo).

 

preparación

Empecemos a prepararnos Ollama (pip install ollama==0.4.7), una biblioteca que permite a los usuarios ejecutar grandes modelos de lenguaje de código abierto localmente, sin necesidad de servicios en la nube, lo que brinda mayor control sobre la privacidad y el rendimiento de los datos. Dado que se ejecuta localmente, ningún dato de chat sale de tu dispositivo.

Primero, debes descargar Ollama desde el sitio web.

A continuación, en la línea de comandos de tu dispositivo, usa el comando para descargar el modelo de lenguaje grande (LLM) que hayas elegido. Yo usaré Qwen de Alibaba , ya que es inteligente y ligero.

Una vez completada la descarga, puedes cambiar a Python y comenzar a escribir código.

import ollama
llm = "qwen2.5"

Probemos el modelo de lenguaje grande:

stream = ollama.generate(model=llm, prompt='''what time is it?''', stream=True)
for chunk in stream:
    print(chunk['response'], end='', flush=True)

Series temporales

Una serie temporal es una secuencia de datos medidos durante un período de tiempo, que se utiliza frecuentemente para análisis y pronósticos. Permite observar cómo cambian las variables con el tiempo y se emplea para identificar tendencias y patrones estacionales. Las series temporales son una herramienta poderosa en el análisis estadístico y la predicción.

Voy a crear un conjunto de datos de series temporales ficticio para usarlo como ejemplo.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

## create data
np.random.seed(1) #<--for reproducibility
length = 30
ts = pd.DataFrame(data=np.random.randint(low=0, high=15, size=length),
                  columns=['y'],
                  index=pd.date_range(start='2023-01-01', freq='MS', periods=length).strftime('%Y-%m'))

## plot
ts.plot(kind="bar", figsize=(10,3), legend=False, color="black").grid(axis='y')

Por lo general, los conjuntos de datos de series temporales tienen una estructura muy simple, con la variable principal como columna y el tiempo como índice.

Antes de convertirlo en una cadena, quiero asegurarme de que todo esté colocado bajo una columna, para que no perdamos ninguna información.

dtf = ts.reset_index().rename(columns={"index":"date"})
dtf.head()

Luego, se debe cambiar el tipo de datos de DataFrame a Dictionary.

data = dtf.to_dict(orient='records')
data[0:5]

Finalmente, del diccionario a la cadena de texto.

str_data = "\n".join([str(row) for row in data])
str_data

Ahora que tenemos una cadena de texto, podemos insertarla en una solicitud que cualquier modelo de lenguaje puede procesar. Al pegar un conjunto de datos en una solicitud, el Modelo de Lenguaje Grande (LLM) lee los datos como texto plano, pero aun así puede comprender la estructura y el significado basándose en los patrones observados durante el entrenamiento.

prompt = f'''
Analyze this dataset, it contains monthly sales data of an online retail product:
{str_data}
'''

Podemos iniciar fácilmente una conversación con un Modelo de Lenguaje Grande (MLG) . Cabe destacar que, actualmente, no se trata de un agente, ya que carece de herramientas; simplemente utilizamos el modelo lingüístico. Si bien no procesa números como una computadora, el MLG puede reconocer nombres de columnas, patrones temporales, tendencias y valores atípicos, especialmente con conjuntos de datos pequeños. Puede simular análisis y explicar resultados, pero no realizará cálculos precisos de forma independiente, puesto que no ejecuta código como un agente.

messages = [{"role":"system", "content":prompt}]

while True:
    ## User
    q = input('🙂 >')
    if q == "quit":
        break
    messages.append( {"role":"user", "content":q} )
   
    ## Model
    agent_res = ollama.chat(model=llm, messages=messages, tools=[])
    res = agent_res["message"]["content"]
   
    ## Response
    print("👽 >", f"\x1b[1;30m{res}\x1b[0m")
    messages.append( {"role":"assistant", "content":res} )

El Modelo de Lenguaje a Gran Escala (LLM, por sus siglas en inglés) reconoce números y comprende el contexto general, del mismo modo que podría comprender una receta o una línea de código.

Como puede verse, utilizar modelos de lenguaje a gran escala (LLM, por sus siglas en inglés) para analizar series temporales es excelente para obtener información y entablar conversaciones rápidamente.

Agente

Los modelos de lenguaje grandes (LLM) son excelentes para generar ideas y explorar conceptos iniciales, mientras que un agente puede ejecutar código. Por lo tanto, puede manejar tareas más complejas, como gráficos, pronósticos y detección de anomalías. Entonces, vamos a crear las herramientas.

En ocasiones, tratar la respuesta final como una herramienta puede ser más eficaz. Por ejemplo, si un agente realiza varias acciones para generar resultados intermedios, la respuesta final puede considerarse la herramienta que integra toda esta información en una respuesta coherente. Al diseñarla de esta manera, se logra una mayor personalización y control sobre los resultados.

def final_answer(text:str) -> str:
    return text

tool_final_answer = {'type':'function', 'function':{
  'name': 'final_answer',
  'description': 'Returns a natural language response to the user',
  'parameters': {'type': 'object',
                'required': ['text'],
                'properties': {'text': {'type':'str', 'description':'natural language response'}}
}}}

final_answer(text="hi")

Luego, la herramienta de codificación.

import io
import contextlib

def code_exec(code:str) -> str:
    output = io.StringIO()
    with contextlib.redirect_stdout(output):
        try:
            exec(code)
        except Exception as e:
            print(f"Error: {e}")
    return output.getvalue()

tool_code_exec = {'type':'function', 'function':{
  'name': 'code_exec',
  'description': 'Execute python code. Use always the function print() to get the output.',
  'parameters': {'type': 'object',
                'required': ['code'],
                'properties': {
                    'code': {'type':'str', 'description':'code to execute'},
}}}}

code_exec("from datetime import datetime; print(datetime.now().strftime('%H:%M'))")

Además, añadiré algunas funciones de utilidad para usar la herramienta y para ejecutar el agente.

dic_tools = {"final_answer":final_answer, "code_exec":code_exec}

# Utils
def use_tool(agent_res:dict, dic_tools:dict) -> dict:
    ## use tool
    if "tool_calls" in agent_res["message"].keys():
        for tool in agent_res["message"]["tool_calls"]:
            t_name, t_inputs = tool["function"]["name"], tool["function"]["arguments"]
            if f := dic_tools.get(t_name):
                ### calling tool
                print('🔧 >', f"\x1b[1;31m{t_name} -> Inputs: {t_inputs}\x1b[0m")
                ### tool output
                t_output = f(**tool["function"]["arguments"])
                print(t_output)
                ### final res
                res = t_output
            else:
                print('🤬 >', f"\x1b[1;31m{t_name} -> NotFound\x1b[0m")
    ## don't use tool
    if agent_res['message']['content'] != '':
        res = agent_res["message"]["content"]
        t_name, t_inputs = '', ''
    return {'res':res, 'tool_used':t_name, 'inputs_used':t_inputs}

Cuando un agente intenta resolver una tarea, quiero que haga un seguimiento de qué herramientas se utilizaron, qué entradas probó y qué resultados obtuvo. El proceso sólo debe detenerse cuando el modelo esté listo para proporcionar la respuesta final.

En cuanto a la herramienta de codificación, observé que los agentes tienden a recrear el dataframe en cada paso. Por lo tanto, utilizaré el refuerzo de memoria para recordarle al modelo que el conjunto de datos ya existe. Esta es una táctica común para lograr el comportamiento deseado. En definitiva, las técnicas de refuerzo de memoria ayudan a conseguir interacciones más significativas y efectivas.

# Start a chat
messages = [{"role":"system", "content":prompt}]
memory = '''
The dataset already exists and it's called 'dtf', don't create a new one.
'''
while True:
    ## User
    q = input('🙂 >')
    if q == "quit":
        break
    messages.append( {"role":"user", "content":q} )

    ## Memory
    messages.append( {"role":"user", "content":memory} )     
   
    ## Model
    available_tools = {"final_answer":tool_final_answer, "code_exec":tool_code_exec}
    res = run_agent(llm, messages, available_tools)
   
    ## Response
    print("👽 >", f"\x1b[1;30m{res}\x1b[0m")
    messages.append( {"role":"assistant", "content":res} )

Crear una trama es algo que un modelo de lenguaje grande (LLM) por sí solo no puede hacer. Pero tenga en cuenta que incluso si los agentes pueden generar imágenes, no pueden verlas, porque el motor sigue siendo, en última instancia, un modelo de lenguaje. Por lo tanto, el usuario es el único que visualiza la trama.

El agente utiliza la biblioteca statsmodels para entrenar un modelo y predecir datos de series temporales.

Cómo manejar grandes marcos de datos

Los modelos de lenguaje grandes (LLM) tienen una memoria limitada, lo que restringe la cantidad de información que pueden procesar al mismo tiempo. Incluso los modelos más sofisticados tienen límites simbólicos (unos pocos cientos de páginas de texto). Además, los modelos de lenguaje grandes (LLM) no retienen la memoria entre sesiones a menos que se incorpore un sistema de recuperación. En la práctica, para trabajar eficazmente con grandes marcos de datos, los desarrolladores a menudo utilizan estrategias como fragmentación, recuperación de generación aumentada (RAG), bases de datos vectoriales y resumen de contenido antes de introducirlo en el modelo.

Vamos a crear un gran conjunto de datos para jugar con él.

import random
import string

length = 1000

dtf = pd.DataFrame(data={
    'Id': [''.join(random.choices(string.ascii_letters, k=5)) for _ in range(length)],
    'Age': np.random.randint(low=18, high=80, size=length),
    'Score': np.random.uniform(low=50, high=100, size=length).round(1),
    'Status': np.random.choice(['Active','Inactive','Pending'], size=length)
})

dtf.tail()

Añadiré una herramienta de búsqueda web para que la inteligencia artificial de propósito general, con capacidad para ejecutar código Python y buscar en Internet, acceda a todo el conocimiento disponible y pueda tomar decisiones basadas en datos.

En Python, la forma más sencilla de crear una herramienta de búsqueda web es utilizando el popular navegador privado. Pato Pato a ganar (pip install duckduckgo-search==6.3.5). Puede utilizar la biblioteca original directamente o importar un shell. LangChain (pip install langchain-community==0.3.17).

from langchain_community.tools import DuckDuckGoSearchResults

def search_web(query:str) -> str:
  return DuckDuckGoSearchResults(backend="news").run(query)

tool_search_web = {'type':'function', 'function':{
  'name': 'search_web',
  'description': 'Search the web',
  'parameters': {'type': 'object',
                'required': ['query'],
                'properties': {
                    'query': {'type':'str', 'description':'the topic or subject to search on the web'},
}}}}

search_web(query="nvidia")

En total, el agente ahora tiene 3 herramientas.

dic_tools = {'final_answer':final_answer,
             'search_web':search_web,
             'code_exec':code_exec}

Como no puedo agregar el marco de datos completo en la solicitud, solo alimentaré las primeras 10 filas para que el LLM pueda comprender el contexto general del conjunto de datos. Además, especificaré dónde encontrar el conjunto de datos completo.

str_data = "\n".join([str(row) for row in dtf.head(10).to_dict(orient='records')])

prompt = f'''
You are a Data Analyst, you will be given a task to solve as best you can.
You have access to the following tools:
- tool 'final_answer' to return a text response.
- tool 'code_exec' to execute Python code.
- tool 'search_web' to search for information on the internet.

If you use the 'code_exec' tool, remember to always use the function print() to get the output.
The dataset already exists and it's called 'dtf', don't create a new one.

This dataset contains credit score for each customer of the bank. Here's the first rows:
{str_data}
'''

Finalmente podemos ejecutar el agente.

messages = [{"role":"system", "content":prompt}]
memory = '''
The dataset already exists and it's called 'dtf', don't create a new one.
'''
while True:
    ## User
    q = input('🙂 >')
    if q == "quit":
        break
    messages.append( {"role":"user", "content":q} )

    ## Memory
    messages.append( {"role":"user", "content":memory} )     
   
    ## Model
    available_tools = {"final_answer":tool_final_answer, "code_exec":tool_code_exec, "search_web":tool_search_web}
    res = run_agent(llm, messages, available_tools)
   
    ## Response
    print("👽 >", f"\x1b[1;30m{res}\x1b[0m")
    messages.append( {"role":"assistant", "content":res} )

En esta interacción, el agente utilizó correctamente la herramienta de codificación. Ahora quiero hacerle usar la otra herramienta también.

Por último, necesito que el agente reúna toda la información obtenida hasta ahora de este chat.

 

Conclusión

Este artículo sirvió como tutorial para demostrar cómo crear agentes desde cero para manipular series temporales y grandes conjuntos de datos . Abordamos las dos formas en que los modelos pueden interactuar con los datos: mediante el lenguaje natural, donde un modelo de lenguaje grande (LLM) interpreta la tabla como una serie utilizando su base de conocimiento, y mediante la creación y ejecución de código, aprovechando herramientas para manipular el conjunto de datos como un objeto.

El código completo de este artículo: GitHub

¡Espero que lo hayas disfrutado! No dudes en contactarme con preguntas y comentarios o para compartir tus proyectos interesantes.

 

Los comentarios están cerrados.