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.
1 2 3 4 5 6 7 |
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:
1 2 3 4 5 |
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:
1 2 3 4 5 |
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:
1 2 3 4 5 |
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:
1 2 3 4 |
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.
1 2 3 4 5 |
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.
1 2 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 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:
1 2 3 4 |
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.
1 2 3 4 |
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.
1 2 3 4 |
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.