Sort data after specific ordering (ascending/descending) in multiple columns
Caveat lector: Incorrect results are generated by this solution, e.g.,
sortByColumn[{{"a", 1, 1}, {"b", 2, 3}, {"a", 3, 2}}, {1, 1, -1}]
returns
{{"a", 1, 1}, {"b", 2, 3}, {"a", 3, 2}}
when the correct result is obviously
{{"a", 1, 1}, {"a", 3, 2}, {"b", 2, 3}}
I've commented on the answer to bring it to the attention of the author, however seeing as they've not been here in some time, I'm also putting this here: I think a highly upvoted and accepted answer needs to be correct. - ciao
Here is my contribution, which has the following benefits over previous answers:
- It sorts both numbers and non-numeric structures
- You can sort any column (not just the first, followed by the second, etc)
- You can sort in either direction (ascending / descending)
- Original order is kept: if you sort on the second column, the first entry will follow the order of the original list. See the example with
{0,-1}
- Edit also allow specifying the priority of the columns. So given
{-1,1}
for the ordering, you can specify{1,2}
to give the higher priority to the second column.
The code is as follows, including my usage
code for my own comments.
Clear[sortByColumn]
sortByColumn::usage =
"Arguments: [Table, Direction, Priority]. Returns the list sorted \
by the directions for each column specified in `Direction`. For \
ascending order, use `1`, and for descending order, use `-1`. For \
sorting more than one column, input `Direction` as a list. For \
example, Direction={-1,1} will sort the first column in descending \
order followed by the second column in ascending order, ignoring any \
other column. To sort on the second column, use {0,1} for the syntax.
When sorting two or more columns, you can provide the `Priority` \
for which column should be sorted first. For example, \
`sortByColumn[data,{-1,1},{1,2}]` would sort first in ascending order \
on the second column (because it has a higher priority) and then in \
descending order on the first column.";
sortByColumn[list_?MatrixQ, dir : _Integer | {__Integer}, priority_: {}] :=
Module[{l = Length@list[[1, All]], w, p, d},
w = Reverse@Range@l;
p = If[Length@priority > 0, PadRight[Flatten@{priority}, l],
p = Range@l];
w = w[[Ordering@p]];
d = PadRight[Flatten@{dir}, l];
Sort[list, NonNegative@Total[(w d MapThread[Order, {##}])] &]]
For example, using the data set provided by Mr. Wizard:
data={{"a", 1, 1}, {"a", 1, 5}, {"a", 1, 3},
{"c", 2, 1}, {"b", 2, 2}, {"b", 2, 3},
{"c", 3, 1}, {"a", 3, 2}, {"a", 3, 3}};
data[[All, 2]] = data[[All, 2]] /. {1 -> "q", 2 -> "r", 3 -> "s"};
Here are the results of some trial runs. First the original:
{a,q,1}
{a,q,2}
{a,q,3}
{c,r,1}
{b,r,2}
{b,r,3}
{c,s,1}
{a,s,2}
{a,s,3}
The result of sortByColumn[data,-1]
.
{c,r,1}
{c,s,1}
{b,r,2}
{b,r,3}
{a,q,1}
{a,q,2}
{a,q,3}
{a,s,2}
{a,s,3}
Result of sortByColumn[data,{0,-1}]
{c,s,1}
{a,s,2}
{a,s,3}
{c,r,1}
{b,r,2}
{b,r,3}
{a,q,1}
{a,q,2}
{a,q,3}
And finally, the result the OP wanted, sortByColumn[data,{1,-1,1}]
{a,s,2}
{a,s,3}
{a,q,1}
{a,q,2}
{a,q,3}
{b,r,2}
{b,r,3}
{c,s,1}
{c,r,1}
An example showing the use of the priority argument: sortByColumn[data, {-1, 1}, {1, 2}]
{a,q,1}
{a,q,5}
{a,q,3}
{c,r,1}
{b,r,2}
{b,r,3}
{c,s,1}
{a,s,2}
{a,s,3}
If you want to keep the rows and your preferences of ordering is first ascending, second descending and third ascending, you can use SortBy
:
SortBy[data, {#[[1]],-#[[2]],#[[3]]}&]
FJRA's method is clever, but it will fail if the reverse-order column is non-numeric.
For example:
data={{"a", 1, 1}, {"a", 1, 5}, {"a", 1, 3},
{"c", 2, 1}, {"b", 2, 2}, {"b", 2, 3},
{"c", 3, 1}, {"a", 3, 2}, {"a", 3, 3}};
data[[All, 2]] = data[[All, 2]] /. {1 -> "q", 2 -> "r", 3 -> "s"};
New method
Here is a new method that I believe is a bit cleaner.
ClearAll[f1, f2]
f1[idx_, {d_, ir___}] :=
Join @@ f1[idx + 1, {ir}] /@
If[d == -1, Reverse, # &] @
SplitBy[SortBy[#, #[[idx]] &], #[[idx]] &] &
f1[__] := Identity
f2[dat_?MatrixQ, dir_?VectorQ] := dat // f1[1, dir]
The second argument is a list of sort directions by column, -1
representing reverse.
f2[data, {1, -1, 1}] // Column
{a,s,2} {a,s,3} {a,q,1} {a,q,3} {a,q,5} {b,r,2} {b,r,3} {c,s,1} {c,r,1}
Old method
Preserved for reference, along with a bug fix.
I cannot think of a way to do this sort with arbitrary data outside of implementing my own sort function. Something like this:
f1[idx_, {d_, ir___}, max_] /; idx <= max :=
Reap[
Sow[#, #[[idx]]] & ~Scan~ #,
If[d == -1, Reverse, # &] @ Union @ #[[All, idx]],
f1[idx + 1, {ir}, max][#2] &
][[2]] ~Flatten~ 2 &
f1[__] = Identity;
f2[dat_?MatrixQ, dir_?VectorQ] := dat // f1[1, dir, Dimensions[dat][[2]]]
That's none too pretty and I hope there is a cleaner way, but it eludes me.