Given a list:
lis = {37.21, 37.21, 37.2, 44, 44, 44, 101, 101}
What is a simple way to extract the second largest elements?
In[1]:= someFunction[lis]
Out[1]= {44, 44, 44}
One way, not highly efficient:
lis = {37.21, 37.21, 37.2, 44, 44, 44, 101, 101};
lis ~Cases~ Union[lis][[-2]]
{44, 44, 44}
This should be a bit more efficient:
ConstantArray @@ Sort[Tally@lis][[-2]]
Caveat: both of these methods rely on sorting and therefore require numeric data.
flinty's method with refinements by both C. E. and me:
Pick[lis, lis, RankedMax[DeleteDuplicates@lis, 2]]
This appears to be the fastest overall and it avoids the sorting issue referenced above.
A quick test of the methods posted so far reveals an interesting pattern. Note that in the benchmark I use a list of a fixed length of one million and vary the number of unique elements within that list.
Adding methods f5, f6, and f7, and a second test with unpackable data.
Performed in Mathematica 10.1
Needs["GeneralUtilities`"]
SetOptions[Benchmark, TimeConstraint -> 30];
f1[lis_] := lis ~Cases~ Union[lis][[-2]]
f2[lis_] := ConstantArray @@ Sort[Tally@lis][[-2]]
f3[lis_] := MaximalBy[DeleteCases[lis, Max@lis], # &] (* Conor/kglr *)
f4[lis_] := Split[Sort@lis][[-2]] (* kglr *)
f5[lis_] := Pick[lis, lis - RankedMax[DeleteDuplicates@lis, 2], 0]; (* flinty/C. E. *)
f6[lis_] := Extract[List/@KeySort[PositionIndex[lis]][[-2]]][lis] (* CA Trevillian *)
f7[lis_] := Pick[lis, lis, RankedMax[DeleteDuplicates@lis, 2]] (* flinty/C.E./me *)
BenchmarkPlot[{f1, f2, f3, f4, f5, f6, f7},
RandomInteger[#, 1*^6] &, 10^Range[6], Joined -> True]
BenchmarkPlot[{f1, f2, f3, f4, f5, f6, f7},
Prepend[0.5]@RandomInteger[#, 1*^6] &, 10^Range[6], Joined -> True]
f1
, f3
, and f4
all stop at n == 10^5
, only f2
goes up to n == 10^6
. Also, the PlotMarkers
are visible in the plot just like in the PlotLegends
.
$\endgroup$
Commented
Jun 13, 2020 at 14:19
SetOptions[BenchmarkPlot, TimeConstraint -> 30]
. I don't need this in version 10.1 when explicitly specifying test points, but I think that should do it.
$\endgroup$
Commented
Jun 13, 2020 at 22:36
another way...
MaximalBy[DeleteCases[lis, Max@lis], # &]
{44, 44, 44}
MaximalBy[DeleteCases[lis, Max@lis], # &]
?
$\endgroup$
Split[ Sort @ lis][[-2]]
{44, 44, 44}
Also
Nearest[DeleteCases[Max @ #] @ #, Max @ #] & @ lis
{44, 44, 44}
Find the second largest unique element:
RankedMax[DeleteDuplicates@lis, 2]
... or alternatively:
Last@TakeLargest[DeleteDuplicates@lis, 2]
There are multiple ways to get them all:
Cases[lis, RankedMax[DeleteDuplicates@lis, 2]]
Cases[lis, Last@TakeLargest[DeleteDuplicates@lis, 2]]
Select[lis, # == Last@TakeLargest[DeleteDuplicates@lis, 2] &]
Pick
to get all, e.g. Pick[lis, lis - RankedMax[DeleteDuplicates@lis, 2], 0];
The Select
one can be sped up by using With
to inject the sought after value into the anonymous function (but it will still be much slower than alternatives).
$\endgroup$
Pick[lis, lis, RankedMax[DeleteDuplicates@lis, 2]]
$\endgroup$
Commented
Jun 14, 2020 at 6:58
This is probably terribly expensive compared to other methods, but I think it could be done better too, regardless...also I find it odd that Ordering
doesn't manage for duplicated values...
Extract[List/@KeySort[PositionIndex[lis]][[-2]]][lis]
{44, 44, 44}
You can just grab the positions directly with
KeySort[PositionIndex[lis]][[-2]]
{4, 5, 6}
Though, I will say this is the only presented method so far that "Extract
s" the second-largest value(s) in a list ;)
This is better to look at:
lis[[KeySort[PositionIndex[lis]][[-2]]]]
Another solution:
lis // DeleteCases[#, Max@#]& // Cases[#, Max@#]&
Select[Select[c=Sort[lis],#!=Last[c] &],#==Last[Select[c,#!=Last[c] &]]&]
Another solution using Tally
, SortBy
and ConstanArray
:
ConstantArray[#[[1]], #[[2]]] &@Last@SortBy[Tally[lis], Last]
(*{44, 44, 44}*)
list = {37.21, 37.21, 37.2, 44, 44, 44, 101, 101};
Using TakeLargestBy
spl = Split @ list
{{37.21, 37.21}, {37.2}, {44, 44, 44}, {101, 101}}
n = 2;
TakeLargestBy[spl, First, n][[n]]
{44, 44, 44}
To also get the position (in the splitted list)
n = 3;
TakeLargestBy[spl -> {"Element", "Index"}, First, n][[n]]
{{37.21, 37.21}, 1}
In[7]:= lis[[PositionLargest[lis, 2][[2]]]] Out[7]= {44, 44, 44}
$\endgroup$