"""
================================
K-Nearest Neighbors Classification
Медот K-ближайших соседей
================================
"""
print(__doc__)

#Разберём подробно как работает программа на python
'''Для програмной реализации испоользуется стандартная библиотека scikit-learn.
Можно написать не используя, для этого нужно самому задать выборку и алгоритм который будет с ней работать.'''
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn import neighbors, datasets

'''объявляется глобалльная переменная и присваивается ей значение. Эта переменная хранит значение K - количество ближайших 
соседей которое будет искаться. '''
n_neighbors = 15 #K-целое значение, указанное пользователем. Количество соседей которое будет искаться
iris = datasets.load_iris() # выборка iris представляет из себя матрицу 150 строк на 4 столбца
# чтобы посмотреть наберите# print("iris = ",iris) или print("iris = ",iris.data)
X = iris.data[:, :2] #присваиваем X значения двух первых столбцов из выборки iris
# чтобы посмотреть наберите# print("iris.data[:, :2] = ",iris.data[:, :2])

'''Так как нам нужна выборка точек на плоскости, то нам нужна таблица 150 строк на 2 столбца, 
номер строки - номер точки,   первый столбец хранит координату точек по горизонтали, 
а второй по вертикали. Объявим переменную присвоим ей матрицу с двумя стоблцами

 выборка iris описана в библиотеке sklearn
 выборка iris представляет таблицу 150 строк и 4 стоблца
 первое двоеточие означает что берём все 150 строк
 :2 означает что берём только 2 стоблца из всех что есть, в данном случае из 4-х

 присваиваем X
 X = iris.data[:, :]  все 150 строк и все 4 (признаки) столбцы
 X = iris.data[:, :2] все 150 строк и первые 2 (признака) столбца
 X = iris.data[:2, :]  первые 2 строки и все 4 (признаки) столбцы
 X = iris.data[:2, :2]  первые 2 строки и первые 2 (признака) столбца

 Пока что у нас есть матрица X = iris.data[:, :2] 150 строк на 2 столбца или же 150 точек, 
 каждая из которых имеет  порядковый номер - номер строки и координаты одна по горизонтали 
 и одна повертикали. Теперь нужно каждой точке сопоставить признак 1 из трёх. 
 В данном случае это 1 из трёх цветов. Объявим переменную "y" присвоим ей строку признаков iris.target.
'''
y = iris.target #массив ответов, 150 цифр. ответ на каждую точку может быть один из трёх 0, 1, 2
h = 0.02  # размер шага в сетке
# объявим цвета точек на плоскости и цвета которыми будем закрашивать плоскость.
cmap_light = ListedColormap(['#bafffc', '#ffdede','#abffbd']) #цвета площадок
cmap_bold = ListedColormap(['Blue', 'Red', 'Lime']) #цвета точек
'''
У нас есть заданная плоскость и заданныее точки на плоскости с заданными координатами и цветами. 
На основании этих точек нужно закрасить всю плоскость чтобы куда бы мы ни тыкнули новую точку, 
было известно, к какому классу она будет относится. Для этого разобъем плоскость на маленькие 
квадраты размером h = 0.02 подобно тому как разбита плоскость в тетраде в клетку.
Шаг лучше всего подобрать такой, чтобы размер одной клетки совпадал с размером пикселя на экране. 
Можно меньше, но будет производится много излишних вычислений и на один физический пиксель 
будут попадать несколько квадратов. Если шаг взять больше то будут видны квадраты на плоскости.
'''
#вывод данных не обязательно делать
print('150 точек. первая координата по горизонтали вторая по вертикали X = \n',X,'\n')
print('150 ответов к точкам - принадлежность к одному из трёх классов y = \n',y,'\n')


'''
далее я объявил функуию def knn(weights_):
  параметр weights_ указывает правило по которому будут классифицироваться клетки на плоскости
'''
def knn(weights_): # параметр весовой функции можт принимать 2 значения 'uniform' или 'distance'
    clf = neighbors.KNeighborsClassifier(n_neighbors, weights = weights_) # объявляю экземпляр класса
    clf.fit(X, y) # помещает в экземпляр класса данные
    '''
     нужно найти размер сетки, необходимый для посторения
     для этого пробегаемся по всем значениям выборки X
     находим максимальные и минимальные значения и прибавляем отнимаем 1 чтобы взять с небольшим запасом      
    '''

    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1# находим минимальное и максимальное значение в первом столбце
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1# находим минимальное и максимальное значение во втором столбце
    '''
     X[:, 0].min() минимальное значение из выборки X[:, 0]
     выборка  X[:, 0] - один стобец из выборки с четырьмя столбцами X[:, :]
     X[:, 1] по вертикали
     X[:, 0] по горизонтали    
    '''

    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),  np.arange(y_min, y_max, h))
    print('xx = ', xx)
    print('yy = ', yy)
    '''
     ф-я np.arange(начало, конец, шаг) возвращает одномерный массив 
     равномерно распределённую величину от начала до конца с шагом
     математически это можно представить как разбиение отрезка на равные отрезки заданной длины
     например np.arange(0, 0.5, 0.1) вернёт [ 0.   0.1  0.2  0.3  0.4]
     например np.arange(0, 0.5, 0.2) вернёт [ 0.   0.2  0.4] 
     получили
       xx, yy = np.meshgrid([массив],  [массив])
     ф-я np.meshgrid() принимает 2 одномерных массива и возвращает 2 матрицы. потом присваиевает 
     2 возвращаемые матрицы двум переменным xx yy     
     как работает np.meshgrid  лучше посмотреть на картинке     
    '''

    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]) # Z хранит ответы для каждой точки на плоскости 3 цвета 3 разных значения
    '''
    xx и yy 2 матрицы
    
    xx.ravel() = объединяет все строки двумерного массива xx в одну строку одномерного мссива
    например:
    A = [[1,4], 
         [2,5], 
         [3,6]]
    A.ravel() = [1, 4, 2, 5, 3, 6] 

    np.c_ объединяет в одну матрицу два массива
    например
    a = [1, 2, 3]
    b = [4, 5, 6]
    np.c_[a, b] =   [[1,4], 
                     [2,5], 
                     [3,6]]
                     
                np.c_[xx.ravel(), yy.ravel()]  возвращает координаты пикселей которе нужно раскрасить трёмя цветами          
    clf.predict(np.c_[xx.ravel(), yy.ravel()]) возвращает ответы для кадой точки на плосости
                
    print("np.c_[xx.ravel(), yy.ravel()] = ", np.c_[xx.ravel(), yy.ravel()] )# np.c_ объединяет в одиу матрицу два массива     
    '''

    # Поместим результат в цветовую схему
    Z = Z.reshape(xx.shape)

    print("Z = ",Z)
    print ("xx.shape = ",xx.shape)
    
    # отрисовка по обучающей выборке
    plt.figure() #рисует 'uniform' . без этой строки рисует только 'distance'
    plt.pcolormesh(xx, yy, Z, cmap=cmap_light)#отрисовка цветов фона плоскости
    # сначала раскрасил плоскость цветами потом нарисовал точки. 
    # Если сначала нарисовать точки, по при закраске плоскости точки будут закрашены.
    plt.scatter(X[:, 0], X[:, 1], c=y, cmap=cmap_bold, edgecolor='black', s=20)#отрисовка точек на плоскости
        #edgecolor='black' цвет ободков вокруг точек.
    plt.title("3-Class classification (k = %i, weights = '%s')"% (n_neighbors, weights_ ))#отрисовка заголовка
    plt.show() #показать на экране рисунки

'''
при прогнозировании к какому классу относится участок можно использовать различные параметры весовой функции
'uniform' означает равномерную фесовую функцию. Рассояние от классифицируемого объекта до заданных 
точек не учитываеется учитывается только объектов (точек) какого класса больше
'distance' означает что учитывается не только количество объектов (точек) того или иного класса до
классифицируемого объекта но и расстояние на котором они расположены до классифицируемого объекта
вызываю функцию, подставляя различные параметры весовой функции

при вызове используется равномерная весовая функция и весовая функция расстояния
отрисовываются 2 картинки
'''

knn('uniform')#вклад в классификацию каждой точки одинаковый
knn('distance')#Используется весовая функция расстояния
