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: “ é um número inteiro maior do que 3”. Aqui, é 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
Daí, é imediato que e são avaliações do predicado que geram valor-verdade falso, enquanto e geram valor-verdade verdadeiro. Por outro lado, teria valor indeterminado, já que é um valor fora do domínio de discurso , aqui equivalente a . Adiante, vamos explorar o significado do símbolo .
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 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: (a soma de qualquer número natural com zero é o próprio número).
quantificador existencial: representado pelo símbolo 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: (existe um (ou pelo menos um) inteiro cujo quadrado é 4 – de fato, existem dois: –2 e 2).
quantificador de unicidade: representado pelo símbolo 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: (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:
exige que a propriedade seja verdadeira sempre;
exige que a propriedade seja verdadeira pelo menos uma vez;
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 e , 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) é verdadeiroTrueall([False, False, False])Falseany([False, False, False])Falseany([True, False, False])TrueD = [-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]
TrueNegação de proposições quantificadas¶
O termo predicado, ou função proposicional, é utilizado sempre que uma declaração contiver uma variável livre (ex. é 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:
( é par e é ímpar)
( é 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,
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,
Isto posto, as duas proposições acima equivalem a:
e
,
as quais refletem as leis de De Morgan para quantificadores, resumidas na tabela abaixo.
| Equivalência | Descrição | Interpretação |
|---|---|---|
| Negação do universal | A negação de “para todo , é verdadeiro” é “existe pelo menos um para o qual é falso.” | |
| Negação do existencial | A negação de “existe pelo menos um para o qual é verdadeiro” é “para todo , é falso.” |
Vejamos exemplos complementares:
not all(x > 0 for x in D)Truenot any(x > 0 for x in D)FalseSimulaçã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 yPor 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-.
Uma função anônima tem a seguinte forma:
lambda lista_de_argumentos: expressãoAlguns 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 DTrue
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)) == 1Podemos 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)Falseexiste(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)FalseO exemplo abaixo é um pouco mais elaborado. Ele verifica em uma lista de 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 é -ário se seus argumentos formam uma ênupla do tipo . Comumente, trabalhamos com casos simples e dizemos que um predicado é:
unário, se tiver apenas um argumento. Ex.:
binário, se tiver dois argumentos. Ex.:
ternário, se tiver três argumentos. Ex.:
Em particular, convenciona-se que uma proposição 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; -ários, tabelas em bancos de dados e outras estruturas de dados maiores.
Predicados binários¶
Em geral, as letras e 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, é 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 -á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 ;No primeiro
elif, avalia-se o predicado ;No segundo
elif, ;No
else,
em que em .
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()*100np.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: boolPredicado 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: boolEstatí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