【问题标题】:How do you create a "reverse pivot" in Google Sheets?您如何在 Google 表格中创建“反向枢轴”?
【发布时间】:2014-09-17 05:58:10
【问题描述】:

我正在尝试生成“反向枢轴”功能。我已经为这样的功能搜索了很长时间,但找不到已经存在的功能。

我有一个包含多达 20 列和数百行的汇总表,但是我想将其转换为平面列表,以便我可以导入数据库(或者甚至使用平面数据从!)

所以,我有这种格式的数据:

Customer 1 Customer 2 Customer 3
Product 1 1 2 3
Product 2 4 5 6
Product 3 7 8 9

并且需要转换成这种格式:

 Customer  |  Product  | Qty
-----------+-----------+----
Customer 1 | Product 1 |   1
Customer 1 | Product 2 |   4
Customer 1 | Product 3 |   7
Customer 2 | Product 1 |   2
Customer 2 | Product 2 |   5
Customer 2 | Product 3 |   8
Customer 3 | Product 1 |   3
Customer 3 | Product 2 |   6
Customer 3 | Product 3 |   9

我创建了一个函数,该函数将从sheet1 读取范围并将重新格式化的行附加到同一张表的底部,但是我试图让它工作,以便我可以在sheet2 上使用该函数这将从sheet1 读取整个范围。

无论我尝试什么,我似乎都无法让它工作,并且想知道是否有人可以给我任何指示?

这是我目前所拥有的:

function readRows() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var rows = sheet.getDataRange();
  var numRows = rows.getNumRows();
  var values = rows.getValues();

  heads = values[0]
  
  for (var i = 1; i <= numRows - 1; i++) {
    for (var j = 1; j <= values[0].length - 1; j++) {
       var row = [values[i][0], values[0][j], values[i][j]];
       sheet.appendRow(row)
    }
  }
};

【问题讨论】:

    标签: google-apps-script google-sheets pivot-table unpivot


    【解决方案1】:

    这基本上是数组操作...下面是执行您想要的操作并将结果写回现有数据下方的代码。

    如果您愿意,当然可以调整它以在新纸上书写。

    function transformData(){
      var sheet = SpreadsheetApp.getActiveSheet();
      var data = sheet.getDataRange().getValues();//read whole sheet
      var output = [];
      var headers = data.shift();// get headers
      var empty = headers.shift();//remove empty cell on the left
      var products = [];
        for(var d in data){
          var p = data[d].shift();//get product names in first column of each row
          products.push(p);//store
        }
      Logger.log('headers = '+headers);
      Logger.log('products = '+products);
      Logger.log('data only ='+data);
      for(var h in headers){
        for(var p in products){  // iterate with 2 loops (headers and products)
          var row = [];
          row.push(headers[h]);
          row.push(products[p]);
          row.push(data[p][h])
          output.push(row);//collect data in separate rows in output array
        }
      }
      Logger.log('output array = '+output);
      sheet.getRange(sheet.getLastRow()+1,1,output.length,output[0].length).setValues(output);
    }
    

    自动将结果写入新工作表,将最后一行代码替换为:

      var ns = SpreadsheetApp.getActive().getSheets().length+1
      SpreadsheetApp.getActiveSpreadsheet().insertSheet('New Sheet'+ns,ns).getRange(1,1,output.length,output[0].length).setValues(output);
    

    【讨论】:

      【解决方案2】:

      如果您的数据有一个唯一的键列,this spreadsheet 可能有您需要的内容。

      您的未透视表将包含:

      • 关键栏目=OFFSET(data!$A$1,INT((ROW()-2)/5)+1,0)
      • 列标题列=OFFSET(data!$A$1,0,IF(MOD(ROW()-1,5)=0,5,MOD(ROW()-1,5)))
      • 单元格值列=INDEX(data!$A$1:$F$100,MATCH(A2,data!$A$1:$A$100,FALSE),MATCH(B2,data!$A$1:$F$1,FALSE))

      其中5 是要取消透视的列数。


      我没有制作电子表格。我在导致我提出这个问题的同一搜索中偶然发现它。

      【讨论】:

        【解决方案3】:

        我编写了一个简单的通用自定义函数,它是 100% 可重复使用的,您可以对任意大小的表格进行反透视/反向透视。

        在您的情况下,您可以像这样使用它:=unpivot(A1:D4,1,1,"customer","sales")

        因此您可以像使用电子表格中的任何内置数组函数一样使用它。

        请在此处查看 2 个示例: https://docs.google.com/spreadsheets/d/12TBoX2UI_Yu2MA2ZN3p9f-cZsySE4et1slwpgjZbSzw/edit#gid=422214765

        以下为出处:

        /**
         * Unpivot a pivot table of any size.
         *
         * @param {A1:D30} data The pivot table.
         * @param {1} fixColumns Number of columns, after which pivoted values begin. Default 1.
         * @param {1} fixRows Number of rows (1 or 2), after which pivoted values begin. Default 1.
         * @param {"city"} titlePivot The title of horizontal pivot values. Default "column".
         * @param {"distance"[,...]} titleValue The title of pivot table values. Default "value".
         * @return The unpivoted table
         * @customfunction
         */
        function unpivot(data,fixColumns,fixRows,titlePivot,titleValue) {  
          var fixColumns = fixColumns || 1; // how many columns are fixed
          var fixRows = fixRows || 1; // how many rows are fixed
          var titlePivot = titlePivot || 'column';
          var titleValue = titleValue || 'value';
          var ret=[],i,j,row,uniqueCols=1;
        
          // we handle only 2 dimension arrays
          if (!Array.isArray(data) || data.length < fixRows || !Array.isArray(data[0]) || data[0].length < fixColumns)
            throw new Error('no data');
          // we handle max 2 fixed rows
          if (fixRows > 2)
            throw new Error('max 2 fixed rows are allowed');
        
          // fill empty cells in the first row with value set last in previous columns (for 2 fixed rows)
          var tmp = '';
          for (j=0;j<data[0].length;j++)
            if (data[0][j] != '') 
              tmp = data[0][j];
            else
              data[0][j] = tmp;
        
          // for 2 fixed rows calculate unique column number
          if (fixRows == 2)
          {
            uniqueCols = 0;
            tmp = {};
            for (j=fixColumns;j<data[1].length;j++)
              if (typeof tmp[ data[1][j] ] == 'undefined')
              {
                tmp[ data[1][j] ] = 1;
                uniqueCols++;
              }
          }
        
          // return first row: fix column titles + pivoted values column title + values column title(s)
          row = [];
            for (j=0;j<fixColumns;j++) row.push(fixRows == 2 ? data[0][j]||data[1][j] : data[0][j]); // for 2 fixed rows we try to find the title in row 1 and row 2
            for (j=3;j<arguments.length;j++) row.push(arguments[j]);
          ret.push(row);
        
          // processing rows (skipping the fixed columns, then dedicating a new row for each pivoted value)
          for (i=fixRows; i<data.length && data[i].length > 0; i++)
          {
            // skip totally empty or only whitespace containing rows
            if (data[i].join('').replace(/\s+/g,'').length == 0 ) continue;
        
            // unpivot the row
            row = [];
            for (j=0;j<fixColumns && j<data[i].length;j++)
              row.push(data[i][j]);
            for (j=fixColumns;j<data[i].length;j+=uniqueCols)
              ret.push( 
                row.concat([data[0][j]]) // the first row title value
                .concat(data[i].slice(j,j+uniqueCols)) // pivoted values
              );
          }
        
          return ret;
        }
        

        【讨论】:

        • Viktor,在您的带有两行标题的示例中,您仍然会获得一个数据透视表。我想完全取消旋转两行标题。理想情况下,我希望将这些标签视为另外一列的值,而不是看到一列 MIN 和一列 MAX。你的 unpivot 函数可以修改吗?
        • @gciriani 有一个简单的解决方案,使用 unpivot 两次(第二个选项卡 G13 单元格)。您可以像电子表格中的任何其他函数一样堆叠 unpivot 函数:docs.google.com/spreadsheets/d/…
        • titlePivot / titleValue 参数似乎未使用。我错过了什么?
        • 它被使用但通过argument 变量,因此该值的默认回退确实不起作用。然而。 ?
        【解决方案4】:
        =ARRAYFORMULA({"Customer", "Product", "Qty"; 
         QUERY(TRIM(SPLIT(TRANSPOSE(SPLIT(TRANSPOSE(QUERY(TRANSPOSE(QUERY(TRANSPOSE(
         IF(B2:Z<>"", B1:1&"♠"&A2:A&"♠"&B2:Z&"♦", )), , 999^99)), , 999^99)), "♦")), "♠")), 
         "where Col1<>'' order by Col1")})
        

        【讨论】:

        • 不错的解决方案。你打开迭代计算了吗?您的公式应该粘贴到另一张纸上,这样它就可以在没有它的情况下工作,否则您会收到循环引用错误。
        【解决方案5】:

        我认为您没有足够的数组公式答案,所以这里是另一个。

        测试数据(表 1)

        客户公式

        =ArrayFormula(hlookup(int((row(indirect("1:"&Tuples))-1)/Rows)+2,{COLUMN(Sheet1!$1:$1);Sheet1!$1:$1},2))
        

        (使用一些数学方法使其重复并使用 hlookup 在列标题中找到正确的列)

        产品配方

        =ArrayFormula(vlookup(mod(row(indirect("1:"&Tuples))-1,Rows)+2,{row(Sheet1!$A:$A),Sheet1!$A:$A},2))
        

        (使用 mod 和 vlookup 在行标题中查找正确行的类似方法)

        数量公式

        =ArrayFormula(vlookup(mod(row(indirect("1:"&Tuples))-1,Rows)+2,{row(Sheet1!$A:$A),Sheet1!$A:$Z},int((row(indirect("1:"&Tuples))-1)/Rows)+3))
        

        (上述方法的扩展以在二维数组中查找行和列)

        然后将这三个公式组合成一个查询,以过滤掉数量的任何空白值

        =ArrayFormula(query(
           {hlookup(int((row(indirect("1:"&Tuples))-1)/Rows)+2, {COLUMN(Sheet1!$1:$1);Sheet1!$1:$1},2),
            vlookup(mod(row(indirect("1:"&Tuples))-1,Rows)+2,{row(Sheet1!$A:$A),Sheet1!$A:$A},2),
            vlookup(mod(row(indirect("1:"&Tuples))-1,Rows)+2,{row(Sheet1!$A:$A),Sheet1!$A:$Z},int((row(indirect("1:"&Tuples))-1)/Rows)+3)},
        "select * where Col3 is not null"))
        

        注意

        命名范围 Rows 和 Cols 是使用 counta 从数据的第一列和第一行获得的,Tuples 是它们的乘积。单独的公式

        =counta(Sheet1!A:A)
        
        =counta(Sheet1!1:1)
        

        =counta(Sheet1!A:A)*counta(Sheet1!1:1)
        

        如果需要,可以包含在主公式中,但会降低可读性。


        作为参考,这里是适用于当前情况的“标准”拆分/连接解决方​​案(数据限制为 50K):

        =ArrayFormula(split(transpose(split(textjoin("♫",true,transpose(if(Sheet1!B2:Z="","",Sheet1!B1:1&"♪"&Sheet1!A2:A&"♪"&Sheet1!B2:Z))),"♫")),"♪"))
        

        这也相当慢(处理 2401 个数组元素)。如果将计算限制在数据的实际维度上,那么对于小型数据集来说会更快:

        =ArrayFormula(split(transpose(split(textjoin("♫",true,transpose(if(Sheet1!B2:index(Sheet1!B2:Z,counta(Sheet1!A:A),counta(Sheet1!1:1))="","",Sheet1!B1:index(Sheet1!B1:1,counta(Sheet1!1:1))&"♪"&Sheet1!A2:index(Sheet1!A2:A,counta(Sheet1!A:A))&"♪"&Sheet1!B2:index(Sheet1!B2:Z,counta(Sheet1!A:A),counta(Sheet1!1:1))))),"♫")),"♪"))
        

        【讨论】:

        • 事情 1: 这很棒。 事情 2: 你怎么知道Col3 能够识别第三列?某处有一些文档吗?在看到你对这个问题的回答之前,我被 here 阻碍了。
        • 回复@Michael,1。谢谢! 2. 我似乎无法在 Google 的文档中找到对它的引用,但它已经存在了一段时间,您可以使用 Col1 来引用数组的第一列等,例如support.google.com/docs/forum/AAAABuH1jm0wYw_co2pMNQ/…
        【解决方案6】:

        在 V8 引擎上使用简单但功能强大的循环:

        /**
         * Unpivots the given data
         *
         * @return Unpivoted data from array
         * @param {A1:C4} arr 2D Input Array
         * @param {1=} ignoreCols [optional] Number of columns on the left to ignore
         * @customfunction
         */
        const unpivot = (arr, ignoreCols = 1) =>
          ((j, out) => {
            while (++j < arr[0].length)
              ((i) => {
                while (++i < arr.length)
                  out.push([arr[0][j], ...arr[i].slice(0, ignoreCols), arr[i][j]]);
              })(0);
            return out;
          })(ignoreCols - 1, []);
        

        用法:

        =UNPIVOT(A1:C4)
        =UNPIVOT(A1:F4,3)//3 static cols on left
        ={{"Customer","Products","Qty"};UNPIVOT(A1:D4)}//add headers
        

        现场演示:

        /*<ignore>*/console.config({maximize:true,timeStamps:false,autoScroll:false});/*</ignore>*/
        const arr = [
          ['          ', ' Customer 1 ', ' Customer 2 ', ' Customer 3'],
          ['Product 1 ', '          1 ', '          2 ', '          3'],
          ['Product 2 ', '          4 ', '          5 ', '          6'],
          ['Product 3 ', '          7 ', '          8 ', '          9'],
        ];
        console.log("Input table")
        console.table(arr)
        /**
         * Unpivots the given data
         *
         * @return Unpivoted data from array
         * @param {A1:C4} arr 2D Input Array
         * @param {1=} ignoreCols [optional] Number of columns on the left to ignore
         * @customfunction
         */
        const unpivot = (arr, ignoreCols = 1) =>
          ((j, out) => {
            while (++j < arr[0].length)
              ((i) => {
                while (++i < arr.length)
                  out.push([arr[0][j], ...arr[i].slice(0, ignoreCols), arr[i][j]]);
              })(0);
            return out;
          })(ignoreCols - 1, []);
        console.log("Output table")
        console.table(unpivot(arr));
        console.log("Output table with 2 static columns")
        console.table(unpivot(arr,2));
        &lt;!-- https://meta.stackoverflow.com/a/375985/ --&gt;    &lt;script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"&gt;&lt;/script&gt;

        以下所有功能均出于历史原因提供,但已弃用

        在 V8 引擎上使用 ES6-Array.flatMap

        /**
         * Unpivots the given data
         *
         * @return Unpivoted data from array
         * @param {object[][]} arr 2D Input Array
         * @param {object[][]=} headers [optional] Custom headers for output
         * @customfunction
         * @deprecated
         */
        function unpivotold(arr, headers) {
          const custHeader = arr.shift();
          custHeader.shift();
          const out = arr.flatMap(([prod, ...qty]) =>
            qty.map((num, i) => [custHeader[i], prod, num])
          );
          if (headers) out.unshift(headers[0]);
          return out;
        }
        

        用法:

        =UNPIVOTOLD(A1:F4,{A1,"Month","Sales"})
        

        使用array.reducearray.splice 进行数组操作 - 简约方法:

        /**
         * Unpivots the given data
         *
         * @deprecated
         * @return Unpivoted data from array
         * @param {A1:F4} arr 2D Input Array
         * @param {3} numCol Number of static columns on the left
         * @param {A1:C1} headers [optional] Custom headers for output
         * @customfunction
         */
        function unpivotod(arr, numCol, headers) {
          var out = arr.reduce(function(acc, row) {
            var left = row.splice(0, numCol); //static columns on left
            row.forEach(function(col, i) {
              acc.push(left.concat([acc[0][i + numCol], col])); //concat left and unpivoted right and push as new array to accumulator
            });
            return acc;
          }, arr.splice(0, 1));//headers in arr as initial value
          headers ? out.splice(0, 1, headers[0]) : null; //use custom headers, if present.
          return out;
        }
        

        用法:

        =UNPIVOTOD(A1:F4,1,{A1,"Month","Sales"})//Outputs 1 static and 2 unpivoted columns from 1 static and 4+ pivoted columns 
        

        【讨论】:

        • 第一个函数不允许指定左侧的静态列数?
        • @philmcole 现在可以了
        【解决方案7】:

        这里是另一种选择:

        =arrayformula
        (
           { "PRODUCT","CUSTOMER","QTY";
             split 
             ( transpose ( split 
                           ( textjoin("✫" ,false,filter(Sheet2!A2:A,Sheet2!A2:A<>"") & "✤" &
                                      filter(Sheet2!B1:1,Sheet2!B1:1<>""))
                             ,"✫",true,false)),"✤",true,false
             ),
             transpose ( split ( textjoin ( "✤", false, transpose ( filter 
             ( 
               indirect( "Sheet2!B2:"  & MID(address(1,COUNTA( Sheet2!B1:1)+1), 2,
                                             FIND("$",address(1,COUNTA( Sheet2!B1:1)+1),2)-2)
                       )   
               , Sheet2!A2:A<>""
               ))),"✤",true,false)
             )
           }
         )
        

        解释:

        1. "PRODUCT","CUSTOMER","QTY"
           -- Use for giving title
        
        2. split 
           ( transpose ( split 
                       ( textjoin("✫" ,false,filter(Sheet2!A2:A,Sheet2!A2:A<>"") & "✤" &
                                  filter(Sheet2!B1:1,Sheet2!B1:1<>""))
                       ,"✫",true,false)),"✤",true,false
           )
           -- Use for distributing Row1 and ColumnA, to be Product and Customer Columns
        
        3. transpose ( split ( textjoin ( "✤", false, transpose ( filter 
           ( 
             indirect( "Sheet2!B2:"  & MID(address(1,COUNTA( Sheet2!B1:1)+1), 2,
                                         FIND("$",address(1,COUNTA( Sheet2!B1:1)+1),2)-2)
                     )   
             , Sheet2!A2:A<>""
             ))),"✤",true,false)
           )
           --use to distributed data qty to Qty Column
        

        Sheet2 图片:

        结果表图片:

        【讨论】:

          【解决方案8】:

          输入表

          此函数将处理许多客户和许多产品,它会将多个客户/产品条目的数量相加,并将其汇总到一个简单的表格中。

          代码:

          function rPVT() {
            var ss=SpreadsheetApp.getActive();
            var sh=ss.getSheetByName('Sheet1');
            var osh=ss.getSheetByName('Sheet2');
            osh.clearContents();
            var vA=sh.getDataRange().getValues();
            var itoh={};
            var pObj={};
            vA[0].forEach(function(h,i){if(h){itoh[i]=h;}});
            for(var i=1;i<vA.length;i++) {
              for(var j=1;j<vA[i].length;j++) {
                if(!pObj.hasOwnProperty(itoh[j])){pObj[itoh[j]]={};}
                if(!pObj[itoh[j]].hasOwnProperty(vA[i][0])){pObj[itoh[j]][vA[i][0]]=vA[i][j];}else{pObj[itoh[j]][vA[i][0]]+=(vA[i][j]);}
              }
            }
            var oA=[['Customer','Product','Quantity']];  
            Object.keys(pObj).forEach(function(ik){Object.keys(pObj[ik]).forEach(function(jk){oA.push([ik,jk,pObj[ik][jk]]);});});
            osh.getRange(1,1,oA.length,oA[0].length).setValues(oA);
          }
          

          输出表:

          下面的函数读取上面函数的输出Sheet2并将其返回为原始格式。

          function PVT() {
            var ss=SpreadsheetApp.getActive();
            var sh2=ss.getSheetByName('Sheet2');
            var sh3=ss.getSheetByName('Sheet3');
            sh3.clearContents();
            var vA=sh2.getRange(2,1,sh2.getLastRow()-1,sh2.getLastColumn()).getValues();
            pObj={};
            vA.forEach(function(r,i){if(!pObj.hasOwnProperty(r[1])){pObj[r[1]]={};}if(!pObj[r[1]].hasOwnProperty(r[0])){pObj[r[1]][r[0]]=r[2];}else{pObj[r[1]][r[0]]+=r[2];}});
            var oA=[];
            var ikeys=Object.keys(pObj);
            var jkeys=Object.keys(pObj[ikeys[0]]);
            var hkeys=jkeys.slice();
            hkeys.unshift(''); 
            oA.push(hkeys);
            ikeys.forEach(function(ik,i){var row=[];row.push(ik);jkeys.forEach(function(jk,j){row.push(pObj[ik][jk]);});oA.push(row);});
            sh3.getRange(1,1,oA.length,oA[0].length).setValues(oA);
          }
          

          【讨论】:

            【解决方案9】:

            使用展平。它将任何数组转换为单列。

            这是反透视的公式:

            =ARRAYFORMULA(SPLIT(FLATTEN(A2:A12&amp;"?"&amp;B1:F1&amp;"?"&amp;B2:F12),"?"))

            FLATTEN 创建由 Item1?Date1?67455 字符串组成的 1 列数组,然后我们将其拆分。

            copy the sample file尝试。

            更短:

            =index(SPLIT(FLATTEN(A2:A12&amp;"?"&amp;B1:F1&amp;"?"&amp;B2:F12),"?"))


            另请参阅this solution

            它使用 INDIRECT 和设置,因此该公式看起来像一个更通用的解决方案:

            【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-10-16
            • 1970-01-01
            • 2022-08-04
            • 1970-01-01
            • 2020-07-29
            • 2016-11-07
            • 2015-10-20
            相关资源
            最近更新 更多