Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pandas => get index of first and last element by group

I have a dataframe with roughly 100M rows, (1.4Gb in memory)

Given the input:

df.head()

Out[1]:
     id    term     x
0     1     A       3
1     1     B       2
2     2     A       1
3     2     B       1
4     2     F       1
5     2     G       1
6     2     Z       1
7     3     K       1
8     3     M       1
9     3     N       1
10    3     Q       1
11    3     R       1
12    3     Z       1
13    4     F       1

I'd like to retrieve the index of the first row for each id. Example:

Out[1]:
     id    first_idx
0     1    0       
1     2    2       
2     3    7      
2     4    13

My current approach is incredibly slow:

first_row = {}
last_id = None
first_row = None

#iterate over all rows
for idx,r in bow.iterrows():
    cid = r['id']
    if cid != last_id: #is this an ID we haven't seen before?
        first_row[cid] = idx
        last_id = cid

Any advice would be a huge help.

like image 883
Alexander David Avatar asked Nov 04 '17 20:11

Alexander David


2 Answers

Use DataFrameGroupBy.agg:

df = df.index.to_series().groupby(df['id']).first().reset_index(name='x')
print (df)
   id   x
0   1   0
1   2   2
2   3   7
3   4  13

If want also last index values:

df = df.index.to_series().groupby(df['id']).agg(['first','last']).reset_index()
print (df)
   id  first  last
0   1      0     1
1   2      2     6
2   3      7    12
3   4     13    13
like image 44
jezrael Avatar answered Sep 28 '22 06:09

jezrael


I. For generic case

Approach #1 With np.unique -

idx = np.unique(df.id.values, return_index=1)[1]

To get the last indices for each ID, simply use flipped version and subtract from dataframe's length -

len(df)-np.unique(df.id.values[::-1], return_index=1)[1]-1

II. For id col already being sorted

Approach #2-A We can use slicing for noticeable performance boost, as we would be avoiding sorting -

a = df.id.values
idx = np.concatenate(([0],np.flatnonzero(a[1:] != a[:-1])+1))

Approach #2-B With masking (better for lots of id numbers)

a = df.id.values
mask = np.concatenate(([True],a[1:] != a[:-1]))
idx = np.flatnonzero(mask)

For the last index :

np.flatnonzero(np.concatenate((a[1:] != a[:-1],[True])))

Approach #3 For sequential numbers, we can use np.bincount -

a = df.id.values
idx = np.bincount(a).cumsum()[:-1]

Sample run -

In [334]: df
Out[334]: 
    id term  x
0    1    A  3
1    1    B  2
2    2    A  1
3    2    B  1
4    2    F  1
5    2    G  1
6    2    Z  1
7    3    K  1
8    3    M  1
9    3    N  1
10   3    Q  1
11   3    R  1
12   3    Z  1
13   4    F  1

In [335]: idx = np.unique(df.id.values, return_index=1)[1]

In [336]: idx
Out[336]: array([ 0,  2,  7, 13])

If you need the output in a dataframe -

In [337]: a = df.id.values

In [338]: pd.DataFrame(np.column_stack((a[idx], idx)), columns=[['id','first_idx']])
Out[338]: 
   id  first_idx
0   1          0
1   2          2
2   3          7
3   4         13
like image 105
Divakar Avatar answered Sep 28 '22 08:09

Divakar