{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "(Este es el tercer capítulo de la mini-serie de artículos sobre análisis cluster que estamos haciendo en pybonacci, si todavía no has leído [los anteriores artículos les puedes echar un ojo ahora](http://pybonacci.org/tag/agrupamiento-clusters/). Escribir esta tercera parte solo ha tardado ¡¡¡tres años en salir!!!).\n", "\n", "El algoritmo de k-medias es uno de los algoritmos más sencillos de agrupamiento dentro del campo del aprendizaje automático (*machine learning* para los *hipsters*). Sirve para agrupar $N$-elementos en $K$-grupos distintos.\n", "\n", "## Agrupamiento\n", "\n", "(Breve resumen de lo visto en anteriores artículos).\n", "\n", "Las técnicas de [agrupamiento o análisis *Cluster*](https://en.wikipedia.org/wiki/Cluster_analysis) son un tipo de aprendizaje no supervisado ya que no requieren un aprendizaje previo a partir de los datos.\n", "\n", "Son métodos para agrupar datos sin etiqueta en subgrupos. Estos subgrupos pretenden reflejar algún tipo de estructura interna.\n", "\n", "## K-medias\n", "\n", "El algoritmo de [K-medias](https://en.wikipedia.org/wiki/K-means_clustering) es un algoritmo de agrupamiento particional que nos crea $K$ particiones de los $N$ datos (con $N \\geqslant K$). El algoritmo se puede resumir en los siguientes pasos:\n", "\n", "* Seleccionar $K$ puntos de forma aleatoria en un dominio que comprende todos los puntos del conjunto de datos. estos $K$ puntos representan los centros iniciales de los grupos (lo veremos en el método `_init_centroids` de la clase `KMeans` que luego explicaremos más en detalle). No vamos a implementar ningún método de posicionamiento inicial complejo, los centros iniciales serán valores aleatorios colocados entre los umbrales mínimos y máximos del conjunto de puntos (entre las $x$, $y$ mínimas y máximas). El problema que resolveremos será en dos dimensiones, como ya podéis intuir.\n", "\n", "* Asignar cada punto del conjunto de datos al grupo con el centro más próximo (lo veremos en el método `_assign_centroids` de la clase `KMeans`, llegaremos en breve).\n", "\n", "* Recalculamos los centros de los grupos a partir de todos los datos asignados a cada grupo, es decir, calculamos el [centroide](https://es.wikipedia.org/wiki/Centroide) de cada grupo método `_recalculate_centroids` de la clase `KMeans`).\n", "\n", "* Repetir los dos pasos anteriores hasta que se cumpla un condición que puede ser:\n", " * Se ha alcanzado un número máximo de iteraciones (no deseable).\n", " * Los centroides ya no cambian más allá de un rango.\n", " \n", " Nosotros solo comprobaremos el segundo ya que solo vamos a manejar conjuntos pequeños de datos. Le pondremos un límite de variación a nuestra clase `KMeans` que deberán cumplir todos los centroides. La comprobación sobre si parar las iteraciones se realizarán en el método `_check_stop` de la clase KMeans.\n", " \n", "## Todo esto pretende ser más educativo que riguroso\n", "\n", "La implementación que vamos a hacer será en Python puro ya que pretende ser educativa de cara a ayudar a conocer el funcionamiento del algoritmo paso a paso. Tenéis implementaciones mucho más desarrolladas, precisas, con mejor rendimiento,..., en paquetes como [scipy](http://docs.scipy.org/doc/scipy/reference/cluster.vq.html), [scikit-learn](http://scikit-learn.org/stable/modules/clustering.html#k-means) u [otros](https://pypi.python.org/pypi?%3Aaction=search&term=kmeans&submit=search).\n", "\n", "## La implementación\n", "\n", "[AVISO PARA NAVEGANTES: Todo el código está pensado para funcionar en Python 3. Si usas Python 2 deberías empezar a pensar en actualizarte].\n", "\n", "Primero escribo la clase a capón y luego la voy desgranando poco a poco. La he escrito como un iterador de forma que nos permita iterar fácilmente paso a paso (la iteración paso a paso la veremos de forma visual para intentar aportar aun mayor claridad). Para ver más sobre iteradores podéis DuckDuckGoear o ver este [enlace](http://www.diveintopython3.net/iterators.html).\n", "\n", " class KMeans:\n", " def __init__(self, x, y, n_clusters = 1, limit = 10):\n", " self.x = x\n", " self.x_min = min(x)\n", " self.x_max = max(x)\n", " self.y = y\n", " self.y_min = min(y)\n", " self.y_max = max(y)\n", " self.n_clusters = n_clusters\n", " self.limit = limit\n", " self._init_centroids() \n", " self.iterations = 0\n", "\n", " def _init_centroids(self):\n", " self.x_centroids = []\n", " self.y_centroids = []\n", " self.colors = []\n", " for i in range(self.n_clusters):\n", " self.x_centroids.append(randint(self.x_min, self.x_max))\n", " self.y_centroids.append(randint(self.y_min, self.y_max))\n", " r = randint(0,255)\n", " g = randint(0,255)\n", " b = randint(0,255)\n", " color = 'rgb({0},{1},{2})'.format(r, g, b)\n", " self.colors.append(color)\n", " self._assign_centroids()\n", "\n", " def _assign_centroids(self):\n", " self.c = []\n", " # Maximum possible distance to a centroid\n", " dmax = sqrt((self.x_min - self.x_max)**2 + \n", " (self.y_min - self.y_max)**2)\n", " for xi, yi in zip(self.x, self.y):\n", " cluster = 0\n", " d0 = dmax\n", " for i in range(self.n_clusters):\n", " d = sqrt((xi - self.x_centroids[i])**2 + \n", " (yi - self.y_centroids[i])**2)\n", " if d < d0:\n", " cluster = i\n", " d0 = d\n", " self.c.append(cluster)\n", "\n", " def _recalculate_centroids(self):\n", " self._x_centroids = self.x_centroids[:]\n", " self._y_centroids = self.y_centroids[:]\n", " for n in range(self.n_clusters):\n", " x0 = 0\n", " y0 = 0\n", " cont = 0\n", " for i, c in enumerate(self.c):\n", " if c == n:\n", " cont += 1\n", " x0 += self.x[i]\n", " y0 += self.y[i]\n", " self.x_centroids[n] = x0 / cont\n", " self.y_centroids[n] = y0 / cont\n", " self._assign_centroids()\n", "\n", " def _check_stop(self):\n", " for i in range(self.n_clusters):\n", " d = sqrt(\n", " (self._x_centroids[i] - self.x_centroids[i])**2 +\n", " (self._y_centroids[i] - self.y_centroids[i])**2\n", " )\n", " if d > self.limit:\n", " return False\n", " return True\n", "\n", " def __iter__(self):\n", " return self\n", "\n", " def __next__(self):\n", " self.iterations += 1\n", " self._recalculate_centroids()\n", " stop = self._check_stop()\n", " if stop == True:\n", " raise StopIteration\n", " return self\n", " \n", "La clase anterior paso a paso:\n", "\n", "### método \\_\\_init\\_\\_\n", "\n", " def __init__(self, x, y, n_clusters = 1, limit = 10):\n", " self.x = x\n", " self.x_min = min(x)\n", " self.x_max = max(x)\n", " self.y = y\n", " self.y_min = min(y)\n", " self.y_max = max(y)\n", " self.n_clusters = n_clusters\n", " self.limit = limit\n", " self._init_centroids() \n", " self.iterations = 0\n", "\n", "Inicializamos la clase con los valores `x` de los puntos, los valores `y` de los puntos, el número de grupos a usar, `n_clusters` y el límite del desplazamiento de los grupos a partir del cual consideraremos que podemos parar de iterar, `limit`.\n", "\n", "Además, a partir del los valores introducidos, extraemos las ventana umbral donde se colocan los puntos (atributos `x_min`, `x_max`, `y_min` e `y_max`) que después usaremos para inicializar los centroides (mediante `_init_centroids`).\n", "\n", "### método \\_init\\_centroids\n", "\n", " def _init_centroids(self):\n", " self.x_centroids = []\n", " self.y_centroids = []\n", " self.colors = []\n", " for i in range(self.n_clusters):\n", " self.x_centroids.append(randint(self.x_min, self.x_max))\n", " self.y_centroids.append(randint(self.y_min, self.y_max))\n", " r = randint(0,255)\n", " g = randint(0,255)\n", " b = randint(0,255)\n", " color = 'rgb({0},{1},{2})'.format(r, g, b)\n", " self.colors.append(color)\n", " self._assign_centroids()\n", " \n", "Mediante este método, que se usa al inicializar la clase, creamos los centroides iniciales a partir de valores aleatorios situados entre los valores de `x_min`, `x_max`, `y_min` e `y_max`. Además, asignamos unos colores aleatorios a cada centroide (que luego usaremos en la visualización). Pensándolo fríamente, ahora que estoy escribiendo esto, la verdad que `colors` podría estar fuera de esta clase pero lo vamos a dejar así.\n", "\n", "Una vez que se han creado los centroides hacemos la asignación de cada punto del conjunto de puntos $x$ e $y$ a cada centroide mediante el método `_assign_centroids`.\n", "\n", "### método \\_assign\\_centroids\n", "\n", " def _assign_centroids(self):\n", " self.c = []\n", " # Maximum possible distance to a centroid\n", " dmax = sqrt((self.x_min - self.x_max)**2 + \n", " (self.y_min - self.y_max)**2)\n", " for xi, yi in zip(self.x, self.y):\n", " cluster = 0\n", " d0 = dmax\n", " for i in range(self.n_clusters):\n", " d = sqrt((xi - self.x_centroids[i])**2 + \n", " (yi - self.y_centroids[i])**2)\n", " if d < d0:\n", " cluster = i\n", " d0 = d\n", " self.c.append(cluster)\n", "\n", "en el atributo `c` (que es una lista) almacenamos el valor del grupo al que pertenece cada punto del conjunto de datos $x$ e $y$. Para ello, primero tenemos que calcular la distancia de cada punto a cada centroide y el que centroide que tenga menos distancia al punto será el asignado. Por tanto, en este paso, hemos de calcular $N \\cdot K$ distancias.\n", "\n", "### método \\_recalculate\\_centroids\n", "\n", " def _recalculate_centroids(self):\n", " self._x_centroids = self.x_centroids[:]\n", " self._y_centroids = self.y_centroids[:]\n", " for n in range(self.n_clusters):\n", " x0 = 0\n", " y0 = 0\n", " cont = 0\n", " for i, c in enumerate(self.c):\n", " if c == n:\n", " cont += 1\n", " x0 += self.x[i]\n", " y0 += self.y[i]\n", " self.x_centroids[n] = x0 / cont\n", " self.y_centroids[n] = y0 / cont\n", " self._assign_centroids()\n", "\n", "En este paso, recalculamos los centroides. Cada nuevo centroide será el centroide de los puntos asignados a ese centroide. Los antiguos centroides los conservamos para poder compararlos con los nuevos y ver si han variado poco o mucho las nuevas posiciones de los centroides. Una vez que hemos calculado los nuevos centroides y que mantenemos los antiguos asignamos los puntos a los nuevos centroides mediante el método `_assign_centroids` explicado anteriormente.\n", "\n", "### método \\_check\\_stop\n", "\n", " def _check_stop(self):\n", " for i in range(self.n_clusters):\n", " d = sqrt(\n", " (self._x_centroids[i] - self.x_centroids[i])**2 +\n", " (self._y_centroids[i] - self.y_centroids[i])**2\n", " )\n", " if d > self.limit:\n", " return False\n", " return True\n", "\n", "En este método calculamos si la diferencia entre la posición de los centroides antiguos y de los nuevos es superior o inferior al límite o umbral que hemos definido al instanciar la clase. Si cualquiera de los centroides se ha movido más del umbral definido seguiremos iterando (`_check_stop` devolverá `False`), si ninguno supera el umbral le diremos que pare de iterar (`_check_stop` devolverá `True`).\n", "\n", "### métodos \\_\\_iter\\_\\_ y \\_\\_next\\_\\_\n", "\n", " def __iter__(self):\n", " return self\n", "\n", " def __next__(self):\n", " self.iterations += 1\n", " self._recalculate_centroids()\n", " stop = self._check_stop()\n", " if stop == True:\n", " raise StopIteration\n", " return self\n", "\n", "Si os habéis leído el enlace sobre iteradores que he dejado más arriba espero que esto sea sencillo de entender. \n", "\n", "* El método `__iter__` no necesita mayor explicación.\n", "* El método `__next__` incrementa el atributo `iterations`, que no vamos a usar pero que podríamos usar para limitar, por ejemplo, el número de iteraciones, os lo dejo como ejercicio :-D, cada vez que damos un nuevo paso recalculamos los centroides y chequeamos si hay que parar la iteración porque hemos cumplido las condiciones fijadas (sí, el `stop == True` es redundante pero he pretendido que sea lo más claro posible).\n", "\n", "## Visualización\n", "\n", "El hecho de crear la clase como un iterador es porque he pensado que podríamos iterar en cada paso y hacer una visualización interactiva. Como la visualizazión quiero que funciones cuando el notebook está estático he recurrido a usar [Brythonmagic](https://github.com/kikocorreoso/brythonmagic).\n", "\n", "Si queréis saber más sobre Brythonmagic podéis leer el README del enlace anterior o ver [esta entrada](http://pybonacci.org/2014/03/03/presentando-brythonmagic/) con vídeo y todo que muestra el funcionamiento.\n", "\n", "Como resumen, básicamente es una *magic function* para el notebook que nos permite usar brython (una implementación de Python 3 hecha en javascript que funciona en el navegador sin necesidad de ningún kernel detrás).\n", "\n", "Si no lo tienes instalado lo puedes instalar mediante `pip`." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied (use --upgrade to upgrade): brythonmagic in /home/kiko/pyprojs/venv-scipy/lib/python3.4/site-packages\r\n", "Requirement already satisfied (use --upgrade to upgrade): ipython>=1.0 in /home/kiko/pyprojs/venv-scipy/lib/python3.4/site-packages (from brythonmagic)\r\n", "Cleaning up...\r\n" ] } ], "source": [ "!pip install brythonmagic" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Para poder hacer uso de la extensión la hemos de cargar mediante:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%load_ext brythonmagic" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "El paso siguiente nos permite usar toda la maquinaria de brython en el navegador dentro del notebook y es el último paso para que todo funcione." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%HTML\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Podemos comprobar si funciona con el siguiente ejemplo. Después de ejecutar la celda debrías de ver un mensaje en pantalla (esto seré un poco enojante cuando lo veamos en estático puesto que siempre saltará esta ventana...):" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ " \n", "
\n", " \n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%brython\n", "from browser import alert\n", "alert('it works!')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A continuación metemos el código de la clase `KMeans` que ya hemos detallado más arriba y la llamaremos desde otras celdas brython a partir del nombre que le hemos dado en esta celda (`kmeans_class`, ver primera línea de la celda)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ " \n", "
\n", " \n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%brython -s kmeans_class\n", "from math import sqrt\n", "from random import randint\n", "\n", "class KMeans:\n", " def __init__(self, x, y, n_clusters = 1, limit = 10):\n", " self.x = x\n", " self.x_min = min(x)\n", " self.x_max = max(x)\n", " self.y = y\n", " self.y_min = min(y)\n", " self.y_max = max(y)\n", " self.n_clusters = n_clusters\n", " self.limit = limit\n", " self._init_centroids() \n", " self.iterations = 0\n", " \n", " def _init_centroids(self):\n", " self.x_centroids = []\n", " self.y_centroids = []\n", " self.colors = []\n", " for i in range(self.n_clusters):\n", " self.x_centroids.append(randint(self.x_min, self.x_max))\n", " self.y_centroids.append(randint(self.y_min, self.y_max))\n", " r = randint(0,255)\n", " g = randint(0,255)\n", " b = randint(0,255)\n", " color = 'rgb({0},{1},{2})'.format(r, g, b)\n", " self.colors.append(color)\n", " self._assign_centroids()\n", " \n", " def _assign_centroids(self):\n", " self.c = []\n", " # Maximum possible distance to a centroid\n", " dmax = sqrt((self.x_min - self.x_max)**2 + \n", " (self.y_min - self.y_max)**2)\n", " for xi, yi in zip(self.x, self.y):\n", " cluster = 0\n", " d0 = dmax\n", " for i in range(self.n_clusters):\n", " d = sqrt((xi - self.x_centroids[i])**2 + \n", " (yi - self.y_centroids[i])**2)\n", " if d < d0:\n", " cluster = i\n", " d0 = d\n", " self.c.append(cluster)\n", " \n", " def _recalculate_centroids(self):\n", " self._x_centroids = self.x_centroids[:]\n", " self._y_centroids = self.y_centroids[:]\n", " for n in range(self.n_clusters):\n", " x0 = 0\n", " y0 = 0\n", " cont = 0\n", " for i, c in enumerate(self.c):\n", " if c == n:\n", " cont += 1\n", " x0 += self.x[i]\n", " y0 += self.y[i]\n", " self.x_centroids[n] = x0 / cont\n", " self.y_centroids[n] = y0 / cont\n", " self._assign_centroids()\n", " \n", " def _check_stop(self):\n", " for i in range(self.n_clusters):\n", " d = sqrt(\n", " (self._x_centroids[i] - self.x_centroids[i])**2 +\n", " (self._y_centroids[i] - self.y_centroids[i])**2\n", " )\n", " if d > self.limit:\n", " return False\n", " return True\n", " \n", " def __iter__(self):\n", " return self\n", " \n", " def __next__(self):\n", " self.iterations += 1\n", " self._recalculate_centroids()\n", " stop = self._check_stop()\n", " if stop == True:\n", " raise StopIteration\n", " return self" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "El código que figura a continuación es parte de [una librería que empecé hace un tiempo para hacer gráficos en el canvas del navegador de forma simple a partir de Brython](https://bitbucket.org/kikocorreoso/brython-bryplot/src). La funcionalidad básica está en el módulo [*base.py*](https://bitbucket.org/kikocorreoso/brython-bryplot/src/528fa4116d1f8fd1f2ab44feb537d63778edb5d5/bryplot/base.py?at=default&fileviewer=file-view-default) y lo que figura a continuación es parte de ese módulo modificado en algún sitio.\n", "\n", "NO VOY A EXPLICAR ESTE CÓDIGO EN DETALLE PUESTO QUE NO ES PARTE DEL PROPÓSITO DE LA ENTRADA Y NO QUIERO DESVIAR LA ATENCIÓN. SI TENÉIS ALGUNA DUDA O INTERÉS PODÉIS USAR [LOS COMENTARIOS DEL BLOG](http://pybonacci.org/2015/09/28/analisis-cluster-iii-clasificacion-no-supervisada-mediante-k-medias/) PARA ELLO.\n", "\n", "El código a continuación permite dibujar sobre un [canvas HTML5](http://www.html5canvastutorials.com/) diferentes formas. Solo vamos a definir formas para dibujar círculos, para los puntos y los centroides de los grupos, y líneas, para mostrar qué puntos se asignan a qué centroide en cada paso." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ " \n", "
\n", " \n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%brython -s canvas_utils\n", "from browser import document as doc\n", "import math\n", "\n", "## Base classes for higher level objects\n", "class Figure:\n", " \"\"\"\n", " Base class to create other elements.\n", " \"\"\"\n", " def __init__(self, canvasid, \n", " facecolor = \"white\", \n", " edgecolor = \"black\", \n", " borderwidth = None):\n", " \"\"\" \n", " Parameters\n", " ----------\n", " *canvasid*: String\n", " String indicating the canvas id where the image should be \n", " rendered.\n", " *facecolor*: String\n", " String value containing a valid HTML color\n", " *edgecolor*: String\n", " String value containing a valid HTML color\n", " *borderwidth*: Integer\n", " Value indicating the width of the border in pixels.\n", " If not provided it will 0 and the edgecolor will not be\n", " visible\n", " \"\"\"\n", "\n", " if isinstance(canvasid, str):\n", " self.id = canvasid\n", " else:\n", " raise Exception(\"The canvasid parameter should be a string\")\n", " \n", " try:\n", " self.canvas = doc[self.id]\n", " except:\n", " raise Exception(\"No HTML element with id=%s\" %\n", " self.id)\n", " \n", " try:\n", " self._W = self.canvas.width\n", " self._H = self.canvas.height\n", " self._ctx = self.canvas.getContext(\"2d\")\n", " except:\n", " raise Exception(\"You must provide the ID of a element\")\n", " \n", " self.facecolor = facecolor\n", " self.borderwidth = borderwidth\n", " self.edgecolor = edgecolor\n", " self.clf()\n", " \n", " def clf(self):\n", " \"clear the figure\"\n", " self._ctx.save()\n", " \n", " # The following line should clear the canvas but I found a\n", " # problem when I use beginPath ¿¿¿???\n", " #self._ctx.clearRect(0, 0, self._W, self._H)\n", " # So I use the following line that is less performant but\n", " # this operation shouldn't be done very often...\n", " self.canvas.width = self.canvas.width\n", " \n", " self._ctx.fillStyle = self.facecolor\n", " self._ctx.fillRect(0, 0, self._W, self._H)\n", " self._ctx.fill()\n", " if self.borderwidth:\n", " self._ctx.lineWidth = self.borderwidth\n", " self._ctx.strokeStyle = self.edgecolor\n", " self._ctx.strokeRect(0, 0, self._W, self._H)\n", " self._ctx.stroke()\n", " self._ctx.restore()\n", " \n", "\n", "class Shape:\n", " \"\"\"\n", " Base class to create other elements.\n", " \"\"\"\n", " def __init__(self, context, x, y,\n", " facecolor = \"black\", \n", " edgecolor = \"black\",\n", " #alpha = 1,\n", " borderwidth = None):\n", " \"\"\" \n", " Parameters\n", " ----------\n", " *context*: a canvas context\n", " a valid canvas context where the text will be rendered\n", " *x*: int or float\n", " x value for location in pixels\n", " *y*: int or float\n", " y value for location in pixels\n", " *facecolor*: String\n", " String value containing a valid HTML color\n", " *edgecolor*: String\n", " String value containing a valid HTML color\n", " *alpha*: int or float\n", " Value between 0 (transparent) and 1 (opaque) to set the\n", " transparency of the text\n", " *borderwidth*: Integer\n", " Value indicating the width of the border in pixels.\n", " If not provided it will 0 and the edgecolor will not be\n", " visible\n", " \"\"\"\n", " self._ctx = context\n", " self.x = x\n", " self.y = y\n", " self.facecolor = facecolor\n", " self.borderwidth = borderwidth\n", " self.edgecolor = edgecolor\n", " #self.alpha = alpha\n", "\n", "class Circle(Shape):\n", " def __init__(self, *args, radius = 10, **kwargs):\n", " \"\"\"\n", " Parameters\n", " ----------\n", " *radius*: int or float\n", " radius of the circle in pixels.\n", " \"\"\"\n", " Shape.__init__(self, *args, **kwargs)\n", " self.r = radius\n", " self.draw()\n", " \n", " def draw(self):\n", " self._ctx.save()\n", " #self._ctx.globalAlpha = self.alpha\n", " self._ctx.beginPath()\n", " self._ctx.fillStyle = self.facecolor\n", " self._ctx.arc(self.x, self.y, self.r, 0, 2 * math.pi)\n", " self._ctx.fill()\n", " if self.borderwidth:\n", " self._ctx.lineWidth = self.borderwidth\n", " self._ctx.strokeStyle = self.edgecolor\n", " self._ctx.arc(self.x, self.y, self.r, 0, 2 * math.pi)\n", " self._ctx.stroke()\n", " self._ctx.closePath()\n", " self._ctx.restore()\n", "\n", "class Line(Shape):\n", " def __init__(self, *args, polygon = False, borderwidth = 2, **kwargs):\n", " Shape.__init__(self, *args, **kwargs)\n", " self.borderwidth = borderwidth\n", " self.polygon = polygon\n", " self.draw()\n", " \n", " def draw(self):\n", " self._ctx.save()\n", " #self._ctx.globalAlpha = self.alpha\n", " self._ctx.beginPath()\n", " self._ctx.moveTo(self.x[0], self.y[0])\n", " for i in range(len(self.x)):\n", " self._ctx.lineTo(self.x[i], self.y[i])\n", " if self.polygon:\n", " self._ctx.closePath()\n", " if self.facecolor:\n", " self._ctx.fillStyle = self.facecolor\n", " self._ctx.fill()\n", " if self.borderwidth:\n", " self._ctx.lineWidth = self.borderwidth\n", " self._ctx.strokeStyle = self.edgecolor\n", " self._ctx.stroke()\n", " self._ctx.restore()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "El siguiente código solo nos va a valer para establecer una mínima disposición de los elementos contenidos en la visualización.\n", "\n", "Se incluye un canvas, donde se dibujarán las cosas, y un botón, para poder iterar el algoritmo." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": true }, "outputs": [], "source": [ "html = \"\"\"\n", "
\n", "

\n", " \n", "

\n", "

\n", " \n", "

\n", "
\"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Por último, este es el código que realiza todo a partir de los demás. Voy a intentar explicarlo un poco más en detalle:\n", "\n", " fig = Figure('cnvs01', borderwidth = 2)\n", " \n", " n_points = 50\n", " x = [randint(10, fig._W - 10) for value in range(n_points)]\n", " y = [randint(10, fig._H - 10) for value in range(n_points)]\n", "\n", " kmeans = KMeans(x, y, n_clusters = 4, limit = 1)\n", "\n", " def plot(obj):\n", " fig._ctx.save()\n", " fig._ctx.fillStyle= \"#ffffff\"\n", " fig._ctx.globalAlpha = 0.3\n", " fig._ctx.fillRect(2,2,fig._W-4,fig._H-4)\n", " fig._ctx.restore()\n", " x = obj.x\n", " y = obj.y\n", " npoints = len(x)\n", " colors = obj.colors\n", " xc = obj.x_centroids\n", " yc = obj.y_centroids\n", " c = obj.c\n", " for i in range(npoints):\n", " color = colors[c[i]]\n", " Line(fig._ctx, [x[i], xc[c[i]]], [y[i], yc[c[i]]],\n", " facecolor = color, edgecolor = color)\n", " Circle(fig._ctx, x[i], y[i], \n", " facecolor = color, edgecolor = 'black',\n", " borderwidth = 1, radius = 4)\n", " for xci, yci, color in zip(xc, yc, colors):\n", " Circle(fig._ctx, xci, yci, \n", " facecolor = color, edgecolor = 'black',\n", " borderwidth = 1, radius = 8)\n", "\n", " def update(ev):\n", " plot(kmeans)\n", " try:\n", " next(kmeans)\n", " except:\n", " #doc['button'].disabled = True\n", " del doc['button']\n", "\n", " doc['button'].bind('click', update)\n", " \n", "* `fig = Figure('cnvs01', borderwidth = 2)` inicializamos el canvas con un borde negro usando la clase Figure creada más arriba.\n", "* Definimos el número de puntos a usar y las posiciones de los puntos.\n", "* Inicializamos el objeto mediante `kmeans = KMeans(x, y, n_clusters = 4, limit = 1)`. En este caso queremos que tenga 4 clusters (`n_clusters`) y que el límite (`limit`) para dejar de iterar sea cuando los centroides se muevan un pixel o menos.\n", "* La función `plot` hace varias cosas. \n", " 1. Primero suaviza los colores de la imagen previa antes de la actual iteración (esto está más relacionado con el canvas HTML5 y con javascript y no hace falta entenderlo mucho más allá de lo comentado en esta línea)\n", " ```\n", " fig._ctx.save()\n", " fig._ctx.fillStyle= \"#ffffff\"\n", " fig._ctx.globalAlpha = 0.3\n", " fig._ctx.fillRect(2,2,fig._W-4,fig._H-4)\n", " fig._ctx.restore()\n", " ```\n", " 2. Después extraemos todos los datos del objeto (`kmeans`) que vamos a usar dentro de la función.\n", " ```\n", " x = obj.x\n", " y = obj.y\n", " npoints = len(x)\n", " colors = obj.colors\n", " xc = obj.x_centroids\n", " yc = obj.y_centroids\n", " c = obj.c\n", " ```\n", " 3. Dibujamos las líneas entre los puntos y los centroides y los puntos con colores asociados a cada grupo\n", " ```\n", " for i in range(npoints):\n", " color = colors[c[i]]\n", " Line(fig._ctx, [x[i], xc[c[i]]], [y[i], yc[c[i]]],\n", " facecolor = color, edgecolor = color)\n", " Circle(fig._ctx, x[i], y[i], \n", " facecolor = color, edgecolor = 'black',\n", " borderwidth = 1, radius = 4)\n", " ```\n", " 4. Y finalmente se dibujan los centroides de cada grupo con un círculo un poco más grande que los propios puntos\n", " ```\n", " for xci, yci, color in zip(xc, yc, colors):\n", " Circle(fig._ctx, xci, yci, \n", " facecolor = color, edgecolor = 'black',\n", " borderwidth = 1, radius = 8)\n", " ```\n", "* Creamos, además, una función `update` que será la que llama a la función `plot` y la que llama a una nueva iteración. El código que figura en el bloque `except`, `del doc['button']`, es más de la parte de brython y sirve para que el botón desaparezca una vez que el iterador ha quedado exhausto (se han agotado las iteraciones).\n", "* La última línea de código, `doc['button'].bind('click', update)`, asocia el evento *click* del botón a la función `update` que he comentado anteriormente.\n", "\n", "Si ahora ejecutamos la siguiente celda de código deberíamos ver un canvas de 500px x 500px con un botón debajo. Si vamos pulsando al botón veremos como nueva iteración en acción, además de ver las anteriores iteraciones de forma difuminada. Una vez que hemos alcanzado la condición de que los centroides no se mueven más desaparecerá el botón (para no teneros ahí pulsando y que me digáis que eso no sigue funcionando...)." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ " \n", "
\n", "
\n", "

\n", " \n", "

\n", "

\n", " \n", "

\n", "
\n", " \n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%brython -S kmeans_class canvas_utils -h html\n", "\n", "fig = Figure('cnvs01', borderwidth = 2)\n", "\n", "n_points = 50\n", "\n", "x = [randint(10, fig._W - 10) for value in range(n_points)]\n", "y = [randint(10, fig._H - 10) for value in range(n_points)]\n", "\n", "kmeans = KMeans(x, y, n_clusters = 4, limit = 1)\n", "\n", "def plot(obj):\n", " fig._ctx.save()\n", " fig._ctx.fillStyle= \"#ffffff\"\n", " fig._ctx.globalAlpha = 0.3\n", " fig._ctx.fillRect(2,2,fig._W-4,fig._H-4)\n", " fig._ctx.restore()\n", " x = obj.x\n", " y = obj.y\n", " npoints = len(x)\n", " colors = obj.colors\n", " xc = obj.x_centroids\n", " yc = obj.y_centroids\n", " c = obj.c\n", " for i in range(npoints):\n", " color = colors[c[i]]\n", " Line(fig._ctx, [x[i], xc[c[i]]], [y[i], yc[c[i]]],\n", " facecolor = color, edgecolor = color)\n", " Circle(fig._ctx, x[i], y[i], \n", " facecolor = color, edgecolor = 'black',\n", " borderwidth = 1, radius = 4)\n", " for xci, yci, color in zip(xc, yc, colors):\n", " Circle(fig._ctx, xci, yci, \n", " facecolor = color, edgecolor = 'black',\n", " borderwidth = 1, radius = 8)\n", "\n", "def update(ev):\n", " plot(kmeans)\n", " try:\n", " next(kmeans)\n", " except:\n", " #doc['button'].disabled = True\n", " del doc['button']\n", " \n", "doc['button'].bind('click', update)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Conclusiones\n", "\n", "Espero que haya quedado claro el funcionamiento básico del algoritmo K-Medias. También espero que si veis alguna errata en el código me aviséis o hagáis un *Pull Request* a nuestro repositorio de notebooks.\n", "\n", "Podéis ver este notebook funcionando incluso en estático en [el siguiente enlace](http://nbviewer.jupyter.org/github/Pybonacci/notebooks/blob/master/K-Medias.ipynb).\n", "\n", "Este notebook ha sido [publicado en el blog de pybonacci](http://pybonacci.org/2015/09/28/analisis-cluster-iii-clasificacion-no-supervisada-mediante-k-medias/)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.4.0" } }, "nbformat": 4, "nbformat_minor": 0 }