Calcular estatísticas particulares com o Privacy on Beam

Talvez você acredite que estatísticas agregadas não vazam informações sobre os indivíduos cujos dados compõem essas estatísticas. No entanto, há muitas maneiras de um invasor descobrir as informações confidenciais presentes em um conjunto de dados de uma estatística agregada.

Para proteger a privacidade das pessoas, você aprenderá a produzir estatísticas particulares usando as agregações particulares diferenciadas do Privacy on Beam. O Privacy on Beam é um framework de privacidade diferencial que funciona com o Apache Beam (link em inglês).

O que queremos dizer com "particular"?

Ao usar a palavra "particular" ao longo deste codelab, queremos dizer que a saída é produzida de uma maneira que não vaza nenhuma informação particular sobre os indivíduos nos dados. Podemos fazer isso usando a privacidade diferencial, uma forte noção de privacidade para anonimização. A anonimização é o processo de agregar dados de vários usuários para proteger a privacidade deles. Todos os métodos de anonimização usam a agregação, mas nem todos os métodos de agregação alcançam a anonimização. A privacidade diferencial, por outro lado, oferece garantias mensuráveis em relação ao vazamento de informações e à privacidade.

Vamos ver um exemplo simples para entender melhor a privacidade diferencial.

Este gráfico de barras mostra o movimento de um pequeno restaurante em uma noite específica. Muitos clientes chegam às 19h, e o restaurante fica completamente vazio à 1h da manhã:

a43dbf3e2c6de596.png

Isso parece útil!

Mas aí aparece o problema. Quando um novo cliente chega, esse fato é imediatamente revelado pelo gráfico de barras. Observe o gráfico: está claro que há um novo cliente e que ele chegou aproximadamente à 1h da manhã:

bda96729e700a9dd.png

Isso não é bom do ponto de vista da privacidade. Uma estatística verdadeiramente anônima não pode revelar contribuições individuais. Ao colocar esses dois gráficos lado a lado, isso fica ainda mais aparente: o gráfico de barras laranja tem um cliente extra que chegou aproximadamente à 1h da manhã:

d562ddf799288894.png

Isso não é nada bom. O que podemos fazer?

Deixaremos os gráficos de barras um pouco menos precisos adicionando ruído aleatório.

Veja os dois gráficos abaixo. Embora não sejam totalmente precisos, eles ainda são úteis e não revelam contribuições individuais. Ótimo!

838a0293cd4fcfe3.gif

A privacidade diferencial está acrescentando a quantidade certa de ruído aleatório para mascarar contribuições individuais.

Nossa análise foi um pouco simplificada demais. A implementação correta da privacidade diferencial é mais complexa e tem várias sutilezas de implementação inesperadas. Assim como a criptografia, criar sua própria implementação de privacidade diferencial pode não ser uma boa ideia. Você pode usar o Privacy on Beam em vez de implementar sua solução. Não crie sua própria privacidade diferencial.

Neste codelab, mostraremos como realizar análises particulares diferenciadas usando o Privacy on Beam.

Você não precisa fazer o download do Privacy on Beam para seguir o codelab porque todo o código e os gráficos relevantes podem ser encontrados neste documento. No entanto, se você deseja fazer o download para brincar com o código, execute-o você mesmo ou use a Privacidade no Beam mais tarde, sinta-se à vontade para fazer isso seguindo as etapas abaixo.

Observe que este codelab é para a versão 1.0.0 da biblioteca.

Primeiro, faça o download do Privacy on Beam:

https://github.com/google/differential-privacy/archive/refs/tags/v1.0.0.tar.gz (link em inglês)

Ou você pode clonar o repositório do GitHub:

git clone --branch v1.0.0 https://github.com/google/differential-privacy.git

O Privacy on Beam está no diretório privacy-on-beam/ de nível superior.

O código deste codelab e o conjunto de dados estão no diretório privacy-on-beam/codelab/.

Também é necessário ter o Bazel instalado no computador. Veja as instruções de instalação para seu sistema operacional no site do Bazel (link em inglês).

Imagine que você tem um restaurante e gostaria de compartilhar algumas estatísticas sobre ele, como divulgar os horários em que as pessoas mais costuma visitá-lo. Felizmente, você conhece a privacidade diferencial e a anonimização, então quer fazer isso de uma maneira que não gere vazamento de informações sobre os visitantes.

O código para esse exemplo está localizado em codelab/count.go.

Primeiro, carregue um conjunto de dados fictícios contendo visitas ao seu restaurante em uma segunda-feira. O código para fazer isso não é interessante para este codelab, mas você pode conferi-lo em codelab/main.go, codelab/utils.go e codelab/visit.go.

ID do visitante

Horário de chegada

Tempo gasto (min)

Dinheiro gasto (euros)

1

9h30

26

24

2

11h54

53

17

3

13h05

81

33

Primeiro, você produzirá um gráfico de barras não particular dos horários de visita ao seu restaurante usando o Beam no exemplo de código abaixo. O Scope é uma representação do pipeline, e cada nova operação que fazemos nos dados é adicionada ao Scope. A função CountVisitsPerHour usa um Scope e um conjunto de visitas, que é representado como uma PCollection no Beam. Ela extrai o horário de cada visita aplicando a função extractVisitHour no conjunto. Em seguida, ela conta as ocorrências de cada horário e as retorna.

func CountVisitsPerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("CountVisitsPerHour")
    visitHours := beam.ParDo(s, extractVisitHour, col)
    visitsPerHour := stats.Count(s, visitHours)
    return visitsPerHour
}

func extractVisitHour(v Visit) int {
    return v.TimeEntered.Hour()
}

Isso gera um bom gráfico de barras (executando bazel run codelab -- --example="count" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/count.csv --output_chart_file=$(pwd)/count.png) no diretório atual chamado de count.png:

a179766795d4e64a.png

A próxima etapa é converter o pipeline e o gráfico de barras em um gráfico particular. Faremos o seguinte:

Primeiro, chame MakePrivateFromStruct em uma PCollection<V> para receber uma PrivatePCollection<V>. A entrada PCollection precisa ser um conjunto de estruturas. Precisamos inserir uma PrivacySpec e um idFieldPath como entrada para MakePrivateFromStruct.

spec := pbeam.NewPrivacySpec(epsilon, delta)
pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

A PrivacySpec é uma estrutura que contém os parâmetros de privacidade diferencial (épsilon e delta) que queremos usar para tornar os dados anônimos. Você não precisa se preocupar com eles por enquanto. Teremos uma seção opcional se você quiser saber mais sobre isso.

O idFieldPath é o caminho do campo de identificador de usuário dentro da estrutura (no nosso caso, Visit). Aqui, o identificador de usuário dos visitantes é o campo VisitorID de Visit.

Em seguida, chamamos pbeam.Count() em vez de stats.Count(), a pbeam.Count() usa como entrada uma estrutura CountParams que contém parâmetros, como MaxValue, que afetam a precisão da saída.

visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
    // Visitors can visit the restaurant once (one hour) a day
    MaxPartitionsContributed: 1,
    // Visitors can visit the restaurant once within an hour
    MaxValue:                 1,
})

Da mesma forma, MaxPartitionsContributed limita com quantos horários de visita um usuário pode contribuir. A expectativa é que cada usuário visite o restaurante no máximo uma vez por dia (ou não para nós importa se ele visita várias vezes ao longo do dia), então definimos o valor de 1. Falaremos mais detalhadamente sobre esses parâmetros em uma seção opcional.

O MaxValue limita quantas vezes um único usuário pode contribuir com os valores que estamos contando. Neste caso específico, os valores contados são horários de visita, e esperamos que um usuário visite o restaurante apenas uma vez (ou não importa para nós se ele visita várias vezes por hora), então definimos esse parâmetro como 1.

No final, o código ficará assim:

func PrivateCountVisitsPerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateCountVisitsPerHour")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, delta)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    visitHours := pbeam.ParDo(s, extractVisitHour, pCol)
    visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Visitors can visit the restaurant once within an hour
        MaxValue:                 1,
    })
    return visitsPerHour
}

Vemos um gráfico de barras semelhante (count_dp.png) para a estatística particular diferenciada. O comando anterior executa os pipelines particulares e não particulares:

d6a0ace1acd3c760.png

Parabéns! Você calculou sua primeira estatística particular diferenciada.

O gráfico de barras mostrado quando você executa o código pode ser diferente desse. Não tem problema. Devido ao ruído na privacidade diferencial, você terá um gráfico de barras diferente sempre que executar o código, mas é possível ver que eles são mais ou menos parecidos com o gráfico não particular original.

Observe que é muito importante que as garantias de privacidade não executem o pipeline várias vezes, por exemplo, para conseguir um gráfico de barras com melhor aparência. O motivo para não executar novamente seus pipelines é explicado na seção "Calcular várias estatísticas".

Na seção anterior, talvez você tenha notado que descartamos todas as visitas (dados) de algumas partições (horários).

d7fbc5d86d91e54a.png

Isso acontece devido à seleção/limitação da partição, uma etapa importante para assegurar garantias de privacidade diferencial quando a existência de partições de saída depende dos dados do usuário. Quando esse é o caso, a simples existência de uma partição de saída pode causar o vazamento da existência de um usuário individual nos dados. Consulte esta postagem do blog (link em inglês) para uma explicação sobre por que isso viola a privacidade. Para evitar essa situação, o Privacy on Beam mantém somente as partições que têm um número suficiente de usuários.

Quando a lista de partições de saída não depende dos dados particulares do usuário, isto é, são informações públicas, não precisamos dessa etapa para seleção de partição. Esse é o caso do nosso exemplo com o restaurante: sabemos o horário de funcionamento dele (das 9h às 21h).

O código para esse exemplo está localizado em codelab/public_partitions.go.

Basta criar uma PCollection de horários entre 9 e 21 (exclusivos) e inseri-la no campo PublicPartitions de CountParams:

func PrivateCountVisitsPerHourWithPublicPartitions(s beam.Scope,
    col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateCountVisitsPerHourWithPublicPartitions")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours
    // (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    visitHours := pbeam.ParDo(s, extractVisitHour, pCol)
    visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Visitors can visit the restaurant once within an hour
        MaxValue:                 1,
        // Visitors only visit during work hours
        PublicPartitions:         hours,
    })
    return visitsPerHour
}

É possível definir o delta como 0 se você estiver usando partições públicas e o ruído de Laplace (padrão), como no caso acima.

Quando executamos o pipeline com partições públicas, com bazel run codelab -- --example="public_partitions" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/public_partitions.csv --output_chart_file=$(pwd)/public_partitions.png, temos:

7c950fbe99fec60a.png

Como podemos ver, agora mantemos as partições 9, 10 e 16, que descartamos anteriormente, sem partições públicas.

O uso de partições públicas não só permite manter mais partições, como também acrescenta cerca de metade do ruído a cada partição em comparação com quando elas não são usadas, já que não há gasto nenhum do orçamento de privacidade, ou seja, épsilon e delta, na seleção de partição. É por isso que a diferença entre as contagens bruta e particular é um pouco menor quando comparada à execução anterior.

Há duas informações importantes a serem lembradas ao usar partições públicas:

  1. Tenha cuidado ao derivar a lista de partições dos dados brutos: se você não fizer isso de maneira particular diferenciada, por exemplo, simplesmente ler a lista de todas as partições nos dados do usuário, seu pipeline não fornecerá mais garantias de privacidade diferencial. Consulte a seção avançada abaixo para saber como fazer isso de maneira particular diferenciada.
  2. Se não houver dados (por exemplo, visitas) para algumas das partições públicas, o ruído será aplicado a essas partições para preservar a privacidade diferencial. Por exemplo, se usássemos horários entre 0 e 24 (em vez de 9 e 21), todos os horários ficariam com ruído e poderiam mostrar algumas visitas mesmo não havendo nenhuma.

(Avançado) Derivar partições de dados

Se você está executando várias agregações com a mesma lista de partições de saída não públicas no mesmo pipeline, é possível derivar a lista de partições uma vez usando SelectPartitions() e fornecendo as partições para cada agregação como a entrada PublicPartition. Isso é seguro do ponto de vista da privacidade e também permite que você adicione menos ruído, devido ao uso do orçamento de privacidade na seleção de partição apenas uma vez para todo o pipeline.

Agora que sabemos como contar itens de maneira particular diferenciada, vamos calcular médias. Mais especificamente, calcularemos a duração média da permanência dos visitantes.

O código para esse exemplo está localizado em codelab/mean.go.

Normalmente, para calcular uma média não particular para duração de permanência, usamos stats.MeanPerKey() com uma etapa de pré-processamento que converte a PCollection de entrada em uma PCollection<K,V>, em que K é o horário da visita e V é o tempo que o visitante ficou no restaurante.

func MeanTimeSpent(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("MeanTimeSpent")
    hourToTimeSpent := beam.ParDo(s, extractVisitHourAndTimeSpentFn, col)
    meanTimeSpent := stats.MeanPerKey(s, hourToTimeSpent)
    return meanTimeSpent
}

func extractVisitHourAndTimeSpentFn(v Visit) (int, int) {
    return v.TimeEntered.Hour(), v.MinutesSpent
}

Isso produz um bom gráfico de barras (executando bazel run codelab -- --example="mean" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/mean.csv --output_chart_file=$(pwd)/mean.png) no diretório atual chamado de mean.png:

bc2df28bf94b3721.png

Para aplicar a privacidade diferencial, convertemos novamente nossa PCollection em uma PrivatePCollection e substituímos stats.MeanPerKey() por pbeam.MeanPerKey(). Assim como Count, temos MeanParams que contêm alguns parâmetros, como MinValue e MaxValue, que afetam a precisão. O MinValue e o MaxValue representam os limites que temos para a contribuição de cada usuário para cada chave.

meanTimeSpent := pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
    // Visitors can visit the restaurant once (one hour) a day
    MaxPartitionsContributed:     1,
    // Visitors can visit the restaurant once within an hour
    MaxContributionsPerPartition: 1,
    // Minimum time spent per user (in mins)
    MinValue:                     0,
    // Maximum time spent per user (in mins)
    MaxValue:                     60,
})

Nesse caso, cada chave representa um horário, e os valores são o tempo gasto pelos visitantes. Definimos o MinValue como 0, porque não esperamos que os visitantes gastem menos de 0 minutos no restaurante. Definimos o MaxValue como 60, o que significa que, se um visitante ficar mais de 60 minutos, agiremos como se ele tivesse ficado apenas por esse período.

No final, o código ficará assim:

func PrivateMeanTimeSpent(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateMeanTimeSpent")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours
    // (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    hourToTimeSpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    meanTimeSpent := pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed:     1,
        // Visitors can visit the restaurant once within an hour
        MaxContributionsPerPartition: 1,
        // Minimum time spent per user (in mins)
        MinValue:                     0,
        // Maximum time spent per user (in mins)
        MaxValue:                     60,
        // Visitors only visit during work hours
        PublicPartitions:             hours,
    })
    return meanTimeSpent
}

Vemos um gráfico de barras semelhante (mean_dp.png) para a estatística particular diferenciada. O comando anterior executa os pipelines particulares e não particulares:

e8ac6a9bf9792287.png

De modo semelhante à contagem, como essa é uma operação particular diferenciada, teremos resultados diferentes sempre que a executarmos. No entanto, é possível ver que a duração da permanência particular diferenciada não está muito longe do resultado real.

Outra estatística interessante que podemos considerar é a receita por hora ao longo do dia.

O código para esse exemplo está localizado em codelab/sum.go.

Mais uma vez, começaremos com a versão não particular. Com um pré-processamento no nosso conjunto de dados fictício, podemos criar uma PCollection<K,V>, em que K é o horário de visita e V é o dinheiro gasto pelo visitante no restaurante. Para calcular uma receita não particular por hora, podemos simplesmente somar todo o dinheiro que os visitantes gastam chamando stats.SumPerKey():

func RevenuePerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("RevenuePerHour")
    hourToMoneySpent := beam.ParDo(s, extractVisitHourAndMoneySpent, col)
    revenues := stats.SumPerKey(s, hourToMoneySpent)
    return revenues
}

func extractVisitHourAndMoneySpent(v Visit) (int, int) {
    return v.TimeEntered.Hour(), v.MoneySpent
}

Isso produz um bom gráfico de barras (executando bazel run codelab -- --example="sum" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/sum.csv --output_chart_file=$(pwd)/sum.png) no diretório atual chamado de sum.png:

548619173fad0c9a.png

Para aplicar a privacidade diferencial, convertemos novamente nossa PCollection em uma PrivatePCollection e substituímos stats.SumPerKey() por pbeam.SumPerKey(). Assim como Count e MeanPerKey, temos SumParams que contêm alguns parâmetros, como MinValue e MaxValue, que afetam a precisão.

revenues := pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
    // Visitors can visit the restaurant once (one hour) a day
    MaxPartitionsContributed: 1,
    // Minimum money spent per user (in euros)
    MinValue:                 0,
    // Maximum money spent per user (in euros)
    MaxValue:                 40,
})

Neste caso, MinValue e MaxValue representam os limites que temos para o dinheiro que cada visitante gasta. Definimos o MinValue como 0 porque não esperamos que os visitantes gastem menos de 0 euros no restaurante. Definimos o MaxValue como 40, o que significa que, se um visitante gastar mais de 40 euros, agiremos como se ele tivesse gastado 40.

No final, o código ficará assim:

func PrivateRevenuePerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateRevenuePerHour")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours
    // (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    hourToMoneySpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    revenues := pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Minimum money spent per user (in euros)
        MinValue:                 0,
        // Maximum money spent per user (in euros)
        MaxValue:                 40,
        // Visitors only visit during work hours
        PublicPartitions:         hours,
    })
    return revenues
}

Vemos um gráfico de barras semelhante (sum_dp.png) para a estatística particular diferenciada. O comando anterior executa os pipelines particulares e não particulares:

e86a11a22a5a2dc.png

De modo semelhante à contagem e à média, como essa é uma operação particular diferenciada, teremos resultados diferentes sempre que a executarmos. No entanto, é possível ver que o resultado particular diferenciado é muito próximo das receitas por hora reais.

Na maioria das vezes, você pode ter interesse em calcular várias estatísticas sobre os mesmos dados, de modo semelhante ao que foi feito com contagem, a média e a soma. Geralmente, é mais simples e fácil fazer isso em um único pipeline do Beam e em um único binário. Também é possível fazer isso com o Privacy on Beam. Você pode escrever um único pipeline para executar suas transformações e seus cálculos e use uma única PrivacySpec para todo o pipeline.

Além de ser mais conveniente fazer isso com uma única PrivacySpec, também é melhor em termos de privacidade. Os parâmetros épsilon e delta fornecidos para a PrivacySpec representam algo chamado orçamento de privacidade, que é uma medida da quantidade de privacidade dos usuários nos dados que você está vazando.

É importante se lembrar de que o orçamento de privacidade é cumulativo: se você executar um pipeline com um determinado épsilon ε e delta δ uma única vez, você gastará um orçamento de (ε, δ). Se ele for executado pela segunda vez, você terá gastado um orçamento total de (2ε, 2δ). Da mesma forma, se você calcular várias estatísticas com uma PrivacySpec e, consecutivamente, um orçamento de privacidade de (ε, δ), você terá gastado um orçamento total de (2ε, 2δ). Isso significa que você está degradando as garantias de privacidade.

Para contornar isso, quando você quiser calcular várias estatísticas sobre os mesmos dados, use uma única PrivacySpec com o orçamento total que você quer usar. Em seguida, você precisa especificar o épsilon e o delta que pretende usar para cada agregação. No final, você terá a mesma garantia geral de privacidade. No entanto, quanto mais épsilon e delta uma agregação específica tiver, maior será a precisão.

Para ver isso em ação, podemos calcular as três estatísticas (contagem, média e soma) que calculamos separadamente antes em um único pipeline.

O código para esse exemplo está localizado em codelab/multiple.go. Observe que estamos dividindo o orçamento total (ε, δ) igualmente entre as três agregações:

func ComputeCountMeanSum(s beam.Scope, col beam.PCollection) (visitsPerHour, meanTimeSpent, revenues beam.PCollection) {
    s = s.Scope("ComputeCountMeanSum")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    // Budget is shared by count, mean and sum.
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours
    // (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    visitHours := pbeam.ParDo(s, extractVisitHour, pCol)
    visitsPerHour = pbeam.Count(s, visitHours, pbeam.CountParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Visitors can visit the restaurant once within an hour
        MaxValue:                 1,
        // Visitors only visit during work hours
        PublicPartitions:         hours,
    })

    hourToTimeSpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    meanTimeSpent = pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed:     1,
        // Visitors can visit the restaurant once within an hour
        MaxContributionsPerPartition: 1,
        // Minimum time spent per user (in mins)
        MinValue:                     0,
        // Maximum time spent per user (in mins)
        MaxValue:                     60,
        // Visitors only visit during work hours
        PublicPartitions:             hours,
    })

    hourToMoneySpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    revenues = pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Minimum money spent per user (in euros)
        MinValue:                 0,
        // Maximum money spent per user (in euros)
        MaxValue:                 40,
        // Visitors only visit during work hours
        PublicPartitions:         hours,
    })

    return visitsPerHour, meanTimeSpent, revenues
}

Você viu alguns parâmetros mencionados neste codelab: épsilon, delta, maxPartitionsContributed etc. Podemos dividi-los em duas categorias: parâmetros de privacidade e parâmetros de utilidade.

Parâmetros de privacidade

Épsilon e delta são os parâmetros que quantificam a privacidade que fornecemos usando a privacidade diferencial. Mais precisamente, eles são uma medida da quantidade de informações que um possível invasor conseguiria sobre os dados analisando a saída anonimizada. Quanto mais épsilon e delta, mais informações o invasor consegue sobre os dados, o que é um risco de privacidade.

Por outro lado, quanto menos épsilon e delta, mais ruído você precisa adicionar à saída para que ela fique anônima, e você precisa ter um número maior de usuários únicos em cada partição para manter essa partição na saída anonimizada. Portanto, há uma compensação entre utilidade e privacidade aqui.

No Beam on Privacy, é preciso se atentar às garantias de privacidade que você quer na saída anonimizada ao especificar o orçamento de privacidade total na PrivacySpec. A ressalva é que, se você quiser que as garantias de privacidade sejam mantidas, será necessário seguir as instruções deste codelab sobre não exceder seu orçamento. Para isso, tenha uma PrivacySpec separada para cada agregação ou execute o pipeline várias vezes.

Para saber mais sobre privacidade diferencial e o significado dos parâmetros de privacidade, acesse os materiais de consulta (link em inglês).

Parâmetros de utilidade

Esses são parâmetros que não afetam as garantias de privacidade, contanto que as orientações sobre como usar o Privacy on Beam sejam seguidas corretamente. No entanto, eles afetam a precisão e, consequentemente, a utilidade da saída. Esses parâmetros são fornecidos nas estruturas Params de cada agregação, por exemplo, CountParams, SumParams etc. Eles são usados para escalonar o ruído que está sendo adicionado.

Um parâmetro de utilidade fornecido em Params e aplicável a todas as agregações é o MaxPartitionsContributed. Uma partição corresponde a uma chave da PCollection gerada por uma operação de agregação do Privacy On Beam, ou seja, Count, SumPerKey etc. Portanto, o MaxPartitionsContributed limita com quantos valores de chave diferentes um usuário pode contribuir na saída. Se um usuário contribuir nos dados com mais chaves do que o definido por MaxPartitionsContributed, algumas das contribuições serão descartadas para contribuir exatamente com o valor de chaves de MaxPartitionsContributed.

De modo semelhante ao MaxPartitionsContributed, a maioria das agregações tem um parâmetro MaxContributionsPerPartition. Ele é fornecido nas estruturas Params, e cada agregação pode ter valores separados para ele. Ao contrário do MaxPartitionsContributed, o MaxContributionsPerPartition limita a contribuição de um usuário para cada chave. Em outras palavras, um usuário pode contribuir apenas com valores de MaxContributionsPerPartition para cada chave.

O ruído adicionado à saída é escalonado por MaxPartitionsContributed e MaxContributionsPerPartition. Por isso, há uma compensação aqui: MaxPartitionsContributed e MaxContributionsPerPartition maiores significam que você manterá mais dados, mas acabará com um resultado com mais ruído.

Algumas agregações exigem o MinValue e o MaxValue. Eles especificam os limites para as contribuições de cada usuário. Se um usuário contribuir com um valor inferior ao MinValue, esse valor será aumentado para o MinValue. Da mesma forma, se um usuário contribuir com um valor maior que o MaxValue, ele será reduzido para o MaxValue. Isso significa que, para manter mais valores originais, é necessário especificar limites maiores. De modo semelhante a MaxPartitionsContributed e MaxContributionsPerPartition, o ruído é escalonado pelo tamanho dos limites. Assim, limites maiores significam que você manterá mais dados, mas terá um resultado com mais ruído.

O último parâmetro sobre o qual falaremos é o NoiseKind. É possível usar dois mecanismos de ruído diferentes no Privacy On Beam: GaussianNoise e LaplaceNoise. Ambos têm vantagens e desvantagens, mas a distribuição de Laplace oferece uma utilidade melhor com limites de contribuição baixos, e é por isso que o Privacy On Beam o utiliza como padrão. No entanto, se você quiser usar um ruído de distribuição gaussiano, pode fornecer uma variável pbeam.GaussianNoise{} a Params.

Bom trabalho! Você terminou o codelab sobre o Privacy on Beam. Você aprendeu muito sobre a privacidade diferencial e o Privacy on Beam:

  • Como transformar sua PCollection em uma PrivatePCollection chamando MakePrivateFromStruct.
  • Como usar Count para calcular contagens particulares diferenciadas.
  • Como usar MeanPerKey para calcular médias particulares diferenciadas.
  • Como usar SumPerKey para calcular somas particulares diferenciadas.
  • Como calcular várias estatísticas com uma única PrivacySpec em um único pipeline.
  • (Opcional) Como personalizar a PrivacySpec e parâmetros de agregação (CountParams, MeanParams, SumParams).

Porém, há muitas outras agregações (por exemplo, quantis, contando valores distintos) que você pode fazer com o Privacy on Beam. Saiba mais sobre isso no repositório do GitHub ou no godoc (links em inglês).

Se tiver tempo, envie um feedback sobre este codelab respondendo a uma pesquisa (em inglês).