Có thể xoay dữ liệu bằng LINQ không?


171

Tôi tự hỏi nếu có thể sử dụng LINQ để xoay dữ liệu từ bố cục sau:

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

vào một cái gì đó như thế này:

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

Câu trả lời:


190

Một cái gì đó như thế này?

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

GroupBytrong Linq không hoạt động giống như SQL. Trong SQL, bạn nhận được khóa và tổng hợp (hình dạng hàng / cột). Trong Linq, bạn nhận được khóa và bất kỳ yếu tố nào là con của khóa (hình dạng phân cấp). Để xoay vòng, bạn phải chiếu hệ thống phân cấp trở lại thành một dạng hàng / cột mà bạn chọn.


Danh sách này có phải là một IEnumerable trước khi bạn có thể áp dụng trục không? Hoặc điều này cũng có thể được thực hiện trên IQueryable từ EF (mà không phải cụ thể hóa danh sách trong bộ nhớ)?
Rob Vermeulen

@RobVermeulen Tôi có thể dịch truy vấn đó sang sql, vì vậy tôi mong muốn EF cũng có thể dịch nó. Tôi đoán thử xem sao?
Amy B

Tôi đã thử nó, và nó là loại công việc. Mặc dù SQL Profiler cho thấy rằng EF sẽ không dịch nó thành truy vấn trục (nhanh) mà là một vài truy vấn phụ chậm hơn.
Rob Vermeulen

12

Tôi đã trả lời câu hỏi tương tự bằng phương pháp mở rộng 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

(+) triển khai chung
(-) chắc chắn chậm hơn Amy B's

Bất cứ ai cũng có thể cải thiện việc triển khai của tôi (tức là phương thức thực hiện thứ tự các cột & hàng)?


7

Cách tiếp cận gọn gàng nhất cho điều này, tôi nghĩ, là sử dụng một tra cứu:

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

2

Dưới đây là một cách chung hơn một chút về cách xoay vòng dữ liệu bằng 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();
}

trong đó ValueKey là một lớp đặc biệt đại diện cho khóa đa chiều:

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

Cách tiếp cận này có thể được sử dụng để nhóm theo kích thước N (n> 2) và sẽ hoạt động tốt đối với các bộ dữ liệu khá nhỏ. Đối với các bộ dữ liệu lớn (tối đa 1 triệu bản ghi và hơn thế nữa) hoặc cho các trường hợp khi cấu hình trục không thể được mã hóa, tôi đã viết thư viện PivotData đặc biệt (miễn phí):

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

2

Đây là cách hiệu quả nhất:

Kiểm tra cách tiếp cận sau. Thay vì lặp qua nhóm khách hàng mỗi lần cho mỗi tháng.

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

Hoặc cái này:

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

Giải pháp hoàn chỉnh:

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

-4

Nhóm dữ liệu của bạn theo tháng và sau đó chiếu nó thành dữ liệu mới với các cột cho mỗi tháng. Bảng mới sẽ là bảng trụ của bạn.


Tôi không thể dự tính làm thế nào điều này sẽ hoạt động, nhưng tôi đủ tò mò để yêu cầu bạn bao gồm một số mã ví dụ.
Josh Gallagher
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.