I have written what I think is a more or less complete version of JoinAcross
with code below. I've done this to try to avoid the misbehavior I've observed in the function, namely the memory leak in JoinAcross
that I've documented in my other post linked here. This function is critical to several a package I've written for backtesting financial trading strategies and so performance is key. The memory leak has crashed my wolframscipts and so I set out to write my own version but I'm running out of ideas of how to shave those last few microseconds off the performance to put it on par with the built-in JoinAcross
. Here's the code for my version which I've called joinAcross
.
Can someone please help me to improve the performance of this function or help make this better in any way?
For large lists of associations this approach also becomes super memory hungry due to the use of Outer
. Is there a way to avoid that?
I believe the starting place to investigate is my use of Select
in getting the left or right matches. Is there a faster way of getting those without first getting the inner matches?
Thank you in advance for your help and hope this function helps others as well who often use Association
's or JoinAcross
.
ClearAll[joinAcross];
Options[joinAcross] = {KeyCollisionFunction -> Left};
joinAcross::badjspec =
"Invalid join specification. Enter either \"Inner\", \"Outer\", \
\"Left\", or \"Right\".";
joinAcross::badkcspec =
"Invalid KeyCollisionFunction specification. Enter None, Left, \
Right, or a valid function of one paramter returning a list of two \
strings such as {\"First\"->#,\"Second\"->#}.";
joinAcross::keynx = "Key `1` does not exist in Association `2`.";
joinAcross[alist_, blist_, spec : (_String | {__String}),
jspec : ("Inner" | "Outer" | "Left" | "Right"),
opts : OptionsPattern[]] :=
Module[{
kcspec = OptionValue[KeyCollisionFunction], keyfunc, valfunc,
speclist = Flatten[{spec}],
akdata, akdata1, akdata2,
keys,
nonspeclist,
kcfunc,
assocCombos,
innerMatches, innerSpecs, innerMergedMatches, innerResults = {},
patt,
leftMergedMatches, leftResults = {},
rightMergedMatches, rightResults = {},
result = {}
},
(*Handle key collision function to get key and value functions.*)
\
{keyfunc, valfunc} = Switch[kcspec,
None, {Identity, (Missing["Unmatched"] &)},
Left, {Identity, First},
Right, {Identity, Last},
Function[{_, _}], {kcspec, ({First@#, Last@#} &)},
_, Message[joinAcross::badkcspec]; Return[$Failed]
];
akdata =
KeyUnion[Catenate[{alist, blist}], (Missing["Unmatched"] &)];
{akdata1, akdata2} = TakeDrop[akdata, Length@alist];
assocCombos = Flatten[Outer[List, akdata1, akdata2], 1];
keys =
Flatten[
Intersection[DeleteDuplicates@Keys@data1,
DeleteDuplicates@Keys@data2], 1];
nonspeclist = Complement[keys, speclist];
kcfunc = Which[
MemberQ[speclist, #1], #1 -> First@#2,
MemberQ[nonspeclist, #1] && kcspec =!= None,
Thread[Rule[keyfunc[#1], valfunc[#2]]],
MemberQ[nonspeclist, #1] && kcspec === None, Nothing,
True, #1 ->
SelectFirst[#2, ! MissingQ[#] &, Missing["Unmatched"]]
] &;
innerMatches =
Select[assocCombos, #[[1, speclist]] === #[[2, speclist]] &];
innerSpecs = Values@innerMatches[[All, 1, speclist]];
innerMergedMatches = Merge[#, Identity] & /@ innerMatches;
innerResults =
Table[
Association @@
KeyValueMap[kcfunc, innerMergedMatch], {innerMergedMatch,
innerMergedMatches}];
If[jspec == "Inner", Return[innerResults]];
(*Get inner matches key pattern.*)
patt =
Alternatives @@
KeyValuePattern /@ Normal@innerMatches[[All, 1, speclist]];
(*Right join.*)
If[jspec == "Right",
rightMergedMatches =
Merge[#, {Missing["Unmatched"], Last@#} &] & /@
Select[
assocCombos, #[[1, speclist]] =!= #[[2, speclist]] &&
FreeQ[#[[2]], patt] &] // DeleteDuplicates;
rightResults =
Table[
Association @@
KeyValueMap[kcfunc, rightMergedMatch], {rightMergedMatch,
rightMergedMatches}];
Return[Catenate[{innerResults, rightResults}]];
];
(*Left join.*)
If[jspec == "Left",
leftMergedMatches =
Merge[#, {First@#, Missing["Unmatched"]} &] & /@
Select[
assocCombos, #[[1, speclist]] =!= #[[2, speclist]] &&
FreeQ[#[[1]], patt] &] // DeleteDuplicates;
leftResults =
Table[
Association @@
KeyValueMap[kcfunc, leftMergedMatch], {leftMergedMatch,
leftMergedMatches}];
Return[Catenate[{innerResults, leftResults}]];
];
(*Outer join.*)
If[jspec == "Outer",
rightMergedMatches =
Merge[#, {Missing["Unmatched"], Last@#} &] & /@
Select[
assocCombos, #[[1, speclist]] =!= #[[2, speclist]] &&
FreeQ[#[[2]], patt] &] // DeleteDuplicates;
rightResults =
Table[
Association @@
KeyValueMap[kcfunc, rightMergedMatch], {rightMergedMatch,
rightMergedMatches}];
leftMergedMatches =
Merge[#, {First@#, Missing["Unmatched"]} &] & /@
Select[
assocCombos, #[[1, speclist]] =!= #[[2, speclist]] &&
FreeQ[#[[1]], patt] &] // DeleteDuplicates;
leftResults =
Table[
Association @@
KeyValueMap[kcfunc, leftMergedMatch], {leftMergedMatch,
leftMergedMatches}];
Return[Catenate[{innerResults, rightResults, leftResults}]]
];
];
I just use some test data to compare the outputs from this version to the built-in JoinAcross
with the following random data:
SeedRandom[1234];
data1 = Table[<|"Date" -> DatePlus[Today, t],
"Value" -> RandomReal[]|>, {t, -5, 15}];
data2 = Table[<|"Date" -> DatePlus[Today, t],
"Value" -> RandomReal[]|>, {t, 1, 5}];