Obtener datos para el diagrama de histograma

82

¿Hay alguna forma de especificar tamaños de contenedor en MySQL? En este momento, estoy probando la siguiente consulta SQL:

select total, count(total) from faults GROUP BY total;

Los datos que se están generando son lo suficientemente buenos, pero hay demasiadas filas. Lo que necesito es una forma de agrupar los datos en contenedores predefinidos. Puedo hacer esto desde un lenguaje de scripting, pero ¿hay alguna forma de hacerlo directamente en SQL?

Ejemplo:

+-------+--------------+
| total | count(total) |
+-------+--------------+
|    30 |            1 | 
|    31 |            2 | 
|    33 |            1 | 
|    34 |            3 | 
|    35 |            2 | 
|    36 |            6 | 
|    37 |            3 | 
|    38 |            2 | 
|    41 |            1 | 
|    42 |            5 | 
|    43 |            1 | 
|    44 |            7 | 
|    45 |            4 | 
|    46 |            3 | 
|    47 |            2 | 
|    49 |            3 | 
|    50 |            2 | 
|    51 |            3 | 
|    52 |            4 | 
|    53 |            2 | 
|    54 |            1 | 
|    55 |            3 | 
|    56 |            4 | 
|    57 |            4 | 
|    58 |            2 | 
|    59 |            2 | 
|    60 |            4 | 
|    61 |            1 | 
|    63 |            2 | 
|    64 |            5 | 
|    65 |            2 | 
|    66 |            3 | 
|    67 |            5 | 
|    68 |            5 | 
------------------------

Lo que estoy buscando:

+------------+---------------+
| total      | count(total)  |
+------------+---------------+
|    30 - 40 |            23 | 
|    40 - 50 |            15 | 
|    50 - 60 |            51 | 
|    60 - 70 |            45 | 
------------------------------

Supongo que esto no se puede lograr de una manera sencilla, pero una referencia a cualquier procedimiento almacenado relacionado también estaría bien.

Leyenda
fuente
No estoy exactamente seguro de lo que está preguntando. la salida de ejemplo podría ayudar.
Berek Bryan
¡Lo siento! Acabo de actualizar mi publicación con un ejemplo.
Legend

Respuestas:

159

Esta es una publicación sobre una forma súper rápida y sucia de crear un histograma en MySQL para valores numéricos.

Hay muchas otras formas de crear histogramas que son mejores y más flexibles, usando declaraciones CASE y otros tipos de lógica compleja. Este método me gana una y otra vez, ya que es tan fácil de modificar para cada caso de uso, y tan breve y conciso. Así es como lo haces:

SELECT ROUND(numeric_value, -2)    AS bucket,
       COUNT(*)                    AS COUNT,
       RPAD('', LN(COUNT(*)), '*') AS bar
FROM   my_table
GROUP  BY bucket;

Simplemente cambie numeric_value a lo que sea su columna, cambie el incremento de redondeo y listo. He hecho que las barras estén en escala logarítmica, para que no crezcan demasiado cuando tienes valores grandes.

numeric_value debe compensarse en la operación ROUNDing, según el incremento de redondeo, para garantizar que el primer depósito contenga tantos elementos como los siguientes depósitos.

por ejemplo, con ROUND (valor_numérico, -1), valor_numérico en el rango [0,4] (5 elementos) se colocará en el primer depósito, mientras que [5,14] (10 elementos) en el segundo, [15,24] en el tercero, a menos que numeric_value se desplace adecuadamente a través de ROUND (numeric_value - 5, -1).

Este es un ejemplo de una consulta de este tipo en algunos datos aleatorios que se ve bastante bien. Lo suficientemente bueno para una evaluación rápida de los datos.

+--------+----------+-----------------+
| bucket | count    | bar             |
+--------+----------+-----------------+
|   -500 |        1 |                 |
|   -400 |        2 | *               |
|   -300 |        2 | *               |
|   -200 |        9 | **              |
|   -100 |       52 | ****            |
|      0 |  5310766 | *************** |
|    100 |    20779 | **********      |
|    200 |     1865 | ********        |
|    300 |      527 | ******          |
|    400 |      170 | *****           |
|    500 |       79 | ****            |
|    600 |       63 | ****            |
|    700 |       35 | ****            |
|    800 |       14 | ***             |
|    900 |       15 | ***             |
|   1000 |        6 | **              |
|   1100 |        7 | **              |
|   1200 |        8 | **              |
|   1300 |        5 | **              |
|   1400 |        2 | *               |
|   1500 |        4 | *               |
+--------+----------+-----------------+

Algunas notas: Los rangos que no tienen coincidencia no aparecerán en el recuento; no tendrá un cero en la columna del recuento. Además, estoy usando la función REDONDEAR aquí. Puede reemplazarlo fácilmente con TRUNCATE si cree que tiene más sentido para usted.

Lo encontré aquí http://blog.shlomoid.com/2011/08/how-to-quickly-create-histogram-in.html

Jaro
fuente
1
A partir de MySQL 8.0.3, ahora tiene la capacidad de crear estadísticas de histograma para proporcionar más estadísticas al optimizador; consulte mysqlserverteam.com/histogram-statistics-in-mysql
Jaro
Ni siquiera necesita la parte "barra" de la consulta; los números mismos ya forman un histograma / gráfico de barras logarítmicas.
enarmónico
31

La respuesta de Mike DelGaudio es la forma en que lo hago, pero con un ligero cambio:

select floor(mycol/10)*10 as bin_floor, count(*)
from mytable
group by 1
order by 1

¿La ventaja? Puede hacer que los contenedores sean tan grandes o pequeños como desee. ¿Contenedores de tamaño 100? floor(mycol/100)*100. ¿Contenedores de tamaño 5? floor(mycol/5)*5.

Bernardo.

Bernardo Siu
fuente
como carillonator dijo que su grupo por & ordenar por mejor debería ser bin_floor o 1 - Votaré a favor si lo corrige, esta es la mejor respuesta para mí
BM
Muy bien, @bm. Cambiado según lo sugerido por carillonator.
Bernardo Siu
y si desea un nombre de columna más agradable, puede hacerloconcat(floor(mycol/5)*5," to ",floor(mycol/5)*5+5)
alex9311
En realidad, esto es mejor que simple round(mycol, -2)de la respuesta aceptada, ya que permite al usuario definir cualquier "rango" no decimal. Solo usaría en roundlugar de, floorya que redondea correctamente los números.
Meridius
16
SELECT b.*,count(*) as total FROM bins b 
left outer join table1 a on a.value between b.min_value and b.max_value 
group by b.min_value

Los contenedores de la tabla contienen columnas min_value y max_value que definen los contenedores. tenga en cuenta que el operador "unirse ... en x ENTRE yyz" es inclusivo.

table1 es el nombre de la tabla de datos

Ofri Raviv
fuente
2
¿Por qué la coloración de sintaxis para SQL es tan mala? ¿Cómo puedo mejorar esto? Tal vez debería publicarlo en meta;)
Ofri Raviv
2
En este caso es necesaria una tabla de plantilla para definir min y max. Solo con SQL no es posible.
Cesar
SQL Guru! Exactamente lo que quería. Supongo que se debe tener cuidado al crear la tabla de contenedores. De lo contrario, todo funciona perfectamente. :) Gracias. Acabo de terminar de escribir un script de Python, pero esto es justo lo que necesitaba ...
Leyenda
@Legend: En realidad, soy bastante n00b cuando se trata de SQL. pero esta fue una pregunta interesante y útil, así que me gustó el ejercicio ...
Ofri Raviv
1
Es importante ver la respuesta de @David West (que debería haber sido un comentario, aquí) sobre cómo COUNT (*) produce 1 cuando debería producir cero. Puede que eso no sea un gran problema para usted, pero puede sesgar los datos estadísticos y hacer que parezca un poco tonto si alguien se da cuenta :)
Christopher Schultz
11

La respuesta de Ofri Raviv es muy cercana pero incorrecta. La count(*)habrá 1incluso si hay cero resultados en un intervalo de histograma. La consulta debe modificarse para usar un condicional sum:

SELECT b.*, SUM(a.value IS NOT NULL) AS total FROM bins b
  LEFT JOIN a ON a.value BETWEEN b.min_value AND b.max_value
GROUP BY b.min_value;
David West
fuente
10
select "30-34" as TotalRange,count(total) as Count from table_name
   where total between 30 and 34
union (
select "35-39" as TotalRange,count(total) as Count from table_name 
   where total between 35 and 39)
union (
select "40-44" as TotalRange,count(total) as Count from table_name
   where total between 40 and 44)
union (
select "45-49" as TotalRange,count(total) as Count from table_name
   where total between 45 and 49)
etc ....

Siempre que no haya demasiados intervalos, esta es una solución bastante buena.

sammy
fuente
1
+1 Esta es la única solución aquí que permite que los contenedores sean de diferente tamaño
Gabe Moothart
genial - no hay necesidad de mesas adicionales
NiRR
+1 Esta es la solución más flexible en mi opinión, y parece ajustarse mejor al caso de uso de querer bin desde dentro de SQL. en cualquier caso en el que los rangos de bin deben derivarse programáticamente, es probable que sea mejor hacerlo fuera de SQL. de nuevo en mi opinión
Ryan McCoy
4

Hice un procedimiento que se puede usar para generar automáticamente una tabla temporal para bins de acuerdo con un número o tamaño específico, para su uso posterior con la solución de Ofri Raviv.

CREATE PROCEDURE makebins(numbins INT, binsize FLOAT) # binsize may be NULL for auto-size
BEGIN
 SELECT FLOOR(MIN(colval)) INTO @binmin FROM yourtable;
 SELECT CEIL(MAX(colval)) INTO @binmax FROM yourtable;
 IF binsize IS NULL 
  THEN SET binsize = CEIL((@binmax-@binmin)/numbins); # CEIL here may prevent the potential creation a very small extra bin due to rounding errors, but no good where floats are needed.
 END IF;
 SET @currlim = @binmin;
 WHILE @currlim + binsize < @binmax DO
  INSERT INTO bins VALUES (@currlim, @currlim+binsize);
  SET @currlim = @currlim + binsize;
 END WHILE;
 INSERT INTO bins VALUES (@currlim, @maxbin);
END;

DROP TABLE IF EXISTS bins; # be careful if you have a bins table of your own.
CREATE TEMPORARY TABLE bins (
minval INT, maxval INT, # or FLOAT, if needed
KEY (minval), KEY (maxval) );# keys could perhaps help if using a lot of bins; normally negligible

CALL makebins(20, NULL);  # Using 20 bins of automatic size here. 

SELECT bins.*, count(*) AS total FROM bins
LEFT JOIN yourtable ON yourtable.value BETWEEN bins.minval AND bins.maxval
GROUP BY bins.minval

Esto generará el recuento del histograma solo para los contenedores que se llenan. David West debería tener razón en su corrección, pero por alguna razón, los contenedores vacíos no aparecen en el resultado para mí (a pesar del uso de LEFT JOIN, no entiendo por qué).

Dologan
fuente
3

Eso debería funcionar. No tan elegante pero aún así:

select count(mycol - (mycol mod 10)) as freq, mycol - (mycol mod 10) as label
from mytable
group by mycol - (mycol mod 10)
order by mycol - (mycol mod 10) ASC

a través de Mike DelGaudio

Renaud
fuente
3
SELECT
    CASE
        WHEN total <= 30 THEN "0-30"
        WHEN total <= 40 THEN "31-40"       
        WHEN total <= 50 THEN "41-50"
        ELSE "50-"
    END as Total,
    count(*) as count
GROUP BY Total 
ORDER BY Total;
Cebra
fuente
2

Agrupación de igual ancho en un recuento dado de agrupaciones:

WITH bins AS(
   SELECT min(col) AS min_value
        , ((max(col)-min(col)) / 10.0) + 0.0000001 AS bin_width
   FROM cars
)
SELECT tab.*,
   floor((col-bins.min_value) / bins.bin_width ) AS bin
FROM tab, bins;

Tenga en cuenta que el 0,0000001 está ahí para asegurarse de que los registros con el valor igual a max (col) no hagan su propio contenedor por sí mismos. Además, la constante aditiva está ahí para asegurarse de que la consulta no falle en la división por cero cuando todos los valores de la columna son idénticos.

También tenga en cuenta que el recuento de bins (10 en el ejemplo) debe escribirse con una marca decimal para evitar la división de enteros (el bin_width no ajustado puede ser decimal).

user824276
fuente
El WITH something ASes muy útil si usted tiene que calcular el valor que va en los contenedores.
Rúnar Berg