Nevin Jethmalani
Nevin Jethmalani

Reputation: 2826

Weighted Average of elements in dictionary

I have a dictionary like this:

let tempDict = [100:2, 101:3, 102:4]

I need to find the weighted average of all the elements in the dictionary. So I essentially want to do this

let keyMultValues = 100*2+101*3+102*4
let valTotal = 2+3+4
let final = keyMultValues/valTotal

I know I can loop through all the items in a dictionary but I thought maybe there is a more efficient and cleaner way to do this.

Is there a better way than this?

var keyMultValues = 0.0
tempDict.forEach({
    keyMultValues += $0.key*$0.value
})
let valTotal = reduce(priceDict.values, 0, +) //this part isn't working properly so any help you can give here would be great. I am using swift 4

Upvotes: 1

Views: 275

Answers (3)

CRD
CRD

Reputation: 53010

You started with:

var keyMultValues = 0
tempDict.forEach({
   keyMultValues += $0.key*$0.value
})
let valTotal = reduce(priceDict.values, 0, +) //this part isn't working properly so any help you can give here would be great. I am using swift 4

and ask:

Is there a better way than this?

Well your error is you've written the reduce incorrectly, it should have been:

tempDict.values.reduce(0, +)

and with this fix your code produces the correct (using integer arithmetic) result.

However your code does two passes over the data, the foreach and the reduce (and the other answers at the time of writing do two or three). Your own solution shows you only need to do one pass, just modify it to follow the same pattern for valTotal as you used for keyMultValues:

var keyMultValues = 0
var valTotal = 0
tempDict.forEach({
   let val = $0.value // we are going to use it twice
   keyMultValues += $0.key * val
   valTotal += val
})
let final = keyMultValues / valTotal

This uses just one pass over the data.

If your numbers are large you might need to do two passes using the "divide as you go algorithm", and if you wish to find the floating-point weighted average then you need to throw a few Double casts in. Both of those are covered by @Rob's answer.

Is one pass over the data really better (faster, less memory, whatever) than two? That is left as an exercise! I added this answer just to show you how close your original attempt was to a correct solution.

Upvotes: 0

Rob
Rob

Reputation: 437917

Two observations:

  1. If you're doing weighted average, you probably don't want to do integer calculation. You probably want floating point result.

  2. You may want to avoid overflow issues stemming from multiplying all the keys by the values and then adding them up and then dividing by the sum of the values. You might want to do that value by value:

Thus:

let sumOfWeights = dictionary.reduce(0.0) { $0 + Double($1.value) }
let weightedAverage = dictionary.reduce(0.0) { $0 + Double($1.key) * Double($1.value) / sumOfWeights }

Note, this works with the original values:

let dictionary = [100:2, 101:3, 102:4]

This also works with large values, too, e.g.:

let dictionary = [
    1_000_000_000: 20_000_000_000,
    1_000_000_001: 30_000_000_000,
    1_000_000_002: 40_000_000_000
]

Upvotes: 2

Alexander
Alexander

Reputation: 63331

How about this?

let keyValueProductsSum = dict.lazy.map(*).reduce(0, +)
let valueSum = dict.values.reduce(0, +)
let weightedAverage = keyValueProductsSum / valueSum

Upvotes: 0

Related Questions