FeatureDistributionComparison.java
package org.hammer.audio.experimental.acoustic.wingbeat;
import java.util.Locale;
import java.util.Objects;
import org.hammer.audio.experimental.acoustic.dataset.DatasetAnalytics;
/**
* Comparison of the distribution of a single feature between a synthetic dataset and a real
* dataset.
*
* <p>A {@code FeatureDistributionComparison} captures descriptive statistics for the same feature
* extracted from two corpora — synthetic (simulated) recordings and real field recordings — along
* with computed absolute and relative differences between the two means.
*
* <p>Use {@link #of(String, DatasetAnalytics.DistributionStats, DatasetAnalytics.DistributionStats)
* } to construct an instance from pre-computed statistics and {@link #toMarkdown()} to render a
* human-readable comparison table suitable for evaluation reports.
*
* @param featureName human-readable feature name; must not be blank
* @param syntheticStats descriptive statistics over the synthetic dataset; must not be {@code null}
* @param realStats descriptive statistics over the real dataset; must not be {@code null}
* @param absoluteDifference absolute difference between the two means ({@code |real − synthetic|});
* must be finite
* @param relativeDifference relative difference as a fraction ({@code absoluteDifference /
* |syntheticMean|}, or {@code 0} when the synthetic mean is zero); must be finite
*/
@SuppressWarnings("PMD.DanglingJavadoc")
public record FeatureDistributionComparison(
String featureName,
DatasetAnalytics.DistributionStats syntheticStats,
DatasetAnalytics.DistributionStats realStats,
double absoluteDifference,
double relativeDifference) {
/** Validate fields. */
public FeatureDistributionComparison {
Objects.requireNonNull(featureName, "featureName");
Objects.requireNonNull(syntheticStats, "syntheticStats");
Objects.requireNonNull(realStats, "realStats");
if (featureName.isBlank()) {
throw new IllegalArgumentException("featureName must not be blank");
}
if (!Double.isFinite(absoluteDifference)) {
throw new IllegalArgumentException("absoluteDifference must be finite");
}
if (!Double.isFinite(relativeDifference)) {
throw new IllegalArgumentException("relativeDifference must be finite");
}
}
/**
* Build a comparison from pre-computed statistics for the same feature in two corpora.
*
* <p>The absolute difference is {@code |realMean − syntheticMean|}. The relative difference is
* {@code absoluteDifference / |syntheticMean|} when {@code syntheticMean != 0}, or {@code 0}
* otherwise.
*
* @param featureName feature name; must not be blank
* @param synthetic statistics over the synthetic corpus; must not be {@code null}
* @param real statistics over the real corpus; must not be {@code null}
* @return comparison record; never {@code null}
*/
public static FeatureDistributionComparison of(
String featureName,
DatasetAnalytics.DistributionStats synthetic,
DatasetAnalytics.DistributionStats real) {
Objects.requireNonNull(featureName, "featureName");
Objects.requireNonNull(synthetic, "synthetic");
Objects.requireNonNull(real, "real");
double absDiff = Math.abs(real.mean() - synthetic.mean());
double relDiff = synthetic.mean() == 0.0 ? 0.0 : absDiff / Math.abs(synthetic.mean());
return new FeatureDistributionComparison(featureName, synthetic, real, absDiff, relDiff);
}
/**
* Render this comparison as a Markdown table section.
*
* @return Markdown snippet; never {@code null}
*/
@SuppressWarnings({
"PMD.ConsecutiveLiteralAppends",
"PMD.ConsecutiveAppendsShouldReuse",
"PMD.AvoidDuplicateLiterals"
})
public String toMarkdown() {
StringBuilder sb = new StringBuilder(256);
sb.append("### ").append(featureName).append("\n\n");
sb.append("| Statistic | Synthetic | Real | Δ abs | Δ rel |\n|---|---:|---:|---:|---:|\n");
sb.append("| Count | ")
.append(syntheticStats.count())
.append(" | ")
.append(realStats.count())
.append(" | | |\n");
sb.append("| Mean | ")
.append(String.format(Locale.ROOT, "%.3f", syntheticStats.mean()))
.append(" | ")
.append(String.format(Locale.ROOT, "%.3f", realStats.mean()))
.append(" | ")
.append(String.format(Locale.ROOT, "%.3f", absoluteDifference))
.append(" | ")
.append(String.format(Locale.ROOT, "%.1f%%", relativeDifference * 100.0))
.append(" |\n");
sb.append("| Std dev | ")
.append(String.format(Locale.ROOT, "%.3f", syntheticStats.stddev()))
.append(" | ")
.append(String.format(Locale.ROOT, "%.3f", realStats.stddev()))
.append(" | | |\n");
sb.append("| Min | ")
.append(String.format(Locale.ROOT, "%.3f", syntheticStats.min()))
.append(" | ")
.append(String.format(Locale.ROOT, "%.3f", realStats.min()))
.append(" | | |\n");
sb.append("| Max | ")
.append(String.format(Locale.ROOT, "%.3f", syntheticStats.max()))
.append(" | ")
.append(String.format(Locale.ROOT, "%.3f", realStats.max()))
.append(" | | |\n");
sb.append('\n');
return sb.toString();
}
}