Sentencias en LINQ (V): Funciones de agregación


Agregación, agrupación… son dos conceptos que parecen iguales pero que corresponden a características distintas dentro del álgebra relacional.

La agrupación, tal y como vimos en artículos anteriores, consiste en generar un conjunto de datos que poseen una característica común. Por ejemplo, agrupar los pedidos por cliente implicaría la obtención de varios subconjuntos (un subconjunto por cliente) compuestos por un dato que identifique unívocamente al cliente y sus pedidos asociados. Para realizar esta operación hacíamos uso de la sentencia group by.

La agregación está relacionada con la agrupación, ya que en lugar de proyectar un conjunto de datos asociados a otro, realiza una operación aritmética sobre uno o varios de estos datos. Por ejemplo, en lugar de obtener toda la información de los pedidos asociados a un cliente, una operación de agregación consistiría en obtener, por ejemplo el número de pedidos asociados a un cliente. Como podremos imaginar, todo cálculo de agregación debe tener asociada, por definición, una operación de agregación.

Las operaciones de agregación, por lo tanto, son de carácter matemático, y se suelen corresponder con las siguientes cinco operaciones:

  • Cuenta (Count): devuelve el número de registros pertenecientes a la agrupación.
  • Sumatorio (Sum): devuelve la suma de todos los valores de un campo numérico concreto perteneciente a la agrupación.
  • Máximo (Max): devuelve el máximo de los valores de un campo numérico concreto perteneciente a la agrupación.
  • Mínimo (Min): devuelve el mínimo de los valores de un campo numérico concreto perteneciente a la agrupación.
  • Media (Average): devuelve la media aritmética de un campo numérico concreto perteneciente a la agrupación.

Existen muchas otras posibles operaciones de agrupación, generalmente de carácter estadístico, como la varianza o desviación típica. Sin embargo, las cinco operaciones anteriores son las más extendidas en cualquier modelo de datos relacional, y por lo tanto, las más utilizadas.

Count

Devuelve el número de registros de la agrupación. Por ejemplo, el número de líneas de pedido pertenecientes a un pedido.


            var consulta = from lineaPedido in DataLists.ListaLineasPedido
                           group lineaPedido by lineaPedido.IdPedido into grupo
                           select new
                           {
                               IdPedido = grupo.Key,
                               NumPedidos = grupo.Count()
                           };

Distinct

Es posible restringir la operación count para que únicamente tenga en consideración los elementos únicos dentro de un conjunto de datos. De este modo, evitaremos contar elementos iguales dos veces. Por ejemplo, un cliente puede haber realizado más de un pedido, pero no todos los clientes tienen por qué haber realizado algún pedido.

Con la siguiente sentencia obtendremos los clientes que han realizado al menos un pedido, ya que si un mismo identificador de cliente aparece más de una vez, éste no será tenido en cuenta para el cómputo, devolviendo únicamente los registros distintos. Así, el siguiente código efectuará un cómputo de todos los elementos presentes en el listado:


            var consulta = from pedido in DataLists.ListaPedidos
                           select pedido.IdCliente;

            Console.WriteLine(string.Format("Existe un total de {0} pedidos realizados por clientes.",
                consulta.Count()));

Sin embargo, si anteponemos el método Distinct antes de ejecutar el método Count, filtraremos el resultado ignorando aquellos valores repetidos:


            var consulta = from pedido in DataLists.ListaPedidos
                           select pedido.IdCliente;

            Console.WriteLine(string.Format("Existe un total de {0} clientes distintos que han realizado pedidos.",
                consulta.Distinct().Count()));

No obstante, este caso es sencillo: se trata de un listado de números sobre los que es muy sencillo realizar una comparación. Pero, ¿qué ocurriría si el listado fuese de pedidos completos? Hagamos la prueba:


            var consulta = from pedido in DataLists.ListaPedidos
                           select pedido;

            Console.WriteLine(string.Format("Existe un total de {0} clientes distintos que han realizado pedidos.",
                consulta.Distinct().Count()));

¡¡Error!! Hemos realizado un cómputo de todos los objetos distintos dentro del listado. Y aunque existan pedidos que pertenezcan a un mismo cliente, lo que realiza el método Distinct es ignorar aquellos elementos que sean completamente iguales, por lo que al no haber dos pedidos iguales, devolverá su totalidad.

¿Cómo haremos entonces para recuperar únicamente aquellos cuyo IdCliente sea distinto? Pues haciendo uso de algo que vimos brevemente en el artículo anterior: las expresiones lambda.

Los objetos que implementan las interfaces IEnumerable e IQueryable exponen un método Select() que permite realizar proyecciones tal y como realizamos de forma nativa en LINQ. Así, los siguientes métodos serían equivalentes:


            var consultaLinq = from pedido in DataLists.ListaPedidos
                           select pedido.IdCliente;

            var consultaLambda = DataLists.ListaPedidos.Select(pedido => pedido.IdCliente);

El método Select puede recibir como argumento un delegado a una función que se encargue de realizar el filtrado. Por lo tanto, si le pasamos una expresión lambda con el campo que queremos utilizar como filtro, restringiremos automáticamente los campos que recuperará la invocación de este método.


            var consulta = from pedido in DataLists.ListaPedidos
                           select pedido;

            Console.WriteLine(string.Format("Existe un total de {0} clientes distintos que han realizado pedidos.",
                consulta.Select(elemento => elemento.IdCliente).Distinct().Count()));

Si queremos ir más allá, podríamos combinar una sentencia LINQ con una expresión lambda y hacer uso del método First o FirstOrDefault para recuperar únicamente el primer elemento de cada grupo, obteniendo así el primer pedido realizado por cada cliente. Así, su conteo simbolizaría el número de clientes que han realizado pedidos.


            var consulta = (from pedido in DataLists.ListaPedidos
                             select pedido).GroupBy(ped => ped.IdCliente).Select(p => p.FirstOrDefault());

Sum

La siguiente función de agregación será Sum, que devuelve una suma de todos los valores de un campo concreto perteneciente a la lista. Así, la siguiente operación devolvería la suma del beneficio obtenido por todas las ventas:


            // Recuperamos una lista de registros compuesto por un elemento float
            // que contiene el producto de la cantidad por el precio.
            var consulta = (from lineaPedido in DataLists.ListaLineasPedido
                            join producto in DataLists.ListaProductos
                             on lineaPedido.IdProducto equals producto.Id
                            select lineaPedido.Cantidad * producto.Precio);

            // Usamos la función de agregación Sum para calcular la suma de todos los elementos
            float resultadoTotal = consulta.Sum();

            // Mostramos los elementos de la consulta
            int i = 1;
            foreach (var valor in consulta)
            {
                Console.WriteLine(string.Format("Cantidad obtenida en la línea de venta {0}: {1}",
                    i++, valor));
            }

            // Mostramos el total
            Console.WriteLine(string.Format("Total ingresos: {0}",
                resultadoTotal));

Esto nos mostrará la siguiente información:

Sin embargo, la solución no es del todo elegante: un listado de elementos float proporciona muy poca información. Podemos refinar un poco los datos haciendo que la consulta devuelva un tipo anónimo con un poco más de información y añadiendo una expresión lambda al método Sum que le indique cuál de los campos debe sumar.


            // Recuperamos una lista de registros anónimos compuestos por cuatro campos
            // que identifican nombre, precio, cantidad y precio total.
            var consulta = (from lineaPedido in DataLists.ListaLineasPedido
                            join producto in DataLists.ListaProductos
                             on lineaPedido.IdProducto equals producto.Id
                            select new
                            {
                                NombreProducto = producto.Descripcion,
                                PrecioUnitario = producto.Precio,
                                Unidades = lineaPedido.Cantidad,
                                PrecioTotal = lineaPedido.Cantidad * producto.Precio
                            });

            // Usamos la función de agregación Sum para calcular la suma del campo "PrecioTotal"
            // de todos los elementos de tipo anónimo devueltos por la consulta.
            float resultadoTotal = consulta.Sum(elemento => elemento.PrecioTotal);

            // Mostramos los elementos de la consulta
            foreach (var elemento in consulta)
            {
                Console.WriteLine(string.Format("Producto {0}: {1} EUR x {2} = {3}",
                    elemento.NombreProducto, elemento.PrecioUnitario, elemento.Unidades, elemento.PrecioTotal));
            }

            // Mostramos el total
            Console.WriteLine(string.Format("Total ingresos: {0}",
                resultadoTotal));

Como observamos, el resultado proporciona un poco más de información:

Por supuesto, es posible utilizar este método combinándolo con otras operaciones, como por ejemplo una agrupación.


            // Recuperamos una lista de registros anónimos compuestos por cuatro campos
            // que identifican nombre, precio, cantidad y precio total.
            var consulta = (from lineaPedido in DataLists.ListaLineasPedido
                            join producto in DataLists.ListaProductos
                             on lineaPedido.IdProducto equals producto.Id
                            group lineaPedido by new
                            {
                                IdPedido = lineaPedido.IdPedido,
                                NombreProducto = producto.Descripcion,
                                PrecioUnitario = producto.Precio,
                                Unidades = lineaPedido.Cantidad
                            } into grupoPedido
                            select new
                            {
                                NombreProducto = grupoPedido.Key.NombreProducto,
                                PrecioProducto = grupoPedido.Key.PrecioUnitario,
                                Unidades = grupoPedido.Key.Unidades,
                                CantidadElementos = grupoPedido.Select(linea => linea.Cantidad),
                                PrecioLinea = grupoPedido.Sum(linea => linea.Cantidad) * grupoPedido.Key.PrecioUnitario
                            });

            // Usamos la función de agregación Sum para calcular la suma del campo "PrecioTotal"
            // de todos los elementos de tipo anónimo devueltos por la consulta.
            float resultadoTotal = consulta.Sum(elemento => elemento.PrecioLinea);

            // Mostramos los elementos de la consulta
            foreach (var elemento in consulta)
            {
                Console.WriteLine(string.Format("Producto {0}: {1} EUR x {2} = {3}",
                    elemento.NombreProducto, elemento.PrecioProducto, elemento.Unidades, elemento.PrecioLinea));
            }

El resultado, como vemos, sera el mismo que en el caso anterior.

Max

Devuelve el máximo valor de un campo concreto dentro de un listado. Así, en el código anterior podríamos obtener el valor máximo de una venta aplicándole la siguiente función a la consulta:


            float valorMaximo = consulta.Max(elemento => elemento.PrecioLinea);

            Console.WriteLine(string.Format("La mayor venta ha sido de {0} EUR",
                valorMaximo));

El resultado será el registro cuyo campo PrecioLinea sea mayor, en este caso, un pedido de grapadoras.

Min

Devuelve el mínimo valor de un campo concreto dentro de un listado. Similar al caso anterior.


            float valorMinimo = consulta.Min(elemento => elemento.PrecioLinea);

            Console.WriteLine(string.Format("La menor venta ha sido de {0} EUR",
                valorMinimo));

El resultado será el registro cuyo campo PrecioLinea sea menor, en este caso, un pedido de grapadoras.

Average

Devuelve el valor medio de un campo concreto de un listado. Nuevamente, similar a los casos anteriores.


            float valorMedio = consulta.Average(elemento => (float)elemento.PrecioLinea);

            Console.WriteLine(string.Format("El valor medio de venta ha sido de {0} EUR",
                valorMedio));

Y con esto concluimos la introducción a las funciones de agregación en LINQ.

One comment

  1. Hola a todos, soy nuevo en esto, tengo una necesidad puntual con un código y no hallo en camino en C#
    Les cuento si algo, tengo que importar un archivo de excel, ese archivo de excel es dinámico, crece en número de filas como es normal, pero tambien crecer en numero de columnas, entonces no puedo quemar una opción fija de recorrerlo, lo que estoy tratando de hacer es que después que leeo el archivo y lo recorro, tengo que validar con un dato si existe en la DB, pero eso no sería el problema, la dificultad es crear una datatable dinamica en C# para que cresca tantas filas trae el excel como tantas columnas trae, no sé si me hice entender, alguien puede ayudarme
    lo complejo es que crece el numero de columnas

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s