Current section
Files
Jump to
Current section
Files
lib/kmv.ex
defmodule KMV do
@moduledoc """
K-minimum value sketch to estimate cardinality of sets.
KMV can also be used to perform set operations:
- union
- intersection
- difference
Implementation is based on the paper: "On synopses for distinct-value
estimation under multiset operations" by Beyer et al, 2007.
"""
# KMV struct
defstruct heap: nil, set: nil, max_size: nil, size: nil
@type t :: %KMV{heap: Heap.t, set: Set.t, max_size: integer, size: integer}
#Hash a value and returns value between [0, 1].
@spec hash(any) :: float
defp hash(v) do
Murmur.hash_x64_128(v, 42) / :math.pow(2,128)
end
@doc """
Create a new KMV of max size `k`.
Increasing the size of `k` increases the accuracy.
Returns: `KMV`
## Parameters
- k: Max size of the KMV. Must be greater than 0.
"""
@spec new() :: KMV.t
@spec new(integer) :: KMV.t
def new(k \\ 1024) when k > 0 do
%KMV{heap: Heap.max, set: MapSet.new, max_size: k, size: 0}
end
@doc """
Insert a value into a `KMV`.
Returns: `KMV`
## Parameters
- kmv: `KMV` sketch.
## Example
Create a `KMV` of size 3, insert 6 elements, and estimate cardinality.
iex> a = KMV.new(3)
iex> a = KMV.insert(a, "test")
iex> a = KMV.insert(a, 1)
iex> a = KMV.insert(a, 2)
iex> a = KMV.insert(a, 3)
iex> a = KMV.insert(a, :thisAlsoWorks)
iex> a = KMV.insert(a, [:a, :list, :becomes, :a, :single, :value])
iex> KMV.cardinality(a) |> round
7
"""
@spec insert(KMV.t, any) :: KMV.t
def insert(kmv, value)
def insert(s = %KMV{heap: heap, set: set, max_size: max_size, size: size}, value) when size < max_size do
# Generate the hash for the input
value_hash = hash(value)
if !MapSet.member?(set, value_hash) do
%KMV{heap: Heap.push(heap, value_hash),
set: MapSet.put(set, value_hash),
max_size: max_size,
size: size + 1}
else
s
end
end
def insert(s = %KMV{heap: heap, set: set, max_size: max_size, size: size}, value) do
# Generate the hash for the input
value_hash = hash(value)
if !MapSet.member?(set, value_hash) && value_hash < Heap.root(heap) do
set = MapSet.delete(set, Heap.root(heap))
heap = Heap.pop(heap)
%KMV{heap: Heap.push(heap, value_hash),
set: MapSet.put(set, value_hash),
max_size: max_size,
size: size}
else
s
end
end
@doc """
Estimate the cardinality of the set in the KMV.
Returns: `float`
## Parameters
- kmv: `KMV`
## Example
Insert 10,000 unique values into a new KMV of size 500, and estimate its
cardinality which should be around 10,000.
iex> 1..10_000 |> Enum.into(KMV.new(500)) |> KMV.cardinality |> round
9_693
"""
@spec cardinality(KMV.t) :: float
def cardinality(kmv)
def cardinality(%KMV{heap: _heap, set: _set, max_size: max_size, size: size}) when size < max_size do
# Have inserted fewer unique items than max_size, can return exact count
size
end
def cardinality(%KMV{heap: heap, set: _set, max_size: _max_size, size: size}) do
#
(size - 1) / Heap.root(heap)
end
@doc """
Union a list of `KMV` sketches.
Returns: `KMV`
## Parameters
- kmv_list: A list of `KMV`
## Example
The union the three `KMV` sketches around 12,000.
iex> a = 10_000..15_000 |> Enum.into(KMV.new(500))
iex> b = 13_000..18_000 |> Enum.into(KMV.new(500))
iex> c = 15_000..22_000 |> Enum.into(KMV.new(500))
iex> KMV.union([a, b, c]) |> KMV.cardinality |> round
12_840
"""
@spec union(nonempty_list(KMV.t)) :: KMV.t
def union(kmv_list) when length(kmv_list) > 1 do
# Find the min size of the KMV list
min_size = Enum.min(for %KMV{size: size} <- kmv_list, do: size)
data = (for %KMV{set: set} <- kmv_list, do: set)
|> Enum.reduce(&(MapSet.union(&1, &2)))
|> MapSet.to_list
|> Enum.sort
|> Enum.take(min_size)
heap = data |> Enum.into(Heap.max)
%KMV{heap: heap,
set: MapSet.new(data),
max_size: min_size,
size: min_size}
end
@doc """
Intersection of two `KMV` sketches.
Returns: `float`
## Parameters
- kmv_list: A list of `KMV`
## Example
The intersection of the three `KMV` sketches should be around 25,000.
iex> a = 0..100_000 |> Enum.into(KMV.new(500))
iex> b = 50_000..150_000 |> Enum.into(KMV.new(500))
iex> c = 25_000..75_000 |> Enum.into(KMV.new(500))
iex> KMV.intersection([a, b, c]) |> round
25_074
"""
@spec intersection(nonempty_list(KMV.t)) :: float
def intersection(kmv_list) when length(kmv_list) > 1 do
# Find the min size of the KMV list
size_list = for %KMV{size: size} <- kmv_list, do: size
min_size = Enum.min(size_list)
total_union = union(kmv_list)
cardinality_of_union = cardinality total_union
# Must use the union in the intersection
kmv_list = [total_union | kmv_list]
number_of_intersections = (for %KMV{set: set} <- kmv_list, do: set)
|> Enum.reduce(&(MapSet.intersection(&1, &2)))
|> MapSet.size
(number_of_intersections / min_size) * cardinality_of_union
end
end