You are on page 1of 12

linear-congruential-method

November 8, 2021

1 Avaliação 1: Método congruencial linear (MCL)


Renato Marques de Oliveira, 414212

[ ]: from scipy.stats import chi2, norm


import numpy as np
import matplotlib.pyplot as plt

1.1 Implementando e testando o método


Por velocidade e eficiência, implementaremos o MCL (fórmula geral) utilizando Cython1 , uma
biblioteca que gera código compilado em C que é acessível dentro do Python. Além do MCL,
utilizaremos Cython para implementar uma rotina de ordenamento e uma rotina para calcular a
moda de uma amostra.

[ ]: %load_ext cython

[ ]: %%cython

# Implementation of the LINEAR CONGRUENTIAL METHOD

import numpy as np
from libc.math cimport abs

#############################
## RANDOM NUMBER GENERATOR ##
#############################

cdef unsigned int seed = 1

cdef struct Parameters:


long long unsigned int a, m, b, x

cdef Parameters p
p.a = 16807
p.m = 2147483647
1
O código Cython seguiu o padrão de língua inglesa porque foi inicialmente concebido como um módulo indepen-
dente.

1
p.b = 0
p.x = seed

def print_parameters() -> None:


print(p)

def get_parameters() -> dict:


return p

def set_parameters(a: int, m: int, b: int, x: int) -> None:


p.a = a
p.m = m
p.b = b
p.x = x

def reset():
set_parameters(16807, 2147483647, 0, 1)

def set_seed(int new_value) -> None:


seed = new_value
p.x = new_value

# The main function


cpdef double random():
'''Returns a random value (64 bit) between 0 and 1.'''
p.x = (p.x * p.a + p.b) % p.m
return float(p.x) / p.m

# For efficiency purposes


def random_array(Py_ssize_t n) -> double[:]:
'''Creates an array of n values generated by the present method.'''
array_ = np.zeros(n)
cdef double[:] array = array_
for i in range(n):
array[i] = random()
return array

#######################
## Special functions ##
#######################

# Counts the number of runs for use in "Runs tests"


def number_of_runs(double[:] array) -> int:
cdef int a = 1
cdef double previous_value = array[0]
cdef bint upwards = True
cdef Py_ssize_t i

2
for i in range(1,len(array)):
if upwards and array[i] < array[i - 1]:
upwards = False
a += 1
elif not upwards and array[i] > array[i - 1]:
upwards = True
a += 1
return a

# Merge-sort is useful for calculation of the mode.


# Source: "Merge Sort/Top-down implementation" on Wikipedia.
cpdef void merge_sort(double[:] A):
n = len(A)
cdef double[:] B = np.zeros(n)
for i in range(n): B[i] = A[i]
split_merge(B, 0, n, A)

cdef void split_merge(double[:] B, int i, int j, double[:] A):


if j - i <= 1: return
cdef Py_ssize_t k = (i + j) // 2
split_merge(A, i, k, B)
split_merge(A, k, j, B)
merge(B, i, k, j, A)

cdef void merge(double[:] A, int i, int k, int j, double[:] B):


cdef Py_ssize_t middle = k
for n in range(i, j):
if i < middle and (k >= j or A[i] <= A[k]):
B[n] = A[i]
i += 1
else:
B[n] = A[k]
k += 1

def mode(double[:] A, double epsilon = 1e-4, bint ordered = False) -> float:
"""Efficiently calculates the mode of an 1-dimensional array of floats.

Args:
A (double): The array of floats on which to compute the mode.
epsilon (double, optional): Two values are equal if their difference is␣
,→smaller than epsilon. Defaults to 1e-6.

ordered (bool, optional): If the input is already ordered, prevents the␣


,→algorithm from reordering. Defaults to False.

Returns:
double: The mode of A.

3
"""
cdef Py_ssize_t n = len(A), i = 0
cdef double[:] B = np.zeros(n)
for k in range(n): B[k] = A[k]

if not ordered: merge_sort(B)

cdef unsigned int count = 1, max_count = 0


cdef double mode
while i < n - 1:
if B[i + 1] - B[i] <= epsilon:
count += 1
else:
count = 0

if count > max_count:


max_count = count
mode = B[i]

i += 1

return mode

Em seguida, vamos definir algumas rotinas para testes estatísticos.

[ ]: def teste_qui_quadrado(observado, esperado, graus, significancia = 0.05,␣


,→printar = False):

estatistica_qui = sum([ (f_o - f_e)**2 / f_e for f_o, f_e in zip(observado,␣


,→esperado) ])

valor_critico = chi2.ppf(1 - significancia, graus)

if printar:
print("-------- Teste do qui-quadrado ----------")
print(
"Estatística: {}\nValor crítico: {}".format(estatistica_qui,␣
,→valor_critico)

)
if estatistica_qui >= valor_critico:
print("Rejeita-se H0.")
else:
print("Aceita-se H0.")

return estatistica_qui, valor_critico, estatistica_qui >= valor_critico

[ ]: def teste_normal_padrao(estatistica_z, significancia = 0.05, printar = False):


valor_critico = norm.ppf(1 - significancia)

4
if printar:
print("-------- Teste de normalidade ----------")
print(
"Estatística (|z|): {}\nValor crítico: {}".
,→format(abs(estatistica_z), valor_critico)

)
if abs(estatistica_z) >= valor_critico:
print("Rejeita-se H0.")
else:
print("Aceita-se H0.")

return estatistica_z, valor_critico, abs(estatistica_z) >= valor_critico

A rotina testar_metodo abaixo encapsula todo o processo de teste do nosso gerador de números
aleatórios. Ela segue os passos:
1. Para cada tamanho de amostra N :
1. Cria uma sequência de N números aleatórios.
2. Executa um teste de uniformidade e guarda os resultados.
3. Executa um teste de aleatoriedade e guarda os resultados.
2. Formata os resultados guardados e printa no console, junto com os parâmetros utilizados pelo
método testado.

[ ]: # obs: é preciso instalar o pacote "tabulate" para printar tabelas em forma de␣
,→string

from tabulate import tabulate


N = [1000, 10_000, 1_000_000]

def testar_metodo():

print("----------------------------------\n"
+ "--- Parâmetros: {} ---\n".format(get_parameters())
+ "----------------------------------")

testes_qui = []
testes_Z = []
for n in N:
# O vetor de números aleatórios precisa ser um array do Numpy
sequencia = np.array([ random() for i in range(n) ])

##### TESTES DE UNIFORMIDADE ########


# Agrupamos a amostra em 10 lotes ("bins") igualmente espaçados
# para realizar o teste do qui-quadrado.
observados, _ = np.histogram(sequencia, bins = np.arange(0, 1, 0.1))
esperados = [n / 10] * 10
testes_qui.append(
teste_qui_quadrado(observados, esperados, 9, 0.05)
)

5
##### TESTES DE ALEATORIEDADE (EXECUÇÕES) ######
# Em seguida contamos o número de execuções e realizamos um
# teste de normalidade na quantidade de execuções.
n_execucoes = number_of_runs(sequencia)
mu_execucoes = (2 * n - 1) / 3
sigma_execucoes = np.sqrt((16 * n - 29) / 90)
z = (n_execucoes - mu_execucoes) / sigma_execucoes
testes_Z.append(
teste_normal_padrao(z, 0.05)
)

print("-------- Teste do qui-quadrado ----------")


colunas = ["Amostras", "Estatística", "Valor crítico", "Rejeita H0"]
linhas = [ (amostra,) + teste for amostra, teste in zip(N, testes_qui) ]
print(tabulate(linhas, colunas, tablefmt = "github"))

print("-------- Teste de normalidade ----------")


linhas = [ (amostra,) + teste for amostra, teste in zip(N, testes_Z) ]
print(tabulate(linhas, colunas, tablefmt = "github"))

1.1.1 Teste 1
reset garante que utilizamos os parâmetros padrão definidos no início.

[ ]: reset()
testar_metodo()

----------------------------------
--- Parâmetros: {'a': 16807, 'm': 2147483647, 'b': 0, 'x': 1} ---
----------------------------------
-------- Teste do qui-quadrado ----------
| Amostras | Estatística | Valor crítico | Rejeita H0 |
|------------|---------------|-----------------|--------------|
| 1000 | 6.45 | 16.919 | 0 |
| 10000 | 6.777 | 16.919 | 0 |
| 1000000 | 5.55134 | 16.919 | 0 |
-------- Teste de normalidade ----------
| Amostras | Estatística | Valor crítico | Rejeita H0 |
|------------|---------------|-----------------|--------------|
| 1000 | -2.72747 | 1.64485 | 1 |
| 10000 | -0.719483 | 1.64485 | 0 |
| 1000000 | -1.98828 | 1.64485 | 1 |

6
1.1.2 Teste 2
[ ]: set_parameters(2**18 + 1, 2**35, 0, 1)
testar_metodo()

----------------------------------
--- Parâmetros: {'a': 262145, 'm': 34359738368, 'b': 0, 'x': 1} ---
----------------------------------
-------- Teste do qui-quadrado ----------
| Amostras | Estatística | Valor crítico | Rejeita H0 |
|------------|---------------|-----------------|--------------|
| 1000 | 8900 | 16.919 | 1 |
| 10000 | 89000 | 16.919 | 1 |
| 1000000 | 2896.19 | 16.919 | 1 |
-------- Teste de normalidade ----------
| Amostras | Estatística | Valor crítico | Rejeita H0 |
|------------|---------------|-----------------|--------------|
| 1000 | -49.9453 | 1.64485 | 1 |
| 10000 | -158.097 | 1.64485 | 1 |
| 1000000 | -1581.1 | 1.64485 | 1 |

1.1.3 Teste 3
[ ]: set_parameters(4*20 + 1, 2**35 - 1, 17, 1)
testar_metodo()

----------------------------------
--- Parâmetros: {'a': 81, 'm': 34359738367, 'b': 17, 'x': 1} ---
----------------------------------
-------- Teste do qui-quadrado ----------
| Amostras | Estatística | Valor crítico | Rejeita H0 |
|------------|---------------|-----------------|--------------|
| 1000 | 10.68 | 16.919 | 0 |
| 10000 | 6.777 | 16.919 | 0 |
| 1000000 | 68.8694 | 16.919 | 1 |
-------- Teste de normalidade ----------
| Amostras | Estatística | Valor crítico | Rejeita H0 |
|------------|---------------|-----------------|--------------|
| 1000 | 1.25113 | 1.64485 | 0 |
| 10000 | -1.00411 | 1.64485 | 0 |
| 1000000 | -10.7494 | 1.64485 | 1 |

1.2 Analisando a melhor configuração


O conjunto de parâmetros que passou em mais testes foi o último. Adiante, para cada valor de
semente entre 0 e 29, geraremos 3 amostras de sequências de números aleatórios, de tamanho 1000,
105 e 106 , totalizando 90 amostras cada.

[ ]: from scipy.stats import skew, kurtosis

7
Vamos nos assegurar de que estamos com os parâmetros desejados.

[ ]: set_parameters(4*20 + 1, 2**35 - 1, 17, 1)


print_parameters()

{'a': 81, 'm': 34359738367, 'b': 17, 'x': 1}


A estrutura de dados amostra abaixo guardará os resultados da simulação. Ela é uma lista de
dicionários, com um de seus elementos exemplificados. Ela será preenchida na célula seguinte.

[ ]: amostras = [dict(zip(map(str, N), [[]]*3)) for _ in range(30)]


print(amostras[0])

{'1000': [], '10000': [], '1000000': []}

[ ]: for i in range(30):
set_seed(i)
for n in N:
amostras[i][str(n)] = random_array(n)

Feita a simulação, vamos fazer outra estrutura de dados, resultado, para guardar as estatística
referentes a cada amostra.

[ ]: resultados = [dict(zip(map(str, N), [[]]*3)) for _ in range(30)]

for i in range(30):
for n in list(map(str, N)):
variancia = np.var(amostras[i][n])
mediana = np.median(amostras[i][n])
amostra_ordenada = amostras[i][n].copy()
merge_sort(amostra_ordenada)
q1 = np.median(amostra_ordenada[:(int(n) // 2)])
q3 = np.median(amostra_ordenada[(int(n) // 2):])
quartis = [q1, mediana, q3]
resultados[i][n] = {
'média': np.mean(amostras[i][n]),
'mediana': mediana,
'moda': mode(amostra_ordenada, 1e-5, True),
'assimetria': skew(amostras[i][n]),
'kurtose': kurtosis(amostras[i][n]),
'variância': variancia,
'desvio-padrão': np.sqrt(variancia),
'quartis': quartis
}

A rotina abaixo agrupa os resultados por categoria para facilitar a plotagem.

[ ]: medias = []
medianas = []
modas = []

8
assimetrias = []
kurtoses = []
variancias = []
desvios_padrao = []
quartis = []
for i in range(30):
for n in map(str, N):
medias.append(resultados[i][n]['média'])
medianas.append(resultados[i][n]['mediana'])
modas.append(resultados[i][n]['moda'])
assimetrias.append(resultados[i][n]['assimetria'])
kurtoses.append(resultados[i][n]['kurtose'])
variancias.append(resultados[i][n]['variância'])
desvios_padrao.append(resultados[i][n]['desvio-padrão'])
quartis.append(resultados[i][n]['quartis'])

Finalmente, através de plotagem, comparamos os resultados de cada amostra (rotuladas de 0 a


89) com o valor teórico da distribuição uniforme U (0, 1). O valor teórico é indicado pela linha
horizontal.

[ ]: plt.style.use('ggplot')

fig, ax = plt.subplots(4, 2)

fig.set_size_inches(10, 12)

ax[0][0].set_title('Médias')
ax[0][0].plot(medias)
ax[0][0].plot([0,90], [0.5, 0.5])#, 'r')
#ax[0][0].xaxis.set_visible(False)
ax[0][0].set_xticklabels([])

ax[0][1].set_title('Medianas')
ax[0][1].plot(medianas)
ax[0][1].plot([0,90], [0.5, 0.5])#, 'r')
ax[0][1].set_xticklabels([])

ax[1][0].set_title('Modas')
ax[1][0].plot(modas, 'o')
ax[1][0].set_xticklabels([])

ax[1][1].set_title('Assimetrias')
ax[1][1].plot(assimetrias)
ax[1][1].plot([0,90], [0, 0])#, 'r')
ax[1][1].set_xticklabels([])

ax[2][0].set_title('Kurtoses')

9
ax[2][0].plot(kurtoses)
ax[2][0].plot([0,90], [-6/5, -6/5])#, 'r')
ax[2][0].set_xticklabels([])

ax[2][1].set_title('Variâncias')
ax[2][1].plot(variancias)
ax[2][1].plot([0,90], [1/12, 1/12])#, 'r')
ax[2][1].set_xticklabels([])

ax[3][0].set_title('Desvios-padrão')
ax[3][0].plot(desvios_padrao)
ax[3][0].plot([0,90], [np.sqrt(1/12), np.sqrt(1/12)])#, 'r')
ax[3][0].set_xlabel('Amostra')

ax[3][1].set_title('Quartis')
quartis = np.array(quartis).T
p = ax[3][1].plot(quartis[0])
ax[3][1].plot(quartis[1], color=p[0].get_color())
ax[3][1].plot(quartis[2], color=p[0].get_color())
q = ax[3][1].plot([0,90], [0.25, 0.25])#, 'r')
ax[3][1].plot([0,90], [0.50, 0.50], color=q[0].get_color())#, 'r')
ax[3][1].plot([0,90], [0.75, 0.75], color=q[0].get_color())#, 'r')
ax[3][1].set_xlabel('Amostra');

10
1.2.1 Conclusões
De um modo geral, a atual implementação do MCL com o conjunto de parâmetros a = 4 · 20 + 1,
m = 235 − 1 e b = 17 apresenta uma concordância muito boa com a distribuição uniforme contínua.
Note que a moda de uma amostra de números de ponto flutuante varia com o grau de “precisão”
considerado na igualdade. Por exemplo, vamos variar esse grau abaixo. De qualquer modo, o valor
teórico da moda da distribuição U (0, 1) é qualquer x ∈ [0, 1].

11
[ ]: modas_teste = [[], [], []]
for i in range(30):
for n in map(str, N):
amostra_ordenada = amostras[i][n].copy()
merge_sort(amostra_ordenada)
modas_teste[0].append(mode(amostra_ordenada, 1e-4, True))
modas_teste[1].append(mode(amostra_ordenada, 5e-5, True))
modas_teste[2].append(mode(amostra_ordenada, 1e-5, True))

[ ]: fig, ax = plt.subplots(1,3)
fig.set_size_inches(14, 4)
fig.suptitle('Modas')
for i in range(3):
ax[i].set_title(r'$\varepsilon$ = {}'.format([r'$10^{-4}$',␣
,→r'$5\times10^{-5}$', r'$10^{-5}$'][i]))

ax[i].set_ylim(-0.05, 1.05)
ax[i].plot(modas_teste[i], 'o')

12

You might also like