2010-02-10 4 views
1

J'essaye de prendre un tableau numérique dans F #, et de classer tous les éléments pour que les liens aient le même rang. Fondamentalement, j'essaie de reproduire l'algorithme que j'ai ci-dessous en C#, mais juste pour un tableau de doubles. Aidez-moi?F # Comment Percentile Rank Un tableau de doubles?

rankMatchNum = 0; rankMatchSum = 0; previousScore = -999999999;

 for (int i = 0; i < factorStocks.Count; i++) 
     { 
      //The 1st time through it won't ever match the previous score... 
      if (factorStocks[i].factors[factorName + "_R"] == previousScore) 
      { 
       rankMatchNum = rankMatchNum + 1;  //The count of matching ranks 
       rankMatchSum = rankMatchSum + i + 1; //The rank itself... 
       for (int j = 0; j <= rankMatchNum; j++) 
       { 
        factorStocks[i - j].factors[factorName + "_WR"] = rankMatchSum/(rankMatchNum + 1); 
       } 
      } 
      else 
      { 
       rankMatchNum = 0; 
       rankMatchSum = i + 1; 
       previousScore = factorStocks[i].factors[factorName + "_R"]; 
       factorStocks[i].factors[factorName + "_WR"] = i + 1; 
      } 
     } 

Répondre

5

Voici comment je le ferais, bien que ce ne soit pas une traduction directe de votre code. J'ai fait les choses dans un style fonctionnel, les résultats de tuyauterie d'une transformation à l'autre.

let rank seq = 
    seq 
    |> Seq.countBy (fun x -> x)  // count repeated numbers 
    |> Seq.sortBy (fun (k,v) -> k) // order by key 
    |> Seq.fold (fun (r,l) (_,n) -> // accumulate the number of items seen and the list of grouped average ranks 
     let r'' = r + n    // get the rank after this group is processed 
     let avg = List.averageBy float [r+1 .. r''] // average ranks for this group 
     r'', ([for _ in 1 .. n -> avg]) :: l)  // add a list with avg repeated 
     (0,[])       // seed the fold with rank 0 and an empty list 
     |> snd       // get the final list component, ignoring the component storing the final rank 
     |> List.rev      // reverse the list 
     |> List.collect (fun l -> l) // merge individual lists into final list 

Ou pour copier le style de Mehrdad:

let rank arr = 
    let lt item = arr |> Seq.filter (fun x -> x < item) |> Seq.length 
    let lte item = arr |> Seq.filter (fun x -> x <= item) |> Seq.length 
    let avgR item = [(lt item) + 1 .. (lte item)] |> List.averageBy float 
    Seq.map avgR arr 
+0

@kvb J'espère que ça ne te dérange pas, je reformaté votre code pour jouer agréable avec la syntaxe surligneur – gradbot

+0

On dirait que vous éditez encore, je arrête. – gradbot

+0

@gradbot - Pas de problème, merci de le nettoyer. – kvb

1

Je pense que vous trouverez probablement ce problème beaucoup plus facile à résoudre dans F # si vous repassez ce qui précède d'une manière déclarative plutôt que dans un impératif manière. Voici mon approche de la réécriture déclarative de ce qui précède:

Nous avons d'abord besoin d'une classe wrapper pour décorer nos objets avec une propriété portant le grade.

class Ranked<T> { 
    public T Value { get; private set; } 
    public double Rank { get; private set; } 
    public Ranked(T value, double rank) { 
     this.Value = value; 
     this.Rank = rank; 
    } 
} 

Voici donc votre algorithme de manière déclarative. Notez que elements est votre séquence d'entrée et la séquence résultante est dans le même ordre que elements. Le délégué func est la valeur que vous souhaitez classer elements par.

static class IEnumerableExtensions { 
    public static IEnumerable<Ranked<T>> Rank<T, TRank>(
     this IEnumerable<T> elements, 
     Func<T, TRank> func 
    ) { 
     var groups = elements.GroupBy(x => func(x)); 
     var ranks = groups.OrderBy(g => g.Key) 
          .Aggregate(
           (IEnumerable<double>)new List<double>(), 
           (x, g) => 
            x.Concat(
             Enumerable.Repeat(
              Enumerable.Range(x.Count() + 1, g.Count()).Sum()/(double)g.Count(), 
              g.Count() 
            ) 
           ) 
        ) 
        .GroupBy(r => r) 
        .Select(r => r.Key) 
        .ToArray(); 

     var dict = groups.Select((g, i) => new { g.Key, Index = i }) 
         .ToDictionary(x => x.Key, x => ranks[x.Index]); 

     foreach (T element in elements) { 
      yield return new Ranked<T>(element, dict[func(element)]); 
     }   
    } 
} 

Utilisation:

class MyClass { 
    public double Score { get; private set; } 
    public MyClass(double score) { this.Score = score; } 
} 

List<MyClass> list = new List<MyClass>() { 
    new MyClass(1.414), 
    new MyClass(2.718), 
    new MyClass(2.718), 
    new MyClass(2.718), 
    new MyClass(1.414), 
    new MyClass(3.141), 
    new MyClass(3.141), 
    new MyClass(3.141), 
    new MyClass(1.618) 
}; 
foreach(var item in list.Rank(x => x.Score)) { 
    Console.WriteLine("Score = {0}, Rank = {1}", item.Value.Score, item.Rank); 
} 

Sortie:

Score = 1.414, Rank = 1.5 
Score = 2.718, Rank = 3 
Score = 2.718, Rank = 3 
Score = 2.718, Rank = 3 
Score = 1.414, Rank = 1.5 
Score = 3.141, Rank = 5 
Score = 3.141, Rank = 5 
Score = 3.141, Rank = 5 
Score = 1.618, Rank = 8 

Notez que je ne demande pas la séquence d'entrée à commander. Le code résultant est plus simple si vous appliquez une telle exigence sur la séquence d'entrée. Notez en outre que nous ne monnayons pas la séquence d'entrée et que nous ne modifions pas non plus les éléments d'entrée. Cela rend F # heureux. De là, vous devriez être capable de réécrire facilement ceci en F #.

0

Ce n'est pas un algorithme très efficace (O (n)), mais il est assez court et lisible:

let percentile arr = 
    let rank item = ((arr |> Seq.filter (fun i -> i < item) 
         |> Seq.length |> float) + 1.0) 
       /float (Array.length arr) * 100.0 
    Array.map rank arr 

Vous pourriez jouer avec l'expression fun i -> i < e (ou l'expression + 1.0) à atteindre la manière désirée de classer les résultats:

let arr = [|1.0;2.0;2.0;4.0;3.0;3.0|] 
percentile arr |> print_any;; 

[|16.66666667; 33.33333333; 33.33333333; 100.0; 66.66666667; 66.66666667|] 
+0

C'est assez élégant, mais ne correspond pas au comportement du code C# original. – kvb

+0

@kvb: Je ne voulais pas cloner le code C#. J'ai simplement écrit quelque chose à partir de zéro pour répondre à ce qui est apparemment demandé (titre de la question). Je n'ai pas vraiment compris le code OP. –

0

La solution de Mehrdad est très agréable mais un peu lente pour mes besoins. Le tri initial peut être fait 1 fois. Plutôt que de parcourir les listes à chaque fois pour obtenir le nombre d'éléments < ou < = la cible, nous pouvons utiliser des compteurs. Ceci est plus impératif (aurait pu utiliser un pli):

let GetRanks2 (arr) = 
    let tupleList = arr |> Seq.countBy(fun x -> x) |> Seq.sortBy(fun (x,count) -> x) 
    let map = new System.Collections.Generic.Dictionary<int,float>() 
    let mutable index = 1 
    for (item, count) in tupleList do 
     let c = count 
     let avgRank = 
      let mutable s = 0 
      for i = index to index + c - 1 do 
       s <- s + i 
      float s/float c 
     map.Add(item, avgRank) 
     index <- index + c 
    // 
    map 
Questions connexes