Pregunta

Estoy aprendiendo Clojure y me gustaría recibir algunos consejos sobre el uso idiomático. Como parte de un pequeño paquete de estadísticas, tengo una función para calcular el modo de un conjunto de datos. (Antecedentes: el modo es el valor más común en un conjunto de datos. Hay casi una docena de algoritmos publicados para calcularlo. El que se usa aquí proviene de "Fundamentals of Biostatistics", 6ª edición 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)
    )
  )

Hay un par de cosas sobre las que tengo preguntas:

  1. El argumento. La función acepta una sola secuencia. ¿Es más idiomático aceptar un número variable de argumentos como la función de adición?
  2. código de olor. Parece que el " let " es un poco más complicado de lo que debería ser, tantas asignaciones de variables. ¿Me he perdido algún uso obvio (o no tan obvio) del lenguaje o la biblioteca que haría este método más conciso?

Gracias de antemano por la ayuda.

¿Fue útil?

Solución

Aquí está mi opinión:

  1. Hay muchas funciones básicas de clojure que toman secuencias como argumentos, mientras que otras toman múltiples argumentos, por lo que, en mi opinión, no hay una forma idiomática real. Si ya tiene sus datos en una secuencia, usaría una secuencia como argumento, ya que le ahorrará una llamada para aplicar.

  2. No escribiría una función que devuelva un valor en algunos casos y una lista de valores en otros, porque el código de llamada siempre tendrá que verificar el valor de retorno antes de usarlo. En su lugar, devolvería un solo modo como una secuencia con solo un elemento en él. Pero puede tener sus razones, dependiendo del código que llame a esta función.

Aparte de eso, reescribiría la función de modo así:

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

En lugar de definir una función f, podría usar la función de identidad (a menos que sus datos contengan valores que sean lógicamente falsos). Pero ni siquiera necesitas eso. Encuentro los modos de una manera diferente, lo que me resulta más legible: el mapa amap actúa como una secuencia de entradas de mapa (pares clave-valor). Primero filtro solo aquellas entradas que tienen el valor mx. Luego asigno la función de la tecla en estas, dándome una secuencia de teclas.

Para comprobar si hay modos que no vuelvo a recorrer el mapa. En su lugar, simplemente comparo el número de modos con el número de entradas del mapa. Si son iguales, ¡todos los elementos tienen la misma frecuencia!

Aquí está la función que siempre devuelve una secuencia:

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

Otros consejos

En mi opinión, asignar una función a una colección y luego condensar inmediatamente la lista en un elemento es un signo para usar reducir .

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

En este caso, escribiría el modo fn para tomar una única colección como un argumento, como hiciste. La única razón por la que se me ocurre usar múltiples argumentos para una función como esta es si planeas tener que escribir muchos argumentos literales.

Así que si por ejemplo esto es para una secuencia de comandos REPL interactiva y, a menudo, va a estar escribiendo (modo [1 2 1 2 3]) literalmente, luego debe hacer que la función tome varios argumentos para evitar que escriba el [] extra en la función llama todo el tiempo. Si planeas leer muchos números de un archivo y luego tomar el modo de esos números, entonces haz que la función tome un solo argumento que es una colección para que puedas evitar el uso de aplicable todo el tiempo . Supongo que su caso de uso más común es el último. Creo que apply también agrega la sobrecarga que evita cuando tiene una llamada a la función que acepta un argumento de recopilación.

Estoy de acuerdo con los demás en que debes tener el modo para devolver una lista de resultados, incluso si solo hay uno; Te hará la vida más fácil. Tal vez cambie el nombre de mode mientras esté en ello.

Aquí hay una buena implementación concisa del modo :

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

Esto explota los siguientes hechos:

  • La función frequencies devuelve un mapa de valores - > frecuencias
  • Puede tratar un mapa como una secuencia de pares clave-valor
  • Si ordena esta secuencia por valor (el elemento segundo en cada par), entonces el último elemento en la secuencia representará el modo

EDIT

Si desea manejar el caso de modo múltiple, puede insertar un partition-by para mantener todos los valores con la frecuencia máxima:

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

Me parece bien. Reemplazaría el

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

con

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

(No sé por qué algo como not-nil? no está en clojure.core ; es algo que uno necesita todos los días.)

  

Si hay un solo modo único, se devuelve. Si hay varios modos, se devuelven como una lista. Si no hay modo, es decir, todos los elementos están presentes en la misma frecuencia, se devuelve nil. & Quot;

Podría pensar en simplemente devolver una secuencia cada vez (un elemento o vacío está bien); de lo contrario, los casos deben ser diferenciados por el código de llamada. Al devolver siempre una secuencia, su resultado mágicamente funcionará como un argumento para otras funciones que esperan una secuencia.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top