Natural Language Understanding con Snips NLU en Python

Uno de los campos más importantes de la inteligencia artificial es el del tratamiento del lenguaje natural. Ya en 1955, en el primer evento sobre Inteligencia Artificial, y promovido entre otros por John McCarthy (creador de Lisp) y Claude Shannon (padre de la teoría de la información y promotor del uso del álgebra de boole para la electrónica), estas cuestiones entraron en el listado de temas.

En aquella época se era bastante optimista con las posibilidades de describir el lenguaje natural (inglés, español, …) de forma precisa con un conjunto de reglas de forma similar a las matemáticas. Luego se comprobó que esto no era una tarea tan sencilla.

Hoy día, es un campo donde se avanza mucho todos los días, aprovechando las técnicas de machine learning combinadas con heurísticas propias de cada lenguaje.

Natural Language Understanding nos permite saber qué quiere decir una frase que pronuncia o escribe un usuario. Existen diversos servicios que provee de esta funcionalidad: IBM Watson, Microsoft LUIS y también existe software libre, como Snips NLU.

Snips NLU es una librería hecha en Rust y con interfaz en Python que funciona analizando el texto con datos entrenados gracias a machine learning y da como resultado un intent, o significado de la frase y el valor de los slots, que son variables dentro de la frase.

¿Qué tiempo hará mañana en Molina de Aragón?

Y Snips NLU nos devuelve:

  • intent: obtenerTiempo
  • slots:
    • cuando: mañana
    • donde: Molina de Aragón

Pero para esto, antes hay que hacer un par de cosas.

Instalar Snips NLU

Instala Snips NLU con Pipenv (recomendado) o Pip:

 

Datos de entrenamiento

En primer lugar vamos a crear un listado de frases que todas expresen la intención de obtener el tiempo y lo guardamos en un fichero llamado obtenerTiempo.txt. Así definimos un intent:

La sintaxis es muy sencilla. Cada frase en una línea. Cuando una palabra forme parte de un slot, se usa la sintaxis [NOMBRE SLOT:TIPO](texto). En el caso de [donde:localidad](Frías). Donde es el nombre del slot, localidad es el tipo de dato que va y Frías es el texto original de la frase. En el caso del slot cuando, hemos configurado el tipo como snips/time que es uno de los predefinidos por Snips NLU.

Creamos también un fichero llamado localidad.txt, con los posibles valores que puede tener localidad. Esto no quiere decir que no capture valores fuera de esta lista, como veremos después, pero tienen prioridad si hubiese más tipos. También se puede configurar a que no admita otros valores, pero no lo vamos a ver aquí.

Ahora generamos un fichero JSON listo para ser entrenado con el comando generate-dataset.

Entrenamiento

Ya estamos listos para el entrenamiento. Creamos un fichero Python como este y lo ejecutamos:

El entrenamiento se produce en fit, y esta tarea puede tardar dependiendo del número de datos que metamos. Una vez finalizado, generama un fichero trained.json con el entrenamiento ya realizado.

Hacer preguntas

Ha llegado el momento de hacer preguntas, cargando el fichero de los datos entrenados.

Ahora sería tarea del programador usar el valor del intent y de los slots para dar una respuesta inteligente.

Te animo a que te descargues el proyecto o lo hagas en casa e intentes hacerle preguntas con datos retorcidos a ver qué pasa y si guarda en los slots el valor correcto.

Introducción a Prolog, tutorial en español

Prolog es un lenguaje de programación lógico, quizá uno de los más populares de este paradigma ya que fue el primero en implementarlo, en el año 1972 en Francia.

Durante un tiempo, se creyó que PROLOG supondría la revolución de los lenguajes de programación, siendo uno de los estandartes de los lenguajes de programación de quinta generación. Tanto se creía que Borland, la famosa empresa de compiladores para MS-DOS, tenía Turbo Prolog, junto a Turbo C++, Turbo Pascal, Turbo Assembler y Turbo Basic.

Desafortunadamente, Prolog no triunfó como se esperaba y solo fue usado dentro del mundo de la Inteligencia Artificial. Existe un estándar ISO sobre Prolog (al igual que Ada, C++ y otros lenguajes estandarizados) pero no es demasiado influyente. Tampoco ha habido ningún éxito en alguna de las versiones propuestas para dotarle de orientación a objetos al lenguaje.

Prolog sin embargo ha influenciado a algunos lenguajes como Erlang, que toman algunos aspectos de él.

No obstante, Prolog sigue siendo un lenguaje muy interesante, diferente al resto de lenguajes (tanto imperativos, como funcionales), así que pongámonos a ello.

Actualmente existen varios compiladores de Prolog: SWI Prolog, GNU Prolog, Visual Prolog (al mucha gente no lo considera Prolog de verdad), …

La mejor versión bajo mi punto de vista es SWI Prolog, que tiene una librería de predicados bastante extensa. Es rápido y permite generar ejecutables nativos (realmente no lo son, pero la máquina virtual de Prolog ocupa muy poco y apenas se nota en el tamaño del ejecutable).

Lo podéis descargar gratuitamente desde su página oficial que como curiosiad, está hecha en SWI Prolog. También está en la paquetería de las distribuciones GNU/Linux habituales.

Nuestro primer programa

Para empezar con Prolog voy a tomar un camino distinto de muchos tutoriales y voy a empezar haciendo un programa con Entrada/Salida y que se ejecute como un binario independiente. La potencia de Prolog no está ahí especialmente, pero es un buen punto de partida

Para ello definimos un predicado main de la siguiente forma y lo guardamos en un fichero main.pl.

¿Qué hace el programa? Imprime en pantalla Hola Mundo (write), una nueva línea (nl), lee un string de teclado con el separador \n y lo unifica con la variable Nombre. Esto ya veremos que significa, pero de momento puedes pensar que Nombre es una variable de salida y que a partir de ahí Nombre tiene un valor establecido.

Compilamos con el siguiente comando:

Con esto le decimos a SWI Prolog que el objetivo a demostrar es main y que nos genere un fichero stand_alone (independiente).

Y ejecutamos el fichero ejecutable como uno más.

Ahora que ya sabemos como se generan programas compilados, vamos a introducirnos más en lo que hace especial a Prolog.

La terminal de Prolog

Prolog fue diseñado con una terminal interactiva en mente. La idea era que el usuario fuese introduciendo preguntas y el programa en Prolog fuese contestando. Este enfoque, similar a usar un programa desde el REPL de tu lenguaje, no ha acabado cuajando, pero es necesario pasar por él. Más adelante veremos como con SWI Prolog no hace falta usar la terminal. La terminal se abre escribiendo swipl en la línea de comandos:

Vemos un símbolo de interrogación. Eso nos permite saber que estamos en una terminal de Prolog. Podemos escribir write(“Hola”). y tener nuestro hola mundo tradicional, aunque no es muy interesante, pero sí es muy interesante que después escribe true. Más adelante veremos por qué.

Los programas Prolog

En Prolog los programas son algo diferentes a lo que estamos acostumbrados. Realmente en Prolog no hay programas sino una base de datos. Un programa se compone de predicados, muy parecidos a los del Cálculo de Predicados. Los predicados aportan información sobre las relaciones entre elementos. Todos los predicados tienen que acabar en punto.

Siguiendo las mismas normas que el cálculo de predicados:

  • Las constantes empiezan por minúscula
  • Las variables empiezan por mayúscula
  • Las funciones son constantes seguidas de N teŕminos. Son funciones estrictamente matemáticas.
  • Los predicados pueden ser atómicos o compuestos, con operadores lógicos (and, or, implica, etc)

Prolog durante la ejecución, va a intentar demostrar que el predicado es cierto, usando el algoritmo de backtracking. Y ahí está la verdadera potencia de Prolog. Veamos unos ejemplos:

Guarda ese archivo con extensión .pl y ejecuta swipl comida.pl.

Ahora en la terminal podemos hacer preguntas. ¿Es la manzana una fruta? Prolog responde verdadero. ¿Es la pera una fruta? Prolog responde que falso, porque según el archivo comida.pl, no lo es. Prolog no es inteligente, no sabe que significan las palabras, simplemente actúa siguiendo un conjunto de normas formales.

Hemos dicho que Prolog tiene variables. Una variable en Prolog es un marcador de hueco, es algo que no existe, porque no es ninguna constante en específico. Veamos la potencia de las variables con este otro predicado.

En este caso pedimos demostrar fruta(X).. Prolog buscará la primera solución que demuestra el predicado, que es que X valga manzana. Aquí podemos pulsar ENTER y Prolog se para o pulsar N y Prolog busca otra solución. ¿Potente, verdad? Prolog realiza un proceso interno que se llama unificación, es importante saber como funciona para ver que hace Prolog en realidad.

Unificación

La unificación es un proceso que combina dos predicados en uno que encaja. Para ello buscamos las sustituciones de valores con los que dos predicados son compatibles (realmente solo se pueden modificar las variables). No obstante, Prolog busca siempre el unificador más general, que es aquel que unifica dejando los predicados de forma más genérica posible, es decir, que pueden usarse con más valores.

Espero que esta imagen aclare el concepto de unificación. Básicamente Prolog para intentar demostrar un predicado intentará unificar con otros predicados del programa. Así cuando ponemos por ejemplo comida(manzana) , unifica con comida(X) así que Prolog toma ese predicado para continuar.

Backtracking

Cuando Prolog intenta demostrar un predicado aplica el algoritmo de backtracking. Este algoritmo recorre todas las soluciones posibles pero de forma más inteligente que la fuerza bruta. Backtracking intenta conseguir una solución hasta que un predicado falla, en ese momento, Prolog va hacia atrás y continúa por otra vía que pueda seguir.

Cada predicado es un nodo. Si un predicado falla se vuelve atrás. Esto es muy interesate ya que Prolog técnicamente puede ejecutar código hacia atrás.

Un predicado Prolog sigue esta estructura:

Predicados avanzados

Pero los predicados de Prolog no tienen por qué ser así de simples. Normalmente se usa el operador :- para indicar algo que para que se cumpla la parte de la izquierda, tiene que cumplirse la parte de la derecha antes (en cálculo de predicados es ←).

Por ejemplo, todas las frutas son comidas, así que podemos añadir esto a nuestro archivo.

Y los predicados en Prolog se pueden repetir y repetir y repetir. Prolog siempre intenta demostrar de arriba a abajo, si falla un predicado, prueba con el siguiente más para abajo y termina cuando no hay más predicados que probar. ¡Así que podemos definir comida en base a más predicados!

Operaciones

En Prolog existen varios operadores importantes:

  • , (coma) AND
  • ; (punto y coma) OR
  • A = B, se intenta unificar A y B. Devuelve true si funciona
  • A \= B es falso si A y B unifican
  • A is B, se evalúa B (es decir, se calcula lo que representa) y se unifica con A
  • A =:= B , evalúa A, evalúa B y los compara. Verdadero si son iguales
  • A =\= B, evalúa A, evalúa B y los compara. Falso si son iguales
  • Y muchos otros como =<, >=, >, < que tienen el comportamiento esperado.
  • Las operaciones matemáticas solo se pueden introducir en expresiones que vayan a ser evaluadas.

Quiero prestar especial atención en símbolo de igual que no es asignación sino unificación, pero puede parecerse. Estas dos líneas son equivalentes:

Ya que en ambos casos se unifica X con 5.

Veamos un ejemplo de todo esto, ya mucho más realista.

Lo cargamos con swipl restaurante.pl y podemos contestar a las siguientes preguntas de forma sencilla:

¿Qué valor calórico tiene la comida de paella, trucha y flan?

Dime una comidas equilibrada que lleve naranja de postre

¿Cuántas comidas con naranja hay?

Variables anónimas y predicados predefinidos

Si te fijas en el predicado comidas_con_naranja, es diferente al resto. Esta programado de otra forma. La salida por pantalla se hace con write como ya habíamos visto al principio, write es un predicado que siempre es cierto y en caso de backtracking simplemente pasa. aggregate_all es también otro predicado incluido en SWI Prolog, para en este caso, contar cuantas soluciones tiene el predicado comida(_,_,naranja). X unifica en aggregate_all con el valor que toca (que es una constante, los números son constantes) y en las siguientes sentencias (write), X ha sido sustituido por el valor con el que unificó en aggregate_all.

Por otro lado, ¿qué significa la barra baja? Es una variable anónima. Cuando no usamos una variable en más lugares y no nos interesan sus valores podemos simplemente poner barra baja.

Listas en Prolog

Prolog tiene un tipo de dato más avanzado, las listas. Su tamaño es dinámico y es conveniente distinguir la cabeza de la cola. La cabeza es el primer elemento de una lista, la cola el resto de la lista.

Las listas se crean con corchetes:

sumlist es un predicado de SWI que hace la suma de todos los elementos de una lista.

Con la barra podemos separar cabeza y cola de una lista:

Implementando sumlist

Vamos a ver como se puede implementar sumlist con Prolog de forma sencilla.

El predicado es sencillo, para un caso base de lista vacía, la suma es 0. Para otros casos más complejos separamos la lista en cabeza H y cola T y la suma es N. Esta suma se define como el resultado de sumar T (queda en X) y la cabeza H.

Assert y retract

¿Podemos crear predicados en tiempo real? Claro. Prolog provee de los predicados especiales assert para añadir y retract para eliminar un predicado.

Operador de corte

Prolog tiene un operador muy controvertido, el operador de corte, !. Se trata de un operador que permite no volver hacia atrás. Hay que intentar no usarlo, pero muchas veces mejora el rendimiento.

Metaprogramación

Prolog tiene un gran soporte para la metaprogramación. Assert y Retract son ya formas de metaprogramación, no obstante Prolog tiene muchas más.

call permite ejecutar código Prolog desde Prolog.

setarg permite modificar un término de un predicado y arg unifica una variable con el término de un predicado que queramos. nb_setarg es la versión de setarg que en caso de backtracking no deshace la operación. Un ejemplo de esto lo podemos encontrar en la definición de aggregate_all en la librería de SWI Prolog:

 

Debug con trace

Prolog posee un predicado muy útil para hacer debugging. Es trace y con el predicado que vaya a continuación podremos inspeccionar todas las operaciones del motor de backtracking.

Futures y Tokio: programar de forma asíncrona en Rust

Rust es un lenguaje muy potente y uno de sus principales focos de diseño es la concurrencia. Sin embargo, si damos un repaso por la librería estándar veremos un par de cosas nada más, algo que puede resultar decepcionante. Esto se debe a que Rust no impone ningún modelo específico de concurrencia como sí lo hacen Go o Erlang. Rust nos provee de los ladrillos de construcción necesarios.

De forma estándar, Rust nos provee de Mutex, atómicos, threads, variables de condición, RwLock y un modelo algo más avanzado de mensajería mediante canales de múltiples productores y un único consumidor (mpsc). En el exterior tenemos crates como Actix que nos proveen del modelo de actores en Rust (dicen que es similar a Akka, yo lo desconozco), modelos de mensajería por canales más flexibles (mpmc) y una cosa muy interesante llamado Futures. Los futures o futuros no son esos contratos que se realizan en bolsa sobre el valor futuro de una acción, sino que son una manera cómoda de manejar valores que existirán en el futuro. Si has usado JavaScript o C# igual te suenan las Promises o Promesas y los Task respectivamente. Los futuros de Rust son exactamente lo mismo.

¿Qué es un futuro?

Un futuro es una variable que representa un dato que todavía no existe. Tenemos la promesa de que ese valor existirá en el futuro. La ventaja es que podemos usarlo aun cuando todavía no tenga un valor. Esto además permite escribir código asíncrono con facilidad.

Una diferencia con respecto a los Promises de JavaScript es que los futuros se basan en poll en vez de en push. Es decir, los futuros van a ir preguntando si el valor ya está disponible. Ante esta pregunta se puede responder con un error, con todavía no está disponible y con ya está disponible, aquí tienes.

Veamos un ejemplo muy tonto pero que puede servirnos para entender algunas cosas.

Definimos un nuevo futuro, SumFuture, que devuelve el resultado de una suma. El futuro en su función poll duerme el hilo 1 segundo y después devuelve el resultado correcto. En la función main llamamos a suma que devuelve un futuro en vez del resultado. Con el futuro, esperamos con wait a que se resuelva y lo mostramos. Cuando un futuro se ejecuta se convierte en una tarea. Las tareas necesitan ejecutores. Wait ejecuta los futuros en el mismo hilo desde el que se hace la llamada, pero existen otras opciones. El programa, tarda un segundo en imprimir el número.

Pero esto no sirve para nada

Bueno, quizá ahora parezca una tontería, pero en cuanto introduzcamos más elementos, todo tendrá más sentido.

Una característica de los futuros es que se pueden encadenar, tal que así:

Ejecutores

Los futuros nos ayudan con la concurrencia, esto es porque se puede esperar a varios futuros a la vez sin problema. Una manera de hacerlo es con CpuPool, un ejecutor que tiene un pool de hilos ya creados. Su uso es muy sencillo, en este ejemplo vemos como hago dos operaciones en paralelo:

Con spawn añadimos un nuevo futuro a la ejecución y nos devuelve otro futuro. Con join podemos unir dos futuros en uno solo que se resuelva cuando ambos hayan resuelto. Finalmente hacemos wait en el hilo principal para esperar a que las sumas se hagan e imprimir el resultado. Existen otros ejecutores. Con Tokio usaremos otro.

Tokio

Tokio es una librería que nos permite ejecutar código de entrada/salida de forma asíncrona, usando futuros por debajo. Son especialmente útiles, la parte de red de Tokio y sus ejecutores correspondientes.

En este ejemplo tenemos un pequeño servidor TCP que se mantiene a la espera de clientes, imprime un mensaje y cierra la conexión. Este servidor solo utiliza un hilo, no obstante, ya usa futuros. server es un futuro infinito, que nunca acaba, que ejecutamos en el hilo actual con run->spawn. El run solo es necesario para ejecutar el primer futuro, el que mantendrá viva la aplicación. spawn empieza a ejecutar el futuro, que entonces se pasa a denominar tarea. Aquí es Tokio en vez de CpuPool, el planificador de tareas.

Servidor de Echo en Tokio

Ahora vamos a ir con un ejemplo más interesante, ahora el cliente y el servidor estarán conectados durante un tiempo, mandando el cliente un mensaje y el servidor respondiendo. Pero queremos que haya varios clientes simultáneamente. Usando futuros y Tokio podemos hacerlo en un mismo hilo (al estilo Node.js).

¿Qué hacemos ahora? Ahora a cada socket de cliente asignamos una tarea, que se encarga, de forma independiente, de ir leyendo línea a línea (gracias a LineCodec).

A partir de aquí se programa en forma de stream, un patrón muy común en la programación con futuros.

En el primer map imprimimos la línea en el servidor y la seguimos pasando por el stream. El siguiente paso es escribir en writer, con forward imprimimos y mandamos esa línea al cliente. Forward a su vez devuelve una tupla con datos (útil para seguir haciendo cosas). Como no los necesitamos, hacemos un map cuya única finalidad sea descartar los valores y finalmente un map_err para capturar posibles errores. Una vez hecho esto tenemos un futuro listo para ser ejecutado. Iniciamos la tarea con spawn y nos olvidamos, pasando a esperar a por un nuevo cliente.

Ahora, en el servidor podemos manejar varios clientes a la vez, cada uno en una tarea distinta, dentro de un mismo hilo.

Cancelando la tarea

Esta tarea que maneja el cliente es infinita, es decir, no se para. La razón es que reader es un lector que genera un futuro infinito esperando a nuevas líneas. Pero, ¿existe algún método para parar la tarea desde el servidor?

Esto no es algo demasiado trivial, pero se puede hacer de manera sencilla usando canales MPSC de Rust y futuros.

La idea aquí es hacer que de alguna manera, el futuro pueda resolverse y así finalizar la tarea. Una función interesante es select2 que devuelve un futuro fusión de dos futuros. Este futuro se resuelve (devuelve valor y acaba la tarea) cuando alguno de ellos lo haga. Como el futuro de reader nunca acabará, entonces basta con poner un futuro que cuando queramos cerrar la conexión resolveremos.

Este futuro es cancel de tipo Cancellable. No viene en la librería, lo he creado arriba y lo que hace es esperar a que el extremo del canal reciba una comunicación. El valor nos da igual, simplemente que se haya producido un ping. Una vez hecho eso, el futuro resuelve y la conexión se cierra.

En el ejemplo, cuando el cliente manda bye la conexión se cierra.

Y esto es una breve introducción a los futuros y a Tokio en Rust. Todavía queda ver como podemos usar async/await para no tener que escribir todo esto en forma de stream.

Generar frases con cadenas de Markov. Machine Learning en Python

Hoy vamos a hacer un ejercicio muy sencillo de machine learning. Para ello usaremos cadenas de Markov. Trataremos de generar frases totalmente nuevas basadas en otras frases que le demos como entrada.

En mi caso voy a usar las frases del presentador argentino afincado en España, Héctor del Mar, porque siempre me han parecido bastante graciosas y tiene unas cuantas.

Héctor del Mar es el de la derecha. Para quien no le conozca, suele comentar los shows de la WWE

¿Qué son las cadenas de Markov?

Las cadenas de Markov es un modelo probabilístico que impone que la probabilidad de que suceda algo solo depende del estado anterior. Aplicado a nuestro caso con palabras, la probabilidad de que una palabra sea la siguiente de la frase solo depende de la palabra anterior. Observemos este grafo:

En él se han introducido dos frases: El coche rojo y El coche verde. La probabilidad de que coche sea la palabra que va después de El es del 100%, pero de que rojo sea la siguiente palabra después de coche es del 50%. Con este ejemplo, parece bastante limitado, pero la cosa cambia cuando metemos muchas frases y muchas palabras se repiten.

Para este ejemplo no obstante, usaré las dos últimas palabras como estado anterior, ya que suele dar resultados mucho más legibles (aunque pueden darse con más probabilidad frases que ya existen).

Obteniendo los datos

El primer paso será tener las frases en un formato óptimo. Para ello usaré requests y BeautifulSoup4. Las frases las voy a sacar de Wikiquote.

Generando el grafo de Markov

Ahora hay que generar el grafo de Markov. Para ello vamos a usar un diccionario, donde en la clave tendremos el estado anterior, es decir, las dos últimas palabras (en forma de tupla). El contenido será una lista con todas las palabras a las que puede saltar. Al ser una lista, puede haber palabras repetidas, lo que efectivamente hará aumentar su probabilidad.

Aprovechando Python, voy a usar un defaultdict para simplificar el código, ya que con él me voy a asegurar de que todos los accesos al diccionario me van a devolver una lista.

Generando una frase nueva

Ahora viene el último paso, generrar una frase nueva. Para ello, empezamos con el estado START,START y seguimos el grafo hasta que acabemos. Para elegir la siguiente palabra de la lista usamos random.choice. La frase que se va generando se queda en una lista hasta que finalmente devolvemos un string completo.

Resultados

Veamos los resultados:

Las frases en rojo son frases que dijo de verdad. Las frases en verde son frases generadas por este machine learning. La tasa de frases nuevas no es muy elevada, pero son más del 50%. Y todas son bastante divertidas.

El código fuente completo de Markov-HectorDelMar está en el repositorio Git del blog: https://github.com/aarroyoc/blog-ejemplos/tree/master/markov-hector-del-mar

Ahora que ya sabes usar cadenas de Markov, ¿por qué no meter como dato de entrada El Quijote?, ¿o los tuits de algún político famoso?, ¿o las entradas de este blog? Las posibilidades son infinitas.

Para despedirme, como diría el Héctor del Mar de verdad:

Aquí estoy porque he venido, porque he venido aquí estoy, si no le gusta mi canto, como he venido, me voy. ¡Nos vamos, don Fernando!

Estadística en Python: ajustar datos a una distribución (parte VII)

Ya hemos visto con anterioridad que existen modelos que nos hacen la vida más sencilla. Sin embargo, en esos modelos conocíamos ciertos datos de antemano. ¿Qué pasa si tenemos datos y queremos ver si podemos estar ante un modelo de los ya definidos?

Este tipo de ajuste es muy interesante, ya que nos permite saber si los datos en bruto pueden parecerse a los modelos de Normal u otros y aprovecharlo.

Ajuste de datos a una distribución

Para ajustar datos a una distribución, todas las distribuciones continuas de SciPy cuentan con la función fit. Fit nos devuelve los parámetros con los que ajusta nuestros datos al modelo. ¡Ojo, no nos avisa si es un buen ajuste o no!

Ejemplo: Queremos saber si la altura de los hombres adultos del pueblo de Garray sigue una distribución normal. Para ello tomamos una muestra de 80 alturas de hombres adultos en Garray.
Los datos los tenemos en este CSV, cada altura en una línea:

Vamos a ajustarlo.

En este caso nos informa de que estos datos parecen encajar con los de una distribución normal de media 160,37 y desviación típica 17,41.

¿Cómo de bueno es el ajuste? Kolmogorov

Hemos hecho un ajuste, pero no sabemos qué tan bueno es. Existen varios métodos para poner a prueba los ajustes. Existen varios métodos, siendo los más populares Chi-Cuadrado y Kolmogorov-Smirnov.

Chi-Cuadrado no se puede aplicar directamente sobre distribuciones continuas, aún así voy a explicar como se haría. Sin embargo, primero vamos a probar con Kolmogorov-Smirnov, que en SciPy es ktest.

Este test nos devuelve dos valores: D y el p-valor. Yo voy a fijarme en el p-valor. El p-valor es el nivel mínimo de significación para el cual rechazaremos el ajuste. Cuanto más cerca del 1 esté, más confianza hay en el ajuste, cuanto más cerca de cero esté, menos confianza hay en el ajuste. ¿Pero cómo se interpreta exactamente?

Una forma sencillo de entenderlo es hacer obtener el nivel de significación, que es 1 – NivelConfianza/100. Así para una confianza del 95%, este será de 0.05, para una confianza del 99%, el valor se reduce a 0.01. Si el p-valor es inferior a este nivel de significación, el ajuste probablemente no es correcto.

Aquí algún estadístico más serio se tiraría de los pelos, porque en realidad lo único que se puede demostrar con seguridad es en el caso de que no se ajuste. Si salta que se puede ajustar a una normal, simplemente querría decir que no se puede rechazar la hipótesis de partida (que sea normal), pero no confirma que sea normal.

Esto es algo común en el contraste de hipótesis y con los p-valores en particular y ha suscitado crítica, hasta tal punto que en 2016, la American Statistical Association publicó una serie de consejos para tratar con p-valores. El p-valor en este caso solo puede demostrar que no se ajusta a una normal. No obstante, para ir poco a poco, podemos simplificar esto un poco.

Chi-Cuadrado

Para hacer el test de Chi-Cuadrado primero hay que dividir los datos continuos. Para uso podemos usar la función de NumPy, histogram o la de SciPy binned_statistic. Tenemos que construir una lista con las frecuencias esperadas si siguiéramos al 100% la distribución a la que queremos ajustarnos.

Una vez tengamos una lista con los valores esperados, simplemente podemos comparar las frecuencias reales con las esperadas con chisquare. Esto nos devolverá C y el p-valor. El significado del p-valor es exactamente igual.

Con esto ya hemos visto como empezar con estadística en Python. No sé si haré más artículos de este estilo, si no es así, espero que os hayan gustado esta serie de artículos. El mundo del data science es inmenso y os invito a seguirlo explorando.