Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Predicados

Predicados

Objetivos de aprendizagem

  • Compreender o conceito de predicado, quantificadores, aridade e aplicações em Python

  • Dominar o uso de quantificadores através das funções `any` e `all`, bem como de condicionais como predicados $n$-ários.

  • Criar predicados personalizados (unários, binários e ternários) para resolver problemas reais usando lógica de predicados.

Lógica de predicados

Diferentemente da lógica proposicional, na qual uma proposição deve, necessariamente, ser verdadeira ou falsa, na lógica de predicados, uma proposição pode ter seu valor-verdade alterado em função de uma variável. A presença de uma ou mais variáveis em uma sentença permite que exploremos com mais profundidade as relações entre objetos.

Há três conceitos fundamentais para a compreensão da lógica de predicados, cujo papel é bastante similar aos seus homônimos estudados na Língua Portuguesa:

  • sujeito, o objeto principal da sentença, em geral representado por uma variável.

  • predicado (ou função proposicional), a declaração que aponta a propriedade do sujeito, e

  • domínio (ou universo) de discurso, o conjunto de todos os valores possíveis que uma variável pode assumir, o qual define o contexto de uma sentença.

Considere a declaração: xx é um número inteiro maior do que 3”. Aqui, xx é o sujeito, “maior do que 3” é o predicado e o conjunto dos números inteiros é o domínio de discurso. Matematicamente, essa declaração poderia ser escrita como

P(x):x>3,xD=Z.P(x): x > 3, \forall x \in D=\mathbb{Z}.

Daí, é imediato que P(1)P(-1) e P(0)P(0) são avaliações do predicado que geram valor-verdade falso, enquanto P(4)P(4) e P(7)P(7) geram valor-verdade verdadeiro. Por outro lado, P(π)P(\pi) teria valor indeterminado, já que π\pi é um valor fora do domínio de discurso DD, aqui equivalente a Z\mathbb{Z}. Adiante, vamos explorar o significado do símbolo \forall.

Quantificadores

Para expressar propriedades sobre conjuntos e criar relações entre predicados, a matemática usa três quantificadores principais:

  • quantificador universal: representado pelo símbolo \forall e lido como “para todo” ou “qualquer que seja”, este quantificador é usado quando desejamos fazer referência à validade de uma propriedade para todos os elementos do domínio de discurso. Exemplo: xN,x+0=x\forall x \in \mathbb{N}, x + 0 = x (a soma de qualquer número natural com zero é o próprio número).

  • quantificador existencial: representado pelo símbolo \exists e lido como “existe” ou “existe pelo menos um”, este quantificador é usado quando desejamos afirmar que pelo menos um elemento do domínio de discurso satisfaz uma propriedade. Exemplo: xZ,x2=4\exists x \in \mathbb{Z}, x^2 = 4 (existe um (ou pelo menos um) inteiro cujo quadrado é 4 – de fato, existem dois: –2 e 2).

  • quantificador de unicidade: representado pelo símbolo !\exists ! e lido como “existe um único” ou “existe apenas um”, este quantificador garante que existe um e somente um elemento que satisfaz a uma dada propriedade. Exemplo: !xR,x+2=6\exists ! \, x \in \mathbb{R}, x + 2 = 6 (existe somente um número real que, somado a 2, resulta 6, i.e. 4).

Esses quantificadores são chamados de primeira ordem, porque são anexados a variáveis individuais que se referem a membros do universo de discurso. Em suma, temos a seguinte correspondência:

  • \forall exige que a propriedade seja verdadeira sempre;

  • \exists exige que a propriedade seja verdadeira pelo menos uma vez;

  • !\exists ! exige que a propriedade seja verdadeira exatamente uma vez.

all e any

Em Python, as funções all(x) e any(x), para objetos iteráveis x são as que melhor representam os quantificadores de primeira ordem \forall e \exists, respectivamente, sempre que o domínio de discurso for finito (listas, conjuntos, strings, arrays em geral etc.). Quando aplicadas, elas retornam True ou False como respostas.

Vejamos exemplos:

all([True, True, True])  # equivalente a ∀x ∈ D, P(x) é verdadeiro
True
all([False, False, False])
False
any([False, False, False])
False
any([True, False, False])
True
D = [-1,2,3,4,5]
print([x > 0 for x in D]) # P(x): x > 0 ∀x ∈ D
print(all(x > 0 for x in D))
[False, True, True, True, True]
False
D = [0,2,3,4,5]
print([x <= 0 for x in D]) # P(x): x <= 0 ∀x ∈ D
any([x <= 0 for x in D])
[True, False, False, False, False]
True

Negação de proposições quantificadas

O termo predicado, ou função proposicional, é utilizado sempre que uma declaração contiver uma variável livre (ex. xx é um número primo). Quando atribuímos um valor à variável (ou sujeito) em substituição ao seu caráter genérico, ou usamos quantificadores para eliminar a dependência do valor individual, convertemos o predicado em proposição. Portanto, proposições quantificadas podem ser negadas.

Considere a seguinte proposição:

  1. ¬\neg (xZ,x\exists x \in \mathbb{Z}, x é par e xx é ímpar)

  2. ¬\neg (xZ,x\forall x \in \mathbb{Z}, x é primo)

Ambas são negações de proposições quantificadas que podem ser interpretadas da seguinte forma:

  • Dizer que nenhum elemento do domínio de discurso valida o predicado como verdadeiro equivale a dizer que todos os elementos do conjunto deixam de satisfazer o predicado. Ou seja,

    ¬(xD,I(P(x))=T)(xD,¬I(P(x))).\neg \, (\exists x \in D, \mathcal{I}(P(x)) = T) \equiv (\forall x \in D, \neg \, \mathcal{I}(P(x))).
  • Dizer que nem todos os elementos do domínio de discurso validam o predicado equivale a dizer que existe pelo menos um elemento do conjunto que não satisfaz o predicado. Ou seja,

¬(xD,I(P(x))=T)(xD,¬I(P(x))).\neg \, (\forall x \in D, \mathcal{I}(P(x)) = T) \equiv (\exists x \in D, \neg \, \mathcal{I}(P(x))).

Isto posto, as duas proposições acima equivalem a:

  1. xZ,¬(x eˊ par)¬(x eˊ ıˊmpar)\forall x \in \mathbb{Z}, \neg (x \text{ é par}) \vee \neg (x \text{ é ímpar}) e

  2. xZ,¬(x eˊ primo)\exists x \in \mathbb{Z}, \neg (x \text{ é primo}),

as quais refletem as leis de De Morgan para quantificadores, resumidas na tabela abaixo.

EquivalênciaDescriçãoInterpretação
¬(x,P(x))\neg (\forall x, P(x)) \equiv x,¬P(x)\exists x, \neg P(x)Negação do universalA negação de “para todo xx, P(x)P(x) é verdadeiro” é “existe pelo menos um xx para o qual P(x)P(x) é falso.”
¬(x,P(x))\neg (\exists x, P(x)) \equiv x,¬P(x)\forall x, \neg P(x)Negação do existencialA negação de “existe pelo menos um xx para o qual P(x)P(x) é verdadeiro” é “para todo xx, P(x)P(x) é falso.”

Vejamos exemplos complementares:

not all(x > 0 for x in D)
True
not any(x > 0 for x in D)
False

Simulação de predicados teóricos

A simulação de predicados teóricos em Python pode ser realizada com a implementação de funções regulares ou funções anônimas. Funções regulares são construídas usando a palavra-chave def e possuem a seguinte estrutura:

def P(lista_de_argumentos):
    (...)
    return y

Por outro lado, uma função anônima em Python consiste em uma função cujo nome não é explicitamente definido e que pode ser criada em apenas uma linha de código para executar uma tarefa específica. Funções anônimas são baseadas na palavra-chave lambda. Este nome tem inspiração em uma área da ciência da computação chamada de cálculo-λ\lambda.

Uma função anônima tem a seguinte forma:

lambda lista_de_argumentos: expressão

Alguns exemplos:

# domínio de discurso
D = list(range(1,11))

# predicados simples
def par(x):
    return x % 2 == 0

def impar(x):
    return x % 2 != 0

def primo(x):
    return x > 1 and all(x % i != 0 for i in range(2, x))


print(all(par(x) or impar(x) for x in D)) # todo número em D é par ou ímpar

print(any(primo(x) and par(x) for x in D)) # existe número primo e par em D

print(not any(primo(x) and x > 8 for x in D)) # não existe número primo maior que 8 em D
True
True
True

As funções abaixo são auxiliares para explicar os quantificadores.

def para_todo(D, P):
    """
    Simula quantificador universal.
    D: domínio de discurso
    P: predicado
    """
    return all(P(x) for x in D)

def existe(D, P):
    """ 
    Simula quantificador universal.
    D: domínio de discurso
    P: predicado    
    """
    return any(P(x) for x in D)


def existe_unico(D, P):
    """ 
    Simula quantificador de unicidade através de 
    contagem da elementos em D que satisfazem P.
    D: domínio de discurso
    P: predicado
    """
    return sum(1 for x in D if P(x)) == 1

Podemos combiná-las com predicados escritos através de lambda.

# domínio de discurso
D = {-2,0,2,5,8} 

# predicado
P = lambda x: x > 0

# testes
print(para_todo(D, P))
print(existe(D, P))
print(existe_unico(D, P))
False
True
False

Os predicados simulados podem ser passados diretamente às funções de teste acima como argumentos delas. Neste caso, é indiferente usar uma função anônima ou uma função regular criada anteriormente.

Abaixo temos mais exemplos:

para_todo(D, lambda x: x <= 0)
False
existe(D, lambda x: x >= 0 and x < 6)
True
# predicado simulado pela função regular "par"
existe_unico(D, par)
False
# predicado simulado pela função regular "primo"
para_todo(D, primo)
False

O exemplo abaixo é um pouco mais elaborado. Ele verifica em uma lista de nn letras

from string import ascii_lowercase as minusculas
from random import seed, choices, shuffle

# geração aleatória de n letras minúsculas
seed(5)
n = 10
D = choices(minusculas, k=n)

# embaralhamento
shuffle(D)

# impressão do domínio de discurso
print(D)

# testa se existem 2 consoantes específicas em D
print(existe(D, lambda x: x == "r" or x == "t"))

# testa se existe vogal em D
print(existe(D, lambda x: x in {'a', 'e', 'i', 'o', 'u'}))

# testa se existe vogal em D que não seja 'a' ou 'o'
print(existe_unico(D, lambda x: (x != 'a') or (x != 'o') ))
['m', 't', 'q', 'q', 'a', 'y', 'y', 'x', 't', 'u']
True
True
False

Aridade

Aridade é o número de argumentos ou operandos que uma função ou operação toma. Em nosso contexto, a aridade de predicados diz respeito ao número de variáveis presentes na declaração deles. Diz-se que um predicado é nn-ário se seus argumentos formam uma ênupla do tipo (x1,x2,,xn)(x_1, x_2, \ldots, x_n). Comumente, trabalhamos com casos simples e dizemos que um predicado é:

  • unário, se tiver apenas um argumento. Ex.: P(x):x>2P(x): x > 2

  • binário, se tiver dois argumentos. Ex.: P(x,y):x+y<5P(x,y): x + y < 5

  • ternário, se tiver três argumentos. Ex.: P(x,y,z):xyz=0P(x,y,z): x - y - z = 0

Em particular, convenciona-se que uma proposição pp tem aridade 0, pois possui apenas um valor-verdade booleano (True ou False). Predicados unários retratam propriedades de objetos; binários, relações entre dois objetos; ternários, relações entre três objetos; nn-ários, tabelas em bancos de dados e outras estruturas de dados maiores.

Predicados binários

Em geral, as letras xx e yy são usadas para identificar as duas variáveis aparecendo em predicados binários, mas podemos usar quaisquer outras. Nos exemplos a seguir, elas serão misturadas.

D = range(1,4)

P1 = lambda x, y: x + y > 3
P2 = lambda x, z: x == 2 or x - z > 1

print(all([P1(x,y) for x in D for y in D]))

print(any([P1(x,y) for x in D for y in D]))
False
True

Neste exemplo, criamos um teste de predicado com restrições.

# Conjunto de pessoas
D = {"Arian", "Bert", "Celin", "Dal", "Emm"}

# Predicado binário
def P(m: str, n: str):
    ok = {("Emm", "Dal"), ("Bert", "Celin"), ("Bert", "Arian")}
    return (m, n) in ok

# Pergunta: existe pelo menos um casal permitido na festa?
exist_ok = any(P(m, n) 
                   for m in D 
                   for n in D 
                   if m != n)

print(exist_ok) 

# Pergunta: existe alguém que não é Bert nem Celin e 
# que forma casal com alguém que não seja Dal?
exist_ = any(P(m, n)
             for m in D
             for n in D
             if m != n
             and m not in {"Bert", "Celin"}
             and n != "Dal")

print(exist_)
True
False

Predicados ternários

Usualmente, zz é a terceira letra empregada quando falamos de três variáveis. Em predicados ternários, essa prática também é comum. Porém, como observamos antes, podemos usar quaisquer outras letras. Nos exemplos a seguir, elas serão misturadas.

# divisor comum
P = lambda a, b, c: a % b == 0 and c % b == 0

# região da esfera unitária
Q = lambda x, y, z: x**2 + y**2 + z**2 < 1

print(P(1,2,3))
print(Q(0.5,0.5,0.5))
False
True

Condicionais como predicados

Em lógica e em programação, as estruturas condicionais se... senão... então... – em Python expressas por if, elif e else – podem ser vistas não apenas como comandos de controle de fluxo, mas também como predicados compostos de aridade maior que 1. A estrutura implementa uma função definida por casos, ou seja, um predicado nn-ário construído por disjunção exclusiva de predicados mais simples.

Vejamos este exemplo, que testa se um triângulo pode ser criado a partir das medidas passadas como argumento e, se sim, o tipo do triângulo.

def tc_tri(a, b, c):
    """
    Testa e classifica um triângulo dadas as suas medidas.
    """
    
    # condição para ser triângulo
    test = a > 0 and b > 0 and c > 0 and \
           a + b > c and a + c > b and b + c > a        
    
    if not test:
        return "Não é triângulo."
    elif a == b == c:
        return "Equilátero"
    elif a == b or b == c or a == c:
        return "Isósceles"
    else:
        return "Escaleno"

# valores para variáveis
A = [1,4,7,9]
B = [3,8,10,9]
C = [2,4,6,9]

# domínio de discurso (por concatenação)
D = [A, B, C]

# teste
for a, b, c in zip(D[0], D[1], D[2]):
    print(tc_tri(a,b,c))
Não é triângulo.
Não é triângulo.
Escaleno
Equilátero

A função tc_tri verifica quatro condições mutuamente exclusivas que usam três variáveis. Logo, no exemplo, temos quatro predicados ternários sendo avaliados, sendo a condição padrão (else) um predicado “implícito”:

  • Em if, avalia-se o predicado P1(a,b,c):¬(a>0b>0c>0p(a,b,c))P_1(a,b,c): \neg (a > 0 \wedge b > 0 \wedge c > 0 \wedge p(a,b,c));

  • No primeiro elif, avalia-se o predicado P2(a,b,c):a=b=cP_2(a,b,c): a = b = c;

  • No segundo elif, P3(a,b,c):(a=b)(b=c)(a=c)P_3(a,b,c): (a = b) \vee (b = c) \vee (a = c);

  • No else, P4(a,b,c):¬P2(a,b,c)¬P3(a,b,c)¬(P2(a,b,c)P3(a,b,c)),P_4(a,b,c):\neg P_2(a,b,c) \wedge \neg P_3(a,b,c) \equiv \neg (P_2(a,b,c) \vee P_3(a,b,c)),

em que p(a,b,c):(a+b>c)(a+c>b)(b+c>a)p(a,b,c): (a + b > c) \wedge (a + c > b) \wedge (b + c > a) em P1(a,b,c)P_1(a,b,c).

Exercícios aplicados resolvidos

I. Considere um campo de futebol padrão FIFA (105m comprimento x 68m largura), com origem (0,0) no centro do campo. Você está trabalhando com um sistema de rastreamento de lances que registra, em tempo real, as coordenadas 3D da bola no domínio do campo até à altura máxima de 12 metros com uma área excedente de 5 metros além das quatro linhas. Desenvolva predicados unários, binários e ternários para verificar o percentual de tempo em que a bola permanece no solo ou no ar dentro da área de jogo a partir do registro de posições, entre outras estatísticas úteis.

# Conteúdo resumido de '3-bolas_posicoes.csv'.

t,x,y,z
0,-10.562628677930888,-24.79809822533326,0
1,-9.092792300854816,-25.240348978986184,0
2,-7.6229559237787425,-25.68259973263911,0
3,-6.153119546702669,-26.124850486292033,0
4,-4.683283169626597,-26.567101239944954,0
5,-3.213446792550524,-27.00935199359788,0
6,-1.7436104154744498,-27.451602747250803,0
7,-0.27377403839837733,-27.893853500903727,0
8,1.1960623386776952,-28.336104254556652,0
...

Resolução

Vamos primeiramente ler o arquivo e tratar as situações caso a caso.

# leitura do arquivo
import pandas as pd
df = pd.read_csv("../examples/3-bola_posicoes.csv")

Alguns predicados, a priori, podem ser estruturados como segue:

# unário: checa se bola está no ar
P = lambda z: z > 1e-5

# binário: checa se bola está na área de jogo
Q = lambda x, y: (-105/2 <= x <= 105/2) and (-68/2 <= y <= 68/2)

# ternário: checa se bola está em jogo
R = lambda x, y, z: (z >= 0) and Q(x,y)

Para responder às perguntas desejadas, no entanto, a aplicação pode ser direta ou indireta.

  • Predicado unário

# aplicações isoladas (pouco úteis)
P(df['z'][12]), P(df['z'][90]), P(df['z'][112])
(np.True_, np.False_, np.False_)
# aplicação direta
P(df['z']).head(), P(df['z']).tail()
(0 False 1 False 2 False 3 False 4 False Name: z, dtype: bool, 295 False 296 False 297 False 298 False 299 False Name: z, dtype: bool)
# contagem de valores
P(df['z']).value_counts()
z False 282 True 18 Name: count, dtype: int64
# % de permanência em solo
P(df['z']).value_counts().values[0]/P(df['z']).value_counts().sum()*100
np.float64(94.0)
  • Predicado binário

# aplicações isoladas (pouco úteis)
Q(df['x'][12], df['y'][12]), Q(df['x'][90], df['x'][90]), Q(df['x'][112], df['y'][112])
(np.True_, np.False_, np.True_)
# aplicação direta requer adaptações para ser Pythônica
df['bola_dentro'] = df['x'].between(-52.5, 52.5) & df['y'].between(-34.0, 34.0)
df['bola_dentro'].head()
0 True 1 True 2 True 3 True 4 True Name: bola_dentro, dtype: bool
  • Predicado ternário

# aplicação direta
df['bola_solo'] = df['x'].between(-52.5, 52.5) & \
                  df['y'].between(-34.0, 34.0) & \
                  df['z'] < 1e-5
df['bola_solo'].head()                  
0 True 1 True 2 True 3 True 4 True Name: bola_solo, dtype: bool
  • Estatísticas

# tempo todo de bola no solo?
all(df['bola_solo'])
False
# Quantos segundos a bola ficou fora
segundos_fora = (~df['bola_dentro']).sum()
print(f"A bola esteve fora do campo por {segundos_fora} segundos."
      f"({segundos_fora/len(df)*100:.1f}%)")

print(f"A bola esteve dentro do campo por {len(df) - segundos_fora} segundos."
      f"({(len(df) - segundos_fora)/len(df)*100:.1f}%)")
A bola esteve fora do campo por 107 segundos.(35.7%)
A bola esteve dentro do campo por 193 segundos.(64.3%)
# Primeira e última vez que saiu
primeira_saida = df[~df['bola_dentro']]['t'].iloc[0]
ultima_saida   = df[~df['bola_dentro']]['t'].iloc[-1]
print(f"Primeira saída: t = {primeira_saida}s")
print(f"Última saída:   t = {ultima_saida}s")
Primeira saída: t = 26s
Última saída:   t = 292s