import csv
import math
import random
import os.path
import operator
import numpy as np
import matplotlib.pyplot as plt
from copy import deepcopy
from functools import reduce

"""
    FURB - Bacharelado em Ciências da Computação
    Inteligência Artificial
    Trabalho 03 - Fase 03 - Regressão Polinomial - Overfitting
    Equipe: Adriner Maranho de Andrade, Fábio Luiz Fischer, Felipe Anselmo dos Santos, Jorge Guilherme Kohn

    Faça um script demo_regressaop que faz o seguinte:
        a) Baixe o arquivo data_preg.mat ou data_preg.svg. A primeira coluna representa os valores de x e a segunda coluna representa os valores de y.
        b) Faça o Gráfico de dispersão dos dados.
        c) Use a função polyfit para gerar a linha de regressão para N = 1 e trace-o no gráfico de dispersão na cor vermelha (plot (x, y, 'r')).
        (observe que nesta função a numeração coeficiente é invertida! β0=βN, β1=βN−1, β2=βN−2 , ...βN=β0)
        d) Trace a linha de regressão para N = 2 no gráfico na cor verde.
        e) Trace a linha de regressão para N = 3 no gráfico na cor preta.
        f) Trace a linha de regressão para N = 8 no gráfico na cor amarela.
        g) Calcule o Erro Quadrático Médio (EQM) para cada linha de regressão. Qual é o mais preciso?
            Resposta: A linha de regressão para N=8 é mais precisa para esse cenário pois obteve o menor erro quadrático médio: 0.05870934697363511
        h) Para evitar o overfitting, divida os dados aleatoriamente em Dados de Treinamento e Dados de Teste. Use os primeiros 10% dos dados como conjunto de teste, e o resto como de treinamento.
        i) Repita os passos de c - f, mas agora use apenas os dados de treinamento para ajustar a linha de regressão.
        J) Repita o passo g, mas agora utilize somente os dados de Teste para calcular o erro.
        k) Que método é o mais preciso neste caso?
            Resposta: Apesar de cada regressão poder se sobressair de acordo com os dados selecionados para o treinamento,
                nas execuções realizadas a regressão N=2 teve o melhor resultado em 5 execuções consecutivas de 5000 iterações.
                Em média, a regressão N=2 se saiu melhor em 50% das iterações da execução.

                Execução 1 - 5000 iterações
                    N1 teve o melhor EQM 14 vezes
                    N2 teve o melhor EQM 2779 vezes
                    N3 teve o melhor EQM 1104 vezes
                    N8 teve o melhor EQM 1103 vezes

                Execução 2 - 5000 iterações
                    N1 teve o melhor EQM 27 vezes
                    N2 teve o melhor EQM 2721 vezes
                    N3 teve o melhor EQM 1112 vezes
                    N8 teve o melhor EQM 1140 vezes

                Execução 3 - 5000 iterações
                    N1 teve o melhor EQM 25 vezes
                    N2 teve o melhor EQM 2718 vezes
                    N3 teve o melhor EQM 1137 vezes
                    N8 teve o melhor EQM 1120 vezes

                Execução 4 - 5000 iterações
                    N1 teve o melhor EQM 23 vezes
                    N2 teve o melhor EQM 2708 vezes
                    N3 teve o melhor EQM 1133 vezes
                    N8 teve o melhor EQM 1136 vezes

                Execução 5 - 5000 iterações
                    N1 teve o melhor EQM 14 vezes
                    N2 teve o melhor EQM 2725 vezes
                    N3 teve o melhor EQM 1108 vezes
                    N8 teve o melhor v 1153 vezes
"""

def strnum(x):
    return round(x, 2)

class DataSet:
    def __init__(self, x, y):
        self.x = np.array(x)
        self.y = np.array(y)
        self.lenght = len(self.x)

class RegressaoPolinomial:
    def __init__(self, dataset):
        self.dataset = dataset

    def regressaop(self, n, outro=None):
        # Chama a função polyfit e inverte seu resultado, pois sua numeração coeficiente é invertida
        b = np.polyfit(self.dataset.x, self.dataset.y, n)[::-1]

        def somatoriap(b, x):
            resultado = 0
            for i in range(n+1):
                resultado += b[i] * x ** i
            return np.array(resultado)
        # Quando outro objeto for passado como parâmetro o valor do polyfit será compartilhado entre o calculo de ambos
        if outro is None:
            return somatoriap(b, self.dataset.x)
        return somatoriap(b, self.dataset.x), somatoriap(b, outro.dataset.x)

    @staticmethod
    def eqm(y1, y2):
        # Se o tamanho dos vetores de entrada for diferente, deve ser feito um broadcasting do maior para o menor,
        # de forma que os elementos faltantes no array menor sejam iguais aos do array maior
        if len(y1) != len(y2):
            y1y = y1 if len(y1) > len(y2) else np.array([y1[i] if i < len(y1) else y2[i] for i in range(len(y2))])
            y2y = y2 if len(y2) > len(y1) else np.array([y2[i] if i < len(y2) else y1[i] for i in range(len(y1))])
            return reduce(operator.add, (y1y - y2y) ** 2) / len(y2y)
        return reduce(operator.add, (y1 - y2) ** 2) / len(y2)

# Realiza leitura do arquivo e parsing dos valores para float
with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "data_preg.csv")) as file:
    raw_data = csv.reader(file, delimiter=',')
    data = [[float(elem) for elem in row] for row in raw_data]

if data is not None:
    ds = DataSet([elem[0] for elem in data], [elem[1] for elem in data])
    rp = RegressaoPolinomial(ds)

    # Cria diagrama de disperção dos pontos do dataset
    fig = plt.figure('Gráfico de disperção')
    plt.title('Vermelho: N1  Verde: N2  Preto: N3  Amarelo: N8')
    plt.grid(True)
    plt.scatter(ds.x, ds.y)

    # Gera a linha de regressão polinomial para N1 em vermelho
    y1 = rp.regressaop(1)
    plt.plot(ds.x, y1, 'r')
    # Gera a linha de regressão polinomial para N2 em verde
    y2 = rp.regressaop(2)
    plt.plot(ds.x, y2, 'g')
    # Gera a linha de regressão polinomial para N3 em preto
    y3 = rp.regressaop(3)
    plt.plot(ds.x, y3, 'k')
    # Gera a linha de regressão polinomial para N8 em amarelo
    y4 = rp.regressaop(8)
    plt.plot(ds.x, y4, 'y')

    # Calcula o erro quadratico médio EQM para cada uma das regressões
    eqm = [[y, RegressaoPolinomial.eqm(y[1], ds.y)] for y in [('N1', y1), ('N2', y2), ('N3', y3), ('N8', y4)]]
    for i in range(len(eqm)):
        print('EQM da regressão de %s' % eqm[i][0][0], eqm[i][1])
    plt.show()

    resultados = []
    # Treinamento para evitar underfitting e overfitting
    # Número de iterações para o treinamento
    iteracoes = 5000
    # 'True' para exibir as regressões, 'False' para exeibir apenas o resultado final
    exibeDiagramaTreinamento = False

    print('\nIniciando treinamento para evitar underfitting e overfitting...')
    print('Utilizando', iteracoes, 'iterações\n')

    for i in range(iteracoes):
        # Cria uma cópia dos dados do dataset e randomiza o endereçamento deles na matriz
        datai = deepcopy(data)
        random.shuffle(datai)

        # Seleciona 10% dos dados aleatórios como dados para teste e os outros 90% para dados de treinamento
        # Ordena os valores para melhor qualidade na exibição do gráfico
        index = round((ds.lenght * 10) / 100)
        data_tes = datai[0:index]
        data_tre = datai[index:ds.lenght]
        data_tre.sort()

        ds_tes = DataSet([elem[0] for elem in data_tes], [elem[1] for elem in data_tes])
        ds_tre = DataSet([elem[0] for elem in data_tre], [elem[1] for elem in data_tre])
        rp_tes = RegressaoPolinomial(ds_tes)
        rp_tre = RegressaoPolinomial(ds_tre)

        if exibeDiagramaTreinamento:
            fig = plt.figure('Treinamento de Regressão Polinominial')
            plt.grid(True)
            plt.scatter(ds_tes.x, ds_tes.y)
            plt.scatter(ds_tre.x, ds_tre.y, c='b')

        # Calcula a regressão polinomial dos datasets de treino e de teste com o mesmo polyfit
        y1_tre, y1_tes = rp_tre.regressaop(1, rp_tes)
        y2_tre, y2_tes = rp_tre.regressaop(2, rp_tes)
        y3_tre, y3_tes = rp_tre.regressaop(3, rp_tes)
        y4_tre, y4_tes = rp_tre.regressaop(8, rp_tes)

        if exibeDiagramaTreinamento:
            plt.plot(ds_tre.x, y1_tre, 'r')
            plt.plot(ds_tre.x, y2_tre, 'g')
            plt.plot(ds_tre.x, y3_tre, 'k')
            plt.plot(ds_tre.x, y4_tre, 'y')

        # Calcula o erro quadratico médio EQM para cada uma das regressões
        eqm_tre = [('N1', y1_tre, y1_tes), ('N2', y2_tre, y2_tes), ('N3', y3_tre, y3_tes), ('N8', y4_tre, y4_tes)]
        eqm = [[y, RegressaoPolinomial.eqm(y[1], y[2])] for y in eqm_tre]

        # Salva o melhor resultado  na lista de resultados
        melhor = None
        for res in eqm:
            melhor = res if melhor is None or res[1] < melhor[1] else melhor
        resultados.append(melhor)

    if exibeDiagramaTreinamento:
        plt.show()
    # Sumariza o resultado do treinamento atravéz da iterações
    # Informa a qunatidade vezes que cada valor de N teve o melhor resultado
    for tre in ['N1', 'N2', 'N3', 'N8']:
        aux = list(filter(lambda res: res[0][0] == tre, resultados))
        print('%s teve o melhor EQM %s vezes' % (tre, len(aux)))

else:
    print('Erro ao ler arquivo')


# NumPy is often used along with packages like SciPy (Scientific Python) and Mat−plotlib (plotting library). This combination is widely used as a replacement for MatLab
# ndarray - an N-dimensional array; collection of items of the same type; items can be accessed using a zero-based index
# Each element in ndarray is an object of data-type object (called dtype)

import numpy as np
a = np.array([1,2,3])  # [1, 2, 3]
a = np.array([[1, 2], [3, 4]])
'''
[[1, 2]
 [3, 4]]
'''

# ndarray.shape - this array attribute returns a tuple consisting of array dimensions
a = np.array([[1,2,3],[4,5,6]])
print(a.shape) # (2, 3)

# It can also be used to resize the array
a.shape=(3,2)
'''
[[1, 2]
 [3, 4]
 [5, 6]]
'''

# NumPy also provides a reshape function to resize an array
a.reshape(3,2)

a.ndim # 2 dimentional array

# numpy.linspace - this function is similar to arange() function.
# In this function, instead of step size, the number of evenly spaced values between the interval is specified
# numpy.linspace(start, stop, num, endpoint, retstep, dtype)
x = np.linspace(10,20,5)  # [10.   12.5   15.   17.5  20.]

# numpy.logspace - this function returns an ndarray object that contains the numbers that are evenly spaced on a log scale.
# Start and stop endpoints of the scale are indices of the base, usually 10.
# numpy.logspace(start, stop, num, endpoint, base, dtype)
# default base is 10
a = np.logspace(1.0, 2.0, num = 10)
'''
[ 10.           12.91549665     16.68100537      21.5443469  27.82559402
  35.93813664   46.41588834     59.94842503      77.42636827    100.    ]
'''
a = np.logspace(1,10,num = 10, base = 2)
# [ 2.     4.     8.    16.    32.    64.   128.   256.    512.   1024.]

# arange funciton produces a list of numbers based on specifications
# numpy.arange(start, stop, step, dtype)
a=np.arange(8) # array([0, 1, 2, 3, 4, 5, 6, 7])
# now reshape it
b = a.reshape(2,2,2)
print(b) # b has three dimensions

b.dtype # its int32
b.itemsize

# specify data type
x = np.array([1,2,3,4,5], dtype = np.float32)

'''
different data types
bool_ Boolean (True or False) stored as a byte
int_ Default integer type (same as C long; normally either int64 or int32)
intc Identical to C int (normally int32 or int64)
intp Integer used for indexing (same as C ssize_t; normally either int32 or int64)
int8 Byte (-128 to 127)
int16 Integer (-32768 to 32767)
int32 Integer (-2147483648 to 2147483647)
int64 Integer (-9223372036854775808 to 9223372036854775807)
uint8 Unsigned integer (0 to 255)
uint16 Unsigned integer (0 to 65535)
uint32 Unsigned integer (0 to 4294967295)
uint64 Unsigned integer (0 to 18446744073709551615)
float_ Shorthand for float64
float16 Half precision float: sign bit, 5 bits exponent, 10 bits mantissa
float32 Single precision float: sign bit, 8 bits exponent, 23 bits mantissa
float64 Double precision float: sign bit, 11 bits exponent, 52 bits mantissa
complex_ Shorthand for complex128
complex64 Complex number, represented by two 32-bit floats (real and imaginary components)
complex128 Complex number, represented by two 64-bit floats (real and imaginary components)
'''

# numpy.empty creates an uninitialized array of specified shape and dtype
x = np.empty([3,2], dtype = int)
print(x)
'''
[[ -572458896         495]
 [          0           0]
 [          1 -2147483648]]
Note − The elements in an array show random values as they are not initialized
'''

# numpy.zeros returns a new array of specified size, filled with zeros
x = np.zeros(5)  # array([0., 0., 0., 0., 0.])
x = np.zeros((5,), dtype = np.int)  # array([0, 0, 0, 0, 0])
x = np.zeros((2,2), dtype = [('x', 'i4'), ('y', 'i4')])
'''
array([[(0, 0), (0, 0)],
       [(0, 0), (0, 0)]], dtype=[('x', '<i4'), ('y', '<i4')])
'''

# numpy.ones returns a new array of specified size and type, filled with ones
x = np.ones(5)  # array([1., 1., 1., 1., 1.])
x = np.ones([2,2], dtype = int)
'''
array([[1, 1],
       [1, 1]])
'''

# numpy.asarray - this function is similar to numpy.array
# # except for the fact that it has fewer parameters.
# # This routine is useful for converting Python sequence into ndarray.

# convert list to ndarray
x = [1,2,3]
a = np.asarray(x)

# dtype is set
x = [1,2,3]
a = np.asarray(x, dtype = float)

# ndarray from tuple
x = (1,2,3)
a = np.asarray(x)

# ndarray from list of tuples
x = [(1,2,3),(4,5)]
a = np.asarray(x)

# numpy.frombuffer - this function interprets a buffer as one-dimensional array.
# Any object that exposes the buffer interface is used as parameter to return an ndarray.
s = 'Hello World'
a = np.frombuffer(s, dtype = 'S1')

# numpy.fromiter
# This function builds an ndarray object from any iterable object.
# A new one-dimensional array is returned by this function.
list = range(5)
it = iter(list)   # obtain iterator object from list
x = np.fromiter(it, dtype = float)   # use iterator to create ndarray

# Slicing one dim array [start:stop:step<one-by-default>]
a = np.arange(10)
slice(2,7,2)  # [2  4  6]
a[2:7:2]  # array([2, 4, 6])
a[2:7] # array([2, 3, 4, 5, 6])
a[5]  # 5 - this is just an individual int not an array
a[7:]  # array([7, 8, 9])

# Slicing two dim array
a = np.arange(9)
a.shape=(3,3)
'''
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
'''
a[2,2] # 8
a[1,:]  # array([3, 4, 5]) - we can skip specifying :
a[:,1] # array([1, 4, 7]) - we cant skip specifying :
a[1:,:] # all columns of row 2 and 3
'''
array([[3, 4, 5],
       [6, 7, 8]])
'''

# NaN (Not a Number) elements can be omitted by using ~ (complement operator)
a = np.array([np.nan, 1,2,np.nan,3,4,5])
print(a[~np.isnan(a)]) # [1. 2. 3. 4. 5.]

# filter out the non-complex elements from an array
a = np.array([1, 2+6j, 5, 3.5+5j])
print(a[np.iscomplex(a)]) # [2. +6.j 3.5+5.j]

'''
Broadcasting
-Ability of NumPy to treat arrays of different shapes during arithmetic operations.
-Arithmetic operations on arrays are usually done on corresponding elements.
-If two arrays are of exactly the same shape, then these operations are smoothly performed.

Broadcasting is possible if the following rules are satisfied −
-Array with smaller ndim than the other is prepended with '1' in its shape.
-Size in each dimension of the output shape is maximum of the input sizes in that dimension.
-An input can be used in calculation, if its size in a particular dimension matches the output size or its value is exactly 1.
-If an input has a dimension size of 1, the first data entry in that dimension is used for all calculations along that dimension.

A set of arrays is said to be broadcastable if the above rules produce a valid result and one of the following is true −
-Arrays have exactly the same shape.
-Arrays have the same number of dimensions and the length of each dimension is either a common length or 1.
-Array having too few dimensions can have its shape prepended with a dimension of length 1, so that the above stated property is true.
'''
a = np.array([[0.0,0.0,0.0],[10.0,10.0,10.0],[20.0,20.0,20.0],[30.0,30.0,30.0]])
b = np.array([1.0,2.0,3.0])
'''
First array:
[[ 0. 0. 0.]
 [ 10. 10. 10.]
 [ 20. 20. 20.]
 [ 30. 30. 30.]]

Second array:
[ 1. 2. 3.]

First Array + Second Array
[[ 1. 2. 3.]
 [ 11. 12. 13.]
 [ 21. 22. 23.]
 [ 31. 32. 33.]]
'''

# Iterating Over Array using numpy.nditer
a = np.arange(0,60,5)
a = a.reshape(3,4)
for x in np.nditer(a): # order of iteration is chosen to match the memory layout of the array (so transpose the array wont help, instead we can change the memory layout)
   print (x) # output 1

b = a.copy(order = 'F')  # we can change the memory layout of the array using order
for x in np.nditer(b):
   print (x) # output 2

for x in np.nditer(a, order = 'F'):  # or we can force nditer object to use a specific order by explicitly mentioning it
   print (x) # output 3
'''
Original array is:
[[ 0 5 10 15]
 [20 25 30 35]
 [40 45 50 55]]

Output 1:
0 5 10 15 20 25 30 35 40 45 50 55

Output 2:
0 20 40 5 25 45 10 30 50 15 35 55

Output 3:
0 20 40 5 25 45 10 30 50 15 35 55
'''

# Transposing an array
a=np.arange(9)
a.shape=(3,3)
a.T

# Modifying Array Values usinf op_flags parameter
a = np.arange(0,60,5)
a = a.reshape(3,4)
print (a) # output 1
for x in np.nditer(a, op_flags = ['readwrite']):
   x[...] = 2*x
print(a) # output 2
'''
Output 1:
[[ 0  5 10 15]
 [20 25 30 35]
 [40 45 50 55]]

Output 2:
[[  0  10  20  30]
 [ 40  50  60  70]
 [ 80  90 100 110]]
'''

# extracting one-dimensional arrays corresponding to each column
a = np.arange(0,60,5)
a = a.reshape(3,4)
print(a)
for x in np.nditer(a, flags = ['external_loop'], order = 'F'):
   print(x)
'''
output 1:
[[ 0  5 10 15]
 [20 25 30 35]
 [40 45 50 55]]

output 2:
[ 0 20 40]
[ 5 25 45]
[10 30 50]
[15 35 55]
'''

# Broadcasting Iteration
a = np.arange(0,60,5)
a = a.reshape(3,4)
print(a) # output 1
b = np.array([1, 2, 3, 4], dtype = int)
print(b) # output  2
for x,y in np.nditer([a,b]):
   print(x+y) # output 3
'''
output 1:
[[ 0  5 10 15]
 [20 25 30 35]
 [40 45 50 55]]

output 2:
[1 2 3 4]

output 3:
1 7 13 19 21 27 33 39 41 47 53 59
'''

# Array Manipulation
# Changing Shape
a = np.arange(8).reshape(2,4)  # reshape a 1x8 (1 row, 8 cols) array into a 2x4 array
a.flat[5] # 5 ; 1-D iterator over the array
a.flatten(order = 'F') # returns a copy of an array collapsed into one dimension
a.ravel(order = 'F') # Returns a contiguous flattened array / copy is made only if needed

# Transpose Operations
np.transpose(a) # same as a.T
a.T

# Changing Dimensions
a = np.arange(4).reshape(1,4)  # [0  1  2  3]
np.broadcast_to(a,(4,4))
'''
[[0  1  2  3]
 [0  1  2  3]
 [0  1  2  3]
 [0  1  2  3]]
'''

# Joining Arrays
a = np.array([[1,2],[3,4]])
'''First array:
[[1 2]
 [3 4]]'''
b = np.array([[5,6],[7,8]])
'''Second array:
[[5 6]
 [7 8]]'''

# concatenate joins a sequence of arrays along an existing axis
np.concatenate((a,b)) # by default it joins on axis=0, stacks horizontally - this is same as np.vstack((a,b))
'''Joining the two arrays along axis 0:
[[1 2]
 [3 4]
 [5 6]
 [7 8]]'''
np.concatenate((a,b),axis = 1) # this is same as np.hstack((a,b))
'''Joining the two arrays along axis 1:
[[1 2 5 6]
 [3 4 7 8]]'''

# stack joins a sequence of arrays along a new axis
np.stack((a,b),0)
'''Stack the two arrays along axis 0:
[[[1 2]
 [3 4]]
 [[5 6]
 [7 8]]]'''
np.stack((a,b),1)
'''Stack the two arrays along axis 1:
[[[1 2]
 [5 6]]
 [[3 4]
 [7 8]]]'''

# Splitting Arrays
a = np.arange(9)
# [0 1 2 3 4 5 6 7 8]
np.split(a,3) # Split the array in 3 equal-sized subarrays:
# [array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8])]
np.split(a,[4,7]) # Split the array at positions indicated in 1-D array:
# [array([0, 1, 2, 3]), array([4, 5, 6]), array([7, 8])]

a = np.arange(16).reshape(4,4)
'''
[[ 0 1 2 3]
 [ 4 5 6 7]
 [ 8 9 10 11]
 [12 13 14 15]]'''
np.hsplit(a,2) # Horizontal splitting
'''[array([[ 0,  1],
       [ 4,  5],
       [ 8,  9],
       [12, 13]]), array([[ 2,  3],
       [ 6,  7],
       [10, 11],
       [14, 15]])]'''
np.vsplit(a,2) # Vertical splitting
'''[array([[0, 1, 2, 3],
       [4, 5, 6, 7]]), array([[ 8,  9, 10, 11],
       [12, 13, 14, 15]])]'''

# Adding / Removing Elements
a = np.array([[1,2,3],[4,5,6]])
'''[[1 2 3]
 [4 5 6]]'''
np.resize(a,(3,3))
'''[[1 2 3]
 [4 5 6]
 [1 2 3]]''' # Observe that first row of a is repeated in b since size is bigger

a = np.array([[1,2,3],[4,5,6]])
'''[[1 2 3]
 [4 5 6]]'''
np.append(a, [7,8,9])  # Append elements to array - Axis parameter not passed. The input array is flattened before append
# [1 2 3 4 5 6 7 8 9]
np.append(a, [[7,8,9]],axis = 0)  # Append elements along axis 0
'''[[1 2 3]
 [4 5 6]
 [7 8 9]]'''
np.append(a, [[5,5,5],[7,8,9]],axis = 1) # Append elements along axis 1
'''[[1 2 3 5 5 5]
 [4 5 6 7 8 9]]'''

a = np.array([[1,2],[3,4],[5,6]])
'''[[1 2]
 [3 4]
 [5 6]]'''
np.insert(a,3,[11,12])  # Axis parameter not passed. The input array is flattened before insertion.
# [ 1 2 3 11 12 4 5 6]
np.insert(a,1,[11],axis = 0)  # Broadcast along axis 0:
'''[[ 1 2]
 [11 11]
 [ 3 4]
 [ 5 6]]'''
np.insert(a,1,11,axis = 1) # Broadcast along axis 1:
'''[[ 1 11 2]
 [ 3 11 4]
 [ 5 11 6]]'''

a = np.arange(12).reshape(3,4)
'''[[ 0 1 2 3]
 [ 4 5 6 7]
 [ 8 9 10 11]]'''
np.delete(a,5)  # no axis specified, so array flattened before delete operation as axis not used
[ 0 1 2 3 4 6 7 8 9 10 11]
np.delete(a,1,axis = 1) # Column 2 deleted
'''[[ 0 2 3]
 [ 4 6 7]
 [ 8 10 11]]'''
np.delete(a, np.s_[::2]) # A slice containing alternate values from array deleted:
# [ 2 4 6 8 10]

# unique function returns an array of unique elements in the input array
a = np.array([5,2,6,2,7,5,6,8,2,9])
np.unique(a)  # Unique values of first array
u,indices = np.unique(a, return_index = True) # Unique array and Indices array
u[indices]  # Reconstruct the original array using indices
u,cnt = np.unique(a,return_counts = True) # Return the count of repetitions of unique elements

# Mathematical/statistical funcitons
a = np.array([1.0,5.55, 123, 0.567, 25.532])
np.around(a)  # Default is 0. If negative, the integer is rounded to position to the left of the decimal point
np.around(a, decimals = 1)  # [   1.    5.6  123.    0.6  25.5]
np.around(a, decimals = -1) # [   0.    10.  120.    0.   30. ]

a = np.array([-1.7, 1.5, -0.2, 0.6, 10])
np.floor(a) # [ -2.   1.  -1.   0.  10.]
np.ceil(a)  # [ -1.   2.  -0.   1.  10.]

# Arithmetic functions
a = np.arange(9, dtype = np.float_).reshape(3,3)
'''[[ 0. 1. 2.]
 [ 3. 4. 5.]
 [ 6. 7. 8.]]'''
b = np.array([10,10,10])
# [10 10 10]
np.add(a,b) # Add the two arrays
[[ 10. 11. 12.]
 [ 13. 14. 15.]
 [ 16. 17. 18.]]
np.subtract(a,b) # Subtract the two arrays
[[-10. -9. -8.]
 [ -7. -6. -5.]
 [ -4. -3. -2.]]
np.multiply(a,b)  # Multiply the two arrays
[[ 0. 10. 20.]
 [ 30. 40. 50.]
 [ 60. 70. 80.]]
np.divide(a,b) # Divide the two arrays
[[ 0. 0.1 0.2]
 [ 0.3 0.4 0.5]
 [ 0.6 0.7 0.8]]

# Dot product
# For 2-D vectors, it is the equivalent to matrix multiplication.
# For 1-D arrays, it is the inner product of the vectors.
# For N-dimensional arrays, it is a sum product over the last axis of a and the second-last axis of b.
np.dot(a,b)
np.dot(b,a) == np.dot(a.T,b)

a=[1,2,3]
b=[2,3,1]
np.power(a,2) # array([1, 4, 9], dtype=int32)
np.power(a,b) # array([1, 8, 3], dtype=int32)

a = np.array([10,20,30])
b = np.array([3,5,7])
np.mod(a,b)  # same as np.remainder(a,b)
# array([1, 0, 2], dtype=int32)

a = np.array([[3,7,5],[8,4,3],[2,4,9]])
np.amin(a,1)  # array of min in each row
np.amin(a,0)  # array of min in each col
np.amin(a)    # overall min element

# ptp function returns the range (maximum-minimum) of values along an axis
a = np.array([[3,7,5],[8,4,3],[2,4,9]])
np.ptp(a)
np.ptp(a, axis = 1)
np.ptp(a, axis = 0)

# Percentile (or a centile) is a measure used in statistics indicating the value below which a given percentage of observations in a group of observations fall
a = np.array([[30,40,70],[80,20,10],[50,90,60]])
np.percentile(a,50)
# 50.0
np.percentile(a,50, axis = 1)  # 1 is rows
# [ 40. 20. 60.]
np.percentile(a,50, axis = 0)  # 0 is columns
# [ 50. 40. 60.]

# Median is defined as the value separating the higher half of a data sample from the lower half
a = np.array([[30,65,70],[80,95,10],[50,90,60]])
np.median(a) # 65.0
np.median(a, axis = 0)  # [ 50. 90. 60.]
np.median(a, axis = 1) # [ 65. 80. 60.]

# np.mean - syntax similar to np.median
# np.average function can be used to calculate weighted average if weights are specified, if they are not specified then this function is same as mean function
# In a multi-dimensional array, the axis for computation can be specified.
a = np.array([1,2,3,4])
np.average(a)
wts = np.array([4,3,2,1])
np.average(a,weights = wts)

# Standard deviation is the square root of the average of squared deviations from mean
# std = sqrt(mean(abs(x - x.mean())**2))
np.std([1,2,3,4])

# Variance is the average of squared deviations - mean(abs(x - x.mean())**2)
# In other words, the standard deviation is the square root of variance
np.var([1,2,3,4])

# sorting
# sort() function returns a sorted copy of the input array
a = np.array([[3,7],[9,1]])
'''[[3 7]
 [9 1]]'''
np.sort(a)
'''[[3 7]
 [1 9]]'''

# # Order parameter in sort function
dt = np.dtype([('name', 'S10'),('age', int)])
a = np.array([("raju",21),("anil",25),("ravi", 17), ("amar",27)], dtype = dt)
# [('raju', 21) ('anil', 25) ('ravi', 17) ('amar', 27)]
np.sort(a, order = 'name')
# [('amar', 27) ('anil', 25) ('raju', 21) ('ravi', 17)]

# numpy.argsort returns the array of indices of sorted array
# this indices array is used to construct the sorted array
x = np.array([3, 1, 2])
y = np.argsort(x)
x[y]  # Reconstruct original array in sorted order

# numpy.argmax() and numpy.argmin()
# return the indices of maximum and minimum elements along the given axis

# numpy.nonzero() function returns the indices of non-zero elements in the input array
# numpy.where(condition) function returns the indices of elements in an input array where the given condition is satisfied
# numpy.extract(condition) function returns the elements satisfying any condition

# id() returns a universal identifier of Python object, similar to the pointer in C
# While executing the functions, some of them return a copy of the input array, while some return the view
a
b=a # b.shape will also change the shape of a
# view - a different view of the same memory content
b=a.view() # b.shape will not change the shape of a, even though both a and b have same id()
# copy - When the contents are physically stored in another location
b=a.copy() # b.shape will not change the shape of a, as a and b have different id()

# MATRIX
# matrix is always two-dimensional, whereas ndarray is an n-dimensional array. Both the objects are inter-convertible.
i = np.matrix('1,2;3,4')
'''[[1  2]
 [3  4]]'''
j = np.asarray(i)
'''[[1  2]
 [3  4]]'''
k = np.asmatrix (j)
'''[[1  2]
 [3  4]]'''

# Matrix functions
np.matlib.empty((2,2)) # returns a new matrix without initializing the entries (it will be filled with random data)
np.matlib.zeros((2,2)) # returns the matrix filled with zeros
np.matlib.ones((2,2))
np.matlib.eye(n = 3, M = 4, k = 0, dtype = float) # returns a matrix with 1 along the diagonal elements and the zeros elsewhere
np.matlib.identity(5, dtype = float) # returns the Identity matrix of the given size. An identity matrix is a square matrix with all diagonal elements as 1
np.matlib.rand(3,3) # returns a matrix of the given size filled with random values

Python-Programming/04-numpy arrays and matrices.py


"""
Given the coordinates of four points in 2D space, return whether the four points could construct a square.

The coordinate (x,y) of a point is represented by an integer array with two integers.

Example:
Input: p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,1]
Output: True
Note:

All the input integers are in the range [-10000, 10000].
A valid square has four equal sides with positive length and four equal angles (90-degree angles).
Input points have no order.
"""
import numpy as np
import math
class Solution:
    def euclid(self,t1,t2):
        return math.sqrt(math.pow(t1[0]-t2[0],2)+math.pow(t1[1]-t2[1],2))
    def validSquare(self, p1, p2, p3, p4):
        """
        :type p1: List[int]
        :type p2: List[int]
        :type p3: List[int]
        :type p4: List[int]
        :rtype: bool
        """
        dist = list()
        dist.append(self.euclid(p1,p2))
        dist.append(self.euclid(p1,p3))
        dist.append(self.euclid(p1,p4))
        dist.append(self.euclid(p2,p3))
        dist.append(self.euclid(p2,p4))
        dist.append(self.euclid(p3,p4))
        set_temp = set(dist)
        if(len(set_temp)!=2):
            return False
        else:
            temp = list()
            for nums in set_temp:
                temp.append(nums)
            temp.sort()
            if round(math.sqrt(2)*temp[0],2) == round(temp[1],2):
                return True
            else:
                return False


from sympy import *

#define sympy symbols for
x , t , z , nu = symbols ('x t z nu')

#for pretty printing:
init_printing ( use_unicode=True)

#take a derivative of  [ sin (x) e ^ x ]
print (" Diffrentiating sin (x) e ^ x ")
print ( diff ( sin ( x ) * exp (x), x))

#integration:
print ( "\n Integrating the result:")
print (  integrate ( exp(x) * sin (x) + exp(x)*cos(x) , x) )

print("\n Integrating with limits   -oo to + oo ")
print ( integrate( sin(x**2) , ( x, -oo , oo ) ) )

print ("\n Finding the limit as x -> 0 " )
print ( limit (sin(x)/x, x, 0 ) )

print ("\n Solving an equation:")
print ( solve ( x ** 2 - 2, x) )

print ("\n Solving a diffrentaial equation:")
y = Function ( 'y' )
print ( dsolve( Eq( y(t).diff(t,t) - y(t), exp(t) ), y(t) ) )

print("\n Finding EigenValues:")
print ( Matrix( [ [1,2],[2,2] ] ).eigenvals() )

#to check mathematical equality, between two expressions say a and b
# if simplify(a-b) evaluates to 0 => a = b
x = symbols('x')
a = (x+1)**2
b = x**2 + 2*x + 1
print ( simplify (a-b) )


# GenerateCurve.py

import numpy as np
import sys

def getCurveNames() :
    return ("line","circle","ellipse","clover","heart","lissajous","figureight")

def generateCurve(argv) :
    nargs = len(argv)
    if nargs==1 :
        print('argument must be one of: ',getCurveNames())
        exit()
    if nargs<2 : raise Exception('must specify curve name')
    name = sys.argv[1]
    if name==str("line") :
         npts = 1000
         x = np.arange(npts,dtype="float64")
         y = np.arange(npts,dtype="float64")
         return {"x":x,"y":y,"name":name}
    if name==str("circle") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         t = np.arange(min, max, inc)
         x = np.cos(2*np.pi*t)
         y = np.sin(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("ellipse") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         t = np.arange(min, max, inc)
         a = 3.0
         if nargs>=3 : a = float(sys.argv[2])
         b = 2.0
         if nargs>=4 : b = float(sys.argv[3])
         x = a*np.cos(2*np.pi*t)
         y = b*np.sin(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("clover") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         nloops = 3
         if nargs>=3 : nloops = float(sys.argv[2])
         print('nloops=',nloops)
         t = np.arange(min, max, inc)
         x = np.sin(nloops*2*np.pi*t)*np.cos(2*np.pi*t)
         y = np.sin(nloops*2*np.pi*t)*np.sin(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("heart") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         t = np.arange(min, max, inc)
         x = (1.0 - np.cos(2*np.pi*t)*np.cos(2*np.pi*t))*np.sin(2*np.pi*t)
         y = (1.0 - np.cos(2*np.pi*t)*np.cos(2*np.pi*t)*np.cos(2*np.pi*t))*np.cos(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("lissajous") :
         min = 0.0
         max = 1.0
         npts = 4000
         inc = (max-min)/npts
         t = np.arange(min, max, inc)
         m = 3
         if nargs>=3 : m = float(sys.argv[2])
         n = 1
         if nargs>=4 : n = float(sys.argv[3])
         x = np.sin(n*2*np.pi*t)
         y = np.cos(m*2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("figureight") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         a = 1
         if nargs>=3 : a = float(sys.argv[2])
         print('a=',a)
         t = np.arange(min, max, inc)
         x = a*np.sin(2*np.pi*t)*np.cos(2*np.pi*t)
         y = a*np.sin(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    raise Exception(name + ' not implemented')

if __name__ == '__main__':
    curveData = generateCurve(sys.argv)
    print('name=',curveData["name"],' len(x)=',len(curveData["x"]),' len(y)=',len(curveData["y"]))


import numpy as np
from numpy.core.records import array

# Request the matrix from the user
A = [float(a) for a in input("Please, Enter the matrix row by row: ").split(" ")]
A_shape = [int(i) for i in input("Please, Enter the dimension of the previous matrix: ").split(" ")]
A = np.array(A).reshape(A_shape)

# Request the starting vector from the user
X = [float(a) for a in input("Please, Enter the starting vector: ").split(" ")]
X_shape = [int(i) for i in input("Please, Enter the dimension of the previous vector: ").split(" ")]
X = np.array(X).reshape(X_shape)

def PowerMethod(A, X, tolerance, ndigits, counter = np.inf):
    print('#' * 50, "The Power Method", '#' * 50)
    i = 1
    B = list()
    print("Iteration No.", 0)
    print(X.T, "---")
    print("Iteraion No.", 1)
    B.append(np.dot(A, X))
    print(B[0].T, "---")
    print("Iteration No.", 2)
    # min = np.amax([abs(b) for b in B[0]])
    # B[0] = np.array([b / min for b in B[0]])
    B.append(np.dot(A, B[0]))
    dominant_eigenvalue = round(np.amax([abs(b) for b in abs(B[1])]) / np.amax([abs(b) for b in abs(B[0])]), ndigits)
    print(B[1].T, dominant_eigenvalue)
    # min = np.amax([abs(b) for b in B[1]])
    # B[1] = np.array([b / min for b in B[1]])
    previous = -1
    while((abs(dominant_eigenvalue - previous) > tolerance or (dominant_eigenvalue == previous)) and i <= counter-2):
        B.append(np.dot(A, B[i]))
        previous = dominant_eigenvalue
        i += 1
        # min = np.amax([abs(b) for b in B[i]])
        # B[i] = np.array([b / min for b in B[i]])
        dominant_eigenvalue = round(np.amax([abs(b) for b in abs(B[i])]) / np.amax([abs(b) for b in abs(B[i - 1])]),ndigits)
        print("Iteration No.", i+1)
        print(B[i].T, dominant_eigenvalue)

    print('#' * 120)
    dominant_eignevector = np.round([(b / np.amin([abs(b) for b in abs(B[i])])) for b in B[i]],ndigits)

    return [dominant_eignevector, dominant_eigenvalue, i+1]

# Example 1: 2 1 1 1 2 1 1 1 2
# Example: 1 -1 0 -2 4 -2 0 -1 2
# PowerMethod(A, X, 0.0001, 5)
[dominant_eignevector, dominant_eigenvalue, i] = PowerMethod(A, X, 0.0001, 5)
print("The Dominant Eigenvalue =", dominant_eigenvalue)
print("The Dominant Eigenvector =", dominant_eignevector.T)
print("Total Number of iterations =", i)

Eigenvalue Approximation/Power Method with visualization.py


# skewness python
# https://www.google.com/search?q=skewness+python&oq=Skewness+python&aqs=chrome.0.0l4j0i22i30l6.3988j0j4&sourceid=chrome&ie=UTF-8
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.skew.html
# https://www.geeksforgeeks.org/scipy-stats-skew-python/

''' Statistical functions
In simple words, skewness is the measure of how much the
probability distribution of a random variable deviates
from the normal distribution.
# https://www.investopedia.com/terms/s/skewness.asp

skewness = 0 : normally distributed.
skewness > 0 : more weight in the left tail of the distribution.
skewness < 0 : more weight in the right tail of the distribution.

'''
# part 1 ----------------------------------
import numpy as np
from scipy.stats import skew
import pandas as pd

arr = np.random.randint(1, 10, 10)
arr = list(arr)
# print(arr)
# # more weight in the right when skew>0,
# # determine skew close enough to zero
# print(skew(arr))
# print(skew([1, 2, 3, 4, 5]))

# part 2 ----------------------------------
# df = pd.read_csv('Data/nba.csv')
df = pd.read_csv('Data/XAUNZD_Daily.csv')

# print(df.tail())

# skewness along the index axis
print(df.skew(axis=0, skipna=True))

# skewness of the data over the column axis
# print(df.skew(axis=1, skipna=True))


import numpy as np
import pandas as pd
from IPython.display import display
import matplotlib.pyplot as plt

n = 1000
mean = (-1, 2)
cov = [[10, 1], [1, 0.5]]
X = np.random.multivariate_normal(mean, cov, n)

df = pd.DataFrame(X)
# Pandas does the centering for us
df = df -df.mean()
print("Centered covariance with Pandas")
covarianceX = df.cov()

print(covarianceX)

# we center it ourselves
X_centered = X - X.mean(axis=0)
print("Centered covariance using numpy")
print(np.cov(X_centered.T))
# extract the relevant columns from the centered design matrix
x = X_centered[:,0]
y = X_centered[:,1]
Cov = np.zeros((2,2))
Cov[0,1] = np.sum(x.T@y)/(n-1.0)
Cov[0,0] = np.sum(x.T@x)/(n-1.0)
Cov[1,1] = np.sum(y.T@y)/(n-1.0)
Cov[1,0]= Cov[0,1]
print("Centered covariance using own code")
print(Cov)

plt.plot(x, y, 'x')
plt.axis('equal')
plt.show()

# diagonalize and obtain eigenvalues, not necessarily sorted
EigValues, EigVectors = np.linalg.eig(Cov)
# sort eigenvectors and eigenvalues
#permute = EigValues.argsort()
#EigValues = EigValues[permute]
#EigVectors = EigVectors[:,permute]
print("Eigenvalues of Covariance matrix")
for i in range(2):
    print(EigValues[i])
FirstEigvector = EigVectors[:,0]
SecondEigvector = EigVectors[:,1]
print("First eigenvector")
print(FirstEigvector)
print("Second eigenvector")
print(SecondEigvector)
#thereafter we do a PCA with Scikit-learn
from sklearn.decomposition import PCA
pca = PCA(n_components = 2)
X2Dsl = pca.fit_transform(X)
print("Eigenvector of largest eigenvalue")
print(pca.components_.T[:, 0])


### Computation on NumPy Arrays: Universal Functions
# Using NP arrays in the traditional iterative manner is slow
# Vectorized operations are preferred - generally implemented in ufuncs

# here's a traditional loop-based approach to operating on arr values to demonstrate
import numpy as np
np.random.seed(0)

def compute_reciprocals(values):
    output = np.empty(len(values))
    for i in range(len(values)):
        output[i] = 1.0 / values[i]
    return output

values = np.random.randint(1, 10, size=5)
# %timeit compute_reciprocals(values)

# now, consider a case with a much bigger array of values:
big_arr = np.random.randint(1, 100, size=1000000)
# %timeit compute_reciprocals(big_array)

# note that both ops are way slower than they should be

##############################
### UFuncs introduction
# Interface for statically typed, compiled routines are known as vectorized operations
# can be achieved by performing an operation on the array, which then is applied to each el
# Advantage of vectorized approach is it pushes loop into the compiled layer, rather than being interpreted piecewise

# sample 1: ufunc operation b/w Scalar type and Array
compute_reciprocals(big_arr)
1.0 / big_arr
# %timeit (1.0 / big_array)

# sample 2: ufunc operation b/w Array type and Array
np.arange(5) / np.arange(1, 6)

# sample 3: ufunc operation on Multi-dimensional array
x = np.arange(9).reshape((3,3))
2 ** x

# the Pythonic way :: replace loops with vectorized expressions like the above when possible

##############################
### Exploring additional UFuncs
# Two types, Unary ufuncs and Binary ufuncs

## Array Arithmetic
# NumPy ufuncs use simple arithmetic operators
x = np.arange(4)
x
x + 5
x - 5
x * 2
x / 2
x // 2  # floor division
-x
x ** 2
x % 2

# can also string together operations, standard operations order respected
-(0.5*x + 1) ** 2

# Arithmetic operators in this context are wrappers for specific NP funcs. I.e. + evaluates to add()
np.add(x, 2)    # long form
x + 2           # wrapper equivalent

# We also have Boolean and Bitwise operators translated to NP ufuncs. Covered in "2.6-boolMasking.py"

## Absolute Value
# np plays nice with python standard library absolute val function abs()
x = np.array([-2, -1, 0, 1, 2])
abs(x)
# np has its own ufunc np.absolute() --- also callable as np.abs()
np.absolute(x)
np.abs(x)

x = np.array([3 - 4j, 4 - 3j, 2 + 0j, 0 + 1j])  # complex nums example
abs(x)
np.abs(x)

## Trigonometric ufuncs
# we can start by defining an array of angles
theta = np.linspace(0, np.pi, 3)
theta
np.sin(theta)
np.cos(theta)
np.tan(theta)
# and inverse trig funcs
x = [-1, 0, 1]
np.arcsin(x)
np.arccos(x)
np.arctan(x)

# side-note: recall that doubles get 15 decimal digits of precision.
#            to get less messy output, must round.
#            NON-Ufunc:  np.round(np.sin(theta), decimals=15) # rounds to 15th (dec precision limit)
#                ufunc:  np.rint( np.sin(theta))              # rounds to 0 th decimal

## Exponents and logarithms
import math;
x = np.array([1, 2, 3])

math.e ** x
np.exp(x)

2 ** x
np.exp2(x)

3 ** x
np.power(3, x)

x = [1, 2, 4, 10]           # NOTE: x is a list --> cannot use ufunc operators like ** or +

np.log(x)                   # natural logarithm
np.log2(x)                  # base 2 logarithm
np.log10(x)                 # base 10 logarithm

# related exp/log funcs, good for precision on small input vals
x = [0, .001, .01, .1]
np.exp(x)
np.expm1(x)             # Calculates ``exp(x) - 1``
np.log1p(x)             # Calculates ``log(1 + x)``

## Specialized ufuncs
# some additional examples: hyperbolic trig funcs, bitwise arithmetic, comparison operators,
# radian > degree conversions, rounding/remainders, etc. See NumPy ufunc docs

# another source of ufuncs :: scipy.special
from scipy import special

# Gamma functions (generalized factorials) & related funcs
x = [1, 5, 10]
special.gamma(x)    # gamma
special.gammaln(x)  # ln|gamma
special.beta(x, 2)  # beta

# Error function (integral of Gaussian), complement, and inverse
x = np.array([0, 0.3, 0.7, 1.0])
special.erf(x)
special.erfc(x)
special.erfinv(x)

###################################
### Advanced UFunc Features
# 3 topics: Out, Aggregates, Outer Products

## Specifying output: using "out"
# We can directly specify mem location to store result of a calculation using "out". Imperative style -> huge efficiency gain
x = np.arange(5)
y = np.empty(5)
np.multiply(x, 10, out=y)

# Can also utilize this with array views
y = np.zeros(10);   y = np.power(2, x, out=y[::2])  # y[::2] is a size=5 View of y. Save result into that view.
y = np.zeros(10);   y = 2 ** x                      # Requires additional temp array + full copy

## Aggregates
# With reduce() method, we can repeatedly execute a given operation to all elements of the array
# returning a single scalar value
x = np.arange(1, 6)
np.add.reduce(x)
np.multiply.reduce(x)

# with accumulate() method, we reduce() while storing all intermediate outputs in array
np.add.accumulate(x)
np.multiply.accumulate(x)

# note that there are dedicated NumPy funcs for these particular cases
# np.sum, np.prod, np.cumsum, np.cumprod - respectively

## Outer products (generalized cross product)
# Any ufunc can compute the output of all pairs of two different inputs with outer()
x = np.arange(1, 6)
np.multiply.outer(x, x)

# ufunc.at() and ufunc.reduceat() will be covered later in Fancy Indexing, and build on this idea

# additional ufuncs available in the NumPy/SciPy docs


import numpy as np
import matplotlib.pyplot as plt

# Part A: Numerical Differentiation Closure
def numerical_diff(f,h):
    def inner(x):
        return (f(x+h) - f(x))/h
    return inner

# Part B:
f = np.log
x = np.linspace(0.2, 0.4, 500)
h = [1e-1, 1e-7, 1e-15]
y_analytical = 1/x
result = {}

for i in h:
    y = numerical_diff(f,i)(x)
    result[i] = y

# Plotting
plt.figure(figsize = (8,5))
plt.plot(x, y_analytical, 'x-', label='Analytical Derivative')
for i in h:
    plt.plot(x, result[i], label='Estimated derivative h = '+str(i))
plt.xlabel("X value")
plt.ylabel("Derivative Value at X")
plt.title("Differentiation Value at X on various h value")
plt.legend()

# Part C:
print("Answer to Q-a: When h value is 1e-7, it most closely approximates the true derivative. \n",
      "When h value is too small: The approximation is jumping around stepwise and not displaying a smooth curve approximation, it amplifies floating point errors in numerical operation such as rounding and division\n",
      "When h value is too large: The approximation is lower than the true value, it doesn't provide a good approximation to the derivative\n")
print("Answer to Q-b: Automatic differentiation avoids the problem of not choosing a good h value. \n"
      "The finite difference approach is quick and easy but suffers from accuracy and stability problems.\n"
      "Symbolic derivatives can be evaluated to machine precision, but can be costly to evaluate.\n"
      "Automatic differentiation (AD) overcomes both of these deficiencies. It is less costly than symbolic differentiation while evaluating derivatives to machine precision.\n"
      "AD uses forward or backward modes to differentiate, via Computational Graph, chain rule and evaluation trace.")

# Show plot
plt.show()
# plt.savefig('P1_fig.png')


#!/usr/bin/env python3
# coding: utf-8

import numpy as np
import mimetypes
import argparse
import matplotlib.pyplot as plt
import csv

def check_file_ext(filename):
    """
    Check file extension
    """
    if mimetypes.guess_type(filename)[0] != 'text/csv':
        raise argparse.ArgumentTypeError('wrong filetype or path')
    return filename

def normalize(x, min, max):
    """
    Data min-max normalization:
    """
    return (x - min) / (max - min)

def denormalize(x, min, max):
    """
    Data min-max denormalization:
    """
    return x * (max - min) + min

def estimate_price(mileage, theta0, theta1):
    """
    Estimate price function:
    """
    return theta0 + theta1 * mileage

def gradient_descent(X, Y, curr_t0, curr_t1, lr):
    """
    Gradient descent and its derivate
    """
    M = len(X)
    deriv_theta0 = 0
    deriv_theta1 = 0
    for i in range(M):
        deriv_theta0 += (1 / M) * ((curr_t0 + (curr_t1 * X[i])) - Y[i])
        deriv_theta1 += (1 / M) * (((curr_t0 + (curr_t1 * X[i])) - Y[i]) * X[i])
    tmp_theta0 = curr_t0 - lr * deriv_theta0
    tmp_theta1 = curr_t1 - lr * deriv_theta1
    return tmp_theta0, tmp_theta1

def cost_function(X, Y, theta0, theta1):
    """
    Evaluate the loss
    """
    M = len(X)
    err = 0.0
    for i in range(M):
        err += (Y[i] - estimate_price(X[i], theta0, theta1)) ** 2
    return err / M

def linear_regression(X, Y, lr, epochs):
    """
    The linear regression function to train the model
    """
    theta0 = 0
    theta1 = 0
    loss = []
    for _ in range(epochs):
        theta0, theta1 = gradient_descent(X, Y, theta0, theta1, lr)
        loss.append(cost_function(X, Y, theta0, theta1))
    return theta0, theta1, loss

def plot(x, y, t0, t1, loss):
    line_x = [0, 1]
    line_y = [(t1 * i) + t0 for i in line_x]
    # plt.figure(1)
    plt.subplot(211)
    plt.plot(loss)
    plt.xlabel('epochs')
    plt.ylabel('loss')
    plt.title('Loss')
    plt.grid(True)
    plt.subplot(212)
    plt.plot(line_x, line_y)
    plt.scatter(x, y, None, 'orange')
    plt.title('Mileage vs. Price')
    plt.xlabel('mileage')
    plt.ylabel('price')
    plt.grid(True)
    plt.subplots_adjust(top=0.92, bottom=0.10, left=0.10, right=0.95, hspace=0.5, wspace=0.35)
    plt.show()

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("filename", type=check_file_ext, help="CSV file path")
    args = parser.parse_args()
    data = np.genfromtxt(args.filename, delimiter=',', skip_header=1)
    X = data[:, 0]
    Y = data[:, 1]
    xmin = np.min(X)
    xmax = np.max(X)
    ymin = np.min(Y)
    ymax = np.max(Y)
    normalized_X = [normalize(x, xmin, xmax) for x in X]
    normalized_Y = [normalize(y, ymin, ymax) for y in Y]
    learning_rate = 0.01
    epochs = 10000
    theta0, theta1, loss = linear_regression(normalized_X, normalized_Y, learning_rate, epochs)
    plot(normalized_X, normalized_Y, theta0, theta1, loss)
    print('Final loss = ', loss[-1])
    with open('out.csv', mode='w') as out:
        out_writer = csv.writer(out, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
        out_writer.writerow(['theta0', 'theta1', 'xmin', 'xmax', 'ymin', 'ymax'])
        out_writer.writerow([theta0, theta1, xmin, xmax, ymin, ymax])

if __name__ == "__main__":
    main()


from sympy import Symbol, symbols
from sympy import simplify
from sympy import sin, cos, atan
from sympy.matrices import Matrix
from sympy import pprint
from sympy import factor,expand,diff
from sympy import sqrt
from sympy import latex
from sympy.vector import CoordSys3D

a,b,x,y,r,t,Vrr,Vrt,Vtt = symbols('a,b,x,y,r,t,V_rr,V_rt,V_tt', real=True)
s11,s12,s13,s14,s15,s21,s22,s23,s24,s25,s31,s32,s33,s34,s35,s41,s4,s43,s44,s45,s51,s52,s53,s54,s55 = symbols('s1:6(1:6)', real=True)
Vxx, Vxy, Vxz, Vyx, Vyy, Vyz, Vzx, Vzy, Vzz = symbols('V_x:z(x:z)', real=True)
x11, x12, x21, x22 = symbols('x_1:3(1:3)')
y11, y12, y21, y22 = symbols('y_1:3(1:3)')
(x1, x2) = symbols('x_1:3')
(y1, y2) = symbols('y_1:3')
(Vr1r1, Vr1r2, Vr2r1, Vr2r2) = symbols('V_r1:3(1:3)')
(Vt1t1, Vt1t2, Vt2t1, Vt2t2) = symbols('V_t1:3(1:3)')
(Vr1t1, Vr1t2, Vr2t1, Vr2t2) = symbols('V_r1:3t1:3')
(Vt1r1, Vt1r2, Vt2r1, Vt2r2) = symbols('V_t1:3r1:3')
(r1,r2) = symbols('r_1:3')

PRINT_LATEX = 1

def printCentral(expression):
  if PRINT_LATEX:
    pprint(latex(expression, mode='equation', mat_str='smallmatrix'))
  else:
    pprint(expression)

if PRINT_LATEX:
    print("\\documentclass[8pt]{article}")
    print("\\usepackage{amsmath}")
    print("\\usepackage[landscape]{geometry}")
    print("\\begin{document}")

# Let's study the simple case u = a*x + b*y, with Cov(x,y)
# Jacobian = [df/dx, df/dy] = [a, b]
# Hence the Cov(u) = J * Cov(x,y) * JT

J = Matrix([[diff(a*x+b*y, x), diff(a*x+b*y, y)]])
Cov_xy = Matrix([[Vxx, Vxy], [Vxy, Vyy]])
print("\nFor the case u = a*x + b*y we have a Cov(u):")
printCentral(expand(J * Cov_xy * J.T))

# Case of cartesian to polar transformation
# x = r cos(t) = f1
# y = r sin(t) = f2
# Rotation matrix R = [[cos(t), -sin(t)], [sin(t), cos(t)]]

V_xy = Matrix([[Vxx, Vxy], [Vxy, Vyy]])
J = Matrix([[cos(t), -sin(t)], [sin(t), cos(t)]])

print("\nFor the case of going from x,y to r,theta(t), using a Rotation")
printCentral(V_xy)

print("\nRotation Matrix:")
printCentral(J)

print("\nFinal(destination) covariance Matrix:")
printCentral(expand(J * V_xy * J.T))

# Case of cartesian to polar transformation
# x = r cos(t) = f1
# y = r sin(t) = f2
# Jacobian = [[df1/dr, df1/dt], [df2/dr, df2/dt]] = [[cos(t), -r*sin(t)], [sin(t), r*cos(t)]]

print("\n\n\nCase of cartesian to polar transformation")
Var_xy = Matrix([[Vxx, Vxy], [Vxy, Vyy]])
print("\nCartesian Variance:")
printCentral(Var_xy)

Jacobian_cart_to_polar = Matrix([[diff(r*cos(t),r) , diff(r*cos(t), t)], [diff(r*sin(t), r), diff(r*sin(t), t)]])
print("\nJacobian-cart-to-polar:")
printCentral(Jacobian_cart_to_polar)

print("\nPolar Variance:")
printCentral(expand(Jacobian_cart_to_polar * Var_xy * Jacobian_cart_to_polar))

# Case of polar coordinates to cartesian transtormation
# r = sqrt(x^2+y^2) = f1
# t = atan(y/x) = f2
# Jacobian = [[df1/dx, df1/dy], [df2/dx, df2/dy]] = [[x/r, y/r], [-y/r^2, x/r^2]]

print("\n\n\nCase of polar to cartesian transformation")
Var_rt = Matrix([[Vrr, Vrt], [Vrt, Vtt]])
print("\nPolar Variance:")
printCentral(Var_rt)

f1 = sqrt(x**2+y**2)
f2 = atan(y/x)
Jacobian_rad_to_cart = Matrix([[f1.diff(x).subs(x**2+y**2,r**2), f1.diff(y).subs(x**2+y**2,r**2)],
                               [factor(f2.diff(x)).subs(x**2+y**2,r**2), factor(f2.diff(y)).subs(x**2+y**2,r**2)]])
print("\nJacobian-rad-to-cart:")
printCentral(Jacobian_rad_to_cart)

print("\nCartesian Variance:")
printCentral(expand(Jacobian_rad_to_cart * Var_rt * Jacobian_rad_to_cart.T))

# Case of polar to cartesian coordinates, with several points using a block matrix.
# Input Cov is: [[Vr1r1, Vr1r2, Vr1t1, Vr1t2],
#                [Vr1r2, Vr2r2, Vr2t1, Vr2t2],
#                [Vt1r1, Vt1r2, Vt1t1, Vt1t2],
#                [Vt2r1, Vt2r2, Vt2t1, Vt2t2]]
#
# Also the Jacobian becomes block-ed
#
# Jacobian_block = [[x1/r1, 0, y1/r1, 0],
#                   [0, x2/r2, 0, y2/r2],
#                   [-y1/r1**2, 0, x1/r1**2, 0],
#                   [0, -y2/r2**2, 0, x2/r2**2]]

print("\n\n\nPolar to Cartesian transformation, using multiple points with block matrices.")

print("\nInput Covariance block matrix:")
Var_block = Matrix([[Vr1r1, Vr1r2, Vr1t1, Vr1t2],
                    [Vr1r2, Vr2r2, Vr2t1, Vr2t2],
                    [Vt1r1, Vt1r2, Vt1t1, Vt1t2],
                    [Vt2r1, Vt2r2, Vt1t2, Vt2t2]])
printCentral(Var_block)

print("\nJacobian block matrix:")
Jacobian_block = Matrix([[x1/r1, 0, y1/r1, 0],
                         [0, x2/r2, 0, y2/r2],
                         [-y1/r1**2, 0, x1/r1**2, 0],
                         [0, -y2/r2**2, 0, x2/r2**2]])
printCentral(Jacobian_block)

print("\nCartesian Covariance block matrix:")
printCentral(expand(Jacobian_block * Var_block * Jacobian_block.T))

# Now we compute the Jacobian for the s variable for the  line fit.

x0,y0,xi,yi,q,R = symbols('X_0, Y_0 x_i, y_i q R')
N = CoordSys3D('N')
o = x0*N.i + y0*N.j
tmp = xi*N.i + yi*N.j
a = -o
b = tmp - o

print("\n\n\nInput values:")
print("\na:")
printCentral(a)
print("\nb:")
printCentral(b)

c_ = expand((a ^ b) & N.k)
d_ = expand(a & b)
c2_d2 = factor(c_**2+d_**2)
k = -q*R/c2_d2

print("\ncross:")
printCentral(c_)

print("\ndot:")
printCentral(d_)

c2_d2 = factor(c_**2+d_**2)

km = k*(x0**2+y0**2)
print("\nkm:")
printCentral(km)

print("\nk:")
printCentral(k)
print("\nk also as:")
printCentral(k.subs(c2_d2, 'dot**2+cross**2'))
print("\ni.e. k:")
printCentral(k.subs(km,'km'))

s = -q * R * atan(c_/d_)
print("\ns:")
printCentral(s.subs(d_,'dot').subs(c_,'cross'))

# compute the Jacobian

Jacobian_s = Matrix([
    factor(diff(s,x0)).subs(k, 'k'),
    factor(diff(s,y0)).subs(k, 'k'),
    simplify(factor(diff(s,R))).subs(d_,'dot').subs(c_,'cross'),
    factor(diff(s,xi)).subs(km, 'km'),
    factor(diff(s,yi)).subs(-km, '-km')
    ])

print("\nJacobian [x0, y0, R, xi, yi]:")
printCentral(Jacobian_s)

print("\nNote that -(b_y - a_y)*dot + (b_x + a_x)*cross is:")
printCentral(factor(-(b.components[N.j] - a.components[N.j])*d_ + (b.components[N.i] + a.components[N.i])*c_))

print("\nNote that (b_x - a_x)*dot - (-a_y - b_y)*cross is:")
printCentral(factor((b.components[N.i] - a.components[N.i])*d_ - (-a.components[N.j] - b.components[N.j])*c_))

print("\nNote that -a_y*dot - a_x*cross is:")
printCentral(factor(-a.components[N.j]*d_ - a.components[N.i]*c_))

print("\nNote that a_x*dot-a_y*cross is:")
printCentral(factor(a.components[N.i]*d_ - a.components[N.j]*c_))

if PRINT_LATEX:
    print("\\end{document}")


# Cross entropy is one out of many possible loss functions (another
# popular one is SVM hinge loss). These loss functions are typically
# written as J(theta) and can be used within gradient descent, which
# is an iterative algorithm to move the parameters (or coefficients)
# towards the optimum values.

# For multi-class problem

# our y needs to be one-hot encoded
# apply softmax

# Average the losses over the entire test set, and you get the cross entropy loss.
import torch
import torch.nn as nn
import numpy as np

# squashes output from 0 to 1
def softmax(x):
    return np.exp(x) / np.sum(np.exp(x), axis = 0)

x = np.array([2.0, 1.0, 0.1])
output = softmax(x)
print('softmax numpy:', output)

x = torch.tensor([2.0, 1.0, 0.1])
output = torch.softmax(x, dim=0)
print(output)

# a lot of times softmax is used with cross entropy
# loss increases as predicted probabiliy diverges from true
# high cross entropy for very wrong prediction

def cross_entropy(actual, predicted):
    loss = -np.sum(actual * np.log(predicted))
    return loss

Y = np.array([1, 0, 0])

Y_pred_good = np.array([.7, .2, .1])
Y_pred_bad = np.array([.1, .3, .6])
l1 = cross_entropy(Y, Y_pred_good)
l2 = cross_entropy(Y, Y_pred_bad)
print(f'Loss1 numpy: {l1:.4f}')
print(f'Loss2 numpy: {l2:.4f}')


# Import the necessary modules.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# This script will demonstrate the usefulness of using the taylor expansion
# to approximate the local shape of a curve. We will look at a simple function,
# y = cos(x), but it may be useful for you to try more complicated functions.

# To start, we'll make an array of x values at which we will evaluate cos(x).
x = np.linspace(-4 * np.pi, 4 * np.pi, 500)
y = np.cos(x)

# Let's evaluat eh first handful of orders.
order_0  = np.ones_like(x)
order_2 = 1 - x**2 / np.math.factorial(2)
order_4 = order_2 + x**4 / np.math.factorial(4)
order_6 = order_4 - x**6 / np.math.factorial(6)

# Now let's plot all of them together.
plt.figure()
plt.plot(x, y, label='cos(x)')
plt.plot(x, order_0, label='0th order')
plt.plot(x, order_2, label='2nd order')
plt.plot(x, order_4, label='4th order')
plt.plot(x, order_6, label='6th order')

# Add the legend and labels.
plt.legend()
plt.xlabel('x')
plt.ylabel('y')

# Change the yrange so it's easier to see.
plt.ylim([-1.5, 1])
plt.show()

# With this, we can see that even the first few expansions are a great
# approximation of the actual behavior.


import numpy as np
from scipy import stats
import math

x = np.array([25,35,10,40,85,75,60,45,50])
y = np.array([63,68,72,62,65,46,51,60,55])
n = x.size

mean_x = np.mean(x)
mean_y = np.mean(y)
print("mean x : ",mean_x)
print("mean y : ",mean_y)

x_i= x - mean_x
y_i = y - mean_y
print("\nxi : ",x_i)
print("yi : ",y_i)

x_i_square = np.square(x_i)
y_i_square = np.square(y_i)
xi_yi = x_i * y_i
print("\nxi**2 : ",x_i_square)
print("yi**2 : ",y_i_square)
print("xi*yi : ",xi_yi)

sum_x_i = np.sum(x_i)
sum_y_i = np.sum(y_i)
sum_xi_square = np.sum(x_i_square)
sum_yi_square = np.sum(y_i_square)
sum_xi_yi = np.sum(xi_yi)

print("\nsum_x_i : ",sum_x_i)
print("sum_y_i : ",sum_y_i)
print("sum_xi_square : ",sum_xi_square)
print("sum_yi_square : ",sum_yi_square)
print("sum_xi_yi : ",sum_xi_yi)

coeff_numerator  = ((n*sum_xi_yi) - (sum_x_i*sum_y_i))
coeff_denominator_1 = math.sqrt((n*sum_xi_square) - math.pow(sum_x_i,2))
coeff_denominator_2 = math.sqrt((n*sum_yi_square) - math.pow(sum_y_i,2))

coeff = coeff_numerator/(coeff_denominator_1*coeff_denominator_2)
print("\n coefficent of corelation : ",coeff)


"""
Problem 7:

What is the 10001st prime number?
"""

import numpy as np

# We will solve this using the sieve of Eratosthenes
# The pseudocode can be found at https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes#Pseudocode

n = 200000
sieve = np.array([True for _ in range(n)])
sieve[0:2] = False
upper_bound = int(np.sqrt(n))

for i in range(2, upper_bound+1):
    if sieve[i]:
        j = i * i
        counter = 0
        while j < n:
            sieve[j] = False
            j = (i * i) + (counter * i)
            counter += 1

primes = np.where(sieve == True)[0]

# The solution is at index 10000 since we start at index 0
solution = primes[10000]
print('solution: ', solution)


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Mon Jan 17 15:05:05 2022

@author: bobrokerson

"""

# Рассмотрим все ту же функцию из задания по линейной алгебре:
# f(x) = sin(x / 5) * exp(x / 10) + 5 * exp(-x / 2), но теперь уже на промежутке [1, 30]
# В первом задании будем искать минимум этой функции на заданном промежутке с помощью scipy.optimize.
# Разумеется, в дальнейшем вы будете использовать методы оптимизации для более сложных функций,
# а f(x) мы рассмотрим как удобный учебный пример.
# Напишите на Питоне функцию, вычисляющую значение f(x) по известному x.
# Будьте внимательны: не забывайте про то, что по умолчанию в питоне целые числа делятся нацело, и о том,
# что функции sin и exp нужно импортировать из модуля math.

from math import sin, exp
import numpy as np
import matplotlib.pyplot as plt

def func(x):
    return sin(x / 5.) * exp(x / 10.) + 5. * exp(-x/ 2.)

xarr = np.arange(1., 31.)
print(xarr)
print("x:", xarr.shape)
yarr = np.array([func(x) for x in xarr])
print(yarr)
print("y:", yarr.shape)

plt.plot(xarr, yarr)
plt.grid(True)
plt.axis([0, 30, -15, 5])
plt.show()

# Изучите примеры использования  scipy.optimize.minimize в документации Scipy (см. "Материалы")
# Попробуйте найти минимум, используя стандартные параметры в функции  scipy.optimize.minimize
# (т.е. задав только функцию и начальное приближение). Попробуйте менять начальное приближение и изучить, меняется ли результат.
# Укажите в scipy.optimize.minimize в качестве метода BFGS (один из самых точных в большинстве случаев градиентных методов оптимизации),
# запустите из начального приближения x=2. Градиент функции при этом указывать не нужно – он будет оценен численно.
# Полученное значение функции в точке минимума - ваш первый ответ по заданию 1, его надо записать с точностью до 2 знака после запятой.

from scipy.optimize import minimize

minFuncVal = minimize(func, 5)
print("Min f(x): ", round(minFuncVal.fun,2), "x = ", minFuncVal.x)
print("Number: ", minFuncVal.nit)

minFuncVal2 = minimize(func, 2, method = 'BFGS')
print("Min f(x) BFGS: ", round(minFuncVal2.fun, 2), "for x = ", minFuncVal2.x)
print("Number: ", minFuncVal2.nit)

minValR1 = np.zeros((2))
minValR1[0] = round(minFuncVal2.fun, 2)
print(minValR1)

# Теперь измените начальное приближение на x=30. Значение функции в точке минимума - ваш второй ответ по заданию 1,
# его надо записать через пробел после первого, с точностью до 2 знака после запятой.

minFuncVal3 = minimize(func, 30, method = 'BFGS')
print("Min f(x) BFGS method: ", minFuncVal3, "for x = ", minFuncVal3.x)
print("Number: ", minFuncVal3)

minValR1[1] = round(minFuncVal3.fun, 2)
print(minValR1)

minValR1[1] = round(minFuncVal3.fun, 2)
print(minValR1)

with open("docvalue.txt", "w") as file:
    for item in minValR1:
        file.write(str(item) + ' ')


import numpy

def tridiag(a, b, c, d):
    """ solve the linear system Ax = d where A has the form:

          a_i x_{i-1} + b_i x_i + c_i x_{i+1} = d_i

        for i = 0, n-1 with a_0 = 0 and c_{n-1} = 0

        In matrix form, b is the main diagonal, a is the subdiagonal and
        c is the superdiagonal. """

    N = len(a)
    if not (len(b) == len(c) == len(d) == N):
        print "ERROR: vectors not the right size"
        return None

    # forward elimination
    cprime = numpy.zeros((N), dtype=a.dtype)
    dprime = numpy.zeros((N), dtype=a.dtype)

    cprime[0] = c[0]/b[0]
    dprime[0] = d[0]/b[0]

    for i in range(1,N-1):
        cprime[i] = c[i]/(b[i] - cprime[i-1]*a[i])
        dprime[i] = (d[i] - dprime[i-1]*a[i])/(b[i] - cprime[i-1]*a[i])

    dprime[N-1] = (d[N-1] - dprime[N-2]*a[N-1])/(b[N-1] - cprime[N-2]*a[N-1])

    # back substitution
    x = numpy.zeros((N), dtype=a.dtype)

    x[N-1] = dprime[N-1]
    for i in reversed(range(0,N-1)):
        x[i] = dprime[i] - cprime[i]*x[i+1]

    return x

def triMultAx(a, b, c, x):
    """ multiply the tridiagonal matrix A by vector x and return the
        product vector d """

    N = len(a)
    if not (len(b) == len(c) == len(x) == N):
        print "ERROR: vectors not the right size"
        return None

    d = numpy.zeros((N), dtype=a.dtype)

    d[0] = b[0]*x[0] + c[0]*x[1]
    for i in range(1,N-1):
        d[i] = a[i]*x[i-1] + b[i]*x[i] + c[i]*x[i+1]

    d[N-1] = a[N-1]*x[N-2] + b[N-1]*x[N-1]

    return d


import numpy as np
from math import *
from sympy import *
from scipy import interpolate
from scipy.misc import comb
from matplotlib import pyplot as plt

def bernstein_poly(i, n, t):
    """
     The Bernstein polynomial of n, i as a function of t
    """

    return comb(n, i) * ( t**(n-i) ) * (1 - t)**i

def bezier_curve(points, nTimes=1000):
    """
       Given a set of control points, return the
       bezier curve defined by the control points.

       points should be a list of lists, or list of tuples
       such as [ [1,1],
                 [2,3],
                 [4,5], ..[Xn, Yn] ]
        However, we aware that the function takes in input as they come. So, if the points
        are not ordered correctly, you will get incorrect results.
        Should be: [ [x1,y1],
                     [x2,y2],
                     ........[xn,yn] ]
        nTimes is the number of time steps, defaults to 1000

    """

    nPoints = len(points)
    xPoints = np.array([p[0] for p in points])
    yPoints = np.array([p[1] for p in points])

    t = np.linspace(0.0, 1.0, nTimes)

    polynomial_array = np.array([ bernstein_poly(i, nPoints-1, t) for i in range(0, nPoints)   ])

    xvals = np.dot(xPoints, polynomial_array)
    yvals = np.dot(yPoints, polynomial_array)

    return xvals, yvals, xPoints, yPoints

def bezier_spline(xPoints, yPoints, numPoints):
    t = symbols('t')
    if numPoints <= 3:
        print("Enter atleast two control point and one pass through point!\n")
    elif numPoints == 4:
        bx = 3 * (xPoints[1] - xPoints[0])
        cx = 3 * (xPoints[2] - xPoints[1]) - bx
        dx = xPoints[3]-xPoints[0]-bx-cx
        by = 3 * (yPoints[1] - yPoints[0])
        cy = 3 * (yPoints[2] - yPoints[1]) - by
        dy = (yPoints[3]-yPoints[0])-by-cy
        print('x(t) = ' + latex(xPoints[0] + bx * t + cx * t ** 2 + dx * t ** 3))
        print('y(t) = ' + latex(xPoints[0]+by*t+cy*t**2+dy*t**3))
    else:
        print("This is an invalid entry. Sorry, please try again.\n")

# understand  you must enter 4 points.
# if you do not enter 4 points the function will
# reject. If you have equations to enter, please enter the points
# in the form
def bezier(user_input, n):
    point = '(';
    foundPoint = False;
    userString = str(user_input);
    try:
        if userString.find(point) != -1:
            foundPoint = True;
        if(foundPoint):
            points = np.array(user_input)
            xvals, yvals, xPoints, yPoints = bezier_curve(user_input, nTimes=1000)
            bezier_spline(xPoints, yPoints, n)
            plt.plot(xvals, yvals)
            plt.plot(xPoints, yPoints, "ro")
            for nr in range( len(user_input)):
                plt.text(xPoints[nr], yPoints[nr], nr)

            plt.show()
        else:
            length = len(user_input)
            xPoints = np.array(user_input[0])
            yPoints = np.array(user_input[1])
            if length == 3:
                zPoints = np.array(user_input[2])
                x1,y1,z1 = xPoints[0],yPoints[0],zPoints[0]
                bx = xPoints[1]
                x2 = eval('(bx+3*x1)/3')
                cx = xPoints[2]
                x3 = eval('(cx+3*x2+bx)/3')
                dx = xPoints[3]
                x4 = eval('dx+x1+bx+cx')
                by = yPoints[1]
                y2 = eval('(by+3*y1)/3')
                cy = yPoints[2]
                y3 = eval('(cy+3*y2+by)/3')
                dy = yPoints[3]
                y4 = eval('dy+y1+by+cy')
                bz = zPoints[1]
                z2 = eval('(bz+3*z1)/3')
                cz = zPoints[2]
                z3 = eval('(cz+3*z2+bz)/3')
                dz = zPoints[3]
                z4 = eval('dz+z1+bz+cz')
                print('Knots: (' + str(x1) + ',' + str(y1) + ',' + str(z1) + ')'+','\
                '(' + str(x4) + ',' + str(y4) + ',' + str(z4) + ')')
                print('Control Points: (' + str(x2) + ',' + str(y2) + ',' + str(z2) + ')'+','\
                '(' + str(x4) + ',' + str(y3) + ',' + str(z3) + ')')
            elif length == 2:
                x1, y1 = xPoints[0], yPoints[0]
                bx = xPoints[1]
                x2 = eval('(bx+3*x1)/3')
                cx = xPoints[2]
                x3 = eval('(cx+3*x2+bx)/3')
                dx = xPoints[3]
                x4 = eval('dx+x1+bx+cx')
                by = yPoints[1]
                y2 = eval('(by+3*y1)/3')
                cy = yPoints[2]
                y3 = eval('(cy+3*y2+by)/3')
                dy = yPoints[3]
                y4 = eval('dy+y1+by+cy')
                print('Knots: (' + str(x1) + ',' + str(y1) + ')' + ',' + '(' + str(x4) + ',' +
                str(y4) + ')')
                print('Control Points: (' + str(x2) + ',' + str(y2) +')' + ','+ '(' + str(x4) + ',' + str(
                    y3) + ')')
            else:
                print("Sorry, you can only use a maximum of 3 equations\n")

    except:
        e = sys.exc_info()[0]
        write_to_page("
Error: %s
" % e)
        print('Sorry, you have not entered correct input\n')
        return -1

def main():
    # nPoints = 4
    # points = np.random.rand(nPoints,2)*200
    nPoints = 4
    points = [[1,0,6,2],[1,-1,0,1],[1,1,6,0]]
    bezier(points,nPoints)

if __name__ == '__main__':
    main()


import numpy
import scipy.linalg
import matplotlib.pyplot as plt

def cspline(x, y, x_interp, B0, Bk):
    # h_i, v_i, d_i, i = 0, 1, ..., k - 1:
    h = x[1:] - x[:-1]
    v = y[1:] - y[:-1]
    d = v / h

    # 1 / h_i, i = 0, 1, ..., k - 1:
    off_diag = 1.0/h
    # 2 * ( 1 / h_i + 1 / h_{i + 1} ), i = 0, 1, ..., k - 2
    diag = 2.0*(off_diag[1:] + off_diag[:-1])
    # 1 / h_i, i = 1, 2, ..., k - 2:
    off_diag = off_diag[1:-1]

    # Compact tridiagonal hermitian matrix:
    A = numpy.zeros((2, diag.size))
    A[0,1:] = off_diag[:]
    A[1,:] = diag

    # Right-hand side:
    z = d/h
    b = 3.0 * (z[:-1] + z[1:])
    b[0] = b[0] - B0/h[0]
    b[-1] = b[-1] - Bk/h[-1]

    # B-coefficients:
    B = numpy.zeros( (b.size + 2, ))
    B[1:-1] = scipy.linalg.solve_banded(A, b)
    B[0] = B0
    B[-1] = Bk

    # C-coefficients:
    C = (3.0*d - 2.0*B[:-1] - B[1:])/h
    # D-coefficients:
    D = (B[:-1] + B[1:] - 2.0*d)/(h**2)

    # Interpolate:
    y_interp = numpy.zeros(x_interp.shape)
    k = 0
    for i in range( x_interp.size ):
        # Find appropriate segment:
        while k < C.size and x_interp[i] > x[k + 1]:
            k = k + 1
        # Compute cubic polinomial:
        h_x = x_interp[i] - x[k]
        y_interp[i] = y[k] + B[k]*h_x + C[k]*(h_x**2) + D[k]*(h_x**3)

    return y_interp

def runge(x):
    return 1.0/(1.0 + 25.0 * x**2)

# 1-) splines naturais: s'(x0) = B0 = 0 e s'(xk) = Bk = 0

B0 = 0
Bk = 0

ek = []
for k in range(1,15):
    x_interp = numpy.linspace(-1.1,1.1,100)

    x = numpy.zeros(k+1)
    for i in range(k+1):
        x[i] = -1 + 2*i/k

    sk = cspline( x, runge(x), x_interp, B0 , Bk )
    plt.plot(x_interp, runge(x_interp), 'r')
    plt.plot(x_interp, sk, 'b')
    plt.plot(x, runge(x), 'ko')
    plt.show()

    ek.append(numpy.amax(numpy.absolute(numpy.subtract(runge(x_interp), sk))))


'''
Library of quantized and non-quantized activation functions.
'''

import numpy as np

def tanh_activation(x):
    '''Tanh activation function.

    Parameters
    ----------
    x : float
        Value of the input pre-activation.

    Returns
    -------
    float
        The corresponding Tanh(x) value.

    '''
    return np.tanh(x)

def sigmoid_activation(x):
    r'''Sigmoid activation function defined as:

    .. math::
        y = \frac{1}{1+e^{-x}}

    Parameters
    ----------
    x : float
        Value of the input pre-activation.

    Returns
    -------
    float
        The result of the Sigmoid function.
    '''
    return 1 / (1 + np.exp(-x))

def sigmoid_tanh_activation(x):
    r'''A Sigmoid activation defined as:

    .. math::
        y = \frac{\tanh\left(\frac{x}{2}+1\right)}{2}

    Parameters
    ----------
    x : float
        Value of the input pre-activation.

    Returns
    -------
    float
        The result of the Sigmoid function.
    '''
    return (np.tanh(x / 2) + 1) / 2

def fast_sigmoid_activation(x):
    r'''Fast Sigmoid activation function defined as:

    .. math::
        y = \frac{x}{1+\lvert x \rvert}

    It can be used to reduce the amount of hardware resources needed
    to implement a Sigmoid function.

    Parameters
    ----------
    x : float
        Value of the input pre-activation.

    Returns
    -------
    float
        The result of the fast Sigmoid.
    '''
    return x / (1 + np.abs(x))

def relu_activation(x):
    '''Rectifier Linear Unit activation function.

    Parameters
    ----------
    x : float
        Value of the input pre-activation.

    Returns
    -------
    float
        Returns ``x`` if ``x`` is larger or equal to 0. Otherwise 0 is returned.
    '''
    return x * (x > 0)

def approximate_sigmoid_activation(x, alpha=0.001):
    r'''Approximate version of the Fast Sigmoid activation function defined as:

    .. math::
        y = 0.5 \frac{x \cdot \alpha}{1+\lvert x \cdot \alpha \rvert} + 0.5

    Parameters
    ----------
    x : float
        Value of the input pre-activation
    alpha : float, optional
        Noise factor, defaults to 0.001

    Returns
    -------
    float
        The result of the approximate Sigmoid.
    '''
    return 0.5 * (x * alpha / (1 + np.abs(x * alpha))) + 0.5

def ramp_activation(x, bound=1):
    '''A modified ReLU activation where the maximum (minimum) value
    for the input is capped at a specified value.

    Parameters
    ----------
    x : numpy.ndarray
        The input tensor
    bound : int, optional
        Upper bound of the activation, by default 1

    Returns
    -------
    numpy.ndarray
        A tensor having the same shape of the input tensor, where
        values are the result of the ramp activation.
    '''
    return np.maximum(-bound, np.minimum(bound, x))


# https://docs.sympy.org/latest/tutorial/matrices.html

from sympy import *
init_printing(use_unicode=False)
from myprint import spprint

spprint(Matrix([[1, -1], [3, 4], [0, 2]]))

spprint(Matrix([1, 2, 3]))

M = Matrix([[1, 2, 3], [3, 2, 1]])
N = Matrix([0, 1, 1])

spprint(M)
spprint(N)
spprint(M*N)

# shape

M = Matrix([[1, 2, 3], [-2, 0, 4]])
spprint(M)

# cannot find shape - is it in sympy-1.8
spprint(shape(M))

# rows and columns

spprint(M.row(0))
spprint(M.col(-1))

M.col_del(0)
spprint(M)

M.row_del(1)
spprint(M)

M = M.row_insert(1, Matrix([[0, 4]]))
spprint(M)

M = M.col_insert(0, Matrix([1, -2]))
spprint(M)

# math et al

M = Matrix([[1, 3], [-2, 3]])
N = Matrix([[0, 3], [0, 7]])
spprint(M + N)
spprint(M*N)
spprint(3*M)
spprint(M**2)
spprint(M**-1)

# this gets error:
# sympy.matrices.common.NonInvertibleMatrixError: Matrix det == 0; not invertible.
#spprint(N**-1)

# transpose

M = Matrix([[1, 2, 3], [4, 5, 6]])
spprint(M)

spprint(M.T)

# constructors

spprint(eye(3))

spprint(eye(4))

spprint(zeros(2, 3))

spprint(ones(3, 2))

spprint(diag(1, 2, 3))

spprint(diag(-1, ones(2, 2), Matrix([5, 7, 5])))

# advanced

M = Matrix([[1, 0, 1], [2, -1, 3], [4, 3, 2]])
spprint(M)

spprint(M.det())

M = Matrix([[1, 0, 1, 3], [2, 3, 4, 7], [-1, -3, -3, -4]])
spprint(M)

spprint(M.rref())

M = Matrix([[1, 2, 3, 0, 0], [4, 10, 0, 0, 1]])

spprint(M)

spprint(M.nullspace())

M = Matrix([[1, 1, 2], [2 ,1 , 3], [3 , 1, 4]])

spprint(M)

spprint(M.columnspace())

M = Matrix([[3, -2,  4, -2], [5,  3, -3, -2], [5, -2,  2, -2], [5, -2, -3,  3]])

spprint(M)

spprint(M.eigenvals())

spprint(M.eigenvects())

# diagonalize

spprint(M)

P, D = M.diagonalize()

spprint(P)

spprint(D)

spprint(P*D*P**-1)

print(P*D*P**-1 == M)

lamda = symbols('lamda')

p = M.charpoly(lamda)

spprint(p)

spprint(factor(p.as_expr()))

# zero testing

from sympy import *
q = Symbol("q", positive = True)
m = Matrix([
[-2*cosh(q/3),      exp(-q),            1],
[      exp(q), -2*cosh(q/3),            1],
[           1,            1, -2*cosh(q/3)]])

spprint(m.nullspace())

import warnings
def my_iszero(x):
    try:
        result = x.is_zero
    except AttributeError:
        result = None

    # Warnings if evaluated into None
    if result is None:
        warnings.warn("Zero testing of {} evaluated into None".format(x))
    return result

spprint(m.nullspace(iszerofunc=my_iszero))

def my_iszero(x):
    try:
        result = x.rewrite(exp).simplify().is_zero
    except AttributeError:
        result = None

    # Warnings if evaluated into None
    if result is None:
        warnings.warn("Zero testing of {} evaluated into None".format(x))
    return result

spprint(m.nullspace(iszerofunc=my_iszero))


from typing import List

import numpy as np

from Common.Primes import sieve_of_atkin

def divisors(number: int) -> List[int]:
    """
    This function calculates and returns a list of all divisors that a given number has, e.g.
    f(number=24) -> [1, 2, 3, 4, 6, 8, 12, 24]
    :param number: The number whose divisors are to be calculated
    :return: A list of integer divisors
    """
    if number < 1:
        raise ValueError(f"The input number must be greater than one ({number} !> 1).")
    if number == 1:
        return [1]

    divisor_list = set()
    sqrt_ceil = int(np.floor(np.sqrt(number)) + 1)
    for i in range(1, sqrt_ceil):
        if number % i == 0:
            divisor_list.add(i)
            divisor_list.add(int(number/i))

    return sorted(list(divisor_list))

def prime_factorization(number: int) -> List[int]:
    """
    This function calculates and returns the prime factorization of an input number. This algorithm doesn't condense
    factor multiples, e.g.
    f(number=24) -> [2, 2, 2, 3] (and not [2, 3])
    :param number: The number whose prime factors are to be calculated
    :return: A list of integer prime factors
    """
    if number <= 1:
        return [number]

    factors_list = []
    primes_list = sieve_of_atkin(number)

    for prime in primes_list:
        while number % prime == 0:
            factors_list += [prime]
            number /= prime

    if int(number) != 1:
        raise ArithmeticError(f"The prime factorization for the {number}; the number, {number} != 1")
    else:
        return factors_list


'''
Objective
In this challenge, we practice calculating the mean, median, and mode. Check out the Tutorial tab for learning materials and an instructional video!

Task
Given an array, , of  integers, calculate and print the respective mean, median, and mode on separate lines. If your array contains more than one modal value, choose the numerically smallest one.

Note: Other than the modal value (which will always be an integer), your answers should be in decimal form, rounded to a scale of  decimal place (i.e., ,  format).

Input Format

The first line contains an integer, , denoting the number of elements in the array.
The second line contains  space-separated integers describing the array's elements.

Constraints

, where  is the  element of the array.
Output Format

Print  lines of output in the following order:

Print the mean on a new line, to a scale of  decimal place (i.e., , ).
Print the median on a new line, to a scale of  decimal place (i.e., , ).
Print the mode on a new line; if more than one such value exists, print the numerically smallest one.
Sample Input

10
64630 11735 14216 99233 14470 4978 73429 38120 51135 67060
Sample Output

43900.6
44627.5
4978
Explanation

Mean:
We sum all  elements in the array, divide the sum by , and print our result on a new line.

Median:
To calculate the median, we need the elements of the array to be sorted in either non-increasing or non-decreasing order. The sorted array . We then average the two middle elements:

and print our result on a new line.
Mode:
We can find the number of occurrences of all the elements in the array:

 4978 : 1
11735 : 1
14216 : 1
14470 : 1
38120 : 1
51135 : 1
64630 : 1
67060 : 1
73429 : 1
99233 : 1
Every number occurs once, making  the maximum number of occurrences for any number in . Because we have multiple values to choose from, we want to select the smallest one, , and print it on a new line.
'''

# Enter your code here. Read input from STDIN. Print output to STDOUT
import numpy as np
from scipy import stats

n = int(input())
x = list(map(int, input().split()))

x = np.array(x)

print(round(np.mean(x),1))
print(round(np.median(x),1))
print(int(stats.mode(x)[0]))

10 Days of Statistics/Day 0/Mean-Median-Mode.py


# %% [markdown]
# # Least squares
# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/YoniChechik/AI_is_Math/blob/master/c_04a_curve_fitting/least_squares.ipynb)
# ## Linear LS
# Let's generate some noisy data
# %%
import numpy as np
import matplotlib.pyplot as plt

figsize = (10, 10)

np.random.seed(123)

# %%
x_max = 10
x_step = 0.01
x = np.arange(0, x_max, x_step)
y = 3 * x - 3

# add noise to data
std = 1
y = y + np.random.normal(scale=std, size=x.shape)

plt.figure(figsize=figsize)
plt.plot(x, y, "*")
plt.title("noisy data")
plt.show()

# %% [markdown]
# ### calc LS matrices and result:
# $$ Xb = y $$
# %%
x_vec = x.reshape(-1, 1)
X = np.concatenate((x_vec, np.ones(x_vec.shape)), axis=1)
print(X.shape)
y_vec = y.reshape(-1, 1)

# %%
b = np.linalg.inv(X.T @ X) @ X.T @ y_vec
print(b)

# plot fit results
plt.figure(figsize=figsize)
plt.plot(x, y, "*")
plt.plot(x, b[0] * x + b[1], "r")
plt.title("noisy data + best LS fit. $b^T$=" + str(b.T))
plt.show()

# %% [markdown]
# ## Comparison to np.linalg.lstsq
# %%
b_np = np.linalg.lstsq(X, y_vec, rcond=None)[0]
mse = np.mean((b - b_np) ** 2)
print(mse)
# %% [markdown]
# ## vertical dataset
# As mentioned in the lecture, LS is not goog at fitting vertical dataset.
# Let's generate data:
# %%
data_sz = 100
x = np.ones(data_sz)
y = np.arange(data_sz)

# add noise to data
std = 0.1
x = x + np.random.normal(scale=std, size=x.shape)

plt.figure(figsize=figsize)
plt.plot(x, y, "*")
axes = plt.gca()
axes.set_xlim([-3, 3])
plt.title("noisy vertical data")
plt.show()

# %% [markdown]
# Calc LS matrices and result
# %%
x_vec = x.reshape(-1, 1)
X = np.concatenate((x_vec, np.ones(x_vec.shape)), axis=1)
y_vec = y.reshape(-1, 1)

# %%
b = np.linalg.inv(X.T @ X) @ X.T @ y_vec
print(b)

# plot fit results
plt.figure(figsize=figsize)
plt.plot(x, y, "*")
x_axis = np.arange(3)
plt.plot(x_axis, b[0] * x_axis + b[1], "r")
plt.title("vertical data + best LS fit. $b^T$=" + str(b.T))
plt.show()

# %% [markdown]
# ## TLS
# Same vertical data, now with total least squares
# %%
X = np.concatenate((x.reshape(-1, 1) - np.mean(x), y.reshape(-1, 1) - np.mean(y)), axis=1)

def linear_tls(X):
    w, v = np.linalg.eig(X.T @ X)
    return v[:, np.argmin(w)]

tls_res = linear_tls(X)

a = tls_res[0]
b = tls_res[1]

c = -a * np.mean(x) - b * np.mean(y)
x_fit = np.array([x.min(), x.max()])
y_fit = -a / b * x_fit - c / b

plt.figure(figsize=figsize)
plt.plot(x, y, "*")
plt.plot(x_fit, y_fit)
axes = plt.gca()
axes.set_xlim([-3, 3])
axes.set_ylim([0, 100])
plt.title("noisy vertical data + linear TLS fit")
plt.show()

# %% [markdown]
# ## Example for non linear LS
# %%
x_step = 0.01
x = np.arange(-10, 10 + x_step, x_step)

y = 0.5 * x ** 2 + 2 * x + 5

# add noise to data
std = 5
y = y + np.random.normal(scale=std, size=y.shape)

plt.figure(figsize=figsize)
plt.plot(x, y, "*")
plt.title("noisy parabola data")
plt.show()

# %%
# calc LS matrices
x_vec = x.reshape(-1, 1)
X = np.concatenate((x_vec ** 2, x_vec, np.ones(x_vec.shape)), axis=1)
y_vec = y.reshape(-1, 1)

b = np.linalg.lstsq(X, y_vec, rcond=None)[0]
print(b)
# %%
plt.figure(figsize=figsize)
plt.plot(x, y, "*")
plt.plot(x, b[0] * x ** 2 + b[1] * x + b[2], "r")
plt.title("data + best LS fit. $b^T$=" + str(b.T))
plt.show()

# %% [markdown]
# ## Outliers
# As mentioned, LS has a problem with outliers:
# %%
x_max = 10

x = np.arange(10)
y = 4 * x + 2

# let's change the last data point
y[-1] -= 20

plt.figure(figsize=figsize)
plt.plot(x, y, "*")
plt.title("data with outlier")
plt.show()

# %%
# calc LS matrices
x_vec = x.reshape(-1, 1)
X = np.concatenate((x_vec, np.ones(x_vec.shape)), axis=1)
y_vec = y.reshape(-1, 1)

# %%
b = np.linalg.inv(X.T @ X) @ X.T @ y_vec
print(b)

# plot fit results
plt.figure(figsize=figsize)
plt.plot(x, y, "*")
x_axis = np.arange(x_max)
plt.plot(x_axis, b[0] * x_axis + b[1], "r")
plt.title("data with outlier + best LS fit. $b^T$=" + str(b.T))
plt.show()

# %% [markdown]
# ## RANSAC
# ### arrange data
# %%

x = np.arange(0, 10, 0.1)
y = 3 * x - 3

# add noise to data
std = 1
y = y + np.random.normal(scale=std, size=x.shape)

# add random noise unrelated to noisy line
noise_sz = int(x.shape[0] * 1)
x_noise = np.random.uniform(x.min(), x.max(), size=noise_sz)
y_noise = np.random.uniform(y.min(), y.max(), size=noise_sz)

plt.figure(figsize=figsize)
plt.plot(x, y, "*")
plt.plot(x_noise, y_noise, "*")
plt.show()

# %%
x = np.concatenate((x, x_noise))
y = np.concatenate((y, y_noise))

# %% [markdown]
# ### Run RANSAC
# %%

def basic_ransac(x, TH):
    # ====== choose 2 random inds
    rand_indices = np.random.choice(x.shape[0], size=2)

    # ====== build LS data:
    x_vec = x[rand_indices].reshape(-1, 1)
    X = np.concatenate((x_vec, np.ones(x_vec.shape)), axis=1)
    y_vec = y[rand_indices].reshape(-1, 1)

    b = np.linalg.lstsq(X, y_vec, rcond=None)[0].flatten()

    # ====== build fitted line
    line_p1 = np.array([x.min(), b[0] * x.min() + b[1]])
    line_p2 = np.array([x.max(), b[0] * x.max() + b[1]])
    inliers_ind = []

    # ====== distance of fit line from each sample to determine inliers
    for j in range(x.shape[0]):
        p_j = np.array([x[j], y[j]])

        # https://en.wikipedia.org/wiki/Cross_product#Geometric_meaning
        # |a X b| = |a||b|sin(t) -> |a X b|/|b| = |a|sin(t)
        d_j = np.linalg.norm(np.cross(line_p1 - p_j, line_p2 - line_p1)) / np.linalg.norm(line_p2 - line_p1)
        if d_j <= TH:
            inliers_ind.append(j)

    inliers_ind = np.array(inliers_ind)
    return b, inliers_ind

# %%
TH = 1
num_cycles = 10

num_best_inliers = 0
best_cycle_ind = -1
inliers_ind_list = []
b_list = []

for i in range(num_cycles):
    b, inliers_ind = basic_ransac(x, TH)
    inliers_ind_list.append(inliers_ind)
    b_list.append(b)

    # ====== save best model
    if num_best_inliers < inliers_ind.shape[0]:
        num_best_inliers = inliers_ind.shape[0]
        best_cycle_ind = i
# %%
# plot best fit
plt.rcParams["figure.figsize"] = [20, 20]

for i in range(num_cycles):
    plt.subplot(int(num_cycles / 2), 2, i + 1)
    ax = plt.gca()
    x_axis = np.arange(11)
    ax.plot(x_axis, b_list[i][0] * x_axis + b_list[i][1], "r")
    ax.plot(x, y, "*")
    ax.plot(x[inliers_ind_list[i]], y[inliers_ind_list[i]], "*k")
    ax.set_xlim([0, 10])
    ax.set_ylim([0, 25])
    if i == best_cycle_ind:
        plt.title("!!! BEST FIT !!! num inliers: " + str(inliers_ind_list[i].shape[0]))
    else:
        plt.title("num inliers: " + str(inliers_ind_list[i].shape[0]))
plt.show()

# %% [markdown]
# ### Test RANSAC with sklearn package (a known machine learning package in python)
# Code taken from: https://scikit-learn.org/stable/auto_examples/linear_model/plot_ransac.html
# %%
from sklearn import linear_model

X = x.reshape(-1, 1)

lr = linear_model.LinearRegression()
lr.fit(X, y)

# Robustly fit linear model with RANSAC algorithm
ransac = linear_model.RANSACRegressor()
ransac.fit(X, y)
inlier_mask = ransac.inlier_mask_
outlier_mask = np.logical_not(inlier_mask)

# Predict data of estimated models
line_X = np.arange(X.min(), X.max())[:, np.newaxis]
line_y = lr.predict(line_X)
line_y_ransac = ransac.predict(line_X)

plt.figure()
plt.scatter(X[inlier_mask], y[inlier_mask], color="yellowgreen", marker=".", label="Inliers")
plt.scatter(X[outlier_mask], y[outlier_mask], color="red", marker=".", label="Outliers")
plt.plot(line_X, line_y, color="navy", linewidth=2, label="Linear regressor")
plt.plot(line_X, line_y_ransac, color="cornflowerblue", linewidth=2, label="RANSAC regressor")
plt.legend(loc="lower right")
plt.title("RANSAC using sklearn package")
plt.show()
# %%


import numpy as np
import numpy.linalg as la

small_number= 1e-14

def gram_schmidt_basis(A):

    # set B as a copy of A, since its values are going to be altered
    B = np.array(A, dtype=np.float_)

    # looping over the vectors, starting with zero, label them with i
    for i in range(B.shape[1]) :

        # Inside the above loop, looping all previous j vectors to substract
        for j in range(i):

            # Substracting the overlap with previous vectors using the corresponding dot product
            # current vector is B[:, i], previous vector is B[:, j]
            B[:, i] = B[:,i] - B[:,i] @ B[:,j] * B[:,j]

        # normalisation test for B[:, i]
        if la.norm(B[:, i]) > small_number:
            B[:, i] = B[:, i] / la.norm(B[:, i])
        else :
            B[:, i] = np.zeros_like(B[:, i])

    # returning the result
    return B

# using the Gram-schmidt process to calculate the dimension spanned by a list of vectors
# the sum of all the norms will be the final number of dimensions (since each vector is normalized to 1 or 0)
def calc_dimensions(A):
    return np.sum(la.norm(gram_schmidt_basis(A), axis=0))

# test the function
vector_v = np.array([[1,0,2,6],
                    [0,1,8,2],
                    [2,8,3,1],
                    [1,-6,2,3]], dtype=np.float_)

gram_schmidt_basis(vector_v)

# find number of dimensions of "vector_v"
calc_dimensions(vector_v)


"""
General mathematical transformations and statistical methods.

"""

import numpy as np
from scipy.stats import zscore

def modified_zscore(x: np.ndarray) -> np.ndarray:
    """
    Modified z-score transformation.

    The modified z score might be more robust than the standard z-score because
    it relies on the median for calculating the z-score. It is less influenced
    by outliers when compared to the standard z-score.

    Parameters
    ----------
    x: (N,) np.ndarray
        numbers

    Returns
    -------
    z: (N,) np.ndarray
        z-scored numbers computed using modified z-score

    """
    med = np.median(x)
    med_abs_dev = np.median(np.abs(x - med))
    return (x - med) / (1.486 * med_abs_dev)

def find_outliers(scores: np.ndarray,
                  threshold: float = 3.0,
                  max_iter: int = 5,
                  tail: int = 0) -> np.ndarray:
    """
    Find outliers via iterated z-scoring.

    This procedure compares absolute z-scores against the threshold.
    After excluding outliers, the comparison is repeated until no
    outliers are present.

    Parameters
    ----------
    scores : (N,) np.ndarray
        The scores for which to find outliers.
    threshold : float, optional
        The value above which a feature is classified as outlier.
    max_iter : int, optional
        The maximum number of iterations.
    tail : one of {0, 1, -1}, optional
        Whether to search for outliers on both extremes of the z-scores (0),
        or on just the positive (1) or negative (-1) side.

    Returns
    -------
    bad_idx : (M,) np.ndarray[int]
        The indices of outliers found in `scores`.

    Notes
    -----
    This code adapted from mne.preprocessing.bads._find_outliers

    """
    bad_idx = list()
    remaining_idx = list(range(len(scores)))

    for _ in range(max_iter):
        x = scores[remaining_idx]
        if tail == 0:
            this_z = np.abs(zscore(x))
        elif tail == 1:
            this_z = zscore(x)
        elif tail == -1:
            this_z = -zscore(x)
        else:
            raise ValueError("Tail parameter %s not recognised." % tail)

        local_bad = this_z > threshold
        if not np.any(local_bad):
            break

        ix_to_remove = [remaining_idx[i] for i in np.where(local_bad)[0]]
        for ix in ix_to_remove:
            bad_idx.append(ix)
            remaining_idx.remove(ix)

    return np.array(bad_idx, dtype=int)

def rms(x: np.ndarray) -> float:
    """ Compute root-mean-square. """
    return np.sqrt(np.mean(np.power(x, 2)))

def logistic4(x: np.ndarray, a: float, b: float, c: float, d: float) -> np.ndarray:
    """
    4PL logistic equation.

    Parameters
    ----------
    x: (N,) np.ndarray
        scalar array, eg logarithmic concentration of a drug
    a: float
        response as x -> 0
    b: float
        slope
    c: float
        inflection point, eg EC50
    d: float
        response as x -> inf

    Returns
    -------
    (N,) np.ndarray
        transformation of `x`

    """
    return d + ((a - d) / (1.0 + ((-x / c) ** b)))


#!/usr/bin/env python

from sympy import sieve
import time

def primality_test_simple(n):
    """素数判定（単純な実装）"""

    n = int(n)
    if n < 2:
        return False
    elif n == 2:
        return True
    else:
        for i in range(2, n, 1):
            if n % i == 0:
                return False
    return True

def do_2(start, end, test_func=primality_test_simple):
    """素直な実装"""
    print("\n単純なforループ")
    #t1 = time.time()
    start = int(start)
    end = int(end)
    prime_list = []
    print("start ...")

    for n in range(start, end+1, 1):
        if test_func(n):
            prime_list.append(n)

    #print('prime number is ...')
    #print(prime_list)
    print(start, "から", end, "に素数は", len(prime_list), "個ある")
    #print("time = ", time.time() - t1)
    return prime_list

def primality_test_using_sieve(n):
    """素数判定（sieve使用）
    """

    n = int(n)
    if n in sieve:
        return True
    else:
        return False

def do_2_using_sieve(start, end):
    """sieve使用"""

    print("\nsieve使用")
    #t1 = time.time()
    start = int(start)
    end = int(end)
    print("start ...")

    prime_list = [i for i in sieve.primerange(start, end+1)]

    #print('prime number is ...')
    #print(prime_list)
    print(start, "から", end, "に素数は")
    print("Total = ", len(prime_list), "個ある")
    #print("time = ", time.time() - t1)
    return prime_list

if __name__ == '__main__':
    start = time.time()
    #print(primality_test_simple(999961))  # 100万以下で最大の素数
    do_2(1, 100000)
    #do_2_using_sieve(2, 100000)
    print("実行時間 = ", time.time() - start, " [秒]\n")


import math

from pathlib import Path
from numpy import array
import numpy as np

from manim import *

ANGLE_IN_DEGREES = -90
angle = math.radians(ANGLE_IN_DEGREES)

TRANSFORMATION_MATRIX = np.array([
    [math.cos(angle), math.sin(angle)],
    [-math.sin(angle), math.cos(angle)]
])

FONT_SIZE = 75

def convert_to_3d(vect):
    return np.array([*list(vect), 0])

def get_transform_matrix_text(matrix_2d):
    return MathTex(
        f"\\begin{{bmatrix}}{round(matrix_2d[0][0])} && {round(matrix_2d[0][1])} \\\\ {round(matrix_2d[1][0])} && {round(matrix_2d[1][1])}\\end{{bmatrix}}", font_size=FONT_SIZE)

class TransformationMatrix(Scene):
    def construct(self):
        VECT1 = np.array([3, 2])
        VECT1_COLOR = "#b9b28b"

        TRANS_VEC = TRANSFORMATION_MATRIX @ VECT1
        VECT2_COLOR = "#b98b99"

        VECT3_COLOR = "#8ba7b9"

        vect1 = Line(start=ORIGIN, end=convert_to_3d(VECT1), stroke_color=VECT1_COLOR).add_tip()

        vect1_name = MathTex(r"\begin{bmatrix}a \\ b\end{bmatrix}").next_to(vect1.get_end(), UP + RIGHT * 2, buff=0.1).set_color(VECT1_COLOR)

        trans_vect1 = Line(start=ORIGIN, end=convert_to_3d(TRANS_VEC), stroke_color=VECT1_COLOR, stroke_width=10).add_tip().set_color(VECT2_COLOR)

        # self.camera.frame_center = np.array([0, 1, 0])

        numberplane = NumberPlane(
            background_line_style={
                "stroke_opacity": 0.4
            }
        )

        matrix_text = get_transform_matrix_text(TRANSFORMATION_MATRIX)
        matrix_text.set_color("#222")

        vector_part = MathTex(f"\\begin{{bmatrix}}{VECT1[0]} \\\\ {VECT1[1]}\end{{bmatrix}}", font_size=FONT_SIZE).set_color(VECT1_COLOR)
        equals_part = MathTex(" = ", font_size=FONT_SIZE).set_color("#222")
        trans_vect_part = MathTex(f'\\begin{{bmatrix}}{round(TRANS_VEC[0], 2)} \\\\ {round(TRANS_VEC[1], 2)}\\end{{bmatrix}}', font_size=FONT_SIZE).set_color(VECT2_COLOR)

        group = VGroup(matrix_text, vector_part, equals_part, trans_vect_part).arrange()
        group.move_to(ORIGIN)
        group.shift(DOWN * 2)

        self.add(vect1, trans_vect1, numberplane, group)

if __name__ == '__main__':
    # Generate animated gif.
    config.background_color = WHITE

    config.frame_height = 8
    config.frame_width = 8

    config.pixel_width = 300
    config.pixel_height = 300

    config.output_file = Path(__file__).resolve().parent.parent.parent / Path('notes/_media/transformation-matrix-cover')
    config.save_last_frame = True

    scene = TransformationMatrix()
    scene.render()


import math
from scipy.fftpack import fft, ifft

def conv(signal1, signal2):
    """Discrete convolution by definition"""

    n = len(signal1) + len(signal2) - 1
    out = []

    for i in range(n):
        s = 0

        for j in range(i + 1):
            if j < len(signal1) and i - j  < len(signal2):
                s += signal1[j] * signal2[i - j]

        out.append(s)

    return out

def conv_fft(signal1, signal2):
    """Convolution using fft and convolutional theorem"""

    signal1 = signal1.copy()
    signal2 = signal2.copy()

    # pad signals to same len
    max_len = max(len(signal1), len(signal2))

    for i in range(max_len - len(signal1)):
        signal1.append(0)
    for i in range(max_len - len(signal2)):
        signal2.append(0)

    fft_s1 = fft(signal1)
    fft_s2 = fft(signal2)
    out = []

    for i in range(len(signal1)):
        out.append(fft_s1[i] * fft_s2[i])

    return list(ifft(out))

def main():
    # Example convolution with sin and cos
    s1 = [math.sin(x) for x in range(5)]
    s2 = [math.cos(x) for x in range(5)]

    print("Discrete Convolution")
    print(conv(s1, s2))

    print("FFT Convolution")
    print(conv_fft(s1, s2))

if __name__ == "__main__":
    main()