2009-10-21 12 views
8

Estoy aprendiendo Clojure y me gustaría obtener 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í es de "Fundamentos de la bioestadística" 6ª edición de Bernard Rosner.)Función de modo idiomático en Clojure

(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 que tengo preguntas sobre: ​​

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

Gracias de antemano por la ayuda.

Respuesta

4

Esta es mi opinión:.

  1. Hay muchos núcleo Funciones de clojure que toman secuencias como argumentos, mientras que otras toman múltiples argumentos, por lo que No hay una forma real idiomática en mi opinión. Si ya tiene sus datos en una secuencia, usaría un seq 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 cambio, devolvería un modo único como un seq con solo un elemento. Pero puede tener sus razones, dependiendo del código que llama a esta función.

Aparte de eso, lo reescribir la función del modo como esto:

(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 se puede utilizar la función de identidad (a menos que los datos contienen valores que son lógicamente falsa). Pero ni siquiera necesitas eso.Encuentro los modos de una manera diferente, que es más legible para mí: el mapa de amap actúa como una secuencia de entradas de mapas (pares clave-valor). Primero, filtro solo aquellas entradas que tienen el valor mx. Luego mapeo la función clave en estos, dándome una secuencia de claves.

Para comprobar si hay modos, 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 un siguientes:

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

"La función que define es realmente la función de identidad (ya que nil es lógicamente falsa)." No, ni mucho menos.Compare los resultados de (identidad de mapa [verdadero falso nulo 1]) y (número de mapa (no (nulo?% 1)) [verdadero falso nulo 1]). – pmf

+0

Tienes razón, por supuesto, no es la misma función. Quise decir que él podría usar la función de identidad en su lugar en este ejemplo. Voy a corregir eso. –

+0

Gracias por el análisis y la sugerencia. Ese es solo el cambio de perspectiva que estaba buscando. – clartaq

2

Me parece bien. Me reemplazar 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 se necesita todos los días.)

Si hay un único modo único, se devuelve. Si hay modos múltiples, se devuelven como una lista. Si no hay modo, es decir, todos los elementos están presentes en la misma frecuencia, se devuelve nulo. "

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

+0

Gracias por la sugerencia. La forma en que se configuraron los valores de retorno no tenía sentido. Fue una esperanza efímera e inútil de utilizar la función de la misma manera que usé la media y la mediana, que devuelven un valor único. – clartaq

5

En mi opinión, la cartografía de alguna función sobre una colección de condensación y luego inmediatamente la lista a un elemento es un signo de usar reduce.

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

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

Por lo tanto, si p. esto es para una secuencia de comandos interactiva REPL y con frecuencia escribirás (mode [1 2 1 2 3]) literalmente, entonces deberías tener la función de tomar múltiples argumentos, para evitar que digas el [] extra en la llamada a la función todo el tiempo. Si planea leer muchos números de un archivo y luego tomar el modo de esos números, haga que la función tome un solo argumento que sea una colección para que pueda guardarse de usar apply todo el tiempo. Supongo que tu caso de uso más común es el último. Creo que apply también agrega una sobrecarga que evita cuando tiene una llamada de función que toma un argumento de recopilación.

Estoy de acuerdo con los demás que debe tener mode devolver una lista de resultados, incluso si solo hay uno; te hará la vida más fácil. Quizás cambie el nombre modes mientras lo hace.

+0

Tomé su consejo y cambié el nombre de mi segunda función a modos. :-) –

+0

(inc (o (h n) 0)) es lo mismo que (inc (h n 0)) :) –

+0

Ah, claro, siempre me olvido de esa opción de valor predeterminado. Gracias. –

4

Aquí es una buena aplicación concisa de mode:

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

Este explota los siguientes hechos:

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

EDITAR

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

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

¿Cuál es el caso donde el modo no es único o no existe? – georgek

+1

Gracias por responder a una pregunta tan antigua (creo que en los anteriores 1.0 días). La función de frecuencias no existía en ese momento. Es bueno recibir un prod para revisar las funciones anteriores para la oportunidad de actualizar. – clartaq

Cuestiones relacionadas