From 7017d0f0d0d70a66c684125071a2b5f908e26d7c Mon Sep 17 00:00:00 2001 From: Thomas Nipen Date: Sat, 8 Aug 2015 19:15:27 -0700 Subject: [PATCH] Initial add --- .gitignore | 8 + .pbuilderrc | 95 ++ .travis.yml | 23 + LICENSE | 24 + README.rst | 129 ++ examples/T_kf_0.nc | Bin 0 -> 62228 bytes examples/T_raw_0.nc | Bin 0 -> 62228 bytes examples/example.txt | 6 + image.jpg | Bin 0 -> 136577 bytes makefile | 6 + requirements.txt | 4 + setup.py | 108 ++ tests/Metric_Deterministic_test.py | 29 + tests/Metric_test.py | 18 + verif/.gitignore | 1 + verif/Common.py | 162 +++ verif/Data.py | 536 ++++++++ verif/Input.py | 252 ++++ verif/Metric.py | 923 +++++++++++++ verif/Output.py | 2035 ++++++++++++++++++++++++++++ verif/Station.py | 26 + verif/__init__.py | 559 ++++++++ 22 files changed, 4944 insertions(+) create mode 100644 .gitignore create mode 100644 .pbuilderrc create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 examples/T_kf_0.nc create mode 100644 examples/T_raw_0.nc create mode 100644 examples/example.txt create mode 100644 image.jpg create mode 100644 makefile create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/Metric_Deterministic_test.py create mode 100644 tests/Metric_test.py create mode 100644 verif/.gitignore create mode 100644 verif/Common.py create mode 100644 verif/Data.py create mode 100644 verif/Input.py create mode 100644 verif/Metric.py create mode 100644 verif/Output.py create mode 100644 verif/Station.py create mode 100644 verif/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3391bdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.pyc +dist +build +*.egg-info +*~ +.coverage +cover +.*.swp diff --git a/.pbuilderrc b/.pbuilderrc new file mode 100644 index 0000000..393cdd9 --- /dev/null +++ b/.pbuilderrc @@ -0,0 +1,95 @@ +# Based on https://wiki.ubuntu.com/PbuilderHowto (2015-04-21, last edited by osamu) +# License: http://creativecommons.org/licenses/by-sa/3.0/ +# Support for ccache based on http://failshell.io/devel/pbuilder-and-ccache-howto/ + +# Codenames for Debian suites according to their alias. Update these when +# needed. +UNSTABLE_CODENAME="sid" +TESTING_CODENAME="jessie" +STABLE_CODENAME="wheezy" +STABLE_BACKPORTS_SUITE="$STABLE_CODENAME-backports" + +# List of Debian suites. +DEBIAN_SUITES=($UNSTABLE_CODENAME $TESTING_CODENAME $STABLE_CODENAME + "unstable" "testing" "stable") + +# List of Ubuntu suites. Update these when needed. +UBUNTU_SUITES=("trusty" "precise" "lucid") + +# Mirrors to use. Update these to your preferred mirror. +DEBIAN_MIRROR="ftp.us.debian.org" +UBUNTU_MIRROR="mirrors.kernel.org" + +# Optionally use the changelog of a package to determine the suite to use if +# none set. +if [ -z "${DIST}" ] && [ -r "debian/changelog" ]; then + DIST=$(dpkg-parsechangelog | awk '/^Distribution: / {print $2}') + DIST="${DIST%%-*}" + # Use the unstable suite for certain suite values. + if $(echo "experimental UNRELEASED" | grep -q $DIST); then + DIST="$UNSTABLE_CODENAME" + fi + # Use the stable suite for stable-backports. + if $(echo "$STABLE_BACKPORTS_SUITE" | grep -q $DIST); then + DIST="$STABLE_CODENAME" + fi +fi + +# Optionally set a default distribution if none is used. Note that you can set +# your own default (i.e. ${DIST:="unstable"}). +: ${DIST:="$(lsb_release --short --codename)"} + +# Optionally change Debian release states in $DIST to their names. +case "$DIST" in + unstable) + DIST="$UNSTABLE_CODENAME" + ;; + testing) + DIST="$TESTING_CODENAME" + ;; + stable) + DIST="$STABLE_CODENAME" + ;; +esac + +# Optionally set the architecture to the host architecture if none set. Note +# that you can set your own default (i.e. ${ARCH:="i386"}). +: ${ARCH:="$(dpkg --print-architecture)"} + +NAME="$DIST" +if [ -n "${ARCH}" ]; then + NAME="$NAME-$ARCH" + DEBOOTSTRAPOPTS=("--arch" "$ARCH" "${DEBOOTSTRAPOPTS[@]}") +fi +BASETGZ="/var/cache/pbuilder/$NAME-base.tgz" +# Optionally, set BASEPATH (and not BASETGZ) if using cowbuilder +# BASEPATH="/var/cache/pbuilder/$NAME/base.cow/" +DISTRIBUTION="$DIST" +BUILDRESULT="/var/cache/pbuilder/$NAME/result/" +APTCACHE="/var/cache/pbuilder/$NAME/aptcache/" +BUILDPLACE="/var/cache/pbuilder/build/" + +if $(echo ${DEBIAN_SUITES[@]} | grep -q $DIST); then + # Debian configuration + MIRRORSITE="http://$DEBIAN_MIRROR/debian/" + COMPONENTS="main contrib non-free" + DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--keyring=/usr/share/keyrings/debian-archive-keyring.gpg") + +elif $(echo ${UBUNTU_SUITES[@]} | grep -q $DIST); then + # Ubuntu configuration + MIRRORSITE="http://$UBUNTU_MIRROR/ubuntu/" + COMPONENTS="main restricted universe multiverse" + DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg") +else + echo "Unknown distribution: $DIST" + exit 1 +fi + +# ccache +export CCACHE_DIR="/var/cache/pbuilder/ccache/$DIST" +export PATH="/usr/lib/ccache:${PATH}" +mkdir -p "${CCACHE_DIR}" +chown -R 1234:root "${CCACHE_DIR}" +chmod 755 "${CCACHE_DIR}" +EXTRAPACKAGES=ccache +BINDMOUNTS="${CCACHE_DIR}" diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cc7512a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: python +python: + - "2.7_with_system_site_packages" + +install: + - sudo apt-get install libblas-dev + - sudo apt-get install liblapack-dev + - sudo apt-get install gfortran + - sudo apt-get install -qq python-numpy python-scipy python-matplotlib + - pip install coveralls + - sudo python setup.py build + - sudo python setup.py install + +script: + - nosetests --with-coverage --cover-erase --cover-package=verif + +after_success: + - coveralls + +env: + - DIST=lucid + +os: linux diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11fb23a --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2015 Weather Forecast Research Team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +* Neither the name of the Weather Forecast Research Team nor the +names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE WEATHER FORECAST RESEARCH TEAM BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..be6655f --- /dev/null +++ b/README.rst @@ -0,0 +1,129 @@ +Forecast verification software +============================== + +.. image:: https://travis-ci.org/tnipen/verif.svg?branch=master + :target: https://travis-ci.org/tnipen/verif +.. image:: https://coveralls.io/repos/tnipen/verif/badge.svg?branch=master&service=github + :target: https://coveralls.io/github/tnipen/verif?branch=master + +This software computes verification statistics for weather forecasts at point locations. It can be used to +document the quality of one forecasting system but can also be used to compare different weather models and/or +different post-processing methods. + +The program works by parsing NetCDF files with observations and forecasts in a specific format (see "Input +files" below). + +verif is a command-line tool that can therefore be used to automatically create verification figures. + +Developed by Thomas Nipen, David Siuta, and Tim Chui. + +.. image:: image.jpg + :alt: Example plots + :width: 400 + :align: center + +Features +-------- + +* Deterministic metrics such as MAE, bias, RMSE +* Threshold-based metrics such as the equitable threat score (ETS) +* Probabilistic metrics such as brier score, PIT-histogram, reliability diagrams +* Plot statistics as a function of date, forecast horizon, station elevation, latitude, or longitude +* Show statistics on maps +* Export to text +* Options to adjust font sizes, label positions, tick marks, legends, etc +* Anomaly statistics (relative to a baseline like climatology) +* Output to png, jeg, eps, etc and specify image dimensions and DPI. + +For a full list run verif without arguments. + +Requirements +------------ + +* Python +* matplotlib +* numpy +* scipy + +Installation Instructions +------------------------- +To install, just execute: + +.. code-block:: bash + + python setup.py install + +verif will then be installed /usr/local/share/python/ or where ever your python modules are +installed (Look for "Installing verif script to " when installing). Be sure to add this directory +to your $PATH environment variable. + +Example +------- +.. code-block:: bash + +Fake data for testing the program is found in ./examples/. Use the following command to test: + +.. code-block:: bash + + verif examples/T_raw.nc examples/T_kf.nc -m mae + +Text-based input +---------------- +Two data formats are supported. A simple text format for deterministic forecasts has the following format: + +.. code-block:: bash + + date offset lat lon elev obs fcst + 20150101 0 49.2 -122.1 92 3.4 2.1 + 20150101 1 49.2 -122.1 92 4.7 4.2 + 20150101 0 50.3 -120.3 150 0.2 -1.2 + +The first line must describe the columns. The following attributes are recognized: date (in YYYYMMDD), offset (in hours), lat +(in degrees), lon (in degrees), obs (observations), fcst (deterministic forecast). obs and fcst are required and a value of 0 +is used for any missing column. The columns can be in any order. + +NetCDF input +------------ +For more advanced usage, the files must be in NetCDF and have dimensions and attributes as described below in the +example file. The format is still being decided but will be based on NetCDF/CF standard. + +.. code-block:: bash + + netcdf format { + dimensions : + date = UNLIMITED; + offset = 48; + station = 10; + ensemble = 21; + threshold = 11; + quantile = 11; + variables: + int id(station); + int offset(offset); + int date(date); + float threshold(threshold); + float quantile(quantile); + float lat(station); + float lon(station); + float elev(station); + float obs(date, offset, station); // Observations + float ens(date, offset, ensemble, station); // Ensemble forecast + float fcst(date, offset, station); // Deterministic forecast + float cdf(date, offset, threshold, station); // Accumulated prob at threshold + float pdf(date, offset, threshold, station); // Pdf at threshold + float x(date, offset, quantile, station); // Threshold corresponding to quantile + float pit(date, offset, station); // CDF for threshold=observation + + global attributes: + : name = "raw"; // Used as configuration name + : long_name = "Temperature"; // Used to label plots + : standard_name = "air_temperature_2m"; + : Units = "^oC"; + : Conventions = "verif_1.0.0"; + } + +Copyright and license +--------------------- + +Copyright © 2015 UBC Weather Forecast Research Team. verif is licensed under the 3-clause BSD license. See LICENSE +file. diff --git a/examples/T_kf_0.nc b/examples/T_kf_0.nc new file mode 100644 index 0000000000000000000000000000000000000000..0f56b8707d7d01e40b6c61ceade88c552a8bcfff GIT binary patch literal 62228 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 literal 0 HcmV?d00001 diff --git a/examples/example.txt b/examples/example.txt new file mode 100644 index 0000000..4760693 --- /dev/null +++ b/examples/example.txt @@ -0,0 +1,6 @@ +date offset lat lon elev obs fcst +20120101 0 1 1 1 12 13 +20120101 1 0 0 1 13 14 +20120101 22 0 0 2 5 4 +20120101 0 1 1 0 10 11 +20120101 3 2 2 1 2 3 diff --git a/image.jpg b/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..769ee8621b5d3fa325a8a0f8d8b1bd82e3e75e6a GIT binary patch literal 136577 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[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 literal 0 HcmV?d00001 diff --git a/makefile b/makefile new file mode 100644 index 0000000..c98b46b --- /dev/null +++ b/makefile @@ -0,0 +1,6 @@ +coverage: + #nosetests --with-coverage --cover-erase --cover-package=verif --cover-html --cover-branches + nosetests --with-coverage --cover-erase --cover-package=verif --cover-html + +test: + nosetests diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7c35da2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +scipy +numpy +matplotlib +coveralls diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b82b1d3 --- /dev/null +++ b/setup.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the relevant file +with open(path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='verif', + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version='0.1.0', + + description='A verification program for meteorological forecasts and observations', + long_description=long_description, + + # The project's main homepage. + url='https://github.com/tnipen/verif', + + # Author details + author='Thomas Nipen', + author_email='tnipen@gmail.com', + + # Choose your license + license='MIT', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 3 - Alpha', + + # Indicate who your project is intended for + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: BSD License', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], + + # What does your project relate to? + keywords='meteorology verification weather', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=find_packages(exclude=['contrib', 'docs', 'tests*']), + + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=['numpy', 'matplotlib', 'scipy', 'coveralls'], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + extras_require={ + # 'dev': ['check-manifest'], + 'test': ['coverage'], + # 'test': ['pytest'], + }, + + test_suite="tests", + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + #package_data={ + # 'sample': ['package_data.dat'], + #}, + + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. See: + # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa + # In this case, 'data_file' will be installed into '/my_data' + #data_files=[('my_data', ['data/data_file'])], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={ + 'console_scripts': [ + 'verif=verif:main', + ], + }, +) diff --git a/tests/Metric_Deterministic_test.py b/tests/Metric_Deterministic_test.py new file mode 100644 index 0000000..d63719b --- /dev/null +++ b/tests/Metric_Deterministic_test.py @@ -0,0 +1,29 @@ +import unittest +import verif.Metric as Metric +import numpy as np + +class MyTest(unittest.TestCase): + def test_func(self): + obsSet = [[1,2,3], + [1,2], + [1]] + fcstSet = [[0,2,8], + [3,2], + [2]] + metrics = [Metric.Mae(), Metric.Medae(), Metric.Bias(), Metric.Ef()]; + # Metrics in the inner lists, datasets in the outer + expSet = [[2, 1, -4.0/3, 100.0/3], + [1, 1, -1, 50], + [1, 1, -1, 100]] + for s in range(0, len(obsSet)): + obs = obsSet[s] + fcst = fcstSet[s] + for i in range(0, len(metrics)): + metric = metrics[i] + expected = expSet[s][i] + value = metric.computeObsFcst(np.array(obs), np.array(fcst)) + message = metric.name() + " gives " + str(value) + " value for set " + str(s) + " (expected " + str(expected) + ")" + self.assertAlmostEqual(value, expected, msg=message) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/Metric_test.py b/tests/Metric_test.py new file mode 100644 index 0000000..05d1459 --- /dev/null +++ b/tests/Metric_test.py @@ -0,0 +1,18 @@ +import unittest +import verif.Metric as Metric +import numpy as np + +class MyTest(unittest.TestCase): + def test_func(self): + metric = Metric.Mae() + value = metric.computeObsFcst(np.array([2,1,2]),np.array([2,3,1])) + self.assertEqual(value, 1) + value = metric.computeObsFcst(np.array([-2]),np.array([2])) + self.assertEqual(value, 4) + value = metric.computeObsFcst(np.array([]),np.array([])) + self.assertTrue(np.isnan(value)) + # value = metric.computeObsFcst(np.array([2,np.nan,2]),np.array([2,3,1])) + # self.assertEqual(value, 0.5) + +if __name__ == '__main__': + unittest.main() diff --git a/verif/.gitignore b/verif/.gitignore new file mode 100644 index 0000000..a6d7ecd --- /dev/null +++ b/verif/.gitignore @@ -0,0 +1 @@ +temp/ diff --git a/verif/Common.py b/verif/Common.py new file mode 100644 index 0000000..52df16c --- /dev/null +++ b/verif/Common.py @@ -0,0 +1,162 @@ +import datetime +import numpy as np +import sys +from matplotlib.dates import * +from copy import deepcopy +import matplotlib.pyplot as mpl +def convertDates(dates): + numDates = len(dates) + dates2 = np.zeros([numDates], 'float') + for i in range(0, numDates): + year = int(dates[i] / 10000) + month = int(dates[i] / 100 % 100) + day = int(dates[i] % 100) + dates2[i] = date2num(datetime.datetime(year, month, day, 0)) + return dates2 + +def red(text): + return "\033[31m"+text+"\033[0m" + +def removeMargin(): + mpl.subplots_adjust(left=0, right=1, bottom=0, top=1, wspace=0,hspace=0) + +def green(text): + return "\033[32m"+text+"\033[0m" + +def yellow(text): + return "\033[33m"+text+"\033[0m" + +def experimental(): + return yellow("(experimental)") + +def error(message): + print "\033[1;31mError: " + message + "\033[0m" + sys.exit(1) + +def warning(message): + print "\033[1;33mWarning: " + message + "\033[0m" + +# allowable formats: +# num +# num1,num2,num3 +# start:end +# start:step:end +def parseNumbers(numbers): + # Check if valid string + if(any(char not in set('-01234567890.:,') for char in numbers)): + error("Could not translate '" + numbers + "' into numbers") + + values = list() + commaLists = numbers.split(',') + for commaList in commaLists: + colonList = commaList.split(':') + if(len(colonList) == 1): + values.append(float(colonList[0])) + elif(len(colonList) <= 3): + start = float(colonList[0]) + step = 1 + if(len(colonList) == 3): + step = float(colonList[1]) + end = float(colonList[-1]) + 0.0001 # arange does not include the end point + values = values + list(np.arange(start, end, step)) + else: + error("Could not translate '" + numbers + "' into numbers") + return values + +def testParseNumbers(): + print parseNumbers("1,2,3:5,6,7:2:20") + print parseNumbers("1") + +# Sets up subplot for index i (starts at 0) out of N +def subplot(i, N): + [X,Y] = getSubplotSize(N) + mpl.subplot(Y,X,i+1) + +def getSubplotSize(N): + Y = 1 + if(N > 4): + Y= np.ceil(np.sqrt(N)/1.5) + X = np.ceil(N / Y) + return [int(X),int(Y)] +def getMapResolution(lats, lons): + dlat = (max(lats) - min(lats)) + dlon = (max(lons) - min(lons)) + scale = max(dlat, dlon) + if(np.isnan(scale)): + res = "c" + elif(scale > 60): + res = "c" + elif(scale > 1): + res = "i" + elif(scale > 0.1): + res = "h" + elif(scale > 0.01): + res = "f" + else: + res = "c" + return res + +# Fill an area along x, between yLower and yUpper +# Both yLower and yUpper most correspond to points in x (i.e. be in the same order) +def fill(x, yLower, yUpper, col, alpha=1, zorder=0, hatch=''): + # This approach doesn't work, because it doesn't remove points with missing x or y + #X = np.hstack((x, x[::-1])) + #Y = np.hstack((yLower, yUpper[::-1])) + + # Populate a list of non-missing points + X = list() + Y = list() + for i in range(0,len(x)): + if(not( np.isnan(x[i]) or np.isnan(yLower[i]))): + X.append(x[i]) + Y.append(yLower[i]) + for i in range(len(x)-1, -1, -1): + if(not (np.isnan(x[i]) or np.isnan(yUpper[i]))): + X.append(x[i]) + Y.append(yUpper[i]) + if(len(X) > 0): + mpl.fill(X, Y, facecolor=col, alpha=alpha,linewidth=0, zorder=zorder, hatch=hatch) + +def clean(data): + data = data[:].astype(float) + q = deepcopy(data) + mask = np.where(q == -999); + q[mask] = np.nan + mask = np.where(q < -100000); + q[mask] = np.nan + mask = np.where(q > 1e30); + q[mask] = np.nan + return q + +# Date: YYYYMMDD diff: Add this many days +def getDate(date, diff): + year = int(date / 10000) + month = int(date / 100 % 100) + day = int(date % 100) + date2 = datetime.datetime(year, month, day, 0) + datetime.timedelta(diff) + return int(date2.strftime('%Y%m%d')) + +def nanmean(data, **args): + return np.ma.filled(np.ma.masked_array(data,np.isnan(data)).mean(**args), + fill_value=np.nan) +def nanmedian(data, **args): + I = np.where(np.isnan(data.flatten()) == 0)[0] + return np.median(data.flatten()[I]) +def nanmin(data, **args): + return np.ma.filled(np.ma.masked_array(data,np.isnan(data)).min(**args), + fill_value=np.nan) +def nanmax(data, **args): + return np.ma.filled(np.ma.masked_array(data,np.isnan(data)).max(**args), + fill_value=np.nan) +def nanstd(data, **args): + return np.ma.filled(np.ma.masked_array(data,np.isnan(data)).std(**args), + fill_value=np.nan) +def nanpercentile(data, pers): + I = np.where(np.isnan(data.flatten()) == 0)[0] + p = np.percentile(data.flatten()[I], pers) + return p + #return np.ma.filled(np.ma.masked_array(data,np.isnan(data)).percentile(pers), + # fill_value=np.nan) + +def intersect(list1, list2): + return list(set(list1) & set(list2)) diff --git a/verif/Data.py b/verif/Data.py new file mode 100644 index 0000000..e5fb831 --- /dev/null +++ b/verif/Data.py @@ -0,0 +1,536 @@ +from scipy import io +import numpy as np +import verif.Common as Common +import re +import sys +import os +import verif.Input as Input +from matplotlib.dates import * +from matplotlib.ticker import ScalarFormatter + +# Access verification data from a set of COMPS NetCDF files +# Only returns data that is available for all files, for fair comparisons +# i.e if some dates/offsets/locations are missing +# +# filenames: COMPS NetCDF verification files +# dates: Only allow these dates +# offsets: Only allow these offsets +# locations: Only allow these locationIds +# clim: Use this NetCDF file to compute anomaly. Should therefore be a climatological +# forecast. Subtract/divide the forecasts from this file from all forecasts and +# observations from the other files. +# climType: 'subtract', or 'divide' the climatology +# training: Remove the first 'training' days of data (to allow the forecasts to train its +# adaptive parameters) +class Data: + def __init__(self, filenames, dates=None, offsets=None, locations=None, latlonRange=None, elevRange=None, clim=None, + climType="subtract", training=None): + if(not isinstance(filenames, list)): + filenames = [filenames] + self._axis = "date" + self._index = 0 + + # Organize files + self._files = list() + self._cache = list() + self._clim = None + for filename in filenames: + if(not os.path.exists(filename)): + Common.error("File '" + filename + "' is not a valid input file") + # file = io.netcdf.netcdf_file(filename, 'r') + try: + file = Input.Comps(filename) + except: + file = Input.Text(filename) + self._files.append(file) + self._cache.append(dict()) + if(clim != None): + self._clim = io.netcdf.netcdf_file(clim, 'r') + self._cache.append(dict()) + if(not (climType == "subtract" or climType == "divide")): + Common.error("Data: climType must be 'subtract' or 'divide") + self._climType = climType + + # Climatology file + self._files = self._files + [self._clim] + + # Latitude-Longitude range + if(latlonRange != None): + lat = self._files[0].getLats() + lon = self._files[0].getLons() + locId = self._files[0].getStationIds() + latlonLocations = list() + minLon = latlonRange[0] + maxLon = latlonRange[1] + minLat = latlonRange[2] + maxLat = latlonRange[3] + for i in range(0,len(lat)): + currLat = float(lat[i]) + currLon = float(lon[i]) + if(currLat >= minLat and currLat <= maxLat and currLon >= minLon and currLon <= maxLon): + latlonLocations.append(locId[i]) + useLocations = list() + if(locations != None): + for i in range(0, len(locations)): + currLocation = locations[i] + if(currLocation in latlonLocations): + useLocations.append(currLocation) + else: + useLocations = latlonLocations + if(len(useLocations) == 0): + Common.error("No available locations within lat/lon range") + else: + useLocations = locations + + # Elevation range + if(elevRange != None): + lat = self._files[0].getElevs() + locId = self._files[0].getStationIds() + elevLocations = list() + minElev = elevRange[0] + maxElev = elevRange[1] + for i in range(0,len(elev)): + currElev = float(elev[i]) + if(currElev >= minElev and currElev <= maxElev): + elevLocations.append(locId[i]) + useLocations = Common.intersect(useLocations, elevLocations) + if(len(useLocations) == 0): + Common.error("No available locations within elevation range") + + # Find common indicies + self._datesI = Data._getCommonIndices(self._files, "Date", dates) + self._offsetsI = Data._getCommonIndices(self._files, "Offset", offsets) + self._locationsI = Data._getCommonIndices(self._files, "Location", useLocations) + if(len(self._datesI[0]) == 0): + Common.error("No valid dates selected") + if(len(self._offsetsI[0]) == 0): + Common.error("No valid offsets selected") + if(len(self._locationsI[0]) == 0): + Common.error("No valid locations selected") + + # Training + if(training != None): + for f in range(0, len(self._datesI)): + if(len(self._datesI[f]) <= training): + Common.error("Training period too long for " + self.getFilenames()[f] + \ + ". Max training period is " + str(len(self._datesI[f])-1) + ".") + self._datesI[f] = self._datesI[f][training:] + + self._findex = 0 + + # Returns flattened arrays along the set axis/index + def getScores(self, metrics): + if(not isinstance(metrics, list)): + metrics = [metrics] + data = dict() + valid = None + axis = self._getAxisIndex(self._axis) + + # Compute climatology, if needed + doClim = self._clim != None and ("obs" in metrics or "fcst" in metrics) + if(doClim): + temp = self._getScore("fcst", len(self._files)-1) + if(self._axis == "date"): + clim = temp[self._index,:,:].flatten() + elif(self._axis == "offset"): + clim = temp[:,self._index,:].flatten() + elif(self.isLocationAxis(self._axis)): + clim = temp[:,:,self._index].flatten() + elif(self._axis == "none" or self._axis == "threshold"): + clim = temp.flatten() + elif(self._axis == "all"): + clim = temp + else: + clim = 0 + + for i in range(0, len(metrics)): + metric = metrics[i] + temp = self._getScore(metric) + #print self._axis + + if(self._axis == "date"): + data[metric] = temp[self._index,:,:].flatten() + elif(self._axis == "offset"): + data[metric] = temp[:,self._index,:].flatten() + elif(self.isLocationAxis(self._axis)): + data[metric] = temp[:,:,self._index].flatten() + elif(self._axis == "none" or self._axis == "threshold"): + data[metric] = temp.flatten() + elif(self._axis == "all"): + data[metric] = temp + else: + Common.error("Data.py: unrecognized value of self._axis: " + self._axis) + + # Subtract climatology + if(doClim and (metric == "fcst" or metric == "obs")): + if(self._climType == "subtract"): + data[metric] = data[metric] - clim + else: + data[metric] = data[metric] / clim + + # Remove missing values + if(self._axis != "all"): + currValid = (np.isnan(data[metric]) == 0) & (np.isinf(data[metric]) == 0) + if(valid == None): + valid = currValid + else: + valid = (valid & currValid) + if(self._axis != "all"): + I = np.where(valid) + + q = list() + for i in range(0, len(metrics)): + if(self._axis != "all"): + q.append(data[metrics[i]][I]) + else: + q.append(data[metrics[i]]) + + # No valid data + if(q[0].shape[0] == 0): + for i in range(0, len(metrics)): + q[i] = np.nan*np.zeros([1], 'float') + + return q + + # Find indicies of elements that are present in all files + # Merge in values in 'aux' as well + @staticmethod + def _getCommonIndices(files, name, aux=None): + # Find common values among all files + values = aux + for file in files: + if(name == "Date"): + temp = file.getDates() + elif(name == "Offset"): + temp = file.getOffsets() + elif(name == "Location"): + stations = file.getStations() + temp = np.zeros(len(stations)) + for i in range(0,len(stations)): + temp[i] = stations[i].id() + if(values == None): + values = temp + else: + values = np.intersect1d(values, temp) + # Sort values, since for example, dates may not be in an ascending order + values = np.sort(values) + + # Determine which index each value is at + indices = list() + for file in files: + if(name == "Date"): + temp = file.getDates() + elif(name == "Offset"): + temp = file.getOffsets() + elif(name == "Location"): + stations = file.getStations() + temp = np.zeros(len(stations)) + for i in range(0,len(stations)): + temp[i] = stations[i].id() + I = np.where(np.in1d(temp, values))[0] + II = np.zeros(len(I), 'int') + for i in range(0,len(I)): + II[i] = np.where(values[i] == temp)[0] + + indices.append(II) + return indices + + def _getFiles(self): + if(self._clim == None): + return self._files + else: + return self._files[0:-1] + + def getMetrics(self): + metrics = None + for file in self._files: + currMetrics = file.getMetrics() + if(metrics == None): + metrics = currMetrics + else: + metrics = set(metrics) & set(currMetrics) + + return metrics + + def _getIndices(self, axis, findex=None): + if(axis == "date"): + I = self._getDateIndices(findex) + elif(axis == "offset"): + I = self._getOffsetIndices(findex) + elif(axis == "location"): + I = self._getLocationIndices(findex) + else: + Common.error(axis) + return I + def _getDateIndices(self, findex=None): + if(findex == None): + findex = self._findex + return self._datesI[findex] + + def _getOffsetIndices(self, findex=None): + if(findex == None): + findex = self._findex + return self._offsetsI[findex] + + def _getLocationIndices(self, findex=None): + if(findex == None): + findex = self._findex + return self._locationsI[findex] + + def _getScore(self, metric, findex=None): + if(findex == None): + findex = self._findex + + if(metric in self._cache[findex]): + return self._cache[findex][metric] + + # Load all files + for f in range(0, self.getNumFilesWithClim()): + if(not metric in self._cache[f]): + file = self._files[f] + if(not metric in file.getVariables()): + Common.error("Variable '" + metric + "' does not exist in " + + self.getFilenames()[f]) + temp = file.getScores(metric) + dims = file.getDims(metric) + temp = Common.clean(temp) + for i in range(0, len(dims)): + I = self._getIndices(dims[i].lower(), f) + if(i == 0): + temp = temp[I,Ellipsis] + if(i == 1): + temp = temp[:,I,Ellipsis] + if(i == 2): + temp = temp[:,:,I,Ellipsis] + self._cache[f][metric] = temp + + # Remove missing + # If one configuration has a missing value, set all configurations to missing + # This can happen when the dates are available, but have missing values + isMissing = np.isnan(self._cache[0][metric]) + for f in range(1, self.getNumFilesWithClim()): + isMissing = isMissing | (np.isnan(self._cache[f][metric])) + for f in range(0, self.getNumFilesWithClim()): + self._cache[f][metric][isMissing] = np.nan + + return self._cache[findex][metric] + + # Checks that all files have the variable + def hasMetric(self, metric): + for f in range(0, self.getNumFilesWithClim()): + if(not metric in self._files[f].getVariables()): + return False + return True + + def setAxis(self, axis): + self._index = 0 # Reset index + self._axis = axis + def setIndex(self, index): + self._index = index + def setFileIndex(self, index): + self._findex = index + def getNumFiles(self): + return len(self._files) - (self._clim != None) + def getNumFilesWithClim(self): + return len(self._files) + + def getUnits(self): + # TODO: Only check first file? + return self._files[0].getUnits() + + def isLocationAxis(self, axis): + if(axis == None): + return False + prog = re.compile("location.*") + return prog.match(axis) + + def getAxisSize(self, axis=None): + if(axis == None): + axis = self._axis + return len(self.getAxisValues(axis)) + + # What values represent this axis? + def getAxisValues(self, axis=None): + if(axis == None): + axis = self._axis + if(axis == "date"): + return Common.convertDates(self._getScore("Date").astype(int)) + elif(axis == "offset"): + return self._getScore("Offset").astype(int) + elif(axis == "none"): + return [0] + elif(self.isLocationAxis(axis)): + if(axis == "location"): + data = range(0, len(self._getScore("Location"))) + elif(axis == "locationId"): + data = self._getScore("Location").astype(int) + elif(axis == "locationElev"): + data = self._getScore("Elev") + elif(axis == "locationLat"): + data = self._getScore("Lat") + elif(axis == "locationLon"): + data = self._getScore("Lon") + else: + Common.error("Data.getAxisValues has a bad axis name: " + axis) + return data + else: + return [0] + def isAxisContinuous(self, axis=None): + if(axis == None): + axis = self._axis + return axis in ["date", "offset", "threshold"] + + def getAxisFormatter(self, axis=None): + if(axis == None): + axis = self._axis + if(axis == "date"): + return DateFormatter('\n%Y-%m-%d') + else: + return ScalarFormatter() + + + # filename including path + def getFullFilenames(self): + names = list() + files = self._getFiles() + for i in range(0, len(files)): + names.append(files[i].getFilename()) + return names + def getFilename(self, findex=None): + if(findex == None): + findex = self._findex + return self.getFilenames()[findex] + + # Do not include the path + def getFilenames(self): + names = self.getFullFilenames() + for i in range(0, len(names)): + I = names[i].rfind('/') + names[i] = names[i][I+1:] + return names + def getShortNames(self): + names = self.getFilenames() + for i in range(0, len(names)): + I = names[i].rfind('.') + names[i] = names[i][:I] + return names + + def getAxis(self, axis=None): + if(axis == None): + axis = self._axis + return axis + + def getVariable(self): + return self._files[0].getVariable() + + def getVariableAndUnits(self): + return self.getVariable() + " (" + self.getUnits() + ")" + + def getX0(self): + x0 = None + prog = re.compile("Precip.*") + if(prog.match(self.getVariable())): + x0 = 0 + return x0 + + def getX1(self): + x1 = None + prog = re.compile("RH") + if(prog.match(self.getVariable())): + x1 = 100 + return x1 + + def getAxisLabel(self, axis=None): + if(axis == None): + axis = self._axis + if(axis == "date"): + return "Date" + elif(axis == "offset"): + return "Offset (h)" + elif(axis == "locationElev"): + return "Elevation (m)" + elif(axis == "locationLat"): + return "Latitude ($^o$)" + elif(axis == "locationLon"): + return "Longitude ($^o$)" + elif(axis == "threshold"): + return self.getVariableAndUnits() + + def getLats(self): + return self._getScore("Lat") + + def getLons(self): + return self._getScore("Lon") + + def getElevs(self): + return self._getScore("Elev") + + def getLocationIds(self): + return self._getScore("Location") + + def getAxisDescriptions(self, axis=None): + if(axis == None): + axis = self._axis + prog = re.compile("location.*") + if(prog.match(axis)): + descs = list() + ids = self._getScore("Location") + lats = self._getScore("Lat") + lons = self._getScore("Lon") + elevs = self._getScore("Elev") + for i in range(0, len(ids)): + string = "%6d %5.2f %5.2f %5.0f" % (ids[i],lats[i], lons[i], elevs[i]) + descs.append(string) + return descs + if(axis == "date"): + values = self.getAxisValues(axis) + values = num2date(values) + dates = list() + for i in range(0, len(values)): + dates = dates + [values[i].strftime("%Y/%m/%d")] + return dates + else: + return self.getAxisValues(axis) + + def getAxisDescriptionHeader(self, axis=None): + if(axis == None): + axis = self._axis + prog = re.compile("location.*") + if(prog.match(axis)): + return "%6s %5s %5s %5s" % ("id", "lat", "lon", "elev") + else: + return axis + + def _getAxisIndex(self, axis): + if(axis == "date"): + return 0 + elif(axis == "offset"): + return 1 + elif(axis == "location" or axis == "locationId" or axis == "locationElev" or axis == "locationLat" or axis == "locationLon"): + return 2 + else: + return None + + def getPvar(self, threshold): + minus = "" + if(threshold < 0): + # Negative thresholds + minus = "m" + if(abs(threshold - int(threshold)) > 0.01): + var = "p" + minus + str(abs(threshold)).replace(".", "") + else: + var = "p" + minus + str(int(abs(threshold))) + return var + + def getQvar(self, quantile): + quantile = quantile * 100 + minus = "" + if(abs(quantile - int(quantile)) > 0.01): + var = "q" + minus + str(abs(quantile)).replace(".", "") + else: + var = "q" + minus + str(int(abs(quantile))) + + if(not self.hasMetric(var) and quantile == 50): + Common.warning("Could not find q50, using fcst instead") + return "fcst" + return var diff --git a/verif/Input.py b/verif/Input.py new file mode 100644 index 0000000..ca69af1 --- /dev/null +++ b/verif/Input.py @@ -0,0 +1,252 @@ +from scipy import io +import numpy as np +import verif.Station as Station +import verif.Common as Common + +# Abstract base class representing verification data +class Input: + def __init__(self, filename): + self._filename = filename + def getName(self): + pass + def getFilename(self): + return self.getName() + def getDates(self): + pass + def getStations(self): + pass + def getOffsets(self): + pass + def getThresholds(self): + pass + def getQuantiles(self): + pass + def getObs(self): + pass + def getDeterministic(self): + pass + def getOther(self, name): + pass + def getMetrics(self): + pass + def getFilename(self): + return self._filename + def getVariables(self): + pass + def getUnits(self): + pass + def getLats(self): + stations = self.getStations() + lats = np.zeros(len(stations)) + for i in range(0, len(stations)): + lats[i] = stations[i].lat() + return lats + def getLons(self): + stations = self.getStations() + lons = np.zeros(len(stations)) + for i in range(0, len(stations)): + lons[i] = stations[i].lon() + return lons + def getStationIds(self): + stations = self.getStations() + ids = np.zeros(len(stations)) + for i in range(0, len(stations)): + ids[i] = stations[i].id() + return ids + +# Original fileformat used by OutputVerif in COMPS +class Comps(Input): + def __init__(self, filename): + Input.__init__(self, filename) + self._file = io.netcdf.netcdf_file(filename, 'r') + def getName(self): + return self._file.variables + def getStations(self): + lat = Common.clean(self._file.variables["Lat"]) + lon = Common.clean(self._file.variables["Lon"]) + id = Common.clean(self._file.variables["Location"]) + elev = Common.clean(self._file.variables["Elev"]) + stations = list() + for i in range(0, lat.shape[0]): + station = Station.Station(id[i], lat[i], lon[i], elev[i]) + stations.append(station) + return stations + def getScores(self, metric): + temp = Common.clean(self._file.variables[metric]) + return temp + def getDims(self, metric): + return self._file.variables[metric].dimensions + def getDates(self): + return Common.clean(self._file.variables["Date"]) + def getOffsets(self): + return Common.clean(self._file.variables["Offset"]) + def getMetrics(self): + metrics = list() + for (metric, v) in self._file.variables.iteritems(): + if(not metric in ["Date", "Offset", "Location", "Lat", "Lon", "Elev"]): + metrics.append(metric) + return metrics + def getVariables(self): + metrics = list() + for (metric, v) in self._file.variables.iteritems(): + metrics.append(metric) + return metrics + def getUnits(self): + if(hasattr(self._file, "Units")): + if(self._file.Units == ""): + return "No units" + elif(self._file.Units == "%"): + return "%" + else: + return "$" + self._file.Units + "$" + else: + return "No units" + def getVariable(self): + return self._file.Variable + +# New standard format, based on NetCDF/CF +class NetcdfCf(Input): + def __init__(self, filename): + pass + +# Flat text file format +class Text(Input): + def __init__(self, filename): + import csv + Input.__init__(self, filename) + file = open(filename, 'r') + reader = csv.reader(file, delimiter=' ') + + self._dates = set() + self._offsets = set() + self._stations = set() + obs = dict() + fcst = dict() + indices = dict() + header = None + + # Default values if columns not available + offset = 0 + date = 0 + lat = 0 + lon = 0 + elev = 0 + + # Read the data into dictionary with (date,offset,lat,lon,elev) as key and obs/fcst as values + for row in reader: + if(header == None): + # Parse the header so we know what each column represents + header = row + for i in range(0, len(header)): + att = header[i] + if(att == "date"): + indices["date"] = i + elif(att == "offset"): + indices["offset"] = i + elif(att == "lat"): + indices["lat"] = i + elif(att == "lon"): + indices["lon"] = i + elif(att == "elev"): + indices["elev"] = i + elif(att == "obs"): + indices["obs"] = i + elif(att == "fcst"): + indices["fcst"] = i + + # Ensure we have required columns + requiredColumns = ["obs", "fcst"] + for col in requiredColumns: + if(not indices.has_key(col)): + msg = "Could not parse %s: Missing column '%s'" % (filename, col) + Common.error(msg) + else: + if(indices.has_key("date")): + date = float(row[indices["date"]]) + self._dates.add(date) + if(indices.has_key("offset")): + offset = float(row[indices["offset"]]) + self._offsets.add(offset) + if(indices.has_key("lat")): + lat = float(row[indices["lat"]]) + if(indices.has_key("lon")): + lon = float(row[indices["lon"]]) + if(indices.has_key("elev")): + elev = float(row[indices["elev"]]) + station = Station.Station(0, lat, lon, elev) + self._stations.add(station) + obs[(date,offset,lat,lon,elev)] = float(row[indices["obs"]]) + fcst[(date,offset,lat,lon,elev)] = float(row[indices["fcst"]]) + file.close() + self._dates = list(self._dates) + self._offsets = list(self._offsets) + self._stations = list(self._stations) + Ndates = len(self._dates) + Noffsets = len(self._offsets) + Nlocations = len(self._stations) + + # Put the dictionary data into a regular 3D array + self._obs = np.zeros([Ndates, Noffsets, Nlocations], 'float') * np.nan + self._fcst = np.zeros([Ndates, Noffsets, Nlocations], 'float') * np.nan + for d in range(0,len(self._dates)): + date = self._dates[d] + for o in range(0, len(self._offsets)): + offset = self._offsets[o] + for s in range(0, len(self._stations)): + station = self._stations[s] + lat = station.lat() + lon = station.lon() + elev = station.elev() + key = (date,offset,lat,lon,elev) + if(obs.has_key(key)): + self._obs[d][o][s] = obs[key] + if(fcst.has_key(key)): + self._fcst[d][o][s] = fcst[key] + + counter = 0 + for station in self._stations: + station.id(counter) + counter = counter + 1 + self._dates = np.array(self._dates) + self._offsets = np.array(self._offsets) + def getName(self): + return "Unknown" + def getStations(self): + return self._stations + def getScores(self, metric): + if(metric == "obs"): + return self._obs + elif(metric == "fcst"): + return self._fcst + elif(metric == "Offset"): + return self._offsets + else: + Common.error("Cannot find " + metric) + def getDims(self, metric): + if(metric == "obs" or metric == "fcst"): + return ["Date", "Offset", "Location"] + else: + return [metric] + def getDates(self): + return self._dates + def getOffsets(self): + return self._offsets + def getMetrics(self): + metrics = ["obs", "fcst"] + return metrics + def getVariables(self): + metrics = ["obs", "fcst", "Date", "Offset", "Location", "Lat", "Lon", "Elev"] + return metrics + def getUnits(self): + return "Unknown units" + def getVariable(self): + return "Unknown" + +class Fake(Input): + def __init__(self, obs, fcst): + self._obs = obs + self._fcst = fcst + def getObs(self): + return self._obs + def getMean(self): + return self._fcst diff --git a/verif/Metric.py b/verif/Metric.py new file mode 100644 index 0000000..65c27e7 --- /dev/null +++ b/verif/Metric.py @@ -0,0 +1,923 @@ +import numpy as np +import verif.Common as Common +import sys +import inspect +def getAllMetrics(): + temp = inspect.getmembers(sys.modules[__name__], inspect.isclass) + return temp + +# Computes scores for each xaxis value +class Metric: + # Overload these variables + _description = "" # Overwrite this to let the metric show up in the documentation of ./verif + _min = None # Minimum value this metric can produce + _max = None # Maximum value this mertic can produce + _defaultAxis = "offset" # If no axis is specified, use this axis as default + _defaultBinType = None + _reqThreshold = False # Does this metric require thresholds? + _supThreshold = False # Does this metric support thresholds? + _experimental = False # Is this metric not fully tested yet? + _perfectScore = None + + # Compute the score + # data: use getScores([metric1, metric2...]) to get data + # data has already been configured to only retrieve data along a certain dimension + # tRange: [lowerThreshold, upperThreshold] + def compute(self, data, tRange): + #assert(isinstance(tRange, list)) + #assert(len(tRange) == 2) + size = data.getAxisSize() + scores = np.zeros(size, 'float') + # Loop over x-axis + for i in range(0,size): + data.setIndex(i) + x = self.computeCore(data, tRange) + scores[i] = x + return scores + + # Implement this + def computeCore(self, data, tRange): + Common.error("Metric '" + self.getClassName() + "' has not been implemented yet") + + @classmethod + def description(cls): + return cls._description + + + @classmethod + def summary(cls): + desc = cls.description() + if(desc == ""): + return "" + extra = "" + if(cls._experimental): + extra = " " + Common.experimental() + "." + if(cls._perfectScore != None): + extra = extra + " " + "Perfect score " + str(cls._perfectScore) + "." + return desc + "." + extra + + # Does this metric require thresholds in order to be computable? + @classmethod + def requiresThresholds(cls): + return cls._reqThreshold + + # If this metric is to be plotted, along which axis should it be plotted by default? + @classmethod + def defaultAxis(cls): + return cls._defaultAxis + + @classmethod + def defaultBinType(cls): + return cls._defaultBinType + + @classmethod + def perfectScore(cls): + return cls._perfectScore + + # Does it make sense to use '-x threshold' with this metric? + @classmethod + def supportsThreshold(cls): + return cls._supThreshold + + # Minimum value the metric can take on + @classmethod + def min(cls): + return cls._min + + # Maximum value the metric can take on + @classmethod + def max(cls): + return cls._max + + def getClassName(self): + name = self.__class__.__name__ + return name + def label(self, data): + return self.name() + " (" + data.getUnits() + ")" + def name(self): + return self.getClassName() + +class Default(Metric): + def __init__(self, name): + self._name = name + def compute(self, data, tRange): + return data.getScores(self._name) + def name(self): + return self._name + +class Mean(Metric): + def __init__(self, metric): + self._metric = metric + def computeCore(self, data, tRange): + return np.mean(self._metric.compute(data, tRange)) + def name(self): + return "Mean of " + self._metric.name() + +class Median(Metric): + def __init__(self, metric): + self._metric = metric + def computeCore(self, data, tRange): + return np.median(self._metric.compute(data, tRange)) + def name(self): + return "Median of " + self._metric.name() + +class Max(Metric): + def __init__(self, metric): + self._metric = metric + def computeCore(self, data, tRange): + return np.max(self._metric.compute(data, tRange)) + def name(self): + return "Max of " + self._metric.name() + +class Min(Metric): + def __init__(self, metric): + self._metric = metric + def computeCore(self, data, tRange): + return np.min(self._metric.compute(data, tRange)) + def name(self): + return "Min of " + self._metric.name() + +class Deterministic(Metric): + def computeCore(self, data, tRange): + [obs, fcst] = data.getScores(["obs", "fcst"]) + return self.computeObsFcst(obs,fcst) + +class Mae(Deterministic): + _min = 0 + _description = "Mean absolute error" + _perfectScore = 0 + def computeObsFcst(self, obs, fcst): + return np.mean(abs(obs - fcst)) + def name(self): + return "MAE" + +class Medae(Deterministic): + _min = 0 + _description = "Median absolute error" + _perfectScore = 0 + def computeObsFcst(self, obs, fcst): + return np.median(abs(obs - fcst)) + def name(self): + return "MedianAE" + +class Bias(Deterministic): + _description = "Bias" + _perfectScore = 0 + def computeObsFcst(self, obs, fcst): + return np.mean(obs - fcst) + +class Ef(Deterministic): + _description = "Exeedance fraction: percentage of times that forecasts > observations" + _min = 0 + _max = 100 + _perfectScore = 50 + def computeObsFcst(self, obs, fcst): + Nfcst = np.sum(obs < fcst) + return Nfcst / 1.0 / len(fcst) * 100 + def label(self, data): + return "% times fcst > obs" + +class Extreme(Metric): + def calc(self, data, func, variable): + [value] = data.getScores([variable]) + if(len(value) == 0): + return np.nan + return func(value) + +class MaxObs(Extreme): + _description = "Maximum observed value" + def computeCore(self, data, tRange): + return self.calc(data, np.max, "obs") + +class MinObs(Extreme): + _description = "Minimum observed value" + def computeCore(self, data, tRange): + return self.calc(data, np.min, "obs") + +class MaxFcst(Extreme): + _description = "Maximum forecasted value" + def computeCore(self, data, tRange): + return self.calc(data, np.max, "fcst") + +class MinFcst(Extreme): + _description = "Minimum forecasted value" + def computeCore(self, data, tRange): + return self.calc(data, np.min, "fcst") + +class StdError(Deterministic): + _min = 0 + _description = "Standard error (i.e. RMSE if forecast had no bias)" + _perfectScore = 0 + def computeObsFcst(self, obs, fcst): + bias = np.mean(obs - fcst) + return np.mean((obs - fcst - bias)**2)**0.5 + def name(self): + return "Standard error" + +class Std(Deterministic): + _min = 0 + _description = "Standard deviation of forecast" + def computeObsFcst(self, obs, fcst): + return np.std(fcst) + def label(self, data): + return "STD of forecasts (" + data.getUnits() + ")" + +# Returns all PIT values +class Pit(Metric): + _min = 0 + _max = 1 + def __init__(self, name="pit"): + self._name = name + def label(self, data): + return "PIT" + def compute(self, data, tRange): + x0 = data.getX0() + x1 = data.getX1() + if(x0 == None and x1 == None): + [pit] = data.getScores([self._name]) + else: + [obs,pit] = data.getScores(["obs", self._name]) + if(x0 != None): + I = np.where(obs == x0)[0] + pit[I] = np.random.rand(len(I))*pit[I] + if(x1 != None): + I = np.where(obs == x1)[0] + pit[I] = 1 - np.random.rand(len(I))*(1-pit[I]) + #I = np.where((fcst > 2) & (fcst < 2000))[0] + #I = np.where((fcst > 20))[0] + #pit = pit[I] + return pit + def name(self): + return "PIT" + +# Returns all PIT values +class PitDev(Metric): + _min = 0 + #_max = 1 + _perfectScore = 1 + _description = "Deviation of the PIT histogram" + def __init__(self, numBins=11): + self._metric = Pit() + self._bins = np.linspace(0,1,numBins) + def label(self, data): + return "PIT histogram deviation" + def computeCore(self, data, tRange): + pit = self._metric.compute(data, tRange) + I = np.where(np.isnan(pit) == 0)[0] + pit = pit[np.isnan(pit) == 0] + + nb = len(self._bins)-1 + D = self.deviation(pit, nb) + D0 = self.expectedDeviation(pit, nb) + dev = D/D0 + return dev + + def name(self): + return "PIT deviation factor" + @staticmethod + def expectedDeviation(values, numBins): + if(len(values) == 0 or numBins == 0): + return np.nan + return np.sqrt((1.0 - 1.0 / numBins) / (len(values) * numBins)) + @staticmethod + def deviation(values, numBins): + if(len(values) == 0 or numBins == 0): + return np.nan + x = np.linspace(0,1,numBins+1) + n = np.histogram(values, x)[0] + n = n * 1.0 / sum(n) + return np.sqrt(1.0 / numBins * np.sum((n - 1.0 / numBins)**2)) + @staticmethod + def deviationStd(values, numBins): + if(len(values) == 0 or numBins == 0): + return np.nan + n = len(values) + p = 1.0 / numBins + numPerBinStd = np.sqrt(n * p*(1-p)) + std = numPerBinStd/n + return std + # What reduction in ignorance is possible by calibrating the PIT-histogram? + @staticmethod + def ignorancePotential(values, numBins): + if(len(values) == 0 or numBins == 0): + return np.nan + x = np.linspace(0,1,numBins+1) + n = np.histogram(values, x)[0] + n = n * 1.0 / sum(n) + expected = 1.0 / numBins + ign = np.sum(n*np.log2(n/expected))/sum(n) + return ign + +class MarginalRatio(Metric): + _min = 0 + _description = "Ratio of marginal probability of obs to marginal probability of fcst. Use -r" + _perfectScore = 1 + _reqThreshold = True + _supThreshold = True + _defaultAxis = "threshold" + _experimental = True + def computeCore(self, data, tRange): + if(np.isinf(tRange[0])): + pvar = data.getPvar(tRange[1]) + [obs,p1] = data.getScores(["obs",pvar]) + p0 = 0*p1 + elif(np.isinf(tRange[1])): + pvar = data.getPvar(tRange[0]) + [obs,p0] = data.getScores(["obs",pvar]) + p1 = 0*p0+1 + else: + pvar0 = data.getPvar(tRange[0]) + pvar1 = data.getPvar(tRange[1]) + [obs,p0,p1] = data.getScores(["obs",pvar0,pvar1]) + obs = Threshold.within(obs, tRange) + p = p1-p0 + if(np.mean(p) == 0): + return np.nan + return np.mean(obs)/np.mean(p) + def label(self, data): + return "Ratio of marginal probs: Pobs/Pfcst" + +class SpreadSkillDiff(Metric): + _description = "Difference between spread and skill in %" + _perfectScore = 1 + def computeCore(self, data, tRange): + import scipy.stats + [obs,fcst,spread] = data.getScores(["obs", "fcst", "spread"]) + if(len(obs) <= 1): + return np.nan + rmse = np.sqrt(np.mean((obs-fcst)**2)) + spread = np.mean(spread)/2.563103 + return 100 * (spread / rmse - 1) + def name(self): + return "Spread-skill difference" + def label(self, data): + return "Spread-skill difference (%)" + + +class Rmse(Deterministic): + _min = 0 + _description = "Root mean squared error" + _perfectScore = 0 + def computeObsFcst(self, obs, fcst): + return np.mean((obs - fcst)**2)**0.5 + def name(self): + return "RMSE" + +class Rmsf(Deterministic): + _min = 0 + _description = "Root mean squared factor" + _perfectScore = 1 + def computeObsFcst(self, obs, fcst): + return np.exp(np.mean((np.log(fcst/obs))**2)**0.5) + def name(self): + return "RMSE" + +class Crmse(Deterministic): + _min = 0 + _description = "Centered root mean squared error (RMSE without bias)" + _perfectScore = 0 + def computeObsFcst(self, obs, fcst): + bias = np.mean(obs)-np.mean(fcst) + return np.mean((obs - fcst - bias)**2)**0.5 + def name(self): + return "CRMSE" + + +class Cmae(Deterministic): + _min = 0 + _description = "Cube-root mean absolute cubic error" + _perfectScore = 0 + def computeObsFcst(self, obs, fcst): + return (np.mean(abs(obs**3 - fcst**3)))**(1.0/3) + def name(self): + return "CMAE" + +class Dmb(Deterministic): + _description = "Degree of mass balance (obs/fcst)" + _perfectScore = 1 + def computeObsFcst(self, obs, fcst): + return np.mean(obs)/np.mean(fcst) + def name(self): + return "Degree of mass balance (obs/fcst)" + +class Num(Deterministic): + _description = "Number of valid forecasts" + def computeObsFcst(self, obs, fcst): + [fcst] = data.getScores(["fcst"]) + return len(fcst) + def name(self): + return "Number of valid forecasts" + +class Corr(Deterministic): + _min = 0 # Technically -1, but values below 0 are not as interesting + _max = 1 + _description = "Correlation between obesrvations and forecasts" + _perfectScore = 1 + def computeObsFcst(self, obs, fcst): + if(len(obs) <= 1): + return np.nan + return np.corrcoef(obs,fcst)[1,0] + def name(self): + return "Correlation" + def label(self, data): + return "Correlation" + +class RankCorr(Deterministic): + _min = 0 # Technically -1, but values below 0 are not as interesting + _max = 1 + _description = "Rank correlation between obesrvations and forecasts" + _perfectScore = 1 + def computeObsFcst(self, obs, fcst): + import scipy.stats + if(len(obs) <= 1): + return np.nan + return scipy.stats.spearmanr(obs,fcst)[0] + def name(self): + return "Rank correlation" + def label(self, data): + return "Rank correlation" + +class KendallCorr(Deterministic): + _min = 0 # Technically -1, but values below 0 are not as interesting + _max = 1 + _description = "Kendall correlation between obesrvations and forecasts" + _perfectScore = 1 + def computeObsFcst(self, obs, fcst): + import scipy.stats + if(len(obs) <= 1): + return np.nan + return scipy.stats.kendalltau(obs,fcst)[0] + def name(self): + return "Kendall correlation" + def label(self, data): + return "Kendall correlation" + +# Metrics based on 2x2 contingency table for a given threshold +class Threshold(Deterministic): + _reqThreshold = True + _supThreshold = True + # TODO: Which is correct? + # The second is best for precip, when doing Brier score -r 0 + @staticmethod + def within(x, range): + #return (x >= range[0]) & (x < range[1]) + return (x > range[0]) & (x <= range[1]) + +class Within(Threshold): + _min = 0 + _max = 100 + _description = "The percentage of forecasts within some error bound (use -r)" + _defaultBinType = "below" + _perfectScore = 100 + def computeObsFcst(self, obs, fcst): + diff = abs(obs - fcst) + return np.mean(self.within(diff, tRange))*100 + def name(self): + return "Within" + def label(self, data): + return "% of forecasts" + +# Mean y conditioned on x +# For a given range of x-values, what is the average y-value? +class Conditional(Threshold): + def __init__(self, x="obs", y="fcst", func=np.mean): + self._x = x + self._y = y + self._func = func + def computeCore(self, data, tRange): + [obs,fcst] = data.getScores([self._x, self._y]) + I = np.where(self.within(obs, tRange))[0] + return self._func(fcst[I]) + +# Mean x when conditioned on x +# Average x-value that is within a given range. The reason the y-variable is added +# is to ensure that the same data is used for this metric as for the Conditional metric. +class XConditional(Threshold): + def __init__(self, x="obs", y="fcst"): + self._x = x + self._y = y + def computeCore(self, data, tRange): + [obs,fcst] = data.getScores([self._x, self._y]) + I = np.where(self.within(obs, tRange))[0] + return np.median(obs[I]) + +class Count(Threshold): + def __init__(self, x): + self._x = x + def computeCore(self, data, tRange): + values = data.getScores(self._x) + I = np.where(self.within(values, tRange))[0] + return len(I) + +class Bs(Threshold): + _min = 0 + _max = 1 + _description = "Brier score" + _perfectScore = 0 + def __init__(self, numBins=10): + self._edges = np.linspace(0,1.0001,numBins) + def computeCore(self, data, tRange): + # Compute probabilities based on thresholds + p0 = 0 + p1 = 1 + if(tRange[0] != -np.inf and tRange[1] != np.inf): + var0 = data.getPvar(tRange[0]) + var1 = data.getPvar(tRange[1]) + [obs, p0, p1] = data.getScores(["obs", var0, var1]) + elif(tRange[0] != -np.inf): + var0 = data.getPvar(tRange[0]) + [obs, p0] = data.getScores(["obs", var0]) + elif(tRange[1] != np.inf): + var1 = data.getPvar(tRange[1]) + [obs, p1] = data.getScores(["obs", var1]) + obsP = self.within(obs, tRange) + p = p1 - p0 # Prob of obs within range + bs = np.nan*np.zeros(len(p), 'float') + + # Split into bins and compute Brier score on each bin + for i in range(0, len(self._edges)-1): + I = np.where((p >= self._edges[i]) & (p < self._edges[i+1]))[0] + bs[I] = (np.mean(p[I]) - obsP[I])**2 + return Common.nanmean(bs) + + @staticmethod + def getP(data, tRange): + p0 = 0 + p1 = 1 + if(tRange[0] != -np.inf and tRange[1] != np.inf): + var0 = data.getPvar(tRange[0]) + var1 = data.getPvar(tRange[1]) + [obs, p0, p1] = data.getScores(["obs", var0, var1]) + elif(tRange[0] != -np.inf): + var0 = data.getPvar(tRange[0]) + [obs, p0] = data.getScores(["obs", var0]) + elif(tRange[1] != np.inf): + var1 = data.getPvar(tRange[1]) + [obs, p1] = data.getScores(["obs", var1]) + + obsP = Threshold.within(obs, tRange) + p = p1 - p0 # Prob of obs within range + return [obsP, p] + + @staticmethod + def getQ(data, tRange): + p0 = 0 + p1 = 1 + var = data.getQvar(tRange[0]) + [obs, q] = data.getScores(["obs", var]) + + return [obs, q] + + def label(self, data): + return "Brier score" + +class Bss(Threshold): + _min = 0 + _max = 1 + _description = "Brier skill score" + _perfectScore = 1 + def __init__(self, numBins=10): + self._edges = np.linspace(0,1.0001,numBins) + def computeCore(self, data, tRange): + [obsP,p] = Bs.getP(data, tRange) + bs = np.nan*np.zeros(len(p), 'float') + for i in range(0, len(self._edges)-1): + I = np.where((p >= self._edges[i]) & (p < self._edges[i+1]))[0] + if(len(I) > 0): + bs[I] = (np.mean(p[I]) - obsP[I])**2 + bs = Common.nanmean(bs) + bsunc = np.mean(obsP)*(1-np.mean(obsP)) + if(bsunc == 0): + bss = np.nan + else: + bss = (bsunc - bs)/bsunc + + return bss + def label(self, data): + return "Brier skill score" + +class BsRel(Threshold): + _min = 0 + _max = 1 + _description = "Brier score, reliability term" + _perfectScore = 0 + def __init__(self, numBins=11): + self._edges = np.linspace(0,1.0001,numBins) + def computeCore(self, data, tRange): + [obsP,p] = Bs.getP(data, tRange) + + # Break p into bins, and comute reliability + bs = np.nan*np.zeros(len(p), 'float') + for i in range(0, len(self._edges)-1): + I = np.where((p >= self._edges[i]) & (p < self._edges[i+1]))[0] + if(len(I) > 0): + meanObsI = np.mean(obsP[I]) + bs[I] = (np.mean(p[I]) - meanObsI)**2 + return Common.nanmean(bs) + def label(self, data): + return "Brier score, reliability term" + +class BsUnc(Threshold): + _min = 0 + _max = 1 + _description = "Brier score, uncertainty term" + _perfectScore = None + def computeCore(self, data, tRange): + [obsP, p] = Bs.getP(data, tRange) + meanObs = np.mean(obsP) + bs = meanObs*(1 - meanObs) + return bs + def label(self, data): + return "Brier score, uncertainty term" + +class BsRes(Threshold): + _min = 0 + _max = 1 + _description = "Brier score, resolution term" + _perfectScore = 1 + def __init__(self, numBins=10): + self._edges = np.linspace(0,1.0001,numBins) + def computeCore(self, data, tRange): + [obsP, p] = Bs.getP(data, tRange) + bs = np.nan*np.zeros(len(p), 'float') + meanObs = np.mean(obsP) + for i in range(0, len(self._edges)-1): + I = np.where((p >= self._edges[i]) & (p < self._edges[i+1]))[0] + if(len(I) > 0): + meanObsI = np.mean(obsP[I]) + bs[I] = (meanObsI - meanObs)**2 + return Common.nanmean(bs) + def label(self, data): + return "Brier score, resolution term" + +class QuantileScore(Threshold): + _min = 0 + _description = "Quantile score. Requires quantiles to be stored"\ + "(e.g q10, q90...). Use -x to set which quantiles to use." + _perfectScore = 0 + def computeCore(self, data, tRange): + [obs,q] = Bs.getQ(data, tRange) + qs = np.nan*np.zeros(len(q), 'float') + v = q - obs + qs = v * (tRange[0] - (v < 0)) + return np.mean(qs) + +class Ign0(Threshold): + _description = "Ignorance of the binary probability based on threshold" + def computeCore(self, data, tRange): + [obsP,p] = Bs.getP(data, tRange) + + I0 = np.where(obsP == 0)[0] + I1 = np.where(obsP == 1)[0] + ign = -np.log2(p) + ign[I0] = -np.log2(1-p[I0]) + + return np.mean(ign) + + def label(self, data): + return "Binary Ignorance" + +class Spherical(Threshold): + _description = "Spherical probabilistic scoring rule for binary events" + _max = 1 + _min = 0 + def computeCore(self, data, tRange): + [obsP,p] = Bs.getP(data, tRange) + + I0 = np.where(obsP == 0)[0] + I1 = np.where(obsP == 1)[0] + sp = p / np.sqrt(p**2+(1-p)**2) + sp[I0] = (1-p[I0]) / np.sqrt((p[I0])**2+(1-p[I0])**2) + + return np.mean(sp) + + def label(self, data): + return "Spherical score" + +class Contingency(Threshold): + _min = 0 + _max = 1 + _defaultAxis = "threshold" + _reqThreshold = True + @staticmethod + def getAxisFormatter(self, data): + from matplotlib.ticker import ScalarFormatter + return ScalarFormatter() + def label(self, data): + return self.name() + def computeCore(self, data, tRange): + if(tRange == None): + Common.error("Metric " + self.getClassName() + " requires '-r '") + [obs,fcst] = data.getScores(["obs", "fcst"]) + value = np.nan + if(len(fcst) > 0): + # Compute frequencies + a = np.ma.sum((self.within(fcst,tRange)) & (self.within(obs, tRange))) # Hit + b = np.ma.sum((self.within(fcst,tRange)) & (self.within(obs, tRange)==0)) # FA + c = np.ma.sum((self.within(fcst,tRange)==0) & (self.within(obs, tRange))) # Miss + d = np.ma.sum((self.within(fcst,tRange)==0) & (self.within(obs, tRange)==0)) # CR + value = self.calc(a, b, c, d) + if(np.isinf(value)): + value = np.nan + + return value + def name(self): + return self.description() + +class Ets(Contingency): + _description = "Equitable threat score" + _perfectScore = 1 + def calc(self, a, b, c, d): + N = a + b + c + d + ar = (a + b) / 1.0 / N * (a + c) + if(a + b + c - ar == 0): + return np.nan + return (a - ar) / 1.0 / (a + b + c - ar) + def name(self): + return "ETS" + +class Threat(Contingency): + _description = "Threat score" + _perfectScore = 1 + def calc(self, a, b, c, d): + if(a + b + c == 0): + return np.nan + return a / 1.0 / (a + b + c) + +class Pc(Contingency): + _description = "Proportion correct" + _perfectScore = 1 + def calc(self, a, b, c, d): + return (a + d) / 1.0 / (a + b + c + d) + +class Diff(Contingency): + _description = "Difference between false alarms and misses" + _min = -1 + _max = 1 + _perfectScore = 0 + def calc(self, a, b, c, d): + return (b - c) / 1.0 / (b + c) + +class Edi(Contingency): + _description = "Extremal dependency index" + _perfectScore = 1 + def calc(self, a, b, c, d): + N = a + b + c + d + F = b / 1.0 / (b + d) + H = a / 1.0 / (a + c) + if(H == 0 or F == 0): + return np.nan + denom = (np.log(H) + np.log(F)) + if(denom == 0): + return np.nan + return (np.log(F) - np.log(H)) / denom + def name(self): + return "EDI" + +class Sedi(Contingency): + _description = "Symmetric extremal dependency index" + _perfectScore = 1 + def calc(self, a, b, c, d): + N = a + b + c + d + F = b / 1.0 / (b + d) + H = a / 1.0 / (a + c) + if(F == 0 or F == 1 or H == 0 or H == 1): + return np.nan + denom = np.log(F) + np.log(H) + np.log(1-F) + np.log(1-H) + if(denom == 0): + return np.nan + num = np.log(F) - np.log(H) - np.log(1-F) + np.log(1-H) + return num / denom + def name(self): + return "SEDI" + +class Eds(Contingency): + _description = "Extreme dependency score" + _min = None + _perfectScore = 1 + def calc(self, a, b, c, d): + N = a + b + c + d + H = a / 1.0 / (a + c) + p = (a+c)/1.0/N + if(H == 0 or p == 0): + return np.nan + denom = (np.log(p) + np.log(H)) + if(denom == 0): + return np.nan + + return (np.log(p) - np.log(H))/ denom + def name(self): + return "EDS" + +class Seds(Contingency): + _description = "Symmetric extreme dependency score" + _min = None + _perfectScore = 1 + def calc(self, a, b, c, d): + N = a + b + c + d + H = a / 1.0 / (a + c) + p = (a+c)/1.0/N + q = (a+b)/1.0/N + if(q == 0 or H == 0): + return np.nan + denom = np.log(p) + np.log(H) + if(denom == 0): + return np.nan + return (np.log(q) - np.log(H))/(np.log(p) + np.log(H)) + def name(self): + return "SEDS" + +class BiasFreq(Contingency): + _max = None + _description = "Bias frequency (number of fcsts / number of obs)" + _perfectScore = 1 + def calc(self, a, b, c, d): + if(a + c == 0): + return np.nan + return 1.0 * (a + b) / (a + c) + +class Hss(Contingency): + _max = None + _description = "Heidke skill score" + _perfectScore = 1 + def calc(self, a, b, c, d): + denom = ((a+c)*(c+d) + (a+b)*(b+d)) + if(denom == 0): + return np.nan + return 2.0*(a*d-b*c)/denom + +class BaseRate(Contingency): + _description = "Base rate" + _perfectScore = None + def calc(self, a, b, c, d): + if(a + b + c + d == 0): + return np.nan + return (a + c) / 1.0 / (a + b + c + d) + +class Or(Contingency): + _description = "Odds ratio" + _max = None + _perfectScore = None # Should be infinity + def calc(self, a, b, c, d): + if(b * c == 0): + return np.nan + return (a * d) / 1.0 / (b * c) + +class Lor(Contingency): + _description = "Log odds ratio" + _max = None + _perfectScore = None # Should be infinity + def calc(self, a, b, c, d): + if(a * d == 0 or b * c == 0): + return np.nan + return np.log((a * d) / 1.0 / (b * c)) + +class YulesQ(Contingency): + _description = "Yule's Q (Odds ratio skill score)" + _perfectScore = 1 + def calc(self, a, b, c, d): + if(a * d + b * c == 0): + return np.nan + return (a * d - b * c) / 1.0 / (a * d + b * c) + +class Kss(Contingency): + _description = "Hanssen-Kuiper skill score" + _perfectScore = 1 + def calc(self, a, b, c, d): + if((a + c)*(b + d) == 0): + return np.nan + return (a*d-b*c)* 1.0 / ((a + c)*(b + d)) + +class Hit(Contingency): + _description = "Hit rate" + _perfectScore = 1 + def calc(self, a, b, c, d): + if(a + c == 0): + return np.nan + return a / 1.0 / (a + c) + +class Miss(Contingency): + _description = "Miss rate" + _perfectScore = 0 + def calc(self, a, b, c, d): + if(a + c == 0): + return np.nan + return c / 1.0 / (a + c) + +# Fraction of non-events that are forecasted as events +class Fa(Contingency): + _description = "False alarm rate" + _perfectScore = 0 + def calc(self, a, b, c, d): + if(b+d == 0): + return np.nan + return b / 1.0 / (b + d) + +# Fraction of forecasted events that are false alarms +class Far(Contingency): + _description = "False alarm ratio" + _perfectScore = 0 + def calc(self, a, b, c, d): + if(a + b == 0): + return np.nan + return b / 1.0 / (a + b) diff --git a/verif/Output.py b/verif/Output.py new file mode 100644 index 0000000..e731e75 --- /dev/null +++ b/verif/Output.py @@ -0,0 +1,2035 @@ +# -*- coding: ISO-8859-1 -*- +import matplotlib.pyplot as mpl +import re +import datetime +import verif.Common as Common +import verif.Metric as Metric +import numpy as np +import sys +reload(sys) +sys.setdefaultencoding('ISO-8859-1') +#from matplotlib.dates import * +import os +import inspect + +def getAllOutputs(): + temp = inspect.getmembers(sys.modules[__name__], inspect.isclass) + return temp + +def isNumber(s): # tchui (25/05/15) + try: + float(s) + return True + except ValueError: + return False + +class Output: + _description = "" + _defaultAxis = "offset" + _defaultBinType = "above" + _reqThreshold = False + _supThreshold = True + _supX = True + _experimental = False + _legLoc = "best" # Where should the legend go? + + def __init__(self): + self._filename = None + self._thresholds = [None] + leg = None + self.default_lines = ['-','-','-','--'] + self.default_markers = ['o', '', '.', ''] + self.default_colors = ['r', 'b', 'g', [1,0.73,0.2], 'k'] + self._lc = None + self._ls = None + self.colors = None + self.styles = None + self._ms = 8 + self._lw = 2 + self._labfs = 16 + self._tickfs = 16 + self._legfs = 16 + self._figsize = [5,8] + self._showMargin = True + self._xrot = 0 + self._minlth = None + self._majlth = None + self._majwid = None + self._bot = None ####### + self._top = None ####### + self._left = None ####### + self._right = None ####### + #self._pad = pad ###### + self._xaxis = self.defaultAxis() + self._binType = self.defaultBinType() + self._showPerfect = False + self._dpi = 100 + self._xlim = None + self._ylim = None + self._clim = None + self._title = None + self._xlabel = None + self._ylabel = None + self._xticks = None + self._yticks = None + self._tight = False + + @classmethod + def defaultAxis(cls): + return cls._defaultAxis + + @classmethod + def defaultBinType(cls): + return cls._defaultBinType + + @classmethod + def requiresThresholds(cls): + return cls._reqThreshold + + @classmethod + def supportsX(cls): + return cls._supX + + @classmethod + def supportsThreshold(cls): + return cls._supThreshold + + @classmethod + def description(cls): + extra = "" + if(cls._experimental): + extra = " " + Common.experimental() + return cls._description + extra + + @classmethod + def summary(cls): + return cls.description() + + # Produce output independently for each value along this axis + def setAxis(self, axis): + if(axis != None): + self._xaxis = axis + + def setBinType(self, binType): + if(binType != None): + self._binType = binType + + def setThresholds(self, thresholds): + if(thresholds == None): + thresholds = [None] + thresholds = np.array(thresholds) + self._thresholds = thresholds + def setFigsize(self, size): + self._figsize = size + def setFilename(self, filename): + self._filename = filename + def setLegend(self, legend): + self._legNames = legend + def setLegLoc(self, legLoc): # tchui (25/05/15) + self._legLoc = legLoc + def setShowMargin(self, showMargin): + self._showMargin = showMargin + def setDpi(self, dpi): + self._dpi = dpi + def setXLim(self, lim): + if(len(lim) != 2): + Common.error("xlim must be a vector of length 2") + self._xlim = lim + def setYLim(self, lim): + if(len(lim) != 2): + Common.error("ylim must be a vector of length 2") + self._ylim = lim + def setCLim(self, lim): + if(len(lim) != 2): + Common.error("clim must be a vector of length 2") + self._clim = lim + def setMarkerSize(self, ms): + self._ms = ms + def setLineWidth(self, lw): + self._lw = lw + def setLineColor(self,lc): + self._lc = lc + def setLineStyle(self,ls): + self._ls = ls + def setTickFontSize(self, fs): + self._tickfs = fs + def setLabFontSize(self, fs): + self._labfs = fs + def setLegFontSize(self, fs): + self._legfs = fs + def setXRotation(self, xrot): + self._xrot = xrot + def setMinorLength(self, minlth): + self._minlth = minlth + def setMajorLength(self, majlth): + self._majlth = majlth + def setMajorWidth(self, majwid): + self._majwid = majwid + def setBottom(self, bot): + self._bot = bot + def setTop(self, top): + self._top = top + def setLeft(self, left): + self._left = left + def setRight(self, right): + self._right = right + #def setPad(self, pad): + # self._pad = pad + def setShowPerfect(self, showPerfect): + self._showPerfect = showPerfect + def setYlabel(self, ylabel): + self._ylabel = ylabel + def setXlabel(self, xlabel): + self._xlabel = xlabel + def setTitle(self, title): + self._title = title + def setXticks(self, xticks): + self._xticks = xticks + def setYticks(self, yticks): + self._yticks = yticks + def setTight(self,tight): #potato + self._tight = tight + + + # Public + # Call this to create a plot, saves to file + def plot(self, data): + self._plotCore(data) + self._adjustAxes() + self._legend(data, self._legNames) + self._savePlot(data) + # Call this to write text output + def text(self, data): + self._textCore(data) + # Draws a map of the data + def map(self, data): + self._mapCore(data) + #self._legend(data, self._legNames) + self._savePlot(data) + + def _getLegendNames(self, data): + if(self._legNames != None): + names = self._legNames + else: + names = data.getShortNames() + return(names) + + def _plotPerfectScore(self, x, perfect, color="gray", zorder=-1000): + if(perfect == None): + return + if(self._showPerfect): + # Make 'perfect' same length as 'x' + if(not hasattr(perfect, "__len__")): + perfect = perfect*np.ones(len(x), 'float') + mpl.plot(x, perfect, '-', lw=7, color=color, label="ideal", zorder=zorder) + + # Implement these methods + def _plotCore(self, data): + Common.error("This type does not plot") + def _textCore(self, data): + Common.error("This type does not output text") + def _mapCore(self, data): + Common.error("This type does not produce maps") + + # Helper functions + def _getColor(self, i, total): + if(self._lc != None): + firstList = self._lc.split(",") + numList = [] + finalList = [] + + for string in firstList: + if("[" in string): # for rgba args + if(not numList): + string = string.replace("[","") + numList.append(float(string)) + else: + Common.error("Invalid rgba arg \"{}\"".format(string)) + + elif("]" in string): + if(numList): + string = string.replace("]","") + numList.append(float(string)) + finalList.append(numList) + numList = [] + else: + Common.error("Invalid rgba arg \"{}\"".format(string)) + + elif(isNumber(string)): # append to rgba lists if present, otherwise grayscale intensity + if(numList): + numList.append(float(string)) + else: + finalList.append(string) + + else: + if(not numList): # string args and hexcodes + finalList.append(string) + else: + Common.error("Cannot read color args.") + self.colors = finalList + return self.colors[i % len(self.colors)] + + else: # use default colours if no colour input given + self.colors = self.default_colors + return self.colors[i % len(self.default_colors)] + + + def _getStyle(self, i, total, connectingLine=True, lineOnly=False): # edited by tchui (25/05/15) + if(self._ls != None): + listStyles = self._ls.split(",") + I = i % len(listStyles) # loop through input linestyles (independent of colors) + return listStyles[I] + + else: # default linestyles + I = (i / len(self.colors)) % len(self.default_lines) + line = self.default_lines[I] + marker = self.default_markers[I] + if(lineOnly): + return line + if(connectingLine): + return line + marker + return marker + + + # Saves to file, set figure size + def _savePlot(self, data): + if(self._figsize != None): + mpl.gcf().set_size_inches(int(self._figsize[0]), int(self._figsize[1])) + if(not self._showMargin): + Common.removeMargin() + if(self._filename != None): + mpl.savefig(self._filename, bbox_inches='tight', dpi=self._dpi) + else: + fig = mpl.gcf() + fig.canvas.set_window_title(data.getFilenames()[0]) + mpl.show() + def _legend(self, data, names=None): + if(names == None): + mpl.legend(loc=self._legLoc,prop={'size':self._legfs}) + else: + mpl.legend(names, loc=self._legLoc,prop={'size':self._legfs}) + def _getThresholdLimits(self, thresholds): + x = thresholds + if(self._binType == "below"): + lowerT = [-np.inf for i in range(0, len(thresholds))] + upperT = thresholds + elif(self._binType == "above"): + lowerT = thresholds + upperT = [np.inf for i in range(0, len(thresholds))] + elif(self._binType == "within"): + lowerT = thresholds[0:-1] + upperT = thresholds[1:] + x = [(lowerT[i] + upperT[i])/2 for i in range(0, len(lowerT))] + else: + Common.error("Unrecognized bintype") + return [lowerT,upperT,x] + + def _setYAxisLimits(self, metric): + currYlim = mpl.ylim() + ylim = [metric.min(), metric.max()] + if(ylim[0] == None): + ylim[0] = currYlim[0] + if(ylim[1] == None): + ylim[1] = currYlim[1] + mpl.ylim(ylim) + + def _adjustAxes(self): + # Apply adjustements to all subplots + for ax in mpl.gcf().get_axes(): + # Tick font sizes + for tick in ax.xaxis.get_major_ticks(): + tick.label.set_fontsize(self._tickfs) + for tick in ax.yaxis.get_major_ticks(): + tick.label.set_fontsize(self._tickfs) + ax.set_xlabel(ax.get_xlabel(), fontsize=self._labfs) + ax.set_ylabel(ax.get_ylabel(), fontsize=self._labfs) + #mpl.rcParams['axes.labelsize'] = self._labfs + + # Tick lines + if(len(mpl.yticks()[0]) >= 2 and len(mpl.xticks()[0]) >= 2): + # matplotlib crashes if there are fewer than 2 tick lines + # when determining where to put minor ticks + mpl.minorticks_on() + if(not self._minlth == None): + mpl.tick_params('both', length=self._minlth, which='minor') + if(not self._majlth == None): + mpl.tick_params('both', length=self._majlth, width=self._majwid, which='major') + for label in ax.get_xticklabels(): + label.set_rotation(self._xrot) + + for ax in mpl.gcf().get_axes(): + if(self._xlim != None): + mpl.xlim(self._xlim) + if(self._ylim != None): + mpl.ylim(self._ylim) + if(self._clim != None): + mpl.clim(self._clim) + + # Labels + if(self._xlabel != None): + mpl.xlabel(self._xlabel) + if(self._ylabel != None): + mpl.ylabel(self._ylabel) + if(self._title != None): + mpl.title(self._title) + + # Ticks + if(self._xticks != None): + if(len(self._xticks) <= 1): + Common.error("Xticks must have at least 2 values") + mpl.xticks(self._xticks) + if(self._yticks != None): + if(len(self._yticks) <= 1): + Common.error("Yticks must have at least 2 values") + mpl.yticks(self._yticks) + + # Margins + mpl.gcf().subplots_adjust(bottom=self._bot, top=self._top, left=self._left, right=self._right) + + def _plotObs(self, x, y, isCont=True, zorder=0): + if(isCont): + mpl.plot(x, y, ".-", color="gray", lw=5, label="obs", zorder=zorder) + else: + mpl.plot(x, y, "o", color="gray", ms=self._ms, label="obs", zorder=zorder) + + # maxradius: Don't let the circle go outside an envelope circle with this radius (centered on the origin) + def _drawCircle(self, radius, xcenter=0, ycenter=0, maxradius=np.inf, style="--", color="k", lw=1, label=""): + angles = np.linspace(-np.pi/2, np.pi/2, 360) + x = np.sin(angles)*radius + xcenter + y = np.cos(angles)*radius + ycenter + + # Only keep points within the circle + I = np.where(x**2+y**2 < maxradius**2)[0] + if(len(I) == 0): + return + x = x[I] + y = y[I] + mpl.plot(x, y,style,color=color,lw=lw, zorder=-100, label=label) + mpl.plot(x, -y,style,color=color,lw=lw, zorder=-100) + + def _plotConfidence(self, x, y, variance, n, color): + #variance = y*(1-y) # For bins + + # Remove missing points + I = np.where(n != 0)[0] + if(len(I) == 0): + return + x = x[I] + y = y[I] + variance = variance[I] + n = n[I] + + z = 1.96 # 95% confidence interval + type = "wilson" + style = "--" + if type == "normal": + mean = y + lower = mean - z*np.sqrt(variance/n) + upper = mean + z*np.sqrt(variance/n) + elif type == "wilson": + mean = 1/(1+1.0/n*z**2) * ( y + 0.5*z**2/n) + upper = mean + 1/(1+1.0/n*z**2)*z*np.sqrt(variance/n + 0.25*z**2/n**2) + lower = mean - 1/(1+1.0/n*z**2)*z*np.sqrt(variance/n + 0.25*z**2/n**2) + mpl.plot(x, upper, style, color=color, lw=self._lw, ms=self._ms,label="") + mpl.plot(x, lower, style, color=color, lw=self._lw, ms=self._ms,label="") + Common.fill(x, lower, upper, color, alpha=0.3) + +class Default(Output): + _legLoc = "upper left" + def __init__(self, metric): + Output.__init__(self) + # offsets, dates, location, locationElev, threshold + self._metric = metric + if(metric.defaultAxis() != None): + self._xaxis = metric.defaultAxis() + if(metric.defaultBinType() != None): + self._binType = metric.defaultBinType() + self._showRank = False + self._showAcc = False + self._setLegSort = False + + # Settings + self._mapLowerPerc = 0 # Lower percentile (%) to show in colourmap + self._mapUpperPerc = 100 # Upper percentile (%) to show in colourmap + self._mapLabelLocations = False # Show locationIds in map? + + def setShowRank(self, showRank): + self._showRank = showRank + + def setLegSort(self,dls): + self._setLegSort = dls + + def setShowAcc(self, showAcc): + self._showAcc = showAcc + + def getXY(self, data): + thresholds = self._thresholds + axis = data.getAxis() + + [lowerT,upperT,xx] = self._getThresholdLimits(thresholds) + if(axis != "threshold"): + xx = data.getAxisValues() + + filenames = data.getFilenames() + F = data.getNumFiles() + y = None + x = None + for f in range(0, F): + data.setFileIndex(f) + yy = np.zeros(len(xx), 'float') + if(axis == "threshold"): + for i in range(0, len(lowerT)): + yy[i] = self._metric.compute(data, [lowerT[i], upperT[i]]) + else: + for i in range(0, len(lowerT)): + yy = yy + self._metric.compute(data, [lowerT[i], upperT[i]]) + yy = yy / len(thresholds) + + if(sum(np.isnan(yy)) == len(yy)): + Common.warning("No valid scores for " + filenames[f]) + if(y == None): + y = np.zeros([F, len(yy)],'float') + x = np.zeros([F, len(xx)],'float') + y[f,:] = yy + x[f,:] = xx + if(self._showAcc): + y[f,:] = np.nan_to_num(y[f,:]) + y[f,:] = np.cumsum(y[f,:]) + return [x,y] + + def _legend(self, data, names=None): + mpl.legend(loc=self._legLoc,prop={'size':self._legfs}) + + def _plotCore(self, data): + + data.setAxis(self._xaxis) + + # We have to derive the legend list here, because we might want to specify + # the order + labels = np.array(data.getFilenames()) + if(self._legNames): # append legend names to file list + try: + labels[0:len(self._legNames)]=self._legNames + except ValueError: + Common.error("Too many legend names") + + self._legNames = labels + + F = data.getNumFiles() + [x,y] = self.getXY(data) + + # Sort legend entries such that the appear in the same order as the y-values of + # the lines + if(self._setLegSort): + if(not self._showAcc): + averages = (Common.nanmean(y,axis=1)) # averaging for non-acc plots + ids = averages.argsort()[::-1] + + else: + ends = y[:,-1] # take last points for acc plots + ids = ends.argsort()[::-1] + + self._legNames = [self._legNames[i] for i in ids] + + else: + ids = range(0,F) + + if(self._xaxis == "none"): + w = 0.8 + x = np.linspace(1-w/2,len(y)-w/2,len(y)) + mpl.bar(x,y, color='w', lw=self._lw) + mpl.xticks(range(1,len(y)+1), labels) + else: + for f in range(0, F): + color = self._getColor(ids[f], F) # colors and styles to follow labels + style = self._getStyle(ids[f], F, data.isAxisContinuous()) + alpha = (1 if(data.isAxisContinuous()) else 0.55) + mpl.plot(x[ids[f]], y[ids[f]], style, color=color, label=self._legNames[f], lw=self._lw, ms=self._ms, alpha=alpha) + + mpl.xlabel(data.getAxisLabel()) + mpl.ylabel(self._metric.label(data)) + + mpl.gca().xaxis.set_major_formatter(data.getAxisFormatter()) + perfectScore = self._metric.perfectScore() + self._plotPerfectScore(x[0], perfectScore) + + mpl.grid() + if(not self._showAcc): + self._setYAxisLimits(self._metric) + + if(self._tight): + oldTicks=mpl.gca().get_xticks() + diff = oldTicks[1] - oldTicks[0] # keep auto tick interval + tickRange = np.arange(round(np.min(x)),round(np.max(x))+diff,diff) + mpl.gca().set_xticks(tickRange) # make new ticks, to start from the first day of the desired interval + mpl.autoscale(enable=True,axis=u'x',tight=True) # make xaxis tight + + def _textCore(self, data): + thresholds = self._thresholds + + data.setAxis(self._xaxis) + + # Set configuration names + names = self._getLegendNames(data) + + F = data.getNumFiles() + [x,y] = self.getXY(data) + + if(self._filename != None): + sys.stdout = open(self._filename, 'w') + + maxlength = 0 + for name in names: + maxlength = max(maxlength, len(name)) + maxlength = str(maxlength) + + # Header line + fmt = "%-"+maxlength+"s" + lineDesc = data.getAxisDescriptionHeader() + lineDescN = len(lineDesc) + 2 + lineDescFmt = "%-" + str(lineDescN) + "s |" + print lineDescFmt % lineDesc, + if(data.getAxis() == "threshold"): + descs = self._thresholds + else: + descs = data.getAxisDescriptions() + for name in names: + print fmt % name, + print "" + + # Loop over rows + for i in range(0, len(x[0])): + print lineDescFmt % descs[i], + self._printLine(y[:,i], maxlength, "float") + + # Print stats + for func in [Common.nanmin, Common.nanmean, Common.nanmax, Common.nanstd]: + name = func.__name__[3:] + print lineDescFmt % name, + values = np.zeros(F, 'float') + for f in range(0,F): + values[f] = func(y[f,:]) + self._printLine(values, maxlength, "float") + + # Print count stats + for func in [Common.nanmin, Common.nanmax]: + name = func.__name__[3:] + print lineDescFmt % ("num " + name), + values = np.zeros(F, 'float') + for f in range(0,F): + values[f] = np.sum(y[f,:] == func(y,axis=0)) + self._printLine(values, maxlength, "int") + + def _printLine(self, values , colWidth, type="float"): + if(type == "int"): + fmt = "%-"+colWidth+"i" + else: + fmt = "%-"+colWidth+".2f" + missfmt = "%-"+colWidth+"s" + minI = np.argmin(values) + maxI = np.argmax(values) + for f in range(0, len(values)): + value = values[f] + if(np.isnan(value)): + txt = missfmt % "--" + else: + txt = fmt % value + if(minI == f): + print Common.green(txt), + elif(maxI == f): + print Common.red(txt), + else: + print txt, + print "" + + def _mapCore(self, data): + # Use the Basemap package if it is available + # Note that the word 'map' is an object if Basemap is loaded + # otherwise it is a shorthand name for matplotlib. This is possible + # because Basemap shares the plotting command names with matplotlib + hasBasemap = True + try: + from mpl_toolkits.basemap import Basemap + except ImportError: + Common.warning("Cannot load Basemap package") + import matplotlib.pylab as map + hasBasemap = False + + data.setAxis("location") + labels = self._getLegendNames(data) + F = data.getNumFiles() + lats = data.getLats() + lons = data.getLons() + ids = data.getLocationIds() + dlat = (max(lats) - min(lats)) + dlon = (max(lons) - min(lons)) + llcrnrlat= max(-90, min(lats) - dlat/10) + urcrnrlat= min(90, max(lats) + dlat/10) + llcrnrlon= min(lons) - dlon/10 + urcrnrlon= max(lons) + dlon/10 + res = Common.getMapResolution(lats, lons) + dx = pow(10,np.ceil(np.log10(max(lons) - min(lons))))/10 + dy = pow(10,np.ceil(np.log10(max(lats) - min(lats))))/10 + [x,y] = self.getXY(data) + + # Colorbar limits should be the same for all subplots + clim = [Common.nanpercentile(y.flatten(), self._mapLowerPerc), + Common.nanpercentile(y.flatten(), self._mapUpperPerc)] + + symmetricScore = False + cmap=mpl.cm.jet + if(clim[0] < 0 and clim[1] > 0): + symmetricScore = True + clim[0] = -max(-clim[0],clim[1]) + clim[1] = -clim[0] + cmap=mpl.cm.RdBu + + # Forced limits + if(self._clim != None): + clim = self._clim + + std = Common.nanstd(y) + minDiff = std/50 + + for f in range(0, F): + Common.subplot(f,F) + if(hasBasemap): + map = Basemap(llcrnrlon=llcrnrlon,llcrnrlat=llcrnrlat,urcrnrlon=urcrnrlon,urcrnrlat=urcrnrlat,projection='mill', resolution=res) + map.drawcoastlines(linewidth=0.25) + map.drawcountries(linewidth=0.25) + map.drawmapboundary() + # map.drawparallels(np.arange(-90.,120.,dy),labels=[1,0,0,0]) + # map.drawmeridians(np.arange(0.,420.,dx),labels=[0,0,0,1]) + map.fillcontinents(color='coral',lake_color='aqua', zorder=-1) + x0, y0 = map(lons, lats) + else: + x0 = lons + y0 = lats + I = np.where(np.isnan(y[f,:]))[0] + map.plot(x0[I], y0[I], 'kx') + + isMax = (y[f,:] == np.amax(y,0)) & (y[f,:] > np.mean(y,0)+minDiff) + isMin = (y[f,:] == np.amin(y,0)) & (y[f,:] < np.mean(y,0)-minDiff) + isValid = (np.isnan(y[f,:])==0) + if(self._showRank): + lmissing = map.scatter(x0[I], y0[I], s=40, c="k", marker="x") + lsimilar = map.scatter(x0[isValid], y0[isValid], s=40, c="w") + lmax = map.scatter(x0[isMax], y0[isMax], s=40, c="r") + lmin = map.scatter(x0[isMin], y0[isMin], s=40, c="b") + else: + s = 40 + map.scatter(x0, y0, c=y[f,:], s=s, cmap=cmap)#, linewidths = 1 + 2*isMax) + cb = map.colorbar() + cb.set_label(self._metric.label(data)) + cb.set_clim(clim) + mpl.clim(clim) + if(self._mapLabelLocations): + for i in range(0,len(x0)): + #mpl.text(x0[i], y0[i], "(%d,%d)" % (i,locs[i])) + value = y[f,i] + #if(value == max(y[:,i])): + # mpl.plot(x0[i], y0[i], 'ko', mfc=None, mec="k", ms=10) + + if(not np.isnan(value)): + #if(isMax[i]): + # mpl.plot(x0[i], y0[i], 'w.', ms=30, alpha=0.2) + mpl.text(x0[i], y0[i], "%d %3.2f" % (ids[i],value)) + if(self._legNames != None): + names = self._legNames + else: + names = data.getFilenames() + mpl.title(names[f]) + + # Legend + if(self._showRank): + lines = [lmin,lsimilar,lmax,lmissing] + names = ["min", "similar", "max", "missing"] + mpl.figlegend(lines, names, "lower center", ncol=4) + +class Hist(Output): + _reqThreshold = True + _supThreshold = False + def __init__(self, name): + Output.__init__(self) + self._name = name + + # Settings + self._showPercent = True + def getXY(self, data): + F = data.getNumFiles() + allValues = [0]*F + edges = self._thresholds + for f in range(0, F): + data.setFileIndex(f) + allValues[f] = data.getScores(self._name) + + xx = (edges[0:-1]+edges[1:])/2 + y = np.zeros([F, len(xx)],'float') + x = np.zeros([F, len(xx)],'float') + for f in range(0, F): + data.setFileIndex(f) + N = len(allValues[f][0]) + + for i in range(0, len(xx)): + if(i == len(xx)-1): + I = np.where((allValues[f][0] >= edges[i]) & (allValues[f][0] <= edges[i+1]))[0] + else: + I = np.where((allValues[f][0] >= edges[i]) & (allValues[f][0] < edges[i+1]))[0] + y[f,i] = len(I)*1.0#/N + x[f,:] = xx + return [x,y] + + def _plotCore(self, data): + data.setAxis("none") + labels = self._getLegendNames(data) + F = data.getNumFiles() + [x,y] = self.getXY(data) + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + if(self._showPercent): + y[f]= y[f]* 1.0 / sum(y[f]) * 100 + mpl.plot(x[f], y[f], style, color=color, label=labels[f], lw=self._lw, ms=self._ms) + mpl.xlabel(data.getAxisLabel("threshold")) + if(self._showPercent): + mpl.ylabel("Frequency (%)") + else: + mpl.ylabel("Frequency") + mpl.grid() + + def _textCore(self, data): + data.setAxis("none") + labels = self._getLegendNames(data) + + F = data.getNumFiles() + [x,y] = self.getXY(data) + + if(self._filename != None): + sys.stdout = open(self._filename, 'w') + + maxlength = 0 + for label in labels: + maxlength = max(maxlength, len(label)) + maxlength = str(maxlength) + + # Header line + fmt = "%-"+maxlength+"s" + lineDesc = data.getAxisDescriptionHeader() + lineDescN = len(lineDesc) + 2 + lineDescFmt = "%-" + str(lineDescN) + "s |" + print lineDescFmt % lineDesc, + descs = self._thresholds + for label in labels: + print fmt % label, + print "" + + # Loop over rows + for i in range(0, len(x[0])): + print lineDescFmt % descs[i], + self._printLine(y[:,i], maxlength, "int") + + # Print count stats + for func in [Common.nanmin, Common.nanmax]: + name = func.__name__[3:] + print lineDescFmt % ("num " + name), + values = np.zeros(F, 'float') + for f in range(0,F): + values[f] = np.sum(y[f,:] == func(y,axis=0)) + self._printLine(values, maxlength, "int") + + def _printLine(self, values , colWidth, type="float"): + if(type == "int"): + fmt = "%-"+colWidth+"i" + else: + fmt = "%-"+colWidth+".2f" + missfmt = "%-"+colWidth+"s" + minI = np.argmin(values) + maxI = np.argmax(values) + for f in range(0, len(values)): + value = values[f] + if(np.isnan(value)): + txt = missfmt % "--" + else: + txt = fmt % value + if(minI == f): + print Common.green(txt), + elif(maxI == f): + print Common.red(txt), + else: + print txt, + print "" + +class Sort(Output): + _reqThreshold = False + _supThreshold = False + def __init__(self, name): + Output.__init__(self) + self._name = name + + def _plotCore(self, data): + data.setAxis("none") + labels = self._getLegendNames(data) + F = data.getNumFiles() + for f in range(0, F): + data.setFileIndex(f) + [x] = data.getScores(self._name) + x = np.sort(x) + color = self._getColor(f, F) + style = self._getStyle(f, F) + y = np.linspace(0, 1, x.shape[0]) + mpl.plot(x, y, style, color=color, label=labels[f], lw=self._lw, ms=self._ms) + mpl.xlabel("Sorted " + data.getAxisLabel("threshold")) + mpl.grid() + + +class ObsFcst(Output): + _supThreshold = False + _description = "Plot observations and forecasts" + def __init__(self): + Output.__init__(self) + def _plotCore(self, data): + F = data.getNumFiles() + data.setAxis(self._xaxis) + x = data.getAxisValues() + + isCont = data.isAxisContinuous() + + # Obs line + mObs = Metric.Mean(Metric.Default("obs")) + y = mObs.compute(data, None) + self._plotObs(x, y, isCont) + + mFcst = Metric.Mean(Metric.Default("fcst")) + labels = data.getFilenames() + for f in range(0, F): + data.setFileIndex(f) + color = self._getColor(f, F) + style = self._getStyle(f, F, isCont) + + y = mFcst.compute(data, None) + mpl.plot(x, y, style, color=color, label=labels[f], lw=self._lw, + ms=self._ms) + mpl.ylabel(data.getVariableAndUnits()) + mpl.xlabel(data.getAxisLabel()) + mpl.grid() + mpl.gca().xaxis.set_major_formatter(data.getAxisFormatter()) + +class QQ(Output): + _supThreshold = False + _supX = False + _description = "Quantile-quantile plot of obs vs forecasts" + def __init__(self): + Output.__init__(self) + def getXY(self, data): + x = list() + y = list() + F = len(data.getFilenames()) + for f in range(0, F): + data.setFileIndex(f) + [xx,yy] = data.getScores(["obs", "fcst"]) + x.append(np.sort(xx)) + y.append(np.sort(yy)) + return [x,y] + + def _plotCore(self, data): + data.setAxis("none") + data.setIndex(0) + labels = data.getFilenames() + F = data.getNumFiles() + [x,y] = self.getXY(data) + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + + mpl.plot(x[f], y[f], style, color=color, label=labels[f], lw=self._lw, + ms=self._ms) + mpl.ylabel("Sorted forecasts (" + data.getUnits() + ")") + mpl.xlabel("Sorted observations (" + data.getUnits() + ")") + ylim = list(mpl.ylim()) + xlim = list(mpl.xlim()) + axismin = min(min(ylim),min(xlim)) + axismax = max(max(ylim),max(xlim)) + #mpl.plot([axismin,axismax], [axismin,axismax], "--", color=[0.3,0.3,0.3], lw=3, zorder=-100) + self._plotPerfectScore([axismin,axismax], [axismin,axismax]) + mpl.grid() + def _textCore(self, data): + data.setAxis("none") + data.setIndex(0) + labels = data.getFilenames() + F = data.getNumFiles() + + # Header + maxlength = 0 + for name in data.getFilenames(): + maxlength = max(maxlength, len(name)) + maxlength = int(np.ceil(maxlength/2)*2) + fmt = "%"+str(maxlength)+"s" + for filename in data.getFilenames(): + print fmt % filename, + print "" + fmt = "%" + str(int(np.ceil(maxlength/2))) + ".1f" + fmt = fmt + fmt + fmtS = "%" + str(int(np.ceil(maxlength/2))) + "s" + fmtS = fmtS + fmtS + for f in range(0, F): + print fmtS % ("obs", "fcst"), + print "" + + [x,y] = self.getXY(data) + maxPairs = len(x[0]) + for f in range(1, F): + maxPairs = max(maxPairs, len(x[f])) + for i in range(0, maxPairs): + for f in range(0, F): + if(len(x[f]) < i): + print " -- -- " + else: + print fmt % (x[f][i], y[f][i]), + print "\n", + +class Scatter(Output): + _description = "Scatter plot of forecasts vs obs" + _supThreshold = False + _supX = False + def __init__(self): + Output.__init__(self) + def _plotCore(self, data): + data.setAxis("none") + data.setIndex(0) + labels = data.getFilenames() + F = data.getNumFiles() + for f in range(0, F): + data.setFileIndex(f) + color = self._getColor(f, F) + style = self._getStyle(f, F, connectingLine=False) + + [x,y] = data.getScores(["obs","fcst"]) + mpl.plot(x,y, ".", color=color, label=labels[f], lw=self._lw, + ms=self._ms, alpha=0.2) + mpl.ylabel("Forecasts (" + data.getUnits() + ")") + mpl.xlabel("Observations (" + data.getUnits() + ")") + ylim = mpl.ylim() + xlim = mpl.xlim() + axismax = max(max(ylim),max(xlim)) + mpl.plot([0,axismax], [0,axismax], "--", color=[0.3,0.3,0.3], lw=3, zorder=-100) + mpl.grid() + +class Change(Output): + _supThreshold = False + _supX = False + _description = "Forecast skill (MAE) as a function of change in obs from previous day" + def __init__(self): + Output.__init__(self) + + def _plotCore(self, data): + data.setAxis("all") + data.setIndex(0) + labels = data.getFilenames() + # Find range + data.setFileIndex(0) + [obs,fcst] = data.getScores(["obs", "fcst"]) + change = obs[1:,Ellipsis]-obs[0:-1,Ellipsis] + maxChange = np.nanmax(abs(change.flatten())) + edges = np.linspace(-maxChange,maxChange,20) + bins = (edges[1:] + edges[0:-1])/2 + F = data.getNumFiles() + + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + data.setFileIndex(f) + [obs,fcst] = data.getScores(["obs", "fcst"]) + change = obs[1:,Ellipsis]-obs[0:-1,Ellipsis] + err = abs(obs-fcst) + err = err[1:,Ellipsis] + x = np.nan * np.zeros(len(bins), 'float') + y = np.nan * np.zeros(len(bins), 'float') + + for i in range(0, len(bins)): + I = (change > edges[i] ) & (change <= edges[i+1]) + y[i] = Common.nanmean(err[I]) + x[i] = Common.nanmean(change[I]) + mpl.plot(x, y, style, color=color, lw=self._lw, ms=self._ms, label=labels[f]) + self._plotPerfectScore(x, 0) + mpl.xlabel("Daily obs change (" + data.getUnits() + ")") + mpl.ylabel("MAE (" + data.getUnits() + ")") + mpl.grid() + +class Cond(Output): + _description = "Plots forecasts as a function of obs (use -r to specify bin-edges)" + _defaultAxis = "threshold" + _defaultBinType = "within" + _reqThreshold = True + _supThreshold = True + _supX = False + def supportsThreshold(self): + return True + def _plotCore(self, data): + data.setAxis("none") + data.setIndex(0) + [lowerT,upperT,x] = self._getThresholdLimits(self._thresholds) + + labels = data.getFilenames() + F = data.getNumFiles() + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + data.setFileIndex(f) + + of = np.zeros(len(x), 'float') + fo = np.zeros(len(x), 'float') + xof = np.zeros(len(x), 'float') + xfo = np.zeros(len(x), 'float') + mof = Metric.Conditional("obs", "fcst", np.mean) # F | O + mfo = Metric.Conditional("fcst", "obs", np.mean) # O | F + xmof = Metric.XConditional("obs", "fcst") # F | O + xmfo = Metric.XConditional("fcst", "obs") # O | F + mof0 = Metric.Conditional("obs", "fcst", np.mean) # F | O + for i in range(0, len(lowerT)): + fo[i] = mfo.compute(data, [lowerT[i], upperT[i]]) + of[i] = mof.compute(data, [lowerT[i], upperT[i]]) + xfo[i] = xmfo.compute(data, [lowerT[i], upperT[i]]) + xof[i] = xmof.compute(data, [lowerT[i], upperT[i]]) + mpl.plot(xof,of, style, color=color, label=labels[f] + " (F|O)", lw=self._lw, ms=self._ms) + mpl.plot(fo, xfo, style, color=color, label=labels[f] + " (O|F)", lw=self._lw, ms=self._ms, alpha=0.5) + mpl.ylabel("Forecasts (" + data.getUnits() + ")") + mpl.xlabel("Observations (" + data.getUnits() + ")") + ylim = mpl.ylim() + xlim = mpl.xlim() + axismin = min(min(ylim),min(xlim)) + axismax = max(max(ylim),max(xlim)) + #mpl.plot([axismin,axismax], [axismin,axismax], "-", color="k", lw=3, zorder=-100) + self._plotPerfectScore([axismin,axismax], [axismin,axismax]) + mpl.grid() + +class SpreadSkill(Output): + _supThreshold = False + _supX = False + _description = "Spread skill" + def __init__(self): + Output.__init__(self) + + def _plotCore(self, data): + data.setAxis("all") + data.setIndex(0) + labels = data.getFilenames() + F = data.getNumFiles() + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F, connectingLine=False) + data.setFileIndex(f) + + data.setFileIndex(f) + [obs,fcst,spread] = data.getScores(["obs", "fcst", "ensSpread"]) + spread = np.sqrt(spread.flatten()) + skill = abs(obs.flatten()- fcst.flatten()) + #mpl.plot(spread, skill, style, color=color, lw=self._lw, ms=self._ms, label=labels[f]) + x = np.zeros(len(self._thresholds), 'float') + y = np.zeros(len(x), 'float') + for i in range(1, len(self._thresholds)): + I = np.where((np.isnan(spread) == 0) & + (np.isnan(skill) == 0) & + (spread > self._thresholds[i-1]) & + (spread <= self._thresholds[i]))[0] + if(len(I) > 0): + x[i] = np.mean(spread[I]) + y[i] = np.mean(skill[I]) + + style = self._getStyle(f, F) + mpl.plot(x, y, style, color=color, label=labels[f]) + mpl.xlabel("Spread (" + data.getUnits() + ")") + mpl.ylabel("MAE (" + data.getUnits() + ")") + mpl.grid() + +class Count(Output): + _description = "Counts number of forecasts above or within thresholds (use -r to specify bin-edges). Use -binned to count number in bins, instead of number above each threshold." + _defaultAxis = "threshold" + _defaultBinType = "within" + _reqThreshold = True + _supThreshold = True + _supX = False + def _plotCore(self, data): + data.setAxis("none") + data.setIndex(0) + [lowerT,upperT,x] = self._getThresholdLimits(self._thresholds) + + labels = data.getFilenames() + F = data.getNumFiles() + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + data.setFileIndex(f) + + Nobs = np.zeros(len(x), 'float') + Nfcst = np.zeros(len(x), 'float') + obs = Metric.Count("obs") + fcst = Metric.Count("fcst") + for i in range(0, len(lowerT)): + Nobs[i] = obs.compute(data, [lowerT[i], upperT[i]]) + Nfcst[i] = fcst.compute(data, [lowerT[i], upperT[i]]) + mpl.plot(x,Nfcst, style, color=color, label=labels[f], lw=self._lw, ms=self._ms) + self._plotObs(x, Nobs) + mpl.ylabel("Number") + mpl.xlabel(data.getAxisLabel()) + mpl.grid() + +class TimeSeries(Output): + _description = "Plot observations and forecasts as a time series (i.e. by concatinating all offsets). '-x ' has no effect, as it is always shown by date." + _supThreshold = False + _supX = False + def _plotCore(self, data): + F = data.getNumFiles() + data.setAxis("all") + dates = data.getAxisValues("date") + offsets = data.getAxisValues("offset") + + # Connect the last offset of a day with the first offset on the next day + # This only makes sense if the obs/fcst don't span more than a day + connect = min(offsets) + 24 > max(offsets) + minOffset = min(offsets) + + # Obs line + obs = data.getScores("obs")[0] + for d in range(0,obs.shape[0]): + x = dates[d] + offsets/24.0 + y = Common.nanmean(obs[d,:,:], axis=1) + if(connect and d < obs.shape[0]-1): + x = np.insert(x,x.shape[0],dates[d+1]+minOffset/24.0) + y = np.insert(y,y.shape[0],Common.nanmean(obs[d+1,0,:], axis=0)) + + if(d==0): # tchui (10/06/15) + xmin=np.min(x) + elif(d==obs.shape[0]-1): + xmax=np.max(x) + + lab = "obs" if d == 0 else "" + mpl.rcParams['ytick.major.pad']='20' ######This changes the buffer zone between tick labels and the axis. (dsiuta) + #mpl.rcParams['ytick.major.pad']='${self._pad}' + #mpl.rcParams['xtick.major.pad']='${self._pad}' + mpl.rcParams['xtick.major.pad']='20' ######This changes the buffer zone between tick labels and the axis. (dsiuta) + mpl.plot(x, y, ".-", color=[0.3,0.3,0.3], lw=5, label=lab) + + # Forecast lines + labels = data.getFilenames() + for f in range(0, F): + data.setFileIndex(f) + color = self._getColor(f, F) + style = self._getStyle(f, F) + + fcst = data.getScores("fcst")[0] + x = dates[d] + offsets/24.0 + y = Common.nanmean(fcst[d,:,:], axis=1) + if(connect and d < obs.shape[0]-1): + x = np.insert(x,x.shape[0],dates[d+1]+minOffset/24.0) + y = np.insert(y,y.shape[0],Common.nanmean(fcst[d+1,0,:])) + lab = labels[f] if d == 0 else "" + mpl.rcParams['ytick.major.pad']='20' ######This changes the buffer zone between tick labels and the axis. (dsiuta) + mpl.rcParams['xtick.major.pad']='20' ######This changes the buffer zone between tick labels and the axis. (dsiuta) + #mpl.rcParams['ytick.major.pad']='${self._pad}' + #mpl.rcParams['xtick.major.pad']='${self._pad}' + mpl.plot(x, y, style, color=color, lw=self._lw, ms=self._ms, label=lab) + + + #mpl.ylabel(data.getVariableAndUnits()) # "Wind Speed (km/hr)") ###hard coded axis label (dsiuta) + mpl.xlabel(data.getAxisLabel("date")) + if(self._ylabel == None): + mpl.ylabel(data.getVariableAndUnits()) + else: + mpl.ylabel(self._ylabel) + # mpl.ylabel(self._ylabel) # "Wind Speed (km/hr)") ###hard coded axis label (dsiuta) + mpl.grid() + mpl.gca().xaxis.set_major_formatter(data.getAxisFormatter("date")) + + if(self._tight): + oldTicks=mpl.gca().get_xticks() + diff = oldTicks[1] - oldTicks[0] # keep auto tick interval + tickRange = np.arange(round(xmin),round(xmax)+diff,diff) + mpl.gca().set_xticks(tickRange) # make new ticks, to start from the first day of the desired interval + mpl.autoscale(enable=True,axis=u'x',tight=True) # make xaxis tight + + + +class PitHist(Output): + _description = "Histogram of PIT values" + _supThreshold = False + _supX = False + def __init__(self, metric): + Output.__init__(self) + self._numBins = 10 + self._metric = metric + def _legend(self, data,names=None): + pass + def _plotCore(self, data): + F = data.getNumFiles() + labels = self._getLegendNames(data) + for f in range(0, F): + Common.subplot(f,F) + color = self._getColor(f, F) + data.setAxis("none") + data.setIndex(0) + data.setFileIndex(f) + pit = self._metric.compute(data,None) + + width = 1.0 / self._numBins + x = np.linspace(0,1,self._numBins+1) + N = np.histogram(pit, x)[0] + n = N * 1.0 / sum(N) + color = "gray" + xx = x[range(0,len(x)-1)] + mpl.bar(xx, n*100.0, width=width, color=color) + mpl.plot([0,1],[100.0/self._numBins, 100.0/self._numBins], 'k--') + #self._plotPerfectScore([0,1],[100.0/self._numBins, 100.0/self._numBins], "r", 100) + mpl.title(labels[f]); + ytop = 200.0/self._numBins + mpl.gca().set_ylim([0,ytop]) + if(f == 0): + mpl.ylabel("Frequency (%)") + else: + mpl.gca().set_yticks([]) + + # Multiply by 100 to get to percent + std = Metric.PitDev.deviationStd(pit, self._numBins)*100 + + mpl.plot([0,1], [100.0/self._numBins - 2*std, 100.0/self._numBins - 2*std], "r-") + mpl.plot([0,1], [100.0/self._numBins + 2*std, 100.0/self._numBins + 2*std], "r-") + Common.fill([0,1], [100.0/self._numBins - 2*std, 100.0/self._numBins - 2*std], [100.0/self._numBins + + 2*std, 100.0/self._numBins + 2*std], "r", zorder=100, alpha=0.5) + + # Compute calibration deviation + D = Metric.PitDev.deviation(pit, self._numBins) + D0 = Metric.PitDev.expectedDeviation(pit, self._numBins) + ign = Metric.PitDev.ignorancePotential(pit, self._numBins) + mpl.text(0, mpl.ylim()[1], "Dev: %2.4f\nExp: %2.4f\nIgn: %2.4f" % (D,D0,ign), verticalalignment="top") + + mpl.xlabel("Cumulative probability") + +class Reliability(Output): + _description = "Reliability diagram for a certain threshold (-r)" + _reqThreshold = True + _supX = False + _legLoc = "lower right" + def __init__(self): + Output.__init__(self) + self._shadeNoSkill = True + def _plotCore(self, data): + labels = data.getFilenames() + + F = data.getNumFiles() + ax = mpl.gca() + axi = mpl.axes([0.2,0.65,0.2,0.2]) + mpl.sca(ax) + + data.setAxis("none") + data.setIndex(0) + data.setFileIndex(0) + for t in range(0,len(self._thresholds)): + threshold = self._thresholds[t] + var = data.getPvar(threshold) + [obs, p] = data.getScores(["obs", var]) + + # Determine the number of bins to use # (at least 11, at most 25) + N = min(25, max(11, int(len(obs)/1000))) + N = 11 + edges = np.linspace(0,1,N+1) + edges = np.array([0,0.05,0.15,0.25,0.35,0.45,0.55,0.65,0.75,0.85,0.95,1]) + #edges = np.linspace(0,1,101) + x = np.zeros([len(edges)-1,F], 'float') + + y = np.nan*np.zeros([F,len(edges)-1],'float') + n = np.zeros([F,len(edges)-1],'float') + v = np.zeros([F,len(edges)-1],'float') # Variance + # Draw reliability lines + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + data.setFileIndex(f) + data.setAxis("none") + data.setIndex(0) + var = data.getPvar(threshold) + [obs, p] = data.getScores(["obs", var]) + + if(self._binType == "below"): + p = p + obs = obs < threshold + elif(self._binType == "above"): + p = 1 - p + obs = obs > threshold + else: + Common.error("Bin type must be one of 'below' or 'above' for reliability plot") + + clim = np.mean(obs) + # Compute frequencies + for i in range(0,len(edges)-1): + q = (p >= edges[i])& (p < edges[i+1]) + I = np.where(q)[0] + if(len(I) > 0): + n[f,i] = len(obs[I]) + # Need at least 10 data points to be valid + if(n[f,i] >= 1): + y[f,i] = np.mean(obs[I]) + v[f,i] = np.var(obs[I]) + x[i,f] = np.mean(p[I]) + + label = labels[f] + if(not t == 0): + label = "" + mpl.plot(x[:,f], y[f], style, color=color, lw=self._lw, ms=self._ms, label=label) + + # Draw confidence bands (do this separately so that these lines don't sneak into the legend) + for f in range(0, F): + color = self._getColor(f, F) + self._plotConfidence(x[:,f], y[f], v[f], n[f], color=color) + + # Draw lines in inset diagram + if(np.max(n) > 1): + for f in range(0, F): + color = self._getColor(f, F) + axi.plot(x[:,f], n[f], style, color=color, lw=self._lw, ms=self._ms*0.75) + axi.xaxis.set_major_locator(mpl.NullLocator()) + axi.set_yscale('log') + axi.set_title("Number") + axi.grid('on') + mpl.sca(ax) + self._plotObs([0,1], [0,1]) + mpl.axis([0,1,0,1]) + color = "gray" + mpl.plot([0,1], [clim,clim], "--", color=color,label="") # Climatology line + mpl.plot([clim,clim], [0,1], "--", color=color) # Climatology line + mpl.plot([0,1], [clim/2,1-(1-clim)/2], "--", color=color) # No-skill line + if(self._shadeNoSkill): + Common.fill([clim,1], [0,0], [clim,1-(1-clim)/2], col=[1,1,1], zorder=-100, + hatch="\\") + Common.fill([0,clim], [clim/2,clim,0], [1,1], col=[1,1,1], zorder=-100, + hatch="\\") + mpl.xlabel("Forecasted probability") + mpl.ylabel("Observed frequency") + units = " " + data.getUnits() + mpl.title("Reliability diagram for obs > " + str(threshold) + units) + +class IgnContrib(Output): + _description = "Binary Ignorance contribution diagram for a single threshold (-r). "\ + + "Shows how much each probability issued contributes to the total ignorance." + _reqThreshold = True + _supX = False + _legLoc = "upper center" + _experimental = True + def __init__(self): + Output.__init__(self) + def _plotCore(self, data): + labels = data.getFilenames() + + if(len(self._thresholds) != 1): + Common.error("IgnContrib diagram requires exactly one threshold") + threshold = self._thresholds[0] + + F = data.getNumFiles() + + mpl.subplot(2,1,1) + units = " " + data.getUnits() + titlestr = "Ignorance contribution diagram for obs > " + str(self._thresholds[0]) + units + mpl.title(titlestr) + + data.setAxis("none") + data.setIndex(0) + data.setFileIndex(0) + mpl.subplot(2,1,1) + var = data.getPvar(threshold) + [obs, p] = data.getScores(["obs", var]) + + # Determine the number of bins to use # (at least 11, at most 25) + N = min(25, max(11, int(len(obs)/1000))) + edges = np.linspace(0,1,N+1) + + x = np.zeros([F, len(edges)-1], 'float') + y = np.nan*np.zeros([F,len(edges)-1],'float') + n = np.zeros([F,len(edges)-1],'float') + + # Draw reliability lines + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + data.setFileIndex(f) + data.setAxis("none") + data.setIndex(0) + var = data.getPvar(threshold) + [obs, p] = data.getScores(["obs", var]) + + if(self._binType == "below"): + p = p + obs = obs < threshold + elif(self._binType == "above"): + p = 1 - p + obs = obs > threshold + else: + Common.error("Bin type must be one of 'below' or 'above' for reliability plot") + + clim = np.mean(obs) + # Compute frequencies + for i in range(0,len(edges)-1): + q = (p >= edges[i])& (p < edges[i+1]) + I = np.where(q)[0] + if(len(I) > 0): + n[f,i] = len(obs[I]) + x[f,i] = np.mean(p[I]) + # Need at least 10 data points to be valid + if(n[f,i] >= 1): + #y[f,i] = -n[f,i]*(x[f,i]*np.log2(x[f,i]) + (1-x[f,i])*np.log2(1-x[f,i])) + I0 = np.where(obs[I] == 0) + I1 = np.where(obs[I] == 1) + y[f,i] = -np.sum(np.log2(p[I[I1]])) - np.sum(np.log2(1-p[I[I0]])) + + label = labels[f] + mpl.plot(x[f], y[f]/np.sum(n[f])*len(n[f]), style, color=color, lw=self._lw, ms=self._ms, label=label) + mpl.ylabel("Ignorance contribution") + + # Draw expected sharpness + xx = np.linspace(0,1,100) + yy = -(xx*np.log2(xx) + (1-xx)*np.log2(1-xx)) + mpl.plot(xx, yy, "--", color="gray") + yy = -np.log2(clim)*np.ones(len(xx)) + + # Show number in each bin + mpl.subplot(2,1,2) + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + mpl.plot(x[f], n[f], style, color=color, lw=self._lw, ms=self._ms) + mpl.xlabel("Forecasted probability") + mpl.ylabel("N") + + # Switch back to top subpplot, so the legend works + mpl.subplot(2,1,1) + +# doClassic: Use the classic definition, by not varying the forecast threshold +# i.e. using the same threshold for observation and forecast. +class DRoc(Output): + _description = "Plots the receiver operating characteristics curve for the deterministic " \ + + "forecast for a single threshold. Uses different forecast thresholds to create points." + _supX = False + _reqThreshold = True + def __init__(self, fthresholds=None, doNorm=False, doClassic=False): + Output.__init__(self) + self._doNorm = doNorm + self._fthresholds = fthresholds + self._doClassic = doClassic + self._showThresholds = False + def _plotCore(self, data): + threshold = self._thresholds[0] # Observation threshold + if(threshold == None): + Common.error("DRoc plot needs a threshold (use -r)") + + if(self._doClassic): + fthresholds = [threshold] + else: + if(self._fthresholds != None): + fthresholds = self._fthresholds + else: + if(data.getVariable() == "Precip"): + fthresholds = [0,1e-7,1e-6,1e-5,1e-4,0.001,0.005,0.01,0.05,0.1,0.2,0.3,0.5,1,2,3,5,10,20,100] + else: + N = 31 + fthresholds = np.linspace(threshold-10, threshold+10, N) + + F = data.getNumFiles() + labels = data.getFilenames() + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + data.setAxis("none") + data.setIndex(0) + data.setFileIndex(f) + [obs, fcst] = data.getScores(["obs", "fcst"]) + + y = np.nan*np.zeros([len(fthresholds),1],'float') + x = np.nan*np.zeros([len(fthresholds),1],'float') + for i in range(0,len(fthresholds)): + fthreshold = fthresholds[i] + a = np.ma.sum((fcst >= fthreshold) & (obs >= threshold)) # Hit + b = np.ma.sum((fcst >= fthreshold) & (obs < threshold)) # FA + c = np.ma.sum((fcst < fthreshold) & (obs >= threshold)) # Miss + d = np.ma.sum((fcst < fthreshold) & (obs < threshold)) # Correct rejection + if(a + c > 0 and b + d > 0): + y[i] = a / 1.0 / (a + c) + x[i] = b / 1.0 / (b + d) + if(self._doNorm): + from scipy.stats import norm + y[i] = norm.ppf(a / 1.0 / (a + c)) + x[i] = norm.ppf(b / 1.0 / (b + d)) + if(np.isinf(y[i])): + y[i] = np.nan + if(np.isinf(x[i])): + x[i] = np.nan + if(self._showThresholds and (not np.isnan(x[i]) and not np.isnan(y[i]) and f == 0)): + mpl.text(x[i], y[i], "%2.1f" % fthreshold, color=color) + if(not self._doNorm): + # Add end points at 0,0 and 1,1: + xx = x + yy = y + x = np.zeros([len(fthresholds)+2,1], 'float') + y = np.zeros([len(fthresholds)+2,1], 'float') + x[1:-1] = xx + y[1:-1] = yy + x[0] = 1 + y[0] = 1 + x[len(x)-1] = 0 + y[len(y)-1] = 0 + #I = np.where(np.isnan(x)+np.isnan(y)==0) + mpl.plot(x, y, style, color=color, label=labels[f], lw=self._lw, ms=self._ms) + if(self._doNorm): + xlim = mpl.xlim() + ylim = mpl.ylim() + q0 = max(abs(xlim[0]), abs(ylim[0])) + q1 = max(abs(xlim[1]), abs(ylim[1])) + mpl.plot([-q0,q1], [-q0,q1], 'k--') + mpl.xlabel("Normalized false alarm rate") + mpl.ylabel("Normalized hit rate") + else: + mpl.plot([0,1], [0,1], color="k") + mpl.axis([0,1,0,1]) + mpl.xlabel("False alarm rate") + mpl.ylabel("Hit rate") + self._plotPerfectScore([0,0,1], [0,1,1]) + units = " " + data.getUnits() + mpl.title("Threshold: " + str(threshold) + units) + mpl.grid() + +class DRocNorm(DRoc): + _description = "Same as DRoc, except the hit and false alarm rates are transformed using the " \ + "inverse of the standard normal distribution in order to highlight the extreme " \ + "values." + def __init__(self): + DRoc.__init__(self, doNorm=True) + +class DRoc0(DRoc): + _description = "Same as DRoc, except don't use different forecast thresholds: Use the "\ + "same\n threshold for forecast and obs." + def __init__(self): + DRoc.__init__(self, doNorm=False, doClassic=True) + +class Against(Output): + _description = "Plots the forecasts for each pair of configurations against each other. "\ + "Colours indicate which configuration had the best forecast (but only if the difference is "\ + "more than 10% of the standard deviation of the observation)." + _defaultAxis = "none" + _supThreshold = False + _supX = False + _minStdDiff = 0.1 # How big difference should colour kick in (in number of STDs)? + def _plotCore(self, data): + F = data.getNumFiles() + if(F < 2): + Common.error("Cannot use Against plot with less than 2 configurations") + + data.setAxis("none") + data.setIndex(0) + labels = data.getFilenames() + for f0 in range(0,F): + for f1 in range(0,F): + if(f0 != f1 and (F != 2 or f0 == 0)): + if(F > 2): + mpl.subplot(F,F,f0+f1*F+1) + data.setFileIndex(f0) + x = data.getScores("fcst")[0].flatten() + data.setFileIndex(f1) + y = data.getScores("fcst")[0].flatten() + lower = min(min(x),min(y)) + upper = max(max(x),max(y)) + + mpl.plot(x, y, "x", mec="k", ms=self._ms/2, mfc="k", zorder=-1000) + + # Show which forecast is better + data.setFileIndex(f0) + [obsx,x] = data.getScores(["obs","fcst"]) + data.setFileIndex(f1) + [obsy,y] = data.getScores(["obs","fcst"]) + x = x.flatten() + y = y.flatten() + obs = obsx.flatten() + + mpl.plot(x, y, "s", mec="k", ms=self._ms/2, mfc="w", zorder=-500) + + std = np.std(obs)/2 + minDiff = self._minStdDiff*std + if(len(x) == len(y)): + N = 5 + for k in range(0,N): + Ix = abs(obs - y) > abs(obs - x) + std*k/N + Iy = abs(obs - y) + std*k/N < abs(obs - x) + mpl.plot(x[Ix], y[Ix], "r.", ms=self._ms, alpha=k/1.0/N) + mpl.plot(x[Iy], y[Iy], "b.", ms=self._ms, alpha=k/1.0/N) + + # Contour of the frequency + #q = np.histogram2d(x[1,:], x[0,:], [np.linspace(lower,upper,100), np.linspace(lower,upper,100)]) + #[X,Y] = np.meshgrid(q[1],q[2]) + #mpl.contour(X[1:,1:],Y[1:,1:],q[0],[1,100],zorder=90) + + mpl.xlabel(labels[f0], color="r") + mpl.ylabel(labels[f1], color="b") + mpl.grid() + xlim = mpl.xlim() + ylim = mpl.ylim() + lower = min(xlim[0],ylim[0]) + upper = max(xlim[1],ylim[1]) + mpl.xlim([lower, upper]) + mpl.ylim([lower, upper]) + mpl.plot([lower,upper], [lower, upper], '--', color=[0.3,0.3,0.3], lw=3, zorder=100) + if(F == 2): + break + def _legend(self, data, names=None): + pass + +class Taylor(Output): + _description = "Taylor diagram showing correlation and forecast standard deviation" + _supThreshold = False + _supX = False + _defaultAxis = "none" + _legLoc = "upper left" + + def _plotCore(self, data): + data.setAxis(self._xaxis) + data.setIndex(0) + labels = data.getFilenames() + F = data.getNumFiles() + + # Plot points + maxstd = 0 + for f in range(0, F): + data.setFileIndex(f) + color = self._getColor(f, F) + style = self._getStyle(f, F) + + size = data.getAxisSize() + corr = np.zeros(size, 'float') + std = np.zeros(size, 'float') + stdobs = np.zeros(size, 'float') + for i in range(0,size): + data.setIndex(i) + [obs,fcst] = data.getScores(["obs", "fcst"]) + if(len(obs)>0 and len(fcst)>0): + corr[i] = np.corrcoef(obs,fcst)[1,0] + std[i] = np.sqrt(np.var(fcst)) + stdobs[i] = np.sqrt(np.var(obs)) + maxstd = max(maxstd, max(std)) + ang = np.arccos(corr) + x = std * np.cos(ang) + y = std * np.sin(ang) + mpl.plot(x,y, style, color=color, label=labels[f], lw=self._lw, ms=self._ms) + stdobs = np.mean(stdobs) + + # Set axis limits + if(maxstd < 1.25*stdobs): # Enforce a minimum radius beyond the obs-radius + maxstd = 1.25*stdobs + maxstd = int(np.ceil(maxstd)) + mpl.xlim([-maxstd*1.05,maxstd*1.05]) # Allow for some padding outside the outer ring + mpl.ylim([0,maxstd*1.05]) + mpl.xlabel("Standard deviation (" + data.getUnits() + ")") + xticks = mpl.xticks()[0] + mpl.xticks(xticks[xticks>=0]) + mpl.xlim([-maxstd*1.05,maxstd*1.05]) + mpl.ylim([0,maxstd*1.05]) + mpl.text(np.sin(np.pi/4)*maxstd, np.cos(np.pi/4)*maxstd, "Correlation", rotation=-45, + fontsize=self._labfs, horizontalalignment="center", verticalalignment="bottom") + mpl.gca().yaxis.set_visible(False) + mpl.gca().xaxis.set_ticks_position('bottom') + + # Draw obs point/lines + orange = [1,0.8,0.4] + self._drawCircle(stdobs, style='-', lw=5, color=orange) + mpl.plot(stdobs, 0, 's-', color=orange, label="Observation", mew=2, ms=self._ms, clip_on=False) + + # Draw diagonals + corrs = [-1,-0.99,-0.95,-0.9,-0.8,-0.5,0,0.5,0.8,0.9,0.95,0.99] #np.linspace(-1,1,21) + for i in range(0, len(corrs)): + ang = np.arccos(corrs[i]) # Mathematical angle + x = np.cos(ang)*maxstd + y = np.sin(ang)*maxstd + mpl.plot([0, x], [0, y], 'k--') + mpl.text(x, y, str(corrs[i]), verticalalignment="bottom", fontsize=self._labfs) + + # Draw CRMSE rings + xticks = mpl.xticks()[0] + self._drawCircle(0,style="-", color="gray", lw=3, label="CRMSE") + for R in np.linspace(0, 2*max(xticks), 2*2*max(xticks)/(xticks[1]-xticks[0])+1): + if(R > 0): + self._drawCircle(R, xcenter=stdobs, ycenter=0, maxradius=maxstd, style="-", color="gray", lw=3) + x = np.sin(-np.pi/4)*R+stdobs + y = np.cos(np.pi/4)*R + if(x**2+y**2 < maxstd**2): + mpl.text(x, y, str(R), horizontalalignment="right", verticalalignment="bottom", + fontsize=self._labfs, color="gray") + + # Draw std rings + for X in mpl.xticks()[0]: + if(X <= maxstd): + self._drawCircle(X, style=":") + self._drawCircle(maxstd, style="-", lw=3) + + mpl.gca().set_aspect(1) + +class Error(Output): + _description = "Decomposition of RMSE into systematic and unsystematic components" + _supThreshold = False + _supX = True + _defaultAxis = "none" + + def _plotCore(self, data): + data.setAxis(self._xaxis) + data.setIndex(0) + labels = data.getFilenames() + F = data.getNumFiles() + + mpl.gca().set_aspect(1) + mpl.xlabel("Unsystematic error (CRMSE, " + data.getUnits() + ")") + mpl.ylabel("Systematic error (Bias, " + data.getUnits() + ")") + + + # Plot points + size = data.getAxisSize() + serr = np.nan*np.zeros([size,F], 'float') + uerr = np.nan*np.zeros([size,F], 'float') + rmse = np.nan*np.zeros([size,F], 'float') + for f in range(0, F): + data.setFileIndex(f) + color = self._getColor(f, F) + style = self._getStyle(f, F, connectingLine=False) + + for i in range(0,size): + data.setIndex(i) + [obs,fcst] = data.getScores(["obs", "fcst"]) + mfcst = np.mean(fcst) + mobs = np.mean(obs) + if(len(obs)>0 and len(fcst)>0): + serr[i,f] = np.mean(obs-fcst) + rmse[i,f] = np.sqrt(np.mean((obs-fcst)**2)) + uerr[i,f] = np.sqrt(rmse[i,f]**2 - serr[i,f]**2) + # np.sqrt(np.mean((fcst - mfcst) - (obs - mobs))**2) + mpl.plot(uerr[:,f],serr[:,f], style, color=color, label=labels[f], lw=self._lw, ms=self._ms) + xlim = mpl.xlim() + ylim = mpl.ylim() + + # Draw rings + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F, lineOnly=True) + self._drawCircle(Common.nanmean(rmse[:,f]), style=style, color=color) + + # Set axis limits + maxx = xlim[1] + maxy = ylim[1] + miny = min(0,ylim[0]) + # Try to enforce the x-axis and y-axis to be roughly the same size + if(maxy - miny < maxx/2): + maxy = maxx + elif(maxy - miny > maxx*2): + maxx = maxy-miny + mpl.xlim([0, maxx]) # Not possible to have negative CRMSE + mpl.ylim([miny,maxy]) + + # Draw standard RMSE rings + for X in mpl.xticks()[0]: + self._drawCircle(X, style=":") + + mpl.plot([0,maxx], [0,0], 'k-', lw=2) # Draw x-axis line + mpl.grid() + +class Marginal(Output): + _description = "Show marginal distribution for different thresholds" + _reqThreshold = True + _supX = False + _experimental = True + def __init__(self): + Output.__init__(self) + def _plotCore(self, data): + labels = data.getFilenames() + + F = data.getNumFiles() + + data.setAxis("none") + data.setIndex(0) + data.setFileIndex(0) + clim = np.zeros(len(self._thresholds), 'float') + for f in range(0, F): + x = self._thresholds + y = np.zeros([len(self._thresholds)], 'float') + for t in range(0,len(self._thresholds)): + threshold = self._thresholds[t] + data.setFileIndex(f) + data.setAxis("none") + data.setIndex(0) + var = data.getPvar(threshold) + [obs, p] = data.getScores(["obs", var]) + + color = self._getColor(f, F) + style = self._getStyle(f, F) + + if(self._binType == "below"): + p = p + obs = obs < threshold + elif(self._binType == "above"): + p = 1 - p + obs = obs > threshold + else: + Common.error("Bin type must be one of 'below' or 'above' for reliability plot") + + clim[t] = np.mean(obs) + y[t] = np.mean(p) + + label = labels[f] + mpl.plot(x, y, style, color=color, lw=self._lw, ms=self._ms, label=label) + self._plotObs(x, clim) + + mpl.ylim([0,1]) + mpl.xlabel(data.getAxisLabel("threshold")) + mpl.ylabel("Marginal probability") + mpl.grid() + +class Freq(Output): + _description = "Show frequency of obs and forecasts" + _reqThreshold = True + _supX = False + _experimental = True + def __init__(self): + Output.__init__(self) + def _plotCore(self, data): + labels = data.getFilenames() + + F = data.getNumFiles() + + data.setAxis("none") + data.setIndex(0) + data.setFileIndex(0) + + for f in range(0, F): + # Setup x and y: When -b within, we need one less value in the array + N = len(self._thresholds) + x = self._thresholds + if(self._binType == "within"): + N = len(self._thresholds) - 1 + x = (self._thresholds[1:]+self._thresholds[:-1])/2 + y = np.zeros(N, 'float') + clim = np.zeros(N, 'float') + for t in range(0,N): + threshold = self._thresholds[t] + data.setFileIndex(f) + data.setAxis("none") + data.setIndex(0) + [obs, fcst] = data.getScores(["obs", "fcst"]) + + color = self._getColor(f, F) + style = self._getStyle(f, F) + + if(self._binType == "below"): + fcst = fcst < threshold + obs = obs < threshold + elif(self._binType == "above"): + fcst = fcst > threshold + obs = obs > threshold + elif(self._binType == "within"): + fcst = (fcst >= threshold) & (fcst < self._thresholds[t+1]) + obs = (obs >= threshold) & (obs < self._thresholds[t+1]) + + clim[t] = np.mean(obs) + y[t] = np.mean(fcst) + + label = labels[f] + mpl.plot(x, y, style, color=color, lw=self._lw, ms=self._ms, label=label) + self._plotObs(x, clim) + + mpl.ylim([0,1]) + mpl.xlabel(data.getAxisLabel("threshold")) + mpl.ylabel("Frequency " + self._binType) + mpl.grid() + + +class InvReliability(Output): + _description = "Reliability diagram for a certain quantile (-r)" + _reqThreshold = True + _supX = False + _experimental = True + def __init__(self): + Output.__init__(self) + def _plotCore(self, data): + labels = data.getFilenames() + + F = data.getNumFiles() + ax = mpl.gca() + quantiles = self._thresholds + if(quantiles[0] < 0.5): + axi = mpl.axes([0.66,0.65,0.2,0.2]) + else: + axi = mpl.axes([0.66,0.15,0.2,0.2]) + mpl.sca(ax) + + data.setAxis("none") + data.setIndex(0) + data.setFileIndex(0) + for t in range(0,len(quantiles)): + quantile = self._thresholds[t] + var = data.getQvar(quantile) + [obs, p] = data.getScores(["obs", var]) + + # Determine the number of bins to use # (at least 11, at most 25) + N = min(25, max(11, int(len(obs)/1000))) + N = 21 + edges = np.linspace(0,20,N+1) + #edges = [0,0.001,1,2,3,4,5,6,7,8,9,10] + if(data.getVariable() == "Precip"): + edges = np.linspace(0,np.sqrt(Common.nanmax(obs)), N+1)**2 + else: + edges = np.linspace(Common.nanmin(obs),Common.nanmax(obs), N+1) + + #edges = np.zeros(N, 'float') + #perc = np.linspace(0,100,N) + #for i in range(0,N): + # edges[i] = Common.nanpercentile(obs, perc[i]) + #edges = np.unique(edges) + + x = np.zeros([len(edges)-1,F], 'float') + + y = np.nan*np.zeros([F,len(edges)-1],'float') + n = np.zeros([F,len(edges)-1],'float') + v = np.zeros([F,len(edges)-1],'float') + # Draw reliability lines + for f in range(0, F): + color = self._getColor(f, F) + style = self._getStyle(f, F) + data.setFileIndex(f) + data.setAxis("none") + data.setIndex(0) + var = data.getQvar(quantile) + [obs, p] = data.getScores(["obs", var]) + + obs = obs <= p + + # Compute frequencies + for i in range(0,len(edges)-1): + q = (p >= edges[i])& (p < edges[i+1]) + I = np.where(q)[0] + if(len(I) > 0): + n[f,i] = len(obs[I]) + # Need at least 10 data points to be valid + if(n[f,i] >= 2): + y[f,i] = np.mean(obs[I]) + v[f,i] = np.var(obs[I]) + x[i,f] = np.mean(p[I]) + + label = labels[f] + if(not t == 0): + label = "" + mpl.plot(x[:,f], y[f], style, color=color, lw=self._lw, ms=self._ms, label=label) + self._plotObs(edges,0*edges + quantile) + + # Draw confidence bands (do this separately so that these lines don't sneak into the legend) + for f in range(0, F): + color = self._getColor(f, F) + self._plotConfidence(x[:,f], y[f], v[f], n[f], color=color) + axi.plot(x[:,f], n[f], style, color=color, lw=self._lw, ms=self._ms) + axi.xaxis.set_major_locator(mpl.NullLocator()) + axi.set_yscale('log') + axi.set_title("Number") + mpl.sca(ax) + mpl.ylim([0,1]) + color = "gray" + mpl.xlabel(data.getVariableAndUnits()) + mpl.ylabel("Observed frequency") + units = " " + data.getUnits() + mpl.title("Quantile: " + str(quantile*100) + "%") diff --git a/verif/Station.py b/verif/Station.py new file mode 100644 index 0000000..547959f --- /dev/null +++ b/verif/Station.py @@ -0,0 +1,26 @@ +class Station: + def __init__(self, id, lat, lon, elev): + self._id = id + self._lat = lat + self._lon = lon + self._elev = elev + def id(self, value=None): + if value == None: + return self._id + else: + self._id = value + def lat(self, value=None): + if value == None: + return self._lat + else: + self._lat = value + def lon(self, value=None): + if value == None: + return self._lon + else: + self._lon = value + def elev(self, value=None): + if value == None: + return self._elev + else: + self._elev = value diff --git a/verif/__init__.py b/verif/__init__.py new file mode 100644 index 0000000..3f6fe59 --- /dev/null +++ b/verif/__init__.py @@ -0,0 +1,559 @@ +import sys +import os +import verif.Data as Data +import verif.Output as Output +import verif.Metric as Metric +import verif.Common as Common +import matplotlib.pyplot as mpl +import textwrap +def main(): + ############ + # Defaults # + ############ + ifiles = list() + ofile = None + metric = None + locations = None + latlonRange = None + training = 0 + thresholds = None + startDate = None + endDate = None + climFile = None + climType = "subtract" + leg = None + ylabel = None + xlabel = None + title = None + offsets = None + xdim = None + sdim = None + figSize = None + dpi = 100 + debug = False + showText = False + showMap = False + noMargin = False + binType = None + markerSize = None + lineWidth = None + tickFontSize = None + labFontSize = None + legFontSize = None + type = "plot" + XRotation = None + MajorLength = None + MinorLength = None + MajorWidth = None + Bottom = None + Top = None + Right = None + Left = None + Pad = None + showPerfect = None + cType = "mean" + doHist = False + doSort = False + doAcc = False + xlim = None + ylim = None + clim = None + + # Read command line arguments + i = 1 + while(i < len(sys.argv)): + arg = sys.argv[i] + if(arg[0] == '-'): + # Process option + if(arg == "-debug"): + debug = True + elif(arg == "-nomargin"): + noMargin = True + elif(arg == "-sp"): + showPerfect = True + elif(arg == "-hist"): + doHist = True + elif(arg == "-acc"): + doAcc = True + elif(arg == "-sort"): + doSort = True + else: + if(arg == "-f"): + ofile = sys.argv[i+1] + elif(arg == "-l"): + locations = Common.parseNumbers(sys.argv[i+1]) + elif(arg == "-llrange"): + latlonRange = Common.parseNumbers(sys.argv[i+1]) + elif(arg == "-t"): + training = int(sys.argv[i+1]) + elif(arg == "-x"): + xdim = sys.argv[i+1] + elif(arg == "-o"): + offsets = Common.parseNumbers(sys.argv[i+1]) + elif(arg == "-leg"): + leg = unicode(sys.argv[i+1], 'utf8') + elif(arg == "-ylabel"): + ylabel = unicode(sys.argv[i+1], 'utf8') + elif(arg == "-xlabel"): + xlabel = unicode(sys.argv[i+1], 'utf8') + elif(arg == "-title"): + title = unicode(sys.argv[i+1], 'utf8') + elif(arg == "-b"): + binType = sys.argv[i+1] + elif(arg == "-type"): + type = sys.argv[i+1] + elif(arg == "-fs"): + figSize = sys.argv[i+1] + elif(arg == "-dpi"): + dpi = int(sys.argv[i+1]) + elif(arg == "-d"): + startDate = int(sys.argv[i+1]) + endDate = int(sys.argv[i+2]) + i = i + 1 + elif(arg == "-c"): + climFile = sys.argv[i+1] + climType = "subtract" + elif(arg == "-C"): + climFile = sys.argv[i+1] + climType = "divide" + elif(arg == "-xlim"): + xlim = Common.parseNumbers(sys.argv[i+1]) + elif(arg == "-ylim"): + ylim = Common.parseNumbers(sys.argv[i+1]) + elif(arg == "-clim"): + clim = Common.parseNumbers(sys.argv[i+1]) + elif(arg == "-s"): + sdim = sys.argv[i+1] + elif(arg == "-ct"): + cType = sys.argv[i+1] + elif(arg == "-r"): + thresholds = Common.parseNumbers(sys.argv[i+1]) + elif(arg == "-ms"): + markerSize = float(sys.argv[i+1]) + elif(arg == "-lw"): + lineWidth = float(sys.argv[i+1]) + elif(arg == "-tickfs"): + tickFontSize = float(sys.argv[i+1]) + elif(arg == "-labfs"): + labFontSize = float(sys.argv[i+1]) + elif(arg == "-legfs"): + legFontSize = float(sys.argv[i+1]) + elif(arg == "-xrot"): + XRotation = float(sys.argv[i+1]) + elif(arg == "-majlth"): + MajorLength = float(sys.argv[i+1]) + elif(arg == "-minlth"): + MinorLength = float(sys.argv[i+1]) + elif(arg == "-majwid"): + MajorWidth = float(sys.argv[i+1]) + elif(arg == "-bot"): + Bottom = float(sys.argv[i+1]) + elif(arg == "-top"): + Top = float(sys.argv[i+1]) + elif(arg == "-right"): + Right = float(sys.argv[i+1]) + elif(arg == "-left"): + Left = float(sys.argv[i+1]) + elif(arg == "-pad"): + Pad = sys.argv[i+1] + elif(arg == "-m"): + metric = sys.argv[i+1] + else: + Common.error("Flag '" + sys.argv[i] + "' not recognized") + i = i + 1 + else: + ifiles.append(sys.argv[i]) + i = i + 1 + + # Deal with legend entries + if(leg != None): + leg = leg.split(',') + for i in range(0,len(leg)): + leg[i] = leg[i].replace('_', ' ') + + # Limit dates + dates = None + if(startDate != None and endDate != None): + dates = list() + date = startDate + while(date <= endDate): + dates.append(date) + date = Common.getDate(date, 1) + + if(cType != "mean" and cType != "min" and cType != "max" and cType != "median"): + Common.error("'-ct cType' must be one of min, mean, median, or max") + + if(latlonRange != None and len(latlonRange) != 4): + Common.error("-llRange must have exactly 4 values") + + if(len(ifiles) > 0): + data = Data.Data(ifiles, clim=climFile, climType=climType, dates=dates, offsets=offsets, + locations=locations, latlonRange=latlonRange, training=training) + else: + data = None + if(len(sys.argv) == 1 or len(ifiles) == 0 or metric == None): + showDescription(data) + sys.exit() + + if(figSize != None): + figSize = figSize.split(',') + if(len(figSize) != 2): + print "-fs figSize must be in the form: width,height" + sys.exit(1) + + m = None + + # Handle special plots + if(doHist): + pl = Output.Hist(metric) + elif(doSort): + pl = Output.Sort(metric) + elif(metric == "pithist"): + m = Metric.Pit("pit") + pl = Output.PitHist(m) + elif(metric == "obsfcst"): + pl = Output.ObsFcst() + elif(metric == "timeseries"): + pl = Output.TimeSeries() + elif(metric == "qq"): + pl = Output.QQ() + elif(metric == "cond"): + pl = Output.Cond() + elif(metric == "against"): + pl = Output.Against() + elif(metric == "count"): + pl = Output.Count() + elif(metric == "scatter"): + pl = Output.Scatter() + elif(metric == "change"): + pl = Output.Change() + elif(metric == "spreadskill"): + pl = Output.SpreadSkill() + elif(metric == "taylor"): + pl = Output.Taylor() + elif(metric == "error"): + pl = Output.Error() + elif(metric == "droc"): + pl = Output.DRoc() + elif(metric == "droc0"): + pl = Output.DRoc0() + elif(metric == "drocnorm"): + pl = Output.DRocNorm() + elif(metric == "reliability"): + pl = Output.Reliability() + elif(metric == "invreliability"): + pl = Output.InvReliability() + elif(metric == "igncontrib"): + pl = Output.IgnContrib() + elif(metric == "marginal"): + pl = Output.Marginal() + else: + # Standard plots + ''' + # Attempt at automating + metrics = Metric.getAllMetrics() + m = None + for mm in metrics: + if(metric == mm[0].lower() and mm[1].isStandard()): + m = mm[1]() + break + if(m == None): + m = Metric.Default(metric) + ''' + + # Determine metric + if(metric == "rmse"): + m = Metric.Rmse() + elif(metric == "rmsf"): + m = Metric.Rmsf() + elif(metric == "crmse"): + m = Metric.Crmse() + elif(metric == "cmae"): + m = Metric.Cmae() + elif(metric == "dmb"): + m = Metric.Dmb() + elif(metric == "std"): + m = Metric.Std() + elif(metric == "num"): + m = Metric.Num() + elif(metric == "corr"): + m = Metric.Corr() + elif(metric == "rankcorr"): + m = Metric.RankCorr() + elif(metric == "kendallcorr"): + m = Metric.KendallCorr() + elif(metric == "bias"): + m = Metric.Bias() + elif(metric == "ef"): + m = Metric.Ef() + elif(metric == "maxobs"): + m = Metric.MaxObs() + elif(metric == "minobs"): + m = Metric.MinObs() + elif(metric == "maxfcst"): + m = Metric.MaxFcst() + elif(metric == "minfcst"): + m = Metric.MinFcst() + elif(metric == "stderror"): + m = Metric.StdError() + elif(metric == "mae"): + m = Metric.Mae() + elif(metric == "medae"): + m = Metric.Medae() + # Contingency metrics + elif(metric == "ets"): + m = Metric.Ets() + elif(metric == "threat"): + m = Metric.Threat() + elif(metric == "pc"): + m = Metric.Pc() + elif(metric == "diff"): + m = Metric.Diff() + elif(metric == "edi"): + m = Metric.Edi() + elif(metric == "sedi"): + m = Metric.Sedi() + elif(metric == "eds"): + m = Metric.Eds() + elif(metric == "seds"): + m = Metric.Seds() + elif(metric == "biasfreq"): + m = Metric.BiasFreq() + elif(metric == "hss"): + m = Metric.Hss() + elif(metric == "baserate"): + m = Metric.BaseRate() + elif(metric == "yulesq"): + m = Metric.YulesQ() + elif(metric == "or"): + m = Metric.Or() + elif(metric == "lor"): + m = Metric.Lor() + elif(metric == "yulesq"): + m = Metric.YulesQ() + elif(metric == "kss"): + m = Metric.Kss() + elif(metric == "hit"): + m = Metric.Hit() + elif(metric == "miss"): + m = Metric.Miss() + elif(metric == "fa"): + m = Metric.Fa() + elif(metric == "far"): + m = Metric.Far() + # Other threshold + elif(metric == "bs"): + m = Metric.Bs() + elif(metric == "bss"): + m = Metric.Bss() + elif(metric == "bsrel"): + m = Metric.BsRel() + elif(metric == "bsunc"): + m = Metric.BsUnc() + elif(metric == "bsres"): + m = Metric.BsRes() + elif(metric == "ign0"): + m = Metric.Ign0() + elif(metric == "spherical"): + m = Metric.Spherical() + elif(metric == "within"): + m = Metric.Within() + # Probabilistic + elif(metric == "pit"): + m = Metric.Mean(Metric.Pit()) + elif(metric == "pitdev"): + m = Metric.PitDev() + elif(metric == "marginalratio"): + m = Metric.MarginalRatio() + # Default + else: + if(cType == "min"): + m = Metric.Min(Metric.Default(metric)) + elif(cType == "max"): + m = Metric.Max(Metric.Default(metric)) + elif(cType == "median"): + m = Metric.Median(Metric.Default(metric)) + elif(cType == "mean"): + m = Metric.Mean(Metric.Default(metric)) + else: + Common.error("-ct " + cType + " not understood") + + # Output type + if(type == "plot" or type == "text" or type == "map" or type == "maprank"): + pl = Output.Default(m) + pl.setShowAcc(doAcc) + else: + Common.error("Type not understood") + + # Rest dimension of '-x' is not allowed + if(xdim != None and not pl.supportsX()): + Common.warning(metric + " does not support -x. Ignoring it.") + xdim = None + + # Reset dimension if 'threshold' is not allowed + if(xdim == "threshold" and ((not pl.supportsThreshold()) or (not m.supportsThreshold()))): + Common.warning(metric + " does not support '-x threshold'. Ignoring it.") + thresholds = None + xdim = None + + # Create thresholds if needed + if((thresholds == None) and (pl.requiresThresholds() or (m != None and m.requiresThresholds()))): + data.setAxis("none") + obs = data.getScores("obs")[0] + fcst = data.getScores("fcst")[0] + smin = min(min(obs), min(fcst)) + smax = max(max(obs), max(fcst)) + thresholds = np.linspace(smin,smax,10) + Common.warning("Missing '-r '. Automatically setting thresholds.") + + # Set plot parameters + if(markerSize != None): + pl.setMarkerSize(markerSize) + if(lineWidth != None): + pl.setLineWidth(lineWidth) + if(labFontSize != None): + pl.setLabFontSize(labFontSize) + if(legFontSize != None): + pl.setLegFontSize(legFontSize) + if(tickFontSize != None): + pl.setTickFontSize(tickFontSize) + if(XRotation != None): + pl.setXRotation(XRotation) + if(MajorLength != None): + pl.setMajorLength(MajorLength) + if(MinorLength != None): + pl.setMinorLength(MinorLength) + if(MajorWidth != None): + pl.setMajorWidth(MajorWidth) + if(Bottom != None): + pl.setBottom(Bottom) + if(Top != None): + pl.setTop(Top) + if(Right != None): + pl.setRight(Right) + if(Left != None): + pl.setLeft(Left) + if(Pad != None): + pl.setPad(None) + if(binType != None): + pl.setBinType(binType) + if(showPerfect != None): + pl.setShowPerfect(showPerfect) + if(xlim != None): + pl.setXLim(xlim) + if(ylim != None): + pl.setYLim(ylim) + if(clim != None): + pl.setCLim(clim) + pl.setFilename(ofile) + pl.setThresholds(thresholds) + pl.setLegend(leg) + pl.setFigsize(figSize) + pl.setDpi(dpi) + pl.setAxis(xdim) + pl.setShowMargin(not noMargin) + pl.setYlabel(ylabel) + pl.setXlabel(xlabel) + pl.setTitle(title) + + if(type == "text"): + pl.text(data) + elif(type == "map"): + pl.map(data) + elif(type == "maprank"): + pl.setShowRank(True) + pl.map(data) + else: + pl.plot(data) + +def showDescription(data=None): + print "Compute verification scores for COMPS verification files\n" + print "usage: verif files -m metric [-x x-dim] [-r thresholds]" + print " [-l locationIds] [-llrange latLonRange]" + print " [-o offsets] [-d start-date end-date]" + print " [-t training] [-c climFile] [-C ClimFile]" + print " [-xlim xlim] [-ylim ylim] [-c clim]" + print " [-type type] [-leg legend] [-hist] [-sort] [-acc]" + print " [-f imageFile] [-fs figSize] [-dpi dpi]" + print " [-b binType] [-nomargin] [-debug] [-ct ctype]" + print " [-ms markerSize] [-lw lineWidth] [-xrot XRotation]" + print " [-tickfs tickFontSize] [-labfs labFontSize] [-legfs legFontSize]" + print " [-majlth MajorTickLength] [-minlth MinorTickLength] [-majwid MajorTickWidth]" + print " [-bot Bottom] [-top Top] [-left Left] [-right Right]" + print " [-sp] [-ylabel Y Axis Label] [-xlabel X Axis Label]" + print " [-title Title]" + #print " [-pad Pad]" + print "" + print Common.green("Arguments:") + print " files One or more COMPS verification files in NetCDF format." + print " metric Verification score to use. See available metrics below." + print " x-dim Plot this dimension on the x-axis: date, offset, location, locationId," + print " locationElev, locationLat, locationLon, threshold, or none. Not supported by" + print " all metrics. If not specified, then a default is used based on the metric." + print " 'none' collapses all dimensions and computes one value." + print " thresholds Compute scores for these thresholds (only used by some metrics)." + print " locationIds Limit the verification to these location IDs." + print " latLonRange Limit the verification to locations within minlon,maxlon,minlat,maxlat." + print " offsets Limit the verification to these offsets (in hours)." + print " start-date YYYYMMDD. Only use dates from this day and on" + print " end-date YYYYMMDD. Only use dates up to and including this day" + print " training Remove this many days from the beginning of the verification." + print " climFile NetCDF containing climatology data. Subtract all forecasts and" + print " obs with climatology values." + print " ClimFile NetCDF containing climatology data. Divide all forecasts and" + print " obs by climatology values." + print " xlim Force x-axis limits to the two values lower,upper" + print " ylim Force y-axis limits to the two values lower,upper" + print " clim Force colorbar limits to the two values lower,upper" + print " type One of 'plot' (default),'text', 'map', or 'maprank'." + print " -hist Plot values as histogram. Only works for non-derived metrics" + print " -sort Plot values sorted. Only works for non-derived metrics" + print " -acc Plot accumulated values. Only works for non-derived metrics" + print " legend Comma-separated list of legend titles. Use '_' to represent space." + print " imageFile Save image to this filename" + print " figSize Set figure size width,height (in inches). Default 8x6." + print " dpi Resolution of image in dots per inch (default 100)" + print " binType One of 'below', 'within', or 'above'. For threshold plots (ets, hit, within, etc)" + print " 'below/above' computes frequency below/above the threshold, and 'within' computes" + print " the frequency between consecutive thresholds." + print " -nomargin Remove margins (whitespace) in the plot" + print " not x[i] <= T." + print " -debug Show statistics about files" + print " cType Collapsing type: 'min', 'mean', 'median', or 'max. When a score from the file is plotted" + print " (such as -m 'fcst'), the min/mean/meadian/max will be shown for each value on the x-axis" + print " markerSize How big should markers be?" + print " lineWidth How wide should lines be?" + print " XRotation Rotation angle for x-axis labels" + print " tickFontSize Font size for axis ticks" + print " labFontSize Font size for axis labels" + print " legFontSize Font size for legend" + print " MajorTickLength Length of major tick marks" + print " MinorTickLength Length of minor tick marks" + print " MajorTickWidth Adjust the thickness of the major tick marks" + print " Bottom Bottom boundary location for saved figure [range 0-1]" + print " Top Top boundary location for saved figure [range 0-1]" + print " Left Left boundary location for saved figure [range 0-1]" + print " Right Right boundary location for saved figure [range 0-1]" + print " -sp Show a line indicating the perfect score" + print " -ylabel Custom Y-axis Label" + print " -xlabel Custom X-axis Label" + print " -title Custom Title to Chart Top" + print "" + metrics = Metric.getAllMetrics() + outputs = Output.getAllOutputs() + print Common.green("Metrics (-m):") + for m in metrics+outputs: + name = m[0].lower() + desc = m[1].summary() + if(desc != ""): + print " %-14s%s" % (name, textwrap.fill(desc, 80).replace('\n', '\n ')), + print "" + if(data != None): + print "" + print " Or one of the following, which plots the raw score from the file:" + metrics = data.getMetrics() + for metric in metrics: + print " " + metric + +if __name__ == '__main__': + main()