(MINLP) con R y Pyomo

Anuncio
Optimización Entera Mixta No Lineal
(MINLP) con R y Pyomo:
Un ejemplo práctico
Jorge Ayuso Rejas
V Jornadas de Usuarios de R
Etopia-Centro de Arte y Tecnologı́a, Zaragoza
12 y 13 de Diciembre de 2013
Introducción
Lo expuesto en las siguientes páginas es un resumen del trabajo que he ido realizando los últimos meses como analista en Conento (www.conento.com).
Estábamos interesados en mejorar la recomendación que hacemos a nuestros clientes sobre cómo distribuir su presupuesto en una campaña de publicidad. Es importante
tomar una buena decisión a la hora de repartir el presupuesto entre los distintos medios disponibles y tener en cuenta las caracterı́sticas de cada anunciante. Para ello planteamos un problema de optimización usando la información conseguida de nuestros
modelos econométricos en donde recogemos las caracterı́sticas de cada anunciante.
En la primera sección se introducen las curvas de aportes y se plantea el problema
de optimización a resolver.
En la sección dos se explica la implementación del problema en R , haciendo uso
del software Pyomo (Hart et al., 2012) un sustituto libre de AMPL* escrito en Python.
Para terminar, en la última sección veremos un ejemplo práctico con una interfaz
amigable para el usuario final gracias al paquete shiny (RStudio and Inc., 2013).
*
AMPL es un lenguaje de modelado algebraico para programación matemática http://www.ampl.
com.
1
1.
Optimización de Presupuesto
Encontrar la mejor forma de repartir un presupuesto de comunicación para conseguir maximizar las ventas de una empresa/negocio es un ejemplo clásico de optimización. En esta sección plantearemos el problema a optimizar bajo ciertas hipótesis y
simplificaciones para centrarnos en la implementación (sección 2).
Supongamos que una empresa quiere repartir el presupuesto de una semana (se
podrı́a hacer para otros periodos de tiempo) destinado a publicidad en los siguientes
medios: Exterior, Online, Prensa, Radio, Revistas y Televisión.
Para llevar acabo la optimización es necesario una función o funciones objetivos
que maximizar. En nuestro caso esas funciones serán las curvas de aportes que nos
indicarán para cada medio e inversión qué aportes conseguimos (los aportes pueden
ser cualquier serie de negocio del anunciante: altas, contactos, ventas. . . ).
1.1.
Curvas de aportes
Para cada medio “i” definimos su curva de aporte como :
fi (xi ) = eAi −Bi /xi
donde Ai , Bi > 0 parámetros a definir y xi es la inversión semanal en euros en el medio
i. A continuación veamos algunas propiedades de estas curvas:
Las curvas son diferenciables.
Existe un cambio de curvatura en xi =
convexa.
1
B.
2 i
Para valores mayores la curva es
Tienen una ası́ntota horizontal en lı́mx→∞ eAi −Bi /xi = eAi .
Estas tres propiedas son buenas a la hora de usar los algoritmos de optimización. En
el siguiente gráfico se muestra las curvas de nuestro problema, donde los parámetros
Ai , Bi de cada medio se han conseguido en un estudio previo.
2
Curvas de aportes
Televisión
Prensa
750
Aportes
Online
500
Radio
250
Exterior
0
0€
200.000€
400.000€
600.000€
800.000€
Inversión
Observamos como cada curva tiene distintas ası́ntotas, siendo la más alta la televisión. Además podemos ver también qué curva devuelve mejores aportes para cada
inversión. Por ejemplo para una inversión menor que 200.000e, online es el medio
con mayor retorno. Para valores mayores empieza a ser más rentable la prensa y para
valores grandes de inversión el mejor medio es la televisión.
1.2.
Planteamiento del problema
Una vez definidas las curvas, estamos en condiciones de definir el problema de
optimización. Vamos a definir dos versiones del problema, la primera versión será un
problema de optimización no lineal (NLP).
Definición (Problema 1)
Maximizar:
Aportes :=
X
eAi −Bi /xi
i∈medios
sujeto a:
P
i∈medios xi
≤ Presupuesto Total
mini ≤ xi ≤ maxi
∀i ∈ Medios
Donde como antes xi es la inversión semanal en euros en el medio i y Ai , Bi , mini y maxi
parámetros del problema mayores que cero.
En este primer problema estamos obligando que la inversión de cada medio sea
siempre mayor o igual que un mı́nimo definido. Este mı́nimo se define ya que aunque
3
matemáticamente pueda tener sentido invertir una cantidad pequeña, en la realidad
no es posible hacer inversiones arbitrariamente pequeñas en ciertos medios. También
estamos fijando un máximo de inversión aunque al existir una ası́ntota horizontal no
harı́a falta.
Definimos ahora una segunda versión del problema, donde vamos a permitir que
la inversión en cada medio sea también cero.
Definición (Problema 2)
Maximizar:
X
Aportes :=
eAi −Bi /xi
i∈medios
sujeto a:
P
i∈medios xi
≤ Presupuesto Total
xi ∈ [mini , maxi ] ∪ {0}
∀i ∈ Medios
En este caso el dominio de cada variable a optimizar es discontinuo, a la hora de
resolverlo esto puede ser una dificultad. Ası́ que se suele usar variables auxiliares para solventarlo. De este modo el problema 2 podrı́amos re-formularlo de la siguiente
manera:
Definición (Problema 20 )
Maximizar:
Aportes :=
X
wi eAi −Bi /xi
i∈medios
sujeto a:
P
i∈medios
wi xi ≤ Presupuesto Total
wi ∈ {0, 1}
∀i ∈ Medios
mini ≤ xi ≤ maxi
∀i ∈ Medios
Donde hemos introducido una variable binarias para cada medio, las cuales nos
indicarán si existe inversión o no en el medio. De este modo tenemos un problema
de optimización entera mixta no lineal, más conocido por sus siglas en inglés MINLP
(Mixed-Integer Nonlinear Programming).
4
2.
Implementación del problema
Uno de los primeros pasos cuando queremos conocer como realizar algún análisis en R es revisar la página web de “Task Views” (http://cran.r-project.org/web/
views) donde tenemos resumido los principales paquetes divididos por temas de interés. En este caso nos centramos en el de optimización (http://cran.r-project.org/
web/views/Optimization.html).
Podemos encontrar varios paquetes para resolver el problema 1 (NLP). Uno de los
que parece más completo es el paquete nloptr (Johnson, 2013). Pero no se encontró ninguno para resolver el problema 2 (MINLP).
Buscando algoritmos/software libres para resolver problemas MINLP encontramos estos tres:
Bonmin (Bonami and Lee, 2007).
Couenne (Belotti, 2009).
Minotaur (Leyffer et al., 2011).
Los tres tienen en común, además de ser de código abierto, que pueden ser utilizados desde AMPL gracias a la librerı́a ASL: “AMPL Solver Library” (Gay, 1997).
Aunque AMPL no es libre (la librerı́a ASL sı́ lo es) existe una versión estudiante con
la cual podemos hacer pruebas aunque no podrı́amos usarlo para uso comercial (para
ello tendrı́amos que comprar una licencia de AMPL).
Los tres algoritmos también se pueden utilizar directamente desde código C ası́ que
se podrı́a implementar una conexión desde R. Es más, los dos primeros algoritmos pertenecen al proyecto COIN-OR (http://www.coin-or.org/), y usan como optimizador
para la parte NLP Ipopt (Wächter and Biegler, 2006). Ipopt ya tiene implementada una
interfaz con R gracias al paquete ipoptr (Ypma, 2010). También existe un proyecto de un
interfaz para el optimizador Bonmin (https://r-forge.r-project.org/scm/viewvc.
php/pkg/Rbonmin/?root=rino) pero parece abandonado a dı́a de hoy.
Ası́ que se puede probar los optimizadores con AMPL (más fácil que hacer las pruebas directamente en C) sabiendo que se podrı́a después abandonar AMPL y usar sólo
software libre.
2.1.
Implementación del problema con AMPL
Una de las ventajas que nos ofrecen AMPL es poder definir el problema de manera
abstracta y más tarde introducir los datos. Además la sintaxis es bastante sencilla y
muy intuitiva para definir problemas de optimización. Veamos un ejemplo del problema 2 implementado en AMPL:
5
set datos ordered;
param
param
param
param
param
MIN {datos} >= 0;
MAX {datos} >= 0;
A {datos} >= 0;
B {datos} >= 0;
Presupuesto >= 0;
var w {j in datos} binary;
var x {j in datos} <= MAX[j], >= MIN[j];
maximize cost:
sum {j in datos} w[j] *
exp(A[j]-B[j]/x[j]);
subject to
c1: sum {j in datos} w[j]*x[j]
<= Presupuesto ;
option solver bonmin;
Después generamos un fichero con los datos (podemos cambiar los parámetros sin
tener que cambiar el modelo) y AMPL se encarga de definir el problema final y de
las derivadas si son necesarias. A continuación se muestra el formato que necesitamos
para introducir los parámetros:
param: datos : "A" "B" "MIN" "MAX" :=
"Televisión" 7.091114787 270163.8599 183359.5768 421967.2358
"Online" 6.571585223 62164.76381 38488.03122 110009.0971
"Prensa" 6.779516794 105752.5395 71820.20725 165047.6584
"Radio" 6.18270522 64579.77595 43419.72799 102131.3559
"Exterior" 4.168857373 26693.37017 20854.44233 34971.98656;
param Presupuesto := 1050000 ;
Una vez hechas las primeras pruebas y comprobado que los algoritmos funcionaban bien para el problema presentado. Se buscó si existı́a alguna alternativa libre de
AMPL (y ası́ evitar el programa en C para conectar R y los optimizadores). De este
modo llegamos al software Pyomo (Hart et al., 2012) que pertenece al proyecto Coopr.
2.2.
Implementación del problema con R y Pyomo
Pyomo tiene una sintaxis muy parecida que AMPL y al estar escrito en Python tenemos una mayor flexibilidad a la hora de definir nuestro problema. Además al disponer
de la librerı́a ASL puede conectar con los tres optimizadores fácilmente.
Veamos el mismo problema pero esta vez implementado en Pyomo:
6
# Imports
from coopr.pyomo import *
# ***********************************
model = AbstractModel()
# ***********************************
model.datos = Set()
# ***********************************
model.MIN = Param(model.datos, within=NonNegativeReals)
model.MAX = Param(model.datos, within=NonNegativeReals)
model.A = Param(model.datos, within=NonNegativeReals)
model.B = Param(model.datos, within=NonNegativeReals)
model.Presupuesto = Param(within=NonNegativeReals)
# ***********************************
def Level_bounds(model, i):
return (model.MIN[i], model.MAX[i])
model.x = Var(model.datos, bounds=Level_bounds)
model.w = Var(model.datos, within=Binary)
# ***********************************
def Total_Cost_rule(model):
return sum([model.w[j] * exp(model.A[j]-model.B[j]/model.x[j]) \
for j in model.datos])
model.Total_Cost = Objective(rule=Total_Cost_rule,sense=maximize)
# ***********************************
def Demand_rule(model):
return sum([model.w[i] * model.x[i] for i in model.datos]) <= model.Presupuesto
model.Demand = Constraint(rule=Demand_rule)
Observamos como la sintaxis es similar, además el mismo formato usado para introducir los datos en AMPL es valido para Pyomo. Ası́ que construimos una función
en R para exportar los distintos datos a este formato.
Podemos definir cuatro tipos de datos distintos:
Parámetros individuales, por ejemplo el presupuesto.
Parámetros con un ı́ndice, podemos definir a la vez un ı́ndice (set en el lenguaje
de Pyomo) y algunos parámetros asociados a este ı́ndice. En nuestro caso el ı́ndice
serı́an los medios y los parámetros asociados el mı́nimo, máximo, A y B.
Parámetro con dos ı́ndices (matrices), no lo estamos usando por ahora pero es
posible definir una matriz de datos para dos ı́ndices definidos.
Índice sin parámetros, útil por ejemplo para poder usar matrices de parámetros.
7
Veamos una sencilla función para exportar datos desde R al formato adecuado:
data_ampl<-function(x,name=NULL,tipo=NULL){
if( is.null(name) ) stop("Introduza un nombre para el conjunto de datos")
prueba<- capture.output(write.table(x))
if(is.null(tipo)){ # Por defecto se construyen parámetros con un ı́ndice
prueba[1]<-paste("param:",name,":",prueba[1],":=")
}else{
if(tipo=="matriz"){
prueba[1]<-paste("param ",name,"(tr) :",prueba[1],":=")
}else{
if(tipo=="set"){
prueba[1]<-paste("set",name,":=",prueba[1])
}else{
if(tipo=="param"){
prueba<-paste("param",name,":=",x)
}else{
stop("Tipo introducido no definido")
}
}
}
}
prueba[length(prueba)]<-paste(prueba[length(prueba)],";",sep="")
prueba
}
datos
##
##
##
##
##
##
A
B
MIN
MAX
Televisión 7.091 270164 183360 421967
Online
6.572 62165 38488 110009
Prensa
6.780 105753 71820 165048
Radio
6.183 64580 43420 102131
Exterior
4.169 26693 20854 34972
data_ampl(datos,name="datos")
##
##
##
##
##
##
[1]
[2]
[3]
[4]
[5]
[6]
"param: datos : \"A\" \"B\" \"MIN\" \"MAX\" :="
"\"Televisión\" 7.091114787 270163.8599 183359.5768 421967.2358"
"\"Online\" 6.571585223 62164.76381 38488.03122 110009.0971"
"\"Prensa\" 6.779516794 105752.5395 71820.20725 165047.6584"
"\"Radio\" 6.18270522 64579.77595 43419.72799 102131.3559"
"\"Exterior\" 4.168857373 26693.37017 20854.44233 34971.98656;"
Una vez tenemos los datos en el formato adecuado para Pyomo y hemos guardado
8
la definición del modelo en un fichero llamado modelo.py en el directorio de trabajo
de R. Podemos ejecutar el modelo de la siguiente manera:
write(c(data_ampl(datos,name="datos"),
data_ampl(570000,name="Presupuesto",tipo="param")),
"datos.dat")
# NOTA: Bonmin y Pyomo tienen que estar configurado en el PATH
# del sistema o incluir ruta completa
system("pyomo modelo.py datos.dat --solver bonmin",intern=TRUE)[1:13]
##
##
##
##
##
##
##
##
##
##
##
##
##
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
[11]
[12]
[13]
"[
"[
"[
"[
"[
"
"
"
"
"
"
"[
"[
0.00] Setting up Pyomo environment"
0.01] Applying Pyomo preprocessing actions"
0.02] Creating model"
0.08] Applying solver"
0.95] Processing results"
Number of solutions: 1"
Solution Information"
Gap: <undefined>"
Status: optimal"
Function Value: 1429.08707683"
Solver results file: results.json"
0.96] Applying Pyomo postprocessing actions"
0.96] Pyomo Finished"
Podemos ver como resuelve el problema y además guarda los resultados en un
archivo “json”. Ası́ que es fácil recuperar los resultados con la función fromJSON del
paquete RJSONIO (Lang, 2013). En el anexo se muestra como Pyomo interpreta los
datos y crea la función a optimizar.
9
3.
Ejemplo Final
Una vez nos es posible resolver el problema planteado es hora de dar soluciones.
Como no todo el mundo esta familiarizado con R, hemos desarrollado un pequeño interfaz web gracias al paquete shiny (RStudio and Inc., 2013). Gracias a shiny logramos
sin grandes esfuerzos una aplicación web donde el usuario final puede probar varios
escenarios y conseguir el reparto óptimo para cada uno de ellos.
En http://jornadas-r.conento.com se ha publicado una versión libre donde previamente hemos cargado los datos usados en el ejemplo de estas páginas. Además
los código están disponibles en el github de conento http://github.com/Conento/
Optimizador_MINLP. Si tenemos instalado pyomo, bonmin e ipopt en el path del sistema bastarı́a con ejecutar shiny::runGitHub(’Optimizador MINLP’,’Conento’), para
más detalles visitar el github.
El funcionamiento de la aplicación es muy sencillo para el usuario final. Veamos los
pasos a seguir:
1. Introducir el presupuesto total en euros a repartir en una semana y si queremos
permitir inversiones de cero (Problema 2) o por lo contrario queremos invertir en
todos los medios (Problema 1).
2. Eligir los medios de nuestra campaña a optimizar. En muchos casos no queremos
de ninguna manera invertir en un medio ası́ que podemos elimiar los medios que
queramos.
3. En la pestaña de optimización damos al botón “Ejecutar Optimización”.
4. Comprobamos los resultados de la optimización y podemos descargar los resultados en excel pulsando en “Descargar resultados de la optimización”
Como observamos el procedimiento es muy sencillo. Internamente simplemente
recogemos la información del usuario: Presupuesto, Permitir inversiones cero y medios en los que invertir. Y con estas tres opciones usamos la función que definimos
data ampl para pasar los datos a pyomo.
Veamos las lı́neas del código más importantes (el resto del código se puede ver en
el github):
10
# zoom es una matriz con los datos A,B, MIN y MAX de
#
los medios que ha seleccionado el usuario.
# presu es el presupuesto introducido por el usuario.
# input$permitir es una variable lógica que nos indica
#
si permite inversiones cero o no.
aux<-c(data_ampl(zoom,name="datos"),
data_ampl(presu,name="Presupuesto",tipo="param"))
file<- as.numeric(Sys.time())
write(aux,paste(file,".dat",sep=""))
if( isolate(input$permitir)){
system(paste("pyomo bonmin.py ",file,
".dat --solver bonmin --save-results ",file,
".json > ",file,".txt",sep=""))
}else{
system(paste("pyomo ipopt.py ",file,
".dat --solver ipopt --save-results ",file,
".json > ",file,".txt",sep=""))
}
salida<-fromJSON(content=paste(file,".json",sep=""))
unlink(paste(file,"*",sep="") )
De esta manera hemos pasado al optimizador los datos del problema con el escenario del usuario y hemos recuperado la información en la variable salida. Después
tratamos la salida y hacemos uso de los paquetes rCharts (Vaidyanathan, 2013) y googleVis (Gesmann and de Castillo, 2011) para conseguir el gráfico de reparto y la tabla
respectivamente.
11
4.
Conclusiones
En estas páginas hemos visto como conectar R con el software Pyomo y resolver
ası́ el problema planteado. Además aprovechando el gran ecosistema que nos proporciona R realizamos una aplicación potente y muy sencilla para el usuario final. Esto
último lo conseguimos gracias a paquetes como shiny, rCharts o googleVis.
Una vez comprendido el funcionamiento podemos extenderlo de manera fácil. Por
ejemplo, optimizar varias semanas teniendo en cuenta el efecto de la prolongación de
la publicidad más conocido por el término en inglés Advertising Adstock (Broadbent,
1979). También optimizar problemas más complicados ya que gracias a Pyomo tenemos gran flexibilidad para definir problemas usando python.
Para el futuro queda pendiente empaquetar esta idea en un paquete de R incluyendo Pyomo y algunos optimizadores para ası́ conseguir una distribución final más fácil
para el usuario de R.
12
5.
Bibliografı́a
Belotti, P. (2009). Couenne: a user’s manual. Department of Mathematical Sciences, Clemson University, Clemson, SC, available at http://www. coin-or. org/Couenne/couenneusermanual. pdf, accessed April, 23:2012.
Bonami, P. and Lee, J. (2007). Bonmin users’ manual. accessed November, 4:2008.
Broadbent, S. (1979). One way tv advertisements work. Journal of the Market Research.
Gay, D. M. (1997). Hooking your solver to ampl. Technical report, Technical Report
93-10, AT&T Bell Laboratories, Murray Hill, NJ, 1993, revised.
Gesmann, M. and de Castillo, D. (2011). googlevis: Interface between r and the google
visualisation api. The R Journal, 3(2):40–44.
Hart, W., Laird, C., Watson, J.-P., and Woodruff, D. (2012). Pyomo - Optimization Modeling in Python, volume 67 of Springer Optimization and Its Applications. Springer.
Johnson, S. G. (2013). The nlopt nonlinear-optimization package.
Lang, D. T. (2013). RJSONIO: Serialize R objects to JSON, JavaScript Object Notation. R
package version 1.0-3.
Leyffer, S., Linderoth, J., Luedtke, J., Mahajan, A., and Munson, T. (2011). Minotaur, a
toolkit for solving mixed-integer nonlinear optimization problems.
R Core Team (2013). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria.
RStudio and Inc. (2013). shiny: Web Application Framework for R. R package version
0.7.0.
Vaidyanathan, R. (2013). rCharts: Interactive Charts using Polycharts.js. R package version 0.3.51.
Wächter, A. and Biegler, L. T. (2006). On the implementation of an interior-point filter
line-search algorithm for large-scale nonlinear programming. Mathematical programming, 106(1):25–57.
Ypma, J. (2010). Introduction to ipoptr: an r interface to ipopt.
13
Anexo
Interpretación de los datos introducidos a Pyomo:
1 Set Declarations
datos :
Dim=0
Dimen=1
Size=5 Domain=None Ordered=False
Model=unknown
['Exterior', 'Online', 'Prensa', 'Radio', 'Televisión']
Bounds=None
0 RangeSet Declarations
5 Param Declarations
A : Size=5 Domain=NonNegativeReals
Exterior : 4.168857373
Online : 6.571585223
Prensa : 6.779516794
Radio : 6.18270522
Televisión : 7.091114787
B : Size=5 Domain=NonNegativeReals
Exterior : 26693.37017
Online : 62164.76381
Prensa : 105752.5395
Radio : 64579.77595
Televisión : 270163.8599
MAX : Size=5 Domain=NonNegativeReals
Exterior : 34971.98656
Online : 110009.0971
Prensa : 165047.6584
Radio : 102131.3559
Televisión : 421967.2358
MIN : Size=5 Domain=NonNegativeReals
Exterior : 20854.44233
Online : 38488.03122
Prensa : 71820.20725
Radio : 43419.72799
Televisión : 183359.5768
Presupuesto : Size=1 Domain=NonNegativeReals
1050000
2 Var Declarations
w : Size=5 Domain=Binary
Key : Initial Value : Lower Bound : Upper Bound : Current Value: Fixed: Stale
Exterior : None : 0 : 1 : None : False : True
Online : None : 0 : 1 : None : False : True
14
Prensa : None : 0 : 1 : None : False : True
Radio : None : 0 : 1 : None : False : True
Televisión : None : 0 : 1 : None : False : True
x : Size=5 Domain=Reals
Key : Initial Value : Lower Bound : Upper Bound : Current Value: Fixed: Stale
Exterior : None : 20854.44233 : 34971.98656 : None : False : True
Online : None : 38488.03122 : 110009.0971 : None : False : True
Prensa : None : 71820.20725 : 165047.6584 : None : False : True
Radio : None : 43419.72799 : 102131.3559 : None : False : True
Televisión : None : 183359.5768 : 421967.2358 : None : False : True
1 Objective Declarations
Total_Cost : Size=1
sum( prod( num=( w[Exterior] , exp( sum( 4.168857373 , -1 *
prod( num=( 26693.37017 ) , denom=( x[Exterior] ) ) ) ) ) ) ,
prod( num=( w[Radio] , exp( sum( 6.18270522 , -1 * prod( num=( 64579.77595 ) ,
denom=( x[Radio] ) ) ) ) ) ) , prod( num=( w[Prensa] ,
exp( sum( 6.779516794 , -1 *prod( num=( 105752.5395 ) ,
denom=( x[Prensa] ) ) ) ) ) ) , prod( num=( w[Televisión] ,
exp( sum( 7.091114787 , -1 * prod( num=( 270163.8599 ) ,
denom=( x[Televisión] ) ) ) )) ) , prod( num=( w[Online] ,
exp( sum( 6.571585223 , -1 * prod( num=( 62164.76381 ) ,
denom=( x[Online] ) ) ) ) ) ) )
1 Constraint Declarations
Demand : Size=1
-Inf
<=
sum( prod( num=( w[Exterior] , x[Exterior] ) ) ,
prod( num=( w[Radio] , x[Radio] ) ) ,
prod( num=( w[Prensa] , x[Prensa] ) ) ,
prod( num=( w[Televisión] , x[Televisión] ) ) ,
prod( num=( w[Online] , x[Online] ) ) )
<=
Presupuesto
0 Block Declarations
15
Descargar