Repository navigation
Description
I was working on a project where I was taking multiple spectra and then dividing the counts per pixel by the exposure time to get a count rate. The spectra were stored in astropy.Tables, the exposure time in the first column followed by 2048 columns of counts per pixel. I wanted to perform a simple operation to divide each column (containing the counts in a given pixel over various exposure times) by the exposure column. This is very simple to do in a numpy array, something like:
array_data[:,1:] /= data[:,0, np.newaxis]
but the only way I could figure out to do the equivalent using astropy.Tables was to iterate column by column in a loop, which looked something like:
# I previously defined 'speccols', a list of columns with spectral data
for cols in speccols:
orig_Table[cols] = orig_Table[cols] / orig_Table['exposure']
It turns out to be a huge performance hit. Working with a structured array as produced by simply calling np.array(orig_Table) was not useful here, since I could not perform the simple operation above.
It was roughly 50 times faster to convert the floating point data in the Table orig_Table into a numpy array array_data using:
# list of columns with floating point data
colnames = orig_Table.colnames
floatcols = [cols for cols in colnames if orig_Table[cols].dtype == np.dtype('float64')]
# Copy just the floating point data to numpy array
fp_data = Table.copy(orig_data[floatcols])
fp_rows = len(fp_data) # Number of rows of floating point data in table
fp_cols = len(floatcols) # Number of columns of floating point data in table
# Create array of zeros same size as table and copy data over column by column
array_data = np.zeros((fp_rows,fp_cols),dtype=np.float64)
for n, name in enumerate(floatcols):
array_data[:,n] = fp_data[name].astype(np.float64)
Is there supposed to be a faster way to do this sort of operation within the astropy.Tables context? Is there a simple way to convert a table into an numpy array of floats, since np.array() doesn't seem to cut it (presumably because to keep all the information in the astropy.Table intact requires structured arrays).
Activity
mhvk commentedon Jun 22, 2017
I'm not 100% on where the performance hit comes from, since in-place should be fast for
Columntoo, but it may be that you are stuck with creating a large float array. Note that you could have a bit of "best of both worlds" if you make the table columns as views of the large array of spectra (usecopy=Falsein theColumnconstructor).pllim commentedon Jun 22, 2017
Maybe using a line profiler can help track down the bottleneck here. You can use it as
%lprunmagic if you use Jupyter Notebook.taldcroft commentedon Jun 22, 2017
Doing the following is worst-case slow:
The problem is that this syntax is actually creating a brand new column and updating the table to effectively remove the original column and add the newly created column. I haven't checked, but using
orig_table[cols] /= orig_table['exposure']should be much faster as it should be doing an in-place update.Beyond that, is there a reason you are not using a vector column where the entire 2048-pixel spectrum is stored in a single column
'spectrum'? This is perfectly allowed and much preferred over 2048 individual columns (as implied in your second sentence).astrofrog commentedon Jun 22, 2017
This is a perfect candidate for adding documentation as described in #5753 😄
JuanCab commentedon Jun 22, 2017
I get Tom's point about the creation of a new column in memory as opposed to in situ operations (you will notice when I performed the operation using numpy arrays, I did the operation in situ). I don't know if that was the main performance bottleneck. I may try playing around with it a bit.
To answer Tom's question about the "reason" for this organization, there were actually two instruments whose data I was recording and I was trying to keep the records together. So the first few columns were data from another instrument, a timestamp, and then the spectral data. I did originally have exposure time, then the spectra in a column, but as I started putting more pertinent data in there, it seemed to make sense to use the ability to refer to columns by name and besides, I couldn't store some of the information (e.g. timestamp, device flags) in the same column since they were of a different dtype.
This prompts some questions:
astrofrog commentedon Jun 22, 2017
In-place operations should be faster on columns, but at the moment it seems like they aren't:
This would indicate a bug in astropy.table?
JuanCab commentedon Jun 22, 2017
And I can confirm that:
is much, much slower than the conversion to numpy approach. So the
in situadvantage just isn't there for astropy.Tables.taldcroft commentedon Jun 22, 2017
@astrofrog - the right test is using a small column so you are timing the table overhead rather than the time for doing the operation. BUT sadly that doesn't help!
However, here is a workaround which gives back a factor of 100 that suggests we need to implement the in-place operators, e.g.
__iadd__or whatnot. Timing of the in-place ops suggests they are still going through the heavyColumninit machinery (that surprises me, but numpy subclassing is never easy).Note that
t['b'].data += 0.1fails because it thinks you are trying to set thedataattribute which is not allowed. Fun!taldcroft commentedon Jun 22, 2017
About vector columns, no problem:
taldcroft commentedon Jun 22, 2017
About docs, 👍 on the performance docs section. I also think it would be worth funding somebody to work on table performance, but that is a separate discussion. 😄
There is the following that makes it pretty clear how tables are internally implemented:
http://astropy.readthedocs.io/en/latest/table/implementation_details.html
Perhaps a sentence up front talking about column-centric implementation and a link here would be worthwhile.
astrofrog commentedon Jun 23, 2017
@taldcroft:
The same operation with Numpy arrays takes only 200ms so I am assuming that the overhead (5x) is from the memory copy not from the actual operation.
mhvk commentedon Jun 23, 2017
The problem does not seem to be with
Column:However, as part of a table it is not so good:
What is worse, though, is that there must be some memory leak: after doing this, python uses about 1GB...
This does not happen if I do:
(or
c = t.columns[0])mhvk commentedon Jun 23, 2017
@JuanCab - from my tests, I think the work-around is clear: you'd need to do something like:
Of course, ideally, this should not be necessary... (and I don't really understand why it is happening)
mhvk commentedon Jun 23, 2017
Note: this also happens with quantity, so it is not
Columnspecific. Indeed,blows up memory usage, so the problem seems to be in
Table.__setitem__.mhvk commentedon Jun 23, 2017
Following the trail:
t.replace_columnalso blows up memory... I'm actually a bit confused with what is done there. Is it meant to copy the data ina?taldcroft commentedon Jun 23, 2017
Yes,
replace_columncopies the data ina. This comes from (and is consistent with) the default ofcopy=Truefor table initialization.About the memory leak...
So this is making a temporary table in order to use the generic Table init machinery to make sense of whatever the user has provided for
col(which might be a column or an array or list ...). Then it makes a reference to that column and uses it inself._init_from_cols, which does NOT copy. So this should all be fine? Maybe_init_from_colson an existing table somehow leaves around a ref to the original cols? But I don't quite see how...docs: Reference issue astropy#6253