如何在Google Sheets中创建“反向枢轴”?

39

我正在尝试创建一个“逆数据透视表”函数。我已经搜索了很长时间,但找不到现成的函数。

我有一个摘要表格,其中包含多达20列和数百行数据,然而我想将其转换为一个扁平列表,以便可以导入到数据库(或者使用扁平数据创建更多的数据透视表!)

因此,我的数据格式如下:

客户1客户2客户3
产品1123
产品2456
产品3789

需要将其转换为以下格式:

 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 Sheets 中取消旋转具有多个列组的数据? - doubleunary
10个回答

36

我写了一个简单的通用自定义函数,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,在你的例子中有两个标题行,但你仍然得到了一个数据透视表。我想要完全取消这两个标题行。理想情况下,我希望不只是看到最小值和最大值各自为一列,而是将它们作为一个附加列的值来显示这些标签。你的unpivot函数能否被修改? - gciriani
@gciriani,有一个简单的解决方案,使用两次unpivot(第二个选项卡G13单元格)。您可以像电子表格中的任何其他函数一样堆叠unpivot函数:https://docs.google.com/spreadsheets/d/12TBoX2UI_Yu2MA2ZN3p9f-cZsySE4et1slwpgjZbSzw/edit#gid=422214765 - Viktor Tabori
标题Pivot / 标题Value参数似乎没有使用。我错过了什么吗? - Nickolay
它是通过argument变量使用的,因此默认值回退确实没有起作用。但是。 - Viktor Tabori

11

随着新的LAMBDAMAKEARRAY函数的出现,我们可以在不进行字符串操作的情况下对数据进行解构。这通过为新数组创建适当的索引号序列来实现,应该比字符串操作更快。

=ARRAYFORMULA(LAMBDA(range,s_cols,
  QUERY(
    MAKEARRAY(ROWS(range)*(COLUMNS(range)-s_cols),s_cols+1,
      LAMBDA(i,j,
        TO_TEXT(
          INDEX(range,
            ROUNDDOWN(1+(i-1)/(COLUMNS(range)-s_cols)),
            if(j>s_cols,MOD(i-1,COLUMNS(range)-s_cols)+s_cols+1,j)
          )
        )
      )
    ),"where Col"&s_cols+1&" is not null"
  )
)(A1:C10,2))

或者作为一个命名函数(UNPIVOT(范围,列)):

=ARRAYFORMULA(
  QUERY(
    MAKEARRAY(ROWS(range)*(COLUMNS(range)-s_cols),s_cols+1,
      LAMBDA(i,j,
        TO_TEXT(
          INDEX(range,
            ROUNDDOWN(1+(i-1)/(COLUMNS(range)-s_cols)),
            if(j>s_cols,MOD(i-1,COLUMNS(range)-s_cols)+s_cols+1,j)
          )
        )
      )
    ),"where Col"&s_cols+1&" is not null"
  )
)
参数:
  • range: 需要展开的范围。例如:A1:C10
  • s_cols: 左侧静态列的数量。例如:2

在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

Live demo:

/*<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));
<!-- https://meta.stackoverflow.com/a/375985/ -->    <script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script>


请查阅历史记录以获取更早版本的已弃用函数


11

那基本上是数组操作...以下是一个可以实现你想要的功能,将结果写回到现有数据下面的代码。

当然,如果你愿意,你可以把它改编成在新工作表上写入。

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);

8
使用FLATTEN函数。它可以将任何数组转换为单列。
这是反转行列的公式:
=ARRAYFORMULA(SPLIT(FLATTEN(A2:A12&""&B1:F1&""&B2:F12),""))

enter image description here

FLATTEN创建了一个由Item1Date167455字符串组成的1列数组,然后我们对其进行拆分。
复制示例文件以尝试。
简化版: =index(SPLIT(FLATTEN(A2:A12&""&B1:F1&""&B2:F12),""))
请参见this solution
它使用了INDIRECT和设置,因此公式看起来更像是一个更普遍的解决方案:enter image description here

3
FLATTEN函数现在似乎已经官方认可了 :) 有一个支持页面,而且在公式自动完成中也出现了:https://support.google.com/docs/answer/10307761 - Nimantha
2
它能够与任何表情符号一起使用吗? - Osm

7

我觉得你还没有足够的数组公式答案,所以这里再给你一个。

测试数据(表1)

enter image description here

客户公式

=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 能够识别第三列?有没有相关文档资料?我在这里陷入困境(https://dev59.com/qmMl5IYBdhLWcg3wgnOl#XxMmoYgBc1ULPQZFSVx6),直到看到你对这个问题的回答。 - Michael
1
回答 @Michael,1. 谢谢!2. 我在Google的文档中似乎找不到相关参考,但是已经有一段时间可以使用Col1来引用数组的第一列等,例如 https://support.google.com/docs/forum/AAAABuH1jm0wYw_co2pMNQ/?hl=en&msgid=qYX2_e85mbIJ&gpf=d/msg/docs/wYw_co2pMNQ/qYX2_e85mbIJ - Tom Sharpe

2
=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")})

0


1
不错的解决方案。你是否开启了迭代计算?你的公式应该粘贴到另一个工作表上,以便在没有它的情况下正常工作,否则你会得到循环引用错误。 - TheMaster

2
这里有另一种选择:
=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图片:

输入图像描述

结果表格图片:

输入图像描述


2

输入表格

enter image description here

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

代码:

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);
}

输出表格:

enter image description here

以下函数读取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);
}

1

一个范围引用

无论客户和产品数量如何,这都可以工作。在这种情况下,使用一个范围引用,即(A1:D4)

=ArrayFormula({SPLIT("Customer|Product|Qty","|");
 QUERY(LAMBDA(r,SPLIT(FLATTEN(
 QUERY({r}, " Select Col1 ", 1)&"+"&
 QUERY({r}, " select "& TEXTJOIN(",",1,REGEXREPLACE("Col#", "#", SEQUENCE(COLUMNS(QUERY(r, " select * limit 0 ", 1))-1,1,2,1)&""))&" limit 0 ", 1)&"+"&
 QUERY({QUERY({r}, " Select "& TEXTJOIN(",",1,REGEXREPLACE("Col#", "#", SEQUENCE(COLUMNS(QUERY({r}, " select * where Col1 <> '' ", 1))-1,1,2,1)&""))&" ", 0)},
 " Select * where Col1 is not null ")),"+"))(A1:D4)," Select * Where Col2 <> '' ")})

enter image description here

演示

当您需要将此表作为另一个公式的输出“放在左侧”时,这个方法很有效。
在这种情况下,使用范围A1:G15进行模拟。

enter image description here

"20列数百行"

enter image description here

命名函数

待定...

使用的公式有
ARRAYFORMULA - SPLIT - QUERY - LAMBDA - FLATTEN - TEXTJOIN
REGEXREPLACE - SEQUENCE - COLUMNS - NOT


1
如果你的数据只有一个唯一键列,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 是要反转的列数。

我并没有制作这个电子表格,我是在搜索与这个问题相关的内容时偶然发现它的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接