daru 0.1.3.1 → 0.1.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/.gitignore +2 -0
- data/.rspec +2 -1
- data/.rspec_formatter.rb +33 -0
- data/.rubocop.yml +26 -2
- data/History.md +38 -0
- data/README.md +22 -13
- data/Rakefile +50 -2
- data/benchmarks/csv_reading.rb +22 -0
- data/daru.gemspec +9 -2
- data/lib/daru.rb +36 -4
- data/lib/daru/accessors/array_wrapper.rb +6 -1
- data/lib/daru/accessors/dataframe_by_row.rb +10 -2
- data/lib/daru/accessors/gsl_wrapper.rb +1 -3
- data/lib/daru/accessors/nmatrix_wrapper.rb +9 -0
- data/lib/daru/category.rb +935 -0
- data/lib/daru/core/group_by.rb +29 -38
- data/lib/daru/core/merge.rb +186 -145
- data/lib/daru/core/query.rb +22 -11
- data/lib/daru/dataframe.rb +976 -885
- data/lib/daru/date_time/index.rb +166 -166
- data/lib/daru/date_time/offsets.rb +66 -77
- data/lib/daru/formatters/table.rb +54 -0
- data/lib/daru/helpers/array.rb +40 -0
- data/lib/daru/index.rb +476 -73
- data/lib/daru/io/io.rb +66 -45
- data/lib/daru/io/sql_data_source.rb +33 -62
- data/lib/daru/iruby/helpers.rb +38 -0
- data/lib/daru/iruby/templates/dataframe.html.erb +52 -0
- data/lib/daru/iruby/templates/dataframe_mi.html.erb +58 -0
- data/lib/daru/iruby/templates/multi_index.html.erb +12 -0
- data/lib/daru/iruby/templates/vector.html.erb +27 -0
- data/lib/daru/iruby/templates/vector_mi.html.erb +36 -0
- data/lib/daru/maths/arithmetic/dataframe.rb +16 -18
- data/lib/daru/maths/arithmetic/vector.rb +4 -6
- data/lib/daru/maths/statistics/dataframe.rb +8 -15
- data/lib/daru/maths/statistics/vector.rb +120 -98
- data/lib/daru/monkeys.rb +12 -40
- data/lib/daru/plotting/gruff.rb +3 -0
- data/lib/daru/plotting/gruff/category.rb +49 -0
- data/lib/daru/plotting/gruff/dataframe.rb +91 -0
- data/lib/daru/plotting/gruff/vector.rb +57 -0
- data/lib/daru/plotting/nyaplot.rb +3 -0
- data/lib/daru/plotting/nyaplot/category.rb +34 -0
- data/lib/daru/plotting/nyaplot/dataframe.rb +187 -0
- data/lib/daru/plotting/nyaplot/vector.rb +46 -0
- data/lib/daru/vector.rb +694 -421
- data/lib/daru/version.rb +1 -1
- data/profile/_base.rb +23 -0
- data/profile/df_to_a.rb +10 -0
- data/profile/filter.rb +13 -0
- data/profile/joining.rb +13 -0
- data/profile/sorting.rb +12 -0
- data/profile/vector_each_with_index.rb +9 -0
- data/spec/accessors/wrappers_spec.rb +2 -4
- data/spec/categorical_spec.rb +1734 -0
- data/spec/core/group_by_spec.rb +52 -2
- data/spec/core/merge_spec.rb +63 -2
- data/spec/core/query_spec.rb +236 -80
- data/spec/dataframe_spec.rb +1373 -79
- data/spec/date_time/data_spec.rb +3 -5
- data/spec/date_time/index_spec.rb +154 -17
- data/spec/date_time/offsets_spec.rb +3 -4
- data/spec/fixtures/empties.dat +2 -0
- data/spec/fixtures/strings.dat +2 -0
- data/spec/formatters/table_formatter_spec.rb +99 -0
- data/spec/helpers_spec.rb +8 -0
- data/spec/index/categorical_index_spec.rb +168 -0
- data/spec/index/index_spec.rb +283 -0
- data/spec/index/multi_index_spec.rb +570 -0
- data/spec/io/io_spec.rb +31 -4
- data/spec/io/sql_data_source_spec.rb +0 -1
- data/spec/iruby/dataframe_spec.rb +172 -0
- data/spec/iruby/helpers_spec.rb +49 -0
- data/spec/iruby/multi_index_spec.rb +37 -0
- data/spec/iruby/vector_spec.rb +107 -0
- data/spec/math/arithmetic/dataframe_spec.rb +71 -13
- data/spec/math/arithmetic/vector_spec.rb +8 -10
- data/spec/math/statistics/dataframe_spec.rb +3 -5
- data/spec/math/statistics/vector_spec.rb +45 -55
- data/spec/monkeys_spec.rb +32 -9
- data/spec/plotting/dataframe_spec.rb +386 -0
- data/spec/plotting/vector_spec.rb +230 -0
- data/spec/shared/vector_display_spec.rb +215 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/vector_spec.rb +905 -138
- metadata +143 -11
- data/.rubocop_todo.yml +0 -44
- data/lib/daru/plotting/dataframe.rb +0 -104
- data/lib/daru/plotting/vector.rb +0 -38
- data/spec/daru_spec.rb +0 -58
- data/spec/index_spec.rb +0 -375
data/lib/daru/core/group_by.rb
CHANGED
@@ -11,20 +11,21 @@ module Daru
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
TUPLE_SORTER = lambda do |a, b|
|
15
|
+
if a && b
|
16
|
+
a.compact <=> b.compact
|
17
|
+
else
|
18
|
+
a ? 1 : -1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
14
22
|
def initialize context, names
|
15
23
|
@groups = {}
|
16
24
|
@non_group_vectors = context.vectors.to_a - names
|
17
25
|
@context = context
|
18
26
|
vectors = names.map { |vec| context[vec].to_a }
|
19
27
|
tuples = vectors[0].zip(*vectors[1..-1])
|
20
|
-
keys =
|
21
|
-
tuples.uniq.sort do |a,b|
|
22
|
-
if a && b
|
23
|
-
a.compact <=> b.compact
|
24
|
-
else
|
25
|
-
a ? 1 : -1
|
26
|
-
end
|
27
|
-
end
|
28
|
+
keys = tuples.uniq.sort(&TUPLE_SORTER)
|
28
29
|
|
29
30
|
keys.each do |key|
|
30
31
|
@groups[key] = all_indices_for(tuples, key)
|
@@ -189,17 +190,9 @@ module Daru
|
|
189
190
|
# # 5 bar two 6 66
|
190
191
|
def get_group group
|
191
192
|
indexes = @groups[group]
|
192
|
-
elements =
|
193
|
-
|
194
|
-
@context.each_vector do |vector|
|
195
|
-
elements << vector.to_a
|
196
|
-
end
|
197
|
-
rows = []
|
193
|
+
elements = @context.each_vector.map(&:to_a)
|
198
194
|
transpose = elements.transpose
|
199
|
-
|
200
|
-
indexes.each do |idx|
|
201
|
-
rows << transpose[idx]
|
202
|
-
end
|
195
|
+
rows = indexes.each.map { |idx| transpose[idx] }
|
203
196
|
|
204
197
|
new_index =
|
205
198
|
begin
|
@@ -207,6 +200,7 @@ module Daru
|
|
207
200
|
rescue IndexError
|
208
201
|
indexes
|
209
202
|
end
|
203
|
+
|
210
204
|
Daru::DataFrame.rows(
|
211
205
|
rows, index: new_index, order: @context.vectors
|
212
206
|
)
|
@@ -224,7 +218,7 @@ module Daru
|
|
224
218
|
# })
|
225
219
|
# df.group_by([:a]).reduce('') { |result, row| result += row[:c]; result }
|
226
220
|
# # =>
|
227
|
-
# # #<Daru::Vector:70343147159900 @name = nil @
|
221
|
+
# # #<Daru::Vector:70343147159900 @name = nil @size = 2 >
|
228
222
|
# # nil
|
229
223
|
# # a ACE
|
230
224
|
# # b BDF
|
@@ -268,33 +262,30 @@ module Daru
|
|
268
262
|
end
|
269
263
|
|
270
264
|
def apply_method method_type, method
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
@groups.each do |_group, indexes|
|
275
|
-
single_row = []
|
276
|
-
@non_group_vectors.each do |ngvector|
|
277
|
-
vec = @context[ngvector]
|
278
|
-
if method_type == :numeric && vec.type == :numeric
|
279
|
-
slice = vec[*indexes]
|
280
|
-
single_row << (slice.is_a?(Daru::Vector) ? slice.send(method) : slice)
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
rows << single_row
|
265
|
+
order = @non_group_vectors.select do |ngvec|
|
266
|
+
method_type == :numeric && @context[ngvec].type == :numeric
|
285
267
|
end
|
286
268
|
|
287
|
-
@
|
288
|
-
order
|
289
|
-
|
269
|
+
rows = @groups.map do |_group, indexes|
|
270
|
+
order.map do |ngvector|
|
271
|
+
slice = @context[ngvector][*indexes]
|
272
|
+
slice.is_a?(Daru::Vector) ? slice.send(method) : slice
|
273
|
+
end
|
290
274
|
end
|
291
275
|
|
292
|
-
index =
|
293
|
-
index = multi_index ? Daru::MultiIndex.from_tuples(index) : Daru::Index.new(index.flatten)
|
276
|
+
index = apply_method_index
|
294
277
|
order = Daru::Index.new(order)
|
295
278
|
Daru::DataFrame.new(rows.transpose, index: index, order: order)
|
296
279
|
end
|
297
280
|
|
281
|
+
def apply_method_index
|
282
|
+
if multi_indexed_grouping?
|
283
|
+
Daru::MultiIndex.from_tuples(@groups.keys)
|
284
|
+
else
|
285
|
+
Daru::Index.new(@groups.keys.flatten)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
298
289
|
def all_indices_for arry, element
|
299
290
|
found, index, indexes = -1, -1, []
|
300
291
|
while found
|
data/lib/daru/core/merge.rb
CHANGED
@@ -1,210 +1,251 @@
|
|
1
1
|
module Daru
|
2
2
|
module Core
|
3
|
-
|
4
|
-
class
|
5
|
-
|
6
|
-
matched = nil
|
7
|
-
hash.keys.each { |d|
|
8
|
-
if matcher.match(Regexp.new(d.to_s))
|
9
|
-
matched = d
|
10
|
-
break
|
11
|
-
end
|
12
|
-
}
|
13
|
-
|
14
|
-
return unless matched
|
15
|
-
|
16
|
-
hash[matcher] = hash[matched]
|
17
|
-
hash.delete matched
|
18
|
-
end
|
3
|
+
class MergeFrame
|
4
|
+
class NilSorter
|
5
|
+
include Comparable
|
19
6
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
diff = (recoded - hk).sort
|
7
|
+
def nil?
|
8
|
+
true
|
9
|
+
end
|
24
10
|
|
25
|
-
|
26
|
-
|
27
|
-
replace_keys_if_duplicates df_hash2, a[1]
|
28
|
-
end
|
11
|
+
def ==(_other)
|
12
|
+
false
|
29
13
|
end
|
30
14
|
|
31
|
-
def
|
32
|
-
|
33
|
-
hsh.each { |k,v| hsh[k] = v.to_a }
|
34
|
-
hsh
|
15
|
+
def <=>(other)
|
16
|
+
other.nil? ? 0 : -1
|
35
17
|
end
|
18
|
+
end
|
36
19
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
values = arr[0].map(&:values)
|
20
|
+
def initialize left_df, right_df, opts={}
|
21
|
+
@on = opts[:on]
|
22
|
+
@keep_left, @keep_right = extract_left_right(opts[:how])
|
41
23
|
|
42
|
-
|
43
|
-
end
|
24
|
+
validate_on!(left_df, right_df)
|
44
25
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
# Conceptually simpler and does the same thing, but slows down the
|
59
|
-
# total merge algorithm by 2x. Would be nice to improve the performance
|
60
|
-
# of df.map(:row)
|
61
|
-
#
|
62
|
-
# df.map(:row) do |row|
|
63
|
-
# key_values = on.map { |key| row[key] }
|
64
|
-
# [key_values, row.to_a]
|
65
|
-
# end
|
66
|
-
end
|
26
|
+
key_sanitizer = ->(h) { sanitize_merge_keys(h.values_at(*on)) }
|
27
|
+
|
28
|
+
@left = df_to_a(left_df)
|
29
|
+
@left.sort_by!(&key_sanitizer)
|
30
|
+
@left_key_values = @left.map(&key_sanitizer)
|
31
|
+
|
32
|
+
@right = df_to_a(right_df)
|
33
|
+
@right.sort_by!(&key_sanitizer)
|
34
|
+
@right_key_values = @right.map(&key_sanitizer)
|
35
|
+
|
36
|
+
@left_keys, @right_keys = merge_keys(left_df, right_df, on)
|
37
|
+
end
|
67
38
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
39
|
+
def join
|
40
|
+
res = []
|
41
|
+
|
42
|
+
until left.empty? && right.empty?
|
43
|
+
lkey = first_left_key
|
44
|
+
rkey = first_right_key
|
45
|
+
|
46
|
+
row(lkey, rkey).tap { |r| res << r if r }
|
73
47
|
end
|
48
|
+
|
49
|
+
Daru::DataFrame.new(res, order: left_keys.values + on + right_keys.values)
|
74
50
|
end
|
75
|
-
end
|
76
51
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :on,
|
55
|
+
:left, :left_key_values, :keep_left, :left_keys,
|
56
|
+
:right, :right_key_values, :keep_right, :right_keys
|
57
|
+
|
58
|
+
attr_accessor :merge_key
|
59
|
+
|
60
|
+
LEFT_RIGHT_COMBINATIONS = {
|
61
|
+
# left right
|
62
|
+
inner: [false, false],
|
63
|
+
left: [true, false],
|
64
|
+
right: [false, true],
|
65
|
+
outer: [true, true]
|
66
|
+
}.freeze
|
67
|
+
|
68
|
+
def extract_left_right(how)
|
69
|
+
LEFT_RIGHT_COMBINATIONS[how] or
|
70
|
+
raise ArgumentError, "Unrecognized join option: #{how}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def sanitize_merge_keys(merge_keys)
|
74
|
+
merge_keys.map { |v| v || NilSorter.new }
|
82
75
|
end
|
83
76
|
|
84
|
-
def
|
85
|
-
|
77
|
+
def df_to_a df
|
78
|
+
# FIXME: much faster than "native" DataFrame#to_a. Should not be
|
79
|
+
h = df.to_h
|
80
|
+
keys = h.keys
|
81
|
+
h.values.map(&:to_a).transpose.map { |r| keys.zip(r).to_h }
|
86
82
|
end
|
87
83
|
|
88
|
-
def
|
89
|
-
|
84
|
+
def merge_keys(df1, df2, on)
|
85
|
+
duplicates =
|
86
|
+
(df1.vectors.to_a + df2.vectors.to_a - on)
|
87
|
+
.group_by(&:itself)
|
88
|
+
.select { |_, g| g.count == 2 }.map(&:first)
|
89
|
+
|
90
|
+
[
|
91
|
+
guard_keys(df1.vectors.to_a - on, duplicates, 1),
|
92
|
+
guard_keys(df2.vectors.to_a - on, duplicates, 2)
|
93
|
+
]
|
90
94
|
end
|
91
95
|
|
92
|
-
def
|
93
|
-
|
96
|
+
def guard_keys keys, duplicates, num
|
97
|
+
keys.map { |v| [v, guard_duplicate(v, duplicates, num)] }.to_h
|
94
98
|
end
|
95
99
|
|
96
|
-
def
|
97
|
-
|
100
|
+
def guard_duplicate val, duplicates, num
|
101
|
+
duplicates.include?(val) ? :"#{val}_#{num}" : val
|
98
102
|
end
|
99
103
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
104
|
+
def row(lkey, rkey)
|
105
|
+
case
|
106
|
+
when !lkey && !rkey
|
107
|
+
# :nocov:
|
108
|
+
# It's just an impossibility handler, can't be covered :)
|
109
|
+
raise 'Unexpected condition met during merge'
|
110
|
+
# :nocov:
|
111
|
+
when lkey == rkey
|
112
|
+
self.merge_key = lkey
|
113
|
+
merge_matching_rows
|
114
|
+
when !rkey || lt(lkey, rkey)
|
115
|
+
left_row_missing_right
|
116
|
+
else # !lkey || lt(rkey, lkey)
|
117
|
+
right_row_missing_left
|
118
|
+
end
|
119
|
+
end
|
103
120
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
121
|
+
def merge_matching_rows
|
122
|
+
if one_to_one_merge?
|
123
|
+
merge_rows(one_to_one_left_row, one_to_one_right_row)
|
124
|
+
elsif one_to_many_merge?
|
125
|
+
merge_rows(one_to_many_left_row, one_to_many_right_row)
|
126
|
+
else
|
127
|
+
result = cartesian_product.shift
|
128
|
+
end_cartesian_product if cartesian_product.empty?
|
129
|
+
result
|
130
|
+
end
|
131
|
+
end
|
111
132
|
|
112
|
-
|
113
|
-
|
133
|
+
def one_to_one_merge?
|
134
|
+
merge_key != next_left_key && merge_key != next_right_key
|
135
|
+
end
|
114
136
|
|
115
|
-
|
137
|
+
def one_to_many_merge?
|
138
|
+
!(merge_key == next_left_key && merge_key == next_right_key)
|
139
|
+
end
|
116
140
|
|
117
|
-
|
118
|
-
|
141
|
+
def one_to_one_left_row
|
142
|
+
left_key_values.shift
|
143
|
+
left.shift
|
144
|
+
end
|
119
145
|
|
120
|
-
|
121
|
-
|
146
|
+
def one_to_many_left_row
|
147
|
+
if next_right_key && first_right_key == next_right_key
|
148
|
+
left.first
|
149
|
+
else
|
150
|
+
left_key_values.shift
|
151
|
+
left.shift
|
152
|
+
end
|
153
|
+
end
|
122
154
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
155
|
+
def one_to_one_right_row
|
156
|
+
right_key_values.shift
|
157
|
+
right.shift
|
158
|
+
end
|
127
159
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
add_merge_row_to_hash([nil, df2_array[idx2]], joined_hash) if right
|
135
|
-
idx2 += 1
|
136
|
-
else
|
137
|
-
raise 'Unexpected condition met during merge'
|
138
|
-
end
|
160
|
+
def one_to_many_right_row
|
161
|
+
if next_left_key && first_left_key == next_left_key
|
162
|
+
right.first
|
163
|
+
else
|
164
|
+
right_key_values.shift
|
165
|
+
right.shift
|
139
166
|
end
|
167
|
+
end
|
140
168
|
|
141
|
-
|
169
|
+
def left_row_missing_right
|
170
|
+
val = one_to_one_left_row
|
171
|
+
expand_row(val, left_keys) if keep_left
|
142
172
|
end
|
143
173
|
|
144
|
-
|
174
|
+
def right_row_missing_left
|
175
|
+
val = one_to_one_right_row
|
176
|
+
expand_row(val, right_keys) if keep_right
|
177
|
+
end
|
145
178
|
|
146
|
-
def
|
147
|
-
|
148
|
-
|
179
|
+
def lt(k1, k2)
|
180
|
+
(k1 <=> k2) == -1
|
181
|
+
end
|
149
182
|
|
150
|
-
|
151
|
-
|
152
|
-
|
183
|
+
def merge_rows lrow, rrow
|
184
|
+
left_keys
|
185
|
+
.map { |from, to| [to, lrow[from]] }.to_h
|
186
|
+
.merge(on.map { |col| [col, lrow[col]] }.to_h)
|
187
|
+
.merge(right_keys.map { |from, to| [to, rrow[from]] }.to_h)
|
188
|
+
end
|
153
189
|
|
154
|
-
|
190
|
+
def expand_row row, renamings
|
191
|
+
renamings
|
192
|
+
.map { |from, to| [to, row[from]] }.to_h
|
193
|
+
.merge(on.map { |col| [col, row[col]] }.to_h)
|
155
194
|
end
|
156
195
|
|
157
|
-
def
|
158
|
-
|
196
|
+
def first_right_key
|
197
|
+
right_key_values.empty? ? nil : right_key_values.first
|
159
198
|
end
|
160
199
|
|
161
|
-
def
|
162
|
-
|
200
|
+
def next_right_key
|
201
|
+
right_key_values.size <= 1 ? nil : right_key_values[1]
|
163
202
|
end
|
164
203
|
|
165
|
-
def
|
166
|
-
|
204
|
+
def first_left_key
|
205
|
+
left_key_values.empty? ? nil : left_key_values.first
|
167
206
|
end
|
168
207
|
|
169
|
-
def
|
170
|
-
|
208
|
+
def next_left_key
|
209
|
+
left_key_values.size <= 1 ? nil : left_key_values[1]
|
171
210
|
end
|
172
211
|
|
173
|
-
def
|
174
|
-
|
212
|
+
def left_rows_at_merge_key
|
213
|
+
left.take_while { |arr| sanitize_merge_keys(arr.values_at(*on)) == merge_key }
|
175
214
|
end
|
176
215
|
|
177
|
-
def
|
178
|
-
|
216
|
+
def right_rows_at_merge_key
|
217
|
+
right.take_while { |arr| sanitize_merge_keys(arr.values_at(*on)) == merge_key }
|
179
218
|
end
|
180
219
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
220
|
+
def cartesian_product
|
221
|
+
@cartesian_product ||= left_rows_at_merge_key.product(right_rows_at_merge_key).map do |left_row, right_row|
|
222
|
+
merge_rows(left_row, right_row)
|
223
|
+
end
|
224
|
+
end
|
186
225
|
|
187
|
-
|
188
|
-
|
226
|
+
def end_cartesian_product
|
227
|
+
left_size = left_rows_at_merge_key.size
|
228
|
+
left_key_values.shift(left_size)
|
229
|
+
left.shift(left_size)
|
189
230
|
|
190
|
-
|
191
|
-
|
231
|
+
right_size = right_rows_at_merge_key.size
|
232
|
+
right_key_values.shift(right_size)
|
233
|
+
right.shift(right_size)
|
234
|
+
@cartesian_product = nil
|
235
|
+
end
|
192
236
|
|
193
|
-
|
237
|
+
def validate_on!(left_df, right_df)
|
238
|
+
@on.each do |on|
|
239
|
+
left_df.has_vector?(on) && right_df.has_vector?(on) or
|
240
|
+
raise ArgumentError, "Both dataframes expected to have #{on.inspect} field"
|
194
241
|
end
|
195
242
|
end
|
196
243
|
end
|
197
244
|
|
198
|
-
# Private module containing methods for join, merge, concat operations on
|
199
|
-
# dataframes and vectors.
|
200
|
-
# @private
|
201
245
|
module Merge
|
202
246
|
class << self
|
203
247
|
def join df1, df2, opts={}
|
204
|
-
|
205
|
-
|
206
|
-
mf = MergeFrame.new df1, df2, on: on
|
207
|
-
mf.send opts[:how], {}
|
248
|
+
MergeFrame.new(df1, df2, opts).join
|
208
249
|
end
|
209
250
|
end
|
210
251
|
end
|