active_median 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47d759f5155e0a9687eeb845cf25a65daae5fe2a475bed1dcc071b742413b8c4
4
- data.tar.gz: 9eb8b10f9ff8526adcea04beb4fa68fb11e61b544f11413925280c5d37f10e2b
3
+ metadata.gz: 86998cba53771c73de5fcc9c5dc8a869a4ff061029828dcf0ce8fab89852e346
4
+ data.tar.gz: 23482bad0017c7b16422e9be9961783a6674576a65955a6d228db22f556ef84b
5
5
  SHA512:
6
- metadata.gz: af49d340ff2b1298d1706381a7879ac3666e9077a56607c2d8809b132f93eef3eb8beb9996aa7599157a3d900ad3951e606413cb936f23d045115d8baead0121
7
- data.tar.gz: ed82a1e318b79372aace81c56d2ca9bd9c59f373be5795f2e595fc8a60c7a7bdd9f9191e5a46069c4ea7405347e3057abb3c1ad6ae794a252cdce6de7f5411b6
6
+ metadata.gz: 6c5f02aa5f51169214536006caa8509b4fcc7041d1be680805388019d092f1517c54fbdb0738f4fa80d89609ea959ba1c741a83460c651da62288916c47e1a5e
7
+ data.tar.gz: d53d94e2835a54521a17977f035c04d40766d958473890cfcacfd4cae5d99d792fdea5c8f18ff975155f0d25d5854134c7770f1d0e578e70d5c4aceddeed115c
@@ -1,19 +1,23 @@
1
- ## 0.2.3
1
+ ## 0.2.4 (2020-03-12)
2
+
3
+ - Added `percentile` method
4
+
5
+ ## 0.2.3 (2019-09-03)
2
6
 
3
7
  - Added support for Mongoid
4
8
  - Dropped support for Rails 4.2
5
9
 
6
- ## 0.2.2
10
+ ## 0.2.2 (2018-10-29)
7
11
 
8
12
  - Added support for MySQL with udf_infusion
9
13
  - Added support for SQL Server and Redshift
10
14
 
11
- ## 0.2.1
15
+ ## 0.2.1 (2018-10-15)
12
16
 
13
17
  - Added support for arrays and hashes
14
18
  - Added compatibility with Groupdate 4
15
19
 
16
- ## 0.2.0
20
+ ## 0.2.0 (2018-10-15)
17
21
 
18
22
  - Added support for MariaDB 10.3.3+ and SQLite
19
23
  - Use `PERCENTILE_CONT` for 4x performance increase
@@ -22,23 +26,23 @@ Breaking
22
26
 
23
27
  - Dropped support for Postgres < 9.4
24
28
 
25
- ## 0.1.4
29
+ ## 0.1.4 (2016-12-03)
26
30
 
27
31
  - Added `drop_function` method
28
32
 
29
- ## 0.1.3
33
+ ## 0.1.3 (2016-03-22)
30
34
 
31
35
  - Added support for ActiveRecord 5.0
32
36
 
33
- ## 0.1.2
37
+ ## 0.1.2 (2014-12-27)
34
38
 
35
39
  - Added support for ActiveRecord 4.2
36
40
 
37
- ## 0.1.1
41
+ ## 0.1.1 (2014-08-12)
38
42
 
39
43
  - 10x faster median
40
44
  - Added tests
41
45
 
42
- ## 0.1.0
46
+ ## 0.1.0 (2014-03-13)
43
47
 
44
48
  - First release
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2019 Andrew Kane
1
+ Copyright (c) 2013-2020 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ActiveMedian
2
2
 
3
- Median for Active Record, Mongoid, arrays, and hashes
3
+ Median and percentile for Active Record, Mongoid, arrays, and hashes
4
4
 
5
5
  Supports:
6
6
 
@@ -11,7 +11,7 @@ Supports:
11
11
 
12
12
  :fire: Uses native functions for blazing performance
13
13
 
14
- [![Build Status](https://travis-ci.org/ankane/active_median.svg)](https://travis-ci.org/ankane/active_median)
14
+ [![Build Status](https://travis-ci.org/ankane/active_median.svg?branch=master)](https://travis-ci.org/ankane/active_median)
15
15
 
16
16
  ## Getting Started
17
17
 
@@ -25,11 +25,19 @@ For MySQL and SQLite, also follow [these instructions](#additional-instructions)
25
25
 
26
26
  ## Models
27
27
 
28
+ Median
29
+
28
30
  ```ruby
29
31
  Item.median(:price)
30
32
  ```
31
33
 
32
- Works with grouping, too.
34
+ Percentile
35
+
36
+ ```ruby
37
+ Item.percentile(:price, 0.95)
38
+ ```
39
+
40
+ Works with grouping, too
33
41
 
34
42
  ```ruby
35
43
  Order.group(:store_id).median(:total)
@@ -37,11 +45,19 @@ Order.group(:store_id).median(:total)
37
45
 
38
46
  ## Arrays and Hashes
39
47
 
48
+ Median
49
+
40
50
  ```ruby
41
51
  [1, 2, 3].median
42
52
  ```
43
53
 
44
- You can also pass a block.
54
+ Percentile
55
+
56
+ ```ruby
57
+ [1, 2, 3].percentile(0.75)
58
+ ```
59
+
60
+ You can also pass a block
45
61
 
46
62
  ```ruby
47
63
  {a: 1, b: 2, c: 3}.median { |k, v| v }
@@ -106,3 +122,13 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
106
122
  - Fix bugs and [submit pull requests](https://github.com/ankane/active_median/pulls)
107
123
  - Write, clarify, or fix documentation
108
124
  - Suggest or add new features
125
+
126
+ To get started with development and testing:
127
+
128
+ ```sh
129
+ git clone https://github.com/ankane/active_median.git
130
+ cd active_median
131
+ createdb active_median_test
132
+ bundle install
133
+ bundle exec rake test
134
+ ```
@@ -6,9 +6,35 @@ module Enumerable
6
6
  elsif !block && respond_to?(:with_scope)
7
7
  with_scope(self) { klass.median(*args) }
8
8
  else
9
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" if args.any?
10
+ percentile(0.5, &block)
11
+ end
12
+ end
13
+ end
14
+
15
+ unless method_defined?(:percentile)
16
+ def percentile(*args, &block)
17
+ if !block && respond_to?(:scoping)
18
+ scoping { @klass.percentile(*args) }
19
+ elsif !block && respond_to?(:with_scope)
20
+ with_scope(self) { klass.percentile(*args) }
21
+ else
22
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" if args.size != 1
23
+
24
+ percentile = args[0].to_f
25
+ raise ArgumentError, "percentile is not between 0 and 1" if percentile < 0 || percentile > 1
26
+
27
+ # uses C=1 variant, like percentile_cont
28
+ # https://en.wikipedia.org/wiki/Percentile#The_linear_interpolation_between_closest_ranks_method
9
29
  sorted = map(&block).sort
10
- len = sorted.length
11
- (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
30
+ x = percentile * (sorted.size - 1)
31
+ r = x % 1
32
+ i = x.floor
33
+ if i == sorted.size - 1
34
+ sorted[-1]
35
+ else
36
+ sorted[i] + r * (sorted[i + 1] - sorted[i])
37
+ end
12
38
  end
13
39
  end
14
40
  end
@@ -1,6 +1,16 @@
1
1
  module ActiveMedian
2
2
  module Model
3
3
  def median(column)
4
+ percentile(column, 0.5)
5
+ end
6
+
7
+ def percentile(column, percentile)
8
+ percentile = percentile.to_f
9
+ raise ArgumentError, "percentile is not between 0 and 1" if percentile < 0 || percentile > 1
10
+
11
+ # prevent SQL injection
12
+ percentile = connection.quote(percentile)
13
+
4
14
  group_values = all.group_values
5
15
 
6
16
  relation =
@@ -15,21 +25,31 @@ module ActiveMedian
15
25
  over = "PARTITION BY #{group_values.join(", ")}"
16
26
  end
17
27
 
18
- select(*group_values, "PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
28
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
19
29
  else
20
30
  # if mysql gets native function, check (and memoize) version first
21
- select(*group_values, "PERCENTILE_CONT(#{column}, 0.50)")
31
+ select(*group_values, "PERCENTILE_CONT(#{column}, #{percentile})")
22
32
  end
23
33
  when /sqlserver/i
24
34
  if group_values.any?
25
35
  over = "PARTITION BY #{group_values.join(", ")}"
26
36
  end
27
37
 
28
- select(*group_values, "PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
38
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column}) OVER (#{over})").unscope(:group)
29
39
  when /sqlite/i
30
- select(*group_values, "MEDIAN(#{column})")
40
+ case percentile.to_f
41
+ when 0
42
+ select(*group_values, "MIN(#{column})")
43
+ when 0.5
44
+ select(*group_values, "MEDIAN(#{column})")
45
+ when 1
46
+ select(*group_values, "MAX(#{column})")
47
+ else
48
+ # LOWER_QUARTILE and UPPER_QUARTILE use different calculation than 0.25 and 0.75
49
+ raise "SQLite only supports 0, 0.5, and 1 percentiles"
50
+ end
31
51
  when /postg/i, /redshift/i # postgis too
32
- select(*group_values, "PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY #{column})")
52
+ select(*group_values, "PERCENTILE_CONT(#{percentile}) WITHIN GROUP (ORDER BY #{column})")
33
53
  else
34
54
  raise "Connection adapter not supported: #{connection.adapter_name}"
35
55
  end
@@ -1,21 +1,30 @@
1
1
  module ActiveMedian
2
2
  module Mongoid
3
- # https://www.compose.com/articles/mongo-metrics-finding-a-happy-median/
4
3
  def median(column)
4
+ percentile(column, 0.5)
5
+ end
6
+
7
+ # https://www.compose.com/articles/mongo-metrics-finding-a-happy-median/
8
+ def percentile(column, percentile)
9
+ percentile = percentile.to_f
10
+ raise ArgumentError, "percentile is not between 0 and 1" if percentile < 0 || percentile > 1
11
+
5
12
  relation =
6
13
  all
7
- .group(_id: nil, count: {"$sum" => 1}, values: {"$push" => "$#{column}"})
8
- .unwind("$values")
9
- .asc(:values)
10
- .project(count: {"$subtract" => ["$count", 1]}, values: 1)
11
- .project(count: 1, values: 1, midpoint: {"$divide" => ["$count", 2]})
12
- .project(count: 1, values: 1, midpoint: 1, high: {"$ceil" => "$midpoint"}, low: {"$floor" => "$midpoint"})
13
- .group(_id: nil, values: {"$push" => "$values"}, high: {"$avg" => "$high"}, low: {"$avg" => "$low"})
14
- .project(beginValue: {"$arrayElemAt" => ["$values", "$high"]}, endValue: {"$arrayElemAt" => ["$values", "$low"]})
15
- .project(median: {"$avg" => ["$beginValue", "$endValue"]})
14
+ .asc(column)
15
+ .group(_id: nil, values: {"$push" => "$#{column}"}, count: {"$sum" => 1})
16
+ .project(values: 1, count: {"$subtract" => ["$count", 1]})
17
+ .project(values: 1, count: 1, x: {"$multiply" => ["$count", percentile]})
18
+ .project(values: 1, count: 1, r: {"$mod" => ["$x", 1]}, i: {"$floor" => "$x"})
19
+ .project(values: 1, count: 1, r: 1, i: 1, i2: {"$add" => ["$i", 1]})
20
+ .project(values: 1, count: 1, r: 1, i: 1, i2: {"$min" => ["$i2", "$count"]})
21
+ .project(r: 1, beginValue: {"$arrayElemAt" => ["$values", "$i"]}, endValue: {"$arrayElemAt" => ["$values", "$i2"]})
22
+ .project(r: 1, beginValue: 1, result: {"$subtract" => ["$endValue", "$beginValue"]})
23
+ .project(beginValue: 1, result: {"$multiply" => ["$result", "$r"]})
24
+ .project(result: {"$add" => ["$beginValue", "$result"]})
16
25
 
17
26
  res = collection.aggregate(relation.pipeline).first
18
- res ? res["median"] : nil
27
+ res ? res["result"] : nil
19
28
  end
20
29
  end
21
30
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveMedian
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_median
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-04 00:00:00.000000000 Z
11
+ date: 2020-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -141,8 +141,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
141
  - !ruby/object:Gem::Version
142
142
  version: '0'
143
143
  requirements: []
144
- rubygems_version: 3.0.3
144
+ rubygems_version: 3.1.2
145
145
  signing_key:
146
146
  specification_version: 4
147
- summary: Median for Active Record, Mongoid, arrays, and hashes
147
+ summary: Median and percentile for Active Record, Mongoid, arrays, and hashes
148
148
  test_files: []