Question

J'apprends Clojure et j'aimerais avoir des conseils sur l'utilisation idiomatique. Dans le cadre d’un petit ensemble de statistiques, j’ai une fonction permettant de calculer le mode d’un ensemble de données. (Contexte: le mode est la valeur la plus courante dans un ensemble de données. Il existe près d’une douzaine d’algorithmes publiés pour le calculer. Celui utilisé ici est tiré de "Fundamentals of Biostatistics", 6e éd de Bernard Rosner.)

(defn tally-map
 " Create a map where the keys are all of the unique elements in the input
   sequence and the values represent the number of times those elements
   occur. Note that the keys may not be formatted as conventional Clojure
   keys, i.e. a colon preceding a symbol."
  [aseq]
  (apply merge-with + (map (fn [x] {x 1}) aseq)))

(defn mode
 " Calculate the mode. Rosner p. 13. The mode is problematic in that it may
   not be unique and may not exist at all for a particular group of data.
   If there is a single unique mode, it is returned. If there are multiple
   modes, they are returned as a list. If there is no mode, that is all
   elements are present in equal frequency, nil is returned."
  [aseq]
  (let [amap (tally-map aseq)
        mx (apply max (vals amap))
        k (keys amap)
        f (fn [x] (not (nil? x)))
        modes (filter f (map #(if (= mx (get amap %)) %) k))
        ]
    (cond (= 1 (count modes)) (first modes)
      (every? #(= mx %) (vals amap)) nil
      :else modes)
    )
  )

J'ai des questions sur quelques points:

  1. l'argument. La fonction accepte une seule séquence. Est-il plus idiomatique d’accepter un nombre variable d’arguments comme la fonction d’addition?
  2. Code odeur. Il semble que le & let; let " est un peu plus compliqué qu'il ne devrait l'être - autant d'assignations de variables. Ai-je oublié des utilisations évidentes (ou moins évidentes) du langage ou de la bibliothèque qui rendraient cette méthode plus concise?

Merci d'avance pour votre aide.

Était-ce utile?

La solution

Voici mon point de vue:

  1. Il existe de nombreuses fonctions de base de clojure qui prennent des séquences comme arguments, tandis que d'autres prennent plusieurs arguments. Il n'y a donc pas de véritable manière idiomatique à mon avis. Si vous avez déjà vos données dans une séquence, j'utiliserais un seq comme argument, car cela vous évitera de l'appeler.

  2. Je n'écrirais pas une fonction qui renvoie une valeur dans certains cas et une liste de valeurs dans d'autres, car le code appelant devra toujours vérifier la valeur renvoyée avant de l'utiliser. Au lieu de cela, je retournerais un seul mode en tant que seq avec un seul élément. Mais vous pouvez avoir vos raisons, selon le code qui appelle cette fonction.

En dehors de cela, je voudrais réécrire la fonction mode comme ceci:

(defn mode [aseq]
  (let [amap (tally-map aseq)
        mx (apply max (vals amap))
        modes (map key (filter #(= mx (val %)) amap))
        c (count modes)]
    (cond
      (= c 1) (first modes)
      (= c (count amap)) nil
      :default modes)))

Au lieu de définir une fonction f, vous pouvez utiliser la fonction identité (sauf si vos données contiennent des valeurs logiquement fausses). Mais vous n'avez même pas besoin de ça. Je trouve les modes d’une manière différente, ce qui m’est plus lisible: Map amap agit comme une séquence d’entrées de carte (paires clé-valeur). D'abord, je ne filtre que les entrées qui ont la valeur mx. Ensuite, je mappe la fonction de touche sur ces touches, en me donnant une séquence de touches.

Pour vérifier s’il existe des modes, je ne reviens pas sur la carte. Au lieu de cela, je compare simplement le nombre de modes au nombre d'entrées sur la carte. S'ils sont égaux, tous les éléments ont la même fréquence!

Voici la fonction qui renvoie toujours un seq:

(defn modes [aseq]
  (let [amap (tally-map aseq)
        mx (apply max (vals amap))
        modes (map key (filter #(= mx (val %)) amap))]
    (when (< (count modes) (count amap)) modes)))

Autres conseils

À mon avis, mapper une fonction sur une collection, puis condenser immédiatement la liste en un seul élément est un signe d’utilisation de réduire .

(defn tally-map [coll]
  (reduce (fn [h n]
            (assoc h n (inc (h n 0))))
          {} coll))

Dans ce cas, j'écrirais le mode fn pour prendre une seule collection en argument, comme vous l'avez fait. La seule raison pour laquelle je peux penser à utiliser plusieurs arguments pour une fonction comme celle-ci est si vous envisagez de devoir taper beaucoup les arguments littéraux.

Donc si par exemple c'est pour un script REPL interactif et vous allez souvent taper (mode [1 2 1 2 3]) littéralement, alors vous devriez avoir la fonction prendre plusieurs arguments, pour vous éviter de taper le [] supplémentaire de la fonction appelle tout le temps. Si vous envisagez de lire de nombreux nombres dans un fichier, puis utilisez le mode de ces chiffres, demandez à la fonction de prendre un seul argument qui est une collection afin de vous éviter d'utiliser apply à tout moment. . Je suppose que votre cas d'utilisation le plus courant est ce dernier cas. Je crois que apply ajoute également une surcharge que vous évitez lorsque vous avez un appel de fonction qui prend un argument de collection.

Je suis d'accord avec les autres pour dire que le mode devrait renvoyer une liste de résultats, même s'il n'y en a qu'un. ça va vous rendre la vie plus facile. Peut-être le renommer modes tant que vous y êtes.

Voici une belle implémentation concise du mode :

(defn mode [data] 
  (first (last (sort-by second (frequencies data)))))

Ceci exploite les faits suivants:

  • La fonction fréquences renvoie une carte de valeurs - > fréquences
  • Vous pouvez traiter une carte comme une séquence de paires clé-valeur
  • Si vous triez cette séquence par valeur (l'élément second de chaque paire), le dernier élément de la séquence représentera le mode

MODIFIER

Si vous souhaitez gérer le cas de mode multiple, vous pouvez insérer un partition par supplémentaire pour conserver toutes les valeurs avec la fréquence maximale:

(defn modes [data] 
  (->> data
       frequencies 
       (sort-by second)
       (partition-by second)
       last
       (map first)))

Ça me va. Je remplacerais le

f (fn [x] (not (nil? x)))
mode (filter f (map #(if (= mx (get amap %)) %) k))

avec

mode (remove nil? (map #(if (= mx (get amap %)) %) k))

(Je ne sais pas pourquoi quelque chose comme not-nil? n'est pas dans clojure.core ; c'est quelque chose dont on a besoin tous les jours.)

  

S'il existe un seul mode unique, il est renvoyé. S'il existe plusieurs modes, ils sont renvoyés sous forme de liste. S'il n'y a pas de mode, c'est-à-dire que tous les éléments sont présents à la même fréquence, la valeur renvoyée est nil. "

Vous pourriez penser à simplement renvoyer un seq à chaque fois (un élément ou vide, c'est bien); sinon, les cas doivent être différenciés par le code appelant. En renvoyant toujours un seq, votre résultat fonctionnera comme par magie en argument pour les autres fonctions qui s’attendent à un seq.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top