【问题标题】:Quartiles in SQL querySQL查询中的四分位数
【发布时间】:2020-10-17 04:31:27
【问题描述】:

我有一个非常简单的表格:

CREATE TABLE IF NOT EXISTS LuxLog (
  Sensor TINYINT,
  Lux INT,
  PRIMARY KEY(Sensor)
)

它包含来自不同传感器的数千条日志。

我希望所有传感器都有 Q1 和 Q3。

我可以对每个数据进行一次查询,但我最好对所有传感器进行一次查询(从一次查询中获取 Q1 和 Q3)

我认为这将是一个相当简单的操作,因为四分位数被广泛使用,并且是频率计算中的主要统计变量之一。事实上,我发现了大量过于复杂的解决方案,而我希望能找到一些简洁明了的解决方案。

谁能给我一个提示?

编辑:这是我在网上找到的一段代码,但对我不起作用:

SELECT  SUBSTRING_INDEX(
        SUBSTRING_INDEX(
            GROUP_CONCAT(                 -- 1) make a sorted list of values
                Lux
                ORDER BY Lux
                SEPARATOR ','
            )
        ,   ','                           -- 2) cut at the comma
        ,   75/100 * COUNT(*)        --    at the position beyond the 90% portion
        )
    ,   ','                               -- 3) cut at the comma
    ,   -1                                --    right after the desired list entry
    )                 AS `75th Percentile`
    FROM    LuxLog
    WHERE   Sensor=12
    AND     Lux<>0

我得到 1 作为返回值,而它应该是一个可以除以 10 的数字 (10,20,30.....1000)

【问题讨论】:

  • 你不是在几个小时前才问过这个问题吗?没有好的答案?
  • “我认为这将是一个相当简单的操作,因为四分位数被广泛使用,并且是频率计算中的主要统计变量之一。”这不是预测任务难易程度的一个很好的基础。与计算方法相比,计算四分位数(甚至只是中位数)在操作上比较复杂。
  • 你说得对,但我现在正在接近 SQL,而且由于我习惯了其他高级编程语言,因此缺少统计包让我很痛苦。
  • SQL 没有很好地被描述为一种编程 语言。它是一种基于关系数据模型的数据定义和数据操作语言。尽管 SQL 通常以及某些实现确实具有一些针对排序的特性,但底层模型是基于(无序)集合的。这与某些类型的任务不匹配,例如计算四分位数。这并不意味着您不能在 SQL 中执行此类计算,但对于某些任务,您最好将 SQL 与另一种语言配对。
  • MySQL 恰好特别受限于此类任务,因为它缺少几个共同的功能,其中任何一个都可以使工作更容易(窗口函数、公共表表达式、各种其他特定功能——甚至无视NTILE())。

标签: mysql sql quantile percentile


【解决方案1】:

参见 SqlFiddle:http://sqlfiddle.com/#!9/accca6/2/6 注意:对于 sqlfiddle,我生成了 100 行,1 到 100 之间的每个整数都有一行,但它是随机顺序(在 excel 中完成)。

代码如下:

SET @number_of_rows := (SELECT COUNT(*) FROM LuxLog);
SET @quartile := (ROUND(@number_of_rows*0.25));
SET @sql_q1 := (CONCAT('(SELECT "Q1" AS quartile_name , Lux, Sensor FROM LuxLog ORDER BY Lux DESC LIMIT 1 OFFSET ', @quartile,')'));
SET @sql_q3 := (CONCAT('( SELECT "Q3" AS quartile_name , Lux, Sensor FROM LuxLog ORDER BY Lux ASC LIMIT 1 OFFSET ', @quartile,');'));
SET @sql := (CONCAT(@sql_q1,' UNION ',@sql_q3));
PREPARE stmt1 FROM @sql;
EXECUTE stmt1;

编辑:

SET @current_sensor := 101;
SET @quartile := (ROUND((SELECT COUNT(*) FROM LuxLog WHERE Sensor = @current_sensor)*0.25));
SET @sql_q1 := (CONCAT('(SELECT "Q1" AS quartile_name , Lux, Sensor FROM LuxLog WHERE Sensor=', @current_sensor,' ORDER BY Lux DESC LIMIT 1 OFFSET ', @quartile,')'));
SET @sql_q3 := (CONCAT('( SELECT "Q3" AS quartile_name , Lux, Sensor FROM LuxLog WHERE Sensor=', @current_sensor,' ORDER BY Lux ASC LIMIT 1 OFFSET ', @quartile,');'));
SET @sql := (CONCAT(@sql_q1,' UNION ',@sql_q3));
PREPARE stmt1 FROM @sql;
EXECUTE stmt1;

基本推理如下: 对于四分位数 1,我们希望从顶部获得 25%,因此我们想知道有多少行,即:

SET @number_of_rows := (SELECT COUNT(*) FROM LuxLog);

现在我们知道了行数,我们想知道其中的 25% 是多少,就是这一行:

SET @quartile := (ROUND(@number_of_rows*0.25));

然后要找到一个四分位数,我们要按 Lux 排序 LuxLog 表,然后获取行号“@quartile”,为了做到这一点,我们将 OFFSET 设置为 @quartile 表示我们要开始选择从行号@quartile 我们说limit 1 表示我们只想检索一行。那是:

SET @sql_q1 := (CONCAT('(SELECT "Q1" AS quartile_name , Lux, Sensor FROM LuxLog ORDER BY Lux DESC LIMIT 1 OFFSET ', @quartile,')'));

我们(几乎)对另一个四分位数做同样的事情,但我们不是从顶部开始(从较高值到较低值),而是从底部开始(它解释了 ASC)。

但是现在我们只是将字符串存储在变量@sql_q1 和@sql_q3 中,所以将它们连接起来,合并查询结果,准备查询并执行它。

【讨论】:

  • 我用我的一小部分数据构建了 Fiddle (sqlfiddle.com/#!9/a14a4/3)(我每天大约有 50k 行)。我还添加了“WHERE Sensor=x”,因为我需要每个传感器的数据,但我不明白如何使用 Fiddle。你想看看吗?
  • @Hamma :抱歉,我认为没有 2 行具有相同的传感器 ^^ 这是适用于给定传感器的代码(请参阅编辑的代码),我正在研究一个为所有人输出的代码传感器,我告诉你什么时候完成。
  • 我会尽快尝试。不要为所有传感器的输出而烦恼,因为我可以为每个传感器运行不同的查询。对我来说重要的是从查询中获取四分位数,而不是从查询中获取数千个原始数据,然后自己计算它们。
  • 如果我们以 w3schools 的定义为例(首先在 Google 上):“PRIMARY KEY 约束唯一地标识数据库表中的每条记录。”。这基本上意味着每一行在主键字段中都有不同的值。它允许您唯一标识一行。这就是为什么拥有多余的 PRIMARY KEY 很奇怪的原因。它更可能是外键,这意味着表中的外键字段链接到另一个表中的主键。请参阅:stackoverflow.com/questions/1692538/…
  • 也许您不需要主键,但您可能需要索引(查看索引的用处:stackoverflow.com/questions/1108/…)。它将使您的查询结果更快。此外,您根本无法在数据库上使用主索引,因为您有多行具有相同的键。当我尝试时,它返回一个错误“重复条目 '101' for key 'PRIMARY'”。
【解决方案2】:

使用 NTILE 非常简单,但它是一个 Postgres 函数。你基本上只是做这样的事情:

SELECT value_you_are_NTILING,
    NTILE(4) OVER (ORDER BY value_you_are_NTILING DESC) AS tiles
FROM
(SELECT math_that_gives_you_the_value_you_are_NTILING_here AS value_you_are_NTILING FROM tablename);

这是我在 SQLFiddle 上为您制作的一个简单示例:http://sqlfiddle.com/#!15/7f05a/1

在 MySQL 中,您将使用 RANK...这是 SQLFiddle:http://www.sqlfiddle.com/#!2/d5587/1(来自下面链接的问题)

MySQL RANK() 的这种使用来自 Stackoverflow,此处回答:Rank function in MySQL

寻找 Salman A 的答案。

【讨论】:

  • 嗯,NTILE() 这个工作最大的问题是 MySQL 没有它(并且问题被标记为 mysql)。
  • 是的,你是对的。我正在使用 MySQL,我只是注意到 NTILE() 不是 MySQL 函数。很抱歉浪费了您的时间。
  • 这不是浪费时间。我在 Mysql 中添加了讨论 RANK 函数的链接。这给了你你正在寻找的东西。
  • 我在我的桌子上尝试了这个例子,我得到了按排名分组的结果。我想为了得到四分位数,我必须要求排名数 totalranks*0.25 和 totalranks*0.75?
  • 我猜是NTILE has been addedMySQL 8
【解决方案3】:

应该这样做:

select
    ll.*,
    if (a.position is not null, 1,
        if (b.position is not null, 2, 
        if (c.position is not null, 3, 
        if (d.position is not null, 4, 0)))
    ) as quartile
from
    luxlog ll
    left outer join luxlog a on ll.position = a.position and a.lux > (select count(*)*0.00 from luxlog) and a.lux <= (select count(*)*0.25 from luxlog)
    left outer join luxlog b on ll.position = b.position and b.lux > (select count(*)*0.25 from luxlog) and b.lux <= (select count(*)*0.50 from luxlog)
    left outer join luxlog c on ll.position = c.position and c.lux > (select count(*)*0.50 from luxlog) and c.lux <= (select count(*)*0.75 from luxlog)
    left outer join luxlog d on ll.position = d.position and d.lux > (select count(*)*0.75 from luxlog)
;    

这是完整的例子:

use example;

drop table if exists luxlog;

CREATE TABLE LuxLog (
  Sensor TINYINT,
  Lux INT,
  position int,
  PRIMARY KEY(Position)
);

insert into luxlog values (0, 1, 10);
insert into luxlog values (0, 2, 20);
insert into luxlog values (0, 3, 30);
insert into luxlog values (0, 4, 40);
insert into luxlog values (0, 5, 50);
insert into luxlog values (0, 6, 60);
insert into luxlog values (0, 7, 70);
insert into luxlog values (0, 8, 80);

select count(*)*.25 from luxlog;
select count(*)*.50 from luxlog;

select
    ll.*,
    a.position,
    b.position,
    if(
        a.position is not null, 1,
        if (b.position is not null, 2, 0)
    ) as quartile
from
    luxlog ll
    left outer join luxlog a on ll.position = a.position and a.lux >= (select count(*)*0.00 from luxlog) and a.lux < (select count(*)*0.25 from luxlog)
    left outer join luxlog b on ll.position = b.position and b.lux >= (select count(*)*0.25 from luxlog) and b.lux < (select count(*)*0.50 from luxlog)
    left outer join luxlog c on ll.position = c.position and c.lux >= (select count(*)*0.50 from luxlog) and c.lux < (select count(*)*0.75 from luxlog)
    left outer join luxlog d on ll.position = d.position and d.lux >= (select count(*)*0.75 from luxlog) and d.lux < (select count(*)*1.00 from luxlog)
;    


select
    ll.*,
    if (a.position is not null, 1,
        if (b.position is not null, 2, 
        if (c.position is not null, 3, 
        if (d.position is not null, 4, 0)))
    ) as quartile
from
    luxlog ll
    left outer join luxlog a on ll.position = a.position and a.lux > (select count(*)*0.00 from luxlog) and a.lux <= (select count(*)*0.25 from luxlog)
    left outer join luxlog b on ll.position = b.position and b.lux > (select count(*)*0.25 from luxlog) and b.lux <= (select count(*)*0.50 from luxlog)
    left outer join luxlog c on ll.position = c.position and c.lux > (select count(*)*0.50 from luxlog) and c.lux <= (select count(*)*0.75 from luxlog)
    left outer join luxlog d on ll.position = d.position and d.lux > (select count(*)*0.75 from luxlog)
;    

【讨论】:

  • 这个查询执行了 80 秒,它给了我所有 10k 行的结果。我尝试这样做的主要原因是避免传输所有数据。我只希望从选择中返回操作的结果。
  • 编辑了我给出的排名答案,只为每个 ntile 返回一行。
  • 我仍然得到一个包含数千行查询的表,查询时间为 25 秒,其中只有我将拥有的数据的一小部分。我不明白开头的那两个 select count(*) 应该有什么帮助。
【解决方案4】:

或者你可以像这样使用排名:

select
    ll.*,
    @curRank := @curRank + 1 as rank,
    if (@curRank <= (select count(*)*0.25 from luxlog), 1,
        if (@curRank <= (select count(*)*0.50 from luxlog), 2, 
        if (@curRank <= (select count(*)*0.75 from luxlog), 3, 4))
    ) as quartile
from
    luxlog ll,
    (SELECT @curRank := 0) r
;    

这将只为每个四分位数提供一条记录:

select
    x.quartile, group_concat(position)
from (
    select
        ll.*,
        @curRank := @curRank + 1 as rank,
        if (@curRank > 0 and @curRank <= (select count(*)*0.25 from luxlog), 1,
            if (@curRank > 0 and @curRank <= (select count(*)*0.50 from luxlog), 2, 
            if (@curRank > 0 and @curRank <= (select count(*)*0.75 from luxlog), 3, 4))
        ) as quartile
    from
        luxlog ll,
        (SELECT @curRank := 0) r
) x
group by quartile

+ ------------- + --------------------------- +
| quartile      | group_concat(position)      |
+ ------------- + --------------------------- +
| 1             | 10,20                       |
| 2             | 30,40                       |
| 3             | 50,60                       |
| 4             | 70,80                       |
+ ------------- + --------------------------- +
4 rows

编辑: 删除后的 sqlFiddle 示例 (http://sqlfiddle.com/#!9/a14a4/17) 如下所示

/*SET @number_of_rows := (SELECT COUNT(*) FROM LuxLog);
SET @quartile := (ROUND(@number_of_rows*0.25));
SET @sql_q1 := (CONCAT('(SELECT "Q1" AS quartile_name , Lux, Sensor FROM LuxLog WHERE Sensor=101 ORDER BY Lux DESC LIMIT 1 OFFSET ', @quartile,')'));
SET @sql_q3 := (CONCAT('( SELECT "Q3" AS quartile_name , Lux, Sensor FROM LuxLog WHERE Sensor=101 ORDER BY Lux ASC LIMIT 1 OFFSET ', @quartile,');'));
SET @sql := (CONCAT(@sql_q1,' UNION ',@sql_q3));
PREPARE stmt1 FROM @sql;
EXECUTE stmt1;*/

【讨论】:

  • 我正在尝试测试它,但它没有运行。您可以在此处查看包含我的一小部分数据样本的表格:sqlfiddle.com/#!9/a14a4/6
  • 如果我删除了 sqlfiddle 示例顶部的注释掉的代码 (/* */),你给它的代码对我有用。
【解决方案5】:

这是我提出的用于计算四分位数的查询;它在 ~0.04 秒内运行,有 ~5000 个表行。我包括了最小/最大值,因为我最终使用这些数据来构建四个四分位数范围:

   SELECT percentile_table.percentile, avg(ColumnName) AS percentile_values
    FROM   
        (SELECT @rownum := @rownum + 1 AS `row_number`, 
                   d.ColumnName 
            FROM   PercentileTestTable d, 
                   (SELECT @rownum := 0) r 
            WHERE  ColumnName IS NOT NULL 
            ORDER  BY d.ColumnName
        ) AS t1, 
        (SELECT count(*) AS total_rows 
            FROM   PercentileTestTable d 
            WHERE  ColumnName IS NOT NULL 
        ) AS t2, 
        (SELECT 0 AS percentile 
            UNION ALL 
            SELECT 0.25
            UNION ALL 
            SELECT 0.5
            UNION ALL 
            SELECT 0.75
            UNION ALL 
            SELECT 1
        ) AS percentile_table  
    WHERE  
        (percentile_table.percentile != 0 
            AND percentile_table.percentile != 1 
            AND t1.row_number IN 
            ( 
                floor(( total_rows + 1 ) * percentile_table.percentile), 
                floor(( total_rows + 2 ) * percentile_table.percentile)
            ) 
        ) OR (
            percentile_table.percentile = 0 
            AND t1.row_number = 1
        ) OR (
            percentile_table.percentile = 1 
            AND t1.row_number = total_rows
        )
    GROUP BY percentile_table.percentile; 

在这里提琴:http://sqlfiddle.com/#!9/58c0e2/1

肯定存在性能问题;如果有人对如何改进这一点提出反馈意见,我会很高兴。

样本数据列表:

 3, 4, 4, 4, 7, 10, 11, 12, 14, 16, 17, 18

示例查询输出:

| percentile | percentile_values |
|------------|-------------------|
|          0 |                 3 |
|       0.25 |                 4 |
|        0.5 |              10.5 |
|       0.75 |                15 |
|          1 |                18 |

【讨论】:

    【解决方案6】:

    我将此解决方案与 MYSQL 函数一起使用:

    x 是你想要的百分位数

    array_values 您的 group_concat 值顺序并用 , 分隔,

    DROP FUNCTION IF EXISTS centile;
    
    delimiter $$
    CREATE FUNCTION `centile`(x Text, array_values TEXT) RETURNS text
    BEGIN
    
    Declare DIFF_RANK TEXT;
    Declare RANG_FLOOR INT;
    Declare COUNT INT;
    Declare VALEUR_SUP TEXT;
    Declare VALEUR_INF TEXT;
    
    SET COUNT = LENGTH(array_values) - LENGTH(REPLACE(array_values, ',', '')) + 1;
    SET RANG_FLOOR = FLOOR(ROUND((x) * (COUNT-1),2));
    SET DIFF_RANK = ((x) * (COUNT-1)) - FLOOR(ROUND((x) * (COUNT-1),2));
    
    SET VALEUR_SUP = CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(array_values,',', RANG_FLOOR+2),',',-1) AS DECIMAL);
    SET VALEUR_INF = CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(array_values,',', RANG_FLOOR+1),',',-1) AS DECIMAL);
    
    /****
        https://fr.wikipedia.org/wiki/Quantile
        x_j+1 + g (x_j+2 - x_j+1)       
    ***/
    RETURN  Round((VALEUR_INF + (DIFF_RANK* (VALEUR_SUP-VALEUR_INF) ) ),2);
    
    END$$
    

    例子:

    Select centile(3/4,GROUP_CONCAT(lux ORDER BY lux SEPARATOR ',')) as quartile_3
    FROM LuxLog
    WHERE Sensor=12 AND Lux<>0
    

    【讨论】:

      猜你喜欢
      • 2013-01-09
      • 1970-01-01
      • 2021-02-28
      • 2021-06-22
      • 2015-04-12
      • 2016-07-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多