您如何在 Google 表格中创建“反向枢轴”?

Posted

技术标签:

【中文标题】您如何在 Google 表格中创建“反向枢轴”?【英文标题】:How do you create a "reverse pivot" in Google Sheets? 【发布时间】: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)
    
  
;

【问题讨论】:

【参考方案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.***.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];elsepObj[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];elsepObj[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 和设置,因此该公式看起来像一个更通用的解决方案:

【讨论】:

看起来 FLATTEN 功能现在是官方的 :) 有一个支持页面,它出现在公式完成中:support.google.com/docs/answer/10307761

以上是关于您如何在 Google 表格中创建“反向枢轴”?的主要内容,如果未能解决你的问题,请参考以下文章

Zapier 从 Tsheets 数据在 Google 表格中创建多行

如何在Google Bigquery中创建按日期(每年)分区的表格

在As3中创建一个动态表

如何使用 AppScript 在 BigQuery 中将 Google 工作表持久化为表格

如何在 bigquery 中创建分区表

如何在excel中创建表格区域