├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── contract.rb │ ├── prediction.rb │ ├── dump_data_point_row.rb │ ├── prediction_data_point_row.rb │ ├── data_point_row.rb │ ├── bike_data_point.rb │ ├── weather_data_point.rb │ └── station.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── contracts_controller.rb │ ├── application_controller.rb │ └── stations_controller.rb └── serializers │ ├── station_reduced_serializer.rb │ ├── prediction_serializer.rb │ ├── contract_serializer.rb │ └── station_serializer.rb ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ ├── dump.rake │ ├── predict.rake │ └── fetch.rake ├── datetime.rb ├── forecast.rb ├── holiday.rb └── dump.rb ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── contract_test.rb │ ├── prediction_test.rb │ ├── data_point_row_test.rb │ ├── weather_data_point_test.rb │ ├── bike_data_point_test.rb │ └── station_test.rb ├── controllers │ ├── .keep │ ├── stations_controller_test.rb │ └── contracts_controller_test.rb ├── fixtures │ ├── .keep │ ├── contracts.yml │ ├── bike_data_points.yml │ ├── predictions.yml │ ├── weather_data_points.yml │ ├── stations.yml │ └── data_point_rows.yml ├── integration │ └── .keep └── test_helper.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── prediction ├── requirements.txt ├── rows.py ├── prediction.py └── worker.py ├── bin ├── bundle ├── rake ├── rails ├── spring └── setup ├── config ├── boot.rb ├── initializers │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── backtrace_silencers.rb │ ├── api_keys.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ └── inflections.rb ├── environment.rb ├── api_keys.yml.example ├── routes.rb ├── schedule.rb ├── locales │ └── en.yml ├── secrets.yml ├── application.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── database.yml.example ├── config.ru ├── db ├── migrate │ ├── 20150417083241_add_number_to_stations.rb │ ├── 20150818124538_add_index_on_kind_to_predictions.rb │ ├── 20150804133645_add_last_dump_field_to_station.rb │ ├── 20150417073031_add_contract_to_station.rb │ ├── 20150817131035_add_kind_field_to_prediction.rb │ ├── 20150417085733_add_station_to_bike_data_points.rb │ ├── 20150420074234_add_last_entry_to_stations.rb │ ├── 20150417072704_create_contracts.rb │ ├── 20150417142658_add_latitude_and_longitude_to_contracts.rb │ ├── 20150417095902_stations_last_update_big_int.rb │ ├── 20150807141006_create_predictions.rb │ ├── 20150417085653_create_bike_data_points.rb │ ├── 20150417072838_create_stations.rb │ ├── 20150417085457_create_weather_data_points.rb │ └── 20150807085437_create_data_point_rows.rb ├── seeds.rb └── schema.rb ├── Rakefile ├── .gitignore ├── Gemfile ├── LICENSE ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prediction/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pandas 3 | scipy 4 | scikit-learn 5 | redis 6 | psycopg2 7 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/models/contract.rb: -------------------------------------------------------------------------------- 1 | class Contract < ActiveRecord::Base 2 | has_many :stations, dependent: :destroy 3 | has_many :weather_data_points, dependent: :destroy 4 | end 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/models/contract_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContractTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /test/models/prediction_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PredictionTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_velib_session' 4 | -------------------------------------------------------------------------------- /test/models/data_point_row_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DataPointRowTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /db/migrate/20150417083241_add_number_to_stations.rb: -------------------------------------------------------------------------------- 1 | class AddNumberToStations < ActiveRecord::Migration 2 | def change 3 | add_column :stations, :number, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/contracts.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: MyString 5 | 6 | two: 7 | name: MyString 8 | -------------------------------------------------------------------------------- /app/controllers/contracts_controller.rb: -------------------------------------------------------------------------------- 1 | class ContractsController < ApplicationController 2 | def index 3 | @contracts = Contract.all 4 | 5 | render json: @contracts 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20150818124538_add_index_on_kind_to_predictions.rb: -------------------------------------------------------------------------------- 1 | class AddIndexOnKindToPredictions < ActiveRecord::Migration 2 | def change 3 | add_index :predictions, :kind 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/serializers/station_reduced_serializer.rb: -------------------------------------------------------------------------------- 1 | class StationReducedSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :address, :bike_stands, :latitude, :longitude, :contract_id 3 | end 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require_relative '../config/boot' 7 | require 'rake' 8 | Rake.application.run 9 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /db/migrate/20150804133645_add_last_dump_field_to_station.rb: -------------------------------------------------------------------------------- 1 | class AddLastDumpFieldToStation < ActiveRecord::Migration 2 | def change 3 | add_column :stations, :last_dump, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/stations_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StationsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/contracts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContractsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/serializers/prediction_serializer.rb: -------------------------------------------------------------------------------- 1 | class PredictionSerializer < ActiveModel::Serializer 2 | attributes :timestamp, :available_bikes 3 | 4 | def timestamp 5 | self.object.datetime.to_i 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20150417073031_add_contract_to_station.rb: -------------------------------------------------------------------------------- 1 | class AddContractToStation < ActiveRecord::Migration 2 | def change 3 | add_reference :stations, :contract, index: true, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150817131035_add_kind_field_to_prediction.rb: -------------------------------------------------------------------------------- 1 | class AddKindFieldToPrediction < ActiveRecord::Migration 2 | def change 3 | add_column :predictions, :kind, :string, default: "scikit_lasso" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/api_keys.yml.example: -------------------------------------------------------------------------------- 1 | jcdecaux: 2 | openweathermap: 3 | forecastio: 4 | -------------------------------------------------------------------------------- /db/migrate/20150417085733_add_station_to_bike_data_points.rb: -------------------------------------------------------------------------------- 1 | class AddStationToBikeDataPoints < ActiveRecord::Migration 2 | def change 3 | add_reference :bike_data_points, :station, index: true, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150420074234_add_last_entry_to_stations.rb: -------------------------------------------------------------------------------- 1 | class AddLastEntryToStations < ActiveRecord::Migration 2 | def change 3 | add_column :stations, :last_entry, :integer 4 | add_index :stations, :last_entry 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /app/serializers/contract_serializer.rb: -------------------------------------------------------------------------------- 1 | class ContractSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :latitude, :longitude, :stations_count 3 | 4 | def stations_count 5 | self.object.stations.count 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :contracts, only: [:index] do 3 | resources :stations, only: [:index] 4 | end 5 | 6 | resources :stations, only: [:show] 7 | 8 | root "contracts#index" 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /db/migrate/20150417072704_create_contracts.rb: -------------------------------------------------------------------------------- 1 | class CreateContracts < ActiveRecord::Migration 2 | def change 3 | create_table :contracts do |t| 4 | t.string :name 5 | 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require_relative '../config/boot' 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150417142658_add_latitude_and_longitude_to_contracts.rb: -------------------------------------------------------------------------------- 1 | class AddLatitudeAndLongitudeToContracts < ActiveRecord::Migration 2 | def change 3 | add_column :contracts, :latitude, :float 4 | add_column :contracts, :longitude, :float 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/models/prediction.rb: -------------------------------------------------------------------------------- 1 | class Prediction < ActiveRecord::Base 2 | belongs_to :station 3 | 4 | scope :today, -> { where("predictions.datetime >= ? and predictions.datetime <= ?", DateTime.now.beginning_of_day, DateTime.now.end_of_day) } 5 | scope :of_kind, ->(kind) { where(kind: kind) } 6 | end 7 | -------------------------------------------------------------------------------- /test/fixtures/bike_data_points.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | available_bikes: 1 5 | open: false 6 | weather_data_point_id: 7 | 8 | two: 9 | available_bikes: 1 10 | open: false 11 | weather_data_point_id: 12 | -------------------------------------------------------------------------------- /db/migrate/20150417095902_stations_last_update_big_int.rb: -------------------------------------------------------------------------------- 1 | class StationsLastUpdateBigInt < ActiveRecord::Migration 2 | def up 3 | change_column :stations, :last_update, :bigint 4 | end 5 | 6 | def down 7 | change_column :stations, :last_update, :integer 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/predictions.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | station_id: 5 | datetime: 2015-08-07 16:10:06 6 | available_bikes: 1 7 | 8 | two: 9 | station_id: 10 | datetime: 2015-08-07 16:10:06 11 | available_bikes: 1 12 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | # Learn more: http://github.com/javan/whenever 2 | 3 | every 10.minutes do 4 | rake "fetch", :environment => "development" 5 | end 6 | 7 | every 1.day, at: "11:20 pm" do 8 | rake "dump:all", :environment => "development" 9 | rake "predict:all", :environment => "development" 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20150807141006_create_predictions.rb: -------------------------------------------------------------------------------- 1 | class CreatePredictions < ActiveRecord::Migration 2 | def change 3 | create_table :predictions do |t| 4 | t.references :station, index: true, foreign_key: true 5 | t.datetime :datetime 6 | t.integer :available_bikes 7 | 8 | t.timestamps null: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/weather_data_points.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | weather: MyString 5 | temperature: 1.5 6 | wind_speed: 1.5 7 | humidity: 1 8 | contract_id: 9 | 10 | two: 11 | weather: MyString 12 | temperature: 1.5 13 | wind_speed: 1.5 14 | humidity: 1 15 | contract_id: 16 | -------------------------------------------------------------------------------- /db/migrate/20150417085653_create_bike_data_points.rb: -------------------------------------------------------------------------------- 1 | class CreateBikeDataPoints < ActiveRecord::Migration 2 | def change 3 | create_table :bike_data_points do |t| 4 | t.integer :available_bikes 5 | t.boolean :open 6 | t.references :weather_data_point, index: true, foreign_key: true 7 | 8 | t.timestamps null: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/stations_controller.rb: -------------------------------------------------------------------------------- 1 | class StationsController < ApplicationController 2 | def index 3 | @stations = Station.where(contract_id: params[:contract_id]) 4 | 5 | render json: @stations, each_serializer: StationReducedSerializer 6 | end 7 | 8 | def show 9 | @station = Station.find(params[:id]) 10 | 11 | render json: @station 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20150417072838_create_stations.rb: -------------------------------------------------------------------------------- 1 | class CreateStations < ActiveRecord::Migration 2 | def change 3 | create_table :stations do |t| 4 | t.string :name 5 | t.string :address 6 | t.float :latitude 7 | t.float :longitude 8 | t.integer :bike_stands 9 | t.integer :last_update 10 | 11 | t.timestamps null: false 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/stations.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: MyString 5 | address: MyString 6 | latitude: 1.5 7 | longitude: 1.5 8 | bike_stands: 1 9 | last_update: 1 10 | 11 | two: 12 | name: MyString 13 | address: MyString 14 | latitude: 1.5 15 | longitude: 1.5 16 | bike_stands: 1 17 | last_update: 1 18 | -------------------------------------------------------------------------------- /db/migrate/20150417085457_create_weather_data_points.rb: -------------------------------------------------------------------------------- 1 | class CreateWeatherDataPoints < ActiveRecord::Migration 2 | def change 3 | create_table :weather_data_points do |t| 4 | t.string :weather 5 | t.float :temperature 6 | t.float :wind_speed 7 | t.integer :humidity 8 | t.references :contract, index: true, foreign_key: true 9 | 10 | t.timestamps null: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/models/dump_data_point_row.rb: -------------------------------------------------------------------------------- 1 | class DumpDataPointRow < DataPointRow 2 | def self.new_from_dump(bike_datapoint, holidays) 3 | row = DumpDataPointRow.new 4 | 5 | timestamp = bike_datapoint.created_at.to_datetime 6 | 7 | row.station = bike_datapoint.station 8 | row.open = bike_datapoint.open ? 1 : 0 9 | row.available_bikes = bike_datapoint.available_bikes 10 | 11 | row.build(timestamp, bike_datapoint.weather_data_point, holidays) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/prediction_data_point_row.rb: -------------------------------------------------------------------------------- 1 | class PredictionDataPointRow < DataPointRow 2 | def self.new_for_prediction(station, datetime, hours_ahead, holidays, forecast) 3 | row = PredictionDataPointRow.new 4 | 5 | timestamp = datetime.beginning_of_hour + hours_ahead.hours 6 | weather_data_point = forecast.weather_data_point(timestamp) 7 | 8 | row.open = station.last_entry.open ? 1 : 0 9 | 10 | row.build(timestamp, weather_data_point, holidays) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) 11 | Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } 12 | gem "spring", match[1] 13 | require "spring/binstub" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/serializers/station_serializer.rb: -------------------------------------------------------------------------------- 1 | class StationSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :address, :bike_stands, :latitude, :longitude, :contract_id, :last_entry 3 | has_many :predictions 4 | 5 | def last_entry 6 | { 7 | :timestamp => self.object.last_entry.created_at.to_i, 8 | :availableBikeStands => self.object.last_entry.available_bikes 9 | } 10 | end 11 | 12 | def predictions 13 | self.object.predictions.today.of_kind("scikit_lasso").order(:datetime) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | !/log/.keep 13 | /tmp 14 | 15 | *.pyc 16 | */venv 17 | 18 | config/api_keys.yml 19 | config/database.yml 20 | -------------------------------------------------------------------------------- /config/initializers/api_keys.rb: -------------------------------------------------------------------------------- 1 | module ApiKeys 2 | config_path = "#{Rails.root}/config/api_keys.yml" 3 | if File.exists?(config_path) 4 | @config = YAML.load_file(config_path).symbolize_keys 5 | 6 | # define JCDECAUX, OPENWEATHERMAP & FORECASTIO constants 7 | @config.each do |key, value| 8 | const_set key.upcase, value 9 | end 10 | end 11 | 12 | # better to run on Heroku 13 | ENV.each do |var, value| 14 | if var.end_with?("_APIKEY") 15 | const_set var[0..-8], value 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /lib/tasks/dump.rake: -------------------------------------------------------------------------------- 1 | require './lib/dump' 2 | 3 | namespace :dump do 4 | desc "Dump station with ID given as an environment variable" 5 | task :station => :environment do 6 | dump = Dump.new(60) 7 | dump.station(Station.find_by(id: ENV['ID'])) 8 | end 9 | 10 | desc "Dump all stations" 11 | task :all => :environment do 12 | puts "Dumping all stations..." 13 | 14 | stations = Station.all 15 | dump = Dump.new(60) 16 | stations.each { |st| dump.station(st) } 17 | 18 | puts "Dumped all stations." 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /lib/datetime.rb: -------------------------------------------------------------------------------- 1 | class DateTime 2 | # http://stackoverflow.com/questions/15414831/ruby-determine-season-fall-winter-spring-or-summer#answer-15416170 3 | def season 4 | day_hash = month * 100 + mday 5 | case day_hash 6 | when 101..401 then :winter 7 | when 402..630 then :spring 8 | when 701..930 then :summer 9 | when 1001..1231 then :fall 10 | end 11 | end 12 | 13 | def week 14 | strftime("%U").to_i 15 | end 16 | 17 | def season_mapping 18 | mapping = { 19 | :winter => 0, :spring => 1, :summer => 2, :fall => 3 20 | } 21 | 22 | mapping[self.season] || mapping.values.max + 1 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /db/migrate/20150807085437_create_data_point_rows.rb: -------------------------------------------------------------------------------- 1 | class CreateDataPointRows < ActiveRecord::Migration 2 | def change 3 | create_table :data_point_rows do |t| 4 | t.integer :open 5 | t.integer :weather 6 | t.decimal :temperature 7 | t.decimal :wind_speed 8 | t.integer :humidity 9 | t.integer :hour 10 | t.integer :minute 11 | t.integer :day_of_week 12 | t.integer :week_number 13 | t.integer :season 14 | t.integer :weekend 15 | t.integer :holiday 16 | t.integer :available_bikes 17 | t.references :station, index: true, foreign_key: true 18 | t.string :type 19 | 20 | t.timestamps null: false 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/fixtures/data_point_rows.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | open: false 5 | weather: 1 6 | temperature: 9.99 7 | wind_speed: 9.99 8 | humidity: 1 9 | hour: 1 10 | minute: 1 11 | day_of_week: 1 12 | week_number: 1 13 | season: 1 14 | weekend: false 15 | holiday: false 16 | available_bikes: 1 17 | station_id: 18 | type: 19 | 20 | two: 21 | open: false 22 | weather: 1 23 | temperature: 9.99 24 | wind_speed: 9.99 25 | humidity: 1 26 | hour: 1 27 | minute: 1 28 | day_of_week: 1 29 | week_number: 1 30 | season: 1 31 | weekend: false 32 | holiday: false 33 | available_bikes: 1 34 | station_id: 35 | type: 36 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /app/models/data_point_row.rb: -------------------------------------------------------------------------------- 1 | class DataPointRow < ActiveRecord::Base 2 | belongs_to :station 3 | 4 | def build(timestamp, weather_data_point, holidays) 5 | self.assign_attributes({ 6 | weather: weather_data_point.weather_mapping, 7 | temperature: weather_data_point.temperature, 8 | wind_speed: weather_data_point.wind_speed, 9 | humidity: weather_data_point.humidity, 10 | 11 | hour: timestamp.hour, 12 | minute: timestamp.minute, 13 | day_of_week: timestamp.wday, 14 | week_number: timestamp.week, 15 | season: timestamp.season_mapping, 16 | weekend: [0, 6].include?(timestamp.wday) ? 1 : 0, 17 | holiday: holidays.holiday?(timestamp) ? 1 : 0 18 | }) 19 | 20 | return self 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/models/weather_data_point_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class WeatherDataPointTest < ActiveSupport::TestCase 4 | test "create_empty_bad" do 5 | data = {} 6 | 7 | contract = Contract.take 8 | 9 | assert_not WeatherDataPoint.create_from_json(data, contract).valid? 10 | end 11 | 12 | test "create_good" do 13 | data = { 14 | "weather" => [{ 15 | "main" => "Sunny" 16 | }], 17 | 18 | "main" => { 19 | "temp" => 291.5, 20 | "humidity" => 33, 21 | }, 22 | 23 | "wind" => { 24 | "speed" => 2.2 25 | } 26 | } 27 | 28 | contract = Contract.take 29 | 30 | assert WeatherDataPoint.create_from_json(data, contract).valid? 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /lib/forecast.rb: -------------------------------------------------------------------------------- 1 | module ForecastIO 2 | 3 | class Forecast 4 | attr_reader :weather_data_points 5 | 6 | def initialize(contract, date) 7 | ForecastIO.api_key = ApiKeys::FORECASTIO 8 | data = ForecastIO.forecast( 9 | contract.latitude, contract.longitude, 10 | time: date.to_datetime, params: { units: 'si' } 11 | ) 12 | 13 | @weather_data_points = data.hourly.data.map do |forecast_data| 14 | datapoint = WeatherDataPoint.forecast_io_new(forecast_data, contract) 15 | datapoint.created_at = DateTime.strptime("#{forecast_data.time}", "%s") 16 | datapoint 17 | end 18 | end 19 | 20 | # returns closest weather_data_point to datetime 21 | def weather_data_point(datetime) 22 | @weather_data_points.each do |point| 23 | return point if point.created_at >= datetime 24 | end 25 | 26 | @weather_data_points.last 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | # CHANGE BEFORE DEPLOYING, RUN `rake secret` 15 | secret_key_base: 16 | 17 | test: 18 | # CHANGE BEFORE DEPLOYING, RUN `rake secret` 19 | secret_key_base: 20 | 21 | # Do not keep production secrets in the repository, 22 | # instead read values from the environment. 23 | production: 24 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 25 | -------------------------------------------------------------------------------- /test/models/bike_data_point_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BikeDataPointTest < ActiveSupport::TestCase 4 | test "create_empty_bad" do 5 | data = {} 6 | 7 | weather_data_point = WeatherDataPoint.take 8 | 9 | assert_not BikeDataPoint.create_from_json(data, weather_data_point).valid? 10 | end 11 | 12 | test "create_bad" do 13 | data = { 14 | "last_update" => 42 15 | } 16 | 17 | weather_data_point = WeatherDataPoint.take 18 | 19 | assert_not BikeDataPoint.create_from_json(data, weather_data_point).valid? 20 | end 21 | 22 | test "create_good" do 23 | data = { 24 | "last_update" => 45, 25 | "number" => 42, 26 | "available_bike_stands" => 12, 27 | "status" => "CLOSED" 28 | } 29 | 30 | weather_data_point = WeatherDataPoint.take 31 | 32 | Station.create(number: 42, name: "AA", bike_stands: 12) 33 | 34 | assert BikeDataPoint.create_from_json(data, weather_data_point).valid? 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/holiday.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'nokogiri' 3 | require 'time' 4 | 5 | module Holiday 6 | 7 | class Holiday 8 | def initialize(from, to) 9 | @from = from 10 | @to = to 11 | end 12 | 13 | def holiday?(time) 14 | @from <= time and time <= @to 15 | end 16 | end 17 | 18 | class Holidays 19 | def initialize(holidays) 20 | @holidays = holidays 21 | end 22 | 23 | def holiday?(time) 24 | @holidays.any? { |h| h.holiday?(time) } 25 | end 26 | end 27 | 28 | def self.query 29 | url = "http://telechargement.index-education.com/vacances.xml" 30 | 31 | uri = URI.parse(url) 32 | response = Net::HTTP.get(uri) 33 | 34 | Nokogiri::XML(response) 35 | end 36 | 37 | def self.parse(document) 38 | zone = "C" # Paris 39 | 40 | holidays = document.xpath("//zone[@libelle=\"#{zone}\"]//vacances") 41 | 42 | h = holidays.map do |holiday| 43 | Holiday.new(Time.strptime(holiday["debut"], "%Y/%m/%d"), Time.strptime(holiday["fin"], "%Y/%m/%d")) 44 | end 45 | 46 | Holidays.new(h) 47 | end 48 | 49 | def self.holidays 50 | parse(query) 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Velib 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | 23 | # Do not swallow errors in after_commit/after_rollback callbacks. 24 | config.active_record.raise_in_transactional_callbacks = true 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/models/bike_data_point.rb: -------------------------------------------------------------------------------- 1 | require './lib/datetime' 2 | 3 | class BikeDataPoint < ActiveRecord::Base 4 | belongs_to :weather_data_point 5 | belongs_to :station 6 | 7 | validates :available_bikes, presence: true 8 | validates :open, :inclusion => {:in => [true, false]} 9 | 10 | scope :today, -> { where("bike_data_points.created_at >= ? and bike_data_points.created_at <= ?", DateTime.now.beginning_of_day, DateTime.now.end_of_day) } 11 | 12 | def self.jcdecaux_new(json, weather_data_point) 13 | last_update = json["last_update"] 14 | number = json["number"] 15 | 16 | return if last_update.nil? or number.nil? 17 | 18 | station = Station.find_by(number: number) 19 | 20 | # station not foudn or data already in database 21 | return if station.nil? or station.last_update == last_update 22 | 23 | available_bikes = json["available_bikes"] 24 | open = json["status"] == "OPEN" 25 | 26 | # unix epoch to ruby datetime object 27 | timestamp = Time.at(last_update / 1000).to_datetime 28 | 29 | BikeDataPoint.new( 30 | available_bikes: available_bikes, 31 | open: open, 32 | station: station, 33 | weather_data_point: weather_data_point, 34 | created_at: timestamp 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/models/station_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StationTest < ActiveSupport::TestCase 4 | test "create_empty_bad" do 5 | data = {} 6 | 7 | contract = Contract.take 8 | 9 | assert_not Station.create_from_json_if_not_exists(data, contract).valid? 10 | end 11 | 12 | test "create_bad" do 13 | data = { 14 | "number" => 42, 15 | "name" => "Brave new station 1", 16 | "address" => "20 Rue Alo", 17 | 18 | "position" => { 19 | "lat" => 48.85, 20 | "lng" => 2.35 21 | } 22 | } 23 | 24 | contract = Contract.take 25 | 26 | assert_difference "Station.count", 0 do 27 | Station.create_from_json_if_not_exists(data, contract) 28 | end 29 | end 30 | 31 | test "create_good" do 32 | data = { 33 | "number" => 42, 34 | "name" => "Brave new station 2", 35 | "address" => "20 Rue Alo", 36 | 37 | "position" => { 38 | "lat" => 48.85, 39 | "lng" => 2.35 40 | }, 41 | 42 | "bike_stands" => 42 43 | } 44 | 45 | contract = Contract.take 46 | 47 | assert Station.create_from_json_if_not_exists(data, contract).valid? 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/tasks/predict.rake: -------------------------------------------------------------------------------- 1 | require 'holiday' 2 | require 'forecast' 3 | require 'redis' 4 | 5 | namespace :predict do 6 | task :all => :environment do 7 | puts "Predicting all stations..." 8 | 9 | redis = Redis.new(url: "redis://localhost:6379/0") 10 | 11 | hours_range = 24 12 | date = DateTime.now.hour < 12 ? Date.today : Date.tomorrow # already past midday? predict for tomorrow 13 | action_datetime = date.to_datetime 14 | 15 | holidays = Holiday.holidays 16 | 17 | Contract.all.each do |contract| 18 | forecast = ForecastIO::Forecast.new(contract, date) 19 | 20 | contract.stations.each do |station| 21 | rows = (0..hours_range-1).map do |hour| 22 | PredictionDataPointRow.new_for_prediction(station, action_datetime, hour, holidays, forecast) 23 | end 24 | 25 | results = PredictionDataPointRow.import(rows) 26 | 27 | metadata = { 28 | :action_timestamp => action_datetime.to_i, 29 | :hours_range => hours_range, 30 | :prediction_table => Prediction.table_name, 31 | :rows_table => DumpDataPointRow.table_name, 32 | :station_id => station.id, 33 | :ids => results.ids, 34 | } 35 | 36 | redis.publish("prediction", metadata.to_json) 37 | end 38 | end 39 | 40 | puts "Predicted all stations." 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /prediction/rows.py: -------------------------------------------------------------------------------- 1 | class Row: 2 | def __init__(self, result): 3 | self.result = result 4 | 5 | def to_json(self): 6 | return { column: float(value) for column, value in zip(Row.columns(), self.result) } 7 | 8 | @classmethod 9 | def columns(self): 10 | return ["open", "weather", "temperature", "wind_speed", "humidity", "hour", "minute", 11 | "day_of_week", "week_number", "season", "weekend", "holiday", "available_bikes"] 12 | 13 | 14 | class DumpRow(Row): 15 | @classmethod 16 | def fetch_all(self, cursor, table, station_id): 17 | cursor.execute("SELECT %s FROM %s WHERE type = 'DumpDataPointRow' AND station_id = %d ORDER BY created_at" % (", ".join(Row.columns()), table, station_id)) 18 | 19 | return [DumpRow(result) for result in cursor.fetchall()] 20 | 21 | 22 | class PredictionRow(Row): 23 | def to_json(self): 24 | return { column: float(value) for column, value in zip(PredictionRow.columns(), self.result) } 25 | 26 | @classmethod 27 | def fetch_all(self, cursor, table, ids): 28 | _ids = [str(i) for i in ids] 29 | cursor.execute("SELECT %s FROM %s WHERE type = 'PredictionDataPointRow' AND id IN (%s)" % (", ".join(PredictionRow.columns()), table, ",".join(_ids))) 30 | 31 | return [PredictionRow(result) for result in cursor.fetchall()] 32 | 33 | @classmethod 34 | def columns(self): 35 | return Row.columns()[:-1] # do not take `available_bikes` column 36 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 5 | gem 'rails', '4.2.1' 6 | # Use postgresql as the database for Active Record 7 | gem 'pg' 8 | # Use SCSS for stylesheets 9 | gem 'sass-rails', '~> 5.0' 10 | # Use Uglifier as compressor for JavaScript assets 11 | gem 'uglifier', '>= 1.3.0' 12 | # Use CoffeeScript for .coffee assets and views 13 | gem 'coffee-rails', '~> 4.1.0' 14 | # See https://github.com/rails/execjs#readme for more supported runtimes 15 | # gem 'therubyracer', platforms: :ruby 16 | 17 | # Use jquery as the JavaScript library 18 | gem 'jquery-rails' 19 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 20 | gem 'turbolinks' 21 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 22 | gem 'jbuilder', '~> 2.0' 23 | # bundle exec rake doc:rails generates the API under doc/api. 24 | gem 'sdoc', '~> 0.4.0', group: :doc 25 | 26 | gem 'whenever' 27 | gem 'nokogiri' 28 | gem 'activerecord-import' 29 | gem 'redis' 30 | gem 'active_model_serializers' 31 | gem 'forecast_io' 32 | 33 | group :development, :test do 34 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 35 | gem 'byebug' 36 | 37 | # Access an IRB console on exception pages or by using <%= console %> in views 38 | gem 'web-console', '~> 2.0' 39 | 40 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 41 | gem 'spring' 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | * Copyright (c) 2015, Applidium 2 | * All rights reserved. 3 | * Redistribution and use in source and binary forms, with or without 4 | * modification, are permitted provided that the following conditions are met: 5 | * 6 | * * Redistributions of source code must retain the above copyright 7 | * notice, this list of conditions and the following disclaimer. 8 | * * Redistributions in binary form must reproduce the above copyright 9 | * notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * * Neither the name of Applidium nor the names of its contributors may 12 | * be used to endorse or promote products derived from this software 13 | * without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /prediction/prediction.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import time 3 | import json 4 | import numpy as np 5 | import pandas as pd 6 | from sklearn import linear_model 7 | 8 | # returns unix timestamp of `origin`datetime + `hour` padding 9 | def timestamp(origin, hour): 10 | return int(time.mktime((origin + timedelta(hours=hour)).timetuple())) 11 | 12 | def predict_for_dump(rows_dumped, rows_to_predict, kind): 13 | data = rows_dumped 14 | 15 | features = data.columns[1:] 16 | targets = np.array(data['available_bikes']).astype(int) 17 | 18 | classifiers = { 19 | "scikit_lasso": linear_model.Lasso(alpha=0.1), 20 | "scikit_ridge": linear_model.Ridge(alpha=0.1) 21 | } 22 | 23 | clf = classifiers[kind] if kind in classifiers else classifiers["scikit_lasso"] 24 | clf.fit(data[features], targets) 25 | 26 | rows = clf.predict(rows_to_predict) 27 | 28 | return [int(row) for row in rows] 29 | 30 | def predict(data, dump_rows, prediction_rows, kind): 31 | hours_range = data["hours_range"] 32 | 33 | origin = datetime.fromtimestamp(data["action_timestamp"]) 34 | timestamps = [timestamp(origin, i) for i in xrange(hours_range+1)] 35 | 36 | _rows = json.dumps([row.to_json() for row in dump_rows]) 37 | rows_dumped = pd.read_json(_rows) 38 | 39 | _rows = json.dumps([row.to_json() for row in prediction_rows]) 40 | rows_to_predict = pd.read_json(_rows) 41 | 42 | predictions = predict_for_dump(rows_dumped, rows_to_predict, kind) 43 | 44 | return [{ "timestamp": timestamps[i], "value": value } for i, value in enumerate(predictions)] 45 | -------------------------------------------------------------------------------- /lib/dump.rb: -------------------------------------------------------------------------------- 1 | require './lib/holiday' 2 | 3 | class Dump 4 | def initialize(time_delta) 5 | @time_delta = time_delta 6 | end 7 | 8 | def station(station) 9 | from_datetime = last_datetime(station) 10 | 11 | datapoints = BikeDataPoint.where(station: station).where("created_at >= ?", from_datetime).order(:created_at).to_a 12 | 13 | current_datetime = station.last_dump || datapoints.first.created_at.to_datetime 14 | 15 | current_datetime, i = [from_datetime, 0] 16 | rows = [] 17 | 18 | loop do 19 | i += 1 while i < datapoints.length and datapoints[i].created_at < current_datetime 20 | 21 | break if i == datapoints.length 22 | 23 | datapoint = datapoints[i].dup 24 | datapoint.created_at = current_datetime 25 | 26 | # # find the next weather_data_point after if blank 27 | if datapoint.weather_data_point.blank? 28 | datapoint.weather_data_point = WeatherDataPoint.where("created_at >= ?", current_datetime).order(:created_at).first 29 | end 30 | 31 | rows << DumpDataPointRow.new_from_dump(datapoint, Dump.holidays) 32 | 33 | current_datetime += @time_delta.minute 34 | end 35 | 36 | DumpDataPointRow.import(rows) 37 | 38 | # new last dump timestamp 39 | station.update_attributes({ last_dump: current_datetime }) 40 | end 41 | 42 | private 43 | def last_datetime(station) 44 | station.last_dump || BikeDataPoint.order(:created_at).first.created_at.to_datetime 45 | end 46 | 47 | @@holidays = nil 48 | 49 | def self.holidays 50 | return @@holidays if @@holidays.present? 51 | @@holidays = Holiday.holidays 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /lib/tasks/fetch.rake: -------------------------------------------------------------------------------- 1 | # Fetch data from api 2 | def _fetch(url, params = {}) 3 | uri = URI.parse(url) 4 | uri.query = URI.encode_www_form(params) 5 | 6 | JSON.parse(Net::HTTP.get(uri)) 7 | end 8 | 9 | # Fetch all stations 10 | def fetch_stations(contract) 11 | _fetch( 12 | "https://api.jcdecaux.com/vls/v1/stations", 13 | apiKey: ApiKeys::JCDECAUX, contract: contract 14 | ) 15 | end 16 | 17 | # Fetch weather informations 18 | def fetch_weather(lat, lon) 19 | _fetch( 20 | "http://api.openweathermap.org/data/2.5/weather", 21 | appid: ApiKeys::OPENWEATHERMAP, lat: lat, lon: lon 22 | ) 23 | end 24 | 25 | namespace :fetch do 26 | task :populate => :environment do 27 | puts "Creating Paris contract..." 28 | 29 | contract = Contract.find_or_create_by(name: "Paris", latitude: 48.85, longitude: 2.35) 30 | 31 | puts "Fetching stations..." 32 | 33 | fetched_stations = fetch_stations(contract.name) 34 | 35 | fetched_stations.each do |station| 36 | station = Station.jcdecaux_new(station, contract) 37 | station.save if station.present? 38 | end 39 | 40 | puts "Stations fetched." 41 | end 42 | 43 | task :fetch => :environment do 44 | puts "Fetching data..." 45 | 46 | Contract.all.each do |contract| 47 | puts "Fetching stations for #{contract.name}..." 48 | fetched_stations = fetch_stations(contract.name) 49 | 50 | puts "Fetching weather information..." 51 | fetched_weather = fetch_weather(contract.latitude, contract.longitude) 52 | weather_data_point = WeatherDataPoint.owm_new(fetched_weather, contract) 53 | weather_data_point.save if weather_data_point.present? 54 | 55 | fetched_stations.each do |station| 56 | datapoint = BikeDataPoint.jcdecaux_new(station, weather_data_point) 57 | datapoint.save if datapoint.present? 58 | end 59 | end 60 | 61 | puts "Data fetched." 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /prediction/worker.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | from prediction import predict 4 | import psycopg2 5 | from redis import Redis 6 | from rows import DumpRow, PredictionRow 7 | import time 8 | 9 | 10 | def predict_thread(data, kind, cursor, connection): 11 | prediction_table, rows_table = data["prediction_table"], data["rows_table"] 12 | station_id = data["station_id"] 13 | 14 | dump_rows = DumpRow.fetch_all(cursor, rows_table, station_id) 15 | prediction_rows = PredictionRow.fetch_all(cursor, rows_table, data["ids"]) 16 | 17 | predictions = predict(data, dump_rows, prediction_rows, kind) 18 | 19 | results = [] 20 | 21 | for prediction in predictions: 22 | timestamp = datetime.datetime.utcfromtimestamp(prediction["timestamp"]) 23 | timestamp = datetime.datetime(timestamp.year, timestamp.month, timestamp.day, timestamp.hour) 24 | now = datetime.datetime.utcnow() 25 | 26 | result = "(%d, TIMESTAMP '%s', %d, '%s', TIMESTAMP '%s', TIMESTAMP '%s')" % (station_id, timestamp, prediction["value"], kind, now, now) 27 | results.append(result) 28 | 29 | cursor.execute("INSERT INTO %s (station_id, datetime, available_bikes, kind, created_at, updated_at) VALUES %s" % (prediction_table, ", ".join(results))) 30 | connection.commit() 31 | 32 | 33 | if __name__ == '__main__': 34 | print "Connecting to PostgreSQL..." 35 | 36 | connection = psycopg2.connect("dbname=velib_development user=velib password=velib") 37 | cursor = connection.cursor() 38 | 39 | print "Connecting to Redis..." 40 | 41 | redis = Redis(host="localhost", port=6379, db=0) 42 | 43 | pubsub = redis.pubsub() 44 | pubsub.subscribe("prediction") 45 | 46 | print "Subscribed to 'prediction' channel" 47 | 48 | for item in pubsub.listen(): 49 | if item["type"] != "message": 50 | continue 51 | 52 | data = json.loads(item["data"]) 53 | 54 | print "Received data for station id: %s..." % str(data["station_id"]) 55 | 56 | for kind in ("scikit_lasso", "scikit_ridge"): 57 | predict_thread(data, kind, cursor, connection) 58 | 59 | cursor.close() 60 | connection.close() 61 | -------------------------------------------------------------------------------- /app/models/weather_data_point.rb: -------------------------------------------------------------------------------- 1 | class WeatherDataPoint < ActiveRecord::Base 2 | belongs_to :contract 3 | 4 | validates :weather, presence: true 5 | validates :temperature, presence: true 6 | validates :wind_speed, presence: true 7 | validates :humidity, presence: true 8 | 9 | def self.owm_new(json, contract) 10 | return if ["weather", "main", "wind"].any? { |x| json[x].nil? } 11 | return if json["weather"].empty? 12 | 13 | weather = json["weather"][0]["main"] 14 | temperature = json["main"]["temp"] 15 | humidity = json["main"]["humidity"] 16 | wind_speed = json["wind"]["speed"] 17 | 18 | WeatherDataPoint.new( 19 | weather: weather, 20 | temperature: temperature, 21 | wind_speed: wind_speed, 22 | humidity: humidity, 23 | contract: contract 24 | ) 25 | end 26 | 27 | # maps data from forecast.io to an owm-compliant weather_data_point 28 | def self.forecast_io_new(forecast_data, contract) 29 | weather = forecast_io_to_owm_weather(forecast_data.icon) 30 | temperature = forecast_data.temperature + 273.15 # Celcius -> Kelvin 31 | humidity = (forecast_data.humidity * 100.0).to_i # [0..1] -> [0..100] 32 | wind_speed = forecast_data.windSpeed 33 | 34 | WeatherDataPoint.new( 35 | weather: weather, 36 | temperature: temperature, 37 | wind_speed: wind_speed, 38 | humidity: humidity, 39 | contract: contract 40 | ) 41 | end 42 | 43 | # map open weather map string -> integer (for prediction purpose) 44 | def weather_mapping 45 | mapping = { 46 | "Thunderstorm" => 0, "Drizzle" => 1, "Rain" => 2, "Snow" => 3, 47 | "Atmosphere" => 4, "Clouds" => 5, "Extreme" => 6, "Additional" => 7, 48 | "Clear" => 8, "Mist" => 9 49 | } 50 | 51 | mapping[self.weather] || mapping.values.max + 1 52 | end 53 | 54 | private 55 | def self.forecast_io_to_owm_weather(weather) 56 | # mapping ForecastIO -> OWM 57 | forecast_mapping = { 58 | "clear-day" => "Clear", "clear-night" => "Clear", "rain" => "Rain", 59 | "snow" => "Snow", "sleet" => "Snow", "wind" => "Clouds", "fog" => "Mist", 60 | "partly-cloudy-day" => "Clouds", "partly-cloudy-night" => "Clouds", 61 | "cloudy" => "Clouds", 62 | } 63 | 64 | forecast_mapping[weather] 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /app/models/station.rb: -------------------------------------------------------------------------------- 1 | class Station < ActiveRecord::Base 2 | belongs_to :contract 3 | has_many :bike_data_points, dependent: :destroy 4 | has_one :last_entry, :class_name => "BikeDataPoint" 5 | has_many :predictions 6 | 7 | validates :number, presence: true 8 | validates :name, presence: true 9 | validates :bike_stands, presence: true 10 | 11 | def self.jcdecaux_new(json, contract) 12 | # no coordinates -> no weather informations -> useless 13 | return if json["position"].nil? 14 | 15 | number = json["number"] 16 | latitude = json["position"]["lat"] 17 | longitude = json["position"]["lng"] 18 | bike_stands = json["bike_stands"] 19 | 20 | # 05006 - SAINT JACQUES SOUFFLOT -> Saint Jacques Soufflot 21 | name = json["name"].mb_chars.split(" - ")[1..-1].join(" - ").split.map(&:capitalize).join(" ") 22 | 23 | # 174 RUE SAINT JACQUES - 75005 PARIS -> 174 Rue Saint Jacques - 75005 Paris 24 | address = json["address"].mb_chars.split.map(&:capitalize).join(" ") 25 | 26 | # station already exists? 27 | station = Station.find_by(number: number, contract: contract) 28 | station = Station.new if station.blank? 29 | 30 | station.assign_attributes({ 31 | number: number, 32 | name: name, 33 | address: address, 34 | latitude: latitude, 35 | longitude: longitude, 36 | bike_stands: bike_stands, 37 | contract: contract 38 | }) 39 | 40 | station 41 | end 42 | 43 | def prediction_row(datetime, hours_ahead, holidays, forecast) 44 | current_datetime = datetime + hours_ahead.hours 45 | weather_data_point = forecast.weather_data_point(current_datetime) 46 | 47 | { 48 | :open? => self.last_entry.open ? 1 : 0, # Can we do better? 49 | :weather => weather_data_point.weather_mapping, 50 | :temperature => weather_data_point.temperature, 51 | :wind_speed => weather_data_point.wind_speed, 52 | :humidity => weather_data_point.humidity, 53 | :hour => current_datetime.hour, 54 | :minute => current_datetime.minute, 55 | :day_of_week => current_datetime.wday, 56 | :week_number => current_datetime.week, 57 | :season => current_datetime.season_mapping, 58 | :weekend? => [0, 6].include?(current_datetime.wday) ? 1 : 0, 59 | :holiday? => holidays.holiday?(current_datetime) ? 1 : 0 60 | } 61 | end 62 | 63 | def self.prediction_columns 64 | [:open?, :weather, :temperature, :wind_speed, 65 | :humidity, :hour, :minute, :day_of_week, :week_number, 66 | :season, :weekend?, :holiday?, :available_bikes] 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see rails configuration guide 21 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: 5 23 | 24 | development: 25 | <<: *default 26 | database: velib_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user that initialized the database. 32 | username: velib 33 | 34 | # The password associated with the postgres role (username). 35 | password: velib 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: velib_test 61 | 62 | # As with config/secrets.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password as a unix environment variable when you boot 67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 68 | # for a full rundown on how to provide these environment variables in a 69 | # production deployment. 70 | # 71 | # On Heroku and other platform providers, you may have a full connection URL 72 | # available as an environment variable. For example: 73 | # 74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 75 | # 76 | # You can use this database configuration with: 77 | # 78 | # production: 79 | # url: <%= ENV['DATABASE_URL'] %> 80 | # 81 | production: 82 | <<: *default 83 | database: velib_production 84 | username: velib 85 | password: <%= ENV['VELIB_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bike-Share-Prediction 2 | 3 | This tool allows people wanting to get bikeshares usage prediction. It currently 4 | works with the [Vélib'](http://www.velib.paris/) system but can be expanded to 5 | any bikeshare system, especially any of the 6 | [JCDecaux](https://developer.jcdecaux.com/#/opendata/vls?page=static) system in 7 | a minute. 8 | 9 | An example instance of the Webservice is running on Heroku, you can access it 10 | here: [http://bike-share-prediction-example.herokuapp.com](http://bike-share-prediction-example.herokuapp.com). 11 | It contains stations of the Vélib' contract, but does not make any prediction. 12 | 13 | ## Installation 14 | 15 | * Get API key from [JCDecaux Developer](https://developer.jcdecaux.com/#/account) 16 | * Get API key from [OpenWeatherMap](http://home.openweathermap.org/) 17 | * Get API key from [forecast.io](https://developer.forecast.io/) (max. 1000 calls per day) 18 | * Create a `config/api_keys.yml` file with this format: 19 | ``` 20 | jcdecaux: API_KEY 21 | openweathermap: API_KEY 22 | forecastio: API_KEY 23 | ``` 24 | 25 | * `bundle install` 26 | * `bundle exec rake db:create` 27 | * `bundle exec rake db:migrate` 28 | * `bundle exec whenever -i` 29 | * `bundle exec rake fetch:populate` will create Vélib' contract, you can add other systems here if you want to 30 | * `bundle exec rails server` 31 | * Enjoy. 32 | 33 | Alternatively, you can set API keys as environment variables, suffixed with `\_APIKEY`: 34 | 35 | $ export JCDECAUX_APIKEY=xxxx 36 | $ export OPENWEATHERMAP_APIKEY=xxxx 37 | $ export FORECASTIO_APIKEY=xxxx 38 | $ bundle exec rails server 39 | 40 | ### Prediction backend 41 | 42 | This repository comes packaged with a scikit-learn-based prediction backend, 43 | to get it to run, you will need a [Redis](http://redis.io) server running on 44 | port 6379, if it is running on another port, please change it in 45 | [`prediction/worker.py`](https://github.com/applidium/bike-share-prediction/blob/master/prediction/worker.py#L41) 46 | and [`lib/tasks/predict.rake`](https://github.com/applidium/bike-share-prediction/blob/master/lib/tasks/predict.rake#L9). 47 | 48 | Once Redis is running, run `prediction/worker.py` (install requirements located 49 | in `prediction/requirements.txt` before, works well with Python virtualenv). 50 | 51 | The worker will subscribe to a Redis Pub/Sub channel. Once a day, Rails will 52 | publish metadata asking to predict usage for the next day, the Python worker 53 | will treat these metadata and predict using scikit-learn's `linear_model.Lasso` 54 | and `linear_model.Ridge` algorithms. 55 | 56 | You can add another prediction backend and turn the Python worker off if you 57 | have your own implementation of a prediction algorithm. 58 | 59 | ## Routes 60 | 61 | * `GET /contacts` 62 | * `GET /contracts/:contract_id/stations` 63 | * `GET /stations/:station_id` 64 | 65 | ## Add a contract 66 | 67 | ### JCDecaux 68 | 69 | Simply add a new `Contract` object to the database, the `name` must be the name 70 | used to fetch data from JCDecaux, `latitude` and `longitude` are there for 71 | OpenWeatherMap and forecast.io. 72 | 73 | ### Other 74 | 75 | You will have to write a rake task, indicating how data are fetch, see 76 | `lib/tasks/fetch.rake` for the format. 77 | 78 | ## Future work 79 | 80 | There are a couple of improvements that could be done. Feel free to send us pull 81 | requests if you want to contribute! 82 | 83 | * Add a new prediction method (with scikit-learn or something else) 84 | * Add new bikeshare systems 85 | * More? 86 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150818124538) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | 19 | create_table "bike_data_points", force: :cascade do |t| 20 | t.integer "available_bikes" 21 | t.boolean "open" 22 | t.integer "weather_data_point_id" 23 | t.datetime "created_at", null: false 24 | t.datetime "updated_at", null: false 25 | t.integer "station_id" 26 | end 27 | 28 | add_index "bike_data_points", ["station_id"], name: "index_bike_data_points_on_station_id", using: :btree 29 | add_index "bike_data_points", ["weather_data_point_id"], name: "index_bike_data_points_on_weather_data_point_id", using: :btree 30 | 31 | create_table "contracts", force: :cascade do |t| 32 | t.string "name" 33 | t.datetime "created_at", null: false 34 | t.datetime "updated_at", null: false 35 | t.float "latitude" 36 | t.float "longitude" 37 | end 38 | 39 | create_table "data_point_rows", force: :cascade do |t| 40 | t.integer "open" 41 | t.integer "weather" 42 | t.decimal "temperature" 43 | t.decimal "wind_speed" 44 | t.integer "humidity" 45 | t.integer "hour" 46 | t.integer "minute" 47 | t.integer "day_of_week" 48 | t.integer "week_number" 49 | t.integer "season" 50 | t.integer "weekend" 51 | t.integer "holiday" 52 | t.integer "available_bikes" 53 | t.integer "station_id" 54 | t.string "type" 55 | t.datetime "created_at", null: false 56 | t.datetime "updated_at", null: false 57 | end 58 | 59 | add_index "data_point_rows", ["station_id"], name: "index_data_point_rows_on_station_id", using: :btree 60 | 61 | create_table "predictions", force: :cascade do |t| 62 | t.integer "station_id" 63 | t.datetime "datetime" 64 | t.integer "available_bikes" 65 | t.datetime "created_at", null: false 66 | t.datetime "updated_at", null: false 67 | t.string "kind", default: "scikit_lasso" 68 | end 69 | 70 | add_index "predictions", ["kind"], name: "index_predictions_on_kind", using: :btree 71 | add_index "predictions", ["station_id"], name: "index_predictions_on_station_id", using: :btree 72 | 73 | create_table "stations", force: :cascade do |t| 74 | t.string "name" 75 | t.string "address" 76 | t.float "latitude" 77 | t.float "longitude" 78 | t.integer "bike_stands" 79 | t.integer "last_update", limit: 8 80 | t.datetime "created_at", null: false 81 | t.datetime "updated_at", null: false 82 | t.integer "contract_id" 83 | t.integer "number" 84 | t.integer "last_entry" 85 | t.datetime "last_dump" 86 | end 87 | 88 | add_index "stations", ["contract_id"], name: "index_stations_on_contract_id", using: :btree 89 | add_index "stations", ["last_entry"], name: "index_stations_on_last_entry", using: :btree 90 | 91 | create_table "weather_data_points", force: :cascade do |t| 92 | t.string "weather" 93 | t.float "temperature" 94 | t.float "wind_speed" 95 | t.integer "humidity" 96 | t.integer "contract_id" 97 | t.datetime "created_at", null: false 98 | t.datetime "updated_at", null: false 99 | end 100 | 101 | add_index "weather_data_points", ["contract_id"], name: "index_weather_data_points_on_contract_id", using: :btree 102 | 103 | add_foreign_key "bike_data_points", "stations" 104 | add_foreign_key "bike_data_points", "weather_data_points" 105 | add_foreign_key "data_point_rows", "stations" 106 | add_foreign_key "predictions", "stations" 107 | add_foreign_key "stations", "contracts" 108 | add_foreign_key "weather_data_points", "contracts" 109 | end 110 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.1) 5 | actionpack (= 4.2.1) 6 | actionview (= 4.2.1) 7 | activejob (= 4.2.1) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.1) 11 | actionview (= 4.2.1) 12 | activesupport (= 4.2.1) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 17 | actionview (4.2.1) 18 | activesupport (= 4.2.1) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | active_model_serializers (0.9.3) 24 | activemodel (>= 3.2) 25 | activejob (4.2.1) 26 | activesupport (= 4.2.1) 27 | globalid (>= 0.3.0) 28 | activemodel (4.2.1) 29 | activesupport (= 4.2.1) 30 | builder (~> 3.1) 31 | activerecord (4.2.1) 32 | activemodel (= 4.2.1) 33 | activesupport (= 4.2.1) 34 | arel (~> 6.0) 35 | activerecord-import (0.8.0) 36 | activerecord (>= 3.0) 37 | activesupport (4.2.1) 38 | i18n (~> 0.7) 39 | json (~> 1.7, >= 1.7.7) 40 | minitest (~> 5.1) 41 | thread_safe (~> 0.3, >= 0.3.4) 42 | tzinfo (~> 1.1) 43 | arel (6.0.0) 44 | binding_of_caller (0.7.2) 45 | debug_inspector (>= 0.0.1) 46 | builder (3.2.2) 47 | byebug (4.0.5) 48 | columnize (= 0.9.0) 49 | chronic (0.10.2) 50 | coffee-rails (4.1.0) 51 | coffee-script (>= 2.2.0) 52 | railties (>= 4.0.0, < 5.0) 53 | coffee-script (2.4.1) 54 | coffee-script-source 55 | execjs 56 | coffee-script-source (1.9.1.1) 57 | columnize (0.9.0) 58 | debug_inspector (0.0.2) 59 | erubis (2.7.0) 60 | execjs (2.5.2) 61 | faraday (0.9.1) 62 | multipart-post (>= 1.2, < 3) 63 | forecast_io (2.0.0) 64 | faraday 65 | hashie 66 | multi_json 67 | globalid (0.3.5) 68 | activesupport (>= 4.1.0) 69 | hashie (3.4.2) 70 | i18n (0.7.0) 71 | jbuilder (2.2.13) 72 | activesupport (>= 3.0.0, < 5) 73 | multi_json (~> 1.2) 74 | jquery-rails (4.0.3) 75 | rails-dom-testing (~> 1.0) 76 | railties (>= 4.2.0) 77 | thor (>= 0.14, < 2.0) 78 | json (1.8.2) 79 | loofah (2.0.1) 80 | nokogiri (>= 1.5.9) 81 | mail (2.6.3) 82 | mime-types (>= 1.16, < 3) 83 | mime-types (2.4.3) 84 | mini_portile (0.6.2) 85 | minitest (5.6.0) 86 | multi_json (1.11.0) 87 | multipart-post (2.0.0) 88 | nokogiri (1.6.6.2) 89 | mini_portile (~> 0.6.0) 90 | pg (0.18.1) 91 | rack (1.6.0) 92 | rack-test (0.6.3) 93 | rack (>= 1.0) 94 | rails (4.2.1) 95 | actionmailer (= 4.2.1) 96 | actionpack (= 4.2.1) 97 | actionview (= 4.2.1) 98 | activejob (= 4.2.1) 99 | activemodel (= 4.2.1) 100 | activerecord (= 4.2.1) 101 | activesupport (= 4.2.1) 102 | bundler (>= 1.3.0, < 2.0) 103 | railties (= 4.2.1) 104 | sprockets-rails 105 | rails-deprecated_sanitizer (1.0.3) 106 | activesupport (>= 4.2.0.alpha) 107 | rails-dom-testing (1.0.6) 108 | activesupport (>= 4.2.0.beta, < 5.0) 109 | nokogiri (~> 1.6.0) 110 | rails-deprecated_sanitizer (>= 1.0.1) 111 | rails-html-sanitizer (1.0.2) 112 | loofah (~> 2.0) 113 | railties (4.2.1) 114 | actionpack (= 4.2.1) 115 | activesupport (= 4.2.1) 116 | rake (>= 0.8.7) 117 | thor (>= 0.18.1, < 2.0) 118 | rake (10.4.2) 119 | rdoc (4.2.0) 120 | json (~> 1.4) 121 | redis (3.2.1) 122 | sass (3.4.13) 123 | sass-rails (5.0.3) 124 | railties (>= 4.0.0, < 5.0) 125 | sass (~> 3.1) 126 | sprockets (>= 2.8, < 4.0) 127 | sprockets-rails (>= 2.0, < 4.0) 128 | tilt (~> 1.1) 129 | sdoc (0.4.1) 130 | json (~> 1.7, >= 1.7.7) 131 | rdoc (~> 4.0) 132 | spring (1.3.6) 133 | sprockets (3.0.1) 134 | rack (~> 1.0) 135 | sprockets-rails (2.2.4) 136 | actionpack (>= 3.0) 137 | activesupport (>= 3.0) 138 | sprockets (>= 2.8, < 4.0) 139 | thor (0.19.1) 140 | thread_safe (0.3.5) 141 | tilt (1.4.1) 142 | turbolinks (2.5.3) 143 | coffee-rails 144 | tzinfo (1.2.2) 145 | thread_safe (~> 0.1) 146 | uglifier (2.7.1) 147 | execjs (>= 0.3.0) 148 | json (>= 1.8.0) 149 | web-console (2.1.2) 150 | activemodel (>= 4.0) 151 | binding_of_caller (>= 0.7.2) 152 | railties (>= 4.0) 153 | sprockets-rails (>= 2.0, < 4.0) 154 | whenever (0.9.4) 155 | chronic (>= 0.6.3) 156 | 157 | PLATFORMS 158 | ruby 159 | 160 | DEPENDENCIES 161 | active_model_serializers 162 | activerecord-import 163 | byebug 164 | coffee-rails (~> 4.1.0) 165 | forecast_io 166 | jbuilder (~> 2.0) 167 | jquery-rails 168 | nokogiri 169 | pg 170 | rails (= 4.2.1) 171 | redis 172 | sass-rails (~> 5.0) 173 | sdoc (~> 0.4.0) 174 | spring 175 | turbolinks 176 | uglifier (>= 1.3.0) 177 | web-console (~> 2.0) 178 | whenever 179 | --------------------------------------------------------------------------------