daru_lite 0.3.3 → 0.3.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 +4 -4
- data/lib/daru_lite/data_frame/duplicatable.rb +7 -1
- data/lib/daru_lite/data_frame/fetchable.rb +1 -1
- data/lib/daru_lite/data_frame/iterable.rb +5 -2
- data/lib/daru_lite/data_frame/sortable.rb +13 -5
- data/lib/daru_lite/dataframe.rb +52 -11
- data/lib/daru_lite/index/categorical_index.rb +62 -0
- data/lib/daru_lite/index/multi_index.rb +24 -6
- data/lib/daru_lite/maths/statistics/dataframe.rb +9 -5
- data/lib/daru_lite/version.rb +1 -1
- data/spec/category_spec.rb +26 -0
- data/spec/data_frame/duplicatable_example.rb +23 -0
- data/spec/data_frame/fetchable_example.rb +29 -0
- data/spec/data_frame/iterable_example.rb +16 -0
- data/spec/data_frame/setable_example.rb +31 -0
- data/spec/data_frame/sortable_example.rb +19 -1
- data/spec/dataframe_spec.rb +36 -0
- data/spec/index/categorical_index_spec.rb +68 -0
- data/spec/index/multi_index_spec.rb +39 -0
- data/spec/maths/statistics/dataframe_spec.rb +13 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f09114a32c50e38bfbf8d224919b444a820db2998e46443aa89a999fa856f8f5
|
|
4
|
+
data.tar.gz: 0c27255f631de63d92d01b576cf42e3880e7453e8d20edfcda3969cea6f32ce3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9afdb5835447f5de2af1e7dba83592a8fb5dc4c6981f388609b3408386d03de69ad7a3382e53830d0636d522c462a2b43e988dd38336b67531d8a3ac2e4041a
|
|
7
|
+
data.tar.gz: a47dfc18efa0b88bd6eaf1179b0cb9d6edf9e054124ee6606f857885862af7c2324c06d23dff4e8b585ff9084c394c8e5d1fb9c29a5f3a3e7b73374888ecac8a
|
|
@@ -10,7 +10,13 @@ module DaruLite
|
|
|
10
10
|
# * +vectors_to_dup+ - An Array specifying the names of Vectors to
|
|
11
11
|
# be duplicated. Will duplicate the entire DataFrame if not specified.
|
|
12
12
|
def dup(vectors_to_dup = nil)
|
|
13
|
-
|
|
13
|
+
# No argument: copy by position so a MultiIndex with duplicate tuples (which cannot be
|
|
14
|
+
# re-looked-up unambiguously by name) is preserved intact.
|
|
15
|
+
if vectors_to_dup.nil?
|
|
16
|
+
return DaruLite::DataFrame.new(
|
|
17
|
+
@data.map(&:dup), order: @vectors.dup, index: @index.dup, name: @name, clone: true
|
|
18
|
+
)
|
|
19
|
+
end
|
|
14
20
|
|
|
15
21
|
src = vectors_to_dup.map { |vec| @data[@vectors.pos(vec)].dup }
|
|
16
22
|
new_order = DaruLite::Index.new(vectors_to_dup)
|
|
@@ -226,7 +226,7 @@ module DaruLite
|
|
|
226
226
|
|
|
227
227
|
return @data[pos] if pos.is_a?(Integer)
|
|
228
228
|
|
|
229
|
-
new_vectors =
|
|
229
|
+
new_vectors = @vectors.positions_for(names.flatten).map { |position| @data[position] }
|
|
230
230
|
|
|
231
231
|
pos = pos.drop_left_level(names.size) if names.size < @vectors.width
|
|
232
232
|
|
|
@@ -25,8 +25,11 @@ module DaruLite
|
|
|
25
25
|
def each_vector_with_index
|
|
26
26
|
return to_enum(:each_vector_with_index) unless block_given?
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# Snapshot the data/name pairing up front so the block may safely mutate the DataFrame
|
|
29
|
+
# (e.g. add vectors) mid-iteration, and pair by position so duplicate MultiIndex tuples
|
|
30
|
+
# don't trigger an ambiguous name lookup.
|
|
31
|
+
@data.zip(@vectors.to_a).each do |vector, vector_index| # rubocop:disable Style/ExplicitBlockArgument -- preserve 2-arg yield semantics
|
|
32
|
+
yield vector, vector_index
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
self
|
|
@@ -158,18 +158,26 @@ module DaruLite
|
|
|
158
158
|
|
|
159
159
|
private
|
|
160
160
|
|
|
161
|
+
# Resolve a sort-key name to a single integer position. A MultiIndex with duplicate tuples
|
|
162
|
+
# maps a name to several positions; sorting by such a key uses the first match.
|
|
163
|
+
def sort_vector_position(name)
|
|
164
|
+
@vectors.is_a?(MultiIndex) ? @vectors.positions_for(name).first : @vectors[name]
|
|
165
|
+
end
|
|
166
|
+
|
|
161
167
|
def convert_categorical_vectors(names)
|
|
162
168
|
names.filter_map do |n|
|
|
163
|
-
|
|
169
|
+
position = sort_vector_position(n)
|
|
170
|
+
vector = @data[position]
|
|
171
|
+
next unless vector.category?
|
|
164
172
|
|
|
165
|
-
old = [
|
|
166
|
-
|
|
173
|
+
old = [position, vector]
|
|
174
|
+
@data[position] = DaruLite::Vector.new(vector.to_ints)
|
|
167
175
|
old
|
|
168
176
|
end
|
|
169
177
|
end
|
|
170
178
|
|
|
171
179
|
def restore_categorical_vectors(old)
|
|
172
|
-
old.each { |
|
|
180
|
+
old.each { |position, vector| @data[position] = vector }
|
|
173
181
|
end
|
|
174
182
|
|
|
175
183
|
def sort_build_row(vector_locs, by_blocks, ascending, handle_nils, r1, r2) # rubocop:disable Metrics/ParameterLists
|
|
@@ -223,7 +231,7 @@ module DaruLite
|
|
|
223
231
|
handle_nils = sort_coerce_boolean opts, :handle_nils, false, vector_order.size
|
|
224
232
|
|
|
225
233
|
by_blocks = vector_order.map { |v| (opts[:by] || {})[v] }
|
|
226
|
-
vector_locs = vector_order.map { |v|
|
|
234
|
+
vector_locs = vector_order.map { |v| sort_vector_position(v) }
|
|
227
235
|
|
|
228
236
|
lambda do |index1, index2|
|
|
229
237
|
# Build left and right array to compare two rows
|
data/lib/daru_lite/dataframe.rb
CHANGED
|
@@ -267,8 +267,12 @@ module DaruLite
|
|
|
267
267
|
def delete_vector(vector)
|
|
268
268
|
raise IndexError, "Vector #{vector} does not exist." unless @vectors.include?(vector)
|
|
269
269
|
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
# Delete the first matching column by position: a MultiIndex with duplicate tuples resolves a
|
|
271
|
+
# name to several positions, and removing every duplicate name would desync @vectors from @data.
|
|
272
|
+
# Reuse the index's own #delete_at, which preserves the index type.
|
|
273
|
+
position = @vectors.is_a?(MultiIndex) ? @vectors.positions_for(vector).first : @vectors[vector]
|
|
274
|
+
@data.delete_at(position)
|
|
275
|
+
@vectors = @vectors.delete_at(position)
|
|
272
276
|
|
|
273
277
|
self
|
|
274
278
|
end
|
|
@@ -469,7 +473,9 @@ module DaruLite
|
|
|
469
473
|
@size == other.size &&
|
|
470
474
|
@index == other.index &&
|
|
471
475
|
@vectors == other.vectors &&
|
|
472
|
-
|
|
476
|
+
# Compare columns by position: name-based access is ambiguous (and recurses infinitely)
|
|
477
|
+
# when the vectors are a MultiIndex with duplicate tuples.
|
|
478
|
+
@data == other.data
|
|
473
479
|
end
|
|
474
480
|
|
|
475
481
|
# Converts the specified non category type vectors to category type vectors
|
|
@@ -589,7 +595,7 @@ module DaruLite
|
|
|
589
595
|
end
|
|
590
596
|
|
|
591
597
|
if pos.is_a?(DaruLite::Index)
|
|
592
|
-
assign_multiple_vectors pos, v
|
|
598
|
+
assign_multiple_vectors name, pos, v
|
|
593
599
|
elsif pos == name &&
|
|
594
600
|
(@vectors.include?(name) || (pos.is_a?(Integer) && pos < @data.size))
|
|
595
601
|
|
|
@@ -599,10 +605,14 @@ module DaruLite
|
|
|
599
605
|
end
|
|
600
606
|
end
|
|
601
607
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
608
|
+
# Assign +v+ to every column matching +name+ (a partial key, or a full MultiIndex tuple that is
|
|
609
|
+
# duplicated across several positions). For a MultiIndex, resolve to positions directly: a
|
|
610
|
+
# duplicated tuple re-looked-up by name would return a MultiIndex instead of an integer.
|
|
611
|
+
def assign_multiple_vectors(name, pos, v)
|
|
612
|
+
positions = @vectors.is_a?(MultiIndex) ? @vectors.positions_for(name.flatten) : pos.map { |p| @vectors[p] }
|
|
613
|
+
# Give all but the first match an independent copy so duplicate columns don't share one
|
|
614
|
+
# Vector instance (a later cell mutation would otherwise leak across both).
|
|
615
|
+
positions.each_with_index { |position, i| @data[position] = i.zero? ? v : v.dup }
|
|
606
616
|
end
|
|
607
617
|
|
|
608
618
|
def assign_or_add_vector_rough(name, v)
|
|
@@ -764,11 +774,42 @@ module DaruLite
|
|
|
764
774
|
end
|
|
765
775
|
|
|
766
776
|
def initialize_from_array_of_vectors(source, vectors, index, opts)
|
|
777
|
+
# Build @data positionally rather than round-tripping through a name-keyed hash: duplicate
|
|
778
|
+
# MultiIndex tuples would collapse into a single hash entry and corrupt the columns.
|
|
779
|
+
@vectors = Index.coerce(vectors)
|
|
780
|
+
|
|
781
|
+
first_index = source.first.index
|
|
782
|
+
vectors_have_same_index = source.all? { |vector| first_index == vector.index }
|
|
783
|
+
|
|
767
784
|
clone = opts[:clone] != false
|
|
768
|
-
|
|
769
|
-
|
|
785
|
+
clone = true unless index || vectors_have_same_index
|
|
786
|
+
|
|
787
|
+
@index = deduce_index_from_vectors(index, source, first_index, vectors_have_same_index)
|
|
788
|
+
|
|
789
|
+
@data =
|
|
790
|
+
if !clone
|
|
791
|
+
source.dup
|
|
792
|
+
elsif vectors_have_same_index
|
|
793
|
+
source.map(&:dup)
|
|
794
|
+
else
|
|
795
|
+
source.map.with_index { |vector, position| reindex_vector(vector, @vectors.to_a[position]) }
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def deduce_index_from_vectors(index, source, first_index, vectors_have_same_index)
|
|
800
|
+
if index
|
|
801
|
+
Index.coerce(index)
|
|
802
|
+
elsif vectors_have_same_index
|
|
803
|
+
first_index.dup
|
|
804
|
+
else
|
|
805
|
+
DaruLite::Index.new(source.map { |vector| vector.index.to_a }.flatten.uniq.sort)
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
def reindex_vector(vector, name)
|
|
810
|
+
DaruLite::Vector.new([], name: name, index: @index).tap do |new_vector|
|
|
811
|
+
@index.each { |idx| new_vector[idx] = vector.index.include?(idx) ? vector[idx] : nil }
|
|
770
812
|
end
|
|
771
|
-
initialize(hsh, index: index, order: vectors, name: @name, clone: clone)
|
|
772
813
|
end
|
|
773
814
|
|
|
774
815
|
def initialize_from_array_of_hashes(source, vectors, index, _opts)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
module DaruLite
|
|
2
2
|
class CategoricalIndex < Index
|
|
3
|
+
UNSUPPORTED_RANGE_MSG =
|
|
4
|
+
'CategoricalIndex does not support label-range slicing: ' \
|
|
5
|
+
'categories are unordered and may repeat'.freeze
|
|
6
|
+
|
|
3
7
|
# Create a categorical index object.
|
|
4
8
|
# @param indexes [Array<object>] array of indexes
|
|
5
9
|
# @return [DaruLite::CategoricalIndex] categorical index
|
|
@@ -51,6 +55,10 @@ module DaruLite
|
|
|
51
55
|
# x.pos :a, 1
|
|
52
56
|
# # => [0, 1, 2, 3]
|
|
53
57
|
def pos(*indexes)
|
|
58
|
+
if indexes.size == 1 && indexes.first.is_a?(Range) && !include?(indexes.first)
|
|
59
|
+
return pos_from_range(indexes.first)
|
|
60
|
+
end
|
|
61
|
+
|
|
54
62
|
positions = indexes.map do |index|
|
|
55
63
|
if include? index
|
|
56
64
|
@cat_hash[index]
|
|
@@ -81,6 +89,35 @@ module DaruLite
|
|
|
81
89
|
nil
|
|
82
90
|
end
|
|
83
91
|
|
|
92
|
+
# Returns the category located at a positional value.
|
|
93
|
+
# Mirrors Index#key but reads the categorical structure instead of @keys,
|
|
94
|
+
# which CategoricalIndex never populates.
|
|
95
|
+
# @param value [Integer] positional value
|
|
96
|
+
# @return [object, nil] category at the position, or nil if out of range
|
|
97
|
+
# @example
|
|
98
|
+
# idx = DaruLite::CategoricalIndex.new [:a, :b, :a]
|
|
99
|
+
# idx.key 1 # => :b
|
|
100
|
+
# idx.key 99 # => nil
|
|
101
|
+
def key(value)
|
|
102
|
+
return nil unless value.is_a?(Numeric)
|
|
103
|
+
|
|
104
|
+
to_a[value]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Label-range slicing is not supported: categories are unordered and may
|
|
108
|
+
# repeat, so a range of positions between two labels is ambiguous.
|
|
109
|
+
# @raise [ArgumentError] always
|
|
110
|
+
def slice(*)
|
|
111
|
+
raise ArgumentError, UNSUPPORTED_RANGE_MSG
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Label-range slicing is not supported: categories are unordered and may
|
|
115
|
+
# repeat, so a range of positions between two labels is ambiguous.
|
|
116
|
+
# @raise [ArgumentError] always
|
|
117
|
+
def subset_slice(*)
|
|
118
|
+
raise ArgumentError, UNSUPPORTED_RANGE_MSG
|
|
119
|
+
end
|
|
120
|
+
|
|
84
121
|
# Returns index value from position
|
|
85
122
|
# @param pos [Integer] the position to look for
|
|
86
123
|
# @return [object] category corresponding to position
|
|
@@ -206,6 +243,31 @@ module DaruLite
|
|
|
206
243
|
|
|
207
244
|
private
|
|
208
245
|
|
|
246
|
+
# Single-key position lookup, resolved against the categorical structure.
|
|
247
|
+
# Overrides Index#numeric_pos, which reads the never-populated @relation_hash.
|
|
248
|
+
# @raise [IndexError] when the key is neither a valid category nor position
|
|
249
|
+
def numeric_pos(key)
|
|
250
|
+
pos(key)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Label-range slicing is not supported (see #slice / #subset_slice).
|
|
254
|
+
# @raise [ArgumentError] always
|
|
255
|
+
def preprocess_range(_rng)
|
|
256
|
+
raise ArgumentError, UNSUPPORTED_RANGE_MSG
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# A positional (integer) range resolves to the positions it spans, mirroring
|
|
260
|
+
# pandas iloc / Vector#head / #tail. A non-integer label range is rejected
|
|
261
|
+
# because categories are unordered and may repeat (like pandas .loc on an
|
|
262
|
+
# unordered CategoricalIndex).
|
|
263
|
+
# @raise [ArgumentError] when the range bounds are not integers
|
|
264
|
+
def pos_from_range(rng)
|
|
265
|
+
raise ArgumentError, UNSUPPORTED_RANGE_MSG unless rng.begin.is_a?(Integer) && rng.end.is_a?(Integer)
|
|
266
|
+
|
|
267
|
+
# size.times.to_a[rng] clamps to valid positions; nil (fully out of range) -> []
|
|
268
|
+
size.times.to_a[rng] || []
|
|
269
|
+
end
|
|
270
|
+
|
|
209
271
|
def int_from_cat(cat)
|
|
210
272
|
@cat_hash.keys.index cat
|
|
211
273
|
end
|
|
@@ -227,22 +227,40 @@ module DaruLite
|
|
|
227
227
|
collection.one? ? collection.first : collection
|
|
228
228
|
end
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
# Return every integer position whose tuple matches the (possibly partial) key.
|
|
231
|
+
# Unlike #retrieve_from_tuple, the return type is always an Array of positions,
|
|
232
|
+
# even when a full tuple is duplicated across several positions.
|
|
233
|
+
#
|
|
234
|
+
# @param key [Array] a full or left-anchored partial tuple
|
|
235
|
+
# @return [Array<Integer>] matching positions, in ascending order
|
|
236
|
+
def positions_for(key)
|
|
231
237
|
chosen = []
|
|
232
238
|
|
|
233
239
|
key.each_with_index do |k, depth|
|
|
234
|
-
|
|
240
|
+
# An over-long key runs past the defined levels; guard explicitly rather than letting
|
|
241
|
+
# nil[k] raise NoMethodError.
|
|
242
|
+
level = @levels[depth]
|
|
243
|
+
raise IndexError, "Specified index #{key.inspect} do not exist" if level.nil?
|
|
244
|
+
|
|
245
|
+
level_index = level[k]
|
|
235
246
|
raise IndexError, "Specified index #{key.inspect} do not exist" if level_index.nil?
|
|
236
247
|
|
|
237
|
-
|
|
238
|
-
chosen = find_all_indexes label, level_index, chosen
|
|
248
|
+
chosen = find_all_indexes(@labels[depth], level_index, chosen)
|
|
239
249
|
end
|
|
240
250
|
|
|
251
|
+
chosen
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def retrieve_from_tuple(key)
|
|
255
|
+
chosen = positions_for(key)
|
|
256
|
+
|
|
257
|
+
# Every level component existed individually but no tuple matches the full combination
|
|
258
|
+
# (e.g. [:a, :two, :foo] when that triple was never indexed together).
|
|
259
|
+
raise IndexError, "Specified index #{key.inspect} do not exist" if chosen.empty?
|
|
260
|
+
|
|
241
261
|
return chosen[0] if chosen.size == 1 && key.size == @levels.size
|
|
242
262
|
|
|
243
263
|
multi_index_from_multiple_selections(chosen)
|
|
244
|
-
rescue NoMethodError
|
|
245
|
-
raise IndexError, "Specified index #{key.inspect} do not exist"
|
|
246
264
|
end
|
|
247
265
|
|
|
248
266
|
def multi_index_from_multiple_selections(chosen)
|
|
@@ -166,7 +166,9 @@ module DaruLite
|
|
|
166
166
|
private
|
|
167
167
|
|
|
168
168
|
def apply_method_to_numerics(method, *)
|
|
169
|
-
numerics = @
|
|
169
|
+
numerics = @data.each_with_index
|
|
170
|
+
.map { |v, position| [@vectors.to_a[position], v] }
|
|
171
|
+
.select { |_n, v| v.numeric? }
|
|
170
172
|
computed = numerics.map { |_n, v| v.send(method, *) }
|
|
171
173
|
|
|
172
174
|
DaruLite::DataFrame.new(computed, index: @index, order: numerics.map(&:first), clone: false)
|
|
@@ -187,10 +189,12 @@ module DaruLite
|
|
|
187
189
|
end
|
|
188
190
|
|
|
189
191
|
def compute_stats(method)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
# Iterate by position (via each_vector_with_index) instead of `self[name]`: a MultiIndex
|
|
193
|
+
# with duplicate tuples would make the name lookup ambiguous and collapse the result.
|
|
194
|
+
stats = each_vector_with_index.select { |vector, _name| vector.numeric? }
|
|
195
|
+
.map { |vector, name| [name, vector.send(method)] }
|
|
196
|
+
|
|
197
|
+
DaruLite::Vector.new(stats.map(&:last), index: stats.map(&:first), name: method)
|
|
194
198
|
end
|
|
195
199
|
alias sds std
|
|
196
200
|
alias variance variance_sample
|
data/lib/daru_lite/version.rb
CHANGED
data/spec/category_spec.rb
CHANGED
|
@@ -1189,6 +1189,32 @@ describe DaruLite::Vector, "categorical" do
|
|
|
1189
1189
|
end
|
|
1190
1190
|
end
|
|
1191
1191
|
|
|
1192
|
+
context "positional slicing on a CategoricalIndex-backed Vector" do
|
|
1193
|
+
let(:dv) do
|
|
1194
|
+
DaruLite::Vector.new(
|
|
1195
|
+
[10, 20, 30, 40, 50],
|
|
1196
|
+
index: DaruLite::CategoricalIndex.new([:a, :b, :a, :a, :c])
|
|
1197
|
+
)
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
context "#head" do
|
|
1201
|
+
it { expect(dv.head(2).to_a).to eq [10, 20] }
|
|
1202
|
+
it { expect(dv.head(99).to_a).to eq [10, 20, 30, 40, 50] }
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
context "#tail" do
|
|
1206
|
+
it { expect(dv.tail(2).to_a).to eq [40, 50] }
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
context "#last" do
|
|
1210
|
+
it { expect(dv.last(2).to_a).to eq [40, 50] }
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
context "positional range" do
|
|
1214
|
+
it { expect(dv[0..2].to_a).to eq [10, 20, 30] }
|
|
1215
|
+
end
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1192
1218
|
context "#set_at" do
|
|
1193
1219
|
context DaruLite::Index do
|
|
1194
1220
|
let (:idx) { DaruLite::Index.new [1, 0, :c] }
|
|
@@ -23,6 +23,29 @@ shared_examples_for 'a duplicatable DataFrame' do
|
|
|
23
23
|
expect(subject.vectors.object_id).not_to eq(df_mi.vectors.object_id)
|
|
24
24
|
expect(subject.index.object_id).not_to eq(df_mi.index.object_id)
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
context "with duplicate tuples" do
|
|
28
|
+
let(:dup_df) do
|
|
29
|
+
DaruLite::DataFrame.new(
|
|
30
|
+
[[1, 2], [3, 4], [5, 6], [7, 8]],
|
|
31
|
+
order: DaruLite::MultiIndex.from_tuples([
|
|
32
|
+
[:weighted_count, 'Wave'], [:weighted_count, 'Wave'],
|
|
33
|
+
[:percentage, 'Wave'], [:percentage, 'Wave']
|
|
34
|
+
]),
|
|
35
|
+
index: %w[Yes No]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "dups without corrupting the columns and preserves the MultiIndex" do
|
|
40
|
+
copy = dup_df.dup
|
|
41
|
+
|
|
42
|
+
expect(copy).to eq(dup_df)
|
|
43
|
+
expect(copy.vectors).to be_a(DaruLite::MultiIndex)
|
|
44
|
+
expect(copy.vectors).to eq(dup_df.vectors)
|
|
45
|
+
expect(copy.data.map(&:to_a)).to eq([[1, 2], [3, 4], [5, 6], [7, 8]])
|
|
46
|
+
expect(copy.vectors.object_id).not_to eq(dup_df.vectors.object_id)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
26
49
|
end
|
|
27
50
|
end
|
|
28
51
|
|
|
@@ -79,6 +79,35 @@ shared_examples_for 'a fetchable DataFrame' do
|
|
|
79
79
|
)
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
context "with duplicate tuples in a MultiIndex" do
|
|
84
|
+
let(:dup_df) do
|
|
85
|
+
DaruLite::DataFrame.new(
|
|
86
|
+
[[1, 2], [3, 4], [5, 6], [7, 8]],
|
|
87
|
+
order: DaruLite::MultiIndex.from_tuples([
|
|
88
|
+
[:weighted_count, 'Wave'], [:weighted_count, 'Wave'],
|
|
89
|
+
[:percentage, 'Wave'], [:percentage, 'Wave']
|
|
90
|
+
]),
|
|
91
|
+
index: %w[Yes No]
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "returns a DataFrame with all matching columns for a partial key" do
|
|
96
|
+
result = dup_df[:weighted_count]
|
|
97
|
+
|
|
98
|
+
expect(result).to be_a(DaruLite::DataFrame)
|
|
99
|
+
expect(result.ncols).to eq(2)
|
|
100
|
+
expect(result.map_vectors(&:to_a)).to eq([[1, 2], [3, 4]])
|
|
101
|
+
expect(result.index).to eq(dup_df.index)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "returns a DataFrame with all matching columns for a duplicate full tuple" do
|
|
105
|
+
result = dup_df[:weighted_count, 'Wave']
|
|
106
|
+
|
|
107
|
+
expect(result).to be_a(DaruLite::DataFrame)
|
|
108
|
+
expect(result.map_vectors(&:to_a)).to eq([[1, 2], [3, 4]])
|
|
109
|
+
end
|
|
110
|
+
end
|
|
82
111
|
end
|
|
83
112
|
|
|
84
113
|
describe "#at" do
|
|
@@ -25,6 +25,22 @@ shared_examples_for 'an iterable DataFrame' do
|
|
|
25
25
|
|
|
26
26
|
expect(ret).to eq(df)
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
it "iterates over vectors of a MultiIndex with duplicate tuples" do
|
|
30
|
+
order = DaruLite::MultiIndex.from_tuples([
|
|
31
|
+
[:weighted_count, 'Wave'], [:weighted_count, 'Wave'],
|
|
32
|
+
[:percentage, 'Wave'], [:percentage, 'Wave']
|
|
33
|
+
])
|
|
34
|
+
dup_df = DaruLite::DataFrame.new([[1, 2], [3, 4], [5, 6], [7, 8]], order: order, index: %w[Yes No])
|
|
35
|
+
|
|
36
|
+
tuples = []
|
|
37
|
+
dup_df.each_vector_with_index do |vector, index|
|
|
38
|
+
tuples << index
|
|
39
|
+
expect(vector).to be_a(DaruLite::Vector)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
expect(tuples).to eq(order.to_a)
|
|
43
|
+
end
|
|
28
44
|
end
|
|
29
45
|
|
|
30
46
|
describe "#each_row_with_index" do
|
|
@@ -221,6 +221,37 @@ shared_examples_for 'a setable DataFrame' do
|
|
|
221
221
|
|
|
222
222
|
expect(df_empty[:c, :one, :bar].name).to eq "conebar"
|
|
223
223
|
end
|
|
224
|
+
|
|
225
|
+
context "with duplicate tuples" do
|
|
226
|
+
let(:dup_df) do
|
|
227
|
+
DaruLite::DataFrame.new(
|
|
228
|
+
[[1, 2], [3, 4], [5, 6], [7, 8]],
|
|
229
|
+
order: DaruLite::MultiIndex.from_tuples([
|
|
230
|
+
[:weighted_count, 'Wave'], [:weighted_count, 'Wave'],
|
|
231
|
+
[:percentage, 'Wave'], [:percentage, 'Wave']
|
|
232
|
+
]),
|
|
233
|
+
index: %w[Yes No]
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "assigns every column matching a duplicate full tuple" do
|
|
238
|
+
dup_df[:weighted_count, 'Wave'] = [10, 20]
|
|
239
|
+
|
|
240
|
+
expect(dup_df.data.map(&:to_a)).to eq([[10, 20], [10, 20], [5, 6], [7, 8]])
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "assigns independent Vector instances to duplicate columns" do
|
|
244
|
+
dup_df[:weighted_count, 'Wave'] = [10, 20]
|
|
245
|
+
|
|
246
|
+
expect(dup_df.data[0].object_id).not_to eq(dup_df.data[1].object_id)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "assigns every column matching a partial key" do
|
|
250
|
+
dup_df[:percentage] = [99, 98]
|
|
251
|
+
|
|
252
|
+
expect(dup_df.data.map(&:to_a)).to eq([[1, 2], [3, 4], [99, 98], [99, 98]])
|
|
253
|
+
end
|
|
254
|
+
end
|
|
224
255
|
end
|
|
225
256
|
end
|
|
226
257
|
|
|
@@ -240,7 +240,25 @@ shared_examples_for 'a sortable DataFrame' do
|
|
|
240
240
|
end
|
|
241
241
|
|
|
242
242
|
context DaruLite::MultiIndex do
|
|
243
|
-
|
|
243
|
+
context "with duplicate tuples" do
|
|
244
|
+
let(:dup_df) do
|
|
245
|
+
DaruLite::DataFrame.new(
|
|
246
|
+
[[1, 2], [3, 4], [5, 6], [7, 8]],
|
|
247
|
+
order: DaruLite::MultiIndex.from_tuples([
|
|
248
|
+
[:weighted_count, 'Wave'], [:weighted_count, 'Wave'],
|
|
249
|
+
[:percentage, 'Wave'], [:percentage, 'Wave']
|
|
250
|
+
]),
|
|
251
|
+
index: %w[Yes No]
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "sorts by a duplicated vector without raising" do
|
|
256
|
+
sorted = dup_df.sort([[:percentage, 'Wave']], ascending: [false])
|
|
257
|
+
|
|
258
|
+
expect(sorted.index.to_a).to eq(%w[No Yes])
|
|
259
|
+
expect(sorted.data.map(&:to_a)).to eq([[2, 1], [4, 3], [6, 5], [8, 7]])
|
|
260
|
+
end
|
|
261
|
+
end
|
|
244
262
|
end
|
|
245
263
|
|
|
246
264
|
context DaruLite::CategoricalIndex do
|
data/spec/dataframe_spec.rb
CHANGED
|
@@ -148,6 +148,18 @@ describe DaruLite::DataFrame do
|
|
|
148
148
|
expect(df.a) .to eq(DaruLite::Vector.new([2]*5))
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
+
it "names the reindexed vectors after their order labels when indexes differ" do
|
|
152
|
+
df = DaruLite::DataFrame.new(
|
|
153
|
+
[
|
|
154
|
+
DaruLite::Vector.new([1, 2], index: [:one, :two]),
|
|
155
|
+
DaruLite::Vector.new([3, 4], index: [:two, :three])
|
|
156
|
+
],
|
|
157
|
+
order: [:b, :a]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
expect(df.data.map(&:name)).to eq([:b, :a])
|
|
161
|
+
end
|
|
162
|
+
|
|
151
163
|
it "accepts Index objects for row/col" do
|
|
152
164
|
rows = DaruLite::Index.new [:one, :two, :three, :four, :five]
|
|
153
165
|
cols = DaruLite::Index.new [:a, :b]
|
|
@@ -865,6 +877,30 @@ describe DaruLite::DataFrame do
|
|
|
865
877
|
index: [:one, :two, :three, :four, :five]))
|
|
866
878
|
end
|
|
867
879
|
end
|
|
880
|
+
|
|
881
|
+
context "with duplicate tuples in a MultiIndex" do
|
|
882
|
+
let(:dup_df) do
|
|
883
|
+
DaruLite::DataFrame.new(
|
|
884
|
+
[[1, 2], [3, 4], [5, 6], [7, 8]],
|
|
885
|
+
order: DaruLite::MultiIndex.from_tuples([
|
|
886
|
+
[:weighted_count, 'Wave'], [:weighted_count, 'Wave'],
|
|
887
|
+
[:percentage, 'Wave'], [:percentage, 'Wave']
|
|
888
|
+
]),
|
|
889
|
+
index: %w[Yes No]
|
|
890
|
+
)
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
it "deletes the first matching column, leaving the other in place" do
|
|
894
|
+
dup_df.delete_vector([:weighted_count, 'Wave'])
|
|
895
|
+
|
|
896
|
+
expect(dup_df.ncols).to eq(3)
|
|
897
|
+
expect(dup_df.vectors).to be_a(DaruLite::MultiIndex)
|
|
898
|
+
expect(dup_df.vectors.to_a).to eq([
|
|
899
|
+
[:weighted_count, 'Wave'], [:percentage, 'Wave'], [:percentage, 'Wave']
|
|
900
|
+
])
|
|
901
|
+
expect(dup_df.data.map(&:to_a)).to eq([[3, 4], [5, 6], [7, 8]])
|
|
902
|
+
end
|
|
903
|
+
end
|
|
868
904
|
end
|
|
869
905
|
|
|
870
906
|
context "#delete_vectors" do
|
|
@@ -32,6 +32,24 @@ describe DaruLite::CategoricalIndex do
|
|
|
32
32
|
it { expect { index.pos :e }.to raise_error IndexError }
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
context "label range argument" do
|
|
36
|
+
it { expect { index.pos :a..:c }.to raise_error ArgumentError }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context "positional (integer) range argument" do
|
|
40
|
+
it { expect(index.pos(0..2)).to eq [0, 1, 2] }
|
|
41
|
+
|
|
42
|
+
context "when the range extends past the end" do
|
|
43
|
+
it { expect(index.pos(0..99)).to eq [0, 1, 2, 3, 4] }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context "when a category is itself a Range" do
|
|
48
|
+
let(:index) { described_class.new [(1..2), (3..4), (1..2)] }
|
|
49
|
+
|
|
50
|
+
it { expect(index.pos(1..2)).to eq [0, 2] }
|
|
51
|
+
end
|
|
52
|
+
|
|
35
53
|
context "positional index" do
|
|
36
54
|
it { expect(index.pos 0).to eq 0 }
|
|
37
55
|
end
|
|
@@ -98,6 +116,56 @@ describe DaruLite::CategoricalIndex do
|
|
|
98
116
|
|
|
99
117
|
it { is_expected.to be_nil }
|
|
100
118
|
end
|
|
119
|
+
|
|
120
|
+
context "when given a label range" do
|
|
121
|
+
it { expect { index[:a..:c] }.to raise_error ArgumentError }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context "when given a positional (integer) range" do
|
|
125
|
+
subject { index[0..2] }
|
|
126
|
+
|
|
127
|
+
it { is_expected.to eq [0, 1, 2] }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context "when a category is itself a Range" do
|
|
131
|
+
let(:index) { described_class.new [(1..2), (3..4), (1..2)] }
|
|
132
|
+
|
|
133
|
+
it { expect(index[1..2]).to eq [0, 2] }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "#key" do
|
|
138
|
+
context "when given a position" do
|
|
139
|
+
subject { index.key(1) }
|
|
140
|
+
|
|
141
|
+
it { is_expected.to eq :b }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
context "when the position holds a duplicated category" do
|
|
145
|
+
subject { index.key(3) }
|
|
146
|
+
|
|
147
|
+
it { is_expected.to eq :a }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
context "when the position is out of range" do
|
|
151
|
+
subject { index.key(99) }
|
|
152
|
+
|
|
153
|
+
it { is_expected.to be_nil }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
context "when given a non-numeric value" do
|
|
157
|
+
subject { index.key(:a) }
|
|
158
|
+
|
|
159
|
+
it { is_expected.to be_nil }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe "#slice" do
|
|
164
|
+
it { expect { index.slice(:a, :c) }.to raise_error ArgumentError }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe "#subset_slice" do
|
|
168
|
+
it { expect { index.subset_slice(:a, :c) }.to raise_error ArgumentError }
|
|
101
169
|
end
|
|
102
170
|
|
|
103
171
|
context "#subset" do
|
|
@@ -542,6 +542,45 @@ describe DaruLite::MultiIndex do
|
|
|
542
542
|
# TODO: Add specs for IndexError
|
|
543
543
|
end
|
|
544
544
|
|
|
545
|
+
context "#positions_for" do
|
|
546
|
+
let(:idx) do
|
|
547
|
+
described_class.from_tuples([
|
|
548
|
+
[:b, :one, :bar],
|
|
549
|
+
[:b, :two, :bar],
|
|
550
|
+
[:b, :two, :baz],
|
|
551
|
+
[:b, :one, :foo]
|
|
552
|
+
])
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
it "returns all matching positions for a partial key" do
|
|
556
|
+
expect(idx.positions_for([:b, :one])).to eq([0, 3])
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
it "returns a single-element array for a full unique tuple" do
|
|
560
|
+
expect(idx.positions_for([:b, :one, :bar])).to eq([0])
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
it "returns every position for duplicate tuples" do
|
|
564
|
+
dup_idx = described_class.from_tuples([
|
|
565
|
+
[:weighted_count, 'Wave'],
|
|
566
|
+
[:weighted_count, 'Wave'],
|
|
567
|
+
[:percentage, 'Wave'],
|
|
568
|
+
[:percentage, 'Wave']
|
|
569
|
+
])
|
|
570
|
+
|
|
571
|
+
expect(dup_idx.positions_for([:weighted_count, 'Wave'])).to eq([0, 1])
|
|
572
|
+
expect(dup_idx.positions_for([:weighted_count])).to eq([0, 1])
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
it "raises IndexError for a non-existent key" do
|
|
576
|
+
expect { idx.positions_for([:z]) }.to raise_error(IndexError)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
it "raises IndexError (not NoMethodError) for a key longer than the levels" do
|
|
580
|
+
expect { idx.positions_for([:b, :one, :bar, :extra]) }.to raise_error(IndexError)
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
545
584
|
context "#subset" do
|
|
546
585
|
let(:idx) do
|
|
547
586
|
described_class.from_tuples([
|
|
@@ -20,6 +20,19 @@ describe DaruLite::DataFrame do
|
|
|
20
20
|
it "calculates mean of multi level numeric only vectors and returns values in a DataFrame" do
|
|
21
21
|
# TODO - pending
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
it "calculates mean of a MultiIndex with duplicate tuples without raising" do
|
|
25
|
+
dup_df = DaruLite::DataFrame.new(
|
|
26
|
+
[[1, 3], [2, 4], [5, 7], [6, 8]],
|
|
27
|
+
order: DaruLite::MultiIndex.from_tuples([
|
|
28
|
+
[:weighted_count, 'Wave'], [:weighted_count, 'Wave'],
|
|
29
|
+
[:percentage, 'Wave'], [:percentage, 'Wave']
|
|
30
|
+
]),
|
|
31
|
+
index: %w[Yes No]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
expect(dup_df.mean.to_a).to eq([2.0, 3.0, 6.0, 7.0])
|
|
35
|
+
end
|
|
23
36
|
end
|
|
24
37
|
|
|
25
38
|
context "#variance_sample" do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: daru_lite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Naude-Filonnière
|
|
@@ -550,7 +550,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
550
550
|
- !ruby/object:Gem::Version
|
|
551
551
|
version: '0'
|
|
552
552
|
requirements: []
|
|
553
|
-
rubygems_version: 4.0.
|
|
553
|
+
rubygems_version: 4.0.15
|
|
554
554
|
specification_version: 4
|
|
555
555
|
summary: Data Analysis in RUby, stripped down
|
|
556
556
|
test_files:
|