Repository: pvlib/pvanalytics

Issue:
Add module-temperature/irradiance correlation check
PVFleets QA checks that the module temperature is correlated with the irradiance. This is a pass/fail quality check: if the temperature and irradiance have sufficiently high correlation over the entire series then the temperature sensor passes. It might be useful to apply the correlation check on rolling windows as well and return a boolean series rather than just a pass/fail.

I'm not sure where this should go in the `quality` module.

Discussion:

Patch:
diff --git a/docs/api.rst b/docs/api.rst
index 6abdc27..72ae5f4 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -144,6 +144,20 @@ Quality checks for weather data.
    quality.weather.temperature_limits
    quality.weather.wind_limits
 
+In addition to validating temperature by comparing with limits, module
+temperature should be positively correlated with irradiance. Poor
+correlation could indicate that the sensor has become detached from
+the module, for example. Unlike other functions in the
+:py:mod:`quality` module which return Boolean masks over the input
+series, this function returns a single Boolean value indicating
+whether the entire series has passed (``True``) or failed (``False``)
+the quality check.
+
+.. autosummary::
+   :toctree: generated/
+
+   quality.weather.module_temperature_check
+
 .. rubric:: References
 
 .. [1]  C. N. Long and Y. Shi, An Automated Quality Assessment and Control
diff --git a/pvanalytics/quality/weather.py b/pvanalytics/quality/weather.py
index caa7d88..5e331ba 100644
--- a/pvanalytics/quality/weather.py
+++ b/pvanalytics/quality/weather.py
@@ -1,5 +1,6 @@
 """Quality control functions for weather data."""
 from pvanalytics.quality import util
+from scipy import stats
 
 
 def temperature_limits(air_temperature, limits=(-35.0, 50.0)):
@@ -97,3 +98,30 @@ def wind_limits(wind_speed, limits=(0.0, 50.0)):
         upper_bound=limits[1],
         inclusive_lower=True
     )
+
+
+def module_temperature_check(module_temperature, irradiance,
+                             correlation_min=0.5):
+    """Test whether the module temperature is correlated with irradiance.
+
+    Parameters
+    ----------
+    module_temperature : Series
+        Time series of module temperature.
+    irradiance : Series
+        Time series of irradiance with the same index as
+        `module_temperature`. This should be of relatively high
+        quality (outliers and other problems removed).
+    correlation_min : float, default 0.5
+        Minimum correlation between `module_temperature` and
+        `irradiance` for the module temperature sensor to 'pass'
+
+    Returns
+    -------
+    bool
+        True if the correlation between `module_temperature` and
+        `irradiance` exceeds `correlation_min`.
+
+    """
+    _, _, r, _, _ = stats.linregress(module_temperature, irradiance)
+    return r > correlation_min


Test patch:
diff --git a/pvanalytics/tests/quality/test_weather.py b/pvanalytics/tests/quality/test_weather.py
index c2807e3..a32138e 100644
--- a/pvanalytics/tests/quality/test_weather.py
+++ b/pvanalytics/tests/quality/test_weather.py
@@ -2,6 +2,7 @@
 import pytest
 import pandas as pd
 import numpy as np
+from pvlib import location
 from pandas.util.testing import assert_series_equal
 from pvanalytics.quality import weather
 
@@ -87,3 +88,23 @@ def test_wind_limits(weather_data):
         weather_data['extreme_wind_flag'],
         check_names=False
     )
+
+
+def test_module_temperature():
+    """Module temperature is correlated with GHI."""
+    albuquerque = location.Location(
+        35.0844, -106.6504, altitude=5312, tz='MST'
+    )
+    times = pd.date_range(
+        start='01/01/2020',
+        end='03/01/2020',
+        freq='H',
+        tz='MST'
+    )
+    clearsky = albuquerque.get_clearsky(times, model='simplified_solis')
+    assert weather.module_temperature_check(
+        clearsky['ghi']*0.6, clearsky['ghi']
+    )
+    assert not weather.module_temperature_check(
+        clearsky['ghi']*(-0.6), clearsky['ghi']
+    )