如何統計中位數和TOP1%貢獻率

  在上篇博文中,我分析了手機上網流量分佈極度失衡的問題,並建議用“中位數”和“TOP1%貢獻率”等指標來避免“被平均”問題。
  但是,這些指標並非直接就能得到,需要一些特別的技術實現,本篇主要闡述我是如何在Oracle中用PL/SQL來實現該項分析功能。
  我從2005年開始應用這些指標進行分析和稽覈,由於當時並無統計中位數的MEDIAN函數(10g版本之後纔有),且TOP1%貢獻率的統計並無現成函數,所以就寫了一個專門的統計程序來完成該項功能,一直沿用至今。
  我的實現思路是:該通用統計包在一個程序裏同時實現中位數、平均值、最大值、TOP1%貢獻率、TOP5%貢獻率等統計功能。基礎數據基於我自己整理的用戶寬表DW_ALL,以NTILE函數爲核心,先將用戶按指標一百等分,統計每類的上限和下限;然後在此中間結果基礎上計算所需的各項指標。
  另外,爲了便於增加新的統計指標,所需統計的指標在參數表中定義,由程序自動解析。由於這個統計程序耗時較長,所以每個指標的統計結果先放入臨時表,然後再一起匯入正式的結果表,以實現統計過程的可並行性。


  以下是關於這個程序的幾點說明:
  1、用NTILE函數進行用戶分羣
  該函數的主要功能是將用戶羣分成同等大小的幾類,比如“NTILE(100) OVER(ORDEER BY 出賬收入 DESC)”的結果就是將用戶按出賬收入從高到低分成100組,每個用戶所處的組的組號。
  在對用戶分組的基礎上,統計好每個組的上限、下限、樣本個數和累計值,就能爲後續的指標計算提供基礎。
  
  2、全省和分地區的指標須單獨統計
  分析時,我們既需要分析全省的情況,也要比較各地區的情況。但是,全省的指標並不能從地區的指標中獲得,必須獨立統計。
  
  3、同時完成多項指標的統計
  目前我同時統計的指標有:個數,總和,最大值,均值,中位數,TOP1佔比,TOP5佔比,TOP10佔比,TOP20佔比,TOP30佔比,TOP40佔比,TOP50佔比,TOP60佔比,TOP70佔比,TOP80佔比,TOP90佔比,TOP1最小值,TOP5最小值,TOP10最小值,TOP20最小值,TOP30最小值,TOP40最小值,TOP50最小值,TOP60最小值,TOP70最小值,TOP80最小值,TOP90最小值。
  
  4、通過參數化實現通用化
  需要統計的指標在ETL_CFG_RATIO中定義,這個指標名稱可以隨便定義,關鍵是公式裏用到的字段必須跟DW_ALL寬表中的字段一致。
  比如,指標名稱可以是“手機上網流量”,而公式卻是“GPRS忙時流量+GPRS閒時流量”。
  參數表中的用戶範圍用來區分是統計全省還是地區,其值分“全省”和“地區”兩類。
  該參數表結構如下:
  CREATE TABLE  ETL_CFG_RATIO(
    序號                  CHAR(2)         NOT NULL,
    指標名稱              VARCHAR2(20)    NOT NULL,
    用戶範圍              VARCHAR2(4)     NOT NULL,
    指標公式              VARCHAR2(100)   ,
    備註                  VARCHAR2(20)    ,
    修改時間              DATE           
    )
  
  
  5、通過臨時表實現並行彙總
  由於該類彙總耗時較長,若有十多個指標的彙總,串行執行可能需要耗費40多個小時。爲了實現併發及減少程序衝突,我這邊將彙總結果先置於臨時表。
  臨時表的結構完全相同,表名從TEMP_RATIO_INDEX01到30,當然也可以根據需要進行擴展。參數表中的序號與表名的末兩位相對應,比如02序號中指標的TEMP_RATIO_INDEX02。
  該類表結構如下:
  CREATE TABLE  TEMP_RATIO_INDEX01(
    月份                  CHAR(6)         NOT NULL,
    指標名稱              VARCHAR2(40)    NOT NULL,
    用戶範圍              VARCHAR2(10)    NOT NULL,
    地區名稱              VARCHAR2(6)     NOT NULL,
    品牌名稱              VARCHAR2(16)    NOT NULL,
    分組序號              NUMBER(4,0)     NOT NULL,
    個數                  NUMBER(10,0)    NOT NULL,
    總和                  NUMBER(18,2)    NOT NULL,
    最小值                NUMBER(12,2)    NOT NULL,
    最大值                NUMBER(12,2)    NOT NULL,
    備註                  VARCHAR2(20)   
    )
  
    6、最終結果彙總到兩張表中
  當分指標的彙總都統計完畢並存儲在臨時表中以後,爲了分析和使用方便,最後我將這些彙總都整到兩張彙總表中。其中,STAT_ALL_USER_RATIO1是對臨時表的簡單合併,而STAT_ALL_USER_RATIO2表則存儲計算好的“中位數”和“TOP1%貢獻率”等指標。
  這兩種表結構如下:
  CREATE TABLE  STAT_ALL_USER_RATIO1(
    月份                  CHAR(6)         NOT NULL,
    指標名稱              VARCHAR2(20)    NOT NULL,
    用戶範圍              VARCHAR2(10)    NOT NULL,
    地區名稱              VARCHAR2(4)     NOT NULL,
    品牌名稱              VARCHAR2(12)    NOT NULL,
    分組序號              NUMBER(4,0)     NOT NULL,
    個數                  NUMBER(10,0)    NOT NULL,
    總和                  NUMBER(18,2)    NOT NULL,
    最小值                NUMBER(12,2)    NOT NULL,
    最大值                NUMBER(12,2)    NOT NULL,
    備註                  VARCHAR2(20)   
    )
  
  CREATE TABLE  STAT_ALL_USER_RATIO2(
    月份                  CHAR(6)         NOT NULL,
    指標名稱              VARCHAR2(20)    NOT NULL,
    用戶範圍              VARCHAR2(4)     NOT NULL,
    地區名稱              VARCHAR2(4)     NOT NULL,
    個數                  NUMBER(10,0)    NOT NULL,
    總和                  NUMBER(18,2)    NOT NULL,
    最大值                NUMBER(12,2)    NOT NULL,
    均值                  NUMBER(12,2)    NOT NULL,
    中位數                NUMBER(12,2)    NOT NULL,
    TOP1佔比              NUMBER(6,4)     NOT NULL,
    TOP5佔比              NUMBER(6,4)     NOT NULL,
    TOP10佔比             NUMBER(6,4)     NOT NULL,
    TOP20佔比             NUMBER(6,4)     NOT NULL,
    TOP30佔比             NUMBER(6,4)     NOT NULL,
    TOP40佔比             NUMBER(6,4)     NOT NULL,
    TOP50佔比             NUMBER(6,4)     NOT NULL,
    TOP60佔比             NUMBER(6,4)     NOT NULL,
    TOP70佔比             NUMBER(6,4)     NOT NULL,
    TOP80佔比             NUMBER(6,4)     NOT NULL,
    TOP90佔比             NUMBER(6,4)     NOT NULL,
    TOP1最小值            NUMBER(12,2)    NOT NULL,
    TOP5最小值            NUMBER(12,2)    NOT NULL,
    TOP10最小值           NUMBER(12,2)    NOT NULL,
    TOP20最小值           NUMBER(12,2)    NOT NULL,
    TOP30最小值           NUMBER(12,2)    NOT NULL,
    TOP40最小值           NUMBER(12,2)    NOT NULL,
    TOP50最小值           NUMBER(12,2)    NOT NULL,
    TOP60最小值           NUMBER(12,2)    NOT NULL,
    TOP70最小值           NUMBER(12,2)    NOT NULL,
    TOP80最小值           NUMBER(12,2)    NOT NULL,
    TOP90最小值           NUMBER(12,2)    NOT NULL,
    備註                  VARCHAR2(20)   
    )
   


  =====================================================================
  相關代碼如下,並不複雜,熟悉PL/SQL開發的人應能讀懂
  ====================================================================
  
  /* =============================================================== *
     GET_RATIO_DTL: 按某個指標統計用戶的100等分分佈
   * =============================================================== */
  PROCEDURE GET_RATIO_DTL(p_月份 CHAR,p_目的表 CHAR,p_指標名稱 CHAR,p_指標公式 CHAR,p_用戶範圍 CHAR DEFAULT '*') IS
    v_stat            VARCHAR2(4000);
    v_SQL             VARCHAR2(4000);
  BEGIN
   v_stat :='
      INSERT INTO #目的表
        SELECT ''#月份'',''#指標名稱'',''#用戶範圍'',地區代碼,DECODE(品牌類型,1,1,4,4,6),分組序號,
               count(1),SUM(指標值),min(指標值),max(指標值),null
        FROM
           (SELECT /*+ PARALLEL(T,5) */
                   NTILE(100) OVER (ORDER BY #指標公式 DESC) 分組序號,
                   地區代碼,品牌類型, #指標公式 指標值
              FROM DW_ALL@DW T
              WHERE 月份 =''#月份'' #條件 AND #指標公式>0
           )
        GROUP BY 分組序號,地區代碼,DECODE(品牌類型,1,1,4,4,6)';
  
    v_stat := REPLACE(v_stat,'#月份',p_月份);
    v_stat := REPLACE(v_stat,'#目的表',p_目的表);
    v_stat := REPLACE(v_stat,'#指標名稱',p_指標名稱);
    v_stat := REPLACE(v_stat,'#指標公式',NVL(p_指標公式,p_指標名稱));
  
    IF p_用戶範圍 IN('全省','*') THEN
      EXECUTE IMMEDIATE REPLACE(REPLACE(v_stat,'#條件',''),'#用戶範圍','全省');
    END IF;
  
    IF p_用戶範圍 IN('地區','*') THEN
      v_SQL := REPLACE(v_stat,'#用戶範圍','地區');
     FOR v_地區代碼 IN 570..580 LOOP
       EXECUTE IMMEDIATE REPLACE(v_SQL,'#條件',' AND 地區代碼='''||v_地區代碼||''' ');
      END LOOP;
    END IF;
  END GET_RATIO_DTL;
  
  
  /* =============================================================== *
     GET_RATIO100:統計用戶100等分分佈
   * =============================================================== */
  PROCEDURE GET_RATIO100(p_月份 CHAR,p_序號 CHAR) IS
    v_指標名稱       ETL_CFG_RATIO.指標名稱%TYPE;
    v_用戶範圍       ETL_CFG_RATIO.用戶範圍%TYPE;
    v_指標公式       ETL_CFG_RATIO.指標公式%TYPE;
  BEGIN
   SELECT  指標名稱,用戶範圍,NVL(指標公式,指標名稱) 指標公式
     INTO v_指標名稱,v_用戶範圍,v_指標公式
      FROM ETL_CFG_RATIO
      WHERE 序號 = p_序號;
  
   GET_RATIO_DTL(p_月份,'TEMP_RATIO_INDEX'||p_序號,v_指標名稱,v_指標公式,v_用戶範圍);
   COMMIT;
  END GET_RATIO100;
  
  
  /* =============================================================== *
     GET_RATIO1:合併用戶分佈彙總
   * =============================================================== */
  PROCEDURE GET_RATIO1(p_月份 CHAR) IS
    v_SQL      VARCHAR2(2000);
  BEGIN
   -- 檢查數據源是否全
   FOR rec IN (SELECT * FROM ETL_CFG_RATIO ORDER BY 序號 DESC) LOOP
     IF ETL_TOOL.IS_EMPTY('TEMP_RATIO_INDEX'||rec.序號,p_月份) THEN
       RETURN;
     END IF; 
    END LOOP;
   
    -- 合併彙總
    v_SQL := 'INSERT INTO STAT_ALL_USER_RATIO1 SELECT * FROM TEMP_RATIO_INDEX#序號';
   FOR rec IN (SELECT * FROM ETL_CFG_RATIO ORDER BY 序號 DESC) LOOP
     EXECUTE IMMEDIATE REPLACE(v_SQL,'#序號',rec.序號);   
    END LOOP;
    COMMIT;
  END GET_RATIO1; 
  
  
  /* =============================================================== *
     GET_RATIO2:統計用戶分佈關鍵指標  
   * =============================================================== */
  PROCEDURE GET_RATIO2(p_月份 CHAR) IS
  BEGIN
   DELETE STAT_ALL_USER_RATIO2 WHERE 月份 = p_月份;
   INSERT INTO STAT_ALL_USER_RATIO2
      SELECT  p_月份,指標名稱,用戶範圍,DECODE(用戶範圍,'地區',地區名稱,'全省') 地區名稱,
              SUM(個數),SUM(總和),MAX(最大值),ROUND(SUM(總和)/SUM(個數),2) 均值,
              ROUND(SUM(DECODE(分組序號,50,總和,0))/SUM(DECODE(分組序號,50,個數,0)),2) 中位數,
              SUM(DECODE(分組序號,1,總和,0))/SUM(總和) TOP1佔比,
              SUM(DECODE(SIGN(6-分組序號),1,總和,0))/SUM(總和) TOP5佔比,
              SUM(DECODE(SIGN(11-分組序號),1,總和,0))/SUM(總和) TOP10佔比,
              SUM(DECODE(SIGN(21-分組序號),1,總和,0))/SUM(總和) TOP20佔比,
              SUM(DECODE(SIGN(31-分組序號),1,總和,0))/SUM(總和) TOP30佔比,
              SUM(DECODE(SIGN(41-分組序號),1,總和,0))/SUM(總和) TOP40佔比,
              SUM(DECODE(SIGN(51-分組序號),1,總和,0))/SUM(總和) TOP50佔比,
              SUM(DECODE(SIGN(61-分組序號),1,總和,0))/SUM(總和) TOP60佔比,
              SUM(DECODE(SIGN(71-分組序號),1,總和,0))/SUM(總和) TOP70佔比,
              SUM(DECODE(SIGN(81-分組序號),1,總和,0))/SUM(總和) TOP80佔比,
              SUM(DECODE(SIGN(91-分組序號),1,總和,0))/SUM(總和) TOP90佔比,
              MIN(DECODE(分組序號,1,最小值,NULL)) TOP1最小值,
              MIN(DECODE(分組序號,6,最小值,NULL)) TOP5最小值,
              MIN(DECODE(分組序號,11,最小值,NULL)) TOP10最小值,
              MIN(DECODE(分組序號,21,最小值,NULL)) TOP20最小值,
              MIN(DECODE(分組序號,31,最小值,NULL)) TOP30最小值,
              MIN(DECODE(分組序號,41,最小值,NULL)) TOP40最小值,
              MIN(DECODE(分組序號,51,最小值,NULL)) TOP50最小值,
              MIN(DECODE(分組序號,61,最小值,NULL)) TOP60最小值,
              MIN(DECODE(分組序號,71,最小值,NULL)) TOP70最小值,
              MIN(DECODE(分組序號,81,最小值,NULL)) TOP80最小值,
              MIN(DECODE(分組序號,91,最小值,NULL)) TOP90最小值,
              NULL
      FROM STAT_ALL_USER_RATIO1
      WHERE 月份 = p_月份
      GROUP BY 指標名稱,用戶範圍,DECODE(用戶範圍,'地區',地區名稱,'全省')
      ORDER BY 指標名稱,用戶範圍,地區名稱;
    COMMIT;
  END GET_RATIO2; 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章