├── .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 | [](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 |
--------------------------------------------------------------------------------