predictor 1.0.0 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,10 @@ class Predictor::InputMatrix
3
3
  @opts = opts
4
4
  end
5
5
 
6
+ def parent_redis_key(*append)
7
+ ([@opts.fetch(:redis_prefix)] + append).flatten.compact.join(":")
8
+ end
9
+
6
10
  def redis_key(*append)
7
11
  ([@opts.fetch(:redis_prefix), @opts.fetch(:key)] + append).flatten.compact.join(":")
8
12
  end
@@ -11,30 +15,19 @@ class Predictor::InputMatrix
11
15
  (@opts[:weight] || 1).to_f
12
16
  end
13
17
 
14
- def add_set(set_id, item_ids)
15
- Predictor.redis.multi do
16
- item_ids.each { |item| add_single_nomulti(set_id, item) }
17
- end
18
- end
19
-
20
- def add_set!(set_id, item_ids)
21
- add_set(set_id, item_ids)
22
- item_ids.each { |item_id| process_item!(item_id) }
23
- end
24
-
25
- def add_single(set_id, item_id)
18
+ def add_to_set(set, *items)
19
+ items = items.flatten if items.count == 1 && items[0].is_a?(Array)
26
20
  Predictor.redis.multi do
27
- add_single_nomulti(set_id, item_id)
21
+ items.each { |item| add_single_nomulti(set, item) }
28
22
  end
29
23
  end
30
24
 
31
- def add_single!(set_id, item_id)
32
- add_single(set_id, item_id)
33
- process_item!(item_id)
25
+ def add_set(set, items)
26
+ add_to_set(set, *items)
34
27
  end
35
28
 
36
- def all_items
37
- Predictor.redis.smembers(redis_key(:all_items))
29
+ def add_single(set, item)
30
+ add_to_set(set, item)
38
31
  end
39
32
 
40
33
  def items_for(set)
@@ -45,81 +38,26 @@ class Predictor::InputMatrix
45
38
  Predictor.redis.sunion redis_key(:sets, item)
46
39
  end
47
40
 
48
- def related_items(item_id)
49
- sets = Predictor.redis.smembers(redis_key(:sets, item_id))
41
+ def related_items(item)
42
+ sets = Predictor.redis.smembers(redis_key(:sets, item))
50
43
  keys = sets.map { |set| redis_key(:items, set) }
51
- if keys.length > 0
52
- Predictor.redis.sunion(keys) - [item_id]
53
- else
54
- []
55
- end
56
- end
57
-
58
- def similarity(item1, item2)
59
- Predictor.redis.zscore(redis_key(:similarities, item1), item2)
60
- end
61
-
62
- # calculate all similarities to other items in the matrix for item1
63
- def similarities_for(item1, with_scores: false, offset: 0, limit: -1)
64
- Predictor.redis.zrevrange(redis_key(:similarities, item1), offset, limit == -1 ? limit : offset + (limit - 1), with_scores: with_scores)
65
- end
66
-
67
- def process_item!(item)
68
- cache_similarities_for(item)
69
- end
70
-
71
- def process!
72
- all_items.each do |item|
73
- process_item!(item)
74
- end
44
+ keys.length > 0 ? Predictor.redis.sunion(keys) - [item] : []
75
45
  end
76
46
 
77
- # delete item_id from the matrix
78
- def delete_item!(item_id)
79
- Predictor.redis.srem(redis_key(:all_items), item_id)
80
- Predictor.redis.watch(redis_key(:sets, item_id), redis_key(:similarities, item_id)) do
81
- sets = Predictor.redis.smembers(redis_key(:sets, item_id))
82
- items = Predictor.redis.zrange(redis_key(:similarities, item_id), 0, -1)
47
+ # delete item from the matrix
48
+ def delete_item(item)
49
+ Predictor.redis.watch(redis_key(:sets, item)) do
50
+ sets = Predictor.redis.smembers(redis_key(:sets, item))
83
51
  Predictor.redis.multi do |multi|
84
52
  sets.each do |set|
85
- multi.srem(redis_key(:items, set), item_id)
53
+ multi.srem(redis_key(:items, set), item)
86
54
  end
87
55
 
88
- items.each do |item|
89
- multi.zrem(redis_key(:similarities, item), item_id)
90
- end
91
-
92
- multi.del redis_key(:sets, item_id), redis_key(:similarities, item_id)
56
+ multi.del redis_key(:sets, item)
93
57
  end
94
58
  end
95
59
  end
96
60
 
97
- private
98
-
99
- def add_single_nomulti(set_id, item_id)
100
- Predictor.redis.sadd(redis_key(:all_items), item_id)
101
- Predictor.redis.sadd(redis_key(:items, set_id), item_id)
102
- # add the set_id to the item_id's set--inverting the sets
103
- Predictor.redis.sadd(redis_key(:sets, item_id), set_id)
104
- end
105
-
106
- def cache_similarity(item1, item2)
107
- score = calculate_jaccard(item1, item2)
108
-
109
- if score > 0
110
- Predictor.redis.multi do |multi|
111
- multi.zadd(redis_key(:similarities, item1), score, item2)
112
- multi.zadd(redis_key(:similarities, item2), score, item1)
113
- end
114
- end
115
- end
116
-
117
- def cache_similarities_for(item)
118
- related_items(item).each do |related_item|
119
- cache_similarity(item, related_item)
120
- end
121
- end
122
-
123
61
  def calculate_jaccard(item1, item2)
124
62
  x = nil
125
63
  y = nil
@@ -135,4 +73,13 @@ class Predictor::InputMatrix
135
73
  return 0.0
136
74
  end
137
75
  end
76
+
77
+ private
78
+
79
+ def add_single_nomulti(set, item)
80
+ Predictor.redis.sadd(parent_redis_key(:all_items), item)
81
+ Predictor.redis.sadd(redis_key(:items, set), item)
82
+ # add the set to the item's set--inverting the sets
83
+ Predictor.redis.sadd(redis_key(:sets, item), set)
84
+ end
138
85
  end
@@ -1,3 +1,3 @@
1
1
  module Predictor
2
- VERSION = "1.0.0"
2
+ VERSION = "2.0.0.rc1"
3
3
  end
@@ -8,6 +8,7 @@ describe Predictor::Base do
8
8
  before(:each) do
9
9
  flush_redis!
10
10
  BaseRecommender.input_matrices = {}
11
+ BaseRecommender.limit_similarities_to(nil)
11
12
  end
12
13
 
13
14
  describe "configuration" do
@@ -16,6 +17,11 @@ describe Predictor::Base do
16
17
  BaseRecommender.input_matrices.keys.should == [:myinput]
17
18
  end
18
19
 
20
+ it "should allow a similarity limit" do
21
+ BaseRecommender.limit_similarities_to(100)
22
+ BaseRecommender.similarity_limit.should == 100
23
+ end
24
+
19
25
  it "should retrieve an input_matrix on a new instance" do
20
26
  BaseRecommender.input_matrix(:myinput)
21
27
  sm = BaseRecommender.new
@@ -37,67 +43,56 @@ describe Predictor::Base do
37
43
  end
38
44
  end
39
45
 
40
- describe "process_item!" do
41
- it "should call process_item! on each input_matrix" do
42
- BaseRecommender.input_matrix(:myfirstinput)
43
- BaseRecommender.input_matrix(:mysecondinput)
44
- sm = BaseRecommender.new
45
- sm.myfirstinput.should_receive(:process_item!).with("fnorditem").and_return([["fooitem",0.5]])
46
- sm.mysecondinput.should_receive(:process_item!).with("fnorditem").and_return([["fooitem",0.5]])
47
- sm.process_item!("fnorditem")
48
- end
49
-
50
- it "should call process_item! on each input_matrix and add all outputs to the similarity matrix" do
51
- BaseRecommender.input_matrix(:myfirstinput)
52
- BaseRecommender.input_matrix(:mysecondinput)
46
+ describe "all_items" do
47
+ it "returns all items across all matrices" do
48
+ BaseRecommender.input_matrix(:anotherinput)
49
+ BaseRecommender.input_matrix(:yetanotherinput)
53
50
  sm = BaseRecommender.new
54
- sm.myfirstinput.should_receive(:process_item!).and_return([["fooitem",0.5]])
55
- sm.mysecondinput.should_receive(:process_item!).and_return([["fooitem",0.75], ["baritem", 1.0]])
56
- sm.process_item!("fnorditem")
51
+ sm.add_to_matrix(:anotherinput, 'a', "foo", "bar")
52
+ sm.add_to_matrix(:yetanotherinput, 'b', "fnord", "shmoo", "bar")
53
+ sm.all_items.should include('foo', 'bar', 'fnord', 'shmoo')
54
+ sm.all_items.length.should == 4
57
55
  end
56
+ end
58
57
 
59
- it "should call process_item! on each input_matrix and add all outputs to the similarity matrix with weight" do
60
- BaseRecommender.input_matrix(:myfirstinput, :weight => 4.0)
61
- BaseRecommender.input_matrix(:mysecondinput)
58
+ describe "add_to_matrix" do
59
+ it "calls add_to_set on the given matrix" do
60
+ BaseRecommender.input_matrix(:anotherinput)
62
61
  sm = BaseRecommender.new
63
- sm.myfirstinput.should_receive(:process_item!).and_return([["fooitem",0.5]])
64
- sm.mysecondinput.should_receive(:process_item!).and_return([["fooitem",0.75], ["baritem", 1.0]])
65
- sm.process_item!("fnorditem")
62
+ sm.anotherinput.should_receive(:add_to_set).with('a', 'foo', 'bar')
63
+ sm.add_to_matrix(:anotherinput, 'a', 'foo', 'bar')
66
64
  end
67
- end
68
65
 
69
- describe "all_items" do
70
- it "should retrieve all items from all input matrices" do
66
+ it "adds the items to the all_items storage" do
71
67
  BaseRecommender.input_matrix(:anotherinput)
72
- BaseRecommender.input_matrix(:yetanotherinput)
73
68
  sm = BaseRecommender.new
74
- sm.anotherinput.add_set('a', ["foo", "bar"])
75
- sm.yetanotherinput.add_set('b', ["fnord", "shmoo"])
76
- sm.all_items.length.should == 4
77
- sm.all_items.should include("foo", "bar", "fnord", "shmoo")
69
+ sm.add_to_matrix(:anotherinput, 'a', 'foo', 'bar')
70
+ sm.all_items.should include('foo', 'bar')
78
71
  end
72
+ end
79
73
 
80
- it "should retrieve all items from all input matrices (uniquely)" do
74
+ describe "add_to_matrix!" do
75
+ it "calls add_to_matrix and process_items! for the given items" do
81
76
  BaseRecommender.input_matrix(:anotherinput)
82
- BaseRecommender.input_matrix(:yetanotherinput)
83
77
  sm = BaseRecommender.new
84
- sm.anotherinput.add_set('a', ["foo", "bar"])
85
- sm.yetanotherinput.add_set('b', ["fnord", "bar"])
86
- sm.all_items.length.should == 3
87
- sm.all_items.should include("foo", "bar", "fnord")
78
+ sm.should_receive(:add_to_matrix).with(:anotherinput, 'a', 'foo')
79
+ sm.should_receive(:process_items!).with('foo')
80
+ sm.add_to_matrix!(:anotherinput, 'a', 'foo')
88
81
  end
89
82
  end
90
83
 
91
- describe "process!" do
92
- it "should call process_item for all input_matrix.all_items's" do
84
+ describe "related_items" do
85
+ it "returns items in the sets across all matrices that the given item is also in" do
93
86
  BaseRecommender.input_matrix(:anotherinput)
94
87
  BaseRecommender.input_matrix(:yetanotherinput)
88
+ BaseRecommender.input_matrix(:finalinput)
95
89
  sm = BaseRecommender.new
96
- sm.anotherinput.add_set('a', ["foo", "bar"])
97
- sm.yetanotherinput.add_set('b', ["fnord", "shmoo"])
98
- sm.anotherinput.should_receive(:process!).exactly(1).times
99
- sm.yetanotherinput.should_receive(:process!).exactly(1).times
90
+ sm.anotherinput.add_to_set('a', "foo", "bar")
91
+ sm.yetanotherinput.add_to_set('b', "fnord", "shmoo", "bar")
92
+ sm.finalinput.add_to_set('c', "nada")
100
93
  sm.process!
94
+ sm.related_items("bar").should include("foo", "fnord", "shmoo")
95
+ sm.related_items("bar").length.should == 3
101
96
  end
102
97
  end
103
98
 
@@ -106,13 +101,13 @@ describe Predictor::Base do
106
101
  BaseRecommender.input_matrix(:users, weight: 4.0)
107
102
  BaseRecommender.input_matrix(:tags, weight: 1.0)
108
103
  sm = BaseRecommender.new
109
- sm.users.add_set('me', ["foo", "bar", "fnord"])
110
- sm.users.add_set('not_me', ["foo", "shmoo"])
111
- sm.users.add_set('another', ["fnord", "other"])
112
- sm.users.add_set('another', ["nada"])
113
- sm.tags.add_set('tag1', ["foo", "fnord", "shmoo"])
114
- sm.tags.add_set('tag2', ["bar", "shmoo"])
115
- sm.tags.add_set('tag3', ["shmoo", "nada"])
104
+ sm.users.add_to_set('me', "foo", "bar", "fnord")
105
+ sm.users.add_to_set('not_me', "foo", "shmoo")
106
+ sm.users.add_to_set('another', "fnord", "other")
107
+ sm.users.add_to_set('another', "nada")
108
+ sm.tags.add_to_set('tag1', "foo", "fnord", "shmoo")
109
+ sm.tags.add_to_set('tag2', "bar", "shmoo")
110
+ sm.tags.add_to_set('tag3', "shmoo", "nada")
116
111
  sm.process!
117
112
  predictions = sm.predictions_for('me', matrix_label: :users)
118
113
  predictions.should == ["shmoo", "other", "nada"]
@@ -123,39 +118,9 @@ describe Predictor::Base do
123
118
  predictions = sm.predictions_for('me', matrix_label: :users, offset: 1)
124
119
  predictions.should == ["other", "nada"]
125
120
  end
126
-
127
- it "correctly normalizes predictions" do
128
- BaseRecommender.input_matrix(:users, weight: 1.0)
129
- BaseRecommender.input_matrix(:tags, weight: 2.0)
130
- BaseRecommender.input_matrix(:topics, weight: 4.0)
131
-
132
- sm = BaseRecommender.new
133
-
134
- sm.users.add_set('user1', ["c1", "c2", "c4"])
135
- sm.users.add_set('user2', ["c3", "c4"])
136
- sm.topics.add_set('topic1', ["c1", "c4"])
137
- sm.topics.add_set('topic2', ["c2", "c3"])
138
- sm.tags.add_set('tag1', ["c1", "c2", "c4"])
139
- sm.tags.add_set('tag2', ["c1", "c4"])
140
-
141
- sm.process!
142
-
143
- predictions = sm.predictions_for('user1', matrix_label: :users, with_scores: true, normalize: false)
144
- predictions.should eq([["c3", 4.5]])
145
- predictions = sm.predictions_for('user2', matrix_label: :users, with_scores: true, normalize: false)
146
- predictions.should eq([["c1", 6.5], ["c2", 5.5]])
147
- predictions = sm.predictions_for('user1', matrix_label: :users, with_scores: true, normalize: true)
148
- predictions[0][0].should eq("c3")
149
- predictions[0][1].should be_within(0.001).of(0.592)
150
- predictions = sm.predictions_for('user2', matrix_label: :users, with_scores: true, normalize: true)
151
- predictions[0][0].should eq("c2")
152
- predictions[0][1].should be_within(0.001).of(1.065)
153
- predictions[1][0].should eq("c1")
154
- predictions[1][1].should be_within(0.001).of(0.764)
155
- end
156
121
  end
157
122
 
158
- describe "similarities_for(item_id)" do
123
+ describe "similarities_for" do
159
124
  it "should not throw exception for non existing items" do
160
125
  sm = BaseRecommender.new
161
126
  sm.similarities_for("not_existing_item").length.should == 0
@@ -168,12 +133,12 @@ describe Predictor::Base do
168
133
 
169
134
  sm = BaseRecommender.new
170
135
 
171
- sm.users.add_set('user1', ["c1", "c2", "c4"])
172
- sm.users.add_set('user2', ["c3", "c4"])
173
- sm.topics.add_set('topic1', ["c1", "c4"])
174
- sm.topics.add_set('topic2', ["c2", "c3"])
175
- sm.tags.add_set('tag1', ["c1", "c2", "c4"])
176
- sm.tags.add_set('tag2', ["c1", "c4"])
136
+ sm.users.add_to_set('user1', "c1", "c2", "c4")
137
+ sm.users.add_to_set('user2', "c3", "c4")
138
+ sm.topics.add_to_set('topic1', "c1", "c4")
139
+ sm.topics.add_to_set('topic2', "c2", "c3")
140
+ sm.tags.add_to_set('tag1', "c1", "c2", "c4")
141
+ sm.tags.add_to_set('tag2', "c1", "c4")
177
142
 
178
143
  sm.process!
179
144
  sm.similarities_for("c1", with_scores: true).should eq([["c4", 6.5], ["c2", 2.0]])
@@ -188,24 +153,125 @@ describe Predictor::Base do
188
153
  BaseRecommender.input_matrix(:set1)
189
154
  BaseRecommender.input_matrix(:set2)
190
155
  sm = BaseRecommender.new
191
- sm.set1.add_set "item1", ["foo", "bar"]
192
- sm.set1.add_set "item2", ["nada", "bar"]
193
- sm.set2.add_set "item3", ["bar", "other"]
156
+ sm.set1.add_to_set "item1", "foo", "bar"
157
+ sm.set1.add_to_set "item2", "nada", "bar"
158
+ sm.set2.add_to_set "item3", "bar", "other"
194
159
  sm.sets_for("bar").length.should == 3
195
160
  sm.sets_for("bar").should include("item1", "item2", "item3")
196
161
  sm.sets_for("other").should == ["item3"]
197
162
  end
198
163
  end
199
164
 
165
+ describe "process_items!" do
166
+ context "with no similarity_limit" do
167
+ it "calculates the similarity between the item and all related_items (other items in a set the given item is in)" do
168
+ BaseRecommender.input_matrix(:myfirstinput)
169
+ BaseRecommender.input_matrix(:mysecondinput)
170
+ BaseRecommender.input_matrix(:mythirdinput, weight: 3.0)
171
+ sm = BaseRecommender.new
172
+ sm.myfirstinput.add_to_set 'set1', 'item1', 'item2'
173
+ sm.mysecondinput.add_to_set 'set2', 'item2', 'item3'
174
+ sm.mythirdinput.add_to_set 'set3', 'item2', 'item3'
175
+ sm.mythirdinput.add_to_set 'set4', 'item1', 'item2', 'item3'
176
+ sm.similarities_for('item2').should be_empty
177
+ sm.process_items!('item2')
178
+ similarities = sm.similarities_for('item2', with_scores: true)
179
+ similarities.should include(["item3", 4.0], ["item1", 2.5])
180
+ end
181
+ end
182
+
183
+ context "with a similarity_limit" do
184
+ it "calculates the similarity between the item and all related_items (other items in a set the given item is in), but obeys the similarity_limit" do
185
+ BaseRecommender.input_matrix(:myfirstinput)
186
+ BaseRecommender.input_matrix(:mysecondinput)
187
+ BaseRecommender.input_matrix(:mythirdinput, weight: 3.0)
188
+ BaseRecommender.limit_similarities_to(1)
189
+ sm = BaseRecommender.new
190
+ sm.myfirstinput.add_to_set 'set1', 'item1', 'item2'
191
+ sm.mysecondinput.add_to_set 'set2', 'item2', 'item3'
192
+ sm.mythirdinput.add_to_set 'set3', 'item2', 'item3'
193
+ sm.mythirdinput.add_to_set 'set4', 'item1', 'item2', 'item3'
194
+ sm.similarities_for('item2').should be_empty
195
+ sm.process_items!('item2')
196
+ similarities = sm.similarities_for('item2', with_scores: true)
197
+ similarities.should include(["item3", 4.0])
198
+ similarities.length.should == 1
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "process!" do
204
+ it "should call process_items for all_items's" do
205
+ BaseRecommender.input_matrix(:anotherinput)
206
+ BaseRecommender.input_matrix(:yetanotherinput)
207
+ sm = BaseRecommender.new
208
+ sm.anotherinput.add_to_set('a', "foo", "bar")
209
+ sm.yetanotherinput.add_to_set('b', "fnord", "shmoo")
210
+ sm.all_items.should include("foo", "bar", "fnord", "shmoo")
211
+ sm.should_receive(:process_items!).with(*sm.all_items)
212
+ sm.process!
213
+ end
214
+ end
215
+
216
+ describe "delete_from_matrix!" do
217
+ it "calls delete_item on the matrix" do
218
+ BaseRecommender.input_matrix(:anotherinput)
219
+ BaseRecommender.input_matrix(:yetanotherinput)
220
+ sm = BaseRecommender.new
221
+ sm.anotherinput.add_to_set('a', "foo", "bar")
222
+ sm.yetanotherinput.add_to_set('b', "bar", "shmoo")
223
+ sm.process!
224
+ sm.similarities_for('bar').should include('foo', 'shmoo')
225
+ sm.anotherinput.should_receive(:delete_item).with('foo')
226
+ sm.delete_from_matrix!(:anotherinput, 'foo')
227
+ end
228
+
229
+ it "updates similarities" do
230
+ BaseRecommender.input_matrix(:anotherinput)
231
+ BaseRecommender.input_matrix(:yetanotherinput)
232
+ sm = BaseRecommender.new
233
+ sm.anotherinput.add_to_set('a', "foo", "bar")
234
+ sm.yetanotherinput.add_to_set('b', "bar", "shmoo")
235
+ sm.process!
236
+ sm.similarities_for('bar').should include('foo', 'shmoo')
237
+ sm.delete_from_matrix!(:anotherinput, 'foo')
238
+ sm.similarities_for('bar').should == ['shmoo']
239
+ end
240
+ end
241
+
200
242
  describe "delete_item!" do
201
243
  it "should call delete_item on each input_matrix" do
202
244
  BaseRecommender.input_matrix(:myfirstinput)
203
245
  BaseRecommender.input_matrix(:mysecondinput)
204
246
  sm = BaseRecommender.new
205
- sm.myfirstinput.should_receive(:delete_item!).with("fnorditem")
206
- sm.mysecondinput.should_receive(:delete_item!).with("fnorditem")
247
+ sm.myfirstinput.should_receive(:delete_item).with("fnorditem")
248
+ sm.mysecondinput.should_receive(:delete_item).with("fnorditem")
207
249
  sm.delete_item!("fnorditem")
208
250
  end
251
+
252
+ it "should remove the item from all_items" do
253
+ BaseRecommender.input_matrix(:anotherinput)
254
+ sm = BaseRecommender.new
255
+ sm.anotherinput.add_to_set('a', "foo", "bar")
256
+ sm.process!
257
+ sm.all_items.should include('foo')
258
+ sm.delete_item!('foo')
259
+ sm.all_items.should_not include('foo')
260
+ end
261
+
262
+ it "should remove the item's similarities and also remove the item from related_items' similarities" do
263
+ BaseRecommender.input_matrix(:anotherinput)
264
+ BaseRecommender.input_matrix(:yetanotherinput)
265
+ sm = BaseRecommender.new
266
+ sm.anotherinput.add_to_set('a', "foo", "bar")
267
+ sm.yetanotherinput.add_to_set('b', "bar", "shmoo")
268
+ sm.process!
269
+ sm.similarities_for('bar').should include('foo', 'shmoo')
270
+ sm.similarities_for('shmoo').should include('bar')
271
+ sm.delete_item!('shmoo')
272
+ sm.similarities_for('bar').should_not include('shmoo')
273
+ sm.similarities_for('shmoo').should be_empty
274
+ end
209
275
  end
210
276
 
211
277
  describe "clean!" do
@@ -213,9 +279,9 @@ describe Predictor::Base do
213
279
  BaseRecommender.input_matrix(:set1)
214
280
  BaseRecommender.input_matrix(:set2)
215
281
  sm = BaseRecommender.new
216
- sm.set1.add_set "item1", ["foo", "bar"]
217
- sm.set1.add_set "item2", ["nada", "bar"]
218
- sm.set2.add_set "item3", ["bar", "other"]
282
+ sm.set1.add_to_set "item1", "foo", "bar"
283
+ sm.set1.add_to_set "item2", "nada", "bar"
284
+ sm.set2.add_to_set "item3", "bar", "other"
219
285
  Predictor.redis.keys("#{sm.redis_prefix}:*").should_not be_empty
220
286
  sm.clean!
221
287
  Predictor.redis.keys("#{sm.redis_prefix}:*").should be_empty