1 module magpie.operation;
2 
3 /*
4  * This file will contain work on operations of aggregate and join
5  * aggregate:   Operation to work on a row/ column of DataFrame to give results like mean/median/mode/elc
6  * merge:       It joins two DataFrame in a way similar to that in Pandas or any RDBMS software
7  */
8 
9 import magpie.dataframe: DataFrame;
10 import magpie.group: Group;
11 import magpie.helper: isDataFrame, isGroup;
12 
13 /// Enums for joins
14 enum JoinTypes
15 {
16     left = 0,
17     right = 1,
18     outer = 2,
19     inner = 3,
20 }
21 
22 alias Left = JoinTypes.left;
23 alias Right = JoinTypes.right;
24 alias Inner = JoinTypes.inner;
25 alias Outer = JoinTypes.outer;
26 
27 /++
28 Description: Merges two DataFrames based on indexes
29 @params: type - Type of join: Inner, Outer, Left, Right
30 @params: df1 - The left DataFrame
31 @params: df2 - The right DataFrame
32 @params: lsuffix - Suffix to add in case there is an index collision between left and right DataFrame
33 @parmas: rsuffix - Suffix to add in case there is an index collision between left and right DataFrame
34 +/
35 auto merge(JoinTypes type = Inner, T, U)(T df1, U df2, string lsuffix = "x_", string rsuffix = "y_")
36     if(isDataFrame!T && isDataFrame!U)
37 {
38 
39     assert(df1.indx.row.codes.length == df2.indx.row.codes.length,
40         "Index level mismatch. Cannot merge dataframes with different level of indexing");
41 
42     if(df1.indx.column.codes.length > df2.indx.column.codes.length)
43     {
44         int[] codes;
45         codes.length = df2.indx.column.codes[0].length;
46 
47         foreach(i; 0 .. df1.indx.column.codes.length - df2.indx.column.codes.length)
48         {
49             df2.indx.column.index ~= [[""]];
50             df2.indx.column.codes ~= [codes];
51         }
52     }
53     else if(df1.indx.column.codes.length < df2.indx.column.codes.length)
54     {
55         int[] codes;
56         codes.length = df1.indx.column.codes[0].length;
57         
58         foreach(i; 0 .. df2.indx.column.codes.length - df1.indx.column.codes.length)
59         {
60             df1.indx.column.index ~= [[""]];
61             df1.indx.column.codes ~= [codes];
62         }
63     }
64 
65     DataFrame!(true, df1.RowType, df2.RowType) combinator;
66 
67     import magpie.helper: ensureUnique, transposed;
68     combinator.indx.column = ensureUnique!1(df1.indx, df2.indx, lsuffix, rsuffix).column;
69 
70     string[] unpack(size_t i)
71     {
72         string[] indx;
73         indx.length = combinator.indx.row.codes.length;
74 
75         import std.range: lockstep;
76         foreach(j, a, b; lockstep(combinator.indx.row.index, combinator.indx.row.codes))
77         {
78             if(a.length == 0)
79             {
80                 import std.conv: to;
81                 indx[j] = to!string(b[i]);
82             }
83             else
84             {
85                 indx[j] = a[b[i]];
86             }
87         }
88 
89         return indx;
90     }
91 
92     static if(type == JoinTypes.left)
93     {
94         combinator.indx.row = df1.indx.row;
95         static foreach(i; 0 .. df1.RowType.length)
96             combinator.data[i] = df1.data[i];
97         static foreach(i; 0 .. df2.RowType.length)
98             combinator.data[df1.RowType.length + i].length = df1.rows;
99 
100         foreach(i; 0 .. combinator.indx.row.codes[0].length)
101         {
102             string[] indx = unpack(i);
103             ptrdiff_t pos = df2.indx.getPosition!0(indx);
104 
105             if(pos > -1)
106             {
107                 static foreach(j; 0 .. df2.RowType.length)
108                 {
109                     combinator.data[df1.RowType.length + j][i] = df2.data[j][pos];
110                 }
111             }
112         }
113 
114         combinator.rows = df1.rows;
115         return combinator;
116     }
117     else static if(type == JoinTypes.right)
118     {
119         combinator.indx.row = df2.indx.row;
120         static foreach(i; 0 .. df2.RowType.length)
121             combinator.data[df1.RowType.length + i] = df2.data[i];
122         static foreach(i; 0 .. df1.RowType.length)
123             combinator.data[i].length = df2.rows;
124 
125         foreach(i; 0 .. combinator.indx.row.codes[0].length)
126         {
127             string[] indx = unpack(i);
128             ptrdiff_t pos = df1.indx.getPosition!0(indx);
129 
130             if(pos > -1)
131             {
132                 static foreach(j; 0 .. df1.RowType.length)
133                     combinator.data[j][i] = df1.data[j][pos];
134             }
135         }
136 
137         combinator.rows = df2.rows;
138         return combinator;
139     }
140     else static if(type == JoinTypes.outer)
141     {
142         import magpie.helper: indexUnion;
143         combinator.indx.row = indexUnion!0(df1.indx, df2.indx).row;
144         combinator.indx.row.titles = df1.indx.row.titles;
145 
146         combinator.rows = combinator.indx.row.codes[0].length;
147 
148         static foreach(i; 0 .. combinator.RowType.length)
149             combinator.data[i].length = combinator.rows;
150 
151         foreach(i; 0 .. combinator.rows)
152         {
153             string[] indx = unpack(i);
154 
155             ptrdiff_t pos = df1.indx.getPosition!0(indx);
156             if(pos > -1)
157             {
158                 static foreach(j; 0 .. df1.RowType.length)
159                     combinator.data[j][i] = df1.data[j][pos];
160             }
161 
162             pos = df2.indx.getPosition!0(indx);
163             if(pos > -1)
164             {
165                 static foreach(j; 0 .. df2.RowType.length)
166                     combinator.data[df1.RowType.length + j][i] = df2.data[j][pos];
167             }
168         }
169 
170         return combinator;
171     }
172     else static if(type == JoinTypes.inner)
173     {
174         import magpie.helper: indexIntersection;
175         combinator.indx.row = indexIntersection!0(df1.indx, df2.indx).row;
176         combinator.indx.row.titles = df1.indx.row.titles;
177 
178         combinator.rows = combinator.indx.row.codes[0].length;
179 
180         static foreach(i; 0 .. combinator.RowType.length)
181             combinator.data[i].length = combinator.rows;
182 
183         foreach(i; 0 .. combinator.rows)
184         {
185             string[] indx = unpack(i);
186 
187             ptrdiff_t pos = df1.indx.getPosition!0(indx);
188             if(pos > -1)
189             {
190                 static foreach(j; 0 .. df1.RowType.length)
191                     combinator.data[j][i] = df1.data[j][pos];
192             }
193 
194             pos = df2.indx.getPosition!0(indx);
195             if(pos > -1)
196             {
197                 static foreach(j; 0 .. df2.RowType.length)
198                     combinator.data[df1.RowType.length + j][i] = df2.data[j][pos];
199             }
200         }
201 
202         return combinator;
203     }
204     else static assert(0, "Invalid join type. Available join type: left, right, outer, inner(default)");
205 }
206 
207 // Left Join
208 unittest
209 {
210     import magpie.index: Index;
211 
212     DataFrame!(int, 2) df1;
213     DataFrame!(double, 2) df2;
214 
215     Index i1, i2;
216     i1.setIndex([["Hello", "Hi"]], ["I1"], [["Hello", "Hi"]]);
217     i2.setIndex([["Hello", "Hi"]], ["I1"], [["Hello", "Hi"], ["Hey", "Hey"]]);
218 
219     df1.setFrameIndex(i1);
220     df1.assign!1(0, [1, 2]);
221     df2.setFrameIndex(i2);
222     df2.assign!1(0, [1.0, 2.0]);
223 
224     assert(merge!(JoinTypes.right)(df1, df2).display(true, 200) == "       Hello  Hi  Hello  Hi \n"
225         ~ "I1                Hey    Hey\n"
226         ~ "Hello  1      0   1      nan\n"
227         ~ "Hi     2      0   2      nan\n"
228     );
229 }
230 
231 // Left Join
232 unittest
233 {
234     import magpie.index: Index;
235 
236     DataFrame!(int, 2) df1;
237     DataFrame!(double, 2) df2;
238 
239     Index i1, i2;
240     i1.setIndex([["Hello", "Hey"]], ["I1"], [["Hello", "Hi"]]);
241     i2.setIndex([["Hello", "Hi"]], ["I1"], [["Hello", "Hi"], ["Hey", "Hey"]]);
242 
243     df1.setFrameIndex(i1);
244     df1.assign!1(0, [1, 2]);
245     df2.setFrameIndex(i2);
246     df2.assign!1(0, [1.0, 2.0]);
247 
248     assert(merge!(JoinTypes.left)(df1, df2).display(true, 200) == "       Hello  Hi  Hello  Hi \n"
249         ~ "I1                Hey    Hey\n"
250         ~ "Hello  1      0   1      nan\n"
251         ~ "Hey    2      0   nan    nan\n"
252     );
253 }
254 
255 // Right Join
256 unittest
257 {
258     import magpie.index: Index;
259 
260     DataFrame!(int, 2) df1;
261     DataFrame!(double, 2) df2;
262 
263     Index i1, i2;
264     i1.setIndex([["Hello", "Hi"]], ["I1"], [["Hello", "Hi"]]);
265     i2.setIndex([["Hello", "Hey"]], ["I1"], [["Hello", "Hi"], ["Hey", "Hey"]]);
266 
267     df1.setFrameIndex(i1);
268     df1.assign!1(0, [1, 2]);
269     df2.setFrameIndex(i2);
270     df2.assign!1(0, [1.0, 2.0]);
271 
272     assert(merge!(JoinTypes.right)(df1, df2).display(true, 200) == "       Hello  Hi  Hello  Hi \n"
273         ~ "I1                Hey    Hey\n"
274         ~ "Hello  1      0   1      nan\n"
275         ~ "Hey    0      0   2      nan\n"
276     );
277 }
278 
279 // Outer join
280 unittest
281 {
282     import magpie.index: Index;
283 
284     DataFrame!(int, 2) df1;
285     DataFrame!(double, 2) df2;
286 
287     Index i1, i2;
288     i1.setIndex([["Hello", "Hi"]], ["I1"], [["Hello", "Hi"]]);
289     i2.setIndex([["Hello", "Hey"]], ["I1"], [["Hello", "Hi"], ["Hey", "Hey"]]);
290 
291     df1.setFrameIndex(i1);
292     df1.assign!1(0, [1, 2]);
293     df2.setFrameIndex(i2);
294     df2.assign!1(0, [1.0, 2.0]);
295 
296     assert(merge!(JoinTypes.outer)(df1, df2).display(true, 200) == "       Hello  Hi  Hello  Hi \n"
297         ~ "I1                Hey    Hey\n"
298         ~ "Hello  1      0   1      nan\n"
299         ~ "Hi     2      0   nan    nan\n"
300         ~ "Hey    0      0   2      nan\n"
301     );
302 }
303 
304 // Outer join
305 unittest
306 {
307     import magpie.index: Index;
308 
309     DataFrame!(int, 2) df1;
310     DataFrame!(double, 2) df2;
311 
312     Index i1, i2;
313     i1.setIndex([["Hello", "Hi"]], ["I1"], [["Hello", "Hi"]]);
314     i2.setIndex([["Hello", "Hi"]], ["I1"], [["Hello", "Hi"], ["Hey", "Hey"]]);
315 
316     df1.setFrameIndex(i1);
317     df1.assign!1(0, [1, 2]);
318     df2.setFrameIndex(i2);
319     df2.assign!1(0, [1.0, 2.0]);
320 
321     assert(merge!(JoinTypes.outer)(df1, df2).display(true, 200) == "       Hello  Hi  Hello  Hi \n"
322         ~ "I1                Hey    Hey\n"
323         ~ "Hello  1      0   1      nan\n"
324         ~ "Hi     2      0   2      nan\n"
325     );
326 }
327 
328 // Inner Join
329 unittest
330 {
331     import magpie.index: Index;
332 
333     DataFrame!(int, 2) df1;
334     DataFrame!(double, 2) df2;
335 
336     Index i1, i2;
337     i1.setIndex([["Hello", "Hi"]], ["I1"], [["Hello", "Hi"]]);
338     i2.setIndex([["Hello", "Hey"]], ["I1"], [["Hello", "Hi"], ["Hey", "Hey"]]);
339 
340     df1.setFrameIndex(i1);
341     df1.assign!1(0, [1, 2]);
342     df2.setFrameIndex(i2);
343     df2.assign!1(0, [1.0, 2.0]);
344 
345     assert(merge!(JoinTypes.inner)(df1, df2).display(true, 200) == "       Hello  Hi  Hello  Hi \n"
346         ~ "I1                Hey    Hey\n"
347         ~ "Hello  1      0   1      nan\n"
348     );
349 }