From f0fa44b420b3855586a25fc0ea9545e8409b9fe5 Mon Sep 17 00:00:00 2001 From: Yves Caseau Date: Sat, 21 Jan 2023 11:58:15 +0100 Subject: [PATCH] First release of MMS - CSDM 2012 paper --- README.txt | 77 ++++++++ src/gtes.cl | 413 ++++++++++++++++++++++++++++++++++++++++++ src/init.cl | 32 ++++ src/log.cl | 246 +++++++++++++++++++++++++ src/model.cl | 495 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/simul.cl | 349 ++++++++++++++++++++++++++++++++++++ src/test1.cl | 320 +++++++++++++++++++++++++++++++++ src/test2.cl | 97 ++++++++++ 8 files changed, 2029 insertions(+) create mode 100644 README.txt create mode 100644 src/gtes.cl create mode 100644 src/init.cl create mode 100644 src/log.cl create mode 100644 src/model.cl create mode 100644 src/simul.cl create mode 100644 src/test1.cl create mode 100644 src/test2.cl diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..9ed587a --- /dev/null +++ b/README.txt @@ -0,0 +1,77 @@ +// +-------------------------------------------------------------------------+ +// | Micro Market Simulation (GTES) | +// | readme | +// | Copyright (C) Yves Caseau, 2012 | +// +-------------------------------------------------------------------------+ + +VERSION : V0.2 + +1. Project Description +====================== + +This is a super simplified version of CGS that has nothing to do with telephony +and may be shown in a book (bk16) or in a talk + +Each actor is a company with + - a customer base + - an ARPU + - a structural churn + - an attractiveness ( both a premium / generosity) measured as a delta-price + - fixed costs + variables costs + - a migration fluidity + +The system schema is simple + - churn is computed as structural * price factor + - Pdm is computed as a function of (price + premium) + +Tactic is simply a price vector (arpu = price - no migration involved) +Strategy is a combination of market share (LT) and revenue (ST) + + +we implement a full GTES similar to CGS and produce trajectories and +strategy matrices + + + +2. Version Description: (V0.1) +====================== + +This is the first version, the goal is to be done in a month, to produce two slides +for CSDM + +v0.2: +- restart the experiment in 2014, to produce a test case about Free, and to produce new slides for Compiegne + + +3. Installation: +=============== + +this is a standard module, look at init.cl in wk. + + +4. Claire files +=============== + +log.cl: as usual, the log file => where to look firt to read about the current state +model.cl data model: companies, experiments & scenarios, initialization + utilities +simul.cl simulation of the 36 months = loop + tactic automatas + learning + +5. Related doc +============== +overall description may be found at http://organisationarchitecture.blogspot.com/ + +6. Data +======= + +This project uses scenario files (look at oai to use a similar approach) + + +7.Test and run +============== + +As usual, the test file is test*.cl (currently test1.cl) +The test file contains the configuration + - description of the companies + - definition of the experiments + - go* methods which simply run go(E) + diff --git a/src/gtes.cl b/src/gtes.cl new file mode 100644 index 0000000..0936ff7 --- /dev/null +++ b/src/gtes.cl @@ -0,0 +1,413 @@ +// ******************************************************************** +// * CSS : Game theory simulation of mobile operators * +// * copyright (C) 2007-2010 Yves Caseau * +// * file: gtes.cl * +// ******************************************************************** + + +// this file contains the GTES methods applied to the CGS problem + +// ******************************************************************** +// * Part 1: Learning (Local moves) * +// * Part 2: Optimization Loop (optimize) * +// * Part 3: ExtendedNash Equilibrium (Controlled Convergence) * +// * Part 4: GTES Experiments (MonteCarlo Simulation) * +// * Part 5: Display methods * +// ******************************************************************** + + +// ******************************************************************** +// * Part 1: Learning (Local moves) * +// ******************************************************************** + +OPTI:integer :: 5 // trace parameter for optimization functions +NUM1:integer :: 6 // number of dichotomic steps (to be tuned) +NUM2:integer :: 3 // number of single moves exporation rounds +NUM3:integer :: 200 // number of random draws for 2opt + +// ------------- local move space ------------------------------------- + +// very simple here +NOPT:integer :: (NIT - 1) // the last year must be left in automatic mode +TAG :: integer // tactic fields are indexed through tags + +// read/write accessors - works for any value of NIT = 1,2 or 3 +[read(x:Tactic,i:TAG) : float + -> x.pricing[i] ] + +// this is where we propagate the trends (note that they are bounded) +[write(x:Tactic,i:TAG,v:float) : void + -> x.pricing[i] := v, + if (i = NOPT) x.pricing[i + 1] := v] // one more year + +// nice for debug +[label(i:TAG) : string -> "price_" /+ string!(i)] + +// cute debug +[whatif(c:Company,i:TAG,v:float) -> whatif(c,i,v,false) ] +[whatif(c:Company,i:TAG,v:float,talk?:boolean) + -> let x := c.tactic, v2 := read(x,i) , s2 := c.cursat in + (write(x,i,v), + runLoop(c), + //[0] whatif ~A(~S) = ~A -> sat = ~A vs ~A->~A // label(i),c,v,c.cursat,v2,s2, + if talk? (display(c), explain(c)), + write(x,i,v2)) ] + + +// local opt main loop (Hill-climbing) ----------------------------------------------- +OPTMODE:integer :: 0 // v0.6: 0 = both is absolutely necessary ! + // 1 = optimize only, 2 = optimize2 only + +// optimise the tactic a company +// notice that we keep the results obtained in the previous status, to measure the +// convergence of the results, or their absence ! +// v6: we removed the test to see if a basic tactic worked better ... +[optimize(c:Company) + -> let v1 := runLoop(c) in // used to reset cursat + (for i in (1 .. NUM2) + for p in (1 .. NOPT) (if (OPTMODE != 1) optimize2(c,p), + if (OPTMODE != 2) optimize(c,p)), + trace(TALK,"--- end optimize(~S) -> ~A% [from ~A% - d=~A]\n",c,f%(c.cursat), f%(v1), f%(deplacement(c)))) ] + +// first approach : relative steps (=> does not cross the 0 boundary, keeps the sign) ---------- + +// optimize a given slot in a set of two dichotomic steps +[optimize(c:Company,p:TAG) + -> for i in (1 .. NUM1) optimize(c,p,float!(2 ^ (i - 1))), + trace(OPTI,"best ~A for ~S is ~A => ~A\n", + label(p),c,read(c.tactic,p), c.cursat) ] + +// try to add / retract a (multiplying) increment +[optimize(c:Company,p:TAG,r:float) + -> //[OPTI] ..... start optimize(~S) : ~A @ ~A // c,label(p),r, + let vp := read(c.tactic,p), vr := c.cursat, val := 0.0, + v1 := vp / (1.0 + (1.0 / r)), v2 := vp * (1.0 + (1.0 / r)) in + (write(c.tactic,p,v1), + val := runLoop(c), + //[OPTI] try ~A (vs.~A) for ~A(~S) -> ~A (vs. ~A)// v1,vp,label(p),c,val,vr, + if (val > vr) (vp := v1, vr := val), + write(c.tactic,p,v2), + val := runLoop(c), + //[OPTI] try ~A for ~A(~S) -> ~A // v2,label(p),c,val, + if (val > vr) (vp := v2, vr := val), + write(c.tactic,p,vp), + c.cursat := vr) ] + + +// absolute variant -------------------------------------------------------------------- + +// optimize a given slot in a set of two dichotomic steps +[optimize2(c:Company,p:TAG) + -> for i in (1 .. NUM1) optimize2(c,p,float!(2 ^ (i - 1))), + trace(OPTI,"[2] best ~A for ~S is ~A => ~A\n", + label(p),c,read(c.tactic,p), c.cursat) ] + +SEED:float :: 0.1 +[optimize2(c:Company,p:TAG,r:float) + -> //[OPTI] ..... start optimize2(~S) : ~A @ ~A // c,label(p),r, + let vp := read(c.tactic,p), vr := c.cursat, val := 0.0, + v1 := vp + (SEED / r), v2 := vp - (SEED / r) in + (write(c.tactic,p,v1), + val := runLoop(c), + //[OPTI] try ~A (vs.~A) for ~S(~S) -> ~A (vs. ~A)// v1,vp,label(p),c,val,vr, + if (val > vr) (vp := v1, vr := val), + write(c.tactic,p,v2), + val := runLoop(c), + //[OPTI] try ~A for ~S(~S) -> ~A // v2,label(p),c,val, + if (val > vr) (vp := v2, vr := val), + write(c.tactic,p,vp), + c.cursat := vr) ] + + +// ------------------------------- 2-opt (copied from RTMS) ---------------------------------------------- +OPTI2:integer :: 1 + +// randomized 2-opt, borrowed from SOCC, but smarter:once the first random move is made, try to fix it with optimize +// tries more complex moves which are sometimes necessary +// n is the number of loops +[twoOpt(c:Company) + -> // optimize(c), // first run a single pass + let vr := c.cursat, val := 0.0 in + (let p1 := random(1,NOPT), + p2 := random(1,NOPT), + v1 := read(c.tactic,p1), v2 := read(c.tactic,p2) in + (if (p1 = p2) nil + else let v1new := v1 * (1.0 + ((if random(true) 1.0 else -1.0) / float!(2 ^ random(1,5)))) in + (write(c.tactic,p1,v1new), + //[OPTI2] === shift: ~S(~S) = ~A vs ~A // label(p1),c,read(c.tactic,p1),v1), + if (read(c.tactic,p1) != v1) optimize(c,p2), + val := c.cursat, + trace(OPTI2,"=== try2opt [~A vs ~A] with ~S(~A<-~A) x ~S(~A<-~A)\n", + val,vr,label(p1),read(c.tactic,p1),v1,label(p2),read(c.tactic,p2),v2), + if (val <= vr) (c.cursat := vr, write(c.tactic,p1,v1), write(c.tactic,p2,v2)) + else (vr := val, + trace(OPTI2,"*** improve ~A with ~S:~A x ~S:~A -> ~A\n", + val,label(p1),read(c.tactic,p1),label(p2),read(c.tactic,p2), val))))) ] + + +[twoOptimize(c:Company) + -> for i in (1 .. NUM3) twoOpt(c), + trace(OPTI2,"=== end TwoOpt(~S) with sat =~F%\n",c,c.cursat)] + +// ******************************************************************** +// * Part 2: Optimization Loop (optimize) * +// ******************************************************************** + + +// ----------------------------- main function ------------------------- + +ISTOP:integer :: 0 // stop after X cycles of optimization +IGSTOP:integer :: 0 // stop after Y cycles of g-optimization +MOPT:integer :: 0 // trace the meta-optimization loop - global level +MOPT2:integer :: 0 // trace the meta-optimization loop - fine level + +LCAT :: list("chaos", "war", "fight", "stable") // from worse to better +CHAOS :: 1 // used to understand chaos +WAR :: 2 +FIGHT :: 3 +STABLE :: 4 + +// new in v0.6: (3) overall optimization strategy --------------------------------------------------- + + +// DEBUG : logs for making nice pictures +TRACEON:boolean :: false // default +TRACEG:list :: list() +TRACED:list :: list() + +// sequential optimize loop (old code) +[soptimize(e:Experiment,i:integer,n:integer) + -> let stat? := (2 * i > n) in + (//[MOPT] ============ optimize loop [~A,~S] ======================= // i,stat?, + for c in Company copyTo(c.tactic,c.prevTactic), + pb.cycleDist := 0.0, + for c in Company + (optimize(c), + runLoop(c), // one last run ... to get real stable numbers + pb.cycleDist :+ deplacement(c), + if stat? (storeTrend(e,c), for c in Company storeStat(e,c)), + trace(MOPT,"[soptimize] sat levels: ~A\n", list{list(c,c.cursat) | c in Company})), + trace(MOPT,"[soptimize] total $: ~A, total distance: ~A \n", totalEbitda(),pb.cycleDist), + // DEBUG : logs for making nice pictures + if TRACEON (TRACEG :add integer!(totalEbitda()), + TRACED :add (pb.cycleDist / size(Company))), + if (i = ISTOP) error("stop")) ] + + +// debug method for tuning + reproduce old strategy (good for comparisons !) +[soptimize(e:Experiment) : void + -> for i in (1 .. e.scenario.nIter) soptimize(e,i,e.scenario.nIter)] + +// store a data sample in the stat database +[storeStat(e:Experiment,c:Company) : void + -> storeStat(e,c,e.result.tResult), // result for this monteCarlo run + storeStat(e,c,e.result.gResult) ] // global result for all runs + +// creates a measure that records all the relevant values (sat, ebit, arpu, etc.) for company c +[storeStat(e:Experiment,c:Company,x:TResult) : void + -> let sat := c.cursat, eb := current(c).ebitda, sha := current(c).share, + ap := current(c).arpu, cat := label(c) in + (add(x,c,eb,sha,sat), + addArpu(x,c,ap), + addLabel(x,c,cat)) ] + +// category : this is a key method - translate a quantitative (and false) results +// into a qualitative evaluation +// 1: winner, 2: looser 3: death - let's try a simple schema +SAT_THRESHOLD:float :: 0.5 // varies according to the model & situation +[label(c:Company) : integer + -> let csat := satisfaction(c,NIT) in // use last year's satisfaction + (if (csat > SAT_THRESHOLD & current(c).ebitda > 0.0) 1 + else if (current(c).ebitda < 0.0) 3 + else 2) ] + + +// this method is called each time a company's tactic is optimized +// used to compute the trend in satisfaction -> +[storeTrend(e:Experiment,c:Company) : void + -> let sat := c.cursat, totE := totalEbitda(), dTac := deplacement(c) in + (//[5] == Nash iteration convergence: totE = ~A, dTac = ~A // totE, dTac, + if (c = CSHOW) trace(0,"sat: ~A -> dTac = ~A \n",sat,dTac), + addLog(e,totE), // create a log of total satisfatction numbers + addTrend(e.result.tResult,dTac,totE)) ] + +[totalEbitda() : Price + -> sum(list{c.status[pb.year].ebitda | c in Company}) ] + + +// new in v0.6 : (1) parallel evaluation ----------------------------------------------------- + +// parallel optimize loop +// TODO : add stats +[poptimize(e:Experiment,i:integer,n:integer) + -> let stat? := (2 * i > n) in + (//[MOPT] ============ parallel-optimize loop [~A,~S] ======================= // i,stat?, + pb.cycleDist := 0.0, + for c in Company + (copyTo(c.tactic,c.prevTactic), + optimize(c), + copyTo(c.tactic,c.nextTactic), + copyTo(c.prevTactic,c.tactic)), + for c in Company + (copyTo(c.nextTactic,c.tactic), // all moves in parallel + pb.cycleDist :+ deplacement(c)), + reinit(), // one last run ... + loop(pb), // to get real stable numbers + if TRACEON (TRACEG :add integer!(totalEbitda()), + TRACED :add (pb.cycleDist / size(Company))), + if stat? + (for c in Company storeTrend(e,c), // store moves-related stats (deltas) + for c in Company storeStat(e,c)), // store satisfaction related stats + trace(MOPT,"[poptimize] sat levels: ~A\n", list{list(c,c.cursat) | c in Company}), + trace(MOPT,"[poptimize] total $: ~A, total distance: ~A \n", totalEbitda(),pb.cycleDist), + if (i = ISTOP) error("stop")) ] + + +// debug method for tuning + reproduce old strategy (good for comparisons !) +[poptimize(e:Experiment) : void + -> for i in (1 .. e.scenario.nIter) poptimize(e,i,e.scenario.nIter), + display() ] + + +// ******************************************************************** +// * Part 4: GTES Experiments (MonteCarlo Simulation) * +// ******************************************************************** + +// this needs to be tuned more carefully - i.e. with a better use of +// optimization methods - 3opt & GenOpt +// new in v0.6 - categories are defined through a linear regression +// TODO : tune the numerical values !!!! +// interesting: stability is actually easier to get ... +[categorize(e:Experiment) : integer + -> let l := linearRegression(e.listX,e.listY), + slope := l[1] * length(e.listX), constv := l[2], dev := l[3] in + (//[0] >>> limit = ~A <<<< LR = ~A // constv + slope * length(e.listX),l, + //[0] >>> dev = ~A (vs 0.05), slope = ~A // dev / abs(constv),slope / abs(constv), + if (dev < 0.05 * abs(constv) & abs(slope) < 0.01 * abs(constv)) STABLE // slope is flat + else if (dev < 0.1 * abs(constv) & slope < -0.02 * abs(constv)) WAR + else CHAOS)] + +// run an experiment +RUNLOOP:integer :: 1 // look at CGS v0.6 to add new variants +[run(e:Experiment) : void + -> time_set(), + init(e), + for i in (1 .. e.scenario.nTest) // number of monte-carlo simulation + (//[TALK] start Test case ~A at ~As ===== // i, time_read() / 1000, + loop(pb), + resetLog(e), // starts a fresh log that will be used to categorized + if (RUNLOOP = 1) soptimize(e) + else error("the stuff in CGS is not implemented in MMS :)"), + //[0] ###### end of test case ~A -> ~A (~As) ####### // i, LCAT[categorize(e)], time_read() / 1000, + //[5] [Monte-Carlo] sat levels: ~A [dist log : ~A] // list{list(c,c.cursat) | c in Company},e.listZ, + storeStat(e,i), + if (verbose() >= 1) display(e.result.tResult), + if (i != e.scenario.nTest) reinit(e,true)), + //[0] =================== end of experiment ~S @ ~As ======================= // e,time_read() / 1000, + //[1] === log of Ebit: ~A // e.listY, + display(e.result), + summary(e,time_read()), + trace(0,"result file generated in ~A\n", + Id(*where*) /+ "\\data\\" /+ string!(name(e)) /+ "-" /+ + string!(name(e.scenario))) ] + +// quadratic residue = linear regression error (sum of squares of distance) +[LRdist(e:Experiment) : float -> linearRegression(e.listX,e.listY)[3] ] + +// store the Test data sample in the TResult stat database +[storeStat(e:Experiment,i:integer) : void + -> let cat := categorize(e), mE := mean(e.result.tResult.totalE), dE := stdev%(e.result.tResult.totalE), + mD := mean(e.result.tResult.tacticD), + residue := LRdist(e) in + (//[0] === Test case ~A: category is ~A (~A), ~A$[~A%] - mean D:~A]// i,cat,LCAT[cat],integer!(mE),f%(dE),mD, + addCategory(e.result,cat), + addAverages(e.result,mE,dE,mD,residue), + for c in Company + (add(c.global,globalSat(c)), + for i in (1 .. NIT) add(c.ebitdas[i],c.status[i].ebitda)), + if (cat = STABLE) + (for c in Company storeStat(e,c,e.result.cResult))) ] + + +// display the summary for a full experiment +summary(e:Experiment,runTime:integer) : void + -> let p := fopen(Id(*where*) /+ "\\data\\" /+ string!(name(e)) /+ "-" /+ + string!(name(e.scenario)),"w") in + (use_as_output(p), + display(e,runTime), + display(e.result), + fclose(p)) + +// log a result in a log file - reusable pattern +logResult(pr:property,v:float,e:Experiment) : void + -> let p := fopen(Id(*where*) /+ "\\data\\log","a") in + (use_as_output(p), + printf("[~A:~S=~A on ~S", + substring(date!(0),1,19),pr,v,e), + printf(" sat=~F%,churn=~F%,price=~A]\n", + sum(list{c.cursat | c in Company}) / 3.0, + sum(list{current(c).churn% | c in Company}) / 3.0, + sum(list{current(c).arpu | c in Company}) / 3.0), + fclose(p)) + +// ******************************************************************** +// * Part 5: Display methods * +// ******************************************************************** + +// same thing that may be seen +display(e:Experiment,t:integer) : void + -> (printf("=== Experiment ~S [~A s] ==== (runloop=~A)\n", e,t / 1000,RUNLOOP), + printf(" done on ~A version ~A\n",date!(1),Version), + printf(" scenario = ~S x ~A Tests [~A iterations]\n", + e.scenario,e.scenario.nTest, e.scenario.nIter), + printf(" strategies = ~A \n\n",e.strategies)) + +// display +[display(x:TResult) : void + -> // TODO print the Test results stats ? + for c in Company + printf("------------ ~S [~A] -------------\n~I~I~I",c, + integer!((x.measures[c.index]).ebitda.Reader/num_value), + displayProfile(x.wins[c.index], x.looses[c.index], x.deaths[c.index]), + display(x.measures[c.index]), + displayGlobal(c)) ] + + +// display profile +[displayProfile(x:measure,y:measure,z:measure) : void + -> printf("wins ~F%, looses ~F%, dies ~F% (~F%)\n", + mean(x), mean(y), mean(z), stdev(z)) ] + +// key values +[display(x:Result) : void + -> printf("ebitda: ~F2 M$ [dev: ~F%] ",mean(x.ebitda),stdev%(x.ebitda)), + printf("share: ~F% [dev: ~F%]\n",mean(x.share),stdev%(x.share)), + printf("arpu: ~F2$ [dev: ~F%] ",mean(x.arpu),stdev%(x.arpu)), + printf("=> strategy success: ~F% [%dev: ~F%]\n",mean(x.success), stdev%(x.success)) ] + +// display the ebitda statistics and the overall global satisfaction +[displayGlobal(c:Company) : void + -> printf("trajectory: ~A, global sat: ~F%\n",list{mean(c.ebitdas[i]) | i in (1 .. NIT)},mean(c.global)) ] + + +// full version of display : include meta stats +[display(x:EResult) : void + -> printf("==== experiment avg total Ebit ~F1, total deplacement ~F2 === \n",mean(x.totalE), mean(x.totalD)), + printf("==== deviation of Ebit (1) convergence: ~F% (2) sampling: ~F% \n",mean(x.devE), stdev%(x.totalE)), + display(x.gResult), + printf("==== stable ~F%, WARS ~F%, CHAOS ~F% ======\n", + mean(x.stable), mean(x.war), mean(x.chaos)), + printf("************ stable results *******************************************\n"), + display(x.cResult) ] + + +// ------------------------- our reusable trick ------------------------- + +[ld1() : void -> load(Id(*src* / "mmsv" /+ string!(Version) / "test1")) ] +[ld2() : void -> load(Id(*src* /+ "\\mmsv" /+ string!(Version) /+ "\\test2")) ] + +// we load a file of interpreted code which contains the program description +(#if (compiler.active? = false | compiler.loading? = true) ld1() + else nil) + + diff --git a/src/init.cl b/src/init.cl new file mode 100644 index 0000000..6289bf9 --- /dev/null +++ b/src/init.cl @@ -0,0 +1,32 @@ +(printf("--- load init Micro Market Simulation --- \n")) + +*src* :: "/Users/ycaseau/Dropbox/src" +*where* :: "/Users/ycaseau/proj/MMS" + +(debug(), + verbose() := 2, + safety(compiler) := 5) // ensure safe compiling + remove warnings + + +// module - Yves's version of January 2012 -> lab workbench for Marketing's hypotheses +// KISS model ! keep the same structure as CGS, hence it may be used as a tutorial to CGS :) +m1 :: module(part_of = claire, + source = *src* / "mmsv0.1", + uses = list(Reader), + made_of = list("model","simul","gtes")) + + +// module - Yves's version of October 2014 -> lab workbench for Marketing's hypotheses +// KISS model ! keep the same structure as CGS, hence it may be used as a tutorial to CGS :) +m2 :: module(part_of = claire, + source = *src* / "mmsv0.2", + uses = list(Reader), + made_of = list("model","simul","gtes")) + +// 2022 : move to CLAIRE4 +m3 :: module(part_of = claire, + source = *src* / "mmsv0.3", + uses = list(Reader), + made_of = list("model","simul","gtes")) + + diff --git a/src/log.cl b/src/log.cl new file mode 100644 index 0000000..60197a4 --- /dev/null +++ b/src/log.cl @@ -0,0 +1,246 @@ + + + +--------------------------------------+ + | MMS (GTES) Project log file | + +--------------------------------------+ + + + +// ============================= v0.1 =================================================================== +// 4/11/2012 +start project :-) +From readme: +This is a super simplified version of CGS that has nothing to do with telephony +and may be shown in a book (bk16) or in a talk +; ss + + + +Each actor is a company with + - a customer base + - an ARPU + - a structural churn + - an attractiveness ( both a premium / generosity) measured as a delta-price + - fixed costs + variables costs + +The system schema is simple + - churn is computed as structural * price factor + - Pdm is computed as a function of (price + premium) + +Tactic is simply a price vector (arpu = price - no migration involved) +Strategy is a combination of market share (LT) and revenue (ST) + +we implement a full GTES similar to CGS and produce trajectories and +strategy matrices + +next steps: + +- full clean-up of CGS files ! + write new test files using Free as a test case :) + +(a) Model +-> keep only what is necessary for simul -> 3 phase, trivial model :) + + +(b) trouver des chiffres + +http://www.pcinpact.com/news/70659-orange-615-000-abonnes-free-mobile.htm +http://www.iliad.fr/en/finances/2012/Slideshow_S1_2012_310812.pdf +http://www.pcinpact.com/news/70921-abonnes-bilan-fai-operateurs-mobiles.htm + +(c) simul (3 or 5 years) + + +//5/11/2012 +try to load - done :) +go0() : first test + +// 6/11/2012 +move to Dropbox source +(a) 2011 PdM -> OK (tuned premium) + ajouter le share dans le init (plus facile de comparer :)) -> cf 3YP + +(b) 2012 total acq -> cf ARCEP -> environ 10M de ventes brutes en 2011, un x2 sur S1, donc entre + 16-18M pour 2012 + pr�vision BT pour PNB: 7.6 -> 14, mais attention PP decroit et Entreprise flat + churn BT � 28% + +(c) v�rifier PdM 2012 : Free � 35-40% + on pourrait faire deux tests: + (c1) sans r�action -> cf. le d�but + (c2) avec r�action -> cf pr�visions de chaque op�rateur +- done + +// 10/11/2012 (home PC) +- tune to probable Free (2.6+1+0.7+0.8 -Xmas has little impact -> 5.3) +- tune satisfaction +- add gtes.cl (but only the simple stuff for the CSDM experiments) + + +// 11/11/2012 (laptop) +Next steps: play the scenario where Orange cuts price by half the first year +(1) there is a problem with the variable expense + attention: Bytel is clearly too expensive on the variable (fancy mobile + generous => more interco) + but the fixed is not that different + partially solved by lowering fixed exp for Orange + +// 13/11/2012 +the way we setup strategy (percentage) does not work +it is better to use fixed numbers ... +also, the importance on market share is too strong + +// 14/11/2012 +- fixed the migration (price dependent -> churnRate) +- go(op) pour chaque op, jusqu a ce que la tactique optimis�e ait du sens + Note: twoOpt aurait du sens ici ! + cela se fait surtout en ajustant la strat�gie + + +// 15/11/2012 +- tuning de strat�gie avec des versions pour les 5 op�rateurs +- nash() -> OK + attention, convergence des tactique mais un �quilibre qui se d�place lentement + + +// 17/11/2012 + +lancer une petite version simple de nash() et mesurer la convergence + +E1, E2, E3 : trois niveaux de strat�gie + +SC1, SC2, SC3 : trois sc�narios de couts + +voir quelle strat�gie pourrait laisser les prix � plus de 300� - done :) + + +//18/11/2012 + +implement a mode "minRatio = 0" for Free: expect + - constant marketshare + - linear growth of Ebitda + - linear growth of base + +GTES complet + + (a) randomiser alpha et beta � la main pour sentir (courbe CSDM) + (b) run complet de optimize(e) + (c) lancer run(e) (Monte-Carlo) - cf. go3 + -> done !!! + + +// 25/11/2012 + +-> look at go(E2,Sc1) : why chaos ? + no nash equilibrium : rotation around attractors -> try poptimize (cf. RAIRO, why we introduce garded Nash) + +-> passer � 50, lancer des script pour les tables CSDM + +-> faire les tableur excel (a) sur un papier (quelle courbe) (b) sous Excel + + +// 2/12/2012 : last WE ! + +- ajouter une satisfaction de r�f�rence (par rapport aux chiffres 2012 pour les 3 ops, et par rapport � ?? pour Free) +- sortir les trajectoires moyennes d'EBITDA +- corriger la satisfaction de Free + +// 9/12/2012 : close for CDSM + +// 23/9/2014: reopen ! +- first step is to move to CLAIRE 3.4 ! +- second step to to load & compile +- open v0.2 on laptop + + +// CSDM PITCH ----------------------------------------------------------------------------------------------------------------- + +message: meme un systeme aussi simple r�serve des surprises (cf. erreurs � Bytel) + +Ce qu on apprend: + +- quoi qu'on fasse cela part en guerre +- il n'y pas la place pour 4 � ces niveaux de prix + +- il y a une bataille de couts fixes/ point mort +- si Free n a pas besoin de construire un r�seau, il n est pas le maillon faible + +a faire: d�finir le prix d equilibre p(X) + equilibre = cost(X) / X + +Bytel -> deux axes: + - plus de g�n�rosit� -> augmenter le premium + - reduire les couts + +Note: montrer le taux de chaos -> besoin de m�thodes plus sophistiqu�es + +// SLIDE CDSDM model +- dessin +- equation du mod�le = alpha, beta +- mod�le d entreprise = cost (fixed, variable) + premium + tactique (prix) + strategie + + +// SLIDES CSDM results (2) + +(a) 4 histoire : avant (sans Free) + E1 + E2 + E3 + +(b) variations + - sur les conditions alpha Sc1a+, Sc1a- + - sur les conditions beta Sc1b+, Sc1b- + - sur le r�seau de Free Sc2 + - sur les couts Sc3 + + +Il ne reste plus qu a d�finir le format de r�sultat = courbes + variations + qualification war/stable/chaos + + +// CAVEAT (� mettre dans la slide mod�le) + +- closed market +- uniform market (real life = segmented) +- naive model + +still interesting since +- results with a more sophisticated model are more moderate but same structure +- allows to learn about churn and cycles +- show the reactive behaviour one against another + + +// v0.2 ================================ fresh for business case ================================= + + + +5/1/2014 : reopen & recompile +15/1/2014 : play with Nash loop : create go1 (Instrumented Nash) + + +Design parameters to test before the UTC talk + (1) SMULT : satisfaction multiplicative formula + (2) popt versus sopt + (3) introduction of twoOpt (from RTMS) + (4) NUM2 & nIter (inner loop & outer loop) + +Outcome : + (1) categorization + (2) trace of sigma(ebit) et convergence + + +CONCLUSION: + (a) we have not found the proper multiplicative formula that takes hard constraint into account + => keep to additive formula ! (larger range) + (b) we have a nice list of 2 experiments : E1, E2, E3 with stable Nash ! + + + +TODO + +(1) aller au bout des points fixes ! + - faire des tests sur NUM2 & nIter + - tester PNASH et NASH2 (cf. test1.cl) + +(2) scenario pour business case, expos� UTC Compi�gne + + +// v0.3 ================================ move to CLAIRE 4 ================================= + +// changes (interesting to log to add the change section to the CLAIRE4 doc) +- ephemeral_object : should be added as an alias (to improve portability) +- "slot override for p=index" \ No newline at end of file diff --git a/src/model.cl b/src/model.cl new file mode 100644 index 0000000..e339f36 --- /dev/null +++ b/src/model.cl @@ -0,0 +1,495 @@ +// ******************************************************************** +// * MMS : Micro Market Simulation * +// * copyright (C) 2012-2022 Yves Caseau * +// * file: model.cl * +// ******************************************************************** + +// this file contains the data model for the simulation project + +// ******************************************************************** +// * Part 1: Company Classes * +// * Part 2: System & Simulation classes * +// * Part 3: Simulation & Results * +// * Part 4: Data creation * +// * Part 5: Utility functions * +// ******************************************************************** + +Version :: 0.3 // moved to CLAIRE4 i n 2022 + +TALK:integer :: 1 +SHOW:integer :: 2 +DEBUG:integer :: 2 + +NIT:integer :: 10 // 3 year plan simulations - start with one for debug + +BIGF :: 1e30 + +// add to CLAIRE4 (to make portabulity easier) +ephemeral_object <: object + +// ******************************************************************** +// * Part 1: Company Classes * +// ******************************************************************** + +// Type aliases +Percent :: float +Price :: float // euros or millions of euros +Time :: integer // time is measured in weeks/100 (100 = 1.week) + +f%(x:integer) : float -> (float!(x) / 100.0) +f%(x:float) : integer -> integer!(x * 100.0) + +// Foward +Strategy <: thing +Tactic <: ephemeral_object +Status <: ephemeral_object + +// a Company +// note that variable cost is assumed to be constant (a gross simplification) which means that +// we only use the "expenses" (fixed cost) list to play what-if with cost reduction +Company <: thing( + index:integer = 0, // each company has an index -> list[] access + // problem data model slots (given as part of the description problem) + expenses:list, // fixed yearly expenses (N + 1 since e[1] : start year) + variable:Price, // per subscriber expenses + expenseTrend:Percent, // variable expense reduction trend + fluidity:Percent, // migrations is represented by a percentage of customers + churn%:Percent, // yearly churn rate (default rate) + premium:Price, + // subcomponents + strategy:Strategy, // goals + reference:Strategy, // stable ref point for goals + tactic:Tactic, // how the company react to deltas (reality - goals) + startTactic:Tactic, // original tactic (when GTES starts) + prevTactic:Tactic, // copy of previous tactic within an optim cycle (useful to measure distance) + nextTactic:Tactic, // original tactic + start:Status, // reference point when the simulation start (year 0 - cf previous(o)) + status:list, // current status for each year of the simulation + // slots that are computed + ebitdas:list, // (avg) ebitda trajectory + global:measure, // global satisfaction, measure against a stable reference + cursat:float, // average satisfaction with current tactic + index:integer = 0) // internal index + + +// the status of a company +// this is a set of slots for which we keep the original value and the +// current ones +Status <: ephemeral_object( + ebitda:float, // monthly ebitda + expense:Price, // total yearly expenses + churn%:Percent, // yearly churn rate + base:float, // subscriber base + sales:Price, // monthly income from outgoing traffic + arpu:Price, // average arpu + price:Price, // sell price + acqNum:float, // number of acquisitions (new customers) + share:Percent) // market share + + +// A strategy defines the growth that is expected for EBITDA and market share, using absolute targets +// this is different from CGS (look at a mature market) +Strategy <: thing( + ebitda:Percent, // yearly growth (expected evolution) + share:Percent, // Market share + base:float, // expected customer base (millions) + minRatio:Percent) // absolute min acceptable value for Ebitda/sales ratio + + +// a Tactic defines how the company plays its bets according to three dimensions: +// - ARPU : reduction (%) of the "average offer" +Tactic <: ephemeral_object( + pricing:list) // list per year (NIT) + + +// ******************************************************************** +// * Part 2: System & Simulation classes * +// ******************************************************************** + +Scenario <: thing + +// a market is defined with two parameters +// - price to volume sensitivity (alpha) +// - price sensitivity for churn (beta) +// when we do full GTES, we'll Monte-Carlo those ! + +// common object +Problem <: thing( + // global variables - used by the simulator + startYear:integer = 0, // reference: "zero year" + year:integer = 1, // 1 to 36 + scenario:Scenario, // + acqNum:float, // total market + nLoop:integer = 1, // # of average case + cycleDist:float = 0.0, // sum of deplacements (tactic delta) for one optim cycle + // elasticity constants - famous six - cf. Word document + alpha:float, // price to share elasticity (pi^alpha / sigma(pi^alpha) + beta:float) // same parameter for churn + + +pb :: Problem() + +// a scenario sets the macro-economy parameters that need to be explored +Scenario <: thing( + nIter:integer = 5, // numbre of Optimization iterations + nTest:integer = 1, // numbre of monte-carlo testcases + costs:set[tuple(Company,list)], // fixed cost stucture test-cases + // range of variation for elasticity parameters + alphaMin:float = 0.0, // min value for alpha (price2Volume elasticity) + alphaMax:float = 1.0, + betaMin:float = 0.0, // min value for beta ( + betaMax:float = 1.0) + + +// ******************************************************************** +// * Part 3: Simulation & Results * +// ******************************************************************** + +/* what we measure for one run +Measure <: ephemeral_object( + sum:float = 0.0, + square:float = 0.0, // used for standard deviation + num:float = 0.0) // number of experiments + +// simple methods add, mean, stdev +[add(x:Measure, f:float) : void -> x.num :+ 1.0, x.sum :+ f, x.square :+ f * f ] +[mean(x:Measure) : float -> if (x.num = 0.0) 0.0 else x.sum / x.num] +[stdev(x:Measure) : float + -> let y := ((x.square / x.num) - ((x.sum / x.num) ^ 2.0)) in + (if (y > 0.0) sqrt(y) else 0.0) ] +[stdev%(x:Measure) : Percent -> stdev(x) / mean(x) ] +[reset(x:Measure) : void -> x.square := 0.0, x.num := 0.0, x.sum := 0.0 ] */ + + +// what we measure for one run and one company +Result <: ephemeral_object( + success:measure, // satisfaction w.r.t. strategy (key metric) + ebitda:measure, + share:measure, + arpu:measure) // average + +// add a measure to a Result (for a company) +[add(x:Result,e:Price,sh:Percent,suc:Percent) : void + -> add(x.ebitda,e), + //[DEBUG] --- add ebitda measure: ~A // e, + add(x.share,sh), add(x.success,suc) ] +[addArpu(x:Result,ap:Price) : void + -> add(x.arpu,ap) ] +[reset(x:Result) : void + -> reset(x.arpu), + reset(x.ebitda), reset(x.share), reset(x.success) ] +[makeResult() : Result + -> Result(ebitda = measure(), share = measure(), success = measure(), arpu = measure()) ] + + +// what we measure for each testcase : +// (1) a list of results +// (2) we add a qualitative approach = count categories (wins, looses, death) +TResult <: ephemeral_object( + measures:list, // samples for each company + totalE:measure, // record the totalEbit to detect fight using linear Regression + tacticD:measure, // measures the distance in tactical moves (should get close to 0) + wins:list, // list of 0/1 : boolean vars for wins (category(c)) => vector by company + looses:list, // v0.6 : call this labels + deaths:list) // see findLabel(c) for semantic of win/loose/death + + +// add a sample in the measure database +[add(x:TResult,c:Company,e:Price,sh:Percent,suc:Percent) : void + -> add(x.measures[c.index],e,sh,suc) ] +[addArpu(x:TResult,c:Company,ap:Price) : void + -> addArpu(x.measures[c.index],ap) ] + +[addTrend(x:TResult,dTac:Percent,totE:Price) : void + -> add(x.tacticD,dTac), + add(x.totalE,totE) ] + +// company label (1 to 3 : wins/looses/dies) +[addLabel(x:TResult,c:Company,y:(1 .. 3)) : void + -> add(x.wins[c.index], (if (y = 1) 1.0 else 0.0)), + add(x.looses[c.index], (if (y = 2) 1.0 else 0.0)), + add(x.deaths[c.index], (if (y = 3) 1.0 else 0.0)) ] + +[reset(x:TResult) : void + -> for y in x.measures reset(y), + for y in x.wins reset(y), + for y in x.looses reset(y), + for y in x.measures reset(y) ] + +[makeTResult() : TResult + -> TResult(measures = list{makeResult() | c in Company}, + tacticD = measure(), totalE = measure(), + wins = list{measure() | c in Company}, + looses = list{measure() | c in Company}, + deaths = list{measure() | c in Company}) ] + +// what we measure for one experience is +// (1) global aggregated stat +// (2) count meta categories (stable, unstable(chaos), unstable(war)) +// (3) store results for stable +EResult <: ephemeral_object( + gResult:TResult, // combined stat (raw numbers) + tResult:TResult, // Test result (one by monteCarlo run) + cResult:TResult, // clean result (average of stable tests) + totalE:measure, // total EBITDA (average, TODO = obtained by linear regression) + devE:measure, // stdev of total EBITDA + LRdev:measure, // quadratic residue from LR + totalD:measure, // target deplacement (average, TODO = obtained by linear regression) + stable:measure, // category record (same as labels: binay variables => average is a %) + war:measure, // see category(e) in simul.cl for semantics + fight:measure, // new in v0.6 : + chaos:measure) + +// category +[addCategory(x:EResult,y:(1 .. 4)) : void + -> add(x.fight, (if (y = 3) 1.0 else 0.0)), + add(x.stable, (if (y = 4) 1.0 else 0.0)), + add(x.war, (if (y = 2) 1.0 else 0.0)), + add(x.chaos, (if (y = 1) 1.0 else 0.0)) ] + +// add the averages of ebitda et deplacement +[addAverages(x:EResult,mE:float,dE:float,mD:float,res:float) : void + -> add(x.totalE,mE), add(x.devE,dE), + add(x.totalD,mD), add(x.LRdev,res) ] + +[makeEResult() : EResult + -> EResult(gResult = makeTResult(), + tResult = makeTResult(), + cResult = makeTResult(), + totalE = measure(), totalD = measure(), devE = measure(), LRdev = measure(), + stable = measure(), fight = measure(), war = measure(), chaos = measure()) ] + +// an experiment takes a set of a strategy, a given scenario, optimizes the +// tactics and returns the average Tresult +Experiment <: thing( + scenario:Scenario, + category:integer = 0, // set by the algorithm + result:EResult, // global result + listX:list, // record runs + listY:list, // record total Ebitda + listZ:list, // record deplacements + index:float, // index = number of value in listX/listY + strategies:set[tuple(Company,Strategy)], // set of (company,stategies) to be assigned for this exp. + sTactics:set[tuple(Company,Tactic)]) // same for starting tactics (new in v0.5) + + +// ******************************************************************** +// * Part 4: Data creation * +// ******************************************************************** + +// initialization +[init(o:Company,y:integer,s:Status) : void + -> o.expenses := extendTo(o.expenses,NIT + 1), + if (pb.startYear = 0) pb.startYear := y + else if (y != pb.startYear) error("wrong year: ~A",y), + o.index := size(Company), + if (s.base > 0.0) + (// default case with a history - cf. test.1 file + s.expense := s.sales - s.ebitda, // total expenses = fixed + variable + s.arpu := s.sales / s.base, + s.price := s.arpu, + o.variable := (s.expense - o.expenses[1]) / s.base) // attention expenses[1] = start year + else (s.arpu := 0.0), // assumes that we start with a stable state + o.start := s, + o.global := measure(), + o.ebitdas := list{measure() | i in (1 .. NIT)}, + o.status := list{copy(s) | i in (1 .. NIT)}] + +// trend-based : we provide with the 3 % trends ... +[makeTactic(t1:Percent,t2:Percent,t3:Percent) : Tactic + -> Tactic( pricing = extendTo(list(t1,t2,t3),NIT)) ] // list per year (NIT) + + +// useful : allows to change NIT (extrapolation of all data) +[extendTo(l:list,k:integer) : type[l] + -> let n := length(l) in (for i in (n + 1 .. k) l :add l[n], l) ] + +// useful to implement trends over a few years (new customer, etc.) +// linear interpolation : i is the number of years +[expected(v:float,rate:float,i:integer) : float + -> v * ((1.0 + rate) ^ float!(i)) ] + +// deep copy +[makeTactic(x:Tactic) : Tactic + -> Tactic( pricing = list{x.pricing[y] | y in (1 .. NIT)}) ] + +[copyTo(x:Tactic,y:Tactic) : void + -> for i in (1 .. NIT) y.pricing[i] := x.pricing[i]] + +[yearExp(x:float) : float -> ((1.0 + x) ^ 12.0) - 1.0 ] + + +// initialize an experiment +[init(e:Experiment) : void + -> pb.scenario := e.scenario, + e.result := makeEResult(), + init(pb.scenario), // Monte-Carlo init ... TODO eventually (easier debug this way) + e.listX := list(), + e.listY := list(), + e.listZ := list(), + e.index := 0.0, + for x in e.strategies x[1].strategy := x[2], + for x in e.sTactics x[1].tactic := x[2], + for c in Company + (c.start.share := (c.start.base / sum(list{c.start.base | c in Company})), + c.startTactic := makeTactic(c.tactic), // necessary for GTES (Monte-carlo multiple runs) -> used in reinit + c.prevTactic := makeTactic(c.tactic), // necessary for GTES -> used in reinit + c.nextTactic := makeTactic(c.tactic)), + let lc := list{o in Company | o.start.base > 0.0}, + lr := psi(list{(o.start.arpu - 12.0 * o.premium) | o in lc},pb.alpha) in + (//[TALK] model check: init shares = ~A // lr, + for i in (1 .. length(lc)) lc[i].start.share := lr[i]) ] + +// manage the listX, listY log +[resetLog(e:Experiment) : void + -> e.index := 0.0, + shrink(e.listX,0), + shrink(e.listY,0), + shrink(e.listZ,0) ] + +// record cycle-level data for the optimize loop : total Ebitda and Cycle-Distance, to evaluate connvence +[addLog(e:Experiment,totE:float) : void + -> e.index :+ 1.0, + e.listX :add e.index, + e.listY :add totE, + e.listZ :add pb.cycleDist ] + +// initialize a scenario : Monte-Carlo simulation +// is called for each sequence of the experience called a Test +[init(s:Scenario) + -> //[0] ==== Monte-Carlo Instanciation of ~S ================================== // s, + pb.alpha := randomIn(s.alphaMin,s.alphaMax), + pb.beta := randomIn(s.betaMin,s.betaMax), + if unknown?(scenario,pb) pb.scenario := s, + see(pb), + for l in s.costs + let o := l[1] in + (o.expenses := extendTo(l[2],NIT + 1), // overide cost structure + if (o.start.base > 0.0) o.variable := (o.start.expense - o.expenses[1]) / o.start.base) ] + +[reinit() + -> for c in Company + (c.status[1] := copy(c.start)) ] + +[reinit(e:Experiment,random?:boolean) : void + -> if random? init(e.scenario), + reset(e.result.tResult), + for c in Company copyTo(c.startTactic,c.tactic) ] // original tactic - restart optim at the same point ! + +// easy access to status +[current(o:Company) : Status => (o.status[pb.year]) ] +[previous(o:Company) : Status -> if (pb.year > 1) o.status[pb.year - 1] else o.start ] + +// distance between two tactics normalized +[distance(x:Tactic,y:Tactic) : float + -> let d := 0.0 in + (for i in (1 .. NIT) d :+ sqr(x.pricing[i] - y.pricing[i]), + d) ] + +// euclidian norm (squared) +[norm(x:Tactic) : float + -> let d := 0.0 in + (for i in (1 .. NIT) d :+ sqr(x.pricing[i]), + d) ] + +// measures the deplacement : distance between two successive tactics in an optimization step +// v6: normalized :) +[deplacement(c:Company) : Percent + -> let v1 := distance(c.prevTactic,c.tactic) in + (if (v1 = 0.0) 0.0 else (v1 / max(norm(c.prevTactic),norm(c.tactic)))) ] + +// ******************************************************************** +// * Part 5: Utility functions * +// ******************************************************************** + +sum(s:list) : float + => let d := 0.0 in (for y in s d :+ y, d) + + +== :: operation(precedence = precedence(=)) +==(x:float,y:float) : boolean + -> (abs(x - y) < (abs(x) + abs(y) + 1.0) * 1e-2) + +// three small functions that are used to represent satisfaction +// pos5 is a concave function so that satisfaction weight less than dissatisfaction +// pos is called with a number that varies from -INT to 1.0, hence returns a number less than 1.0 +pos(x:float) : float -> (if (x > 0.0) x else 0.0) +pos5(x:float) : float -> (if (x > 0.1) (x - 0.09) else if (x > 0.0) x / 10.0 else x / 100.0) +pos4(x:float) : float -> (if (x > 0.0) x else (x / 100.0)) + +// v2: pos6 returns something between 0 (for x = -inf) and 10 +pos6(x:float) : float -> min(10.0,pos4(x)) + + + + +// divide each member by the sum so that the list becomes a % distribution list +normalize(l:list) : list + -> let x := sum(l) in list{ (y / x) | y in l} + + +// utility functions ------------------------------------------------- + +/* the pF is my ugly duckling :) ------------------------------------------- +// float print is now standard in v3.4.42 but this is still a cuter print ... +[pF(x:float,i:integer) : void // prinf i numbers + -> if (x < 0.0) (princ("-"), pF(-(x),i)) + else let frac := x - float!(integer!(x + 1e-10)) + 1e-10 in + printf("~A.~I", integer!(x + 1e-10), + pF(integer!(frac * (10.0 ^ float!(i))),i)) ] + +// print the first i digits of an integer +[pF(x:integer,i:integer) : void + -> if (i > 0) let f := 10 ^ (i - 1), d := x / f in + (princ(d), if (i > 1) pF(x mod f, i - 1)) ] + +[p%(x:float) -> pF(x * 100.0,2), princ("%")] +[pF(x:float) -> pF(x,1)] */ + +list%(l:listargs) : list -> list{ f%(x) | x in l} +listP(l:listargs) : list -> list{float!(x) | x in l} + +// ----------------------------------------------------------------------- + + +// random extensions + +[randomIn(a:float,b:float) : float + -> printf("call randomIn ~A,~A \n",a,b), + a + float!(random(integer!((b - a) * 1000.0))) / 1000.0 ] + +[randomChoice?(x:Percent) : boolean + -> random(1000) < integer!(x * 1000.0) ] + +// ======= linear regression ============================================== v0.6 + +// code fragment, origine = CGS, version = 1.0, date = Avril 2010 + +// input = lists of Xi and Yi, returns a triplet (slope, constant factor, deviation) +[linearRegression(lx:list,ly:list) : list + -> let sx := 0.0, sy := 0.0, ssx := 0.0, n := length(lx), sxy := 0.0, + a := 0.0, b := 0.0, sv := 0.0, av_x := 0.0, av_y := 0.0, av_xy := 0.0 in + (assert(length(ly) = n), + for i in (1 .. n) + let x := lx[i], y := ly[i] in + (sx :+ x, ssx :+ (x * x), sy :+ y, sxy :+ (x * y)), + av_x := sx / n, av_y := sy / n, av_xy := sxy / n, + a := (av_xy - av_x * av_y) / ((ssx / n) - av_x * av_x), + b := av_y - a * av_x, + for i in (1 .. n) + let x := lx[i], y := ly[i], v := sqr(y - (a * x + b)) in (sv :+ v), + sv :/ (n - 2), // ref: Wikipedia on linear regression + list(a,b,sqrt(sv))) ] + +[testReg() + -> let l := linearRegression(list(1.0,2.0,3.0), list(0.0,0.0,0.0)) in + assert(l[1] == 0.0 & l[2] == 0.0 & l[3] == 0.0), + let l := linearRegression(list(1.0,2.0,3.0), list(0.0,1.0,2.0)) in + assert(l[1] == 1.0 & l[2] == -1.0 & l[3] == 0.0), + let l := linearRegression(list(1.0,2.0,3.0), list(3.0,5.0,7.0)) in + assert(l[1] == 2.0 & l[2] == 1.0) ] + +// + + + diff --git a/src/simul.cl b/src/simul.cl new file mode 100644 index 0000000..b939c6b --- /dev/null +++ b/src/simul.cl @@ -0,0 +1,349 @@ +// ******************************************************************** +// * MMS : Micro Market Model * +// * copyright (C) 2012 Yves Caseau * +// * file: simul.cl * +// ******************************************************************** + + +// this file contains a naive simulator (yearly runs) +// we simulate each year of market evolution, using +// (a) a churn model +// (b) a sales model +// (c) a cost model +// This is not a differential model (contrary to CGS) + +CSHOW:Company :: unknown // debug : see a company +ISHOW:integer :: 0 // debug : see a month +ISTOP:integer :: 0 // debug : stop at a given month + +// ******************************************************************** +// * Part 1: Utility Methods (psi & all) * +// * Part 2: Simulation Loop * +// * Part 3: Satisfaction * +// * Part 4: Problem-specific display methods * +// ******************************************************************** + +// ******************************************************************** +// * Part 1: Utility Methods (psi & all) * +// ******************************************************************** + + +// === THIS IS THE HEART OF THE CGS MODEL - read document carefully =========== + +// psi is the master function that computes the marketshares (lr) from the prices, +// according to a power law whose exponent is alpha +// TODO : retrouver la r�f�rence � cette loi +[psi(lv:list[float],alpha:float) : list[float] + -> let n := length(lv), lr := make_list(n,float,0.0), s := 0.0 in + (for i in (1 .. n) + let d := (lv[i] ^ -(alpha)) in + (s :+ d, lr[i] := d), + for i in (1 .. n) lr[i] :/ s, + lr)] + + +// ******************************************************************** +// * Part 2: Simulation Loop * +// ******************************************************************** + +// loop runs one simulation loop +[loop(p:Problem) : void + -> for c in Company c.cursat := 0.0, + for i in (1 .. NIT) oneLoop(p,i), + for o in Company o.cursat := satisfaction(o)] // v0.5 : one overall satistaction computation + +[oneLoop(p:Problem, i:integer) : void + -> //[SHOW] ======= start year ~A ==================== // i, + pb.year := i, + getChurn(p), // (1) churn & renewal model + getMarket(p), // (2) market KISS model + for o in Company getEbitda(o), // (3) financial number crunching + if (i = ISTOP) error("stop at ISTOP"), + if (verbose() >= SHOW) for o in Company see(o) ] + + +// returns the results from a simulation +[runLoop(c:Company) : float + -> reinit(), + loop(pb), + c.cursat ] + +[rego() + -> reinit(), + loop(pb), + displayEnd(pb)] + +[rego(e:Experiment) + -> rego(), + for c in Company + add(e.result.tResult,c,current(c).ebitda,current(c).share,c.cursat) ] + +// (1) ------------------------------------------------------------------------ + +// returns the perceived price +[pPrice(o:Company) : Price + -> (o.status[pb.year].price - o.premium * 12.0) ] + +// computes the churn for each operator +// put a bound to avoid meaningless states +[getChurn(p:Problem) + -> let i := p.year, minP := BIGF in + (for o in Company + (o.status[i].price := o.start.price * o.tactic.pricing[i], + minP :min pPrice(o)), + for o in Company + (//[SHOW] ~S: price = ~A vs ~A(min) -> ch ratio = ~A // o,pPrice(o),minP,((pPrice(o) / minP) ^ pb.beta), + o.status[i].churn% := churnRatio(o.churn%,pPrice(o),minP), + if (o.status[i].price > previous(o).price) + (//[DEBUG] price ~A -> ~A : fact =~A // o.status[i].price,previous(o).price,churnRatio(0.1,o.status[i].price,previous(o).price), + o.status[i].churn% := churnRatio(o.status[i].churn%,o.status[i].price,previous(o).price)))) ] + +// computes the churn ratio as a function of oldPrice -> newPrice and a default ratio d% +[churnRatio(d%:float,p1:Price,p2:Price) : Percent + -> min(0.8, d% * (p1 / p2) ^ pb.beta) ] + +// (2) ------------------------------------------------------------------------- + +LOOKY:integer :: 0 +LOOKO:any :: unknown + +// computes the total "aquisition brute" market (sigma of churns) +// distribute using PSI +// get the new base (EoY) +[getMarket(p:Problem) : void + -> let ob := 0.0, nb := 0.0 in + (//[SHOW] === compute new market //, + pb.acqNum := 0.0, // total "acquisition brutes" + for o in Company + let i := pb.year, cs := o.status[i], x := 0.0 in + (cs.base := previous(o).base, + ob :+ cs.base, + x := cs.base * cs.churn%, + pb.acqNum :+ x, + cs.base :- x), + let lr := psi(list{pPrice(o) | o in Company}, pb.alpha) in // computes marketshare + (for o in Company + let cs := o.status[pb.year] in + (cs.share := lr[o.index], + if (LOOKY = pb.year & o = LOOKO) + printf("~S market share = ~A from (~A -> ~A)\n",o,cs.share, + list{pPrice(o) | o in Company},lr), + cs.acqNum := cs.share * pb.acqNum, + //[DEBUG] ~S: ~A -> ~A (~A% c) -> ~A(~A% ms) // o,previous(o).base,cs.base,cs.churn%,cs.base + cs.acqNum,lr[o.index], + cs.base :+ cs.acqNum, + nb :+ cs.base)), + trace(SHOW,"[~A] ~A -> ~A aquisitions -> ~A (total base)\n",pb.year,ob,pb.acqNum,nb)) ] + +//(3) ----------------------------------------------------------------------- + +// financial numbers crunching from volumes - computes the Ebitda +// - compute arpu from price +// - get sales +// - get variable costs +// - deduce ebitda +[getEbitda(o:Company) : void + -> let cs := current(o), + avgOldBase := (previous(o).base + (cs.base - cs.acqNum)) / 2.0, // avg # of old customers + avgBase := avgOldBase + (cs.acqNum / 2.0), // avg # of new customers + oldPrice := previous(o).arpu, + k := churnRatio(o.fluidity,oldPrice,cs.price), + migPrice := min(oldPrice,(oldPrice * (1.0 - k) + cs.price * k)) in // price for old customers + (cs.sales := (avgOldBase * migPrice) + (cs.acqNum / 2.0 * cs.price), + //[1] ~S: sales = ~A (~A * ~A + ~A * ~A) [mig = ~A] // o,cs.sales,avgOldBase,migPrice,cs.acqNum / 2.0,cs.price,k, + cs.arpu := cs.sales / (avgOldBase + cs.acqNum / 2.0), + cs.expense := o.expenses[pb.year + 1] + avgBase * expected(o.variable,o.expenseTrend,pb.year), + cs.ebitda := cs.sales - cs.expense, + trace(DEBUG,"--- ~S: sales = ~A ebitda = ~A\n",o,cs.sales,cs.ebitda)) ] + + + +// ******************************************************************** +// * Part 3: Satisfaction * +// ******************************************************************** + +// v0.2 : introduce a multiplicative version borrowed from RTMS +// note: we have not found a proper way to take hard constraints into account +SATMULT:boolean :: false + +// standardized strategy satisfaction +// use a discounted formula (1� today is better than 1euro in 3 years) +DF:float :: 0.7 +[satisfaction(o:Company) : Percent + -> let d := 0.0,f := 1.0, s := 0.0 in + (for i in (1 .. NIT) + (d :+ satisfaction(o,i) * f, s :+ f, f :* DF), + d / s) ] + +// satisfaction for a given year +[satisfaction(o:Company,y:integer) : Percent + -> satisfaction(o,y,(o.status[y]).ebitda,(o.status[y]).share,(o.status[y]).sales,(o.status[y]).base) ] + +// formula for satisfaction - this is the heart of the model ! +// the satisfaction formula tells how far the performance (average earning, marketshare, sales, base) compare +// with the expected values that are stored in the strategy +[satisfaction(o:Company,y:integer,avgE:Price,avgM:Percent,avgS:Price,avgB:float) : Percent + -> if SATMULT satisfaction2(o,y,avgE,avgM,avgS,avgB) + else satisfaction1(o,y,avgE,avgM,avgS,avgB)] + +// v0.1 version : additive (sum of deltas) +// formula for delta = pos(1 - value/target) = 0 if value > target, what is missing (as a faction) if value < target +// there is a special case for Ebit, since more money is always better (look for pos5 in model.cl) +PENALTY :: 5.0 // going under the minRatio is strongly penalized +[satisfaction1(o:Company,y:integer,avgE:Price,avgM:Percent,avgS:Price,avgB:float) : Percent + -> let minE := avgS * o.strategy.minRatio, + eP := (if (avgE < minE) PENALTY * sqr(avgE - minE) / sqr(expectedEbitda(o,y)) else 0.0), + eE := pos4(1.0 - (avgE / expectedEbitda(o,y))), + eB := pos(1.0 - (avgB / expectedBase(o,y))), + eM := pos(1.0 - (avgM / expectedShare(o,y))) in + (1.0 - eP - eE - eB - eM) ] + +// v0.2 version : multiplicative = product(1 - e_i) +// note: cannot become negative +[satisfaction2(o:Company,y:integer,avgE:Price,avgM:Percent,avgS:Price,avgB:float) : Percent + -> let minE := avgS * o.strategy.minRatio, + eP := (if (avgE < minE) abs(avgE - minE) / (2.0 * (abs(avgE) + expectedEbitda(o,y))) else 0.0), + eE := pos6(1.0 - (avgE / expectedEbitda(o,y))), + eB := pos(1.0 - (avgB / expectedBase(o,y))), + eM := pos(1.0 - (avgM / expectedShare(o,y))) in + (if (eP > 0.0) (0.5 - eP) else 1.0) * (1 - (eE / 10.0)) * (1 - eB) * (1 - eM) ] + + +// talkative version +[explain(o:Company) : void + -> for i in (1 .. NIT) printf("[~A] ~I",i,explain(o,i)) ] + +[explain(o:Company,y:integer) : void + -> explain(o,y,(o.status[y]).ebitda,(o.status[y]).share,(o.status[y]).sales,(o.status[y]).base) ] + +[explain(o:Company,y:integer,avgE:Price,avgM:Percent,avgS:Price,avgB:float) : void + -> let minE := avgS * o.strategy.minRatio, + eP := (if SATMULT (if (avgE < minE) abs(avgE - minE) / (2.0 * (abs(avgE) + expectedEbitda(o,y))) else 0.0) + else (if (avgE < minE) PENALTY * sqr(avgE - minE) / sqr(expectedEbitda(o,y)) else 0.0)), + eE := pos5(1.0 - (avgE / expectedEbitda(o,y))), + eB := pos(1.0 - (avgB / expectedBase(o,y))), + eM := pos(1.0 - (avgM / expectedShare(o,y))) in + (//[5] debug ~S = ~S x ~S // minE, avgS, o.strategy.minRatio, + printf("~S:~F% [~F1 ~F1 ~F1 ~F1] => [~F%/~F%][~F0/~F0][~F0$/~F0$-~F0$]\n",o, + (if SATMULT (if (eP > 0.0) (0.5 - eP) else 1.0) * (1 - (eE / 10.0)) * (1 - eB) * (1 - eM) + else 1.0 - eE - eM - eP - eB), + eP * 100.0, eE * 100.0, eB * 100.0, eM * 100.0, + avgM,expectedShare(o,y), + avgB,expectedBase(o,y), + avgE,expectedEbitda(o,y),minE,0)) ] + +// straightforward for MMS since goals are absolute +// the only special case is for Free (c.strategy.minRatio = 0.0) since growth is +// expected to be linear +[expectedEbitda(c:Company,y:integer) : Price + -> if (c.strategy.minRatio = 0.0) c.strategy.ebitda * float!(y) / float!(NIT) + else c.strategy.ebitda] +[expectedShare(c:Company,y:integer) : Percent -> c.strategy.share] +[expectedBase(c:Company,y:integer) : Percent + -> if (c.strategy.minRatio = 0.0) c.strategy.base * float!(y) / float!(NIT) + else c.strategy.base] + + + +// NEW: global satisfaction works with respect to a global strategy +// formula for satisfaction +GSF:float :: 0.80 +[globalSat(o:Company) : Percent + -> globalSat(o,(o.status[NIT]).ebitda,(o.status[NIT]).share,(o.status[NIT]).sales,(o.status[NIT]).base) ] + +[globalSat(o:Company,avgE:Price,avgM:Percent,avgS:Price,avgB:float) : Percent + -> let minE := avgS * o.strategy.minRatio, + eP := (if (avgE < minE) PENALTY * sqr(avgE - minE) / sqr(o.reference.ebitda * GSF) else 0.0), + eE := pos5(1.0 - (avgE / (o.reference.ebitda * GSF))), + eB := pos(1.0 - (avgB / (o.reference.base * GSF))), + eM := pos(1.0 - (avgM / (o.reference.share * GSF))) in + (1.0 - eE - eM - eP - eB) ] + + +// new in v0.2 - try multiplicative satisfaction from + +// ******************************************************************** +// * Part 4: Problem-specific display methods * +// ******************************************************************** + +[displayEnd(p:Problem) + -> let t := 0.0 in + (for o in Company + (t :+ current(o).base, + printf("=== ~S (~A% -> ~A%)===\n~I",o,f%(o.cursat),f%(satisfaction(o)), + look(o))), + printf("total base = ~F2\n",t)) ] + + +// list all parameters +[see(pb:Problem) + -> let s := pb.scenario in + (printf("_ price to volume sentitivity alpha = ~I [~A - ~A]\n",princ(pb.alpha,3),s.alphaMin,s.alphaMax), + printf("_ price to churn sentitivity beta = ~I [~A - ~A]\n",princ(pb.beta,3),s.betaMin,s.betaMax)) ] + + +// presents a two lines summary of a company +[see(c:Company) : void + -> let s := (if (pb.year = 0) c.start else current(c)) in + printf("~S: ~F1 -> ~F0$ (~F%) arpu@~F1$ \n",c, + s.base, + s.ebitda, + s.share, + s.arpu) ] + +// more detail comparison of current state vs initial +[look(o:Company) : void + -> printf("ebitda: ~F2$ vs ~F2$,",current(o).ebitda,o.start.ebitda), + printf(" with base: ~F2 vs ~F2,",current(o).base,o.start.base), + printf(" share: ~F% vs ~F%;\n",current(o).share,o.start.share), + printf("expenses: ~F2$ vs ~F2$,",current(o).expense,o.start.expense), + printf("sales: ~F2 vs ~F2,",current(o).sales,o.start.sales), + printf("churn: ~F% vs ~F% (price:~F2$ vs ~F2);\n",current(o).churn%, + o.start.churn%,current(o).price,o.start.price) ] + + +// 3YP print format ============================================================================= +// produce a table for each company +[yp() -> for o in Company yp(o)] + +[yp(o:Company) : void + -> princ("\n"), ypSep(), + printf("|~S\tebitda\tbase\tchurn\tshare\tsales\texp\tprice\tarpu\t|\n",o), + ypSep(), + yp(o.start,pb.startYear), + for i in (1 .. NIT) yp(o.status[i],pb.startYear + i), + ypSep()] + +[ypSep() : void + -> printf("+------+-------+-------+-------+-------+-------+-------+-------+--------+\n")] + +[yp(s:Status,i:integer) + -> printf("| ~A\t~F0\t~F2\t~F%\t~F%\t~F0\t~F0\t~F1\t~F1\t|\n",i, + s.ebitda, s.base, s.churn%, s.share, + s.sales, s.expense, s.price,s.arpu) ] // was pF(s.sales / s.base,1)) ] + + + +// satisfaction report (useful for strategy tuning) +[display() -> for o in Company display(o)] + +[display(o:Company) : void + -> printf("~S satisfaction = ~F%\n",o, satisfaction(o)), ypSep(), + printf("|~S\tsat\tebitda\tbase\tshare\tsales\texp\tprice\tarpu\t|\n",o), + ypSep(), + display(o.start,pb.startYear,1.0), + for i in (1 .. NIT) display(o.status[i],pb.startYear + i,satisfaction(o,i)), + ypSep()] + + +[display(s:Status,i:integer,sat%:float) + -> printf("| ~A\t~F2\t~F0\t~F2\t~F%\t~F0\t~F0\t~F1\t~F1\t|\n",i, + sat%, s.ebitda, s.base, s.share, + s.sales, s.expense, s.price, s.arpu) ] + + + +[allSat() + -> printf("===>> ~I\n", + for c in Company printf("~S:~F% ",c,c.cursat)) ] + + diff --git a/src/test1.cl b/src/test1.cl new file mode 100644 index 0000000..c12ff28 --- /dev/null +++ b/src/test1.cl @@ -0,0 +1,320 @@ +// ******************************************************************** +// * MMS : Micro Market Simulation * +// * copyright (C) 2012 Yves Caseau * +// * file: test1.cl * +// ******************************************************************** + +// This is a crude abstraction of the battle of teh four mobile players in 2012 - 2014 + +(printf("=== TEST1 file ... 2012 data ===\n")) + + +// Step 1: describe the companies ------------------------------------ + +XX:any := unknown + +// Bytel is managing to get a decent market share with a high arpu (generosity premium because network is +// less full) +Bytel :: Company( expenses = list(1500.0,1500.0,1400.0,1300.0), expenseTrend = -0.03, + churn% = 0.20, fluidity = 0.5, premium = 3.0) +(init(Bytel,2011,Status(base = 11.3, sales = 5700.0, ebitda = 1200.0)), + XX := Strategy(ebitda = 1200.0, share = 0.18, base = 11.0), + Bytel.reference := Strategy(ebitda = 1200.0, share = 0.18, base = 11.0)) + + +// Orange is the reference operator (premium = 0) +Orange :: Company(expenses = list(3000.0,3000.0,3000.0,3000.0), expenseTrend = -0.03, + churn% = 0.15, fluidity = 0.3) +(init(Orange,2011,Status(base = 27.0, sales = 10800.0, ebitda = 3920.0)), + Orange.reference := Strategy(ebitda = 3900.0, share = 0.33, base = 27.0)) + + +// SFR - (IT & network operations are more expensive) +SFR :: Company(expenses = list(2000.0,1900.0,1700.0,1600.0), expenseTrend = -0.05, + churn% = 0.20, fluidity = 0.4, premium = -2.0) +(init(SFR,2011,Status( base = 21.0, sales = 8400.0 , ebitda = 3520.0)), + SFR.reference := Strategy(ebitda = 3500.0, share = 0.28, base = 21.0)) + +// in this model we add MVNO +// average of Virgin and Lebara :) +MVNO :: Company(expenses = list(300.0,300.0,300.0,300.0), expenseTrend = -0.05, + churn% = 0.25, fluidity = 0.6, premium = -30.0) +(init(MVNO,2011,Status( base = 7.0, sales = 1000.0, ebitda = 100.0)), + MVNO.reference := Strategy(ebitda = 100.0, share = 0.1, base = 7.0)) + +// Free starts in 2012 (hence the init status is different) +// the premium reflect the absence of distribution network (iso prix SFR => 10%) +// attention : the tuning of premium(Free) - done with E0c is sensitive to alpha +Free :: Company(expenses = list(0.0,300.0,400.0,450.0), + churn% = 0.20, fluidity = 0.6, premium = -10.0, + variable = 150.0) // 15�/mois +(init(Free,2011,Status( base = 0.0, price = 280.0)), // 15�/mo ABPU + 8� incoming calls + Free.reference := Strategy(ebitda = 800.0, share = 0.18, base = 10.0)) + +// These tactics are used for the clean test = no change => no change :) +T0b :: makeTactic(0.9,0.85,0.8) +T0o :: makeTactic(0.9,0.88,0.85) // 0.9 -> 0.2 +T0s :: makeTactic(0.95,0.85,0.8) +T0f :: makeTactic(1.0,1.0,1.0) +T0m :: makeTactic(0.95,0.9,0.85) + +T0 :: makeTactic(1.0,1.0,1.0) + +T1f :: makeTactic(4.0,4.0,4.0) // debug & test vector : very high => check that other premium are balanced +T2f :: makeTactic(1.5,1.5,1.5) // debug & test vector : same as other => adjust premium for MS + +// step 2: describe starting global situation ------------------------------ + +NITER :: 10 +NTEST :: 50 +Sc1 :: Scenario(nIter = NITER, nTest = NTEST, + // cost structures + costs = {}, // default + // MonteCarlo Tuning + alphaMin = 3.5, alphaMax = 3.5, + betaMin = 1.0, betaMax = 1.5) + +// variants to see the effect of alpha +Sc1a+ :: Scenario(nIter = NITER, nTest = NTEST, costs = {}, // default + alphaMin = 2.5, alphaMax = 3.0, + betaMin = 1.0, betaMax = 1.5) + +Sc1a- :: Scenario(nIter = NITER, nTest = NTEST, costs = {}, // default + alphaMin = 2.0, alphaMax = 2.5, + betaMin = 1.0, betaMax = 1.5) + +// variants to see the effect of beta +Sc1b- :: Scenario(nIter = NITER, nTest = NTEST, costs = {}, // default + alphaMin = 2.0, alphaMax = 3.0, + betaMin = 1.0, betaMax = 1.2) + +Sc1b+ :: Scenario(nIter = NITER, nTest = NTEST, costs = {}, // default + alphaMin = 2.0, alphaMax = 3.0, + betaMin = 1.2, betaMax = 1.5) + + +// Scenario 2: Free has to build a network - 2M� sur 5 ans -> +Sc2 :: Scenario(nIter = NITER, nTest = NTEST, + // cost structures + costs = {tuple(Free,list(0.0,350.0,800.0,850.0,900.0))}, + // MonteCarlo Tuning + alphaMin = 2.0, alphaMax = 3.0, + betaMin = 1.0, betaMax = 1.5) + + +// Scenario 3: Bouygues Telecom sets its variable costs similar to ORG & SFR, then works out +// a strong reduction plan +Sc3 :: Scenario(nIter = NITER, nTest = NTEST, + // cost structures + costs = {tuple(Bytel,list(2100.0,1900.0,1800.0,1700.0))}, + // MonteCarlo Tuning + alphaMin = 2.0, alphaMax = 2.0, + betaMin = 1.0, betaMax = 1.5) + + +// Manual tuning - beta -> total market, alpha -> market share +(pb.alpha := 3.5, pb.beta := 1.2) + +// Sc0 is a scenario that sets alpha and beta to a precise value +Sc0 :: Scenario(nIter = NITER, nTest = 1, + // cost structures + costs = {}, // default + // MonteCarlo Tuning + alphaMin = 2.0, alphaMax = 2.0, + betaMin = 1.2, betaMax = 1.2) + +// alpha � 2.0 -> Free a 5.4 (go), 2.5 -> 5, 3.0 -> 6.5 +// beta � 1.0 -> bytel churn � 25%, 1.8 -> 35% + +// step 3: describe strategy ---------------------------------------------- + +// we only use reasonably aggressive strategies +// first : not agressive - allows some loss because of Free +// second : totally soft -> defend base +// third : agressive + +/// each player has its stragegy, ... and variants +// Bytel +SB :: Strategy( ebitda = 900.0, share = 0.17, base = 11.0, minRatio = f%(15)) +SB2 :: Strategy( ebitda = 700.0, share = 0.15, base = 10.0, minRatio = f%(10)) +SB3 :: Strategy( ebitda = 1100.0, share = 0.20, base = 12.0, minRatio = f%(15)) + +// Orange +SO :: Strategy( ebitda = 3500.0, share = 0.30, base = 27.0, minRatio = f%(30)) +SO2 :: Strategy( ebitda = 3000.0, share = 0.20, base = 25.0, minRatio = f%(20)) +SO3 :: Strategy( ebitda = 3700.0, share = 0.27, base = 28.0, minRatio = f%(30)) + +// SFR +SR :: Strategy( ebitda = 3000.0, share = 0.27, base = 21.0, minRatio = f%(25)) +SR2 :: Strategy( ebitda = 2500.0, share = 0.18, base = 19.0, minRatio = f%(20)) +SR3 :: Strategy( ebitda = 3200.0, share = 0.22, base = 22.0, minRatio = f%(30)) + +// MVNOS +SM :: Strategy( ebitda = 100.0, share = f%(10), base = 6.0, minRatio = f%(10)) +SM2 :: Strategy( ebitda = 10.0, share = f%(15), base = 5.0, minRatio = f%(10)) +SM3 :: Strategy( ebitda = 10.0, share = f%(15), base = 6.0, minRatio = f%(10)) + +// perceived Free strategy (S4b is more aggressive) +// minRatio = 0% is a marker for linear growth +SF :: Strategy( ebitda = 500.0, share = f%(20), base = 12.0, minRatio = f%(0)) +SF2 :: Strategy( ebitda = 500.0, share = f%(15), base = 10.0, minRatio = f%(0)) +SF3 :: Strategy( ebitda = 500.0, share = f%(30), base = 15.0, minRatio = f%(0)) + + +// special for illustration : no reaction +// attention : cannot do whatif with the sameT0 ! +E0 :: Experiment(scenario = Sc1, + strategies = {tuple(Bytel,SB),tuple(SFR,SR),tuple(Orange,SO),tuple(Free,SF), tuple(MVNO,SM)}, + sTactics = {tuple(Bytel,T0),tuple(Orange,T0),tuple(SFR,T0),tuple(Free,makeTactic(T0)),tuple(MVNO,T0)}) + +// two variants : no Free & average Free (adjust premiums) +E0b :: Experiment(scenario = Sc1, + strategies = {tuple(Bytel,SB),tuple(SFR,SR),tuple(Orange,SO),tuple(Free,SF), tuple(MVNO,SM)}, + sTactics = {tuple(Bytel,T0),tuple(Orange,T0),tuple(SFR,T0),tuple(Free,T1f),tuple(MVNO,T0)}) + +E0c :: Experiment(scenario = Sc1, + strategies = {tuple(Bytel,SB),tuple(SFR,SR),tuple(Orange,SO),tuple(Free,SF), tuple(MVNO,SM)}, + sTactics = {tuple(Bytel,T0),tuple(Orange,T0),tuple(SFR,T0),tuple(Free,T2f),tuple(MVNO,T0)}) + + +// test - default strategies +// Free is dumb => it does not work +E1 :: Experiment(scenario = Sc1, + strategies = {tuple(Bytel,SB),tuple(SFR,SR),tuple(Orange,SO),tuple(Free,SF), tuple(MVNO,SM)}, + sTactics = {tuple(Bytel,T0b),tuple(Orange,T0o),tuple(SFR,T0s),tuple(Free,T0f),tuple(MVNO,T0m)}) + +// variant with no reactions (calibration) +E1b :: Experiment(scenario = Sc1, + strategies = {tuple(Bytel,SB),tuple(SFR,SR),tuple(Orange,SO),tuple(Free,SF), tuple(MVNO,SM)}, + sTactics = {tuple(Bytel,T0f),tuple(Orange,T0f),tuple(SFR,T0f),tuple(Free,T0f),tuple(MVNO,T0f)}) + +// each players goes with a soft strategy (base) +E2 :: Experiment(scenario = Sc1, + strategies = {tuple(Bytel,SB2),tuple(SFR,SR2),tuple(Orange,SO2),tuple(Free,SF2), tuple(MVNO,SM2)}, + sTactics = {tuple(Bytel,T0b),tuple(Orange,T0o),tuple(SFR,T0s),tuple(Free,T0f),tuple(MVNO,T0m)}) + +// each players goes with a hard strategy +E3 :: Experiment(scenario = Sc1, + strategies = {tuple(Bytel,SB3),tuple(SFR,SR3),tuple(Orange,SO3),tuple(Free,SF3), tuple(MVNO,SM3)}, + sTactics = {tuple(Bytel,T0b),tuple(Orange,T0o),tuple(SFR,T0s),tuple(Free,T0f),tuple(MVNO,T0m)}) + + +// base test - run for 3 years - level0 (show the simple model) -------------------------------------- +// meant to run at show level +[go0(e:Experiment) : void + -> NIT := 4, NOPT := 3, + verbose() := 1, TALK := 1, SHOW := 1, DEBUG := 5, + init(e), + trace(0,"=== Init market shares : ~A\n", + psi(list{(o.start.arpu - 12.0 * o.premium) | o in (Company but Free)},pb.alpha)), + loop(pb), + display()] + +[go0() -> go0(E1) ] + +// TO RUN +// go0(E0b) & go0(E0c) + +// level 1 experiment : local opt for tatic adjustment ----------------------------------------------- +[go(e:Experiment) + -> NIT := 4, NOPT := 3, + verbose() := 0, TALK := 1, SHOW := 2, DEBUG := 5, OPTI := 1, + init(e), + loop(pb), + display() ] + +[go() -> go(E1) ] + +[go(e:Experiment,s:Scenario) + -> e.scenario := s, + go(e)] + +[go(c:Company) -> verbose() := 0, optimize(c),runLoop(c),display(c),explain(c)] + +[go(c:Company,s:Strategy) -> c.strategy := s, go(c)] + +// quick test to see if twoOpt help +[go2(c:Company) + -> let v := c.cursat in + (twoOptimize(c), + if (c.cursat > v) trace(0,"Two opt improvement for ~S: ~A->~A\n",c,v,c.cursat)) ] + +// cute whatif to fine tune satisfaction ! +// if go(C) gives something strange, go(c,label,value) does a whatif + explain +[go(c:Company,i:TAG,v:float) -> whatif(c,i,v,true) ] + +// level 2 experiment : simple nash equilibrium search ------------------------------------------------ +PNASH:boolean :: false +NASH2:boolean :: false + +[nash(e:Experiment) : void + -> MOPT := 0, + for i in (1 .. e.scenario.nIter) + (if PNASH poptimize(e,i,e.scenario.nIter) + else soptimize(e,i,e.scenario.nIter), + if NASH2 for c in Company go2(c)), + trace(0,"================== RESULT of NASH Loop (~A x ~S) ====================== \n",e.scenario.nIter,SATMULT), + trace(0,"===> category = ~A \n",LCAT[categorize(e)]), + for c in Company + (add(c.global,globalSat(c)), + for i in (1 .. NIT) add(c.ebitdas[i],c.status[i].ebitda)), + // display all satisfactions for companies + display() ] + +[nash() -> nash(E0)] + +[go2(e:Experiment,s:Scenario) + -> e.scenario := s, + go(e), + nash(e)] + +[go2() -> go2(E1,Sc1)] + + + +// this is a special tuning version of go2 : additional parameters and additional output +// in : number of loops, satisfaction formula, NUM2 (number of 1opt loop) +// out: convergence factor, final EBITDA, traces +[go1(e:Experiment,s:Scenario,n:integer,sm:boolean) + -> e.scenario := s, + s.nIter := n, + SATMULT := sm, + go(e), + TRACEON := true, // logs the ebitda and the convergence + nash(e), + trace(0,"log of convergence : ~A \n",TRACED), + trace(0,"log of results : ~A \n",TRACEG) ] + +[go1(n:integer,sm:boolean) -> go1(E1,Sc1,n,sm) ] + + +// level3 experiment : GTES (with randomization) ------------------------------------------------------- + +[go3(e:Experiment,s:Scenario) + -> e.scenario := s, + NIT := 4, NOPT := 3, + verbose() := 0, + run(e)] + +[go3() -> go3(E1,Sc1)] + +// level4 experiment: "fixed point" = 10 years ---------------------------------------------------------- +[go4(e:Experiment) : void + -> NIT := 10, NOPT := 9, + verbose() := 0, TALK := 1, SHOW := 2, DEBUG := 5, OPTI := 1, + init(e), + loop(pb), + display() ] + +[go4() -> go4(E0)] + + +// interesting function - equilibrium price +[p(c:Company,b:float) : Price + -> c.variable + c.expenses[1] / (12.0 * b) ] + + +// check the satisfaction equation +[ftest(x:float,v:float) : float + -> 1.0 - pos5(1.0 - x / v) ] + diff --git a/src/test2.cl b/src/test2.cl new file mode 100644 index 0000000..fea9093 --- /dev/null +++ b/src/test2.cl @@ -0,0 +1,97 @@ +// ******************************************************************** +// * MMS : Micro Market Simulation * +// * copyright (C) 2012 Yves Caseau * +// * file: test2.cl * +// ******************************************************************** + +// This is a crude abstraction of the battle of teh four mobile players in 2012 - 2014 + +(printf("=== TEST2 file ... 2012 data ===\n")) + + +// Step 1: describe the companies ------------------------------------ + + +// Bytel is managing to get a decent market share with a high arpu (generosity premium because network is +// less full) +Bytel :: Company( expenses = list(1500.0,1500.0,1500.0,1500.0), expenseTrend = -0.00, + churn% = 0.20, fluidity = 0.5, premium = 3.0) +(init(Bytel,2011,Status(base = 11.3, sales = 5700.0, ebitda = 1200.0)), + Bytel.reference := Strategy(ebitda = 1200.0, share = 0.18, base = 11.0)) + + +// Orange is the reference operator (premium = 0) +Orange :: Company(expenses = list(3000.0,3000.0,3000.0,3000.0), expenseTrend = -0.00, + churn% = 0.15, fluidity = 0.3) +(init(Orange,2011,Status(base = 27.0, sales = 10800.0, ebitda = 3920.0)), + Orange.reference := Strategy(ebitda = 3900.0, share = 0.33, base = 27.0)) + + +// SFR - (IT & network operations are more expensive) +SFR :: Company(expenses = list(2000.0,1900.0,1900.0,1900.0), expenseTrend = -0.00, + churn% = 0.20, fluidity = 0.4, premium = -2.0) +(init(SFR,2011,Status( base = 21.0, sales = 8400.0 , ebitda = 3520.0)), + SFR.reference := Strategy(ebitda = 3500.0, share = 0.28, base = 21.0)) + +// in this model we add MVNO +// average of Virgin and Lebara :) +MVNO :: Company(expenses = list(300.0,300.0,300.0,300.0), expenseTrend = -0.00, + churn% = 0.25, fluidity = 0.6, premium = -30.0) +(init(MVNO,2011,Status( base = 7.0, sales = 1000.0, ebitda = 100.0)), + MVNO.reference := Strategy(ebitda = 100.0, share = 0.1, base = 7.0)) + + +// These tactics are used for the clean test = no change => no change :) +T0b :: makeTactic(1.0,1.0,1.0) +T0o :: makeTactic(1.0,1.0,1.0) // 0.9 -> 0.2 +T0s :: makeTactic(1.0,1.0,1.0) +T0m :: makeTactic(0.95,0.9,0.85) + +T0 :: makeTactic(1.0,1.0,1.0) + +// step 2: describe starting global situation ------------------------------ + +NITER :: 6 +NTEST :: 50 +Sc1 :: Scenario(nIter = NITER, nTest = NTEST, + // cost structures + costs = {}, // default + // MonteCarlo Tuning + alphaMin = 2.0, alphaMax = 3.0, + betaMin = 1.0, betaMax = 1.5) + + +// step 3: describe strategy ---------------------------------------------- + +// we only use reasonably aggressive strategies + +/// each player has its stragegy, ... and variants +// Bytel +SB :: Strategy( ebitda = 700.0, share = 0.15, base = 9.0, minRatio = f%(15)) +SB2 :: Strategy( ebitda = 500.0, share = 0.13, base = 8.0, minRatio = f%(10)) +SB3 :: Strategy( ebitda = 900.0, share = 0.17, base = 9.0, minRatio = f%(15)) + +// Orange +SO :: Strategy( ebitda = 3000.0, share = 0.25, base = 25.0, minRatio = f%(30)) +SO2 :: Strategy( ebitda = 2200.0, share = 0.20, base = 23.0, minRatio = f%(20)) +SO3 :: Strategy( ebitda = 3500.0, share = 0.27, base = 25.0, minRatio = f%(30)) + +// SFR +SR :: Strategy( ebitda = 2500.0, share = 0.20, base = 18.0, minRatio = f%(25)) +SR2 :: Strategy( ebitda = 2000.0, share = 0.18, base = 16.0, minRatio = f%(20)) +SR3 :: Strategy( ebitda = 3000.0, share = 0.22, base = 18.0, minRatio = f%(30)) + +// MVNOS +SM :: Strategy( ebitda = 100.0, share = f%(10), base = 6.0, minRatio = f%(10)) +SM2 :: Strategy( ebitda = 10.0, share = f%(15), base = 5.0, minRatio = f%(10)) +SM3 :: Strategy( ebitda = 10.0, share = f%(15), base = 6.0, minRatio = f%(10)) + + + +// each players goes with a hard strategy +F1 :: Experiment(scenario = Sc1, + strategies = {tuple(Bytel,SB),tuple(SFR,SR),tuple(Orange,SO), tuple(MVNO,SM)}, + sTactics = {tuple(Bytel,T0b),tuple(Orange,T0o),tuple(SFR,T0s), tuple(MVNO,T0m)}) + + +