├── .gitignore ├── client ├── .gitignore ├── project │ ├── build.properties │ ├── .gnupg │ │ ├── pubring.gpg │ │ └── secring.gpg │ ├── plugins.sbt │ └── pgp.sbt ├── src │ ├── test │ │ ├── resources │ │ │ ├── forecast-client │ │ │ │ ├── insufficient-data │ │ │ │ │ ├── raw-data │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ └── test.json │ │ │ │ ├── sudden-drop │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ │ ├── weekly-trend │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ │ ├── daily-seasonal │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ │ ├── weekly-random │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ │ ├── weekly-seasonal │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ │ ├── yearly-seasonal │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ └── plot-raw-and-actual.png │ │ │ │ ├── two-weeks-seasonal │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ │ ├── weekly-yearly-seasonal │ │ │ │ │ └── plot-raw.png │ │ │ │ ├── daily-seasonal-with-trend │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ │ ├── weekly-seasonal-with-trend │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ │ ├── yearly-seasonal-with-trend │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ └── plot-raw-and-actual.png │ │ │ │ ├── weekly-yearly-seasonal-with-trend │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ └── plot-raw-and-actual.png │ │ │ │ └── real-data-video-view-supply-with-trend │ │ │ │ │ ├── plot-raw.png │ │ │ │ │ ├── plot-raw-and-actual.png │ │ │ │ │ ├── raw-data │ │ │ │ │ └── test.json │ │ │ └── scripts │ │ │ │ └── plot_results.py │ │ └── scala │ │ │ └── com │ │ │ └── aol │ │ │ └── one │ │ │ └── reporting │ │ │ └── forecastapi │ │ │ └── client │ │ │ ├── ConfigUtilTest.scala │ │ │ ├── ForecastPerformanceTest.scala │ │ │ ├── ForecastQualityTest.scala │ │ │ ├── ITTestUtil.scala │ │ │ └── ForecastClientTest.scala │ └── main │ │ ├── resources │ │ ├── prod.conf │ │ ├── stage.conf │ │ ├── autotests.conf │ │ └── dev.conf │ │ └── scala │ │ └── com │ │ └── aol │ │ └── one │ │ └── reporting │ │ └── forecastapi │ │ └── client │ │ ├── ForecastResponse.scala │ │ ├── ConfigUtil.scala │ │ ├── ForecastRequest.scala │ │ ├── ForecastParams.scala │ │ ├── ForecastHttpClient.scala │ │ └── ForecastClient.scala ├── README.md └── build.sbt ├── server ├── .gitignore ├── docker │ ├── jrobin-1.5.9.jar │ ├── javamelody-core-1.67.0.jar │ ├── Dockerfile │ └── tomcat-users.xml └── src │ └── main │ ├── webapp │ ├── META-INF │ │ └── context.xml │ ├── WEB-INF │ │ ├── candidates_Regress.txt │ │ ├── candidates_Expsm.txt │ │ ├── candidates_Default.txt │ │ └── candidates_Arima.txt │ └── doc │ │ ├── images │ │ ├── throbber.gif │ │ ├── logo_small.png │ │ ├── wordnik_api.png │ │ ├── explorer_icons.png │ │ └── pet_store_api.png │ │ ├── lib │ │ ├── jquery.slideto.min.js │ │ ├── jquery.wiggle.min.js │ │ └── jquery.ba-bbq.min.js │ │ ├── o2c.html │ │ ├── css │ │ └── reset.css │ │ └── index.html │ ├── resources │ ├── forecast-api.properties │ ├── log4j.properties │ ├── logback.xml │ └── schema │ │ └── creative.json │ └── java │ └── com │ └── aol │ └── one │ └── reporting │ └── forecastapi │ └── server │ ├── models │ ├── model │ │ ├── IFSMetricType.java │ │ ├── IFSParameterValue.java │ │ ├── IFSUsageDescription.java │ │ ├── IFSParameterSpec.java │ │ ├── IFSComputation.java │ │ ├── IFSCycle.java │ │ ├── IFSSpikeFilter.java │ │ ├── IFSModelFactory.java │ │ └── IFSNDaysBack.java │ ├── util │ │ ├── TimerCheckpoint.java │ │ ├── Optional.java │ │ ├── Timer.java │ │ ├── GetTimeSeriesFiles.java │ │ ├── Pair.java │ │ └── GetTimeSeries.java │ ├── cs │ │ ├── GetCannedSetCandidates.java │ │ ├── IFSCannedSetSelection.java │ │ ├── IFSCannedSet.java │ │ └── GetCannedSetDefinitions.java │ └── alg │ │ ├── IFSModelImplRW.java │ │ └── IFSModelImplMovAvg.java │ ├── resource │ ├── HealthResource.java │ ├── WelcomeResource.java │ ├── CannedSetResource.java │ ├── SimpleForecastResource.java │ ├── CollectionListResource.java │ ├── EasyForecastResource.java │ └── ImpressionForecastResource.java │ ├── app │ ├── DefaultDocServlet.java │ ├── JerseyServletContainer.java │ ├── IfsCache.java │ └── IfsConfig.java │ ├── jpe │ └── gw │ │ ├── GWException.java │ │ └── GWInterface.java │ ├── util │ ├── WebPath.java │ ├── ForecastUtil.java │ └── RequestValidation.java │ ├── healthcheck │ └── HealthCheckContextListener.java │ ├── model │ └── response │ │ ├── CollectionResponse.java │ │ ├── CannedSetResponse.java │ │ └── ForecastResponse.java │ └── metrics │ └── MetricsContextListener.java ├── script └── deploy.sh ├── .travis.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | **/.DS_Store 3 | target 4 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | .idea 3 | 4 | -------------------------------------------------------------------------------- /client/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.16 2 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/insufficient-data/raw-data: -------------------------------------------------------------------------------- 1 | 3400.0, 2 | 2100.0, 3 | -------------------------------------------------------------------------------- /client/src/main/resources/prod.conf: -------------------------------------------------------------------------------- 1 | http { 2 | max-retry = 3 3 | read-timeout = 15 4 | conn-timeout = 15 5 | } 6 | -------------------------------------------------------------------------------- /client/src/main/resources/stage.conf: -------------------------------------------------------------------------------- 1 | http { 2 | max-retry = 3 3 | read-timeout = 15 4 | conn-timeout = 15 5 | } 6 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | forecast-api.iml 2 | .idea 3 | deployment/deployment-prod.yaml 4 | deployment/deployment-stage.yaml 5 | -------------------------------------------------------------------------------- /server/docker/jrobin-1.5.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/server/docker/jrobin-1.5.9.jar -------------------------------------------------------------------------------- /client/project/.gnupg/pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/project/.gnupg/pubring.gpg -------------------------------------------------------------------------------- /client/project/.gnupg/secring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/project/.gnupg/secring.gpg -------------------------------------------------------------------------------- /client/src/main/resources/autotests.conf: -------------------------------------------------------------------------------- 1 | http { 2 | max-retry = 3 3 | read-timeout = 15 4 | conn-timeout = 15 5 | } 6 | -------------------------------------------------------------------------------- /server/src/main/webapp/META-INF/context.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /server/docker/javamelody-core-1.67.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/server/docker/javamelody-core-1.67.0.jar -------------------------------------------------------------------------------- /client/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") 2 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") 3 | -------------------------------------------------------------------------------- /client/src/main/resources/dev.conf: -------------------------------------------------------------------------------- 1 | http { 2 | max-retry = 3 3 | read-timeout = 15 4 | conn-timeout = 15 5 | } 6 | 7 | plot-actual = false 8 | -------------------------------------------------------------------------------- /server/src/main/webapp/WEB-INF/candidates_Regress.txt: -------------------------------------------------------------------------------- 1 | REG-NONE-PHASE2-WEEK-YEAR 2 | REG-NONE-CONST2-AUTO 3 | AR-NONE-CENDE-NONE 4 | REG-NONE-NONE 5 | -------------------------------------------------------------------------------- /server/src/main/webapp/doc/images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/server/src/main/webapp/doc/images/throbber.gif -------------------------------------------------------------------------------- /server/src/main/webapp/doc/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/server/src/main/webapp/doc/images/logo_small.png -------------------------------------------------------------------------------- /server/src/main/webapp/doc/images/wordnik_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/server/src/main/webapp/doc/images/wordnik_api.png -------------------------------------------------------------------------------- /server/src/main/webapp/WEB-INF/candidates_Expsm.txt: -------------------------------------------------------------------------------- 1 | EXP-NONE-ADD-YEAR 2 | EXP-NONE-MULT-YEAR 3 | EXP-NONE-ADD-AUTO 4 | EXP-NONE-MULT-AUTO 5 | EXP-NONE-NONE 6 | -------------------------------------------------------------------------------- /server/src/main/webapp/doc/images/explorer_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/server/src/main/webapp/doc/images/explorer_icons.png -------------------------------------------------------------------------------- /server/src/main/webapp/doc/images/pet_store_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/server/src/main/webapp/doc/images/pet_store_api.png -------------------------------------------------------------------------------- /client/project/pgp.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0") 2 | 3 | credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credentials") 4 | 5 | -------------------------------------------------------------------------------- /server/src/main/webapp/WEB-INF/candidates_Default.txt: -------------------------------------------------------------------------------- 1 | EXP-NONE-ADD-YEAR 2 | REG-NONE-ADD-AUTO 3 | EXP-NONE-MULT-AUTO 4 | REG-NONE-ADD-WEEK 5 | EXP-NONE-MULT-WEEK 6 | REG-NONE-NONE 7 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/sudden-drop/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/sudden-drop/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-trend/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-trend/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/daily-seasonal/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/daily-seasonal/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-random/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-random/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/insufficient-data/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/insufficient-data/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-seasonal/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-seasonal/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/yearly-seasonal/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/yearly-seasonal/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/two-weeks-seasonal/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/two-weeks-seasonal/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/sudden-drop/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/sudden-drop/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-yearly-seasonal/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-yearly-seasonal/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-trend/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-trend/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/daily-seasonal-with-trend/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/daily-seasonal-with-trend/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/daily-seasonal/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/daily-seasonal/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-seasonal-with-trend/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-seasonal-with-trend/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-seasonal/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-seasonal/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/yearly-seasonal-with-trend/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/yearly-seasonal-with-trend/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/yearly-seasonal/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/yearly-seasonal/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/insufficient-data/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/insufficient-data/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/two-weeks-seasonal/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/two-weeks-seasonal/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-yearly-seasonal-with-trend/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-yearly-seasonal-with-trend/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/daily-seasonal-with-trend/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/daily-seasonal-with-trend/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-seasonal-with-trend/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-seasonal-with-trend/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/yearly-seasonal-with-trend/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/yearly-seasonal-with-trend/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/real-data-video-view-supply-with-trend/plot-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/real-data-video-view-supply-with-trend/plot-raw.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-yearly-seasonal-with-trend/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/weekly-yearly-seasonal-with-trend/plot-raw-and-actual.png -------------------------------------------------------------------------------- /server/src/main/webapp/WEB-INF/candidates_Arima.txt: -------------------------------------------------------------------------------- 1 | ARIMA-2,1,0-1,0,0s-YEAR 2 | ARIMA-0,1,2-0,1,1s-YEAR 3 | ARIMA-2,1,2-0,1,1s-AUTO 4 | ARIMA-1,1,1-1,1,1s-AUTO 5 | ARIMA-0,1,1-0,1,1s-AUTO 6 | ARIMA-2,1,2-NONE 7 | ARIMA-1,1,1-NONE 8 | ARIMA-0,1,1-NONE 9 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/real-data-video-view-supply-with-trend/plot-raw-and-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/aol-on-forecast/master/client/src/test/resources/forecast-client/real-data-video-view-supply-with-trend/plot-raw-and-actual.png -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/sudden-drop/raw-data: -------------------------------------------------------------------------------- 1 | 400.0, 2 | 500.0, 3 | 500.0, 4 | 400.0, 5 | 400.0, 6 | 300.0, 7 | 350.0, 8 | 400.0, 9 | 300.0, 10 | 300.0, 11 | 300.0, 12 | 300.0, 13 | 300.0, 14 | 300.0, 15 | 0.0, 16 | 0.0, 17 | 0.0, 18 | 0.0, 19 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-random/raw-data: -------------------------------------------------------------------------------- 1 | 63.0, 2 | 92.0, 3 | 57.0, 4 | 79.0, 5 | 98.0, 6 | 10.0, 7 | 55.0, 8 | 28.0, 9 | 34.0, 10 | 77.0, 11 | 11.0, 12 | 87.0, 13 | 62.0, 14 | 48.0, 15 | 38.0, 16 | 52.0, 17 | 88.0, 18 | 34.0, 19 | 68.0, 20 | 33.0, 21 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-trend/raw-data: -------------------------------------------------------------------------------- 1 | 1001.0, 2 | 1002.0, 3 | 1003.0, 4 | 1004.0, 5 | 1005.0, 6 | 1006.0, 7 | 1007.0, 8 | 1008.0, 9 | 1009.0, 10 | 1010.0, 11 | 1011.0, 12 | 1012.0, 13 | 1013.0, 14 | 1014.0, 15 | 1015.0, 16 | 1016.0, 17 | 1017.0, 18 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/insufficient-data/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 3400.0, 4 | 2100.0 5 | ], 6 | "horizon": 2, 7 | "expectedConfidence": -1.0, 8 | "allowForecastPercentError": 1.0, 9 | "expectedForecast": [ 10 | 2750.0, 11 | 2750.0 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /server/src/main/resources/forecast-api.properties: -------------------------------------------------------------------------------- 1 | swagger.api.basepath=http://localhost:9072/forecast-api 2 | 3 | 4 | # root logger log level, this property is used in logback.xml which is part of 5 | # the war file for the API 6 | logback.root.logger.level=debug 7 | 8 | ifs.file.change.check.interval=60000 9 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-seasonal/raw-data: -------------------------------------------------------------------------------- 1 | 1.0, 2 | 2.0, 3 | 3.0, 4 | 4.0, 5 | 3.0, 6 | 2.0, 7 | 1.0, 8 | 1.0, 9 | 2.0, 10 | 3.0, 11 | 4.0, 12 | 3.0, 13 | 2.0, 14 | 1.0, 15 | 1.0, 16 | 2.0, 17 | 3.0, 18 | 4.0, 19 | 3.0, 20 | 2.0, 21 | 1.0, 22 | 1.0, 23 | 2.0, 24 | 3.0, 25 | 4.0, 26 | 3.0, 27 | 2.0, 28 | 1.0, 29 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/two-weeks-seasonal/raw-data: -------------------------------------------------------------------------------- 1 | 1.0, 2 | 2.0, 3 | 3.0, 4 | 4.0, 5 | 5.0, 6 | 6.0, 7 | 7.0, 8 | 7.0, 9 | 6.0, 10 | 5.0, 11 | 4.0, 12 | 3.0, 13 | 2.0, 14 | 1.0, 15 | 1.0, 16 | 2.0, 17 | 3.0, 18 | 4.0, 19 | 5.0, 20 | 6.0, 21 | 7.0, 22 | 7.0, 23 | 6.0, 24 | 5.0, 25 | 4.0, 26 | 3.0, 27 | 2.0, 28 | 1.0, 29 | -------------------------------------------------------------------------------- /script/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ "$TRAVIS_BRANCH" == "master" ]; then 6 | echo "pushing client artifact to Maven Central" 7 | cd client 8 | sbt compile publishSigned sonatypeRelease 9 | 10 | echo "push server image to Dockerhub" 11 | cd ../server 12 | docker login -u=$DOCKERHUB_UNAME -p=$DOCKERHUB_PASS 13 | docker push vidible/forecast-api:2.0.4 14 | fi 15 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-seasonal-with-trend/raw-data: -------------------------------------------------------------------------------- 1 | 101.0, 2 | 102.0, 3 | 103.0, 4 | 104.0, 5 | 105.0, 6 | 104.0, 7 | 103.0, 8 | 104.0, 9 | 105.0, 10 | 106.0, 11 | 107.0, 12 | 108.0, 13 | 107.0, 14 | 106.0, 15 | 107.0, 16 | 108.0, 17 | 109.0, 18 | 110.0, 19 | 111.0, 20 | 110.0, 21 | 109.0, 22 | 110.0, 23 | 111.0, 24 | 112.0, 25 | 113.0, 26 | 114.0, 27 | 113.0, 28 | 112.0, 29 | -------------------------------------------------------------------------------- /server/src/main/webapp/doc/lib/jquery.slideto.min.js: -------------------------------------------------------------------------------- 1 | (function(b){b.fn.slideto=function(a){a=b.extend({slide_duration:"slow",highlight_duration:3E3,highlight:true,highlight_color:"#FFFF99"},a);return this.each(function(){obj=b(this);b("body").animate({scrollTop:obj.offset().top},a.slide_duration,function(){a.highlight&&b.ui.version&&obj.effect("highlight",{color:a.highlight_color},a.highlight_duration)})})}})(jQuery); 2 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | ### Forecast client 2 | 3 | Client library for forecast service. 4 | 5 | ### Usage 6 | 7 | ```scala 8 | // forecast second week given first week data 9 | // client chooses service URL based on FORECAST_API_SERVICE_URL env variable. Eg. set it to http://localhost:9072/forecast-api/forecast 10 | val client = new ForecastClientImpl() 11 | val forecast = client.forecast(Array(1, 2, 3, 4, 3, 2, 1), 7) 12 | ``` 13 | -------------------------------------------------------------------------------- /server/src/main/webapp/doc/o2c.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/sudden-drop/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 400.0, 4 | 500.0, 5 | 500.0, 6 | 400.0, 7 | 400.0, 8 | 300.0, 9 | 0.0, 10 | 350.0, 11 | 300.0, 12 | 300.0, 13 | 300.0, 14 | 300.0, 15 | 300.0, 16 | 300.0 17 | ], 18 | "horizon": 3, 19 | "expectedConfidence": 6.53, 20 | "allowForecastPercentError": 1.0, 21 | "expectedForecast": [ 22 | 389.0, 23 | 464.0, 24 | 506.0 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-trend/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 1001.0, 4 | 1002.0, 5 | 1003.0, 6 | 1004.0, 7 | 1005.0, 8 | 1006.0, 9 | 1007.0, 10 | 1008.0, 11 | 1009.0, 12 | 1010.0, 13 | 1011.0, 14 | 1012.0, 15 | 1013.0, 16 | 1014.0 17 | ], 18 | "horizon": 3, 19 | "expectedConfidence": 0.0, 20 | "allowForecastPercentError": 1.0, 21 | "expectedForecast": [ 22 | 1015.0, 23 | 1016.0, 24 | 1017.0 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-random/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 63.0, 4 | 92.0, 5 | 57.0, 6 | 79.0, 7 | 98.0, 8 | 10.0, 9 | 55.0, 10 | 28.0, 11 | 34.0, 12 | 77.0, 13 | 11.0, 14 | 87.0, 15 | 62.0, 16 | 48.0, 17 | 38.0, 18 | 52.0, 19 | 88.0, 20 | 34.0, 21 | 68.0, 22 | 33.0 23 | ], 24 | "horizon": 3, 25 | "expectedConfidence": 45.74, 26 | "allowForecastPercentError": 1.0, 27 | "expectedForecast": [] 28 | } 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | language: scala 4 | 5 | jdk: oraclejdk8 6 | 7 | scala: 8 | - 2.11.8 9 | 10 | cache: 11 | directories: 12 | - $HOME/.m2 13 | - $HOME/.ivy2 14 | 15 | services: 16 | - docker 17 | 18 | script: 19 | # Build server 20 | - cd server && mvn clean install docker:build 21 | # Build client and run tests against server 22 | - docker run --name forecast-api -p=9072:8080 vidible/forecast-api:2.0.3 & 23 | - cd ../client && sbt compile test 24 | - docker kill forecast-api 25 | 26 | after_success: 27 | - cd ../ && ./script/deploy.sh 28 | 29 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-seasonal/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 1.0, 4 | 2.0, 5 | 3.0, 6 | 4.0, 7 | 3.0, 8 | 2.0, 9 | 1.0, 10 | 1.0, 11 | 2.0, 12 | 3.0, 13 | 4.0, 14 | 3.0, 15 | 2.0, 16 | 1.0, 17 | 1.0, 18 | 2.0, 19 | 3.0, 20 | 4.0, 21 | 3.0, 22 | 2.0, 23 | 1.0 24 | ], 25 | "horizon": 7, 26 | "expectedConfidence": 0.0, 27 | "allowForecastPercentError": 1.0, 28 | "expectedForecast": [ 29 | 1.0, 30 | 2.0, 31 | 3.0, 32 | 4.0, 33 | 3.0, 34 | 2.0, 35 | 1.0 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSMetricType.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | /** 10 | * List of supported metrics types. 11 | */ 12 | public enum IFSMetricType { 13 | RMSE, MAPE, MEDAPE, SMAPE, TOTPE 14 | } 15 | -------------------------------------------------------------------------------- /client/src/main/scala/com/aol/one/reporting/forecastapi/client/ForecastResponse.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.fasterxml.jackson.annotation.JsonProperty 10 | 11 | import scala.beans.BeanProperty 12 | 13 | case class ForecastResponse(@BeanProperty @JsonProperty("forecast") forecast: Array[Double]) 14 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/weekly-seasonal-with-trend/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 101.0, 4 | 102.0, 5 | 103.0, 6 | 104.0, 7 | 105.0, 8 | 104.0, 9 | 103.0, 10 | 104.0, 11 | 105.0, 12 | 106.0, 13 | 107.0, 14 | 108.0, 15 | 107.0, 16 | 106.0, 17 | 107.0, 18 | 108.0, 19 | 109.0, 20 | 110.0, 21 | 111.0, 22 | 110.0, 23 | 109.0 24 | ], 25 | "horizon": 7, 26 | "expectedConfidence": 3.8, 27 | "allowForecastPercentError": 1.0, 28 | "expectedForecast": [ 29 | 110.0, 30 | 111.0, 31 | 112.0, 32 | 113.0, 33 | 114.0, 34 | 113.0, 35 | 112.0 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /server/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tomcat:8-jre8-alpine 2 | 3 | ADD tomcat-users.xml $CATALINA_HOME/conf 4 | 5 | HEALTHCHECK CMD curl --fail http://localhost:8080/forecast-api/health || exit 1 6 | 7 | ENV AOL_ENVIRONMENT prod 8 | ADD forecast-api.war /usr/local/tomcat/webapps/ 9 | RUN apk add --update curl 10 | RUN rm -rf webapps/forecast-api && mkdir webapps/forecast-api && unzip -o webapps/forecast-api.war -d webapps/forecast-api/ 11 | RUN mkdir -p /usr/local/tomcat/logs/ 12 | RUN touch /usr/local/tomcat/logs/forecast-api.log 13 | 14 | ADD javamelody-core-1.67.0.jar webapps/forecast-api/WEB-INF/lib 15 | ADD jrobin-1.5.9.jar webapps/forecast-api/WEB-INF/lib 16 | RUN apk add ttf-dejavu 17 | 18 | CMD tail -F /usr/local/tomcat/logs/forecast-api.log & /usr/local/tomcat/bin/catalina.sh run 19 | -------------------------------------------------------------------------------- /client/src/main/scala/com/aol/one/reporting/forecastapi/client/ConfigUtil.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.typesafe.config.{Config, ConfigFactory} 10 | 11 | object ConfigUtil { 12 | 13 | def getConfigFile(env: String): String = Option(env).filterNot(_.isEmpty).getOrElse("dev").toLowerCase() 14 | 15 | def getConfig(): Config = ConfigFactory.load(getConfigFile(sys.env.getOrElse("AOL_ENVIRONMENT", ""))) 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=DEBUG, file 3 | 4 | # Redirect log messages to console 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.Target=System.out 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n 9 | 10 | # Redirect log messages to a log file, support file rolling. 11 | log4j.appender.file=org.apache.log4j.RollingFileAppender 12 | log4j.appender.file.File=ifs-test.log 13 | log4j.appender.file.MaxFileSize=5MB 14 | log4j.appender.file.MaxBackupIndex=10 15 | log4j.appender.file.layout=org.apache.log4j.PatternLayout 16 | log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/resource/HealthResource.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.resource; 8 | 9 | import javax.ws.rs.GET; 10 | import javax.ws.rs.Path; 11 | import javax.ws.rs.Produces; 12 | import javax.ws.rs.core.MediaType; 13 | 14 | @Path("/health") 15 | @Produces(MediaType.TEXT_HTML) 16 | public class HealthResource { 17 | 18 | @GET 19 | public String getIndex() { 20 | return "OK"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/main/webapp/doc/lib/jquery.wiggle.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery Wiggle 3 | Author: WonderGroup, Jordan Thomas 4 | URL: http://labs.wondergroup.com/demos/mini-ui/index.html 5 | License: MIT (http://en.wikipedia.org/wiki/MIT_License) 6 | */ 7 | jQuery.fn.wiggle=function(o){var d={speed:50,wiggles:3,travel:5,callback:null};var o=jQuery.extend(d,o);return this.each(function(){var cache=this;var wrap=jQuery(this).wrap('
').css("position","relative");var calls=0;for(i=1;i<=o.wiggles;i++){jQuery(this).animate({left:"-="+o.travel},o.speed).animate({left:"+="+o.travel*2},o.speed*2).animate({left:"-="+o.travel},o.speed,function(){calls++;if(jQuery(cache).parent().hasClass('wiggle-wrap')){jQuery(cache).parent().replaceWith(cache);} 8 | if(calls==o.wiggles&&jQuery.isFunction(o.callback)){o.callback();}});}});}; -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/util/TimerCheckpoint.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.util; 8 | 9 | public class TimerCheckpoint { 10 | 11 | long time; 12 | String name; 13 | 14 | public long getTime() { 15 | return time; 16 | } 17 | public void setTime(long time) { 18 | this.time = time; 19 | } 20 | public String getName() { 21 | return name; 22 | } 23 | public void setName(String name) { 24 | this.name = name; 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /client/src/test/resources/scripts/plot_results.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018, Oath Inc. 2 | # Licensed under the terms of the Apache Version 2.0 license. 3 | # See LICENSE file in project root directory for terms. 4 | 5 | import matplotlib.pyplot as plt 6 | import sys 7 | 8 | OUTPUT_DIR = "../forecast-client/" 9 | 10 | 11 | def plot(id, historical, forecast): 12 | forecast_plot = [historical[-1]] + forecast 13 | plt.plot(range(1, len(historical) + 1), historical, color='green') 14 | plt.plot(range(len(historical), len(historical) + len(forecast_plot)), forecast_plot, color='blue', linestyle='--') 15 | fig = plt.gcf() 16 | fig.set_size_inches(10, 4) 17 | # plt.title(id, fontsize=12) 18 | dir = OUTPUT_DIR + id + "/" 19 | plt.savefig(dir + "plot-raw-and-actual.png") 20 | plt.gcf().clear() 21 | 22 | 23 | plot(sys.argv[1], sys.argv[2].split(","), sys.argv[3].split(",")) 24 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/app/DefaultDocServlet.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.app; 8 | 9 | import org.apache.catalina.servlets.DefaultServlet; 10 | 11 | import javax.servlet.annotation.WebServlet; 12 | 13 | /** 14 | * The Class DefaultDocServlet. This is a placeholder just so we can add the @WebServlet annotation 15 | * for servlet deployment. 16 | */ 17 | @WebServlet(urlPatterns = {"/doc/*"}) 18 | public class DefaultDocServlet extends DefaultServlet { 19 | private static final long serialVersionUID = 1L; 20 | } 21 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/jpe/gw/GWException.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.jpe.gw; 8 | 9 | /** 10 | * Grid walk algorithm exceptions. 11 | * 12 | * @author Copyright © 2012 John Eldreth All rights reserved. 13 | */ 14 | public class GWException extends Exception { 15 | private static final long serialVersionUID = 1L; 16 | 17 | /** 18 | * Default GWException. 19 | */ 20 | public GWException() { 21 | super("A grid walk algorithm exception occurred."); 22 | } 23 | 24 | /** 25 | * GWException with a specified message. 26 | * 27 | * @param message Exception message. 28 | */ 29 | public GWException( 30 | String message 31 | ) { 32 | super(message); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/util/WebPath.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.util; 8 | 9 | import java.io.UnsupportedEncodingException; 10 | import java.net.URLDecoder; 11 | 12 | public class WebPath { 13 | 14 | private static final String WEB_INF_DIR_NAME = "WEB-INF"; 15 | private static String web_inf_path; 16 | 17 | public static String getWebInfPath() throws UnsupportedEncodingException { 18 | if (web_inf_path == null) { 19 | web_inf_path = URLDecoder.decode(WebPath.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF8"); 20 | web_inf_path = web_inf_path.substring(0, web_inf_path.lastIndexOf(WEB_INF_DIR_NAME) + WEB_INF_DIR_NAME.length()); 21 | } 22 | return web_inf_path; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/real-data-video-view-supply-with-trend/raw-data: -------------------------------------------------------------------------------- 1 | 19668440.0, 2 | 24873847.0, 3 | 27818773.0, 4 | 32631934.0, 5 | 36677927.0, 6 | 39563081.0, 7 | 33565713.0, 8 | 31794775.0, 9 | 39732729.0, 10 | 37053602.0, 11 | 36896278.0, 12 | 40470464.0, 13 | 41759821.0, 14 | 28445750.0, 15 | 24682040.0, 16 | 32925512.0, 17 | 35472280.0, 18 | 40497144.0, 19 | 40211159.0, 20 | 41568115.0, 21 | 32957929.0, 22 | 28379756.0, 23 | 41831700.0, 24 | 42451364.0, 25 | 38425549.0, 26 | 42139185.0, 27 | 47492015.0, 28 | 33875888.0, 29 | 29305097.0, 30 | 41075337.0, 31 | 48524891.0, 32 | 55195938.0, 33 | 47232801.0, 34 | 43792809.0, 35 | 28903448.0, 36 | 28593603.0, 37 | 36122348.0, 38 | 23902979.0, 39 | 24465116.0, 40 | 25090285.0, 41 | 23377300.0, 42 | 16457929.0, 43 | 15579258.0, 44 | 27597149.0, 45 | 27821299.0, 46 | 22574927.0, 47 | 24487199.0, 48 | 26536998.0, 49 | 18994434.0, 50 | 13240386.0, 51 | 22421672.0, 52 | 24241006.0, 53 | 22022113.0, 54 | 19038456.0, 55 | 21904537.0, 56 | 15564092.0, 57 | 15519931.0, 58 | 18374821.0, 59 | 21804044.0, 60 | 23052971.0, 61 | 23330505.0, 62 | 22565482.0, 63 | 16745373.0, 64 | 15152948.0, 65 | 21069259.0, 66 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/two-weeks-seasonal/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 1.0, 4 | 2.0, 5 | 3.0, 6 | 4.0, 7 | 5.0, 8 | 6.0, 9 | 7.0, 10 | 7.0, 11 | 6.0, 12 | 5.0, 13 | 4.0, 14 | 3.0, 15 | 2.0, 16 | 1.0, 17 | 1.0, 18 | 2.0, 19 | 3.0, 20 | 4.0, 21 | 5.0, 22 | 6.0, 23 | 7.0, 24 | 7.0, 25 | 6.0, 26 | 5.0, 27 | 4.0, 28 | 3.0, 29 | 2.0, 30 | 1.0, 31 | 1.0, 32 | 2.0, 33 | 3.0, 34 | 4.0, 35 | 5.0, 36 | 6.0, 37 | 7.0, 38 | 7.0, 39 | 6.0, 40 | 5.0, 41 | 4.0, 42 | 3.0, 43 | 2.0, 44 | 1.0, 45 | 1.0, 46 | 2.0, 47 | 3.0, 48 | 4.0, 49 | 5.0, 50 | 6.0, 51 | 7.0, 52 | 7.0, 53 | 6.0, 54 | 5.0, 55 | 4.0, 56 | 3.0, 57 | 2.0, 58 | 1.0 59 | ], 60 | "horizon": 14, 61 | "expectedConfidence": 0.0, 62 | "allowForecastPercentError": 1.0, 63 | "expectedForecast": [ 64 | 1.0, 65 | 2.0, 66 | 3.0, 67 | 4.0, 68 | 5.0, 69 | 6.0, 70 | 7.0, 71 | 6.0, 72 | 6.0, 73 | 5.0, 74 | 4.0, 75 | 3.0, 76 | 2.0, 77 | 2.0 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /client/src/test/scala/com/aol/one/reporting/forecastapi/client/ConfigUtilTest.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import org.scalatest.mockito.MockitoSugar 10 | import org.scalatest.{BeforeAndAfterEach, Matchers, WordSpec} 11 | 12 | class ConfigUtilTest extends WordSpec with MockitoSugar with Matchers with BeforeAndAfterEach { 13 | 14 | "ConfigUtil" should { 15 | 16 | "getConfig selects correct environment variable" in { 17 | assert("dev" === ConfigUtil.getConfigFile("DEV")) 18 | assert("prod" === ConfigUtil.getConfigFile("prod")) 19 | assert("autotests" === ConfigUtil.getConfigFile("autotests")) 20 | assert("stage" === ConfigUtil.getConfigFile("STAGE")) 21 | 22 | assert("dev" === ConfigUtil.getConfigFile(null)) 23 | assert("dev" === ConfigUtil.getConfigFile("")) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ${catalina.base}/logs/forecast-api.log 10 | 11 | 12 | forecast-api.log.%d{yyyy-MM-dd} 13 | 14 | 15 | 30 16 | 17 | 18 | 19 | %d{yyyy-MM-dd}T%d{HH:mm:ss.SSS, Z} [%thread] %-5level %logger{36}:%line - %msg%n 20 | 21 | 22 | 23 | 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/app/JerseyServletContainer.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.app; 8 | 9 | import org.glassfish.jersey.server.ResourceConfig; 10 | import org.glassfish.jersey.servlet.ServletContainer; 11 | 12 | import javax.servlet.annotation.WebServlet; 13 | 14 | @WebServlet(loadOnStartup = 1) 15 | public class JerseyServletContainer extends ServletContainer { 16 | private static final long serialVersionUID = 1L; 17 | 18 | /** 19 | * Instantiates a new jersey servlet container. 20 | */ 21 | public JerseyServletContainer() { 22 | super(); 23 | } 24 | 25 | /** 26 | * Instantiates a new jersey servlet container. 27 | * 28 | * @param resourceConfig the resource config 29 | */ 30 | public JerseyServletContainer(ResourceConfig resourceConfig) { 31 | super(resourceConfig); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/main/scala/com/aol/one/reporting/forecastapi/client/ForecastRequest.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.fasterxml.jackson.annotation.JsonProperty 10 | 11 | import scala.beans.BeanProperty 12 | 13 | case class ForecastRequest(@BeanProperty @JsonProperty("timeSeries") timeSeries: Array[Double], 14 | @BeanProperty @JsonProperty("spikeFilterWindow") spikeFilterWindow: Int, 15 | @BeanProperty @JsonProperty("cannedSets") cannedSets: Array[String], 16 | @BeanProperty @JsonProperty("numberHoldBack") numberHoldBack: Int, 17 | @BeanProperty @JsonProperty("numberForecasts") numberForecasts: Int, 18 | @BeanProperty @JsonProperty("massageForecast") massageForecast: Boolean = true) { 19 | 20 | def this(timeSeries: Array[Double], numberForecasts: Int, cannedSets: Array[String]) = { 21 | this(timeSeries, ForecastParams.SpikeFilteringWindow, cannedSets, timeSeries.length, numberForecasts) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/build.sbt: -------------------------------------------------------------------------------- 1 | name := "forecast-api-client" 2 | 3 | scalaVersion := "2.11.8" 4 | 5 | version := "3.1.9" 6 | 7 | organization := "com.aol.one.reporting" 8 | 9 | publishMavenStyle := true 10 | 11 | libraryDependencies ++= Seq( 12 | "com.fasterxml.jackson.core" % "jackson-databind" % "2.3.5", 13 | "org.scalaj" %% "scalaj-http" % "2.3.0", 14 | "org.slf4j" % "slf4j-api" % "1.7.10", 15 | "com.typesafe" % "config" % "1.3.0", 16 | 17 | // test 18 | "org.scalatest" %% "scalatest" % "3.0.1" % Test, 19 | "org.powermock" % "powermock-api-mockito" % "1.6.4" % Test, 20 | "junit" % "junit" % "4.12" % Test 21 | ) 22 | 23 | // code coverage 24 | coverageEnabled in(Test, compile) := true 25 | coverageEnabled in(Compile, compile) := false 26 | coverageFailOnMinimum := true 27 | 28 | sonatypeProfileName := organization.value 29 | useGpg := false 30 | 31 | publishTo := { 32 | if (isSnapshot.value) Some(Opts.resolver.sonatypeSnapshots) 33 | else Some(Opts.resolver.sonatypeStaging) 34 | } 35 | 36 | resolvers += Resolver.mavenLocal 37 | 38 | licenses := Seq("MIT" -> url("https://www.apache.org/licenses/LICENSE-2.0")) 39 | homepage := Some(url("https://github.com/yahoo/aol-on-forecast")) 40 | 41 | scmInfo := Some( 42 | ScmInfo( 43 | url("https://github.com/yahoo/aol-on-forecast"), 44 | "scm:git@github.com/yahoo/aol-on-forecast.git" 45 | )) 46 | 47 | developers := List( 48 | Developer( 49 | id="One Reporting Team", 50 | name="One Reporting Team", 51 | email="noreply@oath.org", 52 | url=url("https://github.com/yahoo") 53 | )) 54 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/healthcheck/HealthCheckContextListener.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.healthcheck; 8 | 9 | import com.codahale.metrics.health.HealthCheckRegistry; 10 | import com.codahale.metrics.servlets.HealthCheckServlet.ContextListener; 11 | 12 | import javax.servlet.annotation.WebListener; 13 | 14 | /** 15 | * The class HealthCheckContextListener. The servlet context listener that creates the global health 16 | * check registry on application startup. 17 | */ 18 | @WebListener("Servlet Context Listener that creates global healthcheck registry") 19 | public class HealthCheckContextListener extends ContextListener { 20 | private static HealthCheckRegistry healthChecks = new HealthCheckRegistry(); 21 | 22 | /* 23 | * (non-Javadoc) 24 | * 25 | * @see com.codahale.metrics.servlets.HealthCheckServlet.ContextListener#getHealthCheckRegistry() 26 | */ 27 | @Override 28 | protected HealthCheckRegistry getHealthCheckRegistry() { 29 | return healthChecks; 30 | } 31 | 32 | 33 | /** 34 | * Gets the healthChecks. 35 | * 36 | * @return the healthChecks 37 | */ 38 | public static HealthCheckRegistry getHealthchecks() { 39 | return healthChecks; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/app/IfsCache.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.app; 8 | 9 | import com.aol.one.reporting.forecastapi.server.models.cs.IFSCannedSet; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | public class IfsCache { 15 | 16 | 17 | private Object lock = new Object(); 18 | private Map map; 19 | private Map> list; 20 | private List collectionNames; 21 | 22 | 23 | public void switchCache( 24 | Map map, 25 | Map> list, 26 | List collectionNames) { 27 | synchronized (lock) { 28 | this.map = map; 29 | this.list = list; 30 | this.collectionNames = collectionNames; 31 | } 32 | } 33 | 34 | public Map getMap() { 35 | synchronized (lock) { 36 | return map; 37 | } 38 | } 39 | 40 | public List getList(String collectionName) { 41 | synchronized (lock) { 42 | return list.get(collectionName); 43 | } 44 | } 45 | 46 | public List getCollectionNames() { 47 | synchronized (lock) { 48 | return collectionNames; 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/resource/WelcomeResource.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.resource; 8 | 9 | import com.aol.one.reporting.forecastapi.server.app.IfsConfig; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import javax.ws.rs.GET; 14 | import javax.ws.rs.Path; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.core.MediaType; 17 | import java.io.IOException; 18 | 19 | @Path("/") 20 | @Produces(MediaType.TEXT_HTML) 21 | public class WelcomeResource { 22 | private static final Logger LOG = LoggerFactory.getLogger(WelcomeResource.class); 23 | 24 | @GET 25 | public String getIndex() { 26 | String swaggerBaseUrl = null; 27 | try { 28 | swaggerBaseUrl = IfsConfig.config().getProperty( 29 | "swagger.api.basepath", "swagger.api.basepath.not.set"); 30 | } catch (IOException ex) { 31 | LOG.error("Cannot fetch swagger.api.basepath property.", ex); 32 | swaggerBaseUrl = "swagger.api.basepath.not.set"; 33 | } 34 | String welcome = "" 35 | + "" 36 | + "" 39 | + ""; 40 | LOG.debug(String.format("Welcome this: %s", welcome)); 41 | return welcome; 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /server/src/main/webapp/doc/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 */ 2 | html, 3 | body, 4 | div, 5 | span, 6 | applet, 7 | object, 8 | iframe, 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6, 15 | p, 16 | blockquote, 17 | pre, 18 | a, 19 | abbr, 20 | acronym, 21 | address, 22 | big, 23 | cite, 24 | code, 25 | del, 26 | dfn, 27 | em, 28 | img, 29 | ins, 30 | kbd, 31 | q, 32 | s, 33 | samp, 34 | small, 35 | strike, 36 | strong, 37 | sub, 38 | sup, 39 | tt, 40 | var, 41 | b, 42 | u, 43 | i, 44 | center, 45 | dl, 46 | dt, 47 | dd, 48 | ol, 49 | ul, 50 | li, 51 | fieldset, 52 | form, 53 | label, 54 | legend, 55 | table, 56 | caption, 57 | tbody, 58 | tfoot, 59 | thead, 60 | tr, 61 | th, 62 | td, 63 | article, 64 | aside, 65 | canvas, 66 | details, 67 | embed, 68 | figure, 69 | figcaption, 70 | footer, 71 | header, 72 | hgroup, 73 | menu, 74 | nav, 75 | output, 76 | ruby, 77 | section, 78 | summary, 79 | time, 80 | mark, 81 | audio, 82 | video { 83 | margin: 0; 84 | padding: 0; 85 | border: 0; 86 | font-size: 100%; 87 | font: inherit; 88 | vertical-align: baseline; 89 | } 90 | /* HTML5 display-role reset for older browsers */ 91 | article, 92 | aside, 93 | details, 94 | figcaption, 95 | figure, 96 | footer, 97 | header, 98 | hgroup, 99 | menu, 100 | nav, 101 | section { 102 | display: block; 103 | } 104 | body { 105 | line-height: 1; 106 | } 107 | ol, 108 | ul { 109 | list-style: none; 110 | } 111 | blockquote, 112 | q { 113 | quotes: none; 114 | } 115 | blockquote:before, 116 | blockquote:after, 117 | q:before, 118 | q:after { 119 | content: ''; 120 | content: none; 121 | } 122 | table { 123 | border-collapse: collapse; 124 | border-spacing: 0; 125 | } 126 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/util/Optional.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.util; 8 | 9 | /** 10 | * Class implementing optional value for a specified type. 11 | */ 12 | public final class Optional { 13 | private T Value; 14 | 15 | /** 16 | * Return empty optional. 17 | * 18 | * @return Empty optional. 19 | */ 20 | public static Optional empty() { 21 | return new Optional(); 22 | } 23 | 24 | /** 25 | * Fetch wrapped value. 26 | * 27 | * @return Wrapped value. 28 | */ 29 | public T get() { 30 | return Value; 31 | } 32 | 33 | /** 34 | * Is there a non-empty value wrapped. 35 | * 36 | * @return True if not empty. False otherwise. 37 | */ 38 | public boolean isPresent() { 39 | return Value != null; 40 | } 41 | 42 | /** 43 | * Return non-empty optional. 44 | * 45 | * @param value Value to wrap. 46 | * 47 | * @return Non-empty optional. 48 | */ 49 | public static Optional of( 50 | T value 51 | ) { 52 | return new Optional(value); 53 | } 54 | 55 | /*******************/ 56 | /* Private Methods */ 57 | /*******************/ 58 | 59 | /** 60 | * Default constructor makes empty optional. 61 | */ 62 | private Optional() { 63 | Value = null; 64 | } 65 | 66 | /** 67 | * Fully specified constructor. 68 | * 69 | * @param value Value to wrap. 70 | */ 71 | private Optional( 72 | T value 73 | ) { 74 | Value = value; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/src/main/scala/com/aol/one/reporting/forecastapi/client/ForecastParams.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | object ForecastParams { 10 | 11 | val CannedSet = Array( 12 | "RW-NONE-DAY", 13 | "RW-NONE-DAY-WEEK", 14 | "REG-NONE-ADD-DAY", 15 | "REG-LINEAR-ADD-DAY", 16 | "REG-LINEAR-ADD-DAY-WEEK", 17 | "REG-LINEAR-ADD-DAY-LOG", 18 | "REG-LINEAR-ADD-DAY-WEEK-LOG", 19 | "AR-NONE-DEMEAN-WEEK", 20 | "RW-NONE-WEEK", 21 | "REG-NONE-ADD-WEEK-SAG", 22 | "ARIMA-0,2,2-0,1,1s-WEEK", 23 | "EXP-LINEAR-MULT-YEAR", 24 | "REG-LINEAR-ADD-YEAR", 25 | "REG-NONE-PHASE2-WEEK-YEAR" 26 | ) 27 | 28 | /** 29 | * Until confidence is implemented by IFS we use the most recent 7 days to work out confidence by ourselves. 30 | */ 31 | val ConfidenceHorizon = 7 32 | 33 | /** 34 | * We need at least 2 points to fit a line and at least 3 points for basic exponential smoothing 35 | * If historical data is insufficient we can still make a forecast but we set confidence level to most unreliable. 36 | */ 37 | val MinConfidenceHistorical = 3 38 | 39 | /** 40 | * Enable spike filtering with the specified clipping window size. 41 | * The minimum size is 3 and the maximum is 30. 42 | * The clipping window size is used to define a localized moving average that is used as the basis for identifying outliers as 43 | * well as defining the value to use instead. 44 | */ 45 | val SpikeFilteringWindow = 4 46 | } 47 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSParameterValue.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | /** 10 | * IFS model parameter value. 11 | */ 12 | public final class IFSParameterValue { 13 | private String Parameter; 14 | private String Value; 15 | 16 | /** 17 | * Default constructor. 18 | */ 19 | public IFSParameterValue() { 20 | setParameter(""); 21 | setValue(""); 22 | } 23 | 24 | /** 25 | * Fully specified constructor. 26 | * 27 | * @param parameter Parameter name. 28 | * @param value Parameter value. 29 | */ 30 | public IFSParameterValue( 31 | String parameter, 32 | String value 33 | ) { 34 | setParameter(parameter); 35 | setValue(value); 36 | } 37 | 38 | /** 39 | * Clone this parameter value. 40 | * 41 | * @return Parameter value clone. 42 | */ 43 | public IFSParameterValue clone() { 44 | return new IFSParameterValue(Parameter, Value); 45 | } 46 | 47 | /** 48 | * @return the parameter name 49 | */ 50 | public String getParameter() { 51 | return Parameter; 52 | } 53 | 54 | /** 55 | * @param parameter the parameter to set 56 | */ 57 | public void setParameter(String parameter) { 58 | Parameter = parameter; 59 | } 60 | 61 | /** 62 | * @return the value 63 | */ 64 | public String getValue() { 65 | return Value; 66 | } 67 | 68 | /** 69 | * @param value the value to set 70 | */ 71 | public void setValue(String value) { 72 | Value = value; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/daily-seasonal/raw-data: -------------------------------------------------------------------------------- 1 | 100.0, 2 | 101.0, 3 | 102.0, 4 | 103.0, 5 | 104.0, 6 | 105.0, 7 | 106.0, 8 | 107.0, 9 | 108.0, 10 | 109.0, 11 | 109.5, 12 | 110.0, 13 | 110.0, 14 | 109.5, 15 | 109.0, 16 | 108.0, 17 | 107.0, 18 | 106.0, 19 | 105.0, 20 | 104.0, 21 | 103.0, 22 | 102.0, 23 | 101.0, 24 | 100.0, 25 | 100.0, 26 | 101.0, 27 | 102.0, 28 | 103.0, 29 | 104.0, 30 | 105.0, 31 | 106.0, 32 | 107.0, 33 | 108.0, 34 | 109.0, 35 | 109.5, 36 | 110.0, 37 | 110.0, 38 | 109.5, 39 | 109.0, 40 | 108.0, 41 | 107.0, 42 | 106.0, 43 | 105.0, 44 | 104.0, 45 | 103.0, 46 | 102.0, 47 | 101.0, 48 | 100.0, 49 | 100.0, 50 | 101.0, 51 | 102.0, 52 | 103.0, 53 | 104.0, 54 | 105.0, 55 | 106.0, 56 | 107.0, 57 | 108.0, 58 | 109.0, 59 | 109.5, 60 | 110.0, 61 | 110.0, 62 | 109.5, 63 | 109.0, 64 | 108.0, 65 | 107.0, 66 | 106.0, 67 | 105.0, 68 | 104.0, 69 | 103.0, 70 | 102.0, 71 | 101.0, 72 | 100.0, 73 | 100.0, 74 | 101.0, 75 | 102.0, 76 | 103.0, 77 | 104.0, 78 | 105.0, 79 | 106.0, 80 | 107.0, 81 | 108.0, 82 | 109.0, 83 | 109.5, 84 | 110.0, 85 | 110.0, 86 | 109.5, 87 | 109.0, 88 | 108.0, 89 | 107.0, 90 | 106.0, 91 | 105.0, 92 | 104.0, 93 | 103.0, 94 | 102.0, 95 | 101.0, 96 | 100.0, 97 | 100.0, 98 | 101.0, 99 | 102.0, 100 | 103.0, 101 | 104.0, 102 | 105.0, 103 | 106.0, 104 | 107.0, 105 | 108.0, 106 | 109.0, 107 | 109.5, 108 | 110.0, 109 | 110.0, 110 | 109.5, 111 | 109.0, 112 | 108.0, 113 | 107.0, 114 | 106.0, 115 | 105.0, 116 | 104.0, 117 | 103.0, 118 | 102.0, 119 | 101.0, 120 | 100.0, 121 | 100.0, 122 | 101.0, 123 | 102.0, 124 | 103.0, 125 | 104.0, 126 | 105.0, 127 | 106.0, 128 | 107.0, 129 | 108.0, 130 | 109.0, 131 | 109.5, 132 | 110.0, 133 | 110.0, 134 | 109.5, 135 | 109.0, 136 | 108.0, 137 | 107.0, 138 | 106.0, 139 | 105.0, 140 | 104.0, 141 | 103.0, 142 | 102.0, 143 | 101.0, 144 | 100.0, 145 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/daily-seasonal-with-trend/raw-data: -------------------------------------------------------------------------------- 1 | 100.0, 2 | 101.1, 3 | 102.2, 4 | 103.3, 5 | 104.4, 6 | 105.5, 7 | 106.6, 8 | 107.7, 9 | 108.8, 10 | 109.9, 11 | 110.5, 12 | 111.1, 13 | 111.2, 14 | 110.8, 15 | 110.4, 16 | 109.5, 17 | 108.6, 18 | 107.7, 19 | 106.8, 20 | 105.9, 21 | 105.0, 22 | 104.1, 23 | 103.2, 24 | 102.3, 25 | 102.4, 26 | 103.5, 27 | 104.6, 28 | 105.7, 29 | 106.8, 30 | 107.9, 31 | 109.0, 32 | 110.1, 33 | 111.2, 34 | 112.3, 35 | 112.9, 36 | 113.5, 37 | 113.6, 38 | 113.2, 39 | 112.8, 40 | 111.9, 41 | 111.0, 42 | 110.1, 43 | 109.2, 44 | 108.3, 45 | 107.4, 46 | 106.5, 47 | 105.6, 48 | 104.7, 49 | 104.8, 50 | 105.9, 51 | 107.0, 52 | 108.1, 53 | 109.2, 54 | 110.3, 55 | 111.4, 56 | 112.5, 57 | 113.6, 58 | 114.7, 59 | 115.3, 60 | 115.9, 61 | 116.0, 62 | 115.6, 63 | 115.2, 64 | 114.3, 65 | 113.4, 66 | 112.5, 67 | 111.6, 68 | 110.7, 69 | 109.8, 70 | 108.9, 71 | 108.0, 72 | 107.1, 73 | 107.2, 74 | 108.3, 75 | 109.4, 76 | 110.5, 77 | 111.6, 78 | 112.7, 79 | 113.8, 80 | 114.9, 81 | 116.0, 82 | 117.1, 83 | 117.7, 84 | 118.3, 85 | 118.4, 86 | 118.0, 87 | 117.6, 88 | 116.7, 89 | 115.8, 90 | 114.9, 91 | 114.0, 92 | 113.1, 93 | 112.2, 94 | 111.3, 95 | 110.4, 96 | 109.5, 97 | 109.6, 98 | 110.7, 99 | 111.8, 100 | 112.9, 101 | 114.0, 102 | 115.1, 103 | 116.2, 104 | 117.3, 105 | 118.4, 106 | 119.5, 107 | 120.1, 108 | 120.7, 109 | 120.8, 110 | 120.4, 111 | 120.0, 112 | 119.1, 113 | 118.2, 114 | 117.3, 115 | 116.4, 116 | 115.5, 117 | 114.6, 118 | 113.7, 119 | 112.8, 120 | 111.9, 121 | 112.0, 122 | 113.1, 123 | 114.2, 124 | 115.3, 125 | 116.4, 126 | 117.5, 127 | 118.6, 128 | 119.7, 129 | 120.8, 130 | 121.9, 131 | 122.5, 132 | 123.1, 133 | 123.2, 134 | 122.8, 135 | 122.4, 136 | 121.5, 137 | 120.6, 138 | 119.7, 139 | 118.8, 140 | 117.9, 141 | 117.0, 142 | 116.1, 143 | 115.2, 144 | 114.3, 145 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/real-data-video-view-supply-with-trend/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 19668440.0, 4 | 24873847.0, 5 | 27818773.0, 6 | 32631934.0, 7 | 36677927.0, 8 | 39563081.0, 9 | 33565713.0, 10 | 31794775.0, 11 | 39732729.0, 12 | 37053602.0, 13 | 36896278.0, 14 | 40470464.0, 15 | 41759821.0, 16 | 28445750.0, 17 | 24682040.0, 18 | 32925512.0, 19 | 35472280.0, 20 | 40497144.0, 21 | 40211159.0, 22 | 41568115.0, 23 | 32957929.0, 24 | 28379756.0, 25 | 41831700.0, 26 | 42451364.0, 27 | 38425549.0, 28 | 42139185.0, 29 | 47492015.0, 30 | 33875888.0, 31 | 29305097.0, 32 | 41075337.0, 33 | 48524891.0, 34 | 55195938.0, 35 | 47232801.0, 36 | 43792809.0, 37 | 28903448.0, 38 | 28593603.0, 39 | 36122348.0, 40 | 23902979.0, 41 | 24465116.0, 42 | 25090285.0, 43 | 23377300.0, 44 | 16457929.0, 45 | 15579258.0, 46 | 27597149.0, 47 | 27821299.0, 48 | 22574927.0, 49 | 24487199.0, 50 | 26536998.0, 51 | 18994434.0, 52 | 13240386.0, 53 | 22421672.0, 54 | 24241006.0, 55 | 22022113.0, 56 | 19038456.0, 57 | 21904537.0, 58 | 15564092.0, 59 | 15519931.0, 60 | 18374821.0, 61 | 21804044.0, 62 | 23052971.0, 63 | 23330505.0, 64 | 22565482.0, 65 | 16745373.0, 66 | 15152948.0, 67 | 21069259.0 68 | ], 69 | "horizon": 23, 70 | "expectedConfidence": 71.0, 71 | "allowForecastPercentError": 22.0, 72 | "expectedForecast": [ 73 | 22530872, 74 | 22530872, 75 | 22530872, 76 | 22530872, 77 | 17047464, 78 | 14955562, 79 | 17047464, 80 | 22530872, 81 | 22530872, 82 | 22530872, 83 | 22530872, 84 | 17047464, 85 | 14955562, 86 | 17047464, 87 | 22530872, 88 | 22530872, 89 | 22530872, 90 | 22530872, 91 | 17047464, 92 | 14955562, 93 | 17047464, 94 | 22530872, 95 | 22530872 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /client/src/main/scala/com/aol/one/reporting/forecastapi/client/ForecastHttpClient.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.typesafe.config.Config 10 | import org.slf4j.LoggerFactory 11 | 12 | import scalaj.http._ 13 | 14 | trait ForecastHttpClient { 15 | def get(request: String): String 16 | } 17 | 18 | class ForecastHttpClientImpl(serviceUrl: String) extends ForecastHttpClient { 19 | 20 | private lazy val logger = LoggerFactory.getLogger(this.getClass.getSimpleName) 21 | println(s"Forecast client using service url $serviceUrl") 22 | private val conf = ConfigUtil.getConfig().getConfig("http") 23 | private val requestPart = getRequest(conf) 24 | private val maxRetry = conf.getInt("max-retry") 25 | 26 | def get(request: String): String = _retry(maxRetry)(getResponse(request)) 27 | 28 | private def getResponse(request: String): String = { 29 | val response = requestPart.postData(request).asString 30 | if (response.code != 200) { 31 | throw new RuntimeException(s"Server response was not 200 (SC_OK) response=" + response.code + "body=" + response.body) 32 | } 33 | response.body 34 | } 35 | 36 | private def _retry[String](n: Int)(fn: => String): String = { 37 | try { 38 | fn 39 | } catch { 40 | case e: RuntimeException => 41 | if (n > 1) { 42 | logger.warn(s"Call to forecast service failed, retrying ($n) $e") 43 | _retry(n - 1)(fn) 44 | } 45 | else throw e 46 | } 47 | } 48 | 49 | private def getRequest(conf: Config): HttpRequest = { 50 | Http(serviceUrl) 51 | .header("Content-Type", "application/json") 52 | .timeout(conf.getInt("conn-timeout") * 1000, conf.getInt("read-timeout") * 1000) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSUsageDescription.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | /** 10 | * Usage information description in component form. Support for information 11 | * reuse, particularly models. 12 | * 13 | */ 14 | public final class IFSUsageDescription { 15 | private String Body; 16 | private String Parameters; 17 | private String Summary; 18 | 19 | /** 20 | * Default constructor. 21 | */ 22 | public IFSUsageDescription() { 23 | setBody(""); 24 | setParameters(""); 25 | setSummary(""); 26 | } 27 | 28 | /** 29 | * Fully specified constructor. 30 | * 31 | * @param summary Summary line. 32 | * @param body Overall description. 33 | * @param Parameters Parameter description. 34 | */ 35 | public IFSUsageDescription( 36 | String summary, 37 | String body, 38 | String parameters 39 | ) { 40 | setSummary(summary); 41 | setBody(body); 42 | setParameters(parameters); 43 | } 44 | 45 | /** 46 | * @return the body 47 | */ 48 | public String getBody() { 49 | return Body; 50 | } 51 | 52 | /** 53 | * @param body the body to set 54 | */ 55 | public void setBody(String body) { 56 | Body = body; 57 | } 58 | 59 | /** 60 | * @return the parameters 61 | */ 62 | public String getParameters() { 63 | return Parameters; 64 | } 65 | 66 | /** 67 | * @param parameters the parameters to set 68 | */ 69 | public void setParameters(String parameters) { 70 | Parameters = parameters; 71 | } 72 | 73 | /** 74 | * @return the summary 75 | */ 76 | public String getSummary() { 77 | return Summary; 78 | } 79 | 80 | /** 81 | * @param summary the summary to set 82 | */ 83 | public void setSummary(String summary) { 84 | Summary = summary; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /client/src/test/scala/com/aol/one/reporting/forecastapi/client/ForecastPerformanceTest.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.aol.one.reporting.forecastapi.client.ITTestUtil._ 10 | import org.scalatest.mockito.MockitoSugar 11 | import org.scalatest.{BeforeAndAfterEach, FreeSpec, Matchers} 12 | 13 | class ForecastPerformanceTest extends FreeSpec with MockitoSugar with Matchers with BeforeAndAfterEach { 14 | 15 | /** 16 | * - This test is to check if performance is overly slow. Verify 1second/call. 17 | * - Expected performance in production: 0.2s-0.5s/call. 18 | * - We test thread safety too, by running multiple clients in parallel with different expected forecast results. 19 | */ 20 | "ForecastClient.forecast performance test" - { 21 | 22 | "average 1sec/call" in { 23 | 24 | val performanceTestClients = List( 25 | new PerformanceClient("client-1", 100, "daily-seasonal", 100), 26 | new PerformanceClient("client-2", 100, "weekly-seasonal", 100), 27 | new PerformanceClient("client-3", 100, "yearly-seasonal", 100) 28 | ) 29 | 30 | performanceTestClients.foreach(_.start) 31 | performanceTestClients.foreach(_.join) 32 | performanceTestClients.foreach(client => assert(client.success)) 33 | } 34 | } 35 | 36 | private class PerformanceClient(id: String, calls: Int, scenario: String, expectedCompletionTime: Int) extends Thread { 37 | 38 | var success = false 39 | 40 | override def run() { 41 | val start = System.nanoTime() 42 | for (i <- 1 to calls) { 43 | testForecast(scenario, false) 44 | println(s"$id : $i/$calls") 45 | } 46 | val diff = (System.nanoTime() - start) / (1000 * 1000 * 1000) 47 | assert(diff <= expectedCompletionTime) 48 | success = true 49 | } 50 | } 51 | 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSParameterSpec.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | /** 13 | * IFS model parameter specification. 14 | */ 15 | public final class IFSParameterSpec { 16 | private String Model; 17 | private List ParameterValues; 18 | 19 | /** 20 | * Default constructor. 21 | */ 22 | public IFSParameterSpec() { 23 | setModel(""); 24 | setParameterValues(null); 25 | } 26 | 27 | /** 28 | * Fully specified constructor. 29 | * 30 | * @param model IFS model name. 31 | * @param parameter_values Model parameters. 32 | */ 33 | public IFSParameterSpec( 34 | String model, 35 | List parameter_values 36 | ) { 37 | setModel(model); 38 | setParameterValues(parameter_values); 39 | } 40 | 41 | /** 42 | * Clone this parameter spec. 43 | * 44 | * @return Parameter spec clone. 45 | */ 46 | public IFSParameterSpec clone() { 47 | List values = new ArrayList<>(); 48 | 49 | for (IFSParameterValue value : ParameterValues) { 50 | values.add(value.clone()); 51 | } 52 | return new IFSParameterSpec(Model, values); 53 | } 54 | 55 | /** 56 | * @return the model 57 | */ 58 | public String getModel() { 59 | return Model; 60 | } 61 | 62 | /** 63 | * @param model the model to set 64 | */ 65 | public void setModel(String model) { 66 | Model = model; 67 | } 68 | 69 | /** 70 | * @return the parameterValues 71 | */ 72 | public List getParameterValues() { 73 | return ParameterValues; 74 | } 75 | 76 | /** 77 | * @param parameterValues the parameterValues to set 78 | */ 79 | public void setParameterValues(List parameterValues) { 80 | ParameterValues = parameterValues; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSComputation.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | /** 10 | * Class implementing commonly used computations. 11 | */ 12 | public class IFSComputation { 13 | 14 | /** 15 | * Solve NxN linear equations (Ax = b). 16 | * 17 | * @param matrix Augmented matrix. Includes A in NxN rows and columns 18 | * and b in column N+1. A triangular matrix will be left in its place. 19 | * 20 | * @return Solution to equations (N values). 21 | * 22 | * @throws IFSException if a solution could not be computed or the matrix 23 | * was improperly specified. 24 | */ 25 | public static double[] getLinearEqnSoln( 26 | double[][] matrix 27 | ) throws IFSException { 28 | if (matrix == null || matrix.length < 2) 29 | throw new IFSException(1); 30 | else 31 | for (int i = 0; i < matrix.length; i++) 32 | if (matrix[i].length != (matrix.length+1)) 33 | throw new IFSException(2, i+1, matrix.length+1); 34 | 35 | int n = matrix.length; 36 | double[] x = new double[n]; 37 | int i; 38 | int j; 39 | int k; 40 | int p; 41 | double t; 42 | 43 | for (i = 0; i < n-1; i++) { 44 | for (p = i; p < n; p++) 45 | if (matrix[p][i] != 0.0) 46 | break; 47 | if (p == n) 48 | throw new IFSException(3, i+1); 49 | if (p != i) 50 | for (j = 0; j <= n; j++) { 51 | t = matrix[p][j]; 52 | matrix[p][j] = matrix[i][j]; 53 | matrix[i][j] = t; 54 | } 55 | for (j = i+1; j < n; j++) { 56 | t = matrix[j][i]/matrix[i][i]; 57 | for (k = i; k < n+1; k++) 58 | matrix[j][k] -= t*matrix[i][k]; 59 | } 60 | } 61 | if (matrix[n-1][n-1] == 0.0) 62 | throw new IFSException(4); 63 | x[n-1] = matrix[n-1][n]/matrix[n-1][n-1]; 64 | for (i = n-2; i >= 0; i--) { 65 | t = 0.0; 66 | for (j = i+1; j < n; j++) 67 | t += matrix[i][j]*x[j]; 68 | x[i] = (matrix[i][n]-t)/matrix[i][i]; 69 | } 70 | return x; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSCycle.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | /** 10 | * Class for processing the cycle common model parameter. 11 | */ 12 | public final class IFSCycle { 13 | 14 | /** 15 | * Fetch seasonal cycle. The specification is as follows: 16 | * 17 | * -- If the seasonal adjustment is to be determined 18 | * automatically. 19 | * -- If there is no seasonal adjustment. 20 | * -- If there is no seasonal adjustment. 21 | * -- The seasonal cycle to use. The historical data 22 | * length must be at least 2 times the cycle for the 23 | * seasonal variation to be applied. 24 | * 25 | * @param series Time series to reshape. 26 | * @param spec Cycle specification. See above for possible values. 27 | * 28 | * @return Seasonal cycle. 29 | * 30 | * @throws IFSException for invalid specifications or unexpected series 31 | * values. 32 | */ 33 | public static int getCycle( 34 | double[] series, 35 | String spec 36 | ) throws IFSException { 37 | int cycle = 0; 38 | 39 | try { 40 | cycle = Integer.parseInt(spec); 41 | } 42 | catch (NumberFormatException ex) { 43 | throw new IFSException(72); 44 | } 45 | if (cycle != -1 && cycle < 0) 46 | throw new IFSException(73); 47 | else if (cycle == -1) { 48 | if (series != null) 49 | cycle = IFSDetectSeasonalCycle.getSeasonalCycle(series); 50 | } 51 | 52 | return cycle; 53 | } 54 | 55 | /** 56 | * Canned usage information for ndays_back parameter. 57 | * 58 | * @return Usage string. 59 | */ 60 | public static String usage() { 61 | return( 62 | "\n" 63 | + "cycle=\n" 64 | + " -- The seasonal adjustment is to be determined automatically.\n" 65 | + " -- There is no seasonal adjustment.\n" 66 | + " -- The cycle to mirror in the seasonal adjustment. The historical\n" 67 | + " data length must be at least 2 times the cycle for the seasonal\n" 68 | + " variation to be applied.\n" 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /server/docker/tomcat-users.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 22 | 30 | 37 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/app/IfsConfig.java: -------------------------------------------------------------------------------- 1 | package com.aol.one.reporting.forecastapi.server.app; 2 | 3 | 4 | import com.aol.one.reporting.forecastapi.server.util.WebPath; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.Properties; 11 | 12 | public class IfsConfig { 13 | private final static Logger LOG = LoggerFactory.getLogger(IfsConfig.class); 14 | 15 | private static IfsConfig config; 16 | private final Properties properties; 17 | private static String webInfDir; 18 | 19 | private static IfsCache cache = new IfsCache(); 20 | 21 | public static IfsCache getCache() { 22 | return cache; 23 | } 24 | 25 | public static String getWebInfDir() { 26 | return webInfDir; 27 | } 28 | 29 | public static Properties config() throws IOException { 30 | if (config == null) { 31 | config = new IfsConfig(); 32 | } 33 | return config.properties(); 34 | } 35 | 36 | 37 | private IfsConfig() throws IOException { 38 | // load application properties for configuration 39 | final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 40 | this.properties = new Properties(); 41 | try (final InputStream in = classLoader.getResourceAsStream("forecast-api.properties")) { 42 | if (in == null) { 43 | throw new IOException( 44 | "Configuration file forecast-api.properties not found in classpath"); 45 | } 46 | this.properties.load(in); 47 | } 48 | 49 | // let system properties override what is set in the app's properties file. 50 | // It is a good 51 | // practice, plus it help with test scenarios. 52 | final Properties systemProperties = System.getProperties(); 53 | for (final Object key : properties.keySet()) { 54 | if (systemProperties.containsKey(key)) 55 | this.properties.setProperty(key.toString(), systemProperties.getProperty(key.toString())); 56 | System.out.println(key.toString() + " --> " + this.properties.getProperty(key.toString()).toString()); 57 | } 58 | webInfDir = WebPath.getWebInfPath(); 59 | LOG.debug("WebInfDir : " + webInfDir); 60 | 61 | cache = new IfsCache(); 62 | } 63 | 64 | private Properties properties() { 65 | return this.properties; 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/util/Timer.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.util; 8 | 9 | /** 10 | * Timer class mainly used to time code sections. Uses system nanoTime api 11 | * since it is the most accurate regardless of the OS platform. 12 | */ 13 | public final class Timer { 14 | private long TimeStart; 15 | private long TimeDelta; 16 | private boolean IsRunning; 17 | 18 | /** 19 | * Default constructor. 20 | */ 21 | public Timer() { 22 | TimeStart = 0; 23 | TimeDelta = 0; 24 | IsRunning = false; 25 | } 26 | 27 | /** 28 | * Get millisecond unit timing of where we are currently in running timer 29 | * or the last completed timing. 30 | * 31 | * @return Current or last timing in milliseconds. 32 | */ 33 | public double getTimeMilliSeconds() { 34 | if (IsRunning) 35 | return (System.nanoTime()-TimeStart)/1000000.0; 36 | else 37 | return TimeDelta/1000000.0; 38 | } 39 | 40 | /** 41 | * Get minute unit timing of where we are currently in running timer or 42 | * the last completed timing. 43 | * 44 | * @return Current or last timing in minutes. 45 | */ 46 | public double getTimeMinutes() { 47 | if (this.IsRunning) 48 | return (System.nanoTime()-TimeStart)/60000000000.0; 49 | else 50 | return TimeDelta/60000000000.0; 51 | } 52 | 53 | /** 54 | * Get second unit timing of where we are currently in running timer or 55 | * the last completed timing. 56 | * 57 | * @return Current or last timing in seconds. 58 | */ 59 | public double getTimeSeconds() { 60 | if (this.IsRunning) 61 | return (System.nanoTime()-TimeStart)/1000000000.0; 62 | else 63 | return TimeDelta/1000000000.0; 64 | } 65 | 66 | /** 67 | * Start timer. 68 | * 69 | * @return This timer. 70 | */ 71 | public Timer start() { 72 | if (IsRunning) 73 | return this; 74 | TimeStart = System.nanoTime(); 75 | IsRunning = true; 76 | return this; 77 | } 78 | 79 | /** 80 | * Stop timer. 81 | * 82 | * @return This timer. 83 | */ 84 | public Timer stop() { 85 | if (IsRunning) { 86 | TimeDelta = System.nanoTime() - TimeStart; 87 | IsRunning = false; 88 | } 89 | return this; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/daily-seasonal/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 100.0, 4 | 101.0, 5 | 102.0, 6 | 103.0, 7 | 104.0, 8 | 105.0, 9 | 106.0, 10 | 107.0, 11 | 108.0, 12 | 109.0, 13 | 109.5, 14 | 110.0, 15 | 110.0, 16 | 109.5, 17 | 109.0, 18 | 108.0, 19 | 107.0, 20 | 106.0, 21 | 105.0, 22 | 104.0, 23 | 103.0, 24 | 102.0, 25 | 101.0, 26 | 100.0, 27 | 100.0, 28 | 101.0, 29 | 102.0, 30 | 103.0, 31 | 104.0, 32 | 105.0, 33 | 106.0, 34 | 107.0, 35 | 108.0, 36 | 109.0, 37 | 109.5, 38 | 110.0, 39 | 110.0, 40 | 109.5, 41 | 109.0, 42 | 108.0, 43 | 107.0, 44 | 106.0, 45 | 105.0, 46 | 104.0, 47 | 103.0, 48 | 102.0, 49 | 101.0, 50 | 100.0, 51 | 100.0, 52 | 101.0, 53 | 102.0, 54 | 103.0, 55 | 104.0, 56 | 105.0, 57 | 106.0, 58 | 107.0, 59 | 108.0, 60 | 109.0, 61 | 109.5, 62 | 110.0, 63 | 110.0, 64 | 109.5, 65 | 109.0, 66 | 108.0, 67 | 107.0, 68 | 106.0, 69 | 105.0, 70 | 104.0, 71 | 103.0, 72 | 102.0, 73 | 101.0, 74 | 100.0, 75 | 100.0, 76 | 101.0, 77 | 102.0, 78 | 103.0, 79 | 104.0, 80 | 105.0, 81 | 106.0, 82 | 107.0, 83 | 108.0, 84 | 109.0, 85 | 109.5, 86 | 110.0, 87 | 110.0, 88 | 109.5, 89 | 109.0, 90 | 108.0, 91 | 107.0, 92 | 106.0, 93 | 105.0, 94 | 104.0, 95 | 103.0, 96 | 102.0, 97 | 101.0, 98 | 100.0 99 | ], 100 | "horizon": 48, 101 | "expectedConfidence": 0.0, 102 | "allowForecastPercentError": 10.0, 103 | "expectedForecast": [ 104 | 100.0, 105 | 101.0, 106 | 102.0, 107 | 103.0, 108 | 104.0, 109 | 105.0, 110 | 106.0, 111 | 107.0, 112 | 108.0, 113 | 109.0, 114 | 109.5, 115 | 110.0, 116 | 110.0, 117 | 109.5, 118 | 109.0, 119 | 108.0, 120 | 107.0, 121 | 106.0, 122 | 105.0, 123 | 104.0, 124 | 103.0, 125 | 102.0, 126 | 101.0, 127 | 100.0, 128 | 100.0, 129 | 101.0, 130 | 102.0, 131 | 103.0, 132 | 104.0, 133 | 105.0, 134 | 106.0, 135 | 107.0, 136 | 108.0, 137 | 109.0, 138 | 109.5, 139 | 110.0, 140 | 110.0, 141 | 109.5, 142 | 109.0, 143 | 108.0, 144 | 107.0, 145 | 106.0, 146 | 105.0, 147 | 104.0, 148 | 103.0, 149 | 102.0, 150 | 101.0, 151 | 100.0 152 | ] 153 | } 154 | -------------------------------------------------------------------------------- /client/src/test/resources/forecast-client/daily-seasonal-with-trend/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeSeries": [ 3 | 100.0, 4 | 101.1, 5 | 102.2, 6 | 103.3, 7 | 104.4, 8 | 105.5, 9 | 106.6, 10 | 107.7, 11 | 108.8, 12 | 109.9, 13 | 110.5, 14 | 111.1, 15 | 111.2, 16 | 110.8, 17 | 110.4, 18 | 109.5, 19 | 108.6, 20 | 107.7, 21 | 106.8, 22 | 105.9, 23 | 105.0, 24 | 104.1, 25 | 103.2, 26 | 102.3, 27 | 102.4, 28 | 103.5, 29 | 104.6, 30 | 105.7, 31 | 106.8, 32 | 107.9, 33 | 109.0, 34 | 110.1, 35 | 111.2, 36 | 112.3, 37 | 112.9, 38 | 113.5, 39 | 113.6, 40 | 113.2, 41 | 112.8, 42 | 111.9, 43 | 111.0, 44 | 110.1, 45 | 109.2, 46 | 108.3, 47 | 107.4, 48 | 106.5, 49 | 105.6, 50 | 104.7, 51 | 104.8, 52 | 105.9, 53 | 107.0, 54 | 108.1, 55 | 109.2, 56 | 110.3, 57 | 111.4, 58 | 112.5, 59 | 113.6, 60 | 114.7, 61 | 115.3, 62 | 115.9, 63 | 116.0, 64 | 115.6, 65 | 115.2, 66 | 114.3, 67 | 113.4, 68 | 112.5, 69 | 111.6, 70 | 110.7, 71 | 109.8, 72 | 108.9, 73 | 108.0, 74 | 107.1, 75 | 107.2, 76 | 108.3, 77 | 109.4, 78 | 110.5, 79 | 111.6, 80 | 112.7, 81 | 113.8, 82 | 114.9, 83 | 116.0, 84 | 117.1, 85 | 117.7, 86 | 118.3, 87 | 118.4, 88 | 118.0, 89 | 117.6, 90 | 116.7, 91 | 115.8, 92 | 114.9, 93 | 114.0, 94 | 113.1, 95 | 112.2, 96 | 111.3, 97 | 110.4, 98 | 109.5 99 | ], 100 | "horizon": 48, 101 | "expectedConfidence": 0.21, 102 | "allowForecastPercentError": 10.0, 103 | "expectedForecast": [ 104 | 109.6, 105 | 110.7, 106 | 111.8, 107 | 112.9, 108 | 114.0, 109 | 115.1, 110 | 116.2, 111 | 117.3, 112 | 118.4, 113 | 119.5, 114 | 120.1, 115 | 120.7, 116 | 120.8, 117 | 120.4, 118 | 120.0, 119 | 119.1, 120 | 118.2, 121 | 117.3, 122 | 116.4, 123 | 115.5, 124 | 114.6, 125 | 113.7, 126 | 112.8, 127 | 111.9, 128 | 112.0, 129 | 113.1, 130 | 114.2, 131 | 115.3, 132 | 116.4, 133 | 117.5, 134 | 118.6, 135 | 119.7, 136 | 120.8, 137 | 121.9, 138 | 122.5, 139 | 123.1, 140 | 123.2, 141 | 122.8, 142 | 122.4, 143 | 121.5, 144 | 120.6, 145 | 119.7, 146 | 118.8, 147 | 117.9, 148 | 117.0, 149 | 116.1, 150 | 115.2, 151 | 114.3 152 | ] 153 | } 154 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/util/GetTimeSeriesFiles.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.util; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.FileInputStream; 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.io.InputStreamReader; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 18 | 19 | 20 | /** 21 | * Class implementing methods for fetching time series from a file of time 22 | * series file paths. 23 | */ 24 | public final class GetTimeSeriesFiles { 25 | 26 | /** 27 | * Fetch a file of time series files. Each time series is returned in an 28 | * array of time series vectors. The time series vectors are added to the 29 | * array in the order in which their file paths are listed. 30 | * 31 | * @param ts_files File containing time series file paths. 32 | * 33 | * @return An array of time series arrays. 34 | * 35 | * @throws IFSException Thrown if an error is encountered reading the 36 | * values. 37 | */ 38 | public static double[][] getTimeSeriesFiles( 39 | String ts_files 40 | ) throws IFSException { 41 | BufferedReader fin = null; 42 | List tss_l = new ArrayList(); 43 | String line = null; 44 | String ts_file = null; 45 | 46 | try { 47 | 48 | fin = new BufferedReader(new InputStreamReader( 49 | new FileInputStream(ts_files))); 50 | while ((line = fin.readLine()) != null) { 51 | ts_file = line.trim(); 52 | tss_l.add(GetTimeSeries.getTimeSeries(ts_file)); 53 | } 54 | fin.close(); 55 | 56 | } catch (SecurityException ex) { 57 | throw new IFSException("File access error occurred. " 58 | + ex.getMessage()); 59 | } catch (FileNotFoundException ex) { 60 | throw new IFSException("File read error occurred. " 61 | + ex.getMessage()); 62 | } catch (IOException ex) { 63 | throw new IFSException("Unexpected error occurred in reading " 64 | + "time series. " 65 | + ex.getMessage()); 66 | } 67 | 68 | return tss_l.toArray(new double[tss_l.size()][]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/src/test/scala/com/aol/one/reporting/forecastapi/client/ForecastQualityTest.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.aol.one.reporting.forecastapi.client.ITTestUtil._ 10 | import org.scalatest.mockito.MockitoSugar 11 | import org.scalatest.{BeforeAndAfterEach, FreeSpec, Matchers} 12 | 13 | class ForecastQualityTest extends FreeSpec with MockitoSugar with Matchers with BeforeAndAfterEach { 14 | 15 | "ForecastClient.forecast weekly" - { 16 | 17 | "forecast weekly seasonal data with high confidence" in { 18 | testForecast("weekly-seasonal") 19 | } 20 | 21 | "forecast weekly trend data with high confidence" in { 22 | testForecast("weekly-trend") 23 | } 24 | 25 | "forecast weekly seasonal + trend data with high confidence" in { 26 | testForecast("weekly-seasonal-with-trend") 27 | } 28 | 29 | "forecast weekly random data with low confidence" in { 30 | testForecast("weekly-random") 31 | } 32 | } 33 | 34 | "ForecastClient.forecast 2 weeks" - { 35 | 36 | "forecast weekly seasonal data with high confidence" in { 37 | testForecast("two-weeks-seasonal") 38 | } 39 | } 40 | 41 | "ForecastClient.forecast daily" - { 42 | "forecast daily seasonal data with high confidence" in { 43 | testForecast("daily-seasonal") 44 | } 45 | 46 | "forecast daily seasonal + trend data with high confidence" in { 47 | testForecast("daily-seasonal-with-trend") 48 | } 49 | } 50 | 51 | "ForecastClient.forecast yearly" - { 52 | "forecast yearly seasonal data with high confidence" in { 53 | testForecast("yearly-seasonal") 54 | } 55 | 56 | "forecast yearly seasonal + trend data with high confidence" in { 57 | testForecast("yearly-seasonal-with-trend") 58 | } 59 | 60 | "forecast weekly and yearly seasonal + trend data with high confidence" in { 61 | testForecast("weekly-yearly-seasonal-with-trend") 62 | } 63 | 64 | "forecast supply forecast with trend" in { 65 | testForecast("real-data-video-view-supply-with-trend") 66 | } 67 | } 68 | 69 | "ForecastClient.forecast edge case" - { 70 | "forecast insufficient data with low confidence" in { 71 | testForecast("insufficient-data") 72 | } 73 | 74 | "forecast sudden drop with non negative forecast and low confidence" in { 75 | testForecast("sudden-drop") 76 | } 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/model/response/CollectionResponse.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.model.response; 8 | 9 | import com.fasterxml.jackson.annotation.JsonProperty; 10 | import com.wordnik.swagger.annotations.ApiModel; 11 | import com.wordnik.swagger.annotations.ApiModelProperty; 12 | 13 | import java.util.Arrays; 14 | 15 | @ApiModel(value = "Collection Set Response") 16 | public class CollectionResponse implements Comparable { 17 | 18 | @ApiModelProperty(value = "Name of the collection") 19 | private String collectionName; 20 | 21 | @ApiModelProperty(value = "Array of Canned Sets", dataType = "CannedSetResponse[]") 22 | private CannedSetResponse[] cannedSets; 23 | 24 | 25 | public CollectionResponse() { 26 | 27 | } 28 | 29 | public CollectionResponse( 30 | @JsonProperty("collectionName") String collectionName, 31 | @JsonProperty("cannedSets") CannedSetResponse[] cannedSets 32 | 33 | ) { 34 | this.collectionName = collectionName; 35 | this.cannedSets = cannedSets; 36 | } 37 | 38 | public String getCollectionName() { 39 | return collectionName; 40 | } 41 | 42 | public void setCollectionName(String collectionName) { 43 | this.collectionName = collectionName; 44 | } 45 | 46 | public CannedSetResponse[] getCannedSets() { 47 | return cannedSets; 48 | } 49 | 50 | public void setCannedSets(CannedSetResponse[] cannedSets) { 51 | this.cannedSets = cannedSets; 52 | } 53 | 54 | @Override 55 | public boolean equals(Object o) { 56 | if (this == o) return true; 57 | if (o == null || getClass() != o.getClass()) 58 | return false; 59 | 60 | CollectionResponse that = (CollectionResponse) o; 61 | 62 | if (!Arrays.equals(cannedSets, that.cannedSets)) 63 | return false; 64 | if (!collectionName.equals(that.collectionName)) 65 | return false; 66 | 67 | return true; 68 | } 69 | 70 | @Override 71 | public int hashCode() { 72 | int result = collectionName.hashCode(); 73 | result = 31 * result + Arrays.hashCode(cannedSets); 74 | return result; 75 | } 76 | 77 | @Override 78 | public int compareTo(CollectionResponse o) { 79 | return collectionName.compareTo(o.collectionName); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/util/Pair.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.util; 8 | 9 | /** 10 | * Class implementing something similar to C++ pair STL class. 11 | */ 12 | public class Pair { 13 | private A First; 14 | private B Second; 15 | 16 | /** 17 | * Fully specified constructor. 18 | * 19 | * @param first First pair entry. 20 | * @param second Second pair entry. 21 | */ 22 | public Pair( 23 | A first, 24 | B second 25 | ) { 26 | First = first; 27 | Second = second; 28 | } 29 | 30 | /** 31 | * @return Object hash code. 32 | */ 33 | public int hashCode() { 34 | int hash_first = First != null ? First.hashCode() : 0; 35 | int hash_second = Second != null ? Second.hashCode() : 0; 36 | 37 | return (hash_first + hash_second) * hash_second + hash_first; 38 | } 39 | 40 | /* 41 | * (non-Javadoc) 42 | * @see java.lang.Object#equals(java.lang.Object) 43 | */ 44 | public boolean equals( 45 | Object other 46 | ) { 47 | if (other instanceof Pair) { 48 | @SuppressWarnings("unchecked") 49 | Pair other_pair = (Pair)other; 50 | 51 | return 52 | ((First == other_pair.First || 53 | (First != null && other_pair.First != null && 54 | First.equals(other_pair.First))) && 55 | (Second == other_pair.Second || 56 | (Second != null && other_pair.Second != null && 57 | Second.equals(other_pair.Second)))); 58 | } 59 | 60 | return false; 61 | } 62 | 63 | /** 64 | * @return First entry. 65 | */ 66 | public A getFirst() { 67 | return First; 68 | } 69 | 70 | /** 71 | * @return Second entry. 72 | */ 73 | public B getSecond() { 74 | return Second; 75 | } 76 | 77 | /** 78 | * @param first First entry value. 79 | */ 80 | public void setFirst( 81 | A first 82 | ) { 83 | First = first; 84 | } 85 | 86 | /** 87 | * @param second Second entry value. 88 | */ 89 | public void setSecond( 90 | B second 91 | ) { 92 | Second = second; 93 | } 94 | 95 | /** 96 | * @return String representation. 97 | */ 98 | public String toString() { 99 | return "(" + First + ", " + Second + ")"; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/src/main/scala/com/aol/one/reporting/forecastapi/client/ForecastClient.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper} 10 | 11 | trait ForecastClient { 12 | 13 | /** 14 | * Forecast data 15 | * 16 | * @param historical - historical data to base forecasts on 17 | * @param horizon - number of data points to forecast into the future 18 | * @return forecasts and confidence level 19 | */ 20 | def forecast(historical: Array[Double], horizon: Int): Forecast 21 | } 22 | 23 | case class Forecast(values: Array[Double], confidence: Double) 24 | 25 | 26 | /** Forecast client 27 | * 28 | * @see http://service-location:port/forecast-api/doc/OverviewIFS.2015.pdf 29 | */ 30 | class ForecastClientImpl(client: ForecastHttpClient) extends ForecastClient { 31 | 32 | def this(serviceUrl: String) = this(new ForecastHttpClientImpl(serviceUrl)) 33 | 34 | private val objectMapper = new ObjectMapper() 35 | objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 36 | 37 | override def forecast(historical: Array[Double], horizon: Int): Forecast = { 38 | if (historical.isEmpty) { 39 | Forecast(Array.fill(horizon)(0.0), Double.MaxValue) 40 | } else { 41 | val c = confidence(historical) 42 | val forecast = forecastInternal(historical, horizon) 43 | Forecast(forecast, c) 44 | } 45 | } 46 | 47 | private def forecastInternal(historical: Array[Double], horizon: Int): Array[Double] = { 48 | val request = new ForecastRequest(historical, horizon, ForecastParams.CannedSet) 49 | val requestJson = objectMapper.writeValueAsString(request) 50 | val response = client.get(requestJson) 51 | val forecastResponse = objectMapper.readValue(response, classOf[ForecastResponse]) 52 | forecastResponse.forecast 53 | } 54 | 55 | private def confidence(historical: Array[Double]): Double = { 56 | val cHistorical = historical.take(historical.length - ForecastParams.ConfidenceHorizon) 57 | if (cHistorical.length < ForecastParams.MinConfidenceHistorical) { 58 | Double.MaxValue 59 | } else { 60 | val actual = historical.takeRight(ForecastParams.ConfidenceHorizon) 61 | val forecast = forecastInternal(cHistorical, ForecastParams.ConfidenceHorizon) 62 | val errorValues = (actual.map(math.max(_, 1)), forecast).zipped.map((a, f) => math.abs(a - f) / a) 63 | (errorValues.sum / errorValues.length) * 100 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/model/response/CannedSetResponse.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.model.response; 8 | 9 | import com.fasterxml.jackson.annotation.JsonProperty; 10 | import com.wordnik.swagger.annotations.ApiModel; 11 | import com.wordnik.swagger.annotations.ApiModelProperty; 12 | 13 | @ApiModel(value = "Canned Set name and Description") 14 | public class CannedSetResponse implements Comparable { 15 | 16 | @ApiModelProperty(value = "Name of canned set") 17 | private String cannedSetName; 18 | 19 | @ApiModelProperty(value = "Description of the canned set") 20 | private String description; 21 | 22 | 23 | public CannedSetResponse() { 24 | 25 | } 26 | 27 | 28 | public CannedSetResponse( 29 | @JsonProperty("cannedSetName") String cannedSetName, 30 | @JsonProperty("description") String description 31 | ) { 32 | this.cannedSetName = cannedSetName; 33 | this.description = description; 34 | } 35 | 36 | public String getCannedSetName() { 37 | return cannedSetName; 38 | } 39 | 40 | 41 | public void setCannedSetName(String cannedSetName) { 42 | this.cannedSetName = cannedSetName; 43 | } 44 | 45 | public String getDescription() { 46 | return description; 47 | } 48 | 49 | public void setDescription(String description) { 50 | this.description = description; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | StringBuilder sb = new StringBuilder(); 56 | sb.append("CannedSetResponse [ cannedSetName : ").append(cannedSetName); 57 | sb.append(" description :").append(description); 58 | return sb.toString(); 59 | } 60 | 61 | @Override 62 | public boolean equals(Object o) { 63 | if (this == o) return true; 64 | if (o == null || getClass() != o.getClass()) return false; 65 | 66 | CannedSetResponse that = (CannedSetResponse) o; 67 | 68 | if (cannedSetName != null ? !cannedSetName.equals(that.cannedSetName) : that.cannedSetName != null) 69 | return false; 70 | if (description != null ? !description.equals(that.description) : that.description != null) return false; 71 | 72 | return true; 73 | } 74 | 75 | @Override 76 | public int hashCode() { 77 | int result = cannedSetName != null ? cannedSetName.hashCode() : 0; 78 | result = 31 * result + (description != null ? description.hashCode() : 0); 79 | return result; 80 | } 81 | 82 | @Override 83 | public int compareTo(CannedSetResponse o) { 84 | return cannedSetName.compareTo(o.cannedSetName); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/jpe/gw/GWInterface.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.jpe.gw; 8 | 9 | import java.io.PrintStream; 10 | 11 | /** 12 | * A class wanting to use grid walk algorithm (GW) optimization capabilities 13 | * must implement this interface to allow the GW to determine when optimal 14 | * weights have been identified. 15 | * 16 | * @author Copyright © 2012 John Eldreth All rights reserved. 17 | */ 18 | public interface GWInterface { 19 | 20 | /** 21 | * Fetch number of optimization accuracy levels. 22 | * 23 | * @return Number of optimization accuracy levels. 24 | */ 25 | public int getNumLevels(); 26 | 27 | /** 28 | * Fetch number of weights to walk. 29 | * 30 | * @return Number of weights. 31 | */ 32 | public int getNumWeights(); 33 | 34 | /** 35 | * This method is given a set of weights and it is up to this method 36 | * to determine its rating (noting the smaller the rating, the higher 37 | * the likelihood the weights are optimal). 38 | * 39 | * @param weights Weight vector to rate. 40 | * 41 | * @return Weight vector rating. 42 | */ 43 | public double getRating(double[] weights); 44 | 45 | /** 46 | * Fetch step size for an optimization accuracy level and a weight index. 47 | * 48 | * @param level Optimization accuracy level (0-based). 49 | * @param weight_idx Weight index. 50 | * 51 | * @return Optimization step size. 52 | */ 53 | public double getStepSize(int level, int weight_idx); 54 | 55 | /** 56 | * Fetch lower bound for a weight index. 57 | * 58 | * @param weight_idx Weight index. 59 | * 60 | * @return Weight lower bound. 61 | */ 62 | public double getWeightLowerBound(int weight_idx); 63 | 64 | /** 65 | * Fetch upper bound for a weight index. 66 | * 67 | * @param weight_idx Weight index. 68 | * 69 | * @return Weight upper bound. 70 | */ 71 | public double getWeightUpperBound(int weight_idx); 72 | 73 | /** 74 | * Print trace information. 75 | * 76 | * @param trace_out Where to print trace info if trace is enabled. 77 | * @param iteration Optimization iteration. Can be 0 on initialization. 78 | * @param level Optimization accuracy level (0-based). 79 | * @param weight_idx Weight index. Can be -1 when no specific weight is 80 | * being adjusted. 81 | * @param weights Weight vector. 82 | * @param rating Weight vector rating. 83 | */ 84 | public void printTrace(PrintStream trace_out, int iteration, int level, int weight_idx, 85 | double[] weights, double rating); 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/VerizonAdPlatforms/aol-on-forecast.svg?branch=master)](https://travis-ci.org/OathAdPlatforms/aol-on-forecast) 2 | 3 | ## Forecast API 4 | 5 | Forecast API is a REST service for making time-series forecasting. 6 | It is suitable for making forecasts that exhibit daily, weekly and yearly 7 | seasonalities. Trends through time can also be detected. For example you 8 | can use it to forecast number of webpage views for the coming week given 9 | data for the past month. 10 | 11 | ## Quickstart 12 | 13 | Run the forecast-api docker image and make a rest call to get forecasts 14 | 15 | docker run --name forecast-api -p=9072:8080 vidible/forecast-api:2.0.3 16 | curl -X POST -H "Content-Type: application/json" -d '{ "timeSeries": [ 0, 1, 2, 3, 2, 1, 0, 0, 1, 2, 3, 2, 1, 0, 0 ], "numberForecasts": 7 }' "http://localhost:9072/forecast-api/forecast" 17 | 18 | Expected response 19 | 20 | { 21 | "forecast" : [ 1.0, 2.0, 3.0, 2.0, 1.0, 0.0, 0.0 ], 22 | "selectedCannedSet" : "RW-NONE-WEEK", 23 | "time" : 68 24 | } 25 | 26 | ## Example scenarios 27 | 28 | Here are some forecast scenarios. Solid line shows historical data and dotted lines are forecasts. 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ## Java client 37 | 38 | Forecast-API comes with a java based client. It should be straightforward 39 | to write a simple REST client for other languages. 40 | 41 | ##### build.sbt 42 | 43 | ```scala 44 | "com.aol.one.reporting" % "forecast-api-client" % INSERT_LATEST_VERSION 45 | ``` 46 | 47 | ##### pom.xml 48 | ```xml 49 | 50 | com.aol.one.reporting 51 | forecast-api-client_2.11 52 | INSERT_LATEST_VERSION 53 | 54 | ``` 55 | 56 | ##### Usage 57 | 58 | Example below provides a timeseries with 14 data points and requests forecast for the next 7 data points: 59 | 60 | ```scala 61 | val client = new ForecastClientImpl("http://localhost:9072/forecast-api/forecast") 62 | val forecast = client.forecast(Array(1, 2, 3, 4, 3, 2, 1, 1, 2, 3, 4, 3, 2, 1), 7) 63 | ``` 64 | 65 | ## Docs 66 | - Forecast API makes use of a few algorithms including ARIMA, Regression 67 | and exponential smoothing. Head over to the [wiki](https://github.com/vidible/aol-on-forecast/wiki) 68 | to learn more. 69 | - Swagger docs can be found at http://localhost:9072/forecast-api. 70 | 71 | ## Build from source 72 | 73 | Server: 74 | 75 | cd server 76 | mvn install 77 | 78 | Client: 79 | 80 | cd client 81 | sbt compile 82 | 83 | 84 | ## Contributors 85 | 86 | - Alexey Lipodat 87 | - Paul Eldreth 88 | - Sergey Likhoman 89 | - Terry Choi 90 | - Tilaye Y. Alemu 91 | - Venkata Vittala 92 | 93 | ## License 94 | Forecast API is released under the Apache License, Version 2.0 95 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSSpikeFilter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | import com.aol.one.reporting.forecastapi.server.jpe.spikesmooth.SpikeSmooth; 10 | 11 | /** 12 | * Class for processing the spike_filter common model parameter. 13 | */ 14 | public final class IFSSpikeFilter { 15 | public final static int Disable = 0; 16 | public final static int MinClippingWindow = 3; 17 | public final static int MaxClippingWindow = 30; 18 | 19 | /** 20 | * Fetch spike filtered series with a string clipping_window parameter. 21 | * Same as the integer clipping_window version except string is used 22 | * allowing a model parameter value to be specified directly. 23 | * 24 | * @see getSpikeFilteredSeries(double[] series, int clipping_window). 25 | */ 26 | public static double[] getSpikeFilteredSeries( 27 | double[] series, 28 | String clipping_window 29 | ) throws IFSException { 30 | int i_clipping_window = 0; 31 | 32 | try { 33 | i_clipping_window = Integer.parseInt(clipping_window); 34 | } 35 | catch (NumberFormatException ex) { 36 | throw new IFSException(40); 37 | } 38 | 39 | return(getSpikeFilteredSeries(series, i_clipping_window)); 40 | } 41 | 42 | /** 43 | * Fetch spike filtered series with an integer clipping_window parameter. 44 | * 45 | * @param series Time series to filter. 46 | * @param clipping_window Size of clipping window. A value of 0 disables 47 | * the filter (i.e. original series is returned). Otherwise clipping 48 | * window is expected to be in the range 3 to 30 inclusive. 49 | * 50 | * @return Original series or filtered series depending on clipping_window 51 | * value. 52 | * 53 | * @throws IFSException Thrown for invalid clipping window specifications 54 | * or an unexpected error occurred. 55 | */ 56 | public static double[] getSpikeFilteredSeries( 57 | double[] series, 58 | int clipping_window 59 | ) throws IFSException { 60 | if (series == null 61 | || series.length < MinClippingWindow 62 | || clipping_window == Disable) 63 | return(series); 64 | 65 | if (clipping_window != Disable 66 | && !(MinClippingWindow <= clipping_window 67 | && clipping_window <= MaxClippingWindow)) 68 | throw new IFSException(41); 69 | 70 | double[] filtered_series 71 | = SpikeSmooth.smoothSpike(series, clipping_window); 72 | if (filtered_series == null) 73 | throw new IFSException(42); 74 | 75 | return(filtered_series); 76 | } 77 | 78 | /** 79 | * Canned usage information for spike_filter parameter. 80 | * 81 | * @return Usage string. 82 | */ 83 | public static String usage() { 84 | return( 85 | "\n" 86 | + "spike_filter=<0 | [3,30]>\n" 87 | + " 0 -- Disable spike filtering. Time series returned is not changed.\n" 88 | + " [3,30] -- Enable spike filtering with the specified clipping\n" 89 | + " window size. The minimum size is 3 and the maximum is 30.\n" 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/cs/GetCannedSetCandidates.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.cs; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.FileInputStream; 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.io.InputStreamReader; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 19 | 20 | /** 21 | * Class implementing methods for fetching canned set candidates from a file. 22 | */ 23 | public final class GetCannedSetCandidates { 24 | 25 | /** 26 | * Fetch the canned set candidates from the designated file. The file 27 | * contains a list of canned set ids (one id per line). Each canned set 28 | * id is compared against a collection of canned set definitions to arrive 29 | * at a list of candidate canned sets. The list of canned set ids has the 30 | * following format: 31 | * 32 | * 33 | * ... 34 | * 35 | * 36 | * @param canned_set_candidate_file File containing canned set ids. 37 | * @param canned_set_definitions Collection of canned sets to reference. 38 | * 39 | * @return List of candidate canned sets. 40 | * 41 | * @throws IFSException Thrown if canned set ids have an unexpected format 42 | * or any of the canned set ids cannot be found in the canned set 43 | * collection. 44 | */ 45 | @SuppressWarnings("resource") 46 | public static List getCannedSetCandidates( 47 | String canned_set_candidate_file, 48 | Map canned_set_definitions 49 | ) throws IFSException { 50 | BufferedReader in = null; 51 | String line = null; 52 | List candidates = new ArrayList(); 53 | String canned_set_id = null; 54 | IFSCannedSet canned_set = null; 55 | int line_num = 0; 56 | 57 | try { 58 | in = new BufferedReader(new InputStreamReader( 59 | new FileInputStream(canned_set_candidate_file))); 60 | while ((line = in.readLine()) != null) { 61 | line_num++; 62 | canned_set_id = line.trim(); 63 | canned_set = canned_set_definitions.get(canned_set_id); 64 | if (canned_set == null) 65 | throw new IFSException(String.format("Candidate canned set id " 66 | + "'%s' at line %d is not a canned set definition.", 67 | canned_set_id, line_num)); 68 | candidates.add(canned_set); 69 | } 70 | in.close(); 71 | } catch (FileNotFoundException ex) { 72 | throw new IFSException(String.format("Could not read '%s': %s", 73 | canned_set_candidate_file, ex.getMessage())); 74 | } catch (SecurityException ex) { 75 | throw new IFSException(String.format("Could not read '%s': %s", 76 | canned_set_candidate_file, ex.getMessage())); 77 | } catch (IOException ex) { 78 | throw new IFSException(String.format("Could not read '%s': %s", 79 | canned_set_candidate_file, ex.getMessage())); 80 | } 81 | 82 | return candidates; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/alg/IFSModelImplRW.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.alg; 8 | 9 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 10 | import com.aol.one.reporting.forecastapi.server.models.model.IFSModel; 11 | import com.aol.one.reporting.forecastapi.server.models.model.IFSParameterValue; 12 | import com.aol.one.reporting.forecastapi.server.models.model.IFSUsageDescription; 13 | 14 | import java.util.List; 15 | 16 | /** 17 | * Forecasting model that implements a random walk with an optional cycle. 18 | * Basically the most recent specified number if values are copied forward 19 | * as the forecast. If the series is less than the specified number of values 20 | * in length, the most recent value is copied forward. The model supports the 21 | * following specific parameters: 22 | * 23 | * cycle -- Number of most recent points to copy forward. If there are fewer 24 | * fewer points, the most recent value will be copied forward. If set 25 | * to -1, the cycle, if any, is set automatically. 26 | * 27 | */ 28 | public final class IFSModelImplRW extends IFSModel { 29 | public static final String ModelName = "model_rw"; 30 | private static final IFSUsageDescription UsageDescription 31 | = new IFSUsageDescription( 32 | "Forecast model that implements a random walk with an optional cycle.\n", 33 | "The model is implemented by copying forward the most recent specified\n" 34 | + "number of values. If the series is less than the specified number of\n" 35 | + "values in length, the most recent value is copied forward.\n", 36 | "\n"); 37 | 38 | /* Implements execModel method. 39 | * 40 | * @see com.aol.ifs.soa.common.IFSModel#execModel(double[]) 41 | */ 42 | @Override 43 | protected String execModel( 44 | double[] series, 45 | double[] forecasts, 46 | int cycle 47 | ) throws IFSException { 48 | if (series.length < cycle) 49 | for (int i = 0; i < forecasts.length; i++) 50 | forecasts[i] = series[series.length-1]; 51 | else { 52 | if(cycle==0) 53 | cycle=1; 54 | for (int i = 0; i < forecasts.length; i++) 55 | forecasts[i] = series[series.length - cycle + i % cycle]; 56 | } 57 | return String.format("rw::cycle:%d", cycle); 58 | } 59 | 60 | /* Implements getModelName method. 61 | * 62 | * @see com.aol.ifs.soa.common.IFSModelInterface#getModelName() 63 | */ 64 | @Override 65 | public String getModelName() { 66 | return ModelName; 67 | } 68 | 69 | /* Implements getUsage method. 70 | * 71 | * @see com.aol.ifs.soa.common.IFSModelInterface#getUsage() 72 | */ 73 | @Override 74 | public IFSUsageDescription getUsage() { 75 | return UsageDescription; 76 | } 77 | 78 | /* Implements injectParameters method. 79 | * 80 | * @see com.aol.ifs.soa.common.IFSModel#injectParameters(List) 81 | */ 82 | @Override 83 | protected void injectParameters( 84 | List parameters 85 | ) throws IFSException { 86 | if (parameters != null && parameters.size() > 0) 87 | for (IFSParameterValue parameter : parameters) 88 | throw new IFSException(23, getModelName(), 89 | parameter.getParameter()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/metrics/MetricsContextListener.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.metrics; 8 | 9 | import com.codahale.metrics.JmxReporter; 10 | import com.codahale.metrics.MetricRegistry; 11 | import com.codahale.metrics.servlets.AdminServlet; 12 | import com.codahale.metrics.servlets.HealthCheckServlet; 13 | import com.codahale.metrics.servlets.MetricsServlet; 14 | import com.codahale.metrics.servlets.MetricsServlet.ContextListener; 15 | import com.codahale.metrics.servlets.PingServlet; 16 | import com.codahale.metrics.servlets.ThreadDumpServlet; 17 | 18 | import javax.servlet.ServletContextEvent; 19 | import javax.servlet.annotation.WebListener; 20 | import javax.servlet.annotation.WebServlet; 21 | 22 | 23 | /** 24 | * Servlet context listener to make the metrics registry for this app available to the Metrics 25 | * Servlet provided by the Metrics library. 26 | */ 27 | @WebListener("Servlet Context Listener that creates global metrics registry") 28 | public class MetricsContextListener extends ContextListener { 29 | 30 | private final static MetricRegistry METRICS = new MetricRegistry(); 31 | JmxReporter reporter = null; 32 | 33 | /** 34 | * Gets the metrics. 35 | * 36 | * @return the metrics 37 | */ 38 | public static MetricRegistry getMetrics() { 39 | return METRICS; 40 | } 41 | 42 | 43 | /* 44 | * (non-Javadoc) 45 | * 46 | * @see com.codahale.metrics.servlets.MetricsServlet.ContextListener#getMetricRegistry() 47 | */ 48 | @Override 49 | protected MetricRegistry getMetricRegistry() { 50 | return METRICS; 51 | } 52 | 53 | @Override 54 | public void contextDestroyed(ServletContextEvent sce) { 55 | super.contextDestroyed(sce); 56 | this.reporter.stop(); 57 | this.reporter = null; 58 | } 59 | 60 | @Override 61 | public void contextInitialized(ServletContextEvent sce) { 62 | super.contextInitialized(sce); 63 | 64 | // start up Metrics JMX Reporter 65 | this.reporter = JmxReporter.forRegistry(getMetrics()).build(); 66 | this.reporter.start(); 67 | } 68 | 69 | @WebServlet(urlPatterns = {"/admin/*"}, loadOnStartup = 1) 70 | public static class MetricsAdminServlet extends AdminServlet { 71 | private static final long serialVersionUID = 1L; 72 | 73 | } 74 | 75 | @WebServlet(urlPatterns = {"/admin/ping/*"}, loadOnStartup = 1) 76 | public static class MetricsPingServlet extends PingServlet { 77 | private static final long serialVersionUID = 1L; 78 | 79 | } 80 | 81 | @WebServlet(urlPatterns = {"/admin/healthcheck/*"}, loadOnStartup = 1) 82 | public static class MetricsHealthCheckServlet extends HealthCheckServlet { 83 | private static final long serialVersionUID = 1L; 84 | 85 | } 86 | 87 | @WebServlet(urlPatterns = {"/admin/metrics/*"}, loadOnStartup = 1) 88 | public static class MetricsMetricsServlet extends MetricsServlet { 89 | private static final long serialVersionUID = 1L; 90 | 91 | } 92 | 93 | @WebServlet(urlPatterns = {"/admin/threads/*"}, loadOnStartup = 1) 94 | public static class MetricsThreadsServlet extends ThreadDumpServlet { 95 | private static final long serialVersionUID = 1L; 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/model/response/ForecastResponse.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | package com.aol.one.reporting.forecastapi.server.model.response; 7 | 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.wordnik.swagger.annotations.ApiModel; 10 | import com.wordnik.swagger.annotations.ApiModelProperty; 11 | 12 | import java.util.Arrays; 13 | 14 | @ApiModel(value = "Forecast response object including selected CannedSet and time to respond") 15 | public class ForecastResponse { 16 | 17 | @ApiModelProperty(value = "Real numbers from nearest to farthest", required = true) 18 | private double[] forecast; 19 | 20 | @ApiModelProperty(value = "Name of canned set used to produce forecast") 21 | private String selectedCannedSet; 22 | 23 | @ApiModelProperty(value = "Number of milliseconds to to produce forecast", required = true) 24 | private long time; 25 | 26 | public ForecastResponse() { 27 | 28 | } 29 | 30 | 31 | public ForecastResponse( 32 | @JsonProperty("forecast") double[] forecast, 33 | @JsonProperty("selectedCannedSet") String selectedCannedSet, 34 | @JsonProperty("time") long time 35 | ) { 36 | this.forecast = forecast; 37 | this.selectedCannedSet = selectedCannedSet; 38 | this.time = time; 39 | } 40 | 41 | public double[] getForecast() { 42 | return forecast; 43 | } 44 | 45 | public void setForecast(double[] forecast) { 46 | this.forecast = forecast; 47 | } 48 | 49 | 50 | public String getSelectedCannedSet() { 51 | return selectedCannedSet; 52 | } 53 | 54 | public void setSelectedCannedSet(String selectedCannedSet) { 55 | this.selectedCannedSet = selectedCannedSet; 56 | } 57 | 58 | public long getTime() { 59 | return time; 60 | } 61 | 62 | public void setTime(long time) { 63 | this.time = time; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | StringBuilder sb = new StringBuilder(); 69 | boolean first = true; 70 | sb.append("Forecast : ["); 71 | for (double val : forecast) { 72 | if (first) { 73 | first = false; 74 | sb.append(String.format("%12f", val)); 75 | } else { 76 | sb.append(", ").append(String.format("%12f", val)); 77 | } 78 | } 79 | sb.append("], Selected Canned Set :") 80 | .append(selectedCannedSet) 81 | .append(String.format(", Elapsed Millis : %10d", time)); 82 | return sb.toString(); 83 | } 84 | 85 | @Override 86 | public boolean equals(Object o) { 87 | if (this == o) return true; 88 | if (o == null || getClass() != o.getClass()) return false; 89 | 90 | ForecastResponse that = (ForecastResponse) o; 91 | 92 | if (time != that.time) return false; 93 | if (!Arrays.equals(forecast, that.forecast)) return false; 94 | if (selectedCannedSet != null ? !selectedCannedSet.equals(that.selectedCannedSet) : that.selectedCannedSet != null) 95 | return false; 96 | 97 | return true; 98 | } 99 | 100 | @Override 101 | public int hashCode() { 102 | int result = forecast != null ? Arrays.hashCode(forecast) : 0; 103 | result = 31 * result + (selectedCannedSet != null ? selectedCannedSet.hashCode() : 0); 104 | result = 31 * result + (int) (time ^ (time >>> 32)); 105 | return result; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/src/test/scala/com/aol/one/reporting/forecastapi/client/ITTestUtil.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.fasterxml.jackson.annotation.JsonProperty 10 | import com.fasterxml.jackson.databind.ObjectMapper 11 | import org.scalatest.FreeSpec 12 | 13 | object ITTestUtil extends FreeSpec { 14 | 15 | private val forecastClient = new ForecastClientImpl(serviceUrl) 16 | private val plotActual = ConfigUtil.getConfig().getBoolean("plot-actual") 17 | 18 | def testForecast(scenarioId: String, plotIfEnabled: Boolean = true): Unit = { 19 | val scenario = getTestCase(scenarioId + "/test.json") 20 | 21 | val result = forecastClient.forecast(scenario.timeSeries, scenario.horizon) 22 | 23 | // empty forecast in test files signals not to check forecasts for the test case 24 | if (!scenario.expectedForecast.isEmpty) { 25 | assertForecast(scenario.expectedForecast, result.values, scenario.allowedError) 26 | if (plotIfEnabled && plotActual) { 27 | plotForecast(scenarioId, scenario, result) 28 | } 29 | } 30 | assertConfidence(scenario.expectedConfidence, result.confidence) 31 | } 32 | 33 | private def assertConfidence(expected: Double, actual: Double) = { 34 | // confidence of -1 in the test files signals high confidence value 35 | if (expected == -1.0) { 36 | assert(actual > 50.0) 37 | } else { 38 | assert(actual <= expected) 39 | } 40 | } 41 | 42 | private def assertForecast(expected: Array[Double], actual: Array[Double], allowedError: Double) = { 43 | org.junit.Assert.assertEquals(expected.length, actual.length) 44 | (expected, actual).zipped.foreach((e, a) => { 45 | val forecastError = 100 * math.abs(e - a) / math.max(a, 0.00001) 46 | try { 47 | assertDouble(forecastError, 0.0, allowedError) 48 | } catch { 49 | case ex: AssertionError => 50 | throw new AssertionError(s"Expected: $e actual: $a allowed error: $allowedError" 51 | + "\nExpected full: " + expected.map(_.toInt).mkString(",") 52 | + "\nActual full: " + actual.map(_.toInt).mkString(","), ex) 53 | } 54 | }) 55 | } 56 | 57 | private def assertDouble(expected: Double, actual: Double, threshold: Double): Unit = { 58 | org.junit.Assert.assertEquals(expected, actual, threshold) 59 | } 60 | 61 | private def getResourceFile(file: String) = getClass.getClassLoader.getResourceAsStream(file) 62 | 63 | private def getTestCase(testCaseFile: String): TestCase = { 64 | new ObjectMapper().readValue(getResourceFile("forecast-client/" + testCaseFile), classOf[TestCase]) 65 | } 66 | 67 | private def plotForecast(scenarioId: String, scenario: TestCase, forecast: Forecast) = { 68 | val historical = scenario.timeSeries.mkString(",") 69 | val actual = forecast.values.mkString(",") 70 | sys.process.Process(Seq("ls"), new java.io.File("./src/test/resources/scripts")).!! 71 | 72 | sys.process.Process(Seq("python", "plot_results.py", scenarioId, historical, actual), new java.io.File("./src/test/resources/scripts")).!! 73 | } 74 | 75 | private def serviceUrl = sys.env("FORECAST_API_SERVICE_URL") 76 | 77 | case class TestCase(@JsonProperty("timeSeries") timeSeries: Array[Double], 78 | @JsonProperty("horizon") horizon: Int, 79 | @JsonProperty("seasonality") seasonality: String, 80 | @JsonProperty("allowForecastPercentError") allowedError: Double, 81 | @JsonProperty("expectedForecast") expectedForecast: Array[Double], 82 | @JsonProperty("expectedConfidence") expectedConfidence: Double) 83 | 84 | } 85 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/cs/IFSCannedSetSelection.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.cs; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import com.aol.one.reporting.forecastapi.server.models.model.IFSDetectSeasonalCycle; 13 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 14 | import com.aol.one.reporting.forecastapi.server.models.model.IFSSpikeFilter; 15 | import com.aol.one.reporting.forecastapi.server.models.model.IFSStatistics; 16 | 17 | /** 18 | * Class implementing canned set selection. Given canned set constraints 19 | * and context, the appropriate canned set is selected. 20 | */ 21 | public final class IFSCannedSetSelection { 22 | 23 | /** 24 | * Given constraints and context, select a canned set. 25 | * 26 | * @param constraints Canned set selection constraints. 27 | * @param context Canned set selection context. 28 | * 29 | * @return Selected canned set. 30 | * 31 | * @throws IFSException thrown if constraints or context are improperly 32 | * specified. 33 | */ 34 | public static IFSCannedSet selectCannedSet( 35 | IFSCannedSetSelectionConstraints constraints, 36 | IFSCannedSetSelectionContext context 37 | ) throws IFSException { 38 | if (constraints == null) 39 | throw new IFSException(64); 40 | if (context == null) 41 | throw new IFSException(65); 42 | 43 | List canned_set_list = new ArrayList(); 44 | 45 | if (context.getSeries().length <= constraints.getNumPointsNewUB()) { 46 | canned_set_list.add(constraints.getCannedSetNoneNew()); 47 | canned_set_list.add(constraints.getCannedSetWeekNew()); 48 | } else if (constraints.getProfitCentersDecline().contains(context.getProfitCenter())) { 49 | canned_set_list.add(constraints.getCannedSetDecline()); 50 | } else if (context.getSeries().length > constraints.getNumPointsYearLB() 51 | && round(IFSStatistics.getACF(context.getSeriesLast(constraints.getNumPointsYearLB()), 52 | true, constraints.getLagYear(), constraints.getLagYear())[0]) 53 | >= constraints.getACFYearLB()) { 54 | for (IFSCannedSet canned_set : context.getCannedSetCandidates()) 55 | if (canned_set.getName().toLowerCase().indexOf("-year") >= 0) 56 | canned_set_list.add(canned_set); 57 | if (canned_set_list.isEmpty()) 58 | throw new IFSException(66, context.getID()); 59 | } else if (IFSDetectSeasonalCycle.getSeasonalCycle( 60 | IFSSpikeFilter.getSpikeFilteredSeries(context.getSeries(), 61 | constraints.getSpikeFilterWindow())) > 1) { 62 | for (IFSCannedSet canned_set : context.getCannedSetCandidates()) 63 | if (canned_set.getName().toLowerCase().endsWith("-auto")) 64 | canned_set_list.add(canned_set); 65 | if (canned_set_list.isEmpty()) 66 | throw new IFSException(67, context.getID()); 67 | } else { 68 | for (IFSCannedSet canned_set : context.getCannedSetCandidates()) 69 | if (!canned_set.getName().toLowerCase().endsWith("-auto") 70 | && !canned_set.getName().toLowerCase().endsWith("-year")) 71 | canned_set_list.add(canned_set); 72 | if (canned_set_list.isEmpty()) 73 | throw new IFSException(68, context.getID()); 74 | } 75 | 76 | return IFSCannedSetCompetition.competeCannedSets(context, canned_set_list); 77 | } 78 | 79 | /*******************/ 80 | /* Private Methods */ 81 | /*******************/ 82 | 83 | /** 84 | * Round value to nearest 10^-2 value. 85 | * 86 | * @param value Value to round. 87 | * 88 | * @return Value rounded to 10^-2. 89 | */ 90 | private static double round( 91 | double value 92 | ) { 93 | return (double)Math.round(100.0*value) / 100.0; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /server/src/main/webapp/doc/lib/jquery.ba-bbq.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010 3 | * http://benalman.com/projects/jquery-bbq-plugin/ 4 | * 5 | * Copyright (c) 2010 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | (function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this); -------------------------------------------------------------------------------- /server/src/main/resources/schema/creative.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "description": "A creative", 4 | "type": "object", 5 | "properties": { 6 | "ad_format_id": { "type": "integer", "description": "Ad Format ID", "title": "Ad Format ID", "default": 0}, 7 | "ad_title": { "type": "string", "description": "Ad Title", "title": "Ad Title" }, 8 | "adaptv_cdn_size_in_kb": { "type": "integer", "description": "Adap.tv CDN Size In KB", "title": "Adap.tv CDN Size In KB", "required": "true", "default": 0}, 9 | "advertiser_id": { "type": "number", "description": "Advertiser ID", "title": "Advertiser ID"}, 10 | "brand_id": { "type": "number", "description": "Brand ID", "title": "Brand ID"}, 11 | "campaign_id": { "type": "integer", "description": "Campaign ID", "title": "Campaign ID"}, 12 | "click_through_url": { "type": "string", "description": "Click Through URL", "title": "Click Through URL"}, 13 | "companion_height": { "type": "integer", "description": "Companion Height", "title": "Companion Height", "default": -2}, 14 | "companion_width": { "type": "integer", "description": "Companion Width", "title": "Companion Width", "default": -2}, 15 | "created_at": { "type": "string", "format": "date-time", "description": "Created At", "title": "Created At", "required": "true"}, 16 | "creative_plugin_properties": { "type": "string", "description": "Creative Plugin Properties", "title": "Creative Plugin Properties"}, 17 | "description": { "type": "string", "description": "Description", "title": "Description"}, 18 | "duration": { "type": "integer", "description": "Duration", "title": "Duration", "default": 0}, 19 | "external_id": { "type": "string", "description": "External ID", "title": "External ID", "required": "true", "default": ""}, 20 | "first_submission_date": { "type": "string", "format": "date-time", "description": "First Submission Date", "title": "First Submission Date"}, 21 | "height": { "type": "integer", "description": "Height", "title": "Height", "default": 0}, 22 | "id": { "type": "integer", "description": "ID", "title": "ID", "required": "true"}, 23 | "is_third_party_served": { "type": "integer", "description": "Is Third Party Served", "title": "Is Third Party Served", "required": "true", "default": 0}, 24 | "is_tps_skippable": { "type": "integer", "description": "Is TPS Skippable", "title": "Is TPS Skippable", "required": "true", "default": 0}, 25 | "is_valid_plugin_properties": { "type": "integer", "description": "Is Valid Plugin Properties", "title": "Is Valid Plugin Properties", "required": "true", "default": 0}, 26 | "name": { "type": "string", "description": "Name", "title": "Name", "required": "true", "default": ""}, 27 | "organization_id": { "type": "integer", "description": "Organization ID", "title": "Organization ID", "required": "true", "default": 0}, 28 | "reviewed_by_ad_ops": { "type": "integer", "description": "Reviewed By Ad Ops", "title": "Reviewed By Ad Ops", "default": 0}, 29 | "reviewed_on_time": { "type": "integer", "description": "Reviewed On Time", "title": "Reviewed On Time", "default": 0}, 30 | "reviewer": { "type": "string", "description": "Reviewer", "title": "Reviewer"}, 31 | "skippable": {"type": "string", "description": "Skippable", "title": "Skippable", "enum": ["ONLY_WHEN_MANDATORY", "WHENEVER_POSSIBLE", "NEVER"], "required": "true", "default": "NEVER"}, 32 | "status": { "type": "integer", "description": "Status", "title": "Status", "required": "true", "default": 0}, 33 | "survey_label": { "type": "string", "description": "Survey Label", "title": "Survey Label"}, 34 | "survey_url": { "type": "string", "description": "Survey URL", "title": "Survey URL"}, 35 | "template_type": { "type": "integer", "description": "Template Type", "title": "Template Type"}, 36 | "updated_at": { "type": "string", "format": "date-time", "description": "Updated At", "title": "Updated At", "required": "true"}, 37 | "vertical_id": { "type": "integer", "description": "Vertical ID", "title": "Vertical ID"}, 38 | "width": { "type": "integer", "description": "Width", "title": "Width", "default": 0} 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/util/GetTimeSeries.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.util; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.FileInputStream; 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.io.InputStreamReader; 14 | import java.util.ArrayList; 15 | 16 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 17 | 18 | 19 | /** 20 | * Class supporting the reading of a time series from a file or standard 21 | * input. If a "--" is provided for the file name, the time series is read 22 | * from standard input. There is expected one value of the time series per 23 | * line in the file. Also the ordering is from least recent to most recent 24 | * value. 25 | */ 26 | public final class GetTimeSeries { 27 | 28 | /** 29 | * Fetch a time series from a file or standard input. The series is 30 | * returned in a vector unless an error is encountered. 31 | * 32 | * @param input The file path or "--" if the values are from standard input. 33 | * 34 | * @return A vector containing the time series. 35 | * 36 | * @throws IFSException Thrown if an error is encountered reading the 37 | * values. 38 | */ 39 | public static double[] getTimeSeries( 40 | String input 41 | ) throws IFSException { 42 | 43 | // Fetch the time series from a file or standard input. 44 | 45 | ArrayList time_series = null; 46 | double value = 0.0; 47 | BufferedReader in = null; 48 | String line = null; 49 | double[] ts = null; 50 | int i = 0; 51 | 52 | try { 53 | time_series = new ArrayList(); 54 | in = getFile(input); 55 | while ((line = in.readLine()) != null) { 56 | try { 57 | value = Double.parseDouble(line); 58 | time_series.add(value); 59 | } 60 | catch (NumberFormatException ex) { 61 | throw new IFSException("Time series value '" 62 | + line 63 | + "' not a number. " 64 | + ex.getMessage()); 65 | } 66 | } 67 | if (!input.equals("--")) 68 | in.close(); 69 | } 70 | catch (SecurityException ex) { 71 | throw new IFSException("File access error occurred. " 72 | + ex.getMessage()); 73 | } 74 | catch (FileNotFoundException ex) { 75 | throw new IFSException("File read error occurred. " 76 | + ex.getMessage()); 77 | } 78 | catch (IOException ex) { 79 | throw new IFSException("Unexpected error occurred in reading " 80 | + "time series. " 81 | + ex.getMessage()); 82 | } 83 | i = 0; 84 | ts = new double[time_series.size()]; 85 | for (Double ts_value : time_series) 86 | ts[i++] = ts_value.doubleValue(); 87 | 88 | return ts; 89 | } 90 | 91 | /*******************/ 92 | /* Private Methods */ 93 | /*******************/ 94 | 95 | /** 96 | * Set up a buffered reader for a time series file or standard input. 97 | * 98 | * @param file_name Time series file or -- for standard input. 99 | * 100 | * @throws FileNotFoundException 101 | * @throws SecurityException 102 | */ 103 | private static BufferedReader getFile( 104 | String file_name 105 | ) throws FileNotFoundException, SecurityException { 106 | BufferedReader in = null; 107 | 108 | if (file_name.equals("--")) { 109 | in = new BufferedReader(new InputStreamReader(System.in)); 110 | return(in); 111 | } 112 | 113 | in = new BufferedReader(new InputStreamReader( 114 | new FileInputStream(file_name))); 115 | return(in); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/alg/IFSModelImplMovAvg.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.alg; 8 | 9 | import java.util.List; 10 | 11 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 12 | import com.aol.one.reporting.forecastapi.server.models.model.IFSModel; 13 | import com.aol.one.reporting.forecastapi.server.models.model.IFSParameterValue; 14 | import com.aol.one.reporting.forecastapi.server.models.model.IFSUsageDescription; 15 | 16 | /** 17 | * Class implementing moving average forecasting model. The most recent point 18 | * window is averaged and the value is copied forward as the forecast. The 19 | * model supports the following specific parameters: 20 | * 21 | * window -- Number of most recent points to use in computing the average. If 22 | * there are fewer points, those will be used instead. 23 | * 24 | * If there are less than 2 points in the series a random walk forecast is 25 | * produced. 26 | */ 27 | public final class IFSModelImplMovAvg extends IFSModel { 28 | public static final String ModelName = "model_movavg"; 29 | private static final IFSUsageDescription UsageDescription 30 | = new IFSUsageDescription( 31 | "Moving average forecast model implementation.\n", 32 | "Implements the moving average forecast model which averages the most\n" 33 | + "recent point window and uses that as the forecast.\n", 34 | "\n" 35 | + "window=<# points> -- # of recent points to average. If there are fewer\n" 36 | + " points, those will be used instead. The default is 14.\n" 37 | ); 38 | 39 | private static final int DefaultWindowSize = 14; 40 | private static final int MinSeriesLength = 2; 41 | 42 | private int Window = DefaultWindowSize; 43 | 44 | /* (non-Javadoc) 45 | * @see com.aol.ifs.soa.common.IFSModel#execModel(double[], double[]) 46 | */ 47 | @Override 48 | protected String execModel( 49 | double[] series, 50 | double[] forecasts, 51 | int cycle 52 | ) throws IFSException { 53 | int n = series.length; 54 | int nf = forecasts.length; 55 | 56 | if (n < MinSeriesLength) { 57 | for (int i = 0; i < nf; i++) 58 | forecasts[i] = series[n-1]; 59 | return "rw"; 60 | } 61 | 62 | double avg = 0.0; 63 | double sum = 0.0; 64 | int nv = 0; 65 | 66 | if (n >= Window) { 67 | for (int i = n-Window; i < n; i++) 68 | sum += series[i]; 69 | avg = sum/(double)Window; 70 | nv = Window; 71 | } else { 72 | for (double value : series) 73 | sum += value; 74 | avg = sum/(double)n; 75 | nv = n; 76 | } 77 | 78 | for (int i = 0; i < nf; i++) 79 | forecasts[i] = avg; 80 | 81 | return String.format("movavg::avg:%.3f,nv:%d", avg, nv); 82 | } 83 | 84 | /* (non-Javadoc) 85 | * @see com.aol.ifs.soa.common.IFSModel#getModelName() 86 | */ 87 | @Override 88 | public String getModelName() { 89 | return ModelName; 90 | } 91 | 92 | /* (non-Javadoc) 93 | * @see com.aol.ifs.soa.common.IFSModel#getUsage() 94 | */ 95 | @Override 96 | public IFSUsageDescription getUsage() { 97 | return UsageDescription; 98 | } 99 | 100 | /* (non-Javadoc) 101 | * @see com.aol.ifs.soa.common.IFSModel#injectParameters(java.util.List) 102 | */ 103 | @Override 104 | protected void injectParameters( 105 | List parameters 106 | ) throws IFSException { 107 | int window = DefaultWindowSize; 108 | 109 | if (parameters != null && parameters.size() > 0) 110 | for (IFSParameterValue parameter : parameters) 111 | if (parameter.getParameter().equals("window")) { 112 | try { 113 | window = Integer.parseInt(parameter.getValue()); 114 | } 115 | catch (NumberFormatException ex) { 116 | throw new IFSException(25, getModelName(), "window"); 117 | } 118 | if (window < 1) 119 | throw new IFSException(25, getModelName(), "window"); 120 | } else 121 | throw new IFSException(23, getModelName(), 122 | parameter.getParameter()); 123 | 124 | Window = window; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /client/src/test/scala/com/aol/one/reporting/forecastapi/client/ForecastClientTest.scala: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | * *******************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.client 8 | 9 | import com.fasterxml.jackson.databind.ObjectMapper 10 | import org.mockito.Mockito 11 | import org.scalatest.mockito.MockitoSugar 12 | import org.scalatest.{BeforeAndAfterEach, Matchers, WordSpec} 13 | 14 | class ForecastClientTest extends WordSpec with MockitoSugar with Matchers with BeforeAndAfterEach { 15 | 16 | private val objectMapper = new ObjectMapper() 17 | private val dummyValues = Array[Double](1, 2, 3) 18 | private val dummyResponseJson = objectMapper.writeValueAsString(ForecastResponse(dummyValues)) 19 | private var httpClient: ForecastHttpClient = _ 20 | private var forecastClient: ForecastClientImpl = _ 21 | 22 | override def beforeEach: Unit = { 23 | httpClient = Mockito.mock(classOf[ForecastHttpClient]) 24 | forecastClient = new ForecastClientImpl(httpClient) 25 | } 26 | 27 | "ForecastClient" should { 28 | 29 | "use appropriate canned set for short term daily forecast" in { 30 | val (request, requestJson) = buildRequest(Array[Double](1, 2, 3, 4, 5, 6, 7), 1) 31 | prepareResponse(requestJson, dummyResponseJson) 32 | 33 | val forecast = forecastClient.forecast(request.timeSeries, request.numberForecasts) 34 | assert(dummyValues === forecast.values) 35 | } 36 | 37 | "use appropriate canned set for long term daily forecast" in { 38 | val (request, requestJson) = buildRequest(Array[Double](1, 2, 3, 4, 5, 6, 7, 8), 20) 39 | prepareResponse(requestJson, dummyResponseJson) 40 | 41 | val forecast = forecastClient.forecast(request.timeSeries, request.numberForecasts) 42 | assert(dummyValues === forecast.values) 43 | } 44 | 45 | "sets lowest confidence and zero forecast on empty historical data" in { 46 | val (request, requestJson) = buildRequest(Array[Double](), 3) 47 | prepareResponse(requestJson, dummyResponseJson) 48 | 49 | val forecast = forecastClient.forecast(request.timeSeries, request.numberForecasts) 50 | 51 | assert(Double.MaxValue === forecast.confidence) 52 | assert(List[Double](0, 0, 0) === forecast.values) 53 | } 54 | 55 | "sets lowest confidence on insufficient historical data" in { 56 | val (request, requestJson) = buildRequest(Array[Double](1, 2), 3) 57 | prepareResponse(requestJson, dummyResponseJson) 58 | 59 | val forecast = forecastClient.forecast(request.timeSeries, request.numberForecasts) 60 | 61 | assert(Double.MaxValue === forecast.confidence) 62 | assert(dummyValues === forecast.values) 63 | } 64 | 65 | "calculates correct confidence on sufficient historical data" in { 66 | val hist = Array[Double](1, 1, 2, 2, 0, 0, 0, 1, 1, 2, 2, 0, 0, 0) 67 | val forecastValues = Array[Double](1, 1, 1, 1, 1, 1, 1) 68 | 69 | // mock confidence call 70 | val (_, confidenceRequestJson) = buildRequest(hist.take(7), 7) 71 | val (_, confidenceResponseJson) = buildResponse(forecastValues) 72 | prepareResponse(confidenceRequestJson, confidenceResponseJson) 73 | 74 | // mock forecast call 75 | val (forecastRequest, forecastRequestJson) = buildRequest(hist, 7) 76 | val (_, forecastResponseJson) = buildResponse(forecastValues) 77 | prepareResponse(forecastRequestJson, forecastResponseJson) 78 | 79 | val forecast = forecastClient.forecast(forecastRequest.timeSeries, forecastRequest.numberForecasts) 80 | 81 | val error = List(0, 0, 0.5, 0.5, 0, 0, 0) 82 | val expectedError = error.sum * 100.0 / error.length // -> error(hist - forecastValues) for the first 7 entries 83 | assert(math.abs(forecast.confidence - expectedError) < 0.0001) 84 | } 85 | } 86 | 87 | private def buildRequest(historical: Array[Double], horizon: Int) = { 88 | val request = new ForecastRequest(historical, horizon, ForecastParams.CannedSet) 89 | (request, objectMapper.writeValueAsString(request)) 90 | } 91 | 92 | private def buildResponse(forecast: Array[Double]) = { 93 | val response = ForecastResponse(forecast) 94 | (response, objectMapper.writeValueAsString(response)) 95 | } 96 | 97 | private def prepareResponse(request: String, response: String) = Mockito.when(httpClient.get(request)).thenReturn(response) 98 | } 99 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/resource/CannedSetResource.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.resource; 8 | 9 | 10 | import com.aol.one.reporting.forecastapi.server.model.response.CannedSetResponse; 11 | import com.aol.one.reporting.forecastapi.server.service.ForecastService; 12 | import com.codahale.metrics.annotation.ExceptionMetered; 13 | import com.codahale.metrics.annotation.Timed; 14 | import com.fasterxml.jackson.core.JsonProcessingException; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | import com.fasterxml.jackson.databind.ObjectWriter; 17 | import com.wordnik.swagger.annotations.Api; 18 | import com.wordnik.swagger.annotations.ApiOperation; 19 | import com.wordnik.swagger.annotations.ApiResponse; 20 | import com.wordnik.swagger.annotations.ApiResponses; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.servlet.http.HttpServletRequest; 25 | import javax.ws.rs.Consumes; 26 | import javax.ws.rs.GET; 27 | import javax.ws.rs.Path; 28 | import javax.ws.rs.Produces; 29 | import javax.ws.rs.QueryParam; 30 | import javax.ws.rs.core.Context; 31 | import javax.ws.rs.core.HttpHeaders; 32 | import javax.ws.rs.core.MediaType; 33 | import javax.ws.rs.core.Response; 34 | import java.util.List; 35 | 36 | /** 37 | * The Class EasyForecastResource. A JAX-RS resource with API method to generate 38 | * forecast for a given time series with at least one value. 39 | */ 40 | @Path("/impression-forecast-service/v1") 41 | @Produces(MediaType.APPLICATION_JSON) 42 | @Consumes(MediaType.APPLICATION_JSON) 43 | @Api(value = "Available Canned Sets", description = "List of Available Canned Sets", position = 3) 44 | public class CannedSetResource { 45 | 46 | private static final Logger LOG = LoggerFactory.getLogger(CannedSetResource.class); 47 | 48 | private static final String ACCEPT_HEADERS = "accept"; 49 | @Context 50 | private HttpHeaders headers; 51 | @Context 52 | private HttpServletRequest httpServletRequest; 53 | 54 | 55 | /** 56 | * Generate easy forecast for give time series 57 | * 58 | * @return forecastResponse object 59 | */ 60 | @GET 61 | @Path("/canned-set-list") 62 | @Timed 63 | @ExceptionMetered 64 | @ApiOperation(value = "Available Canned Sets", 65 | notes = "List all available canned sets.", 66 | responseContainer = "List", response = CannedSetResponse.class) 67 | @ApiResponses({ 68 | @ApiResponse(code = 200, message = "List CannedSets successful"), 69 | @ApiResponse(code = 404, message = "Failed to List CannedSets"), 70 | @ApiResponse(code = 500, message = "Internal server error due to encoding the data"), 71 | @ApiResponse(code = 400, message = "Bad request due to decoding the data"), 72 | @ApiResponse(code = 412, message = "Pre condition failed due to required data not found")}) 73 | 74 | public Response cannedSetList(@QueryParam("regex") String regex) { 75 | long start = System.currentTimeMillis(); 76 | ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); 77 | 78 | 79 | List response = null; 80 | LOG.debug("Get List of canned set name and its definitions"); 81 | String json = null; 82 | try { 83 | 84 | response = ForecastService.getCannedSets(regex); 85 | json = ow.writeValueAsString(response); 86 | if (headers.getRequestHeaders().get(HttpHeaders.ACCEPT).contains(MediaType.APPLICATION_JSON)) { 87 | if (response != null) { 88 | return Response.ok().entity(json).build(); 89 | } else { 90 | return Response.status(404).build(); 91 | } 92 | } 93 | } catch (JsonProcessingException e) { 94 | return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); 95 | } catch (Exception e) { 96 | LOG.error("Failed to generate forecast selected canned sets Error : " + e.getMessage(), e); 97 | String message = e.getMessage(); 98 | return Response.status(Response.Status.PRECONDITION_FAILED).entity(message).type("text/plain").build(); 99 | } 100 | return Response.ok().entity(json).build(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/cs/IFSCannedSet.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.cs; 8 | 9 | import com.aol.one.reporting.forecastapi.server.models.model.IFSParameterSpec; 10 | 11 | /** 12 | * Class implementing named model parameter specifications 13 | * ({@link IFSParameterSpec}. A named parameter specification has ordering, 14 | * equality, and hashing attributes. 15 | */ 16 | public final class IFSCannedSet implements Comparable { 17 | private String Name; 18 | private String Description; 19 | private IFSParameterSpec ParameterSpec; 20 | 21 | /** 22 | * Default constructor 23 | */ 24 | public IFSCannedSet() { 25 | setName(null); 26 | setDescription(null); 27 | setParameterSpec(null); 28 | } 29 | 30 | /** 31 | * Fully specified constructor. 32 | * 33 | * @param name Canned set name. 34 | * @param description Canned set description. 35 | * @param parameter_spec Canned set model parameter specification. 36 | */ 37 | public IFSCannedSet( 38 | String name, 39 | String description, 40 | IFSParameterSpec parameter_spec 41 | ) { 42 | setName(name); 43 | setDescription(description); 44 | setParameterSpec(parameter_spec); 45 | } 46 | 47 | /** 48 | * Clone this canned set. 49 | * 50 | * @return Canned set clone. 51 | */ 52 | public IFSCannedSet clone() { 53 | return new IFSCannedSet(Name, Description, ParameterSpec.clone()); 54 | } 55 | 56 | /** 57 | * Order canned sets according to canned set name. 58 | * 59 | * @param that Canned set to compare with. 60 | * 61 | * @return 0 if canned set names are equal. -1 if this canned set name 62 | * lexically precedes that canned set name. 1 otherwise. 63 | */ 64 | public int compareTo( 65 | IFSCannedSet that 66 | ) { 67 | if (this.getName() == null && that.getName() == null) 68 | return 0; 69 | else if (this.getName() != null && that.getName() == null) 70 | return 1; 71 | else if (this.getName() == null && that.getName() != null) 72 | return -1; 73 | else 74 | return this.getName().compareTo(that.getName()); 75 | } 76 | 77 | /** 78 | * Are two canned sets equal? 79 | * 80 | * @param obj Canned set to compare with. 81 | * 82 | * @return True if canned sets have the same name. False if they do not. 83 | */ 84 | @Override 85 | public boolean equals(Object obj) { 86 | if (this == obj) 87 | return(true); 88 | if (obj == null) 89 | return(false); 90 | if (getClass() != obj.getClass()) 91 | return(false); 92 | 93 | IFSCannedSet that = (IFSCannedSet)obj; 94 | 95 | if (this.getName() == null || that.getName() == null) 96 | return(false); 97 | 98 | return(this.getName().equals(that.getName())); 99 | } 100 | 101 | /** 102 | * Fetch canned set description. 103 | * 104 | * @return Canned set description. 105 | */ 106 | public String getDescription() { 107 | return Description; 108 | } 109 | 110 | /** 111 | * Fetch canned set name. 112 | * 113 | * @return Canned set name. 114 | */ 115 | public String getName() { 116 | return Name; 117 | } 118 | 119 | /** 120 | * Fetch parameter specification. 121 | * 122 | * @return Model parameter specification. 123 | */ 124 | public IFSParameterSpec getParameterSpec() { 125 | return ParameterSpec; 126 | } 127 | 128 | /** 129 | * Hash a canned set object based on canned set name. 130 | * 131 | * @return Hash code of canned set name. 0 if name is null. 132 | */ 133 | @Override 134 | public int hashCode() { 135 | if (getName() == null) 136 | return 0; 137 | else 138 | return getName().hashCode(); 139 | } 140 | 141 | /** 142 | * Set canned set description. 143 | * 144 | * @param description Canned set description. 145 | */ 146 | public void setDescription( 147 | String description 148 | ) { 149 | Description = description; 150 | } 151 | 152 | /** 153 | * Set canned set name. 154 | * 155 | * @param name Canned set name. 156 | */ 157 | public void setName( 158 | String name 159 | ) { 160 | Name = name; 161 | } 162 | 163 | /** 164 | * Set model parameter specification. 165 | * 166 | * @param parameter_spec Canned set model parameter specification. 167 | */ 168 | public void setParameterSpec( 169 | IFSParameterSpec parameter_spec 170 | ) { 171 | ParameterSpec = parameter_spec; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/resource/SimpleForecastResource.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | package com.aol.one.reporting.forecastapi.server.resource; 7 | 8 | import com.aol.one.reporting.forecastapi.server.model.request.ImpressionForecastRequest; 9 | import com.aol.one.reporting.forecastapi.server.model.response.ForecastResponse; 10 | import com.aol.one.reporting.forecastapi.server.service.ForecastService; 11 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 12 | import com.codahale.metrics.annotation.ExceptionMetered; 13 | import com.codahale.metrics.annotation.Timed; 14 | import com.fasterxml.jackson.core.JsonProcessingException; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | import com.fasterxml.jackson.databind.ObjectWriter; 17 | import com.wordnik.swagger.annotations.Api; 18 | import com.wordnik.swagger.annotations.ApiOperation; 19 | import com.wordnik.swagger.annotations.ApiResponse; 20 | import com.wordnik.swagger.annotations.ApiResponses; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.servlet.http.HttpServletRequest; 25 | import javax.validation.Valid; 26 | import javax.validation.constraints.NotNull; 27 | import javax.ws.rs.Consumes; 28 | import javax.ws.rs.POST; 29 | import javax.ws.rs.Path; 30 | import javax.ws.rs.Produces; 31 | import javax.ws.rs.core.Context; 32 | import javax.ws.rs.core.HttpHeaders; 33 | import javax.ws.rs.core.MediaType; 34 | import javax.ws.rs.core.Response; 35 | 36 | @Path("/forecast") 37 | @Produces(MediaType.APPLICATION_JSON) 38 | @Consumes(MediaType.APPLICATION_JSON) 39 | @Api( 40 | value = "Forecast", 41 | description = "Produce forecast given a historical impression time series and a list of canned set names", 42 | position = 5 43 | ) 44 | public class SimpleForecastResource { 45 | 46 | private static final Logger LOG = LoggerFactory.getLogger(SimpleForecastResource.class); 47 | 48 | @Context 49 | private HttpHeaders headers; 50 | @Context 51 | private HttpServletRequest httpServletRequest; 52 | 53 | 54 | /** 55 | * Generate forecast given time-series 56 | * 57 | * @return forecastResponse object 58 | */ 59 | @POST 60 | @Path("/") 61 | @Timed 62 | @ExceptionMetered 63 | @ApiOperation(value = "Forecast", 64 | notes = "Produce a forecast given a historical time-series and a list of canned` set names", 65 | response = ForecastResponse.class) 66 | @ApiResponses({ 67 | @ApiResponse(code = 200, message = "Forecast successful"), 68 | @ApiResponse(code = 404, message = "Failed to calculate forecast"), 69 | @ApiResponse(code = 500, message = "Internal server error due to encoding the data"), 70 | @ApiResponse(code = 400, message = "Bad request due to decoding the data"), 71 | @ApiResponse(code = 412, message = "Pre condition failed due to required data not found")}) 72 | 73 | public Response generateForecast( 74 | @Valid @NotNull final ImpressionForecastRequest forecastRequest) { 75 | long start = System.currentTimeMillis(); 76 | ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); 77 | 78 | ForecastResponse response = null; 79 | LOG.debug("Get a forecast for a given time series"); 80 | String json = null; 81 | try { 82 | response = ForecastService.impressionForecast(forecastRequest, start); 83 | json = ow.writeValueAsString(response); 84 | 85 | if (headers.getRequestHeaders().get(HttpHeaders.ACCEPT).contains(MediaType.APPLICATION_JSON)) { 86 | if (response != null) { 87 | return Response.ok().entity(json).build(); 88 | } else { 89 | return Response.status(404).build(); 90 | } 91 | } 92 | } catch (JsonProcessingException e) { 93 | return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); 94 | } catch (IFSException ifsException) { 95 | Response.status(Response.Status.BAD_REQUEST).build(); 96 | } catch (Exception e) { 97 | LOG.error("Failed to generate forecast. Error : " + e.getMessage(), e); 98 | String message = e.getMessage(); 99 | return Response.status(Response.Status.PRECONDITION_FAILED).entity(message).type("text/plain").build(); 100 | } 101 | return Response.ok().entity(json).build(); 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/resource/CollectionListResource.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.resource; 8 | 9 | 10 | import com.aol.one.reporting.forecastapi.server.model.response.CollectionResponse; 11 | import com.aol.one.reporting.forecastapi.server.service.ForecastService; 12 | import com.codahale.metrics.annotation.ExceptionMetered; 13 | import com.codahale.metrics.annotation.Timed; 14 | import com.fasterxml.jackson.core.JsonProcessingException; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | import com.fasterxml.jackson.databind.ObjectWriter; 17 | import com.wordnik.swagger.annotations.Api; 18 | import com.wordnik.swagger.annotations.ApiOperation; 19 | import com.wordnik.swagger.annotations.ApiResponse; 20 | import com.wordnik.swagger.annotations.ApiResponses; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.servlet.http.HttpServletRequest; 25 | import javax.ws.rs.Consumes; 26 | import javax.ws.rs.GET; 27 | import javax.ws.rs.Path; 28 | import javax.ws.rs.Produces; 29 | import javax.ws.rs.QueryParam; 30 | import javax.ws.rs.core.Context; 31 | import javax.ws.rs.core.HttpHeaders; 32 | import javax.ws.rs.core.MediaType; 33 | import javax.ws.rs.core.Response; 34 | 35 | /** 36 | * The Class EasyForecastResource. A JAX-RS resource with API method to generate 37 | * forecast for a given time series with at least one value. 38 | */ 39 | @Path("/impression-forecast-service/v1") 40 | @Produces(MediaType.APPLICATION_JSON) 41 | @Consumes(MediaType.APPLICATION_JSON) 42 | @Api( 43 | value = "Collection List", 44 | description = "List available canned set collections.", 45 | position = 4 46 | ) 47 | public class CollectionListResource { 48 | 49 | /** 50 | * The Constant log. 51 | */ 52 | private static final Logger LOG = LoggerFactory.getLogger(CollectionListResource.class); 53 | 54 | private static final String ACCEPT_HEADERS = "accept"; 55 | @Context 56 | private HttpHeaders headers; 57 | @Context 58 | private HttpServletRequest httpServletRequest; 59 | 60 | 61 | /** 62 | * Generate easy forecast for give time series 63 | * 64 | * @return forecastResponse object 65 | */ 66 | @GET 67 | @Path("/canned-set-collection-list") 68 | @Timed 69 | @ExceptionMetered 70 | @ApiOperation(value = "Collection List", 71 | notes = "List available canned set collections.", 72 | responseContainer = "Array", response = CollectionResponse.class) 73 | @ApiResponses({ 74 | @ApiResponse(code = 200, message = "Collection Name list successful"), 75 | @ApiResponse(code = 404, message = "Failed to List CannedSets"), 76 | @ApiResponse(code = 500, message = "Internal server error due to encoding the data"), 77 | @ApiResponse(code = 400, message = "Bad request due to decoding the data"), 78 | @ApiResponse(code = 412, message = "Pre condition failed due to required data not found") 79 | }) 80 | 81 | public Response collectionList(@QueryParam("regex") String regex) { 82 | long start = System.currentTimeMillis(); 83 | ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); 84 | 85 | CollectionResponse[] collectionResponse = null; 86 | 87 | 88 | LOG.debug("List of collection names and canned definitions"); 89 | String json = null; 90 | try { 91 | 92 | collectionResponse = ForecastService.getCollectionCannedSets(regex); 93 | 94 | json = ow.writeValueAsString(collectionResponse); 95 | if (headers.getRequestHeaders().get(HttpHeaders.ACCEPT).contains(MediaType.APPLICATION_JSON)) { 96 | if (collectionResponse != null) { 97 | return Response.ok().entity(json).build(); 98 | } else { 99 | return Response.status(404).build(); 100 | } 101 | } 102 | } catch (JsonProcessingException e) { 103 | return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); 104 | } catch (Exception e) { 105 | LOG.error("Failed to generate forecast selected canned sets Error : " + e.getMessage(), e); 106 | String message = e.getMessage(); 107 | return Response.status(Response.Status.PRECONDITION_FAILED).entity(message).type("text/plain").build(); 108 | } 109 | return Response.ok().entity(json).build(); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/util/ForecastUtil.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.util; 8 | 9 | import com.aol.one.reporting.forecastapi.server.app.IfsCache; 10 | import com.aol.one.reporting.forecastapi.server.model.request.EasyForecastRequest; 11 | import com.aol.one.reporting.forecastapi.server.model.request.SelectionForecastRequest; 12 | import com.aol.one.reporting.forecastapi.server.models.cs.IFSCannedSet; 13 | import com.aol.one.reporting.forecastapi.server.models.model.IFSParameterSpec; 14 | import com.aol.one.reporting.forecastapi.server.models.model.IFSParameterValue; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | public final class ForecastUtil { 20 | 21 | private ForecastUtil() {} 22 | 23 | 24 | public static List changeSpikeFilterWindow(List list, Integer sfw) { 25 | if (sfw == -1) 26 | return list; 27 | List newList = new ArrayList<>(); 28 | for(IFSCannedSet ifsCannedSet : list) { 29 | IFSCannedSet newIfsCannedSet = ifsCannedSet.clone(); 30 | IFSParameterSpec spec = newIfsCannedSet.getParameterSpec(); 31 | List paramList = spec.getParameterValues(); 32 | List newParamList = new ArrayList<>(); 33 | for(IFSParameterValue parameterValue : paramList) { 34 | IFSParameterValue newParameterValue = new IFSParameterValue(); 35 | newParameterValue.setParameter(new String(parameterValue.getParameter())); 36 | if (parameterValue.getParameter().equals("spike_filter")) { 37 | newParameterValue.setValue(new String(sfw.toString())); 38 | } else { 39 | newParameterValue.setValue(new String(parameterValue.getValue())); 40 | } 41 | newParamList.add(newParameterValue); 42 | } 43 | spec.setParameterValues(newParamList); 44 | newList.add(newIfsCannedSet); 45 | } 46 | return newList; 47 | } 48 | 49 | public static void messageForecast(double[] forecast) { 50 | for(int index = 0; index < forecast.length; index++) { 51 | if (forecast[index] < 0.0) 52 | forecast[index] = 0.0; 53 | else 54 | forecast[index] = Math.round(forecast[index]); 55 | } 56 | } 57 | 58 | public static List setupCannedSets(IfsCache cache, SelectionForecastRequest request) throws Exception { 59 | List newCannedSets = new ArrayList<>(); 60 | checkTypeAndAdd(request.getYearlyCannedSetList(),1,cache,newCannedSets); 61 | checkTypeAndAdd(request.getSeasonalCannedSetList(),2,cache,newCannedSets); 62 | checkTypeAndAdd(request.getNonSeasonalCannedSetList(),3,cache,newCannedSets); 63 | return newCannedSets; 64 | } 65 | 66 | 67 | private static void checkTypeAndAdd( 68 | String[] sets, 69 | int type, 70 | IfsCache cache, 71 | List newCannedSets 72 | ) throws Exception { 73 | List defaultList = cache.getList(EasyForecastRequest.CANNED_SET_DEFAULT_COLLECTION_NAME); 74 | String cannedSetType = null; 75 | String attribute = null; 76 | switch(type) { 77 | case 1: cannedSetType = "YEAR"; 78 | attribute = "YearlyCannedSetList"; 79 | break; 80 | case 2: cannedSetType = "AUTO"; 81 | attribute = "SeasonalCannedSetList"; 82 | break; 83 | case 3: cannedSetType = "OTHER"; 84 | attribute = "NonSeasonalCannedSetList"; 85 | break; 86 | } 87 | if (sets != null && sets.length > 0) { 88 | for(String cannedSet : sets) { 89 | IFSCannedSet ifsCannedSet = cache.getMap().get(cannedSet); 90 | if (ifsCannedSet == null) { 91 | throw new Exception("Invalid canned set name : " + cannedSet + " in " + attribute); 92 | } 93 | newCannedSets.add(ifsCannedSet); 94 | } 95 | } else { 96 | for(IFSCannedSet ifsCannedSet : defaultList) { 97 | String cannedSet = ifsCannedSet.getName(); 98 | if ((type == 1 || type == 2) && cannedSet.endsWith(cannedSetType)) 99 | newCannedSets.add(ifsCannedSet); 100 | else if (type == 3) { 101 | if (!(cannedSet.endsWith("YEAR") || cannedSet.endsWith("AUTO"))) 102 | newCannedSets.add(ifsCannedSet); 103 | 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSModelFactory.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | import java.util.List; 10 | import java.util.TreeMap; 11 | 12 | import com.aol.one.reporting.forecastapi.server.models.alg.IFSModelImplAR; 13 | import com.aol.one.reporting.forecastapi.server.models.alg.IFSModelImplARIMA; 14 | import com.aol.one.reporting.forecastapi.server.models.alg.IFSModelImplExpSm; 15 | import com.aol.one.reporting.forecastapi.server.models.alg.IFSModelImplMovAvg; 16 | import com.aol.one.reporting.forecastapi.server.models.alg.IFSModelImplRW; 17 | import com.aol.one.reporting.forecastapi.server.models.alg.IFSModelImplRegress; 18 | 19 | /** 20 | * Class that manufactures forecast models given the model name, model 21 | * parameters, and time series data. 22 | */ 23 | @SuppressWarnings("unchecked") 24 | public final class IFSModelFactory { 25 | @SuppressWarnings("rawtypes") 26 | private static TreeMap Models = new TreeMap(); 27 | static { 28 | Models.put(IFSModelImplAR.ModelName, 29 | IFSModelImplAR.class); 30 | Models.put(IFSModelImplARIMA.ModelName, 31 | IFSModelImplARIMA.class); 32 | Models.put(IFSModelImplExpSm.ModelName, 33 | IFSModelImplExpSm.class); 34 | Models.put(IFSModelImplMovAvg.ModelName, 35 | IFSModelImplMovAvg.class); 36 | Models.put(IFSModelImplRegress.ModelName, 37 | IFSModelImplRegress.class); 38 | Models.put(IFSModelImplRW.ModelName, 39 | IFSModelImplRW.class); 40 | } 41 | 42 | /** 43 | * Create a forecast model based on model name. The model created can be 44 | * reused to do more than one set of forecasts. The setup method can be 45 | * used to process common parameters that reshape the time series data 46 | * and pass along specific parameters to the model. 47 | * 48 | * @param model_name Name of the forecast model to create. 49 | * 50 | * @return Forecast model. 51 | * 52 | * @throws IFSException if the model name is unknown. 53 | */ 54 | public static IFSModel create( 55 | String model_name 56 | ) throws IFSException { 57 | if (model_name == null || model_name.equals("")) { 58 | throw new IFSException(13); 59 | } 60 | 61 | Class model_class = Models.get(model_name); 62 | 63 | if (model_class == null) { 64 | throw new IFSException(14, model_name); 65 | } 66 | 67 | IFSModel model = null; 68 | 69 | try { 70 | model = model_class.newInstance(); 71 | } catch (IllegalAccessException ex) { 72 | throw new IFSException(15, model_name, ex.getMessage()); 73 | } catch (InstantiationException ex) { 74 | throw new IFSException(15, model_name, ex.getMessage()); 75 | } 76 | 77 | return(model); 78 | } 79 | 80 | /** 81 | * Setup a forecast model based on time series data and the model 82 | * parameters. The parameters can include common ones that cause 83 | * the time series to be re-shaped as well as parameters specific to 84 | * the model. This method allows forecast models to be reused. 85 | * 86 | * @param model Forecast model to setup. 87 | * @param series Time series data. 88 | * @param parameters Model parameters (common and specific). 89 | * 90 | * @return Forecast model. 91 | * 92 | * @throws IFSException if there is a problem with the time series, 93 | * the model parameters, or a null model was passed. 94 | */ 95 | public static void setup( 96 | IFSModel model, 97 | double[] series, 98 | List parameters 99 | ) throws IFSException { 100 | if (model == null) { 101 | throw new IFSException(16); 102 | } 103 | 104 | model.setSeries(series); 105 | model.setParameters(parameters); 106 | } 107 | 108 | /** 109 | * Fetch usage information for all the supported models. 110 | * 111 | * @return Model usage information. 112 | */ 113 | public static String usage() { 114 | StringBuffer usage_info = new StringBuffer(); 115 | Class model_class = null; 116 | IFSModel model = null; 117 | IFSUsageDescription usage_desc = null; 118 | int i = 1; 119 | 120 | for (String model_name : Models.keySet()) { 121 | model_class = Models.get(model_name); 122 | try { 123 | model = model_class.newInstance(); 124 | } catch (InstantiationException ex) { 125 | System.err.println("Cannot create forecast model '" 126 | + model_name 127 | + "'. " 128 | + ex.getMessage()); 129 | System.exit(1); 130 | } catch (IllegalAccessException ex) { 131 | System.err.println("Cannot create forecast model '" 132 | + model_name 133 | + "'. " 134 | + ex.getMessage()); 135 | System.exit(1); 136 | } 137 | usage_desc = model.getUsage(); 138 | usage_info.append("\n"); 139 | usage_info.append(String.format("%2d. %-14.14s -- %s\n", 140 | i, model_name, usage_desc.getSummary())); 141 | usage_info.append(usage_desc.getBody()); 142 | usage_info.append(usage_desc.getParameters()); 143 | i++; 144 | } 145 | 146 | return(usage_info.toString()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/util/RequestValidation.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.util; 8 | 9 | import com.aol.one.reporting.forecastapi.server.app.IfsCache; 10 | import com.aol.one.reporting.forecastapi.server.models.cs.IFSCannedSet; 11 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 12 | import com.aol.one.reporting.forecastapi.server.models.model.IFSParameterValue; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.util.List; 17 | 18 | public final class RequestValidation { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(RequestValidation.class); 21 | 22 | public static Integer spikeFilter(Integer sfw) throws Exception { 23 | if (sfw != null) { 24 | if (sfw < -1 || sfw == 1 || sfw == 2 || sfw > 30) { 25 | LOG.error("Invalid SpikeFilterWindow : " + sfw + " specified in request"); 26 | throw new Exception("Invalid spikeFilterWindow value : " + sfw); 27 | } 28 | 29 | } else { 30 | sfw = -1; 31 | } 32 | return sfw; 33 | } 34 | 35 | public static void numberForecasts(int numberForecasts) throws Exception { 36 | if (numberForecasts < 1) { 37 | LOG.error("Invalid number of forecasts : " + numberForecasts + " in request"); 38 | throw new Exception("Invalid number of forecasts : " + numberForecasts + " in request"); 39 | } 40 | } 41 | 42 | 43 | public static void timeSeries(double[] timeSeries) throws Exception { 44 | if (timeSeries == null || timeSeries.length < 1) { 45 | LOG.error("Invalid Time Series data in request"); 46 | throw new Exception("Invalid Time Series data in request"); 47 | } 48 | } 49 | 50 | 51 | public static void easyRequestCacheValidation(IfsCache cache, String name) throws IFSException, Exception { 52 | 53 | if (!cache.getCollectionNames().contains(name)) { 54 | LOG.error("Invalid Canned Set Collection Name : " + name); 55 | throw new Exception("Invalid Canned Set Collection name : " + name); 56 | } 57 | 58 | IFSCannedSet arNoneNone = cache.getMap().get("AR-NONE-NONE"); 59 | IFSCannedSet avgNone28New = cache.getMap().get("AVG-NONE-28-NEW"); 60 | IFSCannedSet regNoneAddAutoNew = cache.getMap().get("REG-NONE-ADD-AUTO-NEW"); 61 | 62 | if (arNoneNone == null) { 63 | LOG.error("AR-NONE-NONE default canned set is null"); 64 | throw new IFSException("AR-NONE-NONE default canned set is null"); 65 | } 66 | 67 | if (avgNone28New == null) { 68 | LOG.error("AVG-NONE-28-NEW is null"); 69 | throw new IFSException("AVG-NONE-28-NEW default canned set is null"); 70 | } 71 | 72 | if (regNoneAddAutoNew == null) { 73 | LOG.error("REG-NONE-ADD-AUTO-NEW is null"); 74 | throw new IFSException("REG-NONE-ADD-AUTO-NEW default canned set is null"); 75 | } 76 | if (cache.getList(name) == null) { 77 | LOG.error("Cached CannedSet List is null"); 78 | throw new IFSException("Cached CannedSet List null"); 79 | } 80 | LOG.debug("CannedSet List :"); 81 | for (IFSCannedSet ifsCannedSet : cache.getList(name)) { 82 | LOG.debug("CannedSet : " + ifsCannedSet.getName()); 83 | for (IFSParameterValue param : ifsCannedSet.getParameterSpec().getParameterValues()) 84 | LOG.debug(" Parameter Key : " + param.getParameter() + " Value :" + param.getValue()); 85 | } 86 | } 87 | 88 | public static void selectionRequestCacheValidation(IfsCache cache, List ifsCannedSets) throws IFSException, Exception { 89 | IFSCannedSet arNoneNone = cache.getMap().get("AR-NONE-NONE"); 90 | IFSCannedSet avgNone28New = cache.getMap().get("AVG-NONE-28-NEW"); 91 | IFSCannedSet regNoneAddAutoNew = cache.getMap().get("REG-NONE-ADD-AUTO-NEW"); 92 | 93 | if (arNoneNone == null) { 94 | LOG.error("AR-NONE-NONE is null"); 95 | throw new IFSException("AR-NONE-NONE default canned set is null"); 96 | } 97 | 98 | if (avgNone28New == null) { 99 | LOG.error("AVG-NONE-28-NEW is null"); 100 | throw new IFSException("AVG-NONE-28-NEW default canned set is null"); 101 | } 102 | 103 | if (regNoneAddAutoNew == null) { 104 | LOG.error("REG-NONE-ADD-AUTO-NEW is null"); 105 | throw new IFSException("REG-NONE-ADD-AUTO-NEW default canned set is null"); 106 | } 107 | 108 | LOG.debug("CannedSet List :"); 109 | for (IFSCannedSet ifsCannedSet : ifsCannedSets) { 110 | LOG.debug("CannedSet : " + ifsCannedSet.getName()); 111 | for (IFSParameterValue param : ifsCannedSet.getParameterSpec().getParameterValues()) 112 | LOG.debug(" Parameter Key : " + param.getParameter() + " Value :" + param.getValue()); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/cs/GetCannedSetDefinitions.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.cs; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.FileInputStream; 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.io.InputStreamReader; 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.StringTokenizer; 18 | 19 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 20 | import com.aol.one.reporting.forecastapi.server.models.model.IFSParameterSpec; 21 | import com.aol.one.reporting.forecastapi.server.models.model.IFSParameterValue; 22 | 23 | /** 24 | * Class implementing methods for fetching canned set definitions from a file. 25 | */ 26 | public final class GetCannedSetDefinitions { 27 | 28 | /** 29 | * Fetch the canned set definitions from a specified file. The file may 30 | * contain no definitions, but, if it does, there must be one parameter 31 | * definition per line in the following format: 32 | * 33 | * 34 | * 35 | * Note each canned set must have a special parameter 'model' that indicates 36 | * the model to be used. 37 | * 38 | * @param canned_set_definition_file File containing canned set definitions. 39 | * 40 | * @return Canned set definition map which could be empty. 41 | * 42 | * @throws IFSException Thrown if file has an unexpected format. 43 | */ 44 | public static Map getCannedSetDefinitions( 45 | String canned_set_definition_file 46 | ) throws IFSException { 47 | String line = null; 48 | Map definitions = null; 49 | IFSCannedSet canned_set = null; 50 | IFSParameterSpec canned_set_spec = null; 51 | StringTokenizer tokens = null; 52 | int line_num = 0; 53 | String id = null; 54 | String parameter = null; 55 | String value = null; 56 | 57 | try (BufferedReader in = new BufferedReader(new InputStreamReader( 58 | new FileInputStream(canned_set_definition_file)))){ 59 | definitions = new HashMap(); 60 | while ((line = in.readLine()) != null) { 61 | line_num++; 62 | tokens = new StringTokenizer(line, " "); 63 | if (tokens.countTokens() <= 0) { 64 | continue; 65 | } else if (tokens.countTokens() < 3) { 66 | throw new IFSException(String.format("Canned set definition " 67 | + "file '%s' has the wrong format at line %d.", 68 | canned_set_definition_file, line_num)); 69 | } 70 | id = tokens.nextToken(); 71 | parameter = tokens.nextToken(); 72 | value = tokens.nextToken(); 73 | if (parameter.equals("desc")) { 74 | while (tokens.hasMoreTokens()) { 75 | value += " " + tokens.nextToken(); 76 | } 77 | } else if (tokens.hasMoreTokens()) { 78 | throw new IFSException(String.format("Canned set definition " 79 | + "file '%s' has the wrong format at line %d.", 80 | canned_set_definition_file, line_num)); 81 | } 82 | canned_set = definitions.get(id); 83 | if (canned_set == null) { 84 | canned_set = new IFSCannedSet(); 85 | canned_set.setName(id); 86 | canned_set.setDescription(""); 87 | canned_set_spec = new IFSParameterSpec(); 88 | canned_set_spec.setParameterValues(new ArrayList()); 89 | canned_set.setParameterSpec(canned_set_spec); 90 | definitions.put(id, canned_set); 91 | } 92 | canned_set_spec = canned_set.getParameterSpec(); 93 | if (parameter.equals("model")) { 94 | canned_set_spec.setModel(value); 95 | } else if (parameter.equals("desc")) { 96 | canned_set.setDescription(value); 97 | } else { 98 | canned_set_spec.getParameterValues().add( 99 | new IFSParameterValue(parameter, value)); 100 | } 101 | } 102 | } catch (FileNotFoundException ex) { 103 | throw new IFSException(String.format("Could not read '%s': %s", 104 | canned_set_definition_file, ex.getMessage())); 105 | } catch (SecurityException ex) { 106 | throw new IFSException(String.format("Could not read '%s': %s", 107 | canned_set_definition_file, ex.getMessage())); 108 | } catch (IOException ex) { 109 | throw new IFSException(String.format("Could not read '%s': %s", 110 | canned_set_definition_file, ex.getMessage())); 111 | } 112 | 113 | for (IFSCannedSet cs : definitions.values()) { 114 | if (cs.getParameterSpec().getModel() == null) { 115 | throw new IFSException(String.format("Canned set definition " 116 | + "'%s' has no model specified.", cs.getName())); 117 | } 118 | } 119 | 120 | return definitions; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/resource/EasyForecastResource.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.resource; 8 | 9 | 10 | import com.aol.one.reporting.forecastapi.server.model.request.EasyForecastRequest; 11 | import com.aol.one.reporting.forecastapi.server.model.response.ForecastResponse; 12 | import com.aol.one.reporting.forecastapi.server.service.ForecastService; 13 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 14 | import com.codahale.metrics.annotation.ExceptionMetered; 15 | import com.codahale.metrics.annotation.Timed; 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import com.fasterxml.jackson.databind.ObjectMapper; 18 | import com.fasterxml.jackson.databind.ObjectWriter; 19 | import com.wordnik.swagger.annotations.Api; 20 | import com.wordnik.swagger.annotations.ApiOperation; 21 | import com.wordnik.swagger.annotations.ApiResponse; 22 | import com.wordnik.swagger.annotations.ApiResponses; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.validation.Valid; 28 | import javax.validation.constraints.NotNull; 29 | import javax.ws.rs.Consumes; 30 | import javax.ws.rs.POST; 31 | import javax.ws.rs.Path; 32 | import javax.ws.rs.Produces; 33 | import javax.ws.rs.core.Context; 34 | import javax.ws.rs.core.HttpHeaders; 35 | import javax.ws.rs.core.MediaType; 36 | import javax.ws.rs.core.Response; 37 | 38 | /** 39 | * The Class EasyForecastResource. A JAX-RS resource with API method to generate 40 | * forecast for a given time series with at least one value. 41 | */ 42 | @Path("/impression-forecast-service/v1") 43 | @Produces(MediaType.APPLICATION_JSON) 44 | @Consumes(MediaType.APPLICATION_JSON) 45 | @Api( 46 | value = "Easy Forecast", 47 | description = "Produce daily impression forecast given a historical daily impression time series and a canned set collection name.", 48 | position = 1 49 | ) 50 | public class EasyForecastResource { 51 | 52 | /** 53 | * The Constant log. 54 | */ 55 | private static final Logger LOG = LoggerFactory.getLogger(EasyForecastResource.class); 56 | 57 | private static final String ACCEPT_HEADERS = "accept"; 58 | @Context 59 | private HttpHeaders headers; 60 | @Context 61 | private HttpServletRequest httpServletRequest; 62 | 63 | 64 | /** 65 | * Generate easy forecast for give time series 66 | * 67 | * @return forecastResponse object 68 | */ 69 | @POST 70 | @Path("/easy-forecast") 71 | @Timed 72 | @ExceptionMetered 73 | @ApiOperation(value = "Get a forecast for a given time series", 74 | notes = "Get a forecast for a given time series", 75 | response = ForecastResponse.class) 76 | @ApiResponses({ 77 | @ApiResponse(code = 200, message = "Easy Forecast successful"), 78 | @ApiResponse(code = 404, message = "Failed to calculate forecast"), 79 | @ApiResponse(code = 500, message = "Internal server error due to encoding the data"), 80 | @ApiResponse(code = 400, message = "Bad request due to decoding the data"), 81 | @ApiResponse(code = 412, message = "Pre condition failed due to required data not found")}) 82 | 83 | public Response generateForecast( 84 | @Valid @NotNull final EasyForecastRequest easyForecastRequest) { 85 | long start = System.currentTimeMillis(); 86 | ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); 87 | String request = null; 88 | try { 89 | request = ow.writeValueAsString(easyForecastRequest); 90 | } catch (JsonProcessingException ex) { 91 | return Response.status(Response.Status.BAD_REQUEST).build(); 92 | } 93 | 94 | ForecastResponse response = null; 95 | LOG.debug("Get a forecast for a given time series"); 96 | String json = null; 97 | try { 98 | 99 | response = ForecastService.easyForecast(easyForecastRequest, start); 100 | json = ow.writeValueAsString(response); 101 | 102 | if (headers.getRequestHeaders().get(HttpHeaders.ACCEPT).contains(MediaType.APPLICATION_JSON)) { 103 | if (response != null) { 104 | return Response.ok().entity(json).build(); 105 | } else { 106 | return Response.status(404).build(); 107 | } 108 | 109 | } 110 | } catch (JsonProcessingException e) { 111 | return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); 112 | } catch (IFSException ifsException) { 113 | Response.status(Response.Status.BAD_REQUEST).build(); 114 | } catch (Exception e) { 115 | LOG.error("Failed to generate easy forecast Error : " + e.getMessage(), e); 116 | String message = e.getMessage(); 117 | return Response.status(Response.Status.PRECONDITION_FAILED).entity(message).type("text/plain").build(); 118 | } 119 | return Response.ok().entity(json).build(); 120 | 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/resource/ImpressionForecastResource.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | package com.aol.one.reporting.forecastapi.server.resource; 7 | 8 | import com.aol.one.reporting.forecastapi.server.model.request.ImpressionForecastRequest; 9 | import com.aol.one.reporting.forecastapi.server.model.response.ForecastResponse; 10 | import com.aol.one.reporting.forecastapi.server.service.ForecastService; 11 | import com.aol.one.reporting.forecastapi.server.models.model.IFSException; 12 | import com.codahale.metrics.annotation.ExceptionMetered; 13 | import com.codahale.metrics.annotation.Timed; 14 | import com.fasterxml.jackson.core.JsonProcessingException; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | import com.fasterxml.jackson.databind.ObjectWriter; 17 | import com.wordnik.swagger.annotations.Api; 18 | import com.wordnik.swagger.annotations.ApiOperation; 19 | import com.wordnik.swagger.annotations.ApiResponse; 20 | import com.wordnik.swagger.annotations.ApiResponses; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.servlet.http.HttpServletRequest; 25 | import javax.validation.Valid; 26 | import javax.validation.constraints.NotNull; 27 | import javax.ws.rs.Consumes; 28 | import javax.ws.rs.POST; 29 | import javax.ws.rs.Path; 30 | import javax.ws.rs.Produces; 31 | import javax.ws.rs.core.Context; 32 | import javax.ws.rs.core.HttpHeaders; 33 | import javax.ws.rs.core.MediaType; 34 | import javax.ws.rs.core.Response; 35 | 36 | /** 37 | * The Class EasyForecastResource. A JAX-RS resource with API method to generate 38 | * forecast for a given time series with at least one value. 39 | */ 40 | @Path("/impression-forecast-service/v1") 41 | @Produces(MediaType.APPLICATION_JSON) 42 | @Consumes(MediaType.APPLICATION_JSON) 43 | @Api( 44 | value = "Impression Forecast", 45 | description = "Produce an impression forecast given a historical impression time series and a list of canned set names", 46 | position = 5 47 | ) 48 | public class ImpressionForecastResource { 49 | 50 | private static final Logger LOG = LoggerFactory.getLogger(ImpressionForecastResource.class); 51 | 52 | private static final String ACCEPT_HEADERS = "accept"; 53 | @Context 54 | private HttpHeaders headers; 55 | @Context 56 | private HttpServletRequest httpServletRequest; 57 | 58 | 59 | /** 60 | * Generate easy forecast for give time series 61 | * 62 | * @return forecastResponse object 63 | */ 64 | @POST 65 | @Path("/canned-set-competition-forecast") 66 | @Timed 67 | @ExceptionMetered 68 | @ApiOperation(value = "Impression Forecast", 69 | notes = "Produce an impression forecast given a historical impression time series and a list of canned set names", 70 | response = ForecastResponse.class) 71 | @ApiResponses({ 72 | @ApiResponse(code = 200, message = "Impression Forecast successful"), 73 | @ApiResponse(code = 404, message = "Failed to calculate forecast"), 74 | @ApiResponse(code = 500, message = "Internal server error due to encoding the data"), 75 | @ApiResponse(code = 400, message = "Bad request due to decoding the data"), 76 | @ApiResponse(code = 412, message = "Pre condition failed due to required data not found")}) 77 | 78 | public Response generateForecast( 79 | @Valid @NotNull final ImpressionForecastRequest impressionForecastRequest) { 80 | long start = System.currentTimeMillis(); 81 | ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); 82 | String request = null; 83 | try { 84 | request = ow.writeValueAsString(impressionForecastRequest); 85 | } catch (JsonProcessingException ex) { 86 | return Response.status(Response.Status.BAD_REQUEST).build(); 87 | } 88 | 89 | ForecastResponse response = null; 90 | LOG.debug("Get a forecast for a given time series"); 91 | String json = null; 92 | try { 93 | 94 | 95 | response = ForecastService.impressionForecast(impressionForecastRequest, start); 96 | json = ow.writeValueAsString(response); 97 | 98 | if (headers.getRequestHeaders().get(HttpHeaders.ACCEPT).contains(MediaType.APPLICATION_JSON)) { 99 | if (response != null) { 100 | return Response.ok().entity(json).build(); 101 | } else { 102 | return Response.status(404).build(); 103 | } 104 | } 105 | } catch (JsonProcessingException e) { 106 | return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); 107 | } catch (IFSException ifsException) { 108 | Response.status(Response.Status.BAD_REQUEST).build(); 109 | } catch (Exception e) { 110 | LOG.error("Failed to generate easy forecast Error : " + e.getMessage(), e); 111 | String message = e.getMessage(); 112 | return Response.status(Response.Status.PRECONDITION_FAILED).entity(message).type("text/plain").build(); 113 | } 114 | return Response.ok().entity(json).build(); 115 | 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /server/src/main/java/com/aol/one/reporting/forecastapi/server/models/model/IFSNDaysBack.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright 2018, Oath Inc. 3 | * Licensed under the terms of the Apache Version 2.0 license. 4 | * See LICENSE file in project root directory for terms. 5 | ********************************************************************************/ 6 | 7 | package com.aol.one.reporting.forecastapi.server.models.model; 8 | 9 | /** 10 | * Class for processing the ndays_back common model parameter. 11 | */ 12 | public final class IFSNDaysBack { 13 | 14 | /** 15 | * Fetch ndays_back filtered series with a specification parameter. 16 | * The series will be reshaped according to the specification. The 17 | * specification is as follows: 18 | * 19 | * -- Percentage of most recent historical values to use. 20 | * Value must be > 0.0 and <= 1.0. 21 | * p<-fract> -- Percentage of least recent historical values to not use. 22 | * Value must be > -1.0 and < 0.0. 23 | * z -- Remove leading data in historical values up to and 24 | * including last zero as long as data removed is less than or equal 25 | * to the specified percentage of values. If there are no zeroes in 26 | * data or last zero exceeds percentage, no values are removed. Value 27 | * must be > 0.0 and < 1.0. 28 | * -- # most recent historical values to use. 29 | * -- Use all historical values. 30 | * -- # least recent historical values to not use. 31 | * 32 | * @param series Time series to reshape. 33 | * @param spec Reshaping specification. See above for possible values. 34 | * 35 | * @return Reshaped time series. 36 | * 37 | * @throws IFSException for invalid specifications or unexpected series 38 | * values. 39 | */ 40 | public static double[] getNDaysBackSeries( 41 | double[] series, 42 | String spec 43 | ) throws IFSException { 44 | 45 | if (series == null || series.length < 1) 46 | throw new IFSException(31); 47 | else if (spec == null || spec.equals("")) 48 | throw new IFSException(32); 49 | 50 | // Reshape the series. 51 | 52 | int nv = 0; 53 | int tnv = 0; 54 | double fraction = 0.0; 55 | 56 | // Process fractional specification. 57 | 58 | if (spec.charAt(0) == 'p') { 59 | try { 60 | fraction = Double.parseDouble(spec.substring(1)); 61 | } 62 | catch (NumberFormatException ex) { 63 | throw new IFSException(33); 64 | } 65 | if (fraction == 0.0) 66 | throw new IFSException(34); 67 | else if (fraction <= -1.0) 68 | throw new IFSException(35); 69 | else if (fraction > 1.0) 70 | throw new IFSException(36); 71 | 72 | if (fraction > 0.0) 73 | nv = (int)(fraction*series.length); 74 | else { 75 | fraction = -fraction; 76 | tnv = (int)(fraction*series.length); 77 | nv = series.length-tnv; 78 | } 79 | } 80 | 81 | // Process remove initial zeroes specification. 82 | 83 | else if (spec.charAt(0) == 'z') { 84 | int zpos; 85 | double zfrac; 86 | 87 | try { 88 | fraction = Double.parseDouble(spec.substring(1)); 89 | } 90 | catch (NumberFormatException ex) { 91 | throw new IFSException(37); 92 | } 93 | if (fraction <= 0.0 || fraction >= 1.0) 94 | throw new IFSException(38); 95 | 96 | zpos = 0; 97 | for (int i = 0; i < series.length; i++) 98 | if (series[i] == 0.0) 99 | zpos = i+1; 100 | zfrac = (double)zpos / (double)series.length; 101 | if (zfrac <= fraction) 102 | nv = series.length-zpos; 103 | else 104 | nv = series.length; 105 | } 106 | 107 | // Process integer specification. 108 | 109 | else { 110 | try { 111 | tnv = Integer.parseInt(spec); 112 | } 113 | catch (NumberFormatException ex) { 114 | throw new IFSException(39); 115 | } 116 | 117 | if (tnv == 0) 118 | nv = series.length; 119 | else if (tnv > 0) 120 | nv = (tnv > series.length) ? series.length : tnv; 121 | else 122 | nv = series.length-tnv; 123 | if (nv <= 1) 124 | nv = 2; 125 | } 126 | 127 | // Allocate and copy into new series shape. 128 | 129 | double[] new_series = new double[nv]; 130 | 131 | for (int i = series.length-nv, j = 0; i < series.length; j++, i++) 132 | new_series[j] = (i < 0) ? 0 : series[i]; 133 | return(new_series); 134 | } 135 | 136 | /** 137 | * Canned usage information for ndays_back parameter. 138 | * 139 | * @return Usage string. 140 | */ 141 | public static String usage() { 142 | return( 143 | "\n" 144 | + "ndays_back=\n" 145 | + " p -- Percentage of most recent historical values to use.\n" 146 | + " Value must be > 0.0 and <= 1.0.\n" 147 | + " p<-fract> -- Percentage of least recent historical values to not use.\n" 148 | + " Value must be > -1.0 and < 0.0.\n" 149 | + " z -- Remove leading data in historical values up to and\n" 150 | + " including last zero as long as data removed is less than or\n" 151 | + " equal to the specified percentage of values. If there are\n" 152 | + " no zeroes in data or last zero exceeds percentage, no values\n" 153 | + " are removed. Value must be > 0.0 and < 1.0.\n" 154 | + " -- # most recent historical values to use.\n" 155 | + " -- Use all historical values.\n" 156 | + " -- # least recent historical values to not use.\n" 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /server/src/main/webapp/doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI 5 | 7 | 9 | 11 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 78 | 79 | 80 | 81 | 106 | 107 |
 
108 | 109 | 114 | 150 |
151 | 152 | 153 | --------------------------------------------------------------------------------