From e59bec51ef7b9e434574593c9316df5250e24cf8 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Thu, 8 Nov 2018 18:09:27 +0100 Subject: [PATCH] procedure: use 90th percentile to estimate the completion delay --- app/models/procedure.rb | 12 +++++++----- lib/percentile.rb | 31 ++++++++++++++++++++++++++++++ spec/models/procedure_spec.rb | 36 +++++++++++++++++++++++------------ 3 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 lib/percentile.rb diff --git a/app/models/procedure.rb b/app/models/procedure.rb index d8f287ab1..44984c564 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -1,3 +1,5 @@ +require Rails.root.join('lib', 'percentile') + class Procedure < ApplicationRecord MAX_DUREE_CONSERVATION = 36 @@ -305,15 +307,15 @@ class Procedure < ApplicationRecord end def usual_traitement_time - mean_time(:en_construction_at, :processed_at) + percentile_time(:en_construction_at, :processed_at, 90) end def usual_verification_time - mean_time(:en_construction_at, :en_instruction_at) + percentile_time(:en_construction_at, :en_instruction_at, 90) end def usual_instruction_time - mean_time(:en_instruction_at, :processed_at) + percentile_time(:en_instruction_at, :processed_at, 90) end PATH_AVAILABLE = :available @@ -421,14 +423,14 @@ class Procedure < ApplicationRecord true end - def mean_time(start_attribute, end_attribute) + def percentile_time(start_attribute, end_attribute, p) times = dossiers .state_termine .pluck(start_attribute, end_attribute) .map { |(start_date, end_date)| end_date - start_date } if times.present? - times.sum.fdiv(times.size).ceil + times.percentile(p).ceil end end end diff --git a/lib/percentile.rb b/lib/percentile.rb new file mode 100644 index 000000000..00aa9f59c --- /dev/null +++ b/lib/percentile.rb @@ -0,0 +1,31 @@ +# Adapted from https://github.com/thirtysixthspan/descriptive_statistics + +# Copyright (c) 2010-2014 Derrick Parkhurst (derrick.parkhurst@gmail.com) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +class Array + def percentile(p) + values = self.sort + + if values.empty? + return [] + elsif values.size == 1 + return values.first + elsif p == 100 + return values.last + end + + rank = p / 100.0 * (values.size - 1) + lower, upper = values[rank.floor, 2] + lower + (upper - lower) * (rank - rank.floor) + end +end diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index a96599042..ab4fb73d7 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -708,19 +708,31 @@ describe Procedure do describe '#usual_instruction_time' do let(:procedure) { create(:procedure) } - context 'when there is only one dossier' do - let(:dossier) { create(:dossier, procedure: procedure) } - - context 'which is termine' do - before do - dossier.accepte! - processed_date = Time.zone.parse('12/12/2012') - instruction_date = processed_date - 1.day - dossier.update(en_instruction_at: instruction_date, processed_at: processed_date) - end - - it { expect(procedure.usual_instruction_time).to eq(1.day.to_i) } + before do + processed_delays.each do |delay| + dossier = create :dossier, :accepte, procedure: procedure + instruction_date = 1.month.ago + processed_date = instruction_date + delay + dossier.update!(en_instruction_at: instruction_date, processed_at: processed_date) end end + + context 'when there are several processed dossiers' do + let(:processed_delays) { [1.day, 2.days, 2.days, 2.days, 2.days, 3.days, 3.days, 3.days, 3.days, 12.days] } + + it 'returns a time representative of the dossier instruction delay' do + expect(procedure.usual_instruction_time).to be_between(3.days, 4.days) + end + end + + context 'when there is only one processed dossier' do + let(:processed_delays) { [1.day] } + it { expect(procedure.usual_instruction_time).to eq(1.day) } + end + + context 'where there is no processed dossier' do + let(:processed_delays) { [] } + it { expect(procedure.usual_instruction_time).to be_nil } + end end end