Clojure中的惯用模式功能

时间:2009-10-21 14:45:43

标签: statistics clojure

我正在学习Clojure,并希望获得有关惯用法的一些建议。作为小型统计数据包的一部分,我有一个计算一组数据模式的函数。 (背景:模式是一组数据中最常见的值。有几十种已发布的算法可供计算。这里使用的是来自“生物统计学基础”第6版的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)
    )
  )

有几件事我有疑问:

  1. 论点。该函数接受单个序列。接受像加法函数这样的可变数量的参数是不是更惯用?
  2. 代码味道。似乎“let”比它应该更复杂 - 如此多的变量赋值。我是否错过了使该方法更简洁的任何明显(或不那么明显)的语言或库用途?
  3. 提前感谢您的帮助。

4 个答案:

答案 0 :(得分:5)

在我看来,在集合上映射某些函数然后立即将列表缩减为一个项目是使用reduce的标志。

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

在这种情况下,我会写mode fn以单个集合作为参数,就像你一样。我可以想到为这样的函数使用多个参数的唯一原因是你计划必须经常输入文字参数。

所以,例如,这是一个交互式REPL脚本,你经常会按字面意思输入(mode [1 2 1 2 3]),然后你应该让函数接受多个参数,以免你在函数调用中输入额外的[]时间。如果您打算从文件中读取大量数字然后采用这些数字的模式,那么让该函数采用一个集合的参数,这样您就可以避免一直使用apply。我猜你最常见的用例是后者。我相信apply还会增加您在进行带有集合参数的函数调用时避免的开销。

我同意其他人的观点,即使只有一个结果,你应该mode返回结果列表;它会让你的生活更轻松。也许你在它的时候重命名它modes

答案 1 :(得分:4)

这是我的看法:

  1. 有许多核心的clojure函数将序列作为参数,而其他函数则采用多个参数,因此我认为没有真正的惯用法。如果你已经有序列中的数据,我会使用seq作为参数,因为它会为你节省一个应用调用。

  2. 我不会编写一个在某些情况下返回值的函数和在其他情况下返回值的列表,因为调用代码在使用之前总是必须检查返回值。相反,我会将一个模式作为seq返回,只包含一个项目。但您可能有自己的理由,具体取决于调用此函数的代码。

  3. 除此之外,我会像这样重写模式函数:

    (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)))
    

    您可以使用标识函数(除非您的数据包含逻辑上错误的值),而不是定义函数。但你甚至不需要那样做。我以不同的方式找到模式,这对我来说更具可读性:地图amap充当一系列地图条目(键值对)。首先,我只过滤那些值为mx的条目。然后我在这些上映射关键功能,给我一系列按键。

    要检查是否有任何模式,我不会再次循环遍历地图。相反,我只是将模式数量与地图条目数量进行比较。如果它们相等,则所有元素都具有相同的频率!

    这是始终返回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)))
    

答案 2 :(得分:4)

这是mode的简洁实现:

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

这利用了以下事实:

  • frequencies函数返回值的映射 - &gt;频率
  • 您可以将地图视为一系列键值对
  • 如果按值排序此序列(每对中的second项),则序列中的最后一项将代表模式

修改

如果您想处理多模式情况,那么您可以插入额外的partition-by以保持所有值具有最大频率:

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

答案 3 :(得分:2)

对我来说很好看。我会替换

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

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

(我不知道为什么not-nil?之类的东西不在clojure.core中;这是每天都需要的东西。)

  

如果存在单个唯一模式,则返回该模式。如果有多种模式,它们将作为列表返回。如果没有模式,即所有元素都以相等的频率存在,则返回nil。“

你可以考虑每次只返回一个seq(一个元素或空格是好的);否则,必须通过调用代码来区分这些情况。通过始终返回seq,您的结果将神奇地用作期望seq的其他函数的参数。