【数据挖掘】如何保证数据一致性?

一、说明

         我曾经在网络分析服务公司担任数据分析师。此类系统可帮助网站收集和分析客户行为数据。 不言而喻,数据是网络分析服务最宝贵的价值。我的主要目标之一是监控数据质量。

为了确保数据一切正常,我们需要关注两件事:

  • 没有丢失或重复的事件 - >事件和会话数在预期范围内。
  • 数据是正确的 - >每个参数的值分布保持不变,另一个版本尚未开始将所有浏览器记录为 Safari 或完全停止跟踪购买。

        今天,我想告诉大家我处理这项复杂任务的经历。作为奖励,我将展示 ClickHouse 数组函数的示例。

摄影:Luke Chesser on Unsplash

二、什么是网络分析?

        网络分析系统会记录有关网站上事件的大量信息,例如,客户使用的浏览器和操作系统,他们访问了哪些URL,他们在网站上花费了多少时间,甚至他们添加到购物车并购买了哪些产品。所有这些数据都可用于报告(了解有多少客户访问了该网站)或分析(了解痛点并改善客户体验)。您可以在维基百科上找到有关网络分析的更多详细信息。

        我们将使用ClickHouse的匿名网络分析数据。描述如何加载它的指南可以在这里找到。

        让我们看一下数据。 是会话的唯一标识符,而其他参数是此会话的特征。 看起来像数字变量,但它们是浏览器和操作系统的编码名称。存储这些值(如数字),然后在应用程序级别解码值要高效得多。这种优化非常重要,如果您正在处理大数据,可以为您节省 TB 级。VisitIDUserAgentOS

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 │
└─────────────────────┴────────────┴─────────────────────┴──────────┴───────────┴────────────────────────┴──────────┴───────────┴─────┘

        您可能会注意到我在表名后指定了修饰符。我这样做是为了确保数据完全合并,并且每个会话只得到一行。final

        在ClickHouse引擎中经常使用,因为它允许使用而不是(文档中通常的更多详细信息)。使用这种方法,您可以在更新的情况下每个会话有几行,然后系统在后台将其合并。使用修饰符,我们强制了这个过程。CollapsingMergeTreeinsertsupdatesfinal

        我们可以执行两个简单的查询来查看差异。

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 │
└─────────────────┴─────────────────┴─────────┘

        使用在性能上有其自身的缺点。您可以在文档中找到有关它的更多信息。final

三、如何保证数据质量?

        验证没有丢失或重复的事件非常简单。你可以找到很多方法来检测时间序列数据中的异常,从朴素的方法(例如,与前一周相比,事件数在 +20% 或 -20% 以内)到 ML 与 Prophet 或 PyCaret 等库。

        数据一致性是一项比较棘手的任务。正如我之前提到的,网络分析服务跟踪有关客户在网站上行为的大量信息。它们记录了数百个参数,我们需要确保所有这些值看起来都有效。

        参数可以是数字(持续时间或看到的网页数量)或分类(浏览器或操作系统)。对于数值,我们可以使用统计标准来确保分布保持不变——例如,柯尔莫哥罗夫-斯米尔诺夫检验

        因此,在研究了最佳实践之后,我唯一的问题是如何监控分类变量的一致性,是时候讨论它了。

四、分类变量

        让我们以浏览器为例。我们的数据中有 62 个浏览器的唯一值。

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 │
└───────────┴──────────┴────────────────┘

        我们可以将每个浏览器的共享作为数值变量单独监控,但在这种情况下,我们将监控一个字段的至少 12 个时间序列,.每个至少做过一次警报的人都知道,我们监视的变量越少越好。跟踪许多参数时,需要处理大量误报通知。UserAgent

        因此,我开始考虑一种可以显示分布之间差异的指标。这个想法是比较现在 () 和之前 () 的浏览器份额。我们可以根据粒度选择上一个周期:T2T1

  •         对于分钟数据——你可以看上一点,
  •         对于每日数据 - 值得查看一周前的一天,以考虑每周的季节性,
  •         对于月度数据 - 您可以查看一年前的数据。

        让我们看下面的例子。

        我的第一个想法是查看类似于机器学习中使用的L1规范的启发式指标(更多详细信息)。

        对于上面的例子,这个公式将给我们以下结果 — 10%。实际上,这个指标是有意义的——它显示了浏览器已更改的分发事件中的最小份额。

        之后,我和我的老板讨论了这个话题,他在数据科学方面有很多经验。他建议我看看Kullback-Leibler或Jensen-Shannon散度,因为这是计算概率分布之间距离的更有效的方法。

        如果您不记得这些指标或以前从未听说过它们,请不要担心,我站在你的立场上。所以我用谷歌搜索了公式(本文彻底解释了这些概念)和我们示例的计算值。

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

        如您所见,我们计算的距离差异很大。所以现在我们(至少)有三种方法来计算之前和现在浏览器份额之间的差异,下一个问题是为我们的监控任务选择哪种方式。

五、获胜者是...

        估计不同方法性能的最佳方法是查看它们在现实生活中的表现。为此,我们可以模拟数据中的异常并比较效果。

        数据中有两种常见的异常情况:

  • 数据丢失:我们开始丢失来自其中一个浏览器的数据,并且所有其他浏览器的份额都在增加
  • 更改:当来自一个浏览器的流量开始标记为另一个浏览器时。例如,我们现在看到的 10% 的 Safari 事件是未定义的。

        我们可以获取实际的浏览器共享并模拟这些异常。为简单起见,我将把所有份额低于 5% 的浏览器分组到组中。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 │
└─────────┴──────────┴────────────────┘

        是时候模拟这两种情况了。您可以在 GitHub 上找到所有代码。对我们来说,最重要的参数是实际效果——丢失或改变的事件份额。理想情况下,我们希望我们的指标等于这种效果。

        作为模拟的结果,我们得到了两个图表,显示了事实效应和距离指标之间的相关性。

        图表中的每个点都显示一个模拟的结果 — 实际效果和相应的距离。

        您可以很容易地看到 L1 范数是我们任务的最佳指标,因为它最接近线。Kullback-Leibler和Jensen-Shannon的分歧很大,并且根据用例(哪个浏览器正在失去流量)具有不同的级别。distance = share of affected events

        此类指标不适合监控,因为您将无法指定一个阈值,以便在超过 5% 的流量受到影响时向您发出警报。此外,我们无法轻松解释这些指标,而 L1 范数准确地显示了异常的程度。

六、L1范数计算

        现在我们知道什么指标将向我们显示数据的一致性,剩下的最后一个任务是在数据库中实现 L1 范数计算(在我们的例子中是 — ClickHouse)。

        我们可以为它使用广为人知的窗口函数。

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有非常强大的数组函数,在支持窗口函数之前,我已经使用了很长时间。所以我想向你展示这个工具的强大功能。

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 │
└─────────────────────┘

        这种方法对于具有pythonic思维的人来说可能很方便。凭借持久性和创造力,可以使用数组函数编写任何逻辑。

七、警报和监控

        我们有两个查询,向我们显示浏览器在我们数据中的份额波动。可以使用此方法监视感兴趣的数据。

        剩下的唯一一点就是在警报阈值上与团队保持一致。我通常会查看历史数据和以前的异常情况,以获取一些初始级别,然后使用新信息不断调整它们:误报警报或错过的异常。

        此外,在实施监控的过程中,我遇到了一些细微差别,我想简要介绍一下:

  • 例如,数据中存在在监视中没有意义的参数,或者 ,因此请明智地选择要包含的参数。UserIDStartDate
  • 您可能具有高基数的参数。例如,在 Web 分析中,数据具有超过 600K 个唯一值。为其计算指标可能会消耗资源。因此,我建议要么将这些值(例如,采用域或 TLD)存储,要么仅监控顶级值并将其他值分组到单独的组“其他”中。StartURL
  • 您可以使用存储桶对数值使用相同的框架。
  • 在某些情况下,预计数据会发生重大变化。例如,如果您正在监视应用程序版本字段,则在每个版本发布后都会收到警报。此类事件有助于确保您的监控仍在:)

猜你喜欢

转载自blog.csdn.net/gongdiwudu/article/details/132338528