web-dev-qa-db-ja.com

GROUP BYの中央値

次の表_t1_があるとします。

 ================= 
 |タグ| val | -+簡略化のため、valはNULL以外です
 ================= 
 | a1 | v1 | 
 | a1 | v2 | 
 | a1 | v3 | 
 | a1 | v4 | 
 | a1 | v5 | 
 | a2 | v6 | 
 | a2 | v7 | 
 | a2 | v8 | 
 | a2 | v9 | 
 | ... | ... | 
 ================= 

MySQLで以下のスクリプトを実行すると、

_SELECT `tag`, AVG(`val`) FROM `t1` GROUP BY `tag`
_

tagでグループ化された平均値を取得します。

 ================= 
 |タグ| AVG()| 
 ================= 
 | a1 | avg1 | 
 | a2 | avg2 | 
 | a3 | avg3 | 
 | a4 | avg4 | 
 | ... | ... | 
 ================= 

MySQLには、AVG()の他に、集計値を計算するための組み込み関数がいくつかあります(例:SUM()MAX()COUNT()、およびSTD())前述のスクリプトと同じ方法で使用できます。ただし、medianの組み込み関数はありません。

この問題はすでにSEで何度か発生しています。ただし、それらのほとんどは_GROUP BY_のないテーブルに関連しています。 _GROUP BY_のあるものは MySql:Count median grouped by day のようです。ただし、スクリプトは複雑すぎるようです。

質問

この中央値を計算する簡単で簡単な方法(可能な場合)は何ですか?

ファローアップ

受け入れられた答えを補完する優れた記事:
http://danielsetzermann.com/howto/how-to-calculate-the-median-per-group-with-mysql/

4
Mark Messa

このクエリはあなたの質問に答えることができます:medianvalue andgroup by

            SELECT tag, AVG(val) as median
            FROM 
            (
              SELECT tag, val,
                  (SELECT count(*) FROM median t2 WHERE t2.tag = t3.tag) as ct,
                  seq,
                  (SELECT count(*) FROM median t2 WHERE t2.tag < t3.tag) as delta
                FROM (SELECT tag, val, @rownum := @rownum + 1 as seq
                      FROM (SELECT * FROM median ORDER BY tag, val) t1 
                      ORDER BY tag, seq
                    ) t3 CROSS JOIN (SELECT @rownum := 0) x
                HAVING (ct%2 = 0 and seq-delta between floor((ct+1)/2) and floor((ct+1)/2) +1)
                  or (ct%2 <> 0 and seq-delta = (ct+1)/2)
            ) T
            GROUP BY tag
            ORDER BY tag;

私はこのデータセットで試しました(主に here から):

            +------+------+
            | tag  | val  |
            +------+------+
            |    1 |    3 |
            |    1 |   13 |

...(以下の説明を参照)

            |    3 |   12 |
            |    3 |   43 |
            |    3 |   15 |
            +------+------+

そして結果は:

            +------+---------+
            | tag  | median  |
            +------+---------+
            |    1 | 23.0000 |
            |    2 | 22.0000 |
            |    3 | 15.0000 |
            +------+---------+

説明

内部サブクエリが最初に計算されます。シーケンスは(1)(2)(3)(4)です。

-(4)(2行または1行の)平均を計算します

    SELECT tag, AVG(val) as median                          
      FROM 
      (

-(3)中央値を計算するための線を取得します

        SELECT tag, val,                                       
           (SELECT count(*) FROM median t2                    -- +number of lines for the current tag value as ct
              WHERE t2.tag = t3.tag) as ct,
           seq,
           (SELECT count(*) FROM median t2                    -- +number of lines before the current tag value as delta
              WHERE t2.tag < t3.tag) as delta                --     to compute the starting line number of a tag
         FROM (

-(2)タグとシーケンスでデータセットをソートする

                SELECT tag, val,                            
                    @rownum := @rownum + 1 as seq       -- +@rownum enable to create a sequence from 0 by 1
              FROM (

-(1)タグと値でデータセットをソート

                    SELECT * FROM median           
                    ORDER BY tag, val) t1 

-(2)ここに続く

              ORDER BY tag, seq
            ) t3 CROSS JOIN (SELECT @rownum := 0) x            -- +use to set @rownum to 0 (no data)

-(3)ここに続く

         HAVING (ct%2 = 0                                      -- +when ct is even, select the two lines around the middle
                  and seq-delta between floor((ct+1)/2) 
                                and floor((ct+1)/2) +1)
           or (ct%2 <> 0                                       -- +when ct is odd, select the one line in the middle
                  and seq-delta = (floor(ct+1)/2))
      ) T

-(4)ここに続く

      GROUP BY tag
      ORDER BY tag;

データセット:

        after (1)     after (2)           processing (3)   
    +------+------+                   
    | tag  | val  |  ct  delta  seq       seq-delta
    +------+------+                   
    |    1 |    3 |  15    0     1        1         ct : odd ct%2 <> 0  
    |    1 |    5 |  15    0     2        2         floor((ct+1)/2) : 8
    |    1 |    7 |  15    0     3        3         
    |    1 |   12 |  15    0     4        4         
    |    1 |   13 |  15    0     5        5
    |    1 |   14 |  15    0     6        6
    |    1 |   21 |  15    0     7        7
    |    1 |   23 |  15    0     8        8 ---> keep this line
    |    1 |   23 |  15    0     9        9 
    |    1 |   23 |  15    0     10       10
    |    1 |   23 |  15    0     11       11
    |    1 |   29 |  15    0     12       12
    |    1 |   39 |  15    0     13       13
    |    1 |   40 |  15    0     14       14
    |    1 |   56 |  15    0     15       15

    |    2 |    3 |  14    15    16        1         ct : even (ct%2 = 0  )
    |    2 |    5 |  14    15    17        2         floor((ct+1)/2) : 7
    |    2 |    7 |  14    15    18        3         floor((ct+1)/2)+1 : 8
    |    2 |   12 |  14    15    19        4
    |    2 |   13 |  14    15    20        5
    |    2 |   14 |  14    15    21        6
    |    2 |   21 |  14    15    22        7 ---> keep this line
    |    2 |   23 |  14    15    23        8 ---> keep this line
    |    2 |   23 |  14    15    24        9
    |    2 |   23 |  14    15    25        10
    |    2 |   23 |  14    15    26        11
    |    2 |   29 |  14    15    27        12
    |    2 |   40 |  14    15    28        13
    |    2 |   56 |  14    15    29        14

    |    3 |   12 |  3     29    30        1                  ct : odd ct%2 <> 0 
    |    3 |   15 |  3     29    31        2 ---> keep        floor((ct+1)/2) : 2
    |    3 |   43 |  3     29    32        3
    +------+------+

(3)の後のデータセット

    +------+------+------+------+-------+
    | tag  | val  | ct   | seq  | delta |
    +------+------+------+------+-------+
    |    1 |   23 |   15 |    8 |     0 |
    |    2 |   21 |   14 |   22 |    15 |
    |    2 |   23 |   14 |   23 |    15 |
    |    3 |   15 |    3 |   31 |    29 |
    +------+------+------+------+-------+

外部クエリは、タグ値によってavg(val)グループを計算します。

お役に立てれば。

しかし、null値がある場合の中央値計算についてはどうでしょうか?以下のEDIT2を参照してください

代替:関数を使用する

    DELIMITER //
    CREATE FUNCTION median(pTag int)
        RETURNS real
           READS SQL DATA
           DETERMINISTIC
           BEGIN
              DECLARE r real; -- result
    SELECT AVG(val) INTO r
    FROM 
    (
      SELECT val,
           (SELECT count(*) FROM median WHERE tag = pTag) as ct,
           seq
        FROM (SELECT val, @rownum := @rownum + 1 as seq
              FROM (SELECT * FROM median WHERE tag = pTag ORDER BY val ) t1 
              ORDER BY seq
            ) t3 
            CROSS JOIN (SELECT @rownum := 0) x
        HAVING (ct%2 = 0 and seq between floor((ct+1)/2) and floor((ct+1)/2) +1)
          or (ct%2 <> 0 and seq = (ct+1)/2)
    ) T;
    return r;
    END//
    DELIMITER ;

しかし、関数は行ごとに呼び出されます。

SELECT tag, median(tag) FROM median; -- my test table is 'median' too...

このクエリは「より良い」でしょう:

select tag, median(tag) 
  from (select distinct tag from median) t;

私ができることはそれだけです!それが役に立てば幸い!

EDIT2:データのnull値について(例では列val)

行をカウントする2つのサブクエリとデータを取得するサブクエリの両方で、WHERE句:WHERE val IS NOT NULLを使用して、null値がソースデータから省略されていることを示します。

EDIT3(最終編集):@rownum位置の初期化を変更します

クエリの実行で最も早く宣言できるように、最も深いレベルに置く必要があります。

DELIMITER //
CREATE FUNCTION median(pTag int)
    RETURNS real
       READS SQL DATA
       DETERMINISTIC
       BEGIN
          DECLARE r real; -- result
SELECT AVG(val) INTO r
FROM 
(
  SELECT val,
       (SELECT count(*) FROM median WHERE tag = pTag and val is not null)     as ct,
       seq
    FROM (SELECT val, @rownum := @rownum + 1 as seq
          FROM (SELECT * FROM median 
                       CROSS JOIN (SELECT @rownum := 0) x -- INIT @rownum here
             WHERE tag = pTag and val is not null ORDER BY val 
          ) t1 
          ORDER BY seq
       ) t3     
    HAVING (ct%2 = 0 and seq between floor((ct+1)/2) and floor((ct+1)/2) +1)
      or (ct%2 <> 0 and seq = (ct+1)/2)
) T;
return r;
END//
DELIMITER ;

これはクエリでも同じです。

さらに2つのデータセットでテストする:

|    4 | NULL |
|    4 |   10 |
|    4 |   15 |
|    4 |   20 |
|    5 | NULL |
|    5 | NULL |
|    5 | NULL |
+------+------+

39行セット(0.00秒)

+------+--------------+
| tag  | median2(tag) |
+------+--------------+
|    1 |           23 |
|    2 |           22 |
|    3 |           15 |
|    4 |           15 |
|    5 |         NULL |
+------+--------------+
5 rows in set (0.08 sec)
7

ここに質問に関する優れた記事があります: http://danielsetzermann.com/howto/how-to-calculate-the-median-per-group-with-mysql/

1
Zitun

記録のためだけに...
@ PatrickDezecacheの受け入れられた回答に基づいて(マイナーな変更を加えて)、MySQLサーバーで使用しているコードは次のとおりです。

SELECT `tag`, avg(`val`) as `median`
FROM
(
    SELECT
        `tag`,
        `val`,
        (SELECT count(*) FROM `t1` `t2` WHERE `t2`.`tag` = `t3`.`tag`) as `ct`,
        `seq`,
        (SELECT count(*) FROM `t1` `t2` WHERE `t2`.`tag` < `t3`.`tag`) as `delta`

    FROM
        (SELECT `tag`, `val`, @rownum := @rownum + 1 as `seq`
        FROM (SELECT `tag`, `val` FROM `t1` ORDER BY `tag`, `val`) as `t2`
        CROSS JOIN (SELECT @rownum := 0) as `x`
        ORDER BY `tag`, `seq`) as `t3`
    HAVING
        (`ct`%2 = 0 and `seq`-`delta` between floor((`ct`+1)/2) and floor((`ct`+1)/2) +1)
        or (`ct`%2 <> 0 and `seq`-`delta` = (`ct`+1)/2)
) as `t`

GROUP BY `tag`
ORDER BY `tag`;
1
Mark Messa