El objetivo de esta lección es entender como funcionan distintos algoritmos para minería de flujos de datos, es decir información que no está almacenada sino se va adquiriendo de forma incremental.
Nota: Esta lección está basada en el Capítulo 4 del libro Mining of Massive Datasets. Rajaraman, Leskovec and Ullman 2012 y en las notas del curso de Métodos Analíticos del 2013 por Felipe Gonzalez.
Existen diversas razones por las que es necesario minar flujos de datos en tiempo real en memoria, en lugar de almacenarla para realizar consultas en otro momento, por ejemplo,
No obstante queremos tener la capacidad de analizar esta información conforme se genera.
Algunas procesos interesantes que podemos aplicar sobre estos flujos son por ejemplo:
Y algunos otros más simples:
A todo esto, ¿cuáles son algunos ejemplos de flujos?
Comencemos por explorar la forma más simple de análisis de flujos.
¿Cuáles son los algoritmos para encontrar estadisticas descriptivas sobre un flujo?
Las estadísticas de orden tienen una característica especial, esta es, que tienen que conocer el orden de los elementos para poder contestar a la pregunta, ¿Cuál es el valor del elemento que está a la mitad?
Otra posibilidad para consultar información sobre un flujo, es hacer consultas sobre ventanas de tiempo de tamaño $n$ que en general representan los $n$ elementos más recientes del flujo. O guardar un numero de elementos arbitrarios que permitan consultar los datos para un periodo de tiempo $t$.
Supongamos que queremos tomar una musestra de un flujo para posteriormente realizar operaciones sobre de ella, ¿Cómo hacemos para que la muestra sea representativa del flujo total? La estrategia de muestreo depende de que es lo que se quiere muestrear. Por ejemplo, dado un sistema que recibe información de múltiples sensores.
En el primer caso es sencillo realizar el muestreo, generar un número aleatorio entre 0 y $n$ donde $n$ es el inverso de la proporción $1/n$ que se quiere muestrear.
El segundo caso sin embargo, requiere que podamos identificar cuales son los usuarios que estamos muestreando, para que tomemos en cuenta todas las observaciones que les corresponden.
¿Cómo podemos utilizar hashing para no tener que almacenar una lista con todos los usuarios que sí debemos muestrear?
In [1]:
import random
NUM_USERS = 10000
rnd = random.Random(11)
events = list()
for i in range(1000000):
duration = 0
if rnd.random() > 0.2:
duration = rnd.randint(0, 200 - 1)
else:
duration = rnd.randint(200, 1000 - 1)
events.append((rnd.randint(0, NUM_USERS - 1), duration))
In [2]:
def sample(elements, pct):
sampled = list()
for elem in elements:
r = rnd.randint(0,int(1/pct))
if r == 0:
sampled.append(elem)
return sampled
def proportion(sampled, threshold):
count = 0
for event in sampled:
if event[1] > threshold:
count += 1
print "Proporción de eventos > %s: %.4f" % (threshold, count / float(len(sampled)))
In [3]:
# Contar eventos con duración mayor a 200
proportion(events, 200)
sampled = sample(events, .01)
proportion(sampled, 200)
In [4]:
def sampled_per_user(data):
r = rnd.randint(0, len(data))
user = data[r][0]
filtered = [_ for _ in data if _[0] == user]
proportion(filtered, 200)
In [20]:
# Que proporción de los eventos de un usuario aleatorio fueron mayores a 200
for i in range(5):
sampled_per_user(sampled)
Podemos observar que el número de eventos mayores a 200 para los usuarios muestreados no corresponde a los de su distribución. Ya que los eventos que la muestra se hace sobre todos elementos sin tomar en cuenta que algunos eventos para un usuario suceden con menor probabilidad
Nuestro objetivo es muestrear todos los eventos de la proporción correcta de los usuarios.
In [6]:
def sample_users(elements, pct):
seen = dict()
sampled = list()
for elem in elements:
user = elem[0]
if not user in seen:
r = rnd.randint(0,int(1/pct))
seen[user] = (r == 0)
if seen[user]:
sampled.append(elem)
return sampled
In [7]:
user_sampled = sample_users(events, 0.01)
proportion(user_sampled, 200)
In [8]:
# Que proporción de los eventos de un usuario aleatorio fueron mayores a 200
for i in range(5):
sampled_per_user(user_sampled)
El problema con este enfoque es que tenemos que mantener la información sobre todos los usuarios que hemos observado hasta el momento, para poder determinar si los tenemos que muestrear o no.
Una alternativa eficiente en memoria es... utilizar hashes. Si el hash del usuario corresponde a la cubeta 0 lo muestreamos de otra forma no.
In [9]:
def sample_users_hash(elements, pct):
sampled = list()
for elem in elements:
if hash(elem[0]) % int(1/pct) == 0:
sampled.append(elem)
return sampled
In [10]:
user_sampled_hash = sample_users_hash(events, 0.01)
proportion(user_sampled_hash, 200)
In [11]:
for i in range(5):
sampled_per_user(user_sampled_hash)
En este caso la función hash funciona como un generador de números aleatorios que siempre mapea al mismo usuario al mismo número aleatorio.
En general se puede obtener una muestra de una fracción $a/b$ aplicando hashes a $b$ cubetas y muestreando aquellos cuyo hash sea menor a $a$.
In [12]:
def sample_hash(elements, a, b):
sampled = list()
for elem in elements:
if hash(elem[0]) % b < a:
sampled.append(elem)
return sampled
In [13]:
sampled_hash = sample_hash(events, 1, 100)
proportion(sampled_hash, 200)
In [14]:
for i in range(5):
sampled_per_user(sampled_hash)
In [16]:
from collections import defaultdict
def get_proportion(sampled, threshold):
count = 0
for event in sampled:
if event[1] > threshold:
count += 1
return count / float(len(sampled))
def proportion_per_user(sdata):
per_user = defaultdict(int)
per_user_total = defaultdict(int)
for event in sdata:
per_user[event[0]] += (1 if event[1] > 200 else 0)
per_user_total[event[0]] += 1
sm = 0
for user, user_events in per_user.items():
sm += user_events / float(per_user_total[user])
print "Promedio de proporción de eventos por usuario > %s: %.2f @%s" % (200, sm / len(per_user), len(per_user))
proportion_per_user(sampled)
proportion_per_user(user_sampled)
proportion_per_user(user_sampled_hash)
proportion_per_user(events)
Otro proceso común a aplicar sobre flujos es el filtrado, nos gustaría tener un proceso para dejar pasar ciertos registros y no otros, por ejemplo filtrar las direcciones peligrosas en un navegador. Este proceso se complica cuando
Suponiendo que tenemos 1 megabyte de memoria disponible para almacenar esta información podríamos almacenar hasta 1 millon de registros de direcciones peligrosas utilizando un bit por dirección, utilizando... una función hash para mapear cada dirección a uno de los bits.
Cuando queremos validar una dirección,
Es posible que al utilizar este método algunas páginas que no son peligrosas mapen a un bit encendido. En cuyo caso tendremos un falso positivo.
Un filtro de bloom consiste en:
El objeto de esta función es dejar pasar aquellos elementos cuyo valor esté en el conjunto y no rechazar la mayoría de los que no están en el conjunto.
En el caso de las páginas maliciosas lo utilizamos de forma inversa pero el principio es el mismo.
A continuación analizamos las propiedades estadísticas del filtro. La cantidad que nos interesa conocer es cual es la probabilidad de un falso positivo, como función de $n$ la longitud del filtro, $m$ el número de elementos en el conjunto y $k$ el número de funciones.
La probabilidad de que un bit no sea 1 para un elemento y una función $h_k$ es:
$$1 - \frac{1}{n}$$De que no sea 1 para ninguna de las funciones $h_k$ es:
$$\left(1 - \frac{1}{n}\right)^k$$La probabilidad de que después de insertar $m$ elementos un bit aún sea 0 es:
$$\left(1 - \frac{1}{n}\right)^{km}$$Por lo tanto la probabilidad de que un elemento sea 1 después de insertar $m$ elementos es:
$$1 - \left(1 - \frac{1}{n}\right)^{km}$$Cuando insertamos un nuevo elemento cada uno de los las $k$ posiciones tiene una probabilidad de ser uno igual a $1 - \left(1 - \frac{1}{n}\right)^{km}$ por lo tanto la probabilidad de que las $k$ posiciones sean 1 es:
$$\left[1 - \left(1 - \frac{1}{n}\right)^{km}\right]^k \approx \left(1 - e^{-km/n}\right)^k$$Esta aproximación asume independencia entre cada una de las probabilidades, si bien esto no es estrictamente correcto para valores grandes de $n$ y valores chicos de $k$ la aproximación es buena. Ver http://people.scs.carleton.ca/~kranakis/Papers/TR-07-07.pdf
Por lo tanto podemos concluir que la probabilidad de falsos positivos disminuye conforme utilizamos arreglos de bits más grandes (valores más grandes de $n$) e incrementa conforme agregamos más elementos al conjunto (valores más grandes de $m$).
Dado $n$ y $m$ el valor de $k$ que minimiza la probabilidad de falsos positivos es:
$$k = \frac{n}{m}\text{ln }2$$El número de bits $n$, requerido para mantener esta probabilidad de falsos positivos es:
$$n = -\frac{m\text{ ln }p}{(\text{ln }2)^2}$$
In [ ]: