Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## BusinessMath Library

### [2.1.4] - 2026-04-07

**Version 2.1.4** is a follow-up developer-hygiene release that completes
three cleanup items surfaced by the v2.1.3 work but deferred at the time:
the `Examples/` directory format-string violations, the unseeded
`generateRandomReturns` production function, and the duplicated local
`SeededRNG` declarations across 5 test files.

#### Fixed

- **`Examples/` directory `String(format:)` cleanup.** Both
`MultipleLinearRegressionExample.swift` (~22 calls) and
`LinearRegressionConvenienceExample.swift` (~18 calls) now use the
project's `value.number(N)` extension instead of the banned C-style
format ABI. Examples are not part of the package build target, so
these violations didn't fail CI before — but they were user-facing
reference code that propagated the bad pattern. Now consistent with
the rest of the codebase.

- **5 test files no longer declare a local `struct SeededRNG`.** The
duplicated MMIX-LCG generator now lives in a single canonical location
in `Tests/TestSupport/SeededRNG.swift` as the new `MMIXSeededRNG`
type. Bit-identical sequences preserved — no test assertions needed
re-tuning. The 5 files affected are:
- `Tests/BusinessMathTests/Statistics Tests/Descriptor Tests/Descriptives Tests.swift`
- `Tests/BusinessMathTests/Statistics Tests/Descriptor Tests/Dispersion Around the Mean Tests/Dispersion Around the Mean Tests.swift`
- `Tests/BusinessMathTests/Statistics Tests/Regression Tests/LinearRegressionConvenienceTests.swift`
- `Tests/BusinessMathTests/Statistics Tests/Regression Tests/DenseMatrixTests.swift`
- `Tests/BusinessMathTests/Statistics Tests/NonlinearRegressionTests.swift`

The new `TestSupport.MMIXSeededRNG` is a value type (struct) with
`mutating func next()` and `mutating func nextSigned()` — the latter
matches the `[-1, 1]` mapping that `DenseMatrixTests` previously used
inline.

#### Added

- **`generateRandomReturns(count:mean:stdDev:using:)`** — additive
overload of the existing `generateRandomReturns(count:mean:stdDev:)`
that accepts an explicit `RandomNumberGenerator`. Lets callers
(especially tests) supply a deterministic generator for reproducibility.
The unseeded original is now a thin wrapper that creates a
`SystemRandomNumberGenerator` and calls the seeded overload — same
observable behavior for existing callers, no breaking change. Also
fixed an edge case where `Double.random(in: 0.0...1.0)` returning
exactly 0 would cause `log(0) = -inf` in the Box-Muller transform;
guarded with `Double.leastNormalMagnitude`.

- **`TestSupport.MMIXSeededRNG`** — see above.

#### Notes

- Purely additive at the public API level. No types renamed, no
signatures changed.
- All 4817 tests from v2.1.3 continue to pass after the consolidation.
- DocC `.md` files in `Sources/BusinessMath/BusinessMath.docc/` still
contain ~62 `String(format:)` instances in their Swift code samples.
These are documentation, not compiled, but they propagate the bad
pattern to users who copy from the docs. Tracked as a future v2.1.5
cleanup.

### [2.1.3] - 2026-04-07

**Version 2.1.3** is a developer-hygiene release that cleans up the
Expand Down
56 changes: 28 additions & 28 deletions Examples/LinearRegressionConvenienceExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@ public func runLinearRegressionConvenienceExamples() {
do {
let result = try linearRegression(x: advertisingSpend, y: sales)

print("Model: Sales = \(String(format: "%.2f", result.intercept)) + \(String(format: "%.2f", result.coefficients[0]))×Advertising\n")
print("Model: Sales = \(result.intercept.number(2)) + \(result.coefficients[0].number(2))×Advertising\n")

print("Diagnostics:")
print(" • R² = \(String(format: "%.4f", result.rSquared))")
print(" • F-statistic p-value = \(String(format: "%.6f", result.fStatisticPValue))")
print(" • Advertising coefficient: \(String(format: "%.2f", result.coefficients[0]))")
print(" - Standard error: \(String(format: "%.2f", result.standardErrors[1]))")
print(" - p-value: \(String(format: "%.6f", result.pValues[1]))")
print(" - 95% CI: [\(String(format: "%.2f", result.confidenceIntervals[1].lower)), \(String(format: "%.2f", result.confidenceIntervals[1].upper))]\n")
print(" • R² = \(result.rSquared.number(4))")
print(" • F-statistic p-value = \(result.fStatisticPValue.number(6))")
print(" • Advertising coefficient: \(result.coefficients[0].number(2))")
print(" - Standard error: \(result.standardErrors[1].number(2))")
print(" - p-value: \(result.pValues[1].number(6))")
print(" - 95% CI: [\(result.confidenceIntervals[1].lower.number(2)), \(result.confidenceIntervals[1].upper.number(2))]\n")

print("Interpretation:")
print(" For every $1,000 increase in advertising spend,")
print(" sales increase by $\(String(format: "%.2f", result.coefficients[0] * 1000)).\n")
print(" sales increase by $\((result.coefficients[0] * 1000).number(2)).\n")
} catch {
print("Error: \(error)\n")
}
Expand All @@ -60,14 +60,14 @@ public func runLinearRegressionConvenienceExamples() {
do {
let result = try polynomialRegression(x: price, y: revenue, degree: 2)

print("Model: Revenue = \(String(format: "%.2f", result.intercept))")
print(" + \(String(format: "%.2f", result.coefficients[0]))×Price")
print(" + \(String(format: "%.2f", result.coefficients[1]))×Price²\n")
print("Model: Revenue = \(result.intercept.number(2))")
print(" + \(result.coefficients[0].number(2))×Price")
print(" + \(result.coefficients[1].number(2))×Price²\n")

print("Diagnostics:")
print(" • R² = \(String(format: "%.4f", result.rSquared))")
print(" • Adjusted R² = \(String(format: "%.4f", result.adjustedRSquared))")
print(" • VIF: \(result.vif.map { String(format: "%.2f", $0) })\n")
print(" • R² = \(result.rSquared.number(4))")
print(" • Adjusted R² = \(result.adjustedRSquared.number(4))")
print(" • VIF: \(result.vif.map { $0.number(2) })\n")

// Find optimal price (vertex of parabola)
let a = result.coefficients[1]
Expand All @@ -76,12 +76,12 @@ public func runLinearRegressionConvenienceExamples() {
let maxRevenue = result.intercept + b * optimalPrice + a * optimalPrice * optimalPrice

print("Optimal Pricing:")
print(" • Price: $\(String(format: "%.2f", optimalPrice))")
print(" • Maximum Revenue: $\(String(format: "%.2f", maxRevenue))K\n")
print(" • Price: $\(optimalPrice.number(2))")
print(" • Maximum Revenue: $\(maxRevenue.number(2))K\n")

print("Interpretation:")
print(" The quadratic model captures the inverted-U relationship.")
print(" Revenue increases with price up to $\(String(format: "%.2f", optimalPrice)), then decreases.\n")
print(" Revenue increases with price up to $\(optimalPrice.number(2)), then decreases.\n")
} catch {
print("Error: \(error)\n")
}
Expand All @@ -103,15 +103,15 @@ public func runLinearRegressionConvenienceExamples() {
do {
let result = try polynomialRegression(x: time, y: adoption, degree: 3)

print("Model: Adoption = \(String(format: "%.2f", result.intercept))")
print(" + \(String(format: "%.2f", result.coefficients[0]))×t")
print(" + \(String(format: "%.2f", result.coefficients[1]))×t²")
print(" + \(String(format: "%.2f", result.coefficients[2]))×t³\n")
print("Model: Adoption = \(result.intercept.number(2))")
print(" + \(result.coefficients[0].number(2))×t")
print(" + \(result.coefficients[1].number(2))×t²")
print(" + \(result.coefficients[2].number(2))×t³\n")

print("Diagnostics:")
print(" • R² = \(String(format: "%.6f", result.rSquared))")
print(" • R² = \(result.rSquared.number(6))")
print(" • All coefficients significant: \(result.pValues.allSatisfy { $0 < 0.05 } ? "✓" : "✗")")
print(" • VIF values: \(result.vif.map { String(format: "%.1f", $0) })")
print(" • VIF values: \(result.vif.map { $0.number(1) })")

if result.vif.contains(where: { $0 > 10 }) {
print(" ⚠️ High multicollinearity detected (VIF > 10)")
Expand Down Expand Up @@ -143,13 +143,13 @@ public func runLinearRegressionConvenienceExamples() {
let quadraticResult = try polynomialRegression(x: x, y: y, degree: 2)

print("Linear Model:")
print(" • R² = \(String(format: "%.4f", linearResult.rSquared))")
print(" • Residual SE = \(String(format: "%.2f", linearResult.residualStandardError))\n")
print(" • R² = \(linearResult.rSquared.number(4))")
print(" • Residual SE = \(linearResult.residualStandardError.number(2))\n")

print("Quadratic Model:")
print(" • R² = \(String(format: "%.6f", quadraticResult.rSquared))")
print(" • Adjusted R² = \(String(format: "%.6f", quadraticResult.adjustedRSquared))")
print(" • Residual SE = \(String(format: "%.6f", quadraticResult.residualStandardError))\n")
print(" • R² = \(quadraticResult.rSquared.number(6))")
print(" • Adjusted R² = \(quadraticResult.adjustedRSquared.number(6))")
print(" • Residual SE = \(quadraticResult.residualStandardError.number(6))\n")

print("Conclusion:")
print(" The quadratic model is superior (R² closer to 1, lower residual error).")
Expand Down
71 changes: 27 additions & 44 deletions Examples/MultipleLinearRegressionExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,22 @@ do {
let result = try multipleLinearRegression(X: X1, y: sales)

print("\nModel: Sales = β₀ + β₁ × Advertising")
print(String(format: " Sales = %.2f + %.2f × Advertising",
result.intercept, result.coefficients[0]))
print(String(format: "\nR² = %.4f (%.1f%% of variance explained)",
result.rSquared, result.rSquared * 100))
print(String(format: "F-statistic = %.2f (p = %.6f)",
result.fStatistic, result.fStatisticPValue))
print(" Sales = \(result.intercept.number(2)) + \(result.coefficients[0].number(2)) × Advertising")
print("\nR² = \(result.rSquared.number(4)) (\((result.rSquared * 100).number(1))% of variance explained)")
print("F-statistic = \(result.fStatistic.number(2)) (p = \(result.fStatisticPValue.number(6)))")

if result.fStatisticPValue < 0.05 {
print("✓ Model is statistically significant (p < 0.05)")
}

print("\nCoefficient Details:")
print(String(format: " Intercept: %.2f (SE = %.2f, p = %.4f)",
result.intercept, result.standardErrors[0], result.pValues[0]))
print(String(format: " Advertising: %.2f (SE = %.2f, p = %.4f)",
result.coefficients[0], result.standardErrors[1], result.pValues[1]))
print(" Intercept: \(result.intercept.number(2)) (SE = \(result.standardErrors[0].number(2)), p = \(result.pValues[0].number(4)))")
print(" Advertising: \(result.coefficients[0].number(2)) (SE = \(result.standardErrors[1].number(2)), p = \(result.pValues[1].number(4)))")

// Make a prediction
let newAdvertising = 55.0
let predictedSales = result.intercept + result.coefficients[0] * newAdvertising
print(String(format: "\nPrediction: $%.0fk advertising → $%.0fk sales",
newAdvertising, predictedSales))
print("\nPrediction: $\(newAdvertising.number(0))k advertising → $\(predictedSales.number(0))k sales")

} catch {
print("Error: \(error)")
Expand All @@ -70,22 +64,18 @@ do {
let result = try multipleLinearRegression(X: X2, y: prices)

print("\nModel: Price = β₀ + β₁×Size + β₂×Age")
print(String(format: " Price = %.2f + %.4f×Size + %.4f×Age",
result.intercept, result.coefficients[0], result.coefficients[1]))
print(" Price = \(result.intercept.number(2)) + \(result.coefficients[0].number(4))×Size + \(result.coefficients[1].number(4))×Age")

print(String(format: "\nModel Fit: R² = %.4f, Adjusted R² = %.4f",
result.rSquared, result.adjustedRSquared))
print("\nModel Fit: R² = \(result.rSquared.number(4)), Adjusted R² = \(result.adjustedRSquared.number(4))")

print("\nCoefficient Interpretations:")
print(String(format: " Size: $%.2f per sq ft (p = %.4f)",
result.coefficients[0], result.pValues[1]))
print(" Size: $\(result.coefficients[0].number(2)) per sq ft (p = \(result.pValues[1].number(4)))")

if result.pValues[1] < 0.05 {
print(" ✓ Size is a significant predictor")
}

print(String(format: " Age: $%.2f per year (p = %.4f)",
result.coefficients[1], result.pValues[2]))
print(" Age: $\(result.coefficients[1].number(2)) per year (p = \(result.pValues[2].number(4)))")

if result.pValues[2] < 0.05 {
print(" ✓ Age is a significant predictor")
Expand All @@ -96,31 +86,28 @@ do {
let predictorNames = ["Size", "Age"]
for (i, vif) in result.vif.enumerated() {
let status = vif < 5 ? "✓ Low" : vif < 10 ? "⚠️ Moderate" : "✗ High"
print(String(format: " %@: VIF = %.2f (%@)",
predictorNames[i], vif, status))
print(" \(predictorNames[i]): VIF = \(vif.number(2)) (\(status))")
}

// Confidence intervals
print("\n95% Confidence Intervals:")
for i in 0..<result.coefficients.count {
let ci = result.confidenceIntervals[i + 1]
print(String(format: " %@: [%.4f, %.4f]",
predictorNames[i], ci.lower, ci.upper))
print(" \(predictorNames[i]): [\(ci.lower.number(4)), \(ci.upper.number(4))]")
}

// Predict new house
let newHouse = [2500.0, 7.0] // 2500 sq ft, 7 years old
let predictedPrice = result.intercept +
result.coefficients[0] * newHouse[0] +
result.coefficients[1] * newHouse[1]
print(String(format: "\nPrediction: 2500 sq ft, 7 years old → $%.0fk",
predictedPrice))
print("\nPrediction: 2500 sq ft, 7 years old → $\(predictedPrice.number(0))k")

// Residual analysis
print(String(format: "\nResidual Analysis:"))
print(String(format: " Residual Std Error: %.2f", result.residualStandardError))
print("\nResidual Analysis:")
print(" Residual Std Error: \(result.residualStandardError.number(2))")
let meanAbsResidual = result.residuals.map(abs).reduce(0, +) / Double(result.residuals.count)
print(String(format: " Mean Absolute Residual: %.2f", meanAbsResidual))
print(" Mean Absolute Residual: \(meanAbsResidual.number(2))")

// Check for outliers
let outliers = result.residuals.enumerated().filter {
Expand Down Expand Up @@ -153,8 +140,8 @@ do {
let result = try multipleLinearRegression(X: X3, y: y3)

print("\nVIF Analysis:")
print(String(format: " x₁: VIF = %.2f", result.vif[0]))
print(String(format: " x₂: VIF = %.2f", result.vif[1]))
print(" x₁: VIF = \(result.vif[0].number(2))")
print(" x₂: VIF = \(result.vif[1].number(2))")

if result.vif.contains(where: { $0 > 10 }) {
print("\n⚠️ SEVERE multicollinearity detected (VIF > 10)!")
Expand All @@ -169,11 +156,8 @@ do {
// Show unstable coefficients due to multicollinearity
print("\nCoefficient Standard Errors:")
for i in 0..<result.coefficients.count {
print(String(format: " β%d: %.4f (SE = %.4f, relative SE = %.1f%%)",
i + 1,
result.coefficients[i],
result.standardErrors[i + 1],
(result.standardErrors[i + 1] / abs(result.coefficients[i])) * 100))
let relSE = (result.standardErrors[i + 1] / abs(result.coefficients[i])) * 100
print(" β\(i + 1): \(result.coefficients[i].number(4)) (SE = \(result.standardErrors[i + 1].number(4)), relative SE = \(relSE.number(1))%)")
}

} catch {
Expand Down Expand Up @@ -201,14 +185,14 @@ do {
let model2 = try multipleLinearRegression(X: X_complex, y: outcome)

print("\nModel 1 (Simple):")
print(String(format: " R² = %.4f", model1.rSquared))
print(String(format: " Adjusted R² = %.4f", model1.adjustedRSquared))
print(String(format: " Predictors: %d", model1.p))
print(" R² = \(model1.rSquared.number(4))")
print(" Adjusted R² = \(model1.adjustedRSquared.number(4))")
print(" Predictors: \(model1.p)")

print("\nModel 2 (With Noise Predictor):")
print(String(format: " R² = %.4f", model2.rSquared))
print(String(format: " Adjusted R² = %.4f", model2.adjustedRSquared))
print(String(format: " Predictors: %d", model2.p))
print(" R² = \(model2.rSquared.number(4))")
print(" Adjusted R² = \(model2.adjustedRSquared.number(4))")
print(" Predictors: \(model2.p)")

print("\nModel Comparison:")
if model2.adjustedRSquared > model1.adjustedRSquared {
Expand All @@ -220,8 +204,7 @@ do {

// Check if second predictor is significant
if model2.pValues[2] > 0.05 {
print(String(format: " → Second predictor not significant (p = %.4f > 0.05)",
model2.pValues[2]))
print(" → Second predictor not significant (p = \(model2.pValues[2].number(4)) > 0.05)")
}

} catch {
Expand Down
Loading
Loading