| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- import Foundation
- /// allowed values are 0, 1, 2 or 3. It's the index in coefficients
- private var coefficientsRowToUse = 3
- /// Savitzky Golay coefficients
- private let coefficients = [
- [-3.0, 12.0, 17.0, 12.0, -3.0],
- [-2.0, 3.0, 6.0, 7.0, 6.0, 3.0, -2.0],
- [-21.0, 14.0, 39.0, 54.0, 59.0, 54.0, 39.0, 14.0, -21.0],
- [-36.0, 9.0, 44.0, 69.0, 84.0, 89.0, 84.0, 69.0, 44.0, 9.0, -36.0]
- ]
- /// an array with elements of a type that conforms to Smoothable, can be filtered using the Savitzky Golay algorithm
- protocol SavitzkyGolaySmoothable {
- /// value to be smoothed
- var value: Double { get set }
- }
- /// local help class
- private class IsSmoothable: SavitzkyGolaySmoothable {
- var value: Double = 0.0
- init(withValue value: Double = 0.0) {
- self.value = value
- }
- }
- extension Array where Element: SavitzkyGolaySmoothable {
- /// - apply Savitzky Golay filter
- /// - before applying the filter, the array will be prepended and append with a number of elements equal to the filterwidth, filterWidth default 5. Allowed values are 5, 4, 3, 2. If any other value is assigned, then 5 will be used
- /// - ...continue with 5 here in the explanation ...
- /// - for the 5 last elements and 5 first elements, a regression is done. This regression is done used to give values to the 5 prepended and appended values. Which means it's as if we draw a line through the first 5 and 5 last original values, and use this line to give values to the 5 prepended and appended values
- /// - the 5 prepended and appended values are then used in the filter algorithm, which means we can also filter the original 5 first and last elements
- /// see also example https://github.com/JohanDegraeve/xdripswift/wiki/Libre-value-smoothing
- mutating func smoothSavitzkyGolayQuaDratic(withFilterWidth filterWidth: Int = 5) {
- // filterWidthToUse is the value of filterWidth to use in the algorithm. By default filterWidthToUse = parameter value filterWidth
- var filterWidthToUse = filterWidth
- // calculate coefficientsRowToUse based on filterWdith
- switch filterWidth {
- case 5:
- coefficientsRowToUse = 3
- case 4:
- coefficientsRowToUse = 2
- case 3:
- coefficientsRowToUse = 1
- case 2:
- coefficientsRowToUse = 0
- default:
- // invalid filterWidth was given in parameterList, use default value
- coefficientsRowToUse = 3
- filterWidthToUse = 5
- }
- // using 5 here in the comments as value for filterWidthToUse
- // the amount of elements must be at least 5. If that's not the case then don't apply any smoothing
- guard count >= filterWidthToUse else { return }
- // create a new array, to which we will prepend and append 5 elements so that we can do also smoothing for the 5 last and 5 first values of the input array (which is self)
- // the 5 elements will be estimated by doing linear regression of the first 5 and last 5 elements of the original input array respectively
- // this is only a temporary array, but it will hold the elements of the original array, those elements will get a new value when doing the smoothing
- var tempArray = [SavitzkyGolaySmoothable]()
- for element in self {
- tempArray.append(element)
- }
- // now prepend and append with 5 elements, each with a default value 0.0
- for _ in 0 ..< filterWidthToUse {
- tempArray.insert(IsSmoothable(), at: 0)
- tempArray.append(IsSmoothable())
- }
- // so now we have tempArray, of length size of original array + 2 * 5
- // the first 5 and the last 5 elements are of type IsSmoothable with value 0
- // - indicesArray is a help array needed for the function linearRegressionCreator
- // - this will be the first parameter in the call to the linearRegression function, in fact it's an array of IsSmoothable with length = length of tempArray
- // - we give each IsSmoothable the value of the index, meaning from 0 up to (length of tempArray) - 1
- // - in fact it's not really smoothable, it's just because we use isSmoothable in function linearRegressionCreator
- var indicesArray = [SavitzkyGolaySmoothable]()
- for index in 0 ..< (count + (filterWidthToUse * 2)) {
- indicesArray.append(IsSmoothable(withValue: Double(index)))
- }
- /// - this is a piece of code that we will execute two times, once for the firs 5 elements, then for the last 5, so we put it in a closure variable
- /// - it calculates the regression function (which is nothing else but doing y = intercept + slope*x) for range defined by predictorRange in tempArray. It will be used for the 5 first and 5 last real values, ie the 5 first and 5 last real glucose values
- /// - then executes the regression for every element in the range defined by targetRange, again in tempArray
- let doRegression = { (predictorRange: Range<Int>, targetRange: Range<Int>) in
- // calculate the linearRegression function
- let linearRegression = linearRegressionCreator(indicesArray[predictorRange], tempArray[predictorRange])
- // ready to do the linear regression for the targetRange in tempArray
- for index in targetRange {
- tempArray[index].value = linearRegression(indicesArray[index].value)
- }
- }
- // now do the regression for the 5 first elements
- doRegression(filterWidthToUse ..< (filterWidthToUse * 2), 0 ..< filterWidthToUse)
- // now do the regression for the 5 last elements
- doRegression(
- (tempArray.count - filterWidthToUse * 2) ..< (tempArray.count - filterWidthToUse),
- (tempArray.count - filterWidthToUse) ..< tempArray.count
- )
- // now start filtering
- // initialize array that will hold the resulting filtered values
- var filteredValues = [Double]()
- // calculate divider
- let divider = coefficients[coefficientsRowToUse].reduce(0, { x, y in
- x + y
- })
- // filter each original value
- for _ in 0 ..< count {
- // add a new element to filteredValues, start value is 0.0
- // this new value will be the last element, so we access it with index filteredValues.count - 1
- filteredValues.append(0.0)
- // iterate through the coefficients
- for (index, coefficient) in coefficients[coefficientsRowToUse].enumerated() {
- filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] + coefficient *
- tempArray[index + filteredValues.count - 1].value
- }
- filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] / divider
- }
- // now assign the new values to the original objects
- for (index, _) in enumerated() {
- self[index].value = filteredValues[index]
- }
- }
- }
- /// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
- private func multiply(
- _ a: ArraySlice<SavitzkyGolaySmoothable>,
- _ b: ArraySlice<SavitzkyGolaySmoothable>
- ) -> ArraySlice<SavitzkyGolaySmoothable> {
- zip(a, b).map({ IsSmoothable(withValue: $0.value * $1.value) })[0 ..< a.count]
- }
- /// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
- private func average(_ input: ArraySlice<SavitzkyGolaySmoothable>) -> Double {
- (input.reduce(IsSmoothable(), { (x: SavitzkyGolaySmoothable, y: SavitzkyGolaySmoothable) in
- IsSmoothable(withValue: x.value + y.value) })).value / Double(input.count)
- }
- /// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
- private func linearRegressionCreator(
- _ xs: ArraySlice<SavitzkyGolaySmoothable>,
- _ ys: ArraySlice<SavitzkyGolaySmoothable>
- ) -> (Double) -> Double {
- let sum1 = average(multiply(ys, xs)) - average(xs) * average(ys)
- let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
- let slope = sum1 / sum2
- let intercept = average(ys) - slope * average(xs)
- return { x in intercept + slope * x }
- }
|