【问题标题】:Idiomatic Mode function in ClojureClojure 中的惯用模式函数
【发布时间】:2010-12-08 17:59:05
【问题描述】:

我正在学习 Clojure,想要一些关于惯用用法的建议。作为一个小型统计数据包的一部分,我有一个函数来计算一组数据的模式。 (背景:众数是一组数据中最常见的值。有近十几种已发表的算法来计算它。这里使用的一个来自 Bernard Rosner 的“Fundamentals of Biostatistics”第 6 版。)

(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”比它应该的要复杂一些——这么多的变量赋值。我是否错过了语言或库的任何明显(或不那么明显)的使用,这些使用可以使这种方法更简洁?

提前感谢您的帮助。

【问题讨论】:

    标签: statistics clojure


    【解决方案1】:

    在我看来,将一些函数映射到集合上,然后立即将列表压缩为一个项目是使用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

    【讨论】:

    • 我听取了您的建议,并将我的第二个功能重命名为模式。 :-)
    • (inc (or (h n) 0)) 与 (inc (h n 0)) 相同 :)
    • 哦,对了,我总是忘记那个默认值选项。谢谢。
    • 你说得对,最常见的用例是拉入一堆数字并弹出一个结果。感谢您对改进计数图的建议。我一直在使用它,任何改进都会受到赞赏。
    【解决方案2】:

    这是mode 的一个简洁的实现:

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

    这利用了以下事实:

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

    编辑

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

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

    【讨论】:

    • 模式不是唯一的,或者不存在的情况是什么?
    • 感谢您回答这么老的问题(我相信早在 1.0 之前的日子。)当时不存在频率函数。很高兴收到鼓励来审查旧功能以获得更新的机会。
    【解决方案3】:

    这是我的看法:

    1. 有许多核心 clojure 函数将序列作为参数,而另一些则使用多个参数,因此在我看来没有真正的惯用方式。如果您已经拥有序列中的数据,我会使用 seq 作为参数,因为它会为您节省应用调用。

    2. 我不会编写在某些情况下返回值而在其他情况下返回值列表的函数,因为调用代码在使用它之前总是必须检查返回值。相反,我会返回一个单一模式作为 seq,其中只有一个项目。但是你可能有你的理由,这取决于调用这个函数的代码。

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

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

    您可以使用恒等函数来代替定义函数 f(除非您的数据包含逻辑错误的值)。但你甚至不需要那个。我以不同的方式找到模式,这对我来说更具可读性:映射 amap 充当映射条目(键值对)的序列。首先,我只过滤那些具有值 mx 的条目。然后我在这些上映射键功能,给我一个键序列。

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

    这是总是返回序列的函数:

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

    【讨论】:

    • "你定义的函数 f 实际上是恒等函数(因为 nil 在逻辑上是假的)。"不,远非如此。比较 (map identity [true false nil 1]) 和 (map #(not (nil? %1)) [true false nil 1]) 的结果。
    • 你说得对,当然不是同一个功能。我的意思是说他可以在这个例子中使用身份函数。我会改正的。
    • 感谢您的分析和建议。这正是我一直在寻找的视角转变。
    【解决方案4】:

    我觉得不错。我会替换

    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 的函数的参数。

    【讨论】:

    • 感谢您的建议。返回值的设置方式是无稽之谈。以与我使用均值和中位数相同的方式使用该函数,这是一个短暂而徒劳的希望,它们返回单个值。
    猜你喜欢
    • 2016-03-17
    • 2013-01-28
    • 1970-01-01
    • 1970-01-01
    • 2014-02-21
    • 1970-01-01
    • 2012-01-25
    • 1970-01-01
    • 2014-03-12
    相关资源
    最近更新 更多