diff --git a/.github/instructions/dynamicdata-cache.instructions.md b/.github/instructions/dynamicdata-cache.instructions.md
index 1f9865616..da66ceba5 100644
--- a/.github/instructions/dynamicdata-cache.instructions.md
+++ b/.github/instructions/dynamicdata-cache.instructions.md
@@ -820,6 +820,20 @@ Filters Update changes based on reference equality or a custom predicate. If fil
---
+### StdDev
+
+Computes sample standard deviation as `sqrt(sum((value - mean)^2) / (count - 1))`. The configured fallback is returned when a changeset leaves fewer than two items. Integer selectors retain fractional means and variances.
+
+| Input | Behavior |
+|-------|----------|
+| **Add** | Includes the selected value and emits the updated result. |
+| **Update** | Removes the previous selected value, includes the current value, and emits the updated result. |
+| **Remove** | Removes the selected value and emits the updated result or fallback. |
+| **Refresh / Moved** | Does not adjust the aggregate; the current result is emitted for the changeset. |
+| **OnError / OnCompleted** | Forwards the terminal notification. |
+
+In-place property mutations require recomputation, such as `InvalidateWhen`, rather than a Refresh alone.
+
### Property Observation
| Operator | Behavior |
diff --git a/.github/instructions/dynamicdata-list.instructions.md b/.github/instructions/dynamicdata-list.instructions.md
index 1f969b3ff..bfdb3d537 100644
--- a/.github/instructions/dynamicdata-list.instructions.md
+++ b/.github/instructions/dynamicdata-list.instructions.md
@@ -498,6 +498,20 @@ myObservable.ToObservableChangeSet(expireAfter: item => TimeSpan.FromMinutes(5))
---
+### StdDev
+
+Computes sample standard deviation as `sqrt(sum((value - mean)^2) / (count - 1))`. The configured fallback is returned when a changeset leaves fewer than two items. Integer selectors retain fractional means and variances.
+
+| Input | Behavior |
+|-------|----------|
+| **Add / AddRange** | Includes the selected values and emits the updated result. |
+| **Replace** | Removes the previous selected value, includes the current value, and emits the updated result. |
+| **Remove / RemoveRange / Clear** | Removes the selected values and emits the updated result or fallback. |
+| **Refresh / Moved** | Does not adjust the aggregate; the current result is emitted for the changeset. |
+| **OnError / OnCompleted** | Forwards the terminal notification. |
+
+In-place property mutations require recomputation, such as `InvalidateWhen`, rather than a Refresh alone.
+
### Property Observation
```csharp
diff --git a/src/DynamicData.Tests/AggregationTests/StdDevFixture.cs b/src/DynamicData.Tests/AggregationTests/StdDevFixture.cs
new file mode 100644
index 000000000..2f4d2d278
--- /dev/null
+++ b/src/DynamicData.Tests/AggregationTests/StdDevFixture.cs
@@ -0,0 +1,175 @@
+// Copyright (c) 2011-2026 Roland Pheasant. All rights reserved.
+// Roland Pheasant licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Linq;
+using System.Reactive.Linq;
+
+using Bogus;
+
+using DynamicData.Aggregation;
+using DynamicData.Tests.Utilities;
+
+using FluentAssertions;
+
+using Xunit;
+using Xunit.Abstractions;
+
+namespace DynamicData.Tests.AggregationTests;
+
+/// Verifies sample standard deviation for the supported numeric overloads.
+public sealed class StdDevFixture
+{
+ private const int Seed = 0x2409_1071;
+
+ private readonly Randomizer _randomizer = new(Seed);
+
+ public StdDevFixture(ITestOutputHelper output)
+ => output.WriteLine($"{nameof(StdDevFixture)} seed: {Seed:X8}");
+
+ /// Verifies that cache standard deviation applies the sample divisor to the variance.
+ [Theory]
+ [InlineData(nameof(Int32))]
+ [InlineData(nameof(Int64))]
+ [InlineData(nameof(Single))]
+ [InlineData(nameof(Double))]
+ [InlineData(nameof(Decimal))]
+ public void Cache_ThreeEquallySpacedValues_ReportsTheirSpacing(string numericType)
+ {
+ // Arrange
+ var center = _randomizer.Int(-100, 100);
+ var spacing = _randomizer.Int(1, 30);
+ var values = new[] { center - spacing, center, center + spacing };
+ var fallback = _randomizer.Int(1, 100);
+ using var source = new TestSourceCache(static value => value);
+ var standardDeviation = numericType switch
+ {
+ nameof(Int32) => source.Connect().StdDev(static value => value, fallback),
+ nameof(Int64) => source.Connect().StdDev(static value => (long)value, fallback),
+ nameof(Single) => source.Connect().StdDev(static value => (float)value, fallback),
+ nameof(Double) => source.Connect().StdDev(static value => (double)value, fallback),
+ nameof(Decimal) => source.Connect().StdDev(static value => (decimal)value, fallback).Select(static value => (double)value),
+ _ => throw new ArgumentOutOfRangeException(nameof(numericType))
+ };
+ using var subscription = standardDeviation
+ .ValidateSynchronization()
+ .RecordValues(out var results);
+
+ // Act
+ source.AddOrUpdate(values);
+
+ // Assert
+ results.Error.Should().BeNull(because: "all values are within the supported numeric ranges");
+ results.RecordedValues.Should().ContainSingle(because: "one source edit produces one aggregate")
+ .Which.Should().Be(spacing, because: "the sample variance of three equally spaced values is the square of their spacing");
+ }
+
+ /// Verifies that list standard deviation applies the sample divisor to the variance.
+ [Theory]
+ [InlineData(nameof(Int32))]
+ [InlineData(nameof(Int64))]
+ [InlineData(nameof(Single))]
+ [InlineData(nameof(Double))]
+ [InlineData(nameof(Decimal))]
+ public void List_ThreeEquallySpacedValues_ReportsTheirSpacing(string numericType)
+ {
+ // Arrange
+ var center = _randomizer.Int(-100, 100);
+ var spacing = _randomizer.Int(1, 30);
+ var values = new[] { center - spacing, center, center + spacing };
+ var fallback = _randomizer.Int(1, 100);
+ using var source = new TestSourceList();
+ var standardDeviation = numericType switch
+ {
+ nameof(Int32) => source.Connect().StdDev(static value => value, fallback),
+ nameof(Int64) => source.Connect().StdDev(static value => (long)value, fallback),
+ nameof(Single) => source.Connect().StdDev(static value => (float)value, fallback),
+ nameof(Double) => source.Connect().StdDev(static value => (double)value, fallback),
+ nameof(Decimal) => source.Connect().StdDev(static value => (decimal)value, fallback).Select(static value => (double)value),
+ _ => throw new ArgumentOutOfRangeException(nameof(numericType))
+ };
+ using var subscription = standardDeviation
+ .ValidateSynchronization()
+ .RecordValues(out var results);
+
+ // Act
+ source.AddRange(values);
+
+ // Assert
+ results.Error.Should().BeNull(because: "all values are within the supported numeric ranges");
+ results.RecordedValues.Should().ContainSingle(because: "one source edit produces one aggregate")
+ .Which.Should().Be(spacing, because: "the sample variance of three equally spaced values is the square of their spacing");
+ }
+
+ /// Verifies that integer overloads retain the fractional part of the mean when calculating variance.
+ [Theory]
+ [InlineData(nameof(Int32))]
+ [InlineData(nameof(Int64))]
+ public void IntegerValues_FractionalMean_PreservesFractionalVariance(string numericType)
+ {
+ // Arrange
+ var start = _randomizer.Int(-100, 100);
+ var spacing = _randomizer.Int(2, 30);
+ var adjustment = _randomizer.Int(1, 2);
+ var values = new[] { start, start + spacing, start + spacing + spacing + adjustment };
+ var mean = values.Average();
+ var expected = Math.Sqrt(values.Sum(value => Math.Pow(value - mean, 2)) / (values.Length - 1));
+ var fallback = _randomizer.Int(1, 100);
+ using var source = new TestSourceCache(static value => value);
+ var standardDeviation = numericType switch
+ {
+ nameof(Int32) => source.Connect().StdDev(static value => value, fallback),
+ nameof(Int64) => source.Connect().StdDev(static value => (long)value, fallback),
+ _ => throw new ArgumentOutOfRangeException(nameof(numericType))
+ };
+ using var subscription = standardDeviation
+ .ValidateSynchronization()
+ .RecordValues(out var results);
+
+ // Act
+ source.AddOrUpdate(values);
+
+ // Assert
+ results.Error.Should().BeNull(because: "integer inputs with a fractional mean are valid");
+ results.RecordedValues.Should().ContainSingle(because: "one source edit produces one aggregate")
+ .Which.Should().BeApproximately(expected, 1e-10, because: "integer inputs do not imply an integer-valued mean or variance");
+ }
+
+ /// Verifies that clearing the input returns the configured fallback rather than evaluating an undefined variance.
+ [Theory]
+ [InlineData(nameof(Int32))]
+ [InlineData(nameof(Int64))]
+ [InlineData(nameof(Single))]
+ [InlineData(nameof(Double))]
+ [InlineData(nameof(Decimal))]
+ public void Cache_AllItemsRemoved_ReportsFallback(string numericType)
+ {
+ // Arrange
+ var center = _randomizer.Int(-100, 100);
+ var spacing = _randomizer.Int(1, 30);
+ var values = new[] { center - spacing, center, center + spacing };
+ var fallback = _randomizer.Int(1, 100);
+ using var source = new TestSourceCache(static value => value);
+ source.AddOrUpdate(values);
+ var standardDeviation = numericType switch
+ {
+ nameof(Int32) => source.Connect().StdDev(static value => value, fallback),
+ nameof(Int64) => source.Connect().StdDev(static value => (long)value, fallback),
+ nameof(Single) => source.Connect().StdDev(static value => (float)value, fallback),
+ nameof(Double) => source.Connect().StdDev(static value => (double)value, fallback),
+ nameof(Decimal) => source.Connect().StdDev(static value => (decimal)value, fallback).Select(static value => (double)value),
+ _ => throw new ArgumentOutOfRangeException(nameof(numericType))
+ };
+ using var subscription = standardDeviation
+ .ValidateSynchronization()
+ .RecordValues(out var results);
+
+ // Act
+ source.Clear();
+
+ // Assert
+ results.Error.Should().BeNull(because: "an empty collection has a defined fallback");
+ results.RecordedValues[^1].Should().Be(fallback, because: "sample variance is not calculated for fewer than two items");
+ }
+}
diff --git a/src/DynamicData/Aggregation/StdDevEx.cs b/src/DynamicData/Aggregation/StdDevEx.cs
index 30599f9ec..dddbb4c0f 100644
--- a/src/DynamicData/Aggregation/StdDevEx.cs
+++ b/src/DynamicData/Aggregation/StdDevEx.cs
@@ -7,8 +7,12 @@
namespace DynamicData.Aggregation;
///
-/// Extensions for calculating standard deviation.
+/// Extensions for calculating sample standard deviation.
///
+///
+/// Each changeset produces the square root of the sample variance, using the item count minus one as the variance divisor.
+/// When fewer than two items remain, the configured fallback value is returned instead.
+///
public static class StdDevEx
{
///
@@ -139,7 +143,7 @@ public static IObservable StdDev(this IObservableThe value selector.
/// The fallback value.
/// An observable which emits the standard deviation value.
- public static IObservable StdDev(this IObservable> source, Func valueSelector, int fallbackValue = 0) => source.StdDevCalc(t => (long)valueSelector(t), fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0d / (values.Count - 1)));
+ public static IObservable StdDev(this IObservable> source, Func valueSelector, int fallbackValue = 0) => source.StdDevCalc(t => (long)valueSelector(t), fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt((values.SumOfSquares - (((double)values.SumOfItems * values.SumOfItems) / values.Count)) / (values.Count - 1)));
///
/// Continual computation of the standard deviation of the values in the underlying data source.
@@ -150,7 +154,7 @@ public static IObservable StdDev(this IObservableThe fallback value.
/// An observable which emits the standard deviation value.
public static IObservable StdDev(this IObservable> source, Func valueSelector, long fallbackValue = 0) =>
- source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0d / (values.Count - 1)));
+ source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt((values.SumOfSquares - (((double)values.SumOfItems * values.SumOfItems) / values.Count)) / (values.Count - 1)));
///
/// Continual computation of the standard deviation of the values in the underlying data source.
@@ -161,7 +165,7 @@ public static IObservable StdDev(this IObservableThe fallback value.
/// An observable which emits the standard deviation value.
public static IObservable StdDev(this IObservable> source, Func valueSelector, decimal fallbackValue = 0M) =>
- source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0M / (values.Count - 1)));
+ source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Sqrt((values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) / (values.Count - 1)));
///
/// Continual computation of the standard deviation of the values in the underlying data source.
@@ -171,7 +175,7 @@ public static IObservable StdDev(this IObservableThe value selector.
/// The fallback value.
/// An observable which emits the standard deviation value.
- public static IObservable StdDev(this IObservable> source, Func valueSelector, double fallbackValue = 0) => source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0d / (values.Count - 1)));
+ public static IObservable StdDev(this IObservable> source, Func valueSelector, double fallbackValue = 0) => source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt((values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) / (values.Count - 1)));
///
/// Continual computation of the standard deviation of the values in the underlying data source.
@@ -181,7 +185,7 @@ public static IObservable StdDev(this IObservableThe value selector.
/// The fallback value.
/// An observable which emits the standard deviation value.
- public static IObservable StdDev(this IObservable> source, Func valueSelector, float fallbackValue = 0) => source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0d / (values.Count - 1)));
+ public static IObservable StdDev(this IObservable> source, Func valueSelector, float fallbackValue = 0) => source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt((values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) / (values.Count - 1)));
private static IObservable StdDevCalc(this IObservable> source, Func valueSelector, TResult fallbackValue, Func, TValue, StdDev> addAction, Func, TValue, StdDev> removeAction, Func, TResult> resultAction)
{