¿Es posible pivotar datos usando LINQ?
Me pregunto si es posible utilizar LINQ para pivotar datos desde el siguiente diseño:
CustID | OrderDate | Qty
1 | 1/1/2008 | 100
2 | 1/2/2008 | 200
1 | 2/2/2008 | 350
2 | 2/28/2008 | 221
1 | 3/12/2008 | 250
2 | 3/15/2008 | 2150
en algo como esto:
CustID | Jan- 2008 | Feb- 2008 | Mar - 2008 |
1 | 100 | 350 | 250
2 | 200 | 221 | 2150
¿Algo como esto?
List<CustData> myList = GetCustData();
var query = myList
.GroupBy(c => c.CustId)
.Select(g => new {
CustId = g.Key,
Jan = g.Where(c => c.OrderDate.Month == 1).Sum(c => c.Qty),
Feb = g.Where(c => c.OrderDate.Month == 2).Sum(c => c.Qty),
March = g.Where(c => c.OrderDate.Month == 3).Sum(c => c.Qty)
});
GroupBy
en Linq no funciona igual que SQL. En SQL, obtienes la clave y los agregados (forma de fila/columna). En Linq, obtienes la clave y cualquier elemento como hijo de la clave (forma jerárquica). Para pivotar, debe proyectar la jerarquía nuevamente en una forma de fila/columna de su elección.
Respondí una pregunta similar usando el método de extensión linq:
// order s(ource) by OrderDate to have proper column ordering
var r = s.Pivot3(e => e.custID, e => e.OrderDate.ToString("MMM-yyyy")
, lst => lst.Sum(e => e.Qty));
// order r(esult) by CustID
(+) implementación genérica
(-) definitivamente más lenta que la de Amy B
¿Alguien puede mejorar mi implementación (es decir, el método ordena columnas y filas)?
Creo que el mejor enfoque para esto es utilizar una búsqueda:
var query =
from c in myList
group c by c.CustId into gcs
let lookup = gcs.ToLookup(y => y.OrderDate.Month, y => y.Qty)
select new
{
CustId = gcs.Key,
Jan = lookup[1].Sum(),
Feb = lookup[2].Sum(),
Mar = lookup[3].Sum(),
};
Aquí hay una forma un poco más genérica de cómo pivotar datos usando LINQ:
IEnumerable<CustData> s;
var groupedData = s.ToLookup(
k => new ValueKey(
k.CustID, // 1st dimension
String.Format("{0}-{1}", k.OrderDate.Month, k.OrderDate.Year // 2nd dimension
) ) );
var rowKeys = groupedData.Select(g => (int)g.Key.DimKeys[0]).Distinct().OrderBy(k=>k);
var columnKeys = groupedData.Select(g => (string)g.Key.DimKeys[1]).Distinct().OrderBy(k=>k);
foreach (var row in rowKeys) {
Console.Write("CustID {0}: ", row);
foreach (var column in columnKeys) {
Console.Write("{0:####} ", groupedData[new ValueKey(row,column)].Sum(r=>r.Qty) );
}
Console.WriteLine();
}
donde ValueKey es una clase especial que representa una clave multidimensional:
public sealed class ValueKey {
public readonly object[] DimKeys;
public ValueKey(params object[] dimKeys) {
DimKeys = dimKeys;
}
public override int GetHashCode() {
if (DimKeys==null) return 0;
int hashCode = DimKeys.Length;
for (int i = 0; i < DimKeys.Length; i++) {
hashCode ^= DimKeys[i].GetHashCode();
}
return hashCode;
}
public override bool Equals(object obj) {
if ( obj==null || !(obj is ValueKey))
return false;
var x = DimKeys;
var y = ((ValueKey)obj).DimKeys;
if (ReferenceEquals(x,y))
return true;
if (x.Length!=y.Length)
return false;
for (int i = 0; i < x.Length; i++) {
if (!x[i].Equals(y[i]))
return false;
}
return true;
}
}
Este enfoque se puede utilizar para agrupar por N dimensiones (n>2) y funcionará bien para conjuntos de datos bastante pequeños. Para conjuntos de datos grandes (hasta 1 millón de registros y más) o para casos en los que la configuración dinámica no se puede codificar, he escrito una biblioteca especial PivotData (es gratuita):
var pvtData = new PivotData(new []{"CustID","OrderDate"}, new SumAggregatorFactory("Qty"));
pvtData.ProcessData(s, (o, f) => {
var custData = (TT)o;
switch (f) {
case "CustID": return custData.CustID;
case "OrderDate":
return String.Format("{0}-{1}", custData.OrderDate.Month, custData.OrderDate.Year);
case "Qty": return custData.Qty;
}
return null;
} );
Console.WriteLine( pvtData[1, "1-2008"].Value );
// LINQPad Code for Amy B answer
void Main()
{
List<CustData> myList = GetCustData();
var query = myList
.GroupBy(c => c.CustId)
.Select(g => new
{
CustId = g.Key,
Jan = g.Where(c => c.OrderDate.Month == 1).Sum(c => c.Qty),
Feb = g.Where(c => c.OrderDate.Month == 2).Sum(c => c.Qty),
March = g.Where(c => c.OrderDate.Month == 3).Sum(c => c.Qty),
//April = g.Where(c => c.OrderDate.Month == 4).Sum(c => c.Qty),
//May = g.Where(c => c.OrderDate.Month == 5).Sum(c => c.Qty),
//June = g.Where(c => c.OrderDate.Month == 6).Sum(c => c.Qty),
//July = g.Where(c => c.OrderDate.Month == 7).Sum(c => c.Qty),
//August = g.Where(c => c.OrderDate.Month == 8).Sum(c => c.Qty),
//September = g.Where(c => c.OrderDate.Month == 9).Sum(c => c.Qty),
//October = g.Where(c => c.OrderDate.Month == 10).Sum(c => c.Qty),
//November = g.Where(c => c.OrderDate.Month == 11).Sum(c => c.Qty),
//December = g.Where(c => c.OrderDate.Month == 12).Sum(c => c.Qty)
});
query.Dump();
}
/// <summary>
/// --------------------------------
/// CustID | OrderDate | Qty
/// --------------------------------
/// 1 | 1 / 1 / 2008 | 100
/// 2 | 1 / 2 / 2008 | 200
/// 1 | 2 / 2 / 2008 | 350
/// 2 | 2 / 28 / 2008 | 221
/// 1 | 3 / 12 / 2008 | 250
/// 2 | 3 / 15 / 2008 | 2150
/// </ summary>
public List<CustData> GetCustData()
{
List<CustData> custData = new List<CustData>
{
new CustData
{
CustId = 1,
OrderDate = new DateTime(2008, 1, 1),
Qty = 100
},
new CustData
{
CustId = 2,
OrderDate = new DateTime(2008, 1, 2),
Qty = 200
},
new CustData
{
CustId = 1,
OrderDate = new DateTime(2008, 2, 2),
Qty = 350
},
new CustData
{
CustId = 2,
OrderDate = new DateTime(2008, 2, 28),
Qty = 221
},
new CustData
{
CustId = 1,
OrderDate = new DateTime(2008, 3, 12),
Qty = 250
},
new CustData
{
CustId = 2,
OrderDate = new DateTime(2008, 3, 15),
Qty = 2150
},
};
return custData;
}
public class CustData
{
public int CustId;
public DateTime OrderDate;
public uint Qty;
}