Juan Castaño
Juan Castaño

Reputation: 67

Python- group together instances of a class by attribute

I wanted to group together instances of a class based on the value of an attribute. Suppose I've got the following class:

class location:

    def __init__(self,x_coord,y_coord,text):
        self.x_coord=x_coord
        self.y_coord=y_coord
        self.text=text

    def __repr___(self):
        return self.text

mylist=[location(1,0,'Date'),location(5,0,'of'),location(8,0,'Entry'), location(28,0,'Date'),location(29,0,'of'),location(30,0,'Birth') ]

I want to group my list of classes if difference in the x_coord attribute is less than 10, so that

mygroupedlist=[['Date','of','Entry'],['Date','of','Birth']]

Can someone give me a hint?

Upvotes: 2

Views: 4794

Answers (4)

jpp
jpp

Reputation: 164673

You can use a defaultdict of lists and iterate your list of objects, increasing your key each time a difference is greater than or equal to 10.

The solution assumes your x_coord attributes are increasing, i.e. sorted in ascending fashion.

from collections import defaultdict

d = defaultdict(list)

d[0].append(mylist[0])

for item in mylist[1:]:
    last_key = len(d) - 1
    if item.x_coord - next(reversed(d[last_key])).x_coord < 10:
        d[last_key].append(item)
    else:
        d[last_key+1].append(item)

Test to check the ordering is correct:

res = [[i.x_coord for i in x] for x in d.values()]

print(res)

[[1, 5, 8], [28, 29, 30]]

Upvotes: 0

PMende
PMende

Reputation: 5460

If you don't mind using external libraries, you can get probably get better performance by using numpy and pandas.

# Create a dataframe
df = pd.DataFrame(mylist, columns=['locations'])
# Create columns representing the 'x' coords, and the 'text'
df['x'] = df['locations'].apply(lambda x: x.x_coord)
df['text'] = df['locations'].apply(lambda x: x.text)
# Create an indicator array that tells you whether the current row is within 10 of the previous row
closeness_indicator = np.isclose(df['x'], df['x'].shift(1), atol=10)
# Negate that, then take the cumulative sum to get groups:
groups = (~closeness_indicator).cumsum()
# GRoup by that array, then create lists from the grouped text:
df.groupby(groups)[text].apply(list)

Output:

1    [Date, of, Entry]
2    [Date, of, Birth]
Name: text, dtype: object

Upvotes: 1

Andrej Kesely
Andrej Kesely

Reputation: 195438

My attempt, using counter that is increased every time when there's change greater or equal than distance. That way this generator can be supplied with ease to groupby:

def gen(lst, distance=10):
    counter = 0
    for cur, nxt in zip(lst[::1], lst[1::1]):
        yield counter, cur
        if abs(cur.x_coord - nxt.x_coord) >= distance:
            counter += 1
    yield counter, nxt

myGroupedList = [list(i[1] for i in g) for _, g in groupby(gen(mylist), lambda v: v[0])]
print(myGroupedList)

Prints:

[[Date, of, Entry], [Date, of, Birth]]

Upvotes: 0

Patrick Haugh
Patrick Haugh

Reputation: 60974

Here's a solution that uses a stateful function to remember the last item it saw. (Don't show this to any functional programmers). We can then use that function as our key function in a call to itertools.groupby

def grouper(key=lambda x: x, distance=10):
    _marker = object()
    last_seen = _marker
    flag = True
    def close_enough(item):
        nonlocal last_seen, flag
        if last_seen is _marker:
            last_seen = key(item)
            return flag
        diff = abs(key(item) - last_seen)
        last_seen = key(item)
        if diff >= distance:
            flag = not flag
        return flag
    return close_enough

[[i.text for i in g] for k, g in groupby(mylist, key=grouper(lambda x: x.x_coord))]
# [['Date', 'of', 'Entry'], ['Date', 'of', 'Birth']]

Upvotes: 0

Related Questions