├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── trend.rb └── trend │ ├── client.rb │ └── version.rb ├── test ├── test_helper.rb └── trend_test.rb └── trend.gemspec /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | services: 7 | trend: 8 | image: ankane/trend 9 | ports: 10 | - 8000:8000 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: 3.4 16 | bundler-cache: true 17 | - run: bundle exec rake test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.lock 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 (2025-04-03) 2 | 3 | - Removed default url 4 | - Dropped support for Ruby < 3.2 5 | 6 | ## 0.2.1 (2023-10-08) 7 | 8 | - Added warning about hosted version shutdown 9 | 10 | ## 0.2.0 (2023-05-03) 11 | 12 | - Dropped support for Ruby < 3 13 | 14 | ## 0.1.2 (2019-04-10) 15 | 16 | - Extended timeout 17 | - Added `timeout` option 18 | 19 | ## 0.1.1 (2018-10-28) 20 | 21 | - Added support for API keys 22 | - Added experimental `correlation` method 23 | 24 | ## 0.1.0 (2018-05-23) 25 | 26 | - First release 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "benchmark-ips" 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2025 Andrew Kane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trend Ruby 2 | 3 | Ruby client for [Trend](https://github.com/ankane/trend-api), the anomaly detection and forecasting API 4 | 5 | **Note: The [hosted version](https://trendapi.org/) is no longer available. [See how to run the API on your own infrastructure.](https://github.com/ankane/trend-api)** 6 | 7 | [![Build Status](https://github.com/ankane/trend-ruby/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/trend-ruby/actions) 8 | 9 | ## Installation 10 | 11 | Add this line to your application’s Gemfile: 12 | 13 | ```ruby 14 | gem "trend" 15 | ``` 16 | 17 | And set the URL to the API: 18 | 19 | ```ruby 20 | Trend.url = "http://localhost:8000" 21 | ``` 22 | 23 | ## Anomaly Detection 24 | 25 | Detect anomalies in a time series 26 | 27 | ```ruby 28 | # generate series 29 | series = {} 30 | date = Date.parse("2025-01-01") 31 | 28.times do 32 | series[date] = rand(100) 33 | date += 1 34 | end 35 | 36 | # add an anomaly on Apr 21 37 | series[date - 8] = 999 38 | 39 | Trend.anomalies(series) 40 | ``` 41 | 42 | Works great with [Groupdate](https://github.com/ankane/groupdate) 43 | 44 | ```ruby 45 | series = User.group_by_day(:created_at).count 46 | Trend.anomalies(series) 47 | ``` 48 | 49 | ## Forecasting 50 | 51 | Get future predictions for a time series 52 | 53 | ```ruby 54 | series = {} 55 | date = Date.parse("2025-01-01") 56 | 28.times do 57 | series[date] = date.wday 58 | date += 1 59 | end 60 | 61 | Trend.forecast(series) 62 | ``` 63 | 64 | Also works great with Groupdate 65 | 66 | ```ruby 67 | series = User.group_by_day(:created_at).count 68 | Trend.forecast(series) 69 | ``` 70 | 71 | Specify the number of predictions to return 72 | 73 | ```ruby 74 | Trend.forecast(series, count: 3) 75 | ``` 76 | 77 | ## Correlation (Experimental) 78 | 79 | Get the correlation between two time series 80 | 81 | ```ruby 82 | Trend.correlation(series, series2) 83 | ``` 84 | 85 | ## History 86 | 87 | View the [changelog](https://github.com/ankane/trend-ruby/blob/master/CHANGELOG.md) 88 | 89 | ## Contributing 90 | 91 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 92 | 93 | - [Report bugs](https://github.com/ankane/trend-ruby/issues) 94 | - Fix bugs and [submit pull requests](https://github.com/ankane/trend-ruby/pulls) 95 | - Write, clarify, or fix documentation 96 | - Suggest or add new features 97 | 98 | To get started with development: 99 | 100 | ```sh 101 | git clone https://github.com/ankane/trend-ruby.git 102 | cd trend-ruby 103 | bundle install 104 | bundle exec rake test 105 | ``` 106 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task default: :test 11 | 12 | task :benchmark do 13 | require "benchmark/ips" 14 | require "trend" 15 | 16 | series = {} 17 | date = Date.parse("2018-01-01") 18 | 1000.times do 19 | series[date] = rand(100) 20 | date += 1 21 | end 22 | 23 | Trend.url ||= "http://localhost:8000" 24 | 25 | Benchmark.ips do |x| 26 | x.report("anomalies") { Trend.anomalies(series) } 27 | x.report("forecast") { Trend.forecast(series) } 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/trend.rb: -------------------------------------------------------------------------------- 1 | # stdlib 2 | require "date" 3 | require "json" 4 | require "net/http" 5 | require "time" 6 | 7 | # modules 8 | require_relative "trend/client" 9 | require_relative "trend/version" 10 | 11 | module Trend 12 | class Error < StandardError; end 13 | 14 | def self.anomalies(*args) 15 | client.anomalies(*args) 16 | end 17 | 18 | def self.forecast(*args) 19 | client.forecast(*args) 20 | end 21 | 22 | def self.correlation(*args) 23 | client.correlation(*args) 24 | end 25 | 26 | def self.url 27 | @url ||= ENV["TREND_URL"] 28 | end 29 | 30 | def self.url=(url) 31 | @url = url 32 | @client = nil 33 | end 34 | 35 | def self.api_key 36 | @api_key ||= ENV["TREND_API_KEY"] 37 | end 38 | 39 | def self.api_key=(api_key) 40 | @api_key = api_key 41 | @client = nil 42 | end 43 | 44 | # private 45 | def self.client 46 | @client ||= Client.new 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/trend/client.rb: -------------------------------------------------------------------------------- 1 | require "trend/version" 2 | 3 | module Trend 4 | class Client 5 | HEADERS = { 6 | "Content-Type" => "application/json", 7 | "Accept" => "application/json", 8 | "User-Agent" => "trend-ruby/#{Trend::VERSION}" 9 | } 10 | 11 | def initialize(url: nil, api_key: nil, timeout: 30) 12 | @api_key = api_key || Trend.api_key 13 | url ||= Trend.url 14 | if !url 15 | raise ArgumentError, "Trend url not set" 16 | end 17 | @uri = URI.parse(url) 18 | @http = Net::HTTP.new(@uri.host, @uri.port) 19 | @http.use_ssl = true if @uri.scheme == "https" 20 | @http.open_timeout = timeout 21 | @http.read_timeout = timeout 22 | end 23 | 24 | def anomalies(series, params = {}) 25 | resp = make_request("anomalies", series, params) 26 | resp["anomalies"].map { |v| parse_time(v) } 27 | end 28 | 29 | def forecast(series, params = {}) 30 | resp = make_request("forecast", series, params) 31 | Hash[resp["forecast"].map { |k, v| [parse_time(k), v] }] 32 | end 33 | 34 | def correlation(series, series2, params = {}) 35 | resp = make_request("correlation", series, params.merge(series2: series2)) 36 | resp["correlation"] 37 | end 38 | 39 | private 40 | 41 | def make_request(path, series, params) 42 | post_data = { 43 | series: series 44 | }.merge(params) 45 | 46 | path = "#{path}?#{URI.encode_www_form(api_key: @api_key)}" if @api_key 47 | 48 | begin 49 | response = @http.post("/#{path}", post_data.to_json, HEADERS) 50 | rescue Errno::ECONNREFUSED, Timeout::Error => e 51 | raise Trend::Error, e.message 52 | end 53 | 54 | parsed_body = JSON.parse(response.body) rescue {} 55 | 56 | if !response.is_a?(Net::HTTPSuccess) 57 | raise Trend::Error, parsed_body["error"] || "Server returned #{response.code} response" 58 | end 59 | 60 | parsed_body 61 | end 62 | 63 | def parse_time(v) 64 | v.size == 10 ? Date.parse(v) : Time.parse(v) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/trend/version.rb: -------------------------------------------------------------------------------- 1 | module Trend 2 | VERSION = "0.3.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | Bundler.require(:default) 3 | require "minitest/autorun" 4 | require "minitest/pride" 5 | 6 | Trend.url ||= "http://localhost:8000" 7 | -------------------------------------------------------------------------------- /test/trend_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class TrendTest < Minitest::Test 4 | def test_anomalies 5 | series = {} 6 | date = Date.parse("2018-04-01") 7 | 28.times do 8 | series[date] = rand(100) 9 | date += 1 10 | end 11 | series[date - 13] = nil # test nil 12 | series[date - 8] = 999 13 | 14 | assert_equal [Date.parse("2018-04-21")], Trend.anomalies(series) 15 | end 16 | 17 | def test_forecast 18 | series = {} 19 | date = Date.parse("2018-04-01") 20 | 28.times do 21 | series[date] = date.wday 22 | date += 1 23 | end 24 | series.delete(date - 13) # test missing 25 | 26 | forecast = Trend.forecast(series, count: 7) 27 | assert_equal [0, 1, 2, 3, 4, 5, 6], forecast.values 28 | end 29 | 30 | def test_correlation 31 | series = {} 32 | date = Date.parse("2018-04-01") 33 | 28.times do 34 | series[date] = date.wday 35 | date += 1 36 | end 37 | 38 | series2 = series.dup 39 | series2[date - 8] = 10 40 | 41 | correlation = Trend.correlation(series, series2) 42 | assert_equal 0.9522, correlation 43 | end 44 | 45 | def test_correlation_exact 46 | series = {} 47 | date = Date.parse("2018-04-01") 48 | 28.times do 49 | series[date] = date.wday 50 | date += 1 51 | end 52 | 53 | # test positive 54 | correlation = Trend.correlation(series, series) 55 | assert_equal 1, correlation 56 | 57 | # test negative 58 | correlation = Trend.correlation(series, Hash[series.map { |k, v| [k, -v] }]) 59 | assert_equal(-1, correlation) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /trend.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/trend/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "trend" 5 | spec.version = Trend::VERSION 6 | spec.summary = "Ruby client for Trend, the time series API" 7 | spec.homepage = "https://github.com/ankane/trend-ruby" 8 | spec.license = "MIT" 9 | 10 | spec.author = "Andrew Kane" 11 | spec.email = "andrew@ankane.org" 12 | 13 | spec.files = Dir["*.{md,txt}", "{lib}/**/*"] 14 | spec.require_path = "lib" 15 | 16 | spec.required_ruby_version = ">= 3.2" 17 | end 18 | --------------------------------------------------------------------------------