Replace quantiles by percentile characteristic for statistics component (#81027)

* Remove quantiles characteristic

* Add percentile characteristic
This commit is contained in:
Thomas Dietrich 2022-11-14 09:23:02 +01:00 committed by GitHub
parent 727dcd6df6
commit a0b0e4088c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 71 additions and 36 deletions

View File

@ -78,7 +78,7 @@ STAT_DISTANCE_ABSOLUTE = "distance_absolute"
STAT_MEAN = "mean"
STAT_MEDIAN = "median"
STAT_NOISINESS = "noisiness"
STAT_QUANTILES = "quantiles"
STAT_PERCENTILE = "percentile"
STAT_STANDARD_DEVIATION = "standard_deviation"
STAT_SUM = "sum"
STAT_SUM_DIFFERENCES = "sum_differences"
@ -107,7 +107,7 @@ STATS_NUMERIC_SUPPORT = {
STAT_MEAN,
STAT_MEDIAN,
STAT_NOISINESS,
STAT_QUANTILES,
STAT_PERCENTILE,
STAT_STANDARD_DEVIATION,
STAT_SUM,
STAT_SUM_DIFFERENCES,
@ -135,7 +135,6 @@ STATS_NOT_A_NUMBER = {
STAT_DATETIME_OLDEST,
STAT_DATETIME_VALUE_MAX,
STAT_DATETIME_VALUE_MIN,
STAT_QUANTILES,
}
STATS_DATETIME = {
@ -157,6 +156,7 @@ STAT_NUMERIC_RETAIN_UNIT = {
STAT_MEAN,
STAT_MEDIAN,
STAT_NOISINESS,
STAT_PERCENTILE,
STAT_STANDARD_DEVIATION,
STAT_SUM,
STAT_SUM_DIFFERENCES,
@ -177,13 +177,10 @@ CONF_STATE_CHARACTERISTIC = "state_characteristic"
CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size"
CONF_MAX_AGE = "max_age"
CONF_PRECISION = "precision"
CONF_QUANTILE_INTERVALS = "quantile_intervals"
CONF_QUANTILE_METHOD = "quantile_method"
CONF_PERCENTILE = "percentile"
DEFAULT_NAME = "Stats"
DEFAULT_PRECISION = 2
DEFAULT_QUANTILE_INTERVALS = 4
DEFAULT_QUANTILE_METHOD = "exclusive"
ICON = "mdi:calculator"
@ -248,11 +245,8 @@ _PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): vol.Coerce(int),
vol.Optional(CONF_MAX_AGE): cv.time_period,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
vol.Optional(
CONF_QUANTILE_INTERVALS, default=DEFAULT_QUANTILE_INTERVALS
): vol.All(vol.Coerce(int), vol.Range(min=2)),
vol.Optional(CONF_QUANTILE_METHOD, default=DEFAULT_QUANTILE_METHOD): vol.In(
["exclusive", "inclusive"]
vol.Optional(CONF_PERCENTILE, default=50): vol.All(
vol.Coerce(int), vol.Range(min=1, max=99)
),
}
)
@ -283,8 +277,7 @@ async def async_setup_platform(
samples_max_buffer_size=config[CONF_SAMPLES_MAX_BUFFER_SIZE],
samples_max_age=config.get(CONF_MAX_AGE),
precision=config[CONF_PRECISION],
quantile_intervals=config[CONF_QUANTILE_INTERVALS],
quantile_method=config[CONF_QUANTILE_METHOD],
percentile=config[CONF_PERCENTILE],
)
],
update_before_add=True,
@ -303,8 +296,7 @@ class StatisticsSensor(SensorEntity):
samples_max_buffer_size: int,
samples_max_age: timedelta | None,
precision: int,
quantile_intervals: int,
quantile_method: Literal["exclusive", "inclusive"],
percentile: int,
) -> None:
"""Initialize the Statistics sensor."""
self._attr_icon: str = ICON
@ -319,8 +311,7 @@ class StatisticsSensor(SensorEntity):
self._samples_max_buffer_size: int = samples_max_buffer_size
self._samples_max_age: timedelta | None = samples_max_age
self._precision: int = precision
self._quantile_intervals: int = quantile_intervals
self._quantile_method: Literal["exclusive", "inclusive"] = quantile_method
self._percentile: int = percentile
self._value: StateType | datetime = None
self._unit_of_measurement: str | None = None
self._available: bool = False
@ -700,18 +691,10 @@ class StatisticsSensor(SensorEntity):
return cast(float, self._stat_sum_differences()) / (len(self.states) - 1)
return None
def _stat_quantiles(self) -> StateType:
if len(self.states) > self._quantile_intervals:
return str(
[
round(quantile, self._precision)
for quantile in statistics.quantiles(
self.states,
n=self._quantile_intervals,
method=self._quantile_method,
)
]
)
def _stat_percentile(self) -> StateType:
if len(self.states) >= 2:
percentiles = statistics.quantiles(self.states, n=100, method="exclusive")
return percentiles[self._percentile - 1]
return None
def _stat_standard_deviation(self) -> StateType:

View File

@ -391,7 +391,7 @@ async def test_age_limit_expiry(hass: HomeAssistant):
async def test_precision(hass: HomeAssistant):
"""Test correct result with precision set."""
"""Test correct results with precision set."""
assert await async_setup_component(
hass,
"sensor",
@ -433,6 +433,60 @@ async def test_precision(hass: HomeAssistant):
assert state.state == str(round(mean, 3))
async def test_percentile(hass: HomeAssistant):
"""Test correct results for percentile characteristic."""
assert await async_setup_component(
hass,
"sensor",
{
"sensor": [
{
"platform": "statistics",
"name": "test_percentile_omitted",
"entity_id": "sensor.test_monitored",
"state_characteristic": "percentile",
"sampling_size": 20,
},
{
"platform": "statistics",
"name": "test_percentile_default",
"entity_id": "sensor.test_monitored",
"state_characteristic": "percentile",
"sampling_size": 20,
"percentile": 50,
},
{
"platform": "statistics",
"name": "test_percentile_min",
"entity_id": "sensor.test_monitored",
"state_characteristic": "percentile",
"sampling_size": 20,
"percentile": 1,
},
]
},
)
await hass.async_block_till_done()
for value in VALUES_NUMERIC:
hass.states.async_set(
"sensor.test_monitored",
str(value),
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_percentile_omitted")
assert state is not None
assert state.state == str(9.2)
state = hass.states.get("sensor.test_percentile_default")
assert state is not None
assert state.state == str(9.2)
state = hass.states.get("sensor.test_percentile_min")
assert state is not None
assert state.state == str(2.72)
async def test_device_class(hass: HomeAssistant):
"""Test device class, which depends on the source entity."""
assert await async_setup_component(
@ -753,13 +807,11 @@ async def test_state_characteristics(hass: HomeAssistant):
},
{
"source_sensor_domain": "sensor",
"name": "quantiles",
"name": "percentile",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_9": [
round(quantile, 2) for quantile in statistics.quantiles(VALUES_NUMERIC)
],
"unit": None,
"value_9": 9.2,
"unit": "°C",
},
{
"source_sensor_domain": "sensor",