1. Descripción
Para asegurarnos de que todo esté bien con los datos, debemos centrarnos en dos cosas:
- No hay eventos faltantes ni duplicados -> los recuentos de eventos y sesiones están dentro del rango esperado.
- Los datos son correctos -> la distribución de valores para cada parámetro sigue siendo la misma, la otra versión aún tiene que comenzar a registrar todos los navegadores como Safari o dejar de rastrear las compras por completo.
Hoy quiero contarles mi experiencia con esta compleja tarea. Como beneficio adicional, mostraré ejemplos de funciones de matriz de ClickHouse.
Foto de Luke Chesser en Unsplash
2. ¿Qué es el análisis de redes?
Los sistemas de análisis web registran una gran cantidad de información sobre eventos en un sitio web, por ejemplo, qué navegadores y sistemas operativos utilizan los clientes, qué URL visitan, cuánto tiempo pasan en el sitio web e incluso qué productos agregan a sus compras. carros y compra. Todos estos datos se pueden usar para informes (para comprender cuántos clientes visitaron el sitio) o análisis (para comprender los puntos débiles y mejorar la experiencia del cliente). Puede encontrar más detalles sobre análisis web en Wikipedia .
Usaremos los datos anónimos de análisis web de ClickHouse. Puede encontrar una guía que describe cómo cargarlo aquí .
Veamos los datos. es el identificador único de la sesión, mientras que los demás parámetros son características de esta sesión. Parecen variables numéricas, pero son nombres codificados del navegador y del sistema operativo. Es mucho más eficiente almacenar estos valores (como números) y luego decodificar los valores a nivel de aplicación. Esta optimización es muy importante y puede ahorrarle terabytes si se trata de big data.VisitID
UserAgent
OS
SELECT
VisitID,
StartDate,
UTCStartTime,
Duration,
PageViews,
StartURLDomain,
IsMobile,
UserAgent,
OS
FROM datasets.visits_v1
FINAL
LIMIT 10
┌─────────────VisitID─┬──StartDate─┬────────UTCStartTime─┬─Duration─┬─PageViews─┬─StartURLDomain─────────┬─IsMobile─┬─UserAgent─┬──OS─┐
│ 6949594573706600954 │ 2014-03-17 │ 2014-03-17 11:38:42 │ 0 │ 1 │ gruzomoy.sumtel.com.ua │ 0 │ 7 │ 2 │
│ 7763399689682887827 │ 2014-03-17 │ 2014-03-17 18:22:20 │ 24 │ 3 │ gruzomoy.sumtel.com.ua │ 0 │ 2 │ 2 │
│ 9153706821504089082 │ 2014-03-17 │ 2014-03-17 09:41:09 │ 415 │ 9 │ gruzomoy.sumtel.com.ua │ 0 │ 7 │ 35 │
│ 5747643029332244007 │ 2014-03-17 │ 2014-03-17 04:46:08 │ 19 │ 1 │ gruzomoy.sumtel.com.ua │ 0 │ 2 │ 238 │
│ 5868920473837897470 │ 2014-03-17 │ 2014-03-17 10:10:31 │ 11 │ 1 │ gruzomoy.sumtel.com.ua │ 0 │ 3 │ 35 │
│ 6587050697748196290 │ 2014-03-17 │ 2014-03-17 09:06:47 │ 18 │ 2 │ gruzomoy.sumtel.com.ua │ 0 │ 120 │ 35 │
│ 8872348705743297525 │ 2014-03-17 │ 2014-03-17 06:40:43 │ 190 │ 6 │ gruzomoy.sumtel.com.ua │ 0 │ 5 │ 238 │
│ 8890846394730359529 │ 2014-03-17 │ 2014-03-17 02:27:19 │ 0 │ 1 │ gruzomoy.sumtel.com.ua │ 0 │ 57 │ 35 │
│ 7429587367586011403 │ 2014-03-17 │ 2014-03-17 01:13:14 │ 0 │ 1 │ gruzomoy.sumtel.com.ua │ 1 │ 1 │ 12 │
│ 5195928066127503662 │ 2014-03-17 │ 2014-03-17 01:43:02 │ 1926 │ 3 │ gruzomoy.sumtel.com.ua │ 0 │ 2 │ 35 │
└─────────────────────┴────────────┴─────────────────────┴──────────┴───────────┴────────────────────────┴──────────┴───────────┴─────┘
Puede notar que especifiqué modificadores después del nombre de la tabla. Hago esto para asegurarme de que los datos estén completamente combinados y solo obtengo una fila por sesión.final
A menudo se usa en el motor ClickHouse, ya que permite usar en lugar de ( generalmente más detalles en los documentos ). Con este enfoque, puede tener algunas filas por sesión en caso de actualizaciones, que luego el sistema fusiona en segundo plano. Usando modificadores, forzamos el proceso.CollapsingMergeTree
inserts
updates
final
Podemos realizar dos consultas simples para ver la diferencia.
SELECT
uniqExact(VisitID) AS unique_sessions,
sum(Sign) AS number_sessions,
-- number of sessions after collapsing
count() AS rows
FROM datasets.visits_v1
┌─unique_sessions─┬─number_sessions─┬────rows─┐
│ 1676685 │ 1676581 │ 1680609 │
└─────────────────┴─────────────────┴─────────┘
SELECT
uniqExact(VisitID) AS unique_sessions,
sum(Sign) AS number_sessions,
count() AS rows
FROM datasets.visits_v1
FINAL
┌─unique_sessions─┬─number_sessions─┬────rows─┐
│ 1676685 │ 1676721 │ 1676721 │
└─────────────────┴─────────────────┴─────────┘
El uso tiene sus propios inconvenientes en el rendimiento. Puedes encontrar más información al respecto en la documentación .final
3. ¿Cómo garantizar la calidad de los datos?
Verificar que no haya eventos faltantes o duplicados es muy sencillo. Puede encontrar muchas formas de detectar anomalías en los datos de series temporales, desde métodos ingenuos (por ejemplo, número de eventos dentro de +20 % o -20 % en comparación con la semana anterior) hasta ML con bibliotecas como Prophet o PyCaret .
La consistencia de los datos es una tarea complicada. Como mencioné antes, los servicios de análisis web rastrean mucha información sobre el comportamiento de sus clientes en su sitio web. Documentan cientos de parámetros y debemos asegurarnos de que todos estos valores parezcan válidos.
Los parámetros pueden ser numéricos (duración o número de páginas vistas) o categóricos (navegador o sistema operativo). Para los valores, podemos usar criterios estadísticos para garantizar que la distribución permanezca constante, por ejemplo, la prueba de Kolmogorov-Smirnov .
Entonces, después de ver las mejores prácticas, mi única pregunta es cómo monitorear el acuerdo de las variables categóricas, es hora de discutirlo.
4. Variables categóricas
Tomemos un navegador como ejemplo. Tenemos valores únicos para 62 navegadores en nuestros datos.
SELECT uniqExact(UserAgent) AS unique_browsers
FROM datasets.visits_v1
┌─unique_browsers─┐
│ 62 │
└─────────────────┘
SELECT
UserAgent,
count() AS sessions,
round((100. * sessions) / (
SELECT count()
FROM datasets.visits_v1
FINAL
), 2) AS sessions_share
FROM datasets.visits_v1
FINAL
GROUP BY 1
HAVING sessions_share >= 1
ORDER BY sessions_share DESC
┌─UserAgent─┬─sessions─┬─sessions_share─┐
│ 7 │ 493225 │ 29.42 │
│ 2 │ 236929 │ 14.13 │
│ 3 │ 235439 │ 14.04 │
│ 4 │ 196628 │ 11.73 │
│ 120 │ 154012 │ 9.19 │
│ 50 │ 86381 │ 5.15 │
│ 79 │ 63082 │ 3.76 │
│ 121 │ 50245 │ 3 │
│ 1 │ 48688 │ 2.9 │
│ 42 │ 21040 │ 1.25 │
│ 5 │ 20399 │ 1.22 │
│ 71 │ 19893 │ 1.19 │
└───────────┴──────────┴────────────────┘
Podríamos monitorear el recurso compartido de cada navegador individualmente como una variable numérica, pero en este caso estaremos monitoreando al menos 12 series de tiempo para un campo. Como todos los que han hecho una alerta al menos una vez saben, cuantas menos variables, mejor . Al rastrear muchos parámetros, hay muchas notificaciones de falsos positivos con las que lidiar.UserAgent
Entonces comencé a pensar en una métrica que mostraría la diferencia entre las distribuciones. La idea es comparar la cuota de navegador de now() y before(). Podemos elegir el periodo anterior según la granularidad: T2T1
- Para datos minuciosos, puede mirar un poco,
- Para datos diarios, vale la pena mirar el día anterior a la semana para tener en cuenta la estacionalidad semanal,
- Para datos mensuales, puede ver los datos de hace un año.
Veamos un ejemplo a continuación.
Mi primer pensamiento fue mirar una métrica heurística similar a la norma L1 utilizada en el aprendizaje automático ( más detalles ).
Para el ejemplo anterior, esta fórmula nos dará el siguiente resultado: 10 %. En realidad, esta métrica tiene sentido: muestra la proporción más pequeña de eventos de distribución que han cambiado los navegadores.
Posteriormente, discutí este tema con mi jefe, quien tiene mucha experiencia en ciencia de datos. Sugirió que mirara la divergencia Kullback-Leibler o Jensen-Shannon, ya que esta es una forma más eficiente de calcular la distancia entre las distribuciones de probabilidad.
Si no recuerdas estos indicadores o nunca has oído hablar de ellos, no te preocupes, estoy en tu lugar. Así que busqué en Google las fórmulas ( este artículo explica los conceptos a fondo) y los valores calculados para nuestro ejemplo.
import numpy as np
prev = np.array([0.7, 0.2, 0.1])
curr = np.array([0.6, 0.27, 0.13])
def get_kl_divergence(prev, curr):
kl = prev * np.log(prev / curr)
return np.sum(kl)
def get_js_divergence(prev, curr):
mean = (prev + curr)/2
return 0.5*(get_kl_divergence(prev, mean) + get_kl_divergence(curr, mean))
kl = get_kl_divergence(prev, curr)
js = get_js_divergence(prev, curr)
print('KL divergence = %.4f, JS divergence = %.4f' % (kl, js))
# KL divergence = 0.0216, JS divergence = 0.0055
Como puede ver, las distancias que calculamos varían ampliamente. Entonces, ahora que tenemos (al menos) tres formas de calcular la diferencia entre los recursos compartidos de navegador anteriores y actuales, la siguiente pregunta es qué forma elegir para nuestra tarea de monitoreo.
5. El ganador es...
La mejor manera de estimar el rendimiento de diferentes métodos es ver cómo funcionan en la vida real. Para ello, podemos simular anomalías en los datos y comparar los efectos.
Hay dos anomalías comunes en los datos:
- Pérdida de datos: comenzamos a perder datos de uno de los navegadores, y todos los demás navegadores tuvieron una participación cada vez mayor
- Cambiado: cuando el tráfico de un navegador comienza a marcar para otro. Por ejemplo, el 10% de los eventos de Safari que vemos hoy no están definidos.
Podemos obtener el recurso compartido real del navegador y simular estas excepciones. Para simplificar, voy a agrupar todos los navegadores con una participación inferior al 5 % en grupos.browser = 0
WITH browsers AS
(
SELECT
UserAgent,
count() AS raw_sessions,
(100. * count()) / (
SELECT count()
FROM datasets.visits_v1
FINAL
) AS raw_sessions_share
FROM datasets.visits_v1
FINAL
GROUP BY 1
)
SELECT
if(raw_sessions_share >= 5, UserAgent, 0) AS browser,
sum(raw_sessions) AS sessions,
round(sum(raw_sessions_share), 2) AS sessions_share
FROM browsers
GROUP BY browser
ORDER BY sessions DESC
┌─browser─┬─sessions─┬─sessions_share─┐
│ 7 │ 493225 │ 29.42 │
│ 0 │ 274107 │ 16.35 │
│ 2 │ 236929 │ 14.13 │
│ 3 │ 235439 │ 14.04 │
│ 4 │ 196628 │ 11.73 │
│ 120 │ 154012 │ 9.19 │
│ 50 │ 86381 │ 5.15 │
└─────────┴──────────┴────────────────┘
Es hora de simular ambas situaciones. Puedes encontrar todo el código en GitHub . Para nosotros, el parámetro más importante es el efecto real: la proporción de eventos perdidos o modificados. Idealmente, nos gustaría que nuestras métricas fueran iguales a este efecto.
Como resultado de la simulación, obtuvimos dos gráficos que muestran la correlación entre el efecto del hecho y la métrica de distancia.
Cada punto del gráfico muestra el resultado de una simulación: el efecto real y la distancia correspondiente.
Puede ver fácilmente que la norma L1 es la mejor métrica para nuestra tarea, ya que está más cerca de la línea. Kullback-Leibler y Jensen-Shannon divergen ampliamente y tienen diferentes niveles según el caso de uso (qué navegador está perdiendo tráfico).distance = share of affected events
Dichas métricas no son adecuadas para el monitoreo porque no podrá especificar un umbral que le avise cuando se vea afectado más del 5% de su tráfico. Además, no podemos interpretar fácilmente estas métricas, mientras que la norma L1 muestra con precisión el grado de anomalía.
Seis, cálculo de la norma L1
Ahora que sabemos qué métrica nos mostrará la consistencia de los datos, la última tarea pendiente es implementar el cálculo de la norma L1 en la base de datos (en nuestro caso, ClickHouse).
Para ello podemos utilizar las conocidas funciones de ventana.
with browsers as (
select
UserAgent as param,
multiIf(
toStartOfHour(UTCStartTime) = '2014-03-18 12:00:00', 'previous',
toStartOfHour(UTCStartTime) = '2014-03-18 13:00:00', 'current',
'other'
) as event_time,
sum(Sign) as events
from datasets.visits_v1
where (StartDate = '2014-03-18')
-- filter by partition key is a good practice
and (event_time != 'other')
group by param, event_time)
select
sum(abs_diff)/2 as l1_norm
from
(select
param,
sumIf(share, event_time = 'current') as curr_share,
sumIf(share, event_time = 'previous') as prev_share,
abs(curr_share - prev_share) as abs_diff
from
(select
param,
event_time,
events,
sum(events) over (partition by event_time) as total_events,
events/total_events as share
from browsers)
group by param)
┌─────────────l1_norm─┐
│ 0.01515028932687386 │
└─────────────────────┘
ClickHouse tiene funciones de matriz muy poderosas y las he usado durante mucho tiempo antes de admitir funciones de ventana. Así que quiero mostrarles el poder de esta herramienta.
with browsers as (
select
UserAgent as param,
multiIf(
toStartOfHour(UTCStartTime) = '2014-03-18 12:00:00', 'previous',
toStartOfHour(UTCStartTime) = '2014-03-18 13:00:00', 'current',
'other'
) as event_time,
sum(Sign) as events
from datasets.visits_v1
where StartDate = '2014-03-18' -- filter by partition key is a good practice
and event_time != 'other'
group by param, event_time
order by event_time, param)
select l1_norm
from
(select
-- aggregating all param values into arrays
groupArrayIf(param, event_time = 'current') as curr_params,
groupArrayIf(param, event_time = 'previous') as prev_params,
-- calculating params that are present in both time periods or only in one of them
arrayIntersect(curr_params, prev_params) as both_params,
arrayFilter(x -> not has(prev_params, x), curr_params) as only_curr_params,
arrayFilter(x -> not has(curr_params, x), prev_params) as only_prev_params,
-- aggregating all events into arrays
groupArrayIf(events, event_time = 'current') as curr_events,
groupArrayIf(events, event_time = 'previous') as prev_events,
-- calculating events shares
arrayMap(x -> x / arraySum(curr_events), curr_events) as curr_events_shares,
arrayMap(x -> x / arraySum(prev_events), prev_events) as prev_events_shares,
-- filtering shares for browsers that are present in both periods
arrayFilter(x, y -> has(both_params, y), curr_events_shares, curr_params) as both_curr_events_shares,
arrayFilter(x, y -> has(both_params, y), prev_events_shares, prev_params) as both_prev_events_shares,
-- filtering shares for browsers that are present only in one of periods
arrayFilter(x, y -> has(only_curr_params, y), curr_events_shares, curr_params) as only_curr_events_shares,
arrayFilter(x, y -> has(only_prev_params, y), prev_events_shares, prev_params) as only_prev_events_shares,
-- calculating the abs differences and l1 norm
arraySum(arrayMap(x, y -> abs(x - y), both_curr_events_shares, both_prev_events_shares)) as both_abs_diff,
1/2*(both_abs_diff + arraySum(only_curr_events_shares) + arraySum(only_prev_events_shares)) as l1_norm
from browsers)
┌─────────────l1_norm─┐
│ 0.01515028932687386 │
└─────────────────────┘
Este enfoque puede ser útil para personas con una mente pitónica. Con persistencia y creatividad, se puede escribir cualquier lógica utilizando funciones de matriz.
7. Alertas y Monitoreo
Tenemos dos consultas que nos muestran fluctuaciones en la proporción de navegadores en nuestros datos. Los datos de interés se pueden monitorear usando este método.
Lo único que queda es alinearse con el equipo en el umbral de alerta. Por lo general, miro los datos históricos y las anomalías anteriores para obtener algunos niveles iniciales y luego sigo ajustándolos con nueva información: alertas de falsos positivos o anomalías perdidas.
Además, mientras implementaba el monitoreo, me encontré con algunos matices que me gustaría cubrir brevemente:
- Por ejemplo, hay parámetros en los datos que no tienen sentido en el monitoreo o, por lo tanto, elija sabiamente qué parámetros incluir.
UserID
StartDate
- Es posible que tenga parámetros con alta cardinalidad. Por ejemplo, en análisis web, los datos tienen más de 600 000 valores únicos. Calcular métricas para ello puede consumir recursos. Por lo tanto, sugeriría almacenar estos valores (por ejemplo, tomar dominio o TLD), o simplemente monitorear los valores de nivel superior y agrupar los otros valores en un grupo separado "Otro".
StartURL
- Puede usar el mismo marco para valores usando cubos.
- En algunos casos, se espera que los datos cambien significativamente. Por ejemplo, si está monitoreando el campo de versión de la aplicación, recibirá una alerta después de que se publique cada versión. Dichos eventos ayudan a garantizar que su monitoreo aún esté allí :)