LINQ group items. A single item may be in several groups - c#

I have an IEnumerable of items that I would like to group by associated categories. The items are grouped by the categories that are associated with them - which is a List - so a single item can potentially be a part of multiple categories.
var categories = numbers.SelectMany(x => x.Categories).Distinct();
var query =
from cat in categories
select new {Key = cat,
Values = numbers.Where(n => n.Categories.Contains(cat))};
I use the above code, and it does in fact work, but I was wondering if there was a more efficient way of doing this because this operation will likely perform slowly when numbers contains thousands of values.
I am pretty much asking for a refactoring of the code to be more efficient.

You can use LINQ's built-in grouping capabilities, which should be faster than a contains lookup. However, as with any performance-related question, you should really write code to collect performance metrics before deciding how to rewrite code that you know works. It may turn out that there's no performance problem at all for the volumes you will be working with.
So, here's the code. This isn't tested, but something like it should work:
var result = from n in numbers
from c in n.Categories
select new {Key = c, n.Value}
into x group x by x.Key into g
select g;
Each group contains a key and a sequence of values that belong to that key:
foreach( var group in result )
{
Console.WriteLine( group.Key );
foreach( var value in group )
Console.WriteLine( value );
}

Related

Lambda Function to find most popular word in a List C# [duplicate]

This question already has answers here:
How to Count Duplicates in List with LINQ
(7 answers)
Closed 2 years ago.
I currently have what I believe is a lambda function with C# (fairly new to coding & haven't used a lambda function before so go easy), which adds duplicate strings (From FilteredList) in a list and counts the number of occurrences and stores that value in count. I only want the most used word from the list which I've managed to do by the "groups.OrderBy()... etc) line, however I'm pretty sure that I've made this very complicated for myself and very inefficient. As well as by adding the dictionary and the key value pairs.
var groups =
from s in FilteredList
group s by s into g
// orderby g descending
select new
{
Stuff = g.Key,
Count = g.Count()
};
groups = groups.OrderBy(g => g.Count).Reverse().Take(1);
var dictionary = groups.ToDictionary(g => g.Stuff, g => g.Count);
foreach (KeyValuePair<string, int> kvp in dictionary)
{
Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value);
}
Would someone please either help me through this and explain a little bit of this too me or at least point me in the direction of some learning materials which may help me better understand this.
For extra info: The FilteredList comes from a large piece of external text, read into a List of strings (split by delimiters), minus a list of string stop words.
Also, if this is not a lambda function or I've got any of the info in here incorrect, please kindly correct me so I can fix the question to be more relevant & help me find an answer.
Thanks in advance.
Yes, I think you have overcomplicated it somewhat.. Assuming your list of words is like:
var words = new[] { "what's", "the", "most", "most", "most", "mentioned", "word", "word" };
You can get the most mentioned word with:
words.GroupBy(w => w).OrderByDescending(g => g.Count()).First().Key;
Of course, you'd probably want to assign it to a variable, and presentationally you might want to break it into multiple lines:
var mostFrequentWord = words
.GroupBy(w => w) //make a list of sublists of words, like a dictionary of word:list<word>
.OrderByDescending(g => g.Count()) //order by sublist count descending
.First() //take the first list:sublist
.Key; //take the word
The GroupBy produces a collection of IGroupings, which is like a Dictionary<string, List<string>>. It maps each word (the key of the dictionary) to a list of all the occurrences of that word. In my example data, the IGrouping with the Key of "most" will be mapped to a List<string> of {"most","most","most"} which has the highest count of elements at 3. If we OrderByDescending the grouping based on the Count() of each of the lists then take the First, we'll get the IGrouping with a Key of "most", so all we need to do to retrieve the actual word is pull the Key out
If the word is just one of the properties of a larger object, then you can .GroupBy(o => o.Word). If you want some other property from the IGrouping such as its first or last then you can take that instead of the Key, but bear in mind that the property you end up taking might be different each time unless you enforce ordering of the list inside the grouping
If you want to make this more efficient than you can install MoreLinq and use MaxBy; getting the Max word By the count of the lists means you can avoid a sort operation. You could also avoid LINQ and use a dictionary:
string[] words = new[] { "what", "is", "the", "most", "most", "most", "mentioned", "word", "word" };
var maxK = "";
var maxV = -1;
var d = new Dictionary<string, int>();
foreach(var w in words){
if(!d.ContainsKey(w))
d[w] = 0;
d[w]++;
if(d[w] > maxV){
maxK = w;
maxV = d[w];
}
}
Console.WriteLine(maxK);
This keeps a dictionary that counts words as it goes, and will be more efficient than the LINQ route as it needs only a single pass of the word list, plus the associated dictionary lookups in contrast to "convert wordlist to list of sublists, sort list of sublists by sublist count, take first list item"
This should work:
var mostPopular = groups
.GroupBy(item => new {item.Stuff, item.Count})
.Select(g=> g.OrderByDescending(x=> x.Count).FirstOrDefault())
.ToList();
OrderByDescending along with .First() combines your usage of OrderBy, Reverse() and Take.
First part is a Linq operation to read the groups from the FilteredList.
var groups =
from s in FilteredList
group s by s into g
// orderby g descending
select new
{
Stuff = g.Key,
Count = g.Count()
};
The Lambda usage starts when the => signal is used. Basically means it's going to be computed at run time and an object of that type/format is to be created.
Example on your code:
groups = groups.OrderBy(g => g.Count).Reverse().Take(1);
Reading this, it is going to have an object 'g' that represents the elements on 'groups' with a property 'Count'. Being a list, it allows the 'Reverse' to be applied and the 'Take' to get the first element only.
As for documentation, best to search inside Stack Overflow, please check these links:
C# Lambda expressions: Why should I use them? - StackOverflow
Lambda Expressions in C# - external
Using a Lambda Expression Over a List in C# - external
Second step: if the data is coming from an external source and there are no performance issues, you can leave the code to refactor onwards. A more detail data analysis needs to be made to ensure another algorithm works.

How to access content of the groups shaped by LINQ GroupBy?

I have collection of Customers, potentially group-able by their preferences (oranges, apples)
CustomerID | Preference | Age
1 oranges 35
2 apples 32
... ... ...
100 oranges 48
I need kind of of summary table so can group Customers into new collection like this:
var GroupedCustomers = Customers
.GroupBy (a => new {a.Preference, ...}) //could be grouped by more complex compound key
.Select (a => new { CustomerPreference = a.Key.Prefence, TotalOrders = a.Count () })
How can I access the inner collections of each group with all original properties of their members (e.g. "Age" of each customer)? For example, I need to bind the list of "orange lovers" to a GridView and compute their average age.
The issue in the actual case is a complex compound key and hundreds of groups so I don't want to enumerate them every time from the original collection.
You need to bind the ungrouped collection to the GridView, then you can apply the filters, keeping the View and the ViewModel synchronized
Follow the documentation example under How to: Group, Sort, and Filter Data in the DataGrid Control
Comments
Binding is just subcase. I want to find general approach. Let say I
want to proceed with some math manipulation on the many properties of
the group members (like age). Does you answer mean whatever I apply
GroupBy, all properties that weren't been included to the group key
are lost?
No, they aren't. Add the list of the grouped items to the group, for example
var GroupedCustomers = Customers
.GroupBy (c=> new {a.Preference, ...}, //could be grouped by more complex compound key
c => c,
(key, g) => new {
CustomerPreference = key.Preference,
SubItems = g.ToList(), // <= this is your inner collection
...})
This my own attempt.
We should limit initial statement by GroupBy only to keep collection unchanged:
var GroupedCustomers = Customers
.GroupBy (a => new {a.Preference, ...});
Then, the inner collections would be as simple as:
var GroupByIndex = GroupedCustomers[0]; //retrieve entire group by its index
var GroupdByContent = GroupedCustomers.First(i => i.Key.Preference == "oranges") //retrieve group by key

Combine two lists into one based on property

I would like to ask whether there's an elegant and efficient way to merge two lists of MyClass into one?
MyClass looks like this:
ID: int
Name: string
ExtID: int?
and the lists are populated from different sources and objects in lists do share ID, so it looks like that:
MyClass instance from List1
ID = someInt
Name = someString
ExtID = null
And MyClass instance from List2
ID = someInt (same as List1)
Name = someString (same as List1)
ExtID = someInt
What I basically need is to combine these two lists, so the outcome is a list containing:
ID = someInt (from List1)
Name = someString (from List1)
ExtID = someInt (null if no corresponding item - based on ID - on List2)
I know I can do this simply using foreach loop, but I'd love to know if there's more elegant and maybe preferred (due to performance, readability) method?
There are many approaches depending on what is the priority, ex. Union + Lookup:
//this will create a key value pairs: id -> matching instances
var idMap = list1.Union(list2).ToLookup(myClass => myClass.ID);
//now just select for each ID the instance you want, ex. with some value
var mergedInstances = idMap.Select(row =>
row.FirstOrDefault(myClass => myClass.ExtId.HasValue) ?? row.First());
The benefit of above is that it will work with whatever amount of whatever lists even if they contain many duplicated isntances and then you can easily modify the conditions of merging
A small improvement would be to extract a method to merge instances:
MyClass MergeInstances(IEnumerable<MyClass> instances){
return instances.FirstOrDefault(myClass => myClass.ExtId.HasValue)
?? instances.First(); //or whatever else you imagine
}
and now just use it in the code above
var mergedInstances = idMap.Select(MergeInstances);
Clean, flexible, simple, no additional conditions. Performance wise not perfect, but who cares.
Edit: since performance is the priority, some more options
Do a lookup like above but only for the smaller list. Then iterate through the bigger and do the needed changes O(m log m) + O(n). m - smaller list size, n- bigger list size - should be fastest.
Order both lists by elements ids. Create a for loop, that iterates through both of them keeping current index to the element with same id for both lists. Move index to the next smallest id found in both list, if one has it only, move only this on. O(n log n) + O(m log m) + O(n);
Is this what you want
var joined = from Item1 in list1
join Item2 in list2
on Item1.Id equals Item2.Id // join on some property
select new MyClass(Item1.Id, Item1.Name, Item1.ExtID??Item2.ExtID);
Edit: If you're looking for an outer join,
var query = from Item1 in list1
join Item2 in list2 on Item1.Id equals Item2.Id into gj
from sublist2 in gj.DefaultIfEmpty()
select new MyClass(Item1.Id, Item1.Name, sublist2??string.empty);
Readability wise, using foreach loop is not a too bad idea..
I'd sugest creating the foreach loop in a method of that class, so everytime you needed to do such thing you'd use something like
instanceList1.MergeLists(instanceList2)
and with this method, you could control everything you wanted withing the merge operation.

How way to operate on single items at an index in group in IGrouping?

I have simplified groupby code below, where I know there exists at most two records for each group, with each group being grouped by the value at index two in string a array. I want to iterate through the list of keys in the IGrouping, and combine some values in each Group then add that result to a final list, but I am new to LINQ so don't exactly know how to access these first and/or second values at an index.
Can anyone shed some light on how to do this?
each Group derived from var lines is something like this:
key string[]
---- -------------
123 A, stuff, stuff
123 B, stuff, stuff
and I want the result to be a string[] that combines elements of each group in the "final" list like:
string[]
-------
A, B
my code:
var lines = File.ReadAllLines(#path).Skip(1).Select(r => r.Split('\t')).ToList();
List<string[]> final = new List<string[]>();
var groups = lines.GroupBy(r => r[2]);
foreach (var pairs in groups)
{
// I need to combine items in each group here; maybe a for() statement would be better so I can peek ahead??
foreach (string[] item in pairs)
{
string[] s = new string[] { };
s[0] = item[0];
s[1] = second item in group - not sure what to do here or if I am going aboout this the right way
final.Add(s);
}
}
There's not too much support on the subject either, so I figured it may be helpful to somebody.
It sounds like all you're missing is calling ToList or ToArray on the group:
foreach (var group in groups)
{
List<string[]> pairs = group.ToList();
// Now you can access pairs[0] for the first item in the group,
// pairs[1] for the second item, pairs.Count to check how many
// items there are, or whatever.
}
Or you could avoid creating a list, and call Count(), ElementAt(), ElementAtOrDefault() etc on the group.
Now depending on what you're actually doing in the body of your nested foreach loop (it's not clear, and the code you've given so far won't work because you're trying to assign a value into an empty array) you may be able to get away with:
var final = lines.GroupBy(r => r[2])
.Select(g => ...)
.ToList()
where the ... is "whatever you want to do with a group". If you can possibly do that, it would make the code a lot clearer.
With more information in the question, it looks like you want just:
var final = lines.GroupBy(r => r[2])
.Select(g => g.Select(array => array[0]).ToArray())
.ToList()

Lambda or LINQ for Complex Filter?

I have a need to filter a large collection of objects (in memory) to select only those which meet ALL of the selected categories.
This is essentially the SQL query I'm looking to replicate, but I've been unable to come up with a good C# alternative:
select distinct o.ObjectId
from Object o
join ObjectCategories oc on oc.ObjectId = o.ObjectId
where oc.CategoryId in (1)
and oc.CategoryId in (2)
and oc.CategoryId in (3)
... and so on...
...where 1, 2, and 3 represent the values in an indeterminate number of user-selected categories.
Have your user selected category ID's in a List and then you can use Contains.
select distinct o.ObjectId
from Object o
join ObjectCategories oc on oc.ObjectId = o.ObjectId
where yourCategoryList.Contains(oc=>oc.categoryID);
var results = ObjectCategories.Where(t2 => ObjectList.Any(t1 => t2.Contains(t1)) == true)
you can count the number of matches and if it is equal to the list you are checking against, then you have all the matches.
Consider using Dynamic LINQ. It allows you to use string expressions instead of lambda expressions. You should be able to do what you want using something similar to:
var qry = ObjectCategories.Where(
"ObjectList.CategoryId.Contains(1) AND ObjectList.CategoryId.Contains(2) ..."
);
There is a pretty solid implemention of Dynamic LINQ here: https://github.com/NArnott/System.Linq.Dynamic

Categories