Ist es möglich, Daten mit LINQ zu schwenken?

171

Ich frage mich, ob es möglich ist, mit LINQ Daten aus dem folgenden Layout zu schwenken:

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

in so etwas:

CustID  | Jan- 2008 | Feb- 2008 | Mar - 2008 |
1       | 100       | 350       |  250
2       | 200       | 221       | 2150
Tim Lentine
quelle

Antworten:

190

Etwas wie das?

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

GroupByin Linq funktioniert nicht wie SQL. In SQL erhalten Sie den Schlüssel und die Aggregate (Zeilen- / Spaltenform). In Linq erhalten Sie den Schlüssel und alle Elemente als untergeordnete Elemente des Schlüssels (hierarchische Form). Zum Schwenken müssen Sie die Hierarchie zurück in eine Zeilen- / Spaltenform Ihrer Wahl projizieren.

Amy B.
quelle
Muss die Liste eine IEnumerable sein, bevor Sie den Pivot anwenden können? Oder kann dies auch auf einem IQueryable von EF durchgeführt werden (ohne dass die Liste im Speicher materialisiert werden muss)?
Rob Vermeulen
@RobVermeulen Ich könnte diese Abfrage in SQL übersetzen, daher würde ich erwarten, dass EF sie auch übersetzen kann. Probieren Sie es aus, denke ich?
Amy B
Ich habe es getestet und es funktioniert irgendwie. Obwohl SQL Profiler zeigt, dass EF es nicht in eine (schnelle) Pivot-Abfrage übersetzt, sondern in einige langsamere Unterabfragen.
Rob Vermeulen
12

Ich habe eine ähnliche Frage mit der Linq-Erweiterungsmethode beantwortet :

// 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

(+) generische Implementierung
(-) definitiv langsamer als die von Amy B.

Kann jemand meine Implementierung verbessern (dh die Methode führt die Reihenfolge der Spalten und Zeilen durch)?

Sanjaya.Tio
quelle
7

Ich denke, der beste Ansatz hierfür ist die Verwendung einer Suche:

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(),
    };
Rätselhaftigkeit
quelle
2

Hier ist eine etwas allgemeinere Methode zum Schwenken von Daten mit 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();
}

Dabei ist ValueKey eine spezielle Klasse, die einen mehrdimensionalen Schlüssel darstellt:

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

Dieser Ansatz kann zur Gruppierung nach N-Dimensionen (n> 2) verwendet werden und funktioniert gut für relativ kleine Datensätze. Für große Datenmengen (bis zu 1 Mio. Datensätze und mehr) oder für Fälle, in denen die Pivot-Konfiguration nicht fest codiert werden kann, habe ich eine spezielle PivotData- Bibliothek geschrieben (kostenlos):

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 );  
Vitaliy Fedorchenko
quelle
2

Dies ist der effizienteste Weg:

Überprüfen Sie den folgenden Ansatz. Anstatt die Kundengruppe jedes Mal für jeden Monat zu durchlaufen.

var query = myList
    .GroupBy(c => c.CustId)
    .Select(g => {
        var results = new CustomerStatistics();
        foreach (var customer in g)
        {
            switch (customer.OrderDate.Month)
            {
                case 1:
                    results.Jan += customer.Qty;
                    break;
                case 2:
                    results.Feb += customer.Qty;
                    break;
                case 3:
                    results.March += customer.Qty;
                    break;
                default:
                    break;
            }
        }
        return  new
        {
            CustId = g.Key,
            results.Jan,
            results.Feb,
            results.March
        };
    });

Oder dieses :

var query = myList
    .GroupBy(c => c.CustId)
    .Select(g => {
        var results = g.Aggregate(new CustomerStatistics(), (result, customer) => result.Accumulate(customer), customerStatistics => customerStatistics.Compute());
        return  new
        {
            CustId = g.Key,
            results.Jan,
            results.Feb,
            results.March
        };
    });

Komplette Lösung:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            IEnumerable<CustData> myList = GetCustData().Take(100);

            var query = myList
                .GroupBy(c => c.CustId)
                .Select(g =>
                {
                    CustomerStatistics results = g.Aggregate(new CustomerStatistics(), (result, customer) => result.Accumulate(customer), customerStatistics => customerStatistics.Compute());
                    return new
                    {
                        CustId = g.Key,
                        results.Jan,
                        results.Feb,
                        results.March
                    };
                });
            Console.ReadKey();
        }

        private static IEnumerable<CustData> GetCustData()
        {
            Random random = new Random();
            int custId = 0;
            while (true)
            {
                custId++;
                yield return new CustData { CustId = custId, OrderDate = new DateTime(2018, random.Next(1, 4), 1), Qty = random.Next(1, 50) };
            }
        }

    }
    public class CustData
    {
        public int CustId { get; set; }
        public DateTime OrderDate { get; set; }
        public int Qty { get; set; }
    }
    public class CustomerStatistics
    {
        public int Jan { get; set; }
        public int Feb { get; set; }
        public int March { get; set; }
        internal CustomerStatistics Accumulate(CustData customer)
        {
            switch (customer.OrderDate.Month)
            {
                case 1:
                    Jan += customer.Qty;
                    break;
                case 2:
                    Feb += customer.Qty;
                    break;
                case 3:
                    March += customer.Qty;
                    break;
                default:
                    break;
            }
            return this;
        }
        public CustomerStatistics Compute()
        {
            return this;
        }
    }
}
Ali Bayat
quelle
-4

Gruppieren Sie Ihre Daten nach Monat und projizieren Sie sie dann in eine neue Datentabelle mit Spalten für jeden Monat. Die neue Tabelle wäre Ihre Pivot-Tabelle.

mattlant
quelle
Ich kann mir nicht vorstellen, wie das funktionieren würde, aber ich bin neugierig genug, Sie zu bitten, einen Beispielcode beizufügen.
Josh Gallagher