Replace quantiles by percentile characteristic for statistics component (#81027)
* Remove quantiles characteristic * Add percentile characteristic
This commit is contained in:
parent
727dcd6df6
commit
a0b0e4088c
2 changed files with 71 additions and 36 deletions
|
@ -78,7 +78,7 @@ STAT_DISTANCE_ABSOLUTE = "distance_absolute"
|
||||||
STAT_MEAN = "mean"
|
STAT_MEAN = "mean"
|
||||||
STAT_MEDIAN = "median"
|
STAT_MEDIAN = "median"
|
||||||
STAT_NOISINESS = "noisiness"
|
STAT_NOISINESS = "noisiness"
|
||||||
STAT_QUANTILES = "quantiles"
|
STAT_PERCENTILE = "percentile"
|
||||||
STAT_STANDARD_DEVIATION = "standard_deviation"
|
STAT_STANDARD_DEVIATION = "standard_deviation"
|
||||||
STAT_SUM = "sum"
|
STAT_SUM = "sum"
|
||||||
STAT_SUM_DIFFERENCES = "sum_differences"
|
STAT_SUM_DIFFERENCES = "sum_differences"
|
||||||
|
@ -107,7 +107,7 @@ STATS_NUMERIC_SUPPORT = {
|
||||||
STAT_MEAN,
|
STAT_MEAN,
|
||||||
STAT_MEDIAN,
|
STAT_MEDIAN,
|
||||||
STAT_NOISINESS,
|
STAT_NOISINESS,
|
||||||
STAT_QUANTILES,
|
STAT_PERCENTILE,
|
||||||
STAT_STANDARD_DEVIATION,
|
STAT_STANDARD_DEVIATION,
|
||||||
STAT_SUM,
|
STAT_SUM,
|
||||||
STAT_SUM_DIFFERENCES,
|
STAT_SUM_DIFFERENCES,
|
||||||
|
@ -135,7 +135,6 @@ STATS_NOT_A_NUMBER = {
|
||||||
STAT_DATETIME_OLDEST,
|
STAT_DATETIME_OLDEST,
|
||||||
STAT_DATETIME_VALUE_MAX,
|
STAT_DATETIME_VALUE_MAX,
|
||||||
STAT_DATETIME_VALUE_MIN,
|
STAT_DATETIME_VALUE_MIN,
|
||||||
STAT_QUANTILES,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
STATS_DATETIME = {
|
STATS_DATETIME = {
|
||||||
|
@ -157,6 +156,7 @@ STAT_NUMERIC_RETAIN_UNIT = {
|
||||||
STAT_MEAN,
|
STAT_MEAN,
|
||||||
STAT_MEDIAN,
|
STAT_MEDIAN,
|
||||||
STAT_NOISINESS,
|
STAT_NOISINESS,
|
||||||
|
STAT_PERCENTILE,
|
||||||
STAT_STANDARD_DEVIATION,
|
STAT_STANDARD_DEVIATION,
|
||||||
STAT_SUM,
|
STAT_SUM,
|
||||||
STAT_SUM_DIFFERENCES,
|
STAT_SUM_DIFFERENCES,
|
||||||
|
@ -177,13 +177,10 @@ CONF_STATE_CHARACTERISTIC = "state_characteristic"
|
||||||
CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size"
|
CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size"
|
||||||
CONF_MAX_AGE = "max_age"
|
CONF_MAX_AGE = "max_age"
|
||||||
CONF_PRECISION = "precision"
|
CONF_PRECISION = "precision"
|
||||||
CONF_QUANTILE_INTERVALS = "quantile_intervals"
|
CONF_PERCENTILE = "percentile"
|
||||||
CONF_QUANTILE_METHOD = "quantile_method"
|
|
||||||
|
|
||||||
DEFAULT_NAME = "Stats"
|
DEFAULT_NAME = "Stats"
|
||||||
DEFAULT_PRECISION = 2
|
DEFAULT_PRECISION = 2
|
||||||
DEFAULT_QUANTILE_INTERVALS = 4
|
|
||||||
DEFAULT_QUANTILE_METHOD = "exclusive"
|
|
||||||
ICON = "mdi:calculator"
|
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_SAMPLES_MAX_BUFFER_SIZE): vol.Coerce(int),
|
||||||
vol.Optional(CONF_MAX_AGE): cv.time_period,
|
vol.Optional(CONF_MAX_AGE): cv.time_period,
|
||||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
|
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
|
||||||
vol.Optional(
|
vol.Optional(CONF_PERCENTILE, default=50): vol.All(
|
||||||
CONF_QUANTILE_INTERVALS, default=DEFAULT_QUANTILE_INTERVALS
|
vol.Coerce(int), vol.Range(min=1, max=99)
|
||||||
): vol.All(vol.Coerce(int), vol.Range(min=2)),
|
|
||||||
vol.Optional(CONF_QUANTILE_METHOD, default=DEFAULT_QUANTILE_METHOD): vol.In(
|
|
||||||
["exclusive", "inclusive"]
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -283,8 +277,7 @@ async def async_setup_platform(
|
||||||
samples_max_buffer_size=config[CONF_SAMPLES_MAX_BUFFER_SIZE],
|
samples_max_buffer_size=config[CONF_SAMPLES_MAX_BUFFER_SIZE],
|
||||||
samples_max_age=config.get(CONF_MAX_AGE),
|
samples_max_age=config.get(CONF_MAX_AGE),
|
||||||
precision=config[CONF_PRECISION],
|
precision=config[CONF_PRECISION],
|
||||||
quantile_intervals=config[CONF_QUANTILE_INTERVALS],
|
percentile=config[CONF_PERCENTILE],
|
||||||
quantile_method=config[CONF_QUANTILE_METHOD],
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
update_before_add=True,
|
update_before_add=True,
|
||||||
|
@ -303,8 +296,7 @@ class StatisticsSensor(SensorEntity):
|
||||||
samples_max_buffer_size: int,
|
samples_max_buffer_size: int,
|
||||||
samples_max_age: timedelta | None,
|
samples_max_age: timedelta | None,
|
||||||
precision: int,
|
precision: int,
|
||||||
quantile_intervals: int,
|
percentile: int,
|
||||||
quantile_method: Literal["exclusive", "inclusive"],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Statistics sensor."""
|
"""Initialize the Statistics sensor."""
|
||||||
self._attr_icon: str = ICON
|
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_buffer_size: int = samples_max_buffer_size
|
||||||
self._samples_max_age: timedelta | None = samples_max_age
|
self._samples_max_age: timedelta | None = samples_max_age
|
||||||
self._precision: int = precision
|
self._precision: int = precision
|
||||||
self._quantile_intervals: int = quantile_intervals
|
self._percentile: int = percentile
|
||||||
self._quantile_method: Literal["exclusive", "inclusive"] = quantile_method
|
|
||||||
self._value: StateType | datetime = None
|
self._value: StateType | datetime = None
|
||||||
self._unit_of_measurement: str | None = None
|
self._unit_of_measurement: str | None = None
|
||||||
self._available: bool = False
|
self._available: bool = False
|
||||||
|
@ -700,18 +691,10 @@ class StatisticsSensor(SensorEntity):
|
||||||
return cast(float, self._stat_sum_differences()) / (len(self.states) - 1)
|
return cast(float, self._stat_sum_differences()) / (len(self.states) - 1)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _stat_quantiles(self) -> StateType:
|
def _stat_percentile(self) -> StateType:
|
||||||
if len(self.states) > self._quantile_intervals:
|
if len(self.states) >= 2:
|
||||||
return str(
|
percentiles = statistics.quantiles(self.states, n=100, method="exclusive")
|
||||||
[
|
return percentiles[self._percentile - 1]
|
||||||
round(quantile, self._precision)
|
|
||||||
for quantile in statistics.quantiles(
|
|
||||||
self.states,
|
|
||||||
n=self._quantile_intervals,
|
|
||||||
method=self._quantile_method,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _stat_standard_deviation(self) -> StateType:
|
def _stat_standard_deviation(self) -> StateType:
|
||||||
|
|
|
@ -391,7 +391,7 @@ async def test_age_limit_expiry(hass: HomeAssistant):
|
||||||
|
|
||||||
|
|
||||||
async def test_precision(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(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"sensor",
|
"sensor",
|
||||||
|
@ -433,6 +433,60 @@ async def test_precision(hass: HomeAssistant):
|
||||||
assert state.state == str(round(mean, 3))
|
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):
|
async def test_device_class(hass: HomeAssistant):
|
||||||
"""Test device class, which depends on the source entity."""
|
"""Test device class, which depends on the source entity."""
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
|
@ -753,13 +807,11 @@ async def test_state_characteristics(hass: HomeAssistant):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source_sensor_domain": "sensor",
|
"source_sensor_domain": "sensor",
|
||||||
"name": "quantiles",
|
"name": "percentile",
|
||||||
"value_0": STATE_UNKNOWN,
|
"value_0": STATE_UNKNOWN,
|
||||||
"value_1": STATE_UNKNOWN,
|
"value_1": STATE_UNKNOWN,
|
||||||
"value_9": [
|
"value_9": 9.2,
|
||||||
round(quantile, 2) for quantile in statistics.quantiles(VALUES_NUMERIC)
|
"unit": "°C",
|
||||||
],
|
|
||||||
"unit": None,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source_sensor_domain": "sensor",
|
"source_sensor_domain": "sensor",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue