-
Notifications
You must be signed in to change notification settings - Fork 8
CHANGE: Expand uncert to include coefficient of variation #210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nocollier
wants to merge
2
commits into
main
Choose a base branch
from
expand-uncert
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -429,34 +429,35 @@ def has_bounds(dset: xr.Dataset | xr.DataArray, dim: str, bounds: str | None) -> | |
| return False | ||
|
|
||
|
|
||
| def get_ancillary_variable(ds: xr.Dataset, varname: str) -> xr.Dataset | None: | ||
| def get_bounds_variable(ds: xr.Dataset, varname: str) -> xr.DataArray: | ||
| """ | ||
| Return the sub-dataset with the ancillary variables found in ds[varname]. | ||
| Return the dataarray defined as the bounds of the input variable. | ||
| """ | ||
| var = ds[varname] | ||
| if "ancillary_variables" not in var.attrs: | ||
| return None | ||
| anc_vars = [v for v in var.attrs["ancillary_variables"].split() if v in ds] | ||
| if not anc_vars: | ||
| da = ds[varname] | ||
| if "bounds" not in da.attrs: | ||
| raise ValueError(f"No 'bounds' attribute found in:\n{da}") | ||
| bnd_var = str(da.attrs.get("bounds")) | ||
| if bnd_var not in ds: | ||
| raise ValueError( | ||
| f"Ancillary variables specified {var.attrs['ancillary_variables']=} but at least one was not found in the dataset {ds=}." | ||
| f"A bound variable is specified {bnd_var=} but was not found in the dataset:\n{ds=}." | ||
| ) | ||
| return ds[anc_vars] | ||
| return ds[bnd_var] | ||
|
|
||
|
|
||
| def get_bounds_variable(ds: xr.Dataset, varname: str) -> xr.DataArray | None: | ||
| def get_ancillary_variable(ds: xr.Dataset, varname: str) -> xr.Dataset: | ||
| """ | ||
| Return the dataarray defined as the bounds of the input variable. | ||
| Return the sub-dataset with the ancillary variables found in `ds[varname]`. | ||
| """ | ||
| var = ds[varname] | ||
| if "bounds" not in var.attrs: | ||
| return None | ||
| bnd_var = var.attrs["bounds"] | ||
| if bnd_var not in ds: | ||
| da = ds[varname] | ||
| if "ancillary_variables" not in da.attrs: | ||
| raise ValueError(f"No 'ancillary_variables' attribute found in:\n{da}") | ||
| anc_possible_vars = [v for v in str(da.attrs.get("ancillary_variables")).split()] | ||
| anc_vars = [v for v in anc_possible_vars if v in ds] | ||
| if not anc_vars: | ||
| raise ValueError( | ||
| f"A bound variable is specified {var.attrs['bounds']=} but was not found in the dataset {ds=}." | ||
| f"Ancillary variables specified {anc_possible_vars=} but at least one was not found in the dataset:\n{ds=}." | ||
| ) | ||
| return ds[bnd_var] | ||
| return ds[anc_vars] | ||
|
|
||
|
|
||
| def compute_cell_measures( | ||
|
|
@@ -1024,56 +1025,74 @@ def shift_lon(dset: xr.Dataset) -> xr.Dataset: | |
| return dset | ||
|
|
||
|
|
||
| def get_scalar_uncertainty(ds: xr.Dataset, varname: str) -> xr.DataArray: | ||
| def separate_bounds(ds: xr.Dataset, varname: str) -> xr.Dataset: | ||
| """ | ||
| Return the bounds as two data arrays if a `bounds` variable is present. | ||
| """ | ||
| Get a scalar uncertainty from the variable. | ||
| da = ds[varname] | ||
| bnd = get_bounds_variable(ds, varname) | ||
| bnd_dim = set(bnd.dims) - set(da.dims) | ||
| # some datasets just gave a single value (like a stdev) | ||
| if not bnd_dim: | ||
| return bnd.to_dataset() | ||
| assert len(bnd_dim) == 1 | ||
| bnd_dim = next(iter(bnd_dim)) | ||
| assert len(ds[bnd_dim]) == 2 | ||
| return xr.Dataset({"low": bnd.isel({bnd_dim: 0}), "high": bnd.isel({bnd_dim: 1})}) | ||
|
|
||
| Note | ||
| ---- | ||
| Uncertainty is indicated either by the presence of the `ancillary_variables` | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Beef up docstrings
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The users (and Morgan) should understand why we cal lthis and the interval version. |
||
| attribute or the `bounds` attribute. In the case of the latter, we will | ||
| return the harmonic mean as a scalar measure of uncertainty. | ||
|
|
||
| def get_scalar_uncertainty(ds: xr.Dataset, varname: str) -> xr.DataArray: | ||
| """ | ||
| Get a scalar uncertainty from the variable if present. | ||
| """ | ||
|
|
||
| def _get_bound_dim(da: xr.DataArray) -> str | None: | ||
| possible = set(da.dims).difference(da.coords) | ||
| if len(possible) > 1: | ||
| raise ValueError( | ||
| f"Ambiguity in determinging the `bounds` dimension, found: {possible} in\n\n{da}" | ||
| ) | ||
| if len(possible) == 0: | ||
| return None | ||
| return possible.pop() | ||
| # Initialize to nothing | ||
| anc = None | ||
|
|
||
| out = None | ||
| # Uncertainty could be an ancillary variable with the same dims | ||
| ds_anc = get_ancillary_variable(ds, varname) | ||
| if ds_anc is not None and len(ds_anc) == 1: | ||
| # We found an ancillary variable, but does it have the same dims | ||
| out = ( | ||
| ds_anc[next(iter(ds_anc))] | ||
| if set(ds[varname].dims) == set(ds_anc.dims) | ||
| else None | ||
| ) | ||
| # Legacy formats allow for uncertainty to be encoded in a DataArray pointed | ||
| # to by a variable's `bounds` attribute. | ||
| try: | ||
| anc = separate_bounds(ds, varname) | ||
| except ValueError: | ||
| pass | ||
|
|
||
| # Uncertainty could be defined using intervals | ||
| if out is None: | ||
| # The current best practice is to encode uncertainty as an ancillary | ||
| # variable. | ||
| if anc is None: | ||
| try: | ||
| out = get_interval_uncertainty(ds, varname) | ||
| except NoUncertainty: | ||
| anc = get_ancillary_variable(ds, varname) | ||
| except ValueError: | ||
| pass | ||
|
|
||
| if out is None: | ||
| raise NoUncertainty() | ||
| # If we are at this point, there is no uncertainty encoded. | ||
| if anc is None or not anc: | ||
| raise NoUncertainty | ||
|
|
||
| # Cleanup if an inverval style uncertainty | ||
| bnd_dim = _get_bound_dim(out) | ||
| if bnd_dim is not None: | ||
| var = ds[varname] | ||
| out = np.sqrt(((var - out) ** 2).sum(dim=bnd_dim)) | ||
| if "units" not in out.attrs: | ||
| out.attrs["units"] = var.attrs["units"] | ||
| # Now that we have ancillary data retrieved, handle the difference cases. | ||
| # This assumes that no non-uncertainty related ancillary data is present. | ||
| out = None | ||
| match len(anc): | ||
| case 1: | ||
| da = anc[next(iter(anc))] | ||
| # This could be a coefficient of variation (std/mean) | ||
| str_attr = str(da.attrs) | ||
| if "coefficient" in str_attr and "variation" in str_attr: | ||
| out = ds[varname] * da | ||
| else: | ||
| # Otherwise we assume it is a standard error or deviation | ||
| out = da | ||
| case 2: | ||
| # Assuming that these relate to a confident interval, subtract the | ||
| # mean and return the geometric mean (add in quadrature). | ||
| out = np.sqrt(sum([(da - ds[varname]) ** 2 for _, da in anc.items()])) | ||
| case _: | ||
| raise ValueError(f"Unexpected (>2) number of ancillary variables:\n{anc=}") | ||
|
|
||
| # Finalize the uncertainty | ||
| out = xr.DataArray(out) | ||
| out.name = "uncert" | ||
| if "units" not in out.attrs: | ||
| out.attrs["units"] = ds[varname].attrs["units"] | ||
| return out | ||
|
|
||
|
|
||
|
|
@@ -1098,8 +1117,14 @@ def _get_bound_dim(da: xr.DataArray) -> str | None: | |
| return possible.pop() | ||
|
|
||
| # What ancillary/bounds variables are found? | ||
| ds_anc = get_ancillary_variable(ds, varname) | ||
| da_bnd = get_bounds_variable(ds, varname) | ||
| try: | ||
| ds_anc = get_ancillary_variable(ds, varname) | ||
| except ValueError: | ||
| ds_anc = None | ||
| try: | ||
| da_bnd = get_bounds_variable(ds, varname) | ||
| except ValueError: | ||
| da_bnd = None | ||
|
|
||
| # Handle if we find ancillary variables | ||
| if ds_anc is not None: | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it better to embed this logic in the
get_scalar_uncertaintyfunction and then returnxr.Dataset()?