Yes, iterating through a dataframe usually is at odds with the spirit of dataframes and numpy arrays. They are best suited for vectorized operations, which are operations applied in bulk to rows/columns of the data structure.
Assuming that you've cast templates
as an int64
or similar, you can use pd.groupby
to do the same sort of grouping as you are doing with your dictionary:
import pandas as pd
df
amino_acid templates
0 CAWSVGQYSNQPQHF 118
1 CASSLRGNQPQHF 635
2 CASSHGTAYEQYF 468
3 CASSLDRLSSGEQYF 239
4 CSVEDGPRGTQYF 51
5 CASSLDRLSSGEQYF 66 # I've added this extra row here to show the effect
# these act as Series objects, so you can add together the
# grouped templates values
df.groupby('amino_acid')
# pd.groupby object
# use pd.Series.sum() to do this:
df.groupby('amino_acid').sum()
templates
amino_acid
CASSHGTAYEQYF 468
CASSLDRLSSGEQYF 305 # this was added for the two amino acids
CASSLRGNQPQHF 635
CAWSVGQYSNQPQHF 118
CSVEDGPRGTQYF 51
So what's going on in df.groupby
? Well, you give it what to group on first. In this case, you group on the value for amino_acid
. This creates a data structure that looks quite familiar if you've used itertools.groupby
: tuples of (grouping key, DataFrame)
pairs, where the DataFrame
is a subset that matches the key. For example:
tmp = [(x, y) for x,y in df.groupby('amino_acid')]
[('CASSHGTAYEQYF', amino_acid templates
2 CASSHGTAYEQYF 468),
('CASSLDRLSSGEQYF', amino_acid templates
3 CASSLDRLSSGEQYF 239
5 CASSLDRLSSGEQYF 66),
('CASSLRGNQPQHF', amino_acid templates
1 CASSLRGNQPQHF 635),
('CAWSVGQYSNQPQHF', amino_acid templates
0 CAWSVGQYSNQPQHF 118),
('CSVEDGPRGTQYF', amino_acid templates
4 CSVEDGPRGTQYF 51)]
And per the docs, [df.sum
] will return the sum on the specified axis (1 by default). So, for tmp[1]
, which contains two rows:
tmp[1][1].sum()
amino_acid CASSLDRLSSGEQYFCASSLDRLSSGEQYF
templates 305
dtype: object
Where 305
is the sum. The pd.groupby
object as a whole supports the .sum
call, so we are able to call it like we did above.
# Now, using `to_dict`, you can see that you want what's inside templates
df.groupby("amino_acid").sum().to_dict()
# {'templates': {'CASSHGTAYEQYF': 468, 'CASSLDRLSSGEQYF': 305, 'CASSLRGNQPQHF': 635, 'CAWSVGQYSNQPQHF': 118, 'CSVEDGPRGTQYF': 51}}
# so use the templates attribute to grab it
df.groupby("amino_acid").sum().templates.to_dict()
# {'CASSHGTAYEQYF': 468, 'CASSLDRLSSGEQYF': 305, 'CASSLRGNQPQHF': 635, 'CAWSVGQYSNQPQHF': 118, 'CSVEDGPRGTQYF': 51}
This applies the operations within the dataframe, which is more efficient than a loop. The analogue that you were trying to use could leverage defaultdict
from the collections module, as well. It prevents the need to check for a key's existence, speeding up the loop immensely:
from collections import defaultdict
# specify your input type here
sequence_counts = defaultdict(int)
# it is more pythonic to use tuple-unpacking in loops
# as indexing is less readable
for amino, template in zip(df.amino_acid, df.templates):
sequence_counts[amino] += template
Also, by iterating over the zip
object directly, you don't copy data into memory. list
will aggregate all of the members into a data structure, whereas zip
is a generator that will just produce the members one at a time until exhausted. It's like the difference between for x in range
and for x in list(range)
:
# This will just run for a really really really long time
for i in range(1000000000000):
print(i)
# This will crash your computer, it will never
# get to the print statement, because it must evaluate
# list(range) before it starts the loop
for i in list(range(1000000000000)):
print(i)
df.groupby('amino_acid',sort=False).templates.sum().to_dict()
when you say sequence counts, i am guessing you are trying to maintain the original order. \$\endgroup\$