8
$\begingroup$

I need a function MapDuplicate that maps a function on all the duplicates in a list.

Consider the following list:

list={"f1","f2","f1","f3","f2","f1","f4"};
MapDuplicate[f,list]

would return

{f["f1",1],f["f2",1],f["f1",2],"f3",f["f2",2],f["f1",3],"f4"}

One obvious application of this functionality would be to rename duplicate items:

f[x_,n_]:=x<>"_"<>ToString@n;

Now MapDuplicate[f,list] would return non-colliding field names:

{"f1_1","f2_1","f1_2","f3","f2_2","f1_3","f4"}

Is there a simple way to write MapDuplicate using some builtin or function repository functions?

$\endgroup$
1
  • 3
    $\begingroup$ I have a pain in my arm up-voting all the good answers to this one! $\endgroup$
    – user1066
    Commented Sep 29, 2023 at 13:09

14 Answers 14

6
$\begingroup$
indexDuplicates[f_] := Module[{$c}, $c[_] = 0; 
    # /. x_ /; Counts[#]@x > 1 :> f[x, ++$c[x]]] &;

Examples:

indexDuplicates[foo]@list
{foo["f1", 1], foo["f2", 1], foo["f1", 2], "f3", foo["f2", 2], 
 foo["f1", 3], "f4"}
indexDuplicates[# <> "_" <> ToString@#2 &]@list
{"f1_1", "f2_1", "f1_2", "f3", "f2_2", "f1_3", "f4"}
$\endgroup$
5
$\begingroup$

Using MapIndexed:

MapDuplicate[fn_, list_List] := MapIndexed[
  If[Count[list, #1] == 1
    , #1
    , fn[#1, Count[list[[1 ;; First@#2]], #1]]
    ] &
  , list
  ]

list = {"f1", "f2", "f1", "f3", "f2", "f1", "f4"};

MapDuplicate[g, list]

{g["f1", 1], g["f2", 1], g["f1", 2], "f3", g["f2", 2], g["f1", 3], "f4"}

$\endgroup$
5
$\begingroup$
mapDuplicate[f_] := Module[{$c}, $c[_] = 0; 
  Map[x |-> If[Counts[#] @ x == 1, x, f[x, ++$c[x]]]] @ #] &;

Examples:

list = {"f1", "f2", "f1", "f3", "f2", "f1", "f4"};

mapDuplicate[foo] @ list
{foo["f1", 1], foo["f2", 1], foo["f1", 2], "f3",   
 foo["f2", 2], foo["f1", 3], "f4"}
mapDuplicate[# <> "_" <> ToString @ #2 &] @ list
 {"f1_1", "f2_1", "f1_2", "f3", "f2_2", "f1_3", "f4"}
$\endgroup$
3
  • 1
    $\begingroup$ this produces foo["f3", 1] instead of "f3" $\endgroup$
    – I.M.
    Commented Sep 29, 2023 at 2:51
  • $\begingroup$ Thank you @I.M. I misread the question. $\endgroup$
    – kglr
    Commented Sep 29, 2023 at 3:15
  • 1
    $\begingroup$ @I.M., updated with a fix. $\endgroup$
    – kglr
    Commented Sep 29, 2023 at 3:55
5
$\begingroup$
MapAt[f,#,List/@Cases[PositionIndex[#],x_/;Length[x]>1:>Sequence@@x]]&@list

(* {f[f1],f[f2],f[f1],f3,f[f2],f[f1],f4} *)

mapAtDuplicates

mapAtDuplicates:=Function[{list,fn},ReplacePart[list,First@#1->fn[#2,
  Last@#1]&@@@Thread[{#2,#1}]&@@@(MapAt[MapIndexed[{#1,First@#2}&],
  {2}]/@(KeyValueMap[List]@(Select[PositionIndex[list], Length[#]>1&])))//Flatten]]
mapAtDuplicates[list,{#1,#2}&]

(* {{f1,1},{f2,1},{f1,2},f3,{f2,2},{f1,3},f4} *) 

mapAtDuplicates[list,#1[#2]&]

(* {f1[1],f2[1],f1[2],f3,f2[2],f1[3],f4}  *)

mapAtDuplicates[list,#1<>"_"<>ToString[#2]&]

(* {f1_1,f2_1,f1_2,f3,f2_2,f1_3,f4} *)

Just for Fun: Map Duplicates in a Protein Sequence

(i) Import a sequence (bovine serum albumin, 607 amino acids, single letter code)

importedSequence = Import["http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=protein&id=3336842&rettype=fasta&retmode=text", "Data"];
 
{bsaSequenceHeading, bsaSequenceData} = {#[[1,1]], #[[2,1]]} &@importedSequence;

(ii) Map Duplicates

Short[mapAtDuplicates[StringSplit[bsaSequenceData,""],#1[#2]&],10]

(* 
{M[1],K[1],W[1],V[1],T[1],F[1],I[1],S[1],L[1],L[2],
 L[3],L[4],F[2],S[2],S[3],A[1],Y[1],S[4],R[1],G[1],
 V[2],F[3],R[2],R[3],D[1],T[2],H[1],K[2],S[5],E[1],
 I[2],A[2],H[2],R[4],F[4],K[3],D[2],L[5],<<531>>,V[33],M[5],
 E[57],N[14],F[28],V[34],A[41],F[29],V[35],D[38],K[58],C[33],
 C[34],A[42],A[43],D[39],D[40],K[59],E[58],A[44],C[35],F[30],
 A[45],V[36],E[59],G[17],P[28],K[60],L[64],V[37],V[38],S[32],
 T[34],Q[20],T[35],A[46],L[65],A[47] } *) 

But a criticism of this approach might be that only duplicates are indexed.

(iii) Import a sequence with singletons (Insulin B chain)

importedSequenceTwo = Import["http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=protein&id=2WS1_B&rettype=fasta&retmode=text", "Data"];

{insulinBChainHeading, insulinBChainData} = {#[[1,1]], #[[2,1]]} &@importedSequenceTwo;

(iv) Map Duplicates

mapAtDuplicates[StringSplit[insulinBChainData,""], #1[#2]&]


(* 
 {F[1],V[1],N,Q,H[1],L[1],C[1],G[1],S,H[2],
  L[2],V[2],E[1],A,L[3],Y,L[4],V[3],C[2],G[2],
  E[2],R,G[3],F[2],F[3],X,T[1],P,K,T[2]} 
*) 

cumIndex

cumIndex:=Function[list,ReplacePart[list,First@#1->#2[Last@#1]&@@@Thread[
  {#2,#1}]&@@@(MapAt[MapIndexed[{#1,First@#2}&],{2}]/@(KeyValueMap[List]@(
  PositionIndex[list])))//Flatten]]

(v) Cumulative Index

cumIndex[StringSplit[insulinBChainData,""]]

(*      
 {F[1],V[1],N[1],Q[1],H[1],L[1],C[1],G[1],S[1],H[2],
  L[2],V[2],E[1],A[1],L[3],Y[1],L[4],V[3],C[2],G[2],
  E[2],R[1],G[3],F[2],F[3],X[1],T[1],P[1],K[1],T[2]} 
*) 
$\endgroup$
4
$\begingroup$

SubsetMap will allow you to apply a function to a subset of a list. The function you give to SubsetMap to apply to the subset must work on a list and return a list of the same length.

So, starting with your list,

list={"f1","f2","f1","f3","f2","f1","f4"};

we could try to come up with a function that, when given a list of strings (which we know will be duplicates) returns a list of where the elements are disambiguated in some fashion. I'll choose to do the disambiguation by appending an index.

RenameByIndex[alist : {__String}] := MapIndexed[#1 <> ToString[#2[[1]]] &, alist]

So, we're part way there:

SubsetMap[RenameByIndex, {1, 2, 3}][list]
(* {"f11", "f22", "f13", "f3", "f2", "f1", "f4"} *)

Of course, {1,2,3} isn't where the duplicates are, that was just a demo. So now we need to get indices for duplicates. PositionIndex can get us something useful:

PositionIndex[list]
(* <|f1->{1,3,6},f2->{2,5},f3->{4},f4->{7}|> *)

We can get a SubsetMap function by mapping over the values of that structure:

SubsetMap[RenameByIndex, #] & /@ Values[PositionIndex[list]]
(* {SubsetMap[RenameByIndex,{1,3,6}],SubsetMap[RenameByIndex,{2,5}],SubsetMap[RenameByIndex,{4}],SubsetMap[RenameByIndex,{7}]} *)

Now, that's just a bunch of individual functions. We need to turn that into one function that we can apply to our list. That's a job for Composition:

RenameDuplicates = 
  Composition @@ (SubsetMap[RenameByIndex, #] & /@ Values[PositionIndex[list]])

Let's try it out:

RenameDuplicates[list]
(* {"f11", "f21", "f12", "f31", "f22", "f13", "f41"} *)

Okay, this renamed singletons too. So, we should delete those cases from the position index step:

RenameDuplicates = 
  Composition @@ (SubsetMap[RenameByIndex, #] & /@ DeleteCases[Values[PositionIndex[list]], {_}])

And we really don't want to hard code this to the exact list you provided as an example, so:

RenameDuplicates[list : {__String}] := 
  (Composition @@ (SubsetMap[RenameByIndex, #] & /@ 
    DeleteCases[Values[PositionIndex[list]], {_}]))[list]

Now, this has all been based on the idea of deduping strings, but your comments suggest maybe your list won't always be strings. So, you'll need to adapt this to whatever deduping strategy you want to use in the non-string context. Specifically, you'll need something different for the RenameByIndex function.

$\endgroup$
4
$\begingroup$

A basic approach:

list={"f1","f2","f1","f3","f2","f1","f4"};


w[n_] := With[{x = list[[n]]}, f[x, Count[list[[1 ;; n]], x]]]
Table[w[j], {j, Length[list]}]

yields:

f["f1", 1], f["f2", 1], f["f1", 2], f["f3", 1], f["f2", 2], f["f1", 3], f["f4", 1]}

$\endgroup$
3
$\begingroup$
list = {"f1", "f2", "f1", "f3", "f2", "f1", "f4"};

mapDuplicates[f_, list_] := Module[{m}, m[_] = 0;
  duplicates = Select[list, Counts[list][#] > 1 &];
  (m[#]++; If[MemberQ[duplicates, #], f[#, m[#]], #]) & /@ list]

mapDuplicates[f, list]

{f["f1", 1], f["f2", 1], f["f1", 2], "f3", f["f2", 2], f["f1", 3], "f4"}

$\endgroup$
3
$\begingroup$
list={"f1","f2","f1","f3","f2","f1","f4"}  ; 

ClearAll[map] ;
map[fn_, list_]:=Block[
    {counts, keys},
    counts = DeleteCases[Counts[list], 1] ;
    keys = Keys[counts] ;
    Reverse[Table[If[MemberQ[keys, key], fn[key, counts[key]--], key], {key, Reverse[list]}]]
] ;
list
map[fn, list]

(* {"f1","f2","f1","f3","f2","f1","f4"} *)
(* {fn["f1",1],fn["f2",1],fn["f1",2],"f3",fn["f2",2],fn["f1",3],"f4"} *)
$\endgroup$
3
$\begingroup$
mapAtDuplicates[f_] := Module[{$c}, $c[_] = 0;
   MapApply[If[$c[#] == 1, #, f @ ##] &] @* Map[{#, ++$c[#]} &]];

Examples:

list = {"f1", "f2", "f1", "f3", "f2", "f1", "f4"};

mapAtDuplicates[h] @ list
 {h["f1", 1], h["f2", 1], h["f1", 2], "f3", h["f2", 2], h["f1", 3], "f4"}
mapAtDuplicates[# <> "_" <> ToString @ #2 &] @ list
 {"f1_1", "f2_1", "f1_2", "f3", "f2_2", "f1_3", "f4"}
SeedRandom[1];
rlst = RandomInteger[10, 15];

Grid[{rlst, mapAtDuplicates[Subscript]@rlst}, Dividers -> All]

enter image description here

$\endgroup$
3
$\begingroup$

Quite basic. Easy to follow, hopefully.

MapDuplicate[t_] := Module[{d, s, p},

  d = First /@ DeleteCases[Tally[t], {_, 1}];

  s = f[#, 0] & /@ d;

  If[(p = Position[d, #]) == {}, #,
     p = p[[1, 1]];
     s[[p, 2]] += 1;
     s[[p]]] & /@ t
  ]

MapDuplicate[list]

{f["f1", 1], f["f2", 1], f["f1", 2], "f3", f["f2", 2], f["f1", 3], "f4"}

$\endgroup$
2
$\begingroup$

My attempt is the following:

MapDuplicates[func_, list_List] := 
Module[{resultList = {}, occurrences = <||>, duplicates}, 
duplicates = Select[DeleteDuplicates[list], Count[list, #] > 1 &];
Scan[(If[MemberQ[duplicates, #], 
occurrences[#] = 
If[KeyExistsQ[occurrences, #], occurrences[#] + 1, 1];
AppendTo[resultList, func[#, occurrences[#]]];, 
AppendTo[resultList, #];]) &, list]; resultList]

Testing MapDuplicates:

f[x_, n_] := x <> "_" <> ToString@n;
list = {"f1", "f2", "f1", "f3", "f2", "f1", "f4"};
MapDuplicates[f, list]

(*{"f1_1", "f2_1", "f1_2", "f3", "f2_2", "f1_3", "f4"}*)
$\endgroup$
2
  • $\begingroup$ Elements in the list don't have to be strings, also certainly not the same length. $\endgroup$
    – user13892
    Commented Sep 29, 2023 at 0:32
  • $\begingroup$ See the update, please! $\endgroup$ Commented Sep 29, 2023 at 0:47
2
$\begingroup$

This works if the output doesn't need to be in the same order as the input list:

MapDuplicate[f_, list_] := 
 Flatten[If[#[[2]] > 1, Table[f[#[[1]], n], {n, #[[2]]}], #[[1]]] & /@
    Tally[list]]

MapDuplicate[g, list]

{g["f1", 1], g["f1", 2], g["f1", 3], g["f2", 1], g["f2", 2], "f3", "f4"}

f[x_, n_] := x <> "_" <> ToString@n;
MapDuplicate[f, list]

{"f1_1", "f1_2", "f1_3", "f2_1", "f2_2", "f3", "f4"}

$\endgroup$
2
$\begingroup$
list = {"f1", "f2", "f1", "f3", "f2", "f1", "f4"}

Module[{c = <|# -> 1 & /@ Union[list]|>}, g @@ {#, c[#]++} & /@ list]

{g["f1", 1], g["f2", 1], g["f1", 2], g["f3", 1], g["f2", 2], g["f1", 3], g["f4", 1]}

$\endgroup$
2
$\begingroup$
list = {"f1", "f2", "f1", "f3", "f2", "f1", "f4"};

Using MapIndexed and Ordering to get the original sort order

Query[Ordering @ Ordering @ list] @
 Cases[{f_, {_, n_}} :> g[f, n]] @
  Catenate @ MapIndexed[List, Split @ Sort @ list, {2}]

{g["f1", 1], g["f2", 1], g["f1", 2], g["f3", 1], g["f2", 2], g["f1", 3], g["f4", 1]}

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.