CategoricalVariable feature.#53
Conversation
Read categorical variables using CategoricalArrays.jl
|
@tiemvanderdeure and @evetion |
|
Thanks - this looks much more robust than whatever I was trying in Rasters.jl. One question is if it would be worth putting this in an extension on CategoricalArrays to avoid adding a dependency? |
| function _build_categorical_array( | ||
| raw::AbstractArray{R, N}, mapping::AbstractDict{R, V} | ||
| ) where {V, N, R} | ||
| label_values = V[mapping[c] for c in raw] | ||
| return CategoricalArray{V, N, UInt32}(label_values; levels=_sorted_labels(mapping)) | ||
| end |
There was a problem hiding this comment.
This would be very ineffecient - we are basically deconstructing and then reconstructing the entire categorical array. We need to pass the integer values and the mapping directly to CategoricalArray to avoid this, that will be much faster
There was a problem hiding this comment.
yes, it would be great if we can reuse the underling raw array and wrap it by annotating it with the labels.
This issue at CategoricalArrays.jl discusses a similar use case:
JuliaData/CategoricalArrays.jl#389
In Implementation details it is mentioned that 0 is always the missing value which can be a problem if 0 maps to something different in the NetCDF file.
There was a problem hiding this comment.
Yes, the CategoricalArrays.jl has some annoying limitations. This PR is not focused on performance but just on enabling the feature with a stable interface. If the interface is good then it is possible to improve the performant later on.
At the moment NCDatasets.jl throws an error for NC enum making these variables inaccessible so performance is not the first concern.
There was a problem hiding this comment.
I would say the way we wrap things and convert to CategoricalArray is key to make this work properly so let's get it right now :). Especially because CategoricalArrays has quite a few limitations that we will have to hack our way around
There was a problem hiding this comment.
One of the main problems is that CategoricalArrays requries that a CategoricalPool has consecutive indices starting at 1, which most files will not have. So just using the Variable itself as the underlying array in the CategoricalArray would not work. Maybe we need to do something like have a different dictionary that maps the values in the file to index values, wrap the Variable in that, and then wrap that in a CategoricalArray (where the CategoricalPool comes directly from the mapping, again with changed indices).
There was a problem hiding this comment.
Actually, I am wondering if improving the performance is actually possible within the bounds of the stable API of CategoricalArrays. Would we not also need to scan the whole array to handle the case when 0 is not the missing value?
But of course, the current situation (error with enums) is indeed quite bad.
|
Hi Simon, thanks a lot for this PR! :-) |
|
An alternative way to design this would be to have a concrete type that wraps an AbstractVariable - then packages could call something like I'm suspecting that these AbstractCategoricalVariables would resemble each other quite a lot so there would be redundancy there. |
|
I think there are several ways to read an array of NetCDF enums: Using CategorialArrays: advantages is that the code is quite straightforward for arbitrary enum types. Using jula enums: The storage type can match the underlying NetCDF file and we can load the data without extra copying. We can easily query the name and storage type of the enum type when writing the data. Julia array of symbols: This is a native data structure in julia. But every symbol is 8 bytes. The user would need to provide the name and storage type separately for writing. Is there anything that I forgot or something to be corrected? |
|
I've implemented flag values in HDF5 as Categorical Arrays in SpaceLiDAR here https://github.com/evetion/SpaceLiDAR.jl/blob/59c14d680ad20056c039d7f22e2f09435eb6dfca/src/H5Table/utils.jl#L636. It seems the most pragmatic at the moment, but I like the proposal of enums backed by the original data. Makes comparison feel more logical too (to a symbol thing, not a random string). Then again, maybe its better to extend CategoricalArrays with a simple read only implementation (dict based, no compilation) that takes any backing data? |
I have been a bit undecided about that. My first plan was to make an extension but then I went for the simple solution. I will turn it back into an extension.
Good idea, I will update the PR
CategorialArrays is a pretty common package and it seems to support the main use cases of NetCDF enums. The few examples I have seen of NetCDF enums, they are used as categorical variables. I therefore went for CategorialArrays.jl as the simplest solution to the use cases I am aware off. Your Julia enum example looks really interesting. The reason I initially skipped the idea is, enums are Julia types. Dynamically defining types based on the content of the NetCDF adds a layer of complexity we probably don't need. I will have to try your example to see how complex it really is, but I don't see a clear advantage. Do you have a use case where you would prefer to get a "dynamically defined enum" instead of a "CategoricalArray"? The Julia enum solution might turn out to be elegant, but the CategoricalArray solution is so simple that I can both implement and use it 😅 |
One use case I can think of is when you copy a variable from one NetCDF file to another. An array of Julia Another aspect could be loading data in-place ( I made some tests with CategoricalArrays.jl and apparently Symbols are not supported. I made some benchmarks, to read these 100 000 elements, and these are the run time that I got: (see @tiemvanderdeure comments) . However, I never used CategoricalArrays before so let me know if I did something stupid 😅. I know that we can always optimize later, but I don't have the impression that manipulating directly the But maybe there are other use-cases that I am not aware of where CategoricalArrays is better suited than vector of |
|
@Alexander-Barth
It will probably be possible to handle a lot of the higher level dispatch with |
|
@lupemba these are excellent questions and actually while working compound types I came across these as well 😀.
usertype!(ds,"nc_compound_t",MyStruct)We can also query the type to be used without reading any data with Whether all this works with DiskArrays of course remains to be seen. But al least with an array of custom Another related point, that I came across, is that NetCDF With the corresponding definition of the |
|
@Alexander-Barth @tiemvanderdeure, @evetion |
|
The only thing I would say in favour of doing this with CategoricalArrays is that other packages use it as well. So if a Raster with land cover classes is an input for a machine learning model with MLJ, it will "just work" if it is read as a CategoricalArray, whereas with an enum I need to do some kind of conversion. Maybe we can solve this by making this conversion easy, but it's still an extra step. I did a re-write of this branch in lupemba#3, where reading is much faster and CategoricalVariable is a concrete type that contains any AbstractVariable and a mapping. |
|
@lupemba Actually, I only needed changes in NCDatasets to support @tiemvanderdeure As far as I can tell, the |
Add handling for categorical variables to CommonDataModel. This allows a variable stored as Integers to be automatically converted to a CategoricalArrays.CategoricalArray
This is possible without any updates to DiskArrays and thus makes JuliaIO/DiskArrays.jl#287 irrelevant.
The primary focus is to solve JuliaGeo/NCDatasets.jl#143 but this also requires a PR to NCDatasets.jl.
TODO